[
  {
    "path": ".browserslistrc",
    "content": "# Browsers we support\nChrome >= 73\nChromeAndroid >= 75\nFirefox >= 67\nEdge >= 17\nSafari >= 12.1\niOS >= 11.3\n"
  },
  {
    "path": ".changeset/README.md",
    "content": "# Changesets\n\nHello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works\nwith multi-package repos, or single-package repos to help you version and publish your code. You can\nfind the full documentation for it [in our repository](https://github.com/changesets/changesets)\n\nWe have a quick list of common questions to get you started engaging with this project in\n[our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md)\n"
  },
  {
    "path": ".changeset/config.json",
    "content": "{\n  \"$schema\": \"https://unpkg.com/@changesets/config@2.0.0/schema.json\",\n  \"changelog\": [\n    \"@remix-run/changelog-github\",\n    { \"repo\": \"remix-run/react-router\" }\n  ],\n  \"commit\": false,\n  \"fixed\": [\n    [\n      \"create-react-router\",\n      \"react-router\",\n      \"react-router-dom\",\n      \"@react-router/architect\",\n      \"@react-router/cloudflare\",\n      \"@react-router/dev\",\n      \"@react-router/fs-routes\",\n      \"@react-router/express\",\n      \"@react-router/node\",\n      \"@react-router/remix-routes-option-adapter\",\n      \"@react-router/serve\"\n    ]\n  ],\n  \"linked\": [],\n  \"access\": \"public\",\n  \"baseBranch\": \"dev\",\n  \"updateInternalDependencies\": \"patch\",\n  \"bumpVersionsWithWorkspaceProtocolOnly\": true,\n  \"ignore\": [\"integration\", \"integration-*\", \"@playground/*\"],\n  \"___experimentalUnsafeOptions_WILL_CHANGE_IN_PATCH\": {\n    \"onlyUpdatePeerDependentsWhenOutOfRange\": true\n  }\n}\n"
  },
  {
    "path": ".eslintignore",
    "content": "/fixtures/\nnode_modules/\npnpm-lock.yaml\n/docs/api\nexamples/**/dist/\nworker-configuration.d.ts\n/playground/\n/playground-local/\nintegration/helpers/**/dist/\nintegration/helpers/**/build/\nplaywright-report/\ntest-results/\nbuild.utils.d.ts\n.wrangler/\n.tmp/\n.react-router/\npackages/**/dist/\npackages/react-router-dom/server.d.ts\npackages/react-router-dom/server.js\npackages/react-router-dom/server.mjs\ntutorial/dist/\npublic/\n"
  },
  {
    "path": ".eslintrc",
    "content": "{\n  \"extends\": [\"react-app\"],\n  \"rules\": {\n    \"import/first\": \"off\",\n    \"@typescript-eslint/consistent-type-imports\": \"error\"\n  },\n  \"overrides\": [\n    {\n      \"files\": [\"**/__tests__/**\"],\n      \"plugins\": [\"jest\"],\n      \"extends\": [\"plugin:jest/recommended\"]\n    },\n    {\n      \"files\": [\"integration/**/*.*\"],\n      \"rules\": {\n        \"react-hooks/rules-of-hooks\": \"off\"\n      },\n      \"env\": {\n        \"jest/globals\": false\n      }\n    },\n    {\n      \"files\": [\"packages/react-router-dev/config/default-rsc-entries/*\"],\n      \"rules\": {\n        \"@typescript-eslint/consistent-type-imports\": \"off\"\n      }\n    },\n    {\n      // Only apply JSDoc lint rules to files we auto-generate docs for\n      \"files\": [\n        \"packages/react-router/lib/components.tsx\",\n        \"packages/react-router/lib/hooks.tsx\",\n        \"packages/react-router/lib/dom/lib.tsx\",\n        \"packages/react-router/lib/dom/ssr/components.tsx\",\n        \"packages/react-router/lib/dom/ssr/server.tsx\",\n        \"packages/react-router/lib/dom-export/hydrated-router.tsx\",\n        \"packages/react-router/lib/dom/server.tsx\",\n        \"packages/react-router/lib/router/utils.ts\",\n        \"packages/react-router/lib/rsc/browser.tsx\",\n        \"packages/react-router/lib/rsc/server.rsc.ts\",\n        \"packages/react-router/lib/rsc/server.ssr.tsx\",\n        \"packages/react-router/lib/rsc/html-stream/browser.ts\",\n      ],\n      \"plugins\": [\"jsdoc\"],\n      \"rules\": {\n        \"jsdoc/check-access\": \"error\",\n        \"jsdoc/check-alignment\": \"error\",\n        \"jsdoc/check-param-names\": \"error\",\n        \"jsdoc/check-property-names\": \"error\",\n        \"jsdoc/check-tag-names\": [\n          \"error\",\n          {\n            \"definedTags\": [\"additionalExamples\", \"category\", \"mode\"]\n          }\n        ],\n        \"jsdoc/no-defaults\": \"error\",\n        \"jsdoc/no-multi-asterisks\": [\"error\", { \"allowWhitespace\": true }],\n        \"jsdoc/require-description\": \"error\",\n        \"jsdoc/require-param\": [\"error\", { \"enableRootFixer\": false }],\n        \"jsdoc/require-param-description\": \"error\",\n        \"jsdoc/require-param-name\": \"error\",\n        \"jsdoc/require-returns\": \"error\",\n        \"jsdoc/require-returns-check\": \"error\",\n        \"jsdoc/require-returns-description\": \"error\",\n        \"jsdoc/sort-tags\": [\n          \"error\",\n          {\n            \"tagSequence\": [\n              {\n                \"tags\": [\"description\"]\n              },\n              {\n                \"tags\": [\"example\"]\n              },\n              {\n                \"tags\": [\"additionalExamples\"]\n              },\n              {\n                \"tags\": [\n                  \"name\",\n                  \"public\",\n                  \"private\",\n                  \"category\",\n                  \"mode\",\n                  \"param\",\n                  \"returns\"\n                ]\n              }\n            ]\n          }\n        ]\n      }\n    }\n  ],\n  \"reportUnusedDisableDirectives\": true\n}\n"
  },
  {
    "path": ".github/FUNDING.yml",
    "content": "# These are supported funding model platforms\n\ngithub: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]\npatreon: # Replace with a single Patreon username\nopen_collective: react-router\nko_fi: # Replace with a single Ko-fi username\ntidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel\ncommunity_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry\nliberapay: # Replace with a single Liberapay username\nissuehunt: # Replace with a single IssueHunt username\notechie: # Replace with a single Otechie username\ncustom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/bug_report.yml",
    "content": "name: 🐛 Bug Report\ndescription: Something is wrong with React Router\nlabels:\n  - bug\nbody:\n  - type: markdown\n    attributes:\n      value: |\n        Thank you for helping to improve React Router!\n\n        Please note that **all** bugs must have a **minimal** and **runnable** reproduction, meaning that it is not just pointing to a deployed site or a branch in your existing application, or showing a small code snippet. For more information, please refer to the [Bug/Issue Process Guidelines](https://github.com/remix-run/react-router/blob/main/GOVERNANCE.md#bugissue-process).\n\n        ## If you are using **Framework Mode**, you have 2 preferred options\n\n        ### Option 1 - Create a failing integration test\n\n        🏆 The most helpful reproduction is to use our _bug report integration test_ template:\n\n        1. [Fork `remix-run/react-router`](https://github.com/remix-run/react-router/fork)\n        2. Open [`integration/bug-report-test.ts`](https://github.com/remix-run/react-router/blob/dev/integration/bug-report-test.ts) in your editor\n        3. Follow the instructions to create a failing bug report test\n        4. Link to your forked branch with the failing test in this issue\n\n        ### Option 2 - Create a **minimal** reproduction\n\n        - 🥇 Link to a [StackBlitz](https://reactrouter.com/new) environment\n        - 🥈 Link to a GitHub repository containing a minimal reproduction app\n\n        ## If you are using **Data** or **Declarative Mode**\n\n        Create a **minimal** reproduction\n\n        - 🥇 Link to a CodeSandbox repro: [TS](https://codesandbox.io/templates/react-vite-ts) | [JS](https://codesandbox.io/templates/react-vite)\n        - 🥈 Link to a GitHub repository containing a minimal reproduction app\n\n  - type: textarea\n    id: reproduction\n    attributes:\n      label: Reproduction\n      description: Link to reproduction and steps to reproduce the behavior\n      placeholder: Go to https://stackblitz.com/edit/... and click the \"Submit\" button\n    validations:\n      required: true\n  - type: textarea\n    id: system-info\n    attributes:\n      label: System Info\n      description: Output of `npx envinfo --system --npmPackages '{vite,react-router,@react-router/*}' --binaries --browsers`\n      render: shell\n      placeholder: System, Binaries, Browsers\n    validations:\n      required: true\n  - type: dropdown\n    id: package-manager\n    attributes:\n      label: Used Package Manager\n      description: Select the used package manager\n      options:\n        - npm\n        - yarn\n        - pnpm\n    validations:\n      required: true\n  - type: textarea\n    attributes:\n      label: Expected Behavior\n      description: A concise description of what you expected to happen.\n    validations:\n      required: true\n  - type: textarea\n    attributes:\n      label: Actual Behavior\n      description: A concise description of what you're experiencing.\n    validations:\n      required: true\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/config.yml",
    "content": "blank_issues_enabled: false\ncontact_links:\n  - name: 💡 Feature Request\n    url: https://github.com/remix-run/react-router/discussions/new?category=proposals\n    about: If you've got an idea for a new feature in React Router, please open a new Discussion with the `Proposals` label\n  - name: 🤔 Usage Question (Github Discussions)\n    url: https://github.com/remix-run/remix/discussions/new?category=q-a\n    about: Open a Discussion in GitHub with the `Q&A` label\n  - name: 💬 Remix Discord Channel\n    url: https://rmx.as/discord\n    about: Interact with other people using React Router and Remix 📀\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/documentation_isse.yml",
    "content": "name: 📚 Documentation Issue\ndescription: Something is wrong with the React Router docs\ntitle: \"[Docs]: \"\nlabels:\n  - docs\nbody:\n  - type: markdown\n    attributes:\n      value: |\n        Thank you for contributing!\n\n        For documentation updates - we would happily accept PRs, so feel free to update and\n        open a PR to the `main` branch.  Otherwise let us know in this issue what you felt\n        was missing or incorrect.\n\n  - type: textarea\n    attributes:\n      label: Describe what's incorrect/missing in the documentation\n      description: A concise description of what you expected to see in the docs\n    validations:\n      required: true\n"
  },
  {
    "path": ".github/dependabot.yml",
    "content": "version: 2\nupdates:\n  - package-ecosystem: github-actions\n    directory: /\n    schedule:\n      interval: daily\n"
  },
  {
    "path": ".github/workflows/close-feature-pr.yml",
    "content": "# Close a singular pull request that implements a feature that has not\n# gone through the Proposal process\n# Triggered by adding the `feature-request` label to an issue\n\nname: 🚪 Check Feature PR\n\non:\n  pull_request_target:\n    types: [labeled]\n\njobs:\n  close-feature-pr:\n    name: 🚪 Check Feature PR\n    if: github.repository == 'remix-run/react-router' && github.event.label.name == 'feature-request'\n    runs-on: ubuntu-latest\n    permissions:\n      pull-requests: write\n    steps:\n      - name: ⬇️ Checkout repo\n        uses: actions/checkout@v6\n\n      - name: 🚪 Close PR\n        env:\n          GH_TOKEN: ${{ github.token }}\n        run: |\n          gh pr comment ${{ github.event.pull_request.number }} -F ./scripts/close-feature-pr.md\n          gh pr edit ${{ github.event.pull_request.number }} --remove-label ${{ github.event.label.name }}\n          gh pr close ${{ github.event.pull_request.number }}\n"
  },
  {
    "path": ".github/workflows/close-no-repro-issue.yml",
    "content": "# Close a singular issue without a reproduction\n# Triggered by adding the `no-reproduction` label to an issue\n\nname: 🚪 Close issue without a reproduction\n\non:\n  issues:\n    types: [labeled]\n\njobs:\n  close-no-repro-issue:\n    name: 🚪 Close issue\n    if: github.repository == 'remix-run/react-router' && github.event.label.name == 'no-reproduction'\n    runs-on: ubuntu-latest\n    steps:\n      - name: ⬇️ Checkout repo\n        uses: actions/checkout@v6\n\n      - name: 🚪 Close issue\n        env:\n          GH_TOKEN: ${{ github.token }}\n        run: |\n          gh issue comment ${{ github.event.issue.number }} -F ./scripts/close-no-repro-issue.md\n          gh issue edit ${{ github.event.issue.number }} --remove-label ${{ github.event.label.name }}\n          gh issue close ${{ github.event.issue.number }} -r \"not planned\";\n"
  },
  {
    "path": ".github/workflows/close-no-repro-issues.yml",
    "content": "# This is a bulk-close script that was used initially to find and close issues\n# without a repro, but moving forward we'll likely use the singular version\n# (close-no-repro-issue.yml) on new issues which is driven by a label added to\n# the issue\n\nname: 🚪 Close issues without a reproduction\n\non:\n  workflow_dispatch:\n    inputs:\n      dryRun:\n        type: boolean\n        description: \"Dry Run? (no issues will be closed)\"\n        default: false\n\nconcurrency: ${{ github.workflow }}-${{ github.ref }}\n\njobs:\n  close-no-repro-issues:\n    name: 🚪 Close issues\n    if: github.repository == 'remix-run/react-router'\n    runs-on: ubuntu-latest\n    env:\n      CI: \"true\"\n      GH_TOKEN: ${{ github.token }}\n    steps:\n      - name: ⬇️ Checkout repo\n        uses: actions/checkout@v6\n\n      - name: 📦 Setup pnpm\n        uses: pnpm/action-setup@v4.1.0\n\n      - name: ⎔ Setup node\n        uses: actions/setup-node@v6\n        with:\n          # required for --experimental-strip-types\n          node-version: 22\n          cache: \"pnpm\"\n\n      - name: 📥 Install deps\n        run: pnpm install --frozen-lockfile\n\n      - name: 🚪 Close Issues (Dry Run)\n        if: ${{ inputs.dryRun }}\n        run: node --experimental-strip-types ./scripts/close-no-repro-issues.ts --dryRun\n\n      - name: 🚪 Close Issues\n        if: ${{ ! inputs.dryRun }}\n        run: node --experimental-strip-types ./scripts/close-no-repro-issues.ts\n"
  },
  {
    "path": ".github/workflows/deduplicate-lock-file.yml",
    "content": "name: ⚙️ Deduplicate lock file\n\non:\n  push:\n    branches:\n      - dev\n    paths:\n      - pnpm-lock.yaml\n\nconcurrency:\n  group: ${{ github.workflow }}-${{ github.ref }}\n  cancel-in-progress: true\n\njobs:\n  dedupe:\n    if: github.repository == 'remix-run/react-router'\n    runs-on: ubuntu-latest\n\n    steps:\n      - name: ⬇️ Checkout repo\n        uses: actions/checkout@v6\n        with:\n          token: ${{ secrets.FORMAT_PAT }}\n\n      - name: 📦 Setup pnpm\n        uses: pnpm/action-setup@v4\n\n      - name: ⎔ Setup node\n        uses: actions/setup-node@v6\n        with:\n          node-version-file: \".nvmrc\"\n          cache: \"pnpm\"\n\n      - name: ⚙️ Dedupe lock file\n        run: pnpm dedupe && rm -rf ./node_modules && pnpm install\n\n      - name: 💪 Commit\n        run: |\n          git config --local user.email \"hello@remix.run\"\n          git config --local user.name \"Remix Run Bot\"\n\n          git add .\n          if [ -z \"$(git status --porcelain)\" ]; then\n            echo \"💿 no deduplication needed\"\n            exit 0\n          fi\n          git commit -m \"chore: deduplicate \\`pnpm-lock.yaml\\`\"\n          git push\n          echo \"💿 pushed dedupe changes https://github.com/$GITHUB_REPOSITORY/commit/$(git rev-parse HEAD)\"\n"
  },
  {
    "path": ".github/workflows/docs.yml",
    "content": "name: 📚 Docs\n\non:\n  push:\n    branches:\n      - main\n      - dev\n    paths:\n      - \"packages/**/*.ts\"\n      - \"packages/**/*.tsx\"\n      - \"scripts/docs.ts\"\n  workflow_dispatch:\n    inputs:\n      branch:\n        description: \"Branch to generate docs on\"\n        required: true\n\nconcurrency:\n  group: ${{ github.workflow }}-${{ github.ref }}\n  cancel-in-progress: true\n\njobs:\n  docs:\n    if: github.repository == 'remix-run/react-router'\n    runs-on: ubuntu-latest\n\n    steps:\n      - name: ⬇️ Checkout repo\n        uses: actions/checkout@v6\n        with:\n          token: ${{ secrets.FORMAT_PAT }}\n          ref: ${{ github.event.inputs.branch }}\n\n      - name: 📦 Setup pnpm\n        uses: pnpm/action-setup@v4\n\n      - name: ⎔ Setup node\n        uses: actions/setup-node@v6\n        with:\n          node-version-file: \".nvmrc\"\n          cache: pnpm\n\n      - name: 📥 Install deps\n        run: pnpm install --frozen-lockfile\n\n      - name: 🏗 Build\n        run: pnpm build\n\n      - name: 📚 Generate Typedoc Docs\n        run: pnpm run docs:typedoc\n\n      - name: 📚 Generate Markdown Docs\n        run: pnpm run docs:jsdoc --write\n\n      - name: 💪 Commit\n        run: |\n          git config --local user.email \"hello@remix.run\"\n          git config --local user.name \"Remix Run Bot\"\n\n          git add .\n          if [ -z \"$(git status --porcelain)\" ]; then\n            echo \"💿 no docs changed\"\n            exit 0\n          fi\n          git commit -m \"chore: generate markdown docs from jsdocs\"\n          git push\n          echo \"💿 pushed docs changes https://github.com/$GITHUB_REPOSITORY/commit/$(git rev-parse HEAD)\"\n"
  },
  {
    "path": ".github/workflows/format.yml",
    "content": "name: 👔 Format\n\non:\n  push:\n    branches:\n      - main\n      - dev\n\nconcurrency:\n  group: ${{ github.workflow }}-${{ github.ref }}\n  cancel-in-progress: true\n\njobs:\n  format:\n    if: github.repository == 'remix-run/react-router'\n    runs-on: ubuntu-latest\n\n    steps:\n      - name: ⬇️ Checkout repo\n        uses: actions/checkout@v6\n        with:\n          token: ${{ secrets.FORMAT_PAT }}\n\n      - name: 📦 Setup pnpm\n        uses: pnpm/action-setup@v4\n\n      - name: ⎔ Setup node\n        uses: actions/setup-node@v6\n        with:\n          node-version-file: \".nvmrc\"\n          cache: pnpm\n\n      - name: 📥 Install deps\n        run: pnpm install --frozen-lockfile\n\n      - name: 🔃 Sort contributors.yml\n        run: sort --ignore-case --output contributors.yml contributors.yml\n\n      - name: 👔 Format\n        run: pnpm format\n\n      - name: 💪 Commit\n        run: |\n          git config --local user.email \"hello@remix.run\"\n          git config --local user.name \"Remix Run Bot\"\n\n          git add .\n          if [ -z \"$(git status --porcelain)\" ]; then\n            echo \"💿 no formatting changed\"\n            exit 0\n          fi\n          git commit -m \"chore: format\"\n          git push\n          echo \"💿 pushed formatting changes https://github.com/$GITHUB_REPOSITORY/commit/$(git rev-parse HEAD)\"\n"
  },
  {
    "path": ".github/workflows/integration-full.yml",
    "content": "name: Branch\n\n# main/dev branches will get the full run across\n# all OS/browsers for multiple node versions\n\non:\n  push:\n    branches:\n      - main\n      - dev\n    paths-ignore:\n      - \".changeset/**\"\n      - \"decisions/**\"\n      - \"docs/**\"\n      - \"examples/**\"\n      - \"jest/**\"\n      - \"scripts/**\"\n      - \"tutorial/**\"\n      - \"contributors.yml\"\n      - \"**/*.md\"\n\njobs:\n  build:\n    name: \"⚙️ Build\"\n    if: github.repository == 'remix-run/react-router'\n    uses: ./.github/workflows/shared-build.yml\n\n  integration-ubuntu:\n    name: \"👀 Integration Test\"\n    if: github.repository == 'remix-run/react-router'\n    uses: ./.github/workflows/shared-integration.yml\n    with:\n      os: \"ubuntu-latest\"\n      node_version: \"[20.18, 22]\"\n      browser: '[\"chromium\", \"firefox\"]'\n\n  integration-windows:\n    name: \"👀 Integration Test\"\n    if: github.repository == 'remix-run/react-router'\n    uses: ./.github/workflows/shared-integration.yml\n    with:\n      os: \"windows-latest\"\n      node_version: \"[22]\"\n      browser: '[\"msedge\"]'\n      timeout: 90\n\n  integration-macos:\n    name: \"👀 Integration Test\"\n    if: github.repository == 'remix-run/react-router'\n    uses: ./.github/workflows/shared-integration.yml\n    with:\n      os: \"macos-latest\"\n      node_version: \"[20.18, 22]\"\n      browser: '[\"webkit\"]'\n"
  },
  {
    "path": ".github/workflows/integration-pr-ubuntu.yml",
    "content": "name: PR (Base)\n\n# All PRs touching code will run tests on ubuntu/node/chromium\n\non:\n  pull_request:\n    paths-ignore:\n      - \".changeset/**\"\n      - \"decisions/**\"\n      - \"docs/**\"\n      - \"examples/**\"\n      - \"jest/**\"\n      - \"scripts/**\"\n      - \"tutorial/**\"\n      - \"contributors.yml\"\n      - \"**/*.md\"\n\nconcurrency:\n  group: ${{ github.workflow }}-${{ github.ref }}\n  cancel-in-progress: true\n\njobs:\n  build:\n    name: \"⚙️ Build\"\n    if: github.repository == 'remix-run/react-router'\n    uses: ./.github/workflows/shared-build.yml\n\n  integration-chromium:\n    name: \"👀 Integration Test\"\n    if: github.repository == 'remix-run/react-router'\n    uses: ./.github/workflows/shared-integration.yml\n    with:\n      os: \"ubuntu-latest\"\n      node_version: \"[22]\"\n      browser: '[\"chromium\"]'\n"
  },
  {
    "path": ".github/workflows/integration-pr-windows-macos.yml",
    "content": "name: PR (Full)\n\n# PRs touching @react-router/dev will also run on Ubuntu/Firefox, Windows/Edge and\n# OSX/WebKit as well as an Ubuntu/Chromium run on  Node 20.18.\n\non:\n  pull_request:\n    paths:\n      - \"pnpm-lock.yaml\"\n      - \"integration/**\"\n      - \"packages/react-router-dev/**\"\n      - \"!**/*.md\"\n\nconcurrency:\n  group: ${{ github.workflow }}-${{ github.ref }}\n  cancel-in-progress: true\n\njobs:\n  integration-chromium:\n    name: \"👀 Integration Test\"\n    if: github.repository == 'remix-run/react-router'\n    uses: ./.github/workflows/shared-integration.yml\n    with:\n      os: \"ubuntu-latest\"\n      node_version: \"[20.18]\"\n      browser: '[\"chromium\"]'\n\n  integration-firefox:\n    name: \"👀 Integration Test\"\n    if: github.repository == 'remix-run/react-router'\n    uses: ./.github/workflows/shared-integration.yml\n    with:\n      os: \"ubuntu-latest\"\n      node_version: \"[22]\"\n      browser: '[\"firefox\"]'\n\n  integration-msedge:\n    name: \"👀 Integration Test\"\n    if: github.repository == 'remix-run/react-router'\n    uses: ./.github/workflows/shared-integration.yml\n    with:\n      os: \"windows-latest\"\n      node_version: \"[22]\"\n      browser: '[\"msedge\"]'\n      timeout: 90\n\n  integration-webkit:\n    name: \"👀 Integration Test\"\n    if: github.repository == 'remix-run/react-router'\n    uses: ./.github/workflows/shared-integration.yml\n    with:\n      os: \"macos-latest\"\n      node_version: \"[22]\"\n      browser: '[\"webkit\"]'\n"
  },
  {
    "path": ".github/workflows/no-response.yml",
    "content": "name: 🥺 No Response\n\non:\n  schedule:\n    # Schedule for five minutes after the hour, every hour\n    - cron: \"5 * * * *\"\n\npermissions:\n  issues: write\n  pull-requests: write\n\njobs:\n  stale:\n    if: github.repository == 'remix-run/react-router'\n    runs-on: ubuntu-latest\n    steps:\n      - name: 🥺 Handle Ghosting\n        uses: actions/stale@v10\n        with:\n          days-before-close: 10\n          close-issue-message: >\n            This issue has been automatically closed because we haven't received a\n            response from the original author 🙈. This automation helps keep the issue\n            tracker clean from issues that aren't actionable. Please reach out if you\n            have more information for us! 🙂\n          close-pr-message: >\n            This PR has been automatically closed because we haven't received a\n            response from the original author 🙈. This automation helps keep the issue\n            tracker clean from PRs that aren't actionable. Please reach out if you\n            have more information for us! 🙂\n          # don't automatically mark issues/PRs as stale\n          days-before-stale: -1\n          stale-issue-label: needs-response\n          stale-pr-label: needs-response\n"
  },
  {
    "path": ".github/workflows/release-comment-manual.yml",
    "content": "# Used to manually trigger the release comments workflow\n\nname: 💬 Release Comment\non:\n  workflow_dispatch:\n    inputs:\n      dryRun:\n        description: \"Should this be a dry run? (true/false)\"\n        type: \"boolean\"\n        required: true\n\nconcurrency:\n  group: ${{ github.workflow }}-${{ github.ref }}\n  cancel-in-progress: true\n\nenv:\n  CI: true\n\njobs:\n  comment:\n    name: 📝 Comment on related issues and pull requests\n    if: github.repository == 'remix-run/react-router'\n    runs-on: ubuntu-latest\n    permissions:\n      issues: write # enable commenting on released issues\n      pull-requests: write # enable commenting on released pull requests\n    steps:\n      - name: ⬇️ Checkout repo\n        uses: actions/checkout@v6\n        with:\n          fetch-depth: 0\n\n      # IMPORTANT: if you make changes here, also make them in release.yml\n      - name: 📝 Comment on related issues and pull requests\n        uses: remix-run/release-comment-action@v0.5.1\n        with:\n          DRY_RUN: ${{ github.event.inputs.dryRun }}\n          DIRECTORY_TO_CHECK: \"./packages\"\n          PACKAGE_NAME: \"react-router\"\n          ISSUE_LABELS_TO_REMOVE: \"awaiting release\"\n          ISSUE_LABELS_TO_KEEP_OPEN: \"🗺 Roadmap\"\n"
  },
  {
    "path": ".github/workflows/release.yml",
    "content": "# We use this singular file for all of our releases because we can only specify\n# a singular GitHub workflow file in npm's Trusted Publishing configuration.\n# See https://docs.npmjs.com/trusted-publishers for more info.\n#\n# Specific jobs only run on the proper trigger:\n#\n# - Changesets-driven pre-releases/stable releases\n#   - Trigger: push to release-next/release-v6 branch\n#   - jobs: release -> find_package_version -> comment\n# - Nightly releases\n#   - Trigger: schedule/cron\n#   - jobs: release-nightly\n# - Experimental releases (from a workflow_dispatch trigger)\n#   - Trigger: workflow_dispatch\n#   - jobs: release-experimental\n\nname: 🚢 Release\non:\n  # Changesets-driven prereleases and stable releases\n  push:\n    branches:\n      - \"release-next\"\n      - \"release-v6\"\n  # Nightly releases\n  schedule:\n    - cron: \"0 7 * * *\" # every day at 12AM PST\n  # Experimental Releases\n  workflow_dispatch:\n    inputs:\n      branch:\n        required: true\n\nconcurrency:\n  group: ${{ github.workflow }}-${{ github.ref }}\n  cancel-in-progress: true\n\nenv:\n  CI: true\n\njobs:\n  release:\n    name: 🦋 Changesets Release\n    if: github.repository == 'remix-run/react-router' && github.event_name == 'push'\n    runs-on: ubuntu-latest\n    outputs:\n      published_packages: ${{ steps.changesets.outputs.publishedPackages }}\n      published: ${{ steps.changesets.outputs.published }}\n    permissions:\n      contents: write # enable pushing changes to the origin\n      id-token: write # enable generation of an ID token for publishing\n      pull-requests: write # enable opening a PR for the release\n    steps:\n      - name: ⬇️ Checkout repo\n        uses: actions/checkout@v6\n        with:\n          fetch-depth: 0\n\n      - name: 📦 Setup pnpm\n        uses: pnpm/action-setup@v4\n\n      - name: ⎔ Setup node\n        uses: actions/setup-node@v6\n        with:\n          node-version: 24 # Needed for npm@11 for Trusted Publishing\n          cache: \"pnpm\"\n\n      - name: 📥 Install deps\n        run: pnpm install --frozen-lockfile\n\n        # This action has two responsibilities. The first time the workflow runs\n        # (initial push to a `release-*` branch) it will create a new branch and\n        # then open a PR with the related changes for the new version. After the\n        # PR is merged, the workflow will run again and this action will build +\n        # publish to npm.\n      - name: 🚀 PR / Publish\n        id: changesets\n        # PLEASE KEEP THIS PINNED TO 1.4.10 to avoid a regression in 1.5.*\n        # See https://github.com/changesets/action/issues/465\n        uses: changesets/action@v1.4.10\n        with:\n          version: pnpm run changeset:version\n          commit: \"chore: Update version for release\"\n          title: \"chore: Update version for release\"\n          publish: pnpm run release\n          createGithubReleases: false\n        env:\n          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n\n  find_package_version:\n    name: 🦋 Find Package\n    needs: [release]\n    runs-on: ubuntu-latest\n    if: github.repository == 'remix-run/react-router' && github.event_name == 'push' && github.ref_name != 'release-v6' && needs.release.outputs.published == 'true'\n    outputs:\n      package_version: ${{ steps.find_package_version.outputs.package_version }}\n    steps:\n      - name: ⬇️ Checkout repo\n        uses: actions/checkout@v6\n\n      - name: 📦 Setup pnpm\n        uses: pnpm/action-setup@v4\n\n      - name: ⎔ Setup node\n        uses: actions/setup-node@v6\n        with:\n          node-version: 24 # Needed for npm@11 for Trusted Publishing\n          cache: \"pnpm\"\n\n      - id: find_package_version\n        run: |\n          package_version=$(node ./scripts/find-release-from-changeset.js)\n          echo \"package_version=${package_version}\" >> $GITHUB_OUTPUT\n        env:\n          PACKAGE_VERSION_TO_FOLLOW: \"react-router\"\n          PUBLISHED_PACKAGES: ${{ needs.release.outputs.published_packages }}\n\n  comment:\n    name: 📝 Comment on related issues and pull requests\n    if: github.repository == 'remix-run/react-router' && github.event_name == 'push' && github.ref_name != 'release-v6' && needs.find_package_version.outputs.package_version != ''\n    needs: [release, find_package_version]\n    runs-on: ubuntu-latest\n    permissions:\n      issues: write # enable commenting on released issues\n      pull-requests: write # enable commenting on released pull requests\n    steps:\n      - name: ⬇️ Checkout repo\n        uses: actions/checkout@v6\n        with:\n          fetch-depth: 0\n\n      # IMPORTANT: if you make changes here, also make them in release-comment-manual.yml\n      - name: 📝 Comment on related issues and pull requests\n        uses: remix-run/release-comment-action@v0.5.1\n        with:\n          DIRECTORY_TO_CHECK: \"./packages\"\n          PACKAGE_NAME: \"react-router\"\n          ISSUE_LABELS_TO_REMOVE: \"awaiting release\"\n          ISSUE_LABELS_TO_KEEP_OPEN: \"🗺 Roadmap\"\n\n  # HEADS UP! this \"nightly\" job will only ever run on the `main` branch due to\n  # it being a cron job, and the last commit on main will be what github shows\n  # as the trigger however in the checkout below we specify the `dev` branch,\n  # so all the scripts in this job will be ran from that, confusing i know, so\n  # in some cases we'll need to create multiple PRs when modifying nightly\n  # release processes\n  release-nightly:\n    name: 🌒 Nightly Release\n    if: github.repository == 'remix-run/react-router' && github.event_name == 'schedule'\n    runs-on: ubuntu-latest\n    permissions:\n      contents: write # enable pushing changes to the origin\n      id-token: write # enable generation of an ID token for publishing\n    outputs:\n      # will be undefined if there's no release necessary\n      NEXT_VERSION: ${{ steps.version.outputs.NEXT_VERSION }}\n    steps:\n      - name: ⬇️ Checkout repo\n        uses: actions/checkout@v6\n        with:\n          ref: dev\n          # checkout using a custom token so that we can push later on\n          token: ${{ secrets.GITHUB_TOKEN }}\n          fetch-depth: 0\n\n      - name: 📦 Setup pnpm\n        uses: pnpm/action-setup@v4\n\n      - name: ⎔ Setup node\n        uses: actions/setup-node@v6\n        with:\n          node-version: 24 # Needed for npm@11 for Trusted Publishing\n          cache: \"pnpm\"\n\n      - name: 📥 Install deps\n        run: pnpm install --frozen-lockfile\n\n      - name: 🕵️ Check for changes\n        id: version\n        run: |\n          SHORT_SHA=$(git rev-parse --short HEAD)\n\n          # get latest nightly tag\n          LATEST_NIGHTLY_TAG=$(git tag -l v0.0.0-nightly-\\* --sort=-creatordate | head -n 1)\n\n          # check if last commit to dev starts with the nightly tag we're about\n          # to create (minus the date)\n          # if it is, we'll skip the nightly creation\n          # if not, we'll create a new nightly tag\n          if [[ ${LATEST_NIGHTLY_TAG} == v0.0.0-nightly-${SHORT_SHA}-* ]]; then\n            echo \"🛑 Latest nightly tag is the same as the latest commit sha, skipping nightly release\"\n          else\n            # yyyyMMdd format (e.g. 20221207)\n            DATE=$(date '+%Y%m%d')\n            # v0.0.0-nightly-<short sha>-<date>\n            NEXT_VERSION=0.0.0-nightly-${SHORT_SHA}-${DATE}\n            # set output so it can be used in other jobs\n            echo \"NEXT_VERSION=${NEXT_VERSION}\" >> $GITHUB_OUTPUT\n          fi\n\n      - name: ⤴️ Update version\n        if: steps.version.outputs.NEXT_VERSION\n        run: |\n          git config --local user.email \"hello@remix.run\"\n          git config --local user.name \"Remix Run Bot\"\n          git checkout -b nightly/${{ steps.version.outputs.NEXT_VERSION }}\n          pnpm run version ${{steps.version.outputs.NEXT_VERSION}}\n          git push origin --tags\n\n      - name: 🏗 Build\n        if: steps.version.outputs.NEXT_VERSION\n        run: pnpm build\n\n      - name: 🚀 Publish\n        if: steps.version.outputs.NEXT_VERSION\n        run: pnpm run publish\n\n  release-experimental:\n    name: 🧪 Experimental Release\n    if: github.repository == 'remix-run/react-router' && github.event_name == 'workflow_dispatch'\n    runs-on: ubuntu-latest\n    permissions:\n      contents: write # enable pushing changes to the origin\n      id-token: write # enable generation of an ID token for publishing\n    steps:\n      - name: ⬇️ Checkout repo\n        uses: actions/checkout@v6\n        with:\n          ref: ${{ github.event.inputs.branch }}\n          # checkout using a custom token so that we can push later on\n          token: ${{ secrets.GITHUB_TOKEN }}\n          fetch-depth: 0\n\n      - name: 📦 Setup pnpm\n        uses: pnpm/action-setup@v4\n\n      - name: ⎔ Setup node\n        uses: actions/setup-node@v6\n        with:\n          node-version: 24 # Needed for npm@11 for Trusted Publishing\n          cache: \"pnpm\"\n\n      - name: 📥 Install deps\n        run: pnpm install --frozen-lockfile\n\n      - name: ⤴️ Update version\n        run: |\n          git config --local user.email \"hello@remix.run\"\n          git config --local user.name \"Remix Run Bot\"\n          SHORT_SHA=$(git rev-parse --short HEAD)\n          NEXT_VERSION=0.0.0-experimental-${SHORT_SHA}\n          git checkout -b experimental/${NEXT_VERSION}\n          pnpm run version ${NEXT_VERSION}\n          git push origin --tags\n\n      - name: 🏗 Build\n        run: pnpm build\n\n      - name: 🚀 Publish\n        run: pnpm run publish\n"
  },
  {
    "path": ".github/workflows/shared-build.yml",
    "content": "name: 🛠️ Build\n\non:\n  workflow_call:\n\nenv:\n  CI: true\n\njobs:\n  build:\n    runs-on: ubuntu-latest\n    steps:\n      - name: ⬇️ Checkout repo\n        uses: actions/checkout@v6\n\n      - name: 📦 Setup pnpm\n        uses: pnpm/action-setup@v4\n\n      - name: ⎔ Setup node\n        uses: actions/setup-node@v6\n        with:\n          node-version-file: \".nvmrc\"\n          cache: \"pnpm\"\n\n      # TODO: Track and renable once this has been fixed: https://github.com/google/wireit/issues/1297\n      # - uses: google/wireit@setup-github-actions-caching/v2\n\n      - name: Disable GitHub Actions Annotations\n        run: |\n          echo \"::remove-matcher owner=tsc::\"\n          echo \"::remove-matcher owner=eslint-compact::\"\n          echo \"::remove-matcher owner=eslint-stylish::\"\n\n      - name: 📥 Install deps\n        run: pnpm install --frozen-lockfile\n\n      - name: 🏗 Build\n        run: pnpm build\n"
  },
  {
    "path": ".github/workflows/shared-integration.yml",
    "content": "name: 🧪 Test (Integration)\n\non:\n  workflow_call:\n    inputs:\n      os:\n        required: true\n        type: string\n      node_version:\n        required: true\n        # this is limited to string | boolean | number (https://github.community/t/can-action-inputs-be-arrays/16457)\n        # but we want to pass an array (node_version: \"[20, 22]\"),\n        # so we'll need to manually stringify it for now\n        type: string\n      browser:\n        required: true\n        # this is limited to string | boolean | number (https://github.community/t/can-action-inputs-be-arrays/16457)\n        # but we want to pass an array (browser: \"['chromium', 'firefox']\"),\n        # so we'll need to manually stringify it for now\n        type: string\n      timeout:\n        required: false\n        type: number\n        default: 45\n\nenv:\n  CI: true\n\njobs:\n  integration:\n    name: \"${{ inputs.os }} / node@${{ matrix.node }} / ${{ matrix.browser }}\"\n    strategy:\n      fail-fast: false\n      matrix:\n        node: ${{ fromJSON(inputs.node_version) }}\n        browser: ${{ fromJSON(inputs.browser) }}\n\n    runs-on: ${{ inputs.os }}\n    steps:\n      - name: ⬇️ Checkout repo\n        uses: actions/checkout@v6\n\n      - name: 📦 Setup pnpm\n        uses: pnpm/action-setup@v4\n\n      - name: ⎔ Setup node ${{ matrix.node }}\n        uses: actions/setup-node@v6\n        with:\n          node-version: ${{ matrix.node }}\n          cache: \"pnpm\"\n\n      # TODO: Track and renable once this has been fixed: https://github.com/google/wireit/issues/1297\n      # - uses: google/wireit@setup-github-actions-caching/v2\n\n      - name: Disable GitHub Actions Annotations\n        run: |\n          echo \"::remove-matcher owner=tsc::\"\n          echo \"::remove-matcher owner=eslint-compact::\"\n          echo \"::remove-matcher owner=eslint-stylish::\"\n\n      - name: 📥 Install deps\n        run: pnpm install --frozen-lockfile\n\n      - name: 📥 Install Playwright\n        run: npx playwright install --with-deps ${{ matrix.browser }}\n\n      - name: 👀 Run Integration Tests ${{ matrix.browser }}\n        run: \"pnpm test:integration --project=${{ matrix.browser }}\"\n        timeout-minutes: ${{inputs.timeout}}\n"
  },
  {
    "path": ".github/workflows/support.yml",
    "content": "name: \"Support Requests\"\n\non:\n  issues:\n    types: [labeled, unlabeled, reopened]\n\npermissions:\n  issues: write\n\njobs:\n  action:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: dessant/support-requests@v4\n        with:\n          issue-comment: >\n            :wave: @{issue-author}, we use the issue tracker exclusively for bug reports\n            and feature requests. However, this issue appears to be a support request.\n\n            For usage questions, please use [Stack Overflow](https://stackoverflow.com/questions/tagged/react-router)\n            or [Discord](https://rmx.as/discord) where there are a lot more people ready to help you out, or \n            [post a new question](https://github.com/remix-run/react-router/discussions/new?category=q-a) in the \n            Discussions tab of this repository.\n\n            Please feel free to clarify your issue if you think it was closed prematurely.\n"
  },
  {
    "path": ".github/workflows/test.yml",
    "content": "name: 🧪 Test\n\non:\n  push:\n    branches:\n      - main\n      - dev\n    tags-ignore:\n      - v*\n    paths-ignore:\n      - \"docs/**\"\n      - \"**/README.md\"\n  pull_request:\n    paths-ignore:\n      - \"docs/**\"\n      - \"**/*.md\"\n\nconcurrency:\n  group: ${{ github.workflow }}-${{ github.ref }}\n  cancel-in-progress: true\n\njobs:\n  test:\n    name: \"🧪 Test: (Node: ${{ matrix.node }})\"\n    strategy:\n      fail-fast: false\n      matrix:\n        node:\n          - 20.18\n          - 22\n\n    runs-on: ubuntu-latest\n\n    steps:\n      - name: ⬇️ Checkout repo\n        uses: actions/checkout@v6\n\n      - name: 📦 Setup pnpm\n        uses: pnpm/action-setup@v4\n\n      - name: ⎔ Setup node\n        uses: actions/setup-node@v6\n        with:\n          node-version: ${{ matrix.node }}\n          cache: pnpm\n          check-latest: true\n\n      # TODO: Track and renable once this has been fixed: https://github.com/google/wireit/issues/1297\n      # - uses: google/wireit@setup-github-actions-caching/v2\n\n      - name: Disable GitHub Actions Annotations\n        run: |\n          echo \"::remove-matcher owner=tsc::\"\n          echo \"::remove-matcher owner=eslint-compact::\"\n          echo \"::remove-matcher owner=eslint-stylish::\"\n\n      - name: 📥 Install deps\n        run: pnpm install --frozen-lockfile\n\n      - name: 🏗 Build\n        run: pnpm build\n\n      - name: 🔍 Typecheck\n        run: pnpm typecheck\n\n      - name: 🔬 Lint\n        run: pnpm lint\n\n      - name: 🧪 Run tests\n        run: pnpm test\n"
  },
  {
    "path": ".gitignore",
    "content": ".DS_Store\nnpm-debug.log\n\n/website/build/\nnode_modules/\n\n/examples/*/yarn.lock\n/examples/*/pnpm-lock.yaml\n/examples/*/dist\n/tutorial/dist\n/playground-local/\n/integration/playwright-report\n/integration/test-results\n\n# v5 build files\n/packages/*/cjs/\n/packages/*/esm/\n/packages/*/umd/\n\n# v6 build files\n/build/\n/packages/*/dist/\n/packages/*/LICENSE.md\n\n# v7 build files\n.react-router\n\n.wireit\n.eslintcache\n.tmp\ntsup.config.bundled_*.mjs\nbuild.utils.d.ts\nworker-configuration.d.ts\n/.env\n/NOTES.md\n\n# v7 reference docs\n/public\n"
  },
  {
    "path": ".npmrc",
    "content": "ignore-workspace-cycles=true\nenable-pre-post-scripts=true\n"
  },
  {
    "path": ".nvmrc",
    "content": "22"
  },
  {
    "path": ".vscode/settings.json",
    "content": "{\n  \"typescript.tsdk\": \"node_modules/typescript/lib\",\n  \"typescript.enablePromptUseWorkspaceTsdk\": true\n}\n"
  },
  {
    "path": "AGENTS.md",
    "content": "# React Router Development Guide\n\n## Commands\n\n- **Build**: `pnpm build` (all packages) or `pnpm run --filter <package> build` (single package)\n- **Test (Jest)**: `pnpm test` (all packages), `pnpm test packages/<package>/` (single package), `pnpm test packages/react-router/__tests__/router/fetchers-test.ts` (single file), or `pnpm test -- -t \"action fetch\"` (tests matching name)\n- **Integration tests (Playwright)**: `pnpm test:integration --project chromium` (build + test all), `pnpm test:integration:run --project chromium` (test only, all), `pnpm test:integration:run --project chromium integration/middleware-test.ts` (single file), or `pnpm test:integration:run --project chromium -g \"middleware\"` (tests matching name)\n- **Typecheck**: `pnpm run typecheck`\n- **Lint**: `pnpm run lint`\n- **Docs generation**: `pnpm run docs` (regenerates API docs from JSDoc)\n- **Type generation**: `pnpm run typegen` (Framework Mode only)\n- **Clean**: `pnpm run clean` (git clean -fdX)\n\n## Modes\n\n**Five distinct modes**: Declarative, Data, Framework, RSC Data (unstable), RSC Framework (unstable). **Always identify which mode(s) a feature applies to.**\n\n1. **Declarative**: `<BrowserRouter>`, `<Routes>`, `<Route>`\n2. **Data**: `createBrowserRouter()` with `loader`/`action`, `<RouterProvider>`\n3. **Framework**: Vite plugin + `routes.ts` + Route Module API (route exports like `loader`, `action`, `default`) + type generation + SSR/SPA\n4. **RSC Data** (unstable): RSC runtime APIs, manual bundler setup, runtime route config\n5. **RSC Framework** (unstable): Framework Mode with `unstable_reactRouterRSC` Vite plugin\n\n**RSC mode differences:**\n\n- **RSC Framework**: `unstable_reactRouterRSC` plugin, `@vitejs/plugin-rsc`, different entry points/format\n- **RSC Data**: Manual bundler, runtime route config typically in `src/routes.ts`, `unstable_RSCRouteConfig`, different runtime APIs, `setupRscTest` in `integration/rsc/`\n\n## Architecture\n\n- **Monorepo**: pnpm workspace, packages in `packages/`\n- **Key packages**:\n  - `react-router`: Core (all modes) - `lib/components.tsx`, `lib/hooks.tsx`, `lib/router/`, `lib/dom/`, `lib/rsc/`\n  - `@react-router/dev`: Framework tooling - `vite/plugin.ts` (Framework), `vite/rsc/plugin.ts` (RSC Framework), `typegen/`\n  - `react-router-dom`: Re-exports `react-router` (v6→v7 compat)\n  - `@react-router/node`, `@react-router/cloudflare`, `@react-router/express`: Server adapters\n  - `@react-router/serve`: Minimal server for Framework Mode\n  - `@react-router/fs-routes`: File-system routing (`flatRoutes()`)\n\n## Testing\n\n### Unit Tests (`packages/react-router/__tests__/`)\n\nUse Jest for pure routing logic, pure server runtime behavior, router state, React component behavior. No build required.\n\n```bash\npnpm test                                                          # All packages\npnpm test packages/react-router/                                   # Single package\npnpm test packages/react-router/__tests__/router/fetchers-test.ts  # Single file\npnpm test -- -t \"action fetch\"                                     # Tests matching name\n```\n\n### Integration Tests (`integration/`)\n\nUse Playwright for Vite plugin, build pipeline, SSR/hydration, RSC, type generation.\n\n```bash\npnpm test:integration --project chromium                                     # Build + test all\npnpm test:integration:run --project chromium                                 # Test only, all\npnpm test:integration:run --project chromium integration/middleware-test.ts  # Single file\npnpm test:integration:run --project chromium -g \"middleware\"                 # Tests matching name\n```\n\n**Project**: Always use `chromium` for integration tests, unless explicitly stated otherwise.\n\n**Rebuild when**: First run, after changing `packages/` (not needed for test-only changes)\n\n**Organization**: Use `createFixture()` → `createAppFixture()` → `PlaywrightFixture`. Templates available: `vite-6-template/`, `rsc-vite-framework/`, etc. Test all applicable modes (iterate over template array when behavior should work across modes). Test both states when introducing future flags (one test with flag on, one with flag off).\n\n**RSC testing**:\n\n- **RSC Framework**: Use `createFixture` with `rsc-vite-framework/` template\n- **RSC Data**: Use `setupRscTest` in `integration/rsc/`\n\nTest shared behavior across multiple templates (e.g., `[\"vite-5-template\", \"rsc-vite-framework\"]`). Test RSC-specific features against RSC template.\n\n## routes.ts\n\nFramework Mode uses `routes.ts` in `app/`. Most tests use `flatRoutes()` for file-system routing:\n\n```ts\n// app/routes.ts\nimport { type RouteConfig } from \"@react-router/dev/routes\";\nimport { flatRoutes } from \"@react-router/fs-routes\";\n\nexport default flatRoutes() satisfies RouteConfig;\n```\n\n**File-system conventions** (`app/routes/`):\n\n- `_index.tsx` → `/` (index route)\n- `about.tsx` → `/about`\n- `blog.$slug.tsx` → `/blog/:slug` (URL param)\n- `settings.profile.tsx` → `/settings/profile` (`.` creates nesting)\n- `_layout.tsx` → pathless layout route\n\n**Manual config alternative**:\n\n```ts\nimport { index, route, layout } from \"@react-router/dev/routes\";\nexport default [\n  index(\"./home.tsx\"),\n  route(\"about\", \"./about.tsx\"),\n  layout(\"./auth-layout.tsx\", [route(\"login\", \"./login.tsx\")]),\n];\n```\n\n## Documentation\n\n**Don't edit generated files**: `docs/api/` (from JSDoc), `.react-router/types/` (from typegen)\n\n**Mode indicators**: Every doc needs `[MODES: framework, data, declarative]`\n\n**API docs**: Edit JSDoc in `packages/react-router/lib/`, run `pnpm docs`\n\n**Unstable features**: Prefix `unstable_`, add `unstable: true` to frontmatter, include warning block\n\n## Future Flags\n\n- **Future flags** (`vX_*`): Stable breaking changes for next major\n- **Unstable flags** (`unstable_*`): Experimental, may change\n\nTest both states (on/off) for future flags. Don't break existing behavior without a flag.\n\n## Changesets\n\nWhen making changes that affect users, create a changeset at `.changeset/<unique-meaningful-name>.md`. If iterating on a change that hasn't shipped yet, update the existing changeset file instead of creating a new one.\n\nFormat:\n\n```markdown\n---\n\"react-router\": patch\n\"@react-router/dev\": minor\n---\n\nBrief description of the change\n\n- Additional details if needed\n```\n\n## Branching\n\n- **`main`**: Latest stable release\n- **`dev`**: Active development (branch from here for code changes)\n- **`v6`**: v6.x maintenance\n- Branch from `main` for docs-only changes\n\n## Key Files\n\n| Purpose           | Location                                                    |\n| ----------------- | ----------------------------------------------------------- |\n| Router            | `packages/react-router/lib/router/router.ts`                |\n| React API         | `packages/react-router/lib/components.tsx`, `lib/hooks.tsx` |\n| Vite plugin       | `packages/react-router-dev/vite/plugin.ts`                  |\n| RSC Vite plugin   | `packages/react-router-dev/vite/rsc/plugin.ts`              |\n| Type generation   | `packages/react-router-dev/typegen/`                        |\n| Unit tests        | `packages/react-router/__tests__/`                          |\n| Integration tests | `integration/`                                              |\n| Decision docs     | `decisions/`                                                |\n"
  },
  {
    "path": "CHANGELOG.md",
    "content": "<!-- markdownlint-disable no-duplicate-header no-emphasis-as-heading no-inline-html -->\n\n# React Router Releases\n\nThis page lists all releases/release notes for React Router back to `v6.0.0`. For releases prior to v6, please refer to the [Github Releases Page](https://github.com/remix-run/react-router/releases).\n\nWe manage release notes in this file instead of the paginated Github Releases Page for 2 reasons:\n\n- Pagination in the Github UI means that you cannot easily search release notes for a large span of releases at once\n- The paginated Github interface also cuts off longer releases notes without indication in list view, and you need to click into the detail view to see the full set of release notes\n\n<details>\n  <summary>Table of Contents</summary>\n\n- [React Router Releases](#react-router-releases)\n  - [v7.13.1](#v7131)\n  - [What's Changed](#whats-changed)\n    - [URL Masking (unstable)](#url-masking-unstable)\n    - [Patch Changes](#patch-changes)\n    - [Unstable Changes](#unstable-changes)\n  - [v7.13.0](#v7130)\n    - [Minor Changes](#minor-changes)\n    - [Patch Changes](#patch-changes-1)\n  - [v7.12.0](#v7120)\n    - [Security Notice](#security-notice)\n    - [Minor Changes](#minor-changes-1)\n    - [Patch Changes](#patch-changes-2)\n    - [Unstable Changes](#unstable-changes-1)\n  - [v7.11.0](#v7110)\n    - [What's Changed](#whats-changed-1)\n      - [`vite preview` Support](#vite-preview-support)\n      - [Stabilized Client-side `onError`](#stabilized-client-side-onerror)\n      - [Call-site Revalidation Opt-out (unstable)](#call-site-revalidation-opt-out-unstable)\n    - [Minor Changes](#minor-changes-2)\n    - [Patch Changes](#patch-changes-3)\n    - [Unstable Changes](#unstable-changes-2)\n  - [v7.10.1](#v7101)\n    - [Patch Changes](#patch-changes-4)\n  - [v7.10.0](#v7100)\n    - [What's Changed](#whats-changed-2)\n      - [Stabilized `future.v8_splitRouteModules`](#stabilized-futurev8_splitroutemodules)\n      - [Stabilized `future.v8_viteEnvironmentApi`](#stabilized-futurev8_viteenvironmentapi)\n      - [Stabilized `fetcher.reset()`](#stabilized-fetcherreset)\n      - [Stabilized `DataStrategyMatch.shouldCallHandler()`](#stabilized-datastrategymatchshouldcallhandler)\n    - [Minor Changes](#minor-changes-3)\n    - [Patch Changes](#patch-changes-5)\n    - [Unstable Changes](#unstable-changes-3)\n  - [v7.9.6](#v796)\n    - [Security Notice](#security-notice-1)\n    - [Patch Changes](#patch-changes-6)\n    - [Unstable Changes](#unstable-changes-4)\n  - [v7.9.5](#v795)\n    - [What's Changed](#whats-changed-3)\n      - [Instrumentation (unstable)](#instrumentation-unstable)\n    - [Patch Changes](#patch-changes-7)\n    - [Unstable Changes](#unstable-changes-5)\n  - [v7.9.4](#v794)\n    - [Security Notice](#security-notice-2)\n    - [What's Changed](#whats-changed-4)\n      - [`useRoute()` (unstable)](#useroute-unstable)\n    - [Patch Changes](#patch-changes-8)\n    - [Unstable Changes](#unstable-changes-6)\n  - [v7.9.3](#v793)\n    - [Patch Changes](#patch-changes-9)\n  - [v7.9.2](#v792)\n    - [What's Changed](#whats-changed-5)\n      - [RSC Framework Mode (unstable)](#rsc-framework-mode-unstable)\n      - [Fetcher Reset (unstable)](#fetcher-reset-unstable)\n    - [Patch Changes](#patch-changes-10)\n    - [Unstable Changes](#unstable-changes-7)\n  - [v7.9.1](#v791)\n    - [Patch Changes](#patch-changes-11)\n  - [v7.9.0](#v790)\n    - [Security Notice](#security-notice-3)\n    - [What's Changed](#whats-changed-6)\n      - [Stable Middleware and Context APIs](#stable-middleware-and-context-apis)\n    - [Minor Changes](#minor-changes-4)\n    - [Patch Changes](#patch-changes-12)\n    - [Unstable Changes](#unstable-changes-8)\n  - [v7.8.2](#v782)\n    - [Patch Changes](#patch-changes-13)\n    - [Unstable Changes](#unstable-changes-9)\n  - [v7.8.1](#v781)\n    - [Patch Changes](#patch-changes-14)\n    - [Unstable Changes](#unstable-changes-10)\n  - [v7.8.0](#v780)\n    - [What's Changed](#whats-changed-7)\n      - [Consistently named `loaderData` values](#consistently-named-loaderdata-values)\n      - [Improvements/fixes to the middleware APIs (unstable)](#improvementsfixes-to-the-middleware-apis-unstable)\n    - [Minor Changes](#minor-changes-5)\n    - [Patch Changes](#patch-changes-15)\n    - [Unstable Changes](#unstable-changes-11)\n    - [Changes by Package](#changes-by-package)\n  - [v7.7.1](#v771)\n    - [Patch Changes](#patch-changes-16)\n    - [Unstable Changes](#unstable-changes-12)\n  - [v7.7.0](#v770)\n    - [What's Changed](#whats-changed-8)\n      - [Unstable RSC APIs](#unstable-rsc-apis)\n    - [Minor Changes](#minor-changes-6)\n    - [Patch Changes](#patch-changes-17)\n    - [Unstable Changes](#unstable-changes-13)\n    - [Changes by Package](#changes-by-package-1)\n  - [v7.6.3](#v763)\n    - [Patch Changes](#patch-changes-18)\n  - [v7.6.2](#v762)\n    - [Patch Changes](#patch-changes-19)\n  - [v7.6.1](#v761)\n    - [Patch Changes](#patch-changes-20)\n    - [Unstable Changes](#unstable-changes-14)\n  - [v7.6.0](#v760)\n    - [What's Changed](#whats-changed-9)\n      - [`routeDiscovery` Config Option](#routediscovery-config-option)\n      - [Automatic Types for Future Flags](#automatic-types-for-future-flags)\n    - [Minor Changes](#minor-changes-7)\n    - [Patch Changes](#patch-changes-21)\n    - [Unstable Changes](#unstable-changes-15)\n    - [Changes by Package](#changes-by-package-2)\n  - [v7.5.3](#v753)\n    - [Patch Changes](#patch-changes-22)\n  - [v7.5.2](#v752)\n    - [Security Notice](#security-notice-4)\n    - [Patch Changes](#patch-changes-23)\n  - [v7.5.1](#v751)\n    - [Patch Changes](#patch-changes-24)\n    - [Unstable Changes](#unstable-changes-16)\n  - [v7.5.0](#v750)\n    - [What's Changed](#whats-changed-10)\n      - [`route.lazy` Object API](#routelazy-object-api)\n    - [Minor Changes](#minor-changes-8)\n    - [Patch Changes](#patch-changes-25)\n    - [Unstable Changes](#unstable-changes-17)\n    - [Changes by Package](#changes-by-package-3)\n  - [v7.4.1](#v741)\n    - [Security Notice](#security-notice-5)\n    - [Patch Changes](#patch-changes-26)\n    - [Unstable Changes](#unstable-changes-18)\n  - [v7.4.0](#v740)\n    - [Minor Changes](#minor-changes-9)\n    - [Patch Changes](#patch-changes-27)\n    - [Unstable Changes](#unstable-changes-19)\n    - [Changes by Package](#changes-by-package-4)\n  - [v7.3.0](#v730)\n    - [Minor Changes](#minor-changes-10)\n    - [Patch Changes](#patch-changes-28)\n    - [Unstable Changes](#unstable-changes-20)\n      - [Client-side `context` (unstable)](#client-side-context-unstable)\n      - [Middleware (unstable)](#middleware-unstable)\n        - [Middleware `context` parameter](#middleware-context-parameter)\n      - [`unstable_SerializesTo`](#unstable_serializesto)\n    - [Changes by Package](#changes-by-package-5)\n  - [v7.2.0](#v720)\n    - [What's Changed](#whats-changed-11)\n      - [Type-safe `href` utility](#type-safe-href-utility)\n      - [Prerendering with a SPA Fallback](#prerendering-with-a-spa-fallback)\n      - [Allow a root `loader` in SPA Mode](#allow-a-root-loader-in-spa-mode)\n    - [Minor Changes](#minor-changes-11)\n    - [Patch Changes](#patch-changes-29)\n    - [Unstable Changes](#unstable-changes-21)\n      - [Split Route Modules (unstable)](#split-route-modules-unstable)\n    - [Changes by Package](#changes-by-package-6)\n  - [v7.1.5](#v715)\n    - [Patch Changes](#patch-changes-30)\n  - [v7.1.4](#v714)\n    - [Patch Changes](#patch-changes-31)\n  - [v7.1.3](#v713)\n    - [Patch Changes](#patch-changes-32)\n  - [v7.1.2](#v712)\n    - [Patch Changes](#patch-changes-33)\n  - [v7.1.1](#v711)\n    - [Patch Changes](#patch-changes-34)\n  - [v7.1.0](#v710)\n    - [Minor Changes](#minor-changes-12)\n    - [Patch Changes](#patch-changes-35)\n    - [Changes by Package](#changes-by-package-7)\n  - [v7.0.2](#v702)\n    - [Patch Changes](#patch-changes-36)\n  - [v7.0.1](#v701)\n    - [Patch Changes](#patch-changes-37)\n  - [v7.0.0](#v700)\n    - [Breaking Changes](#breaking-changes)\n      - [Package Restructuring](#package-restructuring)\n      - [Removed Adapter Re-exports](#removed-adapter-re-exports)\n      - [Removed APIs](#removed-apis)\n      - [Minimum Versions](#minimum-versions)\n      - [Adopted Future Flag Behaviors](#adopted-future-flag-behaviors)\n      - [Vite Compiler](#vite-compiler)\n      - [Exposed Router Promises](#exposed-router-promises)\n    - [Other Notable Changes](#other-notable-changes)\n      - [`routes.ts`](#routests)\n      - [Type-safety improvements](#type-safety-improvements)\n      - [Prerendering](#prerendering)\n    - [Major Changes (`react-router`)](#major-changes-react-router)\n    - [Major Changes (`@react-router/*`)](#major-changes-react-router-1)\n    - [Minor Changes](#minor-changes-13)\n    - [Patch Changes](#patch-changes-38)\n    - [Changes by Package](#changes-by-package-8)\n- [React Router v6 Releases](#react-router-v6-releases)\n  - [v6.30.3](#v6303)\n    - [Security Notice](#security-notice-6)\n    - [Patch Changes](#patch-changes-39)\n  - [v6.30.2](#v6302)\n    - [Security Notice](#security-notice-7)\n    - [Patch Changes](#patch-changes-40)\n  - [v6.30.1](#v6301)\n    - [Patch Changes](#patch-changes-41)\n  - [v6.30.0](#v6300)\n    - [Minor Changes](#minor-changes-14)\n    - [Patch Changes](#patch-changes-42)\n  - [v6.29.0](#v6290)\n    - [Minor Changes](#minor-changes-15)\n    - [Patch Changes](#patch-changes-43)\n  - [v6.28.2](#v6282)\n    - [Patch Changes](#patch-changes-44)\n  - [v6.28.1](#v6281)\n    - [Patch Changes](#patch-changes-45)\n  - [v6.28.0](#v6280)\n    - [What's Changed](#whats-changed-12)\n    - [Minor Changes](#minor-changes-16)\n    - [Patch Changes](#patch-changes-46)\n  - [v6.27.0](#v6270)\n    - [What's Changed](#whats-changed-13)\n      - [Stabilized APIs](#stabilized-apis)\n    - [Minor Changes](#minor-changes-17)\n    - [Patch Changes](#patch-changes-47)\n  - [v6.26.2](#v6262)\n    - [Patch Changes](#patch-changes-48)\n  - [v6.26.1](#v6261)\n    - [Patch Changes](#patch-changes-49)\n  - [v6.26.0](#v6260)\n    - [Minor Changes](#minor-changes-18)\n    - [Patch Changes](#patch-changes-50)\n  - [v6.25.1](#v6251)\n    - [Patch Changes](#patch-changes-51)\n  - [v6.25.0](#v6250)\n    - [What's Changed](#whats-changed-14)\n      - [Stabilized `v7_skipActionErrorRevalidation`](#stabilized-v7_skipactionerrorrevalidation)\n    - [Minor Changes](#minor-changes-19)\n    - [Patch Changes](#patch-changes-52)\n  - [v6.24.1](#v6241)\n    - [Patch Changes](#patch-changes-53)\n  - [v6.24.0](#v6240)\n    - [What's Changed](#whats-changed-15)\n      - [Lazy Route Discovery (a.k.a. \"Fog of War\")](#lazy-route-discovery-aka-fog-of-war)\n    - [Minor Changes](#minor-changes-20)\n    - [Patch Changes](#patch-changes-54)\n  - [v6.23.1](#v6231)\n    - [Patch Changes](#patch-changes-55)\n  - [v6.23.0](#v6230)\n    - [What's Changed](#whats-changed-16)\n      - [Data Strategy (unstable)](#data-strategy-unstable)\n      - [Skip Action Error Revalidation (unstable)](#skip-action-error-revalidation-unstable)\n    - [Minor Changes](#minor-changes-21)\n  - [v6.22.3](#v6223)\n    - [Patch Changes](#patch-changes-56)\n  - [v6.22.2](#v6222)\n    - [Patch Changes](#patch-changes-57)\n  - [v6.22.1](#v6221)\n    - [Patch Changes](#patch-changes-58)\n  - [v6.22.0](#v6220)\n    - [What's Changed](#whats-changed-17)\n      - [Core Web Vitals Technology Report Flag](#core-web-vitals-technology-report-flag)\n    - [Minor Changes](#minor-changes-22)\n    - [Patch Changes](#patch-changes-59)\n  - [v6.21.3](#v6213)\n    - [Patch Changes](#patch-changes-60)\n  - [v6.21.2](#v6212)\n    - [Patch Changes](#patch-changes-61)\n  - [v6.21.1](#v6211)\n    - [Patch Changes](#patch-changes-62)\n  - [v6.21.0](#v6210)\n    - [What's Changed](#whats-changed-18)\n      - [`future.v7_relativeSplatPath`](#futurev7_relativesplatpath)\n      - [Partial Hydration](#partial-hydration)\n    - [Minor Changes](#minor-changes-23)\n    - [Patch Changes](#patch-changes-63)\n  - [v6.20.1](#v6201)\n    - [Patch Changes](#patch-changes-64)\n  - [v6.20.0](#v6200)\n    - [Minor Changes](#minor-changes-24)\n    - [Patch Changes](#patch-changes-65)\n  - [v6.19.0](#v6190)\n    - [What's Changed](#whats-changed-19)\n      - [`unstable_flushSync` API](#unstable_flushsync-api)\n    - [Minor Changes](#minor-changes-25)\n    - [Patch Changes](#patch-changes-66)\n  - [v6.18.0](#v6180)\n    - [What's Changed](#whats-changed-20)\n      - [New Fetcher APIs](#new-fetcher-apis)\n      - [Persistence Future Flag (`future.v7_fetcherPersist`)](#persistence-future-flag-futurev7_fetcherpersist)\n    - [Minor Changes](#minor-changes-26)\n    - [Patch Changes](#patch-changes-67)\n  - [v6.17.0](#v6170)\n    - [What's Changed](#whats-changed-21)\n      - [View Transitions 🚀](#view-transitions-)\n    - [Minor Changes](#minor-changes-27)\n    - [Patch Changes](#patch-changes-68)\n  - [v6.16.0](#v6160)\n    - [Minor Changes](#minor-changes-28)\n    - [Patch Changes](#patch-changes-69)\n  - [v6.15.0](#v6150)\n    - [Minor Changes](#minor-changes-29)\n    - [Patch Changes](#patch-changes-70)\n  - [v6.14.2](#v6142)\n    - [Patch Changes](#patch-changes-71)\n  - [v6.14.1](#v6141)\n    - [Patch Changes](#patch-changes-72)\n  - [v6.14.0](#v6140)\n    - [What's Changed](#whats-changed-22)\n      - [JSON/Text Submissions](#jsontext-submissions)\n    - [Minor Changes](#minor-changes-30)\n    - [Patch Changes](#patch-changes-73)\n  - [v6.13.0](#v6130)\n    - [What's Changed](#whats-changed-23)\n      - [`future.v7_startTransition`](#futurev7_starttransition)\n    - [Minor Changes](#minor-changes-31)\n    - [Patch Changes](#patch-changes-74)\n  - [v6.12.1](#v6121)\n    - [Patch Changes](#patch-changes-75)\n  - [v6.12.0](#v6120)\n    - [What's Changed](#whats-changed-24)\n      - [`React.startTransition` support](#reactstarttransition-support)\n    - [Minor Changes](#minor-changes-32)\n    - [Patch Changes](#patch-changes-76)\n  - [v6.11.2](#v6112)\n    - [Patch Changes](#patch-changes-77)\n  - [v6.11.1](#v6111)\n    - [Patch Changes](#patch-changes-78)\n  - [v6.11.0](#v6110)\n    - [Minor Changes](#minor-changes-33)\n    - [Patch Changes](#patch-changes-79)\n  - [v6.10.0](#v6100)\n    - [What's Changed](#whats-changed-25)\n    - [Minor Changes](#minor-changes-34)\n      - [`future.v7_normalizeFormMethod`](#futurev7_normalizeformmethod)\n    - [Patch Changes](#patch-changes-80)\n  - [v6.9.0](#v690)\n    - [What's Changed](#whats-changed-26)\n      - [`Component`/`ErrorBoundary` route properties](#componenterrorboundary-route-properties)\n      - [Introducing Lazy Route Modules](#introducing-lazy-route-modules)\n    - [Minor Changes](#minor-changes-35)\n    - [Patch Changes](#patch-changes-81)\n  - [v6.8.2](#v682)\n    - [Patch Changes](#patch-changes-82)\n  - [v6.8.1](#v681)\n    - [Patch Changes](#patch-changes-83)\n  - [v6.8.0](#v680)\n    - [Minor Changes](#minor-changes-36)\n    - [Patch Changes](#patch-changes-84)\n  - [v6.7.0](#v670)\n    - [Minor Changes](#minor-changes-37)\n    - [Patch Changes](#patch-changes-85)\n  - [v6.6.2](#v662)\n    - [Patch Changes](#patch-changes-86)\n  - [v6.6.1](#v661)\n    - [Patch Changes](#patch-changes-87)\n  - [v6.6.0](#v660)\n    - [What's Changed](#whats-changed-27)\n    - [Minor Changes](#minor-changes-38)\n    - [Patch Changes](#patch-changes-88)\n  - [v6.5.0](#v650)\n    - [What's Changed](#whats-changed-28)\n    - [Minor Changes](#minor-changes-39)\n    - [Patch Changes](#patch-changes-89)\n  - [v6.4.5](#v645)\n    - [Patch Changes](#patch-changes-90)\n  - [v6.4.4](#v644)\n    - [Patch Changes](#patch-changes-91)\n  - [v6.4.3](#v643)\n    - [Patch Changes](#patch-changes-92)\n  - [v6.4.2](#v642)\n    - [Patch Changes](#patch-changes-93)\n  - [v6.4.1](#v641)\n    - [Patch Changes](#patch-changes-94)\n  - [v6.4.0](#v640)\n    - [What's Changed](#whats-changed-29)\n      - [Remix Data APIs](#remix-data-apis)\n    - [Patch Changes](#patch-changes-95)\n  - [v6.3.0](#v630)\n    - [Minor Changes](#minor-changes-40)\n  - [v6.2.2](#v622)\n    - [Patch Changes](#patch-changes-96)\n  - [v6.2.1](#v621)\n    - [Patch Changes](#patch-changes-97)\n  - [v6.2.0](#v620)\n    - [Minor Changes](#minor-changes-41)\n    - [Patch Changes](#patch-changes-98)\n  - [v6.1.1](#v611)\n    - [Patch Changes](#patch-changes-99)\n  - [v6.1.0](#v610)\n    - [Minor Changes](#minor-changes-42)\n    - [Patch Changes](#patch-changes-100)\n  - [v6.0.2](#v602)\n    - [Patch Changes](#patch-changes-101)\n  - [v6.0.1](#v601)\n    - [Patch Changes](#patch-changes-102)\n  - [v6.0.0](#v600)\n\n</details>\n\n<!-- To add a new release, copy from this template:\n\n## v7.X.Y\n\nDate: YYYY-MM-DD\n\n### What's Changed\n\n#### Big New Feature 1\n\n#### Big New Feature 2\n\n### Minor Changes\n\n### Patch Changes\n\n### Unstable Changes\n\n⚠️  _[Unstable features](https://reactrouter.com/community/api-development-strategy#unstable-flags) are not recommended for production use_\n\n**Full Changelog**: [`v7.X.Y...v7.X.Y`](https://github.com/remix-run/react-router/compare/react-router@7.X.Y...react-router@7.X.Y)\n-->\n\n## v7.13.1\n\nDate: 2026-02-23\n\n## What's Changed\n\n### URL Masking (unstable)\n\nThis release includes a new `<Link unstable_mask>` API which brings first-class support for URL masking to Framework/Data Mode ([RFC](https://github.com/remix-run/react-router/discussions/9864)). This allows the same type of UI you could achieve in Declarative Mode via [manual `backgroundLocation` management](https://github.com/remix-run/react-router/tree/main/examples/modal). That example has been converted to Data Mode using the new API [here](https://github.com/remix-run/react-router/tree/main/examples/modal-data-router).\n\n### Patch Changes\n\n- `react-router` - Clear timeout when `turbo-stream` encoding completes ([#14810](https://github.com/remix-run/react-router/pull/14810))\n- `react-router` - Improve error message when `Origin` header is invalid ([#14743](https://github.com/remix-run/react-router/pull/14743))\n- `react-router` - Fix `matchPath` optional params matching without a `\"/\"` separator. ([#14689](https://github.com/remix-run/react-router/pull/14689))\n  - `matchPath(\"/users/:id?\", \"/usersblah\")` now returns null\n  - `matchPath(\"/test_route/:part?\", \"/test_route_more\")` now returns null.\n- `react-router` - Fix `HydrateFallback` rendering during initial lazy route discovery with matching splat route ([#14740](https://github.com/remix-run/react-router/pull/14740))\n- `react-router` - Preserve query parameters and hash on manifest version mismatch reload ([#14813](https://github.com/remix-run/react-router/pull/14813))\n\n### Unstable Changes\n\n⚠️ _[Unstable features](https://reactrouter.com/community/api-development-strategy#unstable-flags) are not recommended for production use_\n\n- `react-router` - RSC: fix null reference exception in bad codepath leading to invalid route tree comparisons ([#14780](https://github.com/remix-run/react-router/pull/14780))\n- `react-router` - RSC: add `unstable_getRequest` API ([#14758](https://github.com/remix-run/react-router/pull/14758))\n- `react-router` - RSC: Update failed origin checks to return a 400 status and appropriate UI instead of a generic 500 ([#14755](https://github.com/remix-run/react-router/pull/14755))\n- `react-router` - Add support for `<Link unstable_mask>` in Framework/Data Mode which allows users to navigate to a URL in the router but \"mask\" the URL displayed in the browser ([#14716](https://github.com/remix-run/react-router/pull/14716))\n  - This is useful for contextual routing usages such as displaying an image in a modal on top of a gallery, but displaying a browser URL directly to the image that can be shared and loaded without the contextual gallery in the background\n  - The masked location, if present, will be available on `useLocation().unstable_mask` so you can detect whether you are currently masked or not\n  - Masked URLs only work for SPA use cases, and will be removed from `history.state` during SSR\n  - This provides a first-class API to mask URLs in Framework/Data Mode to achieve the same behavior you could do in Declarative Mode via [manual `backgroundLocation` management](https://github.com/remix-run/react-router/tree/main/examples/modal).\n\n    ```tsx\n    // routes/gallery.tsx\n    export function clientLoader({ request }: Route.LoaderArgs) {\n      let sp = new URL(request.url).searchParams;\n      return {\n        images: getImages(),\n        // When the router location has the image param, load the modal data\n        modalImage: sp.has(\"image\") ? getImage(sp.get(\"image\")!) : null,\n      };\n    }\n\n    export default function Gallery({ loaderData }: Route.ComponentProps) {\n      return (\n        <>\n          <GalleryGrid>\n            {loaderData.images.map((image) => (\n              <Link\n                key={image.id}\n                {/* Navigate the router to /galley?image=N */}}\n                to={`/gallery?image=${image.id}`}\n                {/* But display /images/N in the URL bar */}}\n                unstable_mask={`/images/${image.id}`}\n              >\n                <img src={image.url} alt={image.alt} />\n              </Link>\n            ))}\n          </GalleryGrid>\n\n          {/* When the modal data exists, display the modal */}\n          {data.modalImage ? (\n            <dialog open>\n              <img src={data.modalImage.url} alt={data.modalImage.alt} />\n            </dialog>\n          ) : null}\n        </>\n      );\n    }\n    ```\n\n**Full Changelog**: [`v7.13.0...v7.13.1`](https://github.com/remix-run/react-router/compare/react-router@7.13.0...react-router@7.13.1)\n\n## v7.13.0\n\nDate: 2026-01-23\n\n### Minor Changes\n\n- `react-router` - Add `crossOrigin` prop to `Links` component ([#14687](https://github.com/remix-run/react-router/pull/14687))\n\n### Patch Changes\n\n- `react-router` - Fix double slash normalization for `useNavigate` paths with a colon ([#14718](https://github.com/remix-run/react-router/pull/14718))\n- `react-router` - Fix missing `nonce` on inline `criticalCss` ([#14691](https://github.com/remix-run/react-router/pull/14691))\n- `react-router` - Update failed origin checks to return a 400 status instead of a 500 ([#14737](https://github.com/remix-run/react-router/pull/14737))\n- `react-router` - Loosen `allowedActionOrigins` glob check so `**` matches all domains ([#14722](https://github.com/remix-run/react-router/pull/14722))\n- `@react-router/dev` - Bump `@remix-run/node-fetch-server` dep ([#14704](https://github.com/remix-run/react-router/pull/14704))\n- `@react-router/fs-routes` - Fix route file paths when routes directory is outside of the app directory ([#13937](https://github.com/remix-run/react-router/pull/13937))\n\n**Full Changelog**: [`v7.12.0...v7.13.0`](https://github.com/remix-run/react-router/compare/react-router@7.12.0...react-router@7.13.0)\n\n## v7.12.0\n\nDate: 2026-01-07\n\n### Security Notice\n\nThis release addresses 3 security vulnerabilities:\n\n- [CSRF in React Router Action/Server Action Request Processing](https://github.com/remix-run/react-router/security/advisories/GHSA-h5cw-625j-3rxh)\n- [XSS via Open Redirects](https://github.com/remix-run/react-router/security/advisories/GHSA-2w69-qvjg-hvjx)\n- [React Router SSR XSS in ScrollRestoration](https://github.com/remix-run/react-router/security/advisories/GHSA-8v8x-cx79-35w7)\n\n### Minor Changes\n\n- `react-router` - Add additional layer of CSRF protection by rejecting submissions to UI routes from external origins ([#14708](https://github.com/remix-run/react-router/pull/14708))\n  - If you need to permit access to specific external origins, there is a new `allowedActionOrigins` config field in `react-router.config.ts` where you can specify external origins\n\n### Patch Changes\n\n- `react-router` - Fix `generatePath` when used with suffixed params (i.e., `/books/:id.json`) ([#14269](https://github.com/remix-run/react-router/pull/14269))\n- `react-router` - Escape HTML in scroll restoration keys ([#14705](https://github.com/remix-run/react-router/pull/14705))\n- `react-router` - Validate redirect locations ([#14706](https://github.com/remix-run/react-router/pull/14706))\n- `@react-router/dev` - Fix `Maximum call stack size exceeded` errors when HMR is triggered against code with cyclic imports ([#14522](https://github.com/remix-run/react-router/pull/14522))\n- `@react-router/dev` - Skip SSR middleware in `vite preview` server for SPA mode ([#14673](https://github.com/remix-run/react-router/pull/14673))\n\n### Unstable Changes\n\n⚠️ _[Unstable features](https://reactrouter.com/community/api-development-strategy#unstable-flags) are not recommended for production use_\n\n- `react-router` - Preserve `clientLoader.hydrate=true` when using `<HydratedRouter unstable_instrumentations>` ([#14674](https://github.com/remix-run/react-router/pull/14674))\n- `react-router` - Pass `<Scripts nonce>` value through to the underlying `importmap` `script` tag when using `future.unstable_subResourceIntegrity` ([#14675](https://github.com/remix-run/react-router/pull/14675))\n- `react-router` - Export `UNSAFE_createMemoryHistory` and `UNSAFE_createHashHistory` alongside `UNSAFE_createBrowserHistory` for consistency ([#14663](https://github.com/remix-run/react-router/pull/14663))\n  - These are not intended to be used for new apps but intended to help apps using `unstable_HistoryRouter` migrate from v6->v7 so they can adopt the newer APIs\n- `@react-router/dev` - Add a new `future.unstable_trailingSlashAwareDataRequests` flag to provide consistent behavior of `request.pathname` inside `middleware`, `loader`, and `action` functions on document and data requests when a trailing slash is present in the browser URL. ([#14644](https://github.com/remix-run/react-router/pull/14644))\n  - Currently, your HTTP and `request` pathnames would be as follows for `/a/b/c` and `/a/b/c/`\n\n    | URL `/a/b/c` | **HTTP pathname** | **`request` pathname`** |\n    | ------------ | ----------------- | ----------------------- |\n    | **Document** | `/a/b/c`          | `/a/b/c` ✅             |\n    | **Data**     | `/a/b/c.data`     | `/a/b/c` ✅             |\n\n    | URL `/a/b/c/` | **HTTP pathname** | **`request` pathname`** |\n    | ------------- | ----------------- | ----------------------- |\n    | **Document**  | `/a/b/c/`         | `/a/b/c/` ✅            |\n    | **Data**      | `/a/b/c.data`     | `/a/b/c` ⚠️             |\n\n  - With this flag enabled, these pathnames will be made consistent though a new `_.data` format for client-side `.data` requests:\n\n    | URL `/a/b/c` | **HTTP pathname** | **`request` pathname`** |\n    | ------------ | ----------------- | ----------------------- |\n    | **Document** | `/a/b/c`          | `/a/b/c` ✅             |\n    | **Data**     | `/a/b/c.data`     | `/a/b/c` ✅             |\n\n    | URL `/a/b/c/` | **HTTP pathname**  | **`request` pathname`** |\n    | ------------- | ------------------ | ----------------------- |\n    | **Document**  | `/a/b/c/`          | `/a/b/c/` ✅            |\n    | **Data**      | `/a/b/c/_.data` ⬅️ | `/a/b/c/` ✅            |\n\n  - This a bug fix but we are putting it behind an opt-in flag because it has the potential to be a \"breaking bug fix\" if you are relying on the URL format for any other application or caching logic\n  - Enabling this flag also changes the format of client side `.data` requests from `/_root.data` to `/_.data` when navigating to `/` to align with the new format - This does not impact the `request` pathname which is still `/` in all cases\n\n**Full Changelog**: [`v7.11.0...v7.12.0`](https://github.com/remix-run/react-router/compare/react-router@7.11.0...react-router@7.12.0)\n\n## v7.11.0\n\nDate: 2025-12-17\n\n### What's Changed\n\nWe've added `vite preview` support and stabilized the client-side `onError` API - please make the appropriate changes if you've adopted the `unstable_onError` API already in a prior release.\n\n#### `vite preview` Support\n\nWe've added support for [`vite preview`](https://vite.dev/guide/cli#vite-preview) when using Framework mode to make it easy to preview your production build.\n\n#### Stabilized Client-side `onError`\n\nThe existing `<RouterProvider unstable_onError>`/`<HydratedRouter unstable_onError>` APIs have been stabilized as `<RouterProvider onError>`/`<HydratedRouter onError>`. Please see the [Error Reporting](https://reactrouter.com/7.11.0/how-to/error-reporting#client-errors) docs for more information.\n\n#### Call-site Revalidation Opt-out (unstable)\n\nWe've added initial unstable support for call-site revalidation opt-out via a new `unstable_defaultShouldRevalidate` flag ([RFC](https://github.com/remix-run/react-router/discussions/10006)). This flag is available on all navigation/fetcher submission APIs to alter standard revalidation behavior. If any routes include a `shouldRevalidate` function, then the flag value will be passed to that function so the route has the final say on revalidation behavior.\n\n```tsx\n<Form method=\"post\" unstable_defaultShouldRevalidate={false} />\nsubmit(data, { method: \"post\", unstable_defaultShouldRevalidate: false })\n<fetcher.Form method=\"post\" unstable_defaultShouldRevalidate={false} />\nfetcher.submit(data, { method: \"post\", unstable_defaultShouldRevalidate: false })\n```\n\nThis flag is also available on non-submission navigational use cases - for example, you may want to opt-out of revalidation when adding a search param that doesn't impact the UI:\n\n```tsx\n<Link to=\"?analytics-param=1\" unstable_defaultShouldRevalidate={false} />;\nnavigate(\"?analytics-param=1\", { unstable_defaultShouldRevalidate: false });\nsetSearchParams(params, { unstable_defaultShouldRevalidate: false });\n```\n\n### Minor Changes\n\n- `react-router` - Stabilize `<HydratedRouter onError>`/`<RouterProvider onError>` ([#14546](https://github.com/remix-run/react-router/pull/14546))\n- `@react-router/dev` - Add `vite preview` support ([#14507](https://github.com/remix-run/react-router/pull/14507))\n\n### Patch Changes\n\n- `react-router` - Fix `unstable_useTransitions` prop on `<Router>` component to permit omission for backwards compatibility ([#14646](https://github.com/remix-run/react-router/pull/14646))\n- `react-router` - Allow redirects to be returned from client side middleware ([#14598](https://github.com/remix-run/react-router/pull/14598))\n- `react-router` - Handle `dataStrategy` implementations that return insufficient result sets by adding errors for routes without any available result ([#14627](https://github.com/remix-run/react-router/pull/14627))\n- `@react-router/serve` - Update `compression` and `morgan` dependencies to address `on-headers` CVE: [GHSA-76c9-3jph-rj3q](https://github.com/advisories/GHSA-76c9-3jph-rj3q) ([#14652](https://github.com/remix-run/react-router/pull/14652))\n\n### Unstable Changes\n\n⚠️ _[Unstable features](https://reactrouter.com/community/api-development-strategy#unstable-flags) are not recommended for production use_\n\n- `react-router` - RSC: Support for throwing `data()` and Response from server component render phase ([#14632](https://github.com/remix-run/react-router/pull/14632))\n  - Response body is not serialized as async work is not allowed as error encoding phase.\n  - If you wish to transmit data to the boundary, throw `data()` instead\n- `react-router` - RSC: Support for throwing `redirect` Response's at render time ([#14596](https://github.com/remix-run/react-router/pull/14596))\n- `react-router` - RSC: `routeRSCServerRequest` replace `fetchServer` with `serverResponse` ([#14597](https://github.com/remix-run/react-router/pull/14597))\n- `@react-router/dev` - RSC (Framework mode): Manual chunking for `react` and `react-router` deps ([#14655](https://github.com/remix-run/react-router/pull/14655))\n- `@react-router/dev` - RSC (Framework mode): Optimize `react-server-dom-webpack` if in project `package.json` ([#14656](https://github.com/remix-run/react-router/pull/14656))\n- `@react-router/{dev,serve}` - RSC (Framework mode): Support custom entrypoints ([#14643](https://github.com/remix-run/react-router/pull/14643))\n- `react-router` - Add a new `unstable_defaultShouldRevalidate` flag to various APIs to allow opt-ing out of standard revalidation behaviors ([#14542](https://github.com/remix-run/react-router/pull/14542))\n\n**Full Changelog**: [`v7.10.1...v7.11.0`](https://github.com/remix-run/react-router/compare/react-router@7.10.1...react-router@7.11.0)\n\n## v7.10.1\n\nDate: 2025-12-04\n\n### Patch Changes\n\n- `react-router` - Update the `useOptimistic` stub we provide for React 18 users to use a stable setter function to avoid potential `useEffect` loops - specifically when using `<Link viewTransition>` ([#14628](https://github.com/remix-run/react-router/pull/14628))\n- `@react-router/dev` - Import ESM package `pkg-types` with a dynamic `import()` to fix issues on Node 20.18 ([#14624](https://github.com/remix-run/react-router/pull/14624))\n- `@react-router/dev` - Update `valibot` dependency to `^1.2.0` to address [GHSA-vqpr-j7v3-hqw9](https://github.com/advisories/GHSA-vqpr-j7v3-hqw9) ([#14608](https://github.com/remix-run/react-router/pull/14608))\n\n**Full Changelog**: [`v7.10.0...v7.10.1`](https://github.com/remix-run/react-router/compare/react-router@7.10.0...react-router@7.10.1)\n\n## v7.10.0\n\nDate: 2025-12-02\n\n### What's Changed\n\nWe've stabilized a handful of existing APIs and future flags in this release, please make the appropriate changes if you'd adopted any of these APIs in their unstable state!\n\n#### Stabilized `future.v8_splitRouteModules`\n\nThe existing `future.unstable_splitRouteModules` flag has been stabilized as `future.v8_splitRouteModules` in `react-router.config.ts`. Please see the [docs](https://reactrouter.com/7.10.0/upgrading/future#futurev8_splitroutemodules) for more information on adopting this flag.\n\n#### Stabilized `future.v8_viteEnvironmentApi`\n\nThe existing `future.unstable_viteEnvironmentApi` flag has been stabilized as `future.v8_viteEnvironmentApi` in `react-router.config.ts`. Please see the [docs](https://reactrouter.com/7.10.0/upgrading/future#futurev8_viteenvironmentapi) for more information on adopting this flag.\n\n#### Stabilized `fetcher.reset()`\n\nThe existing `fetcher.unstable_reset()` API has been stabilized as `fetcher.reset()`.\n\n#### Stabilized `DataStrategyMatch.shouldCallHandler()`\n\nThe existing low-level `DataStrategyMatch.unstable_shouldCallHandler()`/`DataStrategyMatch.unstable_shouldRevalidateArgs` APIs have been stabilized as `DataStrategyMatch.shouldCallHandler()`/`DataStrategyMatch.shouldRevalidateArgs`. Please see the [docs](https://reactrouter.com/7.10.0/how-to/data-strategy) for information about using a custom `dataStrategy` and how to migrate away from the deprecated `DataStrategyMatch.shouldLoad` API if you are using that today.\n\n### Minor Changes\n\n- `react-router` - Stabilize `fetcher.reset()` ([#14545](https://github.com/remix-run/react-router/pull/14545))\n  - ⚠️ This is a breaking change if you have begun using `fetcher.unstable_reset()` - please update your code to use `fetcher.reset()`\n- `react-router` - Stabilize the `dataStrategy` `match.shouldCallHandler()`/`match.shouldRevalidateArgs` APIs ([#14592](https://github.com/remix-run/react-router/pull/14592))\n  - The `match.shouldLoad` API is now marked deprecated in favor of these more powerful alternatives\n  - ⚠️ This is a breaking change if you have begun using `match.unstable_shouldCallHandler()`/`match.unstable_shouldRevalidateArgs` - please update your code to use `match.shouldCallHandler()`/`match.shouldRevalidateArgs`\n- `@react-router/dev` - Stabilize `future.v8_splitRouteModules`, replacing `future.unstable_splitRouteModules` ([#14595](https://github.com/remix-run/react-router/pull/14595))\n  - ⚠️ This is a breaking change if you have begun using `future.unstable_splitRouteModules` - please update your `react-router.config.ts`\n- `@react-router/dev` - Stabilize `future.v8_viteEnvironmentApi`, replacing `future.unstable_viteEnvironmentApi` ([#14595](https://github.com/remix-run/react-router/pull/14595))\n  - ⚠️ This is a breaking change if you have begun using `future.unstable_viteEnvironmentApi` - please update your `react-router.config.ts`\n\n### Patch Changes\n\n- `react-router` - Fix a Framework Mode bug where the `defaultShouldRevalidate` parameter to `shouldRevalidate` would not be correct after `action` returned a 4xx/5xx response (`true` when it should have been `false`) ([#14592](https://github.com/remix-run/react-router/pull/14592))\n  - If your `shouldRevalidate` function relied on that parameter, you may have seen unintended revalidations\n- `react-router` - Fix `fetcher.submit` failing with plain objects containing a `tagName` property ([#14534](https://github.com/remix-run/react-router/pull/14534))\n- `react-router` - Fix the promise returned from `useNavigate` in Framework/Data Mode so that it properly tracks the duration of `popstate` navigations (i.e., `navigate(-1)`) ([#14524](https://github.com/remix-run/react-router/pull/14524))\n- `react-router` - Preserve `statusText` on the `ErrorResponse` instance when throwing `data()` from a route handler ([#14555](https://github.com/remix-run/react-router/pull/14555))\n- `react-router` - Optimize `href()` to avoid backtracking regex on splat ([#14329](https://github.com/remix-run/react-router/pull/14329))\n- `@react-router/dev` - Fix internal type error in `useRoute` types that surfaces when `skipLibCheck` is disabled ([#14577](https://github.com/remix-run/react-router/pull/14577))\n- `@react-router/dev` - Load environment variables before evaluating `routes.ts` ([#14446](https://github.com/remix-run/react-router/pull/14446))\n  - For example, you can now compute your routes based on [`VITE_`-prefixed environment variables](https://vite.dev/guide/env-and-mode#env-variables)\n\n    ```ts\n    // app/routes.ts\n    import { type RouteConfig, route } from \"@react-router/dev/routes\";\n\n    const routes: RouteConfig = [];\n\n    // Only add the route when VITE_ENV_ROUTE is set\n    if (import.meta.env.VITE_ENV_ROUTE === \"my-route\") {\n      routes.push(route(\"my-route\", \"routes/my-route.tsx\"));\n    }\n\n    export default routes;\n    ```\n\n### Unstable Changes\n\n⚠️ _[Unstable features](https://reactrouter.com/community/api-development-strategy#unstable-flags) are not recommended for production use_\n\n- `react-router` - Add `unstable_pattern` to the parameters for client side `unstable_onError` ([#14573](https://github.com/remix-run/react-router/pull/14573))\n- `react-router` - Refactor how `unstable_onError` is called internally by `RouterProvider` to avoid potential strict mode issues ([#14573](https://github.com/remix-run/react-router/pull/14573))\n- `react-router` - Add new `unstable_useTransitions` flag to routers to give users control over the usage of [`React.startTransition`](https://react.dev/reference/react/startTransition) and [`React.useOptimistic`](https://react.dev/reference/react/useOptimistic) ([#14524](https://github.com/remix-run/react-router/pull/14524))\n  - Please see the [docs](https://reactrouter.com/7.10.0/explanation/react-transitions) for more information\n  - Framework Mode + Data Mode:\n    - `<HydratedRouter unstable_transition>`/`<RouterProvider unstable_transition>`\n    - When left unset (current default behavior)\n      - Router state updates are wrapped in `React.startTransition`\n      - ⚠️ This can lead to buggy behaviors if you are wrapping your own navigations/fetchers in `React.startTransition`\n      - You should set the flag to `true` if you run into this scenario to get the enhanced `useOptimistic` behavior (requires React 19)\n    - When set to `true`\n      - Router state updates remain wrapped in `React.startTransition` (as they are without the flag)\n      - `Link`/`Form` navigations will be wrapped in `React.startTransition`\n        - You can drop down to `useNavigate`/`useSubmit` if you wish to opt out of this outer `React.startTransition` call for the navigation\n      - A subset of router state info will be surfaced to the UI _during_ navigations via `React.useOptimistic` (i.e., `useNavigation()`, `useFetchers()`, etc.)\n        - ⚠️ This is a React 19 API so you must also be React 19 to opt into this flag for Framework/Data Mode\n    - When set to `false`\n      - The router will not leverage `React.startTransition` or `React.useOptimistic` on any navigations or state changes\n  - Declarative Mode\n    - `<BrowserRouter unstable_useTransitions>`\n    - When left unset\n      - Router state updates are wrapped in `React.startTransition`\n    - When set to `true`\n      - Router state updates remain wrapped in `React.startTransition` (as they are without the flag)\n      - `Link`/`Form` navigations will be wrapped in `React.startTransition`\n    - When set to `false`\n      - The router will not leverage `React.startTransition` on any navigations or state changes\n\n**Full Changelog**: [`v7.9.6...v7.10.0`](https://github.com/remix-run/react-router/compare/react-router@7.9.6...react-router@7.10.0)\n\n## v7.9.6\n\nDate: 2025-11-13\n\n### Security Notice\n\nThis release addresses 1 security vulnerability:\n\n- [Unexpected external redirect via untrusted paths](https://github.com/remix-run/react-router/security/advisories/GHSA-9jcx-v3wj-wh4m)\n\n### Patch Changes\n\n- `react-router` - Properly handle ancestor thrown middleware errors before `next()` on fetcher submissions ([#14517](https://github.com/remix-run/react-router/pull/14517))\n- `react-router` - Fix issue with splat routes interfering with multiple calls to `patchRoutesOnNavigation` ([#14487](https://github.com/remix-run/react-router/pull/14487))\n- `react-router` - Normalize double-slashes in `resolvePath` ([#14529](https://github.com/remix-run/react-router/pull/14529))\n- `@react-router/dev` - Use a dynamic `import()` to load ESM-only `p-map` dependency to avoid issues on Node 20.18 and below ([#14492](https://github.com/remix-run/react-router/pull/14492))\n- `@react-router/dev` - Short circuit `HEAD` document requests before calling `renderToPipeableStream` in the default `entry.server.tsx` to more closely align with the [spec](https://httpwg.org/specs/rfc9110.html#HEAD) ([#14488](https://github.com/remix-run/react-router/pull/14488))\n\n### Unstable Changes\n\n⚠️ _[Unstable features](https://reactrouter.com/community/api-development-strategy#unstable-flags) are not recommended for production use_\n\n- `react-router` - Add `location`/`params` as arguments to client-side `unstable_onError` to permit enhanced error reporting ([#14509](https://github.com/remix-run/react-router/pull/14509))\n  - ⚠️ This is a breaking change if you've already adopted `unstable_onError`\n  - The second parameter has changed to an object including `errorInfo`, `location`, and `params`:\n\n    ```tsx\n    // <RouterProvider unstable_onError={errorHandler} />\n    // <HydratedRouter unstable_onError={errorHandler} />\n\n    // Before\n    function errorHandler(error: unknown, errorInfo?: React.errorInfo) {\n      /*...*/\n    }\n\n    // After\n    function errorHandler(\n      error: unknown,\n      info: {\n        location: Location;\n        params: Params;\n        errorInfo?: React.ErrorInfo;\n      },\n    ) {\n      /*...*/\n    }\n    ```\n\n**Full Changelog**: [`v7.9.5...v7.9.6`](https://github.com/remix-run/react-router/compare/react-router@7.9.5...react-router@7.9.6)\n\n## v7.9.5\n\nDate: 2025-10-29\n\n### What's Changed\n\n#### Instrumentation (unstable)\n\nThis release adds new `unstable_instrumentation` APIs that will allow you to add runtime instrumentation logic to various aspects of your application (server handler, client navigations/fetches, loaders, actions, middleware, `route.lazy`). For more information, please see the [docs](https://reactrouter.com/7.9.5/how-to/instrumentation).\n\n### Patch Changes\n\n- `react-router` - Ensure action handlers run for routes with middleware even if no loader is present ([#14443](https://github.com/remix-run/react-router/pull/14443))\n- `@react-router/dev` - Ensure route navigation doesn't remove CSS `link` elements used by dynamic imports ([#14463](https://github.com/remix-run/react-router/pull/14463))\n- `@react-router/dev` - Typegen: only register route module types for routes within the app directory ([#14439](https://github.com/remix-run/react-router/pull/14439))\n\n### Unstable Changes\n\n⚠️ _[Unstable features](https://reactrouter.com/community/api-development-strategy#unstable-flags) are not recommended for production use_\n\n- `react-router` - Move `unstable_RSCHydratedRouter` and utils to `react-router/dom` export ([#14457](https://github.com/remix-run/react-router/pull/14457))\n- `react-router` - Add a type-safe `handle` field to `unstable_useRoute()` ([#14462](https://github.com/remix-run/react-router/pull/14462))\n\n  For example:\n\n  ```ts\n  // app/routes/admin.tsx\n  const handle = { hello: \"world\" };\n  ```\n\n  ```ts\n  // app/routes/some-other-route.tsx\n  export default function Component() {\n    const admin = useRoute(\"routes/admin\");\n    if (!admin) throw new Error(\"Not nested within 'routes/admin'\");\n    console.log(admin.handle);\n    //                ^? { hello: string }\n  }\n  ```\n\n- `react-router` - Add `unstable_instrumentations` API to allow users to add observability to their apps by instrumenting route loaders, actions, middlewares, lazy, as well as server-side request handlers and client side navigations/fetches ([#14412](https://github.com/remix-run/react-router/pull/14412))\n  - Framework Mode:\n    - `entry.server.tsx`: `export const unstable_instrumentations = [...]`\n    - `entry.client.tsx`: `<HydratedRouter unstable_instrumentations={[...]} />`\n  - Data Mode\n    - `createBrowserRouter(routes, { unstable_instrumentations: [...] })`\n- `react-router` - Add a new `unstable_pattern` parameter to loaders/actions/middleware which contains the un-interpolated route pattern (i.e., `/blog/:slug`) which is useful for aggregating logs/metrics by route in instrumentation code ([#14412](https://github.com/remix-run/react-router/pull/14412))\n- `@react-router/dev` - Introduce a `prerender.unstable_concurrency` option, to support running the pre-rendering concurrently, potentially speeding up the build ([#14380](https://github.com/remix-run/react-router/pull/14380))\n\n**Full Changelog**: [`v7.9.4...v7.9.5`](https://github.com/remix-run/react-router/compare/react-router@7.9.4...react-router@7.9.5)\n\n## v7.9.4\n\nDate: 2025-10-08\n\n### Security Notice\n\nThis release addresses 1 security vulnerability:\n\n- [Unauthorized file access when using `createFileSessionStorage()` with unsigned cookies](https://github.com/remix-run/react-router/security/advisories/GHSA-9583-h5hc-x8cw)\n\n### What's Changed\n\n#### `useRoute()` (unstable)\n\nThis release includes a new `unstable_useRoute()` hook that provides a type-safe way to access route `loaderData`/`actionData` from a specific route in Framework Mode. Think if it like a better version of `useRouteLoaderData` that works with the typegen system and also supports `actionData`. Check out the changelog entry below for more information.\n\n### Patch Changes\n\n- `@react-router/dev` - Update `valibot` dependency to `^1.1.0` ([#14379](https://github.com/remix-run/react-router/pull/14379))\n- `@react-router/node` - Validate format of incoming session ids in `createFileSessionStorage` ([#14426](https://github.com/remix-run/react-router/pull/14426))\n\n### Unstable Changes\n\n⚠️ _[Unstable features](https://reactrouter.com/community/api-development-strategy#unstable-flags) are not recommended for production use_\n\n- `react-router` - handle external redirects in from server actions ([#14400](https://github.com/remix-run/react-router/pull/14400))\n- `react-router` - New (unstable) `useRoute` hook for accessing data from specific routes ([#14407](https://github.com/remix-run/react-router/pull/14407))\n\n  For example, let's say you have an `admin` route somewhere in your app and you want any child routes of `admin` to all have access to the `loaderData` and `actionData` from `admin.`\n\n  ```tsx\n  // app/routes/admin.tsx\n  import { Outlet } from \"react-router\";\n\n  export const loader = () => ({ message: \"Hello, loader!\" });\n\n  export const action = () => ({ count: 1 });\n\n  export default function Component() {\n    return (\n      <div>\n        {/* ... */}\n        <Outlet />\n        {/* ... */}\n      </div>\n    );\n  }\n  ```\n\n  You might even want to create a reusable widget that all of the routes nested under `admin` could use:\n\n  ```tsx\n  import { unstable_useRoute as useRoute } from \"react-router\";\n\n  export function AdminWidget() {\n    // How to get `message` and `count` from `admin` route?\n  }\n  ```\n\n  In framework mode, `useRoute` knows all your app's routes and gives you TS errors when invalid route IDs are passed in:\n\n  ```tsx\n  export function AdminWidget() {\n    const admin = useRoute(\"routes/dmin\");\n    //                      ^^^^^^^^^^^\n  }\n  ```\n\n  `useRoute` returns `undefined` if the route is not part of the current page:\n\n  ```tsx\n  export function AdminWidget() {\n    const admin = useRoute(\"routes/admin\");\n    if (!admin) {\n      throw new Error(`AdminWidget used outside of \"routes/admin\"`);\n    }\n  }\n  ```\n\n  Note: the `root` route is the exception since it is guaranteed to be part of the current page.\n  As a result, `useRoute` never returns `undefined` for `root`.\n\n  `loaderData` and `actionData` are marked as optional since they could be accessed before the `action` is triggered or after the `loader` threw an error:\n\n  ```tsx\n  export function AdminWidget() {\n    const admin = useRoute(\"routes/admin\");\n    if (!admin) {\n      throw new Error(`AdminWidget used outside of \"routes/admin\"`);\n    }\n    const { loaderData, actionData } = admin;\n    console.log(loaderData);\n    //          ^? { message: string } | undefined\n    console.log(actionData);\n    //          ^? { count: number } | undefined\n  }\n  ```\n\n  If instead of a specific route, you wanted access to the _current_ route's `loaderData` and `actionData`, you can call `useRoute` without arguments:\n\n  ```tsx\n  export function AdminWidget() {\n    const currentRoute = useRoute();\n    currentRoute.loaderData;\n    currentRoute.actionData;\n  }\n  ```\n\n  This usage is equivalent to calling `useLoaderData` and `useActionData`, but consolidates all route data access into one hook: `useRoute`.\n\n  Note: when calling `useRoute()` (without a route ID), TS has no way to know which route is the current route.\n  As a result, `loaderData` and `actionData` are typed as `unknown`.\n  If you want more type-safety, you can either narrow the type yourself with something like `zod` or you can refactor your app to pass down typed props to your `AdminWidget`:\n\n  ```tsx\n  export function AdminWidget({\n    message,\n    count,\n  }: {\n    message: string;\n    count: number;\n  }) {\n    /* ... */\n  }\n  ```\n\n**Full Changelog**: [`v7.9.3...v7.9.4`](https://github.com/remix-run/react-router/compare/react-router@7.9.3...react-router@7.9.4)\n\n## v7.9.3\n\nDate: 2025-09-26\n\n### Patch Changes\n\n- `react-router` - Fix Data Mode regression causing a 404 during initial load in when `middleware` exists without any `loader` functions ([#14393](https://github.com/remix-run/react-router/pull/14393))\n- `react-router` - Do not try to use `turbo-stream` to decode CDN errors that never reached the server ([#14385](https://github.com/remix-run/react-router/pull/14385))\n  - This was logic we used to have in Remix v2 that got lost in the adoption of Single Fetch\n  - This permits the actual CDN error to bubble to the `ErrorBoundary` instead of a generic _\"Unable to decode turbo-stream response\"_ error\n\n**Full Changelog**: [`v7.9.2...v7.9.3`](https://github.com/remix-run/react-router/compare/react-router@7.9.2...react-router@7.9.3)\n\n## v7.9.2\n\nDate: 2025-09-24\n\n### What's Changed\n\nThis release contains a handful of bug fixes, but we think you'll be most excited about the new unstable stuff 😉.\n\n#### RSC Framework Mode (unstable)\n\nThis release includes our first release of unstable support for RSC in Framework Mode! You can read more about it in our [blog post](https://remix.run/blog/rsc-framework-mode-preview) and the [docs](https://reactrouter.com/how-to/react-server-components#rsc-framework-mode).\n\n#### Fetcher Reset (unstable)\n\nThis release also includes a new (long-requested) `fetcher.unstable_reset()` API to reset fetchers back to their initial `idle` state.\n\n### Patch Changes\n\n- `react-router` - Ensure client-side router runs client `middleware` during initialization data load (if required) even if no loaders exist ([#14348](https://github.com/remix-run/react-router/pull/14348))\n- `react-router` - Fix `middleware` prop not being supported on `<Route>` when used with a data router via `createRoutesFromElements` ([#14357](https://github.com/remix-run/react-router/pull/14357))\n- `react-router` - Update `createRoutesStub` to work with `middleware` ([#14348](https://github.com/remix-run/react-router/pull/14348))\n  - You will need to set the `<RoutesStub future={{ v8_middleware: true }} />` flag to enable the proper `context` type\n- `react-router` - Update Lazy Route Discovery manifest requests to use a singular comma-separated `paths` query param instead of repeated `p` query params ([#14321](https://github.com/remix-run/react-router/pull/14321))\n  - This is because Cloudflare has a hard limit of 100 URL search param key/value pairs when used as a key for caching purposes\n  - If more that 100 paths were included, the cache key would be incomplete and could produce false-positive cache hits\n- `react-router` - Fail gracefully on manifest version mismatch logic if `sessionStorage` access is blocked ([#14335](https://github.com/remix-run/react-router/pull/14335))\n- `react-router` - Update `useOutlet` returned element to have a stable identity in-between route changes ([#13382](https://github.com/remix-run/react-router/pull/13382))\n- `react-router` - Handle encoded question mark and hash characters in ancestor splat routes ([#14249](https://github.com/remix-run/react-router/pull/14249))\n- `@react-router/dev` - Switch internal vite plugin Response logic to use `@remix-run/node-fetch-server` ([#13927](https://github.com/remix-run/react-router/pull/13927))\n- `@react-router/dev` - Fix `presets` `future` flags being ignored during config resolution ([#14369](https://github.com/remix-run/react-router/pull/14369))\n\n### Unstable Changes\n\n⚠️ _[Unstable features](https://reactrouter.com/community/api-development-strategy#unstable-flags) are not recommended for production use_\n\n- `react-router` - Add `fetcher.unstable_reset()` API ([#14206](https://github.com/remix-run/react-router/pull/14206))\n- `react-router` - In RSC Data Mode, handle SSR'd client errors and re-try in the browser ([#14342](https://github.com/remix-run/react-router/pull/14342))\n- `react-router` - Enable full transition support for the RSC router ([#14362](https://github.com/remix-run/react-router/pull/14362))\n- `@react-router/dev` - Add unstable support for RSC Framework Mode ([#14336](https://github.com/remix-run/react-router/pull/14336))\n- `@react-router/serve` - Disable `compression()` middleware in RSC framework mode ([#14381](https://github.com/remix-run/react-router/pull/14381))\n\n**Full Changelog**: [`v7.9.1...v7.9.2`](https://github.com/remix-run/react-router/compare/react-router@7.9.1...react-router@7.9.2)\n\n## v7.9.1\n\nDate: 2025-09-12\n\n### Patch Changes\n\n- Fix internal `Future` interface naming from `middleware` -> `v8_middleware` ([#14327](https://github.com/remix-run/react-router/pull/14327))\n\n**Full Changelog**: [`v7.9.0...v7.9.1`](https://github.com/remix-run/react-router/compare/react-router@7.9.0...react-router@7.9.1)\n\n## v7.9.0\n\nDate: 2025-09-12\n\n### Security Notice\n\nThis release addresses 1 security vulnerability:\n\n- [XSS via Meta component when generating script:ld+json tags](https://github.com/remix-run/react-router/security/advisories/GHSA-3cgp-3xvw-98x8)\n\n### What's Changed\n\n#### Stable Middleware and Context APIs\n\nWe have removed the `unstable_` prefix from the following APIs and they are now considered stable and ready for production use:\n\n- [`RouterContextProvider`](https://reactrouter.com/api/utils/RouterContextProvider)\n- [`createContext`](https://reactrouter.com/api/utils/createContext)\n- `createBrowserRouter` [`getContext`](https://reactrouter.com/api/data-routers/createBrowserRouter#optsgetcontext) option\n- `<HydratedRouter>` [`getContext`](https://reactrouter.com/api/framework-routers/HydratedRouter#getcontext) prop\n\nPlease see the [Middleware Docs](https://reactrouter.com/how-to/middleware), the [Middleware RFC](https://github.com/remix-run/remix/discussions/7642), and the [Client-side Context RFC](https://github.com/remix-run/react-router/discussions/9856) for more information.\n\n### Minor Changes\n\n- Stabilize middleware and context APIs ([#14215](https://github.com/remix-run/react-router/pull/14215))\n\n### Patch Changes\n\n- `react-router` - Update `href()` to correctly process routes that have an extension after the parameter or are a single optional parameter ([#13797](https://github.com/remix-run/react-router/pull/13797))\n- `react-router` - Escape HTML in `meta()` JSON-LD content ([#14316](https://github.com/remix-run/react-router/pull/14316))\n\n### Unstable Changes\n\n⚠️ _[Unstable features](https://reactrouter.com/community/api-development-strategy#unstable-flags) are not recommended for production use_\n\n- `react-router` - RSC: Add react-server `Await` component implementation ([#14261](https://github.com/remix-run/react-router/pull/14261))\n- `react-router` - RSC: Fix hydration errors for routes that only have client loaders when using RSC in Data Mode along with a custom basename ([#14264](https://github.com/remix-run/react-router/pull/14264))\n- `react-router` - RSC: Make `href` function available in a `react-server` context ([#14262](https://github.com/remix-run/react-router/pull/14262))\n- `react-router` - RSC: Decode each time `getPayload()` is called to allow for \"in-context\" decoding and hoisting of contextual assets ([#14248](https://github.com/remix-run/react-router/pull/14248))\n\n**Full Changelog**: [`v7.8.2...v7.9.0`](https://github.com/remix-run/react-router/compare/react-router@7.8.2...react-router@7.9.0)\n\n## v7.8.2\n\nDate: 2025-08-22\n\n### Patch Changes\n\n- `react-router` - Maintain `ReadonlyMap` and `ReadonlySet` types in server response data. ([#13092](https://github.com/remix-run/react-router/pull/13092))\n- `react-router` - Fix `basename` usage without a leading slash in data routers ([#11671](https://github.com/remix-run/react-router/pull/11671))\n- `react-router` - Fix `TypeError` if you throw from `patchRoutesOnNavigation` when no partial matches exist ([#14198](https://github.com/remix-run/react-router/pull/14198))\n- `react-router` - Properly escape interpolated param values in `generatePath()` ([#13530](https://github.com/remix-run/react-router/pull/13530))\n- `@react-router/dev` - Fix potential memory leak in default `entry.server` ([#14200](https://github.com/remix-run/react-router/pull/14200))\n\n### Unstable Changes\n\n⚠️ _[Unstable features](https://reactrouter.com/community/api-development-strategy#unstable-flags) are not recommended for production use_\n\n**Client-side `onError`**\n\n- `react-router` - Add `<RouterProvider unstable_onError>`/`<HydratedRouter unstable_onError>` prop for client side error reporting ([#14162](https://github.com/remix-run/react-router/pull/14162))\n\n**Middleware**\n\n- `react-router` - Delay serialization of `.data` redirects to 202 responses until after middleware chain ([#14205](https://github.com/remix-run/react-router/pull/14205))\n- `react-router` - Update client middleware so it returns the `dataStrategy` results up the chain allowing for more advanced post-processing middleware ([#14151](https://github.com/remix-run/react-router/pull/14151), [#14212](https://github.com/remix-run/react-router/pull/14212))\n- `react-router` - Remove Data Mode `future.unstable_middleware` flag from `createBrowserRouter` ([#14213](https://github.com/remix-run/react-router/pull/14213))\n  - This is only needed as a Framework Mode flag because of the route modules and the `getLoadContext` type behavior change\n  - In Data Mode, it's an opt-in feature because it's just a new property on a route object, so there's no behavior changes that necessitate a flag\n\n**RSC**\n\n- `react-router` - Allow opting out of revalidation on server actions with hidden `$SKIP_REVALIDATION` input ([#14154](https://github.com/remix-run/react-router/pull/14154))\n\n**Full Changelog**: [`v7.8.1...v7.8.2`](https://github.com/remix-run/react-router/compare/react-router@7.8.1...react-router@7.8.2)\n\n## v7.8.1\n\nDate: 2025-08-15\n\n### Patch Changes\n\n- `react-router` - Fix usage of optional path segments in nested routes defined using absolute paths ([#14135](https://github.com/remix-run/react-router/pull/14135))\n- `react-router` - Fix optional static segment matching in `matchPath` ([#11813](https://github.com/remix-run/react-router/pull/11813))\n- `react-router` - Fix pre-rendering when a `basename` is set with `ssr:false` ([#13791](https://github.com/remix-run/react-router/pull/13791))\n- `react-router` - Properly convert returned/thrown `data()` values to `Response` instances via `Response.json()` in resource routes and middleware ([#14159](https://github.com/remix-run/react-router/pull/14159), [#14181](https://github.com/remix-run/react-router/pull/14181))\n- `@react-router/dev` - Update generated `Route.MetaArgs` type so `loaderData` is only potentially undefined when an `ErrorBoundary` export is present ([#14173](https://github.com/remix-run/react-router/pull/14173))\n\n### Unstable Changes\n\n⚠️ _[Unstable features](https://reactrouter.com/community/api-development-strategy#unstable-flags) are not recommended for production use_\n\n**Middleware**\n\n- `react-router` - Bubble client pre-`next` middleware errors to the shallowest ancestor that needs to load, not strictly the shallowest ancestor with a loader ([#14150](https://github.com/remix-run/react-router/pull/14150))\n- `react-router` - Propagate non-redirect `Response` values thrown from middleware to the error boundary on document/data requests ([#14182](https://github.com/remix-run/react-router/pull/14182))\n\n**RSC**\n\n- `react-router` - Provide `isRouteErrorResponse` utility in `react-server` environments ([#14166](https://github.com/remix-run/react-router/pull/14166))\n- `react-router` - Handle `meta` and `links` Route Exports in RSC Data Mode ([#14136](https://github.com/remix-run/react-router/pull/14136))\n\n**Full Changelog**: [`v7.8.0...v7.8.1`](https://github.com/remix-run/react-router/compare/react-router@7.8.0...react-router@7.8.1)\n\n## v7.8.0\n\nDate: 2025-08-07\n\n### What's Changed\n\n#### Consistently named `loaderData` values\n\nEver noticed the discrepancies in loader data values handed to you by the framework? Like, we call it `loaderData` in your component props, but then `match.data` in your matches? Yeah, us too - as well as some keen-eyed React Router users who raised this in a proposal. We've added new `loaderData` fields alongside existing `data` fields in a few lingering spots to align with the `loaderData` naming used in the new `Route.*` APIs.\n\n#### Improvements/fixes to the middleware APIs (unstable)\n\nThe biggest set of changes in `7.8.0` are to the `unstable_middleware` API's as we move closer to stabilizing them. If you've adopted the middleware APIs for early testing, please read the middleware changes below carefully. We hope to stabilize these soon so please let us know of any feedback you have on the API's in their current state!\n\n### Minor Changes\n\n- `react-router` - Add `nonce` prop to `Links` & `PrefetchPageLinks` ([#14048](https://github.com/remix-run/react-router/pull/14048))\n- `react-router` - Add `loaderData` arguments/properties alongside existing `data` arguments/properties to provide consistency and clarity between `loaderData` and `actionData` across the board ([#14047](https://github.com/remix-run/react-router/pull/14047))\n  - Updated types: `Route.MetaArgs`, `Route.MetaMatch`, `MetaArgs`, `MetaMatch`, `Route.ComponentProps.matches`, `UIMatch`\n  - `@deprecated` warnings have been added to the existing `data` properties to point users to new `loaderData` properties, in preparation for removing the `data` properties in a future major release\n\n### Patch Changes\n\n- `react-router` - Prevent _\"Did not find corresponding fetcher result\"_ console error when navigating during a `fetcher.submit` revalidation ([#14114](https://github.com/remix-run/react-router/pull/14114))\n- `react-router` - Switch Lazy Route Discovery manifest URL generation to use a standalone `URLSearchParams` instance instead of `URL.searchParams` to avoid a major performance bottleneck in Chrome ([#14084](https://github.com/remix-run/react-router/pull/14084))\n- `react-router` - Adjust internal RSC usage of `React.use` to avoid Webpack compilation errors when using React 18 ([#14113](https://github.com/remix-run/react-router/pull/14113))\n- `react-router` - Remove dependency on `@types/node` in TypeScript declaration files ([#14059](https://github.com/remix-run/react-router/pull/14059))\n- `react-router` - Fix types for `UIMatch` to reflect that the `loaderData`/`data` properties may be `undefined` ([#12206](https://github.com/remix-run/react-router/pull/12206))\n  - When an `ErrorBoundary` is being rendered, not all active matches will have loader data available, since it may have been their `loader` that threw to trigger the boundary\n  - The `UIMatch.data` type was not correctly handing this and would always reflect the presence of data, leading to the unexpected runtime errors when an `ErrorBoundary` was rendered\n  - ⚠️ This may cause some type errors to show up in your code for unguarded `match.data` accesses - you should properly guard for `undefined` values in those scenarios.\n\n    ```tsx\n    // app/root.tsx\n    export function loader() {\n      someFunctionThatThrows(); // ❌ Throws an Error\n      return { title: \"My Title\" };\n    }\n\n    export function Layout({ children }: { children: React.ReactNode }) {\n      let matches = useMatches();\n      let rootMatch = matches[0] as UIMatch<Awaited<ReturnType<typeof loader>>>;\n      //  ^ rootMatch.data is currently incorrectly typed here, so TypeScript does\n      //    not complain if you do the following which throws an error at runtime:\n      let { title } = rootMatch.data; // 💥\n\n      return <html>...</html>;\n    }\n    ```\n\n- `@react-router/dev` - Fix rename without mkdir in Vite plugin ([#14105](https://github.com/remix-run/react-router/pull/14105))\n\n### Unstable Changes\n\n⚠️ _[Unstable features](https://reactrouter.com/community/api-development-strategy#unstable-flags) are not recommended for production use_\n\n**RSC**\n\n- `react-router` - Fix Data Mode issue where routes that return `false` from `shouldRevalidate` would be replaced by an `<Outlet />` ([#14071](https://github.com/remix-run/react-router/pull/14071))\n- `react-router` - Proxy server action side-effect redirects from actions for document and `callServer` requests ([#14131](https://github.com/remix-run/react-router/pull/14131))\n\n**Middleware**\n\n- `react-router` - Change the `unstable_getContext` signature on `RouterProvider`, `HydratedRouter`, and `unstable_RSCHydratedRouter` so that it returns an `unstable_RouterContextProvider` instance instead of a `Map` used to construct the instance internally ([#14097](https://github.com/remix-run/react-router/pull/14097))\n  - See the [docs](https://reactrouter.com/api/data-routers/createBrowserRouter#optsunstable_getcontext) for more information\n  - ⚠️ This is a breaking change if you have adopted the `unstable_getContext` prop\n- `react-router` - Run client middleware on client navigations even if no loaders exist ([#14106](https://github.com/remix-run/react-router/pull/14106))\n- `react-router` - Convert internal middleware implementations to use the new `unstable_generateMiddlewareResponse` API ([#14103](https://github.com/remix-run/react-router/pull/14103))\n- `react-router` - Ensure resource route errors go through `handleError` w/middleware enabled ([#14078](https://github.com/remix-run/react-router/pull/14078))\n- `react-router` - Propagate returned `Response` from server middleware if `next` wasn't called ([#14093](https://github.com/remix-run/react-router/pull/14093))\n- `react-router` - Allow server middlewares to return `data()` values which will be converted into a `Response` ([#14093](https://github.com/remix-run/react-router/pull/14093), [#14128](https://github.com/remix-run/react-router/pull/14128))\n- `react-router` - Update middleware error handling so that the `next` function never throws and instead handles any middleware errors at the proper `ErrorBoundary` and returns the `Response` up through the ancestor `next` function ([#14118](https://github.com/remix-run/react-router/pull/14118))\n  - See the [error handling docs](https://reactrouter.com/how-to/middleware#next-and-error-handling) for more information\n  - ⚠️ This changes existing functionality so if you are currently wrapping `next` calls in `try`/`catch` you should be able to remove those\n- `react-router` - Bubble client-side middleware errors prior to `next` to the appropriate ancestor error boundary ([#14138](https://github.com/remix-run/react-router/pull/14138))\n- `react-router` - When middleware is enabled, make the `context` parameter read-only (`Readonly<unstable_RouterContextProvider>`) so that TypeScript will not allow you to write arbitrary fields to it in loaders, actions, or middleware. ([#14097](https://github.com/remix-run/react-router/pull/14097))\n- `react-router` - Rename and alter the signature/functionality of the `unstable_respond` API in `staticHandler.query`/`staticHandler.queryRoute` ([#14103](https://github.com/remix-run/react-router/pull/14103))\n  - This only impacts users using `createStaticHandler()` for manual data loading during non-Framework Mode SSR\n  - The API has been renamed to `unstable_generateMiddlewareResponse` for clarity\n  - The main functional change is that instead of running the loaders/actions before calling `unstable_respond` and handing you the result, we now pass a `query`/`queryRoute` function as a parameter and you execute the loaders/actions inside your callback, giving you full access to pre-processing and error handling\n  - The `query` version of the API now has a signature of `(query: (r: Request) => Promise<StaticHandlerContext | Response>) => Promise<Response>`\n  - The `queryRoute` version of the API now has a signature of `(queryRoute: (r: Request) => Promise<Response>) => Promise<Response>`\n  - This allows for more advanced usages such as running logic before/after calling `query` and direct error handling of errors thrown from query\n  - ⚠️ This is a breaking change if you've adopted the `staticHandler` `unstable_respond` API\n\n    ```tsx\n    let response = await staticHandler.query(request, {\n      requestContext: new unstable_RouterContextProvider(),\n      async unstable_generateMiddlewareResponse(query) {\n        try {\n          // At this point we've run middleware top-down so we need to call the\n          // handlers and generate the Response to bubble back up the middleware\n          let result = await query(request);\n          if (isResponse(result)) {\n            return result; // Redirects, etc.\n          }\n          return await generateHtmlResponse(result);\n        } catch (error: unknown) {\n          return generateErrorResponse(error);\n        }\n      },\n    });\n    ```\n\n- `@react-router/{architect,cloudflare,express,node}` - Change the `getLoadContext` signature (`type GetLoadContextFunction`) when `future.unstable_middleware` is enabled so that it returns an `unstable_RouterContextProvider` instance instead of a `Map` used to construct the instance internally ([#14097](https://github.com/remix-run/react-router/pull/14097))\n  - This also removes the `type unstable_InitialContext` export\n  - See the [middleware `getLoadContext` docs](https://reactrouter.com/how-to/middleware#changes-to-getloadcontextapploadcontext) for more information\n  - ⚠️ This is a breaking change if you have adopted middleware and are using a custom server with a `getLoadContext` function\n\n### Changes by Package\n\n- [`create-react-router`](https://github.com/remix-run/react-router/blob/react-router%407.8.0/packages/create-react-router/CHANGELOG.md#780)\n- [`react-router`](https://github.com/remix-run/react-router/blob/react-router%407.8.0/packages/react-router/CHANGELOG.md#780)\n- [`@react-router/architect`](https://github.com/remix-run/react-router/blob/react-router%407.8.0/packages/react-router-architect/CHANGELOG.md#780)\n- [`@react-router/cloudflare`](https://github.com/remix-run/react-router/blob/react-router%407.8.0/packages/react-router-cloudflare/CHANGELOG.md#780)\n- [`@react-router/dev`](https://github.com/remix-run/react-router/blob/react-router%407.8.0/packages/react-router-dev/CHANGELOG.md#780)\n- [`@react-router/express`](https://github.com/remix-run/react-router/blob/react-router%407.8.0/packages/react-router-express/CHANGELOG.md#780)\n- [`@react-router/fs-routes`](https://github.com/remix-run/react-router/blob/react-router%407.8.0/packages/react-router-fs-routes/CHANGELOG.md#780)\n- [`@react-router/node`](https://github.com/remix-run/react-router/blob/react-router%407.8.0/packages/react-router-node/CHANGELOG.md#780)\n- [`@react-router/remix-config-routes-adapter`](https://github.com/remix-run/react-router/blob/react-router%407.8.0/packages/react-router-remix-config-routes-adapter/CHANGELOG.md#780)\n- [`@react-router/serve`](https://github.com/remix-run/react-router/blob/react-router%407.8.0/packages/react-router-serve/CHANGELOG.md#780)\n\n**Full Changelog**: [`v7.7.1...v7.8.0`](https://github.com/remix-run/react-router/compare/react-router@7.7.1...react-router@7.8.0)\n\n## v7.7.1\n\nDate: 2025-07-24\n\n### Patch Changes\n\n- `@react-router/dev` - Update to Prettier v3 for formatting when running `react-router reveal --no-typescript` ([#14049](https://github.com/remix-run/react-router/pull/14049))\n\n### Unstable Changes\n\n⚠️ _[Unstable features](https://reactrouter.com/community/api-development-strategy#unstable-flags) are not recommended for production use_\n\n- `react-router` - RSC Data Mode: fix bug where routes with errors weren't forced to revalidate when `shouldRevalidate` returned `false` ([#14026](https://github.com/remix-run/react-router/pull/14026))\n- `react-router` - RSC Data Mode: fix `Matched leaf route at location \"/...\" does not have an element or Component` warnings when error boundaries are rendered ([#14021](https://github.com/remix-run/react-router/pull/14021))\n\n**Full Changelog**: [`v7.7.0...v7.7.1`](https://github.com/remix-run/react-router/compare/react-router@7.7.0...react-router@7.7.1)\n\n## v7.7.0\n\nDate: 2025-07-16\n\n### What's Changed\n\n#### Unstable RSC APIs\n\nWe're excited to introduce experimental support for RSC in Data Mode via the following new APIs:\n\n- [`unstable_RSCHydratedRouter`](https://reactrouter.com/api/rsc/RSCHydratedRouter)\n- [`unstable_RSCStaticRouter`](https://reactrouter.com/api/rsc/RSCStaticRouter)\n- [`unstable_createCallServer`](https://reactrouter.com/api/rsc/createCallServer)\n- [`unstable_getRSCStream`](https://reactrouter.com/api/rsc/getRSCStream)\n- [`unstable_matchRSCServerRequest`](https://reactrouter.com/api/rsc/matchRSCServerRequest)\n- [`unstable_routeRSCServerRequest`](https://reactrouter.com/api/rsc/routeRSCServerRequest)\n\nFor more information, check out the [blog post](https://remix.run/blog/react-router-and-react-server-components) and the [RSC Docs](https://reactrouter.com/how-to/react-server-components).\n\n### Minor Changes\n\n- `create-react-router` - Add Deno as a supported and detectable package manager. Note that this detection will only work with Deno versions 2.0.5 and above. If you are using an older version version of Deno then you must specify the --package-manager CLI flag set to `deno`. ([#12327](https://github.com/remix-run/react-router/pull/12327))\n- `@react-router/remix-config-routes-adapter` - Export `DefineRouteFunction` type alongside `DefineRoutesFunction` ([#13945](https://github.com/remix-run/react-router/pull/13945))\n\n### Patch Changes\n\n- `react-router` - Handle `InvalidCharacterError` when validating cookie signature ([#13847](https://github.com/remix-run/react-router/pull/13847))\n- `react-router` - Pass a copy of `searchParams` to the `setSearchParams` callback function to avoid mutations of the internal `searchParams` instance ([#12784](https://github.com/remix-run/react-router/pull/12784))\n  - This causes bugs if you mutate the current stateful `searchParams` when a navigation is blocked because the internal instance gets out of sync with `useLocation().search`\n- `react-router` - Support invalid `Date` in `turbo-stream` v2 fork ([#13684](https://github.com/remix-run/react-router/pull/13684))\n- `react-router` - In Framework Mode, clear critical CSS in development after initial render ([#13872](https://github.com/remix-run/react-router/pull/13872), [#13995](https://github.com/remix-run/react-router/pull/13995))\n- `react-router` - Strip search parameters from `patchRoutesOnNavigation` `path` param for fetcher calls ([#13911](https://github.com/remix-run/react-router/pull/13911))\n- `react-router` - Skip scroll restoration on `useRevalidator()` calls because they're not new locations ([#13671](https://github.com/remix-run/react-router/pull/13671))\n- `react-router` - Support unencoded UTF-8 routes in prerender config with `ssr` set to `false` ([#13699](https://github.com/remix-run/react-router/pull/13699))\n- `react-router` - Do not throw if the url hash is not a valid URI component ([#13247](https://github.com/remix-run/react-router/pull/13247))\n- `react-router` - Remove `Content-Length` header from Single Fetch responses ([#13902](https://github.com/remix-run/react-router/pull/13902))\n- `react-router` - Fix a regression in `createRoutesStub` introduced with the middleware feature ([#13946](https://github.com/remix-run/react-router/pull/13946))\n  - As part of that work we altered the signature to align with the new middleware APIs without making it backwards compatible with the prior `AppLoadContext` API\n  - This permitted `createRoutesStub` to work if you were opting into middleware and the updated `context` typings, but broke `createRoutesStub` for users not yet opting into middleware\n  - We've reverted this change and re-implemented it in such a way that both sets of users can leverage it\n  - ⚠️ This may be a breaking bug for if you have adopted the unstable Middleware feature and are using `createRoutesStub` with the updated API.\n\n    ```tsx\n    // If you have not opted into middleware, the old API should work again\n    let context: AppLoadContext = {\n      /*...*/\n    };\n    let Stub = createRoutesStub(routes, context);\n\n    // If you have opted into middleware, you should now pass an instantiated\n    // `unstable_routerContextProvider` instead of a `getContext` factory function.\n    let context = new unstable_RouterContextProvider();\n    context.set(SomeContext, someValue);\n    let Stub = createRoutesStub(routes, context);\n    ```\n\n- `@react-router/dev` - Update `vite-node` to `^3.2.2` to support Vite 7 ([#13781](https://github.com/remix-run/react-router/pull/13781))\n- `@react-router/dev` - Properly handle `https` protocol in dev mode ([#13746](https://github.com/remix-run/react-router/pull/13746))\n- `@react-router/dev` - Fix missing styles when Vite's `build.cssCodeSplit` option is disabled ([#13943](https://github.com/remix-run/react-router/pull/13943))\n- `@react-router/dev` - Allow `.mts` and `.mjs` extensions for route config file ([#13931](https://github.com/remix-run/react-router/pull/13931))\n- `@react-router/dev` - Fix prerender file locations when `cwd` differs from project root ([#13824](https://github.com/remix-run/react-router/pull/13824))\n- `@react-router/dev` - Improve chunk error logging when a chunk cannot be found during the build ([#13799](https://github.com/remix-run/react-router/pull/13799))\n- `@react-router/dev` - Fix incorrectly configured `externalConditions` which had enabled `module` condition for externals and broke builds with certain packages (like Emotion) ([#13871](https://github.com/remix-run/react-router/pull/13871))\n\n### Unstable Changes\n\n⚠️ _[Unstable features](https://reactrouter.com/community/api-development-strategy#unstable-flags) are not recommended for production use_\n\n- Add unstable RSC support for Data Mode ([#13700](https://github.com/remix-run/react-router/pull/13700))\n  - For more information, see the [RSC documentation](https://reactrouter.com/how-to/react-server-components)\n\n### Changes by Package\n\n- [`create-react-router`](https://github.com/remix-run/react-router/blob/react-router%407.7.0/packages/create-react-router/CHANGELOG.md#770)\n- [`react-router`](https://github.com/remix-run/react-router/blob/react-router%407.7.0/packages/react-router/CHANGELOG.md#770)\n- [`@react-router/architect`](https://github.com/remix-run/react-router/blob/react-router%407.7.0/packages/react-router-architect/CHANGELOG.md#770)\n- [`@react-router/cloudflare`](https://github.com/remix-run/react-router/blob/react-router%407.7.0/packages/react-router-cloudflare/CHANGELOG.md#770)\n- [`@react-router/dev`](https://github.com/remix-run/react-router/blob/react-router%407.7.0/packages/react-router-dev/CHANGELOG.md#770)\n- [`@react-router/express`](https://github.com/remix-run/react-router/blob/react-router%407.7.0/packages/react-router-express/CHANGELOG.md#770)\n- [`@react-router/fs-routes`](https://github.com/remix-run/react-router/blob/react-router%407.7.0/packages/react-router-fs-routes/CHANGELOG.md#770)\n- [`@react-router/node`](https://github.com/remix-run/react-router/blob/react-router%407.7.0/packages/react-router-node/CHANGELOG.md#770)\n- [`@react-router/remix-config-routes-adapter`](https://github.com/remix-run/react-router/blob/react-router%407.7.0/packages/react-router-remix-config-routes-adapter/CHANGELOG.md#770)\n- [`@react-router/serve`](https://github.com/remix-run/react-router/blob/react-router%407.7.0/packages/react-router-serve/CHANGELOG.md#770)\n\n**Full Changelog**: [`v7.6.3...v7.7.0`](https://github.com/remix-run/react-router/compare/react-router@7.6.3...react-router@7.7.0)\n\n## v7.6.3\n\nDate: 2025-06-27\n\n### Patch Changes\n\n- `react-router` - Do not serialize types for `useRouteLoaderData<typeof clientLoader>` ([#13752](https://github.com/remix-run/react-router/pull/13752))\n  - For types to distinguish a `clientLoader` from a `serverLoader`, you MUST annotate `clientLoader` args:\n\n    ```ts\n    //                                   👇 annotation required to skip serializing types\n    export function clientLoader({}: Route.ClientLoaderArgs) {\n      return { fn: () => \"earth\" };\n    }\n\n    function SomeComponent() {\n      const data = useRouteLoaderData<typeof clientLoader>(\"routes/this-route\");\n      const planet = data?.fn() ?? \"world\";\n      return <h1>Hello, {planet}!</h1>;\n    }\n    ```\n\n- `@react-router/cloudflare` - Remove `tsup` from `peerDependencies` ([#13757](https://github.com/remix-run/react-router/pull/13757))\n- `@react-router/dev` - Add Vite 7 support ([#13748](https://github.com/remix-run/react-router/pull/13748))\n- `@react-router/dev` - Skip `package.json` resolution checks when a custom `entry.server.(j|t)sx` file is provided ([#13744](https://github.com/remix-run/react-router/pull/13744))\n- `@react-router/dev` - Add validation for a route's id not being 'root' ([#13792](https://github.com/remix-run/react-router/pull/13792))\n- `@react-router/fs-routes` `@react-router/remix-config-routes-adapter` - Use `replaceAll` for normalizing windows file system slashes ([#13738](https://github.com/remix-run/react-router/pull/13738))\n- `@react-router/node` - Remove old \"install\" package exports ([#13762](https://github.com/remix-run/react-router/pull/13762))\n\n**Full Changelog**: [`v7.6.2...v7.6.3`](https://github.com/remix-run/react-router/compare/react-router@7.6.2...react-router@7.6.3)\n\n## v7.6.2\n\nDate: 2025-06-03\n\n### Patch Changes\n\n- `create-react-router` - Update `tar-fs` ([#13675](https://github.com/remix-run/react-router/pull/13675))\n- `react-router` - (INTERNAL) Slight refactor of internal `headers()` function processing for use with RSC ([#13639](https://github.com/remix-run/react-router/pull/13639))\n- `react-router` `@react-router/dev` - Avoid additional `with-props` chunk in Framework Mode by moving route module component prop logic from the Vite plugin to `react-router` ([#13650](https://github.com/remix-run/react-router/pull/13650))\n- `@react-router/dev` - When `future.unstable_viteEnvironmentApi` is enabled and an absolute Vite `base` has been configured, ensure critical CSS is handled correctly during development ([#13598](https://github.com/remix-run/react-router/pull/13598))\n- `@react-router/dev` - Update `vite-node` ([#13673](https://github.com/remix-run/react-router/pull/13673))\n- `@react-router/dev` - Fix typegen for non-{.js,.jsx,.ts,.tsx} routes like .mdx ([#12453](https://github.com/remix-run/react-router/pull/12453))\n- `@react-router/dev` - Fix href types for optional dynamic params ([#13725](https://github.com/remix-run/react-router/pull/13725))\n\n  7.6.1 introduced fixes for `href` when using optional static segments,\n  but those fixes caused regressions with how optional dynamic params worked in 7.6.0:\n\n  ```ts\n  // 7.6.0\n  href(\"/users/:id?\"); // ✅\n  href(\"/users/:id?\", { id: 1 }); // ✅\n\n  // 7.6.1\n  href(\"/users/:id?\"); // ❌\n  href(\"/users/:id?\", { id: 1 }); // ❌\n  ```\n\n  Now, optional static segments are expanded into different paths for `href`, but optional dynamic params are not.\n  This way `href` can unambiguously refer to an exact URL path, all while keeping the number of path options to a minimum.\n\n  ```ts\n  // 7.6.2\n\n  // path: /users/:id?/edit?\n  href(\"\n  //    ^ suggestions when cursor is here:\n  //\n  //    /users/:id?\n  //    /users/:id?/edit\n  ```\n\n  Additionally, you can pass `params` from component props without needing to narrow them manually:\n\n  ```ts\n  declare const params: { id?: number };\n\n  // 7.6.0\n  href(\"/users/:id?\", params);\n\n  // 7.6.1\n  href(\"/users/:id?\", params); // ❌\n  \"id\" in params ? href(\"/users/:id\", params) : href(\"/users\"); // works... but is annoying\n\n  // 7.6.2\n  href(\"/users/:id?\", params); // restores behavior of 7.6.0\n  ```\n\n**Full Changelog**: [`v7.6.1...v7.6.2`](https://github.com/remix-run/react-router/compare/react-router@7.6.1...react-router@7.6.2)\n\n## v7.6.1\n\nDate: 2025-05-25\n\n### Patch Changes\n\n- `react-router` - Partially revert optimization added in `7.1.4` to reduce calls to `matchRoutes` because it surfaced other issues ([#13562](https://github.com/remix-run/react-router/pull/13562))\n- `react-router` - Update `Route.MetaArgs` to reflect that `data` can be potentially `undefined` ([#13563](https://github.com/remix-run/react-router/pull/13563))\n  - This is primarily for cases where a route `loader` threw an error to it's own `ErrorBoundary`, but it also arises in the case of a 404 which renders the root `ErrorBoundary`/`meta` but the root `loader` did not run because not routes matched\n- `react-router` - Avoid initial fetcher execution 404 error when Lazy Route Discovery is interrupted by a navigation ([#13564](https://github.com/remix-run/react-router/pull/13564))\n- `react-router` - Properly `href` replaces splats `*` ([#13593](https://github.com/remix-run/react-router/pull/13593))\n  - `href(\"/products/*\", { \"*\": \"/1/edit\" }); // -> /products/1/edit`\n- `@react-router/architect` - Update `@architect/functions` from `^5.2.0` to `^7.0.0` ([#13556](https://github.com/remix-run/react-router/pull/13556))\n- `@react-router/dev` - Prevent typegen with route files that are outside the `app/` directory ([#12996](https://github.com/remix-run/react-router/pull/12996))\n- `@react-router/dev` - Add additional logging to `build` command output when cleaning assets from server build ([#13547](https://github.com/remix-run/react-router/pull/13547))\n- `@react-router/dev` - Don't clean assets from server build when `build.ssrEmitAssets` has been enabled in Vite config ([#13547](https://github.com/remix-run/react-router/pull/13547))\n- `@react-router/dev` - Fix typegen when same route is used at multiple paths ([#13574](https://github.com/remix-run/react-router/pull/13574))\n  - For example, `routes/route.tsx` is used at 4 different paths here:\n\n    ```ts\n    import { type RouteConfig, route } from \"@react-router/dev/routes\";\n    export default [\n      route(\"base/:base\", \"routes/base.tsx\", [\n        route(\"home/:home\", \"routes/route.tsx\", { id: \"home\" }),\n        route(\"changelog/:changelog\", \"routes/route.tsx\", { id: \"changelog\" }),\n        route(\"splat/*\", \"routes/route.tsx\", { id: \"splat\" }),\n      ]),\n      route(\"other/:other\", \"routes/route.tsx\", { id: \"other\" }),\n    ] satisfies RouteConfig;\n    ```\n\n  - Previously, typegen would arbitrarily pick one of these paths to be the \"winner\" and generate types for the route module based on that path\n  - Now, typegen creates unions as necessary for alternate paths for the same route file\n\n- `@react-router/dev` - Better types for `params` ([#13543](https://github.com/remix-run/react-router/pull/13543))\n  - For example:\n\n    ```ts\n    // routes.ts\n    import { type RouteConfig, route } from \"@react-router/dev/routes\";\n\n    export default [\n      route(\"parent/:p\", \"routes/parent.tsx\", [\n        route(\"route/:r\", \"routes/route.tsx\", [\n          route(\"child1/:c1a/:c1b\", \"routes/child1.tsx\"),\n          route(\"child2/:c2a/:c2b\", \"routes/child2.tsx\"),\n        ]),\n      ]),\n    ] satisfies RouteConfig;\n    ```\n\n  - Previously, `params` for `routes/route` were calculated as `{ p: string, r: string }`.\n  - This incorrectly ignores params that could come from child routes\n  - If visiting `/parent/1/route/2/child1/3/4`, the actual params passed to `routes/route` will have a type of `{ p: string, r: string, c1a: string, c1b: string }`\n  - Now, `params` are aware of child routes and autocompletion will include child params as optionals:\n\n    ```ts\n    params.|\n    //     ^ cursor is here and you ask for autocompletion\n    // p: string\n    // r: string\n    // c1a?: string\n    // c1b?: string\n    // c2a?: string\n    // c2b?: string\n    ```\n\n  - You can also narrow the types for `params` as it is implemented as a normalized union of params for each page that includes `routes/route`:\n\n    ```ts\n    if (typeof params.c1a === 'string') {\n      params.|\n      //     ^ cursor is here and you ask for autocompletion\n      // p: string\n      // r: string\n      // c1a: string\n      // c1b: string\n    }\n    ```\n\n- `@react-router/dev` - Fix `href` for optional segments ([#13595](https://github.com/remix-run/react-router/pull/13595))\n  - Type generation now expands paths with optionals into their corresponding non-optional paths\n  - For example, the path `/user/:id?` gets expanded into `/user` and `/user/:id` to more closely model visitable URLs\n  - `href` then uses these expanded (non-optional) paths to construct type-safe paths for your app:\n\n    ```ts\n    // original: /user/:id?\n    // expanded: /user & /user/:id\n    href(\"/user\"); // ✅\n    href(\"/user/:id\", { id: 1 }); // ✅\n    ```\n\n  - This becomes even more important for static optional paths where there wasn't a good way to indicate whether the optional should be included in the resulting path:\n\n    ```ts\n    // original: /products/:id/detail?\n\n    // before\n    href(\"/products/:id/detail?\"); // ❌ How can we tell `href` to include or omit `detail?` segment with a complex API?\n\n    // now\n    // expanded: /products/:id & /products/:id/detail\n    href(\"/product/:id\"); // ✅\n    href(\"/product/:id/detail\"); // ✅\n    ```\n\n### Unstable Changes\n\n⚠️ _[Unstable features](https://reactrouter.com/community/api-development-strategy#unstable-flags) are not recommended for production use_\n\n- `@react-router/dev` - Renamed internal `react-router/route-module` export to `react-router/internal` ([#13543](https://github.com/remix-run/react-router/pull/13543))\n- `@react-router/dev` - Removed `Info` export from generated `+types/*` files ([#13543](https://github.com/remix-run/react-router/pull/13543))\n- `@react-router/dev` - Normalize dirent entry path across node versions when generating SRI manifest ([#13591](https://github.com/remix-run/react-router/pull/13591))\n\n**Full Changelog**: [`v7.6.0...v7.6.1`](https://github.com/remix-run/react-router/compare/react-router@7.6.0...react-router@7.6.1)\n\n## v7.6.0\n\nDate: 2025-05-08\n\n### What's Changed\n\n#### `routeDiscovery` Config Option\n\nWe've added a new config option in `7.6.0` which grants you more control over the Lazy Route Discovery feature. You can now configure the `/__manifest` path if you're running multiple RR applications on the same server, or you can also disable the feature entirely if your application is small enough and the feature isn't necessary.\n\n```ts\n// react-router.config.ts\n\nexport default {\n  // You can modify the manifest path used:\n  routeDiscovery: { mode: \"lazy\", manifestPath: \"/custom-manifest\" }\n\n  // Or you can disable this feature entirely and include all routes in the\n  // manifest on initial document load:\n  routeDiscovery: { mode: \"initial\" }\n\n  // If you don't specify anything, the default config is as follows, which enables\n  // Lazy Route Discovery and makes manifest requests to the `/__manifest` path:\n  // routeDiscovery: { mode: \"lazy\", manifestPath: \"/__manifest\" }\n} satisfies Config;\n```\n\n#### Automatic Types for Future Flags\n\nSome future flags alter the way types should work in React Router. Previously, you had to remember to manually opt-in to the new types. For example, for `future.unstable_middleware`:\n\n```ts\n// react-router.config.ts\n\n// Step 1: Enable middleware\nexport default {\n  future: {\n    unstable_middleware: true,\n  },\n};\n\n// Step 2: Enable middleware types\ndeclare module \"react-router\" {\n  interface Future {\n    unstable_middleware: true; // 👈 Enable middleware types\n  }\n}\n```\n\nIt was up to you to keep the runtime future flags synced with the types for those flags. This was confusing and error-prone.\n\nNow, React Router will automatically enable types for future flags. That means you only need to specify the runtime future flag:\n\n```ts\n// react-router.config.ts\n\n// Step 1: Enable middleware\nexport default {\n  future: {\n    unstable_middleware: true,\n  },\n};\n\n// No step 2! That's it!\n```\n\nBehind the scenes, React Router will generate the corresponding `declare module` into `.react-router/types`. Currently this is done in `.react-router/types/+register.ts` but this is an implementation detail that may change in the future.\n\n### Minor Changes\n\n- `react-router` - Added a new `routeDiscovery` option in `react-router.config.ts` to configure Lazy Route Discovery behavior ([#13451](https://github.com/remix-run/react-router/pull/13451))\n- `react-router` - Add support for route component props in `createRoutesStub` ([#13528](https://github.com/remix-run/react-router/pull/13528))\n  - This allows you to unit test your route components using the props instead of the hooks:\n\n    ```tsx\n    let RoutesStub = createRoutesStub([\n      {\n        path: \"/\",\n        Component({ loaderData }) {\n          let data = loaderData as { message: string };\n          return <pre data-testid=\"data\">Message: {data.message}</pre>;\n        },\n        loader() {\n          return { message: \"hello\" };\n        },\n      },\n    ]);\n\n    render(<RoutesStub />);\n\n    await waitFor(() => screen.findByText(\"Message: hello\"));\n    ```\n\n- `@react-router/dev` - Automatic types for future flags ([#13506](https://github.com/remix-run/react-router/pull/13506))\n\n### Patch Changes\n\nYou may notice this list is a bit larger than usual! The team ate their vegetables last week and spent the week [squashing bugs](https://x.com/BrooksLybrand/status/1918406062920589731) to work on lowering the issue count that had ballooned a bit since the v7 release.\n\n- `react-router` - Fix `react-router` module augmentation for `NodeNext` ([#13498](https://github.com/remix-run/react-router/pull/13498))\n- `react-router` - Don't bundle `react-router` in `react-router/dom` CJS export ([#13497](https://github.com/remix-run/react-router/pull/13497))\n- `react-router` - Fix bug where a submitting `fetcher` would get stuck in a `loading` state if a revalidating `loader` redirected ([#12873](https://github.com/remix-run/react-router/pull/12873))\n- `react-router` - Fix hydration error if a server `loader` returned `undefined` ([#13496](https://github.com/remix-run/react-router/pull/13496))\n- `react-router` - Fix initial load 404 scenarios in data mode ([#13500](https://github.com/remix-run/react-router/pull/13500))\n- `react-router` - Stabilize `useRevalidator`'s `revalidate` function ([#13542](https://github.com/remix-run/react-router/pull/13542))\n- `react-router` - Preserve status code if a `clientAction` throws a `data()` result in framework mode ([#13522](https://github.com/remix-run/react-router/pull/13522))\n- `react-router` - Be defensive against leading double slashes in paths to avoid `Invalid URL` errors from the URL constructor ([#13510](https://github.com/remix-run/react-router/pull/13510))\n  - Note we do not sanitize/normalize these paths - we only detect them so we can avoid the error that would be thrown by `new URL(\"//\", window.location.origin)`\n- `react-router` - Remove `Navigator` declaration for `navigator.connection.saveData` to avoid messing with any other types beyond `saveData` in user land ([#13512](https://github.com/remix-run/react-router/pull/13512))\n- `react-router` - Fix `handleError` `params` values on `.data` requests for routes with a dynamic param as the last URL segment ([#13481](https://github.com/remix-run/react-router/pull/13481))\n- `react-router` - Don't trigger an `ErrorBoundary` UI before the reload when we detect a manifest version mismatch in Lazy Route Discovery ([#13480](https://github.com/remix-run/react-router/pull/13480))\n- `react-router` - Inline `turbo-stream@2.4.1` dependency and fix decoding ordering of `Map`/`Set` instances ([#13518](https://github.com/remix-run/react-router/pull/13518))\n- `react-router` - Only render dev warnings during dev ([#13461](https://github.com/remix-run/react-router/pull/13461))\n- `react-router` - Short circuit post-processing on aborted `dataStrategy` requests ([#13521](https://github.com/remix-run/react-router/pull/13521))\n  - This resolves non-user-facing console errors of the form `Cannot read properties of undefined (reading 'result')`\n- `@react-router/dev` - Support project root directories without a `package.json` if it exists in a parent directory ([#13472](https://github.com/remix-run/react-router/pull/13472))\n- `@react-router/dev` - When providing a custom Vite config path via the CLI `--config`/`-c` flag, default the project root directory to the directory containing the Vite config when not explicitly provided ([#13472](https://github.com/remix-run/react-router/pull/13472))\n- `@react-router/dev` - In a `routes.ts` context, ensure the `--mode` flag is respected for `import.meta.env.MODE` ([#13485](https://github.com/remix-run/react-router/pull/13485))\n  - Previously, `import.meta.env.MODE` within a `routes.ts` context was always `\"development\"` for the `dev` and `typegen --watch` commands, but otherwise resolved to `\"production\"`. These defaults are still in place, but if a `--mode` flag is provided, this will now take precedence.\n- `@react-router/dev` - Ensure consistent project root directory resolution logic in CLI commands ([#13472](https://github.com/remix-run/react-router/pull/13472))\n- `@react-router/dev` - When executing `react-router.config.ts` and `routes.ts` with `vite-node`, ensure that PostCSS config files are ignored ([#13489](https://github.com/remix-run/react-router/pull/13489))\n- `@react-router/dev` - When extracting critical CSS during development, ensure it's loaded from the client environment to avoid issues with plugins that handle the SSR environment differently ([#13503](https://github.com/remix-run/react-router/pull/13503))\n- `@react-router/dev` - Fix \"Status message is not supported by HTTP/2\" error during dev when using HTTPS ([#13460](https://github.com/remix-run/react-router/pull/13460))\n- `@react-router/dev` - Update config when `react-router.config.ts` is created or deleted during development ([#12319](https://github.com/remix-run/react-router/pull/12319))\n- `@react-router/dev` - Skip unnecessary `routes.ts` evaluation before Vite build is started ([#13513](https://github.com/remix-run/react-router/pull/13513))\n- `@react-router/dev` - Fix `TS2300: Duplicate identifier` errors caused by generated types ([#13499](https://github.com/remix-run/react-router/pull/13499))\n- Previously, routes that had the same full path would cause duplicate entries in the generated types for `href` (`.react-router/types/+register.ts`), causing type checking errors\n\n### Unstable Changes\n\n⚠️ _[Unstable features](https://reactrouter.com/community/api-development-strategy#unstable-flags) are not recommended for production use_\n\n- `react-router` - Fix a few bugs with error bubbling in middleware use-cases ([#13538](https://github.com/remix-run/react-router/pull/13538))\n- `@react-router/dev` - When `future.unstable_viteEnvironmentApi` is enabled, ensure that `build.assetsDir` in Vite config is respected when `environments.client.build.assetsDir` is not configured ([#13491](https://github.com/remix-run/react-router/pull/13491))\n\n### Changes by Package\n\n- [`create-react-router`](https://github.com/remix-run/react-router/blob/react-router%407.6.0/packages/create-react-router/CHANGELOG.md#760)\n- [`react-router`](https://github.com/remix-run/react-router/blob/react-router%407.6.0/packages/react-router/CHANGELOG.md#760)\n- [`@react-router/architect`](https://github.com/remix-run/react-router/blob/react-router%407.6.0/packages/react-router-architect/CHANGELOG.md#760)\n- [`@react-router/cloudflare`](https://github.com/remix-run/react-router/blob/react-router%407.6.0/packages/react-router-cloudflare/CHANGELOG.md#760)\n- [`@react-router/dev`](https://github.com/remix-run/react-router/blob/react-router%407.6.0/packages/react-router-dev/CHANGELOG.md#760)\n- [`@react-router/express`](https://github.com/remix-run/react-router/blob/react-router%407.6.0/packages/react-router-express/CHANGELOG.md#760)\n- [`@react-router/fs-routes`](https://github.com/remix-run/react-router/blob/react-router%407.6.0/packages/react-router-fs-routes/CHANGELOG.md#760)\n- [`@react-router/node`](https://github.com/remix-run/react-router/blob/react-router%407.6.0/packages/react-router-node/CHANGELOG.md#760)\n- [`@react-router/remix-config-routes-adapter`](https://github.com/remix-run/react-router/blob/react-router%407.6.0/packages/react-router-remix-config-routes-adapter/CHANGELOG.md#760)\n- [`@react-router/serve`](https://github.com/remix-run/react-router/blob/react-router%407.6.0/packages/react-router-serve/CHANGELOG.md#760)\n\n**Full Changelog**: [`v7.5.3...v7.6.0`](https://github.com/remix-run/react-router/compare/react-router@7.5.3...react-router@7.6.0)\n\n## v7.5.3\n\nDate: 2025-04-28\n\n### Patch Changes\n\n- `react-router` - Fix bug where bubbled action errors would result in `loaderData` being cleared at the handling `ErrorBoundary` route ([#13476](https://github.com/remix-run/react-router/pull/13476))\n- `react-router` - Handle redirects from `clientLoader.hydrate` initial load executions ([#13477](https://github.com/remix-run/react-router/pull/13477))\n\n**Full Changelog**: [`v7.5.2...v7.5.3`](https://github.com/remix-run/react-router/compare/react-router@7.5.2...react-router@7.5.3)\n\n## v7.5.2\n\nDate: 2025-04-24\n\n### Security Notice\n\nFixed 2 security vulnerabilities that could result in cache-poisoning attacks by sending specific headers intended for build-time usage for SPA Mode and Pre-rendering ([GHSA-f46r-rw29-r322](https://github.com/remix-run/react-router/security/advisories/GHSA-f46r-rw29-r322), [GHSA-cpj6-fhp6-mr6j](https://github.com/remix-run/react-router/security/advisories/GHSA-cpj6-fhp6-mr6j)).\n\n### Patch Changes\n\n- `react-router` - Adjust approach for Pre-rendering/SPA Mode via headers ([#13453](https://github.com/remix-run/react-router/pull/13453))\n- `react-router` - Update Single Fetch to also handle the 204 redirects used in `?_data` requests in Remix v2 ([#13364](https://github.com/remix-run/react-router/pull/13364))\n  - This allows applications to trigger a redirect on `.data` requests from outside the scope of React Router (i.e., an `express`/`hono` middleware) the same way they did in Remix v2 before Single Fetch was implemented\n  - This is a bit of an escape hatch - the recommended way to handle this is redirecting from a root route middleware\n  - To use this functionality, you may return from a `.data` request wih a response as follows:\n    - Set a 204 status code\n    - Set an `X-Remix-Redirect: <new-location>` header\n    - Optionally, set `X-Remix-Replace: true` or `X-Remix-Reload-Document: true` headers to replicate `replace()`/`redirectDocument()` functionality\n  - ⚠️ Please note that these responses rely on implementation details that are subject to change without a SemVer major release, and it is recommended you set up integration tests for your application to confirm this functionality is working correctly with each future React Router upgrade\n\n**Full Changelog**: [`v7.5.1...v7.5.2`](https://github.com/remix-run/react-router/compare/react-router@7.5.1...react-router@7.5.2)\n\n## v7.5.1\n\nDate: 2025-04-17\n\n### Patch Changes\n\n- `react-router` - When using the object-based `route.lazy` API, the `HydrateFallback` and `hydrateFallbackElement` properties are now skipped when lazy loading routes after hydration ([#13376](https://github.com/remix-run/react-router/pull/13376))\n  - If you move the code for these properties into a separate file, since the hydrate properties were unused already (if the route wasn't present during hydration), you can avoid downloading them at all. For example:\n\n    ```ts\n    createBrowserRouter([\n      {\n        path: \"/show/:showId\",\n        lazy: {\n          loader: async () => (await import(\"./show.loader.js\")).loader,\n          Component: async () =>\n            (await import(\"./show.component.js\")).Component,\n          HydrateFallback: async () =>\n            (await import(\"./show.hydrate-fallback.js\")).HydrateFallback,\n        },\n      },\n    ]);\n    ```\n\n- `react-router` - Fix single fetch bug where no revalidation request would be made when navigating upwards to a reused parent route ([#13253](https://github.com/remix-run/react-router/pull/13253))\n- `react-router` - Properly revalidate pre-rendered paths when param values change when using `ssr:false` + `prerender` configs ([#13380](https://github.com/remix-run/react-router/pull/13380))\n- `react-router` - Fix pre-rendering when a loader returns a redirect ([#13365](https://github.com/remix-run/react-router/pull/13365))\n- `react-router` - Do not automatically add `null` to `staticHandler.query()` `context.loaderData` if routes do not have loaders ([#13223](https://github.com/remix-run/react-router/pull/13223))\n  - This was a Remix v2 implementation detail inadvertently left in for React Router v7\n  - Now that we allow returning `undefined` from loaders, our prior check of `loaderData[routeId] !== undefined` was no longer sufficient and was changed to a `routeId in loaderData` check - these `null` values can cause issues for this new check\n  - ⚠️ This could be a \"breaking bug fix\" for you if you are doing manual SSR with `createStaticHandler()`/`<StaticRouterProvider>`, and using `context.loaderData` to control `<RouterProvider>` hydration behavior on the client\n\n### Unstable Changes\n\n⚠️ _[Unstable features](https://reactrouter.com/community/api-development-strategy#unstable-flags) are not recommended for production use_\n\n- `react-router` - Add better error messaging when `getLoadContext` is not updated to return a `Map` ([#13242](https://github.com/remix-run/react-router/pull/13242))\n- `react-router` - Update context type for `LoaderFunctionArgs`/`ActionFunctionArgs` when middleware is enabled ([#13381](https://github.com/remix-run/react-router/pull/13381))\n- `react-router` - Add a new `unstable_runClientMiddleware` argument to `dataStrategy` to enable middleware execution in custom `dataStrategy` implementations ([#13395](https://github.com/remix-run/react-router/pull/13395))\n- `react-router` - Add support for the new `unstable_shouldCallHandler`/`unstable_shouldRevalidateArgs` APIs in `dataStrategy` ([#13253](https://github.com/remix-run/react-router/pull/13253))\n\n**Full Changelog**: [`v7.5.0...v7.5.1`](https://github.com/remix-run/react-router/compare/react-router@7.5.0...react-router@7.5.1)\n\n## v7.5.0\n\nDate: 2025-04-04\n\n### What's Changed\n\n#### `route.lazy` Object API\n\nWe've introduced a new `route.lazy` API which gives you more granular control over the lazy loading of route properties that you could not achieve with the `route.lazy()` function signature. This is useful for Framework mode and performance-critical library mode applications.\n\n```ts\ncreateBrowserRouter([\n  {\n    path: \"/show/:showId\",\n    lazy: {\n      loader: async () => (await import(\"./show.loader.js\")).loader,\n      action: async () => (await import(\"./show.action.js\")).action,\n      Component: async () => (await import(\"./show.component.js\")).Component,\n    },\n  },\n]);\n```\n\n⚠️ This is a breaking change if you have adopted the `route.unstable_lazyMiddleware` API which has been removed in favor of `route.lazy.unstable_middleware`. See the `Unstable Changes` section below for more information.\n\n### Minor Changes\n\n- `react-router` - Add granular object-based API for `route.lazy` to support lazy loading of individual route properties ([#13294](https://github.com/remix-run/react-router/pull/13294))\n\n### Patch Changes\n\n- `@react-router/dev` - Update optional `wrangler` peer dependency range to support `wrangler` v4 ([#13258](https://github.com/remix-run/react-router/pull/13258))\n- `@react-router/dev` - Reinstate dependency optimization in the child compiler to fix `depsOptimizer is required in dev mode` errors when using `vite-plugin-cloudflare` and importing Node.js builtins ([#13317](https://github.com/remix-run/react-router/pull/13317))\n\n### Unstable Changes\n\n⚠️ _[Unstable features](https://reactrouter.com/community/api-development-strategy#unstable-flags) are not recommended for production use_\n\n- `react-router` - Introduce `future.unstable_subResourceIntegrity` flag that enables generation of an `importmap` with `integrity` for the scripts that will be loaded by the browser ([#13163](https://github.com/remix-run/react-router/pull/13163))\n- `react-router` - Remove support for the `route.unstable_lazyMiddleware` property ([#13294](https://github.com/remix-run/react-router/pull/13294))\n  - In order to lazily load middleware, you can use the new object-based `route.lazy.unstable_middleware` API\n- `@react-router/dev` - When `future.unstable_viteEnvironmentApi` is enabled, ensure critical CSS in development works when using a custom Vite `base` has been configured ([#13305](https://github.com/remix-run/react-router/pull/13305))\n\n### Changes by Package\n\n- [`create-react-router`](https://github.com/remix-run/react-router/blob/react-router%407.5.0/packages/create-react-router/CHANGELOG.md#750)\n- [`react-router`](https://github.com/remix-run/react-router/blob/react-router%407.5.0/packages/react-router/CHANGELOG.md#750)\n- [`@react-router/architect`](https://github.com/remix-run/react-router/blob/react-router%407.5.0/packages/react-router-architect/CHANGELOG.md#750)\n- [`@react-router/cloudflare`](https://github.com/remix-run/react-router/blob/react-router%407.5.0/packages/react-router-cloudflare/CHANGELOG.md#750)\n- [`@react-router/dev`](http://github.com/remix-run/react-router/blob/react-router%407.5.0/packages/react-router-dev/CHANGELOG.md#750)\n- [`@react-router/express`](https://github.com/remix-run/react-router/blob/react-router%407.5.0/packages/react-router-express/CHANGELOG.md#750)\n- [`@react-router/fs-routes`](https://github.com/remix-run/react-router/blob/react-router%407.5.0/packages/react-router-fs-routes/CHANGELOG.md#750)\n- [`@react-router/node`](https://github.com/remix-run/react-router/blob/react-router%407.5.0/packages/react-router-node/CHANGELOG.md#750)\n- [`@react-router/remix-config-routes-adapter`](https://github.com/remix-run/react-router/blob/react-router%407.5.0/packages/react-router-remix-config-routes-adapter/CHANGELOG.md#750)\n- [`@react-router/serve`](https://github.com/remix-run/react-router/blob/react-router%407.5.0/packages/react-router-serve/CHANGELOG.md#750)\n\n**Full Changelog**: [`v7.4.1...v7.5.0`](https://github.com/remix-run/react-router/compare/react-router@7.4.1...react-router@7.5.0)\n\n## v7.4.1\n\nDate: 2025-03-28\n\n### Security Notice\n\nFixed a security vulnerability that allowed URL manipulation and potential cache pollution via the `Host` and `X-Forwarded-Host` headers due to inadequate port sanitization ([GHSA-4q56-crqp-v477/CVE-2025-31137](https://github.com/remix-run/react-router/security/advisories/GHSA-4q56-crqp-v477)).\n\n### Patch Changes\n\n- `react-router` - Dedupe calls to `route.lazy` functions ([#13260](https://github.com/remix-run/react-router/pull/13260))\n- `@react-router/dev` - Fix path in prerender error messages ([#13257](https://github.com/remix-run/react-router/pull/13257))\n- `@react-router/dev` - Fix typegen for virtual modules when `moduleDetection` is set to `force` ([#13267](https://github.com/remix-run/react-router/pull/13267))\n- `@react-router/express` - Better validation of `x-forwarded-host` header to prevent potential security issues ([#13309](https://github.com/remix-run/react-router/pull/13309))\n\n### Unstable Changes\n\n⚠️ _[Unstable features](https://reactrouter.com/community/api-development-strategy#unstable-flags) are not recommended for production use_\n\n- `react-router` - Fix types on `unstable_MiddlewareFunction` to avoid type errors when a middleware doesn't return a value ([#13311](https://github.com/remix-run/react-router/pull/13311))\n- `react-router` - Add support for `route.unstable_lazyMiddleware` function to allow lazy loading of middleware logic ([#13210](https://github.com/remix-run/react-router/pull/13210))\n  - ⚠️ We do not recommend adoption of this API currently as we are likely going to change it prior to the stable release of middleware\n  - ⚠️ This may be a breaking change if your app is currently returning `unstable_middleware` from `route.lazy`\n  - The `route.unstable_middleware` property is no longer supported in the return value from `route.lazy`\n  - If you want to lazily load middleware, you must use `route.unstable_lazyMiddleware`\n- `@react-router/dev` - When both `future.unstable_middleware` and `future.unstable_splitRouteModules` are enabled, split `unstable_clientMiddleware` route exports into separate chunks when possible ([#13210](https://github.com/remix-run/react-router/pull/13210))\n- `@react-router/dev` - Improve performance of `future.unstable_middleware` by ensuring that route modules are only blocking during the middleware phase when the `unstable_clientMiddleware` has been defined ([#13210](https://github.com/remix-run/react-router/pull/13210))\n\n**Full Changelog**: [`v7.4.0...v7.4.1`](https://github.com/remix-run/react-router/compare/react-router@7.4.0...react-router@7.4.1)\n\n## v7.4.0\n\nDate: 2025-03-19\n\n### Minor Changes\n\n- `@react-router/dev` - Generate types for `virtual:react-router/server-build` module ([#13152](https://github.com/remix-run/react-router/pull/13152))\n\n### Patch Changes\n\n- `react-router` - Fix root loader data on initial load redirects in SPA mode ([#13222](https://github.com/remix-run/react-router/pull/13222))\n- `react-router` - Load ancestor pathless/index routes in lazy route discovery for upwards non-eager-discovery routing ([#13203](https://github.com/remix-run/react-router/pull/13203))\n- `react-router` - Fix `shouldRevalidate` behavior for `clientLoader`-only routes in `ssr:true` apps ([#13221](https://github.com/remix-run/react-router/pull/13221))\n- `@react-router/dev` - Fix conflicts with other Vite plugins that use the `configureServer` and/or `configurePreviewServer` hooks ([#13184](https://github.com/remix-run/react-router/pull/13184))\n\n### Unstable Changes\n\n⚠️ _[Unstable features](https://reactrouter.com/community/api-development-strategy#unstable-flags) are not recommended for production use_\n\n- `react-router` - If a middleware throws an error, ensure we only bubble the error itself via `next()` and are no longer leaking the `MiddlewareError` implementation detail ([#13180](https://github.com/remix-run/react-router/pull/13180))\n  - ⚠️ This may be a breaking change if you are `catch`-ing errors thrown by the `next()` function in your middlewares\n- `react-router` - Fix `RequestHandler` `loadContext` parameter type when middleware is enabled ([#13204](https://github.com/remix-run/react-router/pull/13204))\n- `react-router` - Update `Route.unstable_MiddlewareFunction` to have a return value of `Response | undefined` instead of `Response | void` ([#13199](https://github.com/remix-run/react-router/pull/13199))\n- `@react-router/dev` - When `future.unstable_splitRouteModules` is set to `\"enforce\"`, allow both splittable and unsplittable root route exports since it's always in a single chunk ([#13238](https://github.com/remix-run/react-router/pull/13238))\n- `@react-router/dev` - When `future.unstable_viteEnvironmentApi` is enabled, allow plugins that override the default SSR environment (such as `@cloudflare/vite-plugin`) to be placed before or after the React Router plugin ([#13183](https://github.com/remix-run/react-router/pull/13183))\n\n### Changes by Package\n\n- [`create-react-router`](https://github.com/remix-run/react-router/blob/react-router%407.4.0/packages/create-react-router/CHANGELOG.md#740)\n- [`react-router`](https://github.com/remix-run/react-router/blob/react-router%407.4.0/packages/react-router/CHANGELOG.md#740)\n- [`@react-router/architect`](https://github.com/remix-run/react-router/blob/react-router%407.4.0/packages/react-router-architect/CHANGELOG.md#740)\n- [`@react-router/cloudflare`](https://github.com/remix-run/react-router/blob/react-router%407.4.0/packages/react-router-cloudflare/CHANGELOG.md#740)\n- [`@react-router/dev`](https://github.com/remix-run/react-router/blob/react-router%407.4.0/packages/react-router-dev/CHANGELOG.md#740)\n- [`@react-router/express`](https://github.com/remix-run/react-router/blob/react-router%407.4.0/packages/react-router-express/CHANGELOG.md#740)\n- [`@react-router/fs-routes`](https://github.com/remix-run/react-router/blob/react-router%407.4.0/packages/react-router-fs-routes/CHANGELOG.md#740)\n- [`@react-router/node`](https://github.com/remix-run/react-router/blob/react-router%407.4.0/packages/react-router-node/CHANGELOG.md#740)\n- [`@react-router/remix-config-routes-adapter`](https://github.com/remix-run/react-router/blob/react-router%407.4.0/packages/react-router-remix-config-routes-adapter/CHANGELOG.md#740)\n- [`@react-router/serve`](https://github.com/remix-run/react-router/blob/react-router%407.4.0/packages/react-router-serve/CHANGELOG.md#740)\n\n**Full Changelog**: [`v7.3.0...v7.4.0`](https://github.com/remix-run/react-router/compare/react-router@7.3.0...react-router@7.4.0)\n\n## v7.3.0\n\nDate: 2025-03-06\n\n### Minor Changes\n\n- Add `fetcherKey` as a parameter to `patchRoutesOnNavigation` ([#13061](https://github.com/remix-run/react-router/pull/13061))\n\n### Patch Changes\n\n- `react-router` - Detect and handle manifest-skew issues on new deploys during active sessions ([#13061](https://github.com/remix-run/react-router/pull/13061))\n  - In framework mode, Lazy Route Discovery will now detect manifest version mismatches in active sessions after a new deploy\n  - On navigations to undiscovered routes, this mismatch will trigger a document reload of the destination path\n  - On `fetcher` calls to undiscovered routes, this mismatch will trigger a document reload of the current path\n- `react-router` - Skip resource route flow in dev server in SPA mode ([#13113](https://github.com/remix-run/react-router/pull/13113))\n- `react-router` - Fix single fetch `_root.data` requests when a `basename` is used ([#12898](https://github.com/remix-run/react-router/pull/12898))\n- `react-router` - Fix types for `loaderData` and `actionData` that contained `Record`s ([#13139](https://github.com/remix-run/react-router/pull/13139))\n  - ⚠️ This is a breaking change for users who have already adopted `unstable_SerializesTo` - see the note in the `Unstable Changes` section below for more information\n- `@react-router/dev` - Fix support for custom client `build.rollupOptions.output.entryFileNames` ([#13098](https://github.com/remix-run/react-router/pull/13098))\n- `@react-router/dev` - Fix usage of `prerender` option when `serverBundles` option has been configured or provided by a preset, e.g. `vercelPreset` from `@vercel/react-router` ([#13082](https://github.com/remix-run/react-router/pull/13082))\n- `@react-router/dev` - Fix support for custom `build.assetsDir` ([#13077](https://github.com/remix-run/react-router/pull/13077))\n- `@react-router/dev` - Remove unused dependencies ([#13134](https://github.com/remix-run/react-router/pull/13134))\n- `@react-router/dev` - Stub all routes except root in \"SPA Mode\" server builds to avoid issues when route modules or their dependencies import non-SSR-friendly modules ([#13023](https://github.com/remix-run/react-router/pull/13023))\n- `@react-router/dev` - Remove unused Vite file system watcher ([#13133](https://github.com/remix-run/react-router/pull/13133))\n- `@react-router/dev` - Fix support for custom SSR build input when `serverBundles` option has been configured ([#13107](https://github.com/remix-run/react-router/pull/13107))\n  - ⚠️ Note that for consumers using the `future.unstable_viteEnvironmentApi` and `serverBundles` options together, hyphens are no longer supported in server bundle IDs since they also need to be valid Vite environment names.\n- `@react-router/dev` - Fix dev server when using HTTPS by stripping HTTP/2 pseudo headers from dev server requests ([#12830](https://github.com/remix-run/react-router/pull/12830))\n- `@react-router/dev` - Lazy load Cloudflare platform proxy on first dev server request when using the `cloudflareDevProxy` Vite plugin to avoid creating unnecessary `workerd` processes ([#13016](https://github.com/remix-run/react-router/pull/13016))\n- `@react-router/dev` - Fix duplicated entries in typegen for layout routes and their corresponding index route ([#13140](https://github.com/remix-run/react-router/pull/13140))\n- `@react-router/express` - Update `express` `peerDependency` to include v5 (https://github.com/remix-run/react-router/pull/13064) ([#12961](https://github.com/remix-run/react-router/pull/12961))\n\n### Unstable Changes\n\n⚠️ _[Unstable features](https://reactrouter.com/community/api-development-strategy#unstable-flags) are not recommended for production use_\n\n- `react-router` - Add `context` support to client side data routers (unstable) ([#12941](https://github.com/remix-run/react-router/pull/12941))\n- `react-router` - Support middleware on routes (unstable) ([#12941](https://github.com/remix-run/react-router/pull/12941))\n- `@react-router/dev` - Fix errors with `future.unstable_viteEnvironmentApi` when the `ssr` environment has been configured by another plugin to be a custom `Vite.DevEnvironment` rather than the default `Vite.RunnableDevEnvironment` ([#13008](https://github.com/remix-run/react-router/pull/13008))\n- `@react-router/dev` - When `future.unstable_viteEnvironmentApi` is enabled and the `ssr` environment has `optimizeDeps.noDiscovery` disabled, define `optimizeDeps.entries` and `optimizeDeps.include` ([#13007](https://github.com/remix-run/react-router/pull/13007))\n\n#### Client-side `context` (unstable)\n\nYour application `clientLoader`/`clientAction` functions (or `loader`/`action` in library mode) will now receive a `context` parameter on the client. This is an instance of `unstable_RouterContextProvider` that you use with type-safe contexts (similar to `React.createContext`) and is most useful with the corresponding `unstable_clientMiddleware` API:\n\n```ts\nimport { unstable_createContext } from \"react-router\";\n\ntype User = {\n  /*...*/\n};\n\nconst userContext = unstable_createContext<User>();\n\nconst sessionMiddleware: Route.unstable_ClientMiddlewareFunction = async ({\n  context,\n}) => {\n  let user = await getUser();\n  context.set(userContext, user);\n};\n\nexport const unstable_clientMiddleware = [sessionMiddleware];\n\nexport function clientLoader({ context }: Route.ClientLoaderArgs) {\n  let user = context.get(userContext);\n  let profile = await getProfile(user.id);\n  return { profile };\n}\n```\n\nSimilar to server-side requests, a fresh `context` will be created per navigation (or `fetcher` call). If you have initial data you'd like to populate in the context for every request, you can provide an `unstable_getContext` function at the root of your app:\n\n- Library mode - `createBrowserRouter(routes, { unstable_getContext })`\n- Framework mode - `<HydratedRouter unstable_getContext>`\n\nThis function should return an value of type `unstable_InitialContext` which is a `Map<unstable_RouterContext, unknown>` of context's and initial values:\n\n```ts\nconst loggerContext = unstable_createContext<(...args: unknown[]) => void>();\n\nfunction logger(...args: unknown[]) {\n  console.log(new Date.toISOString(), ...args);\n}\n\nfunction unstable_getContext() {\n  let map = new Map();\n  map.set(loggerContext, logger);\n  return map;\n}\n```\n\n#### Middleware (unstable)\n\nMiddleware is implemented behind a `future.unstable_middleware` flag. To enable, you must enable the flag and the types in your `react-router.config.ts` file:\n\n```ts\nimport type { Config } from \"@react-router/dev/config\";\nimport type { Future } from \"react-router\";\n\ndeclare module \"react-router\" {\n  interface Future {\n    unstable_middleware: true; // 👈 Enable middleware types\n  }\n}\n\nexport default {\n  future: {\n    unstable_middleware: true, // 👈 Enable middleware\n  },\n} satisfies Config;\n```\n\n⚠️ Middleware is unstable and should not be adopted in production. There is at least one known de-optimization in route module loading for `clientMiddleware` that we will be addressing this before a stable release.\n\n⚠️ Enabling middleware contains a breaking change to the `context` parameter passed to your `loader`/`action` functions - see below for more information.\n\nOnce enabled, routes can define an array of middleware functions that will run sequentially before route handlers run. These functions accept the same parameters as `loader`/`action` plus an additional `next` parameter to run the remaining data pipeline. This allows middlewares to perform logic before and after handlers execute.\n\n```tsx\n// Framework mode\nexport const unstable_middleware = [serverLogger, serverAuth]; // server\nexport const unstable_clientMiddleware = [clientLogger]; // client\n\n// Library mode\nconst routes = [\n  {\n    path: \"/\",\n    // Middlewares are client-side for library mode SPA's\n    unstable_middleware: [clientLogger, clientAuth],\n    loader: rootLoader,\n    Component: Root,\n  },\n];\n```\n\nHere's a simple example of a client-side logging middleware that can be placed on the root route:\n\n```tsx\nconst clientLogger: Route.unstable_ClientMiddlewareFunction = async (\n  { request },\n  next,\n) => {\n  let start = performance.now();\n\n  // Run the remaining middlewares and all route loaders\n  await next();\n\n  let duration = performance.now() - start;\n  console.log(`Navigated to ${request.url} (${duration}ms)`);\n};\n```\n\nNote that in the above example, the `next`/`middleware` functions don't return anything. This is by design as on the client there is no \"response\" to send over the network like there would be for middlewares running on the server. The data is all handled behind the scenes by the stateful `router`.\n\nFor a server-side middleware, the `next` function will return the HTTP `Response` that React Router will be sending across the wire, thus giving you a chance to make changes as needed. You may throw a new response to short circuit and respond immediately, or you may return a new or altered response to override the default returned by `next()`.\n\n```tsx\nconst serverLogger: Route.unstable_MiddlewareFunction = async (\n  { request, params, context },\n  next,\n) => {\n  let start = performance.now();\n\n  // 👇 Grab the response here\n  let res = await next();\n\n  let duration = performance.now() - start;\n  console.log(`Navigated to ${request.url} (${duration}ms)`);\n\n  // 👇 And return it here (optional if you don't modify the response)\n  return res;\n};\n```\n\nYou can throw a `redirect` from a middleware to short circuit any remaining processing:\n\n```tsx\nimport { sessionContext } from \"../context\";\nconst serverAuth: Route.unstable_MiddlewareFunction = (\n  { request, params, context },\n  next,\n) => {\n  let session = context.get(sessionContext);\n  let user = session.get(\"user\");\n  if (!user) {\n    session.set(\"returnTo\", request.url);\n    throw redirect(\"/login\", 302);\n  }\n};\n```\n\n_Note that in cases like this where you don't need to do any post-processing you don't need to call the `next` function or return a `Response`._\n\nHere's another example of using a server middleware to detect 404s and check the CMS for a redirect:\n\n```tsx\nconst redirects: Route.unstable_MiddlewareFunction = async ({\n  request,\n  next,\n}) => {\n  // attempt to handle the request\n  let res = await next();\n\n  // if it's a 404, check the CMS for a redirect, do it last\n  // because it's expensive\n  if (res.status === 404) {\n    let cmsRedirect = await checkCMSRedirects(request.url);\n    if (cmsRedirect) {\n      throw redirect(cmsRedirect, 302);\n    }\n  }\n\n  return res;\n};\n```\n\nFor more information on the `middleware` API/design, please see the [decision doc](https://github.com/remix-run/react-router/blob/release-next/decisions/0014-context-middleware.md).\n\n##### Middleware `context` parameter\n\nWhen middleware is enabled, your application will use a different type of `context` parameter in your loaders and actions to provide better type safety. Instead of `AppLoadContext`, `context` will now be an instance of `ContextProvider` that you can use with type-safe contexts (similar to `React.createContext`):\n\n```ts\nimport { unstable_createContext } from \"react-router\";\nimport { Route } from \"./+types/root\";\nimport type { Session } from \"./sessions.server\";\nimport { getSession } from \"./sessions.server\";\n\nlet sessionContext = unstable_createContext<Session>();\n\nconst sessionMiddleware: Route.unstable_MiddlewareFunction = ({\n  context,\n  request,\n}) => {\n  let session = await getSession(request);\n  context.set(sessionContext, session);\n  //                          ^ must be of type Session\n};\n\n// ... then in some downstream middleware\nconst loggerMiddleware: Route.unstable_MiddlewareFunction = ({\n  context,\n  request,\n}) => {\n  let session = context.get(sessionContext);\n  //  ^ typeof Session\n  console.log(session.get(\"userId\"), request.method, request.url);\n};\n\n// ... or some downstream loader\nexport function loader({ context }: Route.LoaderArgs) {\n  let session = context.get(sessionContext);\n  let profile = await getProfile(session.get(\"userId\"));\n  return { profile };\n}\n```\n\nIf you are using a custom server with a `getLoadContext` function, the return value for initial context values passed from the server adapter layer is no longer an object and should now return an `unstable_InitialContext` (`Map<RouterContext, unknown>`):\n\n```ts\nlet adapterContext = unstable_createContext<MyAdapterContext>();\n\nfunction getLoadContext(req, res): unstable_InitialContext {\n  let map = new Map();\n  map.set(adapterContext, getAdapterContext(req));\n  return map;\n}\n```\n\n#### `unstable_SerializesTo`\n\n`unstable_SerializesTo` added a way to register custom serialization types in Single Fetch for other library and framework authors like Apollo. It was implemented with branded type whose branded property that was made optional so that casting arbitrary values was easy:\n\n```ts\n// without the brand being marked as optional\nlet x1 = 42 as unknown as unstable_SerializesTo<number>;\n//          ^^^^^^^^^^\n\n// with the brand being marked as optional\nlet x2 = 42 as unstable_SerializesTo<number>;\n```\n\nHowever, this broke type inference in `loaderData` and `actionData` for any `Record` types as those would now (incorrectly) match `unstable_SerializesTo`. This affected all users, not just those that depended on `unstable_SerializesTo`. To fix this, the branded property of `unstable_SerializesTo` is marked as required instead of optional.\n\nFor library and framework authors using `unstable_SerializesTo`, you may need to add `as unknown` casts before casting to `unstable_SerializesTo`.\n\n### Changes by Package\n\n- [`create-react-router`](https://github.com/remix-run/react-router/blob/react-router%407.3.0/packages/create-react-router/CHANGELOG.md#730)\n- [`react-router`](https://github.com/remix-run/react-router/blob/react-router%407.3.0/packages/react-router/CHANGELOG.md#730)\n- [`@react-router/architect`](https://github.com/remix-run/react-router/blob/react-router%407.3.0/packages/react-router-architect/CHANGELOG.md#730)\n- [`@react-router/cloudflare`](https://github.com/remix-run/react-router/blob/react-router%407.3.0/packages/react-router-cloudflare/CHANGELOG.md#730)\n- [`@react-router/dev`](https://github.com/remix-run/react-router/blob/react-router%407.3.0/packages/react-router-dev/CHANGELOG.md#730)\n- [`@react-router/express`](https://github.com/remix-run/react-router/blob/react-router%407.3.0/packages/react-router-express/CHANGELOG.md#730)\n- [`@react-router/fs-routes`](https://github.com/remix-run/react-router/blob/react-router%407.3.0/packages/react-router-fs-routes/CHANGELOG.md#730)\n- [`@react-router/node`](https://github.com/remix-run/react-router/blob/react-router%407.3.0/packages/react-router-node/CHANGELOG.md#730)\n- [`@react-router/remix-config-routes-adapter`](https://github.com/remix-run/react-router/blob/react-router%407.3.0/packages/react-router-remix-config-routes-adapter/CHANGELOG.md#730)\n- [`@react-router/serve`](https://github.com/remix-run/react-router/blob/react-router%407.3.0/packages/react-router-serve/CHANGELOG.md#730)\n\n**Full Changelog**: [`v7.2.0...v7.3.0`](https://github.com/remix-run/react-router/compare/react-router@7.2.0...react-router@7.3.0)\n\n## v7.2.0\n\nDate: 2025-02-18\n\n### What's Changed\n\n#### Type-safe `href` utility\n\nIn framework mode, we now provide you with a fully type-safe `href` utility to give you all the warm and fuzzy feelings of path auto-completion and param validation for links in your application:\n\n```tsx\nimport { href } from \"react-router\";\n\nexport default function Component() {\n  const link = href(\"/blog/:slug\", { slug: \"my-first-post\" });\n  //                ^ type-safe!     ^ Also type-safe!\n\n  return (\n    <main>\n      <Link to={href(\"/products/:id\", { id: \"asdf\" })} />\n      <NavLink to={href(\"/:lang?/about\", { lang: \"en\" })} />\n    </main>\n  );\n}\n```\n\nYou'll now get type errors if you pass a bad path value or a bad param value:\n\n```ts\nconst badPath = href(\"/not/a/valid/path\");\n//                   ^ Error!\n\nconst badParam = href(\"/blog/:slug\", { oops: \"bad param\" });\n//                                     ^ Error!\n```\n\n#### Prerendering with a SPA Fallback\n\nThis release enhances the ability to use a combination of pre-rendered paths alongside other paths that operate in \"SPA Mode\" when pre-rendering with `ssr:false`.\n\n- If you specify `ssr:false` without a `prerender` config, this is considered \"SPA Mode\" and the generated `index.html` file will only render down to the root route and will be able to hydrate for any valid application path\n- If you specify `ssr:false` with a `prerender` config but _do not_ include the `/` path (i.e., `prerender: ['/blog/post']`), then we still generate a \"SPA Mode\" `index.html` file that can hydrate for any path in the application\n- If you specify `ssr:false` and include the `/` path in your `prerender` config, the generated `index.html` file will be specific to the root index route, so we will now also generate a separate \"SPA Mode\" file in `__spa-fallback.html` that you can serve/hydrate for non-prerendered paths\n\nFor more info, see the [Pre-rendering](https://reactrouter.com/dev/how-to/pre-rendering#pre-rendering-with-a-spa-fallback) docs for more info.\n\n#### Allow a root `loader` in SPA Mode\n\nSPA Mode used to prohibit the use of loaders in all routes so that we could hydrate for any path in the application. However, because the root route is always rendered at build time, we can lift this restriction for the root route.\n\nIn order to use your build-time loader data during pre-rendering, we now also expose the `loaderData` as an optional prop for the `HydrateFallback` component on routes:\n\n- This will be defined so long as the `HydrateFallback` is rendering because _children_ routes are loading\n- This will be `undefined` if the `HydrateFallback` is rendering because the route itself has it's own hydrating `clientLoader`\n  - In SPA mode, this will allow you to render loader root data into the SPA Mode HTML file\n\n### Minor Changes\n\n- `react-router` - New type-safe `href` utility that guarantees links point to actual paths in your app ([#13012](https://github.com/remix-run/react-router/pull/13012))\n- `@react-router/dev` - Generate a \"SPA fallback\" HTML file when pre-rendering the `/` route with `ssr:false` ([#12948](https://github.com/remix-run/react-router/pull/12948))\n- `@react-router/dev` - Allow a `loader` in the root route in SPA mode because it can be called/server-rendered at build time ([#12948](https://github.com/remix-run/react-router/pull/12948))\n  - `Route.HydrateFallbackProps` now also receives `loaderData`\n\n### Patch Changes\n\n- `react-router` - Disable Lazy Route Discovery for all `ssr:false` apps and not just \"SPA Mode\" because there is no runtime server to serve the search-param-configured `__manifest` requests ([#12894](https://github.com/remix-run/react-router/pull/12894))\n  - We previously only disabled this for \"SPA Mode\" but we realized it should apply to all `ssr:false` apps\n  - In those `prerender` scenarios we would pre-render the `/__manifest` file but that makes some unnecessary assumptions about the static file server behaviors\n- `react-router` - Don't apply Single Fetch revalidation de-optimization when in SPA mode since there is no server HTTP request ([#12948](https://github.com/remix-run/react-router/pull/12948))\n- `react-router` - Properly handle revalidations to across a pre-render/SPA boundary ([#13021](https://github.com/remix-run/react-router/pull/13021))\n  - In \"hybrid\" applications where some routes are pre-rendered and some are served from a SPA fallback, we need to avoid making `.data` requests if the path wasn't pre-rendered because the request will 404\n  - We don't know all the pre-rendered paths client-side, however:\n    - All `loader` data in `ssr:false` mode is static because it's generated at build time\n    - A route must use a `clientLoader` to do anything dynamic\n    - Therefore, if a route only has a `loader` and not a `clientLoader`, we disable revalidation by default because there is no new data to retrieve\n    - We short circuit and skip single fetch `.data` request logic if there are no server loaders with `shouldLoad=true` in our single fetch `dataStrategy`\n    - This ensures that the route doesn't cause a `.data` request that would 404 after a submission\n- `react-router` - Align dev server behavior with static file server behavior when `ssr:false` is set ([#12948](https://github.com/remix-run/react-router/pull/12948))\n  - When no `prerender` config exists, only SSR down to the root `HydrateFallback` (SPA Mode)\n  - When a `prerender` config exists but the current path is not pre-rendered, only SSR down to the root `HydrateFallback` (SPA Fallback)\n  - Return a 404 on `.data` requests to non-pre-rendered paths\n- `react-router` - Improve prefetch performance of CSS side effects in framework mode ([#12889](https://github.com/remix-run/react-router/pull/12889))\n- `react-router` - Properly handle interrupted manifest requests in lazy route discovery ([#12915](https://github.com/remix-run/react-router/pull/12915))\n- `@react-router/dev` - Handle custom `envDir` in Vite config ([#12969](https://github.com/remix-run/react-router/pull/12969))\n- `@react-router/dev` - Fix CLI parsing to allow argument-less `npx react-router` usage ([#12925](https://github.com/remix-run/react-router/pull/12925))\n- `@react-router/dev` - Skip action-only resource routes when using `prerender:true` ([#13004](https://github.com/remix-run/react-router/pull/13004))\n- `@react-router/dev` - Enhance invalid export detection when using `ssr:false` ([#12948](https://github.com/remix-run/react-router/pull/12948))\n  - `headers`/`action` functions are prohibited in all routes with `ssr:false` because there will be no runtime server on which to run them\n  - `loader` functions are more nuanced and depend on whether a given route is prerendered\n    - When using `ssr:false` without a `prerender` config, only the `root` route can have a `loader`\n    - When using `ssr:false` with a `prerender` config, only routes matched by a `prerender` path can have a `loader`\n- `@react-router/dev` - Error at build time in `ssr:false` + `prerender` apps for the edge case scenario of: ([#13021](https://github.com/remix-run/react-router/pull/13021))\n  - A parent route has only a `loader` (does not have a `clientLoader`)\n  - The parent route is pre-rendered\n  - The parent route has children routes which are not prerendered\n  - This means that when the child paths are loaded via the SPA fallback, the parent won't have any `loaderData` because there is no server on which to run the `loader`\n  - This can be resolved by either adding a parent `clientLoader` or pre-rendering the child paths\n  - If you add a `clientLoader`, calling the `serverLoader()` on non-prerendered paths will throw a 404\n- `@react-router/dev` - Limit prerendered resource route `.data` files to only the target route ([#13004](https://github.com/remix-run/react-router/pull/13004))\n- `@react-router/dev` - Fix pre-rendering of binary files ([#13039](https://github.com/remix-run/react-router/pull/13039))\n- `@react-router/dev` - Fix typegen for repeated params ([#13012](https://github.com/remix-run/react-router/pull/13012))\n  - In React Router, path parameters are keyed by their name, so for a path pattern like `/a/:id/b/:id?/c/:id`, the last `:id` will set the value for `id` in `useParams` and the `params` prop\n    - For example, `/a/1/b/2/c/3` will result in the value `{ id: 3 }` at runtime\n  - Previously, generated types for params incorrectly modeled repeated params with an array\n    - For example, `/a/1/b/2/c/3` generated a type like `{ id: [1,2,3] }`.\n  - To be consistent with runtime behavior, the generated types now correctly model the \"last one wins\" semantics of path parameters.\n    - For example, `/a/1/b/2/c/3` now generates a type like `{ id: 3 }`.\n- `@react-router/dev` - Fix path to load `package.json` for `react-router --version` ([#13012](https://github.com/remix-run/react-router/pull/13012))\n\n### Unstable Changes\n\n⚠️ _[Unstable features](https://reactrouter.com/community/api-development-strategy#unstable-flags) are not recommended for production use_\n\n- `react-router` - Add `unstable_SerializesTo` brand type for library authors to register types serializable by React Router's streaming format (`turbo-stream`) ([#12264](https://github.com/remix-run/react-router/pull/12264))\n- `@react-router/dev` - Add unstable support for splitting route modules in framework mode via `future.unstable_splitRouteModules` ([#11871](https://github.com/remix-run/react-router/pull/11871))\n- `@react-router/dev` - Add `future.unstable_viteEnvironmentApi` flag to enable experimental Vite Environment API support ([#12936](https://github.com/remix-run/react-router/pull/12936))\n\n#### Split Route Modules (unstable)\n\n> ⚠️ This feature is currently [unstable](https://reactrouter.com/community/api-development-strategy#unstable-flags), enabled by the `future.unstable_splitRouteModules` flag. We’d love any interested users to play with it locally and provide feedback, but we do not recommend using it in production yet.\n>\n> If you do choose to adopt this flag in production, please ensure you do sufficient testing against your production build to ensure that the optimization is working as expected.\n\nOne of the conveniences of the [Route Module API](https://reactrouter.com/start/framework/route-module) is that everything a route needs is in a single file. Unfortunately this comes with a performance cost in some cases when using the `clientLoader`, `clientAction`, and `HydrateFallback` APIs.\n\nAs a basic example, consider this route module:\n\n```tsx filename=routes/example.tsx\nimport { MassiveComponent } from \"~/components\";\n\nexport async function clientLoader() {\n  return await fetch(\"https://example.com/api\").then((response) =>\n    response.json(),\n  );\n}\n\nexport default function Component({ loaderData }) {\n  return <MassiveComponent data={loaderData} />;\n}\n```\n\nIn this example we have a minimal `clientLoader` export that makes a basic fetch call, whereas the default component export is much larger. This is a problem for performance because it means that if we want to navigate to this route client-side, the entire route module must be downloaded before the client loader can start running.\n\nTo visualize this as a timeline:\n\n<docs-info>In the following timeline diagrams, different characters are used within the Route Module bars to denote the different Route Module APIs being exported.</docs-info>\n\n```\nGet Route Module:  |--=======|\nRun clientLoader:            |-----|\nRender:                            |-|\n```\n\nInstead, we want to optimize this to the following:\n\n```\nGet clientLoader:  |--|\nGet Component:     |=======|\nRun clientLoader:     |-----|\nRender:                     |-|\n```\n\nTo achieve this optimization, React Router will split the route module into multiple smaller modules during the production build process. In this case, we'll end up with two separate [virtual modules](https://vite.dev/guide/api-plugin#virtual-modules-convention) — one for the client loader and one for the component and its dependencies.\n\n```tsx filename=routes/example.tsx?route-chunk=clientLoader\nexport async function clientLoader() {\n  return await fetch(\"https://example.com/api\").then((response) =>\n    response.json(),\n  );\n}\n```\n\n```tsx filename=routes/example.tsx?route-chunk=main\nimport { MassiveComponent } from \"~/components\";\n\nexport default function Component({ loaderData }) {\n  return <MassiveComponent data={loaderData} />;\n}\n```\n\n> 💡 This optimization is automatically applied in framework mode, but you can also implement it in library mode via `route.lazy` and authoring your route in multiple files as covered in our blog post on [lazy loading route modules.](https://remix.run/blog/lazy-loading-routes#advanced-usage-and-optimizations)\n\nNow that these are available as separate modules, the client loader and the component can be downloaded in parallel. This means that the client loader can be executed as soon as it's ready without having to wait for the component.\n\nThis optimization is even more pronounced when more Route Module APIs are used. For example, when using `clientLoader`, `clientAction` and `HydrateFallback`, the timeline for a single route module during a client-side navigation might look like this:\n\n```\nGet Route Module:     |--~~++++=======|\nRun clientLoader:                     |-----|\nRender:                                     |-|\n```\n\nThis would instead be optimized to the following:\n\n```\nGet clientLoader:     |--|\nGet clientAction:     |~~|\nGet HydrateFallback:  SKIPPED\nGet Component:        |=======|\nRun clientLoader:        |-----|\nRender:                        |-|\n```\n\nNote that this optimization only works when the Route Module APIs being split don't share code within the same file. For example, the following route module can't be split:\n\n```tsx filename=routes/example.tsx\nimport { MassiveComponent } from \"~/components\";\n\nconst shared = () => console.log(\"hello\");\n\nexport async function clientLoader() {\n  shared();\n  return await fetch(\"https://example.com/api\").then((response) =>\n    response.json(),\n  );\n}\n\nexport default function Component({ loaderData }) {\n  shared();\n  return <MassiveComponent data={loaderData} />;\n}\n```\n\nThis route will still work, but since both the client loader and the component depend on the `shared` function defined within the same file, it will be de-optimized into a single route module.\n\nTo avoid this, you can extract any code shared between exports into a separate file. For example:\n\n```tsx filename=routes/example/shared.tsx\nexport const shared = () => console.log(\"hello\");\n```\n\nYou can then import this shared code in your route module without triggering the de-optimization:\n\n```tsx filename=routes/example/route.tsx\nimport { MassiveComponent } from \"~/components\";\nimport { shared } from \"./shared\";\n\nexport async function clientLoader() {\n  shared();\n  return await fetch(\"https://example.com/api\").then((response) =>\n    response.json(),\n  );\n}\n\nexport default function Component({ loaderData }) {\n  shared();\n  return <MassiveComponent data={loaderData} />;\n}\n```\n\nSince the shared code is in its own module, React Router is now able to split this route module into two separate virtual modules:\n\n```tsx filename=routes/example/route.tsx?route-chunk=clientLoader\nimport { shared } from \"./shared\";\n\nexport async function clientLoader() {\n  shared();\n  return await fetch(\"https://example.com/api\").then((response) =>\n    response.json(),\n  );\n}\n```\n\n```tsx filename=routes/example/route.tsx?route-chunk=main\nimport { MassiveComponent } from \"~/components\";\nimport { shared } from \"./shared\";\n\nexport default function Component({ loaderData }) {\n  shared();\n  return <MassiveComponent data={loaderData} />;\n}\n```\n\nIf your project is particularly performance sensitive, you can set the `unstable_splitRouteModules` future flag to `\"enforce\"`:\n\n```tsx filename=react-router-config.ts\nexport default {\n  future: {\n    unstable_splitRouteModules: \"enforce\",\n  },\n};\n```\n\nThis setting will raise an error if any route modules can't be split:\n\n```\nError splitting route module: routes/example/route.tsx\n\n- clientLoader\n\nThis export could not be split into its own chunk because it shares code with other exports. You should extract any shared code into its own module and then import it within the route module.\n```\n\n### Changes by Package\n\n- [`create-react-router`](https://github.com/remix-run/react-router/blob/react-router%407.2.0/packages/create-react-router/CHANGELOG.md#720)\n- [`react-router`](https://github.com/remix-run/react-router/blob/react-router%407.2.0/packages/react-router/CHANGELOG.md#720)\n- [`@react-router/architect`](https://github.com/remix-run/react-router/blob/react-router%407.2.0/packages/react-router-architect/CHANGELOG.md#720)\n- [`@react-router/cloudflare`](https://github.com/remix-run/react-router/blob/react-router%407.2.0/packages/react-router-cloudflare/CHANGELOG.md#720)\n- [`@react-router/dev`](https://github.com/remix-run/react-router/blob/react-router%407.2.0/packages/react-router-dev/CHANGELOG.md#720)\n- [`@react-router/express`](https://github.com/remix-run/react-router/blob/react-router%407.2.0/packages/react-router-express/CHANGELOG.md#720)\n- [`@react-router/fs-routes`](https://github.com/remix-run/react-router/blob/react-router%407.2.0/packages/react-router-fs-routes/CHANGELOG.md#720)\n- [`@react-router/node`](https://github.com/remix-run/react-router/blob/react-router%407.2.0/packages/react-router-node/CHANGELOG.md#720)\n- [`@react-router/remix-config-routes-adapter`](https://github.com/remix-run/react-router/blob/react-router%407.2.0/packages/react-router-remix-config-routes-adapter/CHANGELOG.md#720)\n- [`@react-router/serve`](https://github.com/remix-run/react-router/blob/react-router%407.2.0/packages/react-router-serve/CHANGELOG.md#720)\n\n**Full Changelog**: [`v7.1.5...v7.2.0`](https://github.com/remix-run/react-router/compare/react-router@7.1.5...react-router@7.2.0)\n\n## v7.1.5\n\nDate: 2025-01-31\n\n### Patch Changes\n\n- `react-router` - Fix regression introduced in `7.1.4` via [#12800](https://github.com/remix-run/react-router/pull/12800) that caused issues navigating to hash routes inside splat routes for applications using Lazy Route Discovery (`patchRoutesOnNavigation`) ([#12927](https://github.com/remix-run/react-router/pull/12927))\n\n**Full Changelog**: [`v7.1.4...v7.1.5`](https://github.com/remix-run/react-router/compare/react-router@7.1.4...react-router@7.1.5)\n\n## v7.1.4\n\nDate: 2025-01-30\n\n### Patch Changes\n\n- `@react-router/dev` - Properly resolve Windows file paths to scan for Vite's dependency optimization when using the `unstable_optimizeDeps` future flag ([#12637](https://github.com/remix-run/react-router/pull/12637))\n- `@react-router/dev` - Fix prerendering when using a custom server - previously we ended up trying to import the users custom server when we actually want to import the virtual server build module ([#12759](https://github.com/remix-run/react-router/pull/12759))\n- `react-router` - Properly handle status codes that cannot have a body in single fetch responses (204, etc.) ([#12760](https://github.com/remix-run/react-router/pull/12760))\n- `react-router` - Properly bubble headers as `errorHeaders` when throwing a `data()` result ([#12846](https://github.com/remix-run/react-router/pull/12846))\n  - Avoid duplication of `Set-Cookie` headers if also returned from `headers`\n- `react-router` - Stop erroring on resource routes that return raw strings/objects and instead serialize them as `text/plain` or `application/json` responses ([#12848](https://github.com/remix-run/react-router/pull/12848))\n  - This only applies when accessed as a resource route without the `.data` extension\n  - When accessed from a Single Fetch `.data` request, they will still be encoded via `turbo-stream`\n- `react-router` - Optimize Lazy Route Discovery path discovery to favor a single `querySelectorAll` call at the `body` level instead of many calls at the sub-tree level ([#12731](https://github.com/remix-run/react-router/pull/12731))\n- `react-router` - Optimize route matching by skipping redundant `matchRoutes` calls when possible ([#12800](https://github.com/remix-run/react-router/pull/12800), [#12882](https://github.com/remix-run/react-router/pull/12882))\n- `react-router` - Internal reorg to clean up some duplicated route module types ([#12799](https://github.com/remix-run/react-router/pull/12799))\n\n**Full Changelog**: [`v7.1.3...v7.1.4`](https://github.com/remix-run/react-router/compare/react-router@7.1.3...react-router@7.1.4)\n\n## v7.1.3\n\nDate: 2025-01-17\n\n### Patch Changes\n\n- `@react-router/dev` - Fix `reveal` and `routes` CLI commands ([#12745](https://github.com/remix-run/react-router/pull/12745))\n\n**Full Changelog**: [`v7.1.2...v7.1.3`](https://github.com/remix-run/react-router/compare/react-router@7.1.2...react-router@7.1.3)\n\n## v7.1.2\n\nDate: 2025-01-16\n\n### Patch Changes\n\n- `react-router` - Fix issue with fetcher data cleanup in the data layer on fetcher unmount ([#12681](https://github.com/remix-run/react-router/pull/12681))\n- `react-router` - Do not rely on `symbol` for filtering out `redirect` responses from loader data ([#12694](https://github.com/remix-run/react-router/pull/12694))\n  - Previously, some projects were getting type checking errors like:\n    ```ts\n    error TS4058: Return type of exported function has or is using name 'redirectSymbol' from external module \"node_modules/...\" but cannot be named.\n    ```\n  - Now that `symbol`s are not used for the `redirect` response type, these errors should no longer be present\n- `@react-router/dev` - Fix default external conditions in Vite v6 ([#12644](https://github.com/remix-run/react-router/pull/12644))\n  - This fixes resolution issues with certain npm packages\n- `@react-router/dev` - Fix mismatch in prerendering html/data files when path is missing a leading slash ([#12684](https://github.com/remix-run/react-router/pull/12684))\n- `@react-router/dev` - Use `module-sync` server condition when enabled in the runtime. This fixes React context mismatches (e.g. `useHref() may be used only in the context of a <Router> component.`) during development on Node 22.10.0+ when using libraries that have a peer dependency on React Router ([#12729](https://github.com/remix-run/react-router/pull/12729))\n- `@react-router/dev` - Fix `react-refresh` source maps ([#12686](https://github.com/remix-run/react-router/pull/12686))\n\n**Full Changelog**: [`v7.1.1...v7.1.2`](https://github.com/remix-run/react-router/compare/react-router@7.1.1...react-router@7.1.2)\n\n## v7.1.1\n\nDate: 2024-12-23\n\n### Patch Changes\n\n- `@react-router/dev` - Fix for a crash when optional args are passed to the CLI ([#12609](https://github.com/remix-run/react-router/pull/12609))\n\n**Full Changelog**: [`v7.1.0...v7.1.1`](https://github.com/remix-run/react-router/compare/react-router@7.1.0...react-router@7.1.1)\n\n## v7.1.0\n\nDate: 2024-12-20\n\n### Minor Changes\n\n- Add support for Vite v6 ([#12469](https://github.com/remix-run/react-router/pull/12469))\n\n### Patch Changes\n\n- `react-router` - Throw unwrapped Single Fetch `redirect` to align with pre-Single Fetch behavior ([#12506](https://github.com/remix-run/react-router/pull/12506))\n- `react-router` - Ignore redirects when inferring loader data types ([#12527](https://github.com/remix-run/react-router/pull/12527))\n- `react-router` - Remove `<Link prefetch>` warning which suffers from false positives in a lazy route discovery world ([#12485](https://github.com/remix-run/react-router/pull/12485))\n- `create-react-router` - Fix missing `fs-extra` dependency ([#12556](https://github.com/remix-run/react-router/pull/12556))\n- `@react-router/dev`/`@react-router/serve` - Properly initialize `NODE_ENV` if not already set for compatibility with React 19 ([#12578](https://github.com/remix-run/react-router/pull/12578))\n- `@react-router/dev` - Remove the leftover/unused `abortDelay` prop from `ServerRouter` and update the default `entry.server.tsx` to use the new `streamTimeout` value for Single Fetch ([#12478](https://github.com/remix-run/react-router/pull/12478))\n  - The `abortDelay` functionality was removed in v7 as it was coupled to the `defer` implementation from Remix v2, but this removal of this prop was missed\n  - If you were still using this prop in your `entry.server` file, it's likely your app is not aborting streams as you would expect and you will need to adopt the new [`streamTimeout`](https://reactrouter.com/explanation/special-files#streamtimeout) value introduced with Single Fetch\n- `@react-router/fs-routes` - Throw error in `flatRoutes` if routes directory is missing ([#12407](https://github.com/remix-run/react-router/pull/12407))\n\n### Changes by Package\n\n- [`create-react-router`](https://github.com/remix-run/react-router/blob/react-router%407.1.0/packages/create-react-router/CHANGELOG.md#710)\n- [`react-router`](https://github.com/remix-run/react-router/blob/react-router%407.1.0/packages/react-router/CHANGELOG.md#710)\n- [`@react-router/architect`](https://github.com/remix-run/react-router/blob/react-router%407.1.0/packages/react-router-architect/CHANGELOG.md#710)\n- [`@react-router/cloudflare`](https://github.com/remix-run/react-router/blob/react-router%407.1.0/packages/react-router-cloudflare/CHANGELOG.md#710)\n- [`@react-router/dev`](https://github.com/remix-run/react-router/blob/react-router%407.1.0/packages/react-router-dev/CHANGELOG.md#710)\n- [`@react-router/express`](https://github.com/remix-run/react-router/blob/react-router%407.1.0/packages/react-router-express/CHANGELOG.md#710)\n- [`@react-router/fs-routes`](https://github.com/remix-run/react-router/blob/react-router%407.1.0/packages/react-router-fs-routes/CHANGELOG.md#710)\n- [`@react-router/node`](https://github.com/remix-run/react-router/blob/react-router%407.1.0/packages/react-router-node/CHANGELOG.md#710)\n- [`@react-router/remix-config-routes-adapter`](https://github.com/remix-run/react-router/blob/react-router%407.1.0/packages/react-router-remix-config-routes-adapter/CHANGELOG.md#710)\n- [`@react-router/serve`](https://github.com/remix-run/react-router/blob/react-router%407.1.0/packages/react-router-serve/CHANGELOG.md#710)\n\n**Full Changelog**: [`v7.0.2...v7.1.0`](https://github.com/remix-run/react-router/compare/react-router@7.0.2...react-router@7.1.0)\n\n## v7.0.2\n\nDate: 2024-12-02\n\n### Patch Changes\n\n- `react-router` - Temporarily only use one build in export map so packages can have a peer dependency on react router ([#12437](https://github.com/remix-run/react-router/pull/12437))\n- `@react-router/dev` - Support `moduleResolution` `Node16` and `NodeNext` ([#12440](https://github.com/remix-run/react-router/pull/12440))\n- `@react-router/dev` - Generate wide `matches` and `params` types for child routes ([#12397](https://github.com/remix-run/react-router/pull/12397))\n  - At runtime, `matches` includes child route matches and `params` include child route path parameters\n  - But previously, we only generated types for parent routes and the current route in `matches` and `params`\n  - To align our generated types more closely to the runtime behavior, we now generate more permissive, wider types when accessing child route information\n\n**Full Changelog**: [`v7.0.1...v7.0.2`](https://github.com/remix-run/react-router/compare/react-router@7.0.1...react-router@7.0.2)\n\n## v7.0.1\n\nDate: 2024-11-22\n\n### Patch Changes\n\n- `@react-router/dev` - Ensure typegen file watcher is cleaned up when Vite dev server restarts ([#12331](https://github.com/remix-run/react-router/pull/12331))\n- `@react-router/dev` - Pass route `error` to `ErrorBoundary` as a prop ([#12338](https://github.com/remix-run/react-router/pull/12338))\n\n**Full Changelog**: [`v7.0.0...v7.0.1`](https://github.com/remix-run/react-router/compare/react-router@7.0.0...react-router@7.0.1)\n\n## v7.0.0\n\nDate: 2024-11-21\n\n### Breaking Changes\n\n#### Package Restructuring\n\n- The `react-router-dom`, `@remix-run/react`, `@remix-run/server-runtime`, and `@remix-run/router` have been collapsed into the `react-router` package\n  - To ease migration, `react-router-dom` is still published in v7 as a re-export of everything from `react-router`\n- The `@remix-run/cloudflare-pages` and `@remix-run/cloudflare-workers` have been collapsed into `@react-router/cloudflare` package`\n- The `react-router-dom-v5-compat` and `react-router-native` packages are removed starting with v7\n\n#### Removed Adapter Re-exports\n\nRemix v2 used to re-export all common `@remix-run/server-runtime` APIs through the various runtime packages (`node`, `cloudflare`, `deno`) so that you wouldn't need an additional `@remix-run/server-runtime` dependency in your `package.json`. With the collapsing of packages into `react-router`, these common APIs are now no longer re-exported through the runtime adapters. You should import all common APIs from `react-router`, and only import runtime-specific APIs from the runtime packages:\n\n```jsx\n// Runtime-specific APIs\nimport { createFileSessionStorage } from \"@react-router/node\";\n// Runtime-agnostic APIs\nimport { redirect, useLoaderData } from \"react-router\";\n```\n\n#### Removed APIs\n\nThe following APIs have been removed in React Router v7:\n\n- `json`\n- `defer`\n- `unstable_composeUploadHandlers`\n- `unstable_createMemoryUploadHandler`\n- `unstable_parseMultipartFormData`\n\n#### Minimum Versions\n\nReact Router v7 requires the following minimum versions:\n\n- `node@20`\n  - React Router no longer provides an `installGlobals` method to [polyfill](https://reactrouter.com/dev/guides/deploying/custom-node#polyfilling-fetch) the `fetch` API\n- `react@18`, `react-dom@18`\n\n#### Adopted Future Flag Behaviors\n\nRemix and React Router follow an [API Development Strategy](https://reactrouter.com/en/main/guides/api-development-strategy) leveraging \"Future Flags\" to avoid introducing a slew of breaking changes in a major release. Instead, breaking changes are introduced in minor releases behind a flag, allowing users to opt-in at their convenience. In the next major release, all future flag behaviors become the default behavior.\n\nThe following previously flagged behaviors are now the default in React Router v7:\n\n- [React Router v6 flags](https://reactrouter.com/en/v6/upgrading/future)\n  - `future.v7_relativeSplatPath`\n  - `future.v7_startTransition`\n  - `future.v7_fetcherPersist`\n  - `future.v7_normalizeFormMethod`\n  - `future.v7_partialHydration`\n  - `future.v7_skipActionStatusRevalidation`\n- [Remix v2 flags](https://remix.run/docs/en/v2/start/future-flags)\n  - `future.v3_fetcherPersist`\n  - `future.v3_relativeSplatPath`\n  - `future.v3_throwAbortReason`\n  - `future.v3_singleFetch`\n  - `future.v3_lazyRouteDiscovery`\n  - `future.v3_optimizeDeps`\n\n#### Vite Compiler\n\nThe [Remix Vite plugin](https://remix.run/docs/en/2.12.1/start/future-flags#vite-plugin) is the proper way to build full-stack SSR apps using React Router v7. The former `esbuild`-based compiler is no longer available.\n\n**Renamed `vitePlugin` and `cloudflareDevProxyVitePlugin`**\n\nFor Remix consumers migrating to React Router, the `vitePlugin` and `cloudflareDevProxyVitePlugin` exports have been renamed and moved ([#11904](https://github.com/remix-run/react-router/pull/11904))\n\n```diff\n-import {\n-  vitePlugin as remix,\n-  cloudflareDevProxyVitePlugin,\n-} from \"@remix/dev\";\n\n+import { reactRouter } from \"@react-router/dev/vite\";\n+import { cloudflareDevProxy } from \"@react-router/dev/vite/cloudflare\";\n```\n\n**Removed `manifest` option**\n\nFor Remix consumers migrating to React Router, the Vite plugin's `manifest` option has been removed. The `manifest` option been superseded by the more powerful `buildEnd` hook since it's passed the `buildManifest` argument. You can still write the build manifest to disk if needed, but you'll most likely find it more convenient to write any logic depending on the build manifest within the `buildEnd` hook itself. ([#11573](https://github.com/remix-run/react-router/pull/11573))\n\nIf you were using the `manifest` option, you can replace it with a `buildEnd` hook that writes the manifest to disk like this:\n\n```js\n// react-router.config.ts\nimport { type Config } from \"@react-router/dev/config\";\nimport { writeFile } from \"node:fs/promises\";\n\nexport default {\n  async buildEnd({ buildManifest }) {\n    await writeFile(\n      \"build/manifest.json\",\n      JSON.stringify(buildManifest, null, 2),\n      \"utf-8\"\n    );\n  },\n} satisfies Config;\n```\n\n#### Exposed Router Promises\n\nBecause React 19 will have first-class support for handling promises in the render pass (via `React.use` and `useAction`), we are now comfortable exposing the promises for the APIs that previously returned `undefined`:\n\n- `useNavigate()`\n- `useSubmit()`\n- `useFetcher().load`\n- `useFetcher().submit`\n- `useRevalidator().revalidate()`\n\n### Other Notable Changes\n\n#### `routes.ts`\n\nWhen using the React Router Vite plugin, routes are defined in `app/routes.ts`. Route config is exported via the `routes` export, conforming to the `RouteConfig` type. Route helper functions `route`, `index`, and `layout` are provided to make declarative type-safe route definitions easier.\n\n```ts\n// app/routes.ts\nimport {\n  type RouteConfig,\n  route,\n  index,\n  layout,\n} from \"@react-router/dev/routes\";\n\nexport const routes: RouteConfig = [\n  index(\"./home.tsx\"),\n  route(\"about\", \"./about.tsx\"),\n\n  layout(\"./auth/layout.tsx\", [\n    route(\"login\", \"./auth/login.tsx\"),\n    route(\"register\", \"./auth/register.tsx\"),\n  ]),\n\n  route(\"concerts\", [\n    index(\"./concerts/home.tsx\"),\n    route(\":city\", \"./concerts/city.tsx\"),\n    route(\"trending\", \"./concerts/trending.tsx\"),\n  ]),\n];\n```\n\nFor Remix consumers migrating to React Router, you can still configure file system routing within `routes.ts` using the `@react-router/fs-routes` package. A minimal route config that reproduces the default Remix setup looks like this:\n\n```ts\n// app/routes.ts\nimport { type RouteConfig } from \"@react-router/dev/routes\";\nimport { flatRoutes } from \"@react-router/fs-routes\";\n\nexport const routes: RouteConfig = flatRoutes();\n```\n\nIf you want to migrate from file system routing to config-based routes, you can mix and match approaches by spreading the results of the async `flatRoutes` function into the array of config-based routes.\n\n```ts\n// app/routes.ts\nimport { type RouteConfig, route } from \"@react-router/dev/routes\";\nimport { flatRoutes } from \"@react-router/fs-routes\";\n\nexport const routes: RouteConfig = [\n  // Example config-based route:\n  route(\"/hello\", \"./routes/hello.tsx\"),\n\n  // File system routes scoped to a different directory:\n  ...(await flatRoutes({\n    rootDirectory: \"fs-routes\",\n  })),\n];\n```\n\nIf you were using Remix's `routes` option to use alternative file system routing conventions, you can adapt these to the new `RouteConfig` format using `@react-router/remix-config-routes-adapter`.\n\nFor example, if you were using [Remix v1 route conventions](https://remix.run/docs/en/1.19.3/file-conventions/routes-files) in Remix v2, you can combine `@react-router/remix-config-routes-adapter` with `@remix-run/v1-route-convention` to adapt this to React Router:\n\n```ts\n// app/routes.ts\nimport { type RouteConfig } from \"@react-router/dev/routes\";\nimport { remixConfigRoutes } from \"@react-router/remix-config-routes-adapter\";\nimport { createRoutesFromFolders } from \"@remix-run/v1-route-convention\";\n\nexport const routes: RouteConfig = remixConfigRoutes(async (defineRoutes) => {\n  return createRoutesFromFolders(defineRoutes, {\n    ignoredFilePatterns: [\"**/.*\", \"**/*.css\"],\n  });\n});\n```\n\nAlso note that, if you were using Remix's `routes` option to define config-based routes, you can also adapt these to the new `RouteConfig` format using `@react-router/remix-config-routes-adapter` with minimal code changes. While this makes for a fast migration path, we recommend migrating any config-based routes from Remix to the new `RouteConfig` format since it's a fairly straightforward migration.\n\n```diff\n// app/routes.ts\n-import { type RouteConfig } from \"@react-router/dev/routes\";\n+import { type RouteConfig, route } from \"@react-router/dev/routes\";\n-import { remixConfigRoutes } from \"@react-router/remix-config-routes-adapter\";\n\n-export const routes: RouteConfig = remixConfigRoutes(async (defineRoutes) => {\n-  defineRoutes((route) => {\n-    route(\"/parent\", \"./routes/parent.tsx\", () => [\n-      route(\"/child\", \"./routes/child.tsx\"),\n-    ]);\n-  });\n-});\n+export const routes: RouteConfig = [\n+  route(\"/parent\", \"./routes/parent.tsx\", [\n+    route(\"/child\", \"./routes/child.tsx\"),\n+  ]),\n+];\n```\n\n#### Type-safety improvements\n\nReact Router now generates types for each of your route modules and passes typed props to route module component exports ([#11961](https://github.com/remix-run/react-router/pull/11961), [#12019](https://github.com/remix-run/react-router/pull/12019)). You can access those types by importing them from `./+types/<route filename without extension>`.\n\nSee [_How To > Route Module Type Safety_](https://reactrouter.com/dev/how-to/route-module-type-safety) and [_Explanations > Type Safety_](https://reactrouter.com/dev/explanation/type-safety) for more details.\n\n#### Prerendering\n\nReact Router v7 includes a new `prerender` config in the vite plugin to support SSG use-cases. This will pre-render your `.html` and `.data` files at build time and so you can serve them statically at runtime from a running server or a CDN ([#11539](https://github.com/remix-run/react-router/pull/11539))\n\n```ts\nexport default defineConfig({\n  plugins: [\n    reactRouter({\n      async prerender({ getStaticPaths }) {\n        let slugs = await fakeGetSlugsFromCms();\n        return [\n          ...getStaticPaths(),\n          ...slugs.map((slug) => `/product/${slug}`),\n        ];\n      },\n    }),\n    tsconfigPaths(),\n  ],\n});\n\nasync function fakeGetSlugsFromCms() {\n  await new Promise((r) => setTimeout(r, 1000));\n  return [\"shirt\", \"hat\"];\n}\n```\n\n### Major Changes (`react-router`)\n\n- Remove the original `defer` implementation in favor of using raw promises via single fetch and `turbo-stream` ([#11744](https://github.com/remix-run/react-router/pull/11744))\n  - This removes these exports from React Router:\n    - `defer`\n    - `AbortedDeferredError`\n    - `type TypedDeferredData`\n    - `UNSAFE_DeferredData`\n    - `UNSAFE_DEFERRED_SYMBOL`\n- Collapse packages into `react-router`([#11505](https://github.com/remix-run/react-router/pull/11505))\n  - `@remix-run/router`\n  - `react-router-dom`\n  - `@remix-run/server-runtime`\n  - `@remix-run/testing`\n  - As a note, the `react-router-dom` package is maintained to ease adoption but it simply re-exports all APIs from `react-router`\n- Drop support for Node 16, React Router SSR now requires Node 18 or higher ([#11391](https://github.com/remix-run/react-router/pull/11391), [#11690](https://github.com/remix-run/react-router/pull/11690))\n- Remove `future.v7_startTransition` flag ([#11696](https://github.com/remix-run/react-router/pull/11696))\n- Expose the underlying router promises from the following APIs for composition in React 19 APIs: ([#11521](https://github.com/remix-run/react-router/pull/11521))\n- Remove `future.v7_normalizeFormMethod` future flag ([#11697](https://github.com/remix-run/react-router/pull/11697))\n- Imports/Exports cleanup ([#11840](https://github.com/remix-run/react-router/pull/11840))\n  - Removed the following exports that were previously public API from `@remix-run/router`\n    - types\n      - `AgnosticDataIndexRouteObject`\n      - `AgnosticDataNonIndexRouteObject`\n      - `AgnosticDataRouteMatch`\n      - `AgnosticDataRouteObject`\n      - `AgnosticIndexRouteObject`\n      - `AgnosticNonIndexRouteObject`\n      - `AgnosticRouteMatch`\n      - `AgnosticRouteObject`\n      - `TrackedPromise`\n      - `unstable_AgnosticPatchRoutesOnMissFunction`\n      - `Action` -> exported as `NavigationType` via `react-router`\n      - `Router` exported as `RemixRouter` to differentiate from RR's `<Router>`\n    - API\n      - `getToPathname` (`@private`)\n      - `joinPaths` (`@private`)\n      - `normalizePathname` (`@private`)\n      - `resolveTo` (`@private`)\n      - `stripBasename` (`@private`)\n      - `createBrowserHistory` -> in favor of `createBrowserRouter`\n      - `createHashHistory` -> in favor of `createHashRouter`\n      - `createMemoryHistory` -> in favor of `createMemoryRouter`\n      - `createRouter`\n      - `createStaticHandler` -> in favor of wrapper `createStaticHandler` in RR Dom\n      - `getStaticContextFromError`\n  - Removed the following exports that were previously public API from `react-router`\n    - `Hash`\n    - `Pathname`\n    - `Search`\n- Remove `future.v7_prependBasename` from the internalized `@remix-run/router` package ([#11726](https://github.com/remix-run/react-router/pull/11726))\n- Remove `future.v7_throwAbortReason` from internalized `@remix-run/router` package ([#11728](https://github.com/remix-run/react-router/pull/11728))\n- Add `exports` field to all packages ([#11675](https://github.com/remix-run/react-router/pull/11675))\n- Renamed `RemixContext` to `FrameworkContext` ([#11705](https://github.com/remix-run/react-router/pull/11705))\n- Update the minimum React version to 18 ([#11689](https://github.com/remix-run/react-router/pull/11689))\n- `PrefetchPageDescriptor` replaced by `PageLinkDescriptor` ([#11960](https://github.com/remix-run/react-router/pull/11960))\n- Remove the `future.v7_partialHydration` flag ([#11725](https://github.com/remix-run/react-router/pull/11725))\n  - This also removes the `<RouterProvider fallbackElement>` prop\n    - To migrate, move the `fallbackElement` to a `hydrateFallbackElement`/`HydrateFallback` on your root route\n  - Also worth nothing there is a related breaking changer with this future flag:\n    - Without `future.v7_partialHydration` (when using `fallbackElement`), `state.navigation` was populated during the initial load\n    - With `future.v7_partialHydration`, `state.navigation` remains in an `\"idle\"` state during the initial load\n- Remove `future.v7_relativeSplatPath` future flag ([#11695](https://github.com/remix-run/react-router/pull/11695))\n- Remove remaining future flags ([#11820](https://github.com/remix-run/react-router/pull/11820))\n  - React Router `v7_skipActionErrorRevalidation`\n  - Remix `v3_fetcherPersist`, `v3_relativeSplatPath`, `v3_throwAbortReason`\n- Rename `createRemixStub` to `createRoutesStub` ([#11692](https://github.com/remix-run/react-router/pull/11692))\n- Remove `@remix-run/router` deprecated `detectErrorBoundary` option in favor of `mapRouteProperties` ([#11751](https://github.com/remix-run/react-router/pull/11751))\n- Add `react-router/dom` subpath export to properly enable `react-dom` as an optional `peerDependency` ([#11851](https://github.com/remix-run/react-router/pull/11851))\n  - This ensures that we don't blindly `import ReactDOM from \"react-dom\"` in `<RouterProvider>` in order to access `ReactDOM.flushSync()`, since that would break `createMemoryRouter` use cases in non-DOM environments\n  - DOM environments should import from `react-router/dom` to get the proper component that makes `ReactDOM.flushSync()` available:\n    - If you are using the Vite plugin, use this in your `entry.client.tsx`:\n      - `import { HydratedRouter } from 'react-router/dom'`\n    - If you are not using the Vite plugin and are manually calling `createBrowserRouter`/`createHashRouter`:\n      - `import { RouterProvider } from \"react-router/dom\"`\n- Remove `future.v7_fetcherPersist` flag ([#11731](https://github.com/remix-run/react-router/pull/11731))\n- Allow returning `undefined` from loaders and actions ([#11680](https://github.com/remix-run/react-router/pull/11680), [#12057]([https://github.com/remix-run/react-router/pull/1205))\n- Use `createRemixRouter`/`RouterProvider` in `entry.client` instead of `RemixBrowser` ([#11469](https://github.com/remix-run/react-router/pull/11469))\n- Remove the deprecated `json` utility ([#12146](https://github.com/remix-run/react-router/pull/12146))\n  - You can use [`Response.json`](https://developer.mozilla.org/en-US/docs/Web/API/Response/json_static) if you still need to construct JSON responses in your app\n\n### Major Changes (`@react-router/*`)\n\n- Remove `future.v3_singleFetch` flag ([#11522](https://github.com/remix-run/react-router/pull/11522))\n- Drop support for Node 16 and 18, update minimum Node version to 20 ([#11690](https://github.com/remix-run/react-router/pull/11690), [#12171](https://github.com/remix-run/react-router/pull/12171))\n  - Remove `installGlobals()` as this should no longer be necessary\n- Add `exports` field to all packages ([#11675](https://github.com/remix-run/react-router/pull/11675))\n- No longer re-export APIs from `react-router` through different runtime/adapter packages ([#11702](https://github.com/remix-run/react-router/pull/11702))\n- For Remix consumers migrating to React Router, the `crypto` global from the [Web Crypto API](https://developer.mozilla.org/en-US/docs/Web/API/Web_Crypto_API) is now required when using cookie and session APIs\n  - This means that the following APIs are provided from `react-router` rather than platform-specific packages: ([#11837](https://github.com/remix-run/react-router/pull/11837))\n    - `createCookie`\n    - `createCookieSessionStorage`\n    - `createMemorySessionStorage`\n    - `createSessionStorage`\n  - For consumers running older versions of Node, the `installGlobals` function from `@remix-run/node` has been updated to define `globalThis.crypto`, using [Node's `require('node:crypto').webcrypto` implementation](https://nodejs.org/api/webcrypto.html)\n  - Since platform-specific packages no longer need to implement this API, the following low-level APIs have been removed:\n    - `createCookieFactory`\n    - `createSessionStorageFactory`\n    - `createCookieSessionStorageFactory`\n    - `createMemorySessionStorageFactory`\n- Consolidate types previously duplicated across `@remix-run/router`, `@remix-run/server-runtime`, and `@remix-run/react` now that they all live in `react-router` ([#12177](https://github.com/remix-run/react-router/pull/12177))\n  - Examples: `LoaderFunction`, `LoaderFunctionArgs`, `ActionFunction`, `ActionFunctionArgs`, `DataFunctionArgs`, `RouteManifest`, `LinksFunction`, `Route`, `EntryRoute`\n  - The `RouteManifest` type used by the \"remix\" code is now slightly stricter because it is using the former `@remix-run/router` `RouteManifest`\n    - `Record<string, Route> -> Record<string, Route | undefined>`\n  - Removed `AppData` type in favor of inlining `unknown` in the few locations it was used\n  - Removed `ServerRuntimeMeta*` types in favor of the `Meta*` types they were duplicated from\n- Migrate Remix v2 type generics to React Router ([#12180](https://github.com/remix-run/react-router/pull/12180))\n  - These generics are provided for Remix v2 migration purposes\n  - These generics and the APIs they exist on should be considered informally deprecated in favor of the new `Route.*` types\n  - Anyone migrating from React Router v6 should probably not leverage these new generics and should migrate straight to the `Route.*` types\n  - For React Router v6 users, these generics are new and should not impact your app, with one exception\n    - `useFetcher` previously had an optional generic (used primarily by Remix v2) that expected the data type\n    - This has been updated in v7 to expect the type of the function that generates the data (i.e., `typeof loader`/`typeof action`)\n    - Therefore, you should update your usages:\n      - ❌ `useFetcher<LoaderData>()`\n      - ✅ `useFetcher<typeof loader>()`\n- Update `cookie` dependency to `^1.0.1` - please see the [release notes](https://github.com/jshttp/cookie/releases) for any breaking changes ([#12172](https://github.com/remix-run/react-router/pull/12172))\n- `@react-router/cloudflare` - For Remix consumers migrating to React Router, all exports from `@remix-run/cloudflare-pages` are now provided for React Router consumers in the `@react-router/cloudflare` package. There is no longer a separate package for Cloudflare Pages. ([#11801](https://github.com/remix-run/react-router/pull/11801))\n- `@react-router/cloudflare` - The `@remix-run/cloudflare-workers` package has been deprecated. Remix consumers migrating to React Router should use the `@react-router/cloudflare` package directly. For guidance on how to use `@react-router/cloudflare` within a Cloudflare Workers context, refer to the Cloudflare Workers template. ([#11801](https://github.com/remix-run/react-router/pull/11801))\n- `@react-router/dev` - For Remix consumers migrating to React Router, the `vitePlugin` and `cloudflareDevProxyVitePlugin` exports have been renamed and moved. ([#11904](https://github.com/remix-run/react-router/pull/11904))\n- `@react-router/dev` - For Remix consumers migrating to React Router who used the Vite plugin's `buildEnd` hook, the resolved `reactRouterConfig` object no longer contains a `publicPath` property since this belongs to Vite, not React Router ([#11575](https://github.com/remix-run/react-router/pull/11575))\n- `@react-router/dev` - For Remix consumers migrating to React Router, the Vite plugin's `manifest` option has been removed ([#11573](https://github.com/remix-run/react-router/pull/11573))\n- `@react-router/dev` - Update default `isbot` version to v5 and drop support for `isbot@3` ([#11770](https://github.com/remix-run/react-router/pull/11770))\n  - If you have `isbot@4` or `isbot@5` in your `package.json`:\n    - You do not need to make any changes\n  - If you have `isbot@3` in your `package.json` and you have your own `entry.server.tsx` file in your repo\n    - You do not need to make any changes\n    - You can upgrade to `isbot@5` independent of the React Router v7 upgrade\n  - If you have `isbot@3` in your `package.json` and you do not have your own `entry.server.tsx` file in your repo\n    - You are using the internal default entry provided by React Router v7 and you will need to upgrade to `isbot@5` in your `package.json`\n- `@react-router/dev` - For Remix consumers migrating to React Router, Vite manifests (i.e. `.vite/manifest.json`) are now written within each build subdirectory, e.g. `build/client/.vite/manifest.json` and `build/server/.vite/manifest.json` instead of `build/.vite/client-manifest.json` and `build/.vite/server-manifest.json`. This means that the build output is now much closer to what you'd expect from a typical Vite project. ([#11573](https://github.com/remix-run/react-router/pull/11573))\n  - Originally the Remix Vite plugin moved all Vite manifests to a root-level `build/.vite` directory to avoid accidentally serving them in production, particularly from the client build. This was later improved with additional logic that deleted these Vite manifest files at the end of the build process unless Vite's `build.manifest` had been enabled within the app's Vite config. This greatly reduced the risk of accidentally serving the Vite manifests in production since they're only present when explicitly asked for. As a result, we can now assume that consumers will know that they need to manage these additional files themselves, and React Router can safely generate a more standard Vite build output.\n\n### Minor Changes\n\n- `react-router` - Params, loader data, and action data as props for route component exports ([#11961](https://github.com/remix-run/react-router/pull/11961))\n- `react-router` - Add route module type generation ([#12019](https://github.com/remix-run/react-router/pull/12019))\n- `react-router` - Remove duplicate `RouterProvider` implementations ([#11679](https://github.com/remix-run/react-router/pull/11679))\n- `react-router` - Stabilize `unstable_dataStrategy` ([#11969](https://github.com/remix-run/react-router/pull/11969))\n- `react-router` - Stabilize `unstable_patchRoutesOnNavigation` ([#11970](https://github.com/remix-run/react-router/pull/11970))\n- `react-router` - Add prefetching support to `Link`/`NavLink` when using Remix SSR ([#11402](https://github.com/remix-run/react-router/pull/11402))\n- `react-router` - Enhance `ScrollRestoration` so it can restore properly on an SSR'd document load ([#11401](https://github.com/remix-run/react-router/pull/11401))\n- `@react-router/dev` - Add support for the `prerender` config in the React Router vite plugin, to support existing SSG use-cases ([#11539](https://github.com/remix-run/react-router/pull/11539))\n- `@react-router/dev` - Remove internal `entry.server.spa.tsx` implementation which was not compatible with the Single Fetch async hydration approach ([#11681](https://github.com/remix-run/react-router/pull/11681))\n- `@react-router/serve`: Update `express.static` configurations to support new `prerender` API ([#11547](https://github.com/remix-run/react-router/pull/11547))\n  - Assets in the `build/client/assets` folder are served as before, with a 1-year immutable `Cache-Control` header\n  - Static files outside of assets, such as pre-rendered `.html` and `.data` files are not served with a specific `Cache-Control` header\n  - `.data` files are served with `Content-Type: text/x-turbo`\n    - For some reason, when adding this via `express.static`, it seems to also add a `Cache-Control: public, max-age=0` to `.data` files\n\n### Patch Changes\n\n- Replace `substr` with `substring` ([#12080](https://github.com/remix-run/react-router/pull/12080))\n- `react-router` - Fix redirects returned from loaders/actions using `data()` ([#12021](https://github.com/remix-run/react-router/pull/12021))\n- `@react-router/dev` - Enable prerendering for resource routes ([#12200](https://github.com/remix-run/react-router/pull/12200))\n- `@react-router/dev` - resolve config directory relative to flat output file structure ([#12187](https://github.com/remix-run/react-router/pull/12187))\n\n### Changes by Package\n\n- [`react-router`](https://github.com/remix-run/react-router/blob/react-router%407.0.0/packages/react-router/CHANGELOG.md#700)\n- [`@react-router/architect`](https://github.com/remix-run/react-router/blob/react-router%407.0.0/packages/react-router-architect/CHANGELOG.md#700)\n- [`@react-router/cloudflare`](https://github.com/remix-run/react-router/blob/react-router%407.0.0/packages/react-router-cloudflare/CHANGELOG.md#700)\n- [`@react-router/dev`](https://github.com/remix-run/react-router/blob/react-router%407.0.0/packages/react-router-dev/CHANGELOG.md#700)\n- [`@react-router/express`](https://github.com/remix-run/react-router/blob/react-router%407.0.0/packages/react-router-express/CHANGELOG.md#700)\n- [`@react-router/fs-routes`](https://github.com/remix-run/react-router/blob/react-router%407.0.0/packages/react-router-fs-routes/CHANGELOG.md#700)\n- [`@react-router/node`](https://github.com/remix-run/react-router/blob/react-router%407.0.0/packages/react-router-node/CHANGELOG.md#700)\n- [`@react-router/remix-config-routes-adapter`](https://github.com/remix-run/react-router/blob/react-router%407.0.0/packages/react-router-remix-config-routes-adapter/CHANGELOG.md#700)\n- [`@react-router/serve`](https://github.com/remix-run/react-router/blob/react-router%407.0.0/packages/react-router-serve/CHANGELOG.md#700)\n\n**Full Changelog**: [`v6.28.0...v7.0.0`](https://github.com/remix-run/react-router/compare/react-router@6.28.0...react-router@7.0.0)\n\n# React Router v6 Releases\n\n## v6.30.3\n\nDate: 2026-01-07\n\n### Security Notice\n\nThis release addresses 1 security vulnerability:\n\n- [XSS via Open Redirects](https://github.com/remix-run/react-router/security/advisories/GHSA-2w69-qvjg-hvjx)\n\n### Patch Changes\n\n- Validate redirect locations ([#14707](https://github.com/remix-run/react-router/pull/14707))\n\n**Full Changelog**: [`v6.30.2...v6.30.3`](https://github.com/remix-run/react-router/compare/react-router@6.30.2...react-router@6.30.3)\n\n## v6.30.2\n\nDate: 2025-11-13\n\n### Security Notice\n\nThis release addresses 1 security vulnerability:\n\n- [Unexpected external redirect via untrusted paths](https://github.com/remix-run/react-router/security/advisories/GHSA-9jcx-v3wj-wh4m)\n\n### Patch Changes\n\n- Normalize double-slashes in `resolvePath` ([#14537](https://github.com/remix-run/react-router/pull/14537))\n\n**Full Changelog**: [`v6.30.1...v6.30.2`](https://github.com/remix-run/react-router/compare/react-router@6.30.1...react-router@6.30.2)\n\n## v6.30.1\n\nDate: 2025-05-20\n\n### Patch Changes\n\n- Partially revert optimization added in `6.29.0` to reduce calls to `matchRoutes` because it surfaced other issues ([#13623](https://github.com/remix-run/react-router/pull/13623))\n- Stop logging invalid warning when `v7_relativeSplatPath` is set to `false` ([#13502](https://github.com/remix-run/react-router/pull/13502))\n\n**Full Changelog**: [`v6.30.0...v6.30.1`](https://github.com/remix-run/react-router/compare/react-router@6.30.0...react-router@6.30.1)\n\n## v6.30.0\n\nDate: 2025-02-27\n\n### Minor Changes\n\n- Add `fetcherKey` as a parameter to `patchRoutesOnNavigation` ([#13109](https://github.com/remix-run/react-router/pull/13109))\n\n### Patch Changes\n\n- Fix regression introduced in `6.29.0` via [#12169](https://github.com/remix-run/react-router/pull/12169) that caused issues navigating to hash routes inside splat routes for applications using Lazy Route Discovery (`patchRoutesOnNavigation`) ([#13108](https://github.com/remix-run/react-router/pull/13108))\n\n**Full Changelog**: [`v6.29.0...v6.30.0`](https://github.com/remix-run/react-router/compare/react-router@6.29.0...react-router@6.30.0)\n\n## v6.29.0\n\nDate: 2025-01-30\n\n### Minor Changes\n\n- Provide the request `signal` as a parameter to `patchRoutesOnNavigation` ([#12900](https://github.com/remix-run/react-router/pull/12900))\n  - This can be used to abort any manifest fetches if the in-flight navigation/fetcher is aborted\n\n### Patch Changes\n\n- Do not log v7 deprecation warnings in production builds ([#12794](https://github.com/remix-run/react-router/pull/12794))\n- Properly bubble headers when throwing a `data()` result ([#12845](https://github.com/remix-run/react-router/pull/12845))\n- Optimize route matching by skipping redundant `matchRoutes` calls when possible ([#12169](https://github.com/remix-run/react-router/pull/12169))\n- Strip search parameters from `patchRoutesOnNavigation` `path` param for fetcher calls ([#12899](https://github.com/remix-run/react-router/pull/12899))\n\n**Full Changelog**: [`v6.28.2...v6.29.0`](https://github.com/remix-run/react-router/compare/react-router@6.28.2...react-router@6.29.0)\n\n## v6.28.2\n\nDate: 2025-01-16\n\n### Patch Changes\n\n- Fix manual fetcher `key` usage when not opted into `future.v7_fetcherPersist` ([#12674](https://github.com/remix-run/react-router/pull/12674))\n- Fix issue with fetcher data cleanup in the data layer on fetcher unmount ([#12674](https://github.com/remix-run/react-router/pull/12674))\n\n**Full Changelog**: [`v6.28.1...v6.28.2`](https://github.com/remix-run/react-router/compare/react-router@6.28.1...react-router@6.28.2)\n\n## v6.28.1\n\nDate: 2024-12-20\n\n### Patch Changes\n\n- Allow users to opt out of v7 deprecation warnings by setting flags to `false` ([#12441](https://github.com/remix-run/react-router/pull/12441))\n\n**Full Changelog**: [`v6.28.0...v6.28.1`](https://github.com/remix-run/react-router/compare/react-router@6.28.0...react-router@6.28.1)\n\n## v6.28.0\n\nDate: 2024-11-06\n\n### What's Changed\n\n- In preparation for v7 we've added deprecation warnings for any future flags that you have not yet opted into. Please use the flags to better prepare for eventually upgrading to v7.\n\n### Minor Changes\n\n- Log deprecation warnings for v7 flags ([#11750](https://github.com/remix-run/react-router/pull/11750))\n  - Add deprecation warnings to `json`/`defer` in favor of returning raw objects\n    - These methods will be removed in React Router v7\n\n### Patch Changes\n\n- Update JSDoc URLs for new website structure (add /v6/ segment) ([#12141](https://github.com/remix-run/react-router/pull/12141))\n\n**Full Changelog**: [`v6.27.0...v6.28.0`](https://github.com/remix-run/react-router/compare/react-router@6.27.0...react-router@6.28.0)\n\n## v6.27.0\n\nDate: 2024-10-11\n\n### What's Changed\n\n#### Stabilized APIs\n\nThis release stabilizes a handful of \"unstable\" APIs in preparation for the [pending](https://x.com/remix_run/status/1841926034868077009) React Router v7 release (see [these](https://remix.run/blog/merging-remix-and-react-router) [posts](https://remix.run/blog/incremental-path-to-react-19) for more info):\n\n- `unstable_dataStrategy` → `dataStrategy` (`createBrowserRouter` and friends) ([Docs](https://reactrouter.com/v6/routers/create-browser-router#optsdatastrategy))\n- `unstable_patchRoutesOnNavigation` → `patchRoutesOnNavigation` (`createBrowserRouter` and friends) ([Docs](https://reactrouter.com/v6/routers/create-browser-router#optspatchroutesonnavigation))\n- `unstable_flushSync` → `flushSync` (`useSubmit`, `fetcher.load`, `fetcher.submit`) ([Docs](https://reactrouter.com/v6/hooks/use-submit#optionsflushsync))\n- `unstable_viewTransition` → `viewTransition` (`<Link>`, `<Form>`, `useNavigate`, `useSubmit`) ([Docs](https://reactrouter.com/v6/components/link#viewtransition))\n\n### Minor Changes\n\n- Stabilize the `unstable_flushSync` option for navigations and fetchers ([#11989](https://github.com/remix-run/react-router/pull/11989))\n- Stabilize the `unstable_viewTransition` option for navigations and the corresponding `unstable_useViewTransitionState` hook ([#11989](https://github.com/remix-run/react-router/pull/11989))\n- Stabilize `unstable_dataStrategy` ([#11974](https://github.com/remix-run/react-router/pull/11974))\n- Stabilize `unstable_patchRoutesOnNavigation` ([#11973](https://github.com/remix-run/react-router/pull/11973))\n  - Add new `PatchRoutesOnNavigationFunctionArgs` type for convenience ([#11967](https://github.com/remix-run/react-router/pull/11967))\n\n### Patch Changes\n\n- Fix bug when submitting to the current contextual route (parent route with an index child) when an `?index` param already exists from a prior submission ([#12003](https://github.com/remix-run/react-router/pull/12003))\n- Fix `useFormAction` bug - when removing `?index` param it would not keep other non-Remix `index` params ([#12003](https://github.com/remix-run/react-router/pull/12003))\n- Fix bug with fetchers not persisting `preventScrollReset` through redirects during concurrent fetches ([#11999](https://github.com/remix-run/react-router/pull/11999))\n- Avoid unnecessary `console.error` on fetcher abort due to back-to-back revalidation calls ([#12050](https://github.com/remix-run/react-router/pull/12050))\n- Fix bugs with `partialHydration` when hydrating with errors ([#12070](https://github.com/remix-run/react-router/pull/12070))\n- Remove internal cache to fix issues with interrupted `patchRoutesOnNavigation` calls ([#12055](https://github.com/remix-run/react-router/pull/12055))\n  - ⚠️ This may be a breaking change if you were relying on this behavior in the `unstable_` API\n  - We used to cache in-progress calls to `patchRoutesOnNavigation` internally so that multiple navigations with the same start/end would only execute the function once and use the same promise\n  - However, this approach was at odds with `patch` short circuiting if a navigation was interrupted (and the `request.signal` aborted) since the first invocation's `patch` would no-op\n  - This cache also made some assumptions as to what a valid cache key might be - and is oblivious to any other application-state changes that may have occurred\n  - So, the cache has been removed because in _most_ cases, repeated calls to something like `import()` for async routes will already be cached automatically - and if not it's easy enough for users to implement this cache in userland\n- Remove internal `discoveredRoutes` FIFO queue from `unstable_patchRoutesOnNavigation` ([#11977](https://github.com/remix-run/react-router/pull/11977))\n  - ⚠️ This may be a breaking change if you were relying on this behavior in the `unstable_` API\n  - This was originally implemented as an optimization but it proved to be a bit too limiting\n  - If you need this optimization you can implement your own cache inside `patchRoutesOnNavigation`\n- Fix types for `RouteObject` within `PatchRoutesOnNavigationFunction`'s `patch` method so it doesn't expect agnostic route objects passed to `patch` ([#11967](https://github.com/remix-run/react-router/pull/11967))\n- Expose errors thrown from `patchRoutesOnNavigation` directly to `useRouteError` instead of wrapping them in a 400 `ErrorResponse` instance ([#12111](https://github.com/remix-run/react-router/pull/12111))\n\n**Full Changelog**: [`v6.26.2...v6.27.0`](https://github.com/remix-run/react-router/compare/react-router@6.26.2...react-router@6.27.0)\n\n## v6.26.2\n\nDate: 2024-09-09\n\n### Patch Changes\n\n- Update the `unstable_dataStrategy` API to allow for more advanced implementations ([#11943](https://github.com/remix-run/react-router/pull/11943))\n  - ⚠️ If you have already adopted `unstable_dataStrategy`, please review carefully as this includes breaking changes to this API\n  - Rename `unstable_HandlerResult` to `unstable_DataStrategyResult`\n  - Change the return signature of `unstable_dataStrategy` from a parallel array of `unstable_DataStrategyResult[]` (parallel to `matches`) to a key/value object of `routeId => unstable_DataStrategyResult`\n    - This allows more advanced control over revalidation behavior because you can opt-into or out-of revalidating data that may not have been revalidated by default (via `match.shouldLoad`)\n  - You should now return/throw a result from your `handlerOverride` instead of returning a `DataStrategyResult`\n    - The return value (or thrown error) from your `handlerOverride` will be wrapped up into a `DataStrategyResult` and returned fromm `match.resolve`\n    - Therefore, if you are aggregating the results of `match.resolve()` into a final results object you should not need to think about the `DataStrategyResult` type\n    - If you are manually filling your results object from within your `handlerOverride`, then you will need to assign a `DataStrategyResult` as the value so React Router knows if it's a successful execution or an error (see examples in the documentation for details)\n  - Added a new `fetcherKey` parameter to `unstable_dataStrategy` to allow differentiation from navigational and fetcher calls\n- Preserve opted-in view transitions through redirects ([#11925](https://github.com/remix-run/react-router/pull/11925))\n- Preserve pending view transitions through a router revalidation call ([#11917](https://github.com/remix-run/react-router/pull/11917))\n- Fix blocker usage when `blocker.proceed` is called quickly/synchronously ([#11930](https://github.com/remix-run/react-router/pull/11930))\n\n**Full Changelog**: [`v6.26.1...v6.26.2`](https://github.com/remix-run/react-router/compare/react-router@6.26.1...react-router@6.26.2)\n\n## v6.26.1\n\nDate: 2024-08-15\n\n### Patch Changes\n\n- Rename `unstable_patchRoutesOnMiss` to `unstable_patchRoutesOnNavigation` to match new behavior ([#11888](https://github.com/remix-run/react-router/pull/11888))\n- Update `unstable_patchRoutesOnNavigation` logic so that we call the method when we match routes with dynamic param or splat segments in case there exists a higher-scoring static route that we've not yet discovered ([#11883](https://github.com/remix-run/react-router/pull/11883))\n  - We also now leverage an internal FIFO queue of previous paths we've already called `unstable_patchRoutesOnNavigation` against so that we don't re-call on subsequent navigations to the same path\n\n**Full Changelog**: [`v6.26.0...v6.26.1`](https://github.com/remix-run/react-router/compare/react-router@6.26.0...react-router@6.26.1)\n\n## v6.26.0\n\nDate: 2024-08-01\n\n### Minor Changes\n\n- Add a new `replace(url, init?)` alternative to `redirect(url, init?)` that performs a `history.replaceState` instead of a `history.pushState` on client-side navigation redirects ([#11811](https://github.com/remix-run/react-router/pull/11811))\n- Add a new `unstable_data()` API for usage with Remix Single Fetch ([#11836](https://github.com/remix-run/react-router/pull/11836))\n  - This API is not intended for direct usage in React Router SPA applications\n  - It is primarily intended for usage with `createStaticHandler.query()` to allow loaders/actions to return arbitrary data along with custom `status`/`headers` without forcing the serialization of data into a `Response` instance\n  - This allows for more advanced serialization tactics via `unstable_dataStrategy` such as serializing via `turbo-stream` in Remix Single Fetch\n  - ⚠️ This removes the `status` field from `HandlerResult`\n    - If you need to return a specific `status` from `unstable_dataStrategy` you should instead do so via `unstable_data()`\n\n### Patch Changes\n\n- Fix internal cleanup of interrupted fetchers to avoid invalid revalidations on navigations ([#11839](https://github.com/remix-run/react-router/pull/11839))\n- Fix initial hydration behavior when using `future.v7_partialHydration` along with `unstable_patchRoutesOnMiss` ([#11838](https://github.com/remix-run/react-router/pull/11838))\n  - During initial hydration, `router.state.matches` will now include any partial matches so that we can render ancestor `HydrateFallback` components\n\n**Full Changelog**: [`v6.25.1...v6.26.0`](https://github.com/remix-run/react-router/compare/react-router@6.25.1...react-router@6.26.0)\n\n## v6.25.1\n\nDate: 2024-07-17\n\n### Patch Changes\n\n- Memoize some `RouterProvider` internals to reduce unnecessary re-renders ([#11803](https://github.com/remix-run/react-router/pull/11803))\n\n**Full Changelog**: [`v6.25.0...v6.25.1`](https://github.com/remix-run/react-router/compare/react-router@6.25.0...react-router@6.25.1)\n\n## v6.25.0\n\nDate: 2024-07-16\n\n### What's Changed\n\n#### Stabilized `v7_skipActionErrorRevalidation`\n\nThis release stabilizes the `future.unstable_skipActionErrorRevalidation` flag into [`future.v7_skipActionErrorRevalidation`](https://reactrouter.com/v6/upgrading/future#v7_skipactionstatusrevalidation) in preparation for the upcoming React Router v7 release.\n\n- When this flag is enabled, actions that return/throw a `4xx/5xx` `Response` will not trigger a revalidation by default\n- This also stabilizes `shouldRevalidate`'s `unstable_actionStatus` parameter to `actionStatus`\n\n### Minor Changes\n\n- Stabilize `future.unstable_skipActionErrorRevalidation` as `future.v7_skipActionErrorRevalidation` ([#11769](https://github.com/remix-run/react-router/pull/11769))\n\n### Patch Changes\n\n- Fix regression and properly decode paths inside `useMatch` so matches/params reflect decoded params ([#11789](https://github.com/remix-run/react-router/pull/11789))\n- Fix bubbling of errors thrown from `unstable_patchRoutesOnMiss` ([#11786](https://github.com/remix-run/react-router/pull/11786))\n- Fix hydration in SSR apps using `unstable_patchRoutesOnMiss` that matched a splat route on the server ([#11790](https://github.com/remix-run/react-router/pull/11790))\n\n**Full Changelog**: [`v6.24.1...v6.25.0`](https://github.com/remix-run/react-router/compare/react-router@6.24.1...react-router@6.25.0)\n\n## v6.24.1\n\nDate: 2024-07-03\n\n### Patch Changes\n\n- Remove `polyfill.io` reference from warning message because the domain was sold and has since been determined to serve malware ([#11741](https://github.com/remix-run/react-router/pull/11741))\n  - See https://sansec.io/research/polyfill-supply-chain-attack\n- Export `NavLinkRenderProps` type for easier typing of custom `NavLink` callback ([#11553](https://github.com/remix-run/react-router/pull/11553))\n- When using `future.v7_relativeSplatPath`, properly resolve relative paths in splat routes that are children of pathless routes ([#11633](https://github.com/remix-run/react-router/pull/11633))\n- Fog of War (unstable): Trigger a new `router.routes` identity/reflow during route patching ([#11740](https://github.com/remix-run/react-router/pull/11740))\n- Fog of War (unstable): Fix initial matching when a splat route matches ([#11759](https://github.com/remix-run/react-router/pull/11759))\n\n**Full Changelog**: [`v6.24.0...v6.24.1`](https://github.com/remix-run/react-router/compare/react-router@6.24.0...react-router@6.24.1)\n\n## v6.24.0\n\nDate: 2024-06-24\n\n### What's Changed\n\n#### Lazy Route Discovery (a.k.a. \"Fog of War\")\n\nWe're really excited to release our new API for \"Lazy Route Discovery\" in `v6.24.0`! For some background information, please check out the original [RFC](https://github.com/remix-run/react-router/discussions/11113). The **tl;dr;** is that ever since we introduced the Data APIs in v6.4 via `<RouterProvider>`, we've been a little bummed that one of the tradeoffs was the lack of a compelling code-splitting story mirroring what we had in the `<BrowserRouter>`/`<Routes>` apps. We took a baby-step towards improving that story with `route.lazy` in `v6.9.0`, but with `v6.24.0` we've gone the rest of the way.\n\nWith \"Fog of War\", you can now load portions of the route tree lazily via the new `unstable_patchRoutesOnMiss` option passed to `createBrowserRouter` (and it's memory/hash counterparts). This gives you a way to hook into spots where React Router is unable to match a given path and patch new routes into the route tree during the navigation (or fetcher call).\n\nHere's a very small example, but please refer to the [documentation](https://reactrouter.com/v6/routers/create-browser-router#optsunstable_patchroutesonmiss) for more information and use cases:\n\n```js\nconst router = createBrowserRouter(\n  [\n    {\n      id: \"root\",\n      path: \"/\",\n      Component: RootComponent,\n    },\n  ],\n  {\n    async unstable_patchRoutesOnMiss({ path, patch }) {\n      if (path === \"/a\") {\n        // Load the `a` route (`{ path: 'a', Component: A }`)\n        let route = await getARoute();\n        // Patch the `a` route in as a new child of the `root` route\n        patch(\"root\", [route]);\n      }\n    },\n  },\n);\n```\n\n### Minor Changes\n\n- Add support for Lazy Route Discovery (a.k.a. \"Fog of War\") ([#11626](https://github.com/remix-run/react-router/pull/11626))\n\n### Patch Changes\n\n- Fix `fetcher.submit` types - remove incorrect `navigate`/`fetcherKey`/`unstable_viewTransition` options because they are only relevant for `useSubmit` ([#11631](https://github.com/remix-run/react-router/pull/11631))\n- Allow falsy `location.state` values passed to `<StaticRouter>` ([#11495](https://github.com/remix-run/react-router/pull/11495))\n\n**Full Changelog**: [`v6.23.1...v6.24.0`](https://github.com/remix-run/react-router/compare/react-router@6.23.1...react-router@6.24.0)\n\n## v6.23.1\n\nDate: 2024-05-10\n\n### Patch Changes\n\n- Allow `undefined` to be resolved through `<Await>` ([#11513](https://github.com/remix-run/react-router/pull/11513))\n- Add defensive `document` check when checking for `document.startViewTransition` availability ([#11544](https://github.com/remix-run/react-router/pull/11544))\n- Change the `react-router-dom/server` import back to `react-router-dom` instead of `index.ts` ([#11514](https://github.com/remix-run/react-router/pull/11514))\n- `@remix-run/router` - Support `unstable_dataStrategy` on `staticHandler.queryRoute` ([#11515](https://github.com/remix-run/react-router/pull/11515))\n\n**Full Changelog**: [`v6.23.0...v6.23.1`](https://github.com/remix-run/react-router/compare/react-router@6.23.0...react-router@6.23.1)\n\n## v6.23.0\n\nDate: 2024-04-23\n\n### What's Changed\n\n#### Data Strategy (unstable)\n\nThe new `unstable_dataStrategy` API is a low-level API designed for advanced use-cases where you need to take control over the data strategy for your `loader`/`action` functions. The default implementation is today's behavior, to fetch all loaders in parallel, but this option allows users to implement more advanced data flows including Remix [\"Single Fetch\"](https://remix.run/docs/guides/single-fetch), user-land middleware/context APIs, automatic loader caching, and more. Please see the [docs](https://reactrouter.com/v6/routers/create-browser-router#unstable_datastrategy) for more information.\n\n**Note:** This is a low-level API intended for advanced use-cases. This overrides React Router's internal handling of `loader`/`action` execution, and if done incorrectly will break your app code. Please use with caution and perform the appropriate testing.\n\n#### Skip Action Error Revalidation (unstable)\n\nCurrently, all active `loader`'s revalidate after any `action` submission, regardless of the `action` result. However, in the majority of cases a `4xx`/`5xx` response from an `action` means that no data was actually changed and the revalidation is unnecessary. We've introduced a new `future.unstable_skipActionErrorRevalidation` flag that changes the behavior here, and we plan to make this the default in future version of React Router.\n\nWith this flag enabled, `action`'s that return/throw a `4xx`/`5xx` response status will no longer automatically revalidate. If you need to revalidate after a `4xx`/`5xx` result with this flag enabled, you can still do that via returning `true` from `shouldRevalidate` - which now also receives a new `unstable_actionStatus` argument alongside `actionResult` so you can make decision based on the status of the `action` response without having to encode it into the action data.\n\n### Minor Changes\n\n- Add a new `unstable_dataStrategy` configuration option ([#11098](https://github.com/remix-run/react-router/pull/11098), [#11377](https://github.com/remix-run/react-router/pull/11377))\n- `@remix-run/router` - Add a new `future.unstable_skipActionRevalidation` future flag ([#11098](https://github.com/remix-run/react-router/pull/11098))\n- `@remix-run/router` - SSR: Added a new `skipLoaderErrorBubbling` options to the `staticHandler.query` method to disable error bubbling by the static handler for use in Remix's Single Fetch implementation ([#11098](https://github.com/remix-run/react-router/pull/11098), ([#11377](https://github.com/remix-run/react-router/pull/11377)))\n\n**Full Changelog**: [`v6.22.3...v6.23.0`](https://github.com/remix-run/react-router/compare/react-router@6.22.3...react-router@6.23.0)\n\n## v6.22.3\n\nDate: 2024-03-07\n\n### Patch Changes\n\n- Fix a `future.v7_partialHydration` bug that would re-run loaders below the boundary on hydration if SSR loader errors bubbled to a parent boundary ([#11324](https://github.com/remix-run/react-router/pull/11324))\n- Fix a `future.v7_partialHydration` bug that would consider the router uninitialized if a route did not have a loader ([#11325](https://github.com/remix-run/react-router/pull/11325))\n\n**Full Changelog**: [`v6.22.2...v6.22.3`](https://github.com/remix-run/react-router/compare/react-router@6.22.2...react-router@6.22.3)\n\n## v6.22.2\n\nDate: 2024-02-28\n\n### Patch Changes\n\n- Preserve hydrated errors during partial hydration runs ([#11305](https://github.com/remix-run/react-router/pull/11305))\n\n**Full Changelog**: [`v6.22.1...v6.22.2`](https://github.com/remix-run/react-router/compare/react-router@6.22.1...react-router@6.22.2)\n\n## v6.22.1\n\nDate: 2024-02-16\n\n### Patch Changes\n\n- Fix encoding/decoding issues with pre-encoded dynamic parameter values ([#11199](https://github.com/remix-run/react-router/pull/11199))\n\n**Full Changelog**: [`v6.22.0...v6.22.1`](https://github.com/remix-run/react-router/compare/react-router@6.22.0...react-router@6.22.1)\n\n## v6.22.0\n\nDate: 2024-02-01\n\n### What's Changed\n\n#### Core Web Vitals Technology Report Flag\n\nIn 2021, the HTTP Archive launched the [Core Web Vitals Technology Report dashboard](https://discuss.httparchive.org/t/new-dashboard-the-core-web-vitals-technology-report/2178):\n\n> By combining the powers of real-user experiences in the Chrome UX Report 26 (CrUX) dataset with web technology detections in HTTP Archive 30, we can get a glimpse into how architectural decisions like choices of CMS platform or JavaScript framework play a role in sites’ CWV performance.\n\nThey use a tool called [`wappalyzer`](https://github.com/HTTPArchive/wappalyzer) to identify what technologies a given website is using by looking for certain scripts, global JS variables, or other identifying characteristics. For example, for Remix applications, they [look for the global `__remixContext`](https://github.com/HTTPArchive/wappalyzer/blob/c2a24ee7c2d07bf9c521f02584ae2dcf603ac0b7/src/technologies/r.json#L1328) variable to identify that a website is using Remix.\n\nIt was brought to our attention that React Router was unable to be reliably identified because there are no identifying global aspects. They are currently [looking for external scripts with `react-router`](https://github.com/HTTPArchive/wappalyzer/blob/c2a24ee7c2d07bf9c521f02584ae2dcf603ac0b7/src/technologies/r.json#L637) in the name. This will identify sites using React Router from a CDN such as `unpkg` - but it will miss the **vast** majority of sites that are installing React Router from the npm registry and bundling it into their JS files. This results in [drastically under-reporting](https://lookerstudio.google.com/s/pixHkNmGbN4) the usage of React Router on the web.\n\nStarting with version `6.22.0`, sites using `react-router-dom` will begin adding a `window.__reactRouterVersion` variable that will be set to a string value of the SemVer major version number (i.e., `window.__reactRouterVersion = \"6\";`) so that they can be properly identified.\n\n### Minor Changes\n\n- Include a `window.__reactRouterVersion` for CWV Report detection ([#11222](https://github.com/remix-run/react-router/pull/11222))\n- Add a `createStaticHandler` `future.v7_throwAbortReason` flag to throw `request.signal.reason` (defaults to a `DOMException`) when a request is aborted instead of an `Error` such as `new Error(\"query() call aborted: GET /path\")` ([#11104](https://github.com/remix-run/react-router/pull/11104))\n  - Please note that `DOMException` was added in Node v17 so you will not get a `DOMException` on Node 16 and below.\n\n### Patch Changes\n\n- Respect the `ErrorResponse` status code if passed to `getStaticContextFormError` ([#11213](https://github.com/remix-run/react-router/pull/11213))\n\n**Full Changelog**: [`v6.21.3...v6.22.0`](https://github.com/remix-run/react-router/compare/react-router@6.21.3...react-router@6.22.0)\n\n## v6.21.3\n\nDate: 2024-01-18\n\n### Patch Changes\n\n- Fix `NavLink` `isPending` when a `basename` is used ([#11195](https://github.com/remix-run/react-router/pull/11195))\n- Remove leftover `unstable_` prefix from `Blocker`/`BlockerFunction` types ([#11187](https://github.com/remix-run/react-router/pull/11187))\n\n**Full Changelog**: [`v6.21.2...v6.21.3`](https://github.com/remix-run/react-router/compare/react-router@6.21.2...react-router@6.21.3)\n\n## v6.21.2\n\nDate: 2024-01-11\n\n### Patch Changes\n\n- Leverage `useId` for internal fetcher keys when available ([#11166](https://github.com/remix-run/react-router/pull/11166))\n- Fix bug where dashes were not picked up in dynamic parameter names ([#11160](https://github.com/remix-run/react-router/pull/11160))\n- Do not attempt to deserialize empty JSON responses ([#11164](https://github.com/remix-run/react-router/pull/11164))\n\n**Full Changelog**: [`v6.21.1...v6.21.2`](https://github.com/remix-run/react-router/compare/react-router@6.21.1...react-router@6.21.2)\n\n## v6.21.1\n\nDate: 2023-12-21\n\n### Patch Changes\n\n- Fix bug with `route.lazy` not working correctly on initial SPA load when `v7_partialHydration` is specified ([#11121](https://github.com/remix-run/react-router/pull/11121))\n- Fix bug preventing revalidation from occurring for persisted fetchers unmounted during the `submitting` phase ([#11102](https://github.com/remix-run/react-router/pull/11102))\n- De-dup relative path logic in `resolveTo` ([#11097](https://github.com/remix-run/react-router/pull/11097))\n\n**Full Changelog**: [`v6.21.0...v6.21.1`](https://github.com/remix-run/react-router/compare/react-router@6.21.0...react-router@6.21.1)\n\n## v6.21.0\n\nDate: 2023-12-13\n\n### What's Changed\n\n#### `future.v7_relativeSplatPath`\n\nWe fixed a splat route path-resolution bug in `6.19.0`, but later determined a large number of applications were relying on the buggy behavior, so we reverted the fix in `6.20.1` (see [#10983](https://github.com/remix-run/react-router/issues/10983), [#11052](https://github.com/remix-run/react-router/issues/11052), [#11078](https://github.com/remix-run/react-router/issues/11078)).\n\nThe buggy behavior is that the default behavior when resolving relative paths inside a splat route would _ignore_ any splat (`*`) portion of the current route path. When the future flag is enabled, splat portions are included in relative path logic within splat routes.\n\nFor more information, please refer to the [`useResolvedPath` docs](https://reactrouter.com/v6/hooks/use-resolved-path#splat-paths) and/or the [detailed changelog entry](https://github.com/remix-run/react-router/blob/main/packages/react-router-dom/CHANGELOG.md#6210).\n\n#### Partial Hydration\n\nWe added a new `future.v7_partialHydration` future flag for the `@remix-run/router` that enables partial hydration of a data router when Server-Side Rendering. This allows you to provide `hydrationData.loaderData` that has values for _some_ initially matched route loaders, but not all. When this flag is enabled, the router will call `loader` functions for routes that do not have hydration loader data during `router.initialize()`, and it will render down to the deepest provided `HydrateFallback` (up to the first route without hydration data) while it executes the unhydrated routes. ([#11033](https://github.com/remix-run/react-router/pull/11033))\n\n### Minor Changes\n\n- Add a new `future.v7_relativeSplatPath` flag to implement a breaking bug fix to relative routing when inside a splat route. ([#11087](https://github.com/remix-run/react-router/pull/11087))\n- Add a new `future.v7_partialHydration` future flag that enables partial hydration of a data router when Server-Side Rendering ([#11033](https://github.com/remix-run/react-router/pull/11033))\n\n### Patch Changes\n\n- Properly handle falsy error values in `ErrorBoundary`'s ([#11071](https://github.com/remix-run/react-router/pull/11071))\n- Catch and bubble errors thrown when trying to unwrap responses from `loader`/`action` functions ([#11061](https://github.com/remix-run/react-router/pull/11061))\n- Fix `relative=\"path\"` issue when rendering `Link`/`NavLink` outside of matched routes ([#11062](https://github.com/remix-run/react-router/pull/11062))\n\n**Full Changelog**: [`v6.20.1...v6.21.0`](https://github.com/remix-run/react-router/compare/react-router@6.20.1...react-router@6.21.0)\n\n## v6.20.1\n\nDate: 2023-12-01\n\n### Patch Changes\n\n- Revert the `useResolvedPath` fix for splat routes due to a large number of applications that were relying on the buggy behavior (see [#11052](https://github.com/remix-run/react-router/issues/11052#issuecomment-1836589329)) ([#11078](https://github.com/remix-run/react-router/pull/11078))\n  - We plan to re-introduce this fix behind a future flag in the next minor version (see [this comment](https://github.com/remix-run/react-router/issues/11052#issuecomment-1836589329))\n  - This fix was included in versions `6.19.0` and `6.20.0`. If you are upgrading from `6.18.0` or earlier, you would not have been impacted by this fix.\n\n**Full Changelog**: [`v6.20.0...v6.20.1`](https://github.com/remix-run/react-router/compare/react-router@6.20.0...react-router@6.20.1)\n\n## v6.20.0\n\nDate: 2023-11-22\n\n> [!WARNING]\n> Please use version `6.20.1` or later instead of `6.20.0`. We discovered that a large number of apps were relying on buggy behavior that was fixed in this release ([#11045](https://github.com/remix-run/react-router/pull/11045)). We reverted the fix in `6.20.1` and will be re-introducing it behind a future flag in a subsequent release. See [#11052](https://github.com/remix-run/react-router/issues/11052#issuecomment-1836589329) for more details.\n\n### Minor Changes\n\n- Export the `PathParam` type from the public API ([#10719](https://github.com/remix-run/react-router/pull/10719))\n\n### Patch Changes\n\n- Do not revalidate unmounted fetchers when `v7_fetcherPersist` is enabled ([#11044](https://github.com/remix-run/react-router/pull/11044))\n- Fix bug with `resolveTo` path resolution in splat routes ([#11045](https://github.com/remix-run/react-router/pull/11045))\n  - This is a follow up to [#10983](https://github.com/remix-run/react-router/pull/10983) to handle the few other code paths using `getPathContributingMatches`\n  - This removes the `UNSAFE_getPathContributingMatches` export from `@remix-run/router` since we no longer need this in the `react-router`/`react-router-dom` layers\n\n**Full Changelog**: [`v6.19.0...v6.20.0`](https://github.com/remix-run/react-router/compare/react-router@6.19.0...react-router@6.20.0)\n\n## v6.19.0\n\nDate: 2023-11-16\n\n> [!WARNING]\n> Please use version `6.20.1` or later instead of `6.19.0`. We discovered that a large number of apps were relying on buggy behavior that was fixed in this release ([#10983](https://github.com/remix-run/react-router/pull/10983)). We reverted the fix in `6.20.1` and will be re-introducing it behind a future flag in a subsequent release. See [#11052](https://github.com/remix-run/react-router/issues/11052#issuecomment-1836589329) for more details.\n\n### What's Changed\n\n#### `unstable_flushSync` API\n\nThis release brings a new `unstable_flushSync` option to the imperative APIs (`useSubmit`, `useNavigate`, `fetcher.submit`, `fetcher.load`) to let users opt-into synchronous DOM updates for pending/optimistic UI.\n\n```js\nfunction handleClick() {\n  submit(data, { flushSync: true });\n  // Everything is flushed to the DOM so you can focus/scroll to your pending/optimistic UI\n  setFocusAndOrScrollToNewlyAddedThing();\n}\n```\n\n### Minor Changes\n\n- Add `unstable_flushSync` option to `useNavigate`/`useSubmit`/`fetcher.load`/`fetcher.submit` to opt-out of `React.startTransition` and into `ReactDOM.flushSync` for state updates ([#11005](https://github.com/remix-run/react-router/pull/11005))\n- Remove the `unstable_` prefix from the [`useBlocker`](https://reactrouter.com/v6/hooks/use-blocker) hook as it's been in use for enough time that we are confident in the API ([#10991](https://github.com/remix-run/react-router/pull/10991))\n  - We do not plan to remove the prefix from `unstable_usePrompt` due to differences in how browsers handle `window.confirm` that prevent React Router from guaranteeing consistent/correct behavior\n\n### Patch Changes\n\n- Fix `useActionData` so it returns proper contextual action data and not _any_ action data in the tree ([#11023](https://github.com/remix-run/react-router/pull/11023))\n- Fix bug in `useResolvedPath` that would cause `useResolvedPath(\".\")` in a splat route to lose the splat portion of the URL path. ([#10983](https://github.com/remix-run/react-router/pull/10983))\n  - ⚠️ This fixes a quite long-standing bug specifically for `\".\"` paths inside a splat route which incorrectly dropped the splat portion of the URL. If you are relative routing via `\".\"` inside a splat route in your application you should double check that your logic is not relying on this buggy behavior and update accordingly.\n- Fix issue where a changing fetcher `key` in a `useFetcher` that remains mounted wasn't getting picked up ([#11009](https://github.com/remix-run/react-router/pull/11009))\n- Fix `useFormAction` which was incorrectly inheriting the `?index` query param from child route `action` submissions ([#11025](https://github.com/remix-run/react-router/pull/11025))\n- Fix `NavLink` `active` logic when `to` location has a trailing slash ([#10734](https://github.com/remix-run/react-router/pull/10734))\n- Fix types so `unstable_usePrompt` can accept a `BlockerFunction` in addition to a `boolean` ([#10991](https://github.com/remix-run/react-router/pull/10991))\n- Fix `relative=\"path\"` bug where relative path calculations started from the full location pathname, instead of from the current contextual route pathname. ([#11006](https://github.com/remix-run/react-router/pull/11006))\n\n  ```jsx\n  <Route path=\"/a\">\n    <Route path=\"/b\" element={<Component />}>\n      <Route path=\"/c\" />\n    </Route>\n  </Route>;\n\n  function Component() {\n    return (\n      <>\n        {/* This is now correctly relative to /a/b, not /a/b/c */}\n        <Link to=\"..\" relative=\"path\" />\n        <Outlet />\n      </>\n    );\n  }\n  ```\n\n**Full Changelog**: [`6.18.0...6.19.0`](https://github.com/remix-run/react-router/compare/react-router@6.18.0...react-router@6.19.0)\n\n## v6.18.0\n\nDate: 2023-10-31\n\n### What's Changed\n\n#### New Fetcher APIs\n\nPer this [RFC](https://github.com/remix-run/remix/discussions/7698), we've introduced some new APIs that give you more granular control over your fetcher behaviors.\n\n- You may now specify your own fetcher identifier via `useFetcher({ key: string })`, which allows you to access the same fetcher instance from different components in your application without prop-drilling\n- Fetcher keys are now exposed on the fetchers returned from `useFetchers` so that they can be looked up by `key`\n- `Form` and `useSubmit` now support optional `navigate`/`fetcherKey` props/params to allow kicking off a fetcher submission under the hood with an optionally user-specified `key`\n  - `<Form method=\"post\" navigate={false} fetcherKey=\"my-key\">`\n  - `submit(data, { method: \"post\", navigate: false, fetcherKey: \"my-key\" })`\n  - Invoking a fetcher in this way is ephemeral and stateless\n  - If you need to access the state of one of these fetchers, you will need to leverage `useFetchers()` or `useFetcher({ key })` to look it up elsewhere\n\n#### Persistence Future Flag (`future.v7_fetcherPersist`)\n\nPer the same [RFC](https://github.com/remix-run/remix/discussions/7698) as above, we've introduced a new `future.v7_fetcherPersist` flag that allows you to opt-into the new fetcher persistence/cleanup behavior. Instead of being immediately cleaned up on unmount, fetchers will persist until they return to an `idle` state. This makes pending/optimistic UI _much_ easier in scenarios where the originating fetcher needs to unmount.\n\n- This is sort of a long-standing bug fix as the `useFetchers()` API was always supposed to only reflect **in-flight** fetcher information for pending/optimistic UI -- it was not intended to reflect fetcher data or hang onto fetchers after they returned to an `idle` state\n- Keep an eye out for the following specific behavioral changes when opting into this flag and check your app for compatibility:\n  - Fetchers that complete _while still mounted_ will no longer appear in `useFetchers()` after completion - they served no purpose in there since you can access the data via `useFetcher().data`\n  - Fetchers that previously unmounted _while in-flight_ will not be immediately aborted and will instead be cleaned up once they return to an `idle` state\n    - They will remain exposed via `useFetchers` while in-flight so you can still access pending/optimistic data after unmount\n    - If a fetcher is no longer mounted when it completes, then it's result will not be post processed - e.g., redirects will not be followed and errors will not bubble up in the UI\n    - However, if a fetcher was re-mounted elsewhere in the tree using the same `key`, then it's result will be processed, even if the originating fetcher was unmounted\n\n### Minor Changes\n\n- Add fetcher `key` APIs and `navigate=false` options ([#10960](https://github.com/remix-run/react-router/pull/10960))\n- Add `future.v7_fetcherPersist` flag ([#10962](https://github.com/remix-run/react-router/pull/10962))\n- Add support for optional path segments in `matchPath` ([#10768](https://github.com/remix-run/react-router/pull/10768))\n\n### Patch Changes\n\n- Fix the `future` prop on `BrowserRouter`, `HashRouter` and `MemoryRouter` so that it accepts a `Partial<FutureConfig>` instead of requiring all flags to be included ([#10962](https://github.com/remix-run/react-router/pull/10962))\n- Fix `router.getFetcher`/`router.deleteFetcher` type definitions which incorrectly specified `key` as an optional parameter ([#10960](https://github.com/remix-run/react-router/pull/10960))\n\n**Full Changelog**: [`6.17.0...6.18.0`](https://github.com/remix-run/react-router/compare/react-router@6.17.0...react-router@6.18.0)\n\n## v6.17.0\n\nDate: 2023-10-16\n\n### What's Changed\n\n#### View Transitions 🚀\n\nWe're excited to release experimental support for the [View Transitions API](https://developer.mozilla.org/en-US/docs/Web/API/ViewTransition) in React Router! You can now trigger navigational DOM updates to be wrapped in `document.startViewTransition` to enable CSS animated transitions on SPA navigations in your application.\n\nThe simplest approach to enabling a View Transition in your React Router app is via the new [`<Link unstable_viewTransition>`](https://reactrouter.com/v6/components/link#unstable_viewtransition) prop. This will cause the navigation DOM update to be wrapped in `document.startViewTransition` which will enable transitions for the DOM update. Without any additional CSS styles, you'll get a basic cross-fade animation for your page.\n\nIf you need to apply more fine-grained styles for your animations, you can leverage the [`unstable_useViewTransitionState`](https://reactrouter.com/v6/hooks/use-view-transition-state) hook which will tell you when a transition is in progress and you can use that to apply classes or styles:\n\n```jsx\nfunction ImageLink(to, src, alt) {\n  const isTransitioning = unstable_useViewTransitionState(to);\n  return (\n    <Link to={to} unstable_viewTransition>\n      <img\n        src={src}\n        alt={alt}\n        style={{\n          viewTransitionName: isTransitioning ? \"image-expand\" : \"\",\n        }}\n      />\n    </Link>\n  );\n}\n```\n\nYou can also use the [`<NavLink unstable_viewTransition>`](https://reactrouter.com/v6/components/nav-link#unstable_viewtransition) shorthand which will manage the hook usage for you and automatically add a `transitioning` class to the `<a>` during the transition:\n\n```css\na.transitioning img {\n  view-transition-name: \"image-expand\";\n}\n```\n\n```jsx\n<NavLink to={to} unstable_viewTransition>\n  <img src={src} alt={alt} />\n</NavLink>\n```\n\nFor an example usage of View Transitions, check out [our fork](https://github.com/brophdawg11/react-router-records) of the awesome [Astro Records](https://github.com/Charca/astro-records) demo.\n\nFor more information on using the View Transitions API, please refer to the [Smooth and simple transitions with the View Transitions API](https://developer.chrome.com/docs/web-platform/view-transitions/) guide from the Google Chrome team.\n\n### Minor Changes\n\n- Add support for view transitions ([#10916](https://github.com/remix-run/react-router/pull/10916))\n\n### Patch Changes\n\n- Log a warning and fail gracefully in `ScrollRestoration` when `sessionStorage` is unavailable ([#10848](https://github.com/remix-run/react-router/pull/10848))\n- Fix `RouterProvider` `future` prop type to be a `Partial<FutureConfig>` so that not all flags must be specified ([#10900](https://github.com/remix-run/react-router/pull/10900))\n- Allow 404 detection to leverage root route error boundary if path contains a URL segment ([#10852](https://github.com/remix-run/react-router/pull/10852))\n- Fix `ErrorResponse` type to avoid leaking internal field ([#10876](https://github.com/remix-run/react-router/pull/10876))\n\n**Full Changelog**: [`6.16.0...6.17.0`](https://github.com/remix-run/react-router/compare/react-router@6.16.0...react-router@6.17.0)\n\n## v6.16.0\n\nDate: 2023-09-13\n\n### Minor Changes\n\n- In order to move towards stricter TypeScript support in the future, we're aiming to replace current usages of `any` with `unknown` on exposed typings for user-provided data. To do this in Remix v2 without introducing breaking changes in React Router v6, we have added generics to a number of shared types. These continue to default to `any` in React Router and are overridden with `unknown` in Remix. In React Router v7 we plan to move these to `unknown` as a breaking change. ([#10843](https://github.com/remix-run/react-router/pull/10843))\n  - `Location` now accepts a generic for the `location.state` value\n  - `ActionFunctionArgs`/`ActionFunction`/`LoaderFunctionArgs`/`LoaderFunction` now accept a generic for the `context` parameter (only used in SSR usages via `createStaticHandler`)\n  - The return type of `useMatches` (now exported as `UIMatch`) accepts generics for `match.data` and `match.handle` - both of which were already set to `unknown`\n- Move the `@private` class export `ErrorResponse` to an `UNSAFE_ErrorResponseImpl` export since it is an implementation detail and there should be no construction of `ErrorResponse` instances in userland. This frees us up to export a `type ErrorResponse` which correlates to an instance of the class via `InstanceType`. Userland code should only ever be using `ErrorResponse` as a type and should be type-narrowing via `isRouteErrorResponse`. ([#10811](https://github.com/remix-run/react-router/pull/10811))\n- Export `ShouldRevalidateFunctionArgs` interface ([#10797](https://github.com/remix-run/react-router/pull/10797))\n- Removed private/internal APIs only required for the Remix v1 backwards compatibility layer and no longer needed in Remix v2 (`_isFetchActionRedirect`, `_hasFetcherDoneAnything`) ([#10715](https://github.com/remix-run/react-router/pull/10715))\n\n### Patch Changes\n\n- Properly encode rendered URIs in server rendering to avoid hydration errors ([#10769](https://github.com/remix-run/react-router/pull/10769))\n- Add method/url to error message on aborted `query`/`queryRoute` calls ([#10793](https://github.com/remix-run/react-router/pull/10793))\n- Fix a race-condition with loader/action-thrown errors on `route.lazy` routes ([#10778](https://github.com/remix-run/react-router/pull/10778))\n- Fix type for `actionResult` on the arguments object passed to `shouldRevalidate` ([#10779](https://github.com/remix-run/react-router/pull/10779))\n\n**Full Changelog**: [`v6.15.0...v6.16.0`](https://github.com/remix-run/react-router/compare/react-router@6.15.0...react-router@6.16.0)\n\n## v6.15.0\n\nDate: 2023-08-10\n\n### Minor Changes\n\n- Add's a new `redirectDocument()` function which allows users to specify that a redirect from a `loader`/`action` should trigger a document reload (via `window.location`) instead of attempting to navigate to the redirected location via React Router ([#10705](https://github.com/remix-run/react-router/pull/10705))\n\n### Patch Changes\n\n- Ensure `useRevalidator` is referentially stable across re-renders if revalidations are not actively occurring ([#10707](https://github.com/remix-run/react-router/pull/10707))\n- Ensure hash history always includes a leading slash on hash pathnames ([#10753](https://github.com/remix-run/react-router/pull/10753))\n- Fixes an edge-case affecting web extensions in Firefox that use `URLSearchParams` and the `useSearchParams` hook ([#10620](https://github.com/remix-run/react-router/pull/10620))\n- Reorder effects in `unstable_usePrompt` to avoid throwing an exception if the prompt is unblocked and a navigation is performed synchronously ([#10687](https://github.com/remix-run/react-router/pull/10687), [#10718](https://github.com/remix-run/react-router/pull/10718))\n- SSR: Do not include hash in `useFormAction()` for unspecified actions since it cannot be determined on the server and causes hydration issues ([#10758](https://github.com/remix-run/react-router/pull/10758))\n- SSR: Fix an issue in `queryRoute` that was not always identifying thrown `Response` instances ([#10717](https://github.com/remix-run/react-router/pull/10717))\n- `react-router-native`: Update `@ungap/url-search-params` dependency from `^0.1.4` to `^0.2.2` ([#10590](https://github.com/remix-run/react-router/pull/10590))\n\n**Full Changelog**: [`v6.14.2...v6.15.0`](https://github.com/remix-run/react-router/compare/react-router@6.14.2...react-router@6.15.0)\n\n## v6.14.2\n\nDate: 2023-07-17\n\n### Patch Changes\n\n- Add missing `<Form state>` prop to populate `history.state` on submission navigations ([#10630](https://github.com/remix-run/react-router/pull/10630))\n- Trigger an error if a `defer` promise resolves/rejects with `undefined` in order to match the behavior of loaders and actions which must return a value or `null` ([#10690](https://github.com/remix-run/react-router/pull/10690))\n- Properly handle fetcher redirects interrupted by normal navigations ([#10674](https://github.com/remix-run/react-router/pull/10674))\n- Initial-load fetchers should not automatically revalidate on GET navigations ([#10688](https://github.com/remix-run/react-router/pull/10688))\n- Properly decode element id when emulating hash scrolling via `<ScrollRestoration>` ([#10682](https://github.com/remix-run/react-router/pull/10682))\n- Typescript: Enhance the return type of `Route.lazy` to prohibit returning an empty object ([#10634](https://github.com/remix-run/react-router/pull/10634))\n- SSR: Support proper hydration of `Error` subclasses such as `ReferenceError`/`TypeError` ([#10633](https://github.com/remix-run/react-router/pull/10633))\n\n**Full Changelog**: [`v6.14.1...v6.14.2`](https://github.com/remix-run/react-router/compare/react-router@6.14.1...react-router@6.14.2)\n\n## v6.14.1\n\nDate: 2023-06-30\n\n### Patch Changes\n\n- Fix loop in `unstable_useBlocker` when used with an unstable blocker function ([#10652](https://github.com/remix-run/react-router/pull/10652))\n- Fix issues with reused blockers on subsequent navigations ([#10656](https://github.com/remix-run/react-router/pull/10656))\n- Updated dependencies:\n  - `@remix-run/router@1.7.1`\n\n**Full Changelog**: [`v6.14.0...v6.14.1`](https://github.com/remix-run/react-router/compare/react-router@6.14.0...react-router@6.14.1)\n\n## v6.14.0\n\nDate: 2023-06-23\n\n### What's Changed\n\n#### JSON/Text Submissions\n\n`6.14.0` adds support for JSON and Text submissions via `useSubmit`/`fetcher.submit` since it's not always convenient to have to serialize into `FormData` if you're working in a client-side SPA. To opt-into these encodings you just need to specify the proper `formEncType`:\n\n**Opt-into `application/json` encoding:**\n\n```js\nfunction Component() {\n  let navigation = useNavigation();\n  let submit = useSubmit();\n  submit({ key: \"value\" }, { method: \"post\", encType: \"application/json\" });\n  // navigation.formEncType => \"application/json\"\n  // navigation.json        => { key: \"value\" }\n}\n\nasync function action({ request }) {\n  // request.headers.get(\"Content-Type\") => \"application/json\"\n  // await request.json()                => { key: \"value\" }\n}\n```\n\n**Opt-into `text/plain` encoding:**\n\n```js\nfunction Component() {\n  let navigation = useNavigation();\n  let submit = useSubmit();\n  submit(\"Text submission\", { method: \"post\", encType: \"text/plain\" });\n  // navigation.formEncType => \"text/plain\"\n  // navigation.text        => \"Text submission\"\n}\n\nasync function action({ request }) {\n  // request.headers.get(\"Content-Type\") => \"text/plain\"\n  // await request.text()                => \"Text submission\"\n}\n```\n\n**⚠️ Default Behavior Will Change in v7**\n\nPlease note that to avoid a breaking change, the default behavior will still encode a simple key/value JSON object into a `FormData` instance:\n\n```jsx\nfunction Component() {\n  let navigation = useNavigation();\n  let submit = useSubmit();\n  submit({ key: \"value\" }, { method: \"post\" });\n  // navigation.formEncType => \"application/x-www-form-urlencoded\"\n  // navigation.formData    => FormData instance\n}\n\nasync function action({ request }) {\n  // request.headers.get(\"Content-Type\") => \"application/x-www-form-urlencoded\"\n  // await request.formData()            => FormData instance\n}\n```\n\nThis behavior will likely change in v7 so it's best to make any JSON object submissions explicit with `formEncType: \"application/x-www-form-urlencoded\"` or `formEncType: \"application/json\"` to ease your eventual v7 migration path.\n\n### Minor Changes\n\n- Add support for `application/json` and `text/plain` encodings for `useSubmit`/`fetcher.submit`. To reflect these additional types, `useNavigation`/`useFetcher` now also contain `navigation.json`/`navigation.text` and `fetcher.json`/`fetcher.text` which include the json/text submission if applicable. ([#10413](https://github.com/remix-run/react-router/pull/10413))\n\n### Patch Changes\n\n- When submitting a form from a `submitter` element, prefer the built-in `new FormData(form, submitter)` instead of the previous manual approach in modern browsers (those that support the new `submitter` parameter) ([#9865](https://github.com/remix-run/react-router/pull/9865))\n  - For browsers that don't support it, we continue to just append the submit button's entry to the end, and we also add rudimentary support for `type=\"image\"` buttons\n  - If developers want full spec-compliant support for legacy browsers, they can use the `formdata-submitter-polyfill`\n- Call `window.history.pushState/replaceState` _before_ updating React Router state (instead of after) so that `window.location` matches `useLocation` during synchronous React 17 rendering ([#10448](https://github.com/remix-run/react-router/pull/10448))\n  - ⚠️ Note: generally apps should not be relying on `window.location` and should always reference `useLocation` when possible, as `window.location` will not be in sync 100% of the time (due to `popstate` events, concurrent mode, etc.)\n- Avoid calling `shouldRevalidate` for fetchers that have not yet completed a data load ([#10623](https://github.com/remix-run/react-router/pull/10623))\n- Strip `basename` from the `location` provided to `<ScrollRestoration getKey>` to match the `useLocation` behavior ([#10550](https://github.com/remix-run/react-router/pull/10550))\n- Strip `basename` from locations provided to `unstable_useBlocker` functions to match the `useLocation` behavior ([#10573](https://github.com/remix-run/react-router/pull/10573))\n- Fix `unstable_useBlocker` key issues in `StrictMode` ([#10573](https://github.com/remix-run/react-router/pull/10573))\n- Fix `generatePath` when passed a numeric `0` value parameter ([#10612](https://github.com/remix-run/react-router/pull/10612))\n- Fix `tsc --skipLibCheck:false` issues on React 17 ([#10622](https://github.com/remix-run/react-router/pull/10622))\n- Upgrade `typescript` to 5.1 ([#10581](https://github.com/remix-run/react-router/pull/10581))\n\n**Full Changelog**: [`v6.13.0...v6.14.0`](https://github.com/remix-run/react-router/compare/react-router@6.13.0...react-router@6.14.0)\n\n## v6.13.0\n\nDate: 2023-06-14\n\n### What's Changed\n\n`6.13.0` is really a patch release in spirit but comes with a SemVer minor bump since we added a new future flag.\n\n#### `future.v7_startTransition`\n\nThe **tl;dr;** is that `6.13.0` is the same as [`6.12.0`](https://github.com/remix-run/react-router/releases/tag/react-router%406.12.0) bue we've moved the usage of `React.startTransition` behind an opt-in `future.v7_startTransition` [future flag](https://reactrouter.com/v6/guides/api-development-strategy) because we found that there are applications in the wild that are currently using `Suspense` in ways that are incompatible with `React.startTransition`.\n\nTherefore, in `6.13.0` the default behavior will no longer leverage `React.startTransition`:\n\n```jsx\n<BrowserRouter>\n  <Routes>{/*...*/}</Routes>\n</BrowserRouter>\n\n<RouterProvider router={router} />\n```\n\nIf you wish to enable `React.startTransition`, pass the future flag to your router component:\n\n```jsx\n<BrowserRouter future={{ v7_startTransition: true }}>\n  <Routes>{/*...*/}</Routes>\n</BrowserRouter>\n\n<RouterProvider router={router} future={{ v7_startTransition: true }}/>\n```\n\nWe recommend folks adopt this flag sooner rather than later to be better compatible with React concurrent mode, but if you run into issues you can continue without the use of `React.startTransition` until v7. Issues usually boil down to creating net-new promises during the render cycle, so if you run into issues when opting into `React.startTransition`, you should either lift your promise creation out of the render cycle or put it behind a `useMemo`.\n\n### Minor Changes\n\n- Move `React.startTransition` usage behinds a future flag ([#10596](https://github.com/remix-run/react-router/pull/10596))\n\n### Patch Changes\n\n- Work around webpack/terser `React.startTransition` minification bug in production mode ([#10588](https://github.com/remix-run/react-router/pull/10588))\n\n**Full Changelog**: [`v6.12.1...v6.13.0`](https://github.com/remix-run/react-router/compare/react-router@6.12.1...react-router@6.13.0)\n\n## v6.12.1\n\nDate: 2023-06-08\n\n> [!WARNING]\n> Please use version `6.13.0` or later instead of `6.12.0`/`6.12.1`. These versions suffered from some Webpack build/minification issues resulting failed builds or invalid minified code in your production bundles. See [#10569](https://github.com/remix-run/react-router/pull/10569) and [#10579](https://github.com/remix-run/react-router/issues/10579) for more details.\n\n### Patch Changes\n\n- Adjust feature detection of `React.startTransition` to fix webpack + react 17 compilation error ([#10569](https://github.com/remix-run/react-router/pull/10569))\n\n**Full Changelog**: [`v6.12.0...v6.12.1`](https://github.com/remix-run/react-router/compare/react-router@6.12.0...react-router@6.12.1)\n\n## v6.12.0\n\nDate: 2023-06-06\n\n> [!WARNING]\n> Please use version `6.13.0` or later instead of `6.12.0`/`6.12.1`. These versions suffered from some Webpack build/minification issues resulting failed builds or invalid minified code in your production bundles. See [#10569](https://github.com/remix-run/react-router/pull/10569) and [#10579](https://github.com/remix-run/react-router/issues/10579) for more details.\n\n### What's Changed\n\n#### `React.startTransition` support\n\nWith `6.12.0` we've added better support for suspending components by wrapping the internal router state updates in [`React.startTransition`](https://react.dev/reference/react/startTransition). This means that, for example, if one of your components in a destination route suspends and you have not provided a [`Suspense`](https://react.dev/reference/react/Suspense) boundary to show a fallback, React will delay the rendering of the new UI and show the old UI until that asynchronous operation resolves. This could be useful for waiting for things such as waiting for images or CSS files to load (and technically, yes, you could use it for data loading but we'd still recommend using loaders for that 😀). For a quick overview of this usage, check out [Ryan's demo on Twitter](https://twitter.com/remix_run/status/1658976420767604736).\n\n### Minor Changes\n\n- Wrap internal router state updates with `React.startTransition` ([#10438](https://github.com/remix-run/react-router/pull/10438))\n\n### Patch Changes\n\n- Allow fetcher revalidations to complete if submitting fetcher is deleted ([#10535](https://github.com/remix-run/react-router/pull/10535))\n- Re-throw `DOMException` (`DataCloneError`) when attempting to perform a `PUSH` navigation with non-serializable state. ([#10427](https://github.com/remix-run/react-router/pull/10427))\n- Ensure revalidations happen when hash is present ([#10516](https://github.com/remix-run/react-router/pull/10516))\n- Upgrade `jest` and `jsdom` ([#10453](https://github.com/remix-run/react-router/pull/10453))\n- Updated dependencies:\n  - `@remix-run/router@1.6.3` ([Changelog](https://github.com/remix-run/react-router/blob/main/packages/router/CHANGELOG.md#163))\n\n**Full Changelog**: [`v6.11.2...v6.12.0`](https://github.com/remix-run/react-router/compare/react-router@6.11.2...react-router@6.12.0)\n\n## v6.11.2\n\nDate: 2023-05-17\n\n### Patch Changes\n\n- Fix `basename` duplication in descendant `<Routes>` inside a `<RouterProvider>` ([#10492](https://github.com/remix-run/react-router/pull/10492))\n- Fix bug where initial data load would not kick off when hash is present ([#10493](https://github.com/remix-run/react-router/pull/10493))\n- Export `SetURLSearchParams` type ([#10444](https://github.com/remix-run/react-router/pull/10444))\n- Fix Remix HMR-driven error boundaries by properly reconstructing new routes and `manifest` in `_internalSetRoutes` ([#10437](https://github.com/remix-run/react-router/pull/10437))\n\n**Full Changelog**: [`v6.11.1...v6.11.2`](https://github.com/remix-run/react-router/compare/react-router@6.11.1...react-router@6.11.2)\n\n## v6.11.1\n\nDate: 2023-05-03\n\n### Patch Changes\n\n- Fix usage of `Component` API within descendant `<Routes>` ([#10434](https://github.com/remix-run/react-router/pull/10434))\n- Fix bug when calling `useNavigate` from `<Routes>` inside a `<RouterProvider>` ([#10432](https://github.com/remix-run/react-router/pull/10432))\n- Fix usage of `<Navigate>` in strict mode when using a data router ([#10435](https://github.com/remix-run/react-router/pull/10435))\n- Fix `basename` handling when navigating without a path ([#10433](https://github.com/remix-run/react-router/pull/10433))\n- \"Same hash\" navigations no longer re-run loaders to match browser behavior (i.e. `/path#hash -> /path#hash`) ([#10408](https://github.com/remix-run/react-router/pull/10408))\n\n**Full Changelog**: [`v6.11.0...v6.11.1`](https://github.com/remix-run/react-router/compare/react-router@6.11.0...react-router@6.11.1)\n\n## v6.11.0\n\nDate: 2023-04-28\n\n### Minor Changes\n\n- Enable `basename` support in `useFetcher` ([#10336](https://github.com/remix-run/react-router/pull/10336))\n  - If you were previously working around this issue by manually prepending the `basename` then you will need to remove the manually prepended `basename` from your `fetcher` calls (`fetcher.load('/basename/route') -> fetcher.load('/route')`)\n- Updated dependencies:\n  - `@remix-run/router@1.6.0` ([Changelog](https://github.com/remix-run/react-router/blob/main/packages/router/CHANGELOG.md#160))\n\n### Patch Changes\n\n- When using a `RouterProvider`, `useNavigate`/`useSubmit`/`fetcher.submit` are now stable across location changes, since we can handle relative routing via the `@remix-run/router` instance and get rid of our dependence on `useLocation()` ([#10336](https://github.com/remix-run/react-router/pull/10336))\n  - When using `BrowserRouter`, these hooks remain unstable across location changes because they still rely on `useLocation()`\n- Fetchers should no longer revalidate on search params changes or routing to the same URL, and will only revalidate on `action` submissions or `router.revalidate` calls ([#10344](https://github.com/remix-run/react-router/pull/10344))\n- Fix inadvertent re-renders when using `Component` instead of `element` on a route definition ([#10287](https://github.com/remix-run/react-router/pull/10287))\n- Fail gracefully on `<Link to=\"//\">` and other invalid URL values ([#10367](https://github.com/remix-run/react-router/pull/10367))\n- Switched from `useSyncExternalStore` to `useState` for internal `@remix-run/router` router state syncing in `<RouterProvider>`. We found some [subtle bugs](https://codesandbox.io/s/use-sync-external-store-loop-9g7b81) where router state updates got propagated _before_ other normal `useState` updates, which could lead to foot guns in `useEffect` calls. ([#10377](https://github.com/remix-run/react-router/pull/10377), [#10409](https://github.com/remix-run/react-router/pull/10409))\n- Log loader/action errors caught by the default error boundary to the console in dev for easier stack trace evaluation ([#10286](https://github.com/remix-run/react-router/pull/10286))\n- Fix bug preventing rendering of descendant `<Routes>` when `RouterProvider` errors existed ([#10374](https://github.com/remix-run/react-router/pull/10374))\n- Fix detection of `useNavigate` in the render cycle by setting the `activeRef` in a layout effect, allowing the `navigate` function to be passed to child components and called in a `useEffect` there ([#10394](https://github.com/remix-run/react-router/pull/10394))\n- Allow `useRevalidator()` to resolve a loader-driven error boundary scenario ([#10369](https://github.com/remix-run/react-router/pull/10369))\n- Enhance `LoaderFunction`/`ActionFunction` return type to prevent `undefined` from being a valid return value ([#10267](https://github.com/remix-run/react-router/pull/10267))\n- Ensure proper 404 error on `fetcher.load` call to a route without a `loader` ([#10345](https://github.com/remix-run/react-router/pull/10345))\n- Decouple `AbortController` usage between revalidating fetchers and the thing that triggered them such that the unmount/deletion of a revalidating fetcher doesn't impact the ongoing triggering navigation/revalidation ([#10271](https://github.com/remix-run/react-router/pull/10271))\n\n**Full Changelog**: [`v6.10.0...v6.11.0`](https://github.com/remix-run/react-router/compare/react-router@6.10.0...react-router@6.11.0)\n\n## v6.10.0\n\nDate: 2023-03-29\n\n### What's Changed\n\nWe recently published a post over on the Remix Blog titled [\"Future Proofing Your Remix App\"](https://remix.run/blog/future-flags) that goes through our strategy to ensure smooth upgrades for your Remix and React Router apps going forward. React Router `6.10.0` adds support for these flags (for data routers) which you can specify when you create your router:\n\n```js\nconst router = createBrowserRouter(routes, {\n  future: {\n    // specify future flags here\n  },\n});\n```\n\nYou can also check out the docs [here](https://reactrouter.com/en/dev/guides/api-development-strategy) and [here](https://reactrouter.com/en/dev/routers/create-browser-router#future).\n\n### Minor Changes\n\n#### `future.v7_normalizeFormMethod`\n\nThe first future flag being introduced is `future.v7_normalizeFormMethod` which will normalize the exposed `useNavigation()/useFetcher()` `formMethod` fields as uppercase HTTP methods to align with the `fetch()` (and some Remix) behavior. ([#10207](https://github.com/remix-run/react-router/pull/10207))\n\n- When `future.v7_normalizeFormMethod` is unspecified or set to `false` (default v6 behavior),\n  - `useNavigation().formMethod` is lowercase\n  - `useFetcher().formMethod` is lowercase\n- When `future.v7_normalizeFormMethod === true`:\n  - `useNavigation().formMethod` is UPPERCASE\n  - `useFetcher().formMethod` is UPPERCASE\n\n### Patch Changes\n\n- Fix `createStaticHandler` to also check for `ErrorBoundary` on routes in addition to `errorElement` ([#10190](https://github.com/remix-run/react-router/pull/10190))\n- Fix route ID generation when using Fragments in `createRoutesFromElements` ([#10193](https://github.com/remix-run/react-router/pull/10193))\n- Provide fetcher submission to `shouldRevalidate` if the fetcher action redirects ([#10208](https://github.com/remix-run/react-router/pull/10208))\n- Properly handle `lazy()` errors during router initialization ([#10201](https://github.com/remix-run/react-router/pull/10201))\n- Remove `instanceof` check for `DeferredData` to be resilient to ESM/CJS boundaries in SSR bundling scenarios ([#10247](https://github.com/remix-run/react-router/pull/10247))\n- Update to latest `@remix-run/web-fetch@4.3.3` ([#10216](https://github.com/remix-run/react-router/pull/10216))\n\n**Full Changelog**: [`v6.9.0...v6.10.0`](https://github.com/remix-run/react-router/compare/react-router@6.9.0...react-router@6.10.0)\n\n## v6.9.0\n\nDate: 2023-03-10\n\n### What's Changed\n\n#### `Component`/`ErrorBoundary` route properties\n\nReact Router now supports an alternative way to define your route `element` and `errorElement` fields as React Components instead of React Elements. You can instead pass a React Component to the new `Component` and `ErrorBoundary` fields if you choose. There is no functional difference between the two, so use whichever approach you prefer 😀. You shouldn't be defining both, but if you do `Component`/`ErrorBoundary` will \"win\"\n\n**Example JSON Syntax**\n\n```jsx\n// Both of these work the same:\nconst elementRoutes = [{\n  path: '/',\n  element: <Home />,\n  errorElement: <HomeError />,\n}]\n\nconst componentRoutes = [{\n  path: '/',\n  Component: Home,\n  ErrorBoundary: HomeError,\n}]\n\nfunction Home() { ... }\nfunction HomeError() { ... }\n```\n\n**Example JSX Syntax**\n\n```jsx\n// Both of these work the same:\nconst elementRoutes = createRoutesFromElements(\n  <Route path='/' element={<Home />} errorElement={<HomeError /> } />\n);\n\nconst componentRoutes = createRoutesFromElements(\n  <Route path='/' Component={Home} ErrorBoundary={HomeError} />\n);\n\nfunction Home() { ... }\nfunction HomeError() { ... }\n```\n\n#### Introducing Lazy Route Modules\n\nIn order to keep your application bundles small and support code-splitting of your routes, we've introduced a new `lazy()` route property. This is an async function that resolves the non-route-matching portions of your route definition (`loader`, `action`, `element`/`Component`, `errorElement`/`ErrorBoundary`, `shouldRevalidate`, `handle`).\n\nLazy routes are resolved on initial load and during the `loading` or `submitting` phase of a navigation or fetcher call. You cannot lazily define route-matching properties (`path`, `index`, `children`) since we only execute your lazy route functions after we've matched known routes.\n\nYour `lazy` functions will typically return the result of a dynamic import.\n\n```jsx\n// In this example, we assume most folks land on the homepage so we include that\n// in our critical-path bundle, but then we lazily load modules for /a and /b so\n// they don't load until the user navigates to those routes\nlet routes = createRoutesFromElements(\n  <Route path=\"/\" element={<Layout />}>\n    <Route index element={<Home />} />\n    <Route path=\"a\" lazy={() => import(\"./a\")} />\n    <Route path=\"b\" lazy={() => import(\"./b\")} />\n  </Route>,\n);\n```\n\nThen in your lazy route modules, export the properties you want defined for the route:\n\n```jsx\nexport async function loader({ request }) {\n  let data = await fetchData(request);\n  return json(data);\n}\n\n// Export a `Component` directly instead of needing to create a React Element from it\nexport function Component() {\n  let data = useLoaderData();\n\n  return (\n    <>\n      <h1>You made it!</h1>\n      <p>{data}</p>\n    </>\n  );\n}\n\n// Export an `ErrorBoundary` directly instead of needing to create a React Element from it\nexport function ErrorBoundary() {\n  let error = useRouteError();\n  return isRouteErrorResponse(error) ? (\n    <h1>\n      {error.status} {error.statusText}\n    </h1>\n  ) : (\n    <h1>{error.message || error}</h1>\n  );\n}\n```\n\nAn example of this in action can be found in the [`examples/lazy-loading-router-provider`](https://github.com/remix-run/react-router/tree/main/examples/lazy-loading-router-provider) directory of the repository. For more info, check out the [`lazy` docs](https://reactrouter.com/v6/route/lazy).\n\n🙌 Huge thanks to @rossipedia for the [Initial Proposal](https://github.com/remix-run/react-router/discussions/9826) and [POC Implementation](https://github.com/remix-run/react-router/pull/9830).\n\n### Minor Changes\n\n- Add support for `route.Component`/`route.ErrorBoundary` properties ([#10045](https://github.com/remix-run/react-router/pull/10045))\n- Add support for `route.lazy` ([#10045](https://github.com/remix-run/react-router/pull/10045))\n\n### Patch Changes\n\n- Improve memoization for context providers to avoid unnecessary re-renders ([#9983](https://github.com/remix-run/react-router/pull/9983))\n- Fix `generatePath` incorrectly applying parameters in some cases ([#10078](https://github.com/remix-run/react-router/pull/10078))\n- `[react-router-dom-v5-compat]` Add missed data router API re-exports ([#10171](https://github.com/remix-run/react-router/pull/10171))\n\n**Full Changelog**: [`v6.8.2...v6.9.0`](https://github.com/remix-run/react-router/compare/react-router@6.8.2...react-router@6.9.0)\n\n## v6.8.2\n\nDate: 2023-02-27\n\n### Patch Changes\n\n- Treat same-origin absolute URLs in `<Link to>` as external if they are outside of the router `basename` ([#10135](https://github.com/remix-run/react-router/pull/10135))\n- Correctly perform a hard redirect for same-origin absolute URLs outside of the router `basename` ([#10076](https://github.com/remix-run/react-router/pull/10076))\n- Fix SSR of absolute `<Link to>` urls ([#10112](https://github.com/remix-run/react-router/pull/10112))\n- Properly escape HTML characters in `StaticRouterProvider` serialized hydration data ([#10068](https://github.com/remix-run/react-router/pull/10068))\n- Fix `useBlocker` to return `IDLE_BLOCKER` during SSR ([#10046](https://github.com/remix-run/react-router/pull/10046))\n- Ensure status code and headers are maintained for `defer` loader responses in `createStaticHandler`'s `query()` method ([#10077](https://github.com/remix-run/react-router/pull/10077))\n- Change `invariant` to an `UNSAFE_invariant` export since it's only intended for internal use ([#10066](https://github.com/remix-run/react-router/pull/10066))\n\n**Full Changelog**: [`v6.8.1...v6.8.2`](https://github.com/remix-run/react-router/compare/react-router@6.8.1...react-router@6.8.2)\n\n## v6.8.1\n\nDate: 2023-02-06\n\n### Patch Changes\n\n- Remove inaccurate console warning for POP navigations and update active blocker logic ([#10030](https://github.com/remix-run/react-router/pull/10030))\n- Only check for differing origin on absolute URL redirects ([#10033](https://github.com/remix-run/react-router/pull/10033))\n- Improved absolute url detection in `Link` component (now also supports `mailto:` urls) ([#9994](https://github.com/remix-run/react-router/pull/9994))\n- Fix partial object (search or hash only) pathnames losing current path value ([#10029](https://github.com/remix-run/react-router/pull/10029))\n\n**Full Changelog**: [`v6.8.0...v6.8.1`](https://github.com/remix-run/react-router/compare/react-router@6.8.0...react-router@6.8.1)\n\n## v6.8.0\n\nDate: 2023-01-26\n\n### Minor Changes\n\nSupport absolute URLs in `<Link to>`. If the URL is for the current origin, it will still do a client-side navigation. If the URL is for a different origin then it will do a fresh document request for the new origin. ([#9900](https://github.com/remix-run/react-router/pull/9900))\n\n```tsx\n<Link to=\"https://neworigin.com/some/path\">    {/* Document request */}\n<Link to=\"//neworigin.com/some/path\">          {/* Document request */}\n<Link to=\"https://www.currentorigin.com/path\"> {/* Client-side navigation */}\n```\n\n### Patch Changes\n\n- Fixes 2 separate issues for revalidating fetcher `shouldRevalidate` calls ([#9948](https://github.com/remix-run/react-router/pull/9948))\n  - The `shouldRevalidate` function was only being called for _explicit_ revalidation scenarios (after a mutation, manual `useRevalidator` call, or an `X-Remix-Revalidate` header used for cookie setting in Remix). It was not properly being called on _implicit_ revalidation scenarios that also apply to navigation `loader` revalidation, such as a change in search params or clicking a link for the page we're already on. It's now correctly called in those additional scenarios.\n  - The parameters being passed were incorrect and inconsistent with one another since the `current*`/`next*` parameters reflected the static `fetcher.load` URL (and thus were identical). Instead, they should have reflected the navigation that triggered the revalidation (as the `form*` parameters did). These parameters now correctly reflect the triggering navigation.\n- Fix bug with search params removal via `useSearchParams` ([#9969](https://github.com/remix-run/react-router/pull/9969))\n- Respect `preventScrollReset` on `<fetcher.Form>` ([#9963](https://github.com/remix-run/react-router/pull/9963))\n- Fix navigation for hash routers on manual URL changes ([#9980](https://github.com/remix-run/react-router/pull/9980))\n- Use `pagehide` instead of `beforeunload` for `<ScrollRestoration>`. This has better cross-browser support, specifically on Mobile Safari. ([#9945](https://github.com/remix-run/react-router/pull/9945))\n- Do not short circuit on hash change only mutation submissions ([#9944](https://github.com/remix-run/react-router/pull/9944))\n- Remove `instanceof` check from `isRouteErrorResponse` to avoid bundling issues on the server ([#9930](https://github.com/remix-run/react-router/pull/9930))\n- Detect when a `defer` call only contains critical data and remove the `AbortController` ([#9965](https://github.com/remix-run/react-router/pull/9965))\n- Send the name as the value when url-encoding `File` `FormData` entries ([#9867](https://github.com/remix-run/react-router/pull/9867))\n- `react-router-dom-v5-compat` - Fix SSR `useLayoutEffect` `console.error` when using `CompatRouter` ([#9820](https://github.com/remix-run/react-router/pull/9820))\n\n**Full Changelog**: [`v6.7.0...v6.8.0`](https://github.com/remix-run/react-router/compare/react-router@6.7.0...react-router@6.8.0)\n\n## v6.7.0\n\nDate: 2023-01-18\n\n### Minor Changes\n\n- Add `unstable_useBlocker`/`unstable_usePrompt` hooks for blocking navigations within the app's location origin ([#9709](https://github.com/remix-run/react-router/pull/9709), [#9932](https://github.com/remix-run/react-router/pull/9932))\n- Add `preventScrollReset` prop to `<Form>` ([#9886](https://github.com/remix-run/react-router/pull/9886))\n\n### Patch Changes\n\n- Added pass-through event listener options argument to `useBeforeUnload` ([#9709](https://github.com/remix-run/react-router/pull/9709))\n- Fix `generatePath` when optional params are present ([#9764](https://github.com/remix-run/react-router/pull/9764))\n- Update `<Await>` to accept `ReactNode` as children function return result ([#9896](https://github.com/remix-run/react-router/pull/9896))\n- Improved absolute redirect url detection in actions/loaders ([#9829](https://github.com/remix-run/react-router/pull/9829))\n- Fix URL creation with memory histories ([#9814](https://github.com/remix-run/react-router/pull/9814))\n- Fix scroll reset if a submission redirects ([#9886](https://github.com/remix-run/react-router/pull/9886))\n- Fix 404 bug with same-origin absolute redirects ([#9913](https://github.com/remix-run/react-router/pull/9913))\n- Streamline `jsdom` bug workaround in tests ([#9824](https://github.com/remix-run/react-router/pull/9824))\n\n**Full Changelog**: [`v6.6.2...v6.7.0`](https://github.com/remix-run/react-router/compare/react-router@6.6.2...react-router@6.7.0)\n\n## v6.6.2\n\nDate: 2023-01-09\n\n### Patch Changes\n\n- Ensure `useId` consistency during SSR ([#9805](https://github.com/remix-run/react-router/pull/9805))\n\n**Full Changelog**: [`v6.6.1...v6.6.2`](https://github.com/remix-run/react-router/compare/react-router@6.6.1...react-router@6.6.2)\n\n## v6.6.1\n\nDate: 2022-12-23\n\n### Patch Changes\n\n- Include submission info in `shouldRevalidate` on action redirects ([#9777](https://github.com/remix-run/react-router/pull/9777), [#9782](https://github.com/remix-run/react-router/pull/9782))\n- Reset `actionData` on action redirect to current location ([#9772](https://github.com/remix-run/react-router/pull/9772))\n\n**Full Changelog**: [`v6.6.0...v6.6.1`](https://github.com/remix-run/react-router/compare/react-router@6.6.0...react-router@6.6.1)\n\n## v6.6.0\n\nDate: 2022-12-21\n\n### What's Changed\n\nThis minor release is primarily to stabilize our SSR APIs for Data Routers now that we've wired up the new `RouterProvider` in Remix as part of the [React Router-ing Remix](https://remix.run/blog/react-routering-remix) work.\n\n### Minor Changes\n\n- Remove `unstable_` prefix from `createStaticHandler`/`createStaticRouter`/`StaticRouterProvider` ([#9738](https://github.com/remix-run/react-router/pull/9738))\n- Add `useBeforeUnload()` hook ([#9664](https://github.com/remix-run/react-router/pull/9664))\n\n### Patch Changes\n\n- Support uppercase `<Form method>` and `useSubmit` method values ([#9664](https://github.com/remix-run/react-router/pull/9664))\n- Fix `<button formmethod>` form submission overriddes ([#9664](https://github.com/remix-run/react-router/pull/9664))\n- Fix explicit `replace` on submissions and `PUSH` on submission to new paths ([#9734](https://github.com/remix-run/react-router/pull/9734))\n- Prevent `useLoaderData` usage in `errorElement` ([#9735](https://github.com/remix-run/react-router/pull/9735))\n- Proper hydration of `Error` objects from `StaticRouterProvider` ([#9664](https://github.com/remix-run/react-router/pull/9664))\n- Skip initial scroll restoration for SSR apps with `hydrationData` ([#9664](https://github.com/remix-run/react-router/pull/9664))\n- Fix a few bugs where loader/action data wasn't properly cleared on errors ([#9735](https://github.com/remix-run/react-router/pull/9735))\n\n**Full Changelog**: [`v6.5.0...v6.6.0`](https://github.com/remix-run/react-router/compare/react-router@6.5.0...react-router@6.6.0)\n\n## v6.5.0\n\nDate: 2022-12-16\n\n### What's Changed\n\nThis release introduces support for [Optional Route Segments](https://github.com/remix-run/react-router/issues/9546). Now, adding a `?` to the end of any path segment will make that entire segment optional. This works for both static segments and dynamic parameters.\n\n**Optional Params Examples**\n\n- `<Route path=\":lang?/about>` will match:\n  - `/:lang/about`\n  - `/about`\n- `<Route path=\"/multistep/:widget1?/widget2?/widget3?\">` will match:\n  - `/multistep`\n  - `/multistep/:widget1`\n  - `/multistep/:widget1/:widget2`\n  - `/multistep/:widget1/:widget2/:widget3`\n\n**Optional Static Segment Example**\n\n- `<Route path=\"/home?\">` will match:\n  - `/`\n  - `/home`\n- `<Route path=\"/fr?/about\">` will match:\n  - `/about`\n  - `/fr/about`\n\n### Minor Changes\n\n- Allows optional routes and optional static segments ([#9650](https://github.com/remix-run/react-router/pull/9650))\n\n### Patch Changes\n\n- Stop incorrectly matching on partial named parameters, i.e. `<Route path=\"prefix-:param\">`, to align with how splat parameters work. If you were previously relying on this behavior then it's recommended to extract the static portion of the path at the `useParams` call site: ([#9506](https://github.com/remix-run/react-router/pull/9506))\n\n```jsx\n// Old behavior at URL /prefix-123\n<Route path=\"prefix-:id\" element={<Comp /> }>\n\nfunction Comp() {\n  let params = useParams(); // { id: '123' }\n  let id = params.id; // \"123\"\n  ...\n}\n\n// New behavior at URL /prefix-123\n<Route path=\":id\" element={<Comp /> }>\n\nfunction Comp() {\n  let params = useParams(); // { id: 'prefix-123' }\n  let id = params.id.replace(/^prefix-/, ''); // \"123\"\n  ...\n}\n```\n\n- Persist `headers` on `loader` `request`'s after SSR document `action` request ([#9721](https://github.com/remix-run/react-router/pull/9721))\n- Fix requests sent to revalidating loaders so they reflect a GET request ([#9660](https://github.com/remix-run/react-router/pull/9660))\n- Fix issue with deeply nested optional segments ([#9727](https://github.com/remix-run/react-router/pull/9727))\n- GET forms now expose a submission on the loading navigation ([#9695](https://github.com/remix-run/react-router/pull/9695))\n- Fix error boundary tracking for multiple errors bubbling to the same boundary ([#9702](https://github.com/remix-run/react-router/pull/9702))\n\n**Full Changelog**: [`v6.4.5...v6.5.0`](https://github.com/remix-run/react-router/compare/react-router@6.4.5...react-router@6.5.0)\n\n## v6.4.5\n\nDate: 2022-12-07\n\n### Patch Changes\n\n- Fix requests sent to revalidating loaders so they reflect a `GET` request ([#9680](https://github.com/remix-run/react-router/pull/9680))\n- Remove `instanceof Response` checks in favor of `isResponse` ([#9690](https://github.com/remix-run/react-router/pull/9690))\n- Fix `URL` creation in Cloudflare Pages or other non-browser-environments ([#9682](https://github.com/remix-run/react-router/pull/9682), [#9689](https://github.com/remix-run/react-router/pull/9689))\n- Add `requestContext` support to static handler `query`/`queryRoute` ([#9696](https://github.com/remix-run/react-router/pull/9696))\n  - Note that the unstable API of `queryRoute(path, routeId)` has been changed to `queryRoute(path, { routeId, requestContext })`\n\n**Full Changelog**: [`v6.4.4...v6.4.5`](https://github.com/remix-run/react-router/compare/react-router@6.4.4...react-router@6.4.5)\n\n## v6.4.4\n\nDate: 2022-11-30\n\n### Patch Changes\n\n- Throw an error if an `action`/`loader` function returns `undefined` as revalidations need to know whether the loader has previously been executed. `undefined` also causes issues during SSR stringification for hydration. You should always ensure your `loader`/`action` returns a value, and you may return `null` if you don't wish to return anything. ([#9511](https://github.com/remix-run/react-router/pull/9511))\n- Properly handle redirects to external domains ([#9590](https://github.com/remix-run/react-router/pull/9590), [#9654](https://github.com/remix-run/react-router/pull/9654))\n- Preserve the HTTP method on 307/308 redirects ([#9597](https://github.com/remix-run/react-router/pull/9597))\n- Support `basename` in static data routers ([#9591](https://github.com/remix-run/react-router/pull/9591))\n- Enhanced `ErrorResponse` bodies to contain more descriptive text in internal 403/404/405 scenarios\n- Fix issues with encoded characters in `NavLink` and descendant `<Routes>` ([#9589](https://github.com/remix-run/react-router/pull/9589), [#9647](https://github.com/remix-run/react-router/pull/9647))\n- Properly serialize/deserialize `ErrorResponse` instances when using built-in hydration ([#9593](https://github.com/remix-run/react-router/pull/9593))\n- Support `basename` in static data routers ([#9591](https://github.com/remix-run/react-router/pull/9591))\n- Updated dependencies:\n  - `@remix-run/router@1.0.4`\n  - `react-router@6.4.4`\n\n**Full Changelog**: [`v6.4.3...v6.4.4`](https://github.com/remix-run/react-router/compare/react-router-dom@6.4.3...react-router-dom@6.4.4)\n\n## v6.4.3\n\nDate: 2022-11-01\n\n### Patch Changes\n\n- Generate correct `<a href>` values when using `createHashRouter` ([#9409](https://github.com/remix-run/react-router/pull/9409))\n- Better handle encoding/matching with special characters in URLs and route paths ([#9477](https://github.com/remix-run/react-router/pull/9477), [#9496](https://github.com/remix-run/react-router/pull/9496))\n- Generate correct `formAction` pathnames when an `index` route also has a `path` ([#9486](https://github.com/remix-run/react-router/pull/9486))\n- Respect `relative=path` prop on `NavLink` ([#9453](https://github.com/remix-run/react-router/pull/9453))\n- Fix `NavLink` behavior for root urls ([#9497](https://github.com/remix-run/react-router/pull/9497))\n- `useRoutes` should be able to return `null` when passing `locationArg` ([#9485](https://github.com/remix-run/react-router/pull/9485))\n- Fix `initialEntries` type in `createMemoryRouter` ([#9498](https://github.com/remix-run/react-router/pull/9498))\n- Support `basename` and relative routing in `loader`/`action` redirects ([#9447](https://github.com/remix-run/react-router/pull/9447))\n- Ignore pathless layout routes when looking for proper submission `action` function ([#9455](https://github.com/remix-run/react-router/pull/9455))\n- Add UMD build for `@remix-run/router` ([#9446](https://github.com/remix-run/react-router/pull/9446))\n- Fix `createURL` in local file execution in Firefox ([#9464](https://github.com/remix-run/react-router/pull/9464))\n\n**Full Changelog**: [`v6.4.2...v6.4.3`](https://github.com/remix-run/react-router/compare/react-router@6.4.2...react-router@6.4.3)\n\n## v6.4.2\n\nDate: 2022-10-06\n\n### Patch Changes\n\n- Respect `basename` in `useFormAction` ([#9352](https://github.com/remix-run/react-router/pull/9352))\n- Fix `IndexRouteObject` and `NonIndexRouteObject` types to make `hasErrorElement` optional ([#9394](https://github.com/remix-run/react-router/pull/9394))\n- Enhance console error messages for invalid usage of data router hooks ([#9311](https://github.com/remix-run/react-router/pull/9311))\n- If an index route has children, it will result in a runtime error. We have strengthened our `RouteObject`/`RouteProps` types to surface the error in TypeScript. ([#9366](https://github.com/remix-run/react-router/pull/9366))\n\n**Full Changelog**: [`v6.4.1...v6.4.2`](https://github.com/remix-run/react-router/compare/react-router@6.4.1...react-router@6.4.2)\n\n## v6.4.1\n\nDate: 2022-09-22\n\n### Patch Changes\n\n- Preserve state from `initialEntries` ([#9288](https://github.com/remix-run/react-router/pull/9288))\n- Preserve `?index` for fetcher get submissions to index routes ([#9312](https://github.com/remix-run/react-router/pull/9312))\n\n**Full Changelog**: [`v6.4.0...v6.4.1`](https://github.com/remix-run/react-router/compare/react-router@6.4.0...react-router@6.4.1)\n\n## v6.4.0\n\nDate: 2022-09-13\n\n### What's Changed\n\n#### Remix Data APIs\n\nWhoa this is a big one! `6.4.0` brings all the data loading and mutation APIs over from Remix. Here's a quick high level overview, but it's recommended you go check out the [docs](https://reactrouter.com/), especially the [feature overview](https://reactrouter.com/en/6.4.0/start/overview) and the [tutorial](https://reactrouter.com/en/6.4.0/start/tutorial).\n\n**New `react-router` APIs**\n\n- Create your router with `createMemoryRouter`\n- Render your router with `<RouterProvider>`\n- Load data with a Route `loader` and mutate with a Route `action`\n- Handle errors with Route `errorElement`\n- Defer non-critical data with `defer` and `Await`\n\n**New `react-router-dom` APIs**\n\n- Create your router with `createBrowserRouter`/`createHashRouter`\n- Submit data with the new `<Form>` component\n- Perform in-page data loads and mutations with `useFetcher()`\n- Defer non-critical data with `defer` and `Await`\n- Manage scroll position with `<ScrollRestoration>`\n- Perform path-relative navigations with `<Link relative=\"path\">` ([#9160](https://github.com/remix-run/react-router/pull/9160))\n\n### Patch Changes\n\n- Path resolution is now trailing slash agnostic ([#8861](https://github.com/remix-run/react-router/pull/8861))\n- `useLocation` returns the scoped location inside a `<Routes location>` component ([#9094](https://github.com/remix-run/react-router/pull/9094))\n- Respect the `<Link replace>` prop if it is defined ([#8779](https://github.com/remix-run/react-router/pull/8779))\n\n**Full Changelog**: [`v6.3.0...v6.4.0`](https://github.com/remix-run/react-router/compare/v6.3.0...react-router%406.4.0)\n\n## v6.3.0\n\nDate: 2022-03-31\n\n### Minor Changes\n\n- Added the v5 to v6 backwards compatibility package 💜 ([#8752](https://github.com/remix-run/react-router/pull/8752)). The official guide can be found [in this discussion](https://github.com/remix-run/react-router/discussions/8753)\n\n**Full Changelog**: [`v6.2.2...v6.3.0`](https://github.com/remix-run/react-router/compare/v6.2.2...v6.3.0)\n\n## v6.2.2\n\nDate: 2022-02-28\n\n### Patch Changes\n\n- Fixed nested splat routes that begin with special URL-safe characters ([#8563](https://github.com/remix-run/react-router/pull/8563))\n- Fixed a bug where index routes were missing route context in some cases ([#8497](https://github.com/remix-run/react-router/pull/8497))\n\n**Full Changelog**: [`v6.2.1...v6.2.2`](https://github.com/remix-run/react-router/compare/v6.2.1...v6.2.2)\n\n## v6.2.1\n\nDate: 2021-12-17\n\n### Patch Changes\n\n- This release updates the internal `history` dependency to `5.2.0`.\n\n**Full Changelog**: [`v6.2.0...v6.2.1`](https://github.com/remix-run/react-router/compare/v6.2.0...v6.2.1)\n\n## v6.2.0\n\nDate: 2021-12-17\n\n### Minor Changes\n\n- We now use statically analyzable CJS exports. This enables named imports in Node ESM scripts ([See the commit](https://github.com/remix-run/react-router/commit/29c7fc8b5f853b0b06ecd0f5682a9bbe6eca0715)).\n\n### Patch Changes\n\n- Fixed the `RouteProps` `element` type, which should be a `ReactNode` ([#8473](https://github.com/remix-run/react-router/pull/8473))\n- Fixed a bug with `useOutlet` for top-level routes ([#8483](https://github.com/remix-run/react-router/pull/8483))\n\n**Full Changelog**: [`v6.1.1...v6.2.0`](https://github.com/remix-run/react-router/compare/v6.1.1...v6.2.0)\n\n## v6.1.1\n\nDate: 2021-12-11\n\n### Patch Changes\n\n- In v6.1.0 we inadvertently shipped a new, undocumented API that will likely introduce bugs ([#7586](https://github.com/remix-run/react-router/pull/7586)). We have flagged `HistoryRouter` as `unstable_HistoryRouter`, as this API will likely need to change before a new major release.\n\n**Full Changelog**: [`v6.1.0...v6.1.1`](https://github.com/remix-run/react-router/compare/v6.1.0...v6.1.1)\n\n## v6.1.0\n\nDate: 2021-12-10\n\n### Minor Changes\n\n- `<Outlet>` can now receive a `context` prop. This value is passed to child routes and is accessible via the new `useOutletContext` hook. See [the API docs](https://reactrouter.com/docs/en/v6/api#useoutletcontext) for details. ([#8461](https://github.com/remix-run/react-router/pull/8461))\n- `<NavLink>` can now receive a child function for access to its props. ([#8164](https://github.com/remix-run/react-router/pull/8164))\n- Improved TypeScript signature for `useMatch` and `matchPath`. For example, when you call `useMatch(\"foo/:bar/:baz\")`, the path is parsed and the return type will be `PathMatch<\"bar\" | \"baz\">`. ([#8030](https://github.com/remix-run/react-router/pull/8030))\n\n### Patch Changes\n\n- Fixed a bug that broke support for base64 encoded IDs on nested routes ([#8291](https://github.com/remix-run/react-router/pull/8291))\n- A few error message improvements ([#8202](https://github.com/remix-run/react-router/pull/8202))\n\n**Full Changelog**: [`v6.0.2...v6.1.0`](https://github.com/remix-run/react-router/compare/v6.0.2...v6.1.0)\n\n## v6.0.2\n\nDate: 2021-11-09\n\n### Patch Changes\n\n- Added the `reloadDocument` prop to `<Link>`. This allows `<Link>` to function like a normal anchor tag by reloading the document after navigation while maintaining the relative `to` resolution ([#8283](https://github.com/remix-run/react-router/pull/8283))\n\n**Full Changelog**: [`v6.0.1...v6.0.2`](https://github.com/remix-run/react-router/compare/v6.0.1...v6.0.2)\n\n## v6.0.1\n\nDate: 2021-11-05\n\n### Patch Changes\n\n- Add a default `<StaticRouter location>` value ([#8243](https://github.com/remix-run/react-router/pull/8243))\n- Add invariant for using `<Route>` inside `<Routes>` to help people make the change ([#8238](https://github.com/remix-run/react-router/pull/8238))\n\n**Full Changelog**: [`v6.0.0...v6.0.1`](https://github.com/remix-run/react-router/compare/v6.0.0...v6.0.1)\n\n## v6.0.0\n\nDate: 2021-11-03\n\nReact Router v6 is here!\n\nPlease go read [our blog post for more information on all the great stuff in v6](https://remix.run/blog/react-router-v6) including [notes about how to upgrade from React Router v5](https://remix.run/blog/react-router-v6#upgrading-to-react-router-v6) and Reach Router.\n"
  },
  {
    "path": "CLA.md",
    "content": "Remix Software, Inc. Individual Contributor License Agreement (\"Agreement\"), v2.0\n\nYou accept and agree to the following terms and conditions for Your present and future Contributions submitted to Remix Software, Inc. (\"Remix\"). Except for the license granted herein to Remix and recipients of software distributed by Remix, You reserve all right, title, and interest in and to Your Contributions.\n\n1. Definitions.\n\n\"You\" (or \"Your\") shall mean the copyright owner or legal entity authorized by the copyright owner that is making this Agreement with Remix. For legal entities, the entity making a Contribution and all other entities that control, are controlled by, or are under common control with that entity are considered to be a single Contributor. For the purposes of this definition, \"control\" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity.\n\n\"Contribution\" shall mean any original work of authorship, including any modifications or additions to an existing work, that is intentionally submitted by You to Remix for inclusion in, or documentation of, any of the products owned or managed by Remix (the \"Work\"). For the purposes of this definition, \"submitted\" means any form of electronic, verbal, or written communication sent to Remix or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, Remix for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by You as \"Not a Contribution.\"\n\n2. Grant of Copyright License. Subject to the terms and conditions of this Agreement, You hereby grant to Remix and to recipients of software distributed by Remix a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare derivative works of, publicly display, publicly perform, sublicense, and distribute Your Contributions and such derivative works.\n\n3. Grant of Patent License. Subject to the terms and conditions of this Agreement, You hereby grant to Remix and to recipients of software distributed by Remix a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by You that are necessarily infringed by Your Contribution(s) alone or by combination of Your Contribution(s) with the Work to which such Contribution(s) was submitted. If any entity institutes patent litigation against You or any other entity (including a cross-claim or counterclaim in a lawsuit) alleging that your Contribution, or the Work to which you have contributed, constitutes direct or contributory patent infringement, then any patent licenses granted to that entity under this Agreement for that Contribution or Work shall terminate as of the date such litigation is filed.\n\n4. You represent that you are legally entitled to grant the above license. If your employer(s) has rights to intellectual property that you create that includes your Contributions, you represent that you have received permission to make Contributions on behalf of that employer, that your employer has waived such rights for your Contributions to Remix, or that your employer has executed a separate Corporate CLA with Remix.\n\n5. You represent that each of Your Contributions is Your original creation (see section 7 for submissions on behalf of others). You represent that Your Contribution submissions include complete details of any third-party license or other restriction (including, but not limited to, related patents and trademarks) of which you are personally aware and which are associated with any part of Your Contributions.\n\n6. You are not expected to provide support for Your Contributions, except to the extent You desire to provide support. You may provide support for free, for a fee, or not at all. Unless required by applicable law or agreed to in writing, You provide Your Contributions on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON- INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE.\n\n7. Should You wish to submit work that is not Your original creation, You may submit it to Remix separately from any Contribution, identifying the complete details of its source and of any license or other restriction (including, but not limited to, related patents, trademarks, and license agreements) of which you are personally aware, and conspicuously marking the work as \"Submitted on behalf of a third-party: [named here]\".\n\n8. You agree to notify Remix of any facts or circumstances of which you become aware that would make these representations inaccurate in any respect.\n"
  },
  {
    "path": "CODE_OF_CONDUCT.md",
    "content": "## Our Pledge\n\nIn the interest of fostering an open and welcoming environment, we as members, contributors, and leaders pledge to make participation in our community a harassment-free experience for everyone, regardless of age, body size, visible or invisible disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, caste, color, religion, or sexual identity and orientation.\n\nWe pledge to act and interact in ways that contribute to an open, welcoming, diverse, inclusive, and healthy community.\n\n## Our Standards\n\nExamples of behavior that contributes to a positive environment for our community include:\n\n- Using welcoming and inclusive language\n- Demonstrating empathy and kindness toward other people\n- Being respectful of differing opinions, viewpoints, and experiences\n- Giving and gracefully accepting constructive feedback\n- Accepting responsibility and apologizing to those affected by our mistakes, and learning from the experience\n- Focusing on what is best not just for us as individuals, but for the overall community\n\nExamples of unacceptable behavior include:\n\n- The use of sexualized language or imagery, and unwelcome sexual attention or advances\n- Trolling, insulting or derogatory comments, and personal or political attacks\n- Public or private harassment\n- Publishing others’ private information, such as a physical or email address, without their explicit permission\n- Other conduct which could reasonably be considered inappropriate in a professional setting\n\n## Our Responsibilities\n\nCommunity leaders and project maintainers are responsible for clarifying and enforcing our standards of acceptable behavior and will take appropriate and fair corrective action in response to any behavior that they deem inappropriate, threatening, offensive, or harmful.\n\nCommunity leaders have the right and responsibility to remove, edit, or reject comments, commits, code, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful.\n\n## Scope\n\nThis Code of Conduct applies within all community spaces, and also applies when an individual is officially representing the community in public spaces. Examples of representing our community include using an official e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event.\n\n## Enforcement\n\nInstances of abusive, harassing, or otherwise unacceptable behavior may be reported to the community leaders responsible for enforcement at [brooks.lybrand@shopify.com](mailto:brooks.lybrand@shopify.com). All complaints will be reviewed and investigated promptly and fairly.\n\nProject maintainers and community leaders are obligated to respect the privacy and security of the reporter of any incident.\n\n## Attribution\n\nThis Code of Conduct is adapted from the [Contributor Covenant](https://www.contributor-covenant.org/), version 2.1, available at [https://www.contributor-covenant.org/version/2/1/code_of_conduct.html](https://www.contributor-covenant.org/version/2/1/code_of_conduct.html).\n\nFor answers to common questions about this code of conduct, see the FAQ at [https://www.contributor-covenant.org/faq](https://www.contributor-covenant.org/faq). Translations are available at [https://www.contributor-covenant.org/translations](https://www.contributor-covenant.org/translations).\n"
  },
  {
    "path": "CONTRIBUTING.md",
    "content": "Please see [our guide to contributing](docs/community/contributing.md).\n"
  },
  {
    "path": "DEVELOPMENT.md",
    "content": "# React Router Development\n\n## Releases\n\nNew 7.x releases should be created from release branches originating from the `dev` branch. If you are doing a 6.x release, please see the [v6 section](#v6-releases) below.\n\nWhen you are ready to begin the release process:\n\n- Make sure you've pulled all the changes from GitHub for both `dev` and `main` branches\n  - `git checkout main && git pull origin main`\n  - `git checkout dev && git pull origin dev`\n- Check out the `dev` branch\n  - `git checkout dev`\n- Create a new `release-next` branch\n  - `git checkout -b release-next`\n  - Technically, any `release-*` branch name will work as this is what triggers our GitHub CI workflow that will ultimately publish the release - but we just always use `release-next`\n  - We are using `release-v6` for [ongoing v6 releases](#v6-releases)\n- Merge `main` into the `release-next` branch\n  - `git merge --no-ff main`\n\nChangesets will do most of the heavy lifting for our releases. When changes are made to the codebase, an accompanying changeset file should be included to document the change. Those files will dictate how Changesets will version our packages and what shows up in the changelogs.\n\n### Starting a new pre-release\n\n- Ensure you are on the new `release-next` branch\n  - `git checkout release-next`\n- Enter Changesets pre-release mode using the `pre` tag:\n  - `pnpm changeset pre enter pre`\n- Commit the change and push the `release-next` branch to GitHub\n  - `git commit -a -m \"Enter prerelease mode\"`\n  - `git push --set-upstream origin release-next`\n- Wait for the changesets CI workflow to finish which will open a PR pointed to `release-next` that will increment all versions and generate the changelogs\n- If you need/want to make any changes to the `CHANGELOG.md` files, you can do so and commit directly to the PR branch\n  - This is usually not required for prereleases\n- Once the changesets files are in good shape, merge the PR to `release-next`\n- Once the PR is merged, the release workflow will publish the updated `X.Y.Z-pre.*` packages to npm\n\n### Prepare the draft release notes\n\n- At this point, you can begin crafting the release notes for the eventual stable release in the root `CHANGELOG.md` file in the repo\n  - Copy the commented out template for a new release and update the version numbers and links accordingly\n  - Copy the relevant changelog entries from all packages into the release notes and adjust accordingly\n    - `find packages -name 'CHANGELOG.md' -mindepth 2 -maxdepth 2 -exec code {} \\;`\n  - Commit these changes directly to the `release-next` branch - they will not trigger a new prerelease since they do not include a changeset\n\n### Iterating a pre-release\n\nYou may need to make changes to a pre-release prior to publishing a final stable release. To do so:\n\n- Branch off of `release-next` and make whatever changes you need\n- Create a new changeset: `pnpm changeset`\n  - **IMPORTANT:** This is required even if you ultimately don't want to include these changes in the logs. Remember, changelogs can be edited prior to publishing, but the Changeset version script needs to see new changesets in order to create a new version\n- Push your branch to GitHub and PR it to `release-next`\n- Once reviewed/approved, merge the PR to the `release-next` branch\n- Wait for the release workflow to finish and the Changesets action to open its PR that will increment all versions\n  - Note: If more changes are needed you can just merge them to `release-next` and this PR will automatically update in place\n- Review the PR, make any adjustments necessary, and merge it into the `release-next` branch\n- Once the PR is merged, the release workflow will publish the updated `X.Y.Z-pre.*` packages to npm\n- Make sure you copy over the new changeset contents into stable release notes in the root `CHANGELOG.md` file in the repo\n\n### Publishing the stable release\n\n- Exit Changesets pre-release mode in the `release-next` branch:\n  - `pnpm changeset pre exit`\n- Commit the edited pre-release file along with any unpublished changesets, and push the `release-next` branch to GitHub\n- Wait for the release workflow to finish - the Changesets action in the workflow will open a PR that will increment all versions and generate the changelogs for the stable release\n- Review the updated `CHANGELOG` files in the PR and make any adjustments necessary\n  - `find packages -name 'CHANGELOG.md' -mindepth 2 -maxdepth 2 -exec code {} \\;`\n  - Our automated release process should have removed prerelease entries\n- Finalize the release notes\n  - This should already be in pretty good shape in the root `CHANGELOG.md` file in the repo because changes have been added with each prerelease\n  - Do a quick double check that all iterated prerelease changesets got copied over\n- Merge the PR into the `release-next` branch\n- Once the PR is merged, the release workflow will publish the updated packages to npm\n- Once the release is published:\n  - Pull the latest `release-next` branch containing the PR you just merged\n  - Merge the `release-next` branch into `main` **using a non-fast-forward merge** and push it up to GitHub\n    - `git checkout main`\n    - `git merge --no-ff release-next`\n    - `git push origin main`\n    - _Note:_ For the `v7.0.0` stable release, there will probably be a bunch of conflicts on `docs/**/*.md` files here because we have made changes to v6 docs but in `dev` we removed a lot of those files in favor of auto-generated API docs. To resolve those conflicts, we should accept the deletion from the `release-next` branch.\n  - Merge the `release-next` branch into `dev` **using a non-fast-forward merge** and push it up to GitHub\n    - `git checkout dev`\n    - `git merge --no-ff release-next`\n    - `git push origin dev`\n  - Convert the `react-router@6.x.y` tag to a Release on GitHub with the name `v6.x.y` and add a deep-link to the release heading in `CHANGELOG.md`\n  - Delete the `release-next` branch locally and on GitHub\n\n### Hotfix releases\n\nHotfix releases follow the same process as standard releases above, but the `release-next` branch should be branched off latest `main` instead of `dev`. Once the stable hotfix is published, the `release-next` branch should be merged back into both `main` and `dev` just like a normal release.\n\n### v6 releases\n\n6.x releases are managed in a similar process to the above but from the `v6` branch, and they do not automatically merge changes back to `dev`/`main`.\n\n- Changes for 6.x should be PR'd to the `v6` branch with a changeset\n- If these changes should also be applied to v7, cherry-pick or re-do those changes against the `dev` branch (including the changeset). These changes will make it to `main` with the next v7 release.\n- Starting the release process for 6.x is the same as outlined above, with a few exceptioins:\n  - Branch from `v6` instead of `dev`\n  - Use `release-v6` instead of `release-next`\n  - Do **not** merge `main` into `release-v6`\n- Steps:\n  - `git checkout v6 && git pull origin v6`\n  - `git checkout -b release-v6`\n  - `pnpm changeset pre enter pre-v6`\n  - The process of the PRs and iterating on prereleases remains the same\n- Once the stable release is out:\n  - Merge `release-v6` back to `v6` with a **Normal Merge**\n  - **Do not** merge `release-v6` to `main`\n  - Manually copy the new root `CHANGELOG.md` entry to `main` and `dev`\n    - We don't worry about backporting individual `packages/*/CHANGELOG.md` updates to `main` for subsequent v6 releases\n  - The _code_ changes should already be in the `dev` branch\n    - This should have happened at the time the v6 change was made (except for changes such as deprecation warnings)\n    - Confirm that the commits in this release are all included in `dev` already, and if not you can manually bring them over by cherry-picking the commit or re-doing the work\n\n### Experimental releases\n\nExperimental releases and hot-fixes do not need to be branched off of `dev`. Experimental releases can be branched from anywhere as they are not intended for general use.\n\n- Create a new branch for the release: `git checkout -b release-experimental`\n- Make whatever changes you need and commit them: `git add . && git commit \"experimental changes!\"`\n- Update version numbers and create a release tag: `pnpm run version:experimental`\n- Push to GitHub: `git push origin --follow-tags`\n- The CI workflow should automatically trigger from the experimental tag to publish the release to npm\n"
  },
  {
    "path": "GOVERNANCE.md",
    "content": "# React Router Open Governance Model <!-- omit in toc -->\n\n- [Overview](#overview)\n- [Design Goals](#design-goals)\n- [Steering Committee](#steering-committee)\n- [Bug/Issue Process](#bugissue-process)\n- [New Feature Process](#new-feature-process)\n- [New Feature Stages](#new-feature-stages)\n  - [Stage 0 — Proposal](#stage-0--proposal)\n  - [Stage 1 — Consideration](#stage-1--consideration)\n  - [Stage 2 — Alpha](#stage-2--alpha)\n  - [Stage 3 — Beta](#stage-3--beta)\n  - [Stage 4 — Stabilization](#stage-4--stabilization)\n  - [Stage 5 — Stable](#stage-5--stable)\n- [Meeting Notes](#meeting-notes)\n\n## Overview\n\nReact Router has been around since 2014 largely under the development and oversight of [Michael Jackson](https://x.com/mjackson) and [Ryan Florence](https://x.com/ryanflorence). After the launch of [Remix](https://remix.run/) in 2021, the subsequent creation of the Remix team, and the merging of Remix v2 into React Router v7[^1][^2], the project shifted from a [Founder-Leader](https://www.redhat.com/en/blog/understanding-open-source-governance-models) model to a \"Steering Committee\" (SC) model that operates on a Request for Comments (RFC) process.\n\n[^1]: https://remix.run/blog/merging-remix-and-react-router\n\n[^2]: https://remix.run/blog/incremental-path-to-react-19\n\nThis document will outline the process in which React Router will continue to evolve and how new features will make their way into the codebase. This is an evergreen document and will be updated as needed to reflect future changes in the process.\n\n## Design Goals\n\nThe following design goals should be considered when considering RFCs for acceptance:\n\n- **Less is More**. React Router has gained a _lot_ of functionality in the past years, but with that comes a bunch of new API surface. It's time to hone in on the core functionality and aim to reduce API surface _without sacrificing capabilities_. This may come in multiple forms, such as condensing a few existing APIs into a singular API, or deprecating current APIs in favor of a new React API.\n- **Routing and Data Focused.** Focus on core router-integrated/router-centric APIs and avoid adding first-class APIs that can be implemented in user-land\n- **Simple Migration Paths.** Major version upgrades don't have to stink. Breaking changes should be implemented behind future flags. Deprecations should be properly marked ahead of time in code and in documentation. Console warnings should be added prior to major releases to nudge developers towards the changes they can begin to make to prep for the upgrade.\n- **Lowest Common Mode.** Features are added at the lowest mode possible (`declarative -> data -> framework`) and then leveraged by the higher-level modes. This ensures that the largest number of React Router applications can leverage them.\n- **Regular Release Cadence**. Aim for major SemVer releases on a ~yearly basis so application developers can prepare in advance.\n\n## Steering Committee\n\nThe Steering Committee will be in charge of accepting RFC's for consideration, approving PRs to land features in an \"unstable\" state, and approving stabilization PRs to land PRs that stabilize features into React Router.\n\nThe SC will initially consist of the Remix team developers:\n\n- Matt Brophy ([`@brophdawg11`](https://github.com/brophdawg11))\n- Pedro Cattori ([`@pcattori`](https://github.com/pcattori))\n- Mark Dalgleish ([`@markdalgleish`](https://github.com/markdalgleish))\n- Jacob Ebey ([`@jacob-ebey`](https://github.com/jacob-ebey))\n- Brooks Lybrand ([`@brookslybrand`](https://github.com/brookslybrand))\n- Sergio Xalambrí ([`@sergiodxa`](https://github.com/sergiodxa))\n- Bryan Ross ([`@rossipedia`](https://github.com/rossipedia))\n\nIn the future, we may add a limited number of heavily involved community members to the SC as well.\n\nTo reduce friction, the SC will primarily operate asynchronously via GitHub, but private and/or public meetings may be scheduled as needed.\n\n## Bug/Issue Process\n\nDue to the large number of React Router applications out there, we have to be a bit strict on the process for filing issues to avoid an overload in GitHub.\n\n- **All** bugs must have a **minimal** and **runnable** reproduction [^3]\n  - _Minimal_ means that it is not just pointing to a deployed site or a branch in your existing application\n  - _Runnable_ means that it is a working application where we can see the issue, not just a few snippets of code that need to be manually reassembled into a running application\n  - The preferred methods for reproductions are:\n    - **Framework Mode**: [StackBlitz](https://reactrouter.com/new) or a GitHub fork with a failing integration test based on [`bug-report-test.ts`](integration/bug-report-test.ts)\n    - **Data/Declarative Modes**: [CodeSandbox (TS)](https://codesandbox.io/templates/react-vite-ts) or [CodeSandbox (JS)](https://codesandbox.io/templates/react-vite)\n  - If StackBlitz/CodeSandbox is not an option, a GitHub repo based on a fresh `npx create-react-router` app is acceptable\n  - Only in extraordinary circumstances will code snippets or maximal reproductions be accepted\n- Issue Review\n  - Issues not meeting the above criteria will be closed and pointed to this document\n  - Non-issues (feature requests, usage questions) will also be closed with a link to this document\n  - The SC will triage issues regularly\n- Fixing Issues\n  - The SC will mark good community issues with an `Accepting PRs` label\n  - These issues will generally be ones that are likely to have a small surface area fix\n  - However, anyone can work on any issue, but there's no guarantee the PR will be accepted if the surface area is too large for expedient review by a core team member\n\n[^3]: https://antfu.me/posts/why-reproductions-are-required\n\n## New Feature Process\n\nThe process for new features being added to React Router will follow a series of stages loosely based on the [TC39 Process](https://tc39.es/process-document/). It is important to note that entrance into any given stage does not imply that an RFC will proceed any further. The stages will act as a funnel with fewer RFCs making it into later stages such that only the strongest RFCs make it into a React Router release in a stable fashion.\n\n> [!NOTE]\n> Most new community-driven features for React Router will go through all stages. Some features, if trivial or obvious enough, may skip stages and be implemented directly as a stable feature.\n\nThis table gives a high-level overview of the stages, but please see the individual stage sections below for more detailed information on the stages and the process for moving an FC through them. Once a feature reaches Stage 2, it will be added to the [Roadmap](https://github.com/orgs/remix-run/projects/5) where it can be tracked as it moves through the stages.\n\n| Stage | Name          | Entrance Criteria                                                                                                                      | Purpose                                                                                                                                                                                                                                                                                                                                                                                                                                                                           |\n| ----- | ------------- | -------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| 0     | Proposal      | Proposal discussion opened on GitHub                                                                                                   | We start with a GitHub Proposal to provide the lowest barrier to RFC submission. Anyone can submit an RFC and community members can review, comment, up-vote without any initial involvement of the SC.                                                                                                                                                                                                                                                                           |\n| 1     | Consideration | Proposal acceptance from 2 SC members                                                                                                  | The consideration phase is the first \"funnel\" for incoming RFCs where the SC can officially express interest in the more popular RFCs. We only require 2 SC members to express interest to move an RFC into the **Consideration** phase to allow for low-friction experimentation of features in the **Alpha** stage.                                                                                                                                                             |\n| 2     | Alpha         | Pull request (PR) opened to implement the feature in an \"unstable\" state                                                               | The **Alpha** stage is the next funnel for RFCs. Once interest has been expressed by the SC in the **Consideration** phase we open the RFC up for a sample PR implementation and a mechanism for community members to alpha test the feature without requiring that anything be shipped in a React Router SemVer release. This stage allows evaluation of the RFC in running applications and consideration of what a practical implementation of the RFC looks like in the code. |\n| 3     | Beta          | PR approval from 2 SC members indicating their acceptance of the PR for an unstable API                                                | A RFC enters the **Beta** stage once enough members of the SC feel comfortable not only with the code for the beta feature, but have also seen positive feedback from alpha testers that the feature is working as expected. Once an **Alpha** stage PR has enough SC approvals, it will be merged and be included in the next React Router release.                                                                                                                              |\n| 4     | Stabilization | At least 1 month in the Beta stage and PR opened to stabilize the APIs. This PR should also include documentation for the new feature. | The **Stabilization** phase exists to ensure that unstable features are available for enough time for applications to update their React Router version and opt-into beta testing. We don't want to rush features through beta testing so that we have maximal feedback prior to stabilizing a feature.                                                                                                                                                                           |\n| 5     | Stable        | PR approval from at least 50% of the SC members indicating their acceptance of the PR for a stable API                                 | A RFC is completed and enters the **Stable** stage once enough members of the SC feel comfortable not only with the code for the stable feature, but have also seen positive feedback from beta testers that the feature is working as expected. Once an **Beta** stage PR has enough SC approvals and has spent the required amount of time in the **Beta** stage, it can be merged and included in the next React Router release.                                               |\n\n## New Feature Stages\n\n### Stage 0 — Proposal\n\n- All new features begin at **Stage 0 — Proposal** when a Request For Comments (RFC) is written up in a GitHub Proposal Discussion\n- Anyone can write an RFC, including core team members and community members\n- The RFC should outline the use-case for the new feature, why current APIs are insufficient for the use-case, and provide potential API surfaces for the feature\n- The proposal should be clear, concise, and provide enough context for the Steering Committee (SC) and community to evaluate its merit\n- Community upvotes on the proposal are used as a signal of interest and demand for the SC — higher upvoted issues are more likely to be considered by the SC members\n- At this stage, community members may feel free to work on sample implementations in a fork of the repo and provide links in the RFC, but a pull request **should not** be opened until it reaches Stage 1\n\n### Stage 1 — Consideration\n\n- A proposal enters **Stage 1 — Consideration** when 2 SC members indicate interest/support for the idea as a valuable addition to React Router\n- These initial supporting SC members will be the champions for the feature and will be loosely responsible for shepherding the feature through the stages of the RFC process\n- At this stage, the proposal is eligible for a sample PR implementation from a core team or community member\n- The SC will indicate at this stage if this is a feature open to a community PR or something the core team would prefer to tackle\n- We will add the `accepting-prs` label to the RFC if we are open to community PRs\n- All PRs at this stage should implement the feature in an \"unstable\" fashion (usually using an `unstable_` prefix on the future flag or API)\n\n### Stage 2 — Alpha\n\n- A proposal enters **Stage 2 — Alpha** once a PR has been opened implementing the feature in an `unstable_` state\n- At this stage, we should open an Issue for the Proposal and add it to the [Roadmap](https://github.com/orgs/remix-run/projects/5)\n- We will remove any `accepting-prs` label and add the `🗺️ Roadmap` label to indicate that this RFc is officially on the roadmap\n- At this stage, we are looking for early community testing _before_ merging any work to the React Router repo — so these PRs should provide a mechanism for community members to opt into to alpha testing\n  - Maintainers can trigger an alpha release from the PR branch by adding the `alpha-release` label, which will kick off an experimental release and comment it back on the PR\n  - Because the alpha release may contain other work committed to `dev` but not yet released in a stable version, it may not be ideal for testing in all cases\n  - In these cases, PR authors may also add the contents for a `.patch` file in a comment that folks can use via [patch-package](https://www.npmjs.com/package/patch-package) or [pnpm patch](https://pnpm.io/cli/patch)\n- Feedback from alpha testers is considered essential for further progress\n- The PR should also contain a changeset documenting the new API for the release notes\n- SC members will review and approve the PR via GitHub reviews\n- Approval at this stage communicates:\n  - The feature is valuable for React Router\n  - The API/code is sufficient for unstable/beta testing, though further iteration may be needed\n  - Code is not required to be in a final state yet, but it must be coded in such a way so as not to introduce regressions to other areas of the API\n  - We have seen enough positive feedback from Alpha testers to move the feature along\n\n### Stage 3 — Beta\n\n- A proposal enters **Stage 3 — Beta** once it receives **Stage 2 — Alpha** PR approvals from 2 SC members and is merged to `dev`\n  - An SC member authoring the `unstable_` PR counts as an implicit approval, so in those cases explicit approval is required from 1 additional SC member\n- This will include the feature in `nightly` releases and the next normal SemVer release for broader beta testing under the `unstable_` flag\n\n### Stage 4 — Stabilization\n\n- A proposal enters **Stage 4 — Stabilization** after a minimum of 1 month in **Stage 3 — Beta** and a PR has been opened to remove the `unstable_` prefixes and stabilize the feature\n- Stabilization PRs should add proper documentation for the feature\n- SC members will review and approve the PR via GitHub reviews\n- Approval at this stage communicates:\n  - Sufficient community feedback has been received from beta testers to trust the API's design and implementation\n  - The code is production-quality and well-tested, with no related regressions\n  - The PR includes documentation for the stable feature\n\n### Stage 5 — Stable\n\n- A proposal enters **Stage 5 — Stable** once it receives **Stage 4 — Stabilization** PR approvals from at least 50% of the SC members and is merged to `dev`\n  - An SC member authoring the stabilization PR counts as an implicit approval\n- This will include the stable feature in `nightly` releases and the next normal SemVer release\n\n## Meeting Notes\n\nThis section captures the notes from the React Router Steering Committee meetings:\n\n<!-- TEMPLATE\n<details>\n  <summary>YYYY-MM-DD Meeting Notes</summary>\n\n  ...\n</details>\n-->\n\n<details>\n<summary>2025-09-23 Meeting Notes</summary>\n\n**Summary**\n\nBrooks announced the planned release of unstable framework RSC support in 7.9.2 and the `fetcher.unstable_reset()` API. Matt and Pedro discussed splitting Ryan's proposal for `useRouteLoaderData` type-safety to separate \"router data\" from \"route data.\" Bryan and Matt reviewed the proposal for new instrumentation APIs. Matt and Jacob decided to close several issues related to ESLint configuration, OpenTelemetry, and module federation.\n\n**Details**\n\n- 7.9.2 will contain unstable support for RSC framework mode as well as the `fetcher.unstable_reset()` API\n- The team reviewed the current instrumentation POC implementation:\n  - RFC: https://github.com/remix-run/react-router/discussions/13749\n  - POC PR: https://github.com/remix-run/react-router/pull/14377\n  - Current `instrumentRouter`/`instrumentRoutes` APIs should be sufficient for various implementations of logging/tracing layered on top\n  - React Router docs can show simple examples of a few types of observability implementations (logging, OTEL, `performance.mark`/`measure`), but will lean on the community to provide packages for specific observability approaches\n  - Jacob raised a good point about the design of the current APIs permitting more than instrumentation because folks could mutate existing handler parameters, so Matt is going to look into ways top provide a subset of read-only information that will prohibit this since it is not an intended use case and would likely be abused in unforeseen ways\n  - Matt will also play around with potential instrumentation utils to see if it is worth shipping anything or just putting them in documentation\n- The committee reviewed and agreed to move forward 2 new RFCs to the \"consideration\" stage:\n  - [Prerender concurrency](https://github.com/remix-run/react-router/discussions/14080)\n  - [Per-route Layout component](https://github.com/remix-run/react-router/discussions/13818)\n- Matt will comment back on the ESLint issue to get it closed out and point to the OpenTelemetry issue to the new instrumentation approach\n- Pedro will start on the route stuff and try to get a PR up for it, once a PR is opened we will also get an issue on the roadmap\n- Jacob will check in with Zach about his interest in the [current work w.r.t. module imports](https://github.com/remix-run/react-router/pull/12638), and Matt will add a comment to the issue asking if it is needed and close it if there is no response in a week.\n\n</details>\n\n<details>\n<summary>2025-09-08 Meeting Notes</summary>\n\n**Summary**\n\nMatt, Bryan, Mark, and Pedro discussed the progress of various features, including middleware, context, the `onError` feature, and RSC framework mode, with most nearing completion or already released. Matt and Bryan also explored the integration of observability and OpenTelemetry with Sentry and React Router, considering OpenTelemetry as a potential standard for JavaScript monitoring. The team decided to focus on current in-progress items instead of reviewing and accepting additional proposals because there are already 10+ proposals in-progress.\n\n**Details**\n\n- Roadmap Review and Release Progress\n  - Matt initiated the meeting by reviewing the public roadmap, starting with [middleware](https://github.com/remix-run/react-router/issues/12695) and [context](https://github.com/remix-run/react-router/issues/14055), which are merged to dev and awaiting a pre-release for version 7.9.0\n  - Bryan confirmed that the [`onError`](https://github.com/remix-run/react-router/issues/12958) feature, released in 7.8.2, is working as expected and providing anticipated data\n  - Mark noted that the RSC framework mode initial release will not be feature complete but is nearing completion, with the main remaining task being error handling during rendering ([RFC](https://github.com/remix-run/react-router/issues/11566))\n- Upcoming Features and API Discussions\n  - Pedro discussed the `useRouterState` hook, noting that Ryan's attention is elsewhere, but they are interested in revisiting it for type safety and potentially replacing the `useRouteLoaderData` hook\n  - Brooks and Pedro agreed that the `useMatches` API is problematic, especially concerning type safety, and suggested finding a solution that does not rely on it\n  - We may be able to keep the distinction that hooks for use in data mode are less type-safe than the typegen equivalents in framework mode, so it might be ok for `useRouterState().matches` to be less type-safe than `Route.ComponentProps[\"matches\"]`\n  - [RFC](https://github.com/remix-run/react-router/issues/13073)\n- Observability and OpenTelemetry Integration\n  - Matt and Bryan discussed the [observability](https://github.com/remix-run/react-router/discussions/13749) feature, which aims to improve Sentry's integration with React Router Apps\n  - Bryan explained that a strict event-based system would not support OpenTelemetry because OpenTelemetry requires bounding code execution within a span, unlike events which are instantaneous\n  - They are considering whether React Router should fully embrace OpenTelemetry as it appears to be becoming a de facto standard for JavaScript monitoring, which could potentially replace the need for a separate event system\n- Meeting Wrap-up and Next Steps\n  - Matt announced that the pre-release for version 7.9.0 would be shipped shortly, with the full release expected by the end of the week\n  - Bryan confirmed that the duplicate loader issue fix will be included in this release\n  - The team decided not to overload themselves with additional tasks, focusing on the current in-progress items\n\n**Action Items**\n\n- Mark will work on stabilizing the split route modules and Vite environment API flags\n- Matt will read through the SvelteKit blog post to understand their approach to OpenTelemetry integration\n- Matt will merge the unstable [`fetcher.reset()`](https://github.com/remix-run/react-router/issues/14207) work after 7.9.0 is released ()\n- Matt will try to pick up the [`<Link onPrefetch>`](https://github.com/remix-run/react-router/discussions/12375) task soon\n- Matt and Pedro will sync up offline to figure out what parts of the consolidated hook can be done better with typegen and decide on the requirements ([RFC](https://github.com/remix-run/react-router/issues/13073))\n</details>\n\n<details>\n<summary>2025-11-04 Meeting Notes</summary>\n\nThe SC reviewed items on the open Proposal for React Router v8\n\n- Confirmed the plan to drop CJS builds for ESM-only builds\n  - We will plan RR v8 for Q2 2026 which aligns nicely with the EOL for Node 20\n  - v8 will have a minimum Node version of 22.12 so that the `require(esm)` feature is not behind an [experimental flag](https://nodejs.org/docs/latest-v22.x/api/modules.html#loading-ecmascript-modules-using-require)\n- Going forward we will aim for a yearly major release in the same Q2 timeframe\n- We would like to try to get `useRouterState` into v8 as the other half of the `unstable_useRoute` coin\n- We think Subresource Integrity (SRI) is ready for stabilization but we would like to ping a few existing users and/or SME's to confirm the implementation is valid\n- Discussed the `unstable_optimizedDeps` feature, confirming it will remain unstable in V8 and then be pseudo-deprecated in favor of RollDown\n  - There are some concerns about RollDown's full bundle mode limiting scalability so we may need to wait until rolldown is ready for testing\n- Decided against making \"type-safe matches\" an immediate V8 necessity due to the API churn\n- RSC implementation will not have a stable API ready for V8 but will be released in a minor version later\n  - We will not be deprecating existing APIs at that time because not everyone should have to use RSC\n- `Vite environment API` and `split route modules` are nearing stabilization\n- Reviewed a new RFC to stop URL normalization in loaders\n\n</details>\n\n<details>\n<summary>2025-11-18 Meeting Notes</summary>\n\nMatt mentioned opening Pull Requests (PRs) to stabilize `fetcher.reset` and `onError` for client-side use\n\n**Stabilizing Fetcher Reset and Client-Side Error Handling**\n\nMatt mentioned opening Pull Requests (PRs) to stabilize `fetcher.reset` and `onError` for client-side use. For client-side `onError`, Matt made a change to align it with `handleError` on the server by passing the `location` and `params` to the error handler, with the goal of not holding off on stabilizing the API . Bryan suggested considering adding the pattern to the error information, which Matt agreed would be useful for filtering errors in Sentry.\n\n**Stabilizing Other Router APIs**\n\nMatt confirmed with Mark that both `split route modules` and the `environment API` are ready to be stabilized. The intent is to batch these stabilizations into a minor release. Other features like observability and a transition flag are still too new for stabilization\n\n**Opt-Out Flag for Custom Navigations**\n\nMatt discussed fast-tracking a flag to allow users to opt out of wrapping their own navigations in `startTransition`. This is needed because the current implementation has bugs related to navigation state not surfacing when custom navigations are wrapped in `startTransition`, particularly affecting `useSyncExternalStore` and causing minor tearing issues with search params. Matt mentioned we could fork off and ship the `false` (opt-out) version of this unstable flag quickly if needed.\n\n**Type-Safe Fetchers Discussion**\n\nThe discussion moved to the highly-anticipated type-safe fetchers feature. Bryan suggested that the definition of a fetcher should be tied to a route at creation time, as fetchers resolve to a single handler (action or loader), where all the type signature glue happens . A challenge is resolving ambiguity when a path matches multiple loaders, such as a layout route and an index route.\n\n**Route ID vs. Pattern for Type-Safe Fetchers**\n\nThe group debated using route ID versus the path pattern for identifying the route. Bryan and Jacob agreed that parameters should be accepted at the invocation site of the fetcher. Mark raised concerns that using route ID might require querying the full manifest, which could be problematic with \"fog of war\" architectures where only a small number of routes are known at runtime. They agreed to use the pattern instead, which doubles as the route ID in a sense and does not require querying the manifest for URL construction.\n\n**Proposed New Hooks for Type-Safe Fetchers**\n\nBryan proposed splitting `useFetcher` into two separate hooks: `useRouteLoader` and `useRouteAction`, bound explicitly to a loader and an action, respectively. This separation is beneficial because a loader is primarily concerned with the GET method, while an action can handle multiple methods (POST, PUT, etc.). The new hooks would return an array with the state/data object and the imperative method (like `submit` or `load`), a pattern which Matt and Mark liked.\n\n**Streamlining State Tracking with React APIs**\n\nThe conversation shifted towards aligning the new hooks with modern React APIs, especially those from React 19. Bryan suggested that the router could offload state tracking to userland by using React's `useTransition` and `useOptimistic` hooks, leading to a much slimmer abstraction. This would replace the existing `fetcher.state` (idle, loading, submitting) and `fetcher.form`.\n\n**Leveraging `useActionState` and Form Actions**\n\nJacob noted that using an asynchronous function for a form's action would also allow for use of the `useActionState` hook from React, which can track the pending status of the form, further streamlining the API. This design would also enable the deprecation of `fetcher.form` in favor of a standard React form. However, the group noted that this approach would be for JS-supported forms and not progressively enhanced forms in an RSC world without JavaScript.\n\n**Call Site Revalidation Opt-Out**\n\nThe group discussed the community PR for a call site revalidation opt-out, specifically the open question of whether a call site option like `shouldRevalidate: false` should override a route's existing `shouldRevalidate` function. Jacob and Bryan agreed with Sergio's recommendation that the call site option should set the new default value passed to the routes, essentially bubbling up, to avoid potential support headaches and data integrity issues that would arise from overriding all revalidation.\n\n**Naming Convention for Revalidation Opt-Out**\n\nBryan suggested a minor design point to name the option passed to `submit` as `defaultShouldRevalidate` for consistency. Matt agreed with this suggestion.\nDefault Route Revalidation Behavior Bryan and Matt discussed the implementation of a default setting for route revalidation, with Bryan expressing concern that people might misuse a hard bypass but agreeing that passing in a default allows the route logic to still determine the revalidation. Matt highlighted that this default would be an easy win for applications with many routes that need a common revalidation behavior, preventing the need to change fifty routes, while still allowing specific routes with their own logic to override the default. They agreed that the route should always be the final authority on revalidation.\n\n**Scope and Granularity of Default Revalidation**\n\nJacob proposed setting a specific route default, perhaps for parent routes but not a view, for scenarios involving submissions, but Matt dismissed this, noting that routes already have a specific place for their logic in the route function. Bryan also suggested that more granular control would lead to excessive complexity. The team concluded that the default should not allow a function, as Jacob argued it should be derived from local state.\n\n**Implementation of 'default revalidate'**\n\nMatt confirmed liking the name \"default revalidate\" and determined that it should apply to all imperative hooks, including navigates and submits. Bryan and Matt agreed that having the routes maintain final control of revalidation makes sense on navigates. Matt mentioned that there is an existing Pull Request for this feature, which they plan to update.\n\n</details>\n\n<details>\n<summary>2025-12-02 Meeting Notes</summary>\n\n**Meeting Status and Stabilizations**\n\nMatt shared that three stabilizations were pushed out: `environment API future flag`, `split route modules future flag`, and `fetch error reset`. Matt noted that `onError` would be in the next release after one last internal refactor to address potential double reporting issues in strict mode.\n\n**`useRoute` and Type-Safe Fetchers**\n\nMatt discussed the plan to complete the other half of `useRoute`, the `useRouterState` functionality, and treating them as a package deal for stabilization. Pedro agreed, emphasizing the need for the type-safe fetcher approach to be cohesive with `useRoute` before stabilization, to avoid having multiple ways of doing things if the ID-based approach is changed later.\n\n**Babel to SWC/Oxide Migration for Performance**\n\nMark raised the proposal to switch from Babel to SWC/Oxide for speed improvements, noting that the stabilization of `split route modules` increases the amount of transformations happening in Babel. Pedro expressed support for making things faster but questioned the priority, as they have yet to see a real use case where Babel is the bottleneck, suggesting current HMR times are sub-40 milliseconds.\n\n**Performance Bottlenecks and Rollup Integration**\n\nPedro explained that a larger architectural problem, the necessity of a full pass over all routes for manifest creation, contributes to performance issues, making the dev server launch time proportional to the number of routes. Mark clarified that Rollup speeds up the build, not dev, and Pedro suggested profiling to determine if Babel is truly the bottleneck out of the 50 milliseconds of overhead. Matt suggested involving the community for hard numbers and potentially waiting to see if Rollup with an AST pipeline API would alleviate the issue, which might necessitate rewriting transforms anyway. Matt asked Mark and Pedro to comment on the proposal, indicating interest but needing more evidence of the bottleneck.\n\n**Fetcher Error Handling and Imperative Usage**\n\nDiscussion returned to an existing, highly voted proposal concerning fetcher error handling and completion. Matt noted that returning promises from `fetcher.load` and `fetcher.submit` partially solved the completion concern, but returning the data is still missing. The other main request is to prevent fetcher errors from triggering the route-level error boundary, for which Matt suggested an opt-in mechanism like `don't bubble errors` or a `handle error` option. Bryan argued that fetchers, being out-of-band network requests, should not bubble up to the route error boundary naturally.\n\n**Inline Action Approach for Fetcher Error Handling**\n\nThe discussion moved towards an inline action approach for error handling, aligning with `client loader` mechanisms, as suggested by Jacob. Matt and Bryan considered how an inline handler could allow users to catch network errors and decide whether to return errors as data or throw. Sergio Daniel Xalambrí questioned whether these changes should be applied to the new type-safe fetchers (e.g., `useRoute action`) instead of evolving `useFetcher`. Matt and Bryan agreed that implementing this work within the new type-safe fetcher APIs, where `submit` would return data and reject on errors, seems like the most appropriate approach.\n\n**Route Masking/Rewrites for Modals**\n\nMatt introduced a resurfaced, high-priority proposal for route masking/rewriting, similar to Next.js's parallel/intercepting routes or Tanstack's route masking. This feature, previously available in declarative mode, allows rendering a modal over a background URL while maintaining a different URL in the bar. Matt suggested an API where the user provides the URL to be displayed in the URL bar, and the router navigates internally to a different URL, likely driven by search parameters. Mark and Bryan agreed that this seems coupled to client-side navigation for UX cases like job details over search results.\n\n**Server Rewrites and Client Router Synchronization**\n\nSergio Daniel Xalambrí noted that people often ask for server rewrites, which in Next.js terms often means URL aliases where a path renders the content of another route, potentially with param rewriting. Matt concluded that true server rewrites are a separate feature but noted that implementing the client-side masking feature would close the gap on what is needed to synchronize rewrite logic with the client router, potentially unlocking future server-side rewrite capabilities. Matt intends to update the proposal and move it to the \"accepting PRs\" stage, noting that the implementation could draw on internals from V6.\n\n**Element Scroll Restoration**\n\nThe last topic discussed was a high-voted proposal for scroll restoration on elements other than the window. Matt explained that a full userland implementation is not reliably possible because the router is the only one that truly knows the moment right before a view changes to reliably capture the scroll position. Matt plans to provide guidance and feedback based on previous PR discussions, hoping the community can finalize the implementation.\n\n</details>\n\n<details>\n<summary>2025-12-16 Meeting Notes</summary>\n\n**Trailing Slash Consistency Bug Fix**\n\nMatt outlined a bug fix related to inconsistent request path names provided to the `loader`/`action` on data requests when a URL has a trailing slash. Matt explained that the solution, developed with Jacob, involves changing the format of the data request URL for trailing slash scenarios to resemble the `_root.data` format, which is being put behind a future flag due to potential breaking changes and cache rule implications. The new format will use `/a/b/c/_.data` when the URL is `/a/b/c/`.\n\nMatt also noted that the future flag provides an opportunity to collapse the `_root.data` format into the new trailing slash format, resulting in two standardized formats for data requests in the future. Bryan asked for clarification on various URL and query parameter configurations, and Matt explained the distinction between the new trailing slash format and the use of the index query parameter, confirming that the new format will be opt-in and mostly non-breaking for users upon adoption\n\n</details>\n"
  },
  {
    "path": "LICENSE.md",
    "content": "MIT License\n\nCopyright (c) React Training LLC 2015-2019\nCopyright (c) Remix Software Inc. 2020-2021\nCopyright (c) Shopify Inc. 2022-2023\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "README.md",
    "content": "[![npm package][npm-badge]][npm] [![build][build-badge]][build]\n\n[npm-badge]: https://img.shields.io/npm/v/react-router-dom.svg\n[npm]: https://www.npmjs.org/package/react-router-dom\n[build-badge]: https://img.shields.io/github/actions/workflow/status/remix-run/react-router/test.yml?branch=dev&style=square\n[build]: https://github.com/remix-run/react-router/actions/workflows/test.yml\n\nReact Router is a multi-strategy router for React bridging the gap from React 18 to React 19. You can use it maximally as a React framework or minimally as a library with your own architecture.\n\n- [Getting Started - Framework](https://reactrouter.com/start/framework/installation)\n- [Getting Started - Library](https://reactrouter.com/start/library/installation)\n- [Upgrade from v6](https://reactrouter.com/upgrading/v6)\n- [Upgrade from Remix](https://reactrouter.com/upgrading/remix)\n- [Changelog](https://github.com/remix-run/react-router/blob/main/CHANGELOG.md)\n\n## Packages\n\n- [`react-router`](./packages/react-router)\n- [`@react-router/dev`](./packages/react-router-dev)\n- [`@react-router/node`](./packages/react-router-node)\n- [`@react-router/cloudflare`](./packages/react-router-cloudflare)\n- [`@react-router/serve`](./packages/react-router-serve)\n- [`@react-router/fs-routes`](./packages/react-router-fs-routes)\n\n## Previous Versions\n\n- [v6](https://reactrouter.com/v6)\n- [v5](https://v5.reactrouter.com/)\n"
  },
  {
    "path": "SECURITY.md",
    "content": "# Security Policy\n\n## Supported Versions\n\nThe following versions are currently being supported with security updates:\n\n| Version | Supported          |\n| ------- | ------------------ |\n| 7.x     | :white_check_mark: |\n| 6.x     | :white_check_mark: |\n| < 6.0   | :x:                |\n\n## Reporting a Vulnerability\n\nWe take security bugs in React Router seriously. We appreciate your efforts to responsibly disclose your findings, and will make every effort to acknowledge your contributions.\n\nTo report a security issue, please use the GitHub Security Advisory [Report a Vulnerability](https://github.com/remix-run/react-router/security/advisories/new) feature.\n\nThe React Router team will send a response indicating the next steps in handling your report. After the initial reply to your report, our team will keep you informed of the progress towards a fix and full announcement, and may ask for additional information or guidance.\n\nGenerally, the full process will look something like this when we receive a new advisory via Github:\n\n- If the advisory is valid, we'll move it into `Draft` status as we begin our investigation\n- We'll inform common hosting platforms of the vulnerability so they can make any preventative changes on their end even before the vulnerability is fixed/published\n  - If you are a hosting provider and you want to be notified right away, please email us at [hello@remix.run](mailto:hello@remix.run) and we'll get you added\n- We'll publish a new version of React Router with a fix\n- We'll update our own sites with the new version\n- After a period of time, potentially up to a month or so, we'll publish the advisory\n  - This gives application developers time to update their applications to the latest version before we make the details of the advisory public\n\nReport security bugs in third-party modules to the person or team maintaining the module. You can also report a vulnerability through the [npm contact form](https://www.npmjs.com/support) by selecting \"I'm reporting a security vulnerability\".\n"
  },
  {
    "path": "build.utils.ts",
    "content": "export function createBanner(packageName: string, version: string) {\n  return `/**\n * ${packageName} v${version}\n *\n * Copyright (c) Remix Software Inc.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE.md file in the root directory of this source tree.\n *\n * @license MIT\n */`;\n}\n"
  },
  {
    "path": "contributors.yml",
    "content": "- 0xEddie\n- 3fuyang\n- 43081j\n- aarbi\n- abdallah-nour\n- abeadam\n- abhi-kr-2100\n- abhijeetpandit7\n- AchThomas\n- acusti\n- adamdotjs\n- adil62\n- adriananin\n- adrienharnay\n- afzalsayed96\n- AhmadMayo\n- Ajayff4\n- akamfoad\n- alany411\n- alberto\n- AlemTuzlak\n- Aleuck\n- alexandernanberg\n- alexanderson1993\n- alexlbr\n- AlexWebLab\n- amitdahan\n- AmRo045\n- amsal\n- AnandShiva\n- Andarist\n- andreasottosson-polestar\n- andreiborza\n- andreiduca\n- antonmontrezor\n- appden\n- apple-yagi\n- arditdine\n- arjunyel\n- arka1002\n- Armanio\n- arnassavickas\n- aroyan\n- Artur-\n- ashusnapx\n- avipatel97\n- awreese\n- aymanemadidi\n- ayushmanchhabra\n- babafemij-k\n- barclayd\n- basan17\n- bavardage\n- bbrowning918\n- BDomzalski\n- bhbs\n- bilalk711\n- bjohn465\n- black5box\n- BlankParticle\n- bmsuseluda\n- bobziroll\n- bravo-kernel\n- Brendonovich\n- briankb\n- BrianT1414\n- Bricklou\n- brockross\n- brookslybrand\n- brophdawg11\n- btav\n- bvangraafeiland\n- camthompson\n- CanRau\n- caprolactam\n- cassidoo\n- chaance\n- chandershekhar22\n- chanmilee-fe\n- chasinhues\n- chensokheng\n- chr33s\n- chrille0313\n- chrisngobanh\n- christopherchudzicki\n- ChristophP\n- christowiz\n- clavery\n- clonemycode\n- Cmoen11\n- codeape2\n- coolport\n- coryhouse\n- ctnelson1997\n- cvbuelow\n- dadamssg\n- damianstasik\n- danielberndt\n- danielweinmann\n- daniilguit\n- dauletbaev\n- david-bezero\n- david-crespo\n- davidbielik\n- dcblair\n- dchenk\n- decadentsavant\n- developit\n- dgrijuela\n- DigitalNaut\n- DimaAmega\n- dimmageiras\n- dmitrytarassov\n- dogxii\n- dokeet\n- doytch\n- Drishtantr\n- EdgeOneDev\n- edmundhung\n- edwin177\n- eiffelwong1\n- ek9\n- ekaansharora\n- elanis\n- elylucas\n- emzoumpo\n- engpetermwangi\n- ericschn\n- ErlendS\n- esadek\n- faergeek\n- fernandojbf\n- FilipJirsak\n- focusotter\n- foxscotch\n- frodi-karlsson\n- frontsideair\n- fucancode\n- fyzhu\n- fz6m\n- gaspard\n- gatzjames\n- gavriguy\n- gchang12\n- Geist5000\n- GeoffKarnov\n- gesposito\n- gianlucca\n- gijo-varghese\n- goldins\n- goodrone\n- gowthamvbhat\n- GraxMonzo\n- guppy0356\n- GuptaSiddhant\n- haivuw\n- hampelm\n- harshmangalam\n- harucn\n- HelpMe-Pls\n- HenriqueLimas\n- hernanif1\n- HeyyyNeo\n- hi-ogawa\n- HK-SHAO\n- holynewbie\n- hongji00\n- hoosierhuy\n- hsbtr\n- hyesungoh\n- iamnishanth\n- ianflynnwork\n- IbraRouisDev\n- igniscyan\n- imjordanxd\n- infoxicator\n- ioNihal\n- IsaiStormBlesed\n- Isammoc\n- iskanderbroere\n- istarkov\n- ivanjeremic\n- ivanjonas\n- Ivanrenes\n- JackPriceBurns\n- jacob-briscoe\n- jacob-ebey\n- jadlr\n- JaffParker\n- jakkku\n- JakubDrozd\n- jamesopstad\n- jamesrwilliams\n- janpaepke\n- jasikpark\n- jasonpaulos\n- jb-1980\n- jclarkin\n- jdufresne\n- jenseng\n- JeraldVin\n- JesusTheHun\n- jimniels\n- jmargeta\n- jmjpro\n- johnpangalos\n- jonkoops\n- joseph0926\n- jplhomer\n- jrakotoharisoa\n- jrestall\n- juanpprieto\n- jungwoo3490\n- justjavac\n- kachun333\n- Kakamotobi\n- kantuni\n- kaoths\n- kapil-patel\n- kapilepatel\n- KaranRandhir\n- kark\n- KAROTT7\n- kddnewton\n- ken0x0a\n- kentcdodds\n- keshavjoshi-ca\n- kettanaito\n- kigawas\n- kilavvy\n- kiliman\n- kirillgroshkov\n- kkirsche\n- kno-raziel\n- knownasilya\n- koojaa\n- KostiantynPopovych\n- KubasuIvanSakwa\n- KutnerUri\n- kylegirard\n- LadyTsukiko\n- landisdesign\n- latin-1\n- lazybean\n- lequangdongg\n- liborgabrhel\n- liuhanqu\n- lkwr\n- lopezac\n- lordofthecactus\n- LordThi\n- louis-young\n- loun4\n- lounsbrough\n- lpaube\n- lqze\n- lukerSpringTree\n- m-dad\n- m-kawafuji\n- m-shojaei\n- machour\n- majamarijan\n- Malien\n- Manc\n- manzano78\n- marc2332\n- markdalgleish\n- markivancho\n- markmals\n- Marlon-Buckley\n- maruffahmed\n- marvinruder\n- mathpaquette\n- matmilbury\n- matt-harro\n- matteogauthier\n- matthewlynch\n- maximevtush\n- maxpou\n- mcansh\n- mcollina\n- MeatSim\n- MenouerBetty\n- Methuselah96\n- mfijas\n- MichaelDeBoey\n- michal-antczak\n- miguelvictor\n- MihailProcudin\n- mikib0\n- minami-minami\n- minthulim\n- mjackson\n- mlewando\n- mm-jpoole\n- mobregozo\n- modex98\n- morleytatro\n- ms10596\n- mspiess\n- mtendekuyokwa19\n- mtliendo\n- namoscato\n- nanianlisao\n- ned-park\n- nenene3\n- ngbrown\n- nichtsam\n- nikeee\n- nilubisan\n- nimrossum\n- Nismit\n- nnhjs\n- noisypigeon\n- nowells\n- Nurai1\n- nwleedev\n- Obi-Dann\n- okalil\n- OlegDev1\n- omahs\n- omar-moquete\n- OnurGvnc\n- otabekshoyimov\n- p13i\n- parched\n- parveen232\n- paulsmithkc\n- pavsoldatov\n- pawelblaszczyk5\n- pcattori\n- penx\n- peterneave\n- petersendidit\n- phildl\n- phryneas\n- pierophp\n- printfn\n- promet99\n- proshunsuke\n- pruszel\n- pwdcd\n- pyitphyoaung\n- qu0b\n- QzCurious\n- redabacha\n- refusado\n- remorses\n- renyu-io\n- reyronald\n- RFCreate\n- richardkall\n- richardscarrott\n- rifaidev\n- rimian\n- robbtraister\n- RobHannay\n- robinvdvleuten\n- rossipedia\n- rtmann\n- rtzll\n- rubeonline\n- ruidi-huang\n- rururux\n- ryanflorence\n- ryanhiebert\n- saengmotmi\n- SailorStat\n- samimsu\n- sanjai451\n- sanketshah19\n- sapphi-red\n- saul-atomrigs\n- sbolel\n- scarf005\n- sealer3\n- seasick\n- senseibarni\n- sergiodxa\n- serranoarevalo\n- sevignator\n- sgalhs\n- sgrishchenko\n- shamsup\n- shihanng\n- shivamsinghchahar\n- silvenon\n- SimenB\n- SirDaev\n- SkayuX\n- skratchdot\n- skrhlm\n- smff\n- smithki\n- soartec-lab\n- sorokya\n- sorrycc\n- souzasmatheus\n- SovietGhost\n- soxtoby\n- srmagura\n- SsongQ-92\n- stasundr\n- stmtk1\n- sukvvon\n- sunnyraindy\n- swalker326\n- szhsin\n- tanayv\n- thecode00\n- theMosaad\n- theostavrides\n- thepedroferrari\n- thethmuu\n- thisiskartik\n- thomasgauvin\n- ThomasTheTitan\n- thomasverleye\n- ThornWu\n- tiborbarsi\n- timdorr\n- timfisher\n- TkDodo\n- tkindy\n- tlinhart\n- tobias-edwards\n- tom-sherman\n- tomasr8\n- TomerAberbach\n- tony-sn\n- TooTallNate\n- torztomasz\n- tosinamuda\n- triangularcube\n- trungpv1601\n- tryonelove\n- TrySound\n- ttys026\n- Tumas2\n- turansky\n- tyankatsu0105\n- underager\n- valerii15298\n- ValiantCat\n- vdusart\n- vesan\n- vezaynk\n- VictorElHajj\n- vijaypushkin\n- vikingviolinist\n- vishwast03\n- vitekzach\n- vladinator1000\n- vonagam\n- WalkAlone0325\n- whxhlgy\n- wilcoxmd\n- willemarcel\n- williamsdyyz\n- willsawyerrrr\n- willsmithte\n- Willvillegas\n- wkovacs64\n- wo-o29\n- wobsoriano\n- woodywoodsta\n- xavier-lc\n- xcsnowcity\n- xdaxer\n- yionr\n- yracnet\n- ytori\n- yuhwan-park\n- yuleicul\n- yuri-poliantsev\n- zeevick10\n- zeromask1337\n- zheng-chuang\n- zxTomw\n"
  },
  {
    "path": "decisions/0001-use-blocker.md",
    "content": "# useBlocker\n\nDate: 2023-01-17\n\nStatus: accepted\n\n## Context\n\nReact Router v5 had a [`<Prompt>`](https://v5.reactrouter.com/core/api/Prompt) component that allowed app developers to indicate when navigation should be blocked (via the `when` prop), and specify a string message to display in a `window.confirm` UI that would let the user confirm the navigation. The _primary_ use case for this is preventing users from losing half-filled form info when navigating away.\n\nThe React Router v6 beta initially had two hooks to replace this component (`useBlocker` and `usePrompt`) but they were [removed](https://github.com/remix-run/react-router/issues/8139#issuecomment-954425560) during the beta release process. The reasoning being:\n\n> As for why it was removed in v6, we decided we'd rather ship with what we have than take even more time to nail down a feature that isn't fully baked.\n\nFolks then started [adding](https://github.com/remix-run/react-router/issues/8139#issuecomment-977790637) this functionality back in manually using `navigator.block` via the `UNSAFE_NavigationContext` so they could upgrade their apps from v5 to v6 while keeping the blocking ability.\n\nHowever, as part of the 6.4 data-routing work, we significantly streamlined and inlined the `history` library and [removed the `block` method](https://github.com/remix-run/react-router/issues/8139#issuecomment-1176523524), causing issues for those using the `UNSAFE_NavigationContext` workaround. Although, there was still ways to achieve this [via an `unstable_HistoryRouter`](https://github.com/remix-run/react-router/issues/8139#issuecomment-1247080906).\n\nIn September we [commented](https://github.com/remix-run/react-router/issues/8139#issuecomment-1262630360) back further advising our stance that storing data in `localStorage` was a preferable pattern to blocking.\n\nOver time though, we did receive some valuable feedback which indicated some other use cases where blocking was useful, and stashing form info in localStorage might not be sufficient:\n\n- [Cancelling long-running processes](https://github.com/remix-run/react-router/issues/8139#issuecomment-970770513)\n- [Waiting for an API call to finish](https://github.com/remix-run/react-router/issues/8139#issuecomment-1272052155)\n- [Waiting for a file upload to complete](https://github.com/remix-run/react-router/issues/8139#issuecomment-1293247842)\n- [Navigating away might very well be an indication that they want to clear the form data](https://github.com/remix-run/react-router/issues/8139#issuecomment-1285824941)\n- [Sensitive form info which cannot be put in `localStorage`](https://github.com/remix-run/react-router/issues/8139#issuecomment-1294245742)\n\nBased on the feedback, we [decided to re-consider](https://github.com/remix-run/react-router/issues/8139#issuecomment-1302775114) but [with known limitations](https://github.com/remix-run/react-router/issues/8139#issuecomment-1302822885) so that we weren't preventing users from benefitting from the awesome new features in 6.4 and beyond.\n\n### Why is this so hard?\n\nHaving not used React Router v5, nor it's blocking functionality, I can only guess at what I _think_ the main pain points were. Then we can look at how we might solve then in a v6 implementation.\n\nBlocking `PUSH`/`REPLACE` navigations is generally straightforward - they come through `history` so we can deal with blockers _before_ we call `window.history.pushState`, and if blocked skip the call all together. This means the URL and the UI remain synced.\n\nBlocking `POP` navigations is different - since we don't know about them via `popstate` until _after_ the URL has been updated - so we're immediately in an unsynced state. I.e., if we've navigated `A -> B -> C` and the user hits the back button - we evaluate our blockers while the UI shows `C`, but the url shows `B`. However, v5 had a way to handle that as well - by storing an `index` on the `location.state` we can determine what the `popstate` delta was and [revert it](https://github.com/remix-run/history/blob/dev/packages/history/index.ts#L398) if the navigation was blocked.\n\nSo what was the issue? I _think_ it boiled down not to _how to block_ but instead in _when to retry_. We exposed a `retry` function to userland as part of `useBlocker` and therefore we lost control over _when_ that function might be called. A retry of a blocked `POP` navigation is _inherently tightly-coupled to the current location oin the history stack_. But by exposing `retry`, we could no longer ensure that `retry` was called from the right location. For example:\n\n1. User is sitting on `C`, with a history stack of `A -> B -> C`\n2. User clicks back to `B`, and the navigation is blocked.\n3. We reset history to `C` and provide a retry of `() => pop(-1)`\n4. User clicks back to `B` and then `retry` gets called, we land on `A`, not the `B` the original blocked transition intended to take us to\n\nSpecifically, part of the issue comes down to the fact that while `window.confirm` is synchronous on the JS thread, is does not prevent additional user interaction with the back/forward buttons. This causes issues with `retry` like the flow described above.\n\n1. User is sitting on `C`, with a history stack of `A -> B -> C`\n2. User clicks back to `B`, and we show the `window.confirm` prompt\n3. _Before answering,_ the user clicks the back button again (at this point the browser is at `B`, so this back button goes to `A`)\n4. In Chrome, this causes `window.confirm` to return `false` (indicating we should block the C->B back button click) but it respects the new back button click!\n5. So now the user is sitting on `A`, but our history library thinks we're on `C` since it thinks we blocked the original back button navigation\n\nIt's also worth noting that these `popstate` blockers don't work on navigations leaving your app - such as cross-origin or full document reloads. To handle those, you need to also wire up a `beforeunload` event listener on `window`. This _does_ block further back-button clicks while it's open so it's not subject to the same issues as `window.confirm` above.\n\n### How can we tackle this in a limited v6 implementation?\n\nHaving played around with some of our POC implementations in v6, I think we've identified a few assumptions we will need to make oin order to implement blocking in a reliable way.\n\n1. The answer to _\"should I block this navigation\"_ must be instantaneous/synchronous. there must be no way for the user to perform any _additional_ navigations while answering this question.\n   1. If this is always instantaneous, it allows us to decide immediately on a `popstate` whether we even need to revert. In v5, we would automatically revert, then run the blocker, then maybe retry the navigation. In v6, non-blocked navigations are a no-op, and blocked navigations are immediately reverted which re-syncs the with the URL _before any other navigations can happen_.\n   2. This assumption therefore excludes the potential for `usePrompt` because while the `window.confirm` function is synchronous, it does not block additional user-initiated navigations. Furthermore, browser behave [very differently](https://github.com/remix-run/react-router/pull/9709#discussion_r1060171714) when it comes to back button clicks while a `window.confirm` prompt is open. Any attempt to support `window.confirm` in React Router will inevitable result in a table in our docs explaining [why and how](https://github.com/remix-run/react-router/pull/9821) each browser behaves differently. This is a non-starter from a UX perspective in my eyes.\n2. Blockers can not persist across navigations\n   1. As soon as a successful navigation is completed, we must reset all blockers since their `retry` functions are inherently stale and therefore calling them can only do more weird things.\n3. There can only be one blocker present at a time\n   1. When Chance and I initially talked through this we had thoughts on how we could maybe support multiple blockers. The use-cases are not immediately obvious, but likely a case when a page has multiple separate forms each of which could be in a valid or half-filled state. This makes the logic potentially very confusing if some block and others don't, and by the time one is cleaned up the other blocks and so on.\n   2. Thankfully, it turns out [this limitation existed in v5](https://github.com/remix-run/history/blob/v4/modules/createTransitionManager.js#L7) as well - so I think we should carry this forward. If, once we add this back to v6 we find compelling use-cases, maybe we can investigate multiple-blocker support in the future.\n\nWith these assumptions in mind, I think we can implement a fairly robust `useBlocker` hook in v that would suffice for the majority (if not all) known use-cases, and we could clearly document where this hook has rough edges. Any usage of `window.confirm` would be left to a userland implementation of `usePrompt` and all of the concerns that come with it are then part of the application and not React Router.\n\n### What are the use-cases, exactly?\n\nAs part of the ongoing Github Discussion, [Chance asked](https://github.com/remix-run/react-router/issues/8139#issuecomment-1332652167) folks if they could elaborate on how they were using the `<Prompt>` component in v5 and specifically if they were using the `getUserConfirmation` prop to customize the experience away from `window.confirm`. As it turns out,. it seems the vast majority of folks were opting _not_ to use `window.confirm`- either via getUserConfirmation or more often via a bit of a hacked implementation of `<Prompt message={() => { ... }} />`.\n\n- Some folks used `getUserConfirmation` to avoid `window.confirm`\n  - _In order to be consistent with the rest of our UI (notably with other similar but not router-related confirmation dialogs), we don't rely on the native window.confirm(), we use material-ui confirmations modals._ [link](https://github.com/remix-run/react-router/issues/8139#issuecomment-1337538869)\n  - _We use getUserConfirmation however we instead default to true and show a toast message._ [link](https://github.com/remix-run/react-router/issues/8139#issuecomment-1337670812)\n- Other folks built custom UI via manual `history.block` usage\n  - _Never happened to need getUserConfirmation prop. When customization was needed we used history.block to build custom prompt around it._ [link](https://github.com/remix-run/react-router/issues/8139#issuecomment-1336392940)\n- Other folks used the `message` prop function to trigger custom modals\n  - _For us, while window.confirm is convenient as a default, custom modal dialog is what the designers/product people want, so we want to be able to provide a react component (either with state like this, or a render prop)._ [link](https://github.com/remix-run/react-router/issues/8139#issuecomment-1337053026)\n  - _we don't use getUserConfirmation, but instead rely on the boolean return option from Prompt.message, specifically the ability to return false to block without a message._ [link](https://github.com/remix-run/react-router/issues/8139#issuecomment-1337707665)\n  - _Our use-case is to use <Prompt> to show a Warning-Confirm-Modal when the User has changed stuff in the Configuration in the state, but has not submitted it._ [link](https://github.com/remix-run/react-router/issues/8139#issuecomment-1339033495)\n  - _we are only using the <Prompt> component (didn't need getUserConfirmation) to show a [custom] confirm dialog if the user tries to change location with an unsaved form._ [link](https://github.com/remix-run/react-router/issues/8139#issuecomment-1340647770)\n  - _We mostly use usePrompt and we hope the new version can still provide flexibility to utilize the window.prompt as well custom modal scenarios._ [link](https://github.com/remix-run/react-router/issues/8139#issuecomment-1343441620)\n\nIn the end, there are maybe 1-2 folks who responded that use the simple `window.confirm` scenario, and instead _almost all_ people are skipping `window.confirm` in favor of a custom dialog. I don't find this very surprising - knowing the look I'd have gotten from prior UX designers if I said that wa the UI were going to ship to our users 😉.\n\n#### (Ab)use-cases\n\nSome folks have mentioned that they don't it to block navigation, but instead to detect _before_ a navigation happens for firing off analytics or what not. While `useBlocker` could be abused for this purpose, that will eventually be solved more accurately via the proposed [Events API](https://github.com/remix-run/react-router/discussions/9565)\n\n## Decision\n\nThe proposal for support in v6 is to implement a single low-level `useBlocker` hook that provides the user enough information to (1) show a custom confirmation alert/dialog/modal/etc. and (2) allow the navigation to proceed if the user accepts the dialog. This would only allow one active blocker at a time in the component tree, and would error or warn if a second `useBlocker` was encountered.\n\n```tsx\ntype Blocker =\n  | {\n      state: \"unblocked\";\n      reset: undefined;\n      proceed: undefined;\n    }\n  | {\n      state: \"blocked\";\n      reset(): void;\n      proceed(): void;\n    }\n  | {\n      state: \"proceeding\";\n      reset: undefined;\n      proceed: undefined;\n    };\n\ndeclare function useBlocker(shouldBlock: boolean | () => boolean): Blocker;\n\nfunction MyFormComponent() {\n  let [formIsDirty, setFormIsDirty] = React.useState(false);\n  let blocker = useBlocker(formIsDirty);\n\n  return (\n    <Form method=\"post\" onChange={(e) => setFormIsDirty(true)}>\n      <label>\n        First name:\n        <input name=\"firstname\" required />\n      </label>\n      <label>\n        Last name:\n        <input name=\"lastname\" required />\n      </label>\n      <button type=\"submit\">Submit</button>\n\n      {blocker.state === \"blocked\" ? (\n        <div>\n          <p>You have unsaved changes!<p>\n          <button onClick={() => blocker.reset()}>\n            Oh shoot - I need them keep me here!\n          </button>\n          <button onClick={() => blocker.proceed()}>\n            I know! They don't matter - let me out of here!\n          </button>\n        </div>\n      ) : blocker.state === \"proceeding\" ? (\n        <p>Navigating away with unsaved changes...</p>\n      ) : null}\n    </Form>\n  );\n}\n```\n\nThe `blocker` received by the user would be either `unblocked`, `blocked`, or `proceeding`:\n\n- `unblocked` is the normal idle state\n- `blocked` means the user tried to navigate and the blocker function returned `true` and the navigation was blocked. When in a `blocked` state the blocker would expose `proceed`/`reset` functions:\n  - `blocker.proceed()` would allow the blocked navigation to happen (and thus lose unsaved changes). This proceed navigation would _not_ re-run the blocker function.\n  - `blocker.reset()` would reset the blocker back to `unblocked` and remain on the current page\n- `proceeding` indicates the navigation from `blocker.proceed()` is in-progress - and essentially reflects the non-`idle` `navigation.state` during that navigation\n\nOther navigations and/or interruptions to proceeding navigations would reset the blocker back to an unblocked state.\n\n~We will not provide a `usePrompt` implementation, however it would be somewhat trivial to implement that on top of `useBlocker` in userland.~\n\nWe decided in the end to include a `usePrompt` even though it's got more broken edge cases than `useBlocker`:\n\n- It's only a handful of lines of code\n- It's more similar to what we had in v5\n- We don't know for sure how many folks were using this in v5, since the github commenters are not a complete sample\n- It has a lower barrier to implement than a custom modal UI\n- We plan to document that it breaks in more cases, in weird ways, and even differently across browsers.\n\n### Blocker State Diagram\n\n```mermaid\ngraph TD;\n    Unblocked -->|navigate| A{shouldBlock?};\n    A -->|false| Unblocked;\n    A -->|true| Blocked;\n    Blocked -->|blocker.proceed| Proceeding;\n    Blocked -->|Unblocked Navigation| Unblocked;\n    Blocked -->|blocker.reset| Unblocked;\n    Proceeding -->|Navigation Complete| Unblocked;\n    Proceeding -->|Navigation Interrupted| Unblocked;\n```\n\n### Open Questions\n\n- Initial implementation is for data-router usage (6.4+). We still need to back-port to 6.3 and earlier to help folks migrate from `v5 -> v6 BrowserRouter -> v6 RouterProvider`\n  - We decided that this can just be net-new 6.4+ API. A v5 app should be able to migrate to a 6.4+ `RouterProvider` just as easily as a 6.3 `BrowserRouter`\n- We should probably pass the `historyAction`/`location` of the active navigation to `shouldBlock()` similar to how v5 did it. Should we also pass the submission (`formMethod`, `formData`, etc.)?\n  - For now we landed on calling the blocker function with `{ currentLocation, nextLocation, historyAction }` to align naming loosely with `shouldRevalidate`. Can always extend that API ion the future if needed (with form submission info).\n- I think since we are not providing `usePrompt`, we should accept a `beforeUnload:boolean` option to add cross-navigation handling in an opt-in fashion.\n  - `beforeUnload` is also unreliable because it does not prevent the user from doing additional back/forward navigations ao this is not included out of the box and can be implemented in user-land.\n"
  },
  {
    "path": "decisions/0001-use-npm-to-manage-npm-dependencies-for-deno-projects.md",
    "content": "# Use `npm` to manage NPM dependencies for Deno projects\n\nDate: 2022-05-10\n\nStatus: accepted\n\n## Context\n\nDeno has three ways to manage dependencies:\n\n1. Inlined URL imports: `import {...} from \"https://deno.land/x/blah\"`\n2. [deps.ts](https://deno.land/manual/examples/manage_dependencies)\n3. [Import maps](https://deno.land/manual/linking_to_external_code/import_maps)\n\nAdditionally, NPM packages can be accessed as Deno modules via [Deno-friendly CDNs](https://deno.land/manual/node/cdns#deno-friendly-cdns) like https://esm.sh.\n\nRemix has some requirements around dependencies:\n\n- Remix treeshakes dependencies that are free of side-effects.\n- Remix sets the environment (dev/prod/test) across all code, including dependencies, at runtime via the `NODE_ENV` environment variable.\n- Remix depends on some NPM packages that should be specified as peer dependencies (notably, `react` and `react-dom`).\n\n### Treeshaking\n\nTo optimize bundle size, Remix [treeshakes](https://esbuild.github.io/api/#tree-shaking) your app's code and dependencies.\nThis also helps to separate browser code and server code.\n\nUnder the hood, the Remix compiler uses [esbuild](https://esbuild.github.io).\nLike other bundlers, `esbuild` uses [`sideEffects` in `package.json` to determine when it is safe to eliminate unused imports](https://esbuild.github.io/api/#conditionally-injecting-a-file).\n\nUnfortunately, URL imports do not have a standard mechanism for marking packages as side-effect free.\n\n### Setting dev/prod/test environment\n\nDeno-friendly CDNs set the environment via a query parameter (e.g. `?dev`), not via an environment variable.\nThat means changing environment requires changing the URL import in the source code.\nWhile you could use multiple import maps (`dev.json`, `prod.json`, etc...) to workaround this, import maps have other limitations:\n\n- standard tooling for managing import maps is not available\n- import maps are not composeable, so any dependencies that use import maps must be manually accounted for\n\n### Specifying peer dependencies\n\nEven if import maps were perfected, CDNs compile each dependency in isolation.\nThat means that specifying peer dependencies becomes tedious and error-prone as the user needs to:\n\n- determine which dependencies themselves depend on `react` (or other similar peer dependency), even if indirectly.\n- manually figure out which `react` version works across _all_ of these dependencies\n- set that version for `react` as a query parameter in _all_ of the URLs for the identified dependencies\n\nIf any dependencies change (added, removed, version change),\nthe user must repeat all of these steps again.\n\n## Decision\n\n### Use `npm` to manage NPM dependencies for Deno\n\nDo not use Deno-friendly CDNs for NPM dependencies in Remix projects using Deno.\n\nUse `npm` and `node_modules/` to manage NPM dependencies like `react` for Remix projects, even when using Deno with Remix.\n\nDeno module dependencies (e.g. from `https://deno.land`) can still be managed via URL imports.\n\n### Allow URL imports\n\nRemix will preserve any URL imports in the built bundles as external dependencies,\nletting your browser runtime and server runtime handle them accordingly.\nThat means that you may:\n\n- use URL imports for the browser\n- use URL imports for the server, if your server runtime supports it\n\nFor example, Node will throw errors for URL imports, while Deno will resolve URL imports as normal.\n\n### Do not support import maps\n\nRemix will not yet support import maps.\n\n## Consequences\n\n- URL imports will not be treeshaken.\n- Users can specify environment via the `NODE_ENV` environment variable at runtime.\n- Users won't have to do error-prone, manual dependency resolution.\n\n### VS Code type hints\n\nUsers may configure an import map for the [Deno extension for VS Code](denoland.vscode-deno) to enable type hints for NPM-managed dependencies within their Deno editor:\n\n`.vscode/resolve_npm_imports_in_deno.json`\n\n```json\n{\n  \"// This import map is used solely for the denoland.vscode-deno extension.\": \"\",\n  \"// Remix does not support import maps.\": \"\",\n  \"// Dependency management is done through `npm` and `node_modules/` instead.\": \"\",\n  \"// Deno-only dependencies may be imported via URL imports (without using import maps).\": \"\",\n\n  \"imports\": {\n    \"react\": \"https://esm.sh/react@18.0.0\",\n    \"react-dom\": \"https://esm.sh/react-dom@18.0.0\",\n    \"react-dom/server\": \"https://esm.sh/react-dom@18.0.0/server\"\n  }\n}\n```\n\n`.vscode/settings.json`\n\n```json\n{\n  \"deno.enable\": true,\n  \"deno.importMap\": \"./.vscode/resolve_npm_imports_in_deno.json\"\n}\n```\n"
  },
  {
    "path": "decisions/0002-do-not-clone-request.md",
    "content": "# Do not clone request\n\nDate: 2022-05-13\n\nStatus: accepted\n\n## Context\n\nTo allow multiple loaders / actions to read the body of a request, we have been cloning the request before forwarding it to user-code. This is not the best thing to do as some runtimes will begin buffering the body to allow for multiple consumers. It also goes against \"the platform\" that states a request body should only be consumed once.\n\n## Decision\n\nDo not clone requests before they are passed to user-code (actions, handleDocumentRequest, handleDataRequest), and remove body from request passed to loaders. Loaders should be thought of as a \"GET\" / \"HEAD\" request handler. These request methods are not allowed to have a body, therefore you should not be reading it in your Remix loader function.\n\n## Consequences\n\nLoaders always receive a null body for the request.\n\nIf you are reading the request body in both an action and handleDocumentRequest or handleDataRequest this will now fail as the body will have already been read. If you wish to continue reading the request body in multiple places for a single request against recommendations, consider using `.clone()` before reading it; just know this comes with tradeoffs.\n"
  },
  {
    "path": "decisions/0002-lazy-route-modules.md",
    "content": "# Lazy Route Modules\n\nDate: 2023-02-21\n\nStatus: accepted\n\n## Context\n\nIn a data-aware React Router application (`<RouterProvider>`), the router needs to be aware of the route tree ahead of time so it can match routes and execute loaders/actions _prior_ to rendering the destination route. This is different than in non-data-aware React Router applications (`<BrowserRouter>`) where you could nest `<Routes>` sub-tree anywhere in your application, and compose together `<React.Suspense>` and `React.lazy()` to dynamically load \"new\" portions of your routing tree as the user navigated through the application. The downside of this approach in `BrowserRouter` is that it's a render-then-fetch cycle which produces network waterfalls and nested spinners, two things that we're aiming to eliminate in `RouterProvider` applications.\n\nThere were ways to [manually code-split][manually-code-split] in a `RouterProvider` application but they can be a bit verbose and tedious to do manually. As a result of this DX, we received a [Remix Route Modules Proposal][proposal] from the community along with a [POC implementation][poc] (thanks `@rossipedia` 🙌).\n\n## Original POC\n\nThe original POC idea was to implement this in user-land where `element`/`errorElement` would be transformed into `React.lazy()` calls and `loader`/`action` would load the module and then execute the `loader`/`action`:\n\n```js\n// Assuming route.module is a function returning a Remix-style route module\nlet Component = React.lazy(route.module);\nroute.element = <Component />;\nroute.loader = async (args) => {\n  const { loader } = await route.module();\n  return typeof loader === \"function\" ? loader(args) : null;\n};\n```\n\nThis approach got us pretty far but suffered from some limitations being done in user-land since it did not have access to some router internals to make for a more seamless integration. Namely, it _had_ to put every possible property onto a route since it couldn't know ahead of time whether the route module would resolve with the matching property. For example, will `import('./route')` return an `errorElement`? Who knows!\n\nTo combat this, a `route.use` property was considered which would allow the user to define the exports of the module:\n\n```js\nconst route = {\n  path: \"/\",\n  module: () => import(\"./route\"),\n  use: [\"loader\", \"element\"],\n};\n```\n\nThis wasn't ideal since it introduced a tight coupling of the file contents and the route definitions.\n\nFurthermore, since the goal of `RouterProvider` is to reduce spinners, it felt incorrect to automatically introduce `React.lazy` and thus expect Suspense boundaries for elements that we expected to be fully fetched _prior_ to rendering the destination route.\n\n## Decision\n\nGiven what we learned from the original POC, we felt we could do this a bit leaner with an implementation inside the router. Data router apps already have an asynchronous pre-render flow where we could hook in and run this logic. A few advantages of doing this inside of the router include:\n\n- We can load at a more specific spot internal to the router\n- We can access the navigation `AbortSignal` in case the `lazy()` call gets interrupted\n- We can also load once and update the internal route definition so subsequent navigations don't have a repeated `lazy()` call\n- We don't have issue with knowing whether or not an `errorElement` exists since we will have updated the route prior to updating any UI state\n\nThis proved to work out quite well as we did our own POC so we went with this approach in the end. Now, any time we enter a `submitting`/`loading` state we first check for a `route.lazy` definition and resolve that promise first and update the internal route definition with the result.\n\nThe resulting API looks like this, assuming you want to load your homepage in the main bundle, but lazily load the code for the `/about` route. Note we're using the new `Component` API introduced along with this work.\n\n```jsx\n// app.jsx\nconst router = createBrowserRouter([\n  {\n    path: \"/\",\n    Component: Layout,\n    children: [\n      {\n        index: true,\n        Component: Home,\n      },\n      {\n        path: \"about\",\n        lazy: () => import(\"./about\"),\n      },\n    ],\n  },\n]);\n```\n\nAnd then your `about.jsx` file would export the properties to be lazily defined on the route:\n\n```jsx\n// about.jsx\nexport function loader() { ... }\n\nexport function Component() { ... }\n```\n\n## Choices\n\nHere's a few choices we made along the way:\n\n### Immutable Route Properties\n\nA route has 3 types of fields defined on it:\n\n- Path matching properties: `path`, `index`, `caseSensitive` and `children`\n  - While not strictly used for matching, `id` is also considered static since it is needed up-front to uniquely identify all defined routes\n- Data loading properties: `loader`, `action`, `hasErrorBoundary`, `shouldRevalidate`\n- Rendering properties: `handle` and the framework-aware `element`/`errorElement`/`Component`/`ErrorBoundary`\n\nThe `route.lazy()` method is focused on lazy-loading the data loading and rendering properties, but cannot update the path matching properties because we have to path match _first_ before we can even identify which matched routes include a `lazy()` function. Therefore, we do not allow path matching route keys to be updated by `lazy()`, and will log a warning if you return one of those properties from your lazy() method.\n\n## Static Route Properties\n\nSimilar to how you cannot override any immutable path-matching properties, you also cannot override any statically defined data-loading or rendering properties (and will log the a console warning if you attempt to). This allows you to statically define aspects that you don't need (or wish) to lazy load. Two potential use-cases her might be:\n\n1. Using a small statically-defined `loader`/`action` which just hits an API endpoint to load/submit data.\n   - In fact this is an interesting option we've optimized React Router to detect this and call any statically defined loader/action handlers in parallel with `lazy` (since `lazy` will be unable to update the `loader`/`action` anyway!). This will provide the ability to obtain the most-optimal parallelization of loading your component in parallel with your data fetches.\n2. Re-using a common statically-defined `ErrorBoundary` across multiple routes\n\n### Addition of route `Component` and `ErrorBoundary` fields\n\nIn React Router v6, routes define `element` properties because it allows static prop passing as well as fitting nicely in the JSX render-tree-defined route trees:\n\n```jsx\n<BrowserRouter>\n  <Routes>\n    <Route path=\"/\" element={<Homepage prop=\"value\" />} />\n  </Routes>\n</BrowserRouter>\n```\n\nHowever, in a React Router 6.4+ landscape when using `RouterProvider`, routes are defined statically up-front to enable data-loading, so using element feels arguably a bit awkward outside of a JSX tree:\n\n```js\nconst routes = [\n  {\n    path: \"/\",\n    element: <Homepage prop=\"value\" />,\n  },\n];\n```\n\nIt also means that you cannot easily use hooks inline, and have to add a level of indirection to access hooks.\n\nThis gets a bit more awkward with the introduction of `lazy()` since your file now has to export a root-level JSX element:\n\n```jsx\n// home.jsx\nexport const element = <Homepage />\n\nfunction Homepage() { ... }\n```\n\nIn reality, what we want in this \"static route definition\" landscape is just the component for the Route:\n\n```js\nconst routes = [\n  {\n    path: \"/\",\n    Component: Homepage,\n  },\n];\n```\n\nThis has a number of advantages in that we can now use inline component functions to access hooks, provide props, etc. And we also simplify the exports of a `lazy()` route module:\n\n```jsx\nconst routes = [\n  {\n    path: \"/\",\n    // You can include just the component\n    Component: Homepage,\n  },\n  {\n    path: \"/a\",\n    // Or you can inline your component and pass props\n    Component: () => <Homepage prop=\"value\" />,\n  },\n  {\n    path: \"/b\",\n    // And even use use hooks without indirection 💥\n    Component: () => {\n      let data = useLoaderData();\n      return <Homepage data={data} />;\n    },\n  },\n];\n```\n\nSo in the end, the work for `lazy()` introduced support for `route.Component` and `route.ErrorBoundary`, which can be statically or lazily defined. They will take precedence over `element`/`errorElement` if both happen to be defined, but for now both are acceptable ways to define routes. We think we'll be expanding the `Component` API in the future for stronger type-safety since we can pass it inferred-type `loaderData` etc. so in the future that _may_ become the preferred API.\n\n### Interruptions\n\nPreviously when a link was clicked or a form was submitted, since we had the `action`/`loader` defined statically up-front, they were immediately executed and there was no chance for an interruption _before calling the handler_. Now that we've introduced the concept of `lazy()` there is a period of time prior to executing the handler where the user could interrupt the navigation by clicking to a new location. In order to keep behavior consistent with lazily-loaded routes and statically defined routes, if a `lazy()` function is interrupted React Router _will still call the returned handler_. As always, the user can leverage `request.signal.aborted` inside the handler to short-circuit on interruption if desired.\n\nThis is important because `lazy()` is only ever run once in an application session. Once lazy has completed it updates the route in place, and all subsequent navigations to that route use the now-statically-defined properties. Without this behavior, routes would behave differently on the _first_ navigation versus _subsequent_ navigations which could introduce subtle and hard-to-track-down bugs.\n\nAdditionally, since `lazy()` functions are intended to return a static definition of route `loader`/`element`/etc. - if multiple navigations happen to the same route in parallel, the first `lazy()` call to resolve will \"win\" and update the route, and the returned values from any other `lazy()` executions will be ignored. This should not be much of an issue in practice though as modern bundlers latch onto the same promise for repeated calls to `import()` so in those cases the first call will still \"win\".\n\n### Error Handling\n\nIf an error is thrown by `lazy()` we catch that in the same logic as if the error was thrown by the `action`/`loader` and bubble it to the nearest `errorElement`.\n\n## Consequences\n\nNot so much as a consequence, but more of limitation - we still require the routing tree up-front for the most efficient data-loading. This means that we can't _yet_ support quite the same nested `<Routes>` use-cases as before (particularly with respect to microfrontends), but we have ideas for how to solve that as an extension of this concept in the future.\n\nAnother slightly edge-case concept we discovered is that in DIY SSR applications using `createStaticHandler` and `StaticRouterProvider`, it's possible to server-render a lazy route and send up its hydration data. But then we may _not_ have those routes loaded in our client-side hydration:\n\n```jsx\nconst routes = [{\n  path: '/',\n  lazy: () => import(\"./route\"),\n}]\nlet router = createBrowserRouter(routes, {\n  hydrationData: window.__hydrationData,\n});\n\n// ⚠️ At this point, the router has the data but not the route definition!\n\nReactDOM.hydrateRoot(\n  document.getElementById(\"app\")!,\n  <RouterProvider router={router} fallbackElement={null} />\n);\n```\n\nIn the above example, we've server-rendered our `/` route and therefore we _don't_ want to render a `fallbackElement` since we already have the SSR'd content, and the router doesn't need to \"initialize\" because we've provided the data in `hydrationData`. However, if we're hydrating into a route that includes `lazy`, then we _do_ need to initialize that lazy route.\n\nThe real solution for this is to do what Remix does and know your matched routes and preload their modules ahead of time and hydrate with synchronous route definitions. This is a non-trivial process through so it's not expected that every DIY SSR use-case will handle it. Instead, the router will not be initialized until any initially matched lazy routes are loaded, and therefore we need to delay the hydration or our `RouterProvider`.\n\nThe recommended way to do this is to manually match routes against the initial location and load/update any lazy routes before creating your router:\n\n```jsx\n// Determine if any of the initial routes are lazy\nlet lazyMatches = matchRoutes(routes, window.location)?.filter(\n  (m) => m.route.lazy\n);\n\n// Load the lazy matches and update the routes before creating your router\n// so we can hydrate the SSR-rendered content synchronously\nif (lazyMatches && lazyMatches.length > 0) {\n  await Promise.all(\n    lazyMatches.map(async (m) => {\n      let routeModule = await m.route.lazy!();\n      Object.assign(m.route, { ...routeModule, lazy: undefined });\n    })\n  );\n}\n\n// Create router and hydrate\nlet router = createBrowserRouter(routes)\nReactDOM.hydrateRoot(\n  document.getElementById(\"app\")!,\n  <RouterProvider router={router} fallbackElement={null} />\n);\n```\n\n[manually-code-split]: https://www.infoxicator.com/en/react-router-6-4-code-splitting\n[proposal]: https://github.com/remix-run/react-router/discussions/9826\n[poc]: https://github.com/remix-run/react-router/pull/9830\n"
  },
  {
    "path": "decisions/0003-data-strategy.md",
    "content": "# Data Strategy\n\nDate: 2024-01-31\n\nStatus: accepted\n\n## Context\n\nIn order to implement \"Single Fetch\" in Remix ([Issue][single-fetch-issue], [RFC][single-fetch-rfc]), we need to expose some level of control over the internal data fetching behaviors of the `@remix-run/router`. This way, while React Router will run loaders in parallel by default, Remix can opt-into making a single fetch call to the server for all loaders.\n\n## Decisions\n\n### `dataStrategy`\n\nTo achieve the above, we propose to add an optional `dataStrategy` config which can be passed in by the application. The idea is that `dataStrategy` will accept an array of `matches` to load and will return a parallel array of results for those matches.\n\n```js\nfunction dataStrategy(arg: DataStrategyFunctionArgs): DataResult[];\n\ninterface DataStrategyFunctionArgs<Context = any>\n  extends DataFunctionArgs<Context> {\n  matches: AgnosticDataStrategyMatch[];\n}\n\ninterface DataFunctionArgs<Context> {\n  request: Request;\n  params: Params;\n  context?: Context;\n}\n```\n\nThere's a [comment][responsibilities-comment] here from Jacob which does a good job of outlining the current responsibilities, but basically React Router in it's current state handles 4 aspects when it comes to executing loaders for a given URL - `dataStrategy` is largely intended to handle step 3:\n\n1. Match routes for URL\n2. Determine what routes to load (via `shouldRevalidate`)\n3. Call `loader` functions in parallel\n4. Decode Responses\n\n### Inputs\n\nThe primary input is `matches`, since the user needs to know what routes match and eed to have loaders executed. We also wanted to provide a way for the user to call the \"default\" internal behavior so they could easily change from parallel to sequential without having to re-invent the wheel and manually call loaders, decode responses, etc. The first idea for this API was to pass a `defaultStrategy(match)` parameter so they could call that per-match:\n\n```js\nfunction dataStrategy({ matches }) {\n  // Call in parallel\n  return Promise.all(matches.map(m => defaultStrategy((m))));\n\n  // Call sequentially\n  let results = []\n  for (let match of matches) {\n    results.push(await defaultStrategy(match))\n  }\n  return results;\n}\n```\n\n⚠️ `defaultStrategy` was eliminated in favor of `match.resolve`.\n\nWe also originally intended to expose a `type: 'loader' | 'action`' field as a way to presumably let them call `match.route.loader`/`match.route.action` directly - but we have since decided against that with the `match.resolve` API.\n\n⚠️ `type` was eliminated in favor of `match.resolve`.\n\n`dataStrategy` is control _when_ handlers are called, not _how_. RR is in charge of calling them with the right parameters.\n\n### Outputs\n\nOriginally, we planned on making the `DataResult` API public, which is a union of the different types of results (`SuccessResult`, `ErrorResult`, `RedirectResult`, `DeferResult`). However, as we kept evolving and did some internal refactoring to separate calling loaders from decoding results - we realized that all we really need is a simpler `HandlerResult`:\n\n```ts\ninterface HandlerResult {\n  type: ResultType.success | ResultType.error;\n  result: any;\n}\n```\n\nIf the user returns us one of those per-match, we can internally convert it to a `DataResult`.\n\n- If `result` is a `Response` then we can handle unwrapping the data and processing any redirects (may produce a `SuccessResult`, `ErrorResult`, or `RedirectResult`)\n- If `result` is a `DeferredData` instance, convert to `DeferResult`\n- If result is anything else we don't touch the data, it's either a `SuccessResult` or `ErrorResult` based on `type`\n  - This is important because it's lets the end user opt into a different decoding strategy of their choice. If they return us a Response, we decode it. If not, we don't touch it.\n\n### Decoding Responses\n\nInitially, we intended for `dataStrategy` to handle (3), and considered an optional `decodeResponse` API for (4) - but we decided that the decoding of responses was a small enough undertaking using standard Fetch APIs (i.e., `res.json`) that it didn't warrant a custom property - and they could just call those APIs directly. The `defaultStrategy` parameter would handle performing 3 the normal way that RR would.\n\n⚠️ `decodeResponse` is made obsolete by `HandlerResult`\n\n### Handling `route.lazy`\n\nThere's a nuanced step we missed in our sequential steps above. If a route was\nusing `route.lazy`, we may need to load the route before we can execute the `loader`. There's two options here:\n\n1. We pre-execute all `route.lazy` methods before calling `dataStrategy`\n2. We let `dataStrategy` execute them accordingly\n\n(1) has a pretty glaring perf issue in that it blocks _any_ loaders from running until _all_ `route.lazy`'s have resolved. So if route A is super small but has a slow loader, and route B is large but has a fast loader:\n\n```\n|-- route a lazy  -->                      |-- route a loader --------------->|\n|-- route b lazy  ------------------------>|-- route b loader -->             |\n```\n\nThis is no bueno. Instead, we want option (2) where the users can run these sequentially per-route - and \"loading the route\" is just part of the \"loading the data\" step\n\n```\n|-- route a lazy  -->|-- route a loader --------------->         |\n|-- route b lazy  ------------------------>|-- route b loader -->|\n```\n\nTherefore, we're introducing the concept of a `DataStrategyMatch` which is just like a `RouteMatch` but the `match.route` field is a `Promise<Route>`. We'll kick off the executions of route.lazy and then you can wait for them to complete prior to calling the loader:\n\n```js\nfunction dataStrategy({ matches, defaultStrategy }) {\n  return Promise.all(\n    matches.map((m) => match.route.then((route) => route.loader(/* ... */))),\n  );\n}\n```\n\nThere are also statically defined properties that live outside of lazy, so those are extended right onto `match.route`. This allows you to define loaders statically and run them in parallel with `route.lazy`:\n\n```js\nfunction dataStrategy({ matches, defaultStrategy }) {\n  // matches[0].route => Promise\n  // matches[0].route.id => string\n  // matches[0].route.index => boolean\n  // matches[0].route.path => string\n}\n```\n\n⚠️ This match.route as a function API was removed in favor of `match.resolve`\n\n### Handling `shouldRevalidate` behavior\n\nWe considered how to handle `shouldRevalidate` behavior. There's sort of 2 basic approaches:\n\n1. We pre-filter and only hand the user `matchesToLoad`\n2. We hand the user all matches and let them filter\n   - This would probably also require a new `defaultShouldRevalidate(match) => boolean` parameter passed to `dataStrategy`\n\nI _think_ (1) is preferred to keep the API at a minimum and avoid leaking into _other_ ways to opt-out of revalidation. We already have an API for that so let's lean into it.\n\nAdditionally, another big con of (2) is that if we want to let them make revalidation decisions inside `dataStrategy` - we need to expose all of the information required for that (`currentUrl`, `currentParams`, `nextUrl`, `nextParams`, `submission` info, `actionResult`, etc.) - the API becomes a mess.\n\nTherefore we are aiming to stick with one and let `shouldRevalidate` be the only way to opt-out of revalidation.\n\n### Handling actions and fetchers\n\nThus far, we've been mostly concerned with how to handle navigational loaders where they are multiple matched routes and loaders to run. But what about actions and fetchers where we only run a handler for a single leaf match? The quick answer to this is to just send a single-length array with the match in question:\n\n```js\n// loaders\nlet matchesToLoad = getMatchesToLoad(request, matches);\nlet results = await dataStrategy({\n  request,\n  params,\n  matches: matchesToLoad,\n  type: \"loader\",\n  defaultStrategy,\n});\n\n// action\nlet actionMatch = getTargetMatch(request, matches);\nlet actionResults = await dataStrategy({\n  request,\n  params,\n  matches: [actionMatch],\n  type: \"action\",\n  defaultStrategy,\n});\nlet actionResult = actionResults[0];\n\n// fetcher loader/action\nlet fetcherMatch = getTargetMatch(request, matches);\nlet fetcherResults = await dataStrategy({\n  request,\n  params,\n  matches: [fetcherMatch],\n  type: \"loader\", // or \"action\"\n  defaultStrategy,\n});\nlet fetcherResult = fetcherResults[0];\n```\n\nThis way, the user's implementation can just always operate on the `matches` array and it'll work for all use cases.\n\n```js\n// Sample strategy to run sequentially\nasync function dataStrategy({ request, params, matches, type }) {\n  let results = [];\n  for (let match of matches) {\n    let result = await match.route[type]({ request, params });\n    result.push(result);\n  }\n  return results;\n}\n```\n\n### What about middlewares?\n\nAs we thought more and more about this API, it became clear that the concept of \"process data for a route\" (step 3 above) was not necessarily limited to the `loader`/`action` and that there are data-related APIs on the horizon such as `middleware` and `context` that would also fall under the `dataStrategy` umbrella! In fact, a well-implemented `dataStrategy` could alleviate the need for first-class APIs - even if only initially. Early adopters could use `dataStrategy` to implement their own middlewares and we could see which patterns rise to the top and adopt them as first class `route.middleware` or whatever.\n\nSo how would middleware work? The general idea is that middleware runs sequentially top-down prior to the loaders running. And if you bring `context` into the equation - they also run top down and middlewares/loaders/actions receive the context from their level and above in the tree - but they do not \"see\" any context from below them in the tree.\n\nA user-land implementation turns out not to be too bad assuming routes define `middleware`/`context` on `handle`:\n\n```js\n// Assume routes look like this:\nlet route = {\n  id: \"parent\",\n  path: \"/parent\",\n  loader: () => {},\n  handle: {\n    // context can provide multiple keyed contexts\n    context: {\n      parent: () => ({ id: \"parent\" }),\n    },\n    // middleware receives context as an argument\n    middleware(context) {\n      context.parent.whatever = \"PARENT MIDDLEWARE\";\n    },\n  },\n};\n\nasync function dataStrategy({ request, params, matches, type }) {\n  // Run context/middleware sequentially\n  let contexts = {};\n  for (let match of matches) {\n    if (m.route.handle?.context) {\n      for (let [id, ctx] of Object.entries(m.route.handle.context)) {\n        contexts[key] = ctx();\n      }\n    }\n    if (m.route.handle?.middleware) {\n      m.route.handle.middleware(context);\n    }\n  }\n\n  // Run loaders in parallel (or run the solo action)\n  return Promise.all(\n    matches.map(async (m, i) => {\n      // Only expose contexts from this level and above\n      let context = matches.slice(0, i + 1).reduce((acc, m) => {\n        Object.keys(m.route.handle?.context).forEach((k) => {\n          acc[k] = contexts[k];\n        });\n        return acc;\n      }, {});\n      try {\n        return {\n          type: ResultType.data,\n          data: await m.route[type]?.({ request, params, context });,\n        };\n      } catch (error) {\n        return {\n          type: ResultType.error,\n          error,\n        };\n      }\n    })\n  );\n}\n```\n\n❌ Nope - this doesn't actually work!\n\nRemember above where we decided to _pre-filter_ the matches based on `shouldRevalidate`? That breaks any concept of middleware since even if we don't intend to load a route, we need to run middleware on all parents before the loader. So we _must_ expose at least the `matches` at or above that level in the tree - and more likely _all_ matches to `dataStrategy` if it's to be able to implement middleware.\n\nAnd then, once we expose _multiple_ matches - we need to tell the user if they're supposed to actually run the handlers on those matches or only on a leaf/target match.\n\nI think there's a few options here:\n\n**Option 1 - `routeMatches` and `handlerMatches`**\n\nWe could add a second array of the \"full\" set of matches for the route and then middleware would operate on that set, and handlers would operate on the filtered set (renamed to `handlerMatches`) here. This still preserves the pre-filtering and keeps `shouldRevalidate` logic out of `dataStrategy`.\n\n```js\nasync function dataStrategy({ request, params, routeMatches, handlerMatches, type }) {\n  // Run context/middleware sequentially\n  let contexts = {};\n  for (let match of routeMatches) { ... }\n\n  // Run loaders in parallel\n  return Promise.all(\n    handlerMatches.map(async (m, i) => { ... })\n  );\n}\n```\n\n**Option 2 - new field on `DataStrategyMatch`**\n\nSince we're already introducing a concept of a `DataStrategyMatch` to handle `route.lazy`, we could lean into that and expose something on those matches that indicate if they need to have their handler run or not?\n\n```js\n// Inside React Router, assume navigate from /a/ -> /b and we don't need to\n// re-run the root loader\nlet dataStrategyMatches = [{\n  route: { id: 'root', loader() {}, ... }\n  runHandler: false // determined via shouldRevalidate\n}, {\n  route: { id: 'b', loader() {}, ... }\n  runHandler: true // determined via shouldRevalidate\n}]\n```\n\nThen, the user could use this to differentiate between middlewares and handlers:\n\n```js\nasync function dataStrategy({ request, params, matches, type }) {\n  // Run context/middleware sequentially\n  let contexts = {};\n  for (let match of matches) { ... }\n\n  // Run loaders in parallel\n  let matchesToLoad = matches.filter(m => m.runHandler);\n  return Promise.all(\n    matchesToLoad.map(async (m, i) => { ... })\n  );\n}\n```\n\n**Option 3 - new function on `DataStrategyMatch`**\n\nExtending on the idea above - it all started to feel super leaky and full of implementation-details. Why are users manually filtering? Or manually passing parameters to loaders/actions? Using a `type` field to know which to call? Waiting on a `match.route` Promise before calling the loader?\n\nThat's wayyyy to many rough edges for us to document and users to get wrong (rightfully so!).\n\nWhy can't we just do it all? Let's wrap _all_ of that up into a single `match.resolve()` function that:\n\n- Waits for `route.lazy` to resolve (if needed)\n- No-ops if the route isn't supposed to revalidate\n  - Open question here if we return the _current_ data from these no-ops or return `undefined`?\n  - We decided _not_ to expose this data for now since we don't have a good use case\n- Knows whether to call the `loader` or the `action`\n- Allows users to pass _additional_ params to loaders/actions for middleware/context use cases.\n\n```js\n// Simplest case - call all loaders in parallel just like current behavior\nfunction dataStrategy({ matches }) {\n  // No more type, defaultStrategy, or match.route promise APIs!\n  return Promise.all(matches.map(match => {\n    // resolve `route.lazy` if needed and call loader/action\n    return m.resolve();\n  });\n}\n\n// More advanced case - call loader sequentially passing a context through\nasync function dataStrategy({ matches }) {\n  let ctx = {};\n  let results = [];\n  for (let match of matches) {\n    // You can pass a \"handlerOverride\" function to resolve giving you control\n    // over how/if to call the handler.  The argument passed to `handler` will\n    // be passed as the second argument to your `loader`/`action`:\n    // function loader({ request }, ctx) {...}\n    let result = await m.resolve((handler) => {\n      return handler(ctx);\n    });\n    results.push(result);\n  });\n  return results;\n}\n\n// More performant case leveraging a middleware type abstraction which lets loaders\n// still run in parallel after sequential middlewares:\nfunction dataStrategy({ matches }) {\n  // Can implement middleware as above since you now get all matches\n  let context = runMiddlewares(matches);\n\n  // Call all loaders in parallel (no params to pass) but you _can_ pass you\n  // own argument to `resolve` and it will come in as `loader({ request }, handlerArg)`\n  // So you can send middleware context through to loaders/actions\n  return Promise.all(matches.map(match => {\n    return m.resolve(context);\n  });\n\n  // Note we don't do any filtering above - if a match doesn't need to load,\n  // `match.resolve` is no-op.  Just like `serverLoader` is a no-op in `clientLoader`\n  // when it doesn't need to run\n}\n\n// Advanced case - single-fetch type approach\n// More advanced case - call loader sequentially passing a context through\nasync function dataStrategy({ matches }) {\n  let singleFetchData = await makeSingleFetchCall()\n  // Assume we get back:\n  // { data: { [routeId]: unknown }, errors: { [routeId]: unknown } }\n  let results = [];\n  for (let match of matches) {\n    // Don't even call the handler since we have the data we need from single fetch\n    let result = await m.resolve(() => {\n      if (singleFetchData.errors?.[m.route.id]) {\n        return {\n          type: 'error',\n          result: singleFetchData.errors?.[m.route.id]\n        }\n      }\n      return {\n        type: 'data',\n        result: singleFetchData.data?.[m.route.id]\n      }\n    });\n    results.push(result);\n  });\n  return results;\n}\n```\n\n## Status codes\n\nInitially, we thought we could just let the `handlerOverride`return or throw and then internally we could convert the returned/thrown valuer into a `HandlerResult`. However, this didn't work for the `unstable_skipActionRevalidation` behavior we wanted to implement with Single Fetch.\n\nIf users returned normal Response's it would be fine, since we could decode the response internally and also know the status. However, if user's wanted to do custom response decoding (i.e., use `turbo-stream` like we did in single fetch) then there was no way to return/throw data _and the status code from the response_ without introducing something like the `ErrorResponse` API which holds a status and data. We decided to make `HandlerResult` public API and put an optional `status` field on it.\n\nThis means that if you just call resolve with no `handlerOverride` you never need to know about `HandlerResult`. If you do pass a `handlerOverride`, then you need to return a proper HandlerResult with `type:\"data\"|\"error\"`.\n\n[single-fetch-issue]: https://github.com/remix-run/remix/issues/7641\n[single-fetch-rfc]: https://github.com/remix-run/remix/discussions/7640\n[responsibilities-comment]: https://github.com/remix-run/remix/issues/7641#issuecomment-1836635069\n"
  },
  {
    "path": "decisions/0003-infer-types-for-useloaderdata-and-useactiondata-from-loader-and-action-via-generics.md",
    "content": "# Infer types for `useLoaderData` and `useActionData` from `loader` and `action` via generics\n\nDate: 2022-07-11\n\nStatus: Superseded by [#0012](./0012-type-inference.md)\n\n## Context\n\nGoal: End-to-end type safety for `useLoaderData` and `useActionData` with great Developer Experience (DX)\n\nRelated discussions:\n\n- [remix-run/remix#1254](https://github.com/remix-run/remix/pull/1254)\n- [remix-run/remix#3276](https://github.com/remix-run/remix/pull/3276)\n\n---\n\nIn Remix v1.6.4, types for both `useLoaderData` and `useActionData` are parameterized with a generic:\n\n```tsx\ntype MyLoaderData = {\n  /* ... */\n};\ntype MyActionData = {\n  /* ... */\n};\n\nexport default function Route() {\n  const loaderData = useLoaderData<MyLoaderData>();\n  const actionData = useActionData<MyActionData>();\n  return <div>{/* ... */}</div>;\n}\n```\n\nFor end-to-end type safety, it is then the user's responsibility to make sure that `loader` and `action` also use the same type in the `json` generic:\n\n```ts\nexport const loader: LoaderFunction = () => {\n  return json<MyLoaderData>({\n    /* ... */\n  });\n};\n\nexport const action: ActionFunction = () => {\n  return json<MyActionData>({\n    /* ... */\n  });\n};\n```\n\n### Diving into `useLoaderData`'s and `useActionData`'s generics\n\nTracing through the `@remix-run/react` source code (v1.6.4), you'll find that `useLoaderData` returns an `any` type that is implicitly type cast to whatever type gets passed into the `useLoaderData` generic:\n\n```ts\n// https://github.com/remix-run/remix/blob/v1.6.4/packages/remix-react/components.tsx#L1370\nexport function useLoaderData<T = AppData>(): T {\n  return useRemixRouteContext().data; //\n}\n\n// https://github.com/remix-run/remix/blob/v1.6.4/packages/remix-react/components.tsx#L73\nfunction useRemixRouteContext(): RemixRouteContextType {\n  /* ... */\n}\n\n// https://github.com/remix-run/remix/blob/v1.6.4/packages/remix-react/components.tsx#L56\ninterface RemixRouteContextType {\n  data: AppData;\n  id: string;\n}\n\n// https://github.com/remix-run/remix/blob/v1.6.4/packages/remix-react/data.ts#L4\nexport type AppData = any;\n```\n\nBoiling this down, the code looks like:\n\n```ts\nlet data: any;\n\n// somewhere else, `loader` gets called an sets `data` to some value\n\nfunction useLoaderData<T>(): T {\n  return data; // <-- Typescript casts this `any` to `T`\n}\n```\n\n`useLoaderData` isn't basing its return type on how `data` was set (i.e. the return value of `loader`) nor is it validating the data.\nIt's just blindly casting `data` to whatever the user passed in for the generic `T`.\n\n### Issues with current approach\n\nThe developer experience is subpar.\nUsers are required to write redundant code for the data types that could have been inferred from the arguments to `json`.\nChanges to the data shape require changing _both_ the declared `type` or `interface` as well as the argument to `json`.\n\nAdditionally, the current approach encourages users to pass the same type to `json` with the `loader` and to `useLoaderData`, but **this is a footgun**!\n`json` can accept data types like `Date` that are JSON serializable, but `useLoaderData` will return the _serialized_ type:\n\n```ts\ntype MyLoaderData = {\n  birthday: Date;\n};\n\nexport const loader: LoaderFunction = () => {\n  return json<MyLoaderData>({ birthday: new Date(\"February 15, 1992\") });\n};\n\nexport default function Route() {\n  const { birthday } = useLoaderData<MyLoaderData>();\n  // ^ `useLoaderData` tricks Typescript into thinking this is a `Date`, when in fact it's a `string`!\n}\n```\n\nAgain, the same goes for `useActionData`.\n\n### Solution criteria\n\n- Return type of `useLoaderData` and `useActionData` should somehow be inferred from `loader` and `action`, not blindly type cast\n- Return type of `loader` and `action` should be inferred\n  - Necessarily, return type of `json` should be inferred from its input\n- No module side-effects (so higher-order functions like `makeLoader` is definitely a no).\n- `json` should allow everything that `JSON.stringify` allows.\n- `json` should allow only what `JSON.stringify` allows.\n- `useLoaderData` should not return anything that `JSON.parse` can't return.\n\n### Key insight: `loader` and `action` are an _implicit_ inputs\n\nWhile there's been interest in inferring the types for `useLoaderData` based on `loader`, there was [hesitance to use a Typescript generic to do so](https://github.com/remix-run/remix/pull/3276#issuecomment-1164764821).\nTypescript generics are apt for specifying or inferring types for _inputs_, not for blindly type casting output types.\n\nA key factor in the decision was identifying that `loader` and `action` are _implicit_ inputs of `useLoaderData` and `useActionData`.\n\nIn other words, if `loader` and `useLoaderData` were guaranteed to run in the same process (and not cross the network), then we could write `useLoaderData(loader)`, specifying `loader` as an explicit input for `useLoaderData`.\n\n```ts\n// _conceptually_ `loader` is an input for `useLoaderData`\nfunction useLoaderData<Loader extends LoaderFunction>(loader: Loader) {\n  /*...*/\n}\n```\n\nThough `loader` and `useLoaderData` exist together in the same file at development-time, `loader` does not exist at runtime in the browser.\nWithout the `loader` argument to infer types from, `useLoaderData` needs a way to learn about `loader`'s type at compile-time.\n\nAdditionally, `loader` and `useLoaderData` are both managed by Remix across the network.\nWhile it's true that Remix doesn't \"own\" the network in the strictest sense, having `useLoaderData` return data that does not correspond to its `loader` is an exceedingly rare edge-case.\n\nSame goes for `useActionData`.\n\n---\n\nA similar case is how [Prisma](https://www.prisma.io/) infers types from database schemas available at runtime, even though there are (exceedingly rare) edge-cases where that database schema _could_ be mutated after compile-time but before run-time.\n\n## Decision\n\nExplicitly provide type of the implicit `loader` input for `useLoaderData` and then infer the return type for `useLoaderData`.\nDo the same for `action` and `useActionData`.\n\n```ts\nexport const loader = async (args: LoaderArgs) => {\n  // ...\n  return json(/*...*/);\n};\n\nexport default function Route() {\n  const data = useLoaderData<typeof loader>();\n  // ...\n}\n```\n\nAdditionally, the inferred return type for `useLoaderData` will only include serializable (JSON) types.\n\n### Return `unknown` when generic is omitted\n\nOmitting the generic for `useLoaderData` or `useActionData` results in `any` being returned.\nThis hides potential type errors from the user.\nInstead, we'll change the return type to `unknown`.\n\n```ts\ntype MyLoaderData = {\n  /*...*/\n};\n\nexport default function Route() {\n  const data = useLoaderData();\n  // ^? unknown\n}\n```\n\nNote: Since this would be a breaking change, changing the return type to `unknown` will be slated for v2.\n\n### Deprecate non-inferred types via generics\n\nPassing in a non-inferred type for `useLoaderData` is hiding an unsafe type cast.\nUsing the `useLoaderData` in this way will be deprecated in favor of an explicit type cast that clearly communicates the assumptions being made:\n\n```ts\ntype MyLoaderData = {\n  /*...*/\n};\n\nexport default function Route() {\n  const dataGeneric = useLoaderData<MyLoaderData>(); // <-- will be deprecated\n  const dataCast = useLoaderData() as MyLoaderData; // <- use this instead\n}\n```\n\n## Consequences\n\n- Users can continue to provide non-inferred types by type casting the result of `useLoaderData` or `useActionData`\n- Users can opt-in to inferred types by using `typeof loader` or `typeof action` at the generic for `useLoaderData` or `useActionData`.\n- Return types for `loader` and `action` will be the sources-of-truth for the types inferred for `useLoaderData` and `useActionData`.\n- Users do not need to write redundant code to align types across the network\n- Return type of `useLoaderData` and `useActionData` will correspond to the JSON _serialized_ types from `json` calls in `loader` and `action`, eliminating a class of errors.\n- `LoaderFunction` and `ActionFunction` should not be used when opting into type inference as they override the inferred return types.[^1]\n\n🚨 Users who opt-in to inferred types **MUST** return a `TypedResponse` from `json` and **MUST NOT** return a bare object:\n\n```ts\nconst loader = () => {\n  // NO\n  return { hello: \"world\" };\n\n  // YES\n  return json({ hello: \"world\" });\n};\n```\n\n[^1]: The proposed `satisfies` operator for Typescript would let `LoaderFunction` and `ActionFunction` enforce function types while preserving the narrower inferred return type: https://github.com/microsoft/TypeScript/issues/47920\n"
  },
  {
    "path": "decisions/0004-streaming-apis.md",
    "content": "---\ntitle: Remix (and React Router) Streaming APIs\n---\n\n# Title\n\nDate: 2022-07-27\n\nStatus: accepted\n\n## Context\n\nRemix aims to provide first-class support for React 18's streaming capabilities. Throughout the development process we went through many iterations and naming schemes around the APIs we plan to build into Remix to support streaming, so this document aims to lay out the final names we chose and the reasons behind it.\n\nIt's also worth nothing that even in a single-page-application without SSR-streaming, the same concepts still apply so these decisions were made with React Router 6.4.0 in mind as well - which will support the same Data APIs from Remix.\n\n## Decision\n\nStreaming in Remix can be thought of as having 3 touch points with corresponding APIs:\n\n1. _Initiating_ a streamed response in your `loader` can be done by returning a `defer(object)` call from your `loader` in which some of the keys on `object` are `Promise` instances\n2. _Accessing_ a streamed response from `useLoaderData`\n   1. No new APIs here - when you return a `defer()` response from your loader, you'll get `Promise` values inside your `useLoaderData` object 👌\n3. _Rendering_ a streamed value (with fallback and error handling) in your component\n   1. You can render a `Promise` from `useLoaderData()` with the `<Await resolve={data.promise}>` component\n   2. `<Await>` accepts an `errorElement` prop to handle error UI\n   3. `<Await>` should be wrapped with a `<React.Suspense>` component to handle your loading UI\n\n## Details\n\nIn the spirit of `#useThePlatform` we've chosen to leverage the `Promise` API to represent these \"eventually available\" values. When Remix receives a `defer()` response back from a `loader`, it needs to serialize that `Promise` over the network to the client application (prompting Jacob to coin the phrase [_\"promise teleportation over the network\"_][promise teleportation] 🔥).\n\n### Initiating\n\nIn order to initiate a streamed response in your `loader`, you can use the `defer()` utility which accepts a JSON object with `Promise` values from your `loader`.\n\n```tsx\nexport async function loader() {\n  return defer({\n    // Await this, don't stream\n    critical: await fetchCriticalData(),\n    // Don't await this - stream it!\n    lazy: fetchLazyData(),\n  });\n}\n```\n\nBy not using `await` on `fetchLazyData()` Remix knows that this value is not ready yet _but eventually will be_ and therefore Remix will leverage a streamed HTTP response allowing it to send up the resolved/rejected value when available. Essentially serializing/teleporting that Promise over the network via a streamed HTTP response.\n\nJust like `json()`, the `defer()` will accept a second optional `responseInit` param that lets you customize the resulting `Response` (i.e., in case you need to set custom headers).\n\nThe name `defer` was settled on as a corollary to `<script defer>` which essentially tells the browser to _\"fetch this script now but don't delay document parsing\"_. In a similar vein, with `defer()` we're telling Remix to _\"fetch this data now but don't delay the HTTP response\"_.\n\nWe decided _not_ to support naked objects due to the ambiguity that would be introduced:\n\n```tsx\n// NOT VALID CODE - This is just an example of the ambiguity that would have\n// been introduced had we chosen to support naked objects :)\n\n// This would NOT be streamed\nfunction exampleLoader1() {\n  return Promise.resolve(5);\n}\n\n// This WOULD be streamed\nfunction exampleLoader2() {\n  return {\n    value: Promise.resolve(5),\n  };\n}\n\n// This would NOT be streamed\nfunction exampleLoader3() {\n  return {\n    value: {\n      nested: Promise.resolve(5),\n    },\n  };\n}\n```\n\n<details>\n  <summary>Other considered API names:</summary>\n  <br/>\n  <ul>\n    <li><code>deferred()</code> - This is just a bit of a weird word that doesn't have much pre-existing semantic meaning. Is this the <code>jQuery.Deferred</code> thing from back in the day? Remix in general wants to avoid needlessly introducing net-new language to an already convoluted landscape!</li>\n    <li><code>stream()</code> - We also thought <code>stream</code> might be a good name since that's what the call is telling Remix to do - stream the responses down to the browser. But - this is also potentially misleading because stream is ambiguous in ths case. Developers may mistakenly think that this gives them back a <code>Stream</code> instance and they can arbitrarily send multiple chunks of data down to the browser over time. This is not how the current API works - but also seems like a really interesting idea for Remix to consider in the future, so we wanted to keep the <code>stream()</code> name available for future use cases.</li>\n  </ul>\n</details>\n\n### Accessing\n\nNo new APIs are needed for the \"Accessing\" stage 🎉. Since we've \"teleported\" these promises over the network, you can access them in your components just as you would with any other data returned from your loader. This value will always be a `Promise`, even after it's been settled.\n\n```tsx\nfunction Component() {\n  const data = useLoaderData();\n  // data.critical is a resolved value\n  // data.lazy is a Promise\n}\n```\n\n### Rendering\n\nIn order to render your `Promise` values from `useLoaderData()`, Remix provides a new `<Await>` component which handles rendering the resolved value, or propagating the rejected value through an `errorElement` or further upwards to the Route-level error boundaries. In order to access the resolved or rejected values, there are two new hooks that only work in the context of an `<Await>` component - `useAsyncValue()` and `useAsyncError()`.\n\nThis examples shows the full set of render-time APIs:\n\n```tsx\nfunction Component() {\n  const data = useLoaderData(); // data.lazy is a Promise\n\n  return (\n    <React.Suspense fallback={<p>Loading...</p>}>\n      <Await resolve={data.lazy} errorElement={<MyError />}>\n        <MyData />\n      </Await>\n    </React.Suspense>\n  );\n}\n\nfunction MyData() {\n  const value = useAsyncValue(); // Get the resolved value\n  return <p>Resolved: {value}</p>;\n}\n\nfunction MyError() {\n  const error = useAsyncError(); // Get the rejected value\n  return <p>Error: {error.message}</p>;\n}\n```\n\nNote that `useAsyncValue` and `useAsyncError` only work in the context of an `<Await>` component.\n\nThe `<Await>` name comes from the fact that for these lazily-rendered promises, we're not `await`-ing the promise in our loader, so instead we need to `<Await>` the promise in our render function and provide a fallback UI. The `resolve` prop is intended to mimic how you'd await a resolved value in plain Javascript:\n\n```tsx\n// This JSX:\n<Await resolve={promiseOrValue} />;\n\n// Aims to resemble this Javascript:\nconst value = await Promise.resolve(promiseOrValue);\n```\n\nJust like `Promise.resolve` can accept a promise or a value, `<Await resolve>` can also accept a promise or a value. This is really useful in case you want to AB test `defer()` responses in the loader - you don't need to change the UI code to render the data.\n\n```tsx\nexport async function loader({ request }: LoaderArgs) {\n  const shouldAwait = isUserInTestGroup(request);\n  return {\n    maybeLazy: shouldAwait ? await fetchData() : fetchData(),\n  };\n}\n\nfunction Component() {\n  const data = useLoaderData();\n\n  // No code forks even if data.maybeLazy is not a Promise!\n  return (\n    <React.Suspense fallback={<p>Loading...</p>}>\n      <Await resolve={data.maybeLazy} errorElement={<MyError />}>\n        <MyData />\n      </Await>\n    </React.Suspense>\n  );\n}\n```\n\n**Additional Notes on `<Await>`**\n\nIf you prefer the render props pattern, you can bypass `useAsyncValue()` and just grab the value directly:\n\n```tsx\n<Await resolve={data.lazy}>{(value) => <p>Resolved: {value}</p>}</Await>\n```\n\nIf you do not provide an `errorElement`, then promise rejections will bubble up to the nearest Route-level error boundary and be accessible via `useRouteError()`.\n\n<details>\n  <summary>Other considered API names:</summary>\n  <br>\n  <p>We originally implemented this as a <code>&lt;Deferred value={promise} fallback={&lt;Loader /&gt;} errorElement={&lt;MyError/&gt;} /></code>, but eventually we chose to remove the built-in <code>&lt;Suspense&gt;</code> boundary for better composability and eventual use with <code>&lt;SuspenseList&gt;</code>.  Once that was removed, and we were only using a <code>Promise</code> it made sense to move to a generic <code>&lt;Await&gt;</code> component that could be used with <em>any</em> promise, not just those coming from <code>defer()</code> in a <code>loader</code></p>\n\n  <p>We also considered various alternatives for the hook names - most notably `useResolvedValue`/`useRejectedValue`.  However, these were a bit too tightly coupled to the `Promise` nomenclature.  Remember, `Await` supports non-Promise values as well as render-errors, so it would be confusing if `useResolvedValue` was handing you a non-Promise value, or if `useRejectedValue` was handing you a render error from a resolved `Promise`.  `useAsyncValue`/`useAsyncError` better encompasses those scenarios as well.</p>\n</details>\n\n## React Router Notes\n\nWith the presence of the `<Await>` component in React Router and because the Promise's don't have to be serialized over the network - you can _technically_ just return raw Promise values on a naked object from your loader. However, this is strongly discouraged because the router will be unaware of these promises and thus won't be able to cancel them if the user navigates away prior to the promise settling.\n\nBy forcing users to call the `defer()` utility, we ensure that the router is able to track the in-flight promises and properly cancel them. It also allows us to handle synchronous rendering of promises that resolve prior to other critical data. Without the `defer()` utility these raw Promises would need to be thrown by the `<Await>` component to the `<Suspense>` boundary a single time to unwrap the value, resulting in a UI flicker.\n\n[promise teleportation]: https://twitter.com/ebey_jacob/status/1548817107546095616\n"
  },
  {
    "path": "decisions/0005-remixing-react-router.md",
    "content": "# Remixing React Router\n\nDate: 2022-07-29\n\nStatus: accepted\n\n- [Remixing React Router](#remixing-react-router)\n  - [Context](#context)\n  - [Decisions](#decisions)\n    - [Move the bulk of logic to a framework-agnostic router](#move-the-bulk-of-logic-to-a-framework-agnostic-router)\n    - [Inline the `history` library into the router](#inline-the-history-library-into-the-router)\n    - [`fetcher.load()` participates in revalidations](#fetcherload-participates-in-revalidations)\n    - [`useTransition` renamed to `useNavigation`](#usetransition-renamed-to-usenavigation)\n    - [Navigations/Fetchers state structure changes](#navigationsfetchers-state-structure-changes)\n    - [`<Form method=\"get\">` is no longer a \"submission\"](#form-methodget-is-no-longer-a-submission)\n    - [Form automatic replace behavior](#form-automatic-replace-behavior)\n    - [`unstable_shouldReload` stabilized as `shouldRevalidate`](#unstable_shouldreload-stabilized-as-shouldrevalidate)\n    - [`<ScrollRestoration getKey>` prop](#scrollrestoration-getkey-prop)\n    - [`<Link preventScrollReset>` prop](#link-preventscrollreset-prop)\n    - [`useRevalidator()` hook](#userevalidator-hook)\n    - [No distinction between Error and Catch boundaries](#no-distinction-between-error-and-catch-boundaries)\n    - [`Request.signal` instead of `signal` param](#requestsignal-instead-of-signal-param)\n    - [React-Router API surface](#react-router-api-surface)\n\n## Context\n\nIn [Remixing React Router][remixing router], Ryan gives an overview of the work we started out to do in bringing the data APIs from Remix (loaders, actions, fetchers) over to `react-router`. We made _many_ decisions along the way that we'll document here. In some cases we decided to proceed with behavior that is different from that of Remix today, or add net-new behavior that does not currently exist in Remix. We'll identify those cases as necessary and provide rationale for the divergence and how we plan to support backwards compatibility.\n\n## Decisions\n\n### Move the bulk of logic to a framework-agnostic router\n\nThankfully this decision was sort of already made by Ryan. Maybe a surprise to some, maybe not, the current transition manager doesn't import or reference `react` or `react-router` a single time. This is by design because the logic being handled has nothing to do with how to render the UI layer. It's all about \"what route am I on?\", \"what route am I going to?\", \"how do I load data for the next route?\", \"how do I interrupt ongoing navigations?\" etc. None of these decisions actually _care_ about how the route and its data will eventually be rendered. Instead, the router simply needs to know whether given routes _have_ components and/or error boundaries - but it doesn't need to know about them or how to render them.\n\nThis is a huge advantage since it's a strict requirement in order to eventually support UI libraries other than React (namely Preact and Vue). So in the end, we have a `@remix-run/router` package with _zero_ dependencies 🔥.\n\n### Inline the `history` library into the router\n\n`react-router@6.3` currently relies on the `history@5` package. When we first started the work, we were intending to bring `history` into the `react-router` repo and create `history@6` and it would still be a standalone package and a dependency of `@remix-run/router`. However, 3 things pushed us in a different direction and caused us to just make history a single file inside of the router, and treat it more as an implementation detail.\n\n**1. History is an implementation detail in a data-aware landscape**\n\nNow that the router is data-aware, it has to manage _both_ route data loading/mutations and the URL (or in-memory location state but for simplicity let's just talk in terms of browser-routers here). In `react-router@6.3`/`history@5` the router was purely _reactive_. It listened for `history` changes and rendered the proper UI. So if a user clicked a link, it updated `history` _and then_ informed the router \"hey you should render this new location\".\n\nThis is no longer the case in a data-aware landscape. Now, when a user clicks a link, we need to first tell the router \"hey the user _intends_ to go to this location.\" In response to that the router can initiate some data fetches but during these fetches we're still on the old page! The user is still looking at the old content, and the URL should reflect that. This fits with the \"browser emulator\" concept as well. If you had a non-JS landscape and a user clicked a link from `/a -> /b` and the server took 5 seconds to send back a response for `/b` - during that time the browser URL bar shows the URL and title for `/a` and a little spinner in the tab. This is exactly how we built the router, it first loads data, then it updates state and tells history to update the URL.\n\nThere's one caveat here when it comes to back/forward button usage. When the user navigates back/forward in the history stack we get a `popstate` event _but the URL has already been updated_. So the best we can do there is react to the new URL. This is _not_ what the browser would do in a non-JS world, but we really have no choice.\n\nAll this being said - history is no longer a simple process of \"update the URL then tell the router to re-render\". History and routing are far more intertwined and behave slightly differently for PUSH/REPLACE than they do for POP navigations. For PUSH/REPLACE we go `router.navigate -> load data -> update state -> update history`, but for POP we `update history -> router.navigate -> load data -> update state`. So in PUSH, the router informs history. But in POP, history informs the router. These nuances made sense to keep the router as the public API and make history more of an internal implementation detail.\n\n**2. History is being superseded via the Navigation API**\n\nWith the pending [Navigation API][navigation api] in the works, there's potentially a not-too-distant future where we aren't using `window.history` at all or in the same way, so by moving our own `history` abstraction to an implementation detail we keep ourselves better poised to adopt the Navigation API in a non-breaking manner.\n\n**3. Initial implementations required it**\n\nIn the first implementations we actually didn't touch the internals of `BrowserRouter` and it's non-data-aware counterparts. And due to the changes we made in `history` to not notify listeners on PUSH/REPLACE wouldn't work. So for a very short period, we actually had both history v5 and this new internal history so we _couldn't_ publish it as history v6 since you can't have multiple dependent versions. Eventually, this went away as we added a `v5Compat` flag to the new history so it could behave like v5 used to when needed.\n\n### `fetcher.load()` participates in revalidations\n\nIn Remix, if you have data on a page from a `fetcher.load()` and you submit a mutation - those fetchers don't revalidate so the data may now be stale if the mutation impacted it. We've changed this in `@remix-run/router` such that revalidation updates _all_ active loaded data including route loaders as well as active `fetcher.load()` calls. These can be opted out of using the normal `shouldRevalidate()` method\n\n**Backwards Compatibility**\n\nWe categorize this as a bug fix since fetchers get stale in current Remix apps\n\n### `useTransition` renamed to `useNavigation`\n\nThis was done for two reasons:\n\n- Avoid confusion with the [`useTransition`][react usetransition] hook in React 18\n- It's more semantically correct because a \"navigation\" is what you trigger as a result of `router.navigate()` or `useNavigate()`\n\n**Backwards Compatibility**\n\nWe plan to export `useNavigation` in Remix and encourage folks to switch, but we will continue to include `useTransition` in a deprecated fashion\n\n### Navigations/Fetchers state structure changes\n\n**`useTransition().type` is removed**\n\nIn Remix, the `useTransition` hook returned a Transition object which had a `state` property of `\"idle\" | \"loading\" | \"submitting\"`. It also had a `type` property which represented sort of \"sub-states\" such as `\"normalLoad\" | \"actionReload\" | \"loaderRedirect\"` etc. In React Router we chose to get rid of the `type` field for 2 reasons:\n\n1. In practice, we found that the _vast_ majority of the time all you needed to reference was the `state`\n2. For scenarios in which you really do need to distinguish, we are pretty sure that in all cases, you can deduce the `type` from `state`, current location (`useLocation`), next location (`useNavigation().location`), and submission info (`useNavigation().formData`).\n\n**`useTransition().submission` is flattened**\n\nAnother area that changes is the `useTransition().submission` property was removed. We found that in practice folks never really needed the submission as a standalone thing, and instead always just cared about the `formMethod` or `formData`. So we flattened them onto the navigation, so `useNavigation()` will return an object of the format:\n\n```\n{\n  state: \"idle\" | \"loading\" | \"submitting\";\n  location: Location;\n  formMethod?: FormMethod;\n  formAction?: string;\n  formEncType?: FormEncType;\n  formData?: FormData;\n}\n```\n\n**Backwards Compatibility**\n\nWe plan to remain backwards compatible here in Remix. Very likely we'll expose the `useNavigation` hook and encourage users to move to that. And then `useTransition` will remain in a deprecated state and it will call `useNavigation` and then backfill the `type` and `submission` properties.\n\n### `<Form method=\"get\">` is no longer a \"submission\"\n\nFunctionally, these two bits of code are identical, with the only difference being that in the `<form>` case you let the user determine the query value.\n\n```html\n<a href=\"/search?query=matt\">Search</a>\n\n<form action=\"/search\">\n  <input name=\"query\" value=\"matt\" />\n  <button type=\"submit\">Search</button>\n</form>\n```\n\nBut, in Remix we were considering the latter a \"submission\" such that `useTransition().state === \"submitting\"`. In order to ensure our \"navigations\" reflect the browser behavior, we have changed this in the router such that GET Form submissions result in `useNavigation().state === \"loading\"`.\n\n**Backwards Compatibility**\n\nThis will be handled in the deprecated `useTransition` hook along with the backfill of `type` and `submission` properties\n\n### Form automatic replace behavior\n\nWhen performing POST navigations, you don't want to end up with a duplicate entry in the history stack which makes back-button routing weird when going through the same page twice. This is further complicated in browsers that hang onto the submission info and thus have to prompt you to warn you of re-submitting your data. We'll look at a few examples to demonstrate, but the intention is that the default behavior of the router should ensure that you can't get yourself into this double-history-entry situation when using `<Form method=\"post\">` submission navigations.\n\nNormal POST submissions that do not redirect will use a `REPLACE`:\n\n- User is on `/` (history stack is `[/]`)\n- Navigates to `/login` (history stack is `[/, /login]`)\n- Fills out and submits the `<Form method=\"post\">`\n- Action does not redirect\n  - At this point, if the action returns a non-redirect and we were to PUSH the navigation we'd end up with a history stack of `[/, /login, /login]` and the user would be in a scenario where it would take them 2 back buttons to get \"through\" the login page from a subsequent route.\n  - To avoid this, when a POST submission does not return a redirect, the router will REPLACE in the history stack, leaving us at `[/, /login]` and avoiding the duplicate history entry\n\nNormal POST submissions that _do_ redirect will use `PUSH` for the redirect:\n\n- User is on `/` (history stack is `[/]`)\n- Navigates to `/login` (history stack is `[/, /login]`)\n- Fills out and submits the `<Form method=\"post\">`\n- Action redirects to `/private`\n  - If we treated this redirect as a REPLACE, we'd be replacing the _initial_ navigation to `/login` since we haven't yet touched history for the POST. This would leave the history stack as `[/, /private]` and we'd lose the fact that we were ever at the login page.\n  - Instead when an action redirects, we'll use a PUSH and in this case the history stack would become `[/, /login, /private]` and the user would be able to navigate back through the login page and to the home page\n\nNote: User's can still be explicit here and use `<Form method=\"post\" replace={shouldReplace}>` and the router will respect the value passed to `replace`.\n\n### `unstable_shouldReload` stabilized as `shouldRevalidate`\n\nWe stabilized the API for when a given route loader should re-run, and changed the name to align with the \"revalidation\" nomenclature and the `useRevalidator` hook. We also leave more control in the hands of the user here. In Remix there were some cases in which you _could not_ opt out of revalidation and if your method did run, you had full control and couldn't necessarily handle one edge case and then say \"do what you otherwise would have done\".\n\nNow, if you provide a `shouldRevalidate` method we will call it during all revalidations and provide you a `defaultShouldRevalidate` boolean value. This allows you to opt out of any revalidation, and also code your own logic to fallback on our default choice:\n\n```tsx\nfunction shouldRevalidate({ defaultShouldRevalidate }) {\n  // Don't revalidate for this case\n  if (someEdgeCase()) {\n    return false;\n  }\n\n  // Otherwise, do what we would have done by default\n  return defaultShouldRevalidate;\n}\n```\n\n### `<ScrollRestoration getKey>` prop\n\nIn Remix, the `<ScrollRestoration>` component made an assumption that we would always restore scroll position based on `location.key`. If the key was the same as a prior location we knew the scroll position for, then we knew you had been there before and we should restore. This works great for back/forward navigations but it's a bit overly restrictive. You cannot choose to restore scroll based on anything other than `key`.\n\nTwitter has a great implementation of this as you click around in their left nav bar - your tweet feed is always at the same place when you click back to it - even though it's a _new_ location in the history stack. This is because they're restoring by pathname here instead of `location.key`. Or maybe you want to maintain scroll position for all routes under a given pathname and you thus want to use a portion of the pathname as the scroll restoration key.\n\nIn React Router we now accept an optional `<ScrollRestoration getKey>` prop where you provide a function that returns the key to use for scroll restoration:\n\n```ts\nfunction getKey(location: Location, matches: DataRouteMatch[]) {\n  // Restore by pathname on /tweets\n  if (location.pathname === \"/tweets\") {\n    return location.pathname;\n  }\n  // Otherwise use the key\n  return location.key;\n}\n```\n\n**Backwards Compatibility**\n\nWe're ok here since the new prop is optional and defaults to using `location.key`\n\n### `<Link preventScrollReset>` prop\n\nIn addition to `<ScrollRestoration>` handling \"restoring\" scroll position on previously visited routes. It also handles \"resetting\" scroll position back to the top on _new_ routes. This is not always desirable if you're clicking around inside a tabbed view or something, so we've introduced a new `<Link preventScrollReset>` prop that lets you disable the scroll reset behavior _for a given navigation_. Note that this \"resetting\" logic happens if and only if we cannot restore scroll to a previously known location for that scroll restoration key.\n\n### `useRevalidator()` hook\n\nThis has been a long time coming - see https://github.com/remix-run/remix/discussions/1996 🙂\n\n### No distinction between Error and Catch boundaries\n\nThe differentiation between error and catch proved to be a bit vague over time and a source of confusion for developers. We chose to go with just a single `errorElement` in the router for simplicity. If you throw anything, it ends up in the error boundary (available via `useRouteError`) and propagates accordingly. With this approach we leave the control in the developers hands and it's easy to maintain a similar split if desired:\n\n```tsx\nfunction NewErrorBoundary() {\n  const error = useRouteError();\n\n  if (error instanceof Response) {\n    return <MyOldCatchBoundary error={error} />;\n  } else {\n    return <MyOldErrorBoundary error={error} />;\n  }\n}\n```\n\n**Backwards Compatibility**\n\nWe have a few options here. In all cases, Remix v1 will provide an internal `errorElement` implementation that will need to do some forking to maintain backwards compatibility.\n\n1. We could introduce a new `ErrorComponent` in Remix v1 and deprecate `ErrorBoundary`/`CatchBoundary` (and eventually drop them in v2)\n   1. Chose this over `ErrorElement` since the thing being exported has not been through `React.createElement`\n2. We could maintain the same behavior of `ErrorBoundary`/`CatchBoundary` in v1 and plan to drop` CatchBoundary` in v2 and send everything to `ErrorBoundary`\n3. Keep the name `ErrorBoundary` and introduce a flag in `remix.config.js` to opt into the new behavior where all errors go to the `ErrorBoundary` and Remix stops separating them out to the catch boundary\n\nThe current favorite is likely option 3, which keeps the most semantic naming for Remix v2 while allowing users to start migrating to the new behavior in v1, thus easing their eventual upgrade to Remix v2.\n\n### `Request.signal` instead of `signal` param\n\nWe dropped the `signal` parameter to loaders and actions because an incoming `Request` already has its own signal!\n\n**Backwards Compatibility**\n\nWe'll need to re-expose the `request.signal` as a standalone `signal` in Remix\n\n### React-Router API surface\n\nInitially, we chose to align closely with the existing `react-router` APIs and introduced a `<DataBrowserRouter>` component (and it's memory/hash siblings) that would internally read the routes and create a router singleton upon first render. But as time went on we noticed some rough non-obvious foot guns with this approach, so we changed it up in [#9227][remove-singleton-pr]. Here's a few of the headaches it was causing:\n\n- Unit tests were a pain because you need to find a way to reset the singleton in-between tests\n  - We used a `_resetModuleScope` method for our tests\n  - ...but this wasn't't exposed to users who may want to do their own tests around our router\n- The JSX children `<Route>` objects caused non-intuitive behavior based on idiomatic react expectations\n  - Conditional runtime `<Route>`'s wouldn't get picked up\n  - Adding new `<Route>`'s during local dev wouldn't get picked up during HMR\n  - Using external state in your elements doesn't work as one might expect (see [#9225][singleton-state-issue])\n\nInstead, we lifted the singleton out into user-land, so that they create the router singleton and manage it outside the react tree - which is what react 18 is encouraging with `useSyncExternalStore` anyways! This also means that since users create the router - there's no longer any difference in the rendering aspect for memory/browser/hash routers (which only impacts router/history creation) - so we got rid of those and trimmed to a simple `RouterProvider`:\n\n```tsx\n// Before\nfunction OldApp() {\n  return (\n    <DataBrowserRouter>\n      <Route path=\"/\" element={<Layout />}>\n        <Route index element={<Home />} />\n      </Route>\n    </DataBrowserRouter>\n  );\n}\n```\n\n```tsx\n//After\nconst router = createBrowserRouter([\n  {\n    path: \"/\",\n    element: <Layout />,\n    children: [\n      {\n        index: true,\n        element: <Home />,\n      },\n    ],\n  },\n]);\n\nfunction NewApp() {\n  return <RouterProvider router={router} />;\n}\n```\n\nIf folks still prefer the JSX notation, they can leverage `createRoutesFromElements` (aliased from `createRoutesFromChildren` since they are not \"children\" in this usage):\n\n```tsx\nconst routes = createRoutesFromElements(\n  <Route path=\"/\" element={<Layout />}>\n    <Route index element={<Home />} />\n  </Route>,\n);\nconst router = createBrowserRouter(routes);\n\nfunction App() {\n  return <RouterProvider router={router} />;\n}\n```\n\nAnd now they can also hook into HMR correctly for router disposal:\n\n```\nif (import.meta.hot) {\n  import.meta.hot.dispose(() => router.dispose());\n}\n```\n\nAnd finally since `<RouterProvider>` accepts a router, it makes unit testing easer since you can create a fresh router with each test.\n\n[remixing router]: https://remix.run/blog/remixing-react-router\n[navigation api]: https://developer.chrome.com/docs/web-platform/navigation-api/\n[react usetransition]: https://reactjs.org/docs/hooks-reference.html#usetransition\n[remove-singleton-pr]: https://github.com/remix-run/react-router/pull/9227\n[singleton-state-issue]: https://github.com/remix-run/react-router/pull/9225\n"
  },
  {
    "path": "decisions/0006-linear-workflow.md",
    "content": "# Linear Workflow\n\nDate: 2022-09-06\n\nStatus: accepted\n\n## Context\n\nThe Remix development team uses Linear internally to manage and prioritize ongoing development of Remix and React-Router. In order to keep this process flowing smoothly, we document here the set of established workflows within Linear, so developers have a documented process to refer to if questions should arise.\n\n## Linear Terms\n\nThis document will use many of the following Linear terms:\n\n- **Issue** - A ticket/task in the Linear system that describes a unit of work to be completed\n- **Cycle** - A group of issues aimed to be completed within a given 1-week period (similar to a \"sprint\" in other Agile workflows)\n- **Project** - A group of issues that comprise a larger scope of work (for example, a new feature)\n  - Projects will almost always span multiple Cycles.\n- **Status** - The state of a given Issue (Todo, In Progress, Done, etc.)\n- **Assignee** - The person the Issue is currently assigned to - this indicates who is expected to take the next action to move the Issue forward\n- **Label** - Linear Issues can be assigned one or more Labels for filtering/searching\n\n## Decision\n\n### Status Definitions\n\nThe Linear workflow is governed by the **Status** field, which has the following options:\n\n- **Triage** - Issue is not yet reviewed and we have no idea if we'll actually do this work\n- **Backlog** - Issue has been accepted as something we would like to do, but is not currently planned\n- **Todo** - Issue has been planned for a given Cycle of work\n- **In Progress** - Issue is actively being worked on in the active Cycle\n- **In Review** - Issue is completed and in a PR receiving feedback\n- **Needs Feedback** - Developer is blocked and needs feedback from someone else before they can be unblocked\n- **Done** - Issue has been completed and merged (note this does not mean released)\n- **Canceled** - Issue is not going to be done\n\n### General Workflow\n\nGenerally, the Linear workflow is as follows:\n\n```mermaid\ngraph TD\n    A(Github/Discord/???) -->|intake| Triage\n    Triage -->|accepted| Backlog\n    Triage -->|rejected| Canceled\n    Backlog -->|planned| Todo\n    Todo -->|picked up| InProgress(In Progress)\n    InProgress -->|PR| InReview(In Review)\n    InProgress -->|stopped| Todo\n    InProgress -->|blocked| NeedsFeedback(Needs Feedback)\n    NeedsFeedback -->|unblocked| InProgress\n    InReview -->|larger changes| InProgress\n    InReview -->|small comments| InReview\n    InReview -->|merged| Done\n```\n\nIn more detail, the following is the _standard_ flow of a Linear Issue:\n\n1. When an idea or bug fix is identified through a Github Discussion/Issue/PR, we will create a Linear Issue in the **Triage** Status to track the task\n   1. This should contain an appropriate title and a link to the original idea in github\n2. The issue will be reviewed by the team (with final approval on larger changes and new APIs coming from Michael/Ryan)\n   1. If we want to move forward with the Issue, it gets moved to the **Backlog** Status\n   2. If we do not want to move forward with the Issue, it get moved to the **Canceled** Status, with appropriate reasoning provided in the Linear Issue and the original Github source.\n3. Periodically, Issues in the **Backlog** Status will be planned into a given cycle, and moved to the **Todo** Status\n4. During the active Cycle, a developer will pick up a ticket to start work and will move it into the **In Progress** Status\n5. Once the work is completed and a PR is opened, the developer will move the Issue to the **In Review** Status and assign it to the person who should perform the review\n6. If feedback is needed, comments can be left on the PR and the ticket can switch between assignees while remaining in the **In Review** status\n   1. Only for larger-scale changes should an Issue be moved back to **In Progress**\n7. Once merged, the Issue can be moved to the **Done** Status\n\nAs always, this is not an absolute workflow and there will be exceptions. Here's a few examples:\n\n- If an issue opened on Github is a small bugfix, it can bypass the **Triage** status and go straight to the **Backlog** status. Or potentially even right into **Todo** if a developer has capacity to pick up the bug in the active (or upcoming) Cycle.\n- If a developer is blocked on moving forward with an Issue, it can be moved to the **Needs Feedback** Status and assigned to the person who can unblock the Issue\n- Sometimes an issue will be abandoned well after it was accepted and moved into the **Backlog** Status, and in these cases an Issue can always be assigned to **Canceled**\n\n### Ongoing Processes\n\n- Any idea/bug coming in via Github or Discord (or elsewhere) can be put into an Issue in the **Triage** status for review\n- Michael and Ryan should review the **Triage** Issues on a regular (weekly?) basis and move tickets to **Backlog** or **Canceled**\n  - If tickets are accepted, they should be given appropriate Labels and/or Projects\n  - If tickets are rejected, we should provide the rationale in the Linear ticket and on the original source (i.e., github)\n- Michael/Ryan (and team?) will decide what to work on in a given Cycle and move tickets from **Backlog** to **Todo** and assign a proper Cycle\n- All team members should review any tickets assigned to them on a regular (daily?) basis and provide reviews/feedback as needed to keep tickets moving along\n- All team members should work from their **Todo** queue during active Cycles\n"
  },
  {
    "path": "decisions/0007-remix-on-react-router-6-4-0.md",
    "content": "# Layering Remix on top of React Router 6.4\n\nDate: 2022-08-16\n\nStatus: accepted\n\n## Context\n\nNow that we're almost done [Remixing React Router][remixing-react-router] and will be shipping `react-router@6.4.0` shortly, it's time for us to start thinking about how we can layer Remix on top of the latest React Router. This will allow us to delete a _bunch_ of code from Remix for handling the Data APIs. This document aims to discuss the changes we foresee making and some potential iterative implementation approaches to avoid a big-bang merge.\n\nFrom an iterative-release viewpoint, there's 4 separate \"functional\" aspects to consider here:\n\n1. Server data loading\n2. Server react component rendering\n3. Client hydration\n4. Client data loading\n\n(1) can be implemented and deployed in isolation. (2) and (3) need to happen together since the contexts/components need to match. And (4) comes for free since the loaders/actions will be included on the routes we create in (3).\n\n## Decision\n\nThe high level approach is as follows\n\n1.  SSR data loading\n    1.  Update `handleResourceRequest` to use `createStaticHandler` behind a flag\n        1.  Aim to get unit and integration tests asserting both flows if possible\n    2.  Update `handleDataRequest` in the same manner\n    3.  Update `handleDocumentRequest` in the same manner\n        1.  Confirm unit and integration tests are all passing\n    4.  Write new `RemixContext` data into `EntryContext` and remove old flow\n2.  Deploy `@remix-run/server-runtime` changes once comfortable\n3.  Handle `@remix-run/react` in a short-lived feature branch\n    1.  server render without hydration (replace `EntryContext` with `RemixContext`)\n    2.  client-side hydration\n    3.  add backwards compatibility changes\n4.  Deploy `@remix-run/react` changes once comfortable\n\n## Details\n\nThere are 2 main areas where we have to make changes:\n\n1. Handling server-side requests in `@remix-run/server-runtime` (mainly in the `server.ts` file)\n2. Handling client-side hydration + routing in `@remix-run/react` (mainly in the `components.ts`, `server.ts` and `browser.ts` files)\n\nSince these are separated by the network chasm, we can actually implement these independent of one another for smaller merges, iterative development, and easier rollbacks should something go wrong.\n\n### Do the server data-fetching migration first\n\nThere's two primary reasons it makes sense to handle the server-side data-fetching logic first:\n\n1. It's a smaller surface area change since there's effectively only 1 new API to work with in `createStaticHandler`\n2. It's easier to implement in a feature-flagged manner since we're on the server and bundle size is not a concern\n\nWe can do this on the server using the [strangler pattern][strangler-pattern] so that we can confirm the new approach is functionally equivalent to the old approach. Depending on how far we take it, we can assert this through unit tests, integration tests, as well as run-time feature flags if desired.\n\nFor example, pseudo code for this might look like the following, where we enable via a flag during local development and potentially unit/integration tests. We can throw exceptions anytime the new static handler results in different SSR data. Once we're confident, we delete the current code and remove the flag conditional.\n\n```tsx\n// Runtime-agnostic flag to enable behavior, will always be committed as\n// `false` initially, and toggled to true during local dev\nconst ENABLE_REMIX_ROUTER = false;\n\nasync function handleDocumentRequest({ request }) {\n  const appState = {\n    trackBoundaries: true,\n    trackCatchBoundaries: true,\n    catchBoundaryRouteId: null,\n    renderBoundaryRouteId: null,\n    loaderBoundaryRouteId: null,\n    error: undefined,\n    catch: undefined,\n  };\n\n  // ... do all the current stuff\n\n  const serverHandoff = {\n    actionData,\n    appState: appState,\n    matches: entryMatches,\n    routeData,\n  };\n\n  const entryContext = {\n    ...serverHandoff,\n    manifest: build.assets,\n    routeModules,\n    serverHandoffString: createServerHandoffString(serverHandoff),\n  };\n\n  // If the flag is enabled, process the request again with the new static\n  // handler and confirm we get the same data on the other side\n  if (ENABLE_REMIX_ROUTER) {\n    const staticHandler = unstable_createStaticHandler(routes);\n    const context = await staticHandler.query(request);\n\n    // Note: == only used for brevity ;)\n    assert(entryContext.matches === context.matches);\n    assert(entryContext.routeData === context.loaderData);\n    assert(entryContext.actionData === context.actionData);\n\n    if (catchBoundaryRouteId) {\n      assert(appState.catch === context.errors[catchBoundaryRouteId]);\n    }\n\n    if (loaderBoundaryRouteId) {\n      assert(appState.error === context.errors[loaderBoundaryRouteId]);\n    }\n  }\n}\n```\n\nWe can also split this into iterative approaches on the server too, and do `handleResourceRequest`, `handleDataRequest`, and `handleDocumentRequest` independently (either just implementation or implementation + release). Doing them in that order would also likely go from least to most complex.\n\n#### Notes\n\n- This can't use `process.env` since the code we're changing is runtime agnostic. We'll go with a local hardcoded variable in `server.ts` for now to avoid runtime-specific ENV variable concerns.\n  - Unit and integration tests may need to have their own copies of this variable as well to remain passing. For example, we have unit tests that assert that a loader is called once for a given route - but when this flag is enabled, that loader will be called twice so we can set up a conditional assertion based on the flag.\n- The `remixContext` sent through `entry.server.ts` will be altered in shape. We consider this an opaque API so not a breaking change.\n\n#### Implementation approach\n\n1. Use `createHierarchicalRoutes` to build RR `DataRouteObject` instances\n   1. See `createStaticHandlerDataRoutes` in the `brophdawg11/rrr` branch\n2. Create a static handler per-request using `unstable_createStaticHandler`\n3. `handleResourceRequest`\n   1. This one should be _really_ simple since it should just send back the raw `Response` from `queryRoute`\n4. `handleDataRequest`\n   1. This is only slightly more complicated than resource routes, as it needs to handle serializing errors and processing redirects into 204 Responses for the client\n5. `handleDocumentRequest`\n   1. This is the big one. It simplifies down pretty far, but has the biggest surface area where some things don't quite match up\n   2. We need to map query \"errors\" to Remix's definition of error/catch and bubble them upwards accordingly.\n      1. For example, in a URL like `/a/b/c`, if C exports a `CatchBoundary` but not an `ErrorBoundary`, then it'll be represented in the `DataRouteObject` with `hasErrorBoundary=true` since the `@remix-run/router` doesn't distinguish\n      2. If C's loader throws an error, the router will \"catch\" that at C's `errorElement`, but we then need to re-bubble that upwards to the nearest `ErrorBoundary`\n      3. See `differentiateCatchVersusErrorBoundaries` in the `brophdawg11/rrr` branch\n   3. New `RemixContext`\n      1. `manifest`, `routeModules`, `staticHandlerContext`, `serverHandoffString`\n      2. Create this alongside `EntryContext` assert the values match\n   4. If we catch an error during render, we'll have tracked the boundaries on `staticHandlerContext` and can use `getStaticContextFromError` to get a new context for the second pass (note the need to re-call `differentiateCatchVersusErrorBoundaries`)\n\n### Do the UI rendering layer second\n\nThe rendering layer in `@remix-run/react` is a bit more of a whole-sale replacement and comes with backwards-compatibility concerns, so it makes sense to do second. However, we can still do this iteratively, we just can't deploy iteratively since the SSR and client HTML need to stay synced (and associated hooks need to read from the same contexts). First, we can focus on getting the SSR document rendered properly without `<Scripts/>`. Then second we'll add in client-side hydration.\n\nThe main changes here include:\n\n- Removal of `RemixEntry` and it's context in favor of a new `RemixContext.Provider` wrapping `DataStaticRouter`/`DataBrowserRouter`\n  - All this context needs is the remix-specific aspects (`manifest`, `routeModules`)\n  - Everything else from the old RemixEntryContext is now in the router contexts (and `staticHandlerContext` during SSR)\n- Some aspects of `@remix-run/react`'s `components.tsx` file are now fully redundant and can be removed completely in favor of re-exporting from `react-router-dom`:\n  - `Form`, `useFormAction`, `useSubmit`, `useMatches`, `useFetchers`\n- Other aspects are largely redundant but need some Remix-specific things, so these will require some adjustments:\n  - `Link`, `useLoaderData`, `useActionData`, `useTransition`, `useFetcher`\n\n#### Backwards Compatibility Notes\n\n- `useLoaderData`/`useActionData` need to retain their generics, and are not currently generic in `react-router`\n- `useTransition` needs `submission` and `type` added\n  - `<Form method=\"get\">` no longer goes into a \"submitting\" state in `react-router-dom`\n- `useFetcher` needs `type` added\n- `unstable_shouldReload` replaced by `shouldRevalidate`\n  - Can we use it if it's there but prefer `shouldRevalidate`?\n- Distinction between error and catch boundaries\n- `Request.signal` - continue to send separate `signal` param\n\n[remixing-react-router]: https://remix.run/blog/remixing-react-router\n[strangler-pattern]: https://martinfowler.com/bliki/StranglerFigApplication.html\n"
  },
  {
    "path": "decisions/0008-only-support-js-conversion-for-app-code.md",
    "content": "# Only support JS conversion for app code\n\nDate: 2023-01-20\n\nStatus: accepted\n\n## Context\n\nRemix defaults to Typescript, but some users prefer to use Javascript.\nWhen creating a new Remix project via `npx create-remix` today, the CLI asks the user if they'd\nprefer to use Typescript or Javascript.\n\n```sh\n❯ npx create-remix@latest\n? Where would you like to create your app? ./my-remix-app\n? What type of app do you want to create? Just the basics\n? Where do you want to deploy? Choose Remix App Server if you're unsure; it's easy to change deployment targets. Remix App Server\n? TypeScript or JavaScript? (Use arrow keys)\n❯ TypeScript\n  JavaScript\n```\n\nOriginally, this was implemented by having separate variants of a template for TS and JS.\nThis worked, but duplicated huge portions of templates making those variants hard to maintain.\nTo fix this, the decision was made to maintain _only_ the TS variant of each template.\nTo provide JS-only Remix projects, the Remix CLI would copy the TS template and then dynamically\nconvert all Typescript-related code to their Javascript equivalents.\n\nWhile converting the code in the Remix app directory (e.g. `app/`) is reliable, conversion of\nTS-related code outside of the `app/` directory are tricky and error-prone.\n\n### 1 Stale references to `.ts` files\n\nFor example, both the [Indie][indie-stack] and [Blues][blues-stack] stacks have broken scripts when selecting `Javascript`,\nbecause they still reference `server.ts` and `seed.ts`.\nThey also still reference TS-only tools like `ts-node` in their scripts.\n\n### 2.a MJS\n\nWhen transpiling code outside of the `app/` directory from TS to JS, the easiest thing to do is\nto convert to ESM-style `.mjs` since Remix app code already uses ESM.\n\nESM-style JS requires one of the following:\n\n- a) Set `\"type\": \"module\"` in `package.json`\n- b) Use `.mjs` extension\n\n(a) immediately breaks Remix's builds since the settings in `package.json` apply to app code, not just code and\nscripts outside of the app directory.\n\n(b) seems more promising, but `.mjs` files require file extensions for import specifiers.\nReliably changing relative imports without extensions to have the proper extensions is\nuntractable since not all imports will be for `.js` files.\n\n```js\n// ./script.mjs (converted from ./script.js)\nimport myHelper from \"./my-helper\";\n\n// Should this be converted to `./my-helper.mjs`?\n// Probably, but can we be sure?\n\nmyHelper();\n```\n\nMaybe there's a way to do this, but the complexity cost seems high.\n\n### 2.b CJS\n\nIf we don't use the `.mjs` extension, Node will default to treating scripts and code outside of the app directory\nas CJS.\nSince CJS doesn't support ESM-style `import`/`export`, we'd then need to convert all `import`s and `export`s to their\nequivalent `require`/`module.exports`.\n\nImportant to remember that the converted code is meant to be read and edited by other developers, so its not acceptable\nto produce a bunch of boilerplate adapters for the imports and exports as would be typical in build output.\n\nConverting the imports and exports may be doable, but again, carries high complexity cost.\n\n## Decision\n\nOnly support JS conversion for app code, not for scripts or code outside of the Remix app directory.\n\n## Consequences\n\nUsers will have three options:\n\n1. Use a Typescript template\n2. Use a Typescript template with app directory converted to JS\n3. Use a dedicated Javascript template\n\n### Converting remaining TS to JS\n\nIf you don't like the remaining TS from option (2) and cannot find a suitable template for option (3),\nyou can still remove any remaining Typescript code manually:\n\n- Remove `tsconfig.json` or replace it with the equivalent `jsconfig.json`\n- Replace TS-only tools with their JS counterparts (e.g. `ts-node` -> `node`)\n- Change any remaining `.ts` files to `.mjs` and update imports and other references (like `package.json` scripts) to refer to the new filename\n\n[indie-stack]: https://github.com/remix-run/indie-stack\n[blues-stack]: https://github.com/remix-run/blues-stack\n"
  },
  {
    "path": "decisions/0009-do-not-rely-on-treeshaking-for-correctness.md",
    "content": "# Do not rely on treeshaking for correctness\n\nDate: 2024-01-02\n\nStatus: accepted\n\n## Context\n\nRemix lets you write code that runs on both the server and the client.\nFor example, it's common to have server and client code in the same route file.\nWhile blending server and client code is convenient, Remix needs to ensure that server-only code is never shipped to the client.\nThis prevents secrets available to the server from leaking into client and also prevents the client from crashing due to code that expects a server environment.\n\nServer-only code comes in three forms:\n\n1. Server-only route exports like `loader`, `action`, `headers`, etc.\n2. Imports from `.server` directories or `.server` files.\n3. Imports from server-only packages like `node:fs`\n\nRemix previously relied on treeshaking to exclude server-only code.\nSpecifically, Remix used [virtual modules for each route][virtual-modules] that re-exported client-safe exports from the route.\nFor a brief stint, Remix instead used [AST transforms][ast-transforms] to remove server-only exports.\nIn either case, Remix would remove server exports from routes and then let the bundler treeshake any unused code and dependencies from the client bundle.\n\nThe main benefit is that server and client code can co-exist in the same module graph and even the same file\nand the treeshaking saves you from the tedium of explicitly marking or separating server-only code yourself.\n\n### Human error\n\nHowever, this is also the main drawback; \"server-only\" is implicit and must be inferred by thorough treeshaking.\nEven if treeshaking were perfect, this approach still leaves the door open to human error.\nIf you or anyone on your team accidentally references server-only code from client code, the bundler will happily include that code in the client.\nYou won't get any indication of this at build time, but only at runtime.\nYour app could crash when trying to execute code meant for the server, or worse, you could accidentally ship secrets to the client.\n\n### `.server` modules\n\nInstead of hoping such an accident never happens, Remix provides a mechanism for ensuring that server-only code is excluded from the client bundle; `.server` modules.\nAny modules with a directory named `.server` are never allowed into the module graph for the client.\nSimilarly, files with a `.server` extension are also excluded from the client module graph.\n\nTheoretically, `.server` modules are a redundancy.\nA perfect module graph with perfect treeshaking shouldn't _need_ `.server` modules.\nBut in practice, `.server` modules are indispensable.\nThey are the only guaranteed way to exclude code from the client.\n\n### An imperfect optimization\n\nAs we already discussed, even if treeshaking were perfect, it would still be a bad idea to rely on it to exclude server-only code.\nBut treeshaking is a hard problem, especially in a language as dynamic as JavaScript.\nIn the real world, treeshaking is not perfect.\n\nThat's why treeshaking is designed to be an _optimization_ that slims down your bundle.\nYour code should already be correct before treeshaking is applied.\nBundlers are allowed to make [their own tradeoffs about how much treeshaking][esbuild-minify-considerations] they want to do.\nAnd that shouldn't [affect Remix's implementation][remix-minify-syntax].\nThey are even allowed to do _less_ treeshaking without needing a major version bump.\n\nAdditionally, code can only be treeshaken if it is known to be side-effect free.\nUnfortunately, even fully side-effect free packages often omit `sideEffects: false` from their `package.json`.\nAnd sometimes side-effects are desired!\nWhat if there's a server-side package with side-effects that we want to include in our server bundle?\nHow could we exclude that from the client bundle?\nThere are ways, but they're all hacky and brittle.\n\n### Vite\n\nRemix is becoming a Vite plugin, but Vite's on-demand compilation in dev is incompatible with treeshaking.\nSince the compilation is on-demand, Vite only knows the _current_ importer for the module, not all possible importers.\n\n### Summary\n\n- Even if treeshaking were perfect, it leaves the door open for human error\n- `.server` modules guarantee that server-only code is excluded from the client\n- Treeshaking is an imperfect _optimization_, so a Remix app should work correctly and exclude server-only code even without treeshaking\n- Vite's architecture makes treeshaking in dev untenable\n\n## Decision\n\nDo not rely on implicit, cross-module treeshaking for correctness.\nInstead:\n\n- Forcibly remove server-only route exports and then explicitly run a dead-code elimination pass\n- Explicitly mark server-only code and throw a build time error if server code is still referenced in the client\n\n## Consequences\n\n- No reliance on optimizations for correctness\n- Build-time errors instead of runtime errors\n- Errors consistent across dev and prod with Vite\n- Exports are assumed to be client-safe unless explicitly marked as server-only\n  - For example, `.server` modules mark all their exports as server-only\n  - Route exports like `loader`, `action`, `headers`, etc. are an exception as they are already known to be server-only\n\n[virtual-modules]: https://github.com/remix-run/remix/blob/71f0e051d895807c349987655325c153903abad8/packages/remix-dev/compiler/js/plugins/routes.ts\n[ast-transforms]: https://github.com/remix-run/remix/pull/5259\n[esbuild-minify-considerations]: https://esbuild.github.io/api/#minify-considerations\n[remix-minify-syntax]: https://github.com/remix-run/remix/blob/bf042e7d340b3cbfdaa389c201e1284fb4d03403/packages/remix-dev/compiler/server/compiler.ts#L80-L88\n"
  },
  {
    "path": "decisions/0010-splitting-up-client-and-server-code-in-vite.md",
    "content": "# Splitting up client and server code in Vite\n\nDate: 2024-02-01\n\nStatus: accepted\n\n## Context\n\nBefore adopting Vite, Remix used to rely on ESbuild's treeshaking to implicitly separate client and server code.\nEven though Vite provides equivalent treeshaking (via Rollup) for builds, it does not perform cross-module treeshaking when running the dev server.\nIn any case, we think its a [bad idea to rely on treeshaking for correctness][decision-0009].\n\nGoals:\n\n1. Simple and robust exclusion of server-only code from the client\n2. Prefer compile-time errors over runtime errors\n3. Typesafety for runtime errors\n4. Avoid performance degradation for common cases\n\n#### Remix's approach before Vite\n\nRemix already provides `.server` modules to explicitly separate client and server code at the module level (Goal 1 ✅).\nHowever, Remix's previous compiler replaced `.server` modules with empty modules.\nWhile this ensured that code from `.server` modules never leaks into the client,\nit also meant that any accidental references to imports from `.server` in the client\nwould result in runtime errors, not compile-time errors (Goal 2 ❌).\n\nTypeScript does not understand that imports from `.server` modules may not exist on the client\nso typechecking does not catch these runtime errors (Goal 3 ❌).\n\nFor example:\n\n```tsx\nimport { getFortune } from \"~/db.server.ts\";\n\nexport default function Route() {\n  const [fortune, setFortune] = useState(null);\n  return (\n    <>\n      {user ? (\n        <h1>Your fortune of the day: {fortune}</h1>\n      ) : (\n        <button onClick={() => setFortune(getFortune())}>\n          Open fortune cookie 🥠\n        </button>\n      )}\n    </>\n  );\n}\n```\n\nYour editor would not show any red squigglies, typechecking in CI would pass, and Remix would build your app without warnings or errors.\nBut you've just shipped a bug that will crash your app anytime a user clicks the \"Get user\" button.\n\n#### How Vite's dev server works\n\nIn development, Vite's dev server compiles requested JavaScript modules on the fly.\nAs a result, Vite must decide how to transform each module without knowing the entire module graph.\nThe Plugin API makes this apparent:[^1]\n\n- `resolveId` only provides the current `importer`\n- `load` and `transform` do not receive any information about the module graph\n\nThis approach lets Vite load and transform each module _once_ and cache the result[^2] which is a keystone for its speed.\n\n#### Handling mixed modules\n\nWhile `.server` modules are a great way to separate client and server code in most cases,\nthere will always be a need to stitch together modules that mix client and server code.\nFor example, you may want to migrate from the previous compiler to Vite without needing to manually split up mixed modules.\n\nBut supporting mixed modules directly in Remix would require compile-time magic which would add substantial complexity.\nNot only would it degrade performance for all users (Goal 4 ❌),\nbut writing compile-time transforms that manipulate the AST is much more error-prone than throwing a compile-time error when `.server` modules are imported by client code.\nDepending on how its implemented, bugs in that compile-time magic could open the door to leaking server code into the client (Goal 1 ❌).\n\n## Decision\n\n- Support `.server` modules (including new `.server` directories) in Remix to split client and server code at the module-level (Goal 1 ✅)\n- Recommend [vite-env-only][vite-env-only] for expression-level separation (Goal 1 ✅)\n- For each Remix route module, remove server-only exports (`loader`, `action`, `headers`) and then explicitly run dead-code eliminate\n- Throw a compile-time error when `.server` modules remained after dead-code elimination (Goal 2 ✅)\n\n## Consequences\n\nUsers are encouraged to primarily use `.server` modules but can always opt for more powerful, expression-level separation with [vite-env-only][vite-env-only].\n\n#### Typesafety\n\nSince Remix now throws when `.server` imports remain in the built client code, there are no remaining runtime errors to catch with typechecking for module-level separation (Goal 3 ✅).\nFor expression-level separation, `vite-env-only` provides optional types (`<T>(_: T) => T | undefined`) which lets TypeScript prevent any runtime errors.\n\n#### Performance\n\nChecking for `.server` modules only requires checking the module's path and does not require AST parsing or transformations, so it's extremely fast (Goal 4 ✅).\n`vite-env-only` does require AST parsing and transformations so it will always be slower than `.server` modules.\n\n[^1]: Vite provides a lower-level module graph API, but the module graph is not guaranteed to be complete as it is only populated as modules are requested.\n\n[^2]: When a file changes on disk, Vite invalidates the corresponding module in its cache to power features like HMR.\n\n[decision-0009]: ./0009-do-not-rely-on-treeshaking-for-correctness.md\n[vite-env-only]: https://github.com/pcattori/vite-env-only\n"
  },
  {
    "path": "decisions/0011-routes-ts.md",
    "content": "# routes.ts\n\nDate: 2024-09-18\n\nStatus: accepted\n\n## Context\n\nNow that Remix is being merged upstream into React Router, we have an opportunity to revisit our approach to route configuration for the Vite plugin.\n\nRemix ships with a default set of file system routing conventions. While convenient for demos, examples, tutorials and simple use cases, conventions like these come with a few major drawbacks:\n\n1. **They're highly contentious.**\n\n   File system routing conventions are inherently subjective, with numerous possible approaches and preferences. For example, some developers prefer flat route folder structures, while others prefer deeply nested directories. Some even prefer a mix of both. These differing opinions make it challenging to create a one-size-fits-all solution.\n\n   When we changed the default routing conventions between Remix v1 and v2, we found this caused friction for some users who didn't see the benefit in migrating and preferred the old conventions. While consumers could opt out of the new conventions, this may not have been obvious to everyone, and it made anyone using another convention feel like they were veering off the blessed path. This was especially pronounced for anyone who chose to stick with the previous convention since it was now being presented as a legacy \"v1\" approach.\n\n   For those that opted out of the built-in convention, the lower level `routes` option that was provided as an escape hatch presented some challenges. It was quite tricky to use since it didn't use a nested object structure to represent the route tree. Instead, it relied on function calls with nested callbacks to define routes, and then Remix used the call stack to determine the route tree for you. This made it more difficult than it should have been to build alternative routing conventions and resulted in some less-than-ideal APIs.\n\n   In hindsight, we feel we could have been less opinionated in this area.\n\n2. **Advanced usage gets convoluted.**\n\n   Filenames are more limited compared to config-based routes since they have fewer characters to work with. They not only need to include the route path and any parameters—they also need to encode further route configuration (index/layout routes, opting out of nested layouts, escaping special characters, etc.), leading to increasingly elaborate naming conventions.\n\n   This is exacerbated by the fact that, while other aspects of the framework's configuration are based in code and provide type safety, file naming conventions provide none of this assistance.\n\n   This also makes it difficult for those looking to move between frameworks since they'd have to learn and memorize a new set of conventions that will likely be similar but not identical.\n\n3. **They force a directory structure on consumers.**\n\n   File system routing conventions dictate a particular way to organize route files. This can be limiting, especially for larger applications where you may prefer to split up your directory structure by team or domain rather than by route.\n\n## Goals\n\n1. Default to a more flexible configuration-based approach that better aligns with React Router's philosophy.\n2. Allow for easier iteration on route configuration APIs and conventions without forcing breaking changes on users.\n3. Improve scalability, maintainability and legibility of complex routing scenarios.\n4. Provide type safety to ensure that routes are defined correctly.\n5. Maintain the option for file system routing for those who prefer it.\n6. Make it easier for the community to create and adopt alternative routing conventions.\n7. Provide a clear migration path for Remix users adopting React Router v7.\n\n## Decisions\n\n### `routes.ts` is mandatory for Vite plugin consumers\n\nAny project using the Vite plugin must have a `routes.ts` file which exports an array of route config objects.\n\n### `routes.ts` is in the `app` directory\n\nThere are a few reasons for this:\n\n1. `app` is currently the only directory that is owned by React Router. If it wasn't in the app directory, we'd either need to take ownership of a `routes.ts` file at the root of the project, or introduce another directory that we own, e.g. `config`. In either case, we'd likely want to allow these paths to be configured in the same way the app directory is, adding further complexity. Instead, it's much simpler to keep the configuration in the existing configurable app directory.\n\n2. All route file paths are resolved relative to the app directory, meaning they can also contain a leading `./`. This makes it feel natural at authoring time for these paths to be written within the app directory itself.\n\n3. The route configuration is inherently tied to the app's specific domain rather than build concerns and framework settings. This is reflected in the rate of change within these files since consumers will regularly update their `routes.ts` file as they work on other files within `app`, whereas the rest of the framework configuration remains fairly stable.\n\n### Route config helpers are the preferred way to define route config objects\n\nWe don't expect consumers to write the low level route configuration objects by hand. Instead, we expect them to use a set of helpers (`route`, `index` and `layout`) to define routes in a more declarative and type-safe way.\n\nThese helpers are scoped to `@react-router/dev/routes` to make it clear which parts of our API are only intended for use within a `routes.ts` context.\n\n### File system routing is supported via a separate package\n\nWe're providing a separate `@react-router/fs-routes` package since we want to discourage consumers from thinking of file system routing as being the primary way to define routes.\n\nThis package exports a `flatRoutes` function that provides the same functionality as Remix v2's file system routing, meaning it's easy to migrate from Remix to React Router v7 without having to convert everything to config-based routes.\n\nSince this function is asynchronous, the `RouteConfig` type supports promises so you don't need to await the result when exporting from `routes.ts`.\n\nNote that this function is named `flatRoutes` to leave room for other conventions in the future.\n\n### Build context is made available via helper functions\n\nIn order to keep the `RouteConfig` type simple, it doesn't provide an interface for accessing build context. Instead, any build context should be provided via helper functions.\n\nAs of the creation of this decision document, the only available build context is the app directory path, provided via the `getAppDirectory` helper, since it's required by file system routing implementations. Any future build context values should be made available in the same way.\n\n### Remix's `routes` option has an adapter for easy migration\n\nSome Remix consumers used the `routes` option to define config-based routes or use community file system routing conventions. To ease the migration, the `@react-router/remix-routes-option-adapter` package provides a `remixRoutesOptionAdapter` function that accepts Remix's `routes` config value as an argument.\n"
  },
  {
    "path": "decisions/0012-type-inference.md",
    "content": "# Type inference\n\nDate: 2024-09-20\n\nStatus: accepted\n\nSupersedes [#0003](./0003-infer-types-for-useloaderdata-and-useactiondata-from-loader-and-action-via-generics.md)\n\n## Context\n\nNow that Remix is being merged upstream into React Router, we have an opportunity to revisit our approach to typesafety.\n\n### Type inference\n\nThere are three major aspects to typesafety in a framework like React Router:\n\n1. **Type inference from the route config**\n\n   Some types are defined in the route config (`routes.ts`) but need to be inferred within a route module.\n\n   For example, let's look at URL path parameters.\n   Remix had no mechanism for inferring path parameters as that information is not present _within_ a route module.\n   If a route's URL path was `/products/:id`, you'd have to manually specify `\"id\"` as a valid path parameter within that route module:\n\n   ```ts\n   const params = useParams<\"id\">();\n   params.id;\n   ```\n\n   This generic was nothing more than a convenient way to do a type cast.\n   You could completely alter the URL path for a route module, typechecking would pass, but then you would get runtime errors.\n\n2. **Type inference within a route**\n\n   Some types are defined within a route module but need to be inferred across route exports.\n\n   For example, loader data is defined by the return type of `loader` but needs to be accessed within the `default` component export:\n\n   ```ts\n   export function loader() {\n     // define here 👇\n     return { planet: \"world\" };\n   }\n\n   export default function Component() {\n     // access here 👇\n     const data = useLoaderData<typeof loader>();\n   }\n   ```\n\n   Unlike the `useParams` generic, this isn't just a type cast.\n   The `useLoaderData` generic ensures that types account for serialization across the network.\n   However, it still requires you to add `typeof loader` every time.\n\n   Not only that, but complex routes get very tricky to type correctly.\n   For example, `clientLoader`s don't run during the initial SSR render, but you can force the `clientLoader` data to always be present in your route component if you set `clientLoader.hydrate = true` _and_ provide a `HydrateFallback`.\n   Here are a couple cases that trip up most users:\n\n   | `loader` | `clientLoader` | `clientLoader.hydrate` | `HydrateFallback` | Generic for `useLoaderData`            |\n   | -------- | -------------- | ---------------------- | ----------------- | -------------------------------------- |\n   | ✅       | ❌             | `false`                | ❌                | `typeof loader`                        |\n   | ❌       | ✅             | `false`                | ❌                | `typeof clientLoader \\| undefined`     |\n   | ✅       | ✅             | `false`                | ❌                | `typeof loader \\| typeof clientLoader` |\n   | ✅       | ✅             | `true`                 | ❌                | `typeof loader \\| typeof clientLoader` |\n   | ✅       | ✅             | `true`                 | ✅                | `typeof clientLoader`                  |\n\n   The generic for `useLoaderData` starts to feel a lot like doing your taxes: there's only one right answer, Remix knows what it is, but you're going to get quizzed on it anyway.\n\n3. **Type inference across routes**\n\n   Some types are defined in one route module but need to be inferred in another route module.\n   This is common when wanting to access loader data of matched routes like when using `useMatches` or `useRouteLoaderData`.\n\n   ```ts\n   import type { loader as otherLoader } from \"../other-route.ts\";\n   // hope the other route is also matched 👇 otherwise this will error at runtime\n   const otherData = useRouteLoaderData<typeof otherLoader>();\n   ```\n\n   Again, its up to you to wire up the generics with correct types.\n   In this case you need to know both types defined in the route config (to know which routes are matched) and types defined in other route modules (to know the loader data for those routes).\n\nIn practice, Remix's generics work fine most of the time.\nBut they are mostly boilerplate and can become error-prone as the app scales.\nAn ideal solution would infer types correctly on your behalf, doing away with tedious generics.\n\n## Goals\n\n- Type inference from the route config (`routes.ts`)\n- Type inference within a route\n- Type inference across routes\n- Same code path for type inference whether using programmatic routing or file-based routing\n- Compatibility with standard tooling for treeshaking, HMR, etc.\n- Minimal impact on runtime API design\n\n## Decisions\n\n### Route exports API\n\nKeep the route module export API as is.\nRoute modules should continue to export separate values for `loader`, `clientLoader`, `action`, `ErrorBoundary`, `default` component, etc.\nThat way standard transforms like treeshaking and React Fast Refresh (HMR) work out-of-the-box.\n\nAdditionally, this approach introduces no breaking changes allowing Remix users to upgrade to React Router v7 more easily.\n\n### Pass path params, loader data, and action data as props\n\nHooks like `useParams`, `useLoaderData`, and `useActionData` are defined once in `react-router` and are meant to be used in _any_ route.\nWithout any coupling to a specific route, inferring route-specific types becomes impossible and would necessitate user-supplied generics.\n\nInstead, each route export should be provided route-specific args:\n\n```ts\n// Imagine that we *somehow* had route-specific types for:\n// - LoaderArgs\n// - ClientLoaderArgs\n// - DefaultProps\n\nexport function loader({ params }: LoaderArgs) {}\n\nexport function clientLoader({ params, serverLoader }: ClientLoaderArgs) {}\n\nexport default function Component({\n  params,\n  loaderData,\n  actionData,\n}: DefaultProps) {\n  // ...\n}\n```\n\nWe'll keep those hooks around for backwards compatibility, but eventually the aim is to deprecate and remove them.\nWe can design new, typesafe alternatives for any edge cases.\n\n### Typegen\n\nWhile React Router will default to programmatic routing, it can easily be configured for file-based routing.\nThat means that sometimes route URLs will only be represented as file paths.\nUnfortunately, TypeScript cannot use the filesystem as part of its type inference nor type checking.\nThe only tenable way to infer types based on file paths is through code generation.\n\nWe _could_ have typegen just for file-based routing, but then we'd need to maintain a separate code path for type inference in programmatic routing.\nTo keep things simple, React Router treats any value returned by `routes.ts` the same; it will not make assumptions about _how_ those routes were constructed and will run typegen in all cases.\n\nTo that end, React Router will generate types for each route module into a special, gitignored `.react-router` directory.\nFor example:\n\n```txt\n- .react-router/\n  - types/\n    - app/\n      - routes/\n        - +types.product.ts\n- app/\n  - routes/\n    - product.tsx\n```\n\nThe path within `.react-router/types` purposefully mirrors the path to the corresponding route module.\nBy setting things up like this, we can use `tsconfig.json`'s [rootDirs](https://www.typescriptlang.org/tsconfig/#rootDirs) option to let you conveniently import from the typegen file as if it was a sibling:\n\n```ts\n// app/routes/product.tsx\nimport { LoaderArgs, DefaultProps } from \"./+types.product\";\n```\n\nTypeScript will even give you import autocompletion for the typegen file and the `+` prefix helps to distinguish it as a special file.\nBig thanks to Svelte Kit for showing us that [`rootDirs` trick](https://svelte.dev/blog/zero-config-type-safety#virtual-files)!\n\n### Watch mode\n\nTypegen solutions often receive criticism due to typegen'd files becoming out of sync during development.\nThis happens because many typegen solutions require you to then rerun a script to update the typegen'd files.\n\nInstead, we'll provide a `--watch` flag for the `react-router typegen` command to automatically regenerate types as files change.\nIt's also straightforward to automatically run commands like `react-router typegen --watch` when opening up any modern editors.\n\nIn the future, we may also kick off typegen watching as part of running a React Router dev server.\n\n## Rejected solutions\n\n### `defineRoute`\n\nEarly on, we considered changing the route module API from many exports to a single `defineRoute` export:\n\n```tsx\nexport default defineRoute({\n  loader() {\n    return { planet: \"world\" };\n  },\n  Component({ loaderData }) {\n    return <h1>Hello, {loaderData.planet}!</h1>;\n  },\n});\n```\n\nThat way `defineRoute` could do some TypeScript magic to infer `loaderData` based on `loader` (type inference within a route).\nWith some more work, we envisioned that `defineRoute` could return utilities like a typesafe `useRouteLoaderData` (type inference across routes).\n\nHowever, there were still many drawbacks with this design:\n\n1. Type inference across function arguments depends on the ordering of those arguments.\n   That means that if you put `Component` before `loader` type inference is busted and you'll get gnarly type errors.\n\n2. Any mechanism expressible solely as code in a route module cannot infer types from the route config (`routes.ts`).\n   That means no type inference for things like path params nor for `<Link to=\"...\" />`.\n\n3. Transforms that expect to operate on module exports can no longer access parts of the route.\n   For example, bundlers would only see one big export so they would bail out of treeshaking route modules.\n   Similarly, React-based HMR via React Fast Refresh looks for React components as exports of a module.\n   It would be possible to augment React component detection for HMR to look within a function call like `defineRoute`, but it significantly ups the complexity.\n\n### `defineLoader` and friends\n\nInstead of a single `defineRoute` function as described above, we could have a `define*` function for each route export:\n\n```tsx\nimport { defineLoader } from \"./+types.product\";\n\nexport const loader = defineLoader(() => {\n  return { planet: \"world\" };\n});\n```\n\nThat would address the most of the drawbacks of the `defineRoute` approach.\nHowever, this adds significant noise to the code.\nIt also means we're introducing a runtime API that only exists for typesafety.\n\nAdditionally, utilities like `defineLoader` are implemented with an `extends` generic that [does not pin point incorrect return statements](https://tsplay.dev/WJP7ZN):\n\n```ts\nconst defineLoader = <T extends Loader>(loader: T): T => loader;\n\nexport const loader = defineLoader(() => {\n  //                               ^^^^^^^\n  // Argument of type '() => \"string\" | 1' is not assignable to parameter of type 'Loader'.\n  //   Type 'string | number' is not assignable to type 'number'.\n  //     Type 'string' is not assignable to type 'number'.(2345)\n\n  if (Math.random() > 0.5) return \"string\"; // 👈 don't you wish the error was here instead?\n  return 1;\n});\n```\n\n### Zero-effort typesafety\n\nSvelte Kit has a [\"zero-effort\" type safety approach](https://svelte.dev/blog/zero-config-type-safety) that uses a TypeScript language service plugin to automatically inject types for framework-specific exports.\nInitially, this seemed like a good fit for React Router too, but we ran into a couple drawbacks:\n\n1. Tools like `typescript-eslint` that need to statically inspect the types of your TS files without running a language server would not be aware of the injected types.\n   There's an open issue for [`typescript-eslint` interop with Svelte Kit](https://github.com/sveltejs/language-tools/issues/2073)\n\n2. Running `tsc` would perform typechecking without any knowledge of our custom language service.\n   To fix this, we would need to wrap `tsc` in our own CLI that programmatically calls the TS typechecker.\n   For Svelte Kit, this isn't as big of an issue since they already need their own typecheck command for the Svelte language: `svelte-check`.\n   But since React Router is pure TypeScript, it would be more natural to invoke `tsc` directly in your `package.json` scripts.\n\n### TypeScript plugin\n\nOriginally, we created a basic TypeScript plugin to automatically run typegen in watch mode.\nOne nice thing about this approach is that it worked across all editors.\n\nHowever, there were a couple drawbacks:\n\n1. A TypeScript plugin will silently fail to run unless you have installed dependencies prior to opening up the project in your editor.\n\n2. A TypeScript plugin requires your editor to use the local (workspace) version of TypeScript.\n   But by default [VSCode won't use the workspace version of TypeScript](https://code.visualstudio.com/docs/typescript/typescript-compiling#_using-the-workspace-version-of-typescript), forcing you to run the `Select TypeScript Version` command every time you open up a new project.\n   You can workaround this via the `typescript.tsdk` and `typescript.enablePromptUseWorkspaceTsdk` options in `.vscode/settings.json`, but those only take effect when that _specific_ directory is opened as by VSCode.\n   For example, if you added `.vscode/settings.json` to a subfolder of a monorepo those options would be ignored when opening the root of the monorepo with VSCode.\n\n3. Debugging a TypeScript plugin is not straightforward as you need to know to run the `Open TS Server log` command in VSCode and sift through verbose logs.\n   Without this knowledge, its hard to know if you've set up typegen correctly.\n   And even if you do know this, its tedious to find out what went wrong.\n\nAfter we decided not to pursue \"zero-effort typesafety\" (as described above), our TypeScript plugin was already a simple passthrough that kicked off typegen as a side-effect.\nThis was an additional indication that maybe a TypeScript plugin was not the right place for our typegen.\n\n## Summary\n\nBy leaning into automated typegen, we radically simplify React Router's runtime APIs while providing strong type inference across the entire framework.\nWe can continue to support programmatic routing _and_ file-based routing in `routes.ts` while providing typesafety with the same approach and same code path.\nWe can design our runtime APIs without introducing bespoke ways to inform TypeScript of the route hierarchy.\n\nThe initial implementation will be focused on typesafety for path params, loader data, and action data.\nThat said, this foundation lets us add type inference for things like `<Link to=\"...\" />` and search params in the future.\n"
  },
  {
    "path": "decisions/0013-react-router-config-ts.md",
    "content": "# `react-router.config.ts`\n\nDate: 2024-11-21\n\nStatus: accepted\n\n## Context\n\nPreviously in Remix and earlier pre-releases of React Router, framework config was passed directly to the Vite plugin as an options object. While this has worked well so far when limited to Vite-specific use cases or simple CLI commands, we've started to run into some limitations as our `react-router` CLI has become more advanced.\n\nSome key issues with the current approach:\n\n1. **Tight coupling with Vite**\n\n   Our CLI commands (`react-router routes` and `react-router typegen`) need access to framework config but have nothing to do with Vite. We previously worked around this by using Vite to resolve `vite.config.ts` and then extracting our React Router config from the resolved Vite config object, but this approach proved to be difficult as we added more features to our CLI.\n\n2. **Limited config watching capabilities**\n\n   The introduction of `react-router typegen --watch` in particular highlighted the limitations of our Vite-coupled approach. We needed to not only resolve our config but also watch for changes. Having this tied to the Vite config made implementing this functionality unnecessarily complex.\n\n3. **Heavy-handed config updates**\n\n   Changes to Vite plugin options are treated like any other change to the Vite config, triggering a full reload of the dev server. This takes away any ability for us to handle config updates more gracefully.\n\n4. **Difficulty with config documentation**\n\n   Documentation of our config options was difficult since we either had to show a complete Vite config file with a lot of extra noise, or only show a call to the `reactRouter` plugin which looked a bit confusing since it was labelled as a `vite.config.ts` file. Neither approach was ideal for clearly explaining our config options while keeping code snippets to a minimum.\n\n## Goals\n\n1. Decouple framework config from Vite\n2. Enable granular config watching for tools like `react-router typegen --watch`\n3. Avoid unnecessary dev server reloads when config changes\n4. Improve documentation by separating framework config from Vite config\n\n## Decisions\n\n### Introduce dedicated `react-router.config.ts` in the root of the project\n\nWe will introduce a dedicated config file, `react-router.config.ts/js`.\n\n### Config is provided via a default export\n\nTo maintain consistency with other JS build tool configuration patterns, we will export the config object as the default export of the `react-router.config.ts` file.\n\n### Change `app/routes.ts` API to use a default export rather than a named `routes` export\n\nNow that we have multiple config files (`react-router.config.ts` and `app/routes.ts`), we should be internally consistent and use default exports for all of our config files. Now is a good time to make this change since the `routes.ts` API hasn't yet had a stable release.\n\n### Any config APIs should be exported from `@react-router/dev/config`\n\nThe exported config object should satisfy the `Config` type from `@react-router/dev/config`. This follows our established pattern of using `@react-router/dev/*` namespaces for dev-time APIs that are scoped to particular files, e.g. `@react-router/dev/routes` and `@react-router/dev/vite`.\n\n### Config file is optional but recommended\n\nWhile the lack of a config file won't be treated as an error, we should include a blank config file in all official templates to make the config options more discoverable and self-documenting.\n\n### Remove options from Vite plugin\n\nThe Vite plugin will no longer accept config options. All framework options will be handled through the dedicated config file.\n\n### Improved config update handling\n\nConfig changes should no longer trigger full dev server reloads. We may re-introduce this behavior in certain cases where it makes sense.\n"
  },
  {
    "path": "decisions/0014-context-middleware.md",
    "content": "# Middleware + Context\n\nDate: 2025-01-22\n\nStatus: accepted\n\n## Context\n\n_Lol \"context\", get it 😉_\n\nThe [Middleware RFC][rfc] is the _most-upvoted_ RFC/Proposal in the React Router repo. We actually tried to build and ship it quite some time ago but realized that without single fetch it didn't make much sense in an SSR world for 2 reasons:\n\n- With the individual HTTP requests per loader, middleware wouldn't actually reduce the # of queries to your DB/API's - it would just be a code convenience with no functional impact\n- Individual HTTP requests meant a lack of a shared request scope across routes\n\nWe've done a lot of work since then to get us to a place where we could ship a middleware API we were happy with:\n\n- Shipped [Single Fetch][single-fetch]\n- Shipped [`dataStrategy`][data-strategy] for DIY middleware in React Router SPAs\n- Iterated on middleware/context APIs in the [Remix the Web][remix-the-web] project\n- Developed a non-invasive type-safe + composable [context][async-provider] API\n\n## Decision\n\n### Leverage a new type-safe `context` API\n\nWe originally considered leaning on our existing `context` (`type AppLoadContext`) value we pass to server-side `loader` and `action` functions as the `context` for middleware functions. Using this would make for an easier adoption of middleware for apps that use `AppLoadContext` today. However, there were a few downsides to that approach.\n\nFirst, the type story is lacking because it's just a global interface you augment via declaration merging so it's not true type safety and is more of a \"trust me on this\" scenario. We've always known it wasn't a great typed API and have always assumed we'd enhance it at some point via a breaking change behind a future flag. The introduction of middleware should result in much _more_ usage of `context` than exists today since it'll open up to user of `react-router-serve` as well. For this reason it made more sense to ship the breaking change flag now for the smaller surface area of `context`-enabled apps users, instead of later for a much larger surface area of apps.\n\nSecond, in order to implement client-side middleware, we need to introduce a new `context` concept on the client - and we would like that to be the same API as we have on the server. So, if we chose to stick with `AppLoadContext`, we'd then have to implement a brand new `ClientAppLoadContext` which would suffer the same type issues out of the gate. It felt lazy to ship a known-subpar-API to the client. Furthermore, even if we did ship it - we'd _still_ want to enhance it later - so we'd be shipping a mediocre client `context` API _knowing_ that we would be breaking shortly after with a better typed API.\n\nThat is why we decided to rip the band-aid off and include the breaking `context` change with the initial release of middleware. When the flag is enabled, we'll be replacing `AppLoadContext` with a new type-safe `context` API that is similar in usage to the `React.createContext` API:\n\n```ts\nlet userContext = unstable_createContext<User>();\n\nconst userMiddleware: Route.unstable_MiddlewareFunction = async ({\n  context,\n  request,\n}) => {\n  context.set(userContext, await getUser(request));\n};\n\nexport const middleware = [userMiddleware];\n\n// In some other route\nexport async function loader({ context }: Route.LoaderArgs) {\n  let user = context.get(userContext);\n  let posts = await getPosts(user);\n  return { posts };\n}\n```\n\nIf you have an app already using `AppLoadContext`, you don't need to split that out, and can instead stick that object into it's own context value and maintain the same shape:\n\n```diff\n+ let appContext = unstable_createContext<AppLoadContext>()\n\nfunction getLoadContext(req, res) {\n  let appLoadContext = { /* your existing object */ };\n\n-  return appLoadContext\n+  return new Map([[appContext, appLoadContext]]);\n}\n\nfunction loader({ context }) {\n-  context.foo.something();\n+  // Hopefully this can be done via find/replace or a codemod\n+  context.get(appContext).foo.something()\n   // ...\n}\n```\n\n#### Client Side Context\n\nIn order to support the same API on the client, we will also add support for a client-side `context` of the same type (which is already a [long requested feature][client-context]). If you need to provide initial values (similar to `getLoadContext` on the server), you can do so with a new `getContext` method which returns a `Map<RouterContext, unknown>`:\n\n```ts\nlet loggerContext = unstable_createContext<(...args: unknown[]) => void>();\n\nfunction getContext() {\n  return new Map([[loggerContext, (...args) => console.log(...args)]])\n}\n\n// library mode\nlet router = createBrowserRouter(routes, { unstable_getContext: getContext })\n\n// framework mode\nreturn <HydratedRouter unstable_getContext={getContext}>\n```\n\n`context` on the server has the advantage of auto-cleanup since it's scoped to a request and thus automatically cleaned up after the request completes. In order to mimic this behavior on the client, we'll create a new object per navigation/fetch.\n\n### API\n\nWe wanted our middleware API to meet a handful of criteria:\n\n- Allow users to perform logic sequentially top-down before handlers are called\n- Allow users to modify the outgoing response bottom-up after handlers are called\n- Allow multiple middlewares per route\n\nThe middleware API we landed on to ship looks as follows:\n\n```ts\nconst myMiddleware: Route.unstable_MiddlewareFunction = async (\n  { request, context },\n  next,\n) => {\n  // Do stuff before the handlers are called\n  context.user = await getUser(request);\n  // Call handlers and generate the Response\n  let res = await next();\n  // Amend the response if needed\n  res.headers.set(\"X-Whatever\", \"stuff\");\n  // Propagate the response up the middleware chain\n  return res;\n};\n\n// Export an array of middlewares per-route which will run left-to-right on\n// the server\nexport const middleware = [myMiddleware];\n\n// You can also export an array of client middlewares that run before/after\n// `clientLoader`/`clientAction`\nconst myClientMiddleware: Route.unstable_ClientMiddlewareFunction = (\n  { context },\n  next,\n) => {\n  //...\n};\n\nexport const clientMiddleware = [myClientSideMiddleware];\n```\n\nIf you only want to perform logic _before_ the request, you can skip calling the `next` function and it'll be called and the response propagated upwards for you automatically:\n\n```ts\nconst myMiddleware: Route.unstable_MiddlewareFunction = async ({\n  request,\n  context,\n}) => {\n  context.user = await getUser(request);\n  // Look ma, no next!\n};\n```\n\nThe only nuance between server and client middleware is that on the server, we want to propagate a `Response` back up the middleware chain, so `next` must call the handlers _and_ generate the final response. In document requests, this will be the rendered HTML document, and in data requests this will be the `turbo-stream` `Response`.\n\nClient-side navigations don't really have this type of singular `Response` - they're just updating a stateful router and triggering a React re-render. Therefore, there is no response to bubble back up and the next function will run handlers but won't return anything so there's nothing to propagate back up the middleware chain.\n\n### Client-side Implementation\n\nFor client side middleware, up until now we've been recommending that if folks want middleware they can add it themselves using `dataStrategy`. Therefore, we can leverage that API and add our middleware implementation inside our default `dataStrategy`. This has the primary advantage of being very simple to implement, but it also means that if folks decide to take control of their own `dataStrategy`, then they take control of the _entire_ data flow. It would have been confusing if a user provided a custom `dataStrategy` in which they wanted to do their own middleware approach - and the router was still running it's own middleware logic before handing off to `dataStrategy`.\n\nIf users _want_ to take control over `loader`/`action` execution but still want to use our middleware flows, we should provide an API for them to do so. The current thought here is to pass them a utility into `dataStrategy` they can leverage:\n\n```ts\nasync function dataStrategy({ request, matches, defaultMiddleware }) {\n  let results = await defaultMiddleware(() => {\n    // custom loader/action execution logic here\n  });\n  return results;\n}\n```\n\nOne consequence of implementing middleware as part of `dataStrategy` is that on client-side submission requests it will run once for the action and again for the loaders. We went back and forth on this a bit and decided this was the right approach because it mimics the current behavior of SPA navigations in a full-stack React Router app since actions and revalidations are separate HTTP requests and thus run the middleware chains independently. We don't expect this to be an issue except in expensive middlewares - and in those cases the context will be shared between the action/loader chains and the second execution can be skipped if necessary:\n\n```ts\nconst expensiveMiddleware: Route.unstable_ClientMiddleware = async function ({\n  request,\n  context,\n}) {\n  // Guard this such that we use the existing value if it exists from the action pass\n  context.something = context.something ?? (await getExpensiveValue());\n};\n```\n\n**Note:** This will make more sense after reading the next section, but it's worth noting that client middlewares _have_ to be run as part of `dataStrategy` to avoid running middlewares for loaders which have opted out of revalidation. The `shouldRevalidate` function decodes which loaders to run and does so using the `actionResult` as an input. so it's impossible to decide which loaders will be _prior_ to running the action. So we need to run middleware once for the action and again for the chosen loaders.\n\n### Server-Side Implementation\n\nServer-side middleware is a bit trickier because it needs to propagate a Response back upwards. This means that it _can't_ be done via `dataStrategy` because on document POST requests we need to know the results of _both_ the action and the loaders so we can render the HTML response. And we need to render the HTML response a single time in `next`, which means middleware can only be run once _per request_ - not once for actions and once for loaders.\n\nThis is an important concept to grasp because it points out a nuance between document and data requests. GET navigations will behave the same because there is a single request/response for both document and data GET navigations. POST navigations are different though:\n\n- A document POST navigation (JS unavailable) is a single request/response to call action+loaders and generate a single HTML response.\n- A data POST navigation (JS available) is 2 separate request/response's - one to call the action and a second revalidation call for the loaders.\n\nThis means that there may be a slight difference in behavior of your middleware when it comes to loaders if you begin doing request-specific logic:\n\n```ts\nfunction weirdMiddleware({ request }) {\n  if (request.method === \"POST\") {\n    // ✅ Runs before the action/loaders on document submissions\n    // ✅ Runs before the action on data submissions\n    // ❌ Does not runs before the loaders on data submission revalidations\n  }\n}\n```\n\nOur suggestion is mostly to avoid doing request-specific logic in middlewares, and if you need to do so, be aware of the behavior differences between document and data requests.\n\n### Scenarios\n\nThe below outlines a few sample scenarios to give you an idea of the flow through middleware chains.\n\nThe simplest scenario is a document `GET /a/b` request:\n\n- Start a `middleware`\n- Start b `middleware`\n- Run a/b `loaders` in parallel\n- Render HTML `Response` to bubble back up via `next()`\n- Finish b `middleware`\n- Finish a `middleware`\n\nIf we introduce `clientMiddleware` but no `clientLoader` and client-side navigate to `/a/b`:\n\n- Start a `clientMiddleware`\n- Start b `clientMiddleware`\n- `GET /a/b.data`\n- Start a `middleware`\n- Start b `middleware`\n- Run a/b `loaders` in parallel\n- Render HTML `Response` to bubble back up via `next()`\n- Finish b `middleware`\n- Finish a `middleware`\n- Respond to client\n- Finish b `clientMiddleware`\n- Finish a `clientMiddleware`\n\nIf we have `clientLoaders` and they don't call server `loaders` (SPA Mode):\n\n- Start a `clientMiddleware`\n- Start b `clientMiddleware`\n- Run a/b `clientLoaders` in parallel\n- _No Response to render here so we can either bubble up `undefined` or potentially a `Location`_\n  - `Location` feels maybe a bit weird and introduces another way to redirect instead of `throw redirect`...\n- Finish b `clientMiddleware`\n- Finish a `clientMiddleware`\n\nIf `clientLoaders` do call `serverLoaders` it gets trickier since they make individual server requests:\n\n- Start a `clientMiddleware`\n- Start b `clientMiddleware`\n- Run a/b `clientLoaders` in parallel\n  - `a` `clientLoader` calls GET `/a/b.data?route=a`\n    - Start a `middleware`\n    - Run a loader\n    - Render turbo-stream `Response` to bubble back up via `next()`\n    - Finish a `middleware`\n  - `b` `clientLoader` calls GET `/a/b.data?route=b`\n    - Start a `middleware`\n    - Start b `middleware`\n    - Run b loader\n    - Render turbo-stream `Response` to bubble back up via `next()`\n    - Finish b `middleware`\n    - Finish a `middleware`\n- Finish b `clientMiddleware`\n- Finish a `clientMiddleware`\n\n### Other Thoughts\n\n- Middleware is data-focused, not an event system\n  - you should not be relying on middleware to track how many users hit a certain page etc\n  - middleware may run once for actions and once for loaders\n  - middleware will run independently for navigational loaders and fetcher loaders\n  - middleware may run many times for revalidations\n  - middleware may not run for revalidation opt outs\n- Middleware allows you to run logic specific to a branch of the tree before/after data fns\n  - logging\n  - auth/redirecting\n  - 404 handling\n\n[rfc]: https://github.com/remix-run/react-router/discussions/9564\n[client-context]: https://github.com/remix-run/react-router/discussions/9856\n[single-fetch]: https://remix.run/docs/en/main/guides/single-fetch\n[data-strategy]: https://reactrouter.com/v6/routers/create-browser-router#optsdatastrategy\n[remix-the-web]: https://github.com/mjackson/remix-the-web\n[async-provider]: https://github.com/ryanflorence/async-provider\n"
  },
  {
    "path": "decisions/0015-observability.md",
    "content": "# Title\n\nDate: 2025-09-22\n\nStatus: proposed\n\n## Context\n\nWe want it to be easy to add observability to production React Router applications. This involves the ability to add logging, error reporting, and performance tracing to your application on both the server and the client.\n\nWe always had a good story for user-facing error _display_ via `ErrorBoundary`, but until recently we only had a server-side error _reporting_ solution via the `entry.server` `handleError` export. In `7.8.2`, we shipped an `unstable_onError` client-side equivalent so it should now be possible to report on errors on the server and client pretty easily.\n\nWe have not historically had great recommendations for the other 2 facets of observability - logging and performance tracing. Middleware, shipped in `7.3.0` and stabilized in `7.9.0` gave us a way to \"wrap\" request handlers at any level of the tree, which provides a good solution for logging and _some_ high-level performance tracing. But it's too coarse-grained and does not allow folks to drill down into their applications.\n\nThis has also been raised in the (currently) 2nd-most upvoted Proposal in the past year: https://github.com/remix-run/react-router/discussions/13749.\n\nOne way to add fine-grained logging/tracing today is to manually include it in all of your loaders and actions, but this is tedious and error-prone.\n\nAnother way is to \"instrument\" the server build, which has long been our suggestion - initially to the folks at Sentry - and over time to RR users here and there in discord and github issues. but, we've never formally documented this as a recommended pattern, and it currently only works on the server and requires that you use a custom server.\n\n## Decision\n\nAdopt instrumentation as a first class API and the recommended way to implement observability in your application.\n\nThere are 2 levels in which we want to instrument:\n\n- handler (server) and router (client) level\n  - instrument the request handler on the server\n  - instrument navigations and fetcher calls on the client\n  - singular instrumentation per operation\n- route level\n  - instrument loaders, actions, middlewares, lazy\n  - multiple instrumentations per operation - multiple routes, multiple middlewares etc.\n\nOn the server, if you are using a custom server, this is already possible by wrapping the react router request handler and walking the `build.routes` tree and wrapping the route handlers.\n\nTo provide the same functionality when using `@react-router/serve` we need to open up a new API. Currently, I am proposing a new `instrumentations` export from `entry.server`. This will be applied to the server build in `createRequestHandler` and that way can work without a custom server. This will also allow custom-server users today to move some more code from their custom server into React Router by leveraging these new exports.\n\nA singular instrumentation function has the following shape:\n\n```tsx\nfunction instrumentationFunction(doTheActualThing, info) {\n  // Do some stuff before starting the thing\n\n  // Do the thing\n  await doTheActualThing();\n\n  // Do some stuff after the thing finishes\n}\n```\n\nThis API allows for a few things:\n\n- Consistent API for instrumenting any async action - from a handler, to a navigation, to a loader, or a middleware\n- By passing no arguments to `doTheActualThing()` and returning no data, this restricts the ability for instrumentation code to alter the actual runtime behavior of the app. I.e., you cannot modify arguments to loaders, nor change data returned from loaders. You can only report on the execution of loaders.\n- The `info` parameter allows us to pass relevant read-only information, such as the `request`, `context`, `routeId`, etc.\n- Nesting the call within a singular scope allows for contextual execution (i.e, `AsyncLocalStorage`) which enables things like nested OTEL traces to work properly\n\nHere's an example of this API on the server:\n\n```tsx\n// entry.server.tsx\n\nexport const instrumentations = [\n  {\n    // Wrap the request handler - applies to _all_ requests handled by RR, including:\n    // - manifest requests\n    // - document requests\n    // - `.data` requests\n    // - resource route requests\n    handler({ instrument }) {\n      // Calling instrument performs the actual instrumentation\n      instrument({\n        // Provide the instrumentation implementation for the request handler\n        async request(handleRequest, { request }) {\n          let start = Date.now();\n          console.log(`Request start: ${request.method} ${request.url}`);\n          try {\n            await handleRequest();\n          } finally {\n            let duration = Date.now() - start;\n            console.log(\n              `Request end: ${request.method} ${request.url} (${duration}ms)`,\n            );\n          }\n        },\n      });\n    },\n    // Instrument an individual route, allowing you to wrap middleware/loader/action/etc.\n    // This also gives you a place to do global \"shouldRevalidate\" which is a nice side\n    // effect as folks have asked for that for a long time\n    route({ instrument, id }) {\n      // `id` is the route id in case you want to instrument only some routes or\n      // instrument in a route-specific manner\n      if (id === \"routes/i-dont-care\") return;\n\n      instrument({\n        loader(callLoader, { request }) {\n          let start = Date.now();\n          console.log(`Loader start: ${request.method} ${request.url}`);\n          try {\n            await callLoader();\n          } finally {\n            let duration = Date.now() - start;\n            console.log(\n              `Loader end: ${request.method} ${request.url} (${duration}ms)`,\n            );\n          }\n        },\n        // action(), middleware(), lazy()\n      });\n    },\n  },\n];\n```\n\nOpen questions:\n\n- On the server we could technically do this at build time, but I don't expect this to have a large startup cost and doing it at build-time just feels a bit more magical and would differ from any examples we want to show in data mode.\n- Another option for custom server folks would be to make these parameters to `createRequestHandler`, but then we'd still need a way for `react-router-server` users to use them and thus we'd still need to support them in `entry.server`, so might as well make it consistent for both.\n\nClient-side, it's a similar story. You could do this today at the route level in Data mode before calling `createBrowserRouter`, and you could wrap `router.navigate`/`router.fetch` after that. but there's no way to instrument the router `initialize` method without \"ejecting\" to using the lower level `createRouter`. And there is no way to do this in framework mode.\n\nI think we can open up APIs similar to those in `entry.server` but do them on `createBrowserRouter` and `HydratedRouter`:\n\n```tsx\n// entry.client.tsx\n\nexport const instrumentations = [{\n  // Instrument router operations\n  router({ instrument }) {\n    instrument({\n      async initialize(callNavigate, info) { /*...*/ },\n      async navigate(callNavigate, info) { /*...*/ },\n      async fetch(callNavigate, info) { /*...*/ },\n    });\n  },\n  route({ instrument, id }) {\n    instrument({\n      lazy(callLazy, info) { /*...*/ },\n      middleware(callMiddleware, info) { /*...*/ },\n      loader(callLoader, info) { /*...*/ },\n      action(callAction, info) { /*...*/ },\n    });\n  },\n}];\n\n// Data mode\nlet router = createBrowserRouter(routes, { instrumentations })\n\n// Framework mode\n<HydratedRouter instrumentations={instrumentations} />\n```\n\nIn both of these cases, we'll handle the instrumentation at the router creation level. And by passing `instrumentRoute` into the router, we can properly instrument future routes discovered via `route.lazy` or `patchRouteOnNavigation`\n\n### Error Handling\n\nIt's important to note that the \"handler\" function will never throw. If the underlying loader/action throws, React Router will catch the error and return it out to you in case you need to perform some conditional logic in your instrumentation function - but your entire instrumentation function is thus guaranteed to run to completion even if the underlying application code errors.\n\n```tsx\nfunction instrumentationFunction(doTheActualThing, info) {\n  let { status, error } = await doTheActualThing();\n  // status is `\"success\" | \"error\"`\n  // `error` will only be defined if status === \"error\"\n\n  if (status === \"error\") {\n    // ...\n  } else {\n    // ...\n  }\n}\n```\n\nYou should not be using the instrumentation logic to report errors though, that's better served by `entry.server.tsx`'s `handleError` and `HydratedRouter`/`RouterProvider` `unstable_onError` props.\n\nIf you throw from your instrumentation function, we do not want that to impact runtime application behavior so React Router will gracefully swallow that error with a console warning and continue running as if you had returned successfully.\n\nIn both of these examples, the handlers and all other instrumentation functions will still run:\n\n```tsx\n// Throwing before calling the handler - we will detect this and still call the\n// handler internally\nfunction instrumentationFunction(doTheActualThing, info) {\n  somethingThatThrows();\n  await doTheActualThing();\n}\n\n// Throwing after calling the handler - error will be caught internally\nfunction instrumentationFunction2(doTheActualThing, info) {\n  await doTheActualThing();\n  somethingThatThrows();\n}\n```\n\n### Composition\n\nInstrumentations is an array so that you can compose together multiple independent instrumentations easily:\n\n```tsx\nlet router = createBrowserRouter(routes, {\n  instrumentations: [logNavigations, addWindowPerfTraces, addSentryPerfTraces],\n});\n```\n\n### Dynamic Instrumentations\n\nBy doing this at runtime, you should be able to enable instrumentation conditionally.\n\nClient side, it's trivial because it can be done on page load and avoid overhead on normal flows:\n\n```tsx\nlet enableInstrumentation = window.location.search.startsWith(\"?DEBUG\");\nlet router = createBrowserRouter(routes, {\n  instrumentations: enableInstrumentation ? [debuggingInstrumentations] : [],\n});\n```\n\nServer side, it's a bit tricker but should be doable with a custom server:\n\n```tsx\n// Assume you export `instrumentations` from entry.server\nlet getBuild = () => import(\"virtual:react-router/server-build\");\n\nlet instrumentedHandler = createRequestHandler({\n  build: getBuild,\n});\n\nlet unInstrumentedHandler = createRequestHandler({\n  build: () =>\n    getBuild().then((m) => ({\n      ...m,\n      entry: {\n        ...m.entry,\n        module: {\n          ...m.entry.module,\n          unstable_instrumentations: undefined,\n        },\n      },\n    })),\n});\n\napp.use((req, res, next) => {\n  let url = new URL(req.url, `http://${req.headers.host}`);\n  if (url.searchParams.has(\"DEBUG\")) {\n    return instrumentedHandler(req, res, next);\n  }\n  return unInstrumentedHandler(req, res, next);\n});\n```\n\n## Alternatives Considered\n\n### Events\n\nOriginally we wanted to add an [Events API](https://github.com/remix-run/react-router/discussions/9565), but this proved to [have issues](https://github.com/remix-run/react-router/discussions/13749#discussioncomment-14135422) with the ability to \"wrap\" logic for easier OTEL instrumentation. These were not [insurmountable](https://github.com/remix-run/react-router/discussions/13749#discussioncomment-14421335), but the solutions didn't feel great.\n\n### patchRoutes\n\nClient side, we also considered whether this could be done via `patchRoutes`, but that's currently intended mostly to add new routes and doesn't work for `route.lazy` routes. In some RSC-use cases it can update parts of an existing route, but it only allows updates for the server-rendered RSC \"elements,\" and doesn't walk the entire child tree to update children routes so it's not an ideal solution for updating loaders in the entire tree.\n\n### Naive Function wrapping\n\nThe original implementation of this proposal was a naive simple wrapping of functions, but we moved away from this because by putting the wrapped function arguments (i.e., loader) in control of the user, they could potentially modify them and abuse the API to change runtime behavior instead of just instrument/observe. We want instrumentation to be limited to that - and it should not be able to change app behavior.\n\n```tsx\nfunction instrumentRoute(route: RouteModule): RequestHandler {\n  let { loader } = route;\n  let newRoute = { ...route };\n  if (loader) {\n    newRoute.loader = (args) => {\n      console.log(\"Loader start\");\n      try {\n        // ⚠️ The user could send whatever they want into the actual loader here\n        return await loader(...args);\n      } finally {\n        console.log(\"Loader end\");\n      }\n    };\n  }\n  return newRoute;\n}\n```\n"
  },
  {
    "path": "decisions/template.md",
    "content": "# Title\n\nDate: YYYY-MM-DD\n\nStatus: proposed | rejected | accepted | deprecated | … | superseded by [0005](0005-example.md)\n\n## Context\n\n## Decision\n\n## Consequences\n"
  },
  {
    "path": "docs/api/components/Await.md",
    "content": "---\ntitle: Await\n---\n\n# Await\n\n<!--\n⚠️ ⚠️ IMPORTANT ⚠️ ⚠️ \n\nThank you for helping improve our documentation!\n\nThis file is auto-generated from the JSDoc comments in the source\ncode, so please edit the JSDoc comments in the file below and this\nfile will be re-generated once those changes are merged.\n\nhttps://github.com/remix-run/react-router/blob/main/packages/react-router/lib/components.tsx\n-->\n\n[MODES: framework, data]\n\n## Summary\n\n[Reference Documentation ↗](https://api.reactrouter.com/v7/functions/react-router.Await.html)\n\nUsed to render promise values with automatic error handling.\n\n**Note:** `<Await>` expects to be rendered inside a [`<React.Suspense>`](https://react.dev/reference/react/Suspense)\n\n```tsx\nimport { Await, useLoaderData } from \"react-router\";\n\nexport async function loader() {\n  // not awaited\n  const reviews = getReviews();\n  // awaited (blocks the transition)\n  const book = await fetch(\"/api/book\").then((res) => res.json());\n  return { book, reviews };\n}\n\nfunction Book() {\n  const { book, reviews } = useLoaderData();\n  return (\n    <div>\n      <h1>{book.title}</h1>\n      <p>{book.description}</p>\n      <React.Suspense fallback={<ReviewsSkeleton />}>\n        <Await\n          resolve={reviews}\n          errorElement={\n            <div>Could not load reviews 😬</div>\n          }\n          children={(resolvedReviews) => (\n            <Reviews items={resolvedReviews} />\n          )}\n        />\n      </React.Suspense>\n    </div>\n  );\n}\n```\n\n## Signature\n\n```tsx\nfunction Await<Resolve>({\n  children,\n  errorElement,\n  resolve,\n}: AwaitProps<Resolve>)\n```\n\n## Props\n\n### children\n\nWhen using a function, the resolved value is provided as the parameter.\n\n```tsx [2]\n<Await resolve={reviewsPromise}>\n  {(resolvedReviews) => <Reviews items={resolvedReviews} />}\n</Await>\n```\n\nWhen using React elements, [`useAsyncValue`](../hooks/useAsyncValue) will provide the\nresolved value:\n\n```tsx [2]\n<Await resolve={reviewsPromise}>\n  <Reviews />\n</Await>\n\nfunction Reviews() {\n  const resolvedReviews = useAsyncValue();\n  return <div>...</div>;\n}\n```\n\n### errorElement\n\nThe error element renders instead of the `children` when the [`Promise`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise)\nrejects.\n\n```tsx\n<Await\n  errorElement={<div>Oops</div>}\n  resolve={reviewsPromise}\n>\n  <Reviews />\n</Await>\n```\n\nTo provide a more contextual error, you can use the [`useAsyncError`](../hooks/useAsyncError) in a\nchild component\n\n```tsx\n<Await\n  errorElement={<ReviewsError />}\n  resolve={reviewsPromise}\n>\n  <Reviews />\n</Await>\n\nfunction ReviewsError() {\n  const error = useAsyncError();\n  return <div>Error loading reviews: {error.message}</div>;\n}\n```\n\nIf you do not provide an `errorElement`, the rejected value will bubble up\nto the nearest route-level [`ErrorBoundary`](../../start/framework/route-module#errorboundary)\nand be accessible via the [`useRouteError`](../hooks/useRouteError) hook.\n\n### resolve\n\nTakes a [`Promise`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise)\nreturned from a [`loader`](../../start/framework/route-module#loader) to be\nresolved and rendered.\n\n```tsx\nimport { Await, useLoaderData } from \"react-router\";\n\nexport async function loader() {\n  let reviews = getReviews(); // not awaited\n  let book = await getBook();\n  return {\n    book,\n    reviews, // this is a promise\n  };\n}\n\nexport default function Book() {\n  const {\n    book,\n    reviews, // this is the same promise\n  } = useLoaderData();\n\n  return (\n    <div>\n      <h1>{book.title}</h1>\n      <p>{book.description}</p>\n      <React.Suspense fallback={<ReviewsSkeleton />}>\n        <Await\n          // and is the promise we pass to Await\n          resolve={reviews}\n        >\n          <Reviews />\n        </Await>\n      </React.Suspense>\n    </div>\n  );\n}\n```\n\n"
  },
  {
    "path": "docs/api/components/Form.md",
    "content": "---\ntitle: Form\n---\n\n# Form\n\n<!--\n⚠️ ⚠️ IMPORTANT ⚠️ ⚠️ \n\nThank you for helping improve our documentation!\n\nThis file is auto-generated from the JSDoc comments in the source\ncode, so please edit the JSDoc comments in the file below and this\nfile will be re-generated once those changes are merged.\n\nhttps://github.com/remix-run/react-router/blob/main/packages/react-router/lib/dom/lib.tsx\n-->\n\n[MODES: framework, data]\n\n## Summary\n\n[Reference Documentation ↗](https://api.reactrouter.com/v7/functions/react-router.Form.html)\n\nA progressively enhanced HTML [`<form>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/form)\nthat submits data to actions via [`fetch`](https://developer.mozilla.org/en-US/docs/Web/API/fetch),\nactivating pending states in [`useNavigation`](../hooks/useNavigation) which enables advanced\nuser interfaces beyond a basic HTML [`<form>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/form).\nAfter a form's `action` completes, all data on the page is automatically\nrevalidated to keep the UI in sync with the data.\n\nBecause it uses the HTML form API, server rendered pages are interactive at a\nbasic level before JavaScript loads. Instead of React Router managing the\nsubmission, the browser manages the submission as well as the pending states\n(like the spinning favicon). After JavaScript loads, React Router takes over\nenabling web application user experiences.\n\n`Form` is most useful for submissions that should also change the URL or\notherwise add an entry to the browser history stack. For forms that shouldn't\nmanipulate the browser [`History`](https://developer.mozilla.org/en-US/docs/Web/API/History)\nstack, use [`<fetcher.Form>`](https://api.reactrouter.com/v7/types/react-router.FetcherWithComponents.html#Form).\n\n```tsx\nimport { Form } from \"react-router\";\n\nfunction NewEvent() {\n  return (\n    <Form action=\"/events\" method=\"post\">\n      <input name=\"title\" type=\"text\" />\n      <input name=\"description\" type=\"text\" />\n    </Form>\n  );\n}\n```\n\n## Props\n\n### action\n\nThe URL to submit the form data to. If `undefined`, this defaults to the\nclosest route in context.\n\n### discover\n\nDefines the form [lazy route discovery](../../explanation/lazy-route-discovery) behavior.\n\n- **render** — default, discover the route when the form renders\n- **none** — don't eagerly discover, only discover if the form is submitted\n\n```tsx\n<Form /> // default (\"render\")\n<Form discover=\"render\" />\n<Form discover=\"none\" />\n```\n\n### encType\n\nThe encoding type to use for the form submission.\n\n```tsx\n<Form encType=\"application/x-www-form-urlencoded\"/>  // Default\n<Form encType=\"multipart/form-data\"/>\n<Form encType=\"text/plain\"/>\n```\n\n### fetcherKey\n\nIndicates a specific fetcherKey to use when using `navigate={false}` so you\ncan pick up the fetcher's state in a different component in a [`useFetcher`](../hooks/useFetcher).\n\n### method\n\nThe HTTP verb to use when the form is submitted. Supports `\"delete\"`,\n`\"get\"`, `\"patch\"`, `\"post\"`, and `\"put\"`.\n\nNative [`<form>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/form)\nonly supports `\"get\"` and `\"post\"`, avoid the other verbs if you'd like to\nsupport progressive enhancement\n\n### navigate\n\nWhen `false`, skips the navigation and submits via a fetcher internally.\nThis is essentially a shorthand for [`useFetcher`](../hooks/useFetcher) + `<fetcher.Form>` where\nyou don't care about the resulting data in this component.\n\n### onSubmit\n\nA function to call when the form is submitted. If you call\n[`event.preventDefault()`](https://developer.mozilla.org/en-US/docs/Web/API/Event/preventDefault)\nthen this form will not do anything.\n\n### preventScrollReset\n\nPrevent the scroll position from resetting to the top of the viewport on\ncompletion of the navigation when using the\n``<ScrollRestoration>`` component\n\n### relative\n\nDetermines whether the form action is relative to the route hierarchy or\nthe pathname. Use this if you want to opt out of navigating the route\nhierarchy and want to instead route based on slash-delimited URL segments.\nSee [`RelativeRoutingType`](https://api.reactrouter.com/v7/types/react-router.RelativeRoutingType.html).\n\n### reloadDocument\n\nForces a full document navigation instead of client side routing and data\nfetch.\n\n### replace\n\nReplaces the current entry in the browser [`History`](https://developer.mozilla.org/en-US/docs/Web/API/History)\nstack when the form navigates. Use this if you don't want the user to be\nable to click \"back\" to the page with the form on it.\n\n### state\n\nState object to add to the [`History`](https://developer.mozilla.org/en-US/docs/Web/API/History)\nstack entry for this navigation\n\n### viewTransition\n\nEnables a [View Transition](https://developer.mozilla.org/en-US/docs/Web/API/View_Transitions_API)\nfor this navigation. To apply specific styles during the transition, see\n[`useViewTransitionState`](../hooks/useViewTransitionState).\n\n### unstable_defaultShouldRevalidate\n\nSpecify the default revalidation behavior after this submission\n\nIf no `shouldRevalidate` functions are present on the active routes, then this\nvalue will be used directly.  Otherwise it will be passed into `shouldRevalidate`\nso the route can make the final determination on revalidation. This can be\nuseful when updating search params and you don't want to trigger a revalidation.\n\nBy default (when not specified), loaders will revalidate according to the routers\nstandard revalidation behavior.\n\n"
  },
  {
    "path": "docs/api/components/Link.md",
    "content": "---\ntitle: Link\n---\n\n# Link\n\n<!--\n⚠️ ⚠️ IMPORTANT ⚠️ ⚠️ \n\nThank you for helping improve our documentation!\n\nThis file is auto-generated from the JSDoc comments in the source\ncode, so please edit the JSDoc comments in the file below and this\nfile will be re-generated once those changes are merged.\n\nhttps://github.com/remix-run/react-router/blob/main/packages/react-router/lib/dom/lib.tsx\n-->\n\n[MODES: framework, data, declarative]\n\n## Summary\n\n[Reference Documentation ↗](https://api.reactrouter.com/v7/functions/react-router.Link.html)\n\nA progressively enhanced [`<a href>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/a)\nwrapper to enable navigation with client-side routing.\n\n```tsx\nimport { Link } from \"react-router\";\n\n<Link to=\"/dashboard\">Dashboard</Link>;\n\n<Link\n  to={{\n    pathname: \"/some/path\",\n    search: \"?query=string\",\n    hash: \"#hash\",\n  }}\n/>;\n```\n\n## Props\n\n### discover\n\n[modes: framework]\n\nDefines the link [lazy route discovery](../../explanation/lazy-route-discovery) behavior.\n\n- **render** — default, discover the route when the link renders\n- **none** — don't eagerly discover, only discover if the link is clicked\n\n```tsx\n<Link /> // default (\"render\")\n<Link discover=\"render\" />\n<Link discover=\"none\" />\n```\n\n### prefetch\n\n[modes: framework]\n\nDefines the data and module prefetching behavior for the link.\n\n```tsx\n<Link /> // default\n<Link prefetch=\"none\" />\n<Link prefetch=\"intent\" />\n<Link prefetch=\"render\" />\n<Link prefetch=\"viewport\" />\n```\n\n- **none** — default, no prefetching\n- **intent** — prefetches when the user hovers or focuses the link\n- **render** — prefetches when the link renders\n- **viewport** — prefetches when the link is in the viewport, very useful for mobile\n\nPrefetching is done with HTML [`<link rel=\"prefetch\">`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/link)\ntags. They are inserted after the link.\n\n```tsx\n<a href=\"...\" />\n<a href=\"...\" />\n<link rel=\"prefetch\" /> // might conditionally render\n```\n\nBecause of this, if you are using `nav :last-child` you will need to use\n`nav :last-of-type` so the styles don't conditionally fall off your last link\n(and any other similar selectors).\n\n### preventScrollReset\n\n[modes: framework, data]\n\nPrevents the scroll position from being reset to the top of the window when\nthe link is clicked and the app is using [`ScrollRestoration`](../components/ScrollRestoration). This only\nprevents new locations resetting scroll to the top, scroll position will be\nrestored for back/forward button navigation.\n\n```tsx\n<Link to=\"?tab=one\" preventScrollReset />\n```\n\n### relative\n\n[modes: framework, data, declarative]\n\nDefines the relative path behavior for the link.\n\n```tsx\n<Link to=\"..\" /> // default: \"route\"\n<Link relative=\"route\" />\n<Link relative=\"path\" />\n```\n\nConsider a route hierarchy where a parent route pattern is `\"blog\"` and a child\nroute pattern is `\"blog/:slug/edit\"`.\n\n- **route** — default, resolves the link relative to the route pattern. In the\nexample above, a relative link of `\"...\"` will remove both `:slug/edit` segments\nback to `\"/blog\"`.\n- **path** — relative to the path so `\"...\"` will only remove one URL segment up\nto `\"/blog/:slug\"`\n\nNote that index routes and layout routes do not have paths so they are not\nincluded in the relative path calculation.\n\n### reloadDocument\n\n[modes: framework, data, declarative]\n\nWill use document navigation instead of client side routing when the link is\nclicked: the browser will handle the transition normally (as if it were an\n[`<a href>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/a)).\n\n```tsx\n<Link to=\"/logout\" reloadDocument />\n```\n\n### replace\n\n[modes: framework, data, declarative]\n\nReplaces the current entry in the [`History`](https://developer.mozilla.org/en-US/docs/Web/API/History)\nstack instead of pushing a new one onto it.\n\n```tsx\n<Link replace />\n```\n\n```\n# with a history stack like this\nA -> B\n\n# normal link click pushes a new entry\nA -> B -> C\n\n# but with `replace`, B is replaced by C\nA -> C\n```\n\n### state\n\n[modes: framework, data, declarative]\n\nAdds persistent client side routing state to the next location.\n\n```tsx\n<Link to=\"/somewhere/else\" state={{ some: \"value\" }} />\n```\n\nThe location state is accessed from the `location`.\n\n```tsx\nfunction SomeComp() {\n  const location = useLocation();\n  location.state; // { some: \"value\" }\n}\n```\n\nThis state is inaccessible on the server as it is implemented on top of\n[`history.state`](https://developer.mozilla.org/en-US/docs/Web/API/History/state)\n\n### to\n\n[modes: framework, data, declarative]\n\nCan be a string or a partial [`Path`](https://api.reactrouter.com/v7/interfaces/react-router.Path.html):\n\n```tsx\n<Link to=\"/some/path\" />\n\n<Link\n  to={{\n    pathname: \"/some/path\",\n    search: \"?query=string\",\n    hash: \"#hash\",\n  }}\n/>\n```\n\n### viewTransition\n\n[modes: framework, data]\n\nEnables a [View Transition](https://developer.mozilla.org/en-US/docs/Web/API/View_Transitions_API)\nfor this navigation.\n\n```jsx\n<Link to={to} viewTransition>\n  Click me\n</Link>\n```\n\nTo apply specific styles for the transition, see [`useViewTransitionState`](../hooks/useViewTransitionState)\n\n### unstable_defaultShouldRevalidate\n\n[modes: framework, data, declarative]\n\nSpecify the default revalidation behavior for the navigation.\n\n```tsx\n<Link to=\"/some/path\" unstable_defaultShouldRevalidate={false} />\n```\n\nIf no `shouldRevalidate` functions are present on the active routes, then this\nvalue will be used directly.  Otherwise it will be passed into `shouldRevalidate`\nso the route can make the final determination on revalidation. This can be\nuseful when updating search params and you don't want to trigger a revalidation.\n\nBy default (when not specified), loaders will revalidate according to the routers\nstandard revalidation behavior.\n\n### unstable_mask\n\n[modes: framework, data]\n\nMasked path for for this navigation, when you want to navigate the router to\none location but display a separate location in the URL bar.\n\nThis is useful for contextual navigations such as opening an image in a modal\non top of a gallery while keeping the underlying gallery active. If a user\nshares the masked URL, or opens the link in a new tab, they will only load\nthe masked location without the underlying contextual location.\n\nThis feature relies on `history.state` and is thus only intended for SPA uses\nand SSR renders will not respect the masking.\n\n```tsx\n// routes/gallery.tsx\nexport function clientLoader({ request }: Route.LoaderArgs) {\n  let sp = new URL(request.url).searchParams;\n  return {\n    images: getImages(),\n    modalImage: sp.has(\"image\") ? getImage(sp.get(\"image\")!) : null,\n  };\n}\n\nexport default function Gallery({ loaderData }: Route.ComponentProps) {\n  return (\n    <>\n      <GalleryGrid>\n       {loaderData.images.map((image) => (\n         <Link\n           key={image.id}\n           to={`/gallery?image=${image.id}`}\n           unstable_mask={`/images/${image.id}`}\n         >\n           <img src={image.url} alt={image.alt} />\n         </Link>\n       ))}\n      </GalleryGrid>\n\n      {data.modalImage ? (\n        <dialog open>\n          <img src={data.modalImage.url} alt={data.modalImage.alt} />\n        </dialog>\n      ) : null}\n    </>\n  );\n}\n```\n\n"
  },
  {
    "path": "docs/api/components/Links.md",
    "content": "---\ntitle: Links\n---\n\n# Links\n\n<!--\n⚠️ ⚠️ IMPORTANT ⚠️ ⚠️ \n\nThank you for helping improve our documentation!\n\nThis file is auto-generated from the JSDoc comments in the source\ncode, so please edit the JSDoc comments in the file below and this\nfile will be re-generated once those changes are merged.\n\nhttps://github.com/remix-run/react-router/blob/main/packages/react-router/lib/dom/ssr/components.tsx\n-->\n\n[MODES: framework]\n\n## Summary\n\n[Reference Documentation ↗](https://api.reactrouter.com/v7/functions/react-router.Links.html)\n\nRenders all the [`<link>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/link)\ntags created by the route module's [`links`](../../start/framework/route-module#links)\nexport. You should render it inside the [`<head>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/head)\nof your document.\n\n```tsx\nimport { Links } from \"react-router\";\n\nexport default function Root() {\n  return (\n    <html>\n      <head>\n        <Links />\n      </head>\n      <body></body>\n    </html>\n  );\n}\n```\n\n## Signature\n\n```tsx\nfunction Links({ nonce, crossOrigin }: LinksProps): React.JSX.Element\n```\n\n## Props\n\n### nonce\n\nA [`nonce`](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Global_attributes/nonce)\nattribute to render on the [`<link>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/link)\nelement\n\n### crossOrigin\n\nA [`crossOrigin`](https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/crossorigin)\nattribute to render on the [`<link>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/link)\nelement\n\n"
  },
  {
    "path": "docs/api/components/Meta.md",
    "content": "---\ntitle: Meta\n---\n\n# Meta\n\n<!--\n⚠️ ⚠️ IMPORTANT ⚠️ ⚠️ \n\nThank you for helping improve our documentation!\n\nThis file is auto-generated from the JSDoc comments in the source\ncode, so please edit the JSDoc comments in the file below and this\nfile will be re-generated once those changes are merged.\n\nhttps://github.com/remix-run/react-router/blob/main/packages/react-router/lib/dom/ssr/components.tsx\n-->\n\n[MODES: framework]\n\n## Summary\n\n[Reference Documentation ↗](https://api.reactrouter.com/v7/functions/react-router.Meta.html)\n\nRenders all the [`<meta>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/meta)\ntags created by the route module's [`meta`](../../start/framework/route-module#meta)\nexport. You should render it inside the [`<head>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/head)\nof your document.\n\n```tsx\nimport { Meta } from \"react-router\";\n\nexport default function Root() {\n  return (\n    <html>\n      <head>\n        <Meta />\n      </head>\n    </html>\n  );\n}\n```\n\n## Signature\n\n```tsx\nfunction Meta(): React.JSX.Element\n```\n\n"
  },
  {
    "path": "docs/api/components/NavLink.md",
    "content": "---\ntitle: NavLink\n---\n\n# NavLink\n\n<!--\n⚠️ ⚠️ IMPORTANT ⚠️ ⚠️ \n\nThank you for helping improve our documentation!\n\nThis file is auto-generated from the JSDoc comments in the source\ncode, so please edit the JSDoc comments in the file below and this\nfile will be re-generated once those changes are merged.\n\nhttps://github.com/remix-run/react-router/blob/main/packages/react-router/lib/dom/lib.tsx\n-->\n\n[MODES: framework, data, declarative]\n\n## Summary\n\n[Reference Documentation ↗](https://api.reactrouter.com/v7/functions/react-router.NavLink.html)\n\nWraps [`<Link>`](../components/Link) with additional props for styling active and\npending states.\n\n- Automatically applies classes to the link based on its `active` and `pending`\nstates, see [`NavLinkProps.className`](https://api.reactrouter.com/v7/interfaces/react-router.NavLinkProps.html#className)\n  - Note that `pending` is only available with Framework and Data modes.\n- Automatically applies `aria-current=\"page\"` to the link when the link is active.\nSee [`aria-current`](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Attributes/aria-current)\non MDN.\n- States are additionally available through the className, style, and children\nrender props. See [`NavLinkRenderProps`](https://api.reactrouter.com/v7/types/react-router.NavLinkRenderProps.html).\n\n```tsx\n<NavLink to=\"/message\">Messages</NavLink>\n\n// Using render props\n<NavLink\n  to=\"/messages\"\n  className={({ isActive, isPending }) =>\n    isPending ? \"pending\" : isActive ? \"active\" : \"\"\n  }\n>\n  Messages\n</NavLink>\n```\n\n## Props\n\n### caseSensitive\n\n[modes: framework, data, declarative]\n\nChanges the matching logic to make it case-sensitive:\n\n| Link                                         | URL           | isActive |\n| -------------------------------------------- | ------------- | -------- |\n| `<NavLink to=\"/SpOnGe-bOB\" />`               | `/sponge-bob` | true     |\n| `<NavLink to=\"/SpOnGe-bOB\" caseSensitive />` | `/sponge-bob` | false    |\n\n### children\n\n[modes: framework, data, declarative]\n\nCan be regular React children or a function that receives an object with the\n`active` and `pending` states of the link.\n\n ```tsx\n <NavLink to=\"/tasks\">\n   {({ isActive }) => (\n     <span className={isActive ? \"active\" : \"\"}>Tasks</span>\n   )}\n </NavLink>\n ```\n\n### className\n\n[modes: framework, data, declarative]\n\nClasses are automatically applied to `NavLink` that correspond to the state.\n\n```css\na.active {\n  color: red;\n}\na.pending {\n  color: blue;\n}\na.transitioning {\n  view-transition-name: my-transition;\n}\n```\n\nOr you can specify a function that receives [`NavLinkRenderProps`](https://api.reactrouter.com/v7/types/react-router.NavLinkRenderProps.html) and\nreturns the `className`:\n\n```tsx\n<NavLink className={({ isActive, isPending }) => (\n  isActive ? \"my-active-class\" :\n  isPending ? \"my-pending-class\" :\n  \"\"\n)} />\n```\n\n### discover\n\n[modes: framework]\n\nDefines the link [lazy route discovery](../../explanation/lazy-route-discovery) behavior.\n\n- **render** — default, discover the route when the link renders\n- **none** — don't eagerly discover, only discover if the link is clicked\n\n```tsx\n<Link /> // default (\"render\")\n<Link discover=\"render\" />\n<Link discover=\"none\" />\n```\n\n### end\n\n[modes: framework, data, declarative]\n\nChanges the matching logic for the `active` and `pending` states to only match\nto the \"end\" of the [`NavLinkProps.to`](https://api.reactrouter.com/v7/interfaces/react-router.NavLinkProps.html#to). If the URL is longer, it will no\nlonger be considered active.\n\n| Link                          | URL          | isActive |\n| ----------------------------- | ------------ | -------- |\n| `<NavLink to=\"/tasks\" />`     | `/tasks`     | true     |\n| `<NavLink to=\"/tasks\" />`     | `/tasks/123` | true     |\n| `<NavLink to=\"/tasks\" end />` | `/tasks`     | true     |\n| `<NavLink to=\"/tasks\" end />` | `/tasks/123` | false    |\n\n`<NavLink to=\"/\">` is an exceptional case because _every_ URL matches `/`.\nTo avoid this matching every single route by default, it effectively ignores\nthe `end` prop and only matches when you're at the root route.\n\n### prefetch\n\n[modes: framework]\n\nDefines the data and module prefetching behavior for the link.\n\n```tsx\n<Link /> // default\n<Link prefetch=\"none\" />\n<Link prefetch=\"intent\" />\n<Link prefetch=\"render\" />\n<Link prefetch=\"viewport\" />\n```\n\n- **none** — default, no prefetching\n- **intent** — prefetches when the user hovers or focuses the link\n- **render** — prefetches when the link renders\n- **viewport** — prefetches when the link is in the viewport, very useful for mobile\n\nPrefetching is done with HTML [`<link rel=\"prefetch\">`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/link)\ntags. They are inserted after the link.\n\n```tsx\n<a href=\"...\" />\n<a href=\"...\" />\n<link rel=\"prefetch\" /> // might conditionally render\n```\n\nBecause of this, if you are using `nav :last-child` you will need to use\n`nav :last-of-type` so the styles don't conditionally fall off your last link\n(and any other similar selectors).\n\n### preventScrollReset\n\n[modes: framework, data]\n\nPrevents the scroll position from being reset to the top of the window when\nthe link is clicked and the app is using [`ScrollRestoration`](../components/ScrollRestoration). This only\nprevents new locations resetting scroll to the top, scroll position will be\nrestored for back/forward button navigation.\n\n```tsx\n<Link to=\"?tab=one\" preventScrollReset />\n```\n\n### relative\n\n[modes: framework, data, declarative]\n\nDefines the relative path behavior for the link.\n\n```tsx\n<Link to=\"..\" /> // default: \"route\"\n<Link relative=\"route\" />\n<Link relative=\"path\" />\n```\n\nConsider a route hierarchy where a parent route pattern is `\"blog\"` and a child\nroute pattern is `\"blog/:slug/edit\"`.\n\n- **route** — default, resolves the link relative to the route pattern. In the\nexample above, a relative link of `\"...\"` will remove both `:slug/edit` segments\nback to `\"/blog\"`.\n- **path** — relative to the path so `\"...\"` will only remove one URL segment up\nto `\"/blog/:slug\"`\n\nNote that index routes and layout routes do not have paths so they are not\nincluded in the relative path calculation.\n\n### reloadDocument\n\n[modes: framework, data, declarative]\n\nWill use document navigation instead of client side routing when the link is\nclicked: the browser will handle the transition normally (as if it were an\n[`<a href>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/a)).\n\n```tsx\n<Link to=\"/logout\" reloadDocument />\n```\n\n### replace\n\n[modes: framework, data, declarative]\n\nReplaces the current entry in the [`History`](https://developer.mozilla.org/en-US/docs/Web/API/History)\nstack instead of pushing a new one onto it.\n\n```tsx\n<Link replace />\n```\n\n```\n# with a history stack like this\nA -> B\n\n# normal link click pushes a new entry\nA -> B -> C\n\n# but with `replace`, B is replaced by C\nA -> C\n```\n\n### state\n\n[modes: framework, data, declarative]\n\nAdds persistent client side routing state to the next location.\n\n```tsx\n<Link to=\"/somewhere/else\" state={{ some: \"value\" }} />\n```\n\nThe location state is accessed from the `location`.\n\n```tsx\nfunction SomeComp() {\n  const location = useLocation();\n  location.state; // { some: \"value\" }\n}\n```\n\nThis state is inaccessible on the server as it is implemented on top of\n[`history.state`](https://developer.mozilla.org/en-US/docs/Web/API/History/state)\n\n### style\n\n[modes: framework, data, declarative]\n\nStyles can also be applied dynamically via a function that receives\n[`NavLinkRenderProps`](https://api.reactrouter.com/v7/types/react-router.NavLinkRenderProps.html) and returns the styles:\n\n```tsx\n<NavLink to=\"/tasks\" style={{ color: \"red\" }} />\n<NavLink to=\"/tasks\" style={({ isActive, isPending }) => ({\n  color:\n    isActive ? \"red\" :\n    isPending ? \"blue\" : \"black\"\n})} />\n```\n\n### to\n\n[modes: framework, data, declarative]\n\nCan be a string or a partial [`Path`](https://api.reactrouter.com/v7/interfaces/react-router.Path.html):\n\n```tsx\n<Link to=\"/some/path\" />\n\n<Link\n  to={{\n    pathname: \"/some/path\",\n    search: \"?query=string\",\n    hash: \"#hash\",\n  }}\n/>\n```\n\n### viewTransition\n\n[modes: framework, data]\n\nEnables a [View Transition](https://developer.mozilla.org/en-US/docs/Web/API/View_Transitions_API)\nfor this navigation.\n\n```jsx\n<Link to={to} viewTransition>\n  Click me\n</Link>\n```\n\nTo apply specific styles for the transition, see [`useViewTransitionState`](../hooks/useViewTransitionState)\n\n"
  },
  {
    "path": "docs/api/components/Navigate.md",
    "content": "---\ntitle: Navigate\n---\n\n# Navigate\n\n<!--\n⚠️ ⚠️ IMPORTANT ⚠️ ⚠️ \n\nThank you for helping improve our documentation!\n\nThis file is auto-generated from the JSDoc comments in the source\ncode, so please edit the JSDoc comments in the file below and this\nfile will be re-generated once those changes are merged.\n\nhttps://github.com/remix-run/react-router/blob/main/packages/react-router/lib/components.tsx\n-->\n\n[MODES: framework, data, declarative]\n\n## Summary\n\n[Reference Documentation ↗](https://api.reactrouter.com/v7/functions/react-router.Navigate.html)\n\nA component-based version of [`useNavigate`](../hooks/useNavigate) to use in a\n[`React.Component` class](https://react.dev/reference/react/Component) where\nhooks cannot be used.\n\nIt's recommended to avoid using this component in favor of [`useNavigate`](../hooks/useNavigate).\n\n```tsx\n<Navigate to=\"/tasks\" />\n```\n\n## Signature\n\n```tsx\nfunction Navigate({ to, replace, state, relative }: NavigateProps): null\n```\n\n## Props\n\n### relative\n\nHow to interpret relative routing in the `to` prop.\nSee [`RelativeRoutingType`](https://api.reactrouter.com/v7/types/react-router.RelativeRoutingType.html).\n\n### replace\n\nWhether to replace the current entry in the [`History`](https://developer.mozilla.org/en-US/docs/Web/API/History)\nstack\n\n### state\n\nState to pass to the new [`Location`](https://api.reactrouter.com/v7/interfaces/react-router.Location.html) to store in [`history.state`](https://developer.mozilla.org/en-US/docs/Web/API/History/state).\n\n### to\n\nThe path to navigate to. This can be a string or a [`Path`](https://api.reactrouter.com/v7/interfaces/react-router.Path.html) object\n\n"
  },
  {
    "path": "docs/api/components/Outlet.md",
    "content": "---\ntitle: Outlet\n---\n\n# Outlet\n\n<!--\n⚠️ ⚠️ IMPORTANT ⚠️ ⚠️ \n\nThank you for helping improve our documentation!\n\nThis file is auto-generated from the JSDoc comments in the source\ncode, so please edit the JSDoc comments in the file below and this\nfile will be re-generated once those changes are merged.\n\nhttps://github.com/remix-run/react-router/blob/main/packages/react-router/lib/components.tsx\n-->\n\n[MODES: framework, data, declarative]\n\n## Summary\n\n[Reference Documentation ↗](https://api.reactrouter.com/v7/functions/react-router.Outlet.html)\n\nRenders the matching child route of a parent route or nothing if no child\nroute matches.\n\n```tsx\nimport { Outlet } from \"react-router\";\n\nexport default function SomeParent() {\n  return (\n    <div>\n      <h1>Parent Content</h1>\n      <Outlet />\n    </div>\n  );\n}\n```\n\n## Signature\n\n```tsx\nfunction Outlet(props: OutletProps): React.ReactElement | null\n```\n\n## Props\n\n### context\n\nProvides a context value to the element tree below the outlet. Use when\nthe parent route needs to provide values to child routes.\n\n```tsx\n<Outlet context={myContextValue} />\n```\n\nAccess the context with [`useOutletContext`](../hooks/useOutletContext).\n\n"
  },
  {
    "path": "docs/api/components/PrefetchPageLinks.md",
    "content": "---\ntitle: PrefetchPageLinks\n---\n\n# PrefetchPageLinks\n\n<!--\n⚠️ ⚠️ IMPORTANT ⚠️ ⚠️ \n\nThank you for helping improve our documentation!\n\nThis file is auto-generated from the JSDoc comments in the source\ncode, so please edit the JSDoc comments in the file below and this\nfile will be re-generated once those changes are merged.\n\nhttps://github.com/remix-run/react-router/blob/main/packages/react-router/lib/dom/ssr/components.tsx\n-->\n\n[MODES: framework]\n\n## Summary\n\n[Reference Documentation ↗](https://api.reactrouter.com/v7/functions/react-router.PrefetchPageLinks.html)\n\nRenders [`<link rel=prefetch|modulepreload>`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLLinkElement/rel)\ntags for modules and data of another page to enable an instant navigation to\nthat page. [`<Link prefetch>`](./Link#prefetch) uses this internally, but you\ncan render it to prefetch a page for any other reason.\n\nFor example, you may render one of this as the user types into a search field\nto prefetch search results before they click through to their selection.\n\n```tsx\nimport { PrefetchPageLinks } from \"react-router\";\n\n<PrefetchPageLinks page=\"/absolute/path\" />\n```\n\n## Signature\n\n```tsx\nfunction PrefetchPageLinks({ page, ...linkProps }: PageLinkDescriptor)\n```\n\n## Props\n\n### page\n\nThe absolute path of the page to prefetch, e.g. `/absolute/path`.\n\n### linkProps\n\nAdditional props to spread onto the [`<link>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/link) tags, such as [`crossOrigin`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLLinkElement/crossOrigin),\n[`integrity`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLLinkElement/integrity),\n[`rel`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLLinkElement/rel),\netc.\n\n"
  },
  {
    "path": "docs/api/components/Route.md",
    "content": "---\ntitle: Route\n---\n\n# Route\n\n<!--\n⚠️ ⚠️ IMPORTANT ⚠️ ⚠️ \n\nThank you for helping improve our documentation!\n\nThis file is auto-generated from the JSDoc comments in the source\ncode, so please edit the JSDoc comments in the file below and this\nfile will be re-generated once those changes are merged.\n\nhttps://github.com/remix-run/react-router/blob/main/packages/react-router/lib/components.tsx\n-->\n\n[MODES: framework, data, declarative]\n\n## Summary\n\n[Reference Documentation ↗](https://api.reactrouter.com/v7/functions/react-router.Route.html)\n\nConfigures an element to render when a pattern matches the current location.\nIt must be rendered within a [`Routes`](../components/Routes) element. Note that these routes\ndo not participate in data loading, actions, code splitting, or any other\nroute module features.\n\n```tsx\n// Usually used in a declarative router\nfunction App() {\n  return (\n    <BrowserRouter>\n      <Routes>\n        <Route index element={<StepOne />} />\n        <Route path=\"step-2\" element={<StepTwo />} />\n        <Route path=\"step-3\" element={<StepThree />} />\n      </Routes>\n   </BrowserRouter>\n  );\n}\n\n// But can be used with a data router as well if you prefer the JSX notation\nconst routes = createRoutesFromElements(\n  <>\n    <Route index loader={step1Loader} Component={StepOne} />\n    <Route path=\"step-2\" loader={step2Loader} Component={StepTwo} />\n    <Route path=\"step-3\" loader={step3Loader} Component={StepThree} />\n  </>\n);\n\nconst router = createBrowserRouter(routes);\n\nfunction App() {\n  return <RouterProvider router={router} />;\n}\n```\n\n## Signature\n\n```tsx\nfunction Route(props: RouteProps): React.ReactElement | null\n```\n\n## Props\n\n### action\n\nThe route action.\nSee [`action`](../../start/data/route-object#action).\n\n### caseSensitive\n\nWhether the path should be case-sensitive. Defaults to `false`.\n\n### Component\n\nThe React Component to render when this route matches.\nMutually exclusive with `element`.\n\n### children\n\nChild Route components\n\n### element\n\nThe React element to render when this Route matches.\nMutually exclusive with `Component`.\n\n### ErrorBoundary\n\nThe React Component to render at this route if an error occurs.\nMutually exclusive with `errorElement`.\n\n### errorElement\n\nThe React element to render at this route if an error occurs.\nMutually exclusive with `ErrorBoundary`.\n\n### handle\n\nThe route handle.\n\n### HydrateFallback\n\nThe React Component to render while this router is loading data.\nMutually exclusive with `hydrateFallbackElement`.\n\n### hydrateFallbackElement\n\nThe React element to render while this router is loading data.\nMutually exclusive with `HydrateFallback`.\n\n### id\n\nThe unique identifier for this route (for use with [`DataRouter`](https://api.reactrouter.com/v7/interfaces/react-router.DataRouter.html)s)\n\n### index\n\nWhether this is an index route.\n\n### lazy\n\nA function that returns a promise that resolves to the route object.\nUsed for code-splitting routes.\nSee [`lazy`](../../start/data/route-object#lazy).\n\n### loader\n\nThe route loader.\nSee [`loader`](../../start/data/route-object#loader).\n\n### path\n\nThe path pattern to match. If unspecified or empty, then this becomes a\nlayout route.\n\n### shouldRevalidate\n\nThe route shouldRevalidate function.\nSee [`shouldRevalidate`](../../start/data/route-object#shouldRevalidate).\n\n"
  },
  {
    "path": "docs/api/components/Routes.md",
    "content": "---\ntitle: Routes\n---\n\n# Routes\n\n<!--\n⚠️ ⚠️ IMPORTANT ⚠️ ⚠️ \n\nThank you for helping improve our documentation!\n\nThis file is auto-generated from the JSDoc comments in the source\ncode, so please edit the JSDoc comments in the file below and this\nfile will be re-generated once those changes are merged.\n\nhttps://github.com/remix-run/react-router/blob/main/packages/react-router/lib/components.tsx\n-->\n\n[MODES: framework, data, declarative]\n\n## Summary\n\n[Reference Documentation ↗](https://api.reactrouter.com/v7/functions/react-router.Routes.html)\n\nRenders a branch of [`<Route>`s](../components/Route) that best matches the current\nlocation. Note that these routes do not participate in [data loading](../../start/framework/route-module#loader),\n[`action`](../../start/framework/route-module#action), code splitting, or\nany other [route module](../../start/framework/route-module) features.\n\n```tsx\nimport { Route, Routes } from \"react-router\";\n\n<Routes>\n  <Route index element={<StepOne />} />\n  <Route path=\"step-2\" element={<StepTwo />} />\n  <Route path=\"step-3\" element={<StepThree />} />\n</Routes>\n```\n\n## Signature\n\n```tsx\nfunction Routes({\n  children,\n  location,\n}: RoutesProps): React.ReactElement | null\n```\n\n## Props\n\n### children\n\nNested [`Route`](../components/Route) elements\n\n### location\n\nThe [`Location`](https://api.reactrouter.com/v7/interfaces/react-router.Location.html) to match against. Defaults to the current location.\n\n"
  },
  {
    "path": "docs/api/components/Scripts.md",
    "content": "---\ntitle: Scripts\n---\n\n# Scripts\n\n<!--\n⚠️ ⚠️ IMPORTANT ⚠️ ⚠️ \n\nThank you for helping improve our documentation!\n\nThis file is auto-generated from the JSDoc comments in the source\ncode, so please edit the JSDoc comments in the file below and this\nfile will be re-generated once those changes are merged.\n\nhttps://github.com/remix-run/react-router/blob/main/packages/react-router/lib/dom/ssr/components.tsx\n-->\n\n[MODES: framework]\n\n## Summary\n\n[Reference Documentation ↗](https://api.reactrouter.com/v7/functions/react-router.Scripts.html)\n\nRenders the client runtime of your app. It should be rendered inside the\n[`<body>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/body)\n of the document.\n\nIf server rendering, you can omit `<Scripts/>` and the app will work as a\ntraditional web app without JavaScript, relying solely on HTML and browser\nbehaviors.\n\n```tsx\nimport { Scripts } from \"react-router\";\n\nexport default function Root() {\n  return (\n    <html>\n      <head />\n      <body>\n        <Scripts />\n      </body>\n    </html>\n  );\n}\n```\n\n## Signature\n\n```tsx\nfunction Scripts(scriptProps: ScriptsProps): React.JSX.Element | null\n```\n\n## Props\n\n### scriptProps\n\nAdditional props to spread onto the [`<script>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script) tags, such as [`crossOrigin`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLScriptElement/crossOrigin),\n[`nonce`](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Global_attributes/nonce),\netc.\n\n"
  },
  {
    "path": "docs/api/components/ScrollRestoration.md",
    "content": "---\ntitle: ScrollRestoration\n---\n\n# ScrollRestoration\n\n<!--\n⚠️ ⚠️ IMPORTANT ⚠️ ⚠️ \n\nThank you for helping improve our documentation!\n\nThis file is auto-generated from the JSDoc comments in the source\ncode, so please edit the JSDoc comments in the file below and this\nfile will be re-generated once those changes are merged.\n\nhttps://github.com/remix-run/react-router/blob/main/packages/react-router/lib/dom/lib.tsx\n-->\n\n[MODES: framework, data]\n\n## Summary\n\n[Reference Documentation ↗](https://api.reactrouter.com/v7/functions/react-router.ScrollRestoration.html)\n\nEmulates the browser's scroll restoration on location changes. Apps should only render one of these, right before the [`Scripts`](../components/Scripts) component.\n\n```tsx\nimport { ScrollRestoration } from \"react-router\";\n\nexport default function Root() {\n  return (\n    <html>\n      <body>\n        <ScrollRestoration />\n        <Scripts />\n      </body>\n    </html>\n  );\n}\n```\n\nThis component renders an inline `<script>` to prevent scroll flashing. The `nonce` prop will be passed down to the script tag to allow CSP nonce usage.\n\n```tsx\n<ScrollRestoration nonce={cspNonce} />\n```\n\n## Signature\n\n```tsx\nfunction ScrollRestoration({\n  getKey,\n  storageKey,\n  ...props\n}: ScrollRestorationProps)\n```\n\n## Props\n\n### getKey\n\nA function that returns a key to use for scroll restoration. This is useful\nfor custom scroll restoration logic, such as using only the pathname so\nthat later navigations to prior paths will restore the scroll. Defaults to\n`location.key`. See [`GetScrollRestorationKeyFunction`](https://api.reactrouter.com/v7/interfaces/react-router.GetScrollRestorationKeyFunction.html).\n\n```tsx\n<ScrollRestoration\n  getKey={(location, matches) => {\n    // Restore based on a unique location key (default behavior)\n    return location.key\n\n    // Restore based on pathname\n    return location.pathname\n  }}\n/>\n```\n\n### nonce\n\nA [`nonce`](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Global_attributes/nonce)\nattribute to render on the [`<script>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script)\nelement\n\n### storageKey\n\nThe key to use for storing scroll positions in [`sessionStorage`](https://developer.mozilla.org/en-US/docs/Web/API/Window/sessionStorage).\nDefaults to `\"react-router-scroll-positions\"`.\n\n"
  },
  {
    "path": "docs/api/components/index.md",
    "content": "---\ntitle: Components\norder: 1\n---\n"
  },
  {
    "path": "docs/api/data-routers/RouterProvider.md",
    "content": "---\ntitle: RouterProvider\n---\n\n# RouterProvider\n\n<!--\n⚠️ ⚠️ IMPORTANT ⚠️ ⚠️ \n\nThank you for helping improve our documentation!\n\nThis file is auto-generated from the JSDoc comments in the source\ncode, so please edit the JSDoc comments in the file below and this\nfile will be re-generated once those changes are merged.\n\nhttps://github.com/remix-run/react-router/blob/main/packages/react-router/lib/components.tsx\n-->\n\n[MODES: data]\n\n## Summary\n\n[Reference Documentation ↗](https://api.reactrouter.com/v7/functions/react-router.RouterProvider.html)\n\nRender the UI for the given [`DataRouter`](https://api.reactrouter.com/v7/interfaces/react-router.DataRouter.html). This component should\ntypically be at the top of an app's element tree.\n\n```tsx\nimport { createBrowserRouter } from \"react-router\";\nimport { RouterProvider } from \"react-router/dom\";\nimport { createRoot } from \"react-dom/client\";\n\nconst router = createBrowserRouter(routes);\ncreateRoot(document.getElementById(\"root\")).render(\n  <RouterProvider router={router} />\n);\n```\n\n<docs-info>Please note that this component is exported both from\n`react-router` and `react-router/dom` with the only difference being that the\nlatter automatically wires up `react-dom`'s [`flushSync`](https://react.dev/reference/react-dom/flushSync)\nimplementation. You _almost always_ want to use the version from\n`react-router/dom` unless you're running in a non-DOM environment.</docs-info>\n\n## Signature\n\n```tsx\nfunction RouterProvider({\n  router,\n  flushSync: reactDomFlushSyncImpl,\n  onError,\n  unstable_useTransitions,\n}: RouterProviderProps): React.ReactElement\n```\n\n## Props\n\n### flushSync\n\nThe [`ReactDOM.flushSync`](https://react.dev/reference/react-dom/flushSync)\nimplementation to use for flushing updates.\n\nYou usually don't have to worry about this:\n- The `RouterProvider` exported from `react-router/dom` handles this internally for you\n- If you are rendering in a non-DOM environment, you can import\n  `RouterProvider` from `react-router` and ignore this prop\n\n### onError\n\nAn error handler function that will be called for any middleware, loader, action,\nor render errors that are encountered in your application.  This is useful for\nlogging or reporting errors instead of in the `ErrorBoundary` because it's not\nsubject to re-rendering and will only run one time per error.\n\nThe `errorInfo` parameter is passed along from\n[`componentDidCatch`](https://react.dev/reference/react/Component#componentdidcatch)\nand is only present for render errors.\n\n```tsx\n<RouterProvider onError=(error, info) => {\n  let { location, params, unstable_pattern, errorInfo } = info;\n  console.error(error, location, errorInfo);\n  reportToErrorService(error, location, errorInfo);\n}} />\n```\n\n### router\n\nThe [`DataRouter`](https://api.reactrouter.com/v7/interfaces/react-router.DataRouter.html) instance to use for navigation and data fetching.\n\n### unstable_useTransitions\n\nControl whether router state updates are internally wrapped in\n[`React.startTransition`](https://react.dev/reference/react/startTransition).\n\n- When left `undefined`, all state updates are wrapped in\n  `React.startTransition`\n  - This can lead to buggy behaviors if you are wrapping your own\n    navigations/fetchers in `startTransition`.\n- When set to `true`, [`Link`](../components/Link) and [`Form`](../components/Form) navigations will be wrapped\n  in `React.startTransition` and router state changes will be wrapped in\n  `React.startTransition` and also sent through\n  [`useOptimistic`](https://react.dev/reference/react/useOptimistic) to\n  surface mid-navigation router state changes to the UI.\n- When set to `false`, the router will not leverage `React.startTransition` or\n  `React.useOptimistic` on any navigations or state changes.\n\nFor more information, please see the [docs](https://reactrouter.com/explanation/react-transitions).\n\n"
  },
  {
    "path": "docs/api/data-routers/StaticRouterProvider.md",
    "content": "---\ntitle: StaticRouterProvider\n---\n\n# StaticRouterProvider\n\n<!--\n⚠️ ⚠️ IMPORTANT ⚠️ ⚠️ \n\nThank you for helping improve our documentation!\n\nThis file is auto-generated from the JSDoc comments in the source\ncode, so please edit the JSDoc comments in the file below and this\nfile will be re-generated once those changes are merged.\n\nhttps://github.com/remix-run/react-router/blob/main/packages/react-router/lib/dom/server.tsx\n-->\n\n[MODES: data]\n\n## Summary\n\n[Reference Documentation ↗](https://api.reactrouter.com/v7/functions/react-router.StaticRouterProvider.html)\n\nA [`DataRouter`](https://api.reactrouter.com/v7/interfaces/react-router.DataRouter.html) that may not navigate to any other [`Location`](https://api.reactrouter.com/v7/interfaces/react-router.Location.html).\nThis is useful on the server where there is no stateful UI.\n\n```tsx\nexport async function handleRequest(request: Request) {\n  let { query, dataRoutes } = createStaticHandler(routes);\n  let context = await query(request));\n\n  if (context instanceof Response) {\n    return context;\n  }\n\n  let router = createStaticRouter(dataRoutes, context);\n  return new Response(\n    ReactDOMServer.renderToString(<StaticRouterProvider ... />),\n    { headers: { \"Content-Type\": \"text/html\" } }\n  );\n}\n```\n\n## Signature\n\n```tsx\nfunction StaticRouterProvider({\n  context,\n  router,\n  hydrate = true,\n  nonce,\n}: StaticRouterProviderProps)\n```\n\n## Props\n\n### context\n\nThe [`StaticHandlerContext`](https://api.reactrouter.com/v7/interfaces/react-router.StaticHandlerContext.html) returned from [`StaticHandler`](https://api.reactrouter.com/v7/interfaces/react-router.StaticHandler.html)'s\n`query`\n\n### hydrate\n\nWhether to hydrate the router on the client (default `true`)\n\n### nonce\n\nThe [`nonce`](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Global_attributes/nonce)\nto use for the hydration [`<script>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script)\ntag\n\n### router\n\nThe static [`DataRouter`](https://api.reactrouter.com/v7/interfaces/react-router.DataRouter.html) from [`createStaticRouter`](../data-routers/createStaticRouter)\n\n"
  },
  {
    "path": "docs/api/data-routers/createBrowserRouter.md",
    "content": "---\ntitle: createBrowserRouter\n---\n\n# createBrowserRouter\n\n<!--\n⚠️ ⚠️ IMPORTANT ⚠️ ⚠️ \n\nThank you for helping improve our documentation!\n\nThis file is auto-generated from the JSDoc comments in the source\ncode, so please edit the JSDoc comments in the file below and this\nfile will be re-generated once those changes are merged.\n\nhttps://github.com/remix-run/react-router/blob/main/packages/react-router/lib/dom/lib.tsx\n-->\n\n[MODES: data]\n\n## Summary\n\n[Reference Documentation ↗](https://api.reactrouter.com/v7/functions/react-router.createBrowserRouter.html)\n\nCreate a new [data router](https://api.reactrouter.com/v7/interfaces/react-router.DataRouter.html) that manages the application\npath via [`history.pushState`](https://developer.mozilla.org/en-US/docs/Web/API/History/pushState)\nand [`history.replaceState`](https://developer.mozilla.org/en-US/docs/Web/API/History/replaceState).\n\n## Signature\n\n```tsx\nfunction createBrowserRouter(\n  routes: RouteObject[],\n  opts?: DOMRouterOpts,\n): DataRouter\n```\n\n## Params\n\n### routes\n\nApplication routes\n\n### opts.basename\n\nBasename path for the application.\n\n### opts.dataStrategy\n\nOverride the default data strategy of running loaders in parallel -\nsee the [docs](../../how-to/data-strategy) for more information.\n\n```tsx\nlet router = createBrowserRouter(routes, {\n  async dataStrategy({\n    matches,\n    request,\n    runClientMiddleware,\n  }) {\n    const matchesToLoad = matches.filter((m) =>\n      m.shouldCallHandler(),\n    );\n\n    const results: Record<string, DataStrategyResult> = {};\n    await runClientMiddleware(() =>\n      Promise.all(\n        matchesToLoad.map(async (match) => {\n          results[match.route.id] = await match.resolve();\n        }),\n      ),\n    );\n    return results;\n  },\n});\n```\n\n### opts.future\n\nFuture flags to enable for the router.\n\n### opts.getContext\n\nA function that returns an [`RouterContextProvider`](../utils/RouterContextProvider) instance\nwhich is provided as the `context` argument to client [`action`](../../start/data/route-object#action)s,\n[`loader`](../../start/data/route-object#loader)s and [middleware](../../how-to/middleware).\nThis function is called to generate a fresh `context` instance on each\nnavigation or fetcher call.\n\n```tsx\nimport {\n  createContext,\n  RouterContextProvider,\n} from \"react-router\";\n\nconst apiClientContext = createContext<APIClient>();\n\nfunction createBrowserRouter(routes, {\n  getContext() {\n    let context = new RouterContextProvider();\n    context.set(apiClientContext, getApiClient());\n    return context;\n  }\n})\n```\n\n### opts.hydrationData\n\nWhen Server-Rendering and opting-out of automatic hydration, the\n`hydrationData` option allows you to pass in hydration data from your\nserver-render. This will almost always be a subset of data from the\n[`StaticHandlerContext`](https://api.reactrouter.com/v7/interfaces/react-router.StaticHandlerContext.html) value you get back from the [`StaticHandler`](https://api.reactrouter.com/v7/interfaces/react-router.StaticHandler.html)'s\n`query` method:\n\n```tsx\nconst router = createBrowserRouter(routes, {\n  hydrationData: {\n    loaderData: {\n      // [routeId]: serverLoaderData\n    },\n    // may also include `errors` and/or `actionData`\n  },\n});\n```\n\n**Partial Hydration Data**\n\nYou will almost always include a complete set of `loaderData` to hydrate a\nserver-rendered app. But in advanced use-cases (such as Framework Mode's\n[`clientLoader`](../../start/framework/route-module#clientLoader)), you may\nwant to include `loaderData` for only some routes that were loaded/rendered\non the server. This allows you to hydrate _some_ of the routes (such as the\napp layout/shell) while showing a `HydrateFallback` component and running\nthe [`loader`](../../start/data/route-object#loader)s for other routes\nduring hydration.\n\nA route [`loader`](../../start/data/route-object#loader) will run during\nhydration in two scenarios:\n\n 1. No hydration data is provided\n    In these cases the `HydrateFallback` component will render on initial\n    hydration\n 2. The `loader.hydrate` property is set to `true`\n    This allows you to run the [`loader`](../../start/data/route-object#loader)\n    even if you did not render a fallback on initial hydration (i.e., to\n    prime a cache with hydration data)\n\n```tsx\nconst router = createBrowserRouter(\n  [\n    {\n      id: \"root\",\n      loader: rootLoader,\n      Component: Root,\n      children: [\n        {\n          id: \"index\",\n          loader: indexLoader,\n          HydrateFallback: IndexSkeleton,\n          Component: Index,\n        },\n      ],\n    },\n  ],\n  {\n    hydrationData: {\n      loaderData: {\n        root: \"ROOT DATA\",\n        // No index data provided\n      },\n    },\n  }\n);\n```\n\n### opts.unstable_instrumentations\n\nArray of instrumentation objects allowing you to instrument the router and\nindividual routes prior to router initialization (and on any subsequently\nadded routes via `route.lazy` or `patchRoutesOnNavigation`).  This is\nmostly useful for observability such as wrapping navigations, fetches,\nas well as route loaders/actions/middlewares with logging and/or performance\ntracing.  See the [docs](../../how-to/instrumentation) for more information.\n\n```tsx\nlet router = createBrowserRouter(routes, {\n  unstable_instrumentations: [logging]\n});\n\n\nlet logging = {\n  router({ instrument }) {\n    instrument({\n      navigate: (impl, info) => logExecution(`navigate ${info.to}`, impl),\n      fetch: (impl, info) => logExecution(`fetch ${info.to}`, impl)\n    });\n  },\n  route({ instrument, id }) {\n    instrument({\n      middleware: (impl, info) => logExecution(\n        `middleware ${info.request.url} (route ${id})`,\n        impl\n      ),\n      loader: (impl, info) => logExecution(\n        `loader ${info.request.url} (route ${id})`,\n        impl\n      ),\n      action: (impl, info) => logExecution(\n        `action ${info.request.url} (route ${id})`,\n        impl\n      ),\n    })\n  }\n};\n\nasync function logExecution(label: string, impl: () => Promise<void>) {\n  let start = performance.now();\n  console.log(`start ${label}`);\n  await impl();\n  let duration = Math.round(performance.now() - start);\n  console.log(`end ${label} (${duration}ms)`);\n}\n```\n\n### opts.patchRoutesOnNavigation\n\nLazily define portions of the route tree on navigations.\nSee [`PatchRoutesOnNavigationFunction`](https://api.reactrouter.com/v7/types/react-router.PatchRoutesOnNavigationFunction.html).\n\nBy default, React Router wants you to provide a full route tree up front via\n`createBrowserRouter(routes)`. This allows React Router to perform synchronous\nroute matching, execute loaders, and then render route components in the most\noptimistic manner without introducing waterfalls. The tradeoff is that your\ninitial JS bundle is larger by definition — which may slow down application\nstart-up times as your application grows.\n\nTo combat this, we introduced [`route.lazy`](../../start/data/route-object#lazy)\nin [v6.9.0](https://github.com/remix-run/react-router/blob/main/CHANGELOG.md#v690)\nwhich lets you lazily load the route _implementation_ ([`loader`](../../start/data/route-object#loader),\n[`Component`](../../start/data/route-object#Component), etc.) while still\nproviding the route _definition_ aspects up front (`path`, `index`, etc.).\nThis is a good middle ground. React Router still knows about your route\ndefinitions (the lightweight part) up front and can perform synchronous\nroute matching, but then delay loading any of the route implementation\naspects (the heavier part) until the route is actually navigated to.\n\nIn some cases, even this doesn't go far enough. For huge applications,\nproviding all route definitions up front can be prohibitively expensive.\nAdditionally, it might not even be possible to provide all route definitions\nup front in certain Micro-Frontend or Module-Federation architectures.\n\nThis is where `patchRoutesOnNavigation` comes in ([RFC](https://github.com/remix-run/react-router/discussions/11113)).\nThis API is for advanced use-cases where you are unable to provide the full\nroute tree up-front and need a way to lazily \"discover\" portions of the route\ntree at runtime. This feature is often referred to as [\"Fog of War\"](https://en.wikipedia.org/wiki/Fog_of_war),\nbecause similar to how video games expand the \"world\" as you move around -\nthe router would be expanding its routing tree as the user navigated around\nthe app - but would only ever end up loading portions of the tree that the\nuser visited.\n\n`patchRoutesOnNavigation` will be called anytime React Router is unable to\nmatch a `path`. The arguments include the `path`, any partial `matches`,\nand a `patch` function you can call to patch new routes into the tree at a\nspecific location. This method is executed during the `loading` portion of\nthe navigation for `GET` requests and during the `submitting` portion of\nthe navigation for non-`GET` requests.\n\n<details>\n  <summary><b>Example <code>patchRoutesOnNavigation</code> Use Cases</b></summary>\n\n  **Patching children into an existing route**\n\n  ```tsx\n  const router = createBrowserRouter(\n    [\n      {\n        id: \"root\",\n        path: \"/\",\n        Component: RootComponent,\n      },\n    ],\n    {\n      async patchRoutesOnNavigation({ patch, path }) {\n        if (path === \"/a\") {\n          // Load/patch the `a` route as a child of the route with id `root`\n          let route = await getARoute();\n          //  ^ { path: 'a', Component: A }\n          patch(\"root\", [route]);\n        }\n      },\n    }\n  );\n  ```\n\n  In the above example, if the user clicks a link to `/a`, React Router\n  won't match any routes initially and will call `patchRoutesOnNavigation`\n  with a `path = \"/a\"` and a `matches` array containing the root route\n  match. By calling `patch('root', [route])`, the new route will be added\n  to the route tree as a child of the `root` route and React Router will\n  perform matching on the updated routes. This time it will successfully\n  match the `/a` path and the navigation will complete successfully.\n\n  **Patching new root-level routes**\n\n  If you need to patch a new route to the top of the tree (i.e., it doesn't\n  have a parent), you can pass `null` as the `routeId`:\n\n  ```tsx\n  const router = createBrowserRouter(\n    [\n      {\n        id: \"root\",\n        path: \"/\",\n        Component: RootComponent,\n      },\n    ],\n    {\n      async patchRoutesOnNavigation({ patch, path }) {\n        if (path === \"/root-sibling\") {\n          // Load/patch the `/root-sibling` route as a sibling of the root route\n          let route = await getRootSiblingRoute();\n          //  ^ { path: '/root-sibling', Component: RootSibling }\n          patch(null, [route]);\n        }\n      },\n    }\n  );\n  ```\n\n  **Patching subtrees asynchronously**\n\n  You can also perform asynchronous matching to lazily fetch entire sections\n  of your application:\n\n  ```tsx\n  let router = createBrowserRouter(\n    [\n      {\n        path: \"/\",\n        Component: Home,\n      },\n    ],\n    {\n      async patchRoutesOnNavigation({ patch, path }) {\n        if (path.startsWith(\"/dashboard\")) {\n          let children = await import(\"./dashboard\");\n          patch(null, children);\n        }\n        if (path.startsWith(\"/account\")) {\n          let children = await import(\"./account\");\n          patch(null, children);\n        }\n      },\n    }\n  );\n  ```\n\n  <docs-info>If in-progress execution of `patchRoutesOnNavigation` is\n  interrupted by a later navigation, then any remaining `patch` calls in\n  the interrupted execution will not update the route tree because the\n  operation was cancelled.</docs-info>\n\n  **Co-locating route discovery with route definition**\n\n  If you don't wish to perform your own pseudo-matching, you can leverage\n  the partial `matches` array and the [`handle`](../../start/data/route-object#handle)\n  field on a route to keep the children definitions co-located:\n\n  ```tsx\n  let router = createBrowserRouter(\n    [\n      {\n        path: \"/\",\n        Component: Home,\n      },\n      {\n        path: \"/dashboard\",\n        children: [\n          {\n            // If we want to include /dashboard in the critical routes, we need to\n            // also include it's index route since patchRoutesOnNavigation will not be\n            // called on a navigation to `/dashboard` because it will have successfully\n            // matched the `/dashboard` parent route\n            index: true,\n            // ...\n          },\n        ],\n        handle: {\n          lazyChildren: () => import(\"./dashboard\"),\n        },\n      },\n      {\n        path: \"/account\",\n        children: [\n          {\n            index: true,\n            // ...\n          },\n        ],\n        handle: {\n          lazyChildren: () => import(\"./account\"),\n        },\n      },\n    ],\n    {\n      async patchRoutesOnNavigation({ matches, patch }) {\n        let leafRoute = matches[matches.length - 1]?.route;\n        if (leafRoute?.handle?.lazyChildren) {\n          let children =\n            await leafRoute.handle.lazyChildren();\n          patch(leafRoute.id, children);\n        }\n      },\n    }\n  );\n  ```\n\n  **A note on routes with parameters**\n\n  Because React Router uses ranked routes to find the best match for a\n  given path, there is an interesting ambiguity introduced when only a\n  partial route tree is known at any given point in time. If we match a\n  fully static route such as `path: \"/about/contact-us\"` then we know we've\n  found the right match since it's composed entirely of static URL segments.\n  Thus, we do not need to bother asking for any other potentially\n  higher-scoring routes.\n\n  However, routes with parameters (dynamic or splat) can't make this\n  assumption because there might be a not-yet-discovered route that scores\n  higher. Consider a full route tree such as:\n\n  ```tsx\n  // Assume this is the full route tree for your app\n  const routes = [\n    {\n      path: \"/\",\n      Component: Home,\n    },\n    {\n      id: \"blog\",\n      path: \"/blog\",\n      Component: BlogLayout,\n      children: [\n        { path: \"new\", Component: NewPost },\n        { path: \":slug\", Component: BlogPost },\n      ],\n    },\n  ];\n  ```\n\n  And then assume we want to use `patchRoutesOnNavigation` to fill this in\n  as the user navigates around:\n\n  ```tsx\n  // Start with only the index route\n  const router = createBrowserRouter(\n    [\n      {\n        path: \"/\",\n        Component: Home,\n      },\n    ],\n    {\n      async patchRoutesOnNavigation({ patch, path }) {\n        if (path === \"/blog/new\") {\n          patch(\"blog\", [\n            {\n              path: \"new\",\n              Component: NewPost,\n            },\n          ]);\n        } else if (path.startsWith(\"/blog\")) {\n          patch(\"blog\", [\n            {\n              path: \":slug\",\n              Component: BlogPost,\n            },\n          ]);\n        }\n      },\n    }\n  );\n  ```\n\n  If the user were to a blog post first (i.e., `/blog/my-post`) we would\n  patch in the `:slug` route. Then, if the user navigated to `/blog/new` to\n  write a new post, we'd match `/blog/:slug` but it wouldn't be the _right_\n  match! We need to call `patchRoutesOnNavigation` just in case there\n  exists a higher-scoring route we've not yet discovered, which in this\n  case there is.\n\n  So, anytime React Router matches a path that contains at least one param,\n  it will call `patchRoutesOnNavigation` and match routes again just to\n  confirm it has found the best match.\n\n  If your `patchRoutesOnNavigation` implementation is expensive or making\n  side effect [`fetch`](https://developer.mozilla.org/en-US/docs/Web/API/fetch)\n  calls to a backend server, you may want to consider tracking previously\n  seen routes to avoid over-fetching in cases where you know the proper\n  route has already been found. This can usually be as simple as\n  maintaining a small cache of prior `path` values for which you've already\n  patched in the right routes:\n\n  ```tsx\n  let discoveredRoutes = new Set();\n\n  const router = createBrowserRouter(routes, {\n    async patchRoutesOnNavigation({ patch, path }) {\n      if (discoveredRoutes.has(path)) {\n        // We've seen this before so nothing to patch in and we can let the router\n        // use the routes it already knows about\n        return;\n      }\n\n      discoveredRoutes.add(path);\n\n      // ... patch routes in accordingly\n    },\n  });\n  ```\n</details>\n\n### opts.window\n\n[`Window`](https://developer.mozilla.org/en-US/docs/Web/API/Window) object\noverride. Defaults to the global `window` instance.\n\n## Returns\n\nAn initialized [data router](https://api.reactrouter.com/v7/interfaces/react-router.DataRouter.html) to pass to [`<RouterProvider>`](../data-routers/RouterProvider)\n\n"
  },
  {
    "path": "docs/api/data-routers/createHashRouter.md",
    "content": "---\ntitle: createHashRouter\n---\n\n# createHashRouter\n\n<!--\n⚠️ ⚠️ IMPORTANT ⚠️ ⚠️ \n\nThank you for helping improve our documentation!\n\nThis file is auto-generated from the JSDoc comments in the source\ncode, so please edit the JSDoc comments in the file below and this\nfile will be re-generated once those changes are merged.\n\nhttps://github.com/remix-run/react-router/blob/main/packages/react-router/lib/dom/lib.tsx\n-->\n\n[MODES: data]\n\n## Summary\n\n[Reference Documentation ↗](https://api.reactrouter.com/v7/functions/react-router.createHashRouter.html)\n\nCreate a new [data router](https://api.reactrouter.com/v7/interfaces/react-router.DataRouter.html) that manages the application\npath via the URL [`hash`](https://developer.mozilla.org/en-US/docs/Web/API/URL/hash).\n\n## Signature\n\n```tsx\nfunction createHashRouter(\n  routes: RouteObject[],\n  opts?: DOMRouterOpts,\n): DataRouter\n```\n\n## Params\n\n### routes\n\nApplication routes\n\n### opts.basename\n\nBasename path for the application.\n\n### opts.future\n\nFuture flags to enable for the router.\n\n### opts.getContext\n\nA function that returns an [`RouterContextProvider`](../utils/RouterContextProvider) instance\nwhich is provided as the `context` argument to client [`action`](../../start/data/route-object#action)s,\n[`loader`](../../start/data/route-object#loader)s and [middleware](../../how-to/middleware).\nThis function is called to generate a fresh `context` instance on each\nnavigation or fetcher call.\n\n```tsx\nimport {\n  createContext,\n  RouterContextProvider,\n} from \"react-router\";\n\nconst apiClientContext = createContext<APIClient>();\n\nfunction createBrowserRouter(routes, {\n  getContext() {\n    let context = new RouterContextProvider();\n    context.set(apiClientContext, getApiClient());\n    return context;\n  }\n})\n```\n\n### opts.hydrationData\n\nWhen Server-Rendering and opting-out of automatic hydration, the\n`hydrationData` option allows you to pass in hydration data from your\nserver-render. This will almost always be a subset of data from the\n[`StaticHandlerContext`](https://api.reactrouter.com/v7/interfaces/react-router.StaticHandlerContext.html) value you get back from the [`StaticHandler`](https://api.reactrouter.com/v7/interfaces/react-router.StaticHandler.html)'s\n`query` method:\n\n```tsx\nconst router = createBrowserRouter(routes, {\n  hydrationData: {\n    loaderData: {\n      // [routeId]: serverLoaderData\n    },\n    // may also include `errors` and/or `actionData`\n  },\n});\n```\n\n**Partial Hydration Data**\n\nYou will almost always include a complete set of `loaderData` to hydrate a\nserver-rendered app. But in advanced use-cases (such as Framework Mode's\n[`clientLoader`](../../start/framework/route-module#clientLoader)), you may\nwant to include `loaderData` for only some routes that were loaded/rendered\non the server. This allows you to hydrate _some_ of the routes (such as the\napp layout/shell) while showing a `HydrateFallback` component and running\nthe [`loader`](../../start/data/route-object#loader)s for other routes\nduring hydration.\n\nA route [`loader`](../../start/data/route-object#loader) will run during\nhydration in two scenarios:\n\n 1. No hydration data is provided\n    In these cases the `HydrateFallback` component will render on initial\n    hydration\n 2. The `loader.hydrate` property is set to `true`\n    This allows you to run the [`loader`](../../start/data/route-object#loader)\n    even if you did not render a fallback on initial hydration (i.e., to\n    prime a cache with hydration data)\n\n```tsx\nconst router = createBrowserRouter(\n  [\n    {\n      id: \"root\",\n      loader: rootLoader,\n      Component: Root,\n      children: [\n        {\n          id: \"index\",\n          loader: indexLoader,\n          HydrateFallback: IndexSkeleton,\n          Component: Index,\n        },\n      ],\n    },\n  ],\n  {\n    hydrationData: {\n      loaderData: {\n        root: \"ROOT DATA\",\n        // No index data provided\n      },\n    },\n  }\n);\n```\n\n### opts.unstable_instrumentations\n\nArray of instrumentation objects allowing you to instrument the router and\nindividual routes prior to router initialization (and on any subsequently\nadded routes via `route.lazy` or `patchRoutesOnNavigation`).  This is\nmostly useful for observability such as wrapping navigations, fetches,\nas well as route loaders/actions/middlewares with logging and/or performance\ntracing.  See the [docs](../../how-to/instrumentation) for more information.\n\n```tsx\nlet router = createBrowserRouter(routes, {\n  unstable_instrumentations: [logging]\n});\n\n\nlet logging = {\n  router({ instrument }) {\n    instrument({\n      navigate: (impl, info) => logExecution(`navigate ${info.to}`, impl),\n      fetch: (impl, info) => logExecution(`fetch ${info.to}`, impl)\n    });\n  },\n  route({ instrument, id }) {\n    instrument({\n      middleware: (impl, info) => logExecution(\n        `middleware ${info.request.url} (route ${id})`,\n        impl\n      ),\n      loader: (impl, info) => logExecution(\n        `loader ${info.request.url} (route ${id})`,\n        impl\n      ),\n      action: (impl, info) => logExecution(\n        `action ${info.request.url} (route ${id})`,\n        impl\n      ),\n    })\n  }\n};\n\nasync function logExecution(label: string, impl: () => Promise<void>) {\n  let start = performance.now();\n  console.log(`start ${label}`);\n  await impl();\n  let duration = Math.round(performance.now() - start);\n  console.log(`end ${label} (${duration}ms)`);\n}\n```\n\n### opts.dataStrategy\n\nOverride the default data strategy of running loaders in parallel -\nsee the [docs](../../how-to/data-strategy) for more information.\n\n```tsx\nlet router = createBrowserRouter(routes, {\n  async dataStrategy({\n    matches,\n    request,\n    runClientMiddleware,\n  }) {\n    const matchesToLoad = matches.filter((m) =>\n      m.shouldCallHandler(),\n    );\n\n    const results: Record<string, DataStrategyResult> = {};\n    await runClientMiddleware(() =>\n      Promise.all(\n        matchesToLoad.map(async (match) => {\n          results[match.route.id] = await match.resolve();\n        }),\n      ),\n    );\n    return results;\n  },\n});\n```\n\n### opts.patchRoutesOnNavigation\n\nLazily define portions of the route tree on navigations.\nSee [`PatchRoutesOnNavigationFunction`](https://api.reactrouter.com/v7/types/react-router.PatchRoutesOnNavigationFunction.html).\n\nBy default, React Router wants you to provide a full route tree up front via\n`createBrowserRouter(routes)`. This allows React Router to perform synchronous\nroute matching, execute loaders, and then render route components in the most\noptimistic manner without introducing waterfalls. The tradeoff is that your\ninitial JS bundle is larger by definition — which may slow down application\nstart-up times as your application grows.\n\nTo combat this, we introduced [`route.lazy`](../../start/data/route-object#lazy)\nin [v6.9.0](https://github.com/remix-run/react-router/blob/main/CHANGELOG.md#v690)\nwhich lets you lazily load the route _implementation_ ([`loader`](../../start/data/route-object#loader),\n[`Component`](../../start/data/route-object#Component), etc.) while still\nproviding the route _definition_ aspects up front (`path`, `index`, etc.).\nThis is a good middle ground. React Router still knows about your route\ndefinitions (the lightweight part) up front and can perform synchronous\nroute matching, but then delay loading any of the route implementation\naspects (the heavier part) until the route is actually navigated to.\n\nIn some cases, even this doesn't go far enough. For huge applications,\nproviding all route definitions up front can be prohibitively expensive.\nAdditionally, it might not even be possible to provide all route definitions\nup front in certain Micro-Frontend or Module-Federation architectures.\n\nThis is where `patchRoutesOnNavigation` comes in ([RFC](https://github.com/remix-run/react-router/discussions/11113)).\nThis API is for advanced use-cases where you are unable to provide the full\nroute tree up-front and need a way to lazily \"discover\" portions of the route\ntree at runtime. This feature is often referred to as [\"Fog of War\"](https://en.wikipedia.org/wiki/Fog_of_war),\nbecause similar to how video games expand the \"world\" as you move around -\nthe router would be expanding its routing tree as the user navigated around\nthe app - but would only ever end up loading portions of the tree that the\nuser visited.\n\n`patchRoutesOnNavigation` will be called anytime React Router is unable to\nmatch a `path`. The arguments include the `path`, any partial `matches`,\nand a `patch` function you can call to patch new routes into the tree at a\nspecific location. This method is executed during the `loading` portion of\nthe navigation for `GET` requests and during the `submitting` portion of\nthe navigation for non-`GET` requests.\n\n<details>\n  <summary><b>Example <code>patchRoutesOnNavigation</code> Use Cases</b></summary>\n\n  **Patching children into an existing route**\n\n  ```tsx\n  const router = createBrowserRouter(\n    [\n      {\n        id: \"root\",\n        path: \"/\",\n        Component: RootComponent,\n      },\n    ],\n    {\n      async patchRoutesOnNavigation({ patch, path }) {\n        if (path === \"/a\") {\n          // Load/patch the `a` route as a child of the route with id `root`\n          let route = await getARoute();\n          //  ^ { path: 'a', Component: A }\n          patch(\"root\", [route]);\n        }\n      },\n    }\n  );\n  ```\n\n  In the above example, if the user clicks a link to `/a`, React Router\n  won't match any routes initially and will call `patchRoutesOnNavigation`\n  with a `path = \"/a\"` and a `matches` array containing the root route\n  match. By calling `patch('root', [route])`, the new route will be added\n  to the route tree as a child of the `root` route and React Router will\n  perform matching on the updated routes. This time it will successfully\n  match the `/a` path and the navigation will complete successfully.\n\n  **Patching new root-level routes**\n\n  If you need to patch a new route to the top of the tree (i.e., it doesn't\n  have a parent), you can pass `null` as the `routeId`:\n\n  ```tsx\n  const router = createBrowserRouter(\n    [\n      {\n        id: \"root\",\n        path: \"/\",\n        Component: RootComponent,\n      },\n    ],\n    {\n      async patchRoutesOnNavigation({ patch, path }) {\n        if (path === \"/root-sibling\") {\n          // Load/patch the `/root-sibling` route as a sibling of the root route\n          let route = await getRootSiblingRoute();\n          //  ^ { path: '/root-sibling', Component: RootSibling }\n          patch(null, [route]);\n        }\n      },\n    }\n  );\n  ```\n\n  **Patching subtrees asynchronously**\n\n  You can also perform asynchronous matching to lazily fetch entire sections\n  of your application:\n\n  ```tsx\n  let router = createBrowserRouter(\n    [\n      {\n        path: \"/\",\n        Component: Home,\n      },\n    ],\n    {\n      async patchRoutesOnNavigation({ patch, path }) {\n        if (path.startsWith(\"/dashboard\")) {\n          let children = await import(\"./dashboard\");\n          patch(null, children);\n        }\n        if (path.startsWith(\"/account\")) {\n          let children = await import(\"./account\");\n          patch(null, children);\n        }\n      },\n    }\n  );\n  ```\n\n  <docs-info>If in-progress execution of `patchRoutesOnNavigation` is\n  interrupted by a later navigation, then any remaining `patch` calls in\n  the interrupted execution will not update the route tree because the\n  operation was cancelled.</docs-info>\n\n  **Co-locating route discovery with route definition**\n\n  If you don't wish to perform your own pseudo-matching, you can leverage\n  the partial `matches` array and the [`handle`](../../start/data/route-object#handle)\n  field on a route to keep the children definitions co-located:\n\n  ```tsx\n  let router = createBrowserRouter(\n    [\n      {\n        path: \"/\",\n        Component: Home,\n      },\n      {\n        path: \"/dashboard\",\n        children: [\n          {\n            // If we want to include /dashboard in the critical routes, we need to\n            // also include it's index route since patchRoutesOnNavigation will not be\n            // called on a navigation to `/dashboard` because it will have successfully\n            // matched the `/dashboard` parent route\n            index: true,\n            // ...\n          },\n        ],\n        handle: {\n          lazyChildren: () => import(\"./dashboard\"),\n        },\n      },\n      {\n        path: \"/account\",\n        children: [\n          {\n            index: true,\n            // ...\n          },\n        ],\n        handle: {\n          lazyChildren: () => import(\"./account\"),\n        },\n      },\n    ],\n    {\n      async patchRoutesOnNavigation({ matches, patch }) {\n        let leafRoute = matches[matches.length - 1]?.route;\n        if (leafRoute?.handle?.lazyChildren) {\n          let children =\n            await leafRoute.handle.lazyChildren();\n          patch(leafRoute.id, children);\n        }\n      },\n    }\n  );\n  ```\n\n  **A note on routes with parameters**\n\n  Because React Router uses ranked routes to find the best match for a\n  given path, there is an interesting ambiguity introduced when only a\n  partial route tree is known at any given point in time. If we match a\n  fully static route such as `path: \"/about/contact-us\"` then we know we've\n  found the right match since it's composed entirely of static URL segments.\n  Thus, we do not need to bother asking for any other potentially\n  higher-scoring routes.\n\n  However, routes with parameters (dynamic or splat) can't make this\n  assumption because there might be a not-yet-discovered route that scores\n  higher. Consider a full route tree such as:\n\n  ```tsx\n  // Assume this is the full route tree for your app\n  const routes = [\n    {\n      path: \"/\",\n      Component: Home,\n    },\n    {\n      id: \"blog\",\n      path: \"/blog\",\n      Component: BlogLayout,\n      children: [\n        { path: \"new\", Component: NewPost },\n        { path: \":slug\", Component: BlogPost },\n      ],\n    },\n  ];\n  ```\n\n  And then assume we want to use `patchRoutesOnNavigation` to fill this in\n  as the user navigates around:\n\n  ```tsx\n  // Start with only the index route\n  const router = createBrowserRouter(\n    [\n      {\n        path: \"/\",\n        Component: Home,\n      },\n    ],\n    {\n      async patchRoutesOnNavigation({ patch, path }) {\n        if (path === \"/blog/new\") {\n          patch(\"blog\", [\n            {\n              path: \"new\",\n              Component: NewPost,\n            },\n          ]);\n        } else if (path.startsWith(\"/blog\")) {\n          patch(\"blog\", [\n            {\n              path: \":slug\",\n              Component: BlogPost,\n            },\n          ]);\n        }\n      },\n    }\n  );\n  ```\n\n  If the user were to a blog post first (i.e., `/blog/my-post`) we would\n  patch in the `:slug` route. Then, if the user navigated to `/blog/new` to\n  write a new post, we'd match `/blog/:slug` but it wouldn't be the _right_\n  match! We need to call `patchRoutesOnNavigation` just in case there\n  exists a higher-scoring route we've not yet discovered, which in this\n  case there is.\n\n  So, anytime React Router matches a path that contains at least one param,\n  it will call `patchRoutesOnNavigation` and match routes again just to\n  confirm it has found the best match.\n\n  If your `patchRoutesOnNavigation` implementation is expensive or making\n  side effect [`fetch`](https://developer.mozilla.org/en-US/docs/Web/API/fetch)\n  calls to a backend server, you may want to consider tracking previously\n  seen routes to avoid over-fetching in cases where you know the proper\n  route has already been found. This can usually be as simple as\n  maintaining a small cache of prior `path` values for which you've already\n  patched in the right routes:\n\n  ```tsx\n  let discoveredRoutes = new Set();\n\n  const router = createBrowserRouter(routes, {\n    async patchRoutesOnNavigation({ patch, path }) {\n      if (discoveredRoutes.has(path)) {\n        // We've seen this before so nothing to patch in and we can let the router\n        // use the routes it already knows about\n        return;\n      }\n\n      discoveredRoutes.add(path);\n\n      // ... patch routes in accordingly\n    },\n  });\n  ```\n</details>\n\n### opts.window\n\n[`Window`](https://developer.mozilla.org/en-US/docs/Web/API/Window) object\noverride. Defaults to the global `window` instance.\n\n## Returns\n\nAn initialized [data router](https://api.reactrouter.com/v7/interfaces/react-router.DataRouter.html) to pass to [`<RouterProvider>`](../data-routers/RouterProvider)\n\n"
  },
  {
    "path": "docs/api/data-routers/createMemoryRouter.md",
    "content": "---\ntitle: createMemoryRouter\n---\n\n# createMemoryRouter\n\n<!--\n⚠️ ⚠️ IMPORTANT ⚠️ ⚠️ \n\nThank you for helping improve our documentation!\n\nThis file is auto-generated from the JSDoc comments in the source\ncode, so please edit the JSDoc comments in the file below and this\nfile will be re-generated once those changes are merged.\n\nhttps://github.com/remix-run/react-router/blob/main/packages/react-router/lib/components.tsx\n-->\n\n[MODES: data]\n\n## Summary\n\n[Reference Documentation ↗](https://api.reactrouter.com/v7/functions/react-router.createMemoryRouter.html)\n\nCreate a new [`DataRouter`](https://api.reactrouter.com/v7/interfaces/react-router.DataRouter.html) that manages the application path using an\nin-memory [`History`](https://developer.mozilla.org/en-US/docs/Web/API/History)\nstack. Useful for non-browser environments without a DOM API.\n\n## Signature\n\n```tsx\nfunction createMemoryRouter(\n  routes: RouteObject[],\n  opts?: MemoryRouterOpts,\n): DataRouter\n```\n\n## Params\n\n### routes\n\nApplication routes\n\n### opts.basename\n\nBasename path for the application.\n\n### opts.dataStrategy\n\nOverride the default data strategy of running loaders in parallel -\nsee the [docs](../../how-to/data-strategy) for more information.\n\n```tsx\nlet router = createBrowserRouter(routes, {\n  async dataStrategy({\n    matches,\n    request,\n    runClientMiddleware,\n  }) {\n    const matchesToLoad = matches.filter((m) =>\n      m.shouldCallHandler(),\n    );\n\n    const results: Record<string, DataStrategyResult> = {};\n    await runClientMiddleware(() =>\n      Promise.all(\n        matchesToLoad.map(async (match) => {\n          results[match.route.id] = await match.resolve();\n        }),\n      ),\n    );\n    return results;\n  },\n});\n```\n\n### opts.future\n\nFuture flags to enable for the router.\n\n### opts.getContext\n\nA function that returns an [`RouterContextProvider`](../utils/RouterContextProvider) instance\nwhich is provided as the `context` argument to client [`action`](../../start/data/route-object#action)s,\n[`loader`](../../start/data/route-object#loader)s and [middleware](../../how-to/middleware).\nThis function is called to generate a fresh `context` instance on each\nnavigation or fetcher call.\n\n### opts.hydrationData\n\nHydration data to initialize the router with if you have already performed\ndata loading on the server.\n\n### opts.initialEntries\n\nInitial entries in the in-memory history stack\n\n### opts.initialIndex\n\nIndex of `initialEntries` the application should initialize to\n\n### opts.unstable_instrumentations\n\nArray of instrumentation objects allowing you to instrument the router and\nindividual routes prior to router initialization (and on any subsequently\nadded routes via `route.lazy` or `patchRoutesOnNavigation`).  This is\nmostly useful for observability such as wrapping navigations, fetches,\nas well as route loaders/actions/middlewares with logging and/or performance\ntracing.  See the [docs](../../how-to/instrumentation) for more information.\n\n```tsx\nlet router = createBrowserRouter(routes, {\n  unstable_instrumentations: [logging]\n});\n\n\nlet logging = {\n  router({ instrument }) {\n    instrument({\n      navigate: (impl, info) => logExecution(`navigate ${info.to}`, impl),\n      fetch: (impl, info) => logExecution(`fetch ${info.to}`, impl)\n    });\n  },\n  route({ instrument, id }) {\n    instrument({\n      middleware: (impl, info) => logExecution(\n        `middleware ${info.request.url} (route ${id})`,\n        impl\n      ),\n      loader: (impl, info) => logExecution(\n        `loader ${info.request.url} (route ${id})`,\n        impl\n      ),\n      action: (impl, info) => logExecution(\n        `action ${info.request.url} (route ${id})`,\n        impl\n      ),\n    })\n  }\n};\n\nasync function logExecution(label: string, impl: () => Promise<void>) {\n  let start = performance.now();\n  console.log(`start ${label}`);\n  await impl();\n  let duration = Math.round(performance.now() - start);\n  console.log(`end ${label} (${duration}ms)`);\n}\n```\n\n### opts.patchRoutesOnNavigation\n\nLazily define portions of the route tree on navigations.\n\n## Returns\n\nAn initialized [`DataRouter`](https://api.reactrouter.com/v7/interfaces/react-router.DataRouter.html) to pass to [`<RouterProvider>`](../data-routers/RouterProvider)\n\n"
  },
  {
    "path": "docs/api/data-routers/createStaticHandler.md",
    "content": "---\ntitle: createStaticHandler\n---\n\n# createStaticHandler\n\n<!--\n⚠️ ⚠️ IMPORTANT ⚠️ ⚠️ \n\nThank you for helping improve our documentation!\n\nThis file is auto-generated from the JSDoc comments in the source\ncode, so please edit the JSDoc comments in the file below and this\nfile will be re-generated once those changes are merged.\n\nhttps://github.com/remix-run/react-router/blob/main/packages/react-router/lib/dom/server.tsx\n-->\n\n[MODES: data]\n\n## Summary\n\n[Reference Documentation ↗](https://api.reactrouter.com/v7/functions/react-router.createStaticHandler.html)\n\nCreate a static handler to perform server-side data loading\n\n```tsx\nexport async function handleRequest(request: Request) {\n  let { query, dataRoutes } = createStaticHandler(routes);\n  let context = await query(request);\n\n  if (context instanceof Response) {\n    return context;\n  }\n\n  let router = createStaticRouter(dataRoutes, context);\n  return new Response(\n    ReactDOMServer.renderToString(<StaticRouterProvider ... />),\n    { headers: { \"Content-Type\": \"text/html\" } }\n  );\n}\n```\n\n## Signature\n\n```tsx\nfunction createStaticHandler(\n  routes: RouteObject[],\n  opts?: CreateStaticHandlerOptions,\n)\n```\n\n## Params\n\n### routes\n\nThe [route objects](https://api.reactrouter.com/v7/types/react-router.RouteObject.html) to create a static handler for\n\n### opts.basename\n\nThe base URL for the static handler (default: `/`)\n\n### opts.future\n\nFuture flags for the static handler\n\n## Returns\n\nA static handler that can be used to query data for the provided\nroutes\n\n"
  },
  {
    "path": "docs/api/data-routers/createStaticRouter.md",
    "content": "---\ntitle: createStaticRouter\n---\n\n# createStaticRouter\n\n<!--\n⚠️ ⚠️ IMPORTANT ⚠️ ⚠️ \n\nThank you for helping improve our documentation!\n\nThis file is auto-generated from the JSDoc comments in the source\ncode, so please edit the JSDoc comments in the file below and this\nfile will be re-generated once those changes are merged.\n\nhttps://github.com/remix-run/react-router/blob/main/packages/react-router/lib/dom/server.tsx\n-->\n\n[MODES: data]\n\n## Summary\n\n[Reference Documentation ↗](https://api.reactrouter.com/v7/functions/react-router.createStaticRouter.html)\n\nCreate a static [`DataRouter`](https://api.reactrouter.com/v7/interfaces/react-router.DataRouter.html) for server-side rendering\n\n```tsx\nexport async function handleRequest(request: Request) {\n  let { query, dataRoutes } = createStaticHandler(routes);\n  let context = await query(request);\n\n  if (context instanceof Response) {\n    return context;\n  }\n\n  let router = createStaticRouter(dataRoutes, context);\n  return new Response(\n    ReactDOMServer.renderToString(<StaticRouterProvider ... />),\n    { headers: { \"Content-Type\": \"text/html\" } }\n  );\n}\n```\n\n## Signature\n\n```tsx\nfunction createStaticRouter(\n  routes: RouteObject[],\n  context: StaticHandlerContext,\n  opts: {\n    future?: Partial<FutureConfig>;\n  } = ,\n): DataRouter {}\n```\n\n## Params\n\n### routes\n\nThe route objects to create a static [`DataRouter`](https://api.reactrouter.com/v7/interfaces/react-router.DataRouter.html) for\n\n### context\n\nThe [`StaticHandlerContext`](https://api.reactrouter.com/v7/interfaces/react-router.StaticHandlerContext.html) returned from [`StaticHandler`](https://api.reactrouter.com/v7/interfaces/react-router.StaticHandler.html)'s `query`\n\n### opts.future\n\nFuture flags for the static [`DataRouter`](https://api.reactrouter.com/v7/interfaces/react-router.DataRouter.html)\n\n## Returns\n\nA static [`DataRouter`](https://api.reactrouter.com/v7/interfaces/react-router.DataRouter.html) that can be used to render the provided routes\n\n"
  },
  {
    "path": "docs/api/data-routers/index.md",
    "content": "---\ntitle: Data Routers\norder: 5\n---\n"
  },
  {
    "path": "docs/api/declarative-routers/BrowserRouter.md",
    "content": "---\ntitle: BrowserRouter\n---\n\n# BrowserRouter\n\n<!--\n⚠️ ⚠️ IMPORTANT ⚠️ ⚠️ \n\nThank you for helping improve our documentation!\n\nThis file is auto-generated from the JSDoc comments in the source\ncode, so please edit the JSDoc comments in the file below and this\nfile will be re-generated once those changes are merged.\n\nhttps://github.com/remix-run/react-router/blob/main/packages/react-router/lib/dom/lib.tsx\n-->\n\n[MODES: declarative]\n\n## Summary\n\n[Reference Documentation ↗](https://api.reactrouter.com/v7/functions/react-router.BrowserRouter.html)\n\nA declarative [`<Router>`](../declarative-routers/Router) using the browser [`History`](https://developer.mozilla.org/en-US/docs/Web/API/History)\nAPI for client-side routing.\n\n## Signature\n\n```tsx\nfunction BrowserRouter({\n  basename,\n  children,\n  unstable_useTransitions,\n  window,\n}: BrowserRouterProps)\n```\n\n## Props\n\n### basename\n\nApplication basename\n\n### children\n\n``<Route>`` components describing your route configuration\n\n### unstable_useTransitions\n\nControl whether router state updates are internally wrapped in\n[`React.startTransition`](https://react.dev/reference/react/startTransition).\n\n- When left `undefined`, all router state updates are wrapped in\n  `React.startTransition`\n- When set to `true`, [`Link`](../components/Link) and [`Form`](../components/Form) navigations will be wrapped\n  in `React.startTransition` and all router state updates are wrapped in\n  `React.startTransition`\n- When set to `false`, the router will not leverage `React.startTransition`\n  on any navigations or state changes.\n\nFor more information, please see the [docs](https://reactrouter.com/explanation/react-transitions).\n\n### window\n\n[`Window`](https://developer.mozilla.org/en-US/docs/Web/API/Window) object\noverride. Defaults to the global `window` instance\n\n"
  },
  {
    "path": "docs/api/declarative-routers/HashRouter.md",
    "content": "---\ntitle: HashRouter\n---\n\n# HashRouter\n\n<!--\n⚠️ ⚠️ IMPORTANT ⚠️ ⚠️ \n\nThank you for helping improve our documentation!\n\nThis file is auto-generated from the JSDoc comments in the source\ncode, so please edit the JSDoc comments in the file below and this\nfile will be re-generated once those changes are merged.\n\nhttps://github.com/remix-run/react-router/blob/main/packages/react-router/lib/dom/lib.tsx\n-->\n\n[MODES: declarative]\n\n## Summary\n\n[Reference Documentation ↗](https://api.reactrouter.com/v7/functions/react-router.HashRouter.html)\n\nA declarative [`<Router>`](../declarative-routers/Router) that stores the location in the\n[`hash`](https://developer.mozilla.org/en-US/docs/Web/API/URL/hash) portion\nof the URL so it is not sent to the server.\n\n## Signature\n\n```tsx\nfunction HashRouter({\n  basename,\n  children,\n  unstable_useTransitions,\n  window,\n}: HashRouterProps)\n```\n\n## Props\n\n### basename\n\nApplication basename\n\n### children\n\n``<Route>`` components describing your route configuration\n\n### unstable_useTransitions\n\nControl whether router state updates are internally wrapped in\n[`React.startTransition`](https://react.dev/reference/react/startTransition).\n\n- When left `undefined`, all router state updates are wrapped in\n  `React.startTransition`\n- When set to `true`, [`Link`](../components/Link) and [`Form`](../components/Form) navigations will be wrapped\n  in `React.startTransition` and all router state updates are wrapped in\n  `React.startTransition`\n- When set to `false`, the router will not leverage `React.startTransition`\n  on any navigations or state changes.\n\nFor more information, please see the [docs](https://reactrouter.com/explanation/react-transitions).\n\n### window\n\n[`Window`](https://developer.mozilla.org/en-US/docs/Web/API/Window) object\noverride. Defaults to the global `window` instance\n\n"
  },
  {
    "path": "docs/api/declarative-routers/HistoryRouter.md",
    "content": "---\ntitle: HistoryRouter\nunstable: true\n---\n\n# unstable_HistoryRouter\n\n<!--\n⚠️ ⚠️ IMPORTANT ⚠️ ⚠️ \n\nThank you for helping improve our documentation!\n\nThis file is auto-generated from the JSDoc comments in the source\ncode, so please edit the JSDoc comments in the file below and this\nfile will be re-generated once those changes are merged.\n\nhttps://github.com/remix-run/react-router/blob/main/packages/react-router/lib/dom/lib.tsx\n-->\n\n[MODES: declarative]\n\n<br />\n<br />\n\n<docs-warning>This API is experimental and subject to breaking changes in \nminor/patch releases. Please use with caution and pay **very** close attention \nto release notes for relevant changes.</docs-warning>\n\n## Summary\n\n[Reference Documentation ↗](https://api.reactrouter.com/v7/functions/react-router.unstable_HistoryRouter.html)\n\nA declarative [`<Router>`](../declarative-routers/Router) that accepts a pre-instantiated\n`history` object.\nIt's important to note that using your own `history` object is highly discouraged\nand may add two versions of the `history` library to your bundles unless you use\nthe same version of the `history` library that React Router uses internally.\n\n## Signature\n\n```tsx\nfunction HistoryRouter({\n  basename,\n  children,\n  history,\n  unstable_useTransitions,\n}: HistoryRouterProps)\n```\n\n## Props\n\n### basename\n\nApplication basename\n\n### children\n\n``<Route>`` components describing your route configuration\n\n### history\n\nA `History` implementation for use by the router\n\n### unstable_useTransitions\n\nControl whether router state updates are internally wrapped in\n[`React.startTransition`](https://react.dev/reference/react/startTransition).\n\n- When left `undefined`, all router state updates are wrapped in\n  `React.startTransition`\n- When set to `true`, [`Link`](../components/Link) and [`Form`](../components/Form) navigations will be wrapped\n  in `React.startTransition` and all router state updates are wrapped in\n  `React.startTransition`\n- When set to `false`, the router will not leverage `React.startTransition`\n  on any navigations or state changes.\n\nFor more information, please see the [docs](https://reactrouter.com/explanation/react-transitions).\n\n"
  },
  {
    "path": "docs/api/declarative-routers/MemoryRouter.md",
    "content": "---\ntitle: MemoryRouter\n---\n\n# MemoryRouter\n\n<!--\n⚠️ ⚠️ IMPORTANT ⚠️ ⚠️ \n\nThank you for helping improve our documentation!\n\nThis file is auto-generated from the JSDoc comments in the source\ncode, so please edit the JSDoc comments in the file below and this\nfile will be re-generated once those changes are merged.\n\nhttps://github.com/remix-run/react-router/blob/main/packages/react-router/lib/components.tsx\n-->\n\n[MODES: declarative]\n\n## Summary\n\n[Reference Documentation ↗](https://api.reactrouter.com/v7/functions/react-router.MemoryRouter.html)\n\nA declarative [`<Router>`](../declarative-routers/Router) that stores all entries in memory.\n\n## Signature\n\n```tsx\nfunction MemoryRouter({\n  basename,\n  children,\n  initialEntries,\n  initialIndex,\n  unstable_useTransitions,\n}: MemoryRouterProps): React.ReactElement\n```\n\n## Props\n\n### basename\n\nApplication basename\n\n### children\n\nNested [`Route`](../components/Route) elements describing the route tree\n\n### initialEntries\n\nInitial entries in the in-memory history stack\n\n### initialIndex\n\nIndex of `initialEntries` the application should initialize to\n\n### unstable_useTransitions\n\nControl whether router state updates are internally wrapped in\n[`React.startTransition`](https://react.dev/reference/react/startTransition).\n\n- When left `undefined`, all router state updates are wrapped in\n  `React.startTransition`\n- When set to `true`, [`Link`](../components/Link) and [`Form`](../components/Form) navigations will be wrapped\n  in `React.startTransition` and all router state updates are wrapped in\n  `React.startTransition`\n- When set to `false`, the router will not leverage `React.startTransition`\n  on any navigations or state changes.\n\nFor more information, please see the [docs](https://reactrouter.com/explanation/react-transitions).\n\n"
  },
  {
    "path": "docs/api/declarative-routers/Router.md",
    "content": "---\ntitle: Router\n---\n\n# Router\n\n<!--\n⚠️ ⚠️ IMPORTANT ⚠️ ⚠️ \n\nThank you for helping improve our documentation!\n\nThis file is auto-generated from the JSDoc comments in the source\ncode, so please edit the JSDoc comments in the file below and this\nfile will be re-generated once those changes are merged.\n\nhttps://github.com/remix-run/react-router/blob/main/packages/react-router/lib/components.tsx\n-->\n\n[MODES: declarative]\n\n## Summary\n\n[Reference Documentation ↗](https://api.reactrouter.com/v7/functions/react-router.Router.html)\n\nProvides location context for the rest of the app.\n\nNote: You usually won't render a `<Router>` directly. Instead, you'll render a\nrouter that is more specific to your environment such as a [`BrowserRouter`](../declarative-routers/BrowserRouter)\nin web browsers or a [`ServerRouter`](../framework-routers/ServerRouter) for server rendering.\n\n## Signature\n\n```tsx\nfunction Router({\n  basename: basenameProp = \"/\",\n  children = null,\n  location: locationProp,\n  navigationType = NavigationType.Pop,\n  navigator,\n  static: staticProp = false,\n  unstable_useTransitions,\n}: RouterProps): React.ReactElement | null\n```\n\n## Props\n\n### basename\n\nThe base path for the application. This is prepended to all locations\n\n### children\n\nNested [`Route`](../components/Route) elements describing the route tree\n\n### location\n\nThe location to match against. Defaults to the current location.\nThis can be a string or a [`Location`](https://api.reactrouter.com/v7/interfaces/react-router.Location.html) object.\n\n### navigationType\n\nThe type of navigation that triggered this `location` change.\nDefaults to `NavigationType.Pop`.\n\n### navigator\n\nThe navigator to use for navigation. This is usually a history object\nor a custom navigator that implements the [`Navigator`](https://api.reactrouter.com/v7/interfaces/react-router.Navigator.html) interface.\n\n### static\n\nWhether this router is static or not (used for SSR). If `true`, the router\nwill not be reactive to location changes.\n\n### unstable_useTransitions\n\nControl whether router state updates are internally wrapped in\n[`React.startTransition`](https://react.dev/reference/react/startTransition).\n\n- When left `undefined`, all router state updates are wrapped in\n  `React.startTransition`\n- When set to `true`, [`Link`](../components/Link) and [`Form`](../components/Form) navigations will be wrapped\n  in `React.startTransition` and all router state updates are wrapped in\n  `React.startTransition`\n- When set to `false`, the router will not leverage `React.startTransition`\n  on any navigations or state changes.\n\nFor more information, please see the [docs](https://reactrouter.com/explanation/react-transitions).\n\n"
  },
  {
    "path": "docs/api/declarative-routers/StaticRouter.md",
    "content": "---\ntitle: StaticRouter\n---\n\n# StaticRouter\n\n<!--\n⚠️ ⚠️ IMPORTANT ⚠️ ⚠️ \n\nThank you for helping improve our documentation!\n\nThis file is auto-generated from the JSDoc comments in the source\ncode, so please edit the JSDoc comments in the file below and this\nfile will be re-generated once those changes are merged.\n\nhttps://github.com/remix-run/react-router/blob/main/packages/react-router/lib/dom/server.tsx\n-->\n\n[MODES: declarative]\n\n## Summary\n\n[Reference Documentation ↗](https://api.reactrouter.com/v7/functions/react-router.StaticRouter.html)\n\nA [`<Router>`](../declarative-routers/Router) that may not navigate to any other [`Location`](https://api.reactrouter.com/v7/interfaces/react-router.Location.html).\nThis is useful on the server where there is no stateful UI.\n\n## Signature\n\n```tsx\nfunction StaticRouter({\n  basename,\n  children,\n  location: locationProp = \"/\",\n}: StaticRouterProps)\n```\n\n## Props\n\n### basename\n\nThe base URL for the static router (default: `/`)\n\n### children\n\nThe child elements to render inside the static router\n\n### location\n\nThe [`Location`](https://api.reactrouter.com/v7/interfaces/react-router.Location.html) to render the static router at (default: `/`)\n\n"
  },
  {
    "path": "docs/api/declarative-routers/index.md",
    "content": "---\ntitle: Declarative Routers\norder: 6\n---\n"
  },
  {
    "path": "docs/api/framework-conventions/client-modules.md",
    "content": "---\ntitle: .client modules\n---\n\n# `.client` modules\n\n[MODES: framework]\n\n## Summary\n\nYou may have a file or dependency that uses module side effects in the browser. You can use `*.client.ts` on file names or nest files within `.client` directories to force them out of server bundles.\n\n```ts filename=feature-check.client.ts\n// this would break the server\nexport const supportsVibrationAPI =\n  \"vibrate\" in window.navigator;\n```\n\nNote that values exported from this module will all be `undefined` on the server, so the only places to use them are in [`useEffect`][use_effect] and user events like click handlers.\n\n```ts\nimport { supportsVibrationAPI } from \"./feature-check.client.ts\";\n\nconsole.log(supportsVibrationAPI);\n// server: undefined\n// client: true | false\n```\n\n<docs-info>\n\nIf you need more sophisticated control over what is included in the client/server bundles, check out the [`vite-env-only` plugin](https://github.com/pcattori/vite-env-only).\n\n</docs-info>\n\n## Usage Patterns\n\n### Individual Files\n\nMark individual files as client-only by adding `.client` to the filename:\n\n```txt\napp/\n├── utils.client.ts        👈 client-only file\n├── feature-detection.client.ts\n└── root.tsx\n```\n\n### Client Directories\n\nMark entire directories as client-only by using `.client` in the directory name:\n\n```txt\napp/\n├── .client/               👈 entire directory is client-only\n│   ├── analytics.ts\n│   ├── feature-detection.ts\n│   └── browser-utils.ts\n├── components/\n└── root.tsx\n```\n\n## Examples\n\n### Browser Feature Detection\n\n```ts filename=app/utils/browser.client.ts\nexport const canUseDOM = typeof window !== \"undefined\";\n\nexport const hasWebGL = !!window.WebGLRenderingContext;\n\nexport const supportsVibrationAPI =\n  \"vibrate\" in window.navigator;\n```\n\n### Client-Only Libraries\n\n```ts filename=app/analytics.client.ts\n// This would break on the server\nimport { track } from \"some-browser-only-analytics-lib\";\n\nexport function trackEvent(eventName: string, data: any) {\n  track(eventName, data);\n}\n```\n\n### Using Client Modules\n\n```tsx filename=app/routes/dashboard.tsx\nimport { useEffect } from \"react\";\nimport {\n  canUseDOM,\n  supportsLocalStorage,\n  supportsVibrationAPI,\n} from \"../utils/browser.client.ts\";\nimport { trackEvent } from \"../analytics.client.ts\";\n\nexport default function Dashboard() {\n  useEffect(() => {\n    // These values are undefined on the server\n    if (canUseDOM && supportsVibrationAPI) {\n      console.log(\"Device supports vibration\");\n    }\n\n    // Safe localStorage usage\n    const savedTheme =\n      supportsLocalStorage.getItem(\"theme\");\n    if (savedTheme) {\n      document.body.className = savedTheme;\n    }\n\n    trackEvent(\"dashboard_viewed\", {\n      timestamp: Date.now(),\n    });\n  }, []);\n\n  return <div>Dashboard</div>;\n}\n```\n\n[use_effect]: https://react.dev/reference/react/useEffect\n"
  },
  {
    "path": "docs/api/framework-conventions/entry.client.tsx.md",
    "content": "---\ntitle: entry.client.tsx\norder: 4\n---\n\n# entry.client.tsx\n\n[MODES: framework]\n\n## Summary\n\n<docs-info>\nThis file is optional\n</docs-info>\n\nThis file is the entry point for the browser and is responsible for hydrating the markup generated by the server in your [server entry module][server-entry]\n\nThis is the first piece of code that runs in the browser. You can initialize any other client-side code here, such as client side libraries, add client only providers, etc.\n\n```tsx filename=app/entry.client.tsx\nimport { startTransition, StrictMode } from \"react\";\nimport { hydrateRoot } from \"react-dom/client\";\nimport { HydratedRouter } from \"react-router/dom\";\n\nstartTransition(() => {\n  hydrateRoot(\n    document,\n    <StrictMode>\n      <HydratedRouter />\n    </StrictMode>\n  );\n});\n```\n\n## Generating `entry.client.tsx`\n\nBy default, React Router will handle hydrating your app on the client for you. You can reveal the default entry client file with the following:\n\n```shellscript nonumber\nnpx react-router reveal\n```\n\n[server-entry]: ./entry.server.tsx\n"
  },
  {
    "path": "docs/api/framework-conventions/entry.server.tsx.md",
    "content": "---\ntitle: entry.server.tsx\norder: 5\n---\n\n# entry.server.tsx\n\n[MODES: framework]\n\n## Summary\n\nThis file is the server-side entry point that controls how your React Router application generates HTTP responses on the server.\n\nThis module should render the markup for the current page using a [`<ServerRouter>`][serverrouter] element with the `context` and `url` for the current request. This markup will (optionally) be re-hydrated once JavaScript loads in the browser using the [client entry module][client-entry].\n\n<docs-info>This file is optional if you are running on Node. If it is not present, a [default implementation][node-streaming-entry-server] will be used.\n<br/>\n<br/>\nIf you are using another runtime (i.e., Cloudflare) then you need to include this file. You can find sample implementations in the [templates repository][templates-repo].</docs-info>\n\n## Generating `entry.server.tsx`\n\nWhen running in Node, React Router will handle generating the HTTP Response for you. You can reveal the default entry server file with the following:\n\n```shellscript nonumber\nnpx react-router reveal\n```\n\n## Exports\n\n### `default`\n\nThe `default` export of this module is a function that lets you create the response, including HTTP status, headers, and HTML, giving you full control over the way the markup is generated and sent to the client.\n\n```tsx filename=app/entry.server.tsx\nimport { PassThrough } from \"node:stream\";\nimport type { EntryContext } from \"react-router\";\nimport { createReadableStreamFromReadable } from \"@react-router/node\";\nimport { ServerRouter } from \"react-router\";\nimport { renderToPipeableStream } from \"react-dom/server\";\n\nexport default function handleRequest(\n  request: Request,\n  responseStatusCode: number,\n  responseHeaders: Headers,\n  routerContext: EntryContext,\n) {\n  return new Promise((resolve, reject) => {\n    const { pipe, abort } = renderToPipeableStream(\n      <ServerRouter\n        context={routerContext}\n        url={request.url}\n      />,\n      {\n        onShellReady() {\n          responseHeaders.set(\"Content-Type\", \"text/html\");\n\n          const body = new PassThrough();\n          const stream =\n            createReadableStreamFromReadable(body);\n\n          resolve(\n            new Response(stream, {\n              headers: responseHeaders,\n              status: responseStatusCode,\n            }),\n          );\n\n          pipe(body);\n        },\n        onShellError(error: unknown) {\n          reject(error);\n        },\n      },\n    );\n  });\n}\n```\n\n### `streamTimeout`\n\nIf you are [streaming] responses, you can export an optional `streamTimeout` value (in milliseconds) that will control the amount of time the server will wait for streamed promises to settle before rejecting outstanding promises and closing the stream.\n\nIt's recommended to decouple this value from the timeout in which you abort the React renderer. You should always set the React rendering timeout to a higher value so it has time to stream down the underlying rejections from your `streamTimeout`.\n\n```tsx lines=[1-2,13-15]\n// Reject all pending promises from handler functions after 10 seconds\nexport const streamTimeout = 10000;\n\nexport default function handleRequest(...) {\n  return new Promise((resolve, reject) => {\n    // ...\n\n    const { pipe, abort } = renderToPipeableStream(\n      <ServerRouter context={routerContext} url={request.url} />,\n      { /* ... */ }\n    );\n\n    // Abort the streaming render pass after 11 seconds to allow the rejected\n    // boundaries to be flushed\n    setTimeout(abort, streamTimeout + 1000);\n  });\n}\n```\n\n### `handleDataRequest`\n\nYou can export an optional `handleDataRequest` function that will allow you to modify the response of a data request. These are the requests that do not render HTML, but rather return the `loader` and `action` data to the browser once client-side hydration has occurred.\n\n```tsx\nexport function handleDataRequest(\n  response: Response,\n  {\n    request,\n    params,\n    context,\n  }: LoaderFunctionArgs | ActionFunctionArgs,\n) {\n  response.headers.set(\"X-Custom-Header\", \"value\");\n  return response;\n}\n```\n\n### `handleError`\n\nBy default, React Router will log encountered server-side errors to the console. If you'd like more control over the logging, or would like to also report these errors to an external service, then you can export an optional `handleError` function which will give you control (and will disable the built-in error logging).\n\n```tsx\nexport function handleError(\n  error: unknown,\n  {\n    request,\n    params,\n    context,\n  }: LoaderFunctionArgs | ActionFunctionArgs,\n) {\n  if (!request.signal.aborted) {\n    sendErrorToErrorReportingService(error);\n    console.error(formatErrorForJsonLogging(error));\n  }\n}\n```\n\n_Note that you generally want to avoid logging when the request was aborted, since React Router's cancellation and race-condition handling can cause a lot of requests to be aborted._\n\n**Streaming Rendering Errors**\n\nWhen you are streaming your HTML responses via [`renderToPipeableStream`][rendertopipeablestream] or [`renderToReadableStream`][rendertoreadablestream], your own `handleError` implementation will only handle errors encountered during the initial shell render. If you encounter a rendering error during subsequent streamed rendering you will need to handle these errors manually since the React Router server has already sent the Response by that point.\n\nFor `renderToPipeableStream`, you can handle these errors in the `onError` callback function. You will need to toggle a boolean in `onShellReady` so you know if the error was a shell rendering error (and can be ignored) or an async\n\nFor an example, please refer to the default [`entry.server.tsx`][node-streaming-entry-server] for Node.\n\n**Thrown Responses**\n\nNote that this does not handle thrown `Response` instances from your `loader`/`action` functions. The intention of this handler is to find bugs in your code which result in unexpected thrown errors. If you are detecting a scenario and throwing a 401/404/etc. `Response` in your `loader`/`action` then it's an expected flow that is handled by your code. If you also wish to log, or send those to an external service, that should be done at the time you throw the response.\n\n[client-entry]: ./entry.client.tsx\n[serverrouter]: ../framework-routers/ServerRouter\n[streaming]: ../../how-to/suspense\n[rendertopipeablestream]: https://react.dev/reference/react-dom/server/renderToPipeableStream\n[rendertoreadablestream]: https://react.dev/reference/react-dom/server/renderToReadableStream\n[node-streaming-entry-server]: https://github.com/remix-run/react-router/blob/dev/packages/react-router-dev/config/defaults/entry.server.node.tsx\n[templates-repo]: https://github.com/remix-run/react-router-templates\n"
  },
  {
    "path": "docs/api/framework-conventions/index.md",
    "content": "---\ntitle: Framework Conventions\norder: 3\n---\n"
  },
  {
    "path": "docs/api/framework-conventions/react-router.config.ts.md",
    "content": "---\ntitle: react-router.config.ts\norder: 3\n---\n\n# react-router.config.ts\n\n[MODES: framework]\n\n## Summary\n\n<docs-info>\nThis file is optional\n</docs-info>\n\n[Reference Documentation ↗](https://api.reactrouter.com/v7/types/_react-router_dev.config.Config.html)\n\nReact Router framework configuration file that lets you customize aspects of your React Router application like server-side rendering, directory locations, and build settings.\n\n```tsx filename=react-router.config.ts\nimport type { Config } from \"@react-router/dev/config\";\n\nexport default {\n  appDirectory: \"app\",\n  buildDirectory: \"build\",\n  ssr: true,\n  prerender: [\"/\", \"/about\"],\n} satisfies Config;\n```\n\n## Options\n\n### `allowedActionOrigins`\n\nAn array of allowed origin hosts for action submissions to UI routes (does not apply to resource routes). Supports micromatch glob patterns (`*` to match one segment, `**` to match multiple).\n\n```tsx filename=react-router.config.ts\nexport default {\n  allowedActionOrigins: [\n    \"example.com\",\n    \"*.example.com\", // sub.example.com\n    \"**.example.com\", // sub.domain.example.com\n  ],\n} satisfies Config;\n```\n\nIf you need to set this value at runtime, you can do in by setting the value on the server build in your custom server. For example, when using `express`:\n\n```ts\nimport express from \"express\";\nimport { createRequestHandler } from \"@react-router/express\";\nimport type { ServerBuild } from \"react-router\";\n\nexport const app = express();\n\nasync function getBuild() {\n  let build: ServerBuild = await import(\n    \"virtual:react-router/server-build\"\n  );\n  return {\n    ...build,\n    allowedActionOrigins:\n      process.env.NODE_ENV === \"development\"\n        ? undefined\n        : [\"staging.example.com\", \"www.example.com\"],\n  };\n}\n\napp.use(createRequestHandler({ build: getBuild }));\n```\n\n### `appDirectory`\n\nThe path to the `app` directory, relative to the root directory. Defaults to `\"app\"`.\n\n```tsx filename=react-router.config.ts\nexport default {\n  appDirectory: \"src\",\n} satisfies Config;\n```\n\n### `basename`\n\nThe React Router app basename. Defaults to `\"/\"`.\n\n```tsx filename=react-router.config.ts\nexport default {\n  basename: \"/my-app\",\n} satisfies Config;\n```\n\n### `buildDirectory`\n\nThe path to the build directory, relative to the project. Defaults to `\"build\"`.\n\n```tsx filename=react-router.config.ts\nexport default {\n  buildDirectory: \"dist\",\n} satisfies Config;\n```\n\n### `buildEnd`\n\nA function that is called after the full React Router build is complete.\n\n```tsx filename=react-router.config.ts\nexport default {\n  buildEnd: async ({\n    buildManifest,\n    reactRouterConfig,\n    viteConfig,\n  }) => {\n    // Custom build logic here\n    console.log(\"Build completed!\");\n  },\n} satisfies Config;\n```\n\n### `future`\n\nEnabled future flags for opting into upcoming features.\n\nSee [Future Flags][future-flags] for more information.\n\n```tsx filename=react-router.config.ts\nexport default {\n  future: {\n    // Enable future flags here\n  },\n} satisfies Config;\n```\n\n### `prerender`\n\nAn array of URLs to prerender to HTML files at build time. Can also be a function returning an array to dynamically generate URLs.\n\nSee [Pre-Rendering][pre-rendering] for more information.\n\n```tsx filename=react-router.config.ts\nexport default {\n  // Static array\n  prerender: [\"/\", \"/about\", \"/contact\"],\n\n  // Or dynamic function\n  prerender: async ({ getStaticPaths }) => {\n    const paths = await getStaticPaths();\n    return [\"/\", ...paths];\n  },\n} satisfies Config;\n```\n\n### `presets`\n\nAn array of React Router plugin config presets to ease integration with other platforms and tools.\n\nSee [Presets][presets] for more information.\n\n```tsx filename=react-router.config.ts\nexport default {\n  presets: [\n    // Add presets here\n  ],\n} satisfies Config;\n```\n\n### `routeDiscovery`\n\nConfigure how routes are discovered and loaded by the client. Defaults to `mode: \"lazy\"` with `manifestPath: \"/__manifest\"`.\n\n**Options:**\n\n- `mode: \"lazy\"` - Routes are discovered as the user navigates (default)\n  - `manifestPath` - Custom path for manifest requests when using `lazy` mode\n- `mode: \"initial\"` - All routes are included in the initial manifest\n\n```tsx filename=react-router.config.ts\nexport default {\n  // Enable lazy route discovery (default)\n  routeDiscovery: {\n    mode: \"lazy\",\n    manifestPath: \"/__manifest\",\n  },\n\n  // Use a custom manifest path\n  routeDiscovery: {\n    mode: \"lazy\",\n    manifestPath: \"/custom-manifest\",\n  },\n\n  // Disable lazy discovery and include all routes initially\n  routeDiscovery: { mode: \"initial\" },\n} satisfies Config;\n```\n\nSee [Lazy Route Discovery][lazy-route-discovery] for more information.\n\n### `serverBuildFile`\n\nThe file name of the server build output. This file should end in a `.js` extension and should be deployed to your server. Defaults to `\"index.js\"`.\n\n```tsx filename=react-router.config.ts\nexport default {\n  serverBuildFile: \"server.js\",\n} satisfies Config;\n```\n\n### `serverBundles`\n\nA function for assigning routes to different server bundles. This function should return a server bundle ID which will be used as the bundle's directory name within the server build directory.\n\nSee [Server Bundles][server-bundles] for more information.\n\n```tsx filename=react-router.config.ts\nexport default {\n  serverBundles: ({ branch }) => {\n    // Return bundle ID based on route branch\n    return branch.some((route) => route.id === \"admin\")\n      ? \"admin\"\n      : \"main\";\n  },\n} satisfies Config;\n```\n\n### `serverModuleFormat`\n\nThe output format of the server build. Defaults to `\"esm\"`.\n\n```tsx filename=react-router.config.ts\nexport default {\n  serverModuleFormat: \"cjs\", // or \"esm\"\n} satisfies Config;\n```\n\n### `ssr`\n\nIf `true`, React Router will server render your application.\n\nIf `false`, React Router will pre-render your application and save it as an `index.html` file with your assets so your application can be deployed as a SPA without server-rendering. See [\"SPA Mode\"][spa-mode] for more information.\n\nDefaults to `true`.\n\n```tsx filename=react-router.config.ts\nexport default {\n  ssr: false, // disabled server-side rendering\n} satisfies Config;\n```\n\n[future-flags]: ../../upgrading/future\n[presets]: ../../how-to/presets\n[server-bundles]: ../../how-to/server-bundles\n[pre-rendering]: ../../how-to/pre-rendering\n[spa-mode]: ../../how-to/spa\n[lazy-route-discovery]: ../../explanation/lazy-route-discovery\n"
  },
  {
    "path": "docs/api/framework-conventions/root.tsx.md",
    "content": "---\ntitle: root.tsx\norder: 1\n---\n\n# root.tsx\n\n[MODES: framework]\n\n## Summary\n\n<docs-info>\nThis file is required\n</docs-info>\n\nThe \"root\" route (`app/root.tsx`) is the only _required_ route in your React Router application because it is the parent to all routes and is in charge of rendering the root `<html>` document.\n\n```tsx filename=app/root.tsx\nimport { Outlet, Scripts } from \"react-router\";\n\nimport \"./global-styles.css\";\n\nexport default function App() {\n  return (\n    <html lang=\"en\">\n      <head>\n        <link rel=\"icon\" href=\"/favicon.ico\" />\n      </head>\n      <body>\n        <Outlet />\n        <Scripts />\n      </body>\n    </html>\n  );\n}\n```\n\n## Components to Render\n\nBecause the root route manages your document, it is the proper place to render a handful of \"document-level\" components React Router provides. These components are to be used once inside your root route and they include everything React Router figured out or built in order for your page to render properly.\n\n```tsx filename=app/root.tsx\nimport {\n  Outlet,\n  Scripts,\n  ScrollRestoration,\n} from \"react-router\";\n\nexport default function App() {\n  return (\n    <html lang=\"en\">\n      <head>\n        <meta charSet=\"utf-8\" />\n        <meta\n          name=\"viewport\"\n          content=\"width=device-width, initial-scale=1\"\n        />\n      </head>\n      <body>\n        {/* Child routes render here */}\n        <Outlet />\n\n        {/* Manages scroll position for client-side transitions */}\n        {/* If you use a nonce-based content security policy for scripts, you must provide the `nonce` prop. Otherwise, omit the nonce prop as shown here. */}\n        <ScrollRestoration />\n\n        {/* Script tags go here */}\n        {/* If you use a nonce-based content security policy for scripts, you must provide the `nonce` prop. Otherwise, omit the nonce prop as shown here. */}\n        <Scripts />\n      </body>\n    </html>\n  );\n}\n```\n\nIf you are not on React 19 or choosing not to use React's [`<link>`][react-link], [`<title>`][react-title], and [`<meta>`][react-meta] components, and instead relying on React Router's [`links`][react-router-links] and [`meta`][react-router-meta] exports, you need to add the following to your root route:\n\n```tsx filename=app/root.tsx\nimport { Links, Meta } from \"react-router\";\n\nexport default function App() {\n  return (\n    <html lang=\"en\">\n      <head>\n        {/* All `meta` exports on all routes will render here */}\n        <Meta />\n\n        {/* All `link` exports on all routes will render here */}\n        <Links />\n      </head>\n      <body>\n        <Outlet />\n        <ScrollRestoration />\n        <Scripts />\n      </body>\n    </html>\n  );\n}\n```\n\n## Layout Export\n\nThe root route supports all [route module exports][route-module].\n\nThe root route also supports an additional optional `Layout` export. The `Layout` component serves 2 purposes:\n\n1. Avoid duplicating your document's \"app shell\" across your root component, `HydrateFallback`, and `ErrorBoundary`\n2. Prevent React from re-mounting your app shell elements when switching between the root component/`HydrateFallback`/`ErrorBoundary` which can cause a FOUC if React removes and re-adds `<link rel=\"stylesheet\">` tags from your `<Links>` component.\n\n`Layout` takes a single `children` prop, which is the `default` export (e.g. `App`), `HydrateFallback`, or `ErrorBoundary`.\n\n```tsx filename=app/root.tsx\nexport function Layout({ children }) {\n  return (\n    <html lang=\"en\">\n      <head>\n        <meta charSet=\"utf-8\" />\n        <meta\n          name=\"viewport\"\n          content=\"width=device-width, initial-scale=1\"\n        />\n        <Meta />\n        <Links />\n      </head>\n      <body>\n        {/* children will be the root Component, ErrorBoundary, or HydrateFallback */}\n        {children}\n        <Scripts />\n        <ScrollRestoration />\n      </body>\n    </html>\n  );\n}\n\nexport default function App() {\n  return <Outlet />;\n}\n\nexport function ErrorBoundary() {}\n```\n\n**A note on `useLoaderData`in the `Layout` Component**\n\n`useLoaderData` is not permitted to be used in `ErrorBoundary` components because it is intended for the happy-path route rendering, and its typings have a built-in assumption that the `loader` ran successfully and returned something. That assumption doesn't hold in an `ErrorBoundary` because it could have been the `loader` that threw and triggered the boundary! In order to access loader data in `ErrorBoundary`'s, you can use `useRouteLoaderData` which accounts for the loader data potentially being `undefined`.\n\nBecause your `Layout` component is used in both success and error flows, this same restriction holds. If you need to fork logic in your `Layout` depending on if it was a successful request or not, you can use `useRouteLoaderData(\"root\")` and `useRouteError()`.\n\n<docs-warn>Because your `<Layout>` component is used for rendering the `ErrorBoundary`, you should be _very defensive_ to ensure that you can render your `ErrorBoundary` without encountering any render errors. If your `Layout` throws another error trying to render the boundary, then it can't be used and your UI will fall back to the very minimal built-in default `ErrorBoundary`.</docs-warn>\n\n```tsx filename=app/root.tsx lines=[6-7,19-29,32-34]\nexport function Layout({\n  children,\n}: {\n  children: React.ReactNode;\n}) {\n  const data = useRouteLoaderData(\"root\");\n  const error = useRouteError();\n\n  return (\n    <html lang=\"en\">\n      <head>\n        <meta charSet=\"utf-8\" />\n        <meta\n          name=\"viewport\"\n          content=\"width=device-width, initial-scale=1\"\n        />\n        <Meta />\n        <Links />\n        <style\n          dangerouslySetInnerHTML={{\n            __html: `\n              :root {\n                --themeVar: ${\n                  data?.themeVar || defaultThemeVar\n                }\n              }\n            `,\n          }}\n        />\n      </head>\n      <body>\n        {data ? (\n          <Analytics token={data.analyticsToken} />\n        ) : null}\n        {children}\n        <ScrollRestoration />\n        <Scripts />\n      </body>\n    </html>\n  );\n}\n```\n\n[route-module]: ../../start/framework/route-module\n[react-link]: https://react.dev/reference/react-dom/components/link\n[react-meta]: https://react.dev/reference/react-dom/components/meta\n[react-title]: https://react.dev/reference/react-dom/components/title\n[react-router-links]: ../../start/framework/route-module#links\n[react-router-meta]: ../../start/framework/route-module#meta\n"
  },
  {
    "path": "docs/api/framework-conventions/routes.ts.md",
    "content": "---\ntitle: routes.ts\norder: 2\n---\n\n# routes.ts\n\n[MODES: framework]\n\n## Summary\n\n<docs-info>\nThis file is required\n</docs-info>\n\n[Reference Documentation ↗](https://api.reactrouter.com/v7/interfaces/_react-router_dev.routes.RouteConfigEntry.html)\n\nConfiguration file that maps URL patterns to route modules in your application.\n\nSee the [routing guide][routing] for more information.\n\n## Examples\n\n### Basic\n\nConfigure your routes as an array of objects.\n\n```tsx filename=app/routes.ts\nimport {\n  type RouteConfig,\n  route,\n} from \"@react-router/dev/routes\";\n\nexport default [\n  route(\"some/path\", \"./some/file.tsx\"),\n  // pattern ^           ^ module file\n] satisfies RouteConfig;\n```\n\nYou can use the following helpers to create route config entries:\n\n- [`route`][route] — Helper function for creating a route config entry\n- [`index`][index] — Helper function for creating a route config entry for an index route\n- [`layout`][layout] — Helper function for creating a route config entry for a layout route\n- [`prefix`][prefix] — Helper function for adding a path prefix to a set of routes without needing to introduce a parent route\n- [`relative`][relative] — Creates a set of route config helpers that resolve file paths relative to the given directory. Designed to support splitting route config into multiple files within different directories\n\n### File-based Routing\n\nIf you prefer to define your routes via file naming conventions rather than configuration, the `@react-router/fs-routes` package provides a [file system routing convention][file-route-conventions]:\n\n```ts filename=app/routes.ts\nimport { type RouteConfig } from \"@react-router/dev/routes\";\nimport { flatRoutes } from \"@react-router/fs-routes\";\n\nexport default flatRoutes() satisfies RouteConfig;\n```\n\n### Route Helpers\n\n[routing]: ../../start/framework/routing\n[route]: https://api.reactrouter.com/v7/functions/_react-router_dev.routes.route.html\n[index]: https://api.reactrouter.com/v7/functions/_react-router_dev.routes.index.html\n[layout]: https://api.reactrouter.com/v7/functions/_react-router_dev.routes.layout.html\n[prefix]: https://api.reactrouter.com/v7/functions/_react-router_dev.routes.prefix.html\n[relative]: https://api.reactrouter.com/v7/functions/_react-router_dev.routes.relative.html\n[file-route-conventions]: ../../how-to/file-route-conventions\n"
  },
  {
    "path": "docs/api/framework-conventions/server-modules.md",
    "content": "---\ntitle: .server modules\n---\n\n# `.server` modules\n\n[MODES: framework]\n\n## Summary\n\nServer-only modules that are excluded from client bundles and only run on the server.\n\n```ts filename=auth.server.ts\n// This would expose secrets on the client if not exported from a server-only module\nexport const JWT_SECRET = process.env.JWT_SECRET;\n\nexport function validateToken(token: string) {\n  // Server-only authentication logic\n}\n```\n\n`.server` modules are a good way to explicitly mark entire modules as server-only. The build will fail if any code in a `.server` file or `.server` directory accidentally ends up in the client module graph.\n\n<docs-warning>\n\nRoute modules should not be marked as `.server` or `.client` as they have special handling and need to be referenced in both server and client module graphs. Attempting to do so will cause build errors.\n\n</docs-warning>\n\n<docs-info>\n\nIf you need more sophisticated control over what is included in the client/server bundles, check out the [`vite-env-only` plugin](https://github.com/pcattori/vite-env-only).\n\n</docs-info>\n\n\n## Usage Patterns\n\n### Individual Files\n\nMark individual files as server-only by adding `.server` to the filename:\n\n```txt\napp/\n├── auth.server.ts         👈 server-only file\n├── database.server.ts\n├── email.server.ts\n└── root.tsx\n```\n\n### Server Directories\n\nMark entire directories as server-only by using `.server` in the directory name:\n\n```txt\napp/\n├── .server/               👈 entire directory is server-only\n│   ├── auth.ts\n│   ├── database.ts\n│   └── email.ts\n├── components/\n└── root.tsx\n```\n\n## Examples\n\n### Database Connection\n\n```ts filename=app/utils/db.server.ts\nimport { PrismaClient } from \"@prisma/client\";\n\n// This would expose database credentials on the client\nconst db = new PrismaClient({\n  datasources: {\n    db: {\n      url: process.env.DATABASE_URL,\n    },\n  },\n});\n\nexport { db };\n```\n\n### Authentication Utilities\n\n```ts filename=app/utils/auth.server.ts\nimport jwt from \"jsonwebtoken\";\nimport bcrypt from \"bcryptjs\";\n\nconst JWT_SECRET = process.env.JWT_SECRET!;\n\nexport function hashPassword(password: string) {\n  return bcrypt.hash(password, 10);\n}\n\nexport function verifyPassword(\n  password: string,\n  hash: string\n) {\n  return bcrypt.compare(password, hash);\n}\n\nexport function createToken(userId: string) {\n  return jwt.sign({ userId }, JWT_SECRET, {\n    expiresIn: \"7d\",\n  });\n}\n\nexport function verifyToken(token: string) {\n  return jwt.verify(token, JWT_SECRET) as {\n    userId: string;\n  };\n}\n```\n\n### Using Server Modules\n\n```tsx filename=app/routes/login.tsx\nimport type { ActionFunctionArgs } from \"react-router\";\nimport { redirect } from \"react-router\";\nimport {\n  hashPassword,\n  createToken,\n} from \"../utils/auth.server\";\nimport { db } from \"../utils/db.server\";\n\nexport async function action({\n  request,\n}: ActionFunctionArgs) {\n  const formData = await request.formData();\n  const email = formData.get(\"email\") as string;\n  const password = formData.get(\"password\") as string;\n\n  // Server-only operations\n  const hashedPassword = await hashPassword(password);\n  const user = await db.user.create({\n    data: { email, password: hashedPassword },\n  });\n\n  const token = createToken(user.id);\n\n  return redirect(\"/dashboard\", {\n    headers: {\n      \"Set-Cookie\": `token=${token}; HttpOnly; Secure; SameSite=Strict`,\n    },\n  });\n}\n\nexport default function Login() {\n  return (\n    <form method=\"post\">\n      <input name=\"email\" type=\"email\" required />\n      <input name=\"password\" type=\"password\" required />\n      <button type=\"submit\">Login</button>\n    </form>\n  );\n}\n```\n"
  },
  {
    "path": "docs/api/framework-routers/HydratedRouter.md",
    "content": "---\ntitle: HydratedRouter\n---\n\n# HydratedRouter\n\n<!--\n⚠️ ⚠️ IMPORTANT ⚠️ ⚠️ \n\nThank you for helping improve our documentation!\n\nThis file is auto-generated from the JSDoc comments in the source\ncode, so please edit the JSDoc comments in the file below and this\nfile will be re-generated once those changes are merged.\n\nhttps://github.com/remix-run/react-router/blob/main/packages/react-router/lib/dom-export/hydrated-router.tsx\n-->\n\n[MODES: framework]\n\n## Summary\n\nFramework-mode router component to be used to hydrate a router from a\n[`ServerRouter`](../framework-routers/ServerRouter). See [`entry.client.tsx`](../framework-conventions/entry.client.tsx).\n\n## Signature\n\n```tsx\nfunction HydratedRouter(props: HydratedRouterProps)\n```\n\n## Props\n\n### getContext\n\nContext factory function to be passed through to [`createBrowserRouter`](../data-routers/createBrowserRouter).\nThis function will be called to create a fresh `context` instance on each\nnavigation/fetch and made available to\n[`clientAction`](../../start/framework/route-module#clientAction)/[`clientLoader`](../../start/framework/route-module#clientLoader)\nfunctions.\n\n### onError\n\nAn error handler function that will be called for any middleware, loader, action,\nor render errors that are encountered in your application.  This is useful for\nlogging or reporting errors instead of in the `ErrorBoundary` because it's not\nsubject to re-rendering and will only run one time per error.\n\nThe `errorInfo` parameter is passed along from\n[`componentDidCatch`](https://react.dev/reference/react/Component#componentdidcatch)\nand is only present for render errors.\n\n```tsx\n<HydratedRouter onError=(error, info) => {\n  let { location, params, unstable_pattern, errorInfo } = info;\n  console.error(error, location, errorInfo);\n  reportToErrorService(error, location, errorInfo);\n}} />\n```\n\n"
  },
  {
    "path": "docs/api/framework-routers/ServerRouter.md",
    "content": "---\ntitle: ServerRouter\n---\n\n# ServerRouter\n\n<!--\n⚠️ ⚠️ IMPORTANT ⚠️ ⚠️ \n\nThank you for helping improve our documentation!\n\nThis file is auto-generated from the JSDoc comments in the source\ncode, so please edit the JSDoc comments in the file below and this\nfile will be re-generated once those changes are merged.\n\nhttps://github.com/remix-run/react-router/blob/main/packages/react-router/lib/dom/ssr/server.tsx\n-->\n\n[MODES: framework]\n\n## Summary\n\n[Reference Documentation ↗](https://api.reactrouter.com/v7/functions/react-router.ServerRouter.html)\n\nThe server entry point for a React Router app in Framework Mode. This\ncomponent is used to generate the HTML in the response from the server. See\n[`entry.server.tsx`](../framework-conventions/entry.server.tsx).\n\n## Signature\n\n```tsx\nfunction ServerRouter({\n  context,\n  url,\n  nonce,\n}: ServerRouterProps): ReactElement\n```\n\n## Props\n\n### context\n\nThe entry context containing the manifest, route modules, and other data\nneeded for rendering.\n\n### nonce\n\nAn optional `nonce` for [Content Security Policy (CSP)](https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/CSP)\ncompliance, used to allow inline scripts to run safely.\n\n### url\n\nThe URL of the request being handled.\n\n"
  },
  {
    "path": "docs/api/framework-routers/index.md",
    "content": "---\ntitle: Framework Routers\norder: 4\n---\n"
  },
  {
    "path": "docs/api/hooks/index.md",
    "content": "---\ntitle: Hooks\norder: 2\n---\n"
  },
  {
    "path": "docs/api/hooks/useActionData.md",
    "content": "---\ntitle: useActionData\n---\n\n# useActionData\n\n<!--\n⚠️ ⚠️ IMPORTANT ⚠️ ⚠️ \n\nThank you for helping improve our documentation!\n\nThis file is auto-generated from the JSDoc comments in the source\ncode, so please edit the JSDoc comments in the file below and this\nfile will be re-generated once those changes are merged.\n\nhttps://github.com/remix-run/react-router/blob/main/packages/react-router/lib/hooks.tsx\n-->\n\n[MODES: framework, data]\n\n## Summary\n\n[Reference Documentation ↗](https://api.reactrouter.com/v7/functions/react-router.useActionData.html)\n\nReturns the [`action`](../../start/framework/route-module#action) data from\nthe most recent `POST` navigation form submission or `undefined` if there\nhasn't been one.\n\n```tsx\nimport { Form, useActionData } from \"react-router\";\n\nexport async function action({ request }) {\n  const body = await request.formData();\n  const name = body.get(\"visitorsName\");\n  return { message: `Hello, ${name}` };\n}\n\nexport default function Invoices() {\n  const data = useActionData();\n  return (\n    <Form method=\"post\">\n      <input type=\"text\" name=\"visitorsName\" />\n      {data ? data.message : \"Waiting...\"}\n    </Form>\n  );\n}\n```\n\n## Signature\n\n```tsx\nfunction useActionData<T = any>(): SerializeFrom<T> | undefined\n```\n\n## Returns\n\nThe data returned from the route's [`action`](../../start/framework/route-module#action)\nfunction, or `undefined` if no [`action`](../../start/framework/route-module#action)\nhas been called\n\n"
  },
  {
    "path": "docs/api/hooks/useAsyncError.md",
    "content": "---\ntitle: useAsyncError\n---\n\n# useAsyncError\n\n<!--\n⚠️ ⚠️ IMPORTANT ⚠️ ⚠️ \n\nThank you for helping improve our documentation!\n\nThis file is auto-generated from the JSDoc comments in the source\ncode, so please edit the JSDoc comments in the file below and this\nfile will be re-generated once those changes are merged.\n\nhttps://github.com/remix-run/react-router/blob/main/packages/react-router/lib/hooks.tsx\n-->\n\n[MODES: framework, data]\n\n## Summary\n\n[Reference Documentation ↗](https://api.reactrouter.com/v7/functions/react-router.useAsyncError.html)\n\nReturns the rejection value from the closest [`<Await>`](../components/Await).\n\n```tsx\nimport { Await, useAsyncError } from \"react-router\";\n\nfunction ErrorElement() {\n  const error = useAsyncError();\n  return (\n    <p>Uh Oh, something went wrong! {error.message}</p>\n  );\n}\n\n// somewhere in your app\n<Await\n  resolve={promiseThatRejects}\n  errorElement={<ErrorElement />}\n/>;\n```\n\n## Signature\n\n```tsx\nfunction useAsyncError(): unknown\n```\n\n## Returns\n\nThe error that was thrown in the nearest [`Await`](../components/Await) component\n\n"
  },
  {
    "path": "docs/api/hooks/useAsyncValue.md",
    "content": "---\ntitle: useAsyncValue\n---\n\n# useAsyncValue\n\n<!--\n⚠️ ⚠️ IMPORTANT ⚠️ ⚠️ \n\nThank you for helping improve our documentation!\n\nThis file is auto-generated from the JSDoc comments in the source\ncode, so please edit the JSDoc comments in the file below and this\nfile will be re-generated once those changes are merged.\n\nhttps://github.com/remix-run/react-router/blob/main/packages/react-router/lib/hooks.tsx\n-->\n\n[MODES: framework, data]\n\n## Summary\n\n[Reference Documentation ↗](https://api.reactrouter.com/v7/functions/react-router.useAsyncValue.html)\n\nReturns the resolved promise value from the closest [`<Await>`](../components/Await).\n\n```tsx\nfunction SomeDescendant() {\n  const value = useAsyncValue();\n  // ...\n}\n\n// somewhere in your app\n<Await resolve={somePromise}>\n  <SomeDescendant />\n</Await>;\n```\n\n## Signature\n\n```tsx\nfunction useAsyncValue(): unknown\n```\n\n## Returns\n\nThe resolved value from the nearest [`Await`](../components/Await) component\n\n"
  },
  {
    "path": "docs/api/hooks/useBeforeUnload.md",
    "content": "---\ntitle: useBeforeUnload\n---\n\n# useBeforeUnload\n\n<!--\n⚠️ ⚠️ IMPORTANT ⚠️ ⚠️ \n\nThank you for helping improve our documentation!\n\nThis file is auto-generated from the JSDoc comments in the source\ncode, so please edit the JSDoc comments in the file below and this\nfile will be re-generated once those changes are merged.\n\nhttps://github.com/remix-run/react-router/blob/main/packages/react-router/lib/dom/lib.tsx\n-->\n\n[MODES: framework, data, declarative]\n\n## Summary\n\n[Reference Documentation ↗](https://api.reactrouter.com/v7/functions/react-router.useBeforeUnload.html)\n\nSet up a callback to be fired on [Window's `beforeunload` event](https://developer.mozilla.org/en-US/docs/Web/API/Window/beforeunload_event).\n\n## Signature\n\n```tsx\nfunction useBeforeUnload(\n  callback: (event: BeforeUnloadEvent) => any,\n  options?: {\n    capture?: boolean;\n  },\n): void\n```\n\n## Params\n\n### callback\n\nThe callback to be called when the [`beforeunload` event](https://developer.mozilla.org/en-US/docs/Web/API/Window/beforeunload_event) is fired.\n\n### options.capture\n\nIf `true`, the event will be captured during the capture phase. Defaults to `false`.\n\n## Returns\n\nNo return value.\n\n"
  },
  {
    "path": "docs/api/hooks/useBlocker.md",
    "content": "---\ntitle: useBlocker\n---\n\n# useBlocker\n\n<!--\n⚠️ ⚠️ IMPORTANT ⚠️ ⚠️ \n\nThank you for helping improve our documentation!\n\nThis file is auto-generated from the JSDoc comments in the source\ncode, so please edit the JSDoc comments in the file below and this\nfile will be re-generated once those changes are merged.\n\nhttps://github.com/remix-run/react-router/blob/main/packages/react-router/lib/hooks.tsx\n-->\n\n[MODES: framework, data]\n\n## Summary\n\n[Reference Documentation ↗](https://api.reactrouter.com/v7/functions/react-router.useBlocker.html)\n\nAllow the application to block navigations within the SPA and present the\nuser a confirmation dialog to confirm the navigation. Mostly used to avoid\nusing half-filled form data. This does not handle hard-reloads or\ncross-origin navigations.\n\nThe [`Blocker`](https://api.reactrouter.com/v7/types/react-router.Blocker.html) object returned by the hook has the following properties:\n\n- **`state`**\n  - `unblocked` - the blocker is idle and has not prevented any navigation\n  - `blocked` - the blocker has prevented a navigation\n  - `proceeding` - the blocker is proceeding through from a blocked navigation\n- **`location`**\n  - When in a `blocked` state, this represents the [`Location`](https://api.reactrouter.com/v7/interfaces/react-router.Location.html) to which\n    we blocked a navigation. When in a `proceeding` state, this is the\n    location being navigated to after a `blocker.proceed()` call.\n- **`proceed()`**\n  - When in a `blocked` state, you may call `blocker.proceed()` to proceed to\n    the blocked location.\n- **`reset()`**\n  - When in a `blocked` state, you may call `blocker.reset()` to return the\n    blocker to an `unblocked` state and leave the user at the current\n    location.\n\n```tsx\n// Boolean version\nlet blocker = useBlocker(value !== \"\");\n\n// Function version\nlet blocker = useBlocker(\n  ({ currentLocation, nextLocation, historyAction }) =>\n    value !== \"\" &&\n    currentLocation.pathname !== nextLocation.pathname\n);\n```\n\n## Signature\n\n```tsx\nfunction useBlocker(shouldBlock: boolean | BlockerFunction): Blocker\n```\n\n## Params\n\n### shouldBlock\n\nEither a boolean or a function returning a boolean which indicates whether the navigation should be blocked. The function format\nreceives a single object parameter containing the `currentLocation`,\n`nextLocation`, and `historyAction` of the potential navigation.\n\n## Returns\n\nA [`Blocker`](https://api.reactrouter.com/v7/types/react-router.Blocker.html) object with state and reset functionality\n\n## Examples\n\n```tsx\nimport { useCallback, useState } from \"react\";\nimport { BlockerFunction, useBlocker } from \"react-router\";\n\nexport function ImportantForm() {\n  const [value, setValue] = useState(\"\");\n\n  const shouldBlock = useCallback<BlockerFunction>(\n    () => value !== \"\",\n    [value]\n  );\n  const blocker = useBlocker(shouldBlock);\n\n  return (\n    <form\n      onSubmit={(e) => {\n        e.preventDefault();\n        setValue(\"\");\n        if (blocker.state === \"blocked\") {\n          blocker.proceed();\n        }\n      }}\n    >\n      <input\n        name=\"data\"\n        value={value}\n        onChange={(e) => setValue(e.target.value)}\n      />\n\n      <button type=\"submit\">Save</button>\n\n      {blocker.state === \"blocked\" ? (\n        <>\n          <p style={{ color: \"red\" }}>\n            Blocked the last navigation to\n          </p>\n          <button\n            type=\"button\"\n            onClick={() => blocker.proceed()}\n          >\n            Let me through\n          </button>\n          <button\n            type=\"button\"\n            onClick={() => blocker.reset()}\n          >\n            Keep me here\n          </button>\n        </>\n      ) : blocker.state === \"proceeding\" ? (\n        <p style={{ color: \"orange\" }}>\n          Proceeding through blocked navigation\n        </p>\n      ) : (\n        <p style={{ color: \"green\" }}>\n          Blocker is currently unblocked\n        </p>\n      )}\n    </form>\n  );\n}\n```\n\n"
  },
  {
    "path": "docs/api/hooks/useFetcher.md",
    "content": "---\ntitle: useFetcher\n---\n\n# useFetcher\n\n<!--\n⚠️ ⚠️ IMPORTANT ⚠️ ⚠️ \n\nThank you for helping improve our documentation!\n\nThis file is auto-generated from the JSDoc comments in the source\ncode, so please edit the JSDoc comments in the file below and this\nfile will be re-generated once those changes are merged.\n\nhttps://github.com/remix-run/react-router/blob/main/packages/react-router/lib/dom/lib.tsx\n-->\n\n[MODES: framework, data]\n\n## Summary\n\n[Reference Documentation ↗](https://api.reactrouter.com/v7/functions/react-router.useFetcher.html)\n\nUseful for creating complex, dynamic user interfaces that require multiple,\nconcurrent data interactions without causing a navigation.\n\nFetchers track their own, independent state and can be used to load data, submit\nforms, and generally interact with [`action`](../../start/framework/route-module#action)\nand [`loader`](../../start/framework/route-module#loader) functions.\n\n```tsx\nimport { useFetcher } from \"react-router\"\n\nfunction SomeComponent() {\n  let fetcher = useFetcher()\n\n  // states are available on the fetcher\n  fetcher.state // \"idle\" | \"loading\" | \"submitting\"\n  fetcher.data // the data returned from the action or loader\n\n  // render a form\n  <fetcher.Form method=\"post\" />\n\n  // load data\n  fetcher.load(\"/some/route\")\n\n  // submit data\n  fetcher.submit(someFormRef, { method: \"post\" })\n  fetcher.submit(someData, {\n    method: \"post\",\n    encType: \"application/json\"\n  })\n\n  // reset fetcher\n  fetcher.reset()\n}\n```\n\n## Signature\n\n```tsx\nfunction useFetcher<T = any>({\n  key,\n}: {\n  key?: string;\n} = ): FetcherWithComponents<SerializeFrom<T>> {}\n```\n\n## Params\n\n### options.key\n\nA unique key to identify the fetcher. \n\nBy default, `useFetcher` generates a unique fetcher scoped to that component.\nIf you want to identify a fetcher with your own key such that you can access\nit from elsewhere in your app, you can do that with the `key` option:\n\n```tsx\nfunction SomeComp() {\n  let fetcher = useFetcher({ key: \"my-key\" })\n  // ...\n}\n\n// Somewhere else\nfunction AnotherComp() {\n  // this will be the same fetcher, sharing the state across the app\n  let fetcher = useFetcher({ key: \"my-key\" });\n  // ...\n}\n```\n\n## Returns\n\nA [`FetcherWithComponents`](https://api.reactrouter.com/v7/types/react-router.FetcherWithComponents.html) object that contains the fetcher's state, data, and components for submitting forms and loading data.\n\n"
  },
  {
    "path": "docs/api/hooks/useFetchers.md",
    "content": "---\ntitle: useFetchers\n---\n\n# useFetchers\n\n<!--\n⚠️ ⚠️ IMPORTANT ⚠️ ⚠️ \n\nThank you for helping improve our documentation!\n\nThis file is auto-generated from the JSDoc comments in the source\ncode, so please edit the JSDoc comments in the file below and this\nfile will be re-generated once those changes are merged.\n\nhttps://github.com/remix-run/react-router/blob/main/packages/react-router/lib/dom/lib.tsx\n-->\n\n[MODES: framework, data]\n\n## Summary\n\n[Reference Documentation ↗](https://api.reactrouter.com/v7/functions/react-router.useFetchers.html)\n\nReturns an array of all in-flight [`Fetcher`](https://api.reactrouter.com/v7/types/react-router.Fetcher.html)s. This is useful for components\nthroughout the app that didn't create the fetchers but want to use their submissions\nto participate in optimistic UI.\n\n```tsx\nimport { useFetchers } from \"react-router\";\n\nfunction SomeComponent() {\n  const fetchers = useFetchers();\n  fetchers[0].formData; // FormData\n  fetchers[0].state; // etc.\n  // ...\n}\n```\n\n## Signature\n\n```tsx\nfunction useFetchers(): (Fetcher & {\n  key: string;\n})[]\n```\n\n## Returns\n\nAn array of all in-flight [`Fetcher`](https://api.reactrouter.com/v7/types/react-router.Fetcher.html)s, each with a unique `key`\nproperty.\n\n"
  },
  {
    "path": "docs/api/hooks/useFormAction.md",
    "content": "---\ntitle: useFormAction\n---\n\n# useFormAction\n\n<!--\n⚠️ ⚠️ IMPORTANT ⚠️ ⚠️ \n\nThank you for helping improve our documentation!\n\nThis file is auto-generated from the JSDoc comments in the source\ncode, so please edit the JSDoc comments in the file below and this\nfile will be re-generated once those changes are merged.\n\nhttps://github.com/remix-run/react-router/blob/main/packages/react-router/lib/dom/lib.tsx\n-->\n\n[MODES: framework, data]\n\n## Summary\n\n[Reference Documentation ↗](https://api.reactrouter.com/v7/functions/react-router.useFormAction.html)\n\nResolves the URL to the closest route in the component hierarchy instead of\nthe current URL of the app.\n\nThis is used internally by [`Form`](../components/Form) to resolve the `action` to the closest\nroute, but can be used generically as well.\n\n```tsx\nimport { useFormAction } from \"react-router\";\n\nfunction SomeComponent() {\n  // closest route URL\n  let action = useFormAction();\n\n  // closest route URL + \"destroy\"\n  let destroyAction = useFormAction(\"destroy\");\n}\n```\n\n## Signature\n\n```tsx\nfunction useFormAction(\n  action?: string,\n  {\n    relative,\n  }: {\n    relative?: RelativeRoutingType;\n  } = ,\n): string {}\n```\n\n## Params\n\n### action\n\nThe action to append to the closest route URL. Defaults to the closest route URL.\n\n### options.relative\n\nThe relative routing type to use when resolving the action. Defaults to `\"route\"`.\n\n## Returns\n\nThe resolved action URL.\n\n"
  },
  {
    "path": "docs/api/hooks/useHref.md",
    "content": "---\ntitle: useHref\n---\n\n# useHref\n\n<!--\n⚠️ ⚠️ IMPORTANT ⚠️ ⚠️ \n\nThank you for helping improve our documentation!\n\nThis file is auto-generated from the JSDoc comments in the source\ncode, so please edit the JSDoc comments in the file below and this\nfile will be re-generated once those changes are merged.\n\nhttps://github.com/remix-run/react-router/blob/main/packages/react-router/lib/hooks.tsx\n-->\n\n[MODES: framework, data, declarative]\n\n## Summary\n\n[Reference Documentation ↗](https://api.reactrouter.com/v7/functions/react-router.useHref.html)\n\nResolves a URL against the current [`Location`](https://api.reactrouter.com/v7/interfaces/react-router.Location.html).\n\n```tsx\nimport { useHref } from \"react-router\";\n\nfunction SomeComponent() {\n  let href = useHref(\"some/where\");\n  // \"/resolved/some/where\"\n}\n```\n\n## Signature\n\n```tsx\nfunction useHref(\n  to: To,\n  {\n    relative,\n  }: {\n    relative?: RelativeRoutingType;\n  } = ,\n): string {}\n```\n\n## Params\n\n### to\n\nThe path to resolve\n\n### options.relative\n\nDefaults to `\"route\"` so routing is relative to the route tree.\nSet to `\"path\"` to make relative routing operate against path segments.\n\n## Returns\n\nThe resolved href string\n\n"
  },
  {
    "path": "docs/api/hooks/useInRouterContext.md",
    "content": "---\ntitle: useInRouterContext\n---\n\n# useInRouterContext\n\n<!--\n⚠️ ⚠️ IMPORTANT ⚠️ ⚠️ \n\nThank you for helping improve our documentation!\n\nThis file is auto-generated from the JSDoc comments in the source\ncode, so please edit the JSDoc comments in the file below and this\nfile will be re-generated once those changes are merged.\n\nhttps://github.com/remix-run/react-router/blob/main/packages/react-router/lib/hooks.tsx\n-->\n\n[MODES: framework, data]\n\n## Summary\n\n[Reference Documentation ↗](https://api.reactrouter.com/v7/functions/react-router.useInRouterContext.html)\n\nReturns `true` if this component is a descendant of a [`Router`](../declarative-routers/Router), useful\nto ensure a component is used within a [`Router`](../declarative-routers/Router).\n\n## Signature\n\n```tsx\nfunction useInRouterContext(): boolean\n```\n\n## Returns\n\nWhether the component is within a [`Router`](../declarative-routers/Router) context\n\n"
  },
  {
    "path": "docs/api/hooks/useLinkClickHandler.md",
    "content": "---\ntitle: useLinkClickHandler\n---\n\n# useLinkClickHandler\n\n<!--\n⚠️ ⚠️ IMPORTANT ⚠️ ⚠️ \n\nThank you for helping improve our documentation!\n\nThis file is auto-generated from the JSDoc comments in the source\ncode, so please edit the JSDoc comments in the file below and this\nfile will be re-generated once those changes are merged.\n\nhttps://github.com/remix-run/react-router/blob/main/packages/react-router/lib/dom/lib.tsx\n-->\n\n[MODES: framework, data, declarative]\n\n## Summary\n\n[Reference Documentation ↗](https://api.reactrouter.com/v7/functions/react-router.useLinkClickHandler.html)\n\nHandles the click behavior for router [`<Link>`](../components/Link) components.This\nis useful if you need to create custom [`<Link>`](../components/Link) components with\nthe same click behavior we use in our exported [`<Link>`](../components/Link).\n\n## Signature\n\n```tsx\nfunction useLinkClickHandler<E extends Element = HTMLAnchorElement>(\n  to: To,\n  {\n    target,\n    replace: replaceProp,\n    unstable_mask,\n    state,\n    preventScrollReset,\n    relative,\n    viewTransition,\n    unstable_defaultShouldRevalidate,\n    unstable_useTransitions,\n  }: {\n    target?: React.HTMLAttributeAnchorTarget;\n    replace?: boolean;\n    unstable_mask?: To;\n    state?: any;\n    preventScrollReset?: boolean;\n    relative?: RelativeRoutingType;\n    viewTransition?: boolean;\n    unstable_defaultShouldRevalidate?: boolean;\n    unstable_useTransitions?: boolean;\n  } = ,\n): (event: React.MouseEvent<E, MouseEvent>) => void {}\n```\n\n## Params\n\n### to\n\nThe URL to navigate to, can be a string or a partial [`Path`](https://api.reactrouter.com/v7/interfaces/react-router.Path.html).\n\n### options.preventScrollReset\n\nWhether to prevent the scroll position from being reset to the top of the viewport on completion of the navigation when\nusing the [`ScrollRestoration`](../components/ScrollRestoration) component. Defaults to `false`.\n\n### options.relative\n\nThe [relative routing type](https://api.reactrouter.com/v7/types/react-router.RelativeRoutingType.html) to use for the link. Defaults to `\"route\"`.\n\n### options.replace\n\nWhether to replace the current [`History`](https://developer.mozilla.org/en-US/docs/Web/API/History) entry instead of pushing a new one. Defaults to `false`.\n\n### options.state\n\nThe state to add to the [`History`](https://developer.mozilla.org/en-US/docs/Web/API/History) entry for this navigation. Defaults to `undefined`.\n\n### options.target\n\nThe target attribute for the link. Defaults to `undefined`.\n\n### options.viewTransition\n\nEnables a [View Transition](https://developer.mozilla.org/en-US/docs/Web/API/View_Transitions_API) for this navigation. To apply specific styles during the transition, see\n[`useViewTransitionState`](../hooks/useViewTransitionState). Defaults to `false`.\n\n### options.unstable_defaultShouldRevalidate\n\nSpecify the default revalidation behavior for the navigation. Defaults to `true`.\n\n### options.unstable_mask\n\nMasked location to display in the browser instead of the router location. Defaults to `undefined`.\n\n### options.unstable_useTransitions\n\nWraps the navigation in [`React.startTransition`](https://react.dev/reference/react/startTransition)\nfor concurrent rendering. Defaults to `false`.\n\n## Returns\n\nA click handler function that can be used in a custom [`Link`](../components/Link) component.\n\n"
  },
  {
    "path": "docs/api/hooks/useLoaderData.md",
    "content": "---\ntitle: useLoaderData\n---\n\n# useLoaderData\n\n<!--\n⚠️ ⚠️ IMPORTANT ⚠️ ⚠️ \n\nThank you for helping improve our documentation!\n\nThis file is auto-generated from the JSDoc comments in the source\ncode, so please edit the JSDoc comments in the file below and this\nfile will be re-generated once those changes are merged.\n\nhttps://github.com/remix-run/react-router/blob/main/packages/react-router/lib/hooks.tsx\n-->\n\n[MODES: framework, data]\n\n## Summary\n\n[Reference Documentation ↗](https://api.reactrouter.com/v7/functions/react-router.useLoaderData.html)\n\nReturns the data from the closest route\n[`loader`](../../start/framework/route-module#loader) or\n[`clientLoader`](../../start/framework/route-module#clientloader).\n\n```tsx\nimport { useLoaderData } from \"react-router\";\n\nexport async function loader() {\n  return await fakeDb.invoices.findAll();\n}\n\nexport default function Invoices() {\n  let invoices = useLoaderData<typeof loader>();\n  // ...\n}\n```\n\n## Signature\n\n```tsx\nfunction useLoaderData<T = any>(): SerializeFrom<T>\n```\n\n## Returns\n\nThe data returned from the route's [`loader`](../../start/framework/route-module#loader) or [`clientLoader`](../../start/framework/route-module#clientloader) function\n\n"
  },
  {
    "path": "docs/api/hooks/useLocation.md",
    "content": "---\ntitle: useLocation\n---\n\n# useLocation\n\n<!--\n⚠️ ⚠️ IMPORTANT ⚠️ ⚠️ \n\nThank you for helping improve our documentation!\n\nThis file is auto-generated from the JSDoc comments in the source\ncode, so please edit the JSDoc comments in the file below and this\nfile will be re-generated once those changes are merged.\n\nhttps://github.com/remix-run/react-router/blob/main/packages/react-router/lib/hooks.tsx\n-->\n\n[MODES: framework, data, declarative]\n\n## Summary\n\n[Reference Documentation ↗](https://api.reactrouter.com/v7/functions/react-router.useLocation.html)\n\nReturns the current [`Location`](https://api.reactrouter.com/v7/interfaces/react-router.Location.html). This can be useful if you'd like to\nperform some side effect whenever it changes.\n\n```tsx\nimport * as React from 'react'\nimport { useLocation } from 'react-router'\n\nfunction SomeComponent() {\n  let location = useLocation()\n\n  React.useEffect(() => {\n    // Google Analytics\n    ga('send', 'pageview')\n  }, [location]);\n\n  return (\n    // ...\n  );\n}\n```\n\n## Signature\n\n```tsx\nfunction useLocation(): Location\n```\n\n## Returns\n\nThe current [`Location`](https://api.reactrouter.com/v7/interfaces/react-router.Location.html) object\n\n"
  },
  {
    "path": "docs/api/hooks/useMatch.md",
    "content": "---\ntitle: useMatch\n---\n\n# useMatch\n\n<!--\n⚠️ ⚠️ IMPORTANT ⚠️ ⚠️ \n\nThank you for helping improve our documentation!\n\nThis file is auto-generated from the JSDoc comments in the source\ncode, so please edit the JSDoc comments in the file below and this\nfile will be re-generated once those changes are merged.\n\nhttps://github.com/remix-run/react-router/blob/main/packages/react-router/lib/hooks.tsx\n-->\n\n[MODES: framework, data, declarative]\n\n## Summary\n\n[Reference Documentation ↗](https://api.reactrouter.com/v7/functions/react-router.useMatch.html)\n\nReturns a [`PathMatch`](https://api.reactrouter.com/v7/interfaces/react-router.PathMatch.html) object if the given pattern matches the current URL.\nThis is useful for components that need to know \"active\" state, e.g.\n[`<NavLink>`](../components/NavLink).\n\n## Signature\n\n```tsx\nfunction useMatch<ParamKey extends ParamParseKey<Path>, Path extends string>(\n  pattern: PathPattern<Path> | Path,\n): PathMatch<ParamKey> | null\n```\n\n## Params\n\n### pattern\n\nThe pattern to match against the current [`Location`](https://api.reactrouter.com/v7/interfaces/react-router.Location.html)\n\n## Returns\n\nThe path match object if the pattern matches, `null` otherwise\n\n"
  },
  {
    "path": "docs/api/hooks/useMatches.md",
    "content": "---\ntitle: useMatches\n---\n\n# useMatches\n\n<!--\n⚠️ ⚠️ IMPORTANT ⚠️ ⚠️ \n\nThank you for helping improve our documentation!\n\nThis file is auto-generated from the JSDoc comments in the source\ncode, so please edit the JSDoc comments in the file below and this\nfile will be re-generated once those changes are merged.\n\nhttps://github.com/remix-run/react-router/blob/main/packages/react-router/lib/hooks.tsx\n-->\n\n[MODES: framework, data]\n\n## Summary\n\n[Reference Documentation ↗](https://api.reactrouter.com/v7/functions/react-router.useMatches.html)\n\nReturns the active route matches, useful for accessing `loaderData` for\nparent/child routes or the route [`handle`](../../start/framework/route-module#handle)\nproperty\n\n## Signature\n\n```tsx\nfunction useMatches(): UIMatch[]\n```\n\n## Returns\n\nAn array of [UI matches](https://api.reactrouter.com/v7/interfaces/react-router.UIMatch.html) for the current route hierarchy\n\n"
  },
  {
    "path": "docs/api/hooks/useNavigate.md",
    "content": "---\ntitle: useNavigate\n---\n\n# useNavigate\n\n<!--\n⚠️ ⚠️ IMPORTANT ⚠️ ⚠️ \n\nThank you for helping improve our documentation!\n\nThis file is auto-generated from the JSDoc comments in the source\ncode, so please edit the JSDoc comments in the file below and this\nfile will be re-generated once those changes are merged.\n\nhttps://github.com/remix-run/react-router/blob/main/packages/react-router/lib/hooks.tsx\n-->\n\n[MODES: framework, data, declarative]\n\n## Summary\n\n[Reference Documentation ↗](https://api.reactrouter.com/v7/functions/react-router.useNavigate.html)\n\nReturns a function that lets you navigate programmatically in the browser in\nresponse to user interactions or effects.\n\nIt's often better to use [`redirect`](../utils/redirect) in [`action`](../../start/framework/route-module#action)/[`loader`](../../start/framework/route-module#loader)\nfunctions than this hook.\n\nThe returned function signature is `navigate(to, options?)`/`navigate(delta)` where:\n\n* `to` can be a string path, a [`To`](https://api.reactrouter.com/v7/types/react-router.To.html) object, or a number (delta)\n* `options` contains options for modifying the navigation\n  * These options work in all modes (Framework, Data, and Declarative):\n    * `relative`: `\"route\"` or `\"path\"` to control relative routing logic\n    * `replace`: Replace the current entry in the [`History`](https://developer.mozilla.org/en-US/docs/Web/API/History) stack\n    * `state`: Optional [`history.state`](https://developer.mozilla.org/en-US/docs/Web/API/History/state) to include with the new [`Location`](https://api.reactrouter.com/v7/interfaces/react-router.Location.html)\n  * These options only work in Framework and Data modes:\n    * `flushSync`: Wrap the DOM updates in [`ReactDom.flushSync`](https://react.dev/reference/react-dom/flushSync)\n    * `preventScrollReset`: Do not scroll back to the top of the page after navigation\n    * `viewTransition`: Enable [`document.startViewTransition`](https://developer.mozilla.org/en-US/docs/Web/API/Document/startViewTransition) for this navigation\n\n```tsx\nimport { useNavigate } from \"react-router\";\n\nfunction SomeComponent() {\n  let navigate = useNavigate();\n  return (\n    <button onClick={() => navigate(-1)}>\n      Go Back\n    </button>\n  );\n}\n```\n\n## Signature\n\n```tsx\nfunction useNavigate(): NavigateFunction\n```\n\n## Returns\n\nA navigate function for programmatic navigation\n\n## Examples\n\n### Navigate to another path\n\n```tsx\nnavigate(\"/some/route\");\nnavigate(\"/some/route?search=param\");\n```\n\n### Navigate with a [`To`](https://api.reactrouter.com/v7/types/react-router.To.html) object\n\nAll properties are optional.\n\n```tsx\nnavigate({\n  pathname: \"/some/route\",\n  search: \"?search=param\",\n  hash: \"#hash\",\n  state: { some: \"state\" },\n});\n```\n\nIf you use `state`, that will be available on the [`Location`](https://api.reactrouter.com/v7/interfaces/react-router.Location.html) object on\nthe next page. Access it with `useLocation().state` (see [`useLocation`](../hooks/useLocation)).\n\n### Navigate back or forward in the history stack\n\n```tsx\n// back\n// often used to close modals\nnavigate(-1);\n\n// forward\n// often used in a multistep wizard workflows\nnavigate(1);\n```\n\nBe cautious with `navigate(number)`. If your application can load up to a\nroute that has a button that tries to navigate forward/back, there may not be\na `[`History`](https://developer.mozilla.org/en-US/docs/Web/API/History)\nentry to go back or forward to, or it can go somewhere you don't expect\n(like a different domain).\n\nOnly use this if you're sure they will have an entry in the [`History`](https://developer.mozilla.org/en-US/docs/Web/API/History)\nstack to navigate to.\n\n### Replace the current entry in the history stack\n\nThis will remove the current entry in the [`History`](https://developer.mozilla.org/en-US/docs/Web/API/History)\nstack, replacing it with a new one, similar to a server side redirect.\n\n```tsx\nnavigate(\"/some/route\", { replace: true });\n```\n\n### Prevent Scroll Reset\n\n[MODES: framework, data]\n\n<br/>\n<br/>\n\nTo prevent [`<ScrollRestoration>`](../components/ScrollRestoration) from resetting\nthe scroll position, use the `preventScrollReset` option.\n\n```tsx\nnavigate(\"?some-tab=1\", { preventScrollReset: true });\n```\n\nFor example, if you have a tab interface connected to search params in the\nmiddle of a page, and you don't want it to scroll to the top when a tab is\nclicked.\n\n### Return Type Augmentation\n\nInternally, `useNavigate` uses a separate implementation when you are in\nDeclarative mode versus Data/Framework mode - the primary difference being\nthat the latter is able to return a stable reference that does not change\nidentity across navigations. The implementation in Data/Framework mode also\nreturns a [`Promise`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise)\nthat resolves when the navigation is completed. This means the return type of\n`useNavigate` is `void | Promise<void>`. This is accurate, but can lead to\nsome red squigglies based on the union in the return value:\n\n- If you're using `typescript-eslint`, you may see errors from\n  [`@typescript-eslint/no-floating-promises`](https://typescript-eslint.io/rules/no-floating-promises)\n- In Framework/Data mode, `React.use(navigate())` will show a false-positive\n  `Argument of type 'void | Promise<void>' is not assignable to parameter of\n  type 'Usable<void>'` error\n\nThe easiest way to work around these issues is to augment the type based on the\nrouter you're using:\n\n```ts\n// If using <BrowserRouter>\ndeclare module \"react-router\" {\n  interface NavigateFunction {\n    (to: To, options?: NavigateOptions): void;\n    (delta: number): void;\n  }\n}\n\n// If using <RouterProvider> or Framework mode\ndeclare module \"react-router\" {\n  interface NavigateFunction {\n    (to: To, options?: NavigateOptions): Promise<void>;\n    (delta: number): Promise<void>;\n  }\n}\n```\n\n"
  },
  {
    "path": "docs/api/hooks/useNavigation.md",
    "content": "---\ntitle: useNavigation\n---\n\n# useNavigation\n\n<!--\n⚠️ ⚠️ IMPORTANT ⚠️ ⚠️ \n\nThank you for helping improve our documentation!\n\nThis file is auto-generated from the JSDoc comments in the source\ncode, so please edit the JSDoc comments in the file below and this\nfile will be re-generated once those changes are merged.\n\nhttps://github.com/remix-run/react-router/blob/main/packages/react-router/lib/hooks.tsx\n-->\n\n[MODES: framework, data]\n\n## Summary\n\n[Reference Documentation ↗](https://api.reactrouter.com/v7/functions/react-router.useNavigation.html)\n\nReturns the current [`Navigation`](https://api.reactrouter.com/v7/types/react-router.Navigation.html), defaulting to an \"idle\" navigation\nwhen no navigation is in progress. You can use this to render pending UI\n(like a global spinner) or read [`FormData`](https://developer.mozilla.org/en-US/docs/Web/API/FormData)\nfrom a form navigation.\n\n```tsx\nimport { useNavigation } from \"react-router\";\n\nfunction SomeComponent() {\n  let navigation = useNavigation();\n  navigation.state;\n  navigation.formData;\n  // etc.\n}\n```\n\n## Signature\n\n```tsx\nfunction useNavigation(): Navigation\n```\n\n## Returns\n\nThe current [`Navigation`](https://api.reactrouter.com/v7/types/react-router.Navigation.html) object\n\n"
  },
  {
    "path": "docs/api/hooks/useNavigationType.md",
    "content": "---\ntitle: useNavigationType\n---\n\n# useNavigationType\n\n<!--\n⚠️ ⚠️ IMPORTANT ⚠️ ⚠️ \n\nThank you for helping improve our documentation!\n\nThis file is auto-generated from the JSDoc comments in the source\ncode, so please edit the JSDoc comments in the file below and this\nfile will be re-generated once those changes are merged.\n\nhttps://github.com/remix-run/react-router/blob/main/packages/react-router/lib/hooks.tsx\n-->\n\n[MODES: framework, data, declarative]\n\n## Summary\n\n[Reference Documentation ↗](https://api.reactrouter.com/v7/functions/react-router.useNavigationType.html)\n\nReturns the current [`Navigation`](https://api.reactrouter.com/v7/types/react-router.Navigation.html) action which describes how the router\ncame to the current [`Location`](https://api.reactrouter.com/v7/interfaces/react-router.Location.html), either by a pop, push, or replace on\nthe [`History`](https://developer.mozilla.org/en-US/docs/Web/API/History) stack.\n\n## Signature\n\n```tsx\nfunction useNavigationType(): NavigationType\n```\n\n## Returns\n\nThe current [`NavigationType`](https://api.reactrouter.com/v7/enums/react-router.NavigationType.html) (`\"POP\"`, `\"PUSH\"`, or `\"REPLACE\"`)\n\n"
  },
  {
    "path": "docs/api/hooks/useOutlet.md",
    "content": "---\ntitle: useOutlet\n---\n\n# useOutlet\n\n<!--\n⚠️ ⚠️ IMPORTANT ⚠️ ⚠️ \n\nThank you for helping improve our documentation!\n\nThis file is auto-generated from the JSDoc comments in the source\ncode, so please edit the JSDoc comments in the file below and this\nfile will be re-generated once those changes are merged.\n\nhttps://github.com/remix-run/react-router/blob/main/packages/react-router/lib/hooks.tsx\n-->\n\n[MODES: framework, data, declarative]\n\n## Summary\n\n[Reference Documentation ↗](https://api.reactrouter.com/v7/functions/react-router.useOutlet.html)\n\nReturns the element for the child route at this level of the route\nhierarchy. Used internally by [`<Outlet>`](../components/Outlet) to render child\nroutes.\n\n## Signature\n\n```tsx\nfunction useOutlet(context?: unknown): React.ReactElement | null\n```\n\n## Params\n\n### context\n\nThe context to pass to the outlet\n\n## Returns\n\nThe child route element or `null` if no child routes match\n\n"
  },
  {
    "path": "docs/api/hooks/useOutletContext.md",
    "content": "---\ntitle: useOutletContext\n---\n\n# useOutletContext\n\n<!--\n⚠️ ⚠️ IMPORTANT ⚠️ ⚠️ \n\nThank you for helping improve our documentation!\n\nThis file is auto-generated from the JSDoc comments in the source\ncode, so please edit the JSDoc comments in the file below and this\nfile will be re-generated once those changes are merged.\n\nhttps://github.com/remix-run/react-router/blob/main/packages/react-router/lib/hooks.tsx\n-->\n\n[MODES: framework, data, declarative]\n\n## Summary\n\n[Reference Documentation ↗](https://api.reactrouter.com/v7/functions/react-router.useOutletContext.html)\n\nReturns the parent route [`<Outlet context>`](../components/Outlet).\n\nOften parent routes manage state or other values you want shared with child\nroutes. You can create your own [context provider](https://react.dev/learn/passing-data-deeply-with-context)\nif you like, but this is such a common situation that it's built-into\n[`<Outlet>`](../components/Outlet).\n\n```tsx\n// Parent route\nfunction Parent() {\n  const [count, setCount] = React.useState(0);\n  return <Outlet context={[count, setCount]} />;\n}\n```\n\n```tsx\n// Child route\nimport { useOutletContext } from \"react-router\";\n\nfunction Child() {\n  const [count, setCount] = useOutletContext();\n  const increment = () => setCount((c) => c + 1);\n  return <button onClick={increment}>{count}</button>;\n}\n```\n\nIf you're using TypeScript, we recommend the parent component provide a\ncustom hook for accessing the context value. This makes it easier for\nconsumers to get nice typings, control consumers, and know who's consuming\nthe context value.\n\nHere's a more realistic example:\n\n```tsx filename=src/routes/dashboard.tsx lines=[14,20]\nimport { useState } from \"react\";\nimport { Outlet, useOutletContext } from \"react-router\";\n\nimport type { User } from \"./types\";\n\ntype ContextType = { user: User | null };\n\nexport default function Dashboard() {\n  const [user, setUser] = useState<User | null>(null);\n\n  return (\n    <div>\n      <h1>Dashboard</h1>\n      <Outlet context={{ user } satisfies ContextType} />\n    </div>\n  );\n}\n\nexport function useUser() {\n  return useOutletContext<ContextType>();\n}\n```\n\n```tsx filename=src/routes/dashboard/messages.tsx lines=[1,4]\nimport { useUser } from \"../dashboard\";\n\nexport default function DashboardMessages() {\n  const { user } = useUser();\n  return (\n    <div>\n      <h2>Messages</h2>\n      <p>Hello, {user.name}!</p>\n    </div>\n  );\n}\n```\n\n## Signature\n\n```tsx\nfunction useOutletContext<Context = unknown>(): Context\n```\n\n## Returns\n\nThe context value passed to the parent [`Outlet`](../components/Outlet) component\n\n"
  },
  {
    "path": "docs/api/hooks/useParams.md",
    "content": "---\ntitle: useParams\n---\n\n# useParams\n\n<!--\n⚠️ ⚠️ IMPORTANT ⚠️ ⚠️ \n\nThank you for helping improve our documentation!\n\nThis file is auto-generated from the JSDoc comments in the source\ncode, so please edit the JSDoc comments in the file below and this\nfile will be re-generated once those changes are merged.\n\nhttps://github.com/remix-run/react-router/blob/main/packages/react-router/lib/hooks.tsx\n-->\n\n[MODES: framework, data, declarative]\n\n## Summary\n\n[Reference Documentation ↗](https://api.reactrouter.com/v7/functions/react-router.useParams.html)\n\nReturns an object of key/value-pairs of the dynamic params from the current\nURL that were matched by the routes. Child routes inherit all params from\ntheir parent routes.\n\nAssuming a route pattern like `/posts/:postId` is matched by `/posts/123`\nthen `params.postId` will be `\"123\"`.\n\n```tsx\nimport { useParams } from \"react-router\";\n\nfunction SomeComponent() {\n  let params = useParams();\n  params.postId;\n}\n```\n\n## Signature\n\n```tsx\nfunction useParams<\n  ParamsOrKey extends string | Record<string, string | undefined> = string,\n>(): Readonly<\n  [ParamsOrKey] extends [string] ? Params<ParamsOrKey> : Partial<ParamsOrKey>\n>\n```\n\n## Returns\n\nAn object containing the dynamic route parameters\n\n## Examples\n\n### Basic Usage\n\n```tsx\nimport { useParams } from \"react-router\";\n\n// given a route like:\n<Route path=\"/posts/:postId\" element={<Post />} />;\n\n// or a data route like:\ncreateBrowserRouter([\n  {\n    path: \"/posts/:postId\",\n    component: Post,\n  },\n]);\n\n// or in routes.ts\nroute(\"/posts/:postId\", \"routes/post.tsx\");\n```\n\nAccess the params in a component:\n\n```tsx\nimport { useParams } from \"react-router\";\n\nexport default function Post() {\n  let params = useParams();\n  return <h1>Post: {params.postId}</h1>;\n}\n```\n\n### Multiple Params\n\nPatterns can have multiple params:\n\n```tsx\n\"/posts/:postId/comments/:commentId\";\n```\n\nAll will be available in the params object:\n\n```tsx\nimport { useParams } from \"react-router\";\n\nexport default function Post() {\n  let params = useParams();\n  return (\n    <h1>\n      Post: {params.postId}, Comment: {params.commentId}\n    </h1>\n  );\n}\n```\n\n### Catchall Params\n\nCatchall params are defined with `*`:\n\n```tsx\n\"/files/*\";\n```\n\nThe matched value will be available in the params object as follows:\n\n```tsx\nimport { useParams } from \"react-router\";\n\nexport default function File() {\n  let params = useParams();\n  let catchall = params[\"*\"];\n  // ...\n}\n```\n\nYou can destructure the catchall param:\n\n```tsx\nexport default function File() {\n  let { \"*\": catchall } = useParams();\n  console.log(catchall);\n}\n```\n\n"
  },
  {
    "path": "docs/api/hooks/usePrompt.md",
    "content": "---\ntitle: usePrompt\nunstable: true\n---\n\n# unstable_usePrompt\n\n<!--\n⚠️ ⚠️ IMPORTANT ⚠️ ⚠️ \n\nThank you for helping improve our documentation!\n\nThis file is auto-generated from the JSDoc comments in the source\ncode, so please edit the JSDoc comments in the file below and this\nfile will be re-generated once those changes are merged.\n\nhttps://github.com/remix-run/react-router/blob/main/packages/react-router/lib/dom/lib.tsx\n-->\n\n[MODES: framework, data]\n\n<br />\n<br />\n\n<docs-warning>This API is experimental and subject to breaking changes in \nminor/patch releases. Please use with caution and pay **very** close attention \nto release notes for relevant changes.</docs-warning>\n\n## Summary\n\n[Reference Documentation ↗](https://api.reactrouter.com/v7/functions/react-router.unstable_usePrompt.html)\n\nWrapper around [`useBlocker`](../hooks/useBlocker) to show a [`window.confirm`](https://developer.mozilla.org/en-US/docs/Web/API/Window/confirm)\nprompt to users instead of building a custom UI with [`useBlocker`](../hooks/useBlocker).\n\nThe `unstable_` flag will not be removed because this technique has a lot of\nrough edges and behaves very differently (and incorrectly sometimes) across\nbrowsers if users click addition back/forward navigations while the\nconfirmation is open. Use at your own risk.\n\n```tsx\nfunction ImportantForm() {\n  let [value, setValue] = React.useState(\"\");\n\n  // Block navigating elsewhere when data has been entered into the input\n  unstable_usePrompt({\n    message: \"Are you sure?\",\n    when: ({ currentLocation, nextLocation }) =>\n      value !== \"\" &&\n      currentLocation.pathname !== nextLocation.pathname,\n  });\n\n  return (\n    <Form method=\"post\">\n      <label>\n        Enter some important data:\n        <input\n          name=\"data\"\n          value={value}\n          onChange={(e) => setValue(e.target.value)}\n        />\n      </label>\n      <button type=\"submit\">Save</button>\n    </Form>\n  );\n}\n```\n\n## Signature\n\n```tsx\nfunction usePrompt({\n  when,\n  message,\n}: {\n  when: boolean | BlockerFunction;\n  message: string;\n}): void\n```\n\n## Params\n\n### options.message\n\nThe message to show in the confirmation dialog.\n\n### options.when\n\nA boolean or a function that returns a boolean indicating whether to block the navigation. If a function is provided, it will receive an\nobject with `currentLocation` and `nextLocation` properties.\n\n## Returns\n\nNo return value.\n\n"
  },
  {
    "path": "docs/api/hooks/useResolvedPath.md",
    "content": "---\ntitle: useResolvedPath\n---\n\n# useResolvedPath\n\n<!--\n⚠️ ⚠️ IMPORTANT ⚠️ ⚠️ \n\nThank you for helping improve our documentation!\n\nThis file is auto-generated from the JSDoc comments in the source\ncode, so please edit the JSDoc comments in the file below and this\nfile will be re-generated once those changes are merged.\n\nhttps://github.com/remix-run/react-router/blob/main/packages/react-router/lib/hooks.tsx\n-->\n\n[MODES: framework, data, declarative]\n\n## Summary\n\n[Reference Documentation ↗](https://api.reactrouter.com/v7/functions/react-router.useResolvedPath.html)\n\nResolves the pathname of the given `to` value against the current\n[`Location`](https://api.reactrouter.com/v7/interfaces/react-router.Location.html). Similar to [`useHref`](../hooks/useHref), but returns a\n[`Path`](https://api.reactrouter.com/v7/interfaces/react-router.Path.html) instead of a string.\n\n```tsx\nimport { useResolvedPath } from \"react-router\";\n\nfunction SomeComponent() {\n  // if the user is at /dashboard/profile\n  let path = useResolvedPath(\"../accounts\");\n  path.pathname; // \"/dashboard/accounts\"\n  path.search; // \"\"\n  path.hash; // \"\"\n}\n```\n\n## Signature\n\n```tsx\nfunction useResolvedPath(\n  to: To,\n  {\n    relative,\n  }: {\n    relative?: RelativeRoutingType;\n  } = ,\n): Path {}\n```\n\n## Params\n\n### to\n\nThe path to resolve\n\n### options.relative\n\nDefaults to `\"route\"` so routing is relative to the route tree.                         Set to `\"path\"` to make relative routing operate against path segments.\n\n## Returns\n\nThe resolved [`Path`](https://api.reactrouter.com/v7/interfaces/react-router.Path.html) object with `pathname`, `search`, and `hash`\n\n"
  },
  {
    "path": "docs/api/hooks/useRevalidator.md",
    "content": "---\ntitle: useRevalidator\n---\n\n# useRevalidator\n\n<!--\n⚠️ ⚠️ IMPORTANT ⚠️ ⚠️ \n\nThank you for helping improve our documentation!\n\nThis file is auto-generated from the JSDoc comments in the source\ncode, so please edit the JSDoc comments in the file below and this\nfile will be re-generated once those changes are merged.\n\nhttps://github.com/remix-run/react-router/blob/main/packages/react-router/lib/hooks.tsx\n-->\n\n[MODES: framework, data]\n\n## Summary\n\n[Reference Documentation ↗](https://api.reactrouter.com/v7/functions/react-router.useRevalidator.html)\n\nRevalidate the data on the page for reasons outside of normal data mutations\nlike [`Window` focus](https://developer.mozilla.org/en-US/docs/Web/API/Window/focus_event)\nor polling on an interval.\n\nNote that page data is already revalidated automatically after actions.\nIf you find yourself using this for normal CRUD operations on your data in\nresponse to user interactions, you're probably not taking advantage of the\nother APIs like [`useFetcher`](../hooks/useFetcher), [`Form`](../components/Form), [`useSubmit`](../hooks/useSubmit) that do\nthis automatically.\n\n```tsx\nimport { useRevalidator } from \"react-router\";\n\nfunction WindowFocusRevalidator() {\n  const revalidator = useRevalidator();\n\n  useFakeWindowFocus(() => {\n    revalidator.revalidate();\n  });\n\n  return (\n    <div hidden={revalidator.state === \"idle\"}>\n      Revalidating...\n    </div>\n  );\n}\n```\n\n## Signature\n\n```tsx\nfunction useRevalidator(): {\n  revalidate: () => Promise<void>;\n  state: DataRouter[\"state\"][\"revalidation\"];\n}\n```\n\n## Returns\n\nAn object with a `revalidate` function and the current revalidation\n`state`\n\n"
  },
  {
    "path": "docs/api/hooks/useRouteError.md",
    "content": "---\ntitle: useRouteError\n---\n\n# useRouteError\n\n<!--\n⚠️ ⚠️ IMPORTANT ⚠️ ⚠️ \n\nThank you for helping improve our documentation!\n\nThis file is auto-generated from the JSDoc comments in the source\ncode, so please edit the JSDoc comments in the file below and this\nfile will be re-generated once those changes are merged.\n\nhttps://github.com/remix-run/react-router/blob/main/packages/react-router/lib/hooks.tsx\n-->\n\n[MODES: framework, data]\n\n## Summary\n\n[Reference Documentation ↗](https://api.reactrouter.com/v7/functions/react-router.useRouteError.html)\n\nAccesses the error thrown during an\n[`action`](../../start/framework/route-module#action),\n[`loader`](../../start/framework/route-module#loader),\nor component render to be used in a route module\n[`ErrorBoundary`](../../start/framework/route-module#errorboundary).\n\n```tsx\nexport function ErrorBoundary() {\n  const error = useRouteError();\n  return <div>{error.message}</div>;\n}\n```\n\n## Signature\n\n```tsx\nfunction useRouteError(): unknown\n```\n\n## Returns\n\nThe error that was thrown during route [loading](../../start/framework/route-module#loader),\n[`action`](../../start/framework/route-module#action) execution, or rendering\n\n"
  },
  {
    "path": "docs/api/hooks/useRouteLoaderData.md",
    "content": "---\ntitle: useRouteLoaderData\n---\n\n# useRouteLoaderData\n\n<!--\n⚠️ ⚠️ IMPORTANT ⚠️ ⚠️ \n\nThank you for helping improve our documentation!\n\nThis file is auto-generated from the JSDoc comments in the source\ncode, so please edit the JSDoc comments in the file below and this\nfile will be re-generated once those changes are merged.\n\nhttps://github.com/remix-run/react-router/blob/main/packages/react-router/lib/hooks.tsx\n-->\n\n[MODES: framework, data]\n\n## Summary\n\n[Reference Documentation ↗](https://api.reactrouter.com/v7/functions/react-router.useRouteLoaderData.html)\n\nReturns the [`loader`](../../start/framework/route-module#loader) data for a\ngiven route by route ID.\n\nRoute IDs are created automatically. They are simply the path of the route file\nrelative to the app folder without the extension.\n\n| Route Filename               | Route ID               |\n| ---------------------------- | ---------------------- |\n| `app/root.tsx`               | `\"root\"`               |\n| `app/routes/teams.tsx`       | `\"routes/teams\"`       |\n| `app/whatever/teams.$id.tsx` | `\"whatever/teams.$id\"` |\n\n```tsx\nimport { useRouteLoaderData } from \"react-router\";\n\nfunction SomeComponent() {\n  const { user } = useRouteLoaderData(\"root\");\n}\n\n// You can also specify your own route ID's manually in your routes.ts file:\nroute(\"/\", \"containers/app.tsx\", { id: \"app\" })\nuseRouteLoaderData(\"app\");\n```\n\n## Signature\n\n```tsx\nfunction useRouteLoaderData<T = any>(\n  routeId: string,\n): SerializeFrom<T> | undefined\n```\n\n## Params\n\n### routeId\n\nThe ID of the route to return loader data from\n\n## Returns\n\nThe data returned from the specified route's [`loader`](../../start/framework/route-module#loader)\nfunction, or `undefined` if not found\n\n"
  },
  {
    "path": "docs/api/hooks/useRoutes.md",
    "content": "---\ntitle: useRoutes\n---\n\n# useRoutes\n\n<!--\n⚠️ ⚠️ IMPORTANT ⚠️ ⚠️ \n\nThank you for helping improve our documentation!\n\nThis file is auto-generated from the JSDoc comments in the source\ncode, so please edit the JSDoc comments in the file below and this\nfile will be re-generated once those changes are merged.\n\nhttps://github.com/remix-run/react-router/blob/main/packages/react-router/lib/hooks.tsx\n-->\n\n[MODES: framework, data, declarative]\n\n## Summary\n\n[Reference Documentation ↗](https://api.reactrouter.com/v7/functions/react-router.useRoutes.html)\n\nHook version of [`<Routes>`](../components/Routes) that uses objects instead of\ncomponents. These objects have the same properties as the component props.\nThe return value of `useRoutes` is either a valid React element you can use\nto render the route tree, or `null` if nothing matched.\n\n```tsx\nimport { useRoutes } from \"react-router\";\n\nfunction App() {\n  let element = useRoutes([\n    {\n      path: \"/\",\n      element: <Dashboard />,\n      children: [\n        {\n          path: \"messages\",\n          element: <DashboardMessages />,\n        },\n        { path: \"tasks\", element: <DashboardTasks /> },\n      ],\n    },\n    { path: \"team\", element: <AboutPage /> },\n  ]);\n\n  return element;\n}\n```\n\n## Signature\n\n```tsx\nfunction useRoutes(\n  routes: RouteObject[],\n  locationArg?: Partial<Location> | string,\n): React.ReactElement | null\n```\n\n## Params\n\n### routes\n\nAn array of [`RouteObject`](https://api.reactrouter.com/v7/types/react-router.RouteObject.html)s that define the route hierarchy\n\n### locationArg\n\nAn optional [`Location`](https://api.reactrouter.com/v7/interfaces/react-router.Location.html) object or pathname string to use instead of the current [`Location`](https://api.reactrouter.com/v7/interfaces/react-router.Location.html)\n\n## Returns\n\nA React element to render the matched route, or `null` if no routes matched\n\n"
  },
  {
    "path": "docs/api/hooks/useSearchParams.md",
    "content": "---\ntitle: useSearchParams\n---\n\n# useSearchParams\n\n<!--\n⚠️ ⚠️ IMPORTANT ⚠️ ⚠️ \n\nThank you for helping improve our documentation!\n\nThis file is auto-generated from the JSDoc comments in the source\ncode, so please edit the JSDoc comments in the file below and this\nfile will be re-generated once those changes are merged.\n\nhttps://github.com/remix-run/react-router/blob/main/packages/react-router/lib/dom/lib.tsx\n-->\n\n[MODES: framework, data, declarative]\n\n## Summary\n\n[Reference Documentation ↗](https://api.reactrouter.com/v7/functions/react-router.useSearchParams.html)\n\nReturns a tuple of the current URL's [`URLSearchParams`](https://developer.mozilla.org/en-US/docs/Web/API/URLSearchParams)\nand a function to update them. Setting the search params causes a navigation.\n\n```tsx\nimport { useSearchParams } from \"react-router\";\n\nexport function SomeComponent() {\n  const [searchParams, setSearchParams] = useSearchParams();\n  // ...\n}\n```\n\n### `setSearchParams` function\n\nThe second element of the tuple is a function that can be used to update the\nsearch params. It accepts the same types as `defaultInit` and will cause a\nnavigation to the new URL.\n\n```tsx\nlet [searchParams, setSearchParams] = useSearchParams();\n\n// a search param string\nsetSearchParams(\"?tab=1\");\n\n// a shorthand object\nsetSearchParams({ tab: \"1\" });\n\n// object keys can be arrays for multiple values on the key\nsetSearchParams({ brand: [\"nike\", \"reebok\"] });\n\n// an array of tuples\nsetSearchParams([[\"tab\", \"1\"]]);\n\n// a `URLSearchParams` object\nsetSearchParams(new URLSearchParams(\"?tab=1\"));\n```\n\nIt also supports a function callback like React's\n[`setState`](https://react.dev/reference/react/useState#setstate):\n\n```tsx\nsetSearchParams((searchParams) => {\n  searchParams.set(\"tab\", \"2\");\n  return searchParams;\n});\n```\n\n<docs-warning>The function callback version of `setSearchParams` does not support\nthe [queueing](https://react.dev/reference/react/useState#setstate-parameters)\nlogic that React's `setState` implements.  Multiple calls to `setSearchParams`\nin the same tick will not build on the prior value.  If you need this behavior,\nyou can use `setState` manually.</docs-warning>\n\n### Notes\n\nNote that `searchParams` is a stable reference, so you can reliably use it\nas a dependency in React's [`useEffect`](https://react.dev/reference/react/useEffect)\nhooks.\n\n```tsx\nuseEffect(() => {\n  console.log(searchParams.get(\"tab\"));\n}, [searchParams]);\n```\n\nHowever, this also means it's mutable. If you change the object without\ncalling `setSearchParams`, its values will change between renders if some\nother state causes the component to re-render and URL will not reflect the\nvalues.\n\n## Signature\n\n```tsx\nfunction useSearchParams(\n  defaultInit?: URLSearchParamsInit,\n): [URLSearchParams, SetURLSearchParams]\n```\n\n## Params\n\n### defaultInit\n\nYou can initialize the search params with a default value, though it **will\nnot** change the URL on the first render.\n\n```tsx\n// a search param string\nuseSearchParams(\"?tab=1\");\n\n// a shorthand object\nuseSearchParams({ tab: \"1\" });\n\n// object keys can be arrays for multiple values on the key\nuseSearchParams({ brand: [\"nike\", \"reebok\"] });\n\n// an array of tuples\nuseSearchParams([[\"tab\", \"1\"]]);\n\n// a `URLSearchParams` object\nuseSearchParams(new URLSearchParams(\"?tab=1\"));\n```\n\n## Returns\n\nA tuple of the current [`URLSearchParams`](https://developer.mozilla.org/en-US/docs/Web/API/URLSearchParams)\nand a function to update them.\n\n"
  },
  {
    "path": "docs/api/hooks/useSubmit.md",
    "content": "---\ntitle: useSubmit\n---\n\n# useSubmit\n\n<!--\n⚠️ ⚠️ IMPORTANT ⚠️ ⚠️ \n\nThank you for helping improve our documentation!\n\nThis file is auto-generated from the JSDoc comments in the source\ncode, so please edit the JSDoc comments in the file below and this\nfile will be re-generated once those changes are merged.\n\nhttps://github.com/remix-run/react-router/blob/main/packages/react-router/lib/dom/lib.tsx\n-->\n\n[MODES: framework, data]\n\n## Summary\n\n[Reference Documentation ↗](https://api.reactrouter.com/v7/functions/react-router.useSubmit.html)\n\nThe imperative version of [`<Form>`](../components/Form) that lets you submit a form\nfrom code instead of a user interaction.\n\n```tsx\nimport { useSubmit } from \"react-router\";\n\nfunction SomeComponent() {\n  const submit = useSubmit();\n  return (\n    <Form onChange={(event) => submit(event.currentTarget)} />\n  );\n}\n```\n\n## Signature\n\n```tsx\nfunction useSubmit(): SubmitFunction\n```\n\n## Returns\n\nA function that can be called to submit a [`Form`](../components/Form) imperatively.\n\n"
  },
  {
    "path": "docs/api/hooks/useViewTransitionState.md",
    "content": "---\ntitle: useViewTransitionState\n---\n\n# useViewTransitionState\n\n<!--\n⚠️ ⚠️ IMPORTANT ⚠️ ⚠️ \n\nThank you for helping improve our documentation!\n\nThis file is auto-generated from the JSDoc comments in the source\ncode, so please edit the JSDoc comments in the file below and this\nfile will be re-generated once those changes are merged.\n\nhttps://github.com/remix-run/react-router/blob/main/packages/react-router/lib/dom/lib.tsx\n-->\n\n[MODES: framework, data]\n\n## Summary\n\n[Reference Documentation ↗](https://api.reactrouter.com/v7/functions/react-router.useViewTransitionState.html)\n\nThis hook returns `true` when there is an active [View Transition](https://developer.mozilla.org/en-US/docs/Web/API/View_Transitions_API)\nto the specified location. This can be used to apply finer-grained styles to\nelements to further customize the view transition. This requires that view\ntransitions have been enabled for the given navigation via [`LinkProps.viewTransition`](https://api.reactrouter.com/v7/interfaces/react-router.LinkProps.html#viewTransition)\n(or the `Form`, `submit`, or `navigate` call)\n\n## Signature\n\n```tsx\nfunction useViewTransitionState(\n  to: To,\n  {\n    relative,\n  }: {\n    relative?: RelativeRoutingType;\n  } = ,\n) {}\n```\n\n## Params\n\n### to\n\nThe [`To`](https://api.reactrouter.com/v7/types/react-router.To.html) location to check for an active [View Transition](https://developer.mozilla.org/en-US/docs/Web/API/View_Transitions_API).\n\n### options.relative\n\nThe relative routing type to use when resolving the `to` location, defaults to `\"route\"`. See [`RelativeRoutingType`](https://api.reactrouter.com/v7/types/react-router.RelativeRoutingType.html) for\nmore details.\n\n## Returns\n\n`true` if there is an active [View Transition](https://developer.mozilla.org/en-US/docs/Web/API/View_Transitions_API)\nto the specified [`Location`](https://api.reactrouter.com/v7/interfaces/react-router.Location.html), otherwise `false`.\n\n"
  },
  {
    "path": "docs/api/index.md",
    "content": "---\ntitle: API\norder: 3\n---\n"
  },
  {
    "path": "docs/api/other-api/adapter.md",
    "content": "---\ntitle: \"@react-router/{adapter}\"\n---\n\n# Server Adapters\n\n## Official Adapters\n\nIdiomatic React Router apps can generally be deployed anywhere because React Router adapts the server's request/response to the [Web Fetch API][web-fetch-api]. It does this through adapters. We maintain a few adapters:\n\n- `@react-router/architect`\n- `@react-router/cloudflare`\n- `@react-router/express`\n\nThese adapters are imported into your server's entry and are not used inside your React Router app itself.\n\nIf you initialized your app with `npx create-react-router@latest` with something other than the built-in [React Router App Server][rr-serve] (`@react-router/serve`), you will note a `server/index.js` file that imports and uses one of these adapters.\n\n<docs-info>If you're using the built-in React Router App Server, you don't interact with this API</docs-info>\n\nEach adapter has the same API. In the future, we may have helpers specific to the platform you're deploying to.\n\n## `@react-router/express`\n\n[Reference Documentation ↗](https://api.reactrouter.com/v7/modules/_react-router_express.html)\n\nHere's an example with [Express][express]:\n\n```ts lines=[1-3,11-22]\nconst {\n  createRequestHandler,\n} = require(\"@react-router/express\");\nconst express = require(\"express\");\n\nconst app = express();\n\n// needs to handle all verbs (GET, POST, etc.)\napp.all(\n  \"*\",\n  createRequestHandler({\n    // `react-router build` and `react-router dev` output files to a build directory,\n    // you need to pass that build to the request handler\n    build: require(\"./build\"),\n\n    // Return anything you want here to be available as `context` in your\n    // loaders and actions. This is where you can bridge the gap between your\n    // server and React Router\n    getLoadContext(req, res) {\n      return {};\n    },\n  }),\n);\n```\n\n### Migrating from the React Router App Server\n\nIf you started an app with the [React Router App Server][rr-serve] but find that you want to take control over the Express server and customize it, it should be fairly straightforward to migrate way from `@react-router/serve`.\n\nYou can refer to the [Express template][express-template] as a reference, but here are the main changes you will need to make:\n\n**1. Update deps**\n\n```shellscript nonumber\nnpm uninstall @react-router/serve\nnpm install @react-router/express compression express morgan cross-env\nnpm install --save-dev @types/express @types/express-serve-static-core @types/morgan\n```\n\n**2. Add a server**\n\nCreate your React Router Express server in `server/app.ts`:\n\n```ts filename=server/app.ts\nimport \"react-router\";\nimport { createRequestHandler } from \"@react-router/express\";\nimport express from \"express\";\n\nexport const app = express();\n\napp.use(\n  createRequestHandler({\n    build: () =>\n      import(\"virtual:react-router/server-build\"),\n  }),\n);\n```\n\nCopy the [`server.js`][express-template-server-js] into your app. This is the boilerplate setup we recommend to allow the same server code to run both the development and production builds of your app. Two separate files are used here so that the main Express server code can be written in TypeScript (`server/app.ts`) and compiled into your server build by React Router, and then executed via `node server.js`.\n\n**3. Update `vite.config.ts` to compile the server**\n\n```tsx filename=vite.config.ts lines=[6-10]\nimport { reactRouter } from \"@react-router/dev/vite\";\nimport { defineConfig } from \"vite\";\nimport tsconfigPaths from \"vite-tsconfig-paths\";\n\nexport default defineConfig(({ isSsrBuild }) => ({\n  build: {\n    rollupOptions: isSsrBuild\n      ? { input: \"./server/app.ts\" }\n      : undefined,\n  },\n  plugins: [reactRouter(), tsconfigPaths()],\n}));\n```\n\n**4. Update `package.json` scripts**\n\nUpdate the `dev` and `start` scripts to use your new Express server:\n\n```json filename=package.json\n{\n  // ...\n  \"scripts\": {\n    \"dev\": \"cross-env NODE_ENV=development node server.js\",\n    \"start\": \"node server.js\"\n    // ...\n  }\n  // ...\n}\n```\n\n## `@react-router/cloudflare`\n\n[Reference Documentation ↗](https://api.reactrouter.com/v7/modules/_react-router_cloudflare.html)\n\nHere's an example with Cloudflare:\n\n```ts\nimport { createRequestHandler } from \"react-router\";\n\ndeclare module \"react-router\" {\n  export interface AppLoadContext {\n    cloudflare: {\n      env: Env;\n      ctx: ExecutionContext;\n    };\n  }\n}\n\nconst requestHandler = createRequestHandler(\n  () => import(\"virtual:react-router/server-build\"),\n  import.meta.env.MODE,\n);\n\nexport default {\n  async fetch(request, env, ctx) {\n    return requestHandler(request, {\n      cloudflare: { env, ctx },\n    });\n  },\n} satisfies ExportedHandler<Env>;\n```\n\n## `@react-router/node`\n\nWhile not a direct \"adapter\" like the above, this package contains utilities for working with Node-based adapters.\n\n[Reference Documentation ↗](https://api.reactrouter.com/v7/modules/_react-router_node.html)\n\n### Node Version Support\n\nReact Router officially supports **Active** and **Maintenance** [Node LTS versions][node-releases] at any given point in time. Dropped support for End of Life Node versions is done in a React Router Minor release.\n\n[express]: https://expressjs.com\n[node-releases]: https://nodejs.org/en/about/previous-releases\n[web-fetch-api]: https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API\n[rr-serve]: ./serve\n[express-template]: https://github.com/remix-run/react-router-templates/tree/main/node-custom-server\n[express-template-server-js]: https://github.com/remix-run/react-router-templates/blob/main/node-custom-server/server.js\n"
  },
  {
    "path": "docs/api/other-api/dev.md",
    "content": "---\ntitle: \"@react-router/dev (CLI)\"\n---\n\n# React Router CLI\n\nThe React Router CLI comes from the `@react-router/dev` package. Make sure it is in your `package.json` `devDependencies` so it doesn't get deployed to your server.\n\nTo get a full list of available commands and flags, run:\n\n```shellscript nonumber\nnpx @react-router/dev -h\n```\n\n## `react-router build`\n\nBuilds your app for production with [Vite][vite]. This command will set `process.env.NODE_ENV` to `production` and minify the output for deployment.\n\n```shellscript nonumber\nreact-router build\n```\n\n| Flag                  | Description                                             | Type                                                | Default     |\n| --------------------- | ------------------------------------------------------- | --------------------------------------------------- | ----------- |\n| `--assetsInlineLimit` | Static asset base64 inline threshold in bytes           | `number`                                            | `4096`      |\n| `--clearScreen`       | Allow/disable clear screen when logging                 | `boolean`                                           |             |\n| `--config`, `-c`      | Use specified config file                               | `string`                                            |             |\n| `--emptyOutDir`       | Force empty outDir when it's outside of root            | `boolean`                                           |             |\n| `--logLevel`, `-l`    | Use specified log level                                 | `\"info\" \\| \"warn\" \\| \"error\" \\| \"silent\" \\| string` |             |\n| `--minify`            | Enable/disable minification, or specify minifier to use | `boolean \\| \"terser\" \\| \"esbuild\"`                  | `\"esbuild\"` |\n| `--mode`, `-m`        | Set env mode                                            | `string`                                            |             |\n| `--profile`           | Start built-in Node.js inspector                        |                                                     |             |\n| `--sourcemapClient`   | Output source maps for client build                     | `boolean \\| \"inline\" \\| \"hidden\"`                   | `false`     |\n| `--sourcemapServer`   | Output source maps for server build                     | `boolean \\| \"inline\" \\| \"hidden\"`                   | `false`     |\n\n## `react-router dev`\n\nRuns your app in development mode with HMR and Hot Data Revalidation (HDR), powered by [Vite][vite].\n\n```shellscript nonumber\nreact-router dev\n```\n\n<docs-info>\n\nWhat is \"Hot Data Revalidation\"?\n\nLike HMR, HDR is a way of hot updating your app without needing to refresh the page.\nThat way you can keep your app state as your edits are applied in your app.\nHMR handles client-side code updates like when you change the components, markup, or styles in your app.\nLikewise, HDR handles server-side code updates.\n\nThat means any time you make a change to the current page (or any code that your current page depends on), React Router will re-fetch data from your [loaders][loaders].\nThat way your app is _always_ up to date with the latest code changes, client-side or server-side.\n\n</docs-info>\n\n| Flag               | Description                                           | Type                                                | Default |\n| ------------------ | ----------------------------------------------------- | --------------------------------------------------- | ------- |\n| `--clearScreen`    | Allow/disable clear screen when logging               | `boolean`                                           |         |\n| `--config`, `-c`   | Use specified config file                             | `string`                                            |         |\n| `--cors`           | Enable CORS                                           | `boolean`                                           |         |\n| `--force`          | Force the optimizer to ignore the cache and re-bundle | `boolean`                                           |         |\n| `--host`           | Specify hostname                                      | `string`                                            |         |\n| `--logLevel`, `-l` | Use specified log level                               | `\"info\" \\| \"warn\" \\| \"error\" \\| \"silent\" \\| string` |         |\n| `--mode`, `-m`     | Set env mode                                          | `string`                                            |         |\n| `--open`           | Open browser on startup                               | `boolean \\| string`                                 |         |\n| `--port`           | Specify port                                          | `number`                                            |         |\n| `--profile`        | Start built-in Node.js inspector                      |                                                     |         |\n| `--strictPort`     | Exit if specified port is already in use              | `boolean`                                           |         |\n\n## `react-router reveal`\n\nReact Router handles the entry points of your application by default.\n\nIf you want to have control over these entry points, you can run `npx react-router reveal` to generate the [`entry.client.tsx`][entry-client] and [`entry.server.tsx`][entry-server] files in your `app` directory. When these files are present, React Router will use them instead of the defaults.\n\n```shellscript nonumber\nnpx react-router reveal\n```\n\n| Flag              | Description                     | Type      | Default |\n| ----------------- | ------------------------------- | --------- | ------- |\n| `--config`, `-c`  | Use specified config file       | `string`  |         |\n| `--mode`, `-m`    | Set env mode                    | `string`  |         |\n| `--no-typescript` | Generate plain JavaScript files | `boolean` | `false` |\n| `--typescript`    | Generate TypeScript files       | `boolean` | `true`  |\n\n## `react-router routes`\n\nPrints the routes in your app to the terminal.\n\n```shellscript nonumber\nreact-router routes\n```\n\nYour route tree will be in a JSX format by default. You can also use the `--json` flag to get the routes in a JSON format.\n\n```shellscript nonumber\nreact-router routes --json\n```\n\n| Flag             | Description                  | Type      | Default |\n| ---------------- | ---------------------------- | --------- | ------- |\n| `--config`, `-c` | Use specified config file    | `string`  |         |\n| `--json`         | Output routes in JSON format | `boolean` | `false` |\n| `--mode`, `-m`   | Set env mode                 | `string`  |         |\n\n## `react-router typegen`\n\nGenerates TypeScript types for your routes. This happens automatically during development, but you can manually run it when needed, e.g., to generate types in CI before running `tsc`.  See [Type Safety][type-safety] for more information.\n\n```shellscript nonumber\nreact-router typegen\n```\n\n| Flag             | Description               | Type      | Default |\n| ---------------- | ------------------------- | --------- | ------- |\n| `--config`, `-c` | Use specified config file | `string`  |         |\n| `--mode`, `-m`   | Set env mode              | `string`  |         |\n| `--watch`        | Watch for changes         | `boolean` | `false` |\n\n[loaders]: ../../start/framework/data-loading\n[vite]: https://vite.dev\n[entry-server]: ../framework-conventions/entry.server.tsx\n[entry-client]: ../framework-conventions/entry.client.tsx\n[type-safety]: ../../explanation/type-safety\n"
  },
  {
    "path": "docs/api/other-api/index.md",
    "content": "---\ntitle: Other API\norder: 9\n---\n"
  },
  {
    "path": "docs/api/other-api/serve.md",
    "content": "---\ntitle: \"@react-router/serve\"\n---\n\n# React Router App Server\n\nReact Router is designed for you to own your server, but if you don't want to set one up, you can use the React Router App Server instead. It's a production-ready but basic Node.js server built with [Express][express].\n\nBy design, we do not provide options to customize the React Router App Server because if you need to customize the underlying `express` server, we'd rather you manage the server completely instead of creating an abstraction to handle all the possible customizations you may require. If you find you want to customize it, you can [migrate to the `@react-router/express` adapter][migrate-to-express].\n\nYou can see the underlying `express` server configuration in [packages/react-router-serve/cli.ts][rr-serve-code]. By default, it uses the following Express middlewares (please refer to their documentation for default behaviors):\n\n- [`compression`][compression]\n- [`express.static`][express-static] (and thus [`serve-static`][serve-static])\n- [`morgan`][morgan]\n\n## `HOST` environment variable\n\nYou can configure the hostname for your Express app via `process.env.HOST` and that value will be passed to the internal [`app.listen`][express-listen] method when starting the server.\n\n```shellscript nonumber\nHOST=127.0.0.1 npx react-router-serve build/index.js\n```\n\n```shellscript nonumber\nreact-router-serve <server-build-path>\n# e.g.\nreact-router-serve build/index.js\n```\n\n## `PORT` environment variable\n\nYou can change the port of the server with an environment variable.\n\n```shellscript nonumber\nPORT=4000 npx react-router-serve build/index.js\n```\n\n## Development Environment\n\nDepending on `process.env.NODE_ENV`, the server will boot in development or production mode.\n\nThe `server-build-path` needs to point to the `serverBuildPath` defined in [`react-router.config.ts`][rr-config].\n\nBecause only the build artifacts (`build/`, `public/build/`) need to be deployed to production, the `react-router.config.ts` is not guaranteed to be available in production, so you need to tell React Router where your server build is with this option.\n\nIn development, `react-router-serve` will ensure the latest code is run by purging the `require` cache for every request. This has some effects on your code you might need to be aware of:\n\n- Any values in the module scope will be \"reset\"\n\n  ```tsx lines=[1-3]\n  // this will be reset for every request because the module cache was\n  // cleared and this will be required brand new\n  const cache = new Map();\n\n  export async function loader({\n    params,\n  }: Route.LoaderArgs) {\n    if (cache.has(params.foo)) {\n      return cache.get(params.foo);\n    }\n\n    const record = await fakeDb.stuff.find(params.foo);\n    cache.set(params.foo, record);\n    return record;\n  }\n  ```\n\n  If you need a workaround for preserving cache in development, you can set up a singleton in your server.\n\n- Any **module side effects** will remain in place! This may cause problems but should probably be avoided anyway.\n\n  ```tsx lines=[1-4]\n  // this starts running the moment the module is imported\n  setInterval(() => {\n    console.log(Date.now());\n  }, 1000);\n\n  export async function loader() {\n    // ...\n  }\n  ```\n\n  If you need to write your code in a way that has these types of module side effects, you should set up your own [@react-router/express][rr-express] server and a tool in development like [`pm2-dev`][pm2-dev] or [`nodemon`][nodemon] to restart the server on file changes instead.\n\nIn production, this doesn't happen. The server boots up, and that's the end of it.\n\n[rr-express]: ./adapter#react-routerexpress\n[express-listen]: https://expressjs.com/en/api.html#app.listen\n[rr-config]: ../framework-conventions/react-router.config.ts\n[rr-serve-code]: https://github.com/remix-run/react-router/blob/main/packages/react-router-serve/cli.ts\n[compression]: https://expressjs.com/en/resources/middleware/compression.html\n[express-static]: https://expressjs.com/en/4x/api.html#express.static\n[serve-static]: https://expressjs.com/en/resources/middleware/serve-static.html\n[morgan]: https://expressjs.com/en/resources/middleware/morgan.html\n[express]: https://expressjs.com\n[migrate-to-express]: ./adapter#migrating-from-the-react-router-app-server\n[pm2-dev]: https://npm.im/pm2-dev\n[nodemon]: https://npm.im/nodemon\n"
  },
  {
    "path": "docs/api/rsc/RSCHydratedRouter.md",
    "content": "---\ntitle: RSCHydratedRouter\nunstable: true\n---\n\n# unstable_RSCHydratedRouter\n\n<!--\n⚠️ ⚠️ IMPORTANT ⚠️ ⚠️ \n\nThank you for helping improve our documentation!\n\nThis file is auto-generated from the JSDoc comments in the source\ncode, so please edit the JSDoc comments in the file below and this\nfile will be re-generated once those changes are merged.\n\nhttps://github.com/remix-run/react-router/blob/main/packages/react-router/lib/rsc/browser.tsx\n-->\n\n[MODES: data]\n\n<br />\n<br />\n\n<docs-warning>This API is experimental and subject to breaking changes in \nminor/patch releases. Please use with caution and pay **very** close attention \nto release notes for relevant changes.</docs-warning>\n\n## Summary\n\nHydrates a server rendered [`unstable_RSCPayload`](https://api.reactrouter.com/v7/types/react-router.unstable_RSCPayload.html) in the browser.\n\n```tsx\nimport { startTransition, StrictMode } from \"react\";\nimport { hydrateRoot } from \"react-dom/client\";\nimport {\n  unstable_getRSCStream as getRSCStream,\n  unstable_RSCHydratedRouter as RSCHydratedRouter,\n} from \"react-router\";\nimport type { unstable_RSCPayload as RSCPayload } from \"react-router\";\n\ncreateFromReadableStream(getRSCStream()).then((payload) =>\n  startTransition(async () => {\n    hydrateRoot(\n      document,\n      <StrictMode>\n        <RSCHydratedRouter\n          createFromReadableStream={createFromReadableStream}\n          payload={payload}\n        />\n      </StrictMode>,\n      { formState: await getFormState(payload) },\n    );\n  }),\n);\n```\n\n## Signature\n\n```tsx\nfunction RSCHydratedRouter({\n  createFromReadableStream,\n  fetch: fetchImplementation = fetch,\n  payload,\n  routeDiscovery = \"eager\",\n  getContext,\n}: RSCHydratedRouterProps)\n```\n\n## Props\n\n### createFromReadableStream\n\nYour `react-server-dom-xyz/client`'s `createFromReadableStream` function,\nused to decode payloads from the server.\n\n### fetch\n\nOptional fetch implementation. Defaults to global [`fetch`](https://developer.mozilla.org/en-US/docs/Web/API/fetch).\n\n### getContext\n\nA function that returns an [`RouterContextProvider`](../utils/RouterContextProvider) instance\nwhich is provided as the `context` argument to client [`action`](../../start/data/route-object#action)s,\n[`loader`](../../start/data/route-object#loader)s and [middleware](../../how-to/middleware).\nThis function is called to generate a fresh `context` instance on each\nnavigation or fetcher call.\n\n### payload\n\nThe decoded [`unstable_RSCPayload`](https://api.reactrouter.com/v7/types/react-router.unstable_RSCPayload.html) to hydrate.\n\n### routeDiscovery\n\n`\"eager\"` or `\"lazy\"` - Determines if links are eagerly discovered, or\ndelayed until clicked.\n\n"
  },
  {
    "path": "docs/api/rsc/RSCStaticRouter.md",
    "content": "---\ntitle: RSCStaticRouter\nunstable: true\n---\n\n# unstable_RSCStaticRouter\n\n<!--\n⚠️ ⚠️ IMPORTANT ⚠️ ⚠️ \n\nThank you for helping improve our documentation!\n\nThis file is auto-generated from the JSDoc comments in the source\ncode, so please edit the JSDoc comments in the file below and this\nfile will be re-generated once those changes are merged.\n\nhttps://github.com/remix-run/react-router/blob/main/packages/react-router/lib/rsc/server.ssr.tsx\n-->\n\n[MODES: data]\n\n<br />\n<br />\n\n<docs-warning>This API is experimental and subject to breaking changes in \nminor/patch releases. Please use with caution and pay **very** close attention \nto release notes for relevant changes.</docs-warning>\n\n## Summary\n\n[Reference Documentation ↗](https://api.reactrouter.com/v7/functions/react-router.unstable_RSCStaticRouter.html)\n\nPre-renders an [`unstable_RSCPayload`](https://api.reactrouter.com/v7/types/react-router.unstable_RSCPayload.html) to HTML. Usually used in\n[`unstable_routeRSCServerRequest`](../rsc/routeRSCServerRequest)'s `renderHTML` callback.\n\n```tsx\nimport { createFromReadableStream } from \"@vitejs/plugin-rsc/ssr\";\nimport * as ReactDomServer from \"react-dom/server.edge\";\nimport {\n  unstable_RSCStaticRouter as RSCStaticRouter,\n  unstable_routeRSCServerRequest as routeRSCServerRequest,\n} from \"react-router\";\n\nrouteRSCServerRequest({\n  request,\n  serverResponse,\n  createFromReadableStream,\n  async renderHTML(getPayload) {\n    const payload = getPayload();\n\n    return await renderHTMLToReadableStream(\n      <RSCStaticRouter getPayload={getPayload} />,\n      {\n        bootstrapScriptContent,\n        formState: await payload.formState,\n      }\n    );\n  },\n});\n```\n\n## Signature\n\n```tsx\nfunction RSCStaticRouter({ getPayload }: RSCStaticRouterProps)\n```\n\n## Props\n\n### getPayload\n\nA function that starts decoding of the [`unstable_RSCPayload`](https://api.reactrouter.com/v7/types/react-router.unstable_RSCPayload.html). Usually passed\nthrough from [`unstable_routeRSCServerRequest`](../rsc/routeRSCServerRequest)'s `renderHTML`.\n\n"
  },
  {
    "path": "docs/api/rsc/createCallServer.md",
    "content": "---\ntitle: createCallServer\nunstable: true\n---\n\n# unstable_createCallServer\n\n<!--\n⚠️ ⚠️ IMPORTANT ⚠️ ⚠️ \n\nThank you for helping improve our documentation!\n\nThis file is auto-generated from the JSDoc comments in the source\ncode, so please edit the JSDoc comments in the file below and this\nfile will be re-generated once those changes are merged.\n\nhttps://github.com/remix-run/react-router/blob/main/packages/react-router/lib/rsc/browser.tsx\n-->\n\n[MODES: data]\n\n<br />\n<br />\n\n<docs-warning>This API is experimental and subject to breaking changes in \nminor/patch releases. Please use with caution and pay **very** close attention \nto release notes for relevant changes.</docs-warning>\n\n## Summary\n\nCreate a React `callServer` implementation for React Router.\n\n```tsx\nimport {\n  createFromReadableStream,\n  createTemporaryReferenceSet,\n  encodeReply,\n  setServerCallback,\n} from \"@vitejs/plugin-rsc/browser\";\nimport { unstable_createCallServer as createCallServer } from \"react-router\";\n\nsetServerCallback(\n  createCallServer({\n    createFromReadableStream,\n    createTemporaryReferenceSet,\n    encodeReply,\n  })\n);\n```\n\n## Signature\n\n```tsx\nfunction createCallServer({\n  createFromReadableStream,\n  createTemporaryReferenceSet,\n  encodeReply,\n  fetch: fetchImplementation = fetch,\n}: {\n  createFromReadableStream: BrowserCreateFromReadableStreamFunction;\n  createTemporaryReferenceSet: () => unknown;\n  encodeReply: EncodeReplyFunction;\n  fetch?: (request: Request) => Promise<Response>;\n})\n```\n\n## Params\n\n### opts.createFromReadableStream\n\nYour `react-server-dom-xyz/client`'s `createFromReadableStream`. Used to decode payloads from the server.\n\n### opts.createTemporaryReferenceSet\n\nA function that creates a temporary reference set for the [RSC](https://react.dev/reference/rsc/server-components)\npayload.\n\n### opts.encodeReply\n\nYour `react-server-dom-xyz/client`'s `encodeReply`. Used when sending payloads to the server.\n\n### opts.fetch\n\nOptional [Fetch API](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API) implementation. Defaults to global [`fetch`](https://developer.mozilla.org/en-US/docs/Web/API/fetch).\n\n## Returns\n\nA function that can be used to call server actions.\n\n"
  },
  {
    "path": "docs/api/rsc/getRSCStream.md",
    "content": "---\ntitle: getRSCStream\nunstable: true\n---\n\n# unstable_getRSCStream\n\n<!--\n⚠️ ⚠️ IMPORTANT ⚠️ ⚠️ \n\nThank you for helping improve our documentation!\n\nThis file is auto-generated from the JSDoc comments in the source\ncode, so please edit the JSDoc comments in the file below and this\nfile will be re-generated once those changes are merged.\n\nhttps://github.com/remix-run/react-router/blob/main/packages/react-router/lib/rsc/html-stream/browser.ts\n-->\n\n[MODES: data]\n\n<br />\n<br />\n\n<docs-warning>This API is experimental and subject to breaking changes in \nminor/patch releases. Please use with caution and pay **very** close attention \nto release notes for relevant changes.</docs-warning>\n\n## Summary\n\nGet the prerendered [RSC](https://react.dev/reference/rsc/server-components)\nstream for hydration. Usually passed directly to your\n`react-server-dom-xyz/client`'s `createFromReadableStream`.\n\n```tsx\nimport { startTransition, StrictMode } from \"react\";\nimport { hydrateRoot } from \"react-dom/client\";\nimport {\n  unstable_getRSCStream as getRSCStream,\n  unstable_RSCHydratedRouter as RSCHydratedRouter,\n} from \"react-router\";\nimport type { unstable_RSCPayload as RSCPayload } from \"react-router\";\n\ncreateFromReadableStream(getRSCStream()).then(\n  (payload: RSCServerPayload) => {\n    startTransition(async () => {\n      hydrateRoot(\n        document,\n        <StrictMode>\n          <RSCHydratedRouter {...props} />\n        </StrictMode>,\n        {\n          // Options\n        }\n      );\n    });\n  }\n);\n```\n\n## Signature\n\n```tsx\nfunction getRSCStream(): ReadableStream\n```\n\n## Returns\n\nA [`ReadableStream`](https://developer.mozilla.org/en-US/docs/Web/API/ReadableStream)\nthat contains the [RSC](https://react.dev/reference/rsc/server-components)\ndata for hydration.\n\n"
  },
  {
    "path": "docs/api/rsc/index.md",
    "content": "---\ntitle: RSC (Unstable)\norder: 7\n---\n"
  },
  {
    "path": "docs/api/rsc/matchRSCServerRequest.md",
    "content": "---\ntitle: matchRSCServerRequest\nunstable: true\n---\n\n# unstable_matchRSCServerRequest\n\n<!--\n⚠️ ⚠️ IMPORTANT ⚠️ ⚠️ \n\nThank you for helping improve our documentation!\n\nThis file is auto-generated from the JSDoc comments in the source\ncode, so please edit the JSDoc comments in the file below and this\nfile will be re-generated once those changes are merged.\n\nhttps://github.com/remix-run/react-router/blob/main/packages/react-router/lib/rsc/server.rsc.ts\n-->\n\n[MODES: data]\n\n<br />\n<br />\n\n<docs-warning>This API is experimental and subject to breaking changes in \nminor/patch releases. Please use with caution and pay **very** close attention \nto release notes for relevant changes.</docs-warning>\n\n## Summary\n\n[Reference Documentation ↗](https://api.reactrouter.com/v7/variables/react-router.unstable_matchRSCServerRequest.html)\n\nMatches the given routes to a [`Request`](https://developer.mozilla.org/en-US/docs/Web/API/Request)\nand returns an [RSC](https://react.dev/reference/rsc/server-components)\n[`Response`](https://developer.mozilla.org/en-US/docs/Web/API/Response)\nencoding an [`unstable_RSCPayload`](https://api.reactrouter.com/v7/types/react-router.unstable_RSCPayload.html) for consumption by an [RSC](https://react.dev/reference/rsc/server-components)\nenabled client router.\n\n```tsx\nimport {\n  createTemporaryReferenceSet,\n  decodeAction,\n  decodeReply,\n  loadServerAction,\n  renderToReadableStream,\n} from \"@vitejs/plugin-rsc/rsc\";\nimport { unstable_matchRSCServerRequest as matchRSCServerRequest } from \"react-router\";\n\nmatchRSCServerRequest({\n  createTemporaryReferenceSet,\n  decodeAction,\n  decodeFormState,\n  decodeReply,\n  loadServerAction,\n  request,\n  routes: routes(),\n  generateResponse(match) {\n    return new Response(\n      renderToReadableStream(match.payload),\n      {\n        status: match.statusCode,\n        headers: match.headers,\n      }\n    );\n  },\n});\n```\n\n## Signature\n\n```tsx\nasync function matchRSCServerRequest({\n  allowedActionOrigins,\n  createTemporaryReferenceSet,\n  basename,\n  decodeReply,\n  requestContext,\n  loadServerAction,\n  decodeAction,\n  decodeFormState,\n  onError,\n  request,\n  routes,\n  generateResponse,\n}: {\n  allowedActionOrigins?: string[];\n  createTemporaryReferenceSet: () => unknown;\n  basename?: string;\n  decodeReply?: DecodeReplyFunction;\n  decodeAction?: DecodeActionFunction;\n  decodeFormState?: DecodeFormStateFunction;\n  requestContext?: RouterContextProvider;\n  loadServerAction?: LoadServerActionFunction;\n  onError?: (error: unknown) => void;\n  request: Request;\n  routes: RSCRouteConfigEntry[];\n  generateResponse: (\n    match: RSCMatch,\n    {\n      onError,\n      temporaryReferences,\n    }: {\n      onError(error: unknown): string | undefined;\n      temporaryReferences: unknown;\n    },\n  ) => Response;\n}): Promise<Response>\n```\n\n## Params\n\n### opts.allowedActionOrigins\n\nOrigin patterns that are allowed to execute actions.\n\n### opts.basename\n\nThe basename to use when matching the request.\n\n### opts.createTemporaryReferenceSet\n\nA function that returns a temporary reference set for the request, used to track temporary references in the [RSC](https://react.dev/reference/rsc/server-components)\nstream.\n\n### opts.decodeAction\n\nYour `react-server-dom-xyz/server`'s `decodeAction` function, responsible for loading a server action.\n\n### opts.decodeFormState\n\nA function responsible for decoding form state for progressively enhanceable forms with React's [`useActionState`](https://react.dev/reference/react/useActionState)\nusing your `react-server-dom-xyz/server`'s `decodeFormState`.\n\n### opts.decodeReply\n\nYour `react-server-dom-xyz/server`'s `decodeReply` function, used to decode the server function's arguments and bind them to the\nimplementation for invocation by the router.\n\n### opts.generateResponse\n\nA function responsible for using your `renderToReadableStream` to generate a [`Response`](https://developer.mozilla.org/en-US/docs/Web/API/Response)\nencoding the [`unstable_RSCPayload`](https://api.reactrouter.com/v7/types/react-router.unstable_RSCPayload.html).\n\n### opts.loadServerAction\n\nYour `react-server-dom-xyz/server`'s `loadServerAction` function, used to load a server action by ID.\n\n### opts.onError\n\nAn optional error handler that will be called with any errors that occur during the request processing.\n\n### opts.request\n\nThe [`Request`](https://developer.mozilla.org/en-US/docs/Web/API/Request) to match against.\n\n### opts.requestContext\n\nAn instance of [`RouterContextProvider`](../utils/RouterContextProvider) that should be created per request, to be passed to [`action`](../../start/data/route-object#action)s,\n[`loader`](../../start/data/route-object#loader)s and [middleware](../../how-to/middleware).\n\n### opts.routes\n\nYour [route definitions](https://api.reactrouter.com/v7/types/react-router.unstable_RSCRouteConfigEntry.html).\n\n## Returns\n\nA [`Response`](https://developer.mozilla.org/en-US/docs/Web/API/Response)\nthat contains the [RSC](https://react.dev/reference/rsc/server-components)\ndata for hydration.\n\n"
  },
  {
    "path": "docs/api/rsc/routeRSCServerRequest.md",
    "content": "---\ntitle: routeRSCServerRequest\nunstable: true\n---\n\n# unstable_routeRSCServerRequest\n\n<!--\n⚠️ ⚠️ IMPORTANT ⚠️ ⚠️ \n\nThank you for helping improve our documentation!\n\nThis file is auto-generated from the JSDoc comments in the source\ncode, so please edit the JSDoc comments in the file below and this\nfile will be re-generated once those changes are merged.\n\nhttps://github.com/remix-run/react-router/blob/main/packages/react-router/lib/rsc/server.ssr.tsx\n-->\n\n[MODES: data]\n\n<br />\n<br />\n\n<docs-warning>This API is experimental and subject to breaking changes in \nminor/patch releases. Please use with caution and pay **very** close attention \nto release notes for relevant changes.</docs-warning>\n\n## Summary\n\n[Reference Documentation ↗](https://api.reactrouter.com/v7/functions/react-router.unstable_routeRSCServerRequest.html)\n\nRoutes the incoming [`Request`](https://developer.mozilla.org/en-US/docs/Web/API/Request)\nto the [RSC](https://react.dev/reference/rsc/server-components) server and\nappropriately proxies the server response for data / resource requests, or\nrenders to HTML for a document request.\n\n```tsx\nimport { createFromReadableStream } from \"@vitejs/plugin-rsc/ssr\";\nimport * as ReactDomServer from \"react-dom/server.edge\";\nimport {\n  unstable_RSCStaticRouter as RSCStaticRouter,\n  unstable_routeRSCServerRequest as routeRSCServerRequest,\n} from \"react-router\";\n\nrouteRSCServerRequest({\n  request,\n  serverResponse,\n  createFromReadableStream,\n  async renderHTML(getPayload) {\n    const payload = getPayload();\n\n    return await renderHTMLToReadableStream(\n      <RSCStaticRouter getPayload={getPayload} />,\n      {\n        bootstrapScriptContent,\n        formState: await payload.formState,\n      }\n    );\n  },\n});\n```\n\n## Signature\n\n```tsx\nasync function routeRSCServerRequest({\n  request,\n  serverResponse,\n  createFromReadableStream,\n  renderHTML,\n  hydrate = true,\n}: {\n  request: Request;\n  serverResponse: Response;\n  createFromReadableStream: SSRCreateFromReadableStreamFunction;\n  renderHTML: (\n    getPayload: () => DecodedPayload,\n    options: {\n      onError(error: unknown): string | undefined;\n      onHeaders(headers: Headers): void;\n    },\n  ) => ReadableStream<Uint8Array> | Promise<ReadableStream<Uint8Array>>;\n  hydrate?: boolean;\n}): Promise<Response>\n```\n\n## Params\n\n### opts.createFromReadableStream\n\nYour `react-server-dom-xyz/client`'s `createFromReadableStream` function, used to decode payloads from the server.\n\n### opts.serverResponse\n\nA Response or partial response generated by the [RSC](https://react.dev/reference/rsc/server-components) handler containing a serialized [`unstable_RSCPayload`](https://api.reactrouter.com/v7/types/react-router.unstable_RSCPayload.html).\n\n### opts.hydrate\n\nWhether to hydrate the server response with the RSC payload. Defaults to `true`.\n\n### opts.renderHTML\n\nA function that renders the [`unstable_RSCPayload`](https://api.reactrouter.com/v7/types/react-router.unstable_RSCPayload.html) to HTML, usually using a [`<RSCStaticRouter>`](../rsc/RSCStaticRouter).\n\n### opts.request\n\nThe request to route.\n\n## Returns\n\nA [`Response`](https://developer.mozilla.org/en-US/docs/Web/API/Response)\nthat either contains the [RSC](https://react.dev/reference/rsc/server-components)\npayload for data requests, or renders the HTML for document requests.\n\n"
  },
  {
    "path": "docs/api/utils/IsCookieFunction.md",
    "content": "---\ntitle: IsCookieFunction\n---\n\n# IsCookieFunction\n\n[MODES: framework, data]\n\n## Summary\n\n[Reference Documentation ↗](https://api.reactrouter.com/v7/functions/react-router.IsCookieFunction.html)\n"
  },
  {
    "path": "docs/api/utils/IsSessionFunction.md",
    "content": "---\ntitle: IsSessionFunction\n---\n\n# IsSessionFunction\n\n[MODES: framework, data]\n\n## Summary\n\n[Reference Documentation ↗](https://api.reactrouter.com/v7/functions/react-router.IsSessionFunction.html)\n"
  },
  {
    "path": "docs/api/utils/RouterContextProvider.md",
    "content": "---\ntitle: RouterContextProvider\n---\n\n# RouterContextProvider\n\n<!--\n⚠️ ⚠️ IMPORTANT ⚠️ ⚠️ \n\nThank you for helping improve our documentation!\n\nThis file is auto-generated from the JSDoc comments in the source\ncode, so please edit the JSDoc comments in the file below and this\nfile will be re-generated once those changes are merged.\n\nhttps://github.com/remix-run/react-router/blob/main/packages/react-router/lib/router/utils.ts\n-->\n\n[MODES: framework, data]\n\n## Summary\n\n[Reference Documentation ↗](https://api.reactrouter.com/v7/classes/react-router.RouterContextProvider.html)\n\nProvides methods for writing/reading values in application context in a\ntype-safe way. Primarily for usage with [middleware](../../how-to/middleware).\n\n```tsx\nimport {\n  createContext,\n  RouterContextProvider\n} from \"react-router\";\n\nconst userContext = createContext<User | null>(null);\nconst contextProvider = new RouterContextProvider();\ncontextProvider.set(userContext, getUser());\n//                               ^ Type-safe\nconst user = contextProvider.get(userContext);\n//    ^ User\n```\n\n"
  },
  {
    "path": "docs/api/utils/createContext.md",
    "content": "---\ntitle: createContext\n---\n\n# createContext\n\n<!--\n⚠️ ⚠️ IMPORTANT ⚠️ ⚠️ \n\nThank you for helping improve our documentation!\n\nThis file is auto-generated from the JSDoc comments in the source\ncode, so please edit the JSDoc comments in the file below and this\nfile will be re-generated once those changes are merged.\n\nhttps://github.com/remix-run/react-router/blob/main/packages/react-router/lib/router/utils.ts\n-->\n\n[MODES: framework, data]\n\n## Summary\n\n[Reference Documentation ↗](https://api.reactrouter.com/v7/functions/react-router.createContext.html)\n\nCreates a type-safe [`RouterContext`](https://api.reactrouter.com/v7/interfaces/react-router.RouterContext.html) object that can be used to\nstore and retrieve arbitrary values in [`action`](../../start/framework/route-module#action)s,\n[`loader`](../../start/framework/route-module#loader)s, and [middleware](../../how-to/middleware).\nSimilar to React's [`createContext`](https://react.dev/reference/react/createContext),\nbut specifically designed for React Router's request/response lifecycle.\n\nIf a `defaultValue` is provided, it will be returned from `context.get()`\nwhen no value has been set for the context. Otherwise, reading this context\nwhen no value has been set will throw an error.\n\n```tsx filename=app/context.ts\nimport { createContext } from \"react-router\";\n\n// Create a context for user data\nexport const userContext =\n  createContext<User | null>(null);\n```\n\n```tsx filename=app/middleware/auth.ts\nimport { getUserFromSession } from \"~/auth.server\";\nimport { userContext } from \"~/context\";\n\nexport const authMiddleware = async ({\n  context,\n  request,\n}) => {\n  const user = await getUserFromSession(request);\n  context.set(userContext, user);\n};\n```\n\n```tsx filename=app/routes/profile.tsx\nimport { userContext } from \"~/context\";\n\nexport async function loader({\n  context,\n}: Route.LoaderArgs) {\n  const user = context.get(userContext);\n\n  if (!user) {\n    throw new Response(\"Unauthorized\", { status: 401 });\n  }\n\n  return { user };\n}\n```\n\n## Signature\n\n```tsx\nfunction createContext<T>(defaultValue?: T): RouterContext<T>\n```\n\n## Params\n\n### defaultValue\n\nAn optional default value for the context. This value will be returned if no value has been set for this context.\n\n## Returns\n\nA [`RouterContext`](https://api.reactrouter.com/v7/interfaces/react-router.RouterContext.html) object that can be used with\n`context.get()` and `context.set()` in [`action`](../../start/framework/route-module#action)s,\n[`loader`](../../start/framework/route-module#loader)s, and [middleware](../../how-to/middleware).\n\n"
  },
  {
    "path": "docs/api/utils/createCookie.md",
    "content": "---\ntitle: createCookie\n---\n\n# createCookie\n\n[MODES: framework, data]\n\n## Summary\n\n[Reference Documentation ↗](https://api.reactrouter.com/v7/functions/react-router.createCookie.html)\n\nCreates a logical container for managing a browser cookie from the server.\n"
  },
  {
    "path": "docs/api/utils/createCookieSessionStorage.md",
    "content": "---\ntitle: createCookieSessionStorage\n---\n\n# createCookieSessionStorage\n\n[MODES: framework, data]\n\n## Summary\n\n[Reference Documentation ↗](https://api.reactrouter.com/v7/functions/react-router.createCookieSessionStorage.html)\n\nCreates and returns a SessionStorage object that stores all session data\ndirectly in the session cookie itself.\n\nThis has the advantage that no database or other backend services are\nneeded, and can help to simplify some load-balanced scenarios. However, it\nalso has the limitation that serialized session data may not exceed the\nbrowser's maximum cookie size. Trade-offs!\n"
  },
  {
    "path": "docs/api/utils/createMemorySessionStorage.md",
    "content": "---\ntitle: createMemorySessionStorage\n---\n\n# createMemorySessionStorage\n\n[MODES: framework, data]\n\n## Summary\n\n[Reference Documentation ↗](https://api.reactrouter.com/v7/functions/react-router.createMemorySessionStorage.html)\n\nCreates and returns a simple in-memory SessionStorage object, mostly useful\nfor testing and as a reference implementation.\n\nNote: This storage does not scale beyond a single process, so it is not\nsuitable for most production scenarios.\n"
  },
  {
    "path": "docs/api/utils/createPath.md",
    "content": "---\ntitle: createPath\n---\n\n# createPath\n\n[MODES: framework, data, declarative]\n\n## Summary\n\n[Reference Documentation ↗](https://api.reactrouter.com/v7/functions/react-router.createPath.html)\n\nCreates a string URL path from the given pathname, search, and hash components.\n\n## Signature\n\n```tsx\ncreatePath(__namedParameters): string\n```\n\n## Params\n\n### \\_\\_namedParameters\n\n[modes: framework, data, declarative]\n\n_No documentation_\n"
  },
  {
    "path": "docs/api/utils/createRequestHandler.md",
    "content": "---\ntitle: createRequestHandler\nhidden: true\n---\n\n# createRequestHandler\n\n[MODES: framework, data, declarative]\n\n## Summary\n\n[Reference Documentation ↗](https://api.reactrouter.com/v7/functions/react-router.createRequestHandler.html)\n"
  },
  {
    "path": "docs/api/utils/createRoutesFromElements.md",
    "content": "---\ntitle: createRoutesFromElements\n---\n\n# createRoutesFromElements\n\n<!--\n⚠️ ⚠️ IMPORTANT ⚠️ ⚠️ \n\nThank you for helping improve our documentation!\n\nThis file is auto-generated from the JSDoc comments in the source\ncode, so please edit the JSDoc comments in the file below and this\nfile will be re-generated once those changes are merged.\n\nhttps://github.com/remix-run/react-router/blob/main/packages/react-router/lib/components.tsx\n-->\n\n[MODES: data]\n\n## Summary\n\n[Reference Documentation ↗](https://api.reactrouter.com/v7/functions/react-router.createRoutesFromElements.html)\n\nCreate route objects from JSX elements instead of arrays of objects.\n\n```tsx\nconst routes = createRoutesFromElements(\n  <>\n    <Route index loader={step1Loader} Component={StepOne} />\n    <Route path=\"step-2\" loader={step2Loader} Component={StepTwo} />\n    <Route path=\"step-3\" loader={step3Loader} Component={StepThree} />\n  </>\n);\n\nconst router = createBrowserRouter(routes);\n\nfunction App() {\n  return <RouterProvider router={router} />;\n}\n```\n\n## Params\n\n### children\n\nThe React children to convert into a route config\n\n### parentPath\n\nThe path of the parent route, used to generate unique IDs. This is used for internal recursion and is not intended to be used by the\napplication developer.\n\n## Returns\n\nAn array of [`RouteObject`](https://api.reactrouter.com/v7/types/react-router.RouteObject.html)s that can be used with a [`DataRouter`](https://api.reactrouter.com/v7/interfaces/react-router.DataRouter.html)\n\n"
  },
  {
    "path": "docs/api/utils/createRoutesStub.md",
    "content": "---\ntitle: createRoutesStub\n---\n\n# createRoutesStub\n\n[MODES: framework, data]\n\n## Summary\n\n[Reference Documentation ↗](https://api.reactrouter.com/v7/functions/react-router.createRoutesStub.html)\n\n## Signature\n\n```tsx\ncreateRoutesStub(routes, context): undefined\n```\n\n## Params\n\n### routes\n\n[modes: framework, data]\n\n_No documentation_\n\n### context\n\n[modes: framework, data]\n\n_No documentation_\n"
  },
  {
    "path": "docs/api/utils/createSearchParams.md",
    "content": "---\ntitle: createSearchParams\n---\n\n# createSearchParams\n\n[MODES: framework, data, declarative]\n\n## Summary\n\n[Reference Documentation ↗](https://api.reactrouter.com/v7/functions/react-router.createSearchParams.html)\n\nCreates a URLSearchParams object using the given initializer.\n\nThis is identical to `new URLSearchParams(init)` except it also\nsupports arrays as values in the object form of the initializer\ninstead of just strings. This is convenient when you need multiple\nvalues for a given key, but don't want to use an array initializer.\n\nFor example, instead of:\n\n```tsx\nlet searchParams = new URLSearchParams([\n  [\"sort\", \"name\"],\n  [\"sort\", \"price\"],\n]);\n```\n\nyou can do:\n\n```\nlet searchParams = createSearchParams({\n  sort: ['name', 'price']\n});\n```\n\n## Signature\n\n```tsx\ncreateSearchParams(init): URLSearchParams\n```\n\n## Params\n\n### init\n\n[modes: framework, data, declarative]\n\n_No documentation_\n"
  },
  {
    "path": "docs/api/utils/createSession.md",
    "content": "---\ntitle: createSession\nhidden: true\n---\n\n# createSession\n\n[MODES: framework, data, declarative]\n\n## Summary\n\n[Reference Documentation ↗](https://api.reactrouter.com/v7/functions/react-router.createSession.html)\n\nCreates a new Session object.\n\nNote: This function is typically not invoked directly by application code.\nInstead, use a `SessionStorage` object's `getSession` method.\n"
  },
  {
    "path": "docs/api/utils/createSessionStorage.md",
    "content": "---\ntitle: createSessionStorage\nhidden: true\n---\n\n# createSessionStorage\n\n[MODES: framework, data, declarative]\n\n## Summary\n\n[Reference Documentation ↗](https://api.reactrouter.com/v7/functions/react-router.createSessionStorage.html)\n\nCreates a SessionStorage object using a SessionIdStorageStrategy.\n\nNote: This is a low-level API that should only be used if none of the\nexisting session storage options meet your requirements.\n"
  },
  {
    "path": "docs/api/utils/data.md",
    "content": "---\ntitle: data\n---\n\n# data\n\n<!--\n⚠️ ⚠️ IMPORTANT ⚠️ ⚠️ \n\nThank you for helping improve our documentation!\n\nThis file is auto-generated from the JSDoc comments in the source\ncode, so please edit the JSDoc comments in the file below and this\nfile will be re-generated once those changes are merged.\n\nhttps://github.com/remix-run/react-router/blob/main/packages/react-router/lib/router/utils.ts\n-->\n\n[MODES: framework, data]\n\n## Summary\n\n[Reference Documentation ↗](https://api.reactrouter.com/v7/functions/react-router.data.html)\n\nCreate \"responses\" that contain `headers`/`status` without forcing\nserialization into an actual [`Response`](https://developer.mozilla.org/en-US/docs/Web/API/Response)\n\n```tsx\nimport { data } from \"react-router\";\n\nexport async function action({ request }: Route.ActionArgs) {\n  let formData = await request.formData();\n  let item = await createItem(formData);\n  return data(item, {\n    headers: { \"X-Custom-Header\": \"value\" }\n    status: 201,\n  });\n}\n```\n\n## Signature\n\n```tsx\nfunction data<D>(data: D, init?: number | ResponseInit)\n```\n\n## Params\n\n### data\n\nThe data to be included in the response.\n\n### init\n\nThe status code or a `ResponseInit` object to be included in the response.\n\n## Returns\n\nA `DataWithResponseInit` instance containing the data and\nresponse init.\n\n"
  },
  {
    "path": "docs/api/utils/generatePath.md",
    "content": "---\ntitle: generatePath\n---\n\n# generatePath\n\n<!--\n⚠️ ⚠️ IMPORTANT ⚠️ ⚠️ \n\nThank you for helping improve our documentation!\n\nThis file is auto-generated from the JSDoc comments in the source\ncode, so please edit the JSDoc comments in the file below and this\nfile will be re-generated once those changes are merged.\n\nhttps://github.com/remix-run/react-router/blob/main/packages/react-router/lib/router/utils.ts\n-->\n\n[MODES: framework, data, declarative]\n\n## Summary\n\n[Reference Documentation ↗](https://api.reactrouter.com/v7/functions/react-router.generatePath.html)\n\nReturns a path with params interpolated.\n\n```tsx\nimport { generatePath } from \"react-router\";\n\ngeneratePath(\"/users/:id\", { id: \"123\" }); // \"/users/123\"\n```\n\n## Signature\n\n```tsx\nfunction generatePath<Path extends string>(\n  originalPath: Path,\n  params: {\n    [key in PathParam<Path>]: string | null;\n  } =  as any,\n): string {}\n```\n\n## Params\n\n### originalPath\n\nThe original path to generate.\n\n### params\n\nThe parameters to interpolate into the path.\n\n## Returns\n\nThe generated path with parameters interpolated.\n\n"
  },
  {
    "path": "docs/api/utils/href.md",
    "content": "---\ntitle: href\n---\n\n# href\n\n[MODES: framework]\n\n## Summary\n\n[Reference Documentation ↗](https://api.reactrouter.com/v7/functions/react-router.href.html)\n\nReturns a resolved URL path for the specified route.\n\n```tsx\nconst h = href(\"/:lang?/about\", { lang: \"en\" })\n// -> `/en/about`\n\n<Link to={href(\"/products/:id\", { id: \"abc123\" })} />\n```\n"
  },
  {
    "path": "docs/api/utils/index.md",
    "content": "---\ntitle: Utils\norder: 8\n---\n"
  },
  {
    "path": "docs/api/utils/isCookie.md",
    "content": "---\ntitle: isCookie\n---\n\n# isCookie\n\n[MODES: framework, data]\n\n## Summary\n\n[Reference Documentation ↗](https://api.reactrouter.com/v7/functions/react-router.isCookie.html)\n\nReturns true if an object is a Remix cookie container.\n"
  },
  {
    "path": "docs/api/utils/isRouteErrorResponse.md",
    "content": "---\ntitle: isRouteErrorResponse\n---\n\n# isRouteErrorResponse\n\n<!--\n⚠️ ⚠️ IMPORTANT ⚠️ ⚠️ \n\nThank you for helping improve our documentation!\n\nThis file is auto-generated from the JSDoc comments in the source\ncode, so please edit the JSDoc comments in the file below and this\nfile will be re-generated once those changes are merged.\n\nhttps://github.com/remix-run/react-router/blob/main/packages/react-router/lib/router/utils.ts\n-->\n\n[MODES: framework, data]\n\n## Summary\n\n[Reference Documentation ↗](https://api.reactrouter.com/v7/functions/react-router.isRouteErrorResponse.html)\n\nCheck if the given error is an [`ErrorResponse`](https://api.reactrouter.com/v7/types/react-router.ErrorResponse.html) generated from a 4xx/5xx\n[`Response`](https://developer.mozilla.org/en-US/docs/Web/API/Response)\nthrown from an [`action`](../../start/framework/route-module#action) or\n[`loader`](../../start/framework/route-module#loader) function.\n\n```tsx\nimport { isRouteErrorResponse } from \"react-router\";\n\nexport function ErrorBoundary({ error }: Route.ErrorBoundaryProps) {\n  if (isRouteErrorResponse(error)) {\n    return (\n      <>\n        <p>Error: `${error.status}: ${error.statusText}`</p>\n        <p>{error.data}</p>\n      </>\n    );\n  }\n\n  return (\n    <p>Error: {error instanceof Error ? error.message : \"Unknown Error\"}</p>\n  );\n}\n```\n\n## Signature\n\n```tsx\nfunction isRouteErrorResponse(error: any): error is ErrorResponse\n```\n\n## Params\n\n### error\n\nThe error to check.\n\n## Returns\n\n`true` if the error is an [`ErrorResponse`](https://api.reactrouter.com/v7/types/react-router.ErrorResponse.html), `false` otherwise.\n\n"
  },
  {
    "path": "docs/api/utils/isSession.md",
    "content": "---\ntitle: isSession\n---\n\n# isSession\n\n[MODES: framework, data]\n\n## Summary\n\n[Reference Documentation ↗](https://api.reactrouter.com/v7/functions/react-router.isSession.html)\n\nReturns true if an object is a React Router session.\n"
  },
  {
    "path": "docs/api/utils/matchPath.md",
    "content": "---\ntitle: matchPath\n---\n\n# matchPath\n\n<!--\n⚠️ ⚠️ IMPORTANT ⚠️ ⚠️ \n\nThank you for helping improve our documentation!\n\nThis file is auto-generated from the JSDoc comments in the source\ncode, so please edit the JSDoc comments in the file below and this\nfile will be re-generated once those changes are merged.\n\nhttps://github.com/remix-run/react-router/blob/main/packages/react-router/lib/router/utils.ts\n-->\n\n[MODES: framework, data, declarative]\n\n## Summary\n\n[Reference Documentation ↗](https://api.reactrouter.com/v7/functions/react-router.matchPath.html)\n\nPerforms pattern matching on a URL pathname and returns information about\nthe match.\n\n## Signature\n\n```tsx\nfunction matchPath<ParamKey extends ParamParseKey<Path>, Path extends string>(\n  pattern: PathPattern<Path> | Path,\n  pathname: string,\n): PathMatch<ParamKey> | null\n```\n\n## Params\n\n### pattern\n\nThe pattern to match against the URL pathname. This can be a string or a [`PathPattern`](https://api.reactrouter.com/v7/interfaces/react-router.PathPattern.html) object. If a string is provided, it will be\ntreated as a pattern with `caseSensitive` set to `false` and `end` set to\n`true`.\n\n### pathname\n\nThe URL pathname to match against the pattern.\n\n## Returns\n\nA path match object if the pattern matches the pathname,\nor `null` if it does not match.\n\n"
  },
  {
    "path": "docs/api/utils/matchRoutes.md",
    "content": "---\ntitle: matchRoutes\n---\n\n# matchRoutes\n\n<!--\n⚠️ ⚠️ IMPORTANT ⚠️ ⚠️ \n\nThank you for helping improve our documentation!\n\nThis file is auto-generated from the JSDoc comments in the source\ncode, so please edit the JSDoc comments in the file below and this\nfile will be re-generated once those changes are merged.\n\nhttps://github.com/remix-run/react-router/blob/main/packages/react-router/lib/router/utils.ts\n-->\n\n[MODES: framework, data, declarative]\n\n## Summary\n\n[Reference Documentation ↗](https://api.reactrouter.com/v7/functions/react-router.matchRoutes.html)\n\nMatches the given routes to a location and returns the match data.\n\n```tsx\nimport { matchRoutes } from \"react-router\";\n\nlet routes = [{\n  path: \"/\",\n  Component: Root,\n  children: [{\n    path: \"dashboard\",\n    Component: Dashboard,\n  }]\n}];\n\nmatchRoutes(routes, \"/dashboard\"); // [rootMatch, dashboardMatch]\n```\n\n## Signature\n\n```tsx\nfunction matchRoutes<\n  RouteObjectType extends AgnosticRouteObject = AgnosticRouteObject,\n>(\n  routes: RouteObjectType[],\n  locationArg: Partial<Location> | string,\n  basename = \"/\",\n): AgnosticRouteMatch<string, RouteObjectType>[] | null\n```\n\n## Params\n\n### routes\n\nThe array of route objects to match against.\n\n### locationArg\n\nThe location to match against, either a string path or a partial [`Location`](https://api.reactrouter.com/v7/interfaces/react-router.Location.html) object\n\n### basename\n\nOptional base path to strip from the location before matching. Defaults to `/`.\n\n## Returns\n\nAn array of matched routes, or `null` if no matches were found.\n\n"
  },
  {
    "path": "docs/api/utils/parsePath.md",
    "content": "---\ntitle: parsePath\n---\n\n# parsePath\n\n[MODES: framework, data, declarative]\n\n## Summary\n\n[Reference Documentation ↗](https://api.reactrouter.com/v7/functions/react-router.parsePath.html)\n\nParses a string URL path into its separate pathname, search, and hash components.\n\n## Signature\n\n```tsx\nparsePath(path): Partial\n```\n\n## Params\n\n### path\n\n[modes: framework, data, declarative]\n\n_No documentation_\n"
  },
  {
    "path": "docs/api/utils/redirect.md",
    "content": "---\ntitle: redirect\n---\n\n# redirect\n\n<!--\n⚠️ ⚠️ IMPORTANT ⚠️ ⚠️ \n\nThank you for helping improve our documentation!\n\nThis file is auto-generated from the JSDoc comments in the source\ncode, so please edit the JSDoc comments in the file below and this\nfile will be re-generated once those changes are merged.\n\nhttps://github.com/remix-run/react-router/blob/main/packages/react-router/lib/router/utils.ts\n-->\n\n[MODES: framework, data]\n\n## Summary\n\n[Reference Documentation ↗](https://api.reactrouter.com/v7/functions/react-router.redirect.html)\n\nA redirect [`Response`](https://developer.mozilla.org/en-US/docs/Web/API/Response).\nSets the status code and the [`Location`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Location)\nheader. Defaults to [`302 Found`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/302).\n\n```tsx\nimport { redirect } from \"react-router\";\n\nexport async function loader({ request }: Route.LoaderArgs) {\n  if (!isLoggedIn(request))\n    throw redirect(\"/login\");\n  }\n\n  // ...\n}\n```\n\n## Params\n\n### url\n\nThe URL to redirect to.\n\n### init\n\nThe status code or a `ResponseInit` object to be included in the response.\n\n## Returns\n\nA [`Response`](https://developer.mozilla.org/en-US/docs/Web/API/Response)\nobject with the redirect status and [`Location`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Location)\nheader.\n\n"
  },
  {
    "path": "docs/api/utils/redirectDocument.md",
    "content": "---\ntitle: redirectDocument\n---\n\n# redirectDocument\n\n<!--\n⚠️ ⚠️ IMPORTANT ⚠️ ⚠️ \n\nThank you for helping improve our documentation!\n\nThis file is auto-generated from the JSDoc comments in the source\ncode, so please edit the JSDoc comments in the file below and this\nfile will be re-generated once those changes are merged.\n\nhttps://github.com/remix-run/react-router/blob/main/packages/react-router/lib/router/utils.ts\n-->\n\n[MODES: framework, data]\n\n## Summary\n\n[Reference Documentation ↗](https://api.reactrouter.com/v7/functions/react-router.redirectDocument.html)\n\nA redirect [`Response`](https://developer.mozilla.org/en-US/docs/Web/API/Response)\nthat will force a document reload to the new location. Sets the status code\nand the [`Location`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Location)\nheader. Defaults to [`302 Found`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/302).\n\n```tsx filename=routes/logout.tsx\nimport { redirectDocument } from \"react-router\";\n\nimport { destroySession } from \"../sessions.server\";\n\nexport async function action({ request }: Route.ActionArgs) {\n  let session = await getSession(request.headers.get(\"Cookie\"));\n  return redirectDocument(\"/\", {\n    headers: { \"Set-Cookie\": await destroySession(session) }\n  });\n}\n```\n\n## Params\n\n### url\n\nThe URL to redirect to.\n\n### init\n\nThe status code or a `ResponseInit` object to be included in the response.\n\n## Returns\n\nA [`Response`](https://developer.mozilla.org/en-US/docs/Web/API/Response)\nobject with the redirect status and [`Location`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Location)\nheader.\n\n"
  },
  {
    "path": "docs/api/utils/renderMatches.md",
    "content": "---\ntitle: renderMatches\n---\n\n# renderMatches\n\n<!--\n⚠️ ⚠️ IMPORTANT ⚠️ ⚠️ \n\nThank you for helping improve our documentation!\n\nThis file is auto-generated from the JSDoc comments in the source\ncode, so please edit the JSDoc comments in the file below and this\nfile will be re-generated once those changes are merged.\n\nhttps://github.com/remix-run/react-router/blob/main/packages/react-router/lib/components.tsx\n-->\n\n[MODES: framework, data, declarative]\n\n## Summary\n\n[Reference Documentation ↗](https://api.reactrouter.com/v7/functions/react-router.renderMatches.html)\n\nRenders the result of [`matchRoutes`](../utils/matchRoutes) into a React element.\n\n## Signature\n\n```tsx\nfunction renderMatches(\n  matches: RouteMatch[] | null,\n): React.ReactElement | null\n```\n\n## Params\n\n### matches\n\nThe array of [route matches](https://api.reactrouter.com/v7/interfaces/react-router.RouteMatch.html) to render\n\n## Returns\n\nA React element that renders the matched routes or `null` if no matches\n\n"
  },
  {
    "path": "docs/api/utils/replace.md",
    "content": "---\ntitle: replace\n---\n\n# replace\n\n<!--\n⚠️ ⚠️ IMPORTANT ⚠️ ⚠️ \n\nThank you for helping improve our documentation!\n\nThis file is auto-generated from the JSDoc comments in the source\ncode, so please edit the JSDoc comments in the file below and this\nfile will be re-generated once those changes are merged.\n\nhttps://github.com/remix-run/react-router/blob/main/packages/react-router/lib/router/utils.ts\n-->\n\n[MODES: framework, data]\n\n## Summary\n\n[Reference Documentation ↗](https://api.reactrouter.com/v7/functions/react-router.replace.html)\n\nA redirect [`Response`](https://developer.mozilla.org/en-US/docs/Web/API/Response)\nthat will perform a [`history.replaceState`](https://developer.mozilla.org/en-US/docs/Web/API/History/replaceState)\ninstead of a [`history.pushState`](https://developer.mozilla.org/en-US/docs/Web/API/History/pushState)\nfor client-side navigation redirects. Sets the status code and the [`Location`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Location)\nheader. Defaults to [`302 Found`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/302).\n\n```tsx\nimport { replace } from \"react-router\";\n\nexport async function loader() {\n  return replace(\"/new-location\");\n}\n```\n\n## Params\n\n### url\n\nThe URL to redirect to.\n\n### init\n\nThe status code or a `ResponseInit` object to be included in the response.\n\n## Returns\n\nA [`Response`](https://developer.mozilla.org/en-US/docs/Web/API/Response)\nobject with the redirect status and [`Location`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Location)\nheader.\n\n"
  },
  {
    "path": "docs/api/utils/resolvePath.md",
    "content": "---\ntitle: resolvePath\n---\n\n# resolvePath\n\n<!--\n⚠️ ⚠️ IMPORTANT ⚠️ ⚠️ \n\nThank you for helping improve our documentation!\n\nThis file is auto-generated from the JSDoc comments in the source\ncode, so please edit the JSDoc comments in the file below and this\nfile will be re-generated once those changes are merged.\n\nhttps://github.com/remix-run/react-router/blob/main/packages/react-router/lib/router/utils.ts\n-->\n\n[MODES: framework, data, declarative]\n\n## Summary\n\n[Reference Documentation ↗](https://api.reactrouter.com/v7/functions/react-router.resolvePath.html)\n\nReturns a resolved [`Path`](https://api.reactrouter.com/v7/interfaces/react-router.Path.html) object relative to the given pathname.\n\n## Signature\n\n```tsx\nfunction resolvePath(to: To, fromPathname = \"/\"): Path\n```\n\n## Params\n\n### to\n\nThe path to resolve, either a string or a partial [`Path`](https://api.reactrouter.com/v7/interfaces/react-router.Path.html) object.\n\n### fromPathname\n\nThe pathname to resolve the path from. Defaults to `/`.\n\n## Returns\n\nA [`Path`](https://api.reactrouter.com/v7/interfaces/react-router.Path.html) object with the resolved pathname, search, and hash.\n\n"
  },
  {
    "path": "docs/community/api-development-strategy.md",
    "content": "---\ntitle: API Development Strategy\n---\n\n# API Development Strategy\n\nReact Router is foundational to your application. We want to make sure that upgrading to new major versions is as smooth as possible while still allowing us to adjust and enhance the behavior and API as the React ecosystem advances.\n\nOur strategy and motivations are discussed in more detail in our [Future Flags][future-flags-blog-post] blog post and our [Open Governance Model][governance].\n\n## Future Flags\n\nWhen an API changes in a breaking way, it is introduced in a future flag. This allows you to opt-in to one change a time before it becomes the default in the next major version.\n\n- Without enabling the future flag, nothing changes about your app\n- Enabling the flag changes the behavior for that feature\n\nAll current future flags are documented in the [Future Flags Guide](../upgrading/future) to help you stay up-to-date.\n\n## Unstable Flags\n\nUnstable flags are for features still being designed and developed and made available to our users to help us get it right.\n\nUnstable flags are not recommended for production:\n\n- they will change without warning and without upgrade paths\n- they will have bugs\n- they aren't documented\n- they may be scrapped completely\n\nWhen you opt-in to an unstable flag you are becoming a contributor to the project, rather than a user. We appreciate your help, but please be aware of the new role!\n\nBecause unstable flags are experimental and not guaranteed to stick around, we ship them in SemVer patch releases because they're not new _stable_/_documented_ APIs. When an unstable flag stabilizes into a Future Flag, that will be released in a SemVer minor release and will be properly documented and added to the [Future Flags Guide](../upgrading/future).\n\nTo learn about current unstable flags, keep an eye on the [CHANGELOG](../start/changelog).\n\n### Example New Feature Flow\n\nThe decision flow for a new feature looks something like this:\n\n<img width=\"400\" src=\"https://reactrouter.com/_docs/feature-flowchart.png\" alt=\"Flowchart of the decision process for how to introduce a new feature\" />\n\n[future-flags-blog-post]: https://remix.run/blog/future-flags\n[governance]: https://github.com/remix-run/react-router/blob/main/GOVERNANCE.md#new-feature-process\n"
  },
  {
    "path": "docs/community/contributing.md",
    "content": "---\ntitle: Contributing\n---\n\n# Contributing to React Router\n\nThanks for contributing, you rock!\n\nWhen it comes to open source, there are many different kinds of contributions that can be made, all of which are valuable. Here are a few guidelines that should help you as you prepare your contribution.\n\n## Open Governance Model\n\nBefore going any further, please read the Open Governance [blog post](https://remix.run/blog/rr-governance) and [document](https://github.com/remix-run/react-router/blob/main/GOVERNANCE.md) for information on how we handle bugs/issues/feature proposals in React Router.\n\n## Setup\n\nBefore you can contribute to the codebase, you will need to fork the repo. This will look a bit different depending on what type of contribution you are making:\n\n- All new features, bug-fixes, or **anything that touches `react-router` code** should be branched off of and merged into the `dev` branch\n- Changes that only touch documentation can be branched off of and merged into the `main` branch\n\nThe following steps will get you set up to contribute changes to this repo:\n\n1. Fork the repo (click the <kbd>Fork</kbd> button at the top right of [this page](https://github.com/remix-run/react-router))\n2. Clone your fork locally\n\n   ```bash\n   # in a terminal, cd to parent directory where you want your clone to be, then\n   git clone https://github.com/<your_github_username>/react-router.git\n   cd react-router\n\n   # if you are making *any* code changes, make sure to checkout the dev branch\n   git checkout dev\n   ```\n\n3. Install dependencies and build. React Router uses [pnpm](https://pnpm.io), so you should too. If you install using `npm`, unnecessary `package-lock.json` files will be generated.\n\n## Think You Found a Bug?\n\nPlease conform to the issue template and provide a **minimal** and **runnable** reproduction. Best is a pull request with a [failing test](https://github.com/remix-run/react-router/blob/dev/integration/bug-report-test.ts). Next best is a link to [StackBlitz](https://reactrouter.com/new), CodeSandbox, or GitHub repository that illustrates the bug.\n\n## Issue Not Getting Attention?\n\nIf you need a bug fixed and nobody is fixing it, your best bet is to provide a fix for it and make a [pull request](https://help.github.com/en/github/collaborating-with-issues-and-pull-requests/creating-a-pull-request). Open source code belongs to all of us, and it's all of our responsibility to push it forward.\n\n## Proposing New or Changed API?\n\n⚠️ _Please do not start with a PR for a new feature._\n\nNew features need to go through the process outlined in the [Open Governance Model](https://github.com/remix-run/react-router/blob/main/GOVERNANCE.md#new-feature-process) and can be started by opening a [Proposal Discussion](https://github.com/remix-run/react-router/discussions/new?category=proposals) on GitHub. Please provide thoughtful comments and some sample code that show what you'd like to do with React Router in your app. It helps the conversation if you can show us how you're limited by the current API first before jumping to a conclusion about what needs to be changed and/or added.\n\nWe have learned by experience that small APIs are usually better, so we may be a little reluctant to add something new unless there's an obvious limitation with the current API. That being said, we are always anxious to hear about cases that we just haven't considered before, so please don't be shy! :)\n\n## Adding an Example?\n\nExamples can be added directly to the `main` branch. Create a branch off of your local clone of `main`. Once you've finished, create a pull request and outline your example.\n\n## Making a Pull Request?\n\nPull requests need only the approval of two or more collaborators to be merged; when the PR author is a collaborator, that counts as one.\n\n<docs-warning>When creating the PR in GitHub, make sure that you set the base to the correct branch. If you are submitting a PR that touches any code, this should be the `dev` branch. You set the base in GitHub when authoring the PR with the dropdown below the \"Compare changes\" heading: <img src=\"https://raw.githubusercontent.com/remix-run/react-router/main/static/base-branch.png\" alt=\"\" width=\"460\" height=\"350\" /></docs-warning>\n\n### Tests\n\nAll commits that fix bugs or add features need one or more tests.\n\n<docs-error>Do not merge code without tests!</docs-error>\n\n### Docs + Examples\n\nAll commits that change or add to the API must be done in a pull request that also updates all relevant examples and docs.\n\nDocumentation is located in the `docs` directory. Once changes make their way into the `main` branch, they will automatically be published to the docs site.\n\nIf you want to preview how the changes will look on the docs site, clone the [`react-router-website` repository](https://github.com/remix-run/react-router-website) and follow the instructions in `README.md` to view your changes locally.\n\n## Development\n\n### Packages\n\nReact Router uses a monorepo to host code for multiple packages. These packages live in the `packages` directory.\n\nWe use [pnpm workspaces](https://pnpm.io/workspaces/) to manage installation of dependencies and running various scripts. To get everything installed, make sure you have [pnpm installed](https://pnpm.io/installation), and then run `pnpm install` from the repo root.\n\n### Building\n\nCalling `pnpm build` from the root directory will run the build, which should take only a few seconds. It's important to build all the packages together because the individual packages have dependencies on one another.\n\n### Testing\n\nBefore running the tests, you need to run a build. After you build, running `pnpm test` from the root directory will run **every** package's tests. If you want to run tests for a specific package, use `pnpm test packages/<package-name>/`:\n\n```bash\n# Test all packages\npnpm test\n\n# Test only @react-router/dev\npnpm test packages/react-router-dev/\n```\n\n## Repository Branching\n\nThis repo maintains separate branches for different purposes. They will look something like this:\n\n```\n- main   > the most recent release and current docs\n- dev    > code under active development between stable releases\n- v6     > the most recent code for a specific major release\n```\n\nThere may be other branches for various features and experimentation, but all of the magic happens from these branches.\n\n## Releases\n\nPlease refer to [DEVELOPMENT.md](https://github.com/remix-run/react-router/blob/main/DEVELOPMENT.md) for an outline of the release process.\n"
  },
  {
    "path": "docs/community/index.md",
    "content": "---\ntitle: Community\norder: 6\n---\n"
  },
  {
    "path": "docs/elements.md",
    "content": "---\ntitle: Markdown Elements\nhidden: true\n---\n\n# Markdown Elements\n\nThis is for testing all the different kinds of markdown that can exist. Whenever I find a styling edge case that exists, I add it to this document. It’s my form of visual regression for all the different kinds of elements that need to be styled across different contexts.\n\n## Headings\n\nHeadings at sizes 4, 5, and 6 are all treated equally. If we start writing prose that needs those headings, we should re-evaluate our lives.\n\n# Heading 1\n\n## Heading 2\n\n### Heading 3\n\n#### Heading 4\n\n##### Heading 5\n\n###### Heading 6\n\n## Tables\n\n| Syntax | Description |\n| ------ | ----------- |\n| Row 1  | Column 2    |\n| Row 2  | Column 2    |\n| Row 3  | Column 2    |\n\n## Callouts\n\nCallouts can be used with the `<docs-*>` elements. They are specifically for calling special attention to pieces of information outside the normal flow of the document.\n\nThere are three supported variations of these elements:\n\n1. `<docs-info>` - For general callouts to bits of information.\n2. `<docs-warning>` - For warning the read about something they should know.\n3. `<docs-error>` - For telling the user they shouldn’t be doing something.\n\nExamples:\n\n<docs-info>`<Link to>` with a `..` behaves differently from a normal `<a href>` when the current URL ends with `/`. `<Link to>` ignores the trailing slash, and removes one URL segment for each `..`. But an `<a href>` value handles `..` differently when the current URL ends with `/` vs when it does not.</docs-info>\n\n<docs-warning>`useMatches` only works with a data router like [`createBrowserRouter`][createbrowserrouter], since they know the full route tree up front and can provide all of the current matches. Additionally, `useMatches` will not match down into any descendant route trees since the router isn't aware of the descendant routes.</docs-warning>\n\n<docs-error>Do not do this</docs-error>\n\n<docs-info>The markup for this is kind of ugly, because (currently) these all have to be inside the `<docs-*>` element without any line breaks _but_ it is possible there could be an image inside these. <img src=\"https://picsum.photos/480/270\" width=\"480\" height=\"270\" /></docs-info>\n\nNote: maybe the semantics for these aren't quite right. There might be other nouns that make sense in the case of docs, like:\n\n- `<docs-info>` could become `<docs-tip>`\n- `<docs-warning>` could become `<docs-important>`\n- `<docs-error>` could become `<docs-warning>` or `<docs-danger>`\n\n## Blockquotes\n\nThis is a `<blockquote>` with multiple lines and styles in it:\n\n> This is my quote.\n>\n> It can have [links]($link), **bold text**, _italic text_, and even `<code>`, all of which should be accounted for. Oh, and don't forget lists:\n>\n> - List item 1\n> - List item 2\n> - List item 3\n>\n> Unordered, or ordered:\n>\n> 1. List item\n> 2. Another list item\n> 3. Yet another list item\n\n## Lists\n\nThis is a list of links, some of which are code:\n\n- This is my first list item\n- [This is my second list item that’s a link][$link]\n- This is my third item that has `<code>` and [`<LinkedCode>` mixed with text][$link]\n\nAnd don't forget about proper styling for `<a>` tags that don’t have an `href`: <a>like this link right here</a>.\n\nAnd then there’s the `<dl>` lists:\n\n<dl>\n  <dt>React</dt>\n  <dd>Respond or behave in a particular way in response to something</dd>\n  <dt>Router</dt>\n  <dd>A device that forwards data packets to the appropriate parts of a computer network.</dd>\n  <dt>Library</dt>\n  <dd>A building or room containing collections of books, periodicals, and sometimes films and recorded music for people to read, borrow, or refer to.</dd>\n  <dd>A collection of programs and software packages made generally available, often loaded and stored on disk for immediate use.</dd>\n</dl>\n\n## Code\n\nNormal code:\n\n```tsx\n<WhateverRouter initialEntries={[\"/events/123\"]}>\n  <Route path=\"/\" element={<Root />} loader={rootLoader}>\n    <Route\n      path=\"events/:id\"\n      element={<Event />}\n      loader={eventLoader}\n    />\n  </Route>\n</WhateverRouter>\n```\n\nWith multiple highlighted lines:\n\n```tsx lines=[1-2,5]\n<WhateverRouter initialEntries={[\"/events/123\"]}>\n  <Route path=\"/\" element={<Root />} loader={rootLoader}>\n    <Route\n      path=\"events/:id\"\n      element={<Event />}\n      loader={eventLoader}\n    />\n  </Route>\n</WhateverRouter>\n```\n\nWith a filename:\n\n```tsx filename=src/main.jsx\n<WhateverRouter initialEntries={[\"/events/123\"]}>\n  <Route path=\"/\" element={<Root />} loader={rootLoader}>\n    <Route\n      path=\"events/:id\"\n      element={<Event />}\n      loader={eventLoader}\n    />\n  </Route>\n</WhateverRouter>\n```\n\nBad code:\n\n```tsx bad\n<WhateverRouter initialEntries={[\"/events/123\"]}>\n  <Route path=\"/\" element={<Root />} loader={rootLoader}>\n    <Route\n      path=\"events/:id\"\n      element={<Event />}\n      loader={eventLoader}\n    />\n  </Route>\n</WhateverRouter>\n```\n\nBad code with highlighted lines and a filename:\n\n```tsx filename=src/main.jsx bad lines=[2-5]\n<WhateverRouter initialEntries={[\"/events/123\"]}>\n  <Routes>\n    <Route path=\"/\" element={<Root />} loader={rootLoader}>\n      <Route\n        path=\"events/:id\"\n        element={<Event />}\n        loader={eventLoader}\n      />\n    </Route>\n  </Routes>\n</WhateverRouter>\n```\n\nLines that overflow:\n\n```html\n<!-- Other HTML for your app goes here -->\n<!-- prettier-ignore -->\n<script src=\"https://unpkg.com/react@>=16.8/umd/react.development.js\" crossorigin></script>\n```\n\n---\n\n[$link]: https://www.youtube.com/watch?v=dQw4w9WgXcQ\n[createbrowserrouter]: ./routers/create-browser-router\n"
  },
  {
    "path": "docs/explanation/README",
    "content": "Explanations:\n\n- Theoretical Knowledge\n- Understanding Oriented\n- Useful when we're studying\n"
  },
  {
    "path": "docs/explanation/backend-for-frontend.md",
    "content": "---\ntitle: Backend For Frontend\n---\n\n# Backend For Frontend\n\n[MODES: framework]\n\n<br/>\n<br/>\n\nWhile React Router can serve as your fullstack application, it also fits perfectly into the \"Backend for Frontend\" architecture.\n\nThe BFF strategy employs a web server with a job scoped to serving the frontend web app and connecting it to the services it needs: your database, mailer, job queues, existing backend APIs (REST, GraphQL), etc. Instead of your UI integrating directly from the browser to these services, it connects to the BFF, and the BFF connects to your services.\n\nMature apps already have a lot of backend application code in Ruby, Elixir, PHP, etc., and there's no reason to justify migrating it all to a server-side JavaScript runtime just to get the benefits of React Router. Instead, you can use your React Router app as a backend for your frontend.\n\nYou can use `fetch` right from your loaders and actions to your backend.\n\n```tsx lines=[7,13,17]\nimport escapeHtml from \"escape-html\";\n\nexport async function loader() {\n  const apiUrl = \"https://api.example.com/some-data.json\";\n  const res = await fetch(apiUrl, {\n    headers: {\n      Authorization: `Bearer ${process.env.API_TOKEN}`,\n    },\n  });\n\n  const data = await res.json();\n\n  const prunedData = data.map((record) => {\n    return {\n      id: record.id,\n      title: record.title,\n      formattedBody: escapeHtml(record.content),\n    };\n  });\n  return { prunedData };\n}\n```\n\nThere are several benefits of this approach vs. fetching directly from the browser. The highlighted lines above show how you can:\n\n1. Simplify third-party integrations and keep tokens and secrets out of client bundles\n2. Prune the data down to send less kB over the network, speeding up your app significantly\n3. Move a lot of code from browser bundles to the server, like `escapeHtml`, which speeds up your app. Additionally, moving code to the server usually makes your code easier to maintain since server-side code doesn't have to worry about UI states for async operations\n\nAgain, React Router can be used as your only server by talking directly to the database and other services with server-side JavaScript APIs, but it also works perfectly as a backend for your frontend. Go ahead and keep your existing API server for application logic and let React Router connect the UI to it.\n"
  },
  {
    "path": "docs/explanation/code-splitting.md",
    "content": "---\ntitle: Automatic Code Splitting\n---\n\n# Automatic Code Splitting\n\n[MODES: framework]\n\n<br/>\n<br/>\n\nWhen using React Router's framework features, your application is automatically code split to improve the performance of initial load times when users visit your application.\n\n## Code Splitting by Route\n\nConsider this simple route config:\n\n```tsx filename=app/routes.ts\nimport {\n  type RouteConfig,\n  route,\n} from \"@react-router/dev/routes\";\n\nexport default [\n  route(\"/contact\", \"./contact.tsx\"),\n  route(\"/about\", \"./about.tsx\"),\n] satisfies RouteConfig;\n```\n\nInstead of bundling all routes into a single giant build, the modules referenced (`contact.tsx` and `about.tsx`) become entry points to the bundler.\n\nBecause these entry points are coupled to URL segments, React Router knows just from a URL which bundles are needed in the browser, and more importantly, which are not.\n\nIf the user visits `\"/about\"` then the bundles for `about.tsx` will be loaded but not `contact.tsx`. This drastically reduces the JavaScript footprint for initial page loads and speeds up your application.\n\n## Removal of Server Code\n\nAny server-only [Route Module APIs][route-module] will be removed from the bundles. Consider this route module:\n\n```tsx\nexport async function loader() {\n  return { message: \"hello\" };\n}\n\nexport async function action() {\n  console.log(Date.now());\n  return { ok: true };\n}\n\nexport async function headers() {\n  return { \"Cache-Control\": \"max-age=300\" };\n}\n\nexport default function Component({ loaderData }) {\n  return <div>{loaderData.message}</div>;\n}\n```\n\nAfter building for the browser, only the `Component` will still be in the bundle, so you can use server-only code in the other module exports.\n\n[route-module]: ../../start/framework/route-module\n"
  },
  {
    "path": "docs/explanation/concurrency.md",
    "content": "---\ntitle: Network Concurrency Management\n---\n\n# Network Concurrency Management\n\n[MODES: framework, data]\n\n<br/>\n<br/>\n\nWhen building web applications, managing network requests can be a daunting task. The challenges of ensuring up-to-date data and handling simultaneous requests often lead to complex logic in the application to deal with interruptions and race conditions. React Router simplifies this process by automating network management while mirroring and expanding upon the intuitive behavior of web browsers.\n\nTo help understand how React Router handles concurrency, it's important to remember that after `form` submissions, React Router will fetch fresh data from the `loader`s. This is called revalidation.\n\n## Natural Alignment with Browser Behavior\n\nReact Router's handling of network concurrency is heavily inspired by the default behavior of web browsers when processing documents.\n\n### Link Navigation\n\n**Browser Behavior**: When you click on a link in a browser and then click on another before the page transition completes, the browser prioritizes the most recent `action`. It cancels the initial request, focusing solely on the latest link clicked.\n\n**React Router Behavior**: React Router manages client-side navigation the same way. When a link is clicked within a React Router application, it initiates fetch requests for each `loader` tied to the target URL. If another navigation interrupts the initial navigation, React Router cancels the previous fetch requests, ensuring that only the latest requests proceed.\n\n### Form Submission\n\n**Browser Behavior**: If you initiate a form submission in a browser and then quickly submit another form again, the browser disregards the first submission, processing only the latest one.\n\n**React Router Behavior**: React Router mimics this behavior when working with forms. If a form is submitted and another submission occurs before the first completes, React Router cancels the original fetch requests. It then waits for the latest submission to complete before triggering page revalidation again.\n\n## Concurrent Submissions and Revalidation\n\nWhile standard browsers are limited to one request at a time for navigations and form submissions, React Router elevates this behavior. Unlike navigation, with [`useFetcher`][use_fetcher] multiple requests can be in flight simultaneously.\n\nReact Router is designed to handle multiple form submissions to server `action`s and concurrent revalidation requests efficiently. It ensures that as soon as new data is available, the state is updated promptly. However, React Router also safeguards against potential pitfalls by refraining from committing stale data when other `action`s introduce race conditions.\n\nFor instance, if three form submissions are in progress, and one completes, React Router updates the UI with that data immediately without waiting for the other two so that the UI remains responsive and dynamic. As the remaining submissions finalize, React Router continues to update the UI, ensuring that the most recent data is displayed.\n\nUsing this key:\n\n- `|`: Submission begins\n- ✓: Action complete, data revalidation begins\n- ✅: Revalidated data is committed to the UI\n- ❌: Request cancelled\n\nWe can visualize this scenario in the following diagram:\n\n```text\nsubmission 1: |----✓-----✅\nsubmission 2:    |-----✓-----✅\nsubmission 3:             |-----✓-----✅\n```\n\nHowever, if a subsequent submission's revalidation completes before an earlier one, React Router discards the earlier data, ensuring that only the most up-to-date information is reflected in the UI:\n\n```text\nsubmission 1: |----✓---------❌\nsubmission 2:    |-----✓-----✅\nsubmission 3:             |-----✓-----✅\n```\n\nBecause the revalidation from submission (2) started later and landed earlier than submission (1), the requests from submission (1) are canceled and only the data from submission (2) is committed to the UI. It was requested later, so it's more likely to contain the updated values from both (1) and (2).\n\n## Potential for Stale Data\n\nIt's unlikely your users will ever experience this, but there are still chances for the user to see stale data in very rare conditions with inconsistent infrastructure. Even though React Router cancels requests for stale data, they will still end up making it to the server. Canceling a request in the browser simply releases browser resources for that request; it can't \"catch up\" and stop it from getting to the server. In extremely rare conditions, a canceled request may change data after the interrupting `action`'s revalidation lands. Consider this diagram:\n\n```text\n     👇 interruption with new submission\n|----❌----------------------✓\n       |-------✓-----✅\n                             👆\n                  initial request reaches the server\n                  after the interrupting submission\n                  has completed revalidation\n```\n\nThe user is now looking at different data than what is on the server. Note that this problem is both extremely rare and exists with default browser behavior, too. The chance of the initial request reaching the server later than both the submission and revalidation of the second is unexpected on any network and server infrastructure. If this is a concern with your infrastructure, you can send timestamps with your form submissions and write server logic to ignore stale submissions.\n\n## Example\n\nIn UI components like comboboxes, each keystroke can trigger a network request. Managing such rapid, consecutive requests can be tricky, especially when ensuring that the displayed results match the most recent query. However, with React Router, this challenge is automatically handled, ensuring that users see the correct results without developers having to micromanage the network.\n\n```tsx filename=app/pages/city-search.tsx\nexport async function loader({ request }) {\n  const { searchParams } = new URL(request.url);\n  const cities = await searchCities(searchParams.get(\"q\"));\n  return cities;\n}\n\nexport function CitySearchCombobox() {\n  const fetcher = useFetcher<typeof loader>();\n\n  return (\n    <fetcher.Form action=\"/city-search\">\n      <Combobox aria-label=\"Cities\">\n        <ComboboxInput\n          name=\"q\"\n          onChange={(event) =>\n            // submit the form onChange to get the list of cities\n            fetcher.submit(event.target.form)\n          }\n        />\n\n        {/* render with the loader's data */}\n        {fetcher.data ? (\n          <ComboboxPopover className=\"shadow-popup\">\n            {fetcher.data.length > 0 ? (\n              <ComboboxList>\n                {fetcher.data.map((city) => (\n                  <ComboboxOption\n                    key={city.id}\n                    value={city.name}\n                  />\n                ))}\n              </ComboboxList>\n            ) : (\n              <span>No results found</span>\n            )}\n          </ComboboxPopover>\n        ) : null}\n      </Combobox>\n    </fetcher.Form>\n  );\n}\n```\n\nAll the application needs to know is how to query the data and how to render it. React Router handles the network.\n\n## Conclusion\n\nReact Router offers developers an intuitive, browser-based approach to managing network requests. By mirroring browser behaviors and enhancing them where needed, it simplifies the complexities of concurrency, revalidation, and potential race conditions. Whether you're building a simple webpage or a sophisticated web application, React Router ensures that your user interactions are smooth, reliable, and always up to date.\n\n[use_fetcher]: ../api/hooks/useFetcher\n"
  },
  {
    "path": "docs/explanation/form-vs-fetcher.md",
    "content": "---\ntitle: Form vs. fetcher\n---\n\n# Form vs. fetcher\n\n[MODES: framework, data]\n\n## Overview\n\nDeveloping in React Router offers a rich set of tools that can sometimes overlap in functionality, creating a sense of ambiguity for newcomers. The key to effective development in React Router is understanding the nuances and appropriate use cases for each tool. This document seeks to provide clarity on when and why to use specific APIs.\n\n## APIs in Focus\n\n- [`<Form>`][form-component]\n- [`useFetcher`][use-fetcher]\n- [`useNavigation`][use-navigation]\n\nUnderstanding the distinctions and intersections of these APIs is vital for efficient and effective React Router development.\n\n## URL Considerations\n\nThe primary criterion when choosing among these tools is whether you want the URL to change or not:\n\n- **URL Change Desired**: When navigating or transitioning between pages, or after certain actions like creating or deleting records. This ensures that the user's browser history accurately reflects their journey through your application.\n  - **Expected Behavior**: In many cases, when users hit the back button, they should be taken to the previous page. Other times the history entry may be replaced but the URL change is important nonetheless.\n\n- **No URL Change Desired**: For actions that don't significantly change the context or primary content of the current view. This might include updating individual fields or minor data manipulations that don't warrant a new URL or page reload. This also applies to loading data with fetchers for things like popovers, combo boxes, etc.\n\n### When the URL Should Change\n\nThese actions typically reflect significant changes to the user's context or state:\n\n- **Creating a New Record**: After creating a new record, it's common to redirect users to a page dedicated to that new record, where they can view or further modify it.\n\n- **Deleting a Record**: If a user is on a page dedicated to a specific record and decides to delete it, the logical next step is to redirect them to a general page, such as a list of all records.\n\nFor these cases, developers should consider using a combination of [`<Form>`][form-component] and [`useNavigation`][use-navigation]. These tools can be coordinated to handle form submission, invoke specific actions, retrieve action-related data through component props, and manage navigation respectively.\n\n### When the URL Shouldn't Change\n\nThese actions are generally more subtle and don't require a context switch for the user:\n\n- **Updating a Single Field**: Maybe a user wants to change the name of an item in a list or update a specific property of a record. This action is minor and doesn't necessitate a new page or URL.\n\n- **Deleting a Record from a List**: In a list view, if a user deletes an item, they likely expect to remain on the list view, with that item no longer in the list.\n\n- **Creating a Record in a List View**: When adding a new item to a list, it often makes sense for the user to remain in that context, seeing their new item added to the list without a full page transition.\n\n- **Loading Data for a Popover or Combobox**: When loading data for a popover or combobox, the user's context remains unchanged. The data is loaded in the background and displayed in a small, self-contained UI element.\n\nFor such actions, [`useFetcher`][use-fetcher] is the go-to API. It's versatile, combining functionalities of these APIs, and is perfectly suited for tasks where the URL should remain unchanged.\n\n## API Comparison\n\nAs you can see, the two sets of APIs have a lot of similarities:\n\n| Navigation/URL API            | Fetcher API          |\n| ----------------------------- | -------------------- |\n| `<Form>`                      | `<fetcher.Form>`     |\n| `actionData` (component prop) | `fetcher.data`       |\n| `navigation.state`            | `fetcher.state`      |\n| `navigation.formAction`       | `fetcher.formAction` |\n| `navigation.formData`         | `fetcher.formData`   |\n\n## Examples\n\n### Creating a New Record\n\n```tsx filename=app/pages/new-recipe.tsx lines=[16,23-24,29]\nimport {\n  Form,\n  redirect,\n  useNavigation,\n} from \"react-router\";\nimport type { Route } from \"./+types/new-recipe\";\n\nexport async function action({\n  request,\n}: Route.ActionArgs) {\n  const formData = await request.formData();\n  const errors = await validateRecipeFormData(formData);\n  if (errors) {\n    return { errors };\n  }\n  const recipe = await db.recipes.create(formData);\n  return redirect(`/recipes/${recipe.id}`);\n}\n\nexport function NewRecipe({\n  actionData,\n}: Route.ComponentProps) {\n  const { errors } = actionData || {};\n  const navigation = useNavigation();\n  const isSubmitting =\n    navigation.formAction === \"/recipes/new\";\n\n  return (\n    <Form method=\"post\">\n      <label>\n        Title: <input name=\"title\" />\n        {errors?.title ? <span>{errors.title}</span> : null}\n      </label>\n      <label>\n        Ingredients: <textarea name=\"ingredients\" />\n        {errors?.ingredients ? (\n          <span>{errors.ingredients}</span>\n        ) : null}\n      </label>\n      <label>\n        Directions: <textarea name=\"directions\" />\n        {errors?.directions ? (\n          <span>{errors.directions}</span>\n        ) : null}\n      </label>\n      <button type=\"submit\">\n        {isSubmitting ? \"Saving...\" : \"Create Recipe\"}\n      </button>\n    </Form>\n  );\n}\n```\n\nThe example leverages [`<Form>`][form-component], component props, and [`useNavigation`][use-navigation] to facilitate an intuitive record creation process.\n\nUsing `<Form>` ensures direct and logical navigation. After creating a record, the user is naturally guided to the new recipe's unique URL, reinforcing the outcome of their action.\n\nThe component props bridge server and client, providing immediate feedback on submission issues. This quick response enables users to rectify any errors without hindrance.\n\nLastly, `useNavigation` dynamically reflects the form's submission state. This subtle UI change, like toggling the button's label, assures users that their actions are being processed.\n\nCombined, these APIs offer a balanced blend of structured navigation and feedback.\n\n### Updating a Record\n\nNow consider we're looking at a list of recipes that have delete buttons on each item. When a user clicks the delete button, we want to delete the recipe from the database and remove it from the list without navigating away from the list.\n\nFirst, consider the basic route setup to get a list of recipes on the page:\n\n```tsx filename=app/pages/recipes.tsx\nimport type { Route } from \"./+types/recipes\";\n\nexport async function loader({\n  request,\n}: Route.LoaderArgs) {\n  return {\n    recipes: await db.recipes.findAll({ limit: 30 }),\n  };\n}\n\nexport default function Recipes({\n  loaderData,\n}: Route.ComponentProps) {\n  const { recipes } = loaderData;\n  return (\n    <ul>\n      {recipes.map((recipe) => (\n        <RecipeListItem key={recipe.id} recipe={recipe} />\n      ))}\n    </ul>\n  );\n}\n```\n\nNow we'll look at the action that deletes a recipe and the component that renders each recipe in the list.\n\n```tsx filename=app/pages/recipes.tsx lines=[10,21,27]\nimport { useFetcher } from \"react-router\";\nimport type { Recipe } from \"./recipe.server\";\nimport type { Route } from \"./+types/recipes\";\n\nexport async function action({\n  request,\n}: Route.ActionArgs) {\n  const formData = await request.formData();\n  const id = formData.get(\"id\");\n  await db.recipes.delete(id);\n  return { ok: true };\n}\n\nexport default function Recipes() {\n  return (\n    // ...\n    // doesn't matter, somewhere it's using <RecipeListItem />\n  )\n}\n\nfunction RecipeListItem({ recipe }: { recipe: Recipe }) {\n  const fetcher = useFetcher();\n  const isDeleting = fetcher.state !== \"idle\";\n\n  return (\n    <li>\n      <h2>{recipe.title}</h2>\n      <fetcher.Form method=\"post\">\n        <input type=\"hidden\" name=\"id\" value={recipe.id} />\n        <button disabled={isDeleting} type=\"submit\">\n          {isDeleting ? \"Deleting...\" : \"Delete\"}\n        </button>\n      </fetcher.Form>\n    </li>\n  );\n}\n```\n\nUsing [`useFetcher`][use-fetcher] in this scenario works perfectly. Instead of navigating away or refreshing the entire page, we want in-place updates. When a user deletes a recipe, the `action` is called and the fetcher manages the corresponding state transitions.\n\nThe key advantage here is the maintenance of context. The user stays on the list when the deletion completes. The fetcher's state management capabilities are leveraged to give real-time feedback: it toggles between `\"Deleting...\"` and `\"Delete\"`, providing a clear indication of the ongoing process.\n\nFurthermore, with each `fetcher` having the autonomy to manage its own state, operations on individual list items become independent, ensuring that actions on one item don't affect the others (though revalidation of the page data is a shared concern covered in [Network Concurrency Management][network-concurrency-management]).\n\nIn essence, `useFetcher` offers a seamless mechanism for actions that don't necessitate a change in the URL or navigation, enhancing the user experience by providing real-time feedback and context preservation.\n\n### Mark Article as Read\n\nImagine you want to mark that an article has been read by the current user, after they've been on the page for a while and scrolled to the bottom. You could make a hook that looks something like this:\n\n```tsx\nimport { useFetcher } from \"react-router\";\n\nfunction useMarkAsRead({ articleId, userId }) {\n  const marker = useFetcher();\n\n  useSpentSomeTimeHereAndScrolledToTheBottom(() => {\n    marker.submit(\n      { userId },\n      {\n        action: `/article/${articleId}/mark-as-read`,\n        method: \"post\",\n      },\n    );\n  });\n}\n```\n\n### User Avatar Details Popup\n\nAnytime you show the user avatar, you could put a hover effect that fetches data from a loader and displays it in a popup.\n\n```tsx filename=app/pages/user-details.tsx\nimport { useState, useEffect } from \"react\";\nimport { useFetcher } from \"react-router\";\nimport type { Route } from \"./+types/user-details\";\n\nexport async function loader({ params }: Route.LoaderArgs) {\n  return await fakeDb.user.find({\n    where: { id: params.id },\n  });\n}\n\ntype LoaderData = Route.ComponentProps[\"loaderData\"];\n\nfunction UserAvatar({ partialUser }) {\n  const userDetails = useFetcher<LoaderData>();\n  const [showDetails, setShowDetails] = useState(false);\n\n  useEffect(() => {\n    if (\n      showDetails &&\n      userDetails.state === \"idle\" &&\n      !userDetails.data\n    ) {\n      userDetails.load(`/user-details/${partialUser.id}`);\n    }\n  }, [showDetails, userDetails, partialUser.id]);\n\n  return (\n    <div\n      onMouseEnter={() => setShowDetails(true)}\n      onMouseLeave={() => setShowDetails(false)}\n    >\n      <img src={partialUser.profileImageUrl} />\n      {showDetails ? (\n        userDetails.state === \"idle\" && userDetails.data ? (\n          <UserPopup user={userDetails.data} />\n        ) : (\n          <UserPopupLoading />\n        )\n      ) : null}\n    </div>\n  );\n}\n```\n\n## Conclusion\n\nReact Router offers a range of tools to cater to varied developmental needs. While some functionalities might seem to overlap, each tool has been crafted with specific scenarios in mind. By understanding the intricacies and ideal applications of `<Form>`, `useFetcher`, and `useNavigation`, along with how data flows through component props, developers can create more intuitive, responsive, and user-friendly web applications.\n\n[form-component]: ../api/components/Form\n[use-fetcher]: ../api/hooks/useFetcher\n[use-navigation]: ../api/hooks/useNavigation\n[network-concurrency-management]: ./concurrency\n"
  },
  {
    "path": "docs/explanation/hot-module-replacement.md",
    "content": "---\ntitle: Hot Module Replacement\n---\n\n# Hot Module Replacement\n\n[MODES: framework]\n\n<br/>\n<br/>\n\nHot Module Replacement is a technique for updating modules in your app without needing to reload the page.\nIt's a great developer experience, and React Router supports it when using Vite.\n\nHMR does its best to preserve browser state across updates.\nFor example, let's say you have form within a modal and you fill out all the fields.\nAs soon as you save any changes to the code, traditional live reload would hard refresh the page causing all of those fields to be reset.\nEvery time you make a change, you'd have to open up the modal _again_ and fill out the form _again_.\n\nBut with HMR, all of that state is preserved _across updates_.\n\n## React Fast Refresh\n\nReact already has mechanisms for updating the DOM via its [virtual DOM][virtual-dom] in response to user interactions like clicking a button.\nWouldn't it be great if React could handle updating the DOM in response to code changes too?\n\nThat's exactly what [React Fast Refresh][react-refresh] is all about!\nOf course, React is all about components, not general JavaScript code, so React Fast Refresh only handles hot updates for exported React components.\n\nBut React Fast Refresh does have some limitations that you should be aware of.\n\n### Class Component State\n\nReact Fast Refresh does not preserve state for class components.\nThis includes higher-order components that internally return classes:\n\n```tsx\nexport class ComponentA extends Component {} // ❌\n\nexport const ComponentB = HOC(ComponentC); // ❌ Won't work if HOC returns a class component\n\nexport function ComponentD() {} // ✅\nexport const ComponentE = () => {}; // ✅\nexport default function ComponentF() {} // ✅\n```\n\n### Named Function Components\n\nFunction components must be named, not anonymous, for React Fast Refresh to track changes:\n\n```tsx\nexport default () => {}; // ❌\nexport default function () {} // ❌\n\nconst ComponentA = () => {};\nexport default ComponentA; // ✅\n\nexport default function ComponentB() {} // ✅\n```\n\n### Supported Exports\n\nReact Fast Refresh can only handle component exports. While React Router manages [route exports like `action`, ` headers`, `links`, `loader`, and `meta`][route-module] for you, any user-defined exports will cause full reloads:\n\n```tsx\n// These exports are handled by the React Router Vite plugin\n// to be HMR-compatible\nexport const meta = { title: \"Home\" }; // ✅\nexport const links = [\n  { rel: \"stylesheet\", href: \"style.css\" },\n]; // ✅\n\n// These exports are removed by the React Router Vite plugin\n// so they never affect HMR\nexport const headers = { \"Cache-Control\": \"max-age=3600\" }; // ✅\nexport const loader = async () => {}; // ✅\nexport const action = async () => {}; // ✅\n\n// This is not a route module export, nor a component export,\n// so it will cause a full reload for this route\nexport const myValue = \"some value\"; // ❌\n\nexport default function Route() {} // ✅\n```\n\n👆 Routes probably shouldn't be exporting random values like that anyway.\nIf you want to reuse values across routes, stick them in their own non-route module:\n\n```ts filename=my-custom-value.ts\nexport const myValue = \"some value\";\n```\n\n### Changing Hooks\n\nReact Fast Refresh cannot track changes for a component when hooks are being added or removed from it, causing full reloads just for the next render. After the hooks have been updated, changes should result in hot updates again. For example, if you add a `useState` to your component, you may lose that component's local state for the next render.\n\nAdditionally, if you are destructuring a hook's return value, React Fast Refresh will not be able to preserve state for the component if the destructured key is removed or renamed.\nFor example:\n\n```tsx\nexport default function Component({ loaderData }) {\n  const { pet } = useMyCustomHook();\n  return (\n    <div>\n      <input />\n      <p>My dog's name is {pet.name}!</p>\n    </div>\n  );\n}\n```\n\nIf you change the key `pet` to `dog`:\n\n```diff\n export default function Component() {\n-  const { pet } = useMyCustomHook();\n+  const { dog } = useMyCustomHook();\n   return (\n     <div>\n       <input />\n-      <p>My dog's name is {pet.name}!</p>\n+      <p>My dog's name is {dog.name}!</p>\n     </div>\n   );\n }\n```\n\nthen React Fast Refresh will not be able to preserve state `<input />` ❌.\n\n### Component Keys\n\nIn some cases, React cannot distinguish between existing components being changed and new components being added. [React needs `key`s][react-keys] to disambiguate these cases and track changes when sibling elements are modified.\n\n[virtual-dom]: https://reactjs.org/docs/faq-internals.html#what-is-the-virtual-dom\n[react-refresh]: https://github.com/facebook/react/tree/main/packages/react-refresh\n[react-keys]: https://react.dev/learn/rendering-lists#why-does-react-need-keys\n[route-module]: ../start/framework/route-module\n"
  },
  {
    "path": "docs/explanation/hydration.md",
    "content": "---\ntitle: Hydration\nhidden: true\n---\n\nThere are a few nuances worth noting around the behavior of `HydrateFallback`:\n\n- It is only relevant on initial document request and hydration, and will not be rendered on any subsequent client-side navigations\n- It is only relevant when you are also setting [`clientLoader.hydrate=true`][hydrate-true] on a given route\n- It is also relevant if you do have a `clientLoader` without a server `loader`, as this implies `clientLoader.hydrate=true` since there is otherwise no loader data at all to return from `useLoaderData`\n  - Even if you do not specify a `HydrateFallback` in this case, React Router will not render your route component and will bubble up to any ancestor `HydrateFallback` component\n  - This is to ensure that `useLoaderData` remains \"happy-path\"\n  - Without a server `loader`, `useLoaderData` would return `undefined` in any rendered route components\n- You cannot render an `<Outlet/>` in a `HydrateFallback` because children routes can't be guaranteed to operate correctly since their ancestor loader data may not yet be available if they are running `clientLoader` functions on hydration (i.e., use cases such as `useRouteLoaderData()` or `useMatches()`)\n"
  },
  {
    "path": "docs/explanation/index-query-param.md",
    "content": "---\ntitle: Index Query Param\n---\n\n# Index Query Param\n\n[MODES: framework, data]\n\n## Overview\n\nYou may find a wild `?index` appearing in the URL of your app when submitting forms.\n\nBecause of nested routes, multiple routes in your route hierarchy can match the URL. Unlike navigations where all matching route [`loader`][loader]s are called to build up the UI, when a [`form`][form_element] is submitted, _only one action is called_.\n\nBecause index routes share the same URL as their parent, the `?index` param lets you disambiguate between the two.\n\n## Understanding Index Routes\n\nFor example, consider the following route structure:\n\n```ts filename=app/routes.ts\nimport {\n  type RouteConfig,\n  route,\n  index,\n} from \"@react-router/dev/routes\";\n\nexport default [\n  route(\"projects\", \"./pages/projects.tsx\", [\n    index(\"./pages/projects/index.tsx\"),\n    route(\":id\", \"./pages/projects/project.tsx\"),\n  ]),\n] satisfies RouteConfig;\n```\n\nThis creates two routes that match `/projects`:\n\n- The parent route (`./pages/projects.tsx`)\n- The index route (`./pages/projects/index.tsx`)\n\n## Form Submission Targeting\n\nFor example, consider the following forms:\n\n```tsx\n<Form method=\"post\" action=\"/projects\" />\n<Form method=\"post\" action=\"/projects?index\" />\n```\n\nThe `?index` param will submit to the index route; the action without the index param will submit to the parent route.\n\nWhen a [`<Form>`][form_component] is rendered in an index route without an [`action`][action], the `?index` param will automatically be appended so that the form posts to the index route. The following form, when submitted, will post to `/projects?index` because it is rendered in the context of the `projects` index route:\n\n```tsx filename=app/pages/projects/index.tsx\nfunction ProjectsIndex() {\n  return <Form method=\"post\" />;\n}\n```\n\nIf you moved the code to the project layout (`./pages/projects.tsx` in this example), it would instead post to `/projects`.\n\nThis applies to `<Form>` and all of its cousins:\n\n```tsx\nfunction Component() {\n  const submit = useSubmit();\n  submit({}, { action: \"/projects\" });\n  submit({}, { action: \"/projects?index\" });\n}\n```\n\n```tsx\nfunction Component() {\n  const fetcher = useFetcher();\n  fetcher.submit({}, { action: \"/projects\" });\n  fetcher.submit({}, { action: \"/projects?index\" });\n  <fetcher.Form action=\"/projects\" />;\n  <fetcher.Form action=\"/projects?index\" />;\n  <fetcher.Form />; // defaults to the route in context\n}\n```\n\n[loader]: ../api/data-routers/loader\n[form_element]: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/form\n[form_component]: ../api/components/Form\n[action]: ../api/data-routers/action\n"
  },
  {
    "path": "docs/explanation/index.md",
    "content": "---\ntitle: Explanations\norder: 5\n---\n"
  },
  {
    "path": "docs/explanation/lazy-route-discovery.md",
    "content": "---\ntitle: Lazy Route Discovery\n---\n\n# Lazy Route Discovery\n\n[MODES: framework]\n\n<br/>\n<br/>\n\nLazy Route Discovery is a performance optimization that loads route information progressively as users navigate through your application, rather than loading the complete route manifest upfront.\n\nWith Lazy Route Discovery enabled (the default), React Router sends only the routes needed for the initial server-side render in the manifest. As users navigate to new parts of your application, additional route information is fetched dynamically and added to the client-side manifest.\n\nThe route manifest contains metadata about your routes (JavaScript/CSS imports, whether routes have `loaders`/`actions`, etc.) but not the actual route module implementations. This allows React Router to understand your application's structure without downloading unnecessary route information.\n\n## Route Discovery Process\n\nWhen a user navigates to a new route that isn't in the current manifest:\n\n1. **Route Discovery Request** - React Router makes a request to the internal `/__manifest` endpoint\n2. **Manifest Patch** - The server responds with the required route information\n3. **Route Loading** - React Router loads the necessary route modules and data\n4. **Navigation** - The user navigates to the new route\n\n## Eager Discovery Optimization\n\nTo prevent navigation waterfalls, React Router implements eager route discovery. All [`<Link>`](../api/components/Link) and [`<NavLink>`](../api/components/NavLink) components rendered on the current page are automatically discovered via a batched request to the server.\n\nThis discovery request typically completes before users click any links, making subsequent navigation feel synchronous even with lazy route discovery enabled.\n\n```tsx\n// Links are automatically discovered by default\n<Link to=\"/dashboard\">Dashboard</Link>\n\n// Opt out of discovery for specific links\n<Link to=\"/admin\" discover=\"none\">Admin</Link>\n```\n\n## Performance Benefits\n\nLazy Route Discovery provides several performance improvements:\n\n- **Faster Initial Load** - Smaller initial bundle size by excluding unused route metadata\n- **Reduced Memory Usage** - Route information is loaded only when needed\n- **Scalability** - Applications with hundreds of routes see more significant benefits\n\n## Configuration\n\nYou can configure route discovery behavior in your `react-router.config.ts`:\n\n```tsx filename=react-router.config.ts\nexport default {\n  // Default: lazy discovery with /__manifest endpoint\n  routeDiscovery: {\n    mode: \"lazy\",\n    manifestPath: \"/__manifest\",\n  },\n\n  // Custom manifest path (useful for multiple apps on same domain)\n  routeDiscovery: {\n    mode: \"lazy\",\n    manifestPath: \"/my-app-manifest\",\n  },\n\n  // Disable lazy discovery (include all routes initially)\n  routeDiscovery: { mode: \"initial\" },\n} satisfies Config;\n```\n\n## Deployment Considerations\n\nWhen using lazy route discovery, ensure your deployment setup handles manifest requests properly:\n\n- **Route Handling** - Ensure `/__manifest` requests reach your React Router handler\n- **CDN Caching** - If using CDN/edge caching, include `version` and `paths` query parameters in your cache key for the manifest endpoint\n- **Multiple Applications** - Use a custom `manifestPath` if running multiple React Router applications on the same domain\n"
  },
  {
    "path": "docs/explanation/location.md",
    "content": "---\ntitle: Location Object\nhidden: true\n---\n\n<!-- put some stuff about what it is and how it can be used, probably good opportunity for a couple how-tos as well with scroll restoration, etc -->\n"
  },
  {
    "path": "docs/explanation/progressive-enhancement.md",
    "content": "---\ntitle: Progressive Enhancement\n---\n\n# Progressive Enhancement\n\n[MODES: framework]\n\n<br/>\n<br/>\n\n> Progressive enhancement is a strategy in web design that puts emphasis on web content first, allowing everyone to access the basic content and functionality of a web page, whilst users with additional browser features or faster Internet access receive the enhanced version instead.\n>\n> <cite>- [Wikipedia][wikipedia]</cite>\n\nWhen using React Router with Server-Side Rendering (the default in framework mode), you can automatically leverage the benefits of progressive enhancement.\n\n## Why Progressive Enhancement Matters\n\nCoined in 2003 by Steven Champeon & Nick Finck, the phrase emerged during a time of varied CSS and JavaScript support across different browsers, with many users actually browsing the web with JavaScript disabled.\n\nToday, we are fortunate to develop for a much more consistent web and where the majority of users have JavaScript enabled.\n\nHowever, we still believe in the core principles of progressive enhancement in React Router. It leads to fast and resilient apps with simple development workflows.\n\n**Performance**: While it's easy to think that only 5% of your users have slow connections, the reality is that 100% of your users have slow connections 5% of the time.\n\n**Resilience**: Everybody has JavaScript disabled until it's loaded.\n\n**Simplicity**: Building your apps in a progressively enhanced way with React Router is actually simpler than building a traditional SPA.\n\n## Performance\n\nServer rendering allows your app to do more things in parallel than a typical [Single Page App (SPA)][spa], making the initial loading experience and subsequent navigations faster.\n\nTypical SPAs send a blank document and only start doing work when JavaScript has loaded:\n\n```\nHTML        |---|\nJavaScript      |---------|\nData                      |---------------|\n                            page rendered 👆\n```\n\nA React Router app can start doing work the moment the request hits the server and stream the response so that the browser can start downloading JavaScript, other assets, and data in parallel:\n\n```\n               👇 first byte\nHTML        |---|-----------|\nJavaScript      |---------|\nData        |---------------|\n              page rendered 👆\n```\n\n## Resilience and Accessibility\n\nWhile your users probably don't browse the web with JavaScript disabled, everybody uses the websites without JavaScript before it finishes loading. React Router embraces progressive enhancement by building on top of HTML, allowing you to build your app in a way that works without JavaScript, and then layer on JavaScript to enhance the experience.\n\nThe simplest case is a `<Link to=\"/account\">`. These render an `<a href=\"/account\">` tag that works without JavaScript. When JavaScript loads, React Router will intercept clicks and handle the navigation with client side routing. This gives you more control over the UX instead of just spinning favicons in the browser tab--but it works either way.\n\nNow consider a simple add to cart button:\n\n```tsx\nexport function AddToCart({ id }) {\n  return (\n    <Form method=\"post\" action=\"/add-to-cart\">\n      <input type=\"hidden\" name=\"id\" value={id} />\n      <button type=\"submit\">Add To Cart</button>\n    </Form>\n  );\n}\n```\n\nWhether JavaScript has loaded or not doesn't matter, this button will add the product to the cart.\n\nWhen JavaScript loads, React Router will intercept the form submission and handle it client side. This allows you to add your own pending UI, or other client side behavior.\n\n## Simplicity\n\nWhen you start to rely on basic features of the web like HTML and URLs, you will find that you reach for client side state and state management much less.\n\nConsider the button from before, with no fundamental change to the code, we can pepper in some client side behavior:\n\n```tsx lines=[1,4,7,10-12,14]\nimport { useFetcher } from \"react-router\";\n\nexport function AddToCart({ id }) {\n  const fetcher = useFetcher();\n\n  return (\n    <fetcher.Form method=\"post\" action=\"/add-to-cart\">\n      <input name=\"id\" value={id} />\n      <button type=\"submit\">\n        {fetcher.state === \"submitting\"\n          ? \"Adding...\"\n          : \"Add To Cart\"}\n      </button>\n    </fetcher.Form>\n  );\n}\n```\n\nThis feature continues to work the very same as it did before when JavaScript is loading, but once JavaScript loads:\n\n- `useFetcher` no longer causes a navigation like `<Form>` does, so the user can stay on the same page and keep shopping\n- The app code determines the pending UI instead of spinning favicons in the browser\n\nIt's not about building it two different ways–once for JavaScript and once without–it's about building it in iterations. Start with the simplest version of the feature and ship it; then iterate to an enhanced user experience.\n\nNot only will the user get a progressively enhanced experience, but the app developer gets to \"progressively enhance\" the UI without changing the fundamental design of the feature.\n\nAnother example where progressive enhancement leads to simplicity is with the URL. When you start with a URL, you don't need to worry about client side state management. You can just use the URL as the source of truth for the UI.\n\n```tsx\nexport function SearchBox() {\n  return (\n    <Form method=\"get\" action=\"/search\">\n      <input type=\"search\" name=\"query\" />\n      <SearchIcon />\n    </Form>\n  );\n}\n```\n\nThis component doesn't need any state management. It just renders a form that submits to `/search`. When JavaScript loads, React Router will intercept the form submission and handle it client side. Here's the next iteration:\n\n```tsx lines=[1,4-6,11]\nimport { useNavigation } from \"react-router\";\n\nexport function SearchBox() {\n  const navigation = useNavigation();\n  const isSearching =\n    navigation.location.pathname === \"/search\";\n\n  return (\n    <Form method=\"get\" action=\"/search\">\n      <input type=\"search\" name=\"query\" />\n      {isSearching ? <Spinner /> : <SearchIcon />}\n    </Form>\n  );\n}\n```\n\nNo fundamental change in architecture, simply a progressive enhancement for both the user and the code.\n\nSee also: [State Management][state_management]\n\n[wikipedia]: https://en.wikipedia.org/wiki/Progressive_enhancement\n[spa]: ../how-to/spa\n[state_management]: ./state-management\n"
  },
  {
    "path": "docs/explanation/race-conditions.md",
    "content": "---\ntitle: Race Conditions\n---\n\n# Race Conditions\n\n[MODES: framework, data]\n\n<br/>\n<br/>\n\nWhile impossible to eliminate every possible race condition in your application, React Router automatically handles the most common race conditions found in web user interfaces.\n\n## Browser Behavior\n\nReact Router's handling of network concurrency is heavily inspired by the behavior of web browsers when processing documents.\n\nConsider clicking a link to a new document, and then clicking a different link before the new page has finished loading. The browser will:\n\n1. cancel the first request\n2. immediately process the new navigation\n\nThe same behavior applies to form submissions. When a pending form submission is interrupted by a new one, the first is canceled and the new submission is immediately processed.\n\n## React Router Behavior\n\nLike the browser, interrupted navigations with links and form submissions will cancel in flight data requests and immediately process the new event.\n\nFetchers are a bit more nuanced since they are not singleton events like navigation. Fetchers can't interrupt other fetcher instances, but they can interrupt themselves and the behavior is the same as everything else: cancel the interrupted request and immediately process the new one.\n\nFetchers do, however, interact with each other when it comes to revalidation. After a fetcher's action request returns to the browser, a revalidation for all page data is sent. This means multiple revalidation requests can be in-flight at the same time. React Router will commit all \"fresh\" revalidation responses and cancel any stale requests. A stale request is any request that started _earlier_ than one that has returned.\n\nThis management of the network prevents the most common UI bugs caused by network race conditions.\n\nSince networks are unpredictable, and your server still processes these cancelled requests, your backend may still experience race conditions and have potential data integrity issues. These risks are the same risks as using default browser behavior with plain HTML `<forms>`, which we consider to be low, and outside the scope of React Router.\n\n## Practical Benefits\n\nConsider building a type-ahead combobox. As the user types, you send a request to the server. As they type each new character you send a new request. It's important to not show the user results for a value that's not in the text field anymore.\n\nWhen using a fetcher, this is automatically managed for you. Consider this pseudo-code:\n\n```tsx\n// route(\"/city-search\", \"./search-cities.ts\")\nexport async function loader({ request }) {\n  const { searchParams } = new URL(request.url);\n  return searchCities(searchParams.get(\"q\"));\n}\n```\n\n```tsx\nexport function CitySearchCombobox() {\n  const fetcher = useFetcher();\n\n  return (\n    <fetcher.Form action=\"/city-search\">\n      <Combobox aria-label=\"Cities\">\n        <ComboboxInput\n          name=\"q\"\n          onChange={(event) =>\n            // submit the form onChange to get the list of cities\n            fetcher.submit(event.target.form)\n          }\n        />\n\n        {fetcher.data ? (\n          <ComboboxPopover className=\"shadow-popup\">\n            {fetcher.data.length > 0 ? (\n              <ComboboxList>\n                {fetcher.data.map((city) => (\n                  <ComboboxOption\n                    key={city.id}\n                    value={city.name}\n                  />\n                ))}\n              </ComboboxList>\n            ) : (\n              <span>No results found</span>\n            )}\n          </ComboboxPopover>\n        ) : null}\n      </Combobox>\n    </fetcher.Form>\n  );\n}\n```\n\nCalls to `fetcher.submit` will cancel pending requests on that fetcher automatically. This ensures you never show the user results for a request for a different input value.\n"
  },
  {
    "path": "docs/explanation/react-transitions.md",
    "content": "---\ntitle: React Transitions\nunstable: true\n---\n\n# React Transitions\n\n[MODES: framework, data, declarative]\n\n<br/>\n<br/>\n\n<docs-warning>The `unstable_useTransitions` prop is experimental and subject to breaking changes in\nminor/patch releases. Please use with caution and pay **very** close attention\nto release notes for relevant changes.</docs-warning>\n\n[React 18][react-18] introduced the concept of \"transitions\" which allow you to differentiate urgent from non-urgent UI updates. To learn more about React Transitions and \"concurrent rendering\" Please refer to React's official documentation:\n\n- [What is Concurrent React][concurrent]\n- [Transitions][transitions]\n- [`React.useTransition`][use-transition]\n- [`React.startTransition`][start-transition]\n\n[React 19][react-19] enhances the async/concurrent landscape by introducing [Actions][actions] and support for using async functions in Transitions. With the support for async Transitions, a new [`React.useOptimistic`][use-optimistic-blog] [hook][use-optimistic] was also introduced that allows you to surface state updates during a Transition to show users instant feedback.\n\n## Transitions in React Router\n\nThe introduction of Transitions in React makes the story of how React Router manages your navigations and router state a bit more complicated. These are powerful APIs but they don't come without some nuance and added complexity. We aim to make React Router work seamlessly with the new React features, but in some cases there may exist some tension between the new React ways to do things and some patterns you are already using in your React Router apps (i.e., pending states, optimistic UI).\n\nTo ensure a smooth adoption story, we've introduced changes related to Transitions behind an opt-in `unstable_useTransitions` flag so that you can upgrade in a non-breaking fashion.\n\n### Current Behavior\n\nWe first leveraged `React.startTransition` to make React Router more Suspense-friendly in React Router [6.13.0][rr-6-13-0] via the `future.v7_startTransition` flag. In v7, that became the default behavior and all router state updates are currently wrapped in `React.startTransition`.\n\nThis default behavior has 2 potential issues that `unstable_useTransitions` is designed to solve:\n\n- There are some valid use cases where you _don't_ want your updates wrapped in `startTransition`\n  - One specific issue is that `React.useSyncExternalStore` updates can't be Transitions ([^1][uses-transition-issue], [^2][uses-transition-tweet]). `useSyncExternalStore` forces a sync update, which means fallbacks can be shown in update transitions that would otherwise avoid showing the fallback.\n  - React Router has a `flushSync` option on navigations to use [`React.flushSync`][flush-sync] for state updates instead, but that's not always a proper solution\n- React 19 has added a new `startTransition(() => Promise))` API as well as a new `useOptimistic` hook to surface updates during Transitions\n  - Without some updates to React Router, `startTransition(() => navigate(path))` doesn't work as you might expect, because we are not using `useOptimistic` internally so router state updates don't surface during the navigation, which breaks hooks like `useNavigation`\n\nTo provide a solution to both of the above issues, we're introducing a new `unstable_useTransitions` prop to the router components that will let you opt-out of using `startTransition` for router state updates (solving the first issue), or opt-into a more enhanced usage of `startTransition` + `useOptimistic` (solving the second issue). Because the current behavior is a bit incomplete with the new React 19 APIs, we plan to make the opt-in behavior the default in React Router v8, but we will likely retain the opt-out flag for use cases such as `useSyncExternalStore`.\n\n### Opt-out via `unstable_useTransitions=false`\n\nIf your application is not \"Transition-friendly\" due to the usage of `useSyncExternalStore` (or other reasons), then you can opt-out via the prop:\n\n```tsx\n// Framework Mode (entry.client.tsx)\n<HydratedRouter unstable_useTransitions={false} />\n\n// Data Mode\n<RouterProvider unstable_useTransitions={false} />\n\n// Declarative Mode\n<BrowserRouter unstable_useTransitions={false} />\n```\n\nThis will stop the router from wrapping internal state updates in `startTransition`.\n\n### Opt-in via `unstable_useTransitions=true`\n\n<docs-info>Opting into this feature in Framework or Data Mode requires that you are using React 19 because it needs access to [`React.useOptimistic`][use-optimistic]</docs-info>\n\nIf you want to make your application play nicely with all of the new React 19 features that rely on concurrent mode and Transitions, then you can opt-in via the new prop:\n\n```tsx\n// Framework Mode (entry.client.tsx)\n<HydratedRouter unstable_useTransitions />\n\n// Data Mode\n<RouterProvider unstable_useTransitions />\n\n// Declarative Mode\n<BrowserRouter unstable_useTransitions />\n```\n\nWith this flag enabled:\n\n- All internal state updates are wrapped in `React.startTransition` (current behavior without the flag)\n- All `<Link>`/`<Form>` navigations will be wrapped in `React.startTransition`, using the promise returned by `useNavigate`/`useSubmit` so that the Transition lasts for the duration of the navigation\n  - `useNavigate`/`useSubmit` do not automatically wrap in `React.startTransition`, so you can opt-out of a Transition-enabled navigation by using those directly\n- In Framework/Data modes, a subset of the router state updates during a navigation will be surfaced to the UI via `useOptimistic`\n  - State related to the _ongoing_ navigation and all fetcher information will be surfaced:\n    - `state.navigation` for `useNavigation()`\n    - `state.revalidation` for `useRevalidator()`\n    - `state.actionData` for `useActionData()`\n    - `state.fetchers` for `useFetcher()` and `useFetchers()`\n  - State related to the _current_ location will not be surfaced:\n    - `state.location` for `useLocation`\n    - `state.matches` for `useMatches()`,\n    - `state.loaderData` for `useLoaderData()`\n    - `state.errors` for `useRouteError()`\n    - etc.\n\nEnabling this flag means that you can now have fully-Transition-enabled navigations that play nicely with any other ongoing Transition-enabled aspects of your application.\n\nThe only APIs that are automatically wrapped in an async Transition are `<Link>` and `<Form>`. For everything else, you need to wrap the operation in `startTransition` yourself.\n\n```tsx\n// Automatically Transition-enabled\n<Link to=\"/path\" />\n<Form method=\"post\" action=\"/path\" />\n\n// Manually Transition-enabled\nstartTransition(() => navigate(\"/path\"));\nstartTransition(() => submit(data, { method: 'post', action: \"/path\" }));\nstartTransition(() => fetcher.load(\"/path\"));\nstartTransition(() => fetcher.submit(data, { method: \"post\", action: \"/path\" }));\n\n// Not Transition-enabled\nnavigate(\"/path\");\nsubmit(data, { method: 'post', action: \"/path\" });\nfetcher.load(\"/path\");\nfetcher.submit(data, { method: \"post\", action: \"/path\" });\n```\n\n**Important:** You must always `return` or `await` the `navigate` promise inside `startTransition` so that the Transition encompasses the full duration of the navigation. If you forget to `return` or `await` the promise, the Transition will end prematurely and things won't work as expected.\n\n```tsx\n// ✅ Returned promise\nstartTransition(() => navigate(\"/path\"));\nstartTransition(() => {\n  setOptimistic(something);\n  return navigate(\"/path\"));\n});\n\n// ✅ Awaited promise\nstartTransition(async () => {\n  setOptimistic(something);\n  await navigate(\"/path\"));\n});\n\n// ❌ Non-returned promise\nstartTransition(() => {\n  setOptimistic(something);\n  navigate(\"/path\"));\n});\n\n// ❌ Non-Awaited promise\nstartTransition(async () => {\n  setOptimistic(something);\n  navigate(\"/path\"));\n});\n```\n\n#### `popstate` navigations\n\nThere is currently a bug with optimistic states and `popstate`. If you need to read the current route during a back navigation, which cannot complete synchronously (e.g. Suspends on uncached data), you can set the optimistic state before navigating back or defer the optimistic update in a timer or microtask.\n\n[react-18]: https://react.dev/blog/2022/03/29/react-v18\n[concurrent]: https://react.dev/blog/2022/03/29/react-v18#what-is-concurrent-react\n[transitions]: https://react.dev/blog/2022/03/29/react-v18#new-feature-transitions\n[use-transition]: https://react.dev/reference/react/useTransition#reference\n[start-transition]: https://react.dev/reference/react/startTransition\n[react-19]: https://react.dev/blog/2024/12/05/react-19\n[actions]: https://react.dev/blog/2024/12/05/react-19#actions\n[use-optimistic-blog]: https://react.dev/blog/2024/12/05/react-19#new-hook-optimistic-updates\n[use-optimistic]: https://react.dev/reference/react/useOptimistic\n[flush-sync]: https://react.dev/reference/react-dom/flushSync\n[rr-6-13-0]: https://github.com/remix-run/react-router/blob/main/CHANGELOG.md#v6130\n[uses-transition-issue]: https://github.com/facebook/react/issues/26382\n[uses-transition-tweet]: https://x.com/rickhanlonii/status/1683636856808775682\n"
  },
  {
    "path": "docs/explanation/route-matching.md",
    "content": "---\ntitle: Route Matching\nhidden: true\n# want to explain how the matching algorithm works with any potential gotchas\n---\n\n# Route Matching\n"
  },
  {
    "path": "docs/explanation/server-client-execution.md",
    "content": "---\ntitle: Server vs. Client Code Execution\nhidden: true\n---\n"
  },
  {
    "path": "docs/explanation/sessions-and-cookies.md",
    "content": "---\ntitle: Sessions and Cookies\n---\n\n# Sessions and Cookies\n\n[MODES: framework, data]\n\n## Sessions\n\nSessions are an important part of websites that allow the server to identify requests coming from the same person, especially when it comes to server-side form validation or when JavaScript is not on the page. Sessions are a fundamental building block of many sites that let users \"log in\", including social, e-commerce, business, and educational websites.\n\nWhen using React Router as your framework, sessions are managed on a per-route basis (rather than something like express middleware) in your `loader` and `action` methods using a \"session storage\" object (that implements the [`SessionStorage`][session-storage] interface). Session storage understands how to parse and generate cookies, and how to store session data in a database or filesystem.\n\n### Using Sessions\n\nThis is an example of a cookie session storage:\n\n```ts filename=app/sessions.server.ts\nimport { createCookieSessionStorage } from \"react-router\";\n\ntype SessionData = {\n  userId: string;\n};\n\ntype SessionFlashData = {\n  error: string;\n};\n\nconst { getSession, commitSession, destroySession } =\n  createCookieSessionStorage<SessionData, SessionFlashData>(\n    {\n      // a Cookie from `createCookie` or the CookieOptions to create one\n      cookie: {\n        name: \"__session\",\n\n        // all of these are optional\n        domain: \"reactrouter.com\",\n        // Expires can also be set (although maxAge overrides it when used in combination).\n        // Note that this method is NOT recommended as `new Date` creates only one date on each server deployment, not a dynamic date in the future!\n        //\n        // expires: new Date(Date.now() + 60_000),\n        httpOnly: true,\n        maxAge: 60,\n        path: \"/\",\n        sameSite: \"lax\",\n        secrets: [\"s3cret1\"],\n        secure: true,\n      },\n    },\n  );\n\nexport { getSession, commitSession, destroySession };\n```\n\nWe recommend setting up your session storage object in `app/sessions.server.ts` so all routes that need to access session data can import from the same spot.\n\nThe input/output to a session storage object are HTTP cookies. `getSession()` retrieves the current session from the incoming request's `Cookie` header, and `commitSession()`/`destroySession()` provide the `Set-Cookie` header for the outgoing response.\n\nYou'll use methods to get access to sessions in your `loader` and `action` functions.\n\nAfter retrieving a session with `getSession`, the returned session object has a handful of methods and properties:\n\n```tsx\nexport async function action({\n  request,\n}: ActionFunctionArgs) {\n  const session = await getSession(\n    request.headers.get(\"Cookie\"),\n  );\n  session.get(\"foo\");\n  session.has(\"bar\");\n  // etc.\n}\n```\n\nSee the [Session API][session-api] for all methods available on the session object.\n\n### Login form example\n\nA login form might look something like this:\n\n```tsx filename=app/routes/login.tsx lines=[4-7,12-14,16,22,25,33-35,46,51,56,61]\nimport { data, redirect } from \"react-router\";\nimport type { Route } from \"./+types/login\";\n\nimport {\n  getSession,\n  commitSession,\n} from \"../sessions.server\";\n\nexport async function loader({\n  request,\n}: Route.LoaderArgs) {\n  const session = await getSession(\n    request.headers.get(\"Cookie\"),\n  );\n\n  if (session.has(\"userId\")) {\n    // Redirect to the home page if they are already signed in.\n    return redirect(\"/\");\n  }\n\n  return data(\n    { error: session.get(\"error\") },\n    {\n      headers: {\n        \"Set-Cookie\": await commitSession(session),\n      },\n    },\n  );\n}\n\nexport async function action({\n  request,\n}: Route.ActionArgs) {\n  const session = await getSession(\n    request.headers.get(\"Cookie\"),\n  );\n  const form = await request.formData();\n  const username = form.get(\"username\");\n  const password = form.get(\"password\");\n\n  const userId = await validateCredentials(\n    username,\n    password,\n  );\n\n  if (userId == null) {\n    session.flash(\"error\", \"Invalid username/password\");\n\n    // Redirect back to the login page with errors.\n    return redirect(\"/login\", {\n      headers: {\n        \"Set-Cookie\": await commitSession(session),\n      },\n    });\n  }\n\n  session.set(\"userId\", userId);\n\n  // Login succeeded, send them to the home page.\n  return redirect(\"/\", {\n    headers: {\n      \"Set-Cookie\": await commitSession(session),\n    },\n  });\n}\n\nexport default function Login({\n  loaderData,\n}: Route.ComponentProps) {\n  const { error } = loaderData;\n\n  return (\n    <div>\n      {error ? <div className=\"error\">{error}</div> : null}\n      <form method=\"POST\">\n        <div>\n          <p>Please sign in</p>\n        </div>\n        <label>\n          Username: <input type=\"text\" name=\"username\" />\n        </label>\n        <label>\n          Password:{\" \"}\n          <input type=\"password\" name=\"password\" />\n        </label>\n      </form>\n    </div>\n  );\n}\n```\n\nAnd then a logout form might look something like this:\n\n```tsx filename=app/routes/logout.tsx\nimport {\n  getSession,\n  destroySession,\n} from \"../sessions.server\";\nimport type { Route } from \"./+types/logout\";\n\nexport async function action({\n  request,\n}: Route.ActionArgs) {\n  const session = await getSession(\n    request.headers.get(\"Cookie\"),\n  );\n  return redirect(\"/login\", {\n    headers: {\n      \"Set-Cookie\": await destroySession(session),\n    },\n  });\n}\n\nexport default function LogoutRoute() {\n  return (\n    <>\n      <p>Are you sure you want to log out?</p>\n      <Form method=\"post\">\n        <button>Logout</button>\n      </Form>\n      <Link to=\"/\">Never mind</Link>\n    </>\n  );\n}\n```\n\n<docs-warning>It's important that you logout (or perform any mutation for that matter) in an `action` and not a `loader`. Otherwise you open your users to [Cross-Site Request Forgery][csrf] attacks.</docs-warning>\n\n### Session Gotchas\n\nBecause of nested routes, multiple loaders can be called to construct a single page. When using `session.flash()` or `session.unset()`, you need to be sure no other loaders in the request are going to want to read that, otherwise you'll get race conditions. Typically if you're using flash, you'll want to have a single loader read it, if another loader wants a flash message, use a different key for that loader.\n\n### Creating custom session storage\n\nReact Router makes it easy to store sessions in your own database if needed. The [`createSessionStorage()`][create-session-storage] API requires a `cookie` (for options for creating a cookie, see [cookies][cookies]) and a set of create, read, update, and delete (CRUD) methods for managing the session data. The cookie is used to persist the session ID.\n\n- `createData` will be called from `commitSession` on the initial session creation when no session ID exists in the cookie\n- `readData` will be called from `getSession` when a session ID exists in the cookie\n- `updateData` will be called from `commitSession` when a session ID already exists in the cookie\n- `deleteData` is called from `destroySession`\n\nThe following example shows how you could do this using a generic database client:\n\n```ts\nimport { createSessionStorage } from \"react-router\";\n\nfunction createDatabaseSessionStorage({\n  cookie,\n  host,\n  port,\n}) {\n  // Configure your database client...\n  const db = createDatabaseClient(host, port);\n\n  return createSessionStorage({\n    cookie,\n    async createData(data, expires) {\n      // `expires` is a Date after which the data should be considered\n      // invalid. You could use it to invalidate the data somehow or\n      // automatically purge this record from your database.\n      const id = await db.insert(data);\n      return id;\n    },\n    async readData(id) {\n      return (await db.select(id)) || null;\n    },\n    async updateData(id, data, expires) {\n      await db.update(id, data);\n    },\n    async deleteData(id) {\n      await db.delete(id);\n    },\n  });\n}\n```\n\nAnd then you can use it like this:\n\n```ts\nconst { getSession, commitSession, destroySession } =\n  createDatabaseSessionStorage({\n    host: \"localhost\",\n    port: 1234,\n    cookie: {\n      name: \"__session\",\n      sameSite: \"lax\",\n    },\n  });\n```\n\nThe `expires` argument to `createData` and `updateData` is the same `Date` at which the cookie itself expires and is no longer valid. You can use this information to automatically purge the session record from your database to save on space, or to ensure that you do not otherwise return any data for old, expired cookies.\n\n### Additional session utils\n\nThere are also several other session utilities available if you need them:\n\n- [`isSession`][is-session]\n- [`createMemorySessionStorage`][create-memory-session-storage]\n- [`createSession`][create-session] (custom storage)\n- [`createFileSessionStorage`][create-file-session-storage] (node)\n- [`createWorkersKVSessionStorage`][create-workers-kv-session-storage] (Cloudflare Workers)\n- [`createArcTableSessionStorage`][create-arc-table-session-storage] (architect, Amazon DynamoDB)\n\n## Cookies\n\nA [cookie][cookie] is a small piece of information that your server sends someone in a HTTP response that their browser will send back on subsequent requests. This technique is a fundamental building block of many interactive websites that adds state so you can build authentication (see [sessions][sessions]), shopping carts, user preferences, and many other features that require remembering who is \"logged in\".\n\nReact Router's [`Cookie` interface][cookie-api] provides a logical, reusable container for cookie metadata.\n\n### Using cookies\n\nWhile you may create these cookies manually, it is more common to use a [session storage][sessions].\n\nIn React Router, you will typically work with cookies in your `loader` and/or `action` functions, since those are the places where you need to read and write data.\n\nLet's say you have a banner on your e-commerce site that prompts users to check out the items you currently have on sale. The banner spans the top of your homepage, and includes a button on the side that allows the user to dismiss the banner so they don't see it for at least another week.\n\nFirst, create a cookie:\n\n```ts filename=app/cookies.server.ts\nimport { createCookie } from \"react-router\";\n\nexport const userPrefs = createCookie(\"user-prefs\", {\n  maxAge: 604_800, // one week\n});\n```\n\nThen, you can `import` the cookie and use it in your `loader` and/or `action`. The `loader` in this case just checks the value of the user preference so you can use it in your component for deciding whether to render the banner. When the button is clicked, the `<form>` calls the `action` on the server and reloads the page without the banner.\n\n### User preferences example\n\n```tsx filename=app/routes/home.tsx lines=[4,9-11,18-20,29]\nimport { Link, Form, redirect } from \"react-router\";\nimport type { Route } from \"./+types/home\";\n\nimport { userPrefs } from \"../cookies.server\";\n\nexport async function loader({\n  request,\n}: Route.LoaderArgs) {\n  const cookieHeader = request.headers.get(\"Cookie\");\n  const cookie =\n    (await userPrefs.parse(cookieHeader)) || {};\n  return { showBanner: cookie.showBanner };\n}\n\nexport async function action({\n  request,\n}: Route.ActionArgs) {\n  const cookieHeader = request.headers.get(\"Cookie\");\n  const cookie =\n    (await userPrefs.parse(cookieHeader)) || {};\n  const bodyParams = await request.formData();\n\n  if (bodyParams.get(\"bannerVisibility\") === \"hidden\") {\n    cookie.showBanner = false;\n  }\n\n  return redirect(\"/\", {\n    headers: {\n      \"Set-Cookie\": await userPrefs.serialize(cookie),\n    },\n  });\n}\n\nexport default function Home({\n  loaderData,\n}: Route.ComponentProps) {\n  return (\n    <div>\n      {loaderData.showBanner ? (\n        <div>\n          <Link to=\"/sale\">Don't miss our sale!</Link>\n          <Form method=\"post\">\n            <input\n              type=\"hidden\"\n              name=\"bannerVisibility\"\n              value=\"hidden\"\n            />\n            <button type=\"submit\">Hide</button>\n          </Form>\n        </div>\n      ) : null}\n      <h1>Welcome!</h1>\n    </div>\n  );\n}\n```\n\n### Cookie attributes\n\nCookies have [several attributes][cookie-attrs] that control when they expire, how they are accessed, and where they are sent. Any of these attributes may be specified either in `createCookie(name, options)`, or during `serialize()` when the `Set-Cookie` header is generated.\n\n```ts\nconst cookie = createCookie(\"user-prefs\", {\n  // These are defaults for this cookie.\n  path: \"/\",\n  sameSite: \"lax\",\n  httpOnly: true,\n  secure: true,\n  expires: new Date(Date.now() + 60_000),\n  maxAge: 60,\n});\n\n// You can either use the defaults:\ncookie.serialize(userPrefs);\n\n// Or override individual ones as needed:\ncookie.serialize(userPrefs, { sameSite: \"strict\" });\n```\n\nPlease read [more info about these attributes][cookie-attrs] to get a better understanding of what they do.\n\n### Signing cookies\n\nIt is possible to sign a cookie to automatically verify its contents when it is received. Since it's relatively easy to spoof HTTP headers, this is a good idea for any information that you do not want someone to be able to fake, like authentication information (see [sessions][sessions]).\n\nTo sign a cookie, provide one or more `secrets` when you first create the cookie:\n\n```ts\nconst cookie = createCookie(\"user-prefs\", {\n  secrets: [\"s3cret1\"],\n});\n```\n\nCookies that have one or more `secrets` will be stored and verified in a way that ensures the cookie's integrity.\n\nSecrets may be rotated by adding new secrets to the front of the `secrets` array. Cookies that have been signed with old secrets will still be decoded successfully in `cookie.parse()`, and the newest secret (the first one in the array) will always be used to sign outgoing cookies created in `cookie.serialize()`.\n\n```ts filename=app/cookies.server.ts\nexport const cookie = createCookie(\"user-prefs\", {\n  secrets: [\"n3wsecr3t\", \"olds3cret\"],\n});\n```\n\n```tsx filename=app/routes/my-route.tsx\nimport { data } from \"react-router\";\nimport { cookie } from \"../cookies.server\";\nimport type { Route } from \"./+types/my-route\";\n\nexport async function loader({\n  request,\n}: Route.LoaderArgs) {\n  const oldCookie = request.headers.get(\"Cookie\");\n  // oldCookie may have been signed with \"olds3cret\", but still parses ok\n  const value = await cookie.parse(oldCookie);\n\n  return data(\"...\", {\n    headers: {\n      // Set-Cookie is signed with \"n3wsecr3t\"\n      \"Set-Cookie\": await cookie.serialize(value),\n    },\n  });\n}\n```\n\n### Additional cookie utils\n\nThere are also several other cookie utilities available if you need them:\n\n- [`isCookie`][is-cookie]\n- [`createCookie`][create-cookie]\n\nTo learn more about each attribute, please see the [MDN Set-Cookie docs][cookie-attrs].\n\n[csrf]: https://developer.mozilla.org/en-US/docs/Glossary/CSRF\n[cookies]: #cookies\n[sessions]: #sessions\n[session-storage]: https://api.reactrouter.com/v7/interfaces/react-router.SessionStorage\n[session-api]: https://api.reactrouter.com/v7/interfaces/react-router.Session\n[is-session]: https://api.reactrouter.com/v7/functions/react-router.isSession\n[cookie-api]: https://api.reactrouter.com/v7/interfaces/react-router.Cookie\n[create-session-storage]: https://api.reactrouter.com/v7/functions/react-router.createSessionStorage\n[create-session]: https://api.reactrouter.com/v7/functions/react-router.createSession\n[create-memory-session-storage]: https://api.reactrouter.com/v7/functions/react-router.createMemorySessionStorage\n[create-file-session-storage]: https://api.reactrouter.com/v7/functions/_react-router_node.createFileSessionStorage\n[create-workers-kv-session-storage]: https://api.reactrouter.com/v7/functions/_react-router_cloudflare.createWorkersKVSessionStorage\n[create-arc-table-session-storage]: https://api.reactrouter.com/v7/functions/_react-router_architect.createArcTableSessionStorage\n[cookie]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Cookies\n[cookie-attrs]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie#attributes\n[is-cookie]: https://api.reactrouter.com/v7/functions/react-router.isCookie\n[create-cookie]: https://api.reactrouter.com/v7/functions/react-router.createCookie\n"
  },
  {
    "path": "docs/explanation/special-files.md",
    "content": "---\ntitle: Special Files\nhidden: true\n---\n\n# Special Files\n\nThe content of this page has been moved to the following:\n\n- [`react-router.config.ts`](../api/framework-conventions/react-router.config.ts) - Optional configuration file for your app\n- [`root.tsx`](../api/framework-conventions/root.tsx) - Required root route that renders the HTML document\n- [`routes.ts`](../api/framework-conventions/routes.ts) - Required route configuration mapping URLs to components\n- [`entry.client.tsx`](../api/framework-conventions/entry.client.tsx) - Optional client-side entry point for hydration\n- [`entry.server.tsx`](../api/framework-conventions/entry.server.tsx) - Optional server-side entry point for rendering\n- [`.server` modules](../api/framework-conventions/server-modules) - Server-only modules excluded from client bundles\n- [`.client` modules](../api/framework-conventions/client-modules) - Client-only modules excluded from server bundles\n"
  },
  {
    "path": "docs/explanation/state-management.md",
    "content": "---\ntitle: State Management\n---\n\n# State Management\n\n[MODES: framework, data]\n\n<br/>\n<br/>\n\nState management in React typically involves maintaining a synchronized cache of server data on the client side. However, when using React Router as your framework, most of the traditional caching solutions become redundant because of how it inherently handles data synchronization.\n\n## Understanding State Management in React\n\nIn a typical React context, when we refer to \"state management\", we're primarily discussing how we synchronize server state with the client. A more apt term could be \"cache management\" because the server is the source of truth and the client state is mostly functioning as a cache.\n\nPopular caching solutions in React include:\n\n- **Redux:** A predictable state container for JavaScript apps.\n- **React Query:** Hooks for fetching, caching, and updating asynchronous data in React.\n- **Apollo:** A comprehensive state management library for JavaScript that integrates with GraphQL.\n\nIn certain scenarios, using these libraries may be warranted. However, with React Router's unique server-focused approach, their utility becomes less prevalent. In fact, most React Router applications forgo them entirely.\n\n## How React Router Simplifies State\n\nReact Router seamlessly bridges the gap between the backend and frontend via mechanisms like loaders, actions, and forms with automatic synchronization through revalidation. This offers developers the ability to directly use server state within components without managing a cache, the network communication, or data revalidation, making most client-side caching redundant.\n\nHere's why using typical React state patterns might be an anti-pattern in React Router:\n\n1. **Network-related State:** If your React state is managing anything related to the network—such as data from loaders, pending form submissions, or navigational states—it's likely that you're managing state that React Router already manages:\n   - **[`useNavigation`][use_navigation]**: This hook gives you access to `navigation.state`, `navigation.formData`, `navigation.location`, etc.\n   - **[`useFetcher`][use_fetcher]**: This facilitates interaction with `fetcher.state`, `fetcher.formData`, `fetcher.data` etc.\n   - **[`loaderData`][loader_data]**: Access the data for a route.\n   - **[`actionData`][action_data]**: Access the data from the latest action.\n\n2. **Storing Data in React Router:** A lot of data that developers might be tempted to store in React state has a more natural home in React Router, such as:\n   - **URL Search Params:** Parameters within the URL that hold state.\n   - **[Cookies][cookies]:** Small pieces of data stored on the user's device.\n   - **[Server Sessions][sessions]:** Server-managed user sessions.\n   - **Server Caches:** Cached data on the server side for quicker retrieval.\n\n3. **Performance Considerations:** At times, client state is leveraged to avoid redundant data fetching. With React Router, you can use the [`Cache-Control`][cache_control_header] headers within `loader`s, allowing you to tap into the browser's native cache. However, this approach has its limitations and should be used judiciously. It's usually more beneficial to optimize backend queries or implement a server cache. This is because such changes benefit all users and do away with the need for individual browser caches.\n\nAs a developer transitioning to React Router, it's essential to recognize and embrace its inherent efficiencies rather than applying traditional React patterns. React Router offers a streamlined solution to state management leading to less code, fresh data, and no state synchronization bugs.\n\n## Examples\n\n### Network Related State\n\nFor examples on using React Router's internal state to manage network related state, refer to [Pending UI][pending_ui].\n\n### URL Search Params\n\nConsider a UI that lets the user customize between list view or detail view. Your instinct might be to reach for React state:\n\n```tsx bad lines=[2,6,9]\nexport function List() {\n  const [view, setView] = useState(\"list\");\n  return (\n    <div>\n      <div>\n        <button onClick={() => setView(\"list\")}>\n          View as List\n        </button>\n        <button onClick={() => setView(\"details\")}>\n          View with Details\n        </button>\n      </div>\n      {view === \"list\" ? <ListView /> : <DetailView />}\n    </div>\n  );\n}\n```\n\nNow consider you want the URL to update when the user changes the view. Note the state synchronization:\n\n```tsx bad lines=[7,16,24]\nimport { useNavigate, useSearchParams } from \"react-router\";\n\nexport function List() {\n  const navigate = useNavigate();\n  const [searchParams] = useSearchParams();\n  const [view, setView] = useState(\n    searchParams.get(\"view\") || \"list\",\n  );\n\n  return (\n    <div>\n      <div>\n        <button\n          onClick={() => {\n            setView(\"list\");\n            navigate(`?view=list`);\n          }}\n        >\n          View as List\n        </button>\n        <button\n          onClick={() => {\n            setView(\"details\");\n            navigate(`?view=details`);\n          }}\n        >\n          View with Details\n        </button>\n      </div>\n      {view === \"list\" ? <ListView /> : <DetailView />}\n    </div>\n  );\n}\n```\n\nInstead of synchronizing state, you can simply read and set the state in the URL directly with boring old HTML forms:\n\n```tsx good lines=[5,9-16]\nimport { Form, useSearchParams } from \"react-router\";\n\nexport function List() {\n  const [searchParams] = useSearchParams();\n  const view = searchParams.get(\"view\") || \"list\";\n\n  return (\n    <div>\n      <Form>\n        <button name=\"view\" value=\"list\">\n          View as List\n        </button>\n        <button name=\"view\" value=\"details\">\n          View with Details\n        </button>\n      </Form>\n      {view === \"list\" ? <ListView /> : <DetailView />}\n    </div>\n  );\n}\n```\n\n### Persistent UI State\n\nConsider a UI that toggles a sidebar's visibility. We have three ways to handle the state:\n\n1. React state\n2. Browser local storage\n3. Cookies\n\nIn this discussion, we'll break down the trade-offs associated with each method.\n\n#### React State\n\nReact state provides a simple solution for temporary state storage.\n\n**Pros**:\n\n- **Simple**: Easy to implement and understand.\n- **Encapsulated**: State is scoped to the component.\n\n**Cons**:\n\n- **Transient**: Doesn't survive page refreshes, returning to the page later, or unmounting and remounting the component.\n\n**Implementation**:\n\n```tsx\nfunction Sidebar() {\n  const [isOpen, setIsOpen] = useState(false);\n  return (\n    <div>\n      <button onClick={() => setIsOpen((open) => !open)}>\n        {isOpen ? \"Close\" : \"Open\"}\n      </button>\n      <aside hidden={!isOpen}>\n        <Outlet />\n      </aside>\n    </div>\n  );\n}\n```\n\n#### Local Storage\n\nTo persist state beyond the component lifecycle, browser local storage is a step-up. See our doc on [Client Data][client_data] for more advanced examples.\n\n**Pros**:\n\n- **Persistent**: Maintains state across page refreshes and component mounts/unmounts.\n- **Encapsulated**: State is scoped to the component.\n\n**Cons**:\n\n- **Requires Synchronization**: React components must sync up with local storage to initialize and save the current state.\n- **Server Rendering Limitation**: The [`window`][window_global] and [`localStorage`][local_storage_global] objects are not accessible during server-side rendering, so state must be initialized in the browser with an effect.\n- **UI Flickering**: On initial page loads, the state in local storage may not match what was rendered by the server and the UI will flicker when JavaScript loads.\n\n**Implementation**:\n\n```tsx\nfunction Sidebar() {\n  const [isOpen, setIsOpen] = useState(false);\n\n  // synchronize initially\n  useLayoutEffect(() => {\n    const isOpen = window.localStorage.getItem(\"sidebar\");\n    setIsOpen(isOpen);\n  }, []);\n\n  // synchronize on change\n  useEffect(() => {\n    window.localStorage.setItem(\"sidebar\", isOpen);\n  }, [isOpen]);\n\n  return (\n    <div>\n      <button onClick={() => setIsOpen((open) => !open)}>\n        {isOpen ? \"Close\" : \"Open\"}\n      </button>\n      <aside hidden={!isOpen}>\n        <Outlet />\n      </aside>\n    </div>\n  );\n}\n```\n\nIn this approach, state must be initialized within an effect. This is crucial to avoid complications during server-side rendering. Directly initializing the React state from `localStorage` will cause errors since `window.localStorage` is unavailable during server rendering.\n\n```tsx bad lines=[4]\nfunction Sidebar() {\n  const [isOpen, setIsOpen] = useState(\n    // error: window is not defined\n    window.localStorage.getItem(\"sidebar\"),\n  );\n\n  // ...\n}\n```\n\nBy initializing the state within an effect, there's potential for a mismatch between the server-rendered state and the state stored in local storage. This discrepancy will lead to brief UI flickering shortly after the page renders and should be avoided.\n\n#### Cookies\n\nCookies offer a comprehensive solution for this use case. However, this method introduces added preliminary setup before making the state accessible within the component.\n\n**Pros**:\n\n- **Server Rendering**: State is available on the server for rendering and even for server actions.\n- **Single Source of Truth**: Eliminates state synchronization hassles.\n- **Persistence**: Maintains state across page loads and component mounts/unmounts. State can even persist across devices if you switch to a database-backed session.\n- **Progressive Enhancement**: Functions even before JavaScript loads.\n\n**Cons**:\n\n- **Boilerplate**: Requires more code because of the network.\n- **Exposed**: The state is not encapsulated to a single component, other parts of the app must be aware of the cookie.\n\n**Implementation**:\n\nFirst we'll need to create a cookie object:\n\n```tsx\nimport { createCookie } from \"react-router\";\nexport const prefs = createCookie(\"prefs\");\n```\n\nNext we set up the server action and loader to read and write the cookie:\n\n```tsx filename=app/routes/sidebar.tsx\nimport { data, Outlet } from \"react-router\";\nimport type { Route } from \"./+types/sidebar\";\n\nimport { prefs } from \"./prefs-cookie\";\n\n// read the state from the cookie\nexport async function loader({\n  request,\n}: Route.LoaderArgs) {\n  const cookieHeader = request.headers.get(\"Cookie\");\n  const cookie = (await prefs.parse(cookieHeader)) || {};\n  return data({ sidebarIsOpen: cookie.sidebarIsOpen });\n}\n\n// write the state to the cookie\nexport async function action({\n  request,\n}: Route.ActionArgs) {\n  const cookieHeader = request.headers.get(\"Cookie\");\n  const cookie = (await prefs.parse(cookieHeader)) || {};\n  const formData = await request.formData();\n\n  const isOpen = formData.get(\"sidebar\") === \"open\";\n  cookie.sidebarIsOpen = isOpen;\n\n  return data(isOpen, {\n    headers: {\n      \"Set-Cookie\": await prefs.serialize(cookie),\n    },\n  });\n}\n```\n\nAfter the server code is set up, we can use the cookie state in our UI:\n\n```tsx\nfunction Sidebar({ loaderData }: Route.ComponentProps) {\n  const fetcher = useFetcher();\n  let { sidebarIsOpen } = loaderData;\n\n  // use optimistic UI to immediately change the UI state\n  if (fetcher.formData?.has(\"sidebar\")) {\n    sidebarIsOpen =\n      fetcher.formData.get(\"sidebar\") === \"open\";\n  }\n\n  return (\n    <div>\n      <fetcher.Form method=\"post\">\n        <button\n          name=\"sidebar\"\n          value={sidebarIsOpen ? \"closed\" : \"open\"}\n        >\n          {sidebarIsOpen ? \"Close\" : \"Open\"}\n        </button>\n      </fetcher.Form>\n      <aside hidden={!sidebarIsOpen}>\n        <Outlet />\n      </aside>\n    </div>\n  );\n}\n```\n\nWhile this is certainly more code that touches more of the application to account for the network requests and responses, the UX is greatly improved. Additionally, state comes from a single source of truth without any state synchronization required.\n\nIn summary, each of the discussed methods offers a unique set of benefits and challenges:\n\n- **React state**: Offers simple but transient state management.\n- **Local Storage**: Provides persistence but with synchronization requirements and UI flickering.\n- **Cookies**: Delivers robust, persistent state management at the cost of added boilerplate.\n\nNone of these are wrong, but if you want to persist the state across visits, cookies offer the best user experience.\n\n### Form Validation and Action Data\n\nClient-side validation can augment the user experience, but similar enhancements can be achieved by leaning more towards server-side processing and letting it handle the complexities.\n\nThe following example illustrates the inherent complexities of managing network state, coordinating state from the server, and implementing validation redundantly on both the client and server sides. It's just for illustration, so forgive any obvious bugs or problems you find.\n\n```tsx bad lines=[2,11,27,38,63]\nexport function Signup() {\n  // A multitude of React State declarations\n  const [isSubmitting, setIsSubmitting] = useState(false);\n\n  const [userName, setUserName] = useState(\"\");\n  const [userNameError, setUserNameError] = useState(null);\n\n  const [password, setPassword] = useState(null);\n  const [passwordError, setPasswordError] = useState(\"\");\n\n  // Replicating server-side logic in the client\n  function validateForm() {\n    setUserNameError(null);\n    setPasswordError(null);\n    const errors = validateSignupForm(userName, password);\n    if (errors) {\n      if (errors.userName) {\n        setUserNameError(errors.userName);\n      }\n      if (errors.password) {\n        setPasswordError(errors.password);\n      }\n    }\n    return Boolean(errors);\n  }\n\n  // Manual network interaction handling\n  async function handleSubmit() {\n    if (validateForm()) {\n      setSubmitting(true);\n      const res = await postJSON(\"/api/signup\", {\n        userName,\n        password,\n      });\n      const json = await res.json();\n      setIsSubmitting(false);\n\n      // Server state synchronization to the client\n      if (json.errors) {\n        if (json.errors.userName) {\n          setUserNameError(json.errors.userName);\n        }\n        if (json.errors.password) {\n          setPasswordError(json.errors.password);\n        }\n      }\n    }\n  }\n\n  return (\n    <form\n      onSubmit={(event) => {\n        event.preventDefault();\n        handleSubmit();\n      }}\n    >\n      <p>\n        <input\n          type=\"text\"\n          name=\"username\"\n          value={userName}\n          onChange={() => {\n            // Synchronizing form state for the fetch\n            setUserName(event.target.value);\n          }}\n        />\n        {userNameError ? <i>{userNameError}</i> : null}\n      </p>\n\n      <p>\n        <input\n          type=\"password\"\n          name=\"password\"\n          onChange={(event) => {\n            // Synchronizing form state for the fetch\n            setPassword(event.target.value);\n          }}\n        />\n        {passwordError ? <i>{passwordError}</i> : null}\n      </p>\n\n      <button disabled={isSubmitting} type=\"submit\">\n        Sign Up\n      </button>\n\n      {isSubmitting ? <BusyIndicator /> : null}\n    </form>\n  );\n}\n```\n\nThe backend endpoint, `/api/signup`, also performs validation and sends error feedback. Note that some essential validation, like detecting duplicate usernames, can only be done server-side using information the client doesn't have access to.\n\n```tsx bad\nexport async function signupHandler(request: Request) {\n  const errors = await validateSignupRequest(request);\n  if (errors) {\n    return { ok: false, errors: errors };\n  }\n  await signupUser(request);\n  return { ok: true, errors: null };\n}\n```\n\nNow, let's contrast this with a React Router-based implementation. The action remains consistent, but the component is vastly simplified due to the direct utilization of server state via `actionData`, and leveraging the network state that React Router inherently manages.\n\n```tsx filename=app/routes/signup.tsx good lines=[20-22]\nimport { useNavigation } from \"react-router\";\nimport type { Route } from \"./+types/signup\";\n\nexport async function action({\n  request,\n}: ActionFunctionArgs) {\n  const errors = await validateSignupRequest(request);\n  if (errors) {\n    return { ok: false, errors: errors };\n  }\n  await signupUser(request);\n  return { ok: true, errors: null };\n}\n\nexport function Signup({\n  actionData,\n}: Route.ComponentProps) {\n  const navigation = useNavigation();\n\n  const userNameError = actionData?.errors?.userName;\n  const passwordError = actionData?.errors?.password;\n  const isSubmitting = navigation.formAction === \"/signup\";\n\n  return (\n    <Form method=\"post\">\n      <p>\n        <input type=\"text\" name=\"username\" />\n        {userNameError ? <i>{userNameError}</i> : null}\n      </p>\n\n      <p>\n        <input type=\"password\" name=\"password\" />\n        {passwordError ? <i>{passwordError}</i> : null}\n      </p>\n\n      <button disabled={isSubmitting} type=\"submit\">\n        Sign Up\n      </button>\n\n      {isSubmitting ? <BusyIndicator /> : null}\n    </Form>\n  );\n}\n```\n\nThe extensive state management from our previous example is distilled into just three code lines. We eliminate the necessity for React state, change event listeners, submit handlers, and state management libraries for such network interactions.\n\nDirect access to the server state is made possible through `actionData`, and network state through `useNavigation` (or `useFetcher`).\n\nAs bonus party trick, the form is functional even before JavaScript loads (see [Progressive Enhancement][progressive_enhancement]). Instead of React Router managing the network operations, the default browser behaviors step in.\n\nIf you ever find yourself entangled in managing and synchronizing state for network operations, React Router likely offers a more elegant solution.\n\n[use_navigation]: https://api.reactrouter.com/v7/functions/react-router.useNavigation\n[use_fetcher]: https://api.reactrouter.com/v7/functions/react-router.useFetcher\n[loader_data]: ../start/framework/data-loading\n[action_data]: ../start/framework/actions\n[cookies]: ./sessions-and-cookies#cookies\n[sessions]: ./sessions-and-cookies#sessions\n[cache_control_header]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control\n[pending_ui]: ../start/framework/pending-ui\n[client_data]: ../how-to/client-data\n[window_global]: https://developer.mozilla.org/en-US/docs/Web/API/Window/window\n[local_storage_global]: https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage\n[progressive_enhancement]: ./progressive-enhancement\n"
  },
  {
    "path": "docs/explanation/type-safety.md",
    "content": "---\ntitle: Type Safety\n---\n\n# Type Safety\n\n[MODES: framework]\n\n<br/>\n<br/>\n\nIf you haven't done so already, check out our guide for [setting up type safety][route-module-type-safety] in a new project.\n\nReact Router generates types for each route in your app to provide type safety for the route module exports.\n\nFor example, let's say you have a `products/:id` route configured:\n\n```ts filename=app/routes.ts\nimport {\n  type RouteConfig,\n  route,\n} from \"@react-router/dev/routes\";\n\nexport default [\n  route(\"products/:id\", \"./routes/product.tsx\"),\n] satisfies RouteConfig;\n```\n\nYou can import route-specific types like so:\n\n```tsx filename=app/routes/product.tsx\nimport type { Route } from \"./+types/product\";\n// types generated for this route 👆\n\nexport function loader({ params }: Route.LoaderArgs) {\n  //                      👆 { id: string }\n  return { planet: `world #${params.id}` };\n}\n\nexport default function Component({\n  loaderData, // 👈 { planet: string }\n}: Route.ComponentProps) {\n  return <h1>Hello, {loaderData.planet}!</h1>;\n}\n```\n\n## How it works\n\nReact Router's type generation executes your route config (`app/routes.ts` by default) to determine the routes for your app.\nIt then generates a `+types/<route file>.d.ts` for each route within a special `.react-router/types/` directory.\nWith [`rootDirs` configured][route-module-type-safety], TypeScript can import these generated files as if they were right next to their corresponding route modules.\n\nFor a deeper dive into some of the design decisions, check out our [type inference decision doc](https://github.com/remix-run/react-router/blob/dev/decisions/0012-type-inference.md).\n\n[route-module-type-safety]: ../how-to/route-module-type-safety\n\n## `typegen` command\n\nYou can manually generate types with the `typegen` command:\n\n```sh\nreact-router typegen\n```\n\nThe following types are generated for each route:\n\n- `LoaderArgs`\n- `ClientLoaderArgs`\n- `ActionArgs`\n- `ClientActionArgs`\n- `HydrateFallbackProps`\n- `ComponentProps` (for the `default` export)\n- `ErrorBoundaryProps`\n\n### --watch\n\nIf you run `react-router dev` — or if your custom server calls `vite.createServer` — then React Router's Vite plugin is already generating up-to-date types for you.\nBut if you really need to run type generation on its own, you can also use `--watch` to automatically regenerate types as files change:\n\n```sh\nreact-router typegen --watch\n```\n"
  },
  {
    "path": "docs/how-to/README",
    "content": "How-To:\n\n- Practical Steps\n- Problem Oriented\n- Useful when we're coding\n"
  },
  {
    "path": "docs/how-to/accessibility.md",
    "content": "---\ntitle: Accessibility\n---\n\n# Accessibility\n\nAccessibility in a React Router app looks a lot like accessibility on the web in general. Using proper semantic markup and following the [Web Content Accessibility Guidelines (WCAG)][wcag] will get you most of the way there.\n\nReact Router makes certain accessibility practices the default where possible and provides APIs to help where it's not.\n\n## Links\n\n[MODES: framework, data, declarative]\n\n<br/>\n<br/>\n\nThe [`<Link>` component][link] renders a standard anchor tag, meaning that you get its accessibility behaviors from the browser for free!\n\nReact Router also provides the [`<NavLink/>`][navlink] which behaves the same as `<Link>`, but it also provides context for assistive technology when the link points to the current page. This is useful for building navigation menus or breadcrumbs.\n\n## Routing\n\n[MODES: framework]\n\n<br/>\n<br/>\n\nIf you are rendering [`<Scripts>`][scripts] in your app, there are some important things to consider to make client-side routing more accessible for your users.\n\nWith a traditional multi-page website we don't have to think about route changes too much. Your app renders an anchor tag, and the browser handles the rest. If your users disable JavaScript, your React Router app should already work this way by default!\n\nWhen the client scripts in React Router are loaded, React Router takes control of routing and prevents the browser's default behavior. React Router doesn't make any assumptions about your UI as the route changes. There are some important features you'll want to consider as a result, including:\n\n- **Focus management:** What element receives focus when the route changes? This is important for keyboard users and can be helpful for screen-reader users.\n- **Live-region announcements:** Screen-reader users also benefit from announcements when a route has changed. You may want to also notify them during certain transition states depending on the nature of the change and how long loading is expected to take.\n\nIn 2019, [Marcy Sutton led and published findings from user research][marcy-sutton-led-and-published-findings-from-user-research] to help developers build accessible client-side routing experiences.\n\n[link]: ../api/components/Link\n[navlink]: ../api/components/NavLink\n[scripts]: ../api/components/Scripts\n[wcag]: https://www.w3.org/WAI/standards-guidelines/wcag/\n[marcy-sutton-led-and-published-findings-from-user-research]: https://www.gatsbyjs.com/blog/2019-07-11-user-testing-accessible-client-routing\n"
  },
  {
    "path": "docs/how-to/client-data.md",
    "content": "---\ntitle: Client Data\n---\n\n# Client Data\n\n[MODES: framework]\n\n<br/>\n<br/>\n\nYou can fetch and mutate data directly in the browser using `clientLoader` and `clientAction` functions.\n\nThese functions are the primary mechanism for data handling when using [SPA mode][spa]. This guide demonstrates common use cases for leveraging client data in Server-Side Rendering (SSR).\n\n## Skip the Server Hop\n\nWhen using React Router with a Backend-For-Frontend (BFF) architecture, you might want to bypass the React Router server and communicate directly with your backend API. This approach requires proper authentication handling and assumes no CORS restrictions. Here's how to implement this:\n\n1. Load the data from server `loader` on the document load\n2. Load the data from the `clientLoader` on all subsequent loads\n\nIn this scenario, React Router will _not_ call the `clientLoader` on hydration - and will only call it on subsequent navigations.\n\n```tsx lines=[4,11]\nexport async function loader({\n  request,\n}: Route.LoaderArgs) {\n  const data = await fetchApiFromServer({ request }); // (1)\n  return data;\n}\n\nexport async function clientLoader({\n  request,\n}: Route.ClientLoaderArgs) {\n  const data = await fetchApiFromClient({ request }); // (2)\n  return data;\n}\n```\n\n## Fullstack State\n\nSometimes you need to combine data from both the server and browser (like IndexedDB or browser SDKs) before rendering a component. Here's how to implement this pattern:\n\n1. Load the partial data from server `loader` on the document load\n2. Export a [`HydrateFallback`][hydratefallback] component to render during SSR because we don't yet have a full set of data\n3. Set `clientLoader.hydrate = true`, this instructs React Router to call the clientLoader as part of initial document hydration\n4. Combine the server data with the client data in `clientLoader`\n\n```tsx lines=[4-6,19-20,23,26]\nexport async function loader({\n  request,\n}: Route.LoaderArgs) {\n  const partialData = await getPartialDataFromDb({\n    request,\n  }); // (1)\n  return partialData;\n}\n\nexport async function clientLoader({\n  request,\n  serverLoader,\n}: Route.ClientLoaderArgs) {\n  const [serverData, clientData] = await Promise.all([\n    serverLoader(),\n    getClientData(request),\n  ]);\n  return {\n    ...serverData, // (4)\n    ...clientData, // (4)\n  };\n}\nclientLoader.hydrate = true as const; // (3)\n\nexport function HydrateFallback() {\n  return <p>Skeleton rendered during SSR</p>; // (2)\n}\n\nexport default function Component({\n  // This will always be the combined set of server + client data\n  loaderData,\n}: Route.ComponentProps) {\n  return <>...</>;\n}\n```\n\n## Choosing Server or Client Data Loading\n\nYou can mix data loading strategies across your application, choosing between server-only or client-only data loading for each route. Here's how to implement both approaches:\n\n1. Export a `loader` when you want to use server data\n2. Export `clientLoader` and a `HydrateFallback` when you want to use client data\n\nA route that only depends on a server loader looks like this:\n\n```tsx filename=app/routes/server-data-route.tsx\nexport async function loader({\n  request,\n}: Route.LoaderArgs) {\n  const data = await getServerData(request);\n  return data;\n}\n\nexport default function Component({\n  loaderData, // (1) - server data\n}: Route.ComponentProps) {\n  return <>...</>;\n}\n```\n\nA route that only depends on a client loader looks like this.\n\n```tsx filename=app/routes/client-data-route.tsx\nexport async function clientLoader({\n  request,\n}: Route.ClientLoaderArgs) {\n  const clientData = await getClientData(request);\n  return clientData;\n}\n// Note: you do not have to set this explicitly - it is implied if there is no `loader`\nclientLoader.hydrate = true;\n\n// (2)\nexport function HydrateFallback() {\n  return <p>Skeleton rendered during SSR</p>;\n}\n\nexport default function Component({\n  loaderData, // (2) - client data\n}: Route.ComponentProps) {\n  return <>...</>;\n}\n```\n\n## Client-Side Caching\n\nYou can implement client-side caching (using memory, localStorage, etc.) to optimize server requests. Here's a pattern that demonstrates cache management:\n\n1. Load the data from server `loader` on the document load\n2. Set `clientLoader.hydrate = true` to prime the cache\n3. Load subsequent navigations from the cache via `clientLoader`\n4. Invalidate the cache in your `clientAction`\n\nNote that since we are not exporting a `HydrateFallback` component, we will SSR the route component and then run the `clientLoader` on hydration, so it's important that your `loader` and `clientLoader` return the same data on initial load to avoid hydration errors.\n\n```tsx lines=[4,26,32,39,46]\nexport async function loader({\n  request,\n}: Route.LoaderArgs) {\n  const data = await getDataFromDb({ request }); // (1)\n  return data;\n}\n\nexport async function action({\n  request,\n}: Route.ActionArgs) {\n  await saveDataToDb({ request });\n  return { ok: true };\n}\n\nlet isInitialRequest = true;\n\nexport async function clientLoader({\n  request,\n  serverLoader,\n}: Route.ClientLoaderArgs) {\n  const cacheKey = generateKey(request);\n\n  if (isInitialRequest) {\n    isInitialRequest = false;\n    const serverData = await serverLoader();\n    cache.set(cacheKey, serverData); // (2)\n    return serverData;\n  }\n\n  const cachedData = await cache.get(cacheKey);\n  if (cachedData) {\n    return cachedData; // (3)\n  }\n\n  const serverData = await serverLoader();\n  cache.set(cacheKey, serverData);\n  return serverData;\n}\nclientLoader.hydrate = true; // (2)\n\nexport async function clientAction({\n  request,\n  serverAction,\n}: Route.ClientActionArgs) {\n  const cacheKey = generateKey(request);\n  cache.delete(cacheKey); // (4)\n  const serverData = await serverAction();\n  return serverData;\n}\n```\n\n[spa]: ../how-to/spa\n[hydratefallback]: ../start/framework/route-module#hydratefallback\n"
  },
  {
    "path": "docs/how-to/data-strategy.md",
    "content": "---\ntitle: Data Strategy\n---\n\n# Data Strategy\n\n[MODES: data]\n\n<br />\n<br />\n\n<docs-warning>This is a low-level API intended for advanced use-cases. This overrides React Router's internal handling of `action`/`loader` execution, and if done incorrectly will break your app code. Please use with caution and perform the appropriate testing.</docs-warning>\n\n## Overview\n\nBy default, React Router is opinionated about how your data is loaded/submitted - and most notably, executes all of your [`loader`][loader] functions in parallel for optimal data fetching. While we think this is the right behavior for most use-cases, we realize that there is no \"one size fits all\" solution when it comes to data fetching for the wide landscape of application requirements.\n\nThe [`dataStrategy`][data-strategy] option gives you full control over how your [`action`][action]/[`loader`][loader] functions are executed and lays the foundation to build in more advanced APIs such as middleware, context, and caching layers. Over time, we expect that we'll leverage this API internally to bring more first class APIs to React Router, but until then (and beyond), this is your way to add more advanced functionality for your application's data needs.\n\n## Usage\n\nA custom `dataStrategy` receives the `loader`/`action` arguments (`request`, `params`, `context`) plus a few more that allow you to decide how you want to control the executions for your application:\n\n- `matches`: An array of `DataStrategyMatch` instances for the routes matched by the current `request`\n- `runClientMiddleware`: A helper function to run the middleware for the matched routes\n- `fetcherKey`: The fetcher key if this is for a fetcher request and not a navigation\n\nA `DataStrategyMatch` is a normal route match plus a few additional fields:\n\n- `shouldCallHandler`: A function that tells you whether this routes handler should be called for this request\n- `shouldRevalidateArgs`: The arguments that to be passed to the routes `shouldRevalidate` for this request\n- ~~`shouldLoad`~~: A boolean field for whether this routes handler should be run for this request\n  - Deprecated in favor of the more powerful `shouldCallHandler` API\n- `resolve`: A function to handle call through to the route handler, and also allow you custom execution of the handler\n\nHere's a basic example that adds logging around the handler executions:\n\n```tsx\nlet router = createBrowserRouter(routes, {\n  async dataStrategy({\n    matches,\n    request,\n    runClientMiddleware,\n  }) {\n    // Determine which matches are expected to be executed for this request.\n    // - For loading navigations, this will return true for new routes + existing\n    //   routes requiring revalidation\n    // - For submission navigations, this will only return true for the action route\n    // - For fetcher calls, this will only return true for the fetcher route\n    const matchesToLoad = matches.filter((m) =>\n      m.shouldCallHandler(),\n    );\n\n    // For each match that we want to execute, call match.resolve() to execute\n    // the handler and store the result\n    const results: Record<string, DataStrategyResult> = {};\n    await runClientMiddleware(() =>\n      Promise.all(\n        matchesToLoad.map(async (match) => {\n          console.log(`Processing ${match.route.id}`);\n          // The resolve function calls through to the route handler\n          results[match.route.id] = await match.resolve();\n        }),\n      ),\n    );\n    return results;\n  },\n});\n```\n\nThe `dataStrategy` function should return a `Record<string, DataStrategyResult>` which contains the result for each handler that was executed. A `DataStrategyResult` is just a wrapper object that indicates if the handler returned or threw:\n\n```ts\ninterface DataStrategyResult {\n  type: \"data\" | \"error\";\n  result: unknown; // data, Error, Response, data()\n}\n```\n\n### Calling Route Middleware\n\nIf you are using `middleware` on your routes, you need to leverage the `callClientMiddleware` helper function to execute `middleware` around your handlers:\n\n```tsx\nlet router = createBrowserRouter(routes, {\n  async dataStrategy({\n    matches,\n    request,\n    runClientMiddleware,\n  }) {\n    const matchesToLoad = matches.filter((m) =>\n      m.shouldCallHandler(),\n    );\n    const results: Record<string, DataStrategyResult> = {};\n\n    // Run middleware and execute handlers at the end of the middleware chain\n    await runClientMiddleware(() =>\n      Promise.all(\n        matchesToLoad.map(async (match) => {\n          results[match.route.id] = await match.resolve();\n        }),\n      ),\n    );\n    return results;\n  },\n});\n```\n\n`runClientMiddleware` takes the same arguments as `dataStrategy` so it can also be easily composed with a standalone `dataStrategy` implementation:\n\n```tsx\nconst loggingDataStrategy: DataStrategyFunction = () => {\n  /* ... */\n};\n\nlet router = createBrowserRouter(routes, {\n  async dataStrategy({ runClientMiddleware }) {\n    let results = await runClientMiddleware(\n      loggingDataStrategy,\n    );\n    return results;\n  },\n});\n```\n\n### Advanced handler execution\n\nIf you want more fine-grained control over the execution of the handler, you can pass a callback to `match.resolve()`:\n\n```tsx\n// Assume a loader shape such as\nfunction loader({ request }, customContext) {...}\n\n// In your dataStrategy, you can pass this context from inside a resolve callback\nawait Promise.all(\n  matchesToLoad.map((match, i) =>\n    match.resolve((handler) => {\n      let customContext = getCustomContext();\n      // Call the handler and p[ass a custom parameter as the handler's second argument\n      return handler(customContext);\n    }),\n  ),\n);\n```\n\n### Custom Revalidation Behavior\n\nIf you want to alter the revalidation behavior, you can pass your own `defaultShouldRevalidate` to `match.shouldCallHandler()` which will pass through to any route level `shouldRevalidate` functions. The arguments that would be passed to the route level `shouldRevalidate` are available on `match.shouldRevalidateArgs`:\n\n```tsx\nconst matchesToLoad = matches.filter((match) => {\n  let defaultShouldRevalidate = customShouldRevalidate(\n    match.shouldRevalidateArgs,\n  );\n  return m.shouldCallHandler(defaultShouldRevalidate);\n});\n```\n\n## Migrating away from `shouldLoad`\n\nNow that we have stabilized the new `match.shouldCallHandler()`/`match.shouldRevalidateArgs` fields, it's recommended to move away from the now-deprecated `match.shouldLoad` API. The prior boolean approach did not allow for custom `dataStrategy`functions to alter the default revalidation behavior, so the new function-based APIs were created to allow that.\n\nThe major difference between these two APIs is that when using `shouldLoad`, calling `resolve()` would _only_ call the handler if `shouldLoad` was `true`. You could safely call it for all matches even if only a subset needed to have their handlers executed.\n\nWith `shouldCallHandler`, you are in charge of which handlers should be called so calling resolve will automatically call the handler. You should only call resolve on a the set of matches you wish to run handlers for.\n\nHere's an example change from the prior API to the new API. Note that we pre-filter the `matchesToLoad` before calling `resolve()`:\n\n```diff\nlet results = {};\n+let matchesToLoad = matches.filter(m => m.shouldCallHandler());\nawait Promise.all(() =>\n-  matches.map((m) => {\n+  matchesToLoad.map((m) => {\n    results[m.route.id] = await m.resolve();\n  }),\n);\nreturn results;\n```\n\n## Advanced Use Cases\n\n### Custom Middleware\n\n<docs-info>This is an unlikely use-case now that React Router has built-in middleware, but if you wish to use a custom middleware you can do so with a `dataStrategy`.</docs-info>\n\nLet's define a middleware on each route via [`handle`](../../start/data/route-object#handle)\nand call middleware sequentially first, then call all\n[`loader`](../../start/data/route-object#loader)s in parallel - providing\nany data made available via the middleware:\n\n```ts\nconst routes = [\n  {\n    id: \"parent\",\n    path: \"/parent\",\n    loader({ request }, context) {\n      // ...\n    },\n    handle: {\n      async middleware({ request }, context) {\n        context.parent = \"PARENT MIDDLEWARE\";\n      },\n    },\n    children: [\n      {\n        id: \"child\",\n        path: \"child\",\n        loader({ request }, context) {\n          // ...\n        },\n        handle: {\n          async middleware({ request }, context) {\n            context.child = \"CHILD MIDDLEWARE\";\n          },\n        },\n      },\n    ],\n  },\n];\n\nlet router = createBrowserRouter(routes, {\n  async dataStrategy({ matches, params, request }) {\n    // Run middleware sequentially and let them add data to `context`\n    let context = {};\n    for (const match of matches) {\n      if (match.route.handle?.middleware) {\n        await match.route.handle.middleware(\n          { request, params },\n          context,\n        );\n      }\n    }\n\n    // Run loaders in parallel with the `context` value\n    let matchesToLoad = matches.filter((m) =>\n      m.shouldCallHandler(),\n    );\n    let results = await Promise.all(\n      matchesToLoad.map((match, i) =>\n        match.resolve((handler) => {\n          // Whatever you pass to `handler` will be passed as the 2nd parameter\n          // to your loader/action\n          return handler(context);\n        }),\n      ),\n    );\n    return results.reduce(\n      (acc, result, i) =>\n        Object.assign(acc, {\n          [matchesToLoad[i].route.id]: result,\n        }),\n      {},\n    );\n  },\n});\n```\n\n### Custom Handler\n\nIt's also possible you don't even want to define a [`loader`](../../start/daoute-object#loader)\nimplementation at the route level. Maybe you want to just determine the\nroutes and issue a single GraphQL request for all of your data. You can do\nthat by setting your `route.loader=true` so it qualifies as \"having a\nloader\", and then store GQL fragments on `route.handle`:\n\n```ts\nconst routes = [\n  {\n    id: \"parent\",\n    path: \"/parent\",\n    loader: true,\n    handle: {\n      gql: gql`\n        fragment Parent on Whatever {\n          parentField\n        }\n      `,\n    },\n    children: [\n      {\n        id: \"child\",\n        path: \"child\",\n        loader: true,\n        handle: {\n          gql: gql`\n            fragment Child on Whatever {\n              childField\n            }\n          `,\n        },\n      },\n    ],\n  },\n];\n\nlet router = createBrowserRouter(routes, {\n  async dataStrategy({ matches, params, request }) {\n    const matchesToLoad = matches.filter((m) =>\n      m.shouldCallHandler(),\n    );\n    // Compose route fragments into a single GQL payload\n    let gql = getFragmentsFromRouteHandles(matchesToLoad);\n    let data = await fetchGql(gql);\n    // Parse results back out into individual route level `DataStrategyResult`'s\n    // keyed by `routeId`\n    let results = parseResultsFromGql(matchesToLoad, data);\n    return results;\n  },\n});\n```\n\nNote that we never actually call `match.resolve()` in this scenario since we don't want to call the handlers defined on the routes. We instead make a single GQL call and split the resulting data back out to the proper routes in `results`.\n\n[loader]: ../start/data/route-object#loader\n[action]: ../start/data/route-object#action\n[data-strategy]: ../api/data-routers/createBrowserRouter#optsdatastrategy\n"
  },
  {
    "path": "docs/how-to/error-boundary.md",
    "content": "---\ntitle: Error Boundaries\n---\n\n# Error Boundaries\n\n[MODES: framework, data]\n\n<br/>\n<br/>\n\nTo avoid rendering an empty page to users, route modules will automatically catch errors in your code and render the closest `ErrorBoundary`.\n\nError boundaries are not intended for rendering form validation errors or error reporting. Please see [Form Validation](./form-validation) and [Error Reporting](./error-reporting) instead.\n\n## 1. Add a root error boundary\n\nAll applications should at a minimum export a root error boundary. This one handles the three main cases:\n\n- Thrown `data` with a status code and text\n- Instances of errors with a stack trace\n- Randomly thrown values\n\n### Framework Mode\n\n[modes: framework]\n\nIn [Framework Mode][picking-a-mode], errors are passed to the route-level error boundary as a prop (see [`Route.ErrorBoundaryProps`][type-safety]), so you don't need to use a hook to grab it:\n\n```tsx filename=root.tsx lines=[1,3-5]\nimport { Route } from \"./+types/root\";\n\nexport function ErrorBoundary({\n  error,\n}: Route.ErrorBoundaryProps) {\n  if (isRouteErrorResponse(error)) {\n    return (\n      <>\n        <h1>\n          {error.status} {error.statusText}\n        </h1>\n        <p>{error.data}</p>\n      </>\n    );\n  } else if (error instanceof Error) {\n    return (\n      <div>\n        <h1>Error</h1>\n        <p>{error.message}</p>\n        <p>The stack trace is:</p>\n        <pre>{error.stack}</pre>\n      </div>\n    );\n  } else {\n    return <h1>Unknown Error</h1>;\n  }\n}\n```\n\n### Data Mode\n\n[modes: data]\n\nIn [Data Mode][picking-a-mode], the `ErrorBoundary` doesn't receive props, so you can access it via `useRouteError`:\n\n```tsx lines=[1,6,16]\nimport { useRouteError } from \"react-router\";\n\nlet router = createBrowserRouter([\n  {\n    path: \"/\",\n    ErrorBoundary: RootErrorBoundary,\n    Component: Root,\n  },\n]);\n\nfunction Root() {\n  /* ... */\n}\n\nfunction RootErrorBoundary() {\n  let error = useRouteError();\n  if (isRouteErrorResponse(error)) {\n    return (\n      <>\n        <h1>\n          {error.status} {error.statusText}\n        </h1>\n        <p>{error.data}</p>\n      </>\n    );\n  } else if (error instanceof Error) {\n    return (\n      <div>\n        <h1>Error</h1>\n        <p>{error.message}</p>\n        <p>The stack trace is:</p>\n        <pre>{error.stack}</pre>\n      </div>\n    );\n  } else {\n    return <h1>Unknown Error</h1>;\n  }\n}\n```\n\n## 2. Write a bug\n\n[modes: framework,data]\n\nIt's not recommended to intentionally throw errors to force the error boundary to render as a means of control flow. Error Boundaries are primarily for catching unintentional errors in your code.\n\n```tsx\nexport async function loader() {\n  return undefined();\n}\n```\n\nThis will render the `instanceof Error` branch of the UI from step 1.\n\nThis is not just for loaders, but for all route module APIs: loaders, actions, components, headers, links, and meta.\n\n## 3. Throw data in loaders/actions\n\n[modes: framework,data]\n\nThere are exceptions to the rule in #2, especially 404s. You can intentionally `throw data()` (with a proper status code) to the closest error boundary when your loader can't find what it needs to render the page. Throw a 404 and move on.\n\n```tsx\nimport { data } from \"react-router\";\n\nexport async function loader({ params }) {\n  let record = await fakeDb.getRecord(params.id);\n  if (!record) {\n    throw data(\"Record Not Found\", { status: 404 });\n  }\n  return record;\n}\n```\n\nThis will render the `isRouteErrorResponse` branch of the UI from step 1.\n\n## 4. Nested error boundaries\n\nWhen an error is thrown, the \"closest error boundary\" will be rendered.\n\n### Framework Mode\n\n[modes: framework]\n\nConsider these nested routes:\n\n```tsx filename=\"routes.ts\"\n// ✅ has error boundary\nroute(\"/app\", \"app.tsx\", [\n  // ❌ no error boundary\n  route(\"invoices\", \"invoices.tsx\", [\n    // ✅ has error boundary\n    route(\"invoices/:id\", \"invoice-page.tsx\", [\n      // ❌ no error boundary\n      route(\"payments\", \"payments.tsx\"),\n    ]),\n  ]),\n]);\n```\n\nThe following table shows which error boundary will render given the origin of the error:\n\n| error origin     | rendered boundary |\n| ---------------- | ----------------- |\n| app.tsx          | app.tsx           |\n| invoices.tsx     | app.tsx           |\n| invoice-page.tsx | invoice-page.tsx  |\n| payments.tsx     | invoice-page.tsx  |\n\n### Data Mode\n\n[modes: data]\n\nIn Data Mode, the equivalent route tree might look like:\n\n```tsx\nlet router = createBrowserRouter([\n  {\n    path: \"/app\",\n    Component: App,\n    ErrorBoundary: AppErrorBoundary, // ✅ has error boundary\n    children: [\n      {\n        path: \"invoices\",\n        Component: Invoices, // ❌ no error boundary\n        children: [\n          {\n            path: \":id\",\n            Component: Invoice,\n            ErrorBoundary: InvoiceErrorBoundary, // ✅ has error boundary\n            children: [\n              {\n                path: \"payments\",\n                Component: Payments, // ❌ no error boundary\n              },\n            ],\n          },\n        ],\n      },\n    ],\n  },\n]);\n```\n\nThe following table shows which error boundary will render given the origin of the error:\n\n| error origin | rendered boundary      |\n| ------------ | ---------------------- |\n| `App`        | `AppErrorBoundary`     |\n| `Invoices`   | `AppErrorBoundary`     |\n| `Invoice`    | `InvoiceErrorBoundary` |\n| `Payments`   | `InvoiceErrorBoundary` |\n\n## Error Sanitization\n\n[modes: framework]\n\nIn Framework Mode when building for production, any errors that happen on the server are automatically sanitized before being sent to the browser to prevent leaking any sensitive server information (like stack traces).\n\nThis means that a thrown `Error` will have a generic message and no stack trace in production in the browser. The original error is untouched on the server.\n\nAlso note that data sent with `throw data(yourData)` is not sanitized as the data there is intended to be rendered.\n\n[picking-a-mode]: ../start/modes\n[type-safety]: ../explanation/type-safety\n"
  },
  {
    "path": "docs/how-to/error-reporting.md",
    "content": "---\ntitle: Error Reporting\n---\n\n# Error Reporting\n\n[MODES: framework,data]\n\n<br/>\n<br/>\n\nReact Router catches errors in your route modules and sends them to [error boundaries](./error-boundary) to prevent blank pages when errors occur. However, `ErrorBoundary` isn't sufficient for logging and reporting errors.\n\n## Server Errors\n\n[modes: framework]\n\nTo access these caught errors on the server, use the `handleError` export of the server entry module.\n\n### 1. Reveal the server entry\n\nIf you don't see [`entry.server.tsx`][entryserver] in your app directory, you're using a default entry. Reveal it with this cli command:\n\n```shellscript nonumber\nreact-router reveal entry.server\n```\n\n### 2. Export your error handler\n\nThis function is called whenever React Router catches an error in your application on the server.\n\n```tsx filename=entry.server.tsx\nimport { type HandleErrorFunction } from \"react-router\";\n\nexport const handleError: HandleErrorFunction = (\n  error,\n  { request },\n) => {\n  // React Router may abort some interrupted requests, don't log those\n  if (!request.signal.aborted) {\n    myReportError(error);\n\n    // make sure to still log the error so you can see it\n    console.error(error);\n  }\n};\n```\n\nSee also:\n\n- [`handleError`][handleError]\n\n## Client Errors\n\nTo access these caught errors on the client, use the `onError` prop on your [`HydratedRouter`][hydratedrouter] or [`RouterProvider`][routerprovider] component.\n\n### Framework Mode\n\n[modes: framework]\n\n#### 1. Reveal the client entry\n\nIf you don't see [`entry.client.tsx`][entryclient] in your app directory, you're using a default entry. Reveal it with this cli command:\n\n```shellscript nonumber\nreact-router reveal entry.client\n```\n\n#### 2. Add your error handler\n\nThis function is called whenever React Router catches an error in your application on the client.\n\n```tsx filename=entry.client.tsx\nimport { type ClientOnErrorFunction } from \"react-router\";\n\nconst onError: ClientOnErrorFunction = (\n  error,\n  { location, params, unstable_pattern, errorInfo },\n) => {\n  myReportError(error, location, errorInfo);\n\n  // make sure to still log the error so you can see it\n  console.error(error, errorInfo);\n};\n\nstartTransition(() => {\n  hydrateRoot(\n    document,\n    <StrictMode>\n      <HydratedRouter onError={onError} />\n    </StrictMode>,\n  );\n});\n```\n\nSee also:\n\n- [`<HydratedRouter onError>`][hydratedrouter-onerror]\n\n### Data Mode\n\n[modes: data]\n\nThis function is called whenever React Router catches an error in your application on the client.\n\n```tsx\nimport { type ClientOnErrorFunction } from \"react-router\";\n\nconst onError: ClientOnErrorFunction = (\n  error,\n  { location, params, unstable_pattern, errorInfo },\n) => {\n  myReportError(error, location, errorInfo);\n\n  // make sure to still log the error so you can see it\n  console.error(error, errorInfo);\n};\n\nfunction App() {\n  return <RouterProvider onError={onError} />;\n}\n```\n\nSee also:\n\n- [`<RouterProvider onError>`][routerprovider-onerror]\n\n[entryserver]: ../api/framework-conventions/entry.server.tsx\n[handleError]: ../api/framework-conventions/entry.server.tsx#handleerror\n[entryclient]: ../api/framework-conventions/entry.client.tsx\n[hydratedrouter]: ../api/framework-routers/HydratedRouter\n[routerprovider]: ../api/data-routers/RouterProvider\n[hydratedrouter-onerror]: ../api/framework-routers/HydratedRouter#onError\n[routerprovider-onerror]: ../api/data-routers/RouterProvider#onError\n"
  },
  {
    "path": "docs/how-to/fetchers.md",
    "content": "---\ntitle: Using Fetchers\n---\n\n# Using Fetchers\n\n[MODES: framework, data]\n\n<br/>\n<br/>\n\nFetchers are useful for creating complex, dynamic user interfaces that require multiple, concurrent data interactions without causing a navigation.\n\nFetchers track their own, independent state and can be used to load data, mutate data, submit forms, and generally interact with loaders and actions.\n\n## Calling Actions\n\nThe most common case for a fetcher is to submit data to an action, triggering a revalidation of route data. Consider the following route module:\n\n```tsx\nimport { useLoaderData } from \"react-router\";\n\nexport async function clientLoader({ request }) {\n  let title = localStorage.getItem(\"title\") || \"No Title\";\n  return { title };\n}\n\nexport default function Component() {\n  let data = useLoaderData();\n  return (\n    <div>\n      <h1>{data.title}</h1>\n    </div>\n  );\n}\n```\n\n### 1. Add an action\n\nFirst we'll add an action to the route for the fetcher to call:\n\n```tsx lines=[7-11]\nimport { useLoaderData } from \"react-router\";\n\nexport async function clientLoader({ request }) {\n  // ...\n}\n\nexport async function clientAction({ request }) {\n  await new Promise((res) => setTimeout(res, 1000));\n  let data = await request.formData();\n  localStorage.setItem(\"title\", data.get(\"title\"));\n  return { ok: true };\n}\n\nexport default function Component() {\n  let data = useLoaderData();\n  // ...\n}\n```\n\n### 2. Create a fetcher\n\nNext create a fetcher and render a form with it:\n\n```tsx lines=[7,12-14]\nimport { useLoaderData, useFetcher } from \"react-router\";\n\n// ...\n\nexport default function Component() {\n  let data = useLoaderData();\n  let fetcher = useFetcher();\n  return (\n    <div>\n      <h1>{data.title}</h1>\n\n      <fetcher.Form method=\"post\">\n        <input type=\"text\" name=\"title\" />\n      </fetcher.Form>\n    </div>\n  );\n}\n```\n\n### 3. Submit the form\n\nIf you submit the form now, the fetcher will call the action and revalidate the route data automatically.\n\n### 4. Render pending state\n\nFetchers make their state available during the async work so you can render pending UI the moment the user interacts:\n\n```tsx lines=[10]\nexport default function Component() {\n  let data = useLoaderData();\n  let fetcher = useFetcher();\n  return (\n    <div>\n      <h1>{data.title}</h1>\n\n      <fetcher.Form method=\"post\">\n        <input type=\"text\" name=\"title\" />\n        {fetcher.state !== \"idle\" && <p>Saving...</p>}\n      </fetcher.Form>\n    </div>\n  );\n}\n```\n\n### 5. Optimistic UI\n\nSometimes there's enough information in the form to render the next state immediately. You can access the form data with `fetcher.formData`:\n\n```tsx lines=[3-4,8]\nexport default function Component() {\n  let data = useLoaderData();\n  let fetcher = useFetcher();\n  let title = fetcher.formData?.get(\"title\") || data.title;\n\n  return (\n    <div>\n      <h1>{title}</h1>\n\n      <fetcher.Form method=\"post\">\n        <input type=\"text\" name=\"title\" />\n        {fetcher.state !== \"idle\" && <p>Saving...</p>}\n      </fetcher.Form>\n    </div>\n  );\n}\n```\n\n### 6. Fetcher Data and Validation\n\nData returned from an action is available in the fetcher's `data` property. This is primarily useful for returning error messages to the user for a failed mutation:\n\n```tsx lines=[7-10,28-32]\n// ...\n\nexport async function clientAction({ request }) {\n  await new Promise((res) => setTimeout(res, 1000));\n  let data = await request.formData();\n\n  let title = data.get(\"title\") as string;\n  if (title.trim() === \"\") {\n    return { ok: false, error: \"Title cannot be empty\" };\n  }\n\n  localStorage.setItem(\"title\", title);\n  return { ok: true, error: null };\n}\n\nexport default function Component() {\n  let data = useLoaderData();\n  let fetcher = useFetcher();\n  let title = fetcher.formData?.get(\"title\") || data.title;\n\n  return (\n    <div>\n      <h1>{title}</h1>\n\n      <fetcher.Form method=\"post\">\n        <input type=\"text\" name=\"title\" />\n        {fetcher.state !== \"idle\" && <p>Saving...</p>}\n        {fetcher.data?.error && (\n          <p style={{ color: \"red\" }}>\n            {fetcher.data.error}\n          </p>\n        )}\n      </fetcher.Form>\n    </div>\n  );\n}\n```\n\n## Loading Data\n\nAnother common use case for fetchers is to load data from a route for something like a combobox.\n\n### 1. Create a search route\n\nConsider the following route with a very basic search:\n\n```tsx filename=./search-users.tsx\n// { path: '/search-users', filename: './search-users.tsx' }\nconst users = [\n  { id: 1, name: \"Ryan\" },\n  { id: 2, name: \"Michael\" },\n  // ...\n];\n\nexport async function loader({ request }) {\n  await new Promise((res) => setTimeout(res, 300));\n  let url = new URL(request.url);\n  let query = url.searchParams.get(\"q\");\n  return users.filter((user) =>\n    user.name.toLowerCase().includes(query.toLowerCase()),\n  );\n}\n```\n\n### 2. Render a fetcher in a combobox component\n\n```tsx\nimport { useFetcher } from \"react-router\";\n\nexport function UserSearchCombobox() {\n  let fetcher = useFetcher();\n  return (\n    <div>\n      <fetcher.Form method=\"get\" action=\"/search-users\">\n        <input type=\"text\" name=\"q\" />\n      </fetcher.Form>\n    </div>\n  );\n}\n```\n\n- The action points to the route we created above: \"/search-users\"\n- The name of the input is \"q\" to match the query parameter\n\n### 3. Add type inference\n\n```tsx lines=[2,5]\nimport { useFetcher } from \"react-router\";\nimport type { loader } from \"./search-users\";\n\nexport function UserSearchCombobox() {\n  let fetcher = useFetcher<typeof loader>();\n  // ...\n}\n```\n\nEnsure you use `import type` so you only import the types.\n\n### 4. Render the data\n\n```tsx lines=[10-16]\nimport { useFetcher } from \"react-router\";\n\nexport function UserSearchCombobox() {\n  let fetcher = useFetcher<typeof loader>();\n  return (\n    <div>\n      <fetcher.Form method=\"get\" action=\"/search-users\">\n        <input type=\"text\" name=\"q\" />\n      </fetcher.Form>\n      {fetcher.data && (\n        <ul>\n          {fetcher.data.map((user) => (\n            <li key={user.id}>{user.name}</li>\n          ))}\n        </ul>\n      )}\n    </div>\n  );\n}\n```\n\nNote you will need to hit \"enter\" to submit the form and see the results.\n\n### 5. Render a pending state\n\n```tsx lines=[12-14]\nimport { useFetcher } from \"react-router\";\n\nexport function UserSearchCombobox() {\n  let fetcher = useFetcher<typeof loader>();\n  return (\n    <div>\n      <fetcher.Form method=\"get\" action=\"/search-users\">\n        <input type=\"text\" name=\"q\" />\n      </fetcher.Form>\n      {fetcher.data && (\n        <ul\n          style={{\n            opacity: fetcher.state === \"idle\" ? 1 : 0.25,\n          }}\n        >\n          {fetcher.data.map((user) => (\n            <li key={user.id}>{user.name}</li>\n          ))}\n        </ul>\n      )}\n    </div>\n  );\n}\n```\n\n### 6. Search on user input\n\nFetchers can be submitted programmatically with `fetcher.submit`:\n\n```tsx lines=[5-7]\n<fetcher.Form method=\"get\" action=\"/search-users\">\n  <input\n    type=\"text\"\n    name=\"q\"\n    onChange={(event) => {\n      fetcher.submit(event.currentTarget.form);\n    }}\n  />\n</fetcher.Form>\n```\n\nNote the input event's form is passed as the first argument to `fetcher.submit`. The fetcher will use that form to submit the request, reading its attributes and serializing the data from its elements.\n"
  },
  {
    "path": "docs/how-to/file-route-conventions.md",
    "content": "---\ntitle: File Route Conventions\n---\n\n# File Route Conventions\n\n[MODES: framework]\n\n<br/>\n<br/>\n\nThe `@react-router/fs-routes` package enables file-convention based route config.\n\n## Setting up\n\nFirst install the `@react-router/fs-routes` package:\n\n```shellscript nonumber\nnpm i @react-router/fs-routes\n```\n\nThen use it to provide route config in your `app/routes.ts` file:\n\n```tsx filename=app/routes.ts\nimport { type RouteConfig } from \"@react-router/dev/routes\";\nimport { flatRoutes } from \"@react-router/fs-routes\";\n\nexport default flatRoutes() satisfies RouteConfig;\n```\n\nAny modules in the `app/routes` directory will become routes in your application by default.\nThe `ignoredRouteFiles` option allows you to specify files that should not be included as routes:\n\n```tsx filename=app/routes.ts\nimport { type RouteConfig } from \"@react-router/dev/routes\";\nimport { flatRoutes } from \"@react-router/fs-routes\";\n\nexport default flatRoutes({\n  ignoredRouteFiles: [\"home.tsx\"],\n}) satisfies RouteConfig;\n```\n\nThis will look for routes in the `app/routes` directory by default, but this can be configured via the `rootDirectory` option which is relative to your app directory:\n\n```tsx filename=app/routes.ts\nimport { type RouteConfig } from \"@react-router/dev/routes\";\nimport { flatRoutes } from \"@react-router/fs-routes\";\n\nexport default flatRoutes({\n  rootDirectory: \"file-routes\",\n}) satisfies RouteConfig;\n```\n\nThe rest of this guide will assume you're using the default `app/routes` directory.\n\n## Basic Routes\n\nThe filename maps to the route's URL pathname, except for `_index.tsx` which is the [index route][index_route] for the [root route][root_route]. You can use `.js`, `.jsx`, `.ts` or `.tsx` file extensions.\n\n```text lines=[3-4]\napp/\n├── routes/\n│   ├── _index.tsx\n│   └── about.tsx\n└── root.tsx\n```\n\n| URL      | Matched Routes          |\n| -------- | ----------------------- |\n| `/`      | `app/routes/_index.tsx` |\n| `/about` | `app/routes/about.tsx`  |\n\nNote that these routes will be rendered in the outlet of `app/root.tsx` because of [nested routing][nested_routing].\n\n## Dot Delimiters\n\nAdding a `.` to a route filename will create a `/` in the URL.\n\n```text lines=[5-7]\n app/\n├── routes/\n│   ├── _index.tsx\n│   ├── about.tsx\n│   ├── concerts.trending.tsx\n│   ├── concerts.salt-lake-city.tsx\n│   └── concerts.san-diego.tsx\n└── root.tsx\n```\n\n| URL                        | Matched Route                            |\n| -------------------------- | ---------------------------------------- |\n| `/`                        | `app/routes/_index.tsx`                  |\n| `/about`                   | `app/routes/about.tsx`                   |\n| `/concerts/trending`       | `app/routes/concerts.trending.tsx`       |\n| `/concerts/salt-lake-city` | `app/routes/concerts.salt-lake-city.tsx` |\n| `/concerts/san-diego`      | `app/routes/concerts.san-diego.tsx`      |\n\nThe dot delimiter also creates nesting, see the [nesting section][nested_routes] for more information.\n\n## Dynamic Segments\n\nUsually your URLs aren't static but data-driven. Dynamic segments allow you to match segments of the URL and use that value in your code. You create them with the `$` prefix.\n\n```text lines=[5]\n app/\n├── routes/\n│   ├── _index.tsx\n│   ├── about.tsx\n│   ├── concerts.$city.tsx\n│   └── concerts.trending.tsx\n└── root.tsx\n```\n\n| URL                        | Matched Route                      |\n| -------------------------- | ---------------------------------- |\n| `/`                        | `app/routes/_index.tsx`            |\n| `/about`                   | `app/routes/about.tsx`             |\n| `/concerts/trending`       | `app/routes/concerts.trending.tsx` |\n| `/concerts/salt-lake-city` | `app/routes/concerts.$city.tsx`    |\n| `/concerts/san-diego`      | `app/routes/concerts.$city.tsx`    |\n\nThe value will be parsed from the URL and passed to various APIs. We call these values \"URL Parameters\". The most useful places to access the URL params are in [loaders] and [actions].\n\n```tsx\nexport async function loader({ params }) {\n  return fakeDb.getAllConcertsForCity(params.city);\n}\n```\n\nYou'll note the property name on the `params` object maps directly to the name of your file: `$city.tsx` becomes `params.city`.\n\nRoutes can have multiple dynamic segments, like `concerts.$city.$date`, both are accessed on the params object by name:\n\n```tsx\nexport async function loader({ params }) {\n  return fake.db.getConcerts({\n    date: params.date,\n    city: params.city,\n  });\n}\n```\n\nSee the [routing guide][routing_guide] for more information.\n\n## Nested Routes\n\nNested Routing is the general idea of coupling segments of the URL to component hierarchy and data. You can read more about it in the [Routing Guide][nested_routing].\n\nYou create nested routes with [dot delimiters][dot_delimiters]. If the filename before the `.` matches another route filename, it automatically becomes a child route to the matching parent. Consider these routes:\n\n```text lines=[5-8]\n app/\n├── routes/\n│   ├── _index.tsx\n│   ├── about.tsx\n│   ├── concerts._index.tsx\n│   ├── concerts.$city.tsx\n│   ├── concerts.trending.tsx\n│   └── concerts.tsx\n└── root.tsx\n```\n\nAll the routes that start with `app/routes/concerts.` will be child routes of `app/routes/concerts.tsx` and render inside the [parent route's outlet][nested_routing].\n\n| URL                        | Matched Route                      | Layout                    |\n| -------------------------- | ---------------------------------- | ------------------------- |\n| `/`                        | `app/routes/_index.tsx`            | `app/root.tsx`            |\n| `/about`                   | `app/routes/about.tsx`             | `app/root.tsx`            |\n| `/concerts`                | `app/routes/concerts._index.tsx`   | `app/routes/concerts.tsx` |\n| `/concerts/trending`       | `app/routes/concerts.trending.tsx` | `app/routes/concerts.tsx` |\n| `/concerts/salt-lake-city` | `app/routes/concerts.$city.tsx`    | `app/routes/concerts.tsx` |\n\nNote you typically want to add an index route when you add nested routes so that something renders inside the parent's outlet when users visit the parent URL directly.\n\nFor example, if the URL is `/concerts/salt-lake-city` then the UI hierarchy will look like this:\n\n```tsx\n<Root>\n  <Concerts>\n    <City />\n  </Concerts>\n</Root>\n```\n\n## Nested URLs without Layout Nesting\n\nSometimes you want the URL to be nested, but you don't want the automatic layout nesting. You can opt out of nesting with a trailing underscore on the parent segment:\n\n```text lines=[8]\n app/\n├── routes/\n│   ├── _index.tsx\n│   ├── about.tsx\n│   ├── concerts.$city.tsx\n│   ├── concerts.trending.tsx\n│   ├── concerts.tsx\n│   └── concerts_.mine.tsx\n└── root.tsx\n```\n\n| URL                        | Matched Route                      | Layout                    |\n| -------------------------- | ---------------------------------- | ------------------------- |\n| `/`                        | `app/routes/_index.tsx`            | `app/root.tsx`            |\n| `/about`                   | `app/routes/about.tsx`             | `app/root.tsx`            |\n| `/concerts/mine`           | `app/routes/concerts_.mine.tsx`    | `app/root.tsx`            |\n| `/concerts/trending`       | `app/routes/concerts.trending.tsx` | `app/routes/concerts.tsx` |\n| `/concerts/salt-lake-city` | `app/routes/concerts.$city.tsx`    | `app/routes/concerts.tsx` |\n\nNote that `/concerts/mine` does not nest with `app/routes/concerts.tsx` anymore, but `app/root.tsx`. The `trailing_` underscore creates a path segment, but it does not create layout nesting.\n\nThink of the `trailing_` underscore as the long bit at the end of your parent's signature, writing you out of the will, removing the segment that follows from the layout nesting.\n\n## Nested Layouts without Nested URLs\n\nWe call these <a name=\"pathless-routes\"><b>Pathless Routes</b></a>\n\nSometimes you want to share a layout with a group of routes without adding any path segments to the URL. A common example is a set of authentication routes that have a different header/footer than the public pages or the logged in app experience. You can do this with a `_leading` underscore.\n\n```text lines=[3-5]\n app/\n├── routes/\n│   ├── _auth.login.tsx\n│   ├── _auth.register.tsx\n│   ├── _auth.tsx\n│   ├── _index.tsx\n│   ├── concerts.$city.tsx\n│   └── concerts.tsx\n└── root.tsx\n```\n\n| URL                        | Matched Route                   | Layout                    |\n| -------------------------- | ------------------------------- | ------------------------- |\n| `/`                        | `app/routes/_index.tsx`         | `app/root.tsx`            |\n| `/login`                   | `app/routes/_auth.login.tsx`    | `app/routes/_auth.tsx`    |\n| `/register`                | `app/routes/_auth.register.tsx` | `app/routes/_auth.tsx`    |\n| `/concerts`                | `app/routes/concerts.tsx`       | `app/routes/concerts.tsx` |\n| `/concerts/salt-lake-city` | `app/routes/concerts.$city.tsx` | `app/routes/concerts.tsx` |\n\nThink of the `_leading` underscore as a blanket you're pulling over the filename, hiding the filename from the URL.\n\n## Optional Segments\n\nWrapping a route segment in parentheses will make the segment optional.\n\n```text lines=[3-5]\n app/\n├── routes/\n│   ├── ($lang)._index.tsx\n│   ├── ($lang).$productId.tsx\n│   └── ($lang).categories.tsx\n└── root.tsx\n```\n\n| URL                        | Matched Route                       |\n| -------------------------- | ----------------------------------- |\n| `/`                        | `app/routes/($lang)._index.tsx`     |\n| `/categories`              | `app/routes/($lang).categories.tsx` |\n| `/en/categories`           | `app/routes/($lang).categories.tsx` |\n| `/fr/categories`           | `app/routes/($lang).categories.tsx` |\n| `/american-flag-speedo`    | `app/routes/($lang)._index.tsx`     |\n| `/en/american-flag-speedo` | `app/routes/($lang).$productId.tsx` |\n| `/fr/american-flag-speedo` | `app/routes/($lang).$productId.tsx` |\n\nYou may wonder why `/american-flag-speedo` is matching the `($lang)._index.tsx` route instead of `($lang).$productId.tsx`. This is because when you have an optional dynamic param segment followed by another dynamic param, it cannot reliably be determined if a single-segment URL such as `/american-flag-speedo` should match `/:lang` `/:productId`. Optional segments match eagerly and thus it will match `/:lang`. If you have this type of setup it's recommended to look at `params.lang` in the `($lang)._index.tsx` loader and redirect to `/:lang/american-flag-speedo` for the current/default language if `params.lang` is not a valid language code.\n\n## Splat Routes\n\nWhile [dynamic segments][dynamic_segments] match a single path segment (the stuff between two `/` in a URL), a splat route will match the rest of a URL, including the slashes.\n\n```text lines=[4,6]\n app/\n├── routes/\n│   ├── _index.tsx\n│   ├── $.tsx\n│   ├── about.tsx\n│   └── files.$.tsx\n└── root.tsx\n```\n\n| URL                                          | Matched Route            |\n| -------------------------------------------- | ------------------------ |\n| `/`                                          | `app/routes/_index.tsx`  |\n| `/about`                                     | `app/routes/about.tsx`   |\n| `/beef/and/cheese`                           | `app/routes/$.tsx`       |\n| `/files`                                     | `app/routes/files.$.tsx` |\n| `/files/talks/react-conf_old.pdf`            | `app/routes/files.$.tsx` |\n| `/files/talks/react-conf_final.pdf`          | `app/routes/files.$.tsx` |\n| `/files/talks/react-conf-FINAL-MAY_2024.pdf` | `app/routes/files.$.tsx` |\n\nSimilar to dynamic route parameters, you can access the value of the matched path on the splat route's `params` with the `\"*\"` key.\n\n```tsx filename=app/routes/files.$.tsx\nexport async function loader({ params }) {\n  const filePath = params[\"*\"];\n  return fake.getFileInfo(filePath);\n}\n```\n\n## Catch-all Route\n\nTo create a route that will match any requests that don't match other defined routes (such as a 404 page), create a file named `$.tsx` within your routes directory:\n\n| URL                            | Matched Route           |\n| ------------------------------ | ----------------------- |\n| `/`                            | `app/routes/_index.tsx` |\n| `/about`                       | `app/routes/about.tsx`  |\n| `/any-invalid-path-will-match` | `app/routes/$.tsx`      |\n\nBy default the matched route will return a 200 response, so be sure to modify your catchall route to return a 404 instead:\n\n```tsx filename=app/routes/$.tsx\nexport async function loader() {\n  return data({}, 404);\n}\n```\n\n## Escaping Special Characters\n\nIf you want one of the special characters used for these route conventions to actually be a part of the URL, you can escape the conventions with `[]` characters. This can be especially helpful for [resource routes][resource_routes] that include an extension in the URL.\n\n| Filename                            | URL                 |\n| ----------------------------------- | ------------------- |\n| `app/routes/sitemap[.]xml.tsx`      | `/sitemap.xml`      |\n| `app/routes/[sitemap.xml].tsx`      | `/sitemap.xml`      |\n| `app/routes/weird-url.[_index].tsx` | `/weird-url/_index` |\n| `app/routes/dolla-bills-[$].tsx`    | `/dolla-bills-$`    |\n| `app/routes/[[so-weird]].tsx`       | `/[so-weird]`       |\n| `app/routes/reports.$id[.pdf].ts`   | `/reports/123.pdf`  |\n\n## Folders for Organization\n\nRoutes can also be folders with a `route.tsx` file inside defining the route module. The rest of the files in the folder will not become routes. This allows you to organize your code closer to the routes that use them instead of repeating the feature names across other folders.\n\nThe files inside a folder have no meaning for the route paths, the route path is completely defined by the folder name.\n\nConsider these routes:\n\n```text\n app/\n├── routes/\n│   ├── _landing._index.tsx\n│   ├── _landing.about.tsx\n│   ├── _landing.tsx\n│   ├── app._index.tsx\n│   ├── app.projects.tsx\n│   ├── app.tsx\n│   └── app_.projects.$id.roadmap.tsx\n└── root.tsx\n```\n\nSome, or all of them can be folders holding their own `route` module inside.\n\n```text\napp/\n├── routes/\n│   ├── _landing._index/\n│   │   ├── route.tsx\n│   │   └── scroll-experience.tsx\n│   ├── _landing.about/\n│   │   ├── employee-profile-card.tsx\n│   │   ├── get-employee-data.server.ts\n│   │   ├── route.tsx\n│   │   └── team-photo.jpg\n│   ├── _landing/\n│   │   ├── footer.tsx\n│   │   ├── header.tsx\n│   │   └── route.tsx\n│   ├── app._index/\n│   │   ├── route.tsx\n│   │   └── stats.tsx\n│   ├── app.projects/\n│   │   ├── get-projects.server.ts\n│   │   ├── project-buttons.tsx\n│   │   ├── project-card.tsx\n│   │   └── route.tsx\n│   ├── app/\n│   │   ├── footer.tsx\n│   │   ├── primary-nav.tsx\n│   │   └── route.tsx\n│   ├── app_.projects.$id.roadmap/\n│   │   ├── chart.tsx\n│   │   ├── route.tsx\n│   │   └── update-timeline.server.ts\n│   └── contact-us.tsx\n└── root.tsx\n```\n\nNote that when you turn a route module into a folder, the route module becomes `folder/route.tsx`, all other modules in the folder will not become routes. For example:\n\n```\n# these are the same route:\napp/routes/app.tsx\napp/routes/app/route.tsx\n\n# as are these\napp/routes/app._index.tsx\napp/routes/app._index/route.tsx\n```\n\n[route-config-file]: ../start/framework/routing#configuring-routes\n[loaders]: ../start/framework/data-loading\n[actions]: ../start/framework/actions\n[routing_guide]: ../start/framework/routing\n[root_route]: ../start/framework/route-module\n[index_route]: ../start/framework/routing#index-routes\n[nested_routing]: ../start/framework/routing#nested-routes\n[nested_routes]: #nested-routes\n[dot_delimiters]: #dot-delimiters\n[dynamic_segments]: #dynamic-segments\n[resource_routes]: ../how-to/resource-routes\n"
  },
  {
    "path": "docs/how-to/file-uploads.md",
    "content": "---\ntitle: File Uploads\n---\n\n# File Uploads\n\n[MODES: framework]\n\n<br/>\n<br/>\n\n_Thank you to David Adams for [writing an original guide](https://programmingarehard.com/2024/09/06/remix-file-uploads-updated.html/) on which this doc is based. You can refer to it for even more examples._\n\n## Basic File Upload\n\n### 1. Setup some routes\n\nYou can setup your routes however you like. This example uses the following structure:\n\n```ts filename=routes.ts\nimport {\n  type RouteConfig,\n  route,\n} from \"@react-router/dev/routes\";\n\nexport default [\n  // ... other routes\n  route(\"user/:id\", \"pages/user-profile.tsx\", [\n    route(\"avatar\", \"api/avatar.tsx\"),\n  ]),\n] satisfies RouteConfig;\n```\n\n### 2. Add the form data parser\n\n`form-data-parser` is a wrapper around `request.formData()` that provides streaming support for handling file uploads.\n\n```shellscript\nnpm i @remix-run/form-data-parser\n```\n\n[See the `form-data-parser` docs for more information][form-data-parser]\n\n### 3. Create a route with an upload action\n\nThe `parseFormData` function takes an `uploadHandler` function as an argument. This function will be called for each file upload in the form.\n\n<docs-warning>\n\nYou must set the form's `enctype` to `multipart/form-data` for file uploads to work.\n\n</docs-warning>\n\n```tsx filename=pages/user-profile.tsx\nimport {\n  type FileUpload,\n  parseFormData,\n} from \"@remix-run/form-data-parser\";\nimport type { Route } from \"./+types/user-profile\";\n\nexport async function action({\n  request,\n}: Route.ActionArgs) {\n  const uploadHandler = async (fileUpload: FileUpload) => {\n    if (fileUpload.fieldName === \"avatar\") {\n      // process the upload and return a File\n    }\n  };\n\n  const formData = await parseFormData(\n    request,\n    uploadHandler,\n  );\n  // 'avatar' has already been processed at this point\n  const file = formData.get(\"avatar\");\n}\n\nexport default function Component() {\n  return (\n    <form method=\"post\" encType=\"multipart/form-data\">\n      <input type=\"file\" name=\"avatar\" />\n      <button>Submit</button>\n    </form>\n  );\n}\n```\n\n## Local Storage Implementation\n\n### 1. Add the storage package\n\n`file-storage` is a key/value interface for storing [File objects][file] in JavaScript. Similar to how `localStorage` allows you to store key/value pairs of strings in the browser, file-storage allows you to store key/value pairs of files on the server.\n\n```shellscript\nnpm i @remix-run/file-storage\n```\n\n[See the `file-storage` docs for more information][file-storage]\n\n### 2. Create a storage configuration\n\nCreate a file that exports a `LocalFileStorage` instance to be used by different routes.\n\n```ts filename=avatar-storage.server.ts\nimport { LocalFileStorage } from \"@remix-run/file-storage/local\";\n\nexport const fileStorage = new LocalFileStorage(\n  \"./uploads/avatars\",\n);\n\nexport function getStorageKey(userId: string) {\n  return `user-${userId}-avatar`;\n}\n```\n\n### 3. Implement the upload handler\n\nUpdate the form's `action` to store files in the `fileStorage` instance.\n\n```tsx filename=pages/user-profile.tsx\nimport {\n  type FileUpload,\n  parseFormData,\n} from \"@remix-run/form-data-parser\";\nimport {\n  fileStorage,\n  getStorageKey,\n} from \"~/avatar-storage.server\";\nimport type { Route } from \"./+types/user-profile\";\n\nexport async function action({\n  request,\n  params,\n}: Route.ActionArgs) {\n  async function uploadHandler(fileUpload: FileUpload) {\n    if (\n      fileUpload.fieldName === \"avatar\" &&\n      fileUpload.type.startsWith(\"image/\")\n    ) {\n      let storageKey = getStorageKey(params.id);\n\n      // FileUpload objects are not meant to stick around for very long (they are\n      // streaming data from the request.body); store them as soon as possible.\n      await fileStorage.set(storageKey, fileUpload);\n\n      // Return a File for the FormData object. This is a LazyFile that knows how\n      // to access the file's content if needed (using e.g. file.stream()) but\n      // waits until it is requested to actually read anything.\n      return fileStorage.get(storageKey);\n    }\n  }\n\n  const formData = await parseFormData(\n    request,\n    uploadHandler,\n  );\n}\n\nexport default function UserPage({\n  actionData,\n  params,\n}: Route.ComponentProps) {\n  return (\n    <div>\n      <h1>User {params.id}</h1>\n      <form\n        method=\"post\"\n        // The form's enctype must be set to \"multipart/form-data\" for file uploads\n        encType=\"multipart/form-data\"\n      >\n        <input type=\"file\" name=\"avatar\" accept=\"image/*\" />\n        <button>Submit</button>\n      </form>\n\n      <img\n        src={`/user/${params.id}/avatar`}\n        alt=\"user avatar\"\n      />\n    </div>\n  );\n}\n```\n\n### 4. Add a route to serve the uploaded file\n\nCreate a [resource route][resource-route] that streams the file as a response.\n\n```tsx filename=api/avatar.tsx\nimport {\n  fileStorage,\n  getStorageKey,\n} from \"~/avatar-storage.server\";\nimport type { Route } from \"./+types/avatar\";\n\nexport async function loader({ params }: Route.LoaderArgs) {\n  const storageKey = getStorageKey(params.id);\n  const file = await fileStorage.get(storageKey);\n\n  if (!file) {\n    throw new Response(\"User avatar not found\", {\n      status: 404,\n    });\n  }\n\n  return new Response(file.stream(), {\n    headers: {\n      \"Content-Type\": file.type,\n      \"Content-Disposition\": `attachment; filename=${file.name}`,\n    },\n  });\n}\n```\n\n[form-data-parser]: https://www.npmjs.com/package/@remix-run/form-data-parser\n[file-storage]: https://www.npmjs.com/package/@remix-run/file-storage\n[file]: https://developer.mozilla.org/en-US/docs/Web/API/File\n[resource-route]: ../how-to/resource-routes\n"
  },
  {
    "path": "docs/how-to/form-validation.md",
    "content": "---\ntitle: Form Validation\n---\n\n# Form Validation\n\n[MODES: framework, data]\n\n<br/>\n<br/>\n\nThis guide walks through a simple signup form implementation. You will likely want to pair these concepts with third-party validation libraries and error components, but this guide only focuses on the moving pieces for React Router.\n\n## 1. Setting Up\n\nWe'll start by creating a basic signup route with form.\n\n```ts filename=app/routes.ts\nimport {\n  type RouteConfig,\n  route,\n} from \"@react-router/dev/routes\";\n\nexport default [\n  route(\"signup\", \"signup.tsx\"),\n] satisfies RouteConfig;\n```\n\n```tsx filename=signup.tsx\nimport type { Route } from \"./+types/signup\";\nimport { useFetcher } from \"react-router\";\n\nexport default function Signup(_: Route.ComponentProps) {\n  let fetcher = useFetcher();\n  return (\n    <fetcher.Form method=\"post\">\n      <p>\n        <input type=\"email\" name=\"email\" />\n      </p>\n\n      <p>\n        <input type=\"password\" name=\"password\" />\n      </p>\n\n      <button type=\"submit\">Sign Up</button>\n    </fetcher.Form>\n  );\n}\n```\n\n## 2. Defining the Action\n\nIn this step, we'll define a server `action` in the same file as our `Signup` component. Note that the aim here is to provide a broad overview of the mechanics involved rather than digging deep into form validation rules or error object structures. We'll use rudimentary checks for the email and password to demonstrate the core concepts.\n\n```tsx filename=signup.tsx\nimport type { Route } from \"./+types/signup\";\nimport { redirect, useFetcher, data } from \"react-router\";\n\nexport default function Signup(_: Route.ComponentProps) {\n  // omitted for brevity\n}\n\nexport async function action({\n  request,\n}: Route.ActionArgs) {\n  const formData = await request.formData();\n  const email = String(formData.get(\"email\"));\n  const password = String(formData.get(\"password\"));\n\n  const errors = {};\n\n  if (!email.includes(\"@\")) {\n    errors.email = \"Invalid email address\";\n  }\n\n  if (password.length < 12) {\n    errors.password =\n      \"Password should be at least 12 characters\";\n  }\n\n  if (Object.keys(errors).length > 0) {\n    return data({ errors }, { status: 400 });\n  }\n\n  // Redirect to dashboard if validation is successful\n  return redirect(\"/dashboard\");\n}\n```\n\nIf any validation errors are found, they are returned from the `action` to the fetcher. This is our way of signaling to the UI that something needs to be corrected, otherwise the user will be redirected to the dashboard.\n\nNote the `data({ errors }, { status: 400 })` call. Setting a 400 status is the web standard way to signal to the client that there was a validation error (Bad Request). In React Router, only 2xx status codes trigger page data revalidation, so sending a 400 status prevents the normal revalidation that would occur after an `action`.\n\n## 3. Displaying Validation Errors\n\nFinally, we'll modify the `Signup` component to display validation errors, if any, from `fetcher.data`.\n\n```tsx filename=signup.tsx lines=[3,8,13-15]\nexport default function Signup(_: Route.ComponentProps) {\n  let fetcher = useFetcher();\n  let errors = fetcher.data?.errors;\n  return (\n    <fetcher.Form method=\"post\">\n      <p>\n        <input type=\"email\" name=\"email\" />\n        {errors?.email ? <em>{errors.email}</em> : null}\n      </p>\n\n      <p>\n        <input type=\"password\" name=\"password\" />\n        {errors?.password ? (\n          <em>{errors.password}</em>\n        ) : null}\n      </p>\n\n      <button type=\"submit\">Sign Up</button>\n    </fetcher.Form>\n  );\n}\n```\n"
  },
  {
    "path": "docs/how-to/headers.md",
    "content": "---\ntitle: HTTP Headers\n---\n\n# HTTP Headers\n\n[MODES: framework]\n\n<br/>\n<br/>\n\nHeaders are primarily defined with the route module `headers` export. You can also set headers in `entry.server.tsx`.\n\n## From Route Modules\n\n```tsx filename=some-route.tsx\nimport { Route } from \"./+types/some-route\";\n\nexport function headers(_: Route.HeadersArgs) {\n  return {\n    \"Content-Security-Policy\": \"default-src 'self'\",\n    \"X-Frame-Options\": \"DENY\",\n    \"X-Content-Type-Options\": \"nosniff\",\n    \"Cache-Control\": \"max-age=3600, s-maxage=86400\",\n  };\n}\n```\n\nYou can return either a [`Headers`](https://developer.mozilla.org/en-US/docs/Web/API/Headers) instance or `HeadersInit`.\n\n## From loaders and actions\n\nWhen the header is dependent on loader data, loaders and actions can also set headers.\n\n### 1. Wrap your return value in `data`\n\n```tsx lines=[1,8]\nimport { data } from \"react-router\";\n\nexport async function loader({ params }: LoaderArgs) {\n  let [page, ms] = await fakeTimeCall(\n    await getPage(params.id),\n  );\n\n  return data(page, {\n    headers: {\n      \"Server-Timing\": `page;dur=${ms};desc=\"Page query\"`,\n    },\n  });\n}\n```\n\n### 2. Return from `headers` export\n\nHeaders from loaders and actions are not sent automatically. You must explicitly return them from the `headers` export.\n\n```tsx\nfunction hasAnyHeaders(headers: Headers): boolean {\n  return [...headers].length > 0;\n}\n\nexport function headers({\n  actionHeaders,\n  loaderHeaders,\n}: HeadersArgs) {\n  return hasAnyHeaders(actionHeaders)\n    ? actionHeaders\n    : loaderHeaders;\n}\n```\n\nOne notable exception is `Set-Cookie` headers, which are automatically preserved from `headers`, `loader`, and `action` in parent routes, even without exporting `headers` from the child route.\n\n## Merging with parent headers\n\nConsider these nested routes\n\n```ts filename=routes.ts\nroute(\"pages\", \"pages-layout-with-nav.tsx\", [\n  route(\":slug\", \"page.tsx\"),\n]);\n```\n\nIf both route modules want to set headers, the headers from the deepest matching route will be sent.\n\nWhen you need to keep both the parent and the child headers, you need to merge them in the child route.\n\n### Appending\n\nThe easiest way is to simply append to the parent headers. This avoids overwriting a header the parent may have set and both are important.\n\n```tsx\nexport function headers({ parentHeaders }: HeadersArgs) {\n  parentHeaders.append(\n    \"Permissions-Policy: geolocation=()\",\n  );\n  return parentHeaders;\n}\n```\n\n### Setting\n\nSometimes it's important to overwrite the parent header. Do this with `set` instead of `append`:\n\n```tsx\nexport function headers({ parentHeaders }: HeadersArgs) {\n  parentHeaders.set(\n    \"Cache-Control\",\n    \"max-age=3600, s-maxage=86400\",\n  );\n  return parentHeaders;\n}\n```\n\nYou can avoid the need to merge headers by only defining headers in \"leaf routes\" (index routes and child routes without children) and not in parent routes.\n\n## From `entry.server.tsx`\n\nThe `handleRequest` export receives the headers from the route module as an argument. You can append global headers here.\n\n```tsx\nexport default async function handleRequest(\n  request,\n  responseStatusCode,\n  responseHeaders,\n  routerContext,\n  loadContext,\n) {\n  // set, append global headers\n  responseHeaders.set(\n    \"X-App-Version\",\n    routerContext.manifest.version,\n  );\n\n  return new Response(await getStream(), {\n    headers: responseHeaders,\n    status: responseStatusCode,\n  });\n}\n```\n\nIf you don't have an `entry.server.tsx` run the `reveal` command:\n\n```shellscript nonumber\nreact-router reveal\n```\n"
  },
  {
    "path": "docs/how-to/index.md",
    "content": "---\ntitle: How-Tos\norder: 4\n---\n"
  },
  {
    "path": "docs/how-to/instrumentation.md",
    "content": "---\ntitle: Instrumentation\nunstable: true\n---\n\n# Instrumentation\n\n[MODES: framework, data]\n\n<br/>\n<br/>\n\n<docs-warning>The instrumentation APIs are experimental and subject to breaking changes in\nminor/patch releases. Please use with caution and pay **very** close attention\nto release notes for relevant changes.</docs-warning>\n\nInstrumentation allows you to add logging, error reporting, and performance tracing to your React Router application without modifying your actual route handlers. This enables comprehensive observability solutions for production applications on both the server and client.\n\n## Overview\n\nWith the React Router Instrumentation APIs, you provide \"wrapper\" functions that execute around your request handlers, router operations, route middlewares, and/or route handlers. This allows you to:\n\n- Monitor application performance\n- Add logging\n- Integrate with observability platforms (Sentry, DataDog, New Relic, etc.)\n- Implement OpenTelemetry tracing\n- Track user behavior and navigation patterns\n\nA key design principle is that instrumentation is **read-only** - you can observe what's happening but cannot modify runtime application behavior by modifying the arguments passed to, or data returned from your route handlers.\n\n<docs-info>\nAs with any instrumentation approach, adding additional code execution at runtime may alter the performance characteristics compared to an uninstrumented application. Keep this in mind and perform appropriate testing and/or leverage conditional instrumentation to avoid a negative UX impact in production.\n</docs-info>\n\n## Quick Start (Framework Mode)\n\n[modes: framework]\n\n### 1. Server-side Instrumentation\n\nAdd instrumentations to your `entry.server.tsx`:\n\n```tsx filename=app/entry.server.tsx\nexport const unstable_instrumentations = [\n  {\n    // Instrument the server handler\n    handler(handler) {\n      handler.instrument({\n        async request(handleRequest, { request }) {\n          let url = `${request.method} ${request.url}`;\n          console.log(`Request start: ${url}`);\n          await handleRequest();\n          console.log(`Request end: ${url}`);\n        },\n      });\n    },\n\n    // Instrument individual routes\n    route(route) {\n      // Skip instrumentation for specific routes if needed\n      if (route.id === \"root\") return;\n\n      route.instrument({\n        async loader(callLoader, { request }) {\n          let url = `${request.method} ${request.url}`;\n          console.log(`Loader start: ${url} - ${route.id}`);\n          await callLoader();\n          console.log(`Loader end: ${url} - ${route.id}`);\n        },\n        // Other available instrumentations:\n        // async action() { /* ... */ },\n        // async middleware() { /* ... */ },\n        // async lazy() { /* ... */ },\n      });\n    },\n  },\n];\n\nexport default function handleRequest(/* ... */) {\n  // Your existing handleRequest implementation\n}\n```\n\n### 2. Client-side Instrumentation\n\nAdd instrumentations to your `entry.client.tsx`:\n\n```tsx filename=app/entry.client.tsx\nimport { startTransition, StrictMode } from \"react\";\nimport { hydrateRoot } from \"react-dom/client\";\nimport { HydratedRouter } from \"react-router/dom\";\n\nconst unstable_instrumentations = [\n  {\n    // Instrument router operations\n    router(router) {\n      router.instrument({\n        // Instrument navigations\n        async navigate(callNavigate, { currentUrl, to }) {\n          let nav = `${currentUrl} → ${to}`;\n          console.log(`Navigation start: ${nav}`);\n          await callNavigate();\n          console.log(`Navigation end: ${nav}`);\n        },\n        // Instrument fetcher calls\n        async fetch(\n          callFetch,\n          { href, currentUrl, fetcherKey },\n        ) {\n          let fetch = `${fetcherKey} → ${href}`;\n          console.log(`Fetcher start: ${fetch}`);\n          await callFetch();\n          console.log(`Fetcher end: ${fetch}`);\n        },\n      });\n    },\n\n    // Instrument individual routes (same as server-side)\n    route(route) {\n      // Skip instrumentation for specific routes if needed\n      if (route.id === \"root\") return;\n\n      route.instrument({\n        async loader(callLoader, { request }) {\n          let url = `${request.method} ${request.url}`;\n          console.log(`Loader start: ${url} - ${route.id}`);\n          await callLoader();\n          console.log(`Loader end: ${url} - ${route.id}`);\n        },\n        // Other available instrumentations:\n        // async action() { /* ... */ },\n        // async middleware() { /* ... */ },\n        // async lazy() { /* ... */ },\n      });\n    },\n  },\n];\n\nstartTransition(() => {\n  hydrateRoot(\n    document,\n    <StrictMode>\n      <HydratedRouter\n        unstable_instrumentations={\n          unstable_instrumentations\n        }\n      />\n    </StrictMode>,\n  );\n});\n```\n\n## Quick Start (Data Mode)\n\n[modes: data]\n\nIn Data Mode, you add instrumentations when creating your router:\n\n```tsx\nimport {\n  createBrowserRouter,\n  RouterProvider,\n} from \"react-router\";\n\nconst unstable_instrumentations = [\n  {\n    // Instrument router operations\n    router(router) {\n      router.instrument({\n        // Instrument navigations\n        async navigate(callNavigate, { currentUrl, to }) {\n          let nav = `${currentUrl} → ${to}`;\n          console.log(`Navigation start: ${nav}`);\n          await callNavigate();\n          console.log(`Navigation end: ${nav}`);\n        },\n        // Instrument fetcher calls\n        async fetch(\n          callFetch,\n          { href, currentUrl, fetcherKey },\n        ) {\n          let fetch = `${fetcherKey} → ${href}`;\n          console.log(`Fetcher start: ${fetch}`);\n          await callFetch();\n          console.log(`Fetcher end: ${fetch}`);\n        },\n      });\n    },\n\n    // Instrument individual routes (same as server-side)\n    route(route) {\n      // Skip instrumentation for specific routes if needed\n      if (route.id === \"root\") return;\n\n      route.instrument({\n        async loader(callLoader, { request }) {\n          let url = `${request.method} ${request.url}`;\n          console.log(`Loader start: ${url} - ${route.id}`);\n          await callLoader();\n          console.log(`Loader end: ${url} - ${route.id}`);\n        },\n        // Other available instrumentations:\n        // async action() { /* ... */ },\n        // async middleware() { /* ... */ },\n        // async lazy() { /* ... */ },\n      });\n    },\n  },\n];\n\nconst router = createBrowserRouter(routes, {\n  unstable_instrumentations,\n});\n\nfunction App() {\n  return <RouterProvider router={router} />;\n}\n```\n\n## Core Concepts\n\n### Instrumentation Levels\n\nThere are different levels at which you can instrument your application. Each instrumentation function receives a second \"info\" parameter containing relevant contextual information for the specific aspect being instrumented.\n\n#### 1. Handler Level (Server)\n\n[modes: framework]\n\nInstruments the top-level request handler that processes all requests to your server:\n\n```tsx filename=entry.server.tsx\nexport const unstable_instrumentations = [\n  {\n    handler(handler) {\n      handler.instrument({\n        async request(handleRequest, { request, context }) {\n          // Runs around ALL requests to your app\n          await handleRequest();\n        },\n      });\n    },\n  },\n];\n```\n\n#### 2. Router Level (Client)\n\n[modes: framework,data]\n\nInstruments client-side router operations like navigations and fetcher calls:\n\n```tsx\nexport const unstable_instrumentations = [\n  {\n    router(router) {\n      router.instrument({\n        async navigate(callNavigate, { to, currentUrl }) {\n          // Runs around navigation operations\n          await callNavigate();\n        },\n        async fetch(\n          callFetch,\n          { href, currentUrl, fetcherKey },\n        ) {\n          // Runs around fetcher operations\n          await callFetch();\n        },\n      });\n    },\n  },\n];\n\n// Framework Mode (entry.client.tsx)\n<HydratedRouter\n  unstable_instrumentations={unstable_instrumentations}\n/>;\n\n// Data Mode\nconst router = createBrowserRouter(routes, {\n  unstable_instrumentations,\n});\n```\n\n#### 3. Route Level (Server + Client)\n\n[modes: framework,data]\n\nInstruments individual route handlers:\n\n```tsx\nconst unstable_instrumentations = [\n  {\n    route(route) {\n      route.instrument({\n        async loader(\n          callLoader,\n          { params, request, context, unstable_pattern },\n        ) {\n          // Runs around loader execution\n          await callLoader();\n        },\n        async action(\n          callAction,\n          { params, request, context, unstable_pattern },\n        ) {\n          // Runs around action execution\n          await callAction();\n        },\n        async middleware(\n          callMiddleware,\n          { params, request, context, unstable_pattern },\n        ) {\n          // Runs around middleware execution\n          await callMiddleware();\n        },\n        async lazy(callLazy) {\n          // Runs around lazy route loading\n          await callLazy();\n        },\n      });\n    },\n  },\n];\n```\n\n### Read-only Design\n\nInstrumentations are designed to be **observational only**. You cannot:\n\n- Modify arguments passed to handlers\n- Change return values from handlers\n- Alter application behavior\n\nThis ensures that instrumentation is safe to add to production applications and cannot introduce bugs in your route logic.\n\n### Error Handling\n\nTo ensure that instrumentation code doesn't impact the runtime application, errors are caught internally and prevented from propagating outward. This design choice shows up in 2 aspects.\n\nFirst, if a \"handler\" function (loader, action, request handler, navigation, etc.) throws an error, that error will not bubble out of the `callHandler` function invoked from your instrumentation. Instead, the `callHandler` function returns a discriminated union result of type `{ type: \"success\", error: undefined } | { type: \"error\", error: unknown }`. This ensures your entire instrumentation function runs without needing any try/catch/finally logic to handle application errors.\n\n```tsx\nexport const unstable_instrumentations = [\n  {\n    route(route) {\n      route.instrument({\n        async loader(callLoader) {\n          let { status, error } = await callLoader();\n\n          if (status === \"error\") {\n            // error case - `error` is defined\n          } else {\n            // success case - `error` is undefined\n          }\n        },\n      });\n    },\n  },\n];\n```\n\nSecond, if your instrumentation function throws an error, React Router will gracefully swallow that so that it does not bubble outward and impact other instrumentations or application behavior. In both of these examples, the handlers and all other instrumentation functions will still run:\n\n```tsx\nexport const unstable_instrumentations = [\n  {\n    route(route) {\n      route.instrument({\n        // Throwing before calling the handler - RR will\n        // catch the error and still call the loader\n        async loader(callLoader) {\n          somethingThatThrows();\n          await callLoader();\n        },\n        // Throwing after calling the handler - RR will\n        // catch the error internally\n        async action(callAction) {\n          await callAction();\n          somethingThatThrows();\n        },\n      });\n    },\n  },\n];\n```\n\n### Composition\n\nYou can compose multiple instrumentations by providing an array:\n\n```tsx\nexport const unstable_instrumentations = [\n  loggingInstrumentation,\n  performanceInstrumentation,\n  errorReportingInstrumentation,\n];\n```\n\nEach instrumentation wraps the previous one, creating a nested execution chain.\n\n### Conditional Instrumentation\n\nYou can enable instrumentation conditionally based on environment or other factors:\n\n```tsx\nexport const unstable_instrumentations =\n  process.env.NODE_ENV === \"production\"\n    ? [productionInstrumentation]\n    : [developmentInstrumentation];\n```\n\n```tsx\n// Or conditionally within an instrumentation\nexport const unstable_instrumentations = [\n  {\n    route(route) {\n      // Only instrument specific routes\n      if (!route.id?.startsWith(\"routes/admin\")) return;\n\n      // Or, only instrument if a query parameter is present\n      let sp = new URL(request.url).searchParams;\n      if (!sp.has(\"DEBUG\")) return;\n\n      route.instrument({\n        async loader() {\n          /* ... */\n        },\n      });\n    },\n  },\n];\n```\n\n## Common Patterns\n\n### Request logging (server)\n\n```tsx\nconst logging: unstable_ServerInstrumentation = {\n  handler({ instrument }) {\n    instrument({\n      request: (fn, { request }) =>\n        log(`request ${request.url}`, fn),\n    });\n  },\n  route({ instrument, id }) {\n    instrument({\n      middleware: (fn) => log(` middleware (${id})`, fn),\n      loader: (fn) => log(`  loader (${id})`, fn),\n      action: (fn) => log(`  action (${id})`, fn),\n    });\n  },\n};\n\nasync function log(\n  label: string,\n  cb: () => Promise<unstable_InstrumentationHandlerResult>,\n) {\n  let start = Date.now();\n  console.log(`➡️ ${label}`);\n  await cb();\n  console.log(`⬅️ ${label} (${Date.now() - start}ms)`);\n}\n\nexport const unstable_instrumentations = [logging];\n```\n\n### OpenTelemetry Integration\n\n```tsx\nimport { trace, SpanStatusCode } from \"@opentelemetry/api\";\n\nconst tracer = trace.getTracer(\"my-app\");\n\nconst otel: unstable_ServerInstrumentation = {\n  handler({ instrument }) {\n    instrument({\n      request: (fn, { request }) =>\n        otelSpan(`request`, { url: request.url }, fn),\n    });\n  },\n  route({ instrument, id }) {\n    instrument({\n      middleware: (fn, { unstable_pattern }) =>\n        otelSpan(\n          \"middleware\",\n          { routeId: id, pattern: unstable_pattern },\n          fn,\n        ),\n      loader: (fn, { unstable_pattern }) =>\n        otelSpan(\n          \"loader\",\n          { routeId: id, pattern: unstable_pattern },\n          fn,\n        ),\n      action: (fn, { unstable_pattern }) =>\n        otelSpan(\n          \"action\",\n          { routeId: id, pattern: unstable_pattern },\n          fn,\n        ),\n    });\n  },\n};\n\nasync function otelSpan(\n  label: string,\n  attributes: Record<string, string>,\n  cb: () => Promise<unstable_InstrumentationHandlerResult>,\n) {\n  return tracer.startActiveSpan(\n    label,\n    { attributes },\n    async (span) => {\n      let { error } = await cb();\n      if (error) {\n        span.recordException(error);\n        span.setStatus({\n          code: SpanStatusCode.ERROR,\n        });\n      }\n      span.end();\n    },\n  );\n}\n\nexport const unstable_instrumentations = [otel];\n```\n\n### Client-side Performance Tracking\n\n```tsx\nconst windowPerf: unstable_ClientInstrumentation = {\n  router({ instrument }) {\n    instrument({\n      navigate: (fn, { to, currentUrl }) =>\n        measure(`navigation:${currentUrl}->${to}`, fn),\n      fetch: (fn, { href }) =>\n        measure(`fetcher:${href}`, fn),\n    });\n  },\n  route({ instrument, id }) {\n    instrument({\n      middleware: (fn) => measure(`middleware:${id}`, fn),\n      loader: (fn) => measure(`loader:${id}`, fn),\n      action: (fn) => measure(`action:${id}`, fn),\n    });\n  },\n};\n\nasync function measure(\n  label: string,\n  cb: () => Promise<unstable_InstrumentationHandlerResult>,\n) {\n  performance.mark(`start:${label}`);\n  await cb();\n  performance.mark(`end:${label}`);\n  performance.measure(\n    label,\n    `start:${label}`,\n    `end:${label}`,\n  );\n}\n\n<HydratedRouter unstable_instrumentations={[windowPerf]} />;\n```\n"
  },
  {
    "path": "docs/how-to/meta.md",
    "content": "---\ntitle: Meta Tags and SEO\nhidden: true\n---\n\n[copy pasted from route module doc]\n\nBy default, meta descriptors will render a [`<meta>` tag][meta-element] in most cases. The two exceptions are:\n\n- `{ title }` renders a `<title>` tag\n- `{ \"script:ld+json\" }` renders a `<script type=\"application/ld+json\">` tag, and its value should be a serializable object that is stringified and injected into the tag.\n\n```tsx\nexport function meta() {\n  return [\n    {\n      \"script:ld+json\": {\n        \"@context\": \"https://schema.org\",\n        \"@type\": \"Organization\",\n        name: \"React Router\",\n        url: \"https://reactrouter.com\",\n      },\n    },\n  ];\n}\n```\n\nA meta descriptor can also render a [`<link>` tag][link-element] by setting the `tagName` property to `\"link\"`. This is useful for `<link>` tags associated with SEO like `canonical` URLs. For asset links like stylesheets and favicons, you should use the [`links` export][links] instead.\n\n```tsx\nexport function meta() {\n  return [\n    {\n      tagName: \"link\",\n      rel: \"canonical\",\n      href: \"https://reactrouter.com\",\n    },\n  ];\n}\n```\n"
  },
  {
    "path": "docs/how-to/middleware.md",
    "content": "---\ntitle: Middleware\n---\n\n# Middleware\n\n[MODES: framework, data]\n\n<br/>\n<br/>\n\n<docs-info>In Framework Mode, you must opt-into middleware via the [`future.v8_middleware`][future-flags] flag because it contains a minor [breaking change][getloadcontext] to the `getLoadContext` function and the loader/action `context` parameter.</docs-info>\n\nMiddleware allows you to run code before and after the [`Response`][Response] generation for the matched path. This enables [common patterns][common-patterns] like authentication, logging, error handling, and data preprocessing in a reusable way.\n\nMiddleware runs in a nested chain, executing from parent routes to child routes on the way \"down\" to your route handlers, then from child routes back to parent routes on the way \"up\" after a [`Response`][Response] is generated.\n\nFor example, on a `GET /parent/child` request, the middleware would run in the following order:\n\n```text\n- Root middleware start\n  - Parent middleware start\n    - Child middleware start\n      - Run loaders, generate HTML Response\n    - Child middleware end\n  - Parent middleware end\n- Root middleware end\n```\n\n<docs-info>There are some slight differences between middleware on the server (framework mode) versus the client (framework/data mode). For the purposes of this document, we'll be referring to Server Middleware in most of our examples as it's the most familiar to users who've used middleware in other HTTP servers in the past. Please refer to the [Server vs Client Middleware][server-client] section below for more information.</docs-info>\n\n## Quick Start (Framework mode)\n\n### 1. Enable the middleware flag\n\nFirst, enable middleware in your [React Router config][rr-config]:\n\n```ts filename=react-router.config.ts\nimport type { Config } from \"@react-router/dev/config\";\n\nexport default {\n  future: {\n    v8_middleware: true,\n  },\n} satisfies Config;\n```\n\n<docs-warning>By enabling the middleware feature, you change the type of the `context` parameter to your [`action`][framework-action]s and [`loader`][framework-loader]s. Please pay attention to the section on [`getLoadContext`][getloadcontext] below if you are actively using `context` today.</docs-warning>\n\n### 2. Create a context\n\nMiddleware uses a `context` provider instance to provide data down the middleware chain.\nYou can create type-safe context objects using [`createContext`][createContext]:\n\n```ts filename=app/context.ts\nimport { createContext } from \"react-router\";\nimport type { User } from \"~/types\";\n\nexport const userContext = createContext<User | null>(null);\n```\n\n### 3. Export middleware from your routes\n\n```tsx filename=app/routes/dashboard.tsx\nimport { redirect } from \"react-router\";\nimport { userContext } from \"~/context\";\n\n// Server-side Authentication Middleware\nasync function authMiddleware({ request, context }) {\n  const user = await getUserFromSession(request);\n  if (!user) {\n    throw redirect(\"/login\");\n  }\n  context.set(userContext, user);\n}\n\nexport const middleware: Route.MiddlewareFunction[] = [\n  authMiddleware,\n];\n\n// Client-side timing middleware\nasync function timingMiddleware({ context }, next) {\n  const start = performance.now();\n  await next();\n  const duration = performance.now() - start;\n  console.log(`Navigation took ${duration}ms`);\n}\n\nexport const clientMiddleware: Route.ClientMiddlewareFunction[] =\n  [timingMiddleware];\n\nexport async function loader({\n  context,\n}: Route.LoaderArgs) {\n  const user = context.get(userContext);\n  const profile = await getProfile(user);\n  return { profile };\n}\n\nexport default function Dashboard({\n  loaderData,\n}: Route.ComponentProps) {\n  return (\n    <div>\n      <h1>Welcome {loaderData.profile.fullName}!</h1>\n      <Profile profile={loaderData.profile} />\n    </div>\n  );\n}\n```\n\n### 4. Update your `getLoadContext` function (if applicable)\n\nIf you're using a custom server and a `getLoadContext` function, you will need to update your implementation to return an instance of [`RouterContextProvider`][RouterContextProvider], instead of a JavaScript object:\n\n```diff\n+import {\n+  createContext,\n+  RouterContextProvider,\n+} from \"react-router\";\nimport { createDb } from \"./db\";\n\n+const dbContext = createContext<Database>();\n\nfunction getLoadContext(req, res) {\n-  return { db: createDb() };\n+  const context = new RouterContextProvider();\n+  context.set(dbContext, createDb());\n+  return context;\n}\n```\n\n## Quick Start (Data Mode)\n\n<docs-info>Note there is no future flag in Data Mode because you can opt-into middleware by adding it to your routes, no breaking changes exist that require a future flag.</docs-info>\n\n### 1. Create a context\n\nMiddleware uses a `context` provider instance to provide data down the middleware chain.\nYou can create type-safe context objects using [`createContext`][createContext]:\n\n```ts\nimport { createContext } from \"react-router\";\nimport type { User } from \"~/types\";\n\nexport const userContext = createContext<User | null>(null);\n```\n\n### 2. Add middleware to your routes\n\n```tsx\nimport { redirect } from \"react-router\";\nimport { userContext } from \"~/context\";\n\nconst routes = [\n  {\n    path: \"/\",\n    middleware: [timingMiddleware], // 👈\n    Component: Root,\n    children: [\n      {\n        path: \"profile\",\n        middleware: [authMiddleware], // 👈\n        loader: profileLoader,\n        Component: Profile,\n      },\n      {\n        path: \"login\",\n        Component: Login,\n      },\n    ],\n  },\n];\n\nasync function timingMiddleware({ context }, next) {\n  const start = performance.now();\n  await next();\n  const duration = performance.now() - start;\n  console.log(`Navigation took ${duration}ms`);\n}\n\nasync function authMiddleware({ context }) {\n  const user = await getUser();\n  if (!user) {\n    throw redirect(\"/login\");\n  }\n  context.set(userContext, user);\n}\n\nexport async function profileLoader({\n  context,\n}: Route.LoaderArgs) {\n  const user = context.get(userContext);\n  const profile = await getProfile(user);\n  return { profile };\n}\n\nexport default function Profile() {\n  let loaderData = useLoaderData();\n  return (\n    <div>\n      <h1>Welcome {loaderData.profile.fullName}!</h1>\n      <Profile profile={loaderData.profile} />\n    </div>\n  );\n}\n```\n\n### 3. Add a `getContext` function (optional)\n\nIf you wish to include a base context on all navigations/fetches, you can add an [`getContext`][getContext] function to your router. This will be called to populate a fresh context on every navigation/fetch.\n\n```tsx\nlet sessionContext = createContext();\n\nconst router = createBrowserRouter(routes, {\n  getContext() {\n    let context = new RouterContextProvider();\n    context.set(sessionContext, getSession());\n    return context;\n  },\n});\n```\n\n<docs-info>This API exists to mirror the `getLoadContext` API on the server in Framework Mode, which exists as a way to hand off values from your HTTP server to the React Router handler. This [`getContext`][getContext] API can be used to hand off global values from the [`window`][window]/[`document`][document] to React Router, but because they're all running in the same context (the browser), you can achieve effectively the same behavior with root route middleware. Therefore, you may not need this API the same way you would on the server - but it's provided for consistency.</docs-warning>\n\n## Core Concepts\n\n### Server vs Client Middleware\n\nServer middleware runs on the server in Framework mode for HTML Document requests and `.data` requests for subsequent navigations and fetcher calls. Because server middleware runs on the server in response to an HTTP [`Request`][request], it returns an HTTP [`Response`][Response] back up the middleware chain via the `next` function:\n\n```ts\nasync function serverMiddleware({ request }, next) {\n  console.log(request.method, request.url);\n  let response = await next();\n  console.log(response.status, request.method, request.url);\n  return response;\n}\n\n// Framework mode only\nexport const middleware: Route.MiddlewareFunction[] = [\n  serverMiddleware,\n];\n```\n\nClient middleware runs in the browser in framework and data mode for client-side navigations and fetcher calls. Client middleware differs from server middleware because there's no HTTP Request, so it doesn't have a `Response` to bubble up. In most cases, you can just ignore the return value from `next` and return nothing from your middleware on the client:\n\n```ts\nasync function clientMiddleware({ request }, next) {\n  console.log(request.method, request.url);\n  await next();\n  console.log(response.status, request.method, request.url);\n}\n\n// Framework mode\nexport const clientMiddleware: Route.ClientMiddlewareFunction[] =\n  [clientMiddleware];\n\n// Or, Data mode\nconst route = {\n  path: \"/\",\n  middleware: [clientMiddleware],\n  loader: rootLoader,\n  Component: Root,\n};\n```\n\nThere may be _some_ cases where you want to do some post-processing based on the result of the loaders/action. In lieu of a `Response`, client middleware bubbles up the value returned from the active [`dataStrategy`][datastrategy] (`Record<string, DataStrategyResult>` - keyed by route id). This allows you to take conditional action in your middleware based on the outcome of the executed `loader`/`action` functions.\n\nHere's an example of the [CMS Redirect on 404][cms-redirect] use case implemented as a client side middleware:\n\n```tsx\nasync function cmsFallbackMiddleware({ request }, next) {\n  const results = await next();\n\n  // Check if we got a 404 from any of our routes and if so, look for a\n  // redirect in our CMS\n  const found404 = Object.values(results).some(\n    (r) =>\n      isRouteErrorResponse(r.result) &&\n      r.result.status === 404,\n  );\n  if (found404) {\n    const cmsRedirect = await checkCMSRedirects(\n      request.url,\n    );\n    if (cmsRedirect) {\n      throw redirect(cmsRedirect, 302);\n    }\n  }\n}\n```\n\n<docs-warning>In a server middleware, you shouldn't be messing with the `Response` body and should only be reading status/headers and setting headers. Similarly, this value should be considered read-only in client middleware because it represents the \"body\" or \"data\" for the resulting navigation which should be driven by loaders/actions - not middleware. This also means that in client middleware, there's usually no need to return the results even if you needed to capture it from `await next()`;</docs-warning>\n\n### When Middleware Runs\n\nIt is very important to understand _when_ your middlewares will run to make sure your application is behaving as you intend.\n\n#### Server Middleware\n\nIn a hydrated Framework Mode app, server middleware is designed such that it prioritizes SPA behavior and does not create new network activity by default. Middleware wraps _existing_ requests and only runs when you _need_ to hit the server.\n\nThis raises the question of what is a \"handler\" in React Router? Is it the route? Or the `loader`? We think \"it depends\":\n\n- On document requests (`GET /route`), the handler is the route — because the response encompasses both the `loader` and the route component\n- On data requests (`GET /route.data`) for client-side navigations, the handler is the [`action`][data-action]/[`loader`][data-loader], because that's all that is included in the response\n\nTherefore:\n\n- Document requests run server middleware whether `loader`s exist or not because we're still in a \"handler\" to render the UI\n- Client-side navigations will only run server middleware if a `.data` request is made to the server for a [`action`][framework-action]/[`loader`][framework-loader]\n\nThis is important behavior for request-annotation middlewares such as logging request durations, checking/setting sessions, setting outgoing caching headers, etc. It would be useless to go to the server and run those types of middlewares when there was no reason to go to the server in the first place. This would result in increased server load and noisy server logs.\n\n```tsx filename=app/root.tsx\n// This middleware won't run on client-side navigations without a `.data` request\nasync function loggingMiddleware({ request }, next) {\n  console.log(`Request: ${request.method} ${request.url}`);\n  let response = await next();\n  console.log(\n    `Response: ${response.status} ${request.method} ${request.url}`,\n  );\n  return response;\n}\n\nexport const middleware: Route.MiddlewareFunction[] = [\n  loggingMiddleware,\n];\n```\n\nHowever, there may be cases where you _want_ to run certain server middlewares on _every_ client-navigation - even if no `loader` exists. For example, a form in the authenticated section of your site that doesn't require a `loader` but you'd rather use auth middleware to redirect users away before they fill out the form — rather than when they submit to the `action`. If your middleware meets these criteria, then you can put a `loader` on the route that contains the middleware to force it to always call the server for client-side navigations involving that route.\n\n```tsx filename=app/_auth.tsx\nfunction authMiddleware({ request }, next) {\n  if (!isLoggedIn(request)) {\n    throw redirect(\"/login\");\n  }\n}\n\nexport const middleware: Route.MiddlewareFunction[] = [\n  authMiddleware,\n];\n\n// By adding a `loader`, we force the `authMiddleware` to run on every\n// client-side navigation involving this route.\nexport async function loader() {\n  return null;\n}\n```\n\n#### Client Middleware\n\nClient middleware is simpler because since we are already on the client and are always making a \"request\" to the router when navigating. Client middlewares will run on every client navigation, regardless of whether there are `loader`s to run.\n\n### Context API\n\nThe new context system provides type safety and prevents naming conflicts and allows you to provide data to nested middlewares and `action`/`loader` functions. In Framework Mode, this replaces the previous `AppLoadContext` API.\n\n```ts\n// ✅ Type-safe\nimport { createContext } from \"react-router\";\nconst userContext = createContext<User>();\n\n// Later in middleware/`loader`s\ncontext.set(userContext, user); // Must be `User` type\nconst user = context.get(userContext); // Returns `User` type\n\n// ❌ Old way (no type safety)\ncontext.user = user; // Could be anything\n```\n\n#### `Context` and `AsyncLocalStorage`\n\nNode provides an [`AsyncLocalStorage`][asynclocalstorage] API which gives you a way to provide values through asynchronous execution contexts. While this is a Node API, most modern runtimes have made it (mostly) available (i.e., [Cloudflare][cloudflare], [Bun][bun], [Deno][deno]).\n\nIn theory, we could have leveraged [`AsyncLocalStorage`][asynclocalstorage] directly as the way to pass values from middlewares to child routes, but the lack of 100% cross-platform compatibility was concerning enough that we wanted to still ship a first-class `context` API so there would be a way to publish reusable middleware packages guaranteed to work in a runtime-agnostic manner.\n\nThat said, this API still works great with React Router middleware and can be used in place of, or alongside of the `context` API:\n\n<docs-info>[`AsyncLocalStorage`][asynclocalstorage] is _especially_ powerful when using [React Server Components](../how-to/react-server-components) because it allows you to provide information from `middleware` to your Server Components and Server Actions because they run in the same server execution context 🤯</docs-info>\n\n```tsx filename=app/user-context.ts\nimport { AsyncLocalStorage } from \"node:async_hooks\";\n\nconst USER = new AsyncLocalStorage<User>();\n\nexport async function provideUser(\n  request: Request,\n  cb: () => Promise<Response>,\n) {\n  let user = await getUser(request);\n  return USER.run(user, cb);\n}\n\nexport function getUser() {\n  return USER.getStore();\n}\n```\n\n```tsx filename=app/root.tsx\nimport { provideUser } from \"./user-context\";\n\nexport const middleware: Route.MiddlewareFunction[] = [\n  async ({ request, context }, next) => {\n    return provideUser(request, async () => {\n      let res = await next();\n      return res;\n    });\n  },\n];\n```\n\n```tsx filename=app/routes/_index.tsx\nimport { getUser } from \"../user-context\";\n\nexport async function loader() {\n  let user = getUser();\n  //...\n}\n```\n\n### The `next` function\n\nThe `next` function logic depends on which route middleware it's being called from:\n\n- When called from a non-leaf middleware, it runs the next middleware in the chain\n- When called from the leaf middleware, it executes any route handlers and generates the resulting [`Response`][Response] for the request\n\n```ts\nconst middleware = async ({ context }, next) => {\n  // Code here runs BEFORE handlers\n  console.log(\"Before\");\n\n  const response = await next();\n\n  // Code here runs AFTER handlers\n  console.log(\"After\");\n\n  return response; // Optional on client, required on server\n};\n```\n\n<docs-warning>You can only call `next()` once per middleware. Calling it multiple times will throw an error</docs-warning>\n\n### Skipping `next()`\n\nIf you don't need to run code after your handlers, you can skip calling `next()`:\n\n```ts\nconst authMiddleware = async ({ request, context }) => {\n  const user = await getUser(request);\n  if (!user) {\n    throw redirect(\"/login\");\n  }\n  context.set(userContext, user);\n  // next() is called automatically\n};\n```\n\n### `next()` and Error Handling\n\nReact Router contains built-in error handling via the route [`ErrorBoundary`][ErrorBoundary] export. Just like when a `action`/`loader` throws, if a `middleware` throws it will be caught and handled at the appropriate [`ErrorBoundary`][ErrorBoundary] and a [`Response`][Response] will be returned through the ancestor `next()` call. This means that the `next()` function should never throw and should always return a [`Response`][Response], so you don't need to worry about wrapping it in a try/catch.\n\nThis behavior is important to allow middleware patterns such as automatically setting required headers on outgoing responses (i.e., committing a session) from a root `middleware`. If any error from a `middleware` caused `next()` to `throw`, we'd miss the execution of ancestor middlewares on the way out and those required headers wouldn't be set.\n\n```tsx filename=routes/parent.tsx\nexport const middleware: Route.MiddlewareFunction[] = [\n  async (_, next) => {\n    let res = await next();\n    //  ^ res.status = 500\n    // This response contains the ErrorBoundary\n    return res;\n  },\n];\n```\n\n```tsx filename=routes/parent.child.tsx\nexport const middleware: Route.MiddlewareFunction[] = [\n  async (_, next) => {\n    let res = await next();\n    //  ^ res.status = 200\n    // This response contains the successful UI render\n    throw new Error(\"Uh oh, something went wrong!\");\n  },\n];\n```\n\nWhich `ErrorBoundary` is rendered will differ based on whether your middleware threw _before_ or _after_ calling then `next()` function. If it throws _after_ then it will bubble up from the throwing route just like a normal loader error because we've already run the loaders and have the appropriate `loaderData` to render in the route components. However, if an error is thrown _before_ calling `next()`, then we haven't called any loaders yet and there is no `loaderData` available. When this happens, we must bubble up to the highest route with a `loader` and start looking for an `ErrorBoundary` there. We cannot render any route components at that level or below without any `loaderData`.\n\n## Changes to `getLoadContext`/`AppLoadContext`\n\n<docs-info>This only applies if you are using a custom server and a custom `getLoadContext` function</docs-info>\n\nMiddleware introduces a breaking change to the `context` parameter generated by `getLoadContext` and passed to your `action`s and `loader`s. The current approach of a module-augmented `AppLoadContext` isn't really type-safe and instead just sort of tells TypeScript to \"trust me\".\n\nMiddleware needs an equivalent `context` on the client for `clientMiddleware`, but we didn't want to duplicate this pattern from the server that we already weren't thrilled with, so we decided to introduce a new API where we could tackle type-safety.\n\nWhen opting into middleware, the `context` parameter changes to an instance of [`RouterContextProvider`][RouterContextProvider]:\n\n```ts\nlet dbContext = createContext<Database>();\nlet context = new RouterContextProvider();\ncontext.set(dbContext, getDb());\n//                     ^ type-safe\nlet db = context.get(dbContext);\n//  ^ Database\n```\n\nIf you're using a custom server and a `getLoadContext` function, you will need to update your implementation to return an instance of [`RouterContextProvider`][RouterContextProvider], instead of a plain JavaScript object:\n\n```diff\n+import {\n+  createContext,\n+  RouterContextProvider,\n+} from \"react-router\";\nimport { createDb } from \"./db\";\n\n+const dbContext = createContext<Database>();\n\nfunction getLoadContext(req, res) {\n-  return { db: createDb() };\n+  const context = new RouterContextProvider();\n+  context.set(dbContext, createDb());\n+  return context;\n}\n```\n\n### Migration from `AppLoadContext`\n\nIf you're currently using `AppLoadContext`, you can migrate incrementally by using your existing module augmentation to augment [`RouterContextProvider`][RouterContextProvider] instead of `AppLoadContext`. Then, update your `getLoadContext` function to return an instance of [`RouterContextProvider`][RouterContextProvider]:\n\n```diff\ndeclare module \"react-router\" {\n-  interface AppLoadContext {\n+  interface RouterContextProvider {\n    db: Database;\n    user: User;\n  }\n}\n\nfunction getLoadContext() {\n  const loadContext = {...};\n-  return loadContext;\n+  let context = new RouterContextProvider();\n+  Object.assign(context, loadContext);\n+  return context;\n}\n```\n\nThis allows you to leave your `action`s/`loader`s untouched during initial adoption of middleware, since they can still read values directly (i.e., `context.db`).\n\n<docs-warning>This approach is only intended to be used as a migration strategy when adopting middleware in React Router v7, allowing you to incrementally migrate to `context.set`/`context.get`. It is not safe to assume this approach will work in the next major version of React Router.</docs-warning>\n\n<docs-warning>The [`RouterContextProvider`][RouterContextProvider] class is also used for the client-side `context` parameter via `<HydratedRouter getContext>` and `<RouterProvider getContext>`. Since `AppLoadContext` is primarily intended as a hand-off from your HTTP server into the React Router handlers, you need to be aware that these augmented fields will not be available in `clientMiddleware`, `clientLoader`, or `clientAction` functions even thought TypeScript will tell you they are (unless, of course, you provide the fields via `getContext` on the client).</docs-warning>\n\n## Common Patterns\n\n### Authentication\n\n```tsx filename=app/middleware/auth.ts\nimport { redirect } from \"react-router\";\nimport { userContext } from \"~/context\";\nimport { getSession } from \"~/sessions.server\";\n\nexport const authMiddleware = async ({\n  request,\n  context,\n}) => {\n  const session = await getSession(request);\n  const userId = session.get(\"userId\");\n\n  if (!userId) {\n    throw redirect(\"/login\");\n  }\n\n  const user = await getUserById(userId);\n  context.set(userContext, user);\n};\n```\n\n```tsx filename=app/routes/protected.tsx\nimport { authMiddleware } from \"~/middleware/auth\";\n\nexport const middleware: Route.MiddlewareFunction[] = [\n  authMiddleware,\n];\n\nexport async function loader({\n  context,\n}: Route.LoaderArgs) {\n  const user = context.get(userContext); // Guaranteed to exist\n  return { user };\n}\n```\n\n### Logging\n\n```tsx filename=app/middleware/logging.ts\nimport { requestIdContext } from \"~/context\";\n\nexport const loggingMiddleware = async (\n  { request, context },\n  next,\n) => {\n  const requestId = crypto.randomUUID();\n  context.set(requestIdContext, requestId);\n\n  console.log(\n    `[${requestId}] ${request.method} ${request.url}`,\n  );\n\n  const start = performance.now();\n  const response = await next();\n  const duration = performance.now() - start;\n\n  console.log(\n    `[${requestId}] Response ${response.status} (${duration}ms)`,\n  );\n\n  return response;\n};\n```\n\n### CMS Redirect on 404\n\n```tsx filename=app/middleware/cms-fallback.ts\nexport const cmsFallbackMiddleware = async (\n  { request },\n  next,\n) => {\n  const response = await next();\n\n  // Check if we got a 404\n  if (response.status === 404) {\n    // Check CMS for a redirect\n    const cmsRedirect = await checkCMSRedirects(\n      request.url,\n    );\n    if (cmsRedirect) {\n      throw redirect(cmsRedirect, 302);\n    }\n  }\n\n  return response;\n};\n```\n\n### Response Headers\n\n```tsx filename=app/middleware/headers.ts\nexport const headersMiddleware = async (\n  { context },\n  next,\n) => {\n  const response = await next();\n\n  // Add security headers\n  response.headers.set(\"X-Frame-Options\", \"DENY\");\n  response.headers.set(\"X-Content-Type-Options\", \"nosniff\");\n\n  return response;\n};\n```\n\n### Conditional Middleware\n\n```tsx\nexport const middleware: Route.MiddlewareFunction[] = [\n  async ({ request, context }, next) => {\n    // Only run auth for POST requests\n    if (request.method === \"POST\") {\n      await ensureAuthenticated(request, context);\n    }\n    return next();\n  },\n];\n```\n\n### Sharing Context Between `action` and `loader`\n\n<docs-info>On the server, this approach only works for document POST requests because `context` is scoped to a request. SPA navigation submissions use separate POST/GET requests so you cannot share `context` between them. This pattern always works in `clientMiddleware`/`clientLoader`/`clientAction` because there's no separate HTTP requests.</docs-info>\n\n```tsx\nconst sharedDataContext = createContext<any>();\n\nexport const middleware: Route.MiddlewareFunction[] = [\n  async ({ request, context }, next) => {\n    // Set data if it doesn't exist\n    // This will only run once for document requests\n    // It will run twice (action request + loader request) in SPA submissions\n    if (!context.get(sharedDataContext)) {\n      context.set(\n        sharedDataContext,\n        await getExpensiveData(),\n      );\n    }\n    return next();\n  },\n];\n\nexport async function action({\n  context,\n}: Route.ActionArgs) {\n  const data = context.get(sharedDataContext);\n  // Use the data...\n}\n\nexport async function loader({\n  context,\n}: Route.LoaderArgs) {\n  const data = context.get(sharedDataContext);\n  // Same data is available here\n}\n```\n\n[future-flags]: ../upgrading/future\n[Response]: https://developer.mozilla.org/en-US/docs/Web/API/Response\n[common-patterns]: #common-patterns\n[server-client]: #server-vs-client-middleware\n[rr-config]: ../api/framework-conventions/react-router.config.ts\n[framework-action]: ../start/framework/route-module#action\n[framework-loader]: ../start/framework/route-module#loader\n[getloadcontext]: #changes-to-getloadcontextapploadcontext\n[datastrategy]: ../api/data-routers/createBrowserRouter#optsdatastrategy\n[cms-redirect]: #cms-redirect-on-404\n[createContext]: ../api/utils/createContext\n[RouterContextProvider]: ../api/utils/RouterContextProvider\n[getContext]: ../api/data-routers/createBrowserRouter#optsgetContext\n[window]: https://developer.mozilla.org/en-US/docs/Web/API/Window\n[document]: https://developer.mozilla.org/en-US/docs/Web/API/Document\n[request]: https://developer.mozilla.org/en-US/docs/Web/API/Request\n[data-action]: ../start/data/route-object#action\n[data-loader]: ../start/data/route-object#loader\n[asynclocalstorage]: https://nodejs.org/api/async_context.html#class-asynclocalstorage\n[cloudflare]: https://developers.cloudflare.com/workers/runtime-apis/nodejs/asynclocalstorage/\n[bun]: https://bun.sh/blog/bun-v0.7.0#asynclocalstorage-support\n[deno]: https://docs.deno.com/api/node/async_hooks/~/AsyncLocalStorage\n[ErrorBoundary]: ../start/framework/route-module#errorboundary\n"
  },
  {
    "path": "docs/how-to/navigation-blocking.md",
    "content": "---\ntitle: Navigation Blocking\n---\n\n# Navigation Blocking\n\n[MODES: framework, data]\n\n<br/>\n<br/>\n\nWhen users are in the middle of a workflow, like filling out an important form, you may want to prevent them from navigating away from the page.\n\nThis example will show:\n\n- Setting up a route with a form and action called with a fetcher\n- Blocking navigation when the form is dirty\n- Showing a confirmation when the user tries to leave the page\n\n## 1. Set up a route with a form\n\nAdd a route with the form, we'll use a \"contact\" route for this example:\n\n```ts filename=routes.ts\nimport {\n  type RouteConfig,\n  index,\n  route,\n} from \"@react-router/dev/routes\";\n\nexport default [\n  index(\"routes/home.tsx\"),\n  route(\"contact\", \"routes/contact.tsx\"),\n] satisfies RouteConfig;\n```\n\nAdd the form to the contact route module:\n\n```tsx filename=routes/contact.tsx\nimport { useFetcher } from \"react-router\";\nimport type { Route } from \"./+types/contact\";\n\nexport async function action({\n  request,\n}: Route.ActionArgs) {\n  let formData = await request.formData();\n  let email = formData.get(\"email\");\n  let message = formData.get(\"message\");\n  console.log(email, message);\n  return { ok: true };\n}\n\nexport default function Contact() {\n  let fetcher = useFetcher();\n\n  return (\n    <fetcher.Form method=\"post\">\n      <p>\n        <label>\n          Email: <input name=\"email\" type=\"email\" />\n        </label>\n      </p>\n      <p>\n        <textarea name=\"message\" />\n      </p>\n      <p>\n        <button type=\"submit\">\n          {fetcher.state === \"idle\" ? \"Send\" : \"Sending...\"}\n        </button>\n      </p>\n    </fetcher.Form>\n  );\n}\n```\n\n## 2. Add dirty state and onChange handler\n\nTo track the dirty state of the form, we'll use a single boolean and a quick form onChange handler. You may want to track the dirty state differently but this works for this guide.\n\n```tsx filename=routes/contact.tsx lines=[2,8-12]\nexport default function Contact() {\n  let [isDirty, setIsDirty] = useState(false);\n  let fetcher = useFetcher();\n\n  return (\n    <fetcher.Form\n      method=\"post\"\n      onChange={(event) => {\n        let email = event.currentTarget.email.value;\n        let message = event.currentTarget.message.value;\n        setIsDirty(Boolean(email || message));\n      }}\n    >\n      {/* existing code */}\n    </fetcher.Form>\n  );\n}\n```\n\n## 3. Block navigation when the form is dirty\n\n```tsx filename=routes/contact.tsx lines=[1,6-8]\nimport { useBlocker } from \"react-router\";\n\nexport default function Contact() {\n  let [isDirty, setIsDirty] = useState(false);\n  let fetcher = useFetcher();\n  let blocker = useBlocker(\n    useCallback(() => isDirty, [isDirty]),\n  );\n\n  // ... existing code\n}\n```\n\nWhile this will now block a navigation, there's no way for the user to confirm it.\n\n## 4. Show confirmation UI\n\nThis uses a simple div, but you may want to use a modal dialog.\n\n```tsx filename=routes/contact.tsx lines=[19-41]\nexport default function Contact() {\n  let [isDirty, setIsDirty] = useState(false);\n  let fetcher = useFetcher();\n  let blocker = useBlocker(\n    useCallback(() => isDirty, [isDirty]),\n  );\n\n  return (\n    <fetcher.Form\n      method=\"post\"\n      onChange={(event) => {\n        let email = event.currentTarget.email.value;\n        let message = event.currentTarget.message.value;\n        setIsDirty(Boolean(email || message));\n      }}\n    >\n      {/* existing code */}\n\n      {blocker.state === \"blocked\" && (\n        <div>\n          <p>Wait! You didn't send the message yet:</p>\n          <p>\n            <button\n              type=\"button\"\n              onClick={() => blocker.proceed()}\n            >\n              Leave\n            </button>{\" \"}\n            <button\n              type=\"button\"\n              onClick={() => blocker.reset()}\n            >\n              Stay here\n            </button>\n          </p>\n        </div>\n      )}\n    </fetcher.Form>\n  );\n}\n```\n\nIf the user clicks \"leave\" then `blocker.proceed()` will proceed with the navigation. If they click \"stay here\" then `blocker.reset()` will clear the blocker and keep them on the current page.\n\n## 5. Reset the blocker when the action resolves\n\nIf the user doesn't click either \"leave\" or \"stay here\", then submits the form, the blocker will still be active. Let's reset the blocker when the action resolves with an effect.\n\n```tsx filename=routes/contact.tsx\nuseEffect(() => {\n  if (fetcher.data?.ok) {\n    if (blocker.state === \"blocked\") {\n      blocker.reset();\n    }\n  }\n}, [fetcher.data]);\n```\n\n## 6. Clear the form when the action resolves\n\nWhile unrelated to navigation blocking, let's clear the form when the action resolves with a ref.\n\n```tsx\nlet formRef = useRef<HTMLFormElement>(null);\n\n// put it on the form\n<fetcher.Form\n  ref={formRef}\n  method=\"post\"\n  onChange={(event) => {\n    // ... existing code\n  }}\n>\n  {/* existing code */}\n</fetcher.Form>;\n```\n\n```tsx\nuseEffect(() => {\n  if (fetcher.data?.ok) {\n    // clear the form in the effect\n    formRef.current?.reset();\n    if (blocker.state === \"blocked\") {\n      blocker.reset();\n    }\n  }\n}, [fetcher.data]);\n```\n\nAlternatively, if a navigation is currently blocked, instead of resetting the blocker, you can proceed through to the blocked navigation.\n\n```tsx\nuseEffect(() => {\n  if (fetcher.data?.ok) {\n    if (blocker.state === \"blocked\") {\n      // proceed with the blocked navigation\n      blocker.proceed();\n    } else {\n      formRef.current?.reset();\n    }\n  }\n}, [fetcher.data]);\n```\n\nIn this case the user flow is:\n\n- User fills out the form\n- User forgets to click \"send\" and clicks a link instead\n- The navigation is blocked, and the confirmation message is shown\n- Instead of clicking \"leave\" or \"stay here\", the user submits the form\n- The user is taken to the requested page\n"
  },
  {
    "path": "docs/how-to/optimize-revalidation.md",
    "content": "---\ntitle: Revalidation Optimization\nhidden: true\n---\n\n[copy pasted]\n\nDuring client-side transitions, React Router will optimize reloading of routes that are already rendering, like not reloading layout routes that aren't changing. In other cases, like form submissions or search param changes, React Router doesn't know which routes need to be reloaded, so it reloads them all to be safe. This ensures your UI always stays in sync with the state on your server.\n\nThis function lets apps further optimize by returning `false` when React Router is about to reload a route. If you define this function on a route module, React Router will defer to your function on every navigation and every revalidation after an action is called. Again, this makes it possible for your UI to get out of sync with your server if you do it wrong, so be careful.\n\n`fetcher.load` calls also revalidate, but because they load a specific URL, they don't have to worry about route param or URL search param revalidations. `fetcher.load`'s only revalidate by default after action submissions and explicit revalidation requests via [`useRevalidator`][use-revalidator].\n"
  },
  {
    "path": "docs/how-to/pre-rendering.md",
    "content": "---\ntitle: Pre-Rendering\n---\n\n# Pre-Rendering\n\n[MODES: framework]\n\n<br/>\n<br/>\n\nPre-Rendering allows you to speed up page loads for static content by rendering pages at build time instead of at runtime.\n\n## Configuration\n\nPre-rendering is enabled via the `prerender` config in `react-router.config.ts`.\n\nThe simplest configuration is a boolean `true` which will pre-render all off the applications static paths based on `routes.ts`:\n\n```ts filename=react-router.config.ts\nimport type { Config } from \"@react-router/dev/config\";\n\nexport default {\n  prerender: true,\n} satisfies Config;\n```\n\nThe boolean `true` will not include any dynamic paths (i.e., `/blog/:slug`) because the parameter values are unknown.\n\nTo configure specific paths including dynamic values, you can specify an array of paths:\n\n```ts filename=react-router.config.ts\nimport type { Config } from \"@react-router/dev/config\";\n\nlet slugs = getPostSlugs();\n\nexport default {\n  prerender: [\n    \"/\",\n    \"/blog\",\n    ...slugs.map((s) => `/blog/${s}`),\n  ],\n} satisfies Config;\n```\n\nIf you need to perform more complex and/or asynchronous logic to determine the paths, you can also provide a function that returns an array of paths. This function provides you with a `getStaticPaths` method you can use to avoid manually adding all of the static paths in your application:\n\n```ts filename=react-router.config.ts\nimport type { Config } from \"@react-router/dev/config\";\n\nexport default {\n  async prerender({ getStaticPaths }) {\n    let slugs = await getPostSlugsFromCMS();\n    return [\n      ...getStaticPaths(), // \"/\" and \"/blog\"\n      ...slugs.map((s) => `/blog/${s}`),\n    ];\n  },\n} satisfies Config;\n```\n\n### Concurrency (unstable)\n\n<docs-warning>This API is experimental and subject to breaking changes in\nminor/patch releases. Please use with caution and pay **very** close attention\nto release notes for relevant changes.</docs-warning>\n\nBy default, pages are pre-rendered one path at a time. You can enable concurrency to pre-render multiple paths in parallel which can speed up build times in many cases. You should experiment with the value that provides the best performance for your app.\n\nTo specify concurrency, move your `prerender` config down into a `prerender.paths` field and you can specify the concurrency in `prerender.unstable_concurrency`:\n\n```ts filename=react-router.config.ts\nimport type { Config } from \"@react-router/dev/config\";\n\nlet slugs = getPostSlugs();\n\nexport default {\n  prerender: {\n    paths: [\n      \"/\",\n      \"/blog\",\n      ...slugs.map((s) => `/blog/${s}`),\n    ],\n    unstable_concurrency: 4,\n  },\n} satisfies Config;\n```\n\n## Pre-Rendering with/without a Runtime Server\n\nPre-Rendering can be used in two ways based on the `ssr` config value:\n\n- Alongside a runtime SSR server with `ssr:true` (the default value)\n- Deployed to a static file server with `ssr:false`\n\n### Pre-rendering with `ssr:true`\n\nWhen pre-rendering with `ssr:true`, you're indicating you will still have a runtime server but you are choosing to pre-render certain paths for quicker Response times.\n\n```ts filename=react-router.config.ts\nimport type { Config } from \"@react-router/dev/config\";\n\nexport default {\n  // Can be omitted - defaults to true\n  ssr: true,\n  prerender: [\"/\", \"/blog\", \"/blog/popular-post\"],\n} satisfies Config;\n```\n\n#### Data Loading and Pre-rendering\n\nThere is no extra application API for pre-rendering. Routes being pre-rendered use the same route `loader` functions as server rendering:\n\n```tsx\nexport async function loader({ request, params }) {\n  let post = await getPost(params.slug);\n  return post;\n}\n\nexport function Post({ loaderData }) {\n  return <div>{loaderData.title}</div>;\n}\n```\n\nInstead of a request coming to your route on a deployed server, the build creates a `new Request()` and runs it through your app just like a server would.\n\nWhen server rendering, requests to paths that have not been pre-rendered will be server rendered as usual.\n\n#### Static File Output\n\nThe rendered result will be written out to your `build/client` directory. You'll notice two files for each path:\n\n- `[url].html` HTML file for initial document requests\n- `[url].data` file for client side navigation browser requests\n\nThe output of your build will indicate what files were pre-rendered:\n\n```sh\n> react-router build\nvite v5.2.11 building for production...\n...\nvite v5.2.11 building SSR bundle for production...\n...\nPrerender: Generated build/client/index.html\nPrerender: Generated build/client/blog.data\nPrerender: Generated build/client/blog/index.html\nPrerender: Generated build/client/blog/my-first-post.data\nPrerender: Generated build/client/blog/my-first-post/index.html\n...\n```\n\nDuring development, pre-rendering doesn't save the rendered results to the public directory, this only happens for `react-router build`.\n\n### Pre-rendering with `ssr:false`\n\nThe above examples assume you are deploying a runtime server but are pre-rendering some static pages to avoid hitting the server, resulting in faster loads.\n\nTo disable runtime SSR and configure pre-rendering to be served from a static file server, you can set the `ssr:false` config flag:\n\n```ts filename=react-router.config.ts\nimport type { Config } from \"@react-router/dev/config\";\n\nexport default {\n  ssr: false, // disable runtime server rendering\n  prerender: true, // pre-render all static routes\n} satisfies Config;\n```\n\nIf you specify `ssr:false` without a `prerender` config, React Router refers to that as [SPA Mode](./spa). In SPA Mode, we render a single HTML file that is capable of hydrating for _any_ of your application paths. It can do this because it only renders the `root` route into the HTML file and then determines which child routes to load based on the browser URL during hydration. This means you can use a `loader` on the root route, but not on any other routes because we don't know which routes to load until hydration in the browser.\n\nIf you want to pre-render paths with `ssr:false`, those matched routes _can_ have loaders because we'll pre-render all of the matched routes for those paths, not just the root. You cannot include `actions` or `headers` functions in any routes when `ssr:false` is set because there will be no runtime server to run them on.\n\n#### Pre-rendering with a SPA Fallback\n\nIf you want `ssr:false` but don't want to pre-render _all_ of your routes - that's fine too! You may have some paths where you need the performance/SEO benefits of pre-rendering, but other pages where a SPA would be fine.\n\nYou can do this using the combination of config options as well - just limit your `prerender` config to the paths that you want to pre-render and React Router will also output a \"SPA Fallback\" HTML file that can be served to hydrate any other paths (using the same approach as [SPA Mode](./spa)).\n\nThis will be written to one of the following paths:\n\n- `build/client/index.html` - If the `/` path is not pre-rendered\n- `build/client/__spa-fallback.html` - If the `/` path is pre-rendered\n\n```ts filename=react-router.config.ts\nimport type { Config } from \"@react-router/dev/config\";\n\nexport default {\n  ssr: false,\n\n  // SPA fallback will be written to build/client/index.html\n  prerender: [\"/about-us\"],\n\n  // SPA fallback will be written to build/client/__spa-fallback.html\n  prerender: [\"/\", \"/about-us\"],\n} satisfies Config;\n```\n\nYou can configure your deployment server to serve this file for any path that otherwise would 404. Some hosts do this by default, but others don't. As an example, a host may support a `_redirects` file to do this:\n\n```\n# If you did not pre-render the `/` route\n/*    /index.html   200\n\n# If you pre-rendered the `/` route\n/*    /__spa-fallback.html   200\n```\n\nIf you're getting 404s at valid routes for your app, it's likely you need to configure your host.\n\nHere's another example of how you can do this with the [`sirv-cli`](https://www.npmjs.com/package/sirv-cli#user-content-single-page-applications) tool:\n\n```sh\n# If you did not pre-render the `/` route\nsirv-cli build/client --single index.html\n\n# If you pre-rendered the `/` route\nsirv-cli build/client --single __spa-fallback.html\n```\n\n#### Invalid Exports\n\nWhen pre-rendering with `ssr:false`, React Router will error at build time if you have invalid exports to help prevent some mistakes that can be easily overlooked.\n\n- `headers`/`action` functions are prohibited in all routes because there will be no runtime server on which to run them\n- When using `ssr:false` without a `prerender` config (SPA Mode), a `loader` is permitted on the root route only\n- When using `ssr:false` with a `prerender` config, a `loader` is permitted on any route matched by a `prerender` path\n  - If you are using a `loader` on a pre-rendered route that has child routes, you will need to make sure the parent `loaderData` can be determined at run-time properly by either:\n    - Pre-rendering all child routes so that the parent `loader` can be called at build-time for each child route path and rendered into a `.data` file, or\n    - Use a `clientLoader` on the parent that can be called at run-time for non-pre-rendered child paths\n"
  },
  {
    "path": "docs/how-to/presets.md",
    "content": "---\ntitle: Presets\n---\n\n# Presets\n\n[MODES: framework]\n\n<br/>\n<br/>\n\nThe [React Router config][react-router-config] supports a `presets` option to ease integration with other tools and hosting providers.\n\n[Presets][preset-type] can only do two things:\n\n- Configure React Router config options on your behalf\n- Validate the resolved config\n\nThe config returned by each preset is merged in the order the presets were defined. Any config directly specified in your React Router config will be merged last. This means that your config will always take precedence over any presets.\n\n## Defining preset config\n\nAs a basic example, let's create a preset that configures a [server bundles function][server-bundles]:\n\n```ts filename=my-cool-preset.ts\nimport type { Preset } from \"@react-router/dev/config\";\n\nexport function myCoolPreset(): Preset {\n  return {\n    name: \"my-cool-preset\",\n    reactRouterConfig: () => ({\n      serverBundles: ({ branch }) => {\n        const isAuthenticatedRoute = branch.some((route) =>\n          route.id.split(\"/\").includes(\"_authenticated\"),\n        );\n\n        return isAuthenticatedRoute\n          ? \"authenticated\"\n          : \"unauthenticated\";\n      },\n    }),\n  };\n}\n```\n\n## Validating config\n\nKeep in mind that other presets and user config can still override the values returned from your preset.\n\nIn our example preset, the `serverBundles` function could be overridden with a different, conflicting implementation. If we want to validate that the final resolved config contains the `serverBundles` function from our preset, we can use the `reactRouterConfigResolved` hook:\n\n```ts filename=my-cool-preset.ts lines=[22-27]\nimport type {\n  Preset,\n  ServerBundlesFunction,\n} from \"@react-router/dev/config\";\n\nconst serverBundles: ServerBundlesFunction = ({\n  branch,\n}) => {\n  const isAuthenticatedRoute = branch.some((route) =>\n    route.id.split(\"/\").includes(\"_authenticated\"),\n  );\n\n  return isAuthenticatedRoute\n    ? \"authenticated\"\n    : \"unauthenticated\";\n};\n\nexport function myCoolPreset(): Preset {\n  return {\n    name: \"my-cool-preset\",\n    reactRouterConfig: () => ({ serverBundles }),\n    reactRouterConfigResolved: ({ reactRouterConfig }) => {\n      if (\n        reactRouterConfig.serverBundles !== serverBundles\n      ) {\n        throw new Error(\"`serverBundles` was overridden!\");\n      }\n    },\n  };\n}\n```\n\nThe `reactRouterConfigResolved` hook should only be used when it would be an error to merge or override your preset's config.\n\n## Using a preset\n\nPresets are designed to be published to npm and used within your React Router config.\n\n```ts filename=react-router.config.ts lines=[6]\nimport type { Config } from \"@react-router/dev/config\";\nimport { myCoolPreset } from \"react-router-preset-cool\";\n\nexport default {\n  // ...\n  presets: [myCoolPreset()],\n} satisfies Config;\n```\n\n[react-router-config]: https://api.reactrouter.com/v7/types/_react-router_dev.config.Config.html\n[preset-type]: https://api.reactrouter.com/v7/types/_react-router_dev.config.Preset.html\n[server-bundles]: ./server-bundles\n"
  },
  {
    "path": "docs/how-to/react-server-components.md",
    "content": "---\ntitle: React Server Components\nunstable: true\n---\n\n# React Server Components\n\n[MODES: framework, data]\n\n<br/>\n<br/>\n\n<docs-warning>React Server Components support is experimental and subject to breaking changes in\nminor/patch releases. Please use with caution and pay **very** close attention\nto release notes for relevant changes.</docs-warning>\n\nReact Server Components (RSC) refers generally to an architecture and set of APIs provided by React since version 19.\n\nFrom the docs:\n\n> Server Components are a new type of Component that renders ahead of time, before bundling, in an environment separate from your client app or SSR server.\n>\n> <cite>- [React \"Server Components\" docs][react-server-components-doc]</cite>\n\nReact Router provides a set of APIs for integrating with RSC-compatible bundlers, allowing you to leverage [Server Components][react-server-components-doc] and [Server Functions][react-server-functions-doc] in your React Router applications.\n\nIf you're unfamiliar with these React features, we recommend reading the official [Server Components documentation][react-server-components-doc] before using React Router's RSC APIs.\n\nRSC support is available in both Framework and Data Modes. For more information on the conceptual difference between these, see [\"Picking a Mode\"][picking-a-mode]. However, note that the APIs and features differ between RSC and non-RSC modes in ways that this guide will cover in more detail.\n\n## Quick Start\n\nThe quickest way to get started is with one of our templates.\n\nThese templates come with React Router RSC APIs already configured, offering you out of the box features such as:\n\n- Server Component Routes\n- Server Side Rendering (SSR)\n- Client Components (via [`\"use client\"`][use-client-docs] directive)\n- Server Functions (via [`\"use server\"`][use-server-docs] directive)\n\n### RSC Framework Mode Template\n\nThe [RSC Framework Mode template][framework-rsc-template] uses the unstable React Router RSC Vite plugin along with the experimental [`@vitejs/plugin-rsc` plugin][vite-plugin-rsc].\n\n```shellscript\nnpx create-react-router@latest --template remix-run/react-router-templates/unstable_rsc-framework-mode\n```\n\n### RSC Data Mode Templates\n\nThe [Vite RSC Data Mode template][vite-rsc-template] uses the experimental Vite `@vitejs/plugin-rsc` plugin.\n\n```shellscript\nnpx create-react-router@latest --template remix-run/react-router-templates/unstable_rsc-data-mode-vite\n```\n\n## RSC Framework Mode\n\nMost APIs and features in RSC Framework Mode are the same as non-RSC Framework Mode, so this guide will focus on the differences.\n\n### New React Router RSC Vite Plugin\n\nRSC Framework Mode uses a different Vite plugin than non-RSC Framework Mode, currently exported as `unstable_reactRouterRSC`.\n\nThis new Vite plugin also has a peer dependency on the experimental `@vitejs/plugin-rsc` plugin. Note that the `@vitejs/plugin-rsc` plugin should be placed after the React Router RSC plugin in your Vite config.\n\n```tsx filename=vite.config.ts\nimport { defineConfig } from \"vite\";\nimport { unstable_reactRouterRSC as reactRouterRSC } from \"@react-router/dev/vite\";\nimport rsc from \"@vitejs/plugin-rsc\";\n\nexport default defineConfig({\n  plugins: [reactRouterRSC(), rsc()],\n});\n```\n\n### Build Output\n\nThe RSC Framework Mode server build file (`build/server/index.js`) now exports a `default` request handler function (`(request: Request) => Promise<Response>`) for document/data requests.\n\nIf needed, you can convert this into a [standard Node.js request listener][node-request-listener] for use with Node's built-in `http.createServer` function (or anything that supports it, e.g. [Express][express]) by using the `createRequestListener` function from [@remix-run/node-fetch-server][node-fetch-server].\n\nFor example, in Express:\n\n```tsx filename=start.js\nimport express from \"express\";\nimport requestHandler from \"./build/server/index.js\";\nimport { createRequestListener } from \"@remix-run/node-fetch-server\";\n\nconst app = express();\n\napp.use(\n  \"/assets\",\n  express.static(\"build/client/assets\", {\n    immutable: true,\n    maxAge: \"1y\",\n  }),\n);\napp.use(express.static(\"build/client\"));\napp.use(createRequestListener(requestHandler));\napp.listen(3000);\n```\n\n### React Elements From Loaders/Actions\n\nIn RSC Framework Mode, loaders and actions can now return React elements along with other data. These elements will only ever be rendered on the server.\n\n```tsx\nimport type { Route } from \"./+types/route\";\n\nexport async function loader() {\n  return {\n    message: \"Message from the server!\",\n    element: <p>Element from the server!</p>,\n  };\n}\n\nexport default function Route({\n  loaderData,\n}: Route.ComponentProps) {\n  return (\n    <>\n      <h1>{loaderData.message}</h1>\n      {loaderData.element}\n    </>\n  );\n}\n```\n\nIf you need to use client-only features (e.g. [Hooks][hooks], event handlers) within React elements returned from loaders/actions, you'll need to extract components using these features into a [client module][use-client-docs]:\n\n```tsx filename=src/routes/counter/counter.tsx\n\"use client\";\n\nexport function Counter() {\n  const [count, setCount] = useState(0);\n  return (\n    <button onClick={() => setCount(count + 1)}>\n      Count: {count}\n    </button>\n  );\n}\n```\n\n```tsx filename=src/routes/counter/route.tsx\nimport type { Route } from \"./+types/route\";\nimport { Counter } from \"./counter\";\n\nexport async function loader() {\n  return {\n    message: \"Message from the server!\",\n    element: (\n      <>\n        <p>Element from the server!</p>\n        <Counter />\n      </>\n    ),\n  };\n}\n\nexport default function Route({\n  loaderData,\n}: Route.ComponentProps) {\n  return (\n    <>\n      <h1>{loaderData.message}</h1>\n      {loaderData.element}\n    </>\n  );\n}\n```\n\n### Server Component Routes\n\nIf a route exports a `ServerComponent` instead of the typical `default` component export, this component along with other route components (`ErrorBoundary`, `HydrateFallback`, `Layout`) will be server components rather than the usual client components.\n\n```tsx\nimport type { Route } from \"./+types/route\";\nimport { Outlet } from \"react-router\";\nimport { getMessage } from \"./message\";\n\nexport async function loader() {\n  return {\n    message: await getMessage(),\n  };\n}\n\nexport function ServerComponent({\n  loaderData,\n}: Route.ComponentProps) {\n  return (\n    <>\n      <h1>Server Component Route</h1>\n      <p>Message from the server: {loaderData.message}</p>\n      <Outlet />\n    </>\n  );\n}\n```\n\nIf you need to use client-only features (e.g. [Hooks][hooks], event handlers) within a server-first route, you'll need to extract components using these features into a [client module][use-client-docs]:\n\n```tsx filename=src/routes/counter/counter.tsx\n\"use client\";\n\nexport function Counter() {\n  const [count, setCount] = useState(0);\n  return (\n    <button onClick={() => setCount(count + 1)}>\n      Count: {count}\n    </button>\n  );\n}\n```\n\n```tsx filename=src/routes/counter/route.tsx\nimport { Counter } from \"./counter\";\n\nexport function ServerComponent() {\n  return (\n    <>\n      <h1>Counter</h1>\n      <Counter />\n    </>\n  );\n}\n```\n\n### `.server`/`.client` Modules\n\nTo avoid confusion with RSC's `\"use server\"` and `\"use client\"` directives, support for [`.server` modules][server-modules] and [`.client` modules][client-modules] is no longer built-in when using RSC Framework Mode.\n\nAs an alternative solution that doesn't rely on file naming conventions, we recommend using the `\"server-only\"` and `\"client-only\"` imports provided by [`@vitejs/plugin-rsc`][vite-plugin-rsc]. For example, to ensure a module is never accidentally included in the client build, simply import from `\"server-only\"` as a side effect within your server-only module.\n\n```ts filename=app/utils/db.ts\nimport \"server-only\";\n\n// Rest of the module...\n```\n\nNote that while there are official npm packages [`server-only`][server-only-package] and [`client-only`][client-only-package] created by the React team, they don't need to be installed. `@vitejs/plugin-rsc` internally handles these imports and provides build-time validation instead of runtime errors.\n\nIf you'd like to quickly migrate existing code that relies on the `.server` and `.client` file naming conventions, we recommend using the [`vite-env-only` plugin][vite-env-only] directly. For example, to ensure `.server` modules aren't accidentally included in the client build:\n\n```tsx filename=vite.config.ts\nimport { defineConfig } from \"vite\";\nimport { denyImports } from \"vite-env-only\";\nimport { unstable_reactRouterRSC as reactRouterRSC } from \"@react-router/dev/vite\";\nimport rsc from \"@vitejs/plugin-rsc\";\n\nexport default defineConfig({\n  plugins: [\n    denyImports({\n      client: { files: [\"**/.server/*\", \"**/*.server.*\"] },\n    }),\n    reactRouterRSC(),\n    rsc(),\n  ],\n});\n```\n\n### MDX Route Support\n\nMDX routes are supported in RSC Framework Mode when using `@mdx-js/rollup` v3.1.1+.\n\nNote that any components exported from an MDX route must also be valid in RSC environments, meaning that they cannot use client-only features like [Hooks][hooks]. Any components that need to use these features should be extracted into a [client module][use-client-docs].\n\n### Custom Entry Files\n\nRSC Framework Mode supports custom entry files, allowing you to customize the behavior of the RSC server, SSR server, and client entry points.\n\nThe plugin will automatically detect custom entry files in your `app` directory:\n\n- `app/entry.rsc.ts` (or `.tsx`) - Custom RSC server entry\n- `app/entry.ssr.ts` (or `.tsx`) - Custom SSR server entry\n- `app/entry.client.tsx` - Custom client entry\n\nIf these files are not found, React Router will use the default entries provided by the framework.\n\n#### Basic Override Pattern\n\nYou can create a custom entry file that wraps or extends the default behavior. For example, to add custom logging to the RSC entry:\n\n```ts filename=app/entry.rsc.ts\nimport defaultEntry from \"@react-router/dev/config/default-rsc-entries/entry.rsc\";\nimport { RouterContextProvider } from \"react-router\";\n\nexport default {\n  fetch(request: Request): Promise<Response> {\n    console.log(\n      \"Custom RSC entry handling request:\",\n      request.url,\n    );\n\n    const requestContext = new RouterContextProvider();\n\n    return defaultEntry.fetch(request, requestContext);\n  },\n};\n\nif (import.meta.hot) {\n  import.meta.hot.accept();\n}\n```\n\nSimilarly, you can customize the SSR entry:\n\n```ts filename=app/entry.ssr.ts\nimport { generateHTML as defaultGenerateHTML } from \"@react-router/dev/config/default-rsc-entries/entry.ssr\";\n\nexport function generateHTML(\n  request: Request,\n  serverResponse: Response,\n): Promise<Response> {\n  console.log(\n    \"Custom SSR entry generating HTML for:\",\n    request.url,\n  );\n\n  return defaultGenerateHTML(request, serverResponse);\n}\n```\n\nAnd for the client:\n\n```ts filename=app/entry.client.ts\nimport \"@react-router/dev/config/default-rsc-entries/entry.client\";\n```\n\n#### Copying Default Entries\n\nFor more advanced customization, you can copy the default entries and modify them as needed. To find the default entries:\n\n1. In your IDE, use \"Go to Definition\" (or Cmd/Ctrl+Click) on the default entry import:\n\n   ```ts\n   import defaultEntry from \"@react-router/dev/config/default-rsc-entries/entry.rsc\";\n   ```\n\n2. Copy the default entry code into your custom file\n\n3. Modify it to suit your needs\n\nThe default entries are located at:\n\n- [`@react-router/dev/config/default-rsc-entries/entry.rsc`][entry-rsc-source]\n- [`@react-router/dev/config/default-rsc-entries/entry.ssr`][entry-ssr-source]\n- [`@react-router/dev/config/default-rsc-entries/entry.client`][entry-client-source]\n\nYou can view the source code on GitHub using the links above, or navigate directly to these files in `node_modules/@react-router/dev/dist/config/default-rsc-entries/`.\n\n<docs-info>\n\nWhen copying default entries, make sure to maintain the required exports:\n\n- `entry.rsc.ts` must export a default object with a `fetch` method\n- `entry.ssr.ts` must export a `generateHTML` function\n- `entry.client.tsx` should handle client-side hydration\n\n</docs-info>\n\n### Unsupported Config Options\n\nFor the initial unstable release, the following options from `react-router.config.ts` are not yet supported in RSC Framework Mode:\n\n- `buildEnd`\n- `prerender`\n- `presets`\n- `routeDiscovery`\n- `serverBundles`\n- `ssr: false` (SPA Mode)\n- `future.v8_splitRouteModules`\n- `future.unstable_subResourceIntegrity`\n\n## RSC Data Mode\n\nThe RSC Framework Mode APIs described above are built on top of lower-level RSC Data Mode APIs.\n\nRSC Data Mode is missing some of the features of RSC Framework Mode (e.g. `routes.ts` config and file system routing, HMR and Hot Data Revalidation), but is more flexible and allows you to integrate with your own bundler and server abstractions.\n\n### Configuring Routes\n\nRoutes are configured as an argument to [`matchRSCServerRequest`][match-rsc-server-request]. At a minimum, you need a path and component:\n\n```tsx\nfunction Root() {\n  return <h1>Hello world</h1>;\n}\n\nmatchRSCServerRequest({\n  // ...other options\n  routes: [{ path: \"/\", Component: Root }],\n});\n```\n\nWhile you can define components inline, we recommend using the `lazy()` option and defining [Route Modules][route-module] for both startup performance and code organization\n\n<docs-info>\n\nThe [Route Module API][route-module] up until now has been a [Framework Mode][framework-mode] only feature. However, the `lazy` field of the RSC route config expects the same exports as the Route Module exports, unifying the APIs even further.\n\n</docs-info>\n\n```tsx filename=app/routes.ts\nimport type { unstable_RSCRouteConfig as RSCRouteConfig } from \"react-router\";\n\nexport function routes() {\n  return [\n    {\n      id: \"root\",\n      path: \"\",\n      lazy: () => import(\"./root/route\"),\n      children: [\n        {\n          id: \"home\",\n          index: true,\n          lazy: () => import(\"./home/route\"),\n        },\n        {\n          id: \"about\",\n          path: \"about\",\n          lazy: () => import(\"./about/route\"),\n        },\n      ],\n    },\n  ] satisfies RSCRouteConfig;\n}\n```\n\n### Server Component Routes\n\nBy default each route's `default` export renders a Server Component\n\n```tsx\nexport default function Home() {\n  return (\n    <main>\n      <article>\n        <h1>Welcome to React Router RSC</h1>\n        <p>\n          You won't find me running any JavaScript in the\n          browser!\n        </p>\n      </article>\n    </main>\n  );\n}\n```\n\nA nice feature of Server Components is that you can fetch data directly from your component by making it asynchronous.\n\n```tsx\nexport default async function Home() {\n  let user = await getUserData();\n\n  return (\n    <main>\n      <article>\n        <h1>Welcome to React Router RSC</h1>\n        <p>\n          You won't find me running any JavaScript in the\n          browser!\n        </p>\n        <p>\n          Hello, {user ? user.name : \"anonymous person\"}!\n        </p>\n      </article>\n    </main>\n  );\n}\n```\n\n<docs-info>\n\nServer Components can also be returned from your loaders and actions. In general, if you are using RSC to build your application, loaders are primarily useful for things like setting `status` codes or returning a `redirect`.\n\nUsing Server Components in loaders can be helpful for incremental adoption of RSC.\n\n</docs-info>\n\n### Server Functions\n\n[Server Functions][react-server-functions-doc] are a React feature that allow you to call async functions executed on the server. They're defined with the [`\"use server\"`][use-server-docs] directive.\n\n```tsx\n\"use server\";\n\nexport async function updateFavorite(formData: FormData) {\n  let movieId = formData.get(\"id\");\n  let intent = formData.get(\"intent\");\n  if (intent === \"add\") {\n    await addFavorite(Number(movieId));\n  } else {\n    await removeFavorite(Number(movieId));\n  }\n}\n```\n\n```tsx\nimport { updateFavorite } from \"./action.ts\";\nexport async function AddToFavoritesForm({\n  movieId,\n}: {\n  movieId: number;\n}) {\n  let isFav = await isFavorite(movieId);\n  return (\n    <form action={updateFavorite}>\n      <input type=\"hidden\" name=\"id\" value={movieId} />\n      <input\n        type=\"hidden\"\n        name=\"intent\"\n        value={isFav ? \"remove\" : \"add\"}\n      />\n      <AddToFavoritesButton isFav={isFav} />\n    </form>\n  );\n}\n```\n\nNote that after server functions are called, React Router will automatically revalidate the route and update the UI with the new server content. You don't have to mess around with any cache invalidation.\n\n### Client Properties\n\nRoutes are defined on the server at runtime, but we can still provide `clientLoader`, `clientAction`, and `shouldRevalidate` through the utilization of client references and `\"use client\"`.\n\n```tsx filename=src/routes/root/client.tsx\n\"use client\";\n\nexport function clientAction() {}\n\nexport function clientLoader() {}\n\nexport function shouldRevalidate() {}\n```\n\nWe can then re-export these from our lazy loaded route module:\n\n```tsx filename=src/routes/root/route.tsx\nexport {\n  clientAction,\n  clientLoader,\n  shouldRevalidate,\n} from \"./route.client\";\n\nexport default function Root() {\n  // ...\n}\n```\n\nThis is also the way we would make an entire route a Client Component.\n\n```tsx filename=src/routes/root/route.tsx lines=[1,11]\nimport { default as ClientRoot } from \"./route.client\";\nexport {\n  clientAction,\n  clientLoader,\n  shouldRevalidate,\n} from \"./route.client\";\n\nexport default function Root() {\n  // Adding a Server Component at the root is required by bundlers\n  // if you're using css side-effects imports.\n  return <ClientRoot />;\n}\n```\n\n### Bundler Configuration\n\nReact Router provides several APIs that allow you to easily integrate with RSC-compatible bundlers, useful if you are using React Router Data Mode to make your own [custom framework][custom-framework].\n\nThe following steps show how to setup a React Router application to use Server Components (RSC) to server-render (SSR) pages and hydrate them for single-page app (SPA) navigations. You don't have to use SSR (or even client-side hydration) if you don't want to. You can also leverage the HTML generation for Static Site Generation (SSG) or Incremental Static Regeneration (ISR) if you prefer. This guide is meant merely to explain how to wire up all the different APIs for a typically RSC-based application.\n\n### Entry points\n\nBesides our [route definitions](#configuring-routes), we will need to configure the following:\n\n1. A server to handle the incoming request, fetch the RSC payload, and convert it into HTML\n2. A React server to generate RSC payloads\n3. A browser handler to hydrate the generated HTML and set the `callServer` function to support post-hydration server actions\n\nThe following naming conventions have been chosen for familiarity and simplicity. Feel free to name and configure your entry points as you see fit.\n\nSee the relevant bundler documentation below for specific code examples for each of the following entry points.\n\nThese examples all use [express][express] and [@remix-run/node-fetch-server][node-fetch-server] for the server and request handling.\n\n**Routes**\n\nSee [Configuring Routes](#configuring-routes).\n\n**Server**\n\n<docs-info>\n\nYou don't have to use SSR at all. You can choose to use RSC to \"prerender\" HTML for Static Site Generation (SSG) or something like Incremental Static Regeneration (ISR).\n\n</docs-info>\n\n`entry.ssr.tsx` is the entry point for the server. It is responsible for handling the request, calling the RSC server, and converting the RSC payload into HTML on document requests (server-side rendering).\n\nRelevant APIs:\n\n- [`routeRSCServerRequest`][route-rsc-server-request]\n- [`RSCStaticRouter`][rsc-static-router]\n\n**RSC Server**\n\n<docs-info>\n\nEven though you have a \"React Server\" and a server responsible for request handling/SSR, you don't actually need to have 2 separate servers. You can simply have 2 separate module graphs within the same server. This is important because React behaves differently when generating RSC payloads vs. when generating HTML to be hydrated on the client.\n\n</docs-info>\n\n`entry.rsc.tsx` is the entry point for the React Server. It is responsible for matching the request to a route and generating RSC payloads.\n\nRelevant APIs:\n\n- [`matchRSCServerRequest`][match-rsc-server-request]\n\n**Browser**\n\n`entry.browser.tsx` is the entry point for the client. It is responsible for hydrating the generated HTML and setting the `callServer` function to support post-hydration server actions.\n\nRelevant APIs:\n\n- [`createCallServer`][create-call-server]\n- [`getRSCStream`][get-rsc-stream]\n- [`RSCHydratedRouter`][rsc-hydrated-router]\n\n### Vite\n\nSee the [@vitejs/plugin-rsc docs][vite-plugin-rsc] for more information. You can also refer to our [Vite RSC Data Mode template][vite-rsc-template] to see a working version.\n\nIn addition to `react`, `react-dom`, and `react-router`, you'll need the following dependencies:\n\n```shellscript\nnpm i -D vite @vitejs/plugin-react @vitejs/plugin-rsc\n```\n\n#### `vite.config.ts`\n\nTo configure Vite, add the following to your `vite.config.ts`:\n\n```ts filename=vite.config.ts\nimport rsc from \"@vitejs/plugin-rsc/plugin\";\nimport react from \"@vitejs/plugin-react\";\nimport { defineConfig } from \"vite\";\n\nexport default defineConfig({\n  plugins: [\n    react(),\n    rsc({\n      entries: {\n        client: \"src/entry.browser.tsx\",\n        rsc: \"src/entry.rsc.tsx\",\n        ssr: \"src/entry.ssr.tsx\",\n      },\n    }),\n  ],\n});\n```\n\n```tsx filename=src/routes/config.ts\nimport type { unstable_RSCRouteConfig as RSCRouteConfig } from \"react-router\";\n\nexport function routes() {\n  return [\n    {\n      id: \"root\",\n      path: \"\",\n      lazy: () => import(\"./root/route\"),\n      children: [\n        {\n          id: \"home\",\n          index: true,\n          lazy: () => import(\"./home/route\"),\n        },\n        {\n          id: \"about\",\n          path: \"about\",\n          lazy: () => import(\"./about/route\"),\n        },\n      ],\n    },\n  ] satisfies RSCRouteConfig;\n}\n```\n\n#### `entry.ssr.tsx`\n\nThe following is a simplified example of a Vite SSR Server.\n\n```tsx filename=src/entry.ssr.tsx\nimport { createFromReadableStream } from \"@vitejs/plugin-rsc/ssr\";\nimport { renderToReadableStream as renderHTMLToReadableStream } from \"react-dom/server.edge\";\nimport {\n  unstable_routeRSCServerRequest as routeRSCServerRequest,\n  unstable_RSCStaticRouter as RSCStaticRouter,\n} from \"react-router\";\n\nexport async function generateHTML(\n  request: Request,\n  serverResponse: Response,\n): Promise<Response> {\n  return await routeRSCServerRequest({\n    // The incoming request.\n    request,\n    // The React Server response\n    serverResponse,\n    // Provide the React Server touchpoints.\n    createFromReadableStream,\n    // Render the router to HTML.\n    async renderHTML(getPayload) {\n      const payload = getPayload();\n\n      const bootstrapScriptContent =\n        await import.meta.viteRsc.loadBootstrapScriptContent(\n          \"index\",\n        );\n\n      return await renderHTMLToReadableStream(\n        <RSCStaticRouter getPayload={getPayload} />,\n        {\n          bootstrapScriptContent,\n          formState: payload.formState,\n        },\n      );\n    },\n  });\n}\n```\n\n#### `entry.rsc.tsx`\n\nThe following is a simplified example of a Vite RSC Server.\n\n```tsx filename=src/entry.rsc.tsx\nimport {\n  createTemporaryReferenceSet,\n  decodeAction,\n  decodeFormState,\n  decodeReply,\n  loadServerAction,\n  renderToReadableStream,\n} from \"@vitejs/plugin-rsc/rsc\";\nimport { unstable_matchRSCServerRequest as matchRSCServerRequest } from \"react-router\";\n\nimport { routes } from \"./routes/config\";\n\nfunction fetchServer(request: Request) {\n  return matchRSCServerRequest({\n    // Provide the React Server touchpoints.\n    createTemporaryReferenceSet,\n    decodeAction,\n    decodeFormState,\n    decodeReply,\n    loadServerAction,\n    // The incoming request.\n    request,\n    // The app routes.\n    routes: routes(),\n    // Encode the match with the React Server implementation.\n    generateResponse(match) {\n      return new Response(\n        renderToReadableStream(match.payload),\n        {\n          status: match.statusCode,\n          headers: match.headers,\n        },\n      );\n    },\n  });\n}\n\nexport default async function handler(request: Request) {\n  // Import the generateHTML function from the client environment\n  const ssr = await import.meta.viteRsc.loadModule<\n    typeof import(\"./entry.ssr\")\n  >(\"ssr\", \"index\");\n\n  return ssr.generateHTML(\n    request,\n    await fetchServer(request),\n  );\n}\n```\n\n#### `entry.browser.tsx`\n\n```tsx filename=src/entry.browser.tsx\nimport {\n  createFromReadableStream,\n  createTemporaryReferenceSet,\n  encodeReply,\n  setServerCallback,\n} from \"@vitejs/plugin-rsc/browser\";\nimport { startTransition, StrictMode } from \"react\";\nimport { hydrateRoot } from \"react-dom/client\";\nimport {\n  unstable_createCallServer as createCallServer,\n  unstable_getRSCStream as getRSCStream,\n  unstable_RSCHydratedRouter as RSCHydratedRouter,\n  type unstable_RSCPayload as RSCServerPayload,\n} from \"react-router\";\n\n// Create and set the callServer function to support post-hydration server actions.\nsetServerCallback(\n  createCallServer({\n    createFromReadableStream,\n    createTemporaryReferenceSet,\n    encodeReply,\n  }),\n);\n\n// Get and decode the initial server payload.\ncreateFromReadableStream<RSCServerPayload>(\n  getRSCStream(),\n).then((payload) => {\n  startTransition(async () => {\n    const formState =\n      payload.type === \"render\"\n        ? await payload.formState\n        : undefined;\n\n    hydrateRoot(\n      document,\n      <StrictMode>\n        <RSCHydratedRouter\n          createFromReadableStream={\n            createFromReadableStream\n          }\n          payload={payload}\n        />\n      </StrictMode>,\n      {\n        formState,\n      },\n    );\n  });\n});\n```\n\n[picking-a-mode]: ../start/modes\n[react-server-components-doc]: https://react.dev/reference/rsc/server-components\n[react-server-functions-doc]: https://react.dev/reference/rsc/server-functions\n[use-client-docs]: https://react.dev/reference/rsc/use-client\n[use-server-docs]: https://react.dev/reference/rsc/use-server\n[route-module]: ../start/framework/route-module\n[framework-mode]: ../start/modes#framework\n[custom-framework]: ../start/data/custom\n[vite-plugin-rsc]: https://github.com/vitejs/vite-plugin-react/tree/main/packages/plugin-rsc\n[match-rsc-server-request]: ../api/rsc/matchRSCServerRequest\n[route-rsc-server-request]: ../api/rsc/routeRSCServerRequest\n[rsc-static-router]: ../api/rsc/RSCStaticRouter\n[create-call-server]: ../api/rsc/createCallServer\n[get-rsc-stream]: ../api/rsc/getRSCStream\n[rsc-hydrated-router]: ../api/rsc/RSCHydratedRouter\n[express]: https://expressjs.com/\n[node-fetch-server]: https://www.npmjs.com/package/@remix-run/node-fetch-server\n[framework-rsc-template]: https://github.com/remix-run/react-router-templates/tree/main/unstable_rsc-framework-mode\n[vite-rsc-template]: https://github.com/remix-run/react-router-templates/tree/main/unstable_rsc-data-mode-vite\n[node-request-listener]: https://nodejs.org/api/http.html#httpcreateserveroptions-requestlistener\n[hooks]: https://react.dev/reference/react/hooks\n[vite-env-only]: https://github.com/pcattori/vite-env-only\n[server-modules]: ../api/framework-conventions/server-modules\n[client-modules]: ../api/framework-conventions/client-modules\n[server-only-package]: https://www.npmjs.com/package/server-only\n[client-only-package]: https://www.npmjs.com/package/client-only\n[entry-rsc-source]: https://github.com/remix-run/react-router/blob/main/packages/react-router-dev/config/default-rsc-entries/entry.rsc.tsx\n[entry-ssr-source]: https://github.com/remix-run/react-router/blob/main/packages/react-router-dev/config/default-rsc-entries/entry.ssr.tsx\n[entry-client-source]: https://github.com/remix-run/react-router/blob/main/packages/react-router-dev/config/default-rsc-entries/entry.client.tsx\n"
  },
  {
    "path": "docs/how-to/resource-routes.md",
    "content": "---\ntitle: Resource Routes\n---\n\n# Resource Routes\n\n[MODES: framework, data]\n\n<br/>\n<br/>\n\nWhen server rendering, routes can serve \"resources\" instead of rendering components, like images, PDFs, JSON payloads, webhooks, etc.\n\n## Defining a Resource Route\n\nA route becomes a resource route by convention when its module exports a loader or action but does not export a default component.\n\nConsider a route that serves a PDF instead of UI:\n\n```ts\nroute(\"/reports/pdf/:id\", \"pdf-report.ts\");\n```\n\n```tsx filename=pdf-report.ts\nimport type { Route } from \"./+types/pdf-report\";\n\nexport async function loader({ params }: Route.LoaderArgs) {\n  const report = await getReport(params.id);\n  const pdf = await generateReportPDF(report);\n  return new Response(pdf, {\n    status: 200,\n    headers: {\n      \"Content-Type\": \"application/pdf\",\n    },\n  });\n}\n```\n\nNote there is no default export. That makes this route a resource route.\n\n## Linking to Resource Routes\n\nWhen linking to resource routes, use `<a>` or `<Link reloadDocument>`, otherwise React Router will attempt to use client side routing and fetching the payload (you'll get a helpful error message if you make this mistake).\n\n```tsx\n<Link reloadDocument to=\"/reports/pdf/123\">\n  View as PDF\n</Link>\n```\n\n## Handling different request methods\n\nGET requests are handled by the `loader`, while POST, PUT, PATCH, and DELETE are handled by the `action`:\n\n```tsx\nimport type { Route } from \"./+types/resource\";\n\nexport function loader(_: Route.LoaderArgs) {\n  return Response.json({ message: \"I handle GET\" });\n}\n\nexport function action(_: Route.ActionArgs) {\n  return Response.json({\n    message: \"I handle everything else\",\n  });\n}\n```\n\n## Return Types\n\nResource Routes are flexible when it comes to the return type - you can return [`Response`][Response] instances or [`data()`][data] objects. A good general rule of thumb when deciding which type to use is:\n\n- If you're using resource routes intended for external consumption, return `Response` instances\n  - Keeps the resulting response encoding explicit in your code rather than having to wonder how React Router might convert `data() -> Response` under the hood\n- If you're accessing resource routes from [fetchers][fetcher] or [`<Form>`][form] submissions, return `data()`\n  - Keeps things consistent with the loaders/actions in your UI routes\n  - Allows you to stream promises down to your UI through `data()`/[`Await`][await]\n\n## Error Handling\n\nThrowing an `Error` from Resource route (or anything other than a `Response`/`data()`) will trigger [`handleError`][handleError] and result in a 500 HTTP Response:\n\n```tsx\nexport function action() {\n  let db = await getDb();\n  if (!db) {\n    // Fatal error - return a 500 response and trigger `handleError`\n    throw new Error(\"Could not connect to DB\");\n  }\n  // ...\n}\n```\n\nIf a resource route generates a `Response` (via `new Response()` or `data()`), it is considered a successful execution and will not trigger `handleError` because the API has successfully produced a Response for the HTTP request. This applies to thrown responses as well as returned responses with a 4xx/5xx status code. This behavior aligns with `fetch()` which does not return a rejected promise on 4xx/5xx Responses.\n\n```tsx\nexport function action() {\n  // Non-fatal error - don't trigger `handleError`:\n  throw new Response(\n    { error: \"Unauthorized\" },\n    { status: 401 },\n  );\n\n  // These 3 are equivalent to the above\n  return new Response(\n    { error: \"Unauthorized\" },\n    { status: 401 },\n  );\n\n  throw data({ error: \"Unauthorized\" }, { status: 401 });\n\n  return data({ error: \"Unauthorized\" }, { status: 401 });\n}\n```\n\n### Error Boundaries\n\n[Error Boundaries][error-boundary] are only applicable when a resource route is accessed from a UI, such as from a [`fetcher`][fetcher] call or a [`<Form>`][form] submission. If you `throw` from your resource route in these cases, it will bubble to the nearest `ErrorBoundary` in the UI.\n\n[handleError]: ../api/framework-conventions/entry.server.tsx#handleerror\n[data]: ../api/utils/data\n[Response]: https://developer.mozilla.org/en-US/docs/Web/API/Response\n[fetcher]: ../api/hooks/useFetcher\n[form]: ../api/components/Form\n[await]: ../api/components/Await\n[error-boundary]: ../start/framework/route-module#errorboundary\n"
  },
  {
    "path": "docs/how-to/route-module-type-safety.md",
    "content": "---\ntitle: Route Module Type Safety\n---\n\n# Route Module Type Safety\n\n[MODES: framework]\n\n<br/>\n<br/>\n\nReact Router generates route-specific types to power type inference for URL params, loader data, and more.\nThis guide will help you set it up if you didn't start with a template.\n\nTo learn more about how type safety works in React Router, check out [Type Safety Explanation](../explanation/type-safety).\n\n## 1. Add `.react-router/` to `.gitignore`\n\nReact Router generates types into a `.react-router/` directory at the root of your app. This directory is fully managed by React Router and should be gitignore'd.\n\n```txt\n.react-router/\n```\n\n## 2. Include the generated types in tsconfig\n\nEdit your tsconfig to get TypeScript to use the generated types. Additionally, `rootDirs` needs to be configured so the types can be imported as relative siblings to route modules.\n\n```json filename=tsconfig.json\n{\n  \"include\": [\".react-router/types/**/*\"],\n  \"compilerOptions\": {\n    \"rootDirs\": [\".\", \"./.react-router/types\"]\n  }\n}\n```\n\nIf you are using multiple `tsconfig` files for your app, you'll need to make these changes in whichever one `include`s your app directory.\nFor example, the [`node-custom-server` template](https://github.com/remix-run/react-router-templates/tree/390fcec476dd336c810280479688fe893da38713/node-custom-server) contains `tsconfig.json`, `tsconfig.node.json`, and `tsconfig.vite.json`. Since `tsconfig.vite.json` is the one that [includes the app directory](https://github.com/remix-run/react-router-templates/blob/390fcec476dd336c810280479688fe893da38713/node-custom-server/tsconfig.vite.json#L4-L6), that's the one that sets up `.react-router/types` for route module type safety.\n\n## 3. Generate types before type checking\n\nIf you want to run type checking as its own command — for example, as part of your Continuous Integration pipeline — you'll need to make sure to generate types _before_ running typechecking:\n\n```json\n{\n  \"scripts\": {\n    \"typecheck\": \"react-router typegen && tsc\"\n  }\n}\n```\n\n## 4. Typing `AppLoadContext`\n\n## Extending app `Context` types\n\nTo define your app's `context` type, add the following in a `.ts` or `.d.ts` file within your project:\n\n```typescript\nimport \"react-router\";\ndeclare module \"react-router\" {\n  interface AppLoadContext {\n    // add context properties here\n  }\n}\n```\n\n## 5. Type-only auto-imports (optional)\n\nWhen auto-importing the `Route` type helper, TypeScript will generate:\n\n```ts filename=app/routes/my-route.tsx\nimport { Route } from \"./+types/my-route\";\n```\n\nBut if you enable [verbatimModuleSyntax](https://www.typescriptlang.org/tsconfig/#verbatimModuleSyntax):\n\n```json filename=tsconfig.json\n{\n  \"compilerOptions\": {\n    \"verbatimModuleSyntax\": true\n  }\n}\n```\n\nThen, you will get the `type` modifier for the import automatically as well:\n\n```ts filename=app/routes/my-route.tsx\nimport type { Route } from \"./+types/my-route\";\n//     ^^^^\n```\n\nThis helps tools like bundlers to detect type-only module that can be safely excluded from the bundle.\n\n## Conclusion\n\nReact Router's Vite plugin should be automatically generating types into `.react-router/types/` anytime you edit your route config (`routes.ts`).\nThat means all you need to do is run `react-router dev` (or your custom dev server) to get to up-to-date types in your routes.\n\nCheck out our [Type Safety Explanation](../explanation/type-safety) for an example of how to pull in those types into your routes.\n"
  },
  {
    "path": "docs/how-to/search-params.md",
    "content": "---\ntitle: Using Search Params\nhidden: true\n---\n"
  },
  {
    "path": "docs/how-to/security.md",
    "content": "---\ntitle: Security\n---\n\n# Security\n\n[MODES: framework]\n\n<br/>\n<br/>\n\nThis is by no means a comprehensive guide, but React Router provides features to help address a few aspects under the _very large_ umbrella that is _Security_.\n\n## `Content-Security-Policy`\n\nIf you are implementing a [Content-Security-Policy (CSP)][csp] in your application, specifically one using the `unsafe-inline` directive, you will need to specify a [`nonce`][nonce] attribute on the inline `<script>` elements rendered in your HTML. This must be specified on any API that generates inline scripts, including:\n\n- [`<Scripts nonce>`][scripts] (`root.tsx`)\n- [`<ScrollRestoration nonce>`][scrollrestoration] (`root.tsx`)\n- [`<ServerRouter nonce>`][serverrouter] (`entry.server.tsx`)\n- [`renderToPipeableStream(..., { nonce })`][renderToPipeableStream] (`entry.server.tsx`)\n- [`renderToReadableStream(..., { nonce })`][renderToReadableStream] (`entry.server.tsx`)\n\n[csp]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/CSP\n[nonce]: https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/nonce\n[renderToPipeableStream]: https://react.dev/reference/react-dom/server/renderToPipeableStream\n[renderToReadableStream]: https://react.dev/reference/react-dom/server/renderToReadableStream\n[scripts]: ../api/components/Scripts\n[scrollrestoration]: ../api/components/ScrollRestoration\n[serverrouter]: ../api/components/ServerRouter\n"
  },
  {
    "path": "docs/how-to/server-bundles.md",
    "content": "---\ntitle: Server Bundles\n---\n\n# Server Bundles\n\n[MODES: framework]\n\n<br/>\n<br/>\n\n<docs-warning>This is an advanced feature designed for hosting provider integrations. When compiling your app into multiple server bundles, there will need to be a custom routing layer in front of your app directing requests to the correct bundle.</docs-warning>\n\nReact Router typically builds your server code into a single bundle that exports a request handler function. However, there are scenarios where you might want to split your route tree into multiple server bundles, each exposing a request handler function for a subset of routes. To provide this flexibility, [`react-router.config.ts`][react-router-config] supports a `serverBundles` option, which is a function for assigning routes to different server bundles.\n\nThe [`serverBundles` function][server-bundles-function] is called for each route in the tree (except for routes that aren't addressable, e.g., pathless layout routes) and returns a server bundle ID that you'd like to assign that route to. These bundle IDs will be used as directory names in your server build directory.\n\nFor each route, this function receives an array of routes leading to and including that route, referred to as the route `branch`. This allows you to create server bundles for different portions of the route tree. For example, you could use this to create a separate server bundle containing all routes within a particular layout route:\n\n```ts filename=react-router.config.ts lines=[5-13]\nimport type { Config } from \"@react-router/dev/config\";\n\nexport default {\n  // ...\n  serverBundles: ({ branch }) => {\n    const isAuthenticatedRoute = branch.some((route) =>\n      route.id.split(\"/\").includes(\"_authenticated\"),\n    );\n\n    return isAuthenticatedRoute\n      ? \"authenticated\"\n      : \"unauthenticated\";\n  },\n} satisfies Config;\n```\n\nEach `route` in the `branch` array contains the following properties:\n\n- `id` — The unique ID for this route, named like its `file` but relative to the app directory and without the extension, e.g., `app/routes/gists.$username.tsx` will have an `id` of `routes/gists.$username`\n- `path` — The path this route uses to match the URL pathname\n- `file` — The absolute path to the entry point for this route\n- `index` — Whether this route is an index route\n\n## Build manifest\n\nWhen the build is complete, React Router will call the `buildEnd` hook, passing a `buildManifest` object. This is useful if you need to inspect the build manifest to determine how to route requests to the correct server bundle.\n\n```ts filename=react-router.config.ts lines=[5-7]\nimport type { Config } from \"@react-router/dev/config\";\n\nexport default {\n  // ...\n  buildEnd: async ({ buildManifest }) => {\n    // ...\n  },\n} satisfies Config;\n```\n\nWhen using server bundles, the build manifest contains the following properties:\n\n- `serverBundles` — An object that maps bundle IDs to the bundle's `id` and `file`\n- `routeIdToServerBundleId` — An object that maps route IDs to their server bundle ID\n- `routes` — A route manifest that maps route IDs to route metadata. This can be used to drive a custom routing layer in front of your React Router request handlers\n\n[react-router-config]: https://api.reactrouter.com/v7/types/_react-router_dev.config.Config.html\n[server-bundles-function]: https://api.reactrouter.com/v7/types/_react-router_dev.config.ServerBundlesFunction.html\n"
  },
  {
    "path": "docs/how-to/spa.md",
    "content": "---\ntitle: Single Page App (SPA)\n---\n\n# Single Page App (SPA)\n\n[MODES: framework]\n\n<br/>\n<br/>\n\n<docs-info>This guide focuses on how to build Single Page Apps with React Router Framework mode. If you're using React Router in declarative or data mode, you can design your own SPA architecture.</docs-info>\n\nWhen using React Router as a framework, you can enable \"SPA Mode\" by setting `ssr:false` in your `react-router.config.ts` file. This will disable runtime server rendering and generate an `index.html` at build time that you can serve and hydrate as a SPA.\n\nTypical Single Page apps send a mostly blank `index.html` template with little more than an empty `<div id=\"root\"></div>`. In contrast, `react-router build` (in SPA Mode) pre-renders your root route at build time into an `index.html` file. This means you can:\n\n- Send more than an empty `<div>`\n- Use a root `loader` to load data for your application shell\n- Use React components to generate the initial page users see (root `HydrateFallback`)\n- Re-enable server rendering later without changing anything about your UI\n\n<docs-info>SPA Mode is a special form of \"Pre-Rendering\" that allows you to serve all paths in your application from the same HTML file. Please refer to the [Pre-Rendering](./pre-rendering) guide if you want to do more extensive pre-rendering.</docs-info>\n\n## 1. Disable Runtime Server Rendering\n\nServer rendering is enabled by default. Set the `ssr` flag to `false` in `react-router.config.ts` to disable it.\n\n```ts filename=react-router.config.ts lines=[4]\nimport { type Config } from \"@react-router/dev/config\";\n\nexport default {\n  ssr: false,\n} satisfies Config;\n```\n\nWith this set to false, the server build will no longer be generated.\n\n<docs-info>It's important to note that setting `ssr:false` only disables _runtime server rendering_. React Router will still server render your root route at _build time_ to generate the `index.html` file. This is why your project still needs a dependency on `@react-router/node` and your routes need to be SSR-safe. That means you can't call `window` or other browser-only APIs during the initial render, even when server rendering is disabled.</docs-info>\n\n## 2. Add a `HydrateFallback` and optional `loader` to your root route\n\nSPA Mode will generate an `index.html` file at build-time that you can serve as the entry point for your SPA. This will only render the root route so that it is capable of hydrating at runtime for any path in your application.\n\nTo provide a better loading UI than an empty `<div>`, you can add a `HydrateFallback` component to your root route to render your loading UI into the `index.html` at build time. This way, it will be shown to users immediately while the SPA is loading/hydrating.\n\n```tsx filename=root.tsx lines=[7-9]\nimport LoadingScreen from \"./components/loading-screen\";\n\nexport function Layout() {\n  return <html>{/*...*/}</html>;\n}\n\nexport function HydrateFallback() {\n  return <LoadingScreen />;\n}\n\nexport default function App() {\n  return <Outlet />;\n}\n```\n\nBecause the root route is server-rendered at build time, you can also use a `loader` in your root route if you choose. This `loader` will be called at build time and the data will be available via the optional `HydrateFallback` `loaderData` prop.\n\n```tsx filename=root.tsx lines=[5,10,14]\nimport { Route } from \"./+types/root\";\n\nexport async function loader() {\n  return {\n    version: await getVersion(),\n  };\n}\n\nexport function HydrateFallback({\n  loaderData,\n}: Route.ComponentProps) {\n  return (\n    <div>\n      <h1>Loading version {loaderData.version}...</h1>\n      <AwesomeSpinner />\n    </div>\n  );\n}\n```\n\nYou cannot include a `loader` in any other routes in your app when using SPA Mode unless you are [pre-rendering those pages](./pre-rendering).\n\n## 3. Use client loaders and client actions\n\nWith server rendering disabled, you can still use `clientLoader` and `clientAction` to manage route data and mutations.\n\n```tsx filename=some-route.tsx\nimport { Route } from \"./+types/some-route\";\n\nexport async function clientLoader({\n  params,\n}: Route.ClientLoaderArgs) {\n  let data = await fetch(`/some/api/stuff/${params.id}`);\n  return data;\n}\n\nexport async function clientAction({\n  request,\n}: Route.ClientActionArgs) {\n  let formData = await request.formData();\n  return await processPayment(formData);\n}\n```\n\n## 4. Direct all URLs to index.html\n\nAfter running `react-router build`, deploy the `build/client` directory to whatever static host you prefer.\n\nCommon to deploying any SPA, you'll need to configure your host to direct all URLs to the `index.html` of the client build. Some hosts do this by default, but others don't. As an example, a host may support a `_redirects` file to do this:\n\n```\n/*    /index.html   200\n```\n\nIf you're getting 404s at valid routes for your app, it's likely you need to configure your host.\n"
  },
  {
    "path": "docs/how-to/status.md",
    "content": "---\ntitle: Status Codes\n---\n\n# Status Codes\n\n[MODES: framework ,data]\n\n<br/>\n<br/>\n\nSet status codes from loaders and actions with `data`.\n\n```tsx filename=app/project.tsx lines=[3,12-15,20,23]\n// route('/projects/:projectId', './project.tsx')\nimport type { Route } from \"./+types/project\";\nimport { data } from \"react-router\";\nimport { fakeDb } from \"../db\";\n\nexport async function action({\n  request,\n}: Route.ActionArgs) {\n  let formData = await request.formData();\n  let title = formData.get(\"title\");\n  if (!title) {\n    return data(\n      { message: \"Invalid title\" },\n      { status: 400 },\n    );\n  }\n\n  if (!projectExists(title)) {\n    let project = await fakeDb.createProject({ title });\n    return data(project, { status: 201 });\n  } else {\n    let project = await fakeDb.updateProject({ title });\n    // the default status code is 200, no need for `data`\n    return project;\n  }\n}\n```\n\nSee [Form Validation](./form-validation) for more information on rendering form errors like this.\n\nAnother common status code is 404:\n\n```tsx\n// route('/projects/:projectId', './project.tsx')\nimport type { Route } from \"./+types/project\";\nimport { data } from \"react-router\";\nimport { fakeDb } from \"../db\";\n\nexport async function loader({ params }: Route.ActionArgs) {\n  let project = await fakeDb.getProject(params.id);\n  if (!project) {\n    // throw to ErrorBoundary\n    throw data(null, { status: 404 });\n  }\n  return project;\n}\n```\n\nSee the [Error Boundaries](./error-boundary) for more information on thrown `data`.\n"
  },
  {
    "path": "docs/how-to/suspense.md",
    "content": "---\ntitle: Streaming with Suspense\n---\n\n# Streaming with Suspense\n\n[MODES: framework, data]\n\n<br/>\n<br/>\n\nStreaming with React Suspense allows apps to speed up initial renders by deferring non-critical data and unblocking UI rendering.\n\nReact Router supports React Suspense by returning promises from loaders and actions.\n\n## 1. Return a promise from loader\n\nReact Router awaits route loaders before rendering route components. To unblock the loader for non-critical data, return the promise instead of awaiting it in the loader.\n\n```tsx\nimport type { Route } from \"./+types/my-route\";\n\nexport async function loader({}: Route.LoaderArgs) {\n  // note this is NOT awaited\n  let nonCriticalData = new Promise((res) =>\n    setTimeout(() => res(\"non-critical\"), 5000),\n  );\n\n  let criticalData = await new Promise((res) =>\n    setTimeout(() => res(\"critical\"), 300),\n  );\n\n  return { nonCriticalData, criticalData };\n}\n```\n\nNote you can't return a single promise, it must be an object with keys.\n\n## 2. Render the fallback and resolved UI\n\nThe promise will be available on `loaderData`, `<Await>` will await the promise and trigger `<Suspense>` to render the fallback UI.\n\n```tsx\nimport * as React from \"react\";\nimport { Await } from \"react-router\";\n\n// [previous code]\n\nexport default function MyComponent({\n  loaderData,\n}: Route.ComponentProps) {\n  let { criticalData, nonCriticalData } = loaderData;\n\n  return (\n    <div>\n      <h1>Streaming example</h1>\n      <h2>Critical data value: {criticalData}</h2>\n\n      <React.Suspense fallback={<div>Loading...</div>}>\n        <Await resolve={nonCriticalData}>\n          {(value) => <h3>Non critical value: {value}</h3>}\n        </Await>\n      </React.Suspense>\n    </div>\n  );\n}\n```\n\n## With React 19\n\nIf you're experimenting with React 19, you can use `React.use` instead of `Await`, but you'll need to create a new component and pass the promise down to trigger the suspense fallback.\n\n```tsx\n<React.Suspense fallback={<div>Loading...</div>}>\n  <NonCriticalUI p={nonCriticalData} />\n</React.Suspense>\n```\n\n```tsx\nfunction NonCriticalUI({ p }: { p: Promise<string> }) {\n  let value = React.use(p);\n  return <h3>Non critical value {value}</h3>;\n}\n```\n\n## Timeouts\n\nBy default, loaders and actions reject any outstanding promises after 4950ms. You can control this by exporting a `streamTimeout` numerical value from your `entry.server.tsx`.\n\n```ts filename=entry.server.tsx\n// Reject all pending promises from handler functions after 10 seconds\nexport const streamTimeout = 10_000;\n```\n"
  },
  {
    "path": "docs/how-to/using-handle.md",
    "content": "---\ntitle: Using handle\n---\n\n# Using `handle`\n\n[MODES: framework]\n\n<br/>\n<br/>\n\nYou can build dynamic UI elements like breadcrumbs based on your route hierarchy using the [`useMatches`][use-matches] hook and [`handle`][handle] route exports.\n\n## Understanding the Basics\n\nReact Router provides access to all route matches and their data throughout your component tree. This allows routes to contribute metadata through the `handle` export that can be rendered by ancestor components.\n\nThe `useMatches` hook combined with `handle` exports enables routes to contribute to rendering processes higher up the component tree than their actual render point. While we'll use breadcrumbs as an example, this pattern works for any scenario where you need routes to provide additional information to their ancestors.\n\n## Defining Route `handle`s\n\nWe'll use a route structure like the following:\n\n```ts filename=app/routes.ts\nimport { route } from \"@react-router/dev/routes\";\n\nexport default [\n  route(\"parent\", \"./routes/parent.tsx\", [\n    route(\"child\", \"./routes/child.tsx\"),\n  ]),\n] satisfies RouteConfig;\n```\n\nAdd a `breadcrumb` property to the \"parent\" route's `handle` export. You can name this property whatever makes sense for your use case.\n\n```tsx filename=app/routes/parent.tsx\nimport { Link } from \"react-router\";\n\nexport const handle = {\n  breadcrumb: () => <Link to=\"/parent\">Some Route</Link>,\n};\n```\n\nYou can define breadcrumbs for child routes as well:\n\n```tsx filename=app/routes/child.tsx\nimport { Link } from \"react-router\";\n\nexport const handle = {\n  breadcrumb: () => (\n    <Link to=\"/parent/child\">Child Route</Link>\n  ),\n};\n```\n\n## Using Route `handle`s\n\nUse the `useMatches` hook in your root layout or any ancestor component to collect and render the components defined in the `handle` export(s):\n\n```tsx filename=app/root.tsx lines=[7,11,22-31]\nimport {\n  Links,\n  Meta,\n  Outlet,\n  Scripts,\n  ScrollRestoration,\n  useMatches,\n} from \"react-router\";\n\nexport function Layout({ children }) {\n  const matches = useMatches();\n\n  return (\n    <html lang=\"en\">\n      <head>\n        <Meta />\n        <Links />\n      </head>\n      <body>\n        <header>\n          <ol>\n            {matches\n              .filter(\n                (match) =>\n                  match.handle && match.handle.breadcrumb,\n              )\n              .map((match, index) => (\n                <li key={index}>\n                  {match.handle.breadcrumb(match)}\n                </li>\n              ))}\n          </ol>\n        </header>\n        {children}\n        <ScrollRestoration />\n        <Scripts />\n      </body>\n    </html>\n  );\n}\n\nexport default function App() {\n  return <Outlet />;\n}\n```\n\nThe `match` object is passed to each breadcrumb function, giving you access to `match.data` (from loaders) and other route information to create dynamic breadcrumbs based on your route's data.\n\nThis pattern provides a clean way for routes to contribute metadata that can be consumed and rendered by ancestor components.\n\n## Additional Resources\n\n- [`useMatches`][use-matches]\n- [`handle`][handle]\n\n[use-matches]: ../api/hooks/useMatches\n[handle]: ../start/framework/route-module#handle\n"
  },
  {
    "path": "docs/how-to/view-transitions.md",
    "content": "---\ntitle: View Transitions\n---\n\n# View Transitions\n\n[MODES: framework, data]\n\n<br/>\n<br/>\n\nEnable smooth animations between page transitions in your React Router applications using the [View Transitions API][view-transitions-api]. This feature allows you to create seamless visual transitions during client-side navigation.\n\n## Basic View Transition\n\n### 1. Enable view transitions on navigation\n\nThe simplest way to enable view transitions is by adding the `viewTransition` prop to your `Link`, `NavLink`, or `Form` components. This automatically wraps the navigation update in `document.startViewTransition()`.\n\n```tsx\n<Link to=\"/about\" viewTransition>\n  About\n</Link>\n```\n\nWithout any additional CSS, this provides a basic cross-fade animation between pages.\n\n### 2. Enable view transitions with programmatic navigation\n\nWhen using programmatic navigation with the `useNavigate` hook, you can enable view transitions by passing the `viewTransition: true` option:\n\n```tsx\nimport { useNavigate } from \"react-router\";\n\nfunction NavigationButton() {\n  const navigate = useNavigate();\n\n  return (\n    <button\n      onClick={() =>\n        navigate(\"/about\", { viewTransition: true })\n      }\n    >\n      About\n    </button>\n  );\n}\n```\n\nThis provides the same cross-fade animation as using the `viewTransition` prop on Link components.\n\nFor more information on using the View Transitions API, please refer to the [\"Smooth transitions with the View Transition API\" guide][view-transitions-guide] from the Google Chrome team.\n\n## Image Gallery Example\n\nLet's build an image gallery that demonstrates how to trigger and use view transitions. We'll create a list of images that expand into a detail view with smooth animations.\n\n### 1. Create the image gallery route\n\n```tsx filename=routes/image-gallery.tsx\nimport { NavLink } from \"react-router\";\n\nexport const images = [\n  \"https://remix.run/blog-images/headers/the-future-is-now.jpg\",\n  \"https://remix.run/blog-images/headers/waterfall.jpg\",\n  \"https://remix.run/blog-images/headers/webpack.png\",\n  // ... more images ...\n];\n\nexport default function ImageGalleryRoute() {\n  return (\n    <div className=\"image-list\">\n      <h1>Image List</h1>\n      <div>\n        {images.map((src, idx) => (\n          <NavLink\n            key={src}\n            to={`/image/${idx}`}\n            viewTransition // Enable view transitions for this link\n          >\n            <p>Image Number {idx}</p>\n            <img\n              className=\"max-w-full contain-layout\"\n              src={src}\n            />\n          </NavLink>\n        ))}\n      </div>\n    </div>\n  );\n}\n```\n\n### 2. Add transition styles\n\nDefine view transition names and animations for elements that should transition smoothly between routes.\n\n```css filename=app.css\n/* Layout styles for the image grid */\n.image-list > div {\n  display: grid;\n  grid-template-columns: repeat(4, 1fr);\n  column-gap: 10px;\n}\n\n.image-list h1 {\n  font-size: 2rem;\n  font-weight: 600;\n}\n\n.image-list img {\n  max-width: 100%;\n  contain: layout;\n}\n\n.image-list p {\n  width: fit-content;\n}\n\n/* Assign transition names to elements during navigation */\n.image-list a.transitioning img {\n  view-transition-name: image-expand;\n}\n\n.image-list a.transitioning p {\n  view-transition-name: image-title;\n}\n```\n\n### 3. Create the image detail route\n\nThe detail view needs to use the same view transition names to create a seamless animation.\n\n```tsx filename=routes/image-details.tsx\nimport { Link } from \"react-router\";\nimport { images } from \"./home\";\nimport type { Route } from \"./+types/image-details\";\n\nexport default function ImageDetailsRoute({\n  params,\n}: Route.ComponentProps) {\n  return (\n    <div className=\"image-detail\">\n      <Link to=\"/\" viewTransition>\n        Back\n      </Link>\n      <h1>Image Number {params.id}</h1>\n      <img src={images[Number(params.id)]} />\n    </div>\n  );\n}\n```\n\n### 4. Add matching transition styles for the detail view\n\n```css filename=app.css\n/* Match transition names from the list view */\n.image-detail h1 {\n  font-size: 2rem;\n  font-weight: 600;\n  width: fit-content;\n  view-transition-name: image-title;\n}\n\n.image-detail img {\n  max-width: 100%;\n  contain: layout;\n  view-transition-name: image-expand;\n}\n```\n\n## Advanced Usage\n\nYou can control view transitions more precisely using either render props or the `useViewTransitionState` hook.\n\n### 1. Using render props\n\n```tsx filename=routes/image-gallery.tsx\n<NavLink to={`/image/${idx}`} viewTransition>\n  {({ isTransitioning }) => (\n    <>\n      <p\n        style={{\n          viewTransitionName: isTransitioning\n            ? \"image-title\"\n            : \"none\",\n        }}\n      >\n        Image Number {idx}\n      </p>\n      <img\n        src={src}\n        style={{\n          viewTransitionName: isTransitioning\n            ? \"image-expand\"\n            : \"none\",\n        }}\n      />\n    </>\n  )}\n</NavLink>\n```\n\n### 2. Using the `useViewTransitionState` hook\n\n```tsx filename=routes/image-gallery.tsx\nfunction NavImage(props: { src: string; idx: number }) {\n  const href = `/image/${props.idx}`;\n  // Hook provides transition state for specific route\n  const isTransitioning = useViewTransitionState(href);\n\n  return (\n    <Link to={href} viewTransition>\n      <p\n        style={{\n          viewTransitionName: isTransitioning\n            ? \"image-title\"\n            : \"none\",\n        }}\n      >\n        Image Number {props.idx}\n      </p>\n      <img\n        src={props.src}\n        style={{\n          viewTransitionName: isTransitioning\n            ? \"image-expand\"\n            : \"none\",\n        }}\n      />\n    </Link>\n  );\n}\n```\n\n[view-transitions-api]: https://developer.mozilla.org/en-US/docs/Web/API/ViewTransition\n[view-transitions-guide]: https://developer.chrome.com/docs/web-platform/view-transitions\n"
  },
  {
    "path": "docs/how-to/webhook.md",
    "content": "---\ntitle: Webhooks\n# can make a quick how-to on creating a webhook, this was copy/pasted from another doc, needs to be reviewed first\nhidden: true\n---\n\n# Webhooks\n\nResource routes can be used to handle webhooks. For example, you can create a webhook that receives notifications from GitHub when a new commit is pushed to a repository:\n\n```tsx\nimport type { Route } from \"./+types/github\";\n\nimport crypto from \"node:crypto\";\n\nexport const action = async ({\n  request,\n}: Route.ActionArgs) => {\n  if (request.method !== \"POST\") {\n    return Response.json(\n      { message: \"Method not allowed\" },\n      {\n        status: 405,\n      },\n    );\n  }\n  const payload = await request.json();\n\n  /* Validate the webhook */\n  const signature = request.headers.get(\n    \"X-Hub-Signature-256\",\n  );\n  const generatedSignature = `sha256=${crypto\n    .createHmac(\"sha256\", process.env.GITHUB_WEBHOOK_SECRET)\n    .update(JSON.stringify(payload))\n    .digest(\"hex\")}`;\n  if (signature !== generatedSignature) {\n    return Response.json(\n      { message: \"Signature mismatch\" },\n      {\n        status: 401,\n      },\n    );\n  }\n\n  /* process the webhook (e.g. enqueue a background job) */\n\n  return Response.json({ success: true });\n};\n```\n"
  },
  {
    "path": "docs/index.md",
    "content": "---\ntitle: React Router Home\norder: 1\n---\n\n# React Router Home\n\nReact Router is a multi-strategy router for React bridging the gap from React 18 to React 19. You can use it maximally as a React framework or as minimally as you want.\n\n## Getting Started\n\nThere are three primary ways, or \"modes\", to use it in your app, so there are three guides to get you started.\n\n- [Declarative](./start/declarative/installation)\n- [Data](./start/data/installation)\n- [Framework](./start/framework/installation)\n\nLearn which mode is right for you in [Picking a Mode](./start/modes).\n\n## Using These Guides\n\nAcross the docs you'll see the following icons:\n\n[MODES: framework, data, declarative]\n\n<p></p>\n\nThese icons indicate which mode the content is relevant to.\n\nAdditional auto-generated reference documentation is available:\n\n[Autogenerated Reference Docs ↗](https://api.reactrouter.com/v7/)\n\n## Upgrading\n\nIf you are caught up on future flags, upgrading from React Router v6 or Remix v2 is generally non-breaking. Remix v2 apps are encouraged to upgrade to React Router v7.\n\n- [Upgrade from v6](./upgrading/v6)\n- [Upgrade from Remix](./upgrading/remix)\n"
  },
  {
    "path": "docs/prettier.config.js",
    "content": "/**\n * @type {import('prettier').Options}\n */\nmodule.exports = {\n  ...require(\"../prettier.config\"),\n  printWidth: 60,\n};\n"
  },
  {
    "path": "docs/start/README",
    "content": "Get them off the ground and acquainted with basics. Docs here are a mix of:\n\nHow-To:\n\n- Practical Steps\n- Problem Oriented\n- Useful when we're coding\n\nExplanation:\n\n- Theoretical Knowledge\n- Understanding Oriented\n- Useful when we're studying\n"
  },
  {
    "path": "docs/start/data/actions.md",
    "content": "---\ntitle: Actions\norder: 5\n---\n\n# Actions\n\n[MODES: data]\n\n## Defining Actions\n\nData mutations are done through Route actions defined on the `action` property of a route object. When the action completes, all loader data on the page is revalidated to keep your UI in sync with the data without writing any code to do it.\n\n```tsx\nimport { createBrowserRouter } from \"react-router\";\nimport { someApi } from \"./api\";\n\nlet router = createBrowserRouter([\n  {\n    path: \"/projects/:projectId\",\n    Component: Project,\n    action: async ({ request }) => {\n      let formData = await request.formData();\n      let title = formData.get(\"title\");\n      let project = await someApi.updateProject({ title });\n      return project;\n    },\n  },\n]);\n```\n\n## Calling Actions\n\nActions are called declaratively through `<Form>` and imperatively through `useSubmit` (or `<fetcher.Form>` and `fetcher.submit`) by referencing the route's path and a \"post\" method.\n\n### Calling actions with a Form\n\n```tsx\nimport { Form } from \"react-router\";\n\nfunction SomeComponent() {\n  return (\n    <Form action=\"/projects/123\" method=\"post\">\n      <input type=\"text\" name=\"title\" />\n      <button type=\"submit\">Submit</button>\n    </Form>\n  );\n}\n```\n\nThis will cause a navigation and a new entry will be added to the browser history.\n\n### Calling actions with useSubmit\n\nYou can submit form data to an action imperatively with `useSubmit`.\n\n```tsx\nimport { useCallback } from \"react\";\nimport { useSubmit } from \"react-router\";\nimport { useFakeTimer } from \"fake-lib\";\n\nfunction useQuizTimer() {\n  let submit = useSubmit();\n\n  let cb = useCallback(() => {\n    submit(\n      { quizTimedOut: true },\n      { action: \"/end-quiz\", method: \"post\" },\n    );\n  }, []);\n\n  let tenMinutes = 10 * 60 * 1000;\n  useFakeTimer(tenMinutes, cb);\n}\n```\n\nThis will cause a navigation and a new entry will be added to the browser history.\n\n### Calling actions with a fetcher\n\nFetchers allow you to submit data to actions (and loaders) without causing a navigation (no new entries in the browser history).\n\n```tsx\nimport { useFetcher } from \"react-router\";\n\nfunction Task() {\n  let fetcher = useFetcher();\n  let busy = fetcher.state !== \"idle\";\n\n  return (\n    <fetcher.Form method=\"post\" action=\"/update-task/123\">\n      <input type=\"text\" name=\"title\" />\n      <button type=\"submit\">\n        {busy ? \"Saving...\" : \"Save\"}\n      </button>\n    </fetcher.Form>\n  );\n}\n```\n\nThey also have the imperative `submit` method.\n\n```tsx\nfetcher.submit(\n  { title: \"New Title\" },\n  { action: \"/update-task/123\", method: \"post\" },\n);\n```\n\nSee the [Using Fetchers][fetchers] guide for more information.\n\n## Accessing Action Data\n\nActions can return data available through `useActionData` in the route component or `fetcher.data` when using a fetcher.\n\n```tsx\nfunction Project() {\n  let actionData = useActionData();\n  return (\n    <div>\n      <h1>Project</h1>\n      <Form method=\"post\">\n        <input type=\"text\" name=\"title\" />\n        <button type=\"submit\">Submit</button>\n      </Form>\n      {actionData ? (\n        <p>{actionData.title} updated</p>\n      ) : null}\n    </div>\n  );\n}\n```\n\n---\n\nNext: [Navigating](./navigating)\n\n[fetchers]: ../../how-to/fetchers\n"
  },
  {
    "path": "docs/start/data/custom.md",
    "content": "---\ntitle: Custom Framework\norder: 8\n---\n\n# Custom Framework\n\n[MODES: data]\n\n## Introduction\n\nInstead of using `@react-router/dev`, you can integrate React Router's framework features (like loaders, actions, fetchers, etc.) into your own bundler and server abstractions with Data Mode.\n\n## Client Rendering\n\n### 1. Create a Router\n\nThe browser runtime API that enables route module APIs (loaders, actions, etc.) is `createBrowserRouter`.\n\nIt takes an array of route objects that support loaders, actions, error boundaries and more. The React Router Vite plugin creates one of these from `routes.ts`, but you can create one manually (or with an abstraction) and use your own bundler.\n\n```tsx\nimport { createBrowserRouter } from \"react-router\";\n\nlet router = createBrowserRouter([\n  {\n    path: \"/\",\n    Component: Root,\n    children: [\n      {\n        path: \"shows/:showId\",\n        Component: Show,\n        loader: ({ request, params }) =>\n          fetch(`/api/show/${params.showId}.json`, {\n            signal: request.signal,\n          }),\n      },\n    ],\n  },\n]);\n```\n\n### 2. Render the Router\n\nTo render the router in the browser, use `<RouterProvider>`.\n\n```tsx\nimport {\n  createBrowserRouter,\n  RouterProvider,\n} from \"react-router\";\nimport { createRoot } from \"react-dom/client\";\n\ncreateRoot(document.getElementById(\"root\")).render(\n  <RouterProvider router={router} />,\n);\n```\n\n### 3. Lazy Loading\n\nRoutes can take most of their definition lazily with the `lazy` property.\n\n```tsx\ncreateBrowserRouter([\n  {\n    path: \"/show/:showId\",\n    lazy: {\n      loader: async () =>\n        (await import(\"./show.loader.js\")).loader,\n      action: async () =>\n        (await import(\"./show.action.js\")).action,\n      Component: async () =>\n        (await import(\"./show.component.js\")).Component,\n    },\n  },\n]);\n```\n\n## Server Rendering\n\nTo server render a custom setup, there are a few server APIs available for rendering and data loading.\n\nThis guide simply gives you some ideas about how it works. For deeper understanding, please see the [Custom Framework Example Repo](https://github.com/remix-run/custom-react-router-framework-example)\n\n### 1. Define Your Routes\n\nRoutes are the same kinds of objects on the server as the client.\n\n```tsx\nexport default [\n  {\n    path: \"/\",\n    Component: Root,\n    children: [\n      {\n        path: \"shows/:showId\",\n        Component: Show,\n        loader: ({ params }) => {\n          return db.loadShow(params.id);\n        },\n      },\n    ],\n  },\n];\n```\n\n### 2. Create a static handler\n\nTurn your routes into a request handler with `createStaticHandler`:\n\n```tsx\nimport { createStaticHandler } from \"react-router\";\nimport routes from \"./some-routes\";\n\nlet { query, dataRoutes } = createStaticHandler(routes);\n```\n\n### 3. Get Routing Context and Render\n\nReact Router works with web fetch [Requests](https://developer.mozilla.org/en-US/docs/Web/API/Request), so if your server doesn't, you'll need to adapt whatever objects it uses to a web fetch `Request` object.\n\nThis step assumes your server receives `Request` objects.\n\n```tsx\nimport { renderToString } from \"react-dom/server\";\nimport {\n  createStaticHandler,\n  createStaticRouter,\n  StaticRouterProvider,\n} from \"react-router\";\n\nimport routes from \"./some-routes.js\";\n\nlet { query, dataRoutes } = createStaticHandler(routes);\n\nexport async function handler(request: Request) {\n  // 1. run actions/loaders to get the routing context with `query`\n  let context = await query(request);\n\n  // If `query` returns a Response, send it raw (a route probably a redirected)\n  if (context instanceof Response) {\n    return context;\n  }\n\n  // 2. Create a static router for SSR\n  let router = createStaticRouter(dataRoutes, context);\n\n  // 3. Render everything with StaticRouterProvider\n  let html = renderToString(\n    <StaticRouterProvider\n      router={router}\n      context={context}\n    />,\n  );\n\n  // Setup headers from action and loaders from deepest match\n  let leaf = context.matches[context.matches.length - 1];\n  let actionHeaders = context.actionHeaders[leaf.route.id];\n  let loaderHeaders = context.loaderHeaders[leaf.route.id];\n  let headers = new Headers(actionHeaders);\n  if (loaderHeaders) {\n    for (let [key, value] of loaderHeaders.entries()) {\n      headers.append(key, value);\n    }\n  }\n\n  headers.set(\"Content-Type\", \"text/html; charset=utf-8\");\n\n  // 4. send a response\n  return new Response(`<!DOCTYPE html>${html}`, {\n    status: context.statusCode,\n    headers,\n  });\n}\n```\n\n### 4. Hydrate in the browser\n\nHydration data is embedded onto `window.__staticRouterHydrationData`, use that to initialize your client side router and render a `<RouterProvider>`.\n\n```tsx\nimport { StrictMode } from \"react\";\nimport { hydrateRoot } from \"react-dom/client\";\nimport { RouterProvider } from \"react-router/dom\";\nimport routes from \"./app/routes.js\";\nimport { createBrowserRouter } from \"react-router\";\n\nlet router = createBrowserRouter(routes, {\n  hydrationData: window.__staticRouterHydrationData,\n});\n\nhydrateRoot(\n  document,\n  <StrictMode>\n    <RouterProvider router={router} />\n  </StrictMode>,\n);\n```\n"
  },
  {
    "path": "docs/start/data/data-loading.md",
    "content": "---\ntitle: Data Loading\norder: 4\n---\n\n# Data Loading\n\n[MODES: data]\n\n## Providing Data\n\nData is provided to route components from route loaders:\n\n```tsx\ncreateBrowserRouter([\n  {\n    path: \"/\",\n    loader: async () => {\n      // return data from here\n      return { records: await getSomeRecords() };\n    },\n    Component: MyRoute,\n  },\n]);\n```\n\n## Accessing Data\n\nThe data is available in route components with `useLoaderData`.\n\n```tsx\nimport { useLoaderData } from \"react-router\";\n\nfunction MyRoute() {\n  const { records } = useLoaderData();\n  return <div>{records.length}</div>;\n}\n```\n\nAs the user navigates between routes, the loaders are called before the route component is rendered.\n\n---\n\nNext: [Actions](./actions)\n"
  },
  {
    "path": "docs/start/data/index.md",
    "content": "---\ntitle: Data Mode\norder: 3\n---\n"
  },
  {
    "path": "docs/start/data/installation.md",
    "content": "---\ntitle: Installation\norder: 1\n---\n\n# Installation\n\n[MODES: data]\n\n## Bootstrap with a Bundler Template\n\nYou can start with a React template from Vite and choose \"React\", otherwise bootstrap your application however you prefer (Parcel, Webpack, etc).\n\n```shellscript nonumber\nnpx create-vite@latest\n```\n\n## Install React Router\n\nNext install React Router from npm:\n\n```shellscript nonumber\nnpm i react-router\n```\n\n## Create a Router and Render\n\nCreate a router and pass it to `RouterProvider`:\n\n```tsx lines=[3-4,6-11,16]\nimport React from \"react\";\nimport ReactDOM from \"react-dom/client\";\nimport { createBrowserRouter } from \"react-router\";\nimport { RouterProvider } from \"react-router/dom\";\n\nconst router = createBrowserRouter([\n  {\n    path: \"/\",\n    element: <div>Hello World</div>,\n  },\n]);\n\nconst root = document.getElementById(\"root\");\n\nReactDOM.createRoot(root).render(\n  <RouterProvider router={router} />,\n);\n```\n\n---\n\nNext: [Routing](./routing)\n"
  },
  {
    "path": "docs/start/data/navigating.md",
    "content": "---\ntitle: Navigating\norder: 6\n---\n\n# Navigating\n\nNavigating in Data Mode is the same as Framework Mode, please see the [Navigating](../framework/navigating) guide for more information.\n\n---\n\nNext: [Pending UI](./pending-ui)\n"
  },
  {
    "path": "docs/start/data/pending-ui.md",
    "content": "---\ntitle: Pending UI\norder: 7\n---\n\n# Pending UI\n\nPending UI is the same as Framework Mode, please see the [Pending UI](../framework/pending-ui) guide for more information.\n\n---\n\nNext: [Custom Framework](./custom)\n"
  },
  {
    "path": "docs/start/data/route-object.md",
    "content": "---\ntitle: Route Object\norder: 3\n---\n\n# Route Object\n\n[MODES: data]\n\n## Introduction\n\nThe objects passed to `createBrowserRouter` are called Route Objects.\n\n```tsx lines=[2-5]\ncreateBrowserRouter([\n  {\n    path: \"/\",\n    Component: App,\n  },\n]);\n```\n\nRoute modules are the foundation of React Router's data features, they define:\n\n- data loading\n- actions\n- revalidation\n- error boundaries\n- and more\n\nThis guide is a quick overview of every route object feature.\n\n## Component\n\nThe `Component` property in a route object defines the component that will render when the route matches.\n\n```tsx lines=[4]\ncreateBrowserRouter([\n  {\n    path: \"/\",\n    Component: MyRouteComponent,\n  },\n]);\n\nfunction MyRouteComponent() {\n  return (\n    <div>\n      <h1>Look ma!</h1>\n      <p>\n        I'm still using React Router after like 10 years.\n      </p>\n    </div>\n  );\n}\n```\n\n## `middleware`\n\nRoute [middleware][middleware] runs sequentially before and after navigations. This gives you a singular place to do things like logging and authentication. The `next` function continues down the chain, and on the leaf route the `next` function executes the loaders/actions for the navigation.\n\n```tsx\ncreateBrowserRouter([\n  {\n    path: \"/\",\n    middleware: [loggingMiddleware],\n    loader: rootLoader,\n    Component: Root,\n    children: [{\n      path: 'auth',\n      middleware: [authMiddleware],\n      loader: authLoader,\n      Component: Auth,\n      children: [...]\n    }]\n  },\n]);\n\nasync function loggingMiddleware({ request }, next) {\n  let url = new URL(request.url);\n  console.log(`Starting navigation: ${url.pathname}${url.search}`);\n  const start = performance.now();\n  await next();\n  const duration = performance.now() - start;\n  console.log(`Navigation completed in ${duration}ms`);\n}\n\nconst userContext = createContext<User>();\n\nasync function authMiddleware ({ context }) {\n  const userId = getUserId();\n\n  if (!userId) {\n    throw redirect(\"/login\");\n  }\n\n  context.set(userContext, await getUserById(userId));\n};\n```\n\nSee also:\n\n- [Middleware][middleware]\n\n## `loader`\n\nRoute loaders provide data to route components before they are rendered.\n\n```tsx\nimport {\n  useLoaderData,\n  createBrowserRouter,\n} from \"react-router\";\n\ncreateBrowserRouter([\n  {\n    path: \"/\",\n    loader: loader,\n    Component: MyRoute,\n  },\n]);\n\nasync function loader({ params }) {\n  return { message: \"Hello, world!\" };\n}\n\nfunction MyRoute() {\n  let data = useLoaderData();\n  return <h1>{data.message}</h1>;\n}\n```\n\nSee also:\n\n- [`loader` params][loader-params]\n\n## `action`\n\nRoute actions allow server-side data mutations with automatic revalidation of all loader data on the page when called from `<Form>`, `useFetcher`, and `useSubmit`.\n\n```tsx\nimport {\n  createBrowserRouter,\n  useLoaderData,\n  useActionData,\n  Form,\n} from \"react-router\";\nimport { TodoList } from \"~/components/TodoList\";\n\ncreateBrowserRouter([\n  {\n    path: \"/items\",\n    action: action,\n    loader: loader,\n    Component: Items,\n  },\n]);\n\nasync function action({ request }) {\n  const data = await request.formData();\n  const todo = await fakeDb.addItem({\n    title: data.get(\"title\"),\n  });\n  return { ok: true };\n}\n\n// this data will be revalidated after the action completes...\nasync function loader() {\n  const items = await fakeDb.getItems();\n  return { items };\n}\n\n// ...so that the list here is updated automatically\nexport default function Items() {\n  let data = useLoaderData();\n  return (\n    <div>\n      <List items={data.items} />\n      <Form method=\"post\" navigate={false}>\n        <input type=\"text\" name=\"title\" />\n        <button type=\"submit\">Create Todo</button>\n      </Form>\n    </div>\n  );\n}\n```\n\n## `shouldRevalidate`\n\nLoader data is automatically revalidated after certain events like navigations and form submissions.\n\nThis hook enables you to opt in or out of the default revalidation behavior. The default behavior is nuanced to avoid calling loaders unnecessarily.\n\nA route loader is revalidated when:\n\n- its own route params change\n- any change to URL search params\n- after an action is called and returns a non-error status code\n\nBy defining this function, you opt out of the default behavior completely and can manually control when loader data is revalidated for navigations and form submissions.\n\n```tsx\nimport type { ShouldRevalidateFunctionArgs } from \"react-router\";\n\nfunction shouldRevalidate(\n  arg: ShouldRevalidateFunctionArgs,\n) {\n  return true; // false\n}\n\ncreateBrowserRouter([\n  {\n    path: \"/\",\n    shouldRevalidate: shouldRevalidate,\n    Component: MyRoute,\n  },\n]);\n```\n\n[`ShouldRevalidateFunctionArgs` Reference Documentation ↗](https://api.reactrouter.com/v7/interfaces/react-router.ShouldRevalidateFunctionArgs.html)\n\nPlease note the default behavior is different in [Framework Mode](../modes).\n\n## `lazy`\n\nMost properties can be lazily imported to reduce the initial bundle size.\n\n```tsx\ncreateBrowserRouter([\n  {\n    path: \"/app\",\n    lazy: async () => {\n      // load component and loader in parallel before rendering\n      const [Component, loader] = await Promise.all([\n        import(\"./app\"),\n        import(\"./app-loader\"),\n      ]);\n      return { Component, loader };\n    },\n  },\n]);\n```\n\n---\n\nNext: [Data Loading](./data-loading)\n\n[loader-params]: https://api.reactrouter.com/v7/interfaces/react-router.LoaderFunctionArgs\n[middleware]: ../../how-to/middleware\n"
  },
  {
    "path": "docs/start/data/routing.md",
    "content": "---\ntitle: Routing\norder: 2\n---\n\n# Routing\n\n[MODES: data]\n\n## Configuring Routes\n\nRoutes are configured as the first argument to `createBrowserRouter`. At a minimum, you need a path and component:\n\n```tsx\nimport { createBrowserRouter } from \"react-router\";\n\nfunction Root() {\n  return <h1>Hello world</h1>;\n}\n\nconst router = createBrowserRouter([\n  { path: \"/\", Component: Root },\n]);\n```\n\nHere is a larger sample route config:\n\n```ts filename=app/routes.ts\ncreateBrowserRouter([\n  {\n    path: \"/\",\n    Component: Root,\n    children: [\n      { index: true, Component: Home },\n      { path: \"about\", Component: About },\n      {\n        path: \"auth\",\n        Component: AuthLayout,\n        children: [\n          { path: \"login\", Component: Login },\n          { path: \"register\", Component: Register },\n        ],\n      },\n      {\n        path: \"concerts\",\n        children: [\n          { index: true, Component: ConcertsHome },\n          { path: \":city\", Component: ConcertsCity },\n          { path: \"trending\", Component: ConcertsTrending },\n        ],\n      },\n    ],\n  },\n]);\n```\n\n## Route Objects\n\nRoute objects define the behavior of a route beyond just the path and component, like data loading and actions. We'll go into more detail in the [Route Object guide](./route-object), but here's a quick example of a loader.\n\n```tsx filename=app/team.tsx\nimport {\n  createBrowserRouter,\n  useLoaderData,\n} from \"react-router\";\n\ncreateBrowserRouter([\n  {\n    path: \"/teams/:teamId\",\n    loader: async ({ params }) => {\n      let team = await fetchTeam(params.teamId);\n      return { name: team.name };\n    },\n    Component: Team,\n  },\n]);\n\nfunction Team() {\n  let data = useLoaderData();\n  return <h1>{data.name}</h1>;\n}\n```\n\n## Nested Routes\n\nRoutes can be nested inside parent routes through `children`.\n\n```ts filename=app/routes.ts\ncreateBrowserRouter([\n  {\n    path: \"/dashboard\",\n    Component: Dashboard,\n    children: [\n      { index: true, Component: Home },\n      { path: \"settings\", Component: Settings },\n    ],\n  },\n]);\n```\n\nThe path of the parent is automatically included in the child, so this config creates both `\"/dashboard\"` and `\"/dashboard/settings\"` URLs.\n\nChild routes are rendered through the `<Outlet/>` in the parent route.\n\n```tsx filename=app/dashboard.tsx\nimport { Outlet } from \"react-router\";\n\nexport default function Dashboard() {\n  return (\n    <div>\n      <h1>Dashboard</h1>\n      {/* will either be <Home> or <Settings> */}\n      <Outlet />\n    </div>\n  );\n}\n```\n\n## Layout Routes\n\nOmitting the `path` in a route creates new [Nested Routes](#nested-routes) for its children without adding any segments to the URL.\n\n```tsx lines=[3,16]\ncreateBrowserRouter([\n  {\n    // no path on this parent route, just the component\n    Component: MarketingLayout,\n    children: [\n      { index: true, Component: Home },\n      { path: \"contact\", Component: Contact },\n    ],\n  },\n\n  {\n    path: \"projects\",\n    children: [\n      { index: true, Component: ProjectsHome },\n      {\n        // again, no path, just a component for the layout\n        Component: ProjectLayout,\n        children: [\n          { path: \":pid\", Component: Project },\n          { path: \":pid/edit\", Component: EditProject },\n        ],\n      },\n    ],\n  },\n]);\n```\n\nNote that:\n\n- `Home` and `Contact` will be rendered into the `MarketingLayout` outlet\n- `Project` and `EditProject` will be rendered into the `ProjectLayout` outlet while `ProjectsHome` will not.\n\n## Index Routes\n\nIndex routes are defined by setting `index: true` on a route object without a path.\n\n```ts\n{ index: true, Component: Home }\n```\n\nIndex routes render into their parent's [Outlet][outlet] at their parent's URL (like a default child route).\n\n```ts lines=[4,5,10,11]\nimport { createBrowserRouter } from \"react-router\";\n\ncreateBrowserRouter([\n  // renders at \"/\"\n  { index: true, Component: Home },\n  {\n    Component: Dashboard,\n    path: \"/dashboard\",\n    children: [\n      // renders at \"/dashboard\"\n      { index: true, Component: DashboardHome },\n      { path: \"settings\", Component: DashboardSettings },\n    ],\n  },\n]);\n```\n\nNote that index routes can't have children.\n\n## Prefix Route\n\nA route with just a path and no component creates a group of routes with a path prefix.\n\n```tsx lines=[3]\ncreateBrowserRouter([\n  {\n    // no component, just a path\n    path: \"/projects\",\n    children: [\n      { index: true, Component: ProjectsHome },\n      { path: \":pid\", Component: Project },\n      { path: \":pid/edit\", Component: EditProject },\n    ],\n  },\n]);\n```\n\nThis creates the routes `/projects`, `/projects/:pid`, and `/projects/:pid/edit` without introducing a layout component.\n\n## Dynamic Segments\n\nIf a path segment starts with `:` then it becomes a \"dynamic segment\". When the route matches the URL, the dynamic segment will be parsed from the URL and provided as `params` to other router APIs.\n\n```ts lines=[2]\n{\n  path: \"teams/:teamId\",\n  loader: async ({ params }) => {\n    // params are available in loaders/actions\n    let team = await fetchTeam(params.teamId);\n    return { name: team.name };\n  },\n  Component: Team,\n}\n```\n\n```tsx\nimport { useParams } from \"react-router\";\n\nfunction Team() {\n  // params are available in components through useParams\n  let params = useParams();\n  // ...\n}\n```\n\nYou can have multiple dynamic segments in one route path:\n\n```ts\n{\n  path: \"c/:categoryId/p/:productId\";\n}\n```\n\n## Optional Segments\n\nYou can make a route segment optional by adding a `?` to the end of the segment.\n\n```ts\n{\n  path: \":lang?/categories\";\n}\n```\n\nYou can have optional static segments, too:\n\n```ts\n{\n  path: \"users/:userId/edit?\";\n}\n```\n\n## Splats\n\nAlso known as \"catchall\" and \"star\" segments. If a route path pattern ends with `/*` then it will match any characters following the `/`, including other `/` characters.\n\n```ts\n{\n  path: \"files/*\";\n  loader: async ({ params }) => {\n    params[\"*\"]; // will contain the remaining URL after files/\n  };\n}\n```\n\nYou can destructure the `*`, you just have to assign it a new name. A common name is `splat`:\n\n```tsx\nconst { \"*\": splat } = params;\n```\n\n---\n\nNext: [Route Object](./route-object)\n\n[outlet]: https://api.reactrouter.com/v7/functions/react-router.Outlet.html\n"
  },
  {
    "path": "docs/start/data/testing.md",
    "content": "---\ntitle: Testing\norder: 9\n---\n\n# Testing\n\nYou can use `createRoutesStub` in data and framework modes. Please refer to the [Testing Guide](../framework/testing).\n"
  },
  {
    "path": "docs/start/declarative/index.md",
    "content": "---\ntitle: Declarative Mode\norder: 4\n---\n"
  },
  {
    "path": "docs/start/declarative/installation.md",
    "content": "---\ntitle: Installation\norder: 1\n---\n\n# Installation\n\n[MODES: declarative]\n\n## Introduction\n\nYou can start with a React template from Vite and choose \"React\", otherwise bootstrap your application however you prefer.\n\n```shellscript nonumber\nnpx create-vite@latest\n```\n\nNext install React Router from npm:\n\n```shellscript nonumber\nnpm i react-router\n```\n\nFinally, render a `<BrowserRouter>` around your application:\n\n```tsx lines=[3,9-11]\nimport React from \"react\";\nimport ReactDOM from \"react-dom/client\";\nimport { BrowserRouter } from \"react-router\";\nimport App from \"./app\";\n\nconst root = document.getElementById(\"root\");\n\nReactDOM.createRoot(root).render(\n  <BrowserRouter>\n    <App />\n  </BrowserRouter>,\n);\n```\n\n---\n\nNext: [Routing](./routing)\n"
  },
  {
    "path": "docs/start/declarative/navigating.md",
    "content": "---\ntitle: Navigating\norder: 3\n---\n\n# Navigating\n\n[MODES: declarative]\n\n## Introduction\n\nUsers navigate your application with `<Link>`, `<NavLink>`, and `useNavigate`.\n\n## NavLink\n\nThis component is for navigation links that need to render an active state.\n\n```tsx\nimport { NavLink } from \"react-router\";\n\nexport function MyAppNav() {\n  return (\n    <nav>\n      <NavLink to=\"/\" end>\n        Home\n      </NavLink>\n      <NavLink to=\"/trending\" end>\n        Trending Concerts\n      </NavLink>\n      <NavLink to=\"/concerts\">All Concerts</NavLink>\n      <NavLink to=\"/account\">Account</NavLink>\n    </nav>\n  );\n}\n```\n\nWhenever a `NavLink` is active, it will automatically have an `.active` class name for easy styling with CSS:\n\n```css\na.active {\n  color: red;\n}\n```\n\nIt also has callback props on `className`, `style`, and `children` with the active state for inline styling or conditional rendering:\n\n```tsx\n// className\n<NavLink\n  to=\"/messages\"\n  className={({ isActive }) =>\n    isActive ? \"text-red-500\" : \"text-black\"\n  }\n>\n  Messages\n</NavLink>\n```\n\n```tsx\n// style\n<NavLink\n  to=\"/messages\"\n  style={({ isActive }) => ({\n    color: isActive ? \"red\" : \"black\",\n  })}\n>\n  Messages\n</NavLink>\n```\n\n```tsx\n// children\n<NavLink to=\"/message\">\n  {({ isActive }) => (\n    <span className={isActive ? \"active\" : \"\"}>\n      {isActive ? \"👉\" : \"\"} Tasks\n    </span>\n  )}\n</NavLink>\n```\n\n## Link\n\nUse `<Link>` when the link doesn't need active styling:\n\n```tsx\nimport { Link } from \"react-router\";\n\nexport function LoggedOutMessage() {\n  return (\n    <p>\n      You've been logged out.{\" \"}\n      <Link to=\"/login\">Login again</Link>\n    </p>\n  );\n}\n```\n\n## useNavigate\n\nThis hook allows the programmer to navigate the user to a new page without the user interacting.\n\nFor normal navigation, it's best to use `Link` or `NavLink`. They provide a better default user experience like keyboard events, accessibility labeling, \"open in new window\", right click context menus, etc.\n\nReserve usage of `useNavigate` to situations where the user is _not_ interacting but you need to navigate, for example:\n\n- After a form submission completes\n- Logging them out after inactivity\n- Timed UIs like quizzes, etc.\n\n```tsx\nimport { useNavigate } from \"react-router\";\n\nexport function LoginPage() {\n  let navigate = useNavigate();\n\n  return (\n    <>\n      <MyHeader />\n      <MyLoginForm\n        onSuccess={() => {\n          navigate(\"/dashboard\");\n        }}\n      />\n      <MyFooter />\n    </>\n  );\n}\n```\n\n---\n\nNext: [Url values](./url-values)\n"
  },
  {
    "path": "docs/start/declarative/routing.md",
    "content": "---\ntitle: Routing\norder: 2\n---\n\n# Routing\n\n[MODES: declarative]\n\n## Configuring Routes\n\nRoutes are configured by rendering `<Routes>` and `<Route>` that couple URL segments to UI elements.\n\n```tsx\nimport React from \"react\";\nimport ReactDOM from \"react-dom/client\";\nimport { BrowserRouter, Routes, Route } from \"react-router\";\nimport App from \"./app\";\n\nconst root = document.getElementById(\"root\");\n\nReactDOM.createRoot(root).render(\n  <BrowserRouter>\n    <Routes>\n      <Route path=\"/\" element={<App />} />\n    </Routes>\n  </BrowserRouter>,\n);\n```\n\nHere's a larger sample config:\n\n```tsx\n<Routes>\n  <Route index element={<Home />} />\n  <Route path=\"about\" element={<About />} />\n\n  <Route element={<AuthLayout />}>\n    <Route path=\"login\" element={<Login />} />\n    <Route path=\"register\" element={<Register />} />\n  </Route>\n\n  <Route path=\"concerts\">\n    <Route index element={<ConcertsHome />} />\n    <Route path=\":city\" element={<City />} />\n    <Route path=\"trending\" element={<Trending />} />\n  </Route>\n</Routes>\n```\n\n## Nested Routes\n\nRoutes can be nested inside parent routes.\n\n```tsx\n<Routes>\n  <Route path=\"dashboard\" element={<Dashboard />}>\n    <Route index element={<Home />} />\n    <Route path=\"settings\" element={<Settings />} />\n  </Route>\n</Routes>\n```\n\nThe path of the parent is automatically included in the child, so this config creates both `\"/dashboard\"` and `\"/dashboard/settings\"` URLs.\n\nChild routes are rendered through the `<Outlet/>` in the parent route.\n\n```tsx filename=app/dashboard.tsx\nimport { Outlet } from \"react-router\";\n\nexport default function Dashboard() {\n  return (\n    <div>\n      <h1>Dashboard</h1>\n      {/* will either be <Home/> or <Settings/> */}\n      <Outlet />\n    </div>\n  );\n}\n```\n\n## Layout Routes\n\nRoutes _without_ a `path` create new nesting for their children, but they don't add any segments to the URL.\n\n```tsx lines=[2,9]\n<Routes>\n  <Route element={<MarketingLayout />}>\n    <Route index element={<MarketingHome />} />\n    <Route path=\"contact\" element={<Contact />} />\n  </Route>\n\n  <Route path=\"projects\">\n    <Route index element={<ProjectsHome />} />\n    <Route element={<ProjectsLayout />}>\n      <Route path=\":pid\" element={<Project />} />\n      <Route path=\":pid/edit\" element={<EditProject />} />\n    </Route>\n  </Route>\n</Routes>\n```\n\n## Index Routes\n\nIndex routes render into their parent's `<Outlet/>` at their parent's URL (like a default child route). They are configured with the `index` prop:\n\n```tsx lines=[4,8]\n<Routes>\n  <Route path=\"/\" element={<Root />}>\n    {/* renders into the outlet in <Root> at \"/\" */}\n    <Route index element={<Home />} />\n\n    <Route path=\"dashboard\" element={<Dashboard />}>\n      {/* renders into the outlet in <Dashboard> at \"/dashboard\" */}\n      <Route index element={<DashboardHome />} />\n      <Route path=\"settings\" element={<Settings />} />\n    </Route>\n  </Route>\n</Routes>\n```\n\nNote that index routes can't have children. If you're expecting that behavior, you probably want a [layout route](#layout-routes).\n\n## Route Prefixes\n\nA `<Route path>` _without_ an `element` prop adds a path prefix to its child routes, without introducing a parent layout.\n\n```tsx filename=app/routes.ts lines=[1]\n<Route path=\"projects\">\n  <Route index element={<ProjectsHome />} />\n  <Route element={<ProjectsLayout />}>\n    <Route path=\":pid\" element={<Project />} />\n    <Route path=\":pid/edit\" element={<EditProject />} />\n  </Route>\n</Route>\n```\n\n## Dynamic Segments\n\nIf a path segment starts with `:` then it becomes a \"dynamic segment\". When the route matches the URL, the dynamic segment will be parsed from the URL and provided as `params` to other router APIs like `useParams`.\n\n```tsx\n<Route path=\"teams/:teamId\" element={<Team />} />\n```\n\n```tsx filename=app/team.tsx\nimport { useParams } from \"react-router\";\n\nexport default function Team() {\n  let params = useParams();\n  // params.teamId\n}\n```\n\nYou can have multiple dynamic segments in one route path:\n\n```tsx\n<Route\n  path=\"/c/:categoryId/p/:productId\"\n  element={<Product />}\n/>\n```\n\n```tsx filename=app/category-product.tsx\nimport { useParams } from \"react-router\";\n\nexport default function CategoryProduct() {\n  let { categoryId, productId } = useParams();\n  // ...\n}\n```\n\nYou should ensure that all dynamic segments in a given path are unique. Otherwise, as the `params` object is populated - latter dynamic segment values will override earlier values.\n\n## Optional Segments\n\nYou can make a route segment optional by adding a `?` to the end of the segment.\n\n```tsx\n<Route path=\":lang?/categories\" element={<Categories />} />\n```\n\nYou can have optional static segments, too:\n\n```tsx\n<Route path=\"users/:userId/edit?\" element={<User />} />\n```\n\n## Splats\n\nAlso known as \"catchall\" and \"star\" segments. If a route path pattern ends with `/*` then it will match any characters following the `/`, including other `/` characters.\n\n```tsx\n<Route path=\"files/*\" element={<File />} />\n```\n\n```tsx\nlet params = useParams();\n// params[\"*\"] will contain the remaining URL after files/\nlet filePath = params[\"*\"];\n```\n\nYou can destructure the `*`, you just have to assign it a new name. A common name is `splat`:\n\n```tsx\nlet { \"*\": splat } = useParams();\n```\n\n## Linking\n\nLink to routes from your UI with `Link` and `NavLink`\n\n```tsx\nimport { NavLink, Link } from \"react-router\";\n\nfunction Header() {\n  return (\n    <nav>\n      {/* NavLink makes it easy to show active states */}\n      <NavLink\n        to=\"/\"\n        className={({ isActive }) =>\n          isActive ? \"active\" : \"\"\n        }\n      >\n        Home\n      </NavLink>\n\n      <Link to=\"/concerts/salt-lake-city\">Concerts</Link>\n    </nav>\n  );\n}\n```\n\n---\n\nNext: [Navigating](./navigating)\n"
  },
  {
    "path": "docs/start/declarative/url-values.md",
    "content": "---\ntitle: URL Values\n---\n\n# URL Values\n\n[MODES: declarative]\n\n## Route Params\n\nRoute params are the parsed values from a dynamic segment.\n\n```tsx\n<Route path=\"/concerts/:city\" element={<City />} />\n```\n\nIn this case, `:city` is the dynamic segment. The parsed value for that city will be available from `useParams`\n\n```tsx\nimport { useParams } from \"react-router\";\n\nfunction City() {\n  let { city } = useParams();\n  let data = useFakeDataLibrary(`/api/v2/cities/${city}`);\n  // ...\n}\n```\n\n## URL Search Params\n\nSearch params are the values after a `?` in the URL. They are accessible from `useSearchParams`, which returns an instance of [`URLSearchParams`](https://developer.mozilla.org/en-US/docs/Web/API/URLSearchParams)\n\n```tsx\nfunction SearchResults() {\n  let [searchParams] = useSearchParams();\n  return (\n    <div>\n      <p>\n        You searched for <i>{searchParams.get(\"q\")}</i>\n      </p>\n      <FakeSearchResults />\n    </div>\n  );\n}\n```\n\n## Location Object\n\nReact Router creates a custom `location` object with some useful information on it accessible with `useLocation`.\n\n```tsx\nfunction useAnalytics() {\n  let location = useLocation();\n  useEffect(() => {\n    sendFakeAnalytics(location.pathname);\n  }, [location]);\n}\n\nfunction useScrollRestoration() {\n  let location = useLocation();\n  useEffect(() => {\n    fakeRestoreScroll(location.key);\n  }, [location]);\n}\n```\n"
  },
  {
    "path": "docs/start/framework/actions.md",
    "content": "---\ntitle: Actions\norder: 6\n---\n\n# Actions\n\n[MODES: framework]\n\n## Introduction\n\nData mutations are done through Route actions. When the action completes, all loader data on the page is revalidated to keep your UI in sync with the data without writing any code to do it.\n\nRoute actions defined with `action` are only called on the server while actions defined with `clientAction` are run in the browser.\n\n## Client Actions\n\nClient actions only run in the browser and take priority over a server action when both are defined.\n\n```tsx filename=app/project.tsx\n// route('/projects/:projectId', './project.tsx')\nimport type { Route } from \"./+types/project\";\nimport { Form } from \"react-router\";\nimport { someApi } from \"./api\";\n\nexport async function clientAction({\n  request,\n}: Route.ClientActionArgs) {\n  let formData = await request.formData();\n  let title = formData.get(\"title\");\n  let project = await someApi.updateProject({ title });\n  return project;\n}\n\nexport default function Project({\n  actionData,\n}: Route.ComponentProps) {\n  return (\n    <div>\n      <h1>Project</h1>\n      <Form method=\"post\">\n        <input type=\"text\" name=\"title\" />\n        <button type=\"submit\">Submit</button>\n      </Form>\n      {actionData ? (\n        <p>{actionData.title} updated</p>\n      ) : null}\n    </div>\n  );\n}\n```\n\n## Server Actions\n\nServer actions only run on the server and are removed from client bundles.\n\n```tsx filename=app/project.tsx\n// route('/projects/:projectId', './project.tsx')\nimport type { Route } from \"./+types/project\";\nimport { Form } from \"react-router\";\nimport { fakeDb } from \"../db\";\n\nexport async function action({\n  request,\n}: Route.ActionArgs) {\n  let formData = await request.formData();\n  let title = formData.get(\"title\");\n  let project = await fakeDb.updateProject({ title });\n  return project;\n}\n\nexport default function Project({\n  actionData,\n}: Route.ComponentProps) {\n  return (\n    <div>\n      <h1>Project</h1>\n      <Form method=\"post\">\n        <input type=\"text\" name=\"title\" />\n        <button type=\"submit\">Submit</button>\n      </Form>\n      {actionData ? (\n        <p>{actionData.title} updated</p>\n      ) : null}\n    </div>\n  );\n}\n```\n\n## Calling Actions\n\nActions are called declaratively through `<Form>` and imperatively through `useSubmit` (or `<fetcher.Form>` and `fetcher.submit`) by referencing the route's path and a \"post\" method.\n\n### Calling actions with a Form\n\n```tsx\nimport { Form } from \"react-router\";\n\nfunction SomeComponent() {\n  return (\n    <Form action=\"/projects/123\" method=\"post\">\n      <input type=\"text\" name=\"title\" />\n      <button type=\"submit\">Submit</button>\n    </Form>\n  );\n}\n```\n\nThis will cause a navigation and a new entry will be added to the browser history.\n\n### Calling actions with useSubmit\n\nYou can submit form data to an action imperatively with `useSubmit`.\n\n```tsx\nimport { useCallback } from \"react\";\nimport { useSubmit } from \"react-router\";\nimport { useFakeTimer } from \"fake-lib\";\n\nfunction useQuizTimer() {\n  let submit = useSubmit();\n\n  let cb = useCallback(() => {\n    submit(\n      { quizTimedOut: true },\n      { action: \"/end-quiz\", method: \"post\" },\n    );\n  }, []);\n\n  let tenMinutes = 10 * 60 * 1000;\n  useFakeTimer(tenMinutes, cb);\n}\n```\n\nThis will cause a navigation and a new entry will be added to the browser history.\n\n### Calling actions with a fetcher\n\nFetchers allow you to submit data to actions (and loaders) without causing a navigation (no new entries in the browser history).\n\n```tsx\nimport { useFetcher } from \"react-router\";\n\nfunction Task() {\n  let fetcher = useFetcher();\n  let busy = fetcher.state !== \"idle\";\n\n  return (\n    <fetcher.Form method=\"post\" action=\"/update-task/123\">\n      <input type=\"text\" name=\"title\" />\n      <button type=\"submit\">\n        {busy ? \"Saving...\" : \"Save\"}\n      </button>\n    </fetcher.Form>\n  );\n}\n```\n\nThey also have the imperative `submit` method.\n\n```tsx\nfetcher.submit(\n  { title: \"New Title\" },\n  { action: \"/update-task/123\", method: \"post\" },\n);\n```\n\nSee the [Using Fetchers][fetchers] guide for more information.\n\n---\n\nNext: [Navigating](./navigating)\n\n[fetchers]: ../../how-to/fetchers\n[data]: ../../api/react-router/data\n"
  },
  {
    "path": "docs/start/framework/data-loading.md",
    "content": "---\ntitle: Data Loading\norder: 5\n---\n\n# Data Loading\n\n[MODES: framework]\n\n## Introduction\n\nData is provided to the route component from `loader` and `clientLoader`.\n\nLoader data is automatically serialized from loaders and deserialized in components. In addition to primitive values like strings and numbers, loaders can return promises, maps, sets, dates and more.\n\nThe type for the `loaderData` prop is [automatically generated][type-safety].\n\n<docs-info>We try to support the same set of [serializable types][serializable-types] that React permits server components to pass as props to client components. This future proofs your application for any eventual migration to [RSC][rsc].</docs-info>\n\n## Client Data Loading\n\n`clientLoader` is used to fetch data on the client. This is useful for pages or full projects that you'd prefer to fetch data from the browser only.\n\n```tsx filename=app/product.tsx\n// route(\"products/:pid\", \"./product.tsx\");\nimport type { Route } from \"./+types/product\";\n\nexport async function clientLoader({\n  params,\n}: Route.ClientLoaderArgs) {\n  const res = await fetch(`/api/products/${params.pid}`);\n  const product = await res.json();\n  return product;\n}\n\n// HydrateFallback is rendered while the client loader is running\nexport function HydrateFallback() {\n  return <div>Loading...</div>;\n}\n\nexport default function Product({\n  loaderData,\n}: Route.ComponentProps) {\n  const { name, description } = loaderData;\n  return (\n    <div>\n      <h1>{name}</h1>\n      <p>{description}</p>\n    </div>\n  );\n}\n```\n\n## Server Data Loading\n\nWhen server rendering, `loader` is used for both initial page loads and client navigations. Client navigations call the loader through an automatic `fetch` by React Router from the browser to your server.\n\n```tsx filename=app/product.tsx\n// route(\"products/:pid\", \"./product.tsx\");\nimport type { Route } from \"./+types/product\";\nimport { fakeDb } from \"../db\";\n\nexport async function loader({ params }: Route.LoaderArgs) {\n  const product = await fakeDb.getProduct(params.pid);\n  return product;\n}\n\nexport default function Product({\n  loaderData,\n}: Route.ComponentProps) {\n  const { name, description } = loaderData;\n  return (\n    <div>\n      <h1>{name}</h1>\n      <p>{description}</p>\n    </div>\n  );\n}\n```\n\nNote that the `loader` function is removed from client bundles so you can use server only APIs without worrying about them being included in the browser.\n\n## Static Data Loading\n\nWhen pre-rendering, loaders are used to fetch data during the production build.\n\n```tsx filename=app/product.tsx\n// route(\"products/:pid\", \"./product.tsx\");\nimport type { Route } from \"./+types/product\";\n\nexport async function loader({ params }: Route.LoaderArgs) {\n  let product = await getProductFromCSVFile(params.pid);\n  return product;\n}\n\nexport default function Product({\n  loaderData,\n}: Route.ComponentProps) {\n  const { name, description } = loaderData;\n  return (\n    <div>\n      <h1>{name}</h1>\n      <p>{description}</p>\n    </div>\n  );\n}\n```\n\nThe URLs to pre-render are specified in `react-router.config.ts`:\n\n```ts filename=react-router.config.ts\nimport type { Config } from \"@react-router/dev/config\";\n\nexport default {\n  async prerender() {\n    let products = await readProductsFromCSVFile();\n    return products.map(\n      (product) => `/products/${product.id}`,\n    );\n  },\n} satisfies Config;\n```\n\nNote that when server rendering, any URLs that aren't pre-rendered will be server rendered as usual, allowing you to pre-render some data at a single route while still server rendering the rest.\n\n## Using Both Loaders\n\n`loader` and `clientLoader` can be used together. The `loader` will be used on the server for initial SSR (or pre-rendering) and the `clientLoader` will be used on subsequent client-side navigations.\n\n```tsx filename=app/product.tsx\n// route(\"products/:pid\", \"./product.tsx\");\nimport type { Route } from \"./+types/product\";\nimport { fakeDb } from \"../db\";\n\nexport async function loader({ params }: Route.LoaderArgs) {\n  return fakeDb.getProduct(params.pid);\n}\n\nexport async function clientLoader({\n  serverLoader,\n  params,\n}: Route.ClientLoaderArgs) {\n  const res = await fetch(`/api/products/${params.pid}`);\n  const serverData = await serverLoader();\n  return { ...serverData, ...res.json() };\n}\n\nexport default function Product({\n  loaderData,\n}: Route.ComponentProps) {\n  const { name, description } = loaderData;\n\n  return (\n    <div>\n      <h1>{name}</h1>\n      <p>{description}</p>\n    </div>\n  );\n}\n```\n\nYou can also force the client loader to run during hydration and before the page renders by setting the `hydrate` property on the function. In this situation you will want to render a `HydrateFallback` component to show a fallback UI while the client loader runs.\n\n```tsx filename=app/product.tsx\nexport async function loader() {\n  /* ... */\n}\n\nexport async function clientLoader() {\n  /* ... */\n}\n\n// force the client loader to run during hydration\nclientLoader.hydrate = true as const; // `as const` for type inference\n\nexport function HydrateFallback() {\n  return <div>Loading...</div>;\n}\n\nexport default function Product() {\n  /* ... */\n}\n```\n\n---\n\nNext: [Actions][actions]\n\nSee also:\n\n- [Streaming with Suspense][streaming]\n- [Client Data][client-data]\n- [Using Fetchers][fetchers]\n\n[type-safety]: ../../explanation/type-safety\n[serializable-types]: https://react.dev/reference/rsc/use-client#serializable-types\n[rsc]: ../../how-to/react-server-components\n[actions]: ./actions\n[streaming]: ../../how-to/suspense\n[client-data]: ../../how-to/client-data\n[fetchers]: ../../how-to/fetchers#loading-data\n"
  },
  {
    "path": "docs/start/framework/deploying.md",
    "content": "---\ntitle: Deploying\norder: 10\n---\n\n# Deploying\n\n[MODES: framework]\n\n## Introduction\n\nReact Router can be deployed two ways:\n\n- Fullstack Hosting\n- Static Hosting\n\nThe official [React Router templates](https://github.com/remix-run/react-router-templates) can help you bootstrap an application or be used as a reference for your own application.\n\nWhen deploying to static hosting, you can deploy React Router the same as any other single page application with React.\n\n## Templates\n\nAfter running the `create-react-router` command, make sure to follow the instructions in the README.\n\n### Node.js with Docker\n\n```\nnpx create-react-router@latest --template remix-run/react-router-templates/default\n```\n\n- Server Rendering\n- Tailwind CSS\n\nThe containerized application can be deployed to any platform that supports Docker, including:\n\n- AWS ECS\n- Google Cloud Run\n- Azure Container Apps\n- Digital Ocean App Platform\n- Fly.io\n- Railway\n\n### Node with Docker (Custom Server)\n\n```\nnpx create-react-router@latest --template remix-run/react-router-templates/node-custom-server\n```\n\n- Server Rendering\n- Tailwind CSS\n- Custom express server for more control\n\nThe containerized application can be deployed to any platform that supports Docker, including:\n\n- AWS ECS\n- Google Cloud Run\n- Azure Container Apps\n- Digital Ocean App Platform\n- Fly.io\n- Railway\n\n### Node with Docker and Postgres\n\n```\nnpx create-react-router@latest --template remix-run/react-router-templates/node-postgres\n```\n\n- Server Rendering\n- Postgres Database with Drizzle\n- Tailwind CSS\n- Custom express server for more control\n\nThe containerized application can be deployed to any platform that supports Docker, including:\n\n- AWS ECS\n- Google Cloud Run\n- Azure Container Apps\n- Digital Ocean App Platform\n- Fly.io\n- Railway\n\n### Vercel\n\nVercel maintains their own template for React Router. Checkout the [Vercel Guide](https://vercel.com/templates/react-router/react-router-boilerplate) for more information.\n\n### Cloudflare Workers\n\nCloudflare maintains their own template for React Router. Checkout the [Cloudflare Guide](https://developers.cloudflare.com/workers/framework-guides/web-apps/react-router/) for more information.\n\n### Netlify\n\nNetlify maintains their own template for React Router. Checkout the [Netlify Guide](https://docs.netlify.com/build/frameworks/framework-setup-guides/react-router/) for more information.\n\n### EdgeOne Pages\n\nEdgeOne Pages maintains their own template for React Router. Checkout the [EdgeOne Pages Guide](https://pages.edgeone.ai/document/framework-react-router) for more information.\n"
  },
  {
    "path": "docs/start/framework/index.md",
    "content": "---\ntitle: Framework Mode\norder: 2\n---\n"
  },
  {
    "path": "docs/start/framework/installation.md",
    "content": "---\ntitle: Installation\norder: 1\n---\n\n# Installation\n\n[MODES: framework]\n\n## Introduction\n\nMost projects start with a template. Let's use a basic template maintained by React Router:\n\n```shellscript nonumber\nnpx create-react-router@latest my-react-router-app\n```\n\nNow change into the new directory and start the app\n\n```shellscript nonumber\ncd my-react-router-app\nnpm i\nnpm run dev\n```\n\nYou can now open your browser to `http://localhost:5173`\n\nYou can [view the template on GitHub][default-template] to see how to manually set up your project.\n\nWe also have a number of [ready to deploy templates][react-router-templates] available for you to get started with:\n\n```shellscript nonumber\nnpx create-react-router@latest --template remix-run/react-router-templates/<template-name>\n```\n\n---\n\nNext: [Routing](./routing)\n\n[manual_usage]: ../how-to/manual-usage\n[default-template]: https://github.com/remix-run/react-router-templates/tree/main/default\n[react-router-templates]: https://github.com/remix-run/react-router-templates\n"
  },
  {
    "path": "docs/start/framework/navigating.md",
    "content": "---\ntitle: Navigating\norder: 6\n---\n\n# Navigating\n\n[MODES: framework]\n\n## Introduction\n\nUsers navigate your application with `<Link>`, `<NavLink>`, `<Form>`, `redirect`, and `useNavigate`.\n\n## NavLink\n\nThis component is for navigation links that need to render active and pending states.\n\n```tsx\nimport { NavLink } from \"react-router\";\n\nexport function MyAppNav() {\n  return (\n    <nav>\n      <NavLink to=\"/\" end>\n        Home\n      </NavLink>\n      <NavLink to=\"/trending\" end>\n        Trending Concerts\n      </NavLink>\n      <NavLink to=\"/concerts\">All Concerts</NavLink>\n      <NavLink to=\"/account\">Account</NavLink>\n    </nav>\n  );\n}\n```\n\n`NavLink` renders default class names for different states for easy styling with CSS:\n\n```css\na.active {\n  color: red;\n}\n\na.pending {\n  animate: pulse 1s infinite;\n}\n\na.transitioning {\n  /* css transition is running */\n}\n```\n\nIt also has callback props on `className`, `style`, and `children` with the states for inline styling or conditional rendering:\n\n```tsx\n// className\n<NavLink\n  to=\"/messages\"\n  className={({ isActive, isPending, isTransitioning }) =>\n    [\n      isPending ? \"pending\" : \"\",\n      isActive ? \"active\" : \"\",\n      isTransitioning ? \"transitioning\" : \"\",\n    ].join(\" \")\n  }\n>\n  Messages\n</NavLink>\n```\n\n```tsx\n// style\n<NavLink\n  to=\"/messages\"\n  style={({ isActive, isPending, isTransitioning }) => {\n    return {\n      fontWeight: isActive ? \"bold\" : \"\",\n      color: isPending ? \"red\" : \"black\",\n      viewTransitionName: isTransitioning ? \"slide\" : \"\",\n    };\n  }}\n>\n  Messages\n</NavLink>\n```\n\n```tsx\n// children\n<NavLink to=\"/tasks\">\n  {({ isActive, isPending, isTransitioning }) => (\n    <span className={isActive ? \"active\" : \"\"}>Tasks</span>\n  )}\n</NavLink>\n```\n\n## Link\n\nUse `<Link>` when the link doesn't need active styling:\n\n```tsx\nimport { Link } from \"react-router\";\n\nexport function LoggedOutMessage() {\n  return (\n    <p>\n      You've been logged out.{\" \"}\n      <Link to=\"/login\">Login again</Link>\n    </p>\n  );\n}\n```\n\n## Form\n\nThe form component can be used to navigate with `URLSearchParams` provided by the user.\n\n```tsx\n<Form action=\"/search\">\n  <input type=\"text\" name=\"q\" />\n</Form>\n```\n\nIf the user enters \"journey\" into the input and submits it, they will navigate to:\n\n```\n/search?q=journey\n```\n\nForms with `<Form method=\"post\" />` will also navigate to the action prop but will submit the data as `FormData` instead of `URLSearchParams`. However, it is more common to `useFetcher()` to POST form data. See [Using Fetchers](../../how-to/fetchers).\n\n## redirect\n\nInside of route loaders and actions, you can return a `redirect` to another URL.\n\n```tsx\nimport { redirect } from \"react-router\";\n\nexport async function loader({ request }) {\n  let user = await getUser(request);\n  if (!user) {\n    return redirect(\"/login\");\n  }\n  return { userName: user.name };\n}\n```\n\nIt is common to redirect to a new record after it has been created:\n\n```tsx\nimport { redirect } from \"react-router\";\n\nexport async function action({ request }) {\n  let formData = await request.formData();\n  let project = await createProject(formData);\n  return redirect(`/projects/${project.id}`);\n}\n```\n\n## useNavigate\n\nThis hook allows the programmer to navigate the user to a new page without the user interacting. Usage of this hook should be uncommon. It's recommended to use the other APIs in this guide when possible.\n\nReserve usage of `useNavigate` to situations where the user is _not_ interacting but you need to navigate, for example:\n\n- Logging them out after inactivity\n- Timed UIs like quizzes, etc.\n\n```tsx\nimport { useNavigate } from \"react-router\";\n\nexport function useLogoutAfterInactivity() {\n  let navigate = useNavigate();\n\n  useFakeInactivityHook(() => {\n    navigate(\"/logout\");\n  });\n}\n```\n\n---\n\nNext: [Pending UI](./pending-ui)\n"
  },
  {
    "path": "docs/start/framework/pending-ui.md",
    "content": "---\ntitle: Pending UI\norder: 7\n---\n\n# Pending UI\n\n[MODES: framework]\n\n## Introduction\n\nWhen the user navigates to a new route, or submits data to an action, the UI should immediately respond to the user's actions with a pending or optimistic state. Application code is responsible for this.\n\n## Global Pending Navigation\n\nWhen the user navigates to a new url, the loaders for the next page are awaited before the next page renders. You can get the pending state from `useNavigation`.\n\n```tsx\nimport { useNavigation } from \"react-router\";\n\nexport default function Root() {\n  const navigation = useNavigation();\n  const isNavigating = Boolean(navigation.location);\n\n  return (\n    <html>\n      <body>\n        {isNavigating && <GlobalSpinner />}\n        <Outlet />\n      </body>\n    </html>\n  );\n}\n```\n\n## Local Pending Navigation\n\nPending indicators can also be localized to the link. NavLink's children, className, and style props can be functions that receive the pending state.\n\n```tsx\nimport { NavLink } from \"react-router\";\n\nfunction Navbar() {\n  return (\n    <nav>\n      <NavLink to=\"/home\">\n        {({ isPending }) => (\n          <span>Home {isPending && <Spinner />}</span>\n        )}\n      </NavLink>\n      <NavLink\n        to=\"/about\"\n        style={({ isPending }) => ({\n          color: isPending ? \"gray\" : \"black\",\n        })}\n      >\n        About\n      </NavLink>\n    </nav>\n  );\n}\n```\n\n## Pending Form Submission\n\nWhen a form is submitted, the UI should immediately respond to the user's actions with a pending state. This is easiest to do with a [fetcher][use_fetcher] form because it has its own independent state (whereas normal forms cause a global navigation).\n\n```tsx filename=app/project.tsx lines=[10-12]\nimport { useFetcher } from \"react-router\";\n\nfunction NewProjectForm() {\n  const fetcher = useFetcher();\n\n  return (\n    <fetcher.Form method=\"post\">\n      <input type=\"text\" name=\"title\" />\n      <button type=\"submit\">\n        {fetcher.state !== \"idle\"\n          ? \"Submitting...\"\n          : \"Submit\"}\n      </button>\n    </fetcher.Form>\n  );\n}\n```\n\nFor non-fetcher form submissions, pending states are available on `useNavigation`.\n\n```tsx filename=app/projects/new.tsx\nimport { useNavigation, Form } from \"react-router\";\n\nfunction NewProjectForm() {\n  const navigation = useNavigation();\n\n  return (\n    <Form method=\"post\" action=\"/projects/new\">\n      <input type=\"text\" name=\"title\" />\n      <button type=\"submit\">\n        {navigation.formAction === \"/projects/new\"\n          ? \"Submitting...\"\n          : \"Submit\"}\n      </button>\n    </Form>\n  );\n}\n```\n\n## Optimistic UI\n\nWhen the future state of the UI is known by the form submission data, an optimistic UI can be implemented for instant UX.\n\n```tsx filename=app/project.tsx lines=[4-7]\nfunction Task({ task }) {\n  const fetcher = useFetcher();\n\n  let isComplete = task.status === \"complete\";\n  if (fetcher.formData) {\n    isComplete =\n      fetcher.formData.get(\"status\") === \"complete\";\n  }\n\n  return (\n    <div>\n      <div>{task.title}</div>\n      <fetcher.Form method=\"post\">\n        <button\n          name=\"status\"\n          value={isComplete ? \"incomplete\" : \"complete\"}\n        >\n          {isComplete ? \"Mark Incomplete\" : \"Mark Complete\"}\n        </button>\n      </fetcher.Form>\n    </div>\n  );\n}\n```\n\n---\n\nNext: [Testing](./testing)\n\n[use_fetcher]: https://api.reactrouter.com/v7/functions/react-router.useFetcher.html\n"
  },
  {
    "path": "docs/start/framework/rendering.md",
    "content": "---\ntitle: Rendering Strategies\norder: 4\n---\n\n# Rendering Strategies\n\n[MODES: framework]\n\n## Introduction\n\nThere are three rendering strategies in React Router:\n\n- Client Side Rendering\n- Server Side Rendering\n- Static Pre-rendering\n\n## Client Side Rendering\n\nRoutes are always client side rendered as the user navigates around the app. If you're looking to build a Single Page App, disable server rendering:\n\n```ts filename=react-router.config.ts\nimport type { Config } from \"@react-router/dev/config\";\n\nexport default {\n  ssr: false,\n} satisfies Config;\n```\n\n## Server Side Rendering\n\n```ts filename=react-router.config.ts\nimport type { Config } from \"@react-router/dev/config\";\n\nexport default {\n  ssr: true,\n} satisfies Config;\n```\n\nServer side rendering requires a deployment that supports it. Though it's a global setting, individual routes can still be statically pre-rendered. Routes can also use client data loading with `clientLoader` to avoid server rendering/fetching for their portion of the UI.\n\n## Static Pre-rendering\n\n```ts filename=react-router.config.ts\nimport type { Config } from \"@react-router/dev/config\";\n\nexport default {\n  // return a list of URLs to prerender at build time\n  async prerender() {\n    return [\"/\", \"/about\", \"/contact\"];\n  },\n} satisfies Config;\n```\n\nPre-rendering is a build-time operation that generates static HTML and client navigation data payloads for a list of URLs. This is useful for SEO and performance, especially for deployments without server rendering. When pre-rendering, route module loaders are used to fetch data at build time.\n\n---\n\nNext: [Data Loading](./data-loading)\n"
  },
  {
    "path": "docs/start/framework/route-module.md",
    "content": "---\ntitle: Route Module\norder: 3\n---\n\n# Route Module\n\n[MODES: framework]\n\n## Introduction\n\nThe files referenced in `routes.ts` are called Route Modules.\n\n```tsx filename=app/routes.ts\nroute(\"teams/:teamId\", \"./team.tsx\"),\n//           route module ^^^^^^^^\n```\n\nRoute modules are the foundation of React Router's framework features, they define:\n\n- automatic code-splitting\n- data loading\n- actions\n- revalidation\n- error boundaries\n- and more\n\nThis guide is a quick overview of every route module feature. The rest of the getting started guides will cover these features in more detail.\n\n## Component (`default`)\n\nThe `default` export in a route module defines the component that will render when the route matches.\n\n```tsx filename=app/routes/my-route.tsx\nexport default function MyRouteComponent() {\n  return (\n    <div>\n      <h1>Look ma!</h1>\n      <p>\n        I'm still using React Router after like 10 years.\n      </p>\n    </div>\n  );\n}\n```\n\n### Props passed to the Component\n\nWhen the component is rendered, it is provided the props defined in `Route.ComponentProps` that React Router will automatically generate for you. These props include:\n\n1. `loaderData`: The data returned from the `loader` function in this route module\n2. `actionData`: The data returned from the `action` function in this route module\n3. `params`: An object containing the route parameters (if any).\n4. `matches`: An array of all the matches in the current route tree.\n\nYou can use these props in place of hooks like `useLoaderData` or `useParams`. This may be preferable because they will be automatically typed correctly for the route.\n\n### Using props\n\n```tsx filename=app/routes/my-route-with-default-params.tsx\nimport type { Route } from \"./+types/route-name\";\n\nexport default function MyRouteComponent({\n  loaderData,\n  actionData,\n  params,\n  matches,\n}: Route.ComponentProps) {\n  return (\n    <div>\n      <h1>Welcome to My Route with Props!</h1>\n      <p>Loader Data: {JSON.stringify(loaderData)}</p>\n      <p>Action Data: {JSON.stringify(actionData)}</p>\n      <p>Route Parameters: {JSON.stringify(params)}</p>\n      <p>Matched Routes: {JSON.stringify(matches)}</p>\n    </div>\n  );\n}\n```\n\n## `middleware`\n\nRoute [middleware][middleware] runs sequentially on the server before and after document and\ndata requests. This gives you a singular place to do things like logging,\nauthentication, and post-processing of responses. The `next` function continues down the chain, and on the leaf route the `next` function executes the loaders/actions for the navigation.\n\nHere's an example middleware to log requests on the server:\n\n```tsx filename=root.tsx\nasync function loggingMiddleware(\n  { request, context },\n  next,\n) {\n  console.log(\n    `${new Date().toISOString()} ${request.method} ${request.url}`,\n  );\n  const start = performance.now();\n  const response = await next();\n  const duration = performance.now() - start;\n  console.log(\n    `${new Date().toISOString()} Response ${response.status} (${duration}ms)`,\n  );\n  return response;\n}\n\nexport const middleware = [loggingMiddleware];\n```\n\nHere's an example middleware to check for logged in users and set the user in\n`context` you can then access from loaders:\n\n```tsx filename=routes/_auth.tsx\nasync function authMiddleware({ request, context }) {\n  const session = await getSession(request);\n  const userId = session.get(\"userId\");\n\n  if (!userId) {\n    throw redirect(\"/login\");\n  }\n\n  const user = await getUserById(userId);\n  context.set(userContext, user);\n}\n\nexport const middleware = [authMiddleware];\n```\n\n<docs-warning>Please make sure you understand [when middleware runs][when-middleware-runs] to make sure your application will behave the way you intend when adding middleware to your routes.</docs-warning>\n\nSee also:\n\n- [`middleware` params][middleware-params]\n- [Middleware][middleware]\n\n## `clientMiddleware`\n\nThis is the client-side equivalent of `middleware` and runs in the browser during client navigations. The only difference from server middleware is that client middleware doesn't return Responses because they're not wrapping an HTTP request on the server.\n\nHere's an example middleware to log requests on the client:\n\n```tsx filename=root.tsx\nasync function loggingMiddleware(\n  { request, context },\n  next,\n) {\n  console.log(\n    `${new Date().toISOString()} ${request.method} ${request.url}`,\n  );\n  const start = performance.now();\n  await next(); // 👈 No Response returned\n  const duration = performance.now() - start;\n  console.log(\n    `${new Date().toISOString()} (${duration}ms)`,\n  );\n  // ✅ No need to return anything\n}\n\nexport const clientMiddleware = [loggingMiddleware];\n```\n\nSee also:\n\n- [Middleware][middleware]\n- [Client Data][client-data]\n\n## `loader`\n\nRoute loaders provide data to route components before they are rendered. They are only called on the server when server rendering or during the build with pre-rendering.\n\n```tsx\nexport async function loader() {\n  return { message: \"Hello, world!\" };\n}\n\nexport default function MyRoute({ loaderData }) {\n  return <h1>{loaderData.message}</h1>;\n}\n```\n\nSee also:\n\n- [`loader` params][loader-params]\n\n## `clientLoader`\n\nCalled only in the browser, route client loaders provide data to route components in addition to, or in place of, route loaders.\n\n```tsx\nexport async function clientLoader({ serverLoader }) {\n  // call the server loader\n  const serverData = await serverLoader();\n  // And/or fetch data on the client\n  const data = getDataFromClient();\n  // Return the data to expose through useLoaderData()\n  return data;\n}\n```\n\nClient loaders can participate in initial page load hydration of server rendered pages by setting the `hydrate` property on the function:\n\n```tsx\nexport async function clientLoader() {\n  // ...\n}\nclientLoader.hydrate = true as const;\n```\n\n<docs-info>\n\nBy using `as const`, TypeScript will infer that the type for `clientLoader.hydrate` is `true` instead of `boolean`.\nThat way, React Router can derive types for `loaderData` based on the value of `clientLoader.hydrate`.\n\n</docs-info>\n\nSee also:\n\n- [`clientLoader` params][client-loader-params]\n- [Client Data][client-data]\n\n## `action`\n\nRoute actions allow server-side data mutations with automatic revalidation of all loader data on the page when called from `<Form>`, `useFetcher`, and `useSubmit`.\n\n```tsx\n// route(\"/list\", \"./list.tsx\")\nimport { Form } from \"react-router\";\nimport { TodoList } from \"~/components/TodoList\";\n\n// this data will be loaded after the action completes...\nexport async function loader() {\n  const items = await fakeDb.getItems();\n  return { items };\n}\n\n// ...so that the list here is updated automatically\nexport default function Items({ loaderData }) {\n  return (\n    <div>\n      <List items={loaderData.items} />\n      <Form method=\"post\" navigate={false} action=\"/list\">\n        <input type=\"text\" name=\"title\" />\n        <button type=\"submit\">Create Todo</button>\n      </Form>\n    </div>\n  );\n}\n\nexport async function action({ request }) {\n  const data = await request.formData();\n  const todo = await fakeDb.addItem({\n    title: data.get(\"title\"),\n  });\n  return { ok: true };\n}\n```\n\nSee also:\n\n- [`action` params][action-params]\n\n## `clientAction`\n\nLike route actions but only called in the browser.\n\n```tsx\nexport async function clientAction({ serverAction }) {\n  fakeInvalidateClientSideCache();\n  // can still call the server action if needed\n  const data = await serverAction();\n  return data;\n}\n```\n\nSee also:\n\n- [`clientAction` params][client-action-params]\n- [Client Data][client-data]\n\n## `ErrorBoundary`\n\nWhen other route module APIs throw, the route module `ErrorBoundary` will render instead of the route component.\n\n```tsx\nimport {\n  isRouteErrorResponse,\n  useRouteError,\n} from \"react-router\";\n\nexport function ErrorBoundary() {\n  const error = useRouteError();\n\n  if (isRouteErrorResponse(error)) {\n    return (\n      <div>\n        <h1>\n          {error.status} {error.statusText}\n        </h1>\n        <p>{error.data}</p>\n      </div>\n    );\n  } else if (error instanceof Error) {\n    return (\n      <div>\n        <h1>Error</h1>\n        <p>{error.message}</p>\n        <p>The stack trace is:</p>\n        <pre>{error.stack}</pre>\n      </div>\n    );\n  } else {\n    return <h1>Unknown Error</h1>;\n  }\n}\n```\n\nSee also:\n\n- [`useRouteError`][use-route-error]\n- [`isRouteErrorResponse`][is-route-error-response]\n\n## `HydrateFallback`\n\nOn initial page load, the route component renders only after the client loader is finished. If exported, a `HydrateFallback` can render immediately in place of the route component.\n\n```tsx filename=routes/client-only-route.tsx\nexport async function clientLoader() {\n  const data = await fakeLoadLocalGameData();\n  return data;\n}\n\nexport function HydrateFallback() {\n  return <p>Loading Game...</p>;\n}\n\nexport default function Component({ loaderData }) {\n  return <Game data={loaderData} />;\n}\n```\n\n## `headers`\n\nThe route `headers` function defines the HTTP headers to be sent with the response when server rendering.\n\n```tsx\nexport function headers() {\n  return {\n    \"X-Stretchy-Pants\": \"its for fun\",\n    \"Cache-Control\": \"max-age=300, s-maxage=3600\",\n  };\n}\n```\n\nSee also:\n\n- [`Headers`][headers]\n\n## `handle`\n\nRoute handle allows apps to add anything to a route match in `useMatches` to create abstractions (like breadcrumbs, etc.).\n\n```tsx\nexport const handle = {\n  its: \"all yours\",\n};\n```\n\nSee also:\n\n- [`useMatches`][use-matches]\n\n## `links`\n\nRoute links define [`<link>` element][link-element]s to be rendered in the document `<head>`.\n\n```tsx\nexport function links() {\n  return [\n    {\n      rel: \"icon\",\n      href: \"/favicon.png\",\n      type: \"image/png\",\n    },\n    {\n      rel: \"stylesheet\",\n      href: \"https://example.com/some/styles.css\",\n    },\n    {\n      rel: \"preload\",\n      href: \"/images/banner.jpg\",\n      as: \"image\",\n    },\n  ];\n}\n```\n\nAll routes links will be aggregated and rendered through the `<Links />` component, usually rendered in your app root:\n\n```tsx\nimport { Links } from \"react-router\";\n\nexport default function Root() {\n  return (\n    <html>\n      <head>\n        <Links />\n      </head>\n\n      <body />\n    </html>\n  );\n}\n```\n\n## `meta`\n\nRoute meta defines [meta tags][meta-element] to be rendered in the `<Meta />` component, usually placed in the `<head>`.\n\n<docs-warning>\n\nSince React 19, [using the built-in `<meta>` element](https://react.dev/reference/react-dom/components/meta) is recommended over the use of the route module's `meta` export.\n\nHere is an example of how to use it and the `<title>` element:\n\n```tsx\nexport default function MyRoute() {\n  return (\n    <div>\n      <title>Very cool app</title>\n      <meta property=\"og:title\" content=\"Very cool app\" />\n      <meta\n        name=\"description\"\n        content=\"This app is the best\"\n      />\n      {/* The rest of your route content... */}\n    </div>\n  );\n}\n```\n\n</docs-warning>\n\n```tsx filename=app/product.tsx\nexport function meta() {\n  return [\n    { title: \"Very cool app\" },\n    {\n      property: \"og:title\",\n      content: \"Very cool app\",\n    },\n    {\n      name: \"description\",\n      content: \"This app is the best\",\n    },\n  ];\n}\n```\n\n```tsx filename=app/root.tsx\nimport { Meta } from \"react-router\";\n\nexport default function Root() {\n  return (\n    <html>\n      <head>\n        <Meta />\n      </head>\n\n      <body />\n    </html>\n  );\n}\n```\n\nThe meta of the last matching route is used, allowing you to override parent routes' meta. It's important to note that the entire meta descriptor array is replaced, not merged. This gives you the flexibility to build your own meta composition logic across pages at different levels.\n\n**See also**\n\n- [`meta` params][meta-params]\n- [`meta` function return types][meta-function]\n\n## `shouldRevalidate`\n\nIn framework mode with SSR, route loaders are automatically revalidated after all navigations and form submissions (this is different from [Data Mode][data-mode-should-revalidate]). This enables middleware and loaders to share a request context and optimize in different ways than they would in Data Mode.\n\nDefining this function allows you to opt out of revalidation for a route loader for navigations and form submissions.\n\n```tsx\nimport type { ShouldRevalidateFunctionArgs } from \"react-router\";\n\nexport function shouldRevalidate(\n  arg: ShouldRevalidateFunctionArgs,\n) {\n  return true;\n}\n```\n\nWhen using [SPA Mode][spa-mode], there are no server loaders to call on navigations, so `shouldRevalidate` behaves the same as it does in [Data Mode][data-mode-should-revalidate].\n\n[`ShouldRevalidateFunctionArgs` Reference Documentation ↗](https://api.reactrouter.com/v7/interfaces/react-router.ShouldRevalidateFunctionArgs.html)\n\n---\n\nNext: [Rendering Strategies](./rendering)\n\n[middleware-params]: https://api.reactrouter.com/v7/types/react-router.MiddlewareFunction.html\n[middleware]: ../../how-to/middleware\n[when-middleware-runs]: ../../how-to/middleware#when-middleware-runs\n[loader-params]: https://api.reactrouter.com/v7/interfaces/react-router.LoaderFunctionArgs\n[client-loader-params]: https://api.reactrouter.com/v7/types/react-router.ClientLoaderFunctionArgs\n[action-params]: https://api.reactrouter.com/v7/interfaces/react-router.ActionFunctionArgs\n[client-action-params]: https://api.reactrouter.com/v7/types/react-router.ClientActionFunctionArgs\n[use-route-error]: ../../api/hooks/useRouteError\n[is-route-error-response]: ../../api/utils/isRouteErrorResponse\n[headers]: https://developer.mozilla.org/en-US/docs/Web/API/Response/headers\n[use-matches]: ../../api/hooks/useMatches\n[link-element]: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/link\n[meta-element]: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/meta\n[meta-params]: https://api.reactrouter.com/v7/interfaces/react-router.MetaArgs\n[meta-function]: https://api.reactrouter.com/v7/types/react-router.MetaDescriptor.html\n[data-mode-should-revalidate]: ../data/route-object#shouldrevalidate\n[spa-mode]: ../../how-to/spa\n[client-data]: ../../how-to/client-data\n"
  },
  {
    "path": "docs/start/framework/routing.md",
    "content": "---\ntitle: Routing\norder: 2\n---\n\n# Routing\n\n[MODES: framework]\n\n## Configuring Routes\n\nRoutes are configured in `app/routes.ts`. Each route has two required parts: a URL pattern to match the URL, and a file path to the route module that defines its behavior.\n\n```ts filename=app/routes.ts\nimport {\n  type RouteConfig,\n  route,\n} from \"@react-router/dev/routes\";\n\nexport default [\n  route(\"some/path\", \"./some/file.tsx\"),\n  // pattern ^           ^ module file\n] satisfies RouteConfig;\n```\n\nHere is a larger sample route config:\n\n```ts filename=app/routes.ts\nimport {\n  type RouteConfig,\n  route,\n  index,\n  layout,\n  prefix,\n} from \"@react-router/dev/routes\";\n\nexport default [\n  index(\"./home.tsx\"),\n  route(\"about\", \"./about.tsx\"),\n\n  layout(\"./auth/layout.tsx\", [\n    route(\"login\", \"./auth/login.tsx\"),\n    route(\"register\", \"./auth/register.tsx\"),\n  ]),\n\n  ...prefix(\"concerts\", [\n    index(\"./concerts/home.tsx\"),\n    route(\":city\", \"./concerts/city.tsx\"),\n    route(\"trending\", \"./concerts/trending.tsx\"),\n  ]),\n] satisfies RouteConfig;\n```\n\nIf you prefer to define your routes via file naming conventions rather than configuration, the `@react-router/fs-routes` package provides a [file system routing convention][file-route-conventions]. You can even combine different routing conventions if you like:\n\n```ts filename=app/routes.ts\nimport {\n  type RouteConfig,\n  route,\n} from \"@react-router/dev/routes\";\nimport { flatRoutes } from \"@react-router/fs-routes\";\n\nexport default [\n  route(\"/\", \"./home.tsx\"),\n\n  ...(await flatRoutes()),\n] satisfies RouteConfig;\n```\n\n## Route Modules\n\nThe files referenced in `routes.ts` define each route's behavior:\n\n```tsx filename=app/routes.ts\nroute(\"teams/:teamId\", \"./team.tsx\"),\n//           route module ^^^^^^^^\n```\n\nHere's a sample route module:\n\n```tsx filename=app/team.tsx\n// provides type safety/inference\nimport type { Route } from \"./+types/team\";\n\n// provides `loaderData` to the component\nexport async function loader({ params }: Route.LoaderArgs) {\n  let team = await fetchTeam(params.teamId);\n  return { name: team.name };\n}\n\n// renders after the loader is done\nexport default function Component({\n  loaderData,\n}: Route.ComponentProps) {\n  return <h1>{loaderData.name}</h1>;\n}\n```\n\nRoute modules have more features like actions, headers, and error boundaries, but they will be covered in the next guide: [Route Modules](./route-module)\n\n## Nested Routes\n\nRoutes can be nested inside parent routes.\n\n```ts filename=app/routes.ts\nimport {\n  type RouteConfig,\n  route,\n  index,\n} from \"@react-router/dev/routes\";\n\nexport default [\n  // parent route\n  route(\"dashboard\", \"./dashboard.tsx\", [\n    // child routes\n    index(\"./home.tsx\"),\n    route(\"settings\", \"./settings.tsx\"),\n  ]),\n] satisfies RouteConfig;\n```\n\nThe path of the parent is automatically included in the child, so this config creates both `\"/dashboard\"` and `\"/dashboard/settings\"` URLs.\n\nChild routes are rendered through the `<Outlet/>` in the parent route.\n\n```tsx filename=app/dashboard.tsx\nimport { Outlet } from \"react-router\";\n\nexport default function Dashboard() {\n  return (\n    <div>\n      <h1>Dashboard</h1>\n      {/* will either be home.tsx or settings.tsx */}\n      <Outlet />\n    </div>\n  );\n}\n```\n\n## Root Route\n\nEvery route in `routes.ts` is nested inside the special `app/root.tsx` module.\n\n## Layout Routes\n\nUsing `layout`, layout routes create new nesting for their children, but they don't add any segments to the URL. It's like the root route but they can be added at any level.\n\n```tsx filename=app/routes.ts lines=[10,16]\nimport {\n  type RouteConfig,\n  route,\n  layout,\n  index,\n  prefix,\n} from \"@react-router/dev/routes\";\n\nexport default [\n  layout(\"./marketing/layout.tsx\", [\n    index(\"./marketing/home.tsx\"),\n    route(\"contact\", \"./marketing/contact.tsx\"),\n  ]),\n  ...prefix(\"projects\", [\n    index(\"./projects/home.tsx\"),\n    layout(\"./projects/project-layout.tsx\", [\n      route(\":pid\", \"./projects/project.tsx\"),\n      route(\":pid/edit\", \"./projects/edit-project.tsx\"),\n    ]),\n  ]),\n] satisfies RouteConfig;\n```\n\nNote that:\n\n- `home.tsx` and `contact.tsx` will be rendered into the `marketing/layout.tsx` outlet without creating any new URL paths\n- `project.tsx` and `edit-project.tsx` will be rendered into the `projects/project-layout.tsx` outlet at `/projects/:pid` and `/projects/:pid/edit` while `projects/home.tsx` will not.\n\n## Index Routes\n\n```ts\nindex(componentFile),\n```\n\nIndex routes render into their parent's [Outlet][outlet] at their parent's URL (like a default child route).\n\n```ts filename=app/routes.ts\nimport {\n  type RouteConfig,\n  route,\n  index,\n} from \"@react-router/dev/routes\";\n\nexport default [\n  // renders into the root.tsx Outlet at /\n  index(\"./home.tsx\"),\n  route(\"dashboard\", \"./dashboard.tsx\", [\n    // renders into the dashboard.tsx Outlet at /dashboard\n    index(\"./dashboard-home.tsx\"),\n    route(\"settings\", \"./dashboard-settings.tsx\"),\n  ]),\n] satisfies RouteConfig;\n```\n\nNote that index routes can't have children.\n\n## Route Prefixes\n\nUsing `prefix`, you can add a path prefix to a set of routes without needing to introduce a parent route.\n\n```tsx filename=app/routes.ts lines=[14]\nimport {\n  type RouteConfig,\n  route,\n  layout,\n  index,\n  prefix,\n} from \"@react-router/dev/routes\";\n\nexport default [\n  layout(\"./marketing/layout.tsx\", [\n    index(\"./marketing/home.tsx\"),\n    route(\"contact\", \"./marketing/contact.tsx\"),\n  ]),\n  ...prefix(\"projects\", [\n    index(\"./projects/home.tsx\"),\n    layout(\"./projects/project-layout.tsx\", [\n      route(\":pid\", \"./projects/project.tsx\"),\n      route(\":pid/edit\", \"./projects/edit-project.tsx\"),\n    ]),\n  ]),\n] satisfies RouteConfig;\n```\n\nNote that this does not introduce a new route into the route tree. Instead, it merely modifies the paths of its children.\n\nFor example, these two sets of routes are equivalent:\n\n```ts filename=app/routes.ts\n// This usage of `prefix`...\nprefix(\"parent\", [\n  route(\"child1\", \"./child1.tsx\"),\n  route(\"child2\", \"./child2.tsx\"),\n])\n\n// ...is equivalent to this:\n[\n  route(\"parent/child1\", \"./child1.tsx\"),\n  route(\"parent/child2\", \"./child2.tsx\"),\n]\n```\n\n## Dynamic Segments\n\nIf a path segment starts with `:` then it becomes a \"dynamic segment\". When the route matches the URL, the dynamic segment will be parsed from the URL and provided as `params` to other router APIs.\n\n```ts filename=app/routes.ts\nroute(\"teams/:teamId\", \"./team.tsx\"),\n```\n\n```tsx filename=app/team.tsx\nimport type { Route } from \"./+types/team\";\n\nexport async function loader({ params }: Route.LoaderArgs) {\n  //                           ^? { teamId: string }\n}\n\nexport default function Component({\n  params,\n}: Route.ComponentProps) {\n  params.teamId;\n  //        ^ string\n}\n```\n\nYou can have multiple dynamic segments in one route path:\n\n```ts filename=app/routes.ts\nroute(\"c/:categoryId/p/:productId\", \"./product.tsx\"),\n```\n\n```tsx filename=app/product.tsx\nimport type { Route } from \"./+types/product\";\n\nasync function loader({ params }: LoaderArgs) {\n  //                    ^? { categoryId: string; productId: string }\n}\n```\n\n## Optional Segments\n\nYou can make a route segment optional by adding a `?` to the end of the segment.\n\n```ts filename=app/routes.ts\nroute(\":lang?/categories\", \"./categories.tsx\"),\n```\n\nYou can have optional static segments, too:\n\n```ts filename=app/routes.ts\nroute(\"users/:userId/edit?\", \"./user.tsx\");\n```\n\n## Splats\n\nAlso known as \"catchall\" and \"star\" segments. If a route path pattern ends with `/*` then it will match any characters following the `/`, including other `/` characters.\n\n```ts filename=app/routes.ts\nroute(\"files/*\", \"./files.tsx\"),\n```\n\n```tsx filename=app/files.tsx\nexport async function loader({ params }: Route.LoaderArgs) {\n  // params[\"*\"] will contain the remaining URL after files/\n}\n```\n\nYou can destructure the `*`, you just have to assign it a new name. A common name is `splat`:\n\n```tsx\nconst { \"*\": splat } = params;\n```\n\nYou can also use a splat to catch requests that don't match any route:\n\n```ts filename=app/routes.ts\nroute(\"*\", \"./catchall.tsx\"); // catchall route,\n```\n\n```tsx filename=app/catchall.tsx\nexport function loader() {\n  throw new Response(\"Page not found\", { status: 404 });\n}\n```\n\n## Component Routes\n\nYou can also use components that match the URL to elements anywhere in the component tree:\n\n```tsx\nimport { Routes, Route } from \"react-router\";\n\nfunction Wizard() {\n  return (\n    <div>\n      <h1>Some Wizard with Steps</h1>\n      <Routes>\n        <Route index element={<StepOne />} />\n        <Route path=\"step-2\" element={<StepTwo />} />\n        <Route path=\"step-3\" element={<StepThree />} />\n      </Routes>\n    </div>\n  );\n}\n```\n\nNote that these routes do not participate in data loading, actions, code splitting, or any other route module features, so their use cases are more limited than those of the route module.\n\n---\n\nNext: [Route Module](./route-module)\n\n[file-route-conventions]: ../../how-to/file-route-conventions\n[outlet]: https://api.reactrouter.com/v7/functions/react-router.Outlet.html\n"
  },
  {
    "path": "docs/start/framework/testing.md",
    "content": "---\ntitle: Testing\norder: 9\n---\n\n# Testing\n\n[MODES: framework, data]\n\n## Introduction\n\nWhen components use things like `useLoaderData`, `<Link>`, etc, they are required to be rendered in context of a React Router app. The `createRoutesStub` function creates that context to test components in isolation.\n\nConsider a login form component that relies on `useActionData`\n\n```tsx\nimport { useActionData } from \"react-router\";\n\nexport function LoginForm() {\n  const actionData = useActionData();\n  const errors = actionData?.errors;\n  return (\n    <Form method=\"post\">\n      <label>\n        <input type=\"text\" name=\"username\" />\n        {errors?.username && <div>{errors.username}</div>}\n      </label>\n\n      <label>\n        <input type=\"password\" name=\"password\" />\n        {errors?.password && <div>{errors.password}</div>}\n      </label>\n\n      <button type=\"submit\">Login</button>\n    </Form>\n  );\n}\n```\n\nWe can test this component with `createRoutesStub`. It takes an array of objects that resemble route modules with loaders, actions, and components.\n\n```tsx\nimport { createRoutesStub } from \"react-router\";\nimport {\n  render,\n  screen,\n  waitFor,\n} from \"@testing-library/react\";\nimport userEvent from \"@testing-library/user-event\";\nimport { LoginForm } from \"./LoginForm\";\n\ntest(\"LoginForm renders error messages\", async () => {\n  const USER_MESSAGE = \"Username is required\";\n  const PASSWORD_MESSAGE = \"Password is required\";\n\n  const Stub = createRoutesStub([\n    {\n      path: \"/login\",\n      Component: LoginForm,\n      action() {\n        return {\n          errors: {\n            username: USER_MESSAGE,\n            password: PASSWORD_MESSAGE,\n          },\n        };\n      },\n    },\n  ]);\n\n  // render the app stub at \"/login\"\n  render(<Stub initialEntries={[\"/login\"]} />);\n\n  // simulate interactions\n  userEvent.click(screen.getByText(\"Login\"));\n  await waitFor(() => screen.findByText(USER_MESSAGE));\n  await waitFor(() => screen.findByText(PASSWORD_MESSAGE));\n});\n```\n\n## Using with Framework Mode Types\n\nIt's important to note that `createRoutesStub` is designed for _unit_ testing of reusable components in your application that rely on on contextual router information (i.e., `loaderData`, `actionData`, `matches`). These components usually obtain this information via the hooks (`useLoaderData`, `useActionData`, `useMatches`) or via props passed down from the ancestor route component. We **strongly** recommend limiting your usage of `createRoutesStub` to unit testing of these types of reusable components.\n\n`createRoutesStub` is _not designed_ for (and is arguably incompatible with) direct testing of Route components using the [`Route.\\*`](../../explanation/type-safety) types available in Framework Mode. This is because the `Route.*` types are derived from your actual application - including the real `loader`/`action` functions as well as the structure of your route tree structure (which defines the `matches` type). When you use `createRoutesStub`, you are providing stubbed values for `loaderData`, `actionData`, and even your `matches` based on the route tree you pass to `createRoutesStub`. Therefore, the types won't align with the `Route.*` types and you'll get type issues trying to use a route component in a route stub.\n\n```tsx filename=routes/login.tsx\nexport default function Login({\n  actionData,\n}: Route.ComponentProps) {\n  return <Form method=\"post\">...</Form>;\n}\n```\n\n```tsx filename=routes/login.test.tsx\nimport LoginRoute from \"./login\";\n\ntest(\"LoginRoute renders error messages\", async () => {\n  const Stub = createRoutesStub([\n    {\n      path: \"/login\",\n      Component: LoginRoute,\n      // ^ ❌ Types of property 'matches' are incompatible.\n      action() {\n        /*...*/\n      },\n    },\n  ]);\n\n  // ...\n});\n```\n\nThese type errors are generally accurate if you try to setup your tests like this. As long as your stubbed `loader`/`action` functions match your real implementations, then the types for `loaderData`/`actionData` will be correct, but if they differ your types will be lying to you.\n\n`matches` is more complicated since you don't usually stub out all of the ancestor routes. In this example, there is no `root` route so `matches` will only contain your test route, while it will contain the root route and any other ancestors at runtime. There's no great way to automatically align the typegen types with the runtime types in your test.\n\nTherefore, if you need to test Route level components, we recommend you do that via an Integration/E2E test (Playwright, Cypress, etc.) against a running application because you're venturing out of unit testing territory when testing your route as a whole.\n\nIf you _need_ to write a unit test against the route, you can add a `@ts-expect-error` comment in your test to silence the TypeScript error:\n\n```tsx\nconst Stub = createRoutesStub([\n  {\n    path: \"/login\",\n    // @ts-expect-error: `matches` won't align between test code and app code\n    Component: LoginRoute,\n    action() {\n      /*...*/\n    },\n  },\n]);\n```\n"
  },
  {
    "path": "docs/start/index.md",
    "content": "---\ntitle: Getting Started\norder: 1\n---\n"
  },
  {
    "path": "docs/start/modes.md",
    "content": "---\ntitle: Picking a Mode\norder: 1\n---\n\n# Picking a Mode\n\nReact Router is a multi-strategy router for React. There are three primary ways, or \"modes\", to use it in your app. Across the docs you'll see these icons indicating which mode the content is relevant to:\n\n[MODES: framework, data, declarative]\n\n<p></p>\n\nThe features available in each mode are additive, so moving from Declarative to Data to Framework simply adds more features at the cost of architectural control. So pick your mode based on how much control or how much help you want from React Router.\n\nThe mode depends on which \"top level\" router API you're using:\n\n## Declarative\n\nDeclarative mode enables basic routing features like matching URLs to components, navigating around the app, and providing active states with APIs like `<Link>`, `useNavigate`, and `useLocation`.\n\n```tsx\nimport { BrowserRouter } from \"react-router\";\n\nReactDOM.createRoot(root).render(\n  <BrowserRouter>\n    <App />\n  </BrowserRouter>,\n);\n```\n\n## Data\n\nBy moving route configuration outside of React rendering, Data Mode adds data loading, actions, pending states and more with APIs like `loader`, `action`, and `useFetcher`.\n\n```tsx\nimport {\n  createBrowserRouter,\n  RouterProvider,\n} from \"react-router\";\n\nlet router = createBrowserRouter([\n  {\n    path: \"/\",\n    Component: Root,\n    loader: loadRootData,\n  },\n]);\n\nReactDOM.createRoot(root).render(\n  <RouterProvider router={router} />,\n);\n```\n\n## Framework\n\nFramework Mode wraps Data Mode with a Vite plugin to add the full React Router experience with:\n\n- type-safe `href`\n- type-safe Route Module API\n- intelligent code splitting\n- SPA, SSR, and static rendering strategies\n- and more\n\n```ts filename=routes.ts\nimport { index, route } from \"@react-router/dev/routes\";\n\nexport default [\n  index(\"./home.tsx\"),\n  route(\"products/:pid\", \"./product.tsx\"),\n];\n```\n\nYou'll then have access to the Route Module API with type-safe params, loaderData, code splitting, SPA/SSR/SSG strategies, and more.\n\n```ts filename=product.tsx\nimport { Route } from \"./+types/product.tsx\";\n\nexport async function loader({ params }: Route.LoaderArgs) {\n  let product = await getProduct(params.pid);\n  return { product };\n}\n\nexport default function Product({\n  loaderData,\n}: Route.ComponentProps) {\n  return <div>{loaderData.product.name}</div>;\n}\n```\n\n## Decision Advice\n\nEvery mode supports any architecture and deployment target, so the question isn't really about if you want SSR, SPA, etc. It's about how much you want to do yourself.\n\n**Use Framework Mode if you:**\n\n- are too new to have an opinion\n- are considering Next.js, Solid Start, SvelteKit, Astro, TanStack Start, etc. and want to compare\n- just want to build something with React\n- might want to server render, might not\n- are coming from Remix (React Router v7 is the \"next version\" after Remix v2)\n- are migrating from Next.js\n\n[→ Get Started with Framework Mode](./framework/installation).\n\n**Use Data Mode if you:**\n\n- want data features but also want to have control over bundling, data, and server abstractions\n- started a data router in v6.4 and are happy with it\n\n[→ Get Started with Data Mode](./data/custom).\n\n**Use Declarative Mode if you:**\n\n- want to use React Router as simply as possible\n- are coming from v6 and are happy with the `<BrowserRouter>`\n- have a data layer that either skips pending states (like local first, background data replication/sync) or has its own abstractions for them\n- are coming from Create React App (you may want to consider framework mode though)\n\n[→ Get Started with Declarative Mode](./declarative/installation).\n\n## API + Mode Availability Table\n\nThis is mostly for the LLMs, but knock yourself out:\n\n| API                            | Framework | Data | Declarative |\n| ------------------------------ | --------- | ---- | ----------- |\n| Await                          | ✅        | ✅   |             |\n| Form                           | ✅        | ✅   |\n| Link                           | ✅        | ✅   | ✅          |\n| `<Link discover>`              | ✅        |      |             |\n| `<Link prefetch>`              | ✅        |      |             |\n| `<Link preventScrollReset>`    | ✅        | ✅   |             |\n| Links                          | ✅        |      |             |\n| Meta                           | ✅        |      |             |\n| NavLink                        | ✅        | ✅   | ✅          |\n| `<NavLink discover>`           | ✅        |      |             |\n| `<NavLink prefetch>`           | ✅        |      |             |\n| `<NavLink preventScrollReset>` | ✅        | ✅   |             |\n| NavLink `isPending`            | ✅        | ✅   |             |\n| Navigate                       | ✅        | ✅   | ✅          |\n| Outlet                         | ✅        | ✅   | ✅          |\n| PrefetchPageLinks              | ✅        |      |             |\n| Route                          | ✅        | ✅   | ✅          |\n| Routes                         | ✅        | ✅   | ✅          |\n| Scripts                        | ✅        |      |             |\n| ScrollRestoration              | ✅        | ✅   |             |\n| ServerRouter                   | ✅        |      |             |\n| usePrompt                      | ✅        | ✅   |             |\n| useActionData                  | ✅        | ✅   |             |\n| useAsyncError                  | ✅        | ✅   |             |\n| useAsyncValue                  | ✅        | ✅   |             |\n| useBeforeUnload                | ✅        | ✅   | ✅          |\n| useBlocker                     | ✅        | ✅   |             |\n| useFetcher                     | ✅        | ✅   |             |\n| useFetchers                    | ✅        | ✅   |             |\n| useFormAction                  | ✅        | ✅   |             |\n| useHref                        | ✅        | ✅   | ✅          |\n| useInRouterContext             | ✅        | ✅   | ✅          |\n| useLinkClickHandler            | ✅        | ✅   | ✅          |\n| useLoaderData                  | ✅        | ✅   |             |\n| useLocation                    | ✅        | ✅   | ✅          |\n| useMatch                       | ✅        | ✅   | ✅          |\n| useMatches                     | ✅        | ✅   |             |\n| useNavigate                    | ✅        | ✅   | ✅          |\n| useNavigation                  | ✅        | ✅   |             |\n| useNavigationType              | ✅        | ✅   | ✅          |\n| useOutlet                      | ✅        | ✅   | ✅          |\n| useOutletContext               | ✅        | ✅   | ✅          |\n| useParams                      | ✅        | ✅   | ✅          |\n| useResolvedPath                | ✅        | ✅   | ✅          |\n| useRevalidator                 | ✅        | ✅   |             |\n| useRouteError                  | ✅        | ✅   |             |\n| useRouteLoaderData             | ✅        | ✅   |             |\n| useRoutes                      | ✅        | ✅   | ✅          |\n| useSearchParams                | ✅        | ✅   | ✅          |\n| useSubmit                      | ✅        | ✅   |             |\n| useViewTransitionState         | ✅        | ✅   |             |\n| isCookieFunction               | ✅        | ✅   |             |\n| isSessionFunction              | ✅        | ✅   |             |\n| createCookie                   | ✅        | ✅   |             |\n| createCookieSessionStorage     | ✅        | ✅   |             |\n| createMemorySessionStorage     | ✅        | ✅   |             |\n| createPath                     | ✅        | ✅   | ✅          |\n| createRoutesFromElements       |           | ✅   |             |\n| createRoutesStub               | ✅        | ✅   |             |\n| createSearchParams             | ✅        | ✅   | ✅          |\n| data                           | ✅        | ✅   |             |\n| generatePath                   | ✅        | ✅   | ✅          |\n| href                           | ✅        |      |             |\n| isCookie                       | ✅        | ✅   |             |\n| isRouteErrorResponse           | ✅        | ✅   |             |\n| isSession                      | ✅        | ✅   |             |\n| matchPath                      | ✅        | ✅   | ✅          |\n| matchRoutes                    | ✅        | ✅   | ✅          |\n| parsePath                      | ✅        | ✅   | ✅          |\n| redirect                       | ✅        | ✅   |             |\n| redirectDocument               | ✅        | ✅   |             |\n| renderMatches                  | ✅        | ✅   | ✅          |\n| replace                        | ✅        | ✅   |             |\n| resolvePath                    | ✅        | ✅   | ✅          |\n"
  },
  {
    "path": "docs/tutorials/README",
    "content": "Tutorials:\n\n- Practical Steps\n- Learning Oriented\n- Useful when we're studying\n"
  },
  {
    "path": "docs/tutorials/address-book.md",
    "content": "---\ntitle: Address Book\norder: 2\n---\n\n# Address Book\n\n[MODES: framework]\n\n<br />\n<br />\n\nWe'll be building a small, but feature-rich address book app that lets you keep track of your contacts. There's no database or other \"production ready\" things, so we can stay focused on the features React Router gives you. We expect it to take 30-45m if you're following along, otherwise it's a quick read.\n\n<docs-info>\n\nYou can also watch our [walkthrough of the React Router Tutorial](https://www.youtube.com/watch?v=pw8FAg07kdo) if you prefer 🎥\n\n</docs-info>\n\n<img class=\"tutorial\" src=\"/_docs/v7_address_book_tutorial/01.webp\" />\n\n👉 **Every time you see this it means you need to do something in the app!**\n\nThe rest is just there for your information and deeper understanding. Let's get to it.\n\n## Setup\n\n👉 **Generate a basic template**\n\n```shellscript nonumber\nnpx create-react-router@latest --template remix-run/react-router/tutorials/address-book\n```\n\nThis uses a pretty bare-bones template but includes our css and data model, so we can focus on React Router.\n\n👉 **Start the app**\n\n```shellscript nonumber\n# cd into the app directory\ncd {wherever you put the app}\n\n# install dependencies if you haven't already\nnpm install\n\n# start the server\nnpm run dev\n```\n\nYou should now be able to open up [http://localhost:5173][http-localhost-5173] and see your app running, though there's not much going on just yet.\n\n## The Root Route\n\nNote the file at `app/root.tsx`. This is what we call the [\"Root Route\"][root-route]. It's the first component in the UI that renders, so it typically contains the global layout for the page, as well as a the default [Error Boundary][error-boundaries].\n\n<details>\n\n<summary>Expand here to see the root component code</summary>\n\n```tsx filename=app/root.tsx\nimport {\n  Form,\n  Scripts,\n  ScrollRestoration,\n  isRouteErrorResponse,\n} from \"react-router\";\nimport type { Route } from \"./+types/root\";\n\nimport appStylesHref from \"./app.css?url\";\n\nexport default function App() {\n  return (\n    <>\n      <div id=\"sidebar\">\n        <h1>React Router Contacts</h1>\n        <div>\n          <Form id=\"search-form\" role=\"search\">\n            <input\n              aria-label=\"Search contacts\"\n              id=\"q\"\n              name=\"q\"\n              placeholder=\"Search\"\n              type=\"search\"\n            />\n            <div\n              aria-hidden\n              hidden={true}\n              id=\"search-spinner\"\n            />\n          </Form>\n          <Form method=\"post\">\n            <button type=\"submit\">New</button>\n          </Form>\n        </div>\n        <nav>\n          <ul>\n            <li>\n              <a href={`/contacts/1`}>Your Name</a>\n            </li>\n            <li>\n              <a href={`/contacts/2`}>Your Friend</a>\n            </li>\n          </ul>\n        </nav>\n      </div>\n    </>\n  );\n}\n\n// The Layout component is a special export for the root route.\n// It acts as your document's \"app shell\" for all route components, HydrateFallback, and ErrorBoundary\n// For more information, see https://reactrouter.com/explanation/special-files#layout-export\nexport function Layout({\n  children,\n}: {\n  children: React.ReactNode;\n}) {\n  return (\n    <html lang=\"en\">\n      <head>\n        <meta charSet=\"utf-8\" />\n        <meta\n          name=\"viewport\"\n          content=\"width=device-width, initial-scale=1\"\n        />\n        <link rel=\"stylesheet\" href={appStylesHref} />\n      </head>\n      <body>\n        {children}\n        <ScrollRestoration />\n        <Scripts />\n      </body>\n    </html>\n  );\n}\n\n// The top most error boundary for the app, rendered when your app throws an error\n// For more information, see https://reactrouter.com/start/framework/route-module#errorboundary\nexport function ErrorBoundary({\n  error,\n}: Route.ErrorBoundaryProps) {\n  let message = \"Oops!\";\n  let details = \"An unexpected error occurred.\";\n  let stack: string | undefined;\n\n  if (isRouteErrorResponse(error)) {\n    message = error.status === 404 ? \"404\" : \"Error\";\n    details =\n      error.status === 404\n        ? \"The requested page could not be found.\"\n        : error.statusText || details;\n  } else if (\n    import.meta.env.DEV &&\n    error &&\n    error instanceof Error\n  ) {\n    details = error.message;\n    stack = error.stack;\n  }\n\n  return (\n    <main id=\"error-page\">\n      <h1>{message}</h1>\n      <p>{details}</p>\n      {stack && (\n        <pre>\n          <code>{stack}</code>\n        </pre>\n      )}\n    </main>\n  );\n}\n```\n\n</details>\n\n## The Contact Route UI\n\nIf you click on one of the sidebar items you'll get the default 404 page. Let's create a route that matches the url `/contacts/1`.\n\n👉 **Create a contact route module**\n\n```shellscript nonumber\nmkdir app/routes\ntouch app/routes/contact.tsx\n```\n\nWe could put this file anywhere we want, but to make things a bit more organized, we'll put all our routes inside the `app/routes` directory.\n\nYou can also use [file-based routing if you prefer][file-route-conventions].\n\n👉 **Configure the route**\n\nWe need to tell React Router about our new route. `routes.ts` is a special file where we can configure all our routes.\n\n```tsx filename=routes.ts lines=[2,5]\nimport type { RouteConfig } from \"@react-router/dev/routes\";\nimport { route } from \"@react-router/dev/routes\";\n\nexport default [\n  route(\"contacts/:contactId\", \"routes/contact.tsx\"),\n] satisfies RouteConfig;\n```\n\nIn React Router, `:` makes a segment dynamic. We just made the following urls match the `routes/contact.tsx` route module:\n\n- `/contacts/123`\n- `/contacts/abc`\n\n👉 **Add the contact component UI**\n\nIt's just a bunch of elements, feel free to copy/paste.\n\n```tsx filename=app/routes/contact.tsx\nimport { Form } from \"react-router\";\n\nimport type { ContactRecord } from \"../data\";\n\nexport default function Contact() {\n  const contact = {\n    first: \"Your\",\n    last: \"Name\",\n    avatar: \"https://placecats.com/200/200\",\n    twitter: \"your_handle\",\n    notes: \"Some notes\",\n    favorite: true,\n  };\n\n  return (\n    <div id=\"contact\">\n      <div>\n        <img\n          alt={`${contact.first} ${contact.last} avatar`}\n          key={contact.avatar}\n          src={contact.avatar}\n        />\n      </div>\n\n      <div>\n        <h1>\n          {contact.first || contact.last ? (\n            <>\n              {contact.first} {contact.last}\n            </>\n          ) : (\n            <i>No Name</i>\n          )}\n          <Favorite contact={contact} />\n        </h1>\n\n        {contact.twitter ? (\n          <p>\n            <a\n              href={`https://twitter.com/${contact.twitter}`}\n            >\n              {contact.twitter}\n            </a>\n          </p>\n        ) : null}\n\n        {contact.notes ? <p>{contact.notes}</p> : null}\n\n        <div>\n          <Form action=\"edit\">\n            <button type=\"submit\">Edit</button>\n          </Form>\n\n          <Form\n            action=\"destroy\"\n            method=\"post\"\n            onSubmit={(event) => {\n              const response = confirm(\n                \"Please confirm you want to delete this record.\",\n              );\n              if (!response) {\n                event.preventDefault();\n              }\n            }}\n          >\n            <button type=\"submit\">Delete</button>\n          </Form>\n        </div>\n      </div>\n    </div>\n  );\n}\n\nfunction Favorite({\n  contact,\n}: {\n  contact: Pick<ContactRecord, \"favorite\">;\n}) {\n  const favorite = contact.favorite;\n\n  return (\n    <Form method=\"post\">\n      <button\n        aria-label={\n          favorite\n            ? \"Remove from favorites\"\n            : \"Add to favorites\"\n        }\n        name=\"favorite\"\n        value={favorite ? \"false\" : \"true\"}\n      >\n        {favorite ? \"★\" : \"☆\"}\n      </button>\n    </Form>\n  );\n}\n```\n\nNow if we click one of the links or visit [`/contacts/1`][contacts-1] we get ... nothing new?\n\n<img class=\"tutorial\" src=\"/_docs/v7_address_book_tutorial/02.webp\" />\n\n## Nested Routes and Outlets\n\nReact Router supports nested routing. In order for child routes to render inside of parent layouts, we need to render an [`Outlet`][outlet-component] in the parent. Let's fix it, open up `app/root.tsx` and render an outlet inside.\n\n👉 **Render an [`<Outlet />`][outlet-component]**\n\n```tsx filename=app/root.tsx lines=[3,15-17]\nimport {\n  Form,\n  Outlet,\n  Scripts,\n  ScrollRestoration,\n  isRouteErrorResponse,\n} from \"react-router\";\n\n// existing imports & exports\n\nexport default function App() {\n  return (\n    <>\n      <div id=\"sidebar\">{/* other elements */}</div>\n      <div id=\"detail\">\n        <Outlet />\n      </div>\n    </>\n  );\n}\n```\n\nNow the child route should be rendering through the outlet.\n\n<img class=\"tutorial\" loading=\"lazy\" src=\"/_docs/v7_address_book_tutorial/03.webp\" />\n\n## Client Side Routing\n\nYou may or may not have noticed, but when we click the links in the sidebar, the browser is doing a full document request for the next URL instead of client side routing, which completely remounts our app.\n\nClient side routing allows our app to update the URL without reloading the entire page. Instead, the app can immediately render new UI. Let's make it happen with [`<Link>`][link-component].\n\n👉 **Change the sidebar `<a href>` to `<Link to>`**\n\n```tsx filename=app/root.tsx lines=[3,20,23]\nimport {\n  Form,\n  Link,\n  Outlet,\n  Scripts,\n  ScrollRestoration,\n  isRouteErrorResponse,\n} from \"react-router\";\n\n// existing imports & exports\n\nexport default function App() {\n  return (\n    <>\n      <div id=\"sidebar\">\n        {/* other elements */}\n        <nav>\n          <ul>\n            <li>\n              <Link to={`/contacts/1`}>Your Name</Link>\n            </li>\n            <li>\n              <Link to={`/contacts/2`}>Your Friend</Link>\n            </li>\n          </ul>\n        </nav>\n      </div>\n      {/* other elements */}\n    </>\n  );\n}\n```\n\nYou can open the network tab in the browser devtools to see that it's not requesting documents anymore.\n\n## Loading Data\n\nURL segments, layouts, and data are more often than not coupled (tripled?) together. We can see it in this app already:\n\n| URL Segment         | Component   | Data               |\n| ------------------- | ----------- | ------------------ |\n| /                   | `<App>`     | list of contacts   |\n| contacts/:contactId | `<Contact>` | individual contact |\n\nBecause of this natural coupling, React Router has data conventions to get data into your route components easily.\n\nFirst we'll create and export a [`clientLoader`][client-loader] function in the root route and then render the data.\n\n👉 **Export a `clientLoader` function from `app/root.tsx` and render the data**\n\n<docs-info>The following code has a type error in it, we'll fix it in the next section</docs-info>\n\n```tsx filename=app/root.tsx lines=[2,6-9,11-12,19-42]\n// existing imports\nimport { getContacts } from \"./data\";\n\n// existing exports\n\nexport async function clientLoader() {\n  const contacts = await getContacts();\n  return { contacts };\n}\n\nexport default function App({ loaderData }) {\n  const { contacts } = loaderData;\n\n  return (\n    <>\n      <div id=\"sidebar\">\n        {/* other elements */}\n        <nav>\n          {contacts.length ? (\n            <ul>\n              {contacts.map((contact) => (\n                <li key={contact.id}>\n                  <Link to={`contacts/${contact.id}`}>\n                    {contact.first || contact.last ? (\n                      <>\n                        {contact.first} {contact.last}\n                      </>\n                    ) : (\n                      <i>No Name</i>\n                    )}\n                    {contact.favorite ? (\n                      <span>★</span>\n                    ) : null}\n                  </Link>\n                </li>\n              ))}\n            </ul>\n          ) : (\n            <p>\n              <i>No contacts</i>\n            </p>\n          )}\n        </nav>\n      </div>\n      {/* other elements */}\n    </>\n  );\n}\n```\n\nThat's it! React Router will now automatically keep that data in sync with your UI. The sidebar should now look like this:\n\n<img class=\"tutorial\" loading=\"lazy\" src=\"/_docs/v7_address_book_tutorial/04.webp\" />\n\nYou may be wondering why we're \"client\" loading data instead of loading the data on the server so we can do server-side rendering (SSR). Right now our contacts site is a [Single Page App][spa], so there's no server-side rendering. This makes it really easy to deploy to any static hosting provider, but we'll talk more about how to enable SSR in a bit so you can learn about all the different [rendering strategies][rendering-strategies] React Router offers.\n\n## Type Safety\n\nYou probably noticed that we didn't assign a type to the `loaderData` prop. Let's fix that.\n\n👉 **Add the `ComponentProps` type to the `App` component**\n\n```tsx filename=app/root.tsx lines=[5-7]\n// existing imports\nimport type { Route } from \"./+types/root\";\n// existing imports & exports\n\nexport default function App({\n  loaderData,\n}: Route.ComponentProps) {\n  const { contacts } = loaderData;\n\n  // existing code\n}\n```\n\nWait, what? Where did these types come from?!\n\nWe didn't define them, yet somehow they already know about the `contacts` property we returned from our `clientLoader`.\n\nThat's because React Router [generates types for each route in your app][type-safety] to provide automatic type safety.\n\n## Adding a `HydrateFallback`\n\nWe mentioned earlier that we are working on a [Single Page App][spa] with no server-side rendering. If you look inside of [`react-router.config.ts`][react-router-config] you'll see that this is configured with a simple boolean:\n\n```tsx filename=react-router.config.ts lines=[4]\nimport { type Config } from \"@react-router/dev/config\";\n\nexport default {\n  ssr: false,\n} satisfies Config;\n```\n\nYou might have started noticing that whenever you refresh the page you get a flash of white before the app loads. Since we're only rendering on the client, there's nothing to show the user while the app is loading.\n\n👉 **Add a `HydrateFallback` export**\n\nWe can provide a fallback that will show up before the app is hydrated (rendering on the client for the first time) with a [`HydrateFallback`][hydrate-fallback] export.\n\n```tsx filename=app/root.tsx lines=[3-10]\n// existing imports & exports\n\nexport function HydrateFallback() {\n  return (\n    <div id=\"loading-splash\">\n      <div id=\"loading-splash-spinner\" />\n      <p>Loading, please wait...</p>\n    </div>\n  );\n}\n```\n\nNow if you refresh the page, you'll briefly see the loading splash before the app is hydrated.\n\n<img class=\"tutorial\" loading=\"lazy\" src=\"/_docs/v7_address_book_tutorial/05.webp\" />\n\n## Index Routes\n\nWhen you load the app and aren't yet on a contact page, you'll notice a big blank page on the right side of the list.\n\n<img class=\"tutorial\" loading=\"lazy\" src=\"/_docs/v7_address_book_tutorial/06.webp\" />\n\nWhen a route has children, and you're at the parent route's path, the `<Outlet>` has nothing to render because no children match. You can think of [index routes][index-route] as the default child route to fill in that space.\n\n👉 **Create an index route for the root route**\n\n```shellscript nonumber\ntouch app/routes/home.tsx\n```\n\n```ts filename=app/routes.ts lines=[2,5]\nimport type { RouteConfig } from \"@react-router/dev/routes\";\nimport { index, route } from \"@react-router/dev/routes\";\n\nexport default [\n  index(\"routes/home.tsx\"),\n  route(\"contacts/:contactId\", \"routes/contact.tsx\"),\n] satisfies RouteConfig;\n```\n\n👉 **Fill in the index component's elements**\n\nFeel free to copy/paste, nothing special here.\n\n```tsx filename=app/routes/home.tsx\nexport default function Home() {\n  return (\n    <p id=\"index-page\">\n      This is a demo for React Router.\n      <br />\n      Check out{\" \"}\n      <a href=\"https://reactrouter.com\">\n        the docs at reactrouter.com\n      </a>\n      .\n    </p>\n  );\n}\n```\n\n<img class=\"tutorial\" loading=\"lazy\" src=\"/_docs/v7_address_book_tutorial/07.webp\" />\n\nVoilà! No more blank space. It's common to put dashboards, stats, feeds, etc. at index routes. They can participate in data loading as well.\n\n## Adding an About Route\n\nBefore we move on to working with dynamic data that the user can interact with, let's add a page with static content we expect to rarely change. An about page will be perfect for this.\n\n👉 **Create the about route**\n\n```shellscript nonumber\ntouch app/routes/about.tsx\n```\n\nDon't forget to add the route to `app/routes.ts`:\n\n```tsx filename=app/routes.ts lines=[4]\nexport default [\n  index(\"routes/home.tsx\"),\n  route(\"contacts/:contactId\", \"routes/contact.tsx\"),\n  route(\"about\", \"routes/about.tsx\"),\n] satisfies RouteConfig;\n```\n\n👉 **Add the about page UI**\n\nNothing too special here, just copy and paste:\n\n```tsx filename=app/routes/about.tsx\nimport { Link } from \"react-router\";\n\nexport default function About() {\n  return (\n    <div id=\"about\">\n      <Link to=\"/\">← Go to demo</Link>\n      <h1>About React Router Contacts</h1>\n\n      <div>\n        <p>\n          This is a demo application showing off some of the\n          powerful features of React Router, including\n          dynamic routing, nested routes, loaders, actions,\n          and more.\n        </p>\n\n        <h2>Features</h2>\n        <p>\n          Explore the demo to see how React Router handles:\n        </p>\n        <ul>\n          <li>\n            Data loading and mutations with loaders and\n            actions\n          </li>\n          <li>\n            Nested routing with parent/child relationships\n          </li>\n          <li>URL-based routing with dynamic segments</li>\n          <li>Pending and optimistic UI</li>\n        </ul>\n\n        <h2>Learn More</h2>\n        <p>\n          Check out the official documentation at{\" \"}\n          <a href=\"https://reactrouter.com\">\n            reactrouter.com\n          </a>{\" \"}\n          to learn more about building great web\n          applications with React Router.\n        </p>\n      </div>\n    </div>\n  );\n}\n```\n\n👉 **Add a link to the about page in the sidebar**\n\n```tsx filename=app/root.tsx lines=[5-7]\nexport default function App() {\n  return (\n    <>\n      <div id=\"sidebar\">\n        <h1>\n          <Link to=\"about\">React Router Contacts</Link>\n        </h1>\n        {/* other elements */}\n      </div>\n      {/* other elements */}\n    </>\n  );\n}\n```\n\nNow navigate to the [about page][about-page] and it should look like this:\n\n<img class=\"tutorial\" loading=\"lazy\" src=\"/_docs/v7_address_book_tutorial/08.webp\" />\n\n## Layout Routes\n\nWe don't actually want the about page to be nested inside of the sidebar layout. Let's move the sidebar to a layout so we can avoid rendering it on the about page. Additionally, we want to avoid loading all the contacts data on the about page.\n\n👉 **Create a layout route for the sidebar**\n\nYou can name and put this layout route wherever you want, but putting it inside of a `layouts` directory will help keep things organized for our simple app.\n\n```shellscript nonumber\nmkdir app/layouts\ntouch app/layouts/sidebar.tsx\n```\n\nFor now just return an [`<Outlet>`][outlet-component].\n\n```tsx filename=app/layouts/sidebar.tsx\nimport { Outlet } from \"react-router\";\n\nexport default function SidebarLayout() {\n  return <Outlet />;\n}\n```\n\n👉 **Move route definitions under the sidebar layout**\n\nWe can define a `layout` route to automatically render the sidebar for all matched routes within it. This is basically what our `root` was, but now we can scope it to specific routes.\n\n```ts filename=app/routes.ts lines=[4,9,12]\nimport type { RouteConfig } from \"@react-router/dev/routes\";\nimport {\n  index,\n  layout,\n  route,\n} from \"@react-router/dev/routes\";\n\nexport default [\n  layout(\"layouts/sidebar.tsx\", [\n    index(\"routes/home.tsx\"),\n    route(\"contacts/:contactId\", \"routes/contact.tsx\"),\n  ]),\n  route(\"about\", \"routes/about.tsx\"),\n] satisfies RouteConfig;\n```\n\n👉 **Move the layout and data fetching to the sidebar layout**\n\nWe want to move the `clientLoader` and everything inside the `App` component to the sidebar layout. It should look like this:\n\n```tsx filename=app/layouts/sidebar.tsx\nimport { Form, Link, Outlet } from \"react-router\";\nimport { getContacts } from \"../data\";\nimport type { Route } from \"./+types/sidebar\";\n\nexport async function clientLoader() {\n  const contacts = await getContacts();\n  return { contacts };\n}\n\nexport default function SidebarLayout({\n  loaderData,\n}: Route.ComponentProps) {\n  const { contacts } = loaderData;\n\n  return (\n    <>\n      <div id=\"sidebar\">\n        <h1>\n          <Link to=\"about\">React Router Contacts</Link>\n        </h1>\n        <div>\n          <Form id=\"search-form\" role=\"search\">\n            <input\n              aria-label=\"Search contacts\"\n              id=\"q\"\n              name=\"q\"\n              placeholder=\"Search\"\n              type=\"search\"\n            />\n            <div\n              aria-hidden\n              hidden={true}\n              id=\"search-spinner\"\n            />\n          </Form>\n          <Form method=\"post\">\n            <button type=\"submit\">New</button>\n          </Form>\n        </div>\n        <nav>\n          {contacts.length ? (\n            <ul>\n              {contacts.map((contact) => (\n                <li key={contact.id}>\n                  <Link to={`contacts/${contact.id}`}>\n                    {contact.first || contact.last ? (\n                      <>\n                        {contact.first} {contact.last}\n                      </>\n                    ) : (\n                      <i>No Name</i>\n                    )}\n                    {contact.favorite ? (\n                      <span>★</span>\n                    ) : null}\n                  </Link>\n                </li>\n              ))}\n            </ul>\n          ) : (\n            <p>\n              <i>No contacts</i>\n            </p>\n          )}\n        </nav>\n      </div>\n      <div id=\"detail\">\n        <Outlet />\n      </div>\n    </>\n  );\n}\n```\n\nAnd inside `app/root.tsx`, `App` should just return an [`<Outlet>`][outlet-component], and all unused imports can be removed. Make sure there is no `clientLoader` in `root.tsx`.\n\n```tsx filename=app/root.tsx lines=[3-10]\n// existing imports and exports\n\nexport default function App() {\n  return <Outlet />;\n}\n```\n\nNow with that shuffling around done, our about page no longer loads contacts data nor is it nested inside of the sidebar layout:\n\n<img class=\"tutorial\" loading=\"lazy\" src=\"/_docs/v7_address_book_tutorial/09.webp\" />\n\n## Pre-rendering a Static Route\n\nIf you refresh the about page, you still see the loading spinner for just a split second before the page render on the client. This is really not a good experience, plus the page is just static information, we should be able to pre-render it as static HTML at build time.\n\n👉 **Pre-render the about page**\n\nInside of `react-router.config.ts`, we can add a [`prerender`][pre-rendering] array to the config to tell React Router to pre-render certain urls at build time. In this case we just want to pre-render the about page.\n\n```ts filename=react-router.config.ts lines=[5]\nimport { type Config } from \"@react-router/dev/config\";\n\nexport default {\n  ssr: false,\n  prerender: [\"/about\"],\n} satisfies Config;\n```\n\nNow if you go to the [about page][about-page] and refresh, you won't see the loading spinner!\n\n<docs-warning>\n\nIf you're still seeing a spinner when you refresh, make sure you deleted the `clientLoader` in `root.tsx`.\n\n</docs-warning>\n\n## Server-Side Rendering\n\nReact Router is a great framework for building [Single Page Apps][spa]. Many applications are served well by only client-side rendering, and _maybe_ statically pre-rendering a few pages at build time.\n\nIf you ever do want to introduce server-side rendering into your React Router application, it's incredibly easy (remember that `ssr: false` boolean from earlier?).\n\n👉 **Enable server-side rendering**\n\n```ts filename=react-router.config.ts lines=[2]\nexport default {\n  ssr: true,\n  prerender: [\"/about\"],\n} satisfies Config;\n```\n\nAnd now... nothing is different? We're still getting our spinner for a split second before the page renders on the client? Plus, aren't we using `clientLoader`, so our data is still being fetched on the client?\n\nThat's right! With React Router you can still use `clientLoader` (and `clientAction`) to do client-side data fetching where you see fit. React Router gives you a lot of flexibility to use the right tool for the job.\n\nLet's switch to using [`loader`][loader], which (you guessed it) is used to fetch data on the server.\n\n👉 **Switch to using `loader` to fetch data**\n\n```tsx filename=app/layouts/sidebar.tsx lines=[3]\n// existing imports\n\nexport async function loader() {\n  const contacts = await getContacts();\n  return { contacts };\n}\n```\n\nWhether you set `ssr` to `true` or `false` depends on you and your users' needs. Both strategies are perfectly valid. For the remainder of this tutorial we're going to use server-side rendering, but know that all rendering strategies are first class citizens in React Router.\n\n## URL Params in Loaders\n\n👉 **Click on one of the sidebar links**\n\nWe should be seeing our old static contact page again, with one difference: the URL now has a real ID for the record.\n\n<img class=\"tutorial\" loading=\"lazy\" src=\"/_docs/v7_address_book_tutorial/10.webp\" />\n\nRemember the `:contactId` part of the route definition in `app/routes.ts`? These dynamic segments will match dynamic (changing) values in that position of the URL. We call these values in the URL \"URL Params\", or just \"params\" for short.\n\nThese `params` are passed to the loader with keys that match the dynamic segment. For example, our segment is named `:contactId` so the value will be passed as `params.contactId`.\n\nThese params are most often used to find a record by ID. Let's try it out.\n\n👉 **Add a `loader` function to the contact page and access data with `loaderData`**\n\n<docs-info>The following code has type errors in it, we'll fix them in the next section</docs-info>\n\n```tsx filename=app/routes/contact.tsx lines=[2-3,5-8,10-13]\n// existing imports\nimport { getContact } from \"../data\";\nimport type { Route } from \"./+types/contact\";\n\nexport async function loader({ params }: Route.LoaderArgs) {\n  const contact = await getContact(params.contactId);\n  return { contact };\n}\n\nexport default function Contact({\n  loaderData,\n}: Route.ComponentProps) {\n  const { contact } = loaderData;\n\n  // existing code\n}\n\n// existing code\n```\n\n<img class=\"tutorial\" loading=\"lazy\" src=\"/_docs/v7_address_book_tutorial/11.webp\" />\n\n## Throwing Responses\n\nYou'll notice that the type of `loaderData.contact` is `ContactRecord | null`. Based on our automatic type safety, TypeScript already knows that `params.contactId` is a string, but we haven't done anything to make sure it's a valid ID. Since the contact might not exist, `getContact` could return `null`, which is why we have type errors.\n\nWe could account for the possibility of the contact being not found in component code, but the webby thing to do is send a proper 404. We can do that in the loader and solve all of our problems at once.\n\n```tsx filename=app/routes/contact.tsx lines=[5-7]\n// existing imports\n\nexport async function loader({ params }: Route.LoaderArgs) {\n  const contact = await getContact(params.contactId);\n  if (!contact) {\n    throw new Response(\"Not Found\", { status: 404 });\n  }\n  return { contact };\n}\n\n// existing code\n```\n\nNow, if the user isn't found, code execution down this path stops and React Router renders the error path instead. Components in React Router can focus only on the happy path 😁\n\n## Data Mutations\n\nWe'll create our first contact in a second, but first let's talk about HTML.\n\nReact Router emulates HTML Form navigation as the data mutation primitive, which used to be the only way prior to the JavaScript cambrian explosion. Don't be fooled by the simplicity! Forms in React Router give you the UX capabilities of client rendered apps with the simplicity of the \"old school\" web model.\n\nWhile unfamiliar to some web developers, HTML `form`s actually cause a navigation in the browser, just like clicking a link. The only difference is in the request: links can only change the URL while `form`s can also change the request method (`GET` vs. `POST`) and the request body (`POST` form data).\n\nWithout client side routing, the browser will serialize the `form`'s data automatically and send it to the server as the request body for `POST`, and as [`URLSearchParams`][url-search-params] for `GET`. React Router does the same thing, except instead of sending the request to the server, it uses client side routing and sends it to the route's [`action`][action] function.\n\nWe can test this out by clicking the \"New\" button in our app.\n\n<img class=\"tutorial\" loading=\"lazy\" src=\"/_docs/v7_address_book_tutorial/12.webp\" />\n\nReact Router sends a 405 because there is no code on the server to handle this form navigation.\n\n## Creating Contacts\n\nWe'll create new contacts by exporting an `action` function in our root route. When the user clicks the \"new\" button, the form will `POST` to the root route action.\n\n👉 **Export an `action` function from `app/root.tsx`**\n\n```tsx filename=app/root.tsx lines=[3,5-8]\n// existing imports\n\nimport { createEmptyContact } from \"./data\";\n\nexport async function action() {\n  const contact = await createEmptyContact();\n  return { contact };\n}\n\n// existing code\n```\n\nThat's it! Go ahead and click the \"New\" button, and you should see a new record pop into the list 🥳\n\n<img class=\"tutorial\" loading=\"lazy\" src=\"/_docs/v7_address_book_tutorial/13.webp\" />\n\nThe `createEmptyContact` method just creates an empty contact with no name or data or anything. But it does still create a record, promise!\n\n> 🧐 Wait a sec ... How did the sidebar update? Where did we call the `action` function? Where's the code to re-fetch the data? Where are `useState`, `onSubmit` and `useEffect`?!\n\nThis is where the \"old school web\" programming model shows up. [`<Form>`][form-component] prevents the browser from sending the request to the server and sends it to your route's `action` function instead with [`fetch`][fetch].\n\nIn web semantics, a `POST` usually means some data is changing. By convention, React Router uses this as a hint to automatically revalidate the data on the page after the `action` finishes.\n\nIn fact, since it's all just HTML and HTTP, you could disable JavaScript and the whole thing will still work. Instead of React Router serializing the form and making a [`fetch`][fetch] request to your server, the browser will serialize the form and make a document request. From there React Router will render the page server side and send it down. It's the same UI in the end either way.\n\nWe'll keep JavaScript around though because we're going to make a better user experience than spinning favicons and static documents.\n\n## Updating Data\n\nLet's add a way to fill the information for our new record.\n\nJust like creating data, you update data with [`<Form>`][form-component]. Let's make a new route module inside `app/routes/edit-contact.tsx`.\n\n👉 **Create the edit contact route**\n\n```shellscript nonumber\ntouch app/routes/edit-contact.tsx\n```\n\nDon't forget to add the route to `app/routes.ts`:\n\n```tsx filename=app/routes.ts lines=[5-8]\nexport default [\n  layout(\"layouts/sidebar.tsx\", [\n    index(\"routes/home.tsx\"),\n    route(\"contacts/:contactId\", \"routes/contact.tsx\"),\n    route(\n      \"contacts/:contactId/edit\",\n      \"routes/edit-contact.tsx\",\n    ),\n  ]),\n  route(\"about\", \"routes/about.tsx\"),\n] satisfies RouteConfig;\n```\n\n👉 **Add the edit page UI**\n\nNothing we haven't seen before, feel free to copy/paste:\n\n```tsx filename=app/routes/edit-contact.tsx\nimport { Form } from \"react-router\";\nimport type { Route } from \"./+types/edit-contact\";\n\nimport { getContact } from \"../data\";\n\nexport async function loader({ params }: Route.LoaderArgs) {\n  const contact = await getContact(params.contactId);\n  if (!contact) {\n    throw new Response(\"Not Found\", { status: 404 });\n  }\n  return { contact };\n}\n\nexport default function EditContact({\n  loaderData,\n}: Route.ComponentProps) {\n  const { contact } = loaderData;\n\n  return (\n    <Form key={contact.id} id=\"contact-form\" method=\"post\">\n      <p>\n        <span>Name</span>\n        <input\n          aria-label=\"First name\"\n          defaultValue={contact.first}\n          name=\"first\"\n          placeholder=\"First\"\n          type=\"text\"\n        />\n        <input\n          aria-label=\"Last name\"\n          defaultValue={contact.last}\n          name=\"last\"\n          placeholder=\"Last\"\n          type=\"text\"\n        />\n      </p>\n      <label>\n        <span>Twitter</span>\n        <input\n          defaultValue={contact.twitter}\n          name=\"twitter\"\n          placeholder=\"@jack\"\n          type=\"text\"\n        />\n      </label>\n      <label>\n        <span>Avatar URL</span>\n        <input\n          aria-label=\"Avatar URL\"\n          defaultValue={contact.avatar}\n          name=\"avatar\"\n          placeholder=\"https://example.com/avatar.jpg\"\n          type=\"text\"\n        />\n      </label>\n      <label>\n        <span>Notes</span>\n        <textarea\n          defaultValue={contact.notes}\n          name=\"notes\"\n          rows={6}\n        />\n      </label>\n      <p>\n        <button type=\"submit\">Save</button>\n        <button type=\"button\">Cancel</button>\n      </p>\n    </Form>\n  );\n}\n```\n\nNow click on your new record, then click the \"Edit\" button. We should see the new route.\n\n<img class=\"tutorial\" loading=\"lazy\" src=\"/_docs/v7_address_book_tutorial/14.webp\" />\n\n## Updating Contacts with `FormData`\n\nThe edit route we just created already renders a `form`. All we need to do is add the `action` function. React Router will serialize the `form`, `POST` it with [`fetch`][fetch], and automatically revalidate all the data.\n\n👉 **Add an `action` function to the edit route**\n\n```tsx filename=app/routes/edit-contact.tsx lines=[1,4,8,6-15]\nimport { Form, redirect } from \"react-router\";\n// existing imports\n\nimport { getContact, updateContact } from \"../data\";\n\nexport async function action({\n  params,\n  request,\n}: Route.ActionArgs) {\n  const formData = await request.formData();\n  const updates = Object.fromEntries(formData);\n  await updateContact(params.contactId, updates);\n  return redirect(`/contacts/${params.contactId}`);\n}\n\n// existing code\n```\n\nFill out the form, hit save, and you should see something like this! <small>(Except easier on the eyes and maybe with the patience to cut watermelon.)</small>\n\n<img class=\"tutorial\" loading=\"lazy\" src=\"/_docs/v7_address_book_tutorial/15.webp\" />\n\n## Mutation Discussion\n\n> 😑 It worked, but I have no idea what is going on here...\n\nLet's dig in a bit...\n\nOpen up `app/routes/edit-contact.tsx` and look at the `form` elements. Notice how they each have a name:\n\n```tsx filename=app/routes/edit-contact.tsx lines=[4]\n<input\n  aria-label=\"First name\"\n  defaultValue={contact.first}\n  name=\"first\"\n  placeholder=\"First\"\n  type=\"text\"\n/>\n```\n\nWithout JavaScript, when a form is submitted, the browser will create [`FormData`][form-data] and set it as the body of the request when it sends it to the server. As mentioned before, React Router prevents that and emulates the browser by sending the request to your `action` function with [`fetch`][fetch] instead, including the [`FormData`][form-data].\n\nEach field in the `form` is accessible with `formData.get(name)`. For example, given the input field from above, you could access the first and last names like this:\n\n```tsx filename=app/routes/edit-contact.tsx  lines=[6,7] nocopy\nexport const action = async ({\n  params,\n  request,\n}: ActionFunctionArgs) => {\n  const formData = await request.formData();\n  const firstName = formData.get(\"first\");\n  const lastName = formData.get(\"last\");\n  // ...\n};\n```\n\nSince we have a handful of form fields, we used [`Object.fromEntries`][object-from-entries] to collect them all into an object, which is exactly what our `updateContact` function wants.\n\n```tsx filename=app/routes/edit-contact.tsx nocopy\nconst updates = Object.fromEntries(formData);\nupdates.first; // \"Some\"\nupdates.last; // \"Name\"\n```\n\nAside from the `action` function, none of these APIs we're discussing are provided by React Router: [`request`][request], [`request.formData`][request-form-data], [`Object.fromEntries`][object-from-entries] are all provided by the web platform.\n\nAfter we finished the `action`, note the [`redirect`][redirect] at the end:\n\n```tsx filename=app/routes/edit-contact.tsx lines=[9]\nexport async function action({\n  params,\n  request,\n}: Route.ActionArgs) {\n  invariant(params.contactId, \"Missing contactId param\");\n  const formData = await request.formData();\n  const updates = Object.fromEntries(formData);\n  await updateContact(params.contactId, updates);\n  return redirect(`/contacts/${params.contactId}`);\n}\n```\n\n`action` and `loader` functions can both return a `Response` (makes sense, since they received a [`Request`][request]!). The [`redirect`][redirect] helper just makes it easier to return a [`Response`][response] that tells the app to change locations.\n\nWithout client side routing, if a server redirected after a `POST` request, the new page would fetch the latest data and render. As we learned before, React Router emulates this model and automatically revalidates the data on the page after the `action` call. That's why the sidebar automatically updates when we save the form. The extra revalidation code doesn't exist without client side routing, so it doesn't need to exist with client side routing in React Router either!\n\nOne last thing. Without JavaScript, the [`redirect`][redirect] would be a normal redirect. However, with JavaScript it's a client-side redirect, so the user doesn't lose client state like scroll positions or component state.\n\n## Redirecting new records to the edit page\n\nNow that we know how to redirect, let's update the action that creates new contacts to redirect to the edit page:\n\n👉 **Redirect to the new record's edit page**\n\n```tsx filename=app/root.tsx lines=[6,12]\nimport {\n  Outlet,\n  Scripts,\n  ScrollRestoration,\n  isRouteErrorResponse,\n  redirect,\n} from \"react-router\";\n// existing imports\n\nexport async function action() {\n  const contact = await createEmptyContact();\n  return redirect(`/contacts/${contact.id}/edit`);\n}\n\n// existing code\n```\n\nNow when we click \"New\", we should end up on the edit page:\n\n<img class=\"tutorial\" loading=\"lazy\" src=\"/_docs/v7_address_book_tutorial/16.webp\" />\n\n## Active Link Styling\n\nNow that we have a bunch of records, it's not clear which one we're looking at in the sidebar. We can use [`NavLink`][nav-link] to fix this.\n\n👉 **Replace `<Link>` with `<NavLink>` in the sidebar**\n\n```tsx filename=app/layouts/sidebar.tsx lines=[1,17-26,28]\nimport { Form, Link, NavLink, Outlet } from \"react-router\";\n\n// existing imports and exports\n\nexport default function SidebarLayout({\n  loaderData,\n}: Route.ComponentProps) {\n  const { contacts } = loaderData;\n\n  return (\n    <>\n      <div id=\"sidebar\">\n        {/* existing elements */}\n        <ul>\n          {contacts.map((contact) => (\n            <li key={contact.id}>\n              <NavLink\n                className={({ isActive, isPending }) =>\n                  isActive\n                    ? \"active\"\n                    : isPending\n                      ? \"pending\"\n                      : \"\"\n                }\n                to={`contacts/${contact.id}`}\n              >\n                {/* existing elements */}\n              </NavLink>\n            </li>\n          ))}\n        </ul>\n        {/* existing elements */}\n      </div>\n      {/* existing elements */}\n    </>\n  );\n}\n```\n\nNote that we are passing a function to `className`. When the user is at the URL that matches `<NavLink to>`, then `isActive` will be true. When it's _about_ to be active (the data is still loading) then `isPending` will be true. This allows us to easily indicate where the user is and also provide immediate feedback when links are clicked but data needs to be loaded.\n\n<img class=\"tutorial\" loading=\"lazy\" src=\"/_docs/v7_address_book_tutorial/17.webp\" />\n\n## Global Pending UI\n\nAs the user navigates the app, React Router will _leave the old page up_ as data is loading for the next page. You may have noticed the app feels a little unresponsive as you click between the list. Let's provide the user with some feedback so the app doesn't feel unresponsive.\n\nReact Router is managing all the state behind the scenes and reveals the pieces you need to build dynamic web apps. In this case, we'll use the [`useNavigation`][use-navigation] hook.\n\n👉 **Use `useNavigation` to add global pending UI**\n\n```tsx filename=app/layouts/sidebar.tsx lines=[6,13,19-21]\nimport {\n  Form,\n  Link,\n  NavLink,\n  Outlet,\n  useNavigation,\n} from \"react-router\";\n\nexport default function SidebarLayout({\n  loaderData,\n}: Route.ComponentProps) {\n  const { contacts } = loaderData;\n  const navigation = useNavigation();\n\n  return (\n    <>\n      {/* existing elements */}\n      <div\n        className={\n          navigation.state === \"loading\" ? \"loading\" : \"\"\n        }\n        id=\"detail\"\n      >\n        <Outlet />\n      </div>\n    </>\n  );\n}\n```\n\n[`useNavigation`][use-navigation] returns the current navigation state: it can be one of `\"idle\"`, `\"loading\"` or `\"submitting\"`.\n\nIn our case, we add a `\"loading\"` class to the main part of the app if we're not idle. The CSS then adds a nice fade after a short delay (to avoid flickering the UI for fast loads). You could do anything you want though, like show a spinner or loading bar across the top.\n\n<img class=\"tutorial\" loading=\"lazy\" src=\"/_docs/v7_address_book_tutorial/18.webp\" />\n\n## Deleting Records\n\nIf we review code in the contact route, we can find the delete button looks like this:\n\n```tsx filename=app/routes/contact.tsx lines=[2]\n<Form\n  action=\"destroy\"\n  method=\"post\"\n  onSubmit={(event) => {\n    const response = confirm(\n      \"Please confirm you want to delete this record.\",\n    );\n    if (!response) {\n      event.preventDefault();\n    }\n  }}\n>\n  <button type=\"submit\">Delete</button>\n</Form>\n```\n\nNote the `action` points to `\"destroy\"`. Like `<Link to>`, `<Form action>` can take a _relative_ value. Since the form is rendered in the route `contacts/:contactId`, then a relative action with `destroy` will submit the form to `contacts/:contactId/destroy` when clicked.\n\nAt this point you should know everything you need to know to make the delete button work. Maybe give it a shot before moving on? You'll need:\n\n1. A new route\n2. An `action` at that route\n3. `deleteContact` from `app/data.ts`\n4. `redirect` to somewhere after\n\n👉 **Configure the \"destroy\" route module**\n\n```shellscript nonumber\ntouch app/routes/destroy-contact.tsx\n```\n\n```tsx filename=app/routes.ts lines=[3-6]\nexport default [\n  // existing routes\n  route(\n    \"contacts/:contactId/destroy\",\n    \"routes/destroy-contact.tsx\",\n  ),\n  // existing routes\n] satisfies RouteConfig;\n```\n\n👉 **Add the destroy action**\n\n```tsx filename=app/routes/destroy-contact.tsx\nimport { redirect } from \"react-router\";\nimport type { Route } from \"./+types/destroy-contact\";\n\nimport { deleteContact } from \"../data\";\n\nexport async function action({ params }: Route.ActionArgs) {\n  await deleteContact(params.contactId);\n  return redirect(\"/\");\n}\n```\n\nAlright, navigate to a record and click the \"Delete\" button. It works!\n\n> 😅 I'm still confused why this all works\n\nWhen the user clicks the submit button:\n\n1. `<Form>` prevents the default browser behavior of sending a new document `POST` request to the server, but instead emulates the browser by creating a `POST` request with client side routing and [`fetch`][fetch]\n2. The `<Form action=\"destroy\">` matches the new route at `contacts/:contactId/destroy` and sends it the request\n3. After the `action` redirects, React Router calls all the `loader`s for the data on the page to get the latest values (this is \"revalidation\"). `loaderData` in `routes/contact.tsx` now has new values and causes the components to update!\n\nAdd a `Form`, add an `action`, React Router does the rest.\n\n## Cancel Button\n\nOn the edit page we've got a cancel button that doesn't do anything yet. We'd like it to do the same thing as the browser's back button.\n\nWe'll need a click handler on the button as well as [`useNavigate`][use-navigate].\n\n👉 **Add the cancel button click handler with `useNavigate`**\n\n```tsx filename=app/routes/edit-contact.tsx lines=[1,8,15]\nimport { Form, redirect, useNavigate } from \"react-router\";\n// existing imports & exports\n\nexport default function EditContact({\n  loaderData,\n}: Route.ComponentProps) {\n  const { contact } = loaderData;\n  const navigate = useNavigate();\n\n  return (\n    <Form key={contact.id} id=\"contact-form\" method=\"post\">\n      {/* existing elements */}\n      <p>\n        <button type=\"submit\">Save</button>\n        <button onClick={() => navigate(-1)} type=\"button\">\n          Cancel\n        </button>\n      </p>\n    </Form>\n  );\n}\n```\n\nNow when the user clicks \"Cancel\", they'll be sent back one entry in the browser's history.\n\n> 🧐 Why is there no `event.preventDefault()` on the button?\n\nA `<button type=\"button\">`, while seemingly redundant, is the HTML way of preventing a button from submitting its form.\n\nTwo more features to go. We're on the home stretch!\n\n## `URLSearchParams` and `GET` Submissions\n\nAll of our interactive UI so far have been either links that change the URL or `form`s that post data to `action` functions. The search field is interesting because it's a mix of both: it's a `form`, but it only changes the URL, it doesn't change data.\n\nLet's see what happens when we submit the search form:\n\n👉 **Type a name into the search field and hit the enter key**\n\nNote the browser's URL now contains your query in the URL as [`URLSearchParams`][url-search-params]:\n\n```\nhttp://localhost:5173/?q=ryan\n```\n\nSince it's not `<Form method=\"post\">`, React Router emulates the browser by serializing the [`FormData`][form-data] into the [`URLSearchParams`][url-search-params] instead of the request body.\n\n`loader` functions have access to the search params from the `request`. Let's use it to filter the list:\n\n👉 **Filter the list if there are `URLSearchParams`**\n\n```tsx filename=app/layouts/sidebar.tsx lines=[3-8]\n// existing imports & exports\n\nexport async function loader({\n  request,\n}: Route.LoaderArgs) {\n  const url = new URL(request.url);\n  const q = url.searchParams.get(\"q\");\n  const contacts = await getContacts(q);\n  return { contacts };\n}\n\n// existing code\n```\n\n<img class=\"tutorial\" loading=\"lazy\" src=\"/_docs/v7_address_book_tutorial/19.webp\" />\n\nBecause this is a `GET`, not a `POST`, React Router _does not_ call the `action` function. Submitting a `GET` `form` is the same as clicking a link: only the URL changes.\n\nThis also means it's a normal page navigation. You can click the back button to get back to where you were.\n\n## Synchronizing URLs to Form State\n\nThere are a couple of UX issues here that we can take care of quickly.\n\n1. If you click back after a search, the form field still has the value you entered even though the list is no longer filtered.\n2. If you refresh the page after searching, the form field no longer has the value in it, even though the list is filtered\n\nIn other words, the URL and our input's state are out of sync.\n\nLet's solve (2) first and start the input with the value from the URL.\n\n👉 **Return `q` from your `loader`, set it as the input's default value**\n\n```tsx filename=app/layouts/sidebar.tsx lines=[9,15,26]\n// existing imports & exports\n\nexport async function loader({\n  request,\n}: Route.LoaderArgs) {\n  const url = new URL(request.url);\n  const q = url.searchParams.get(\"q\");\n  const contacts = await getContacts(q);\n  return { contacts, q };\n}\n\nexport default function SidebarLayout({\n  loaderData,\n}: Route.ComponentProps) {\n  const { contacts, q } = loaderData;\n  const navigation = useNavigation();\n\n  return (\n    <>\n      <div id=\"sidebar\">\n        {/* existing elements */}\n        <div>\n          <Form id=\"search-form\" role=\"search\">\n            <input\n              aria-label=\"Search contacts\"\n              defaultValue={q || \"\"}\n              id=\"q\"\n              name=\"q\"\n              placeholder=\"Search\"\n              type=\"search\"\n            />\n            {/* existing elements */}\n          </Form>\n          {/* existing elements */}\n        </div>\n        {/* existing elements */}\n      </div>\n      {/* existing elements */}\n    </>\n  );\n}\n```\n\nThe input field will show the query if you refresh the page after a search now.\n\nNow for problem (1), clicking the back button and updating the input. We can bring in `useEffect` from React to manipulate the input's value in the DOM directly.\n\n👉 **Synchronize input value with the `URLSearchParams`**\n\n```tsx filename=app/layouts/sidebar.tsx lines=[2,12-17]\n// existing imports\nimport { useEffect } from \"react\";\n\n// existing imports & exports\n\nexport default function SidebarLayout({\n  loaderData,\n}: Route.ComponentProps) {\n  const { contacts, q } = loaderData;\n  const navigation = useNavigation();\n\n  useEffect(() => {\n    const searchField = document.getElementById(\"q\");\n    if (searchField instanceof HTMLInputElement) {\n      searchField.value = q || \"\";\n    }\n  }, [q]);\n\n  // existing code\n}\n```\n\n> 🤔 Shouldn't you use a controlled component and React State for this?\n\nYou could certainly do this as a controlled component. You will have more synchronization points, but it's up to you.\n\n<details>\n\n<summary>Expand this to see what it would look like</summary>\n\n```tsx filename=app/layouts/sidebar.tsx lines=[2,11-12,14-18,30-33,36-37]\n// existing imports\nimport { useEffect, useState } from \"react\";\n\n// existing imports & exports\n\nexport default function SidebarLayout({\n  loaderData,\n}: Route.ComponentProps) {\n  const { contacts, q } = loaderData;\n  const navigation = useNavigation();\n  // the query now needs to be kept in state\n  const [query, setQuery] = useState(q || \"\");\n\n  // we still have a `useEffect` to synchronize the query\n  // to the component state on back/forward button clicks\n  useEffect(() => {\n    setQuery(q || \"\");\n  }, [q]);\n\n  return (\n    <>\n      <div id=\"sidebar\">\n        {/* existing elements */}\n        <div>\n          <Form id=\"search-form\" role=\"search\">\n            <input\n              aria-label=\"Search contacts\"\n              id=\"q\"\n              name=\"q\"\n              // synchronize user's input to component state\n              onChange={(event) =>\n                setQuery(event.currentTarget.value)\n              }\n              placeholder=\"Search\"\n              type=\"search\"\n              // switched to `value` from `defaultValue`\n              value={query}\n            />\n            {/* existing elements */}\n          </Form>\n          {/* existing elements */}\n        </div>\n        {/* existing elements */}\n      </div>\n      {/* existing elements */}\n    </>\n  );\n}\n```\n\n</details>\n\nAlright, you should now be able to click the back/forward/refresh buttons and the input's value should be in sync with the URL and results.\n\n## Submitting `Form`'s `onChange`\n\nWe've got a product decision to make here. Sometimes you want the user to submit the `form` to filter some results, other times you want to filter as the user types. We've already implemented the first, so let's see what it's like for the second.\n\nWe've seen `useNavigate` already, we'll use its cousin, [`useSubmit`][use-submit], for this.\n\n```tsx filename=app/layouts/sidebar.tsx lines=[7,16,27-29]\nimport {\n  Form,\n  Link,\n  NavLink,\n  Outlet,\n  useNavigation,\n  useSubmit,\n} from \"react-router\";\n// existing imports & exports\n\nexport default function SidebarLayout({\n  loaderData,\n}: Route.ComponentProps) {\n  const { contacts, q } = loaderData;\n  const navigation = useNavigation();\n  const submit = useSubmit();\n\n  // existing code\n\n  return (\n    <>\n      <div id=\"sidebar\">\n        {/* existing elements */}\n        <div>\n          <Form\n            id=\"search-form\"\n            onChange={(event) =>\n              submit(event.currentTarget)\n            }\n            role=\"search\"\n          >\n            {/* existing elements */}\n          </Form>\n          {/* existing elements */}\n        </div>\n        {/* existing elements */}\n      </div>\n      {/* existing elements */}\n    </>\n  );\n}\n```\n\nAs you type, the `form` is automatically submitted now!\n\nNote the argument to [`submit`][use-submit]. The `submit` function will serialize and submit any form you pass to it. We're passing in `event.currentTarget`. The `currentTarget` is the DOM node the event is attached to (the `form`).\n\n## Adding Search Spinner\n\nIn a production app, it's likely this search will be looking for records in a database that is too large to send all at once and filter client side. That's why this demo has some faked network latency.\n\nWithout any loading indicator, the search feels kinda sluggish. Even if we could make our database faster, we'll always have the user's network latency in the way and out of our control.\n\nFor a better user experience, let's add some immediate UI feedback for the search. We'll use [`useNavigation`][use-navigation] again.\n\n👉 **Add a variable to know if we're searching**\n\n```tsx filename=app/layouts/sidebar.tsx lines=[9-13]\n// existing imports & exports\n\nexport default function SidebarLayout({\n  loaderData,\n}: Route.ComponentProps) {\n  const { contacts, q } = loaderData;\n  const navigation = useNavigation();\n  const submit = useSubmit();\n  const searching =\n    navigation.location &&\n    new URLSearchParams(navigation.location.search).has(\n      \"q\",\n    );\n\n  // existing code\n}\n```\n\nWhen nothing is happening, `navigation.location` will be `undefined`, but when the user navigates it will be populated with the next location while data loads. Then we check if they're searching with `location.search`.\n\n👉 **Add classes to search form elements using the new `searching` state**\n\n```tsx filename=app/layouts/sidebar.tsx lines=[22,31]\n// existing imports & exports\n\nexport default function SidebarLayout({\n  loaderData,\n}: Route.ComponentProps) {\n  // existing code\n\n  return (\n    <>\n      <div id=\"sidebar\">\n        {/* existing elements */}\n        <div>\n          <Form\n            id=\"search-form\"\n            onChange={(event) =>\n              submit(event.currentTarget)\n            }\n            role=\"search\"\n          >\n            <input\n              aria-label=\"Search contacts\"\n              className={searching ? \"loading\" : \"\"}\n              defaultValue={q || \"\"}\n              id=\"q\"\n              name=\"q\"\n              placeholder=\"Search\"\n              type=\"search\"\n            />\n            <div\n              aria-hidden\n              hidden={!searching}\n              id=\"search-spinner\"\n            />\n          </Form>\n          {/* existing elements */}\n        </div>\n        {/* existing elements */}\n      </div>\n      {/* existing elements */}\n    </>\n  );\n}\n```\n\nBonus points, avoid fading out the main screen when searching:\n\n```tsx filename=app/layouts/sidebar.tsx lines=[13]\n// existing imports & exports\n\nexport default function SidebarLayout({\n  loaderData,\n}: Route.ComponentProps) {\n  // existing code\n\n  return (\n    <>\n      {/* existing elements */}\n      <div\n        className={\n          navigation.state === \"loading\" && !searching\n            ? \"loading\"\n            : \"\"\n        }\n        id=\"detail\"\n      >\n        <Outlet />\n      </div>\n      {/* existing elements */}\n    </>\n  );\n}\n```\n\nYou should now have a nice spinner on the left side of the search input.\n\n<img class=\"tutorial\" loading=\"lazy\" src=\"/_docs/v7_address_book_tutorial/20.webp\" />\n\n## Managing the History Stack\n\nSince the form is submitted for every keystroke, typing the characters \"alex\" and then deleting them with backspace results in a huge history stack 😂. We definitely don't want this:\n\n<img class=\"tutorial\" loading=\"lazy\" src=\"/_docs/v7_address_book_tutorial/21.webp\" />\n\nWe can avoid this by _replacing_ the current entry in the history stack with the next page, instead of pushing into it.\n\n👉 **Use `replace` in `submit`**\n\n```tsx filename=app/layouts/sidebar.tsx lines=[16-19]\n// existing imports & exports\n\nexport default function SidebarLayout({\n  loaderData,\n}: Route.ComponentProps) {\n  // existing code\n\n  return (\n    <>\n      <div id=\"sidebar\">\n        {/* existing elements */}\n        <div>\n          <Form\n            id=\"search-form\"\n            onChange={(event) => {\n              const isFirstSearch = q === null;\n              submit(event.currentTarget, {\n                replace: !isFirstSearch,\n              });\n            }}\n            role=\"search\"\n          >\n            {/* existing elements */}\n          </Form>\n          {/* existing elements */}\n        </div>\n        {/* existing elements */}\n      </div>\n      {/* existing elements */}\n    </>\n  );\n}\n```\n\nAfter a quick check if this is the first search or not, we decide to replace. Now the first search will add a new entry, but every keystroke after that will replace the current entry. Instead of clicking back 7 times to remove the search, users only have to click back once.\n\n## `Form`s Without Navigation\n\nSo far all of our forms have changed the URL. While these user flows are common, it's equally common to want to submit a form _without_ causing a navigation.\n\nFor these cases, we have [`useFetcher`][use-fetcher]. It allows us to communicate with `action`s and `loader`s without causing a navigation.\n\nThe ★ button on the contact page makes sense for this. We aren't creating or deleting a new record, and we don't want to change pages. We simply want to change the data on the page we're looking at.\n\n👉 **Change the `<Favorite>` form to a fetcher form**\n\n```tsx filename=app/routes/contact.tsx lines=[1,10,14,26]\nimport { Form, useFetcher } from \"react-router\";\n\n// existing imports & exports\n\nfunction Favorite({\n  contact,\n}: {\n  contact: Pick<ContactRecord, \"favorite\">;\n}) {\n  const fetcher = useFetcher();\n  const favorite = contact.favorite;\n\n  return (\n    <fetcher.Form method=\"post\">\n      <button\n        aria-label={\n          favorite\n            ? \"Remove from favorites\"\n            : \"Add to favorites\"\n        }\n        name=\"favorite\"\n        value={favorite ? \"false\" : \"true\"}\n      >\n        {favorite ? \"★\" : \"☆\"}\n      </button>\n    </fetcher.Form>\n  );\n}\n```\n\nThis form will no longer cause a navigation, but simply fetch to the `action`. Speaking of which ... this won't work until we create the `action`.\n\n👉 **Create the `action`**\n\n```tsx filename=app/routes/contact.tsx lines=[2,5-13]\n// existing imports\nimport { getContact, updateContact } from \"../data\";\n// existing imports\n\nexport async function action({\n  params,\n  request,\n}: Route.ActionArgs) {\n  const formData = await request.formData();\n  return updateContact(params.contactId, {\n    favorite: formData.get(\"favorite\") === \"true\",\n  });\n}\n\n// existing code\n```\n\nAlright, we're ready to click the star next to the user's name!\n\n<img class=\"tutorial\" loading=\"lazy\" src=\"/_docs/v7_address_book_tutorial/22.webp\" />\n\nCheck that out, both stars automatically update. Our new `<fetcher.Form method=\"post\">` works almost exactly like the `<Form>` we've been using: it calls the action and then all data is revalidated automatically — even your errors will be caught the same way.\n\nThere is one key difference though, it's not a navigation, so the URL doesn't change and the history stack is unaffected.\n\n## Optimistic UI\n\nYou probably noticed the app felt kind of unresponsive when we clicked the favorite button from the last section. Once again, we added some network latency because you're going to have it in the real world.\n\nTo give the user some feedback, we could put the star into a loading state with `fetcher.state` (a lot like `navigation.state` from before), but we can do something even better this time. We can use a strategy called \"Optimistic UI\".\n\nThe fetcher knows the [`FormData`][form-data] being submitted to the `action`, so it's available to you on `fetcher.formData`. We'll use that to immediately update the star's state, even though the network hasn't finished. If the update eventually fails, the UI will revert to the real data.\n\n👉 **Read the optimistic value from `fetcher.formData`**\n\n```tsx filename=app/routes/contact.tsx lines=[9-11]\n// existing code\n\nfunction Favorite({\n  contact,\n}: {\n  contact: Pick<ContactRecord, \"favorite\">;\n}) {\n  const fetcher = useFetcher();\n  const favorite = fetcher.formData\n    ? fetcher.formData.get(\"favorite\") === \"true\"\n    : contact.favorite;\n\n  return (\n    <fetcher.Form method=\"post\">\n      <button\n        aria-label={\n          favorite\n            ? \"Remove from favorites\"\n            : \"Add to favorites\"\n        }\n        name=\"favorite\"\n        value={favorite ? \"false\" : \"true\"}\n      >\n        {favorite ? \"★\" : \"☆\"}\n      </button>\n    </fetcher.Form>\n  );\n}\n```\n\nNow the star _immediately_ changes to the new state when you click it.\n\n---\n\nThat's it! Thanks for giving React Router a shot. We hope this tutorial gives you a solid start to build great user experiences. There's a lot more you can do, so make sure to check out all the [APIs][react-router-apis] 😀\n\n[http-localhost-5173]: http://localhost:5173\n[root-route]: ../explanation/special-files#roottsx\n[error-boundaries]: ../how-to/error-boundary\n[links]: ../start/framework/route-module#links\n[outlet-component]: https://api.reactrouter.com/v7/functions/react-router.Outlet\n[file-route-conventions]: ../how-to/file-route-conventions\n[contacts-1]: http://localhost:5173/contacts/1\n[link-component]: https://api.reactrouter.com/v7/functions/react-router.Link\n[client-loader]: ../start/framework/route-module#clientloader\n[spa]: ../how-to/spa\n[type-safety]: ../explanation/type-safety\n[react-router-config]: ../explanation/special-files#react-routerconfigts\n[rendering-strategies]: ../start/framework/rendering\n[index-route]: ../start/framework/routing#index-routes\n[layout-route]: ../start/framework/routing#layout-routes\n[hydrate-fallback]: ../start/framework/route-module#hydratefallback\n[about-page]: http://localhost:5173/about\n[pre-rendering]: ../how-to/pre-rendering\n[url-search-params]: https://developer.mozilla.org/en-US/docs/Web/API/URLSearchParams\n[loader]: ../start/framework/route-module#loader\n[action]: ../start/framework/route-module#action\n[form-component]: https://api.reactrouter.com/v7/functions/react-router.Form\n[fetch]: https://developer.mozilla.org/en-US/docs/Web/API/fetch\n[form-data]: https://developer.mozilla.org/en-US/docs/Web/API/FormData\n[object-from-entries]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/fromEntries\n[request-form-data]: https://developer.mozilla.org/en-US/docs/Web/API/Request/formData\n[request]: https://developer.mozilla.org/en-US/docs/Web/API/Request\n[redirect]: https://api.reactrouter.com/v7/functions/react-router.redirect\n[response]: https://developer.mozilla.org/en-US/docs/Web/API/Response\n[nav-link]: https://api.reactrouter.com/v7/functions/react-router.NavLink\n[use-navigation]: https://api.reactrouter.com/v7/functions/react-router.useNavigation\n[use-navigate]: https://api.reactrouter.com/v7/functions/react-router.useNavigate\n[use-submit]: https://api.reactrouter.com/v7/functions/react-router.useSubmit\n[use-fetcher]: https://api.reactrouter.com/v7/functions/react-router.useFetcher\n[react-router-apis]: https://api.reactrouter.com/v7/modules/react_router\n"
  },
  {
    "path": "docs/tutorials/advanced-data-fetching.md",
    "content": "---\ntitle: Advanced Data Fetching\nhidden: true\n---\n\n# Advanced Data Fetching\n\n<docs-warning>\n  This document is a work in progress. There's not much to see here (yet).\n</docs-warning>\n"
  },
  {
    "path": "docs/tutorials/index.md",
    "content": "---\ntitle: Tutorials\norder: 3\n---\n"
  },
  {
    "path": "docs/tutorials/quickstart.md",
    "content": "---\ntitle: Quick Start\norder: 1\n---\n\n# Quick Start\n\n[MODES: framework]\n\n<br />\n<br />\n\nThis guide will familiarize you with the basic plumbing required to run a React Router app as quickly as possible. While there are many starter templates with different runtimes, deploy targets, and databases, we're going to create a bare-bones project from scratch.\n\n## Installation\n\nIf you prefer to initialize a batteries-included React Router project, you can use the `create-react-router` CLI to get started with any of our [templates][templates]:\n\n```shellscript nonumber\nnpx create-react-router@latest\n```\n\nHowever, this guide will explain everything the CLI does to set up your project. Instead of using the CLI, you can follow these steps. If you're just getting started with React Router, we recommend following this guide to understand all the different pieces that make up a React Router app.\n\n```shellscript nonumber\nmkdir my-react-router-app\ncd my-react-router-app\nnpm init -y\n\n# install runtime dependencies\nnpm i react-router @react-router/node @react-router/serve isbot react react-dom\n\n# install dev dependencies\nnpm i -D @react-router/dev vite\n```\n\n## Vite Config\n\n```shellscript nonumber\ntouch vite.config.js\n```\n\nSince React Router uses [Vite], you'll need to provide a [Vite config][vite-config] with the React Router Vite plugin. Here's the basic configuration you'll need:\n\n```js filename=vite.config.js\nimport { reactRouter } from \"@react-router/dev/vite\";\nimport { defineConfig } from \"vite\";\n\nexport default defineConfig({\n  plugins: [reactRouter()],\n});\n```\n\n## The Root Route\n\n```shellscript nonumber\nmkdir app\ntouch app/root.jsx\n```\n\n`app/root.jsx` is what we call the \"Root Route\". It's the root layout of your entire app. Here's the basic set of elements you'll need for any project:\n\n```jsx filename=app/root.jsx\nimport { Outlet, Scripts } from \"react-router\";\n\nexport default function App() {\n  return (\n    <html>\n      <head>\n        <link\n          rel=\"icon\"\n          href=\"data:image/x-icon;base64,AA\"\n        />\n      </head>\n      <body>\n        <h1>Hello world!</h1>\n        <Outlet />\n        <Scripts />\n      </body>\n    </html>\n  );\n}\n```\n\n## Additional Routes\n\n```shellscript nonumber\ntouch app/routes.js\n```\n\n`app/routes.js` is where you define your routes. This guide focuses on the minimal setup to get a React Router app up and running, so we don't need to define any routes and can just export an empty array:\n\n```js filename=app/routes.js\nexport default [];\n```\n\nThe existence of `routes.js` is required to build a React Router app; if you're using React Router, we assume you'll want to do some routing eventually. You can read more about defining routes in our [Routing][routing] guide.\n\n## Build and Run\n\nFirst, you will need to specify the type as `module` in `package.json` to satisfy ES module requirements for `react-router` and future versions of Vite.\n\n```shellscript nonumber\nnpm pkg set type=\"module\"\n```\n\nNext build the app for production:\n\n```shellscript nonumber\nnpx react-router build\n```\n\nYou should now see a `build` folder containing a `server` folder (the server version of your app) and a `client` folder (the browser version) with some build artifacts in them. (This is all [configurable][react-router-config].)\n\n👉 **Run the app with `react-router-serve`**\n\nNow you can run your app with `react-router-serve`:\n\n```shellscript nonumber\nnpx react-router-serve build/server/index.js\n```\n\nYou should be able to open up [http://localhost:3000][http-localhost-3000] and see the \"hello world\" page.\n\nAside from the unholy amount of code in `node_modules`, our React Router app is just four files:\n\n```\n├── app/\n│   ├── root.jsx\n│   └── routes.js\n├── package.json\n└── vite.config.js\n```\n\n## Bring Your Own Server\n\nThe `build/server` directory created by `react-router build` is just a module that you run inside a server like Express, Cloudflare Workers, Netlify, Vercel, Fastly, AWS, Deno, Azure, Fastify, Firebase, ... anywhere.\n\n<docs-info>\n\nYou can also use React Router as a Single Page Application with no server. For more information, see our guide on [Single Page Apps][spa].\n\n</docs-info>\n\nIf you don't care to set up your own server, you can use `react-router-serve`. It's a simple `express`-based server maintained by the React Router maintainers. However, React Router is specifically designed to run in _any_ JavaScript environment so that you own your stack. It is expected many —if not most— production apps will have their own server.\n\nJust for kicks, let's stop using `react-router-serve` and use `express` instead.\n\n👉 **Install Express, the React Router Express adapter, and [cross-env] for running in production mode**\n\n```shellscript nonumber\nnpm i express @react-router/express cross-env\n\n# not going to use this anymore\nnpm uninstall @react-router/serve\n```\n\n👉 **Create an Express server**\n\n```shellscript nonumber\ntouch server.js\n```\n\n```js filename=server.js\nimport { createRequestHandler } from \"@react-router/express\";\nimport express from \"express\";\n\nconst app = express();\napp.use(express.static(\"build/client\"));\n\n// notice that your app is \"just a request handler\"\napp.use(\n  createRequestHandler({\n    // and the result of `react-router build` is \"just a module\"\n    build: await import(\"./build/server/index.js\"),\n  }),\n);\n\napp.listen(3000, () => {\n  console.log(\"App listening on http://localhost:3000\");\n});\n```\n\n👉 **Run your app with `express`**\n\n```shellscript nonumber\nnode server.js\n```\n\nNow that you own your server, you can debug your app with whatever tooling your server has. For example, you can inspect your app with Chrome DevTools using the [Node.js inspect flag][inspect]:\n\n```shellscript nonumber\nnode --inspect server.js\n```\n\n## Development Workflow\n\nInstead of stopping, rebuilding, and starting your server all the time, you can run React Router in development using [Vite in middleware mode][vite-middleware]. This enables instant feedback to changes in your app with React Refresh (Hot Module Replacement) and React Router Hot Data Revalidation.\n\nFirst, as a convenience, add `dev` and `start` commands in `package.json` that will run your server in development and production modes respectively:\n\n👉 **Add a \"scripts\" entry to `package.json`**\n\n```jsonc filename=package.json lines=[2-4] nocopy\n{\n  \"scripts\": {\n    \"dev\": \"node ./server.js\",\n    \"start\": \"cross-env NODE_ENV=production node ./server.js\",\n  },\n  // ...\n}\n```\n\n👉 **Add Vite development middleware to your server**\n\nVite middleware is not applied if `process.env.NODE_ENV` is set to `\"production\"`, in which case you'll still be running the regular build output as you did earlier.\n\n```js filename=server.js lines=[6,13-28]\nimport { createRequestHandler } from \"@react-router/express\";\nimport express from \"express\";\n\nconst app = express();\n\nif (process.env.NODE_ENV === \"production\") {\n  app.use(express.static(\"build/client\"));\n  app.use(\n    createRequestHandler({\n      build: await import(\"./build/server/index.js\"),\n    }),\n  );\n} else {\n  const viteDevServer = await import(\"vite\").then((vite) =>\n    vite.createServer({\n      server: { middlewareMode: true },\n    }),\n  );\n  app.use(viteDevServer.middlewares);\n  app.use(\n    createRequestHandler({\n      build: () =>\n        viteDevServer.ssrLoadModule(\n          \"virtual:react-router/server-build\",\n        ),\n    }),\n  );\n}\n\napp.listen(3000, () => {\n  console.log(`Server is running on http://localhost:3000`);\n});\n```\n\n👉 **Start the dev server**\n\n```shellscript nonumber\nnpm run dev\n```\n\nNow you can work on your app with immediate feedback. Give it a try by changing the text in `root.jsx` and watch the changes appear instantly!\n\n## Controlling Server and Browser Entries\n\nThere are default magic files React Router is using that most apps don't need to mess with, but if you want to customize React Router's entry points to the server and browser you can run `react-router reveal` and they'll get dumped into your project.\n\n```shellscript nonumber\nnpx react-router reveal\n```\n\n```\nEntry file entry.client created at app/entry.client.tsx.\nEntry file entry.server created at app/entry.server.tsx.\n```\n\n## Summary\n\nCongrats, you can add React Router to your resume! Summing things up, we've learned:\n\n- React Router framework mode compiles your app into two things:\n  - A request handler that you add to your own JavaScript server\n  - A pile of static assets in your public directory for the browser\n- You can bring your own server with adapters to deploy anywhere\n- You can set up a development workflow with HMR built-in\n\nIn general, React Router is a bit \"guts out\". It requires a few minutes of boilerplate, but now you own your stack.\n\nWhat's next?\n\n- [Address Book Tutorial][address-book-tutorial]\n\n[templates]: ../start/framework/deploying#templates\n[spa]: ../how-to/spa\n[inspect]: https://nodejs.org/en/docs/guides/debugging-getting-started/\n[vite-config]: https://vite.dev/config\n[routing]: ../start/framework/routing\n[http-localhost-3000]: http://localhost:3000\n[vite]: https://vitejs.dev\n[react-router-config]: https://api.reactrouter.com/v7/types/_react-router_dev.config.Config.html\n[vite-middleware]: https://vitejs.dev/guide/ssr#setting-up-the-dev-server\n[cross-env]: https://www.npmjs.com/package/cross-env\n[address-book-tutorial]: ./address-book\n"
  },
  {
    "path": "docs/upgrading/README",
    "content": "How-To:\n\n- Practical Steps\n- Problem Oriented\n- Useful when we're coding\n"
  },
  {
    "path": "docs/upgrading/component-routes.md",
    "content": "---\ntitle: Framework Adoption from Component Routes\norder: 4\n---\n\n# Framework Adoption from Component Routes\n\nIf you are using `<RouterProvider>` please see [Framework Adoption from RouterProvider][upgrade-router-provider] instead.\n\nIf you are using `<Routes>` this is the right place.\n\nThe React Router Vite plugin adds framework features to React Router. This guide will help you adopt the plugin in your app. If you run into any issues, please reach out for help on [Twitter](https://x.com/remix_run) or [Discord](https://rmx.as/discord).\n\n## Features\n\nThe Vite plugin adds:\n\n- Route loaders, actions, and automatic data revalidation\n- Type-safe Routes Modules\n- Automatic route code-splitting\n- Automatic scroll restoration across navigations\n- Optional Static pre-rendering\n- Optional Server rendering\n\nThe initial setup requires the most work. However, once complete, you can adopt new features incrementally, one route at a time.\n\n## Prerequisites\n\nTo use the Vite plugin, your project requires:\n\n- Node.js 20+ (if using Node as your runtime)\n- Vite 5+\n\n## 1. Install the Vite plugin\n\n**👉 Install the React Router Vite plugin**\n\n```shellscript nonumber\nnpm install -D @react-router/dev\n```\n\n**👉 Install a runtime adapter**\n\nWe will assume you are using Node as your runtime.\n\n```shellscript nonumber\nnpm install @react-router/node\n```\n\n**👉 Swap out the React plugin for React Router.**\n\n```diff filename=vite.config.ts\n-import react from '@vitejs/plugin-react'\n+import { reactRouter } from \"@react-router/dev/vite\";\nimport { defineConfig } from \"vite\";\n\n\nexport default defineConfig({\n  plugins: [\n-    react()\n+    reactRouter()\n  ],\n});\n```\n\n## 2. Add the React Router config\n\n**👉 Create a `react-router.config.ts` file**\n\nAdd the following to the root of your project. In this config you can tell React Router about your project, like where to find the app directory and to not use SSR (server-side rendering) for now.\n\n```shellscript nonumber\ntouch react-router.config.ts\n```\n\n```ts filename=react-router.config.ts\nimport type { Config } from \"@react-router/dev/config\";\n\nexport default {\n  appDirectory: \"src\",\n  ssr: false,\n} satisfies Config;\n```\n\n## 3. Add the Root entry point\n\nIn a typical Vite app, the `index.html` file is the entry point for bundling. The React Router Vite plugin moves the entry point to a `root.tsx` file so you can use React to render the shell of your app instead of static HTML, and eventually upgrade to Server Rendering if you want.\n\n**👉 Move your existing `index.html` to `root.tsx`**\n\nFor example, if your current `index.html` looks like this:\n\n```html filename=index.html\n<!DOCTYPE html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"UTF-8\" />\n    <meta\n      name=\"viewport\"\n      content=\"width=device-width, initial-scale=1.0\"\n    />\n    <title>My App</title>\n  </head>\n  <body>\n    <div id=\"root\"></div>\n    <script type=\"module\" src=\"/src/main.tsx\"></script>\n  </body>\n</html>\n```\n\nYou would move that markup into `src/root.tsx` and delete `index.html`:\n\n```shellscript nonumber\ntouch src/root.tsx\n```\n\n```tsx filename=src/root.tsx\nimport {\n  Links,\n  Meta,\n  Outlet,\n  Scripts,\n  ScrollRestoration,\n} from \"react-router\";\n\nexport function Layout({\n  children,\n}: {\n  children: React.ReactNode;\n}) {\n  return (\n    <html lang=\"en\">\n      <head>\n        <meta charSet=\"UTF-8\" />\n        <meta\n          name=\"viewport\"\n          content=\"width=device-width, initial-scale=1.0\"\n        />\n        <title>My App</title>\n        <Meta />\n        <Links />\n      </head>\n      <body>\n        {children}\n        <ScrollRestoration />\n        <Scripts />\n      </body>\n    </html>\n  );\n}\n\nexport default function Root() {\n  return <Outlet />;\n}\n```\n\n## 4. Add client entry module\n\nIn the typical Vite app the `index.html` file points to `src/main.tsx` as the client entry point. React Router uses a file named `src/entry.client.tsx` instead.\n\n**👉 Make `src/entry.client.tsx` your entry point**\n\nIf your current `src/main.tsx` looks like this:\n\n```tsx filename=src/main.tsx\nimport React from \"react\";\nimport ReactDOM from \"react-dom/client\";\nimport { BrowserRouter } from \"react-router\";\nimport \"./index.css\";\nimport App from \"./App\";\n\nReactDOM.createRoot(\n  document.getElementById(\"root\")!,\n).render(\n  <React.StrictMode>\n    <BrowserRouter>\n      <App />\n    </BrowserRouter>\n  </React.StrictMode>,\n);\n```\n\nYou would rename it to `entry.client.tsx` and change it to this:\n\n```tsx filename=src/entry.client.tsx\nimport React from \"react\";\nimport ReactDOM from \"react-dom/client\";\nimport { HydratedRouter } from \"react-router/dom\";\nimport \"./index.css\";\n\nReactDOM.hydrateRoot(\n  document,\n  <React.StrictMode>\n    <HydratedRouter />\n  </React.StrictMode>,\n);\n```\n\n- Use `hydrateRoot` instead of `createRoot`\n- Render a `<HydratedRouter>` instead of your `<App/>` component\n- Note: we stopped rendering the `<App/>` component. We'll bring it back in a later step, but first we want to get the app to boot with the new entry point.\n\n## 5. Shuffle stuff around\n\nBetween `root.tsx` and `entry.client.tsx`, you may want to shuffle some stuff around between them.\n\nIn general:\n\n- `root.tsx` contains any rendering things like context providers, layouts, styles, etc.\n- `entry.client.tsx` should be as minimal as possible\n- Remember to _not_ try to render your existing `<App/>` component yet, we'll do that in a later step\n\nNote that your `root.tsx` file will be statically generated and served as the entry point of your app, so just that module will need to be compatible with server rendering. This is where most of your trouble will come.\n\n## 6. Set up your routes\n\nThe React Router Vite plugin uses a `routes.ts` file to configure your routes. For now we'll add a simple catchall route to get things going.\n\n**👉 Set up a `catchall.tsx` route**\n\n```shellscript nonumber\ntouch src/routes.ts src/catchall.tsx\n```\n\n```ts filename=src/routes.ts\nimport {\n  type RouteConfig,\n  route,\n} from \"@react-router/dev/routes\";\n\nexport default [\n  // * matches all URLs, the ? makes it optional so it will match / as well\n  route(\"*?\", \"catchall.tsx\"),\n] satisfies RouteConfig;\n```\n\n**👉 Render a placeholder route**\n\nEventually we'll replace this with our original `App` component, but for now we'll just render something simple to make sure we can boot the app.\n\n```tsx filename=src/catchall.tsx\nexport default function Component() {\n  return <div>Hello, world!</div>;\n}\n```\n\n[View our guide on configuring routes][configuring-routes] to learn more about the `routes.ts` file.\n\n## 7. Boot the app\n\nAt this point you should be able to boot the app and see the root layout.\n\n**👉 Add `dev` script and run the app**\n\n```json filename=package.json\n\"scripts\": {\n  \"dev\": \"react-router dev\"\n}\n```\n\nNow make sure you can boot your app at this point before moving on:\n\n```shellscript\nnpm run dev\n```\n\nYou will probably want to add `.react-router/` to your `.gitignore` file to avoid tracking unnecessary files in your repository.\n\n```txt\n.react-router/\n```\n\nYou can check out [Type Safety][type-safety] to learn how to fully set up and use autogenerated type safety for params, loader data, and more.\n\n## 8. Render your app\n\nTo get back to rendering your app, we'll update the \"catchall\" route we set up earlier that matches all URLs so that your existing `<Routes>` get a chance to render.\n\n**👉 Update the catchall route to render your app**\n\n```tsx filename=src/catchall.tsx\nimport App from \"./App\";\n\nexport default function Component() {\n  return <App />;\n}\n```\n\nYour app should be back on the screen and working as usual!\n\n## 9. Migrate a route to a Route Module\n\nYou can now incrementally migrate your routes to route modules.\n\nGiven an existing route like this:\n\n```tsx filename=src/App.tsx\n// ...\nimport About from \"./containers/About\";\n\nexport default function App() {\n  return (\n    <Routes>\n      <Route path=\"/about\" element={<About />} />\n    </Routes>\n  );\n}\n```\n\n**👉 Add the route definition to `routes.ts`**\n\n```tsx filename=src/routes.ts\nimport {\n  type RouteConfig,\n  route,\n} from \"@react-router/dev/routes\";\n\nexport default [\n  route(\"/about\", \"./pages/about.tsx\"),\n  route(\"*?\", \"catchall.tsx\"),\n] satisfies RouteConfig;\n```\n\n**👉 Add the route module**\n\nEdit the route module to use the [Route Module API][route-modules]:\n\n```tsx filename=src/pages/about.tsx\nexport async function clientLoader() {\n  // you can now fetch data here\n  return {\n    title: \"About page\",\n  };\n}\n\nexport default function Component({ loaderData }) {\n  return <h1>{loaderData.title}</h1>;\n}\n```\n\nSee [Type Safety][type-safety] to set up autogenerated type safety for params, loader data, and more.\n\nThe first few routes you migrate are the hardest because you often have to access various abstractions a bit differently than before (like in a loader instead of from a hook or context). But once the trickiest bits get dealt with, you get into an incremental groove.\n\n## Enable SSR and/or Pre-rendering\n\nIf you want to enable server rendering and static pre-rendering, you can do so with the `ssr` and `prerender` options in the bundler plugin. For SSR you'll need to also deploy the server build to a server.\n\n```ts filename=react-router.config.ts\nimport type { Config } from \"@react-router/dev/config\";\n\nexport default {\n  ssr: true,\n  async prerender() {\n    return [\"/\", \"/about\", \"/contact\"];\n  },\n} satisfies Config;\n```\n\n[upgrade-router-provider]: ./router-provider\n[configuring-routes]: ../start/framework/routing\n[route-modules]: ../start/framework/route-module\n[type-safety]: ../how-to/route-module-type-safety\n"
  },
  {
    "path": "docs/upgrading/future.md",
    "content": "---\ntitle: Future Flags\norder: 1\n---\n\n# Future Flags and Deprecations\n\nThis guide walks you through the process of adopting future flags in your React Router app. By following this strategy, you will be able to upgrade to the next major version of React Router with minimal changes. To read more about future flags see [API Development Strategy](../community/api-development-strategy).\n\nWe highly recommend you make a commit after each step and ship it instead of doing everything all at once. Most flags can be adopted in any order, with exceptions noted below.\n\n## Update to latest v7.x\n\nFirst update to the latest minor version of v7.x to have the latest future flags. You may see a number of deprecation warnings as you upgrade, which we'll cover below.\n\n👉 Update to latest v7\n\n```sh\nnpm install react-router@7 @react-router/{dev,node,etc.}@7\n```\n\n## `future.v8_middleware`\n\n[MODES: framework]\n\n<br/>\n<br/>\n\n**Background**\n\nMiddleware allows you to run code before and after the [`Response`][Response] generation for the matched path. This enables common patterns like authentication, logging, error handling, and data preprocessing in a reusable way. Please see the [docs](../how-to/middleware) for more information.\n\n👉 **Enable the Flag**\n\n```ts filename=react-router.config.ts\nimport type { Config } from \"@react-router/dev/config\";\n\nexport default {\n  future: {\n    v8_middleware: true,\n  },\n} satisfies Config;\n```\n\n**Update your Code**\n\nIf you're using `react-router-serve`, then you should not need to make any updates to your code.\n\nYou should only need to update your code if you are using the `context` parameter in `loader` and `action` functions. This only applies if you have a custom server with a `getLoadContext` function. Please see the docs on the middleware [`getLoadContext` changes](../how-to/middleware#changes-to-getloadcontextapploadcontext) and the instructions to [migrate to the new API](../how-to/middleware#migration-from-apploadcontext).\n\n## `future.v8_splitRouteModules`\n\n[MODES: framework]\n\n<br/>\n<br/>\n\n**Background**\n\nThis feature enables splitting client-side route exports (`clientLoader`, `clientAction`, `clientMiddleware`, `HydrateFallback`) into separate chunks that can be loaded independently from the route component. This allows these exports to be fetched and executed while the component code is still downloading, improving performance for client-side data loading.\n\nThis can be set to `true` for opt-in behavior, or `\"enforce\"` to require all routes to be splittable (which will cause build failures for routes that cannot be split due to shared code).\n\n👉 **Enable the Flag**\n\n```ts filename=react-router.config.ts\nimport type { Config } from \"@react-router/dev/config\";\n\nexport default {\n  future: {\n    v8_splitRouteModules: true,\n  },\n} satisfies Config;\n```\n\n**Update your Code**\n\nNo code changes are required. This is an optimization feature that works automatically once enabled.\n\n## `future.v8_viteEnvironmentApi`\n\n[MODES: framework]\n\n<br/>\n<br/>\n\n**Background**\n\nThis enables support for the experimental Vite Environment API, which provides a more flexible and powerful way to configure Vite environments. This is only available when using Vite 6+.\n\n👉 **Enable the Flag**\n\n```ts filename=react-router.config.ts\nimport type { Config } from \"@react-router/dev/config\";\n\nexport default {\n  future: {\n    v8_viteEnvironmentApi: true,\n  },\n} satisfies Config;\n```\n\n**Update your Code**\n\nNo code changes are required unless you have custom Vite configuration that needs to be updated for the [Environment API][vite-environment]. Most users won't need to make any changes.\n\n[Response]: https://developer.mozilla.org/en-US/docs/Web/API/Response\n[vite-environment]: https://vite.dev/guide/api-environment\n"
  },
  {
    "path": "docs/upgrading/index.md",
    "content": "---\ntitle: Upgrading\norder: 2\n---\n"
  },
  {
    "path": "docs/upgrading/remix.md",
    "content": "---\ntitle: Upgrading from Remix\norder: 3\n---\n\n# Upgrading from Remix\n\n<docs-info>\n\nReact Router v7 requires the following minimum versions:\n\n- `node@20`\n- `react@18`\n- `react-dom@18`\n\n</docs-info>\n\nReact Router v7 is the next major version of Remix after v2 (see our [\"Incremental Path to React 19\" blog post][incremental-path-to-react-19] for more information).\n\nIf you have enabled all [Remix v2 future flags][v2-future-flags], upgrading from Remix v2 to React Router v7 mainly involves updating dependencies.\n\n<docs-info>\n\nThe majority of steps 2-8 can be automatically updated using a [codemod][codemod] created by community member [James Restall][jrestall].\n\n</docs-info>\n\n## 1. Adopt future flags\n\n**👉 Adopt future flags**\n\nAdopt all existing [future flags][v2-future-flags] in your Remix v2 application.\n\n## 2. Update dependencies\n\nMost of the \"shared\" APIs that used to be re-exported through the runtime-specific packages (`@remix-run/node`, `@remix-run/cloudflare`, etc.) have all been collapsed into `react-router` in v7. So instead of importing from `@react-router/node` or `@react-router/cloudflare`, you'll import those directly from `react-router`.\n\n```diff\n-import { redirect } from \"@remix-run/node\";\n+import { redirect } from \"react-router\";\n```\n\nThe only APIs you should be importing from the runtime-specific packages in v7 are APIs that are specific to that runtime, such as `createFileSessionStorage` for Node and `createWorkersKVSessionStorage` for Cloudflare.\n\n**👉 Run the codemod (automated)**\n\nYou can automatically update your packages and imports with the following [codemod][codemod]. This codemod updates all of your packages and imports. Be sure to commit any pending changes before running the codemod, in case you need to revert.\n\n```shellscript nonumber\nnpx codemod remix/2/react-router/upgrade\n```\n\n**👉 Install the new dependencies**\n\nAfter the codemod updates your dependencies, you need to install the dependencies to remove Remix packages and add the new React Router packages.\n\n```shellscript nonumber\nnpm install\n```\n\n**👉 Update your dependencies (manual)**\n\nIf you prefer not to use the codemod, you can manually update your dependencies.\n\n<details>\n<summary>Expand to see a table of package name changes in alphabetical order</summary>\n\n| Remix v2 Package                   |     | React Router v7 Package                     |\n| ---------------------------------- | --- | ------------------------------------------- |\n| `@remix-run/architect`             | ➡️  | `@react-router/architect`                   |\n| `@remix-run/cloudflare`            | ➡️  | `@react-router/cloudflare`                  |\n| `@remix-run/dev`                   | ➡️  | `@react-router/dev`                         |\n| `@remix-run/express`               | ➡️  | `@react-router/express`                     |\n| `@remix-run/fs-routes`             | ➡️  | `@react-router/fs-routes`                   |\n| `@remix-run/node`                  | ➡️  | `@react-router/node`                        |\n| `@remix-run/react`                 | ➡️  | `react-router`                              |\n| `@remix-run/route-config`          | ➡️  | `@react-router/dev`                         |\n| `@remix-run/routes-option-adapter` | ➡️  | `@react-router/remix-routes-option-adapter` |\n| `@remix-run/serve`                 | ➡️  | `@react-router/serve`                       |\n| `@remix-run/server-runtime`        | ➡️  | `react-router`                              |\n| `@remix-run/testing`               | ➡️  | `react-router`                              |\n\n</details>\n\n## 3. Change `scripts` in `package.json`\n\n<docs-info>\n\nIf you used the codemod you can skip this step as it was automatically completed.\n\n</docs-info>\n\n**👉 Update the scripts in your `package.json`**\n\n| Script      | Remix v2                            |     | React Router v7                            |\n| ----------- | ----------------------------------- | --- | ------------------------------------------ |\n| `dev`       | `remix vite:dev`                    | ➡️  | `react-router dev`                         |\n| `build`     | `remix vite:build`                  | ➡️  | `react-router build`                       |\n| `start`     | `remix-serve build/server/index.js` | ➡️  | `react-router-serve build/server/index.js` |\n| `typecheck` | `tsc`                               | ➡️  | `react-router typegen && tsc`              |\n\n## 4. Add a `routes.ts` file\n\n<docs-info>\n\nIf you used the codemod _and_ Remix v2 `v3_routeConfig` flag, you can skip this step as it was automatically completed.\n\n</docs-info>\n\nIn React Router v7 you define your routes using the `app/routes.ts` file. View the [routing documentation][routing] for more information.\n\n**👉 Update dependencies (if using Remix v2 `v3_routeConfig` flag)**\n\n```diff filename=app/routes.ts\n-import { type RouteConfig } from \"@remix-run/route-config\";\n-import { flatRoutes } from \"@remix-run/fs-routes\";\n-import { remixRoutesOptionAdapter } from \"@remix-run/routes-option-adapter\";\n+import { type RouteConfig } from \"@react-router/dev/routes\";\n+import { flatRoutes } from \"@react-router/fs-routes\";\n+import { remixRoutesOptionAdapter } from \"@react-router/remix-routes-option-adapter\";\n\nexport default [\n  // however your routes are defined\n] satisfies RouteConfig;\n```\n\n**👉 Add a `routes.ts` file (if _not_ using Remix v2 `v3_routeConfig` flag)**\n\n```shellscript nonumber\ntouch app/routes.ts\n```\n\nFor backwards-compatibility, there are a few ways to adopt `routes.ts` to align with your route setup in Remix v2:\n\n1. If you were using the \"flat routes\" [file-based convention][fs-routing], you can continue to use that via the new `@react-router/fs-routes` package:\n\n   ```ts filename=app/routes.ts\n   import { type RouteConfig } from \"@react-router/dev/routes\";\n   import { flatRoutes } from \"@react-router/fs-routes\";\n\n   export default flatRoutes() satisfies RouteConfig;\n   ```\n\n2. If you were using the \"nested\" convention from Remix v1 via the `@remix-run/v1-route-convention` package, you can continue using that as well in conjunction with `@react-router/remix-routes-option-adapter`:\n\n   ```ts filename=app/routes.ts\n   import { type RouteConfig } from \"@react-router/dev/routes\";\n   import { remixRoutesOptionAdapter } from \"@react-router/remix-routes-option-adapter\";\n   import { createRoutesFromFolders } from \"@remix-run/v1-route-convention\";\n\n   export default remixRoutesOptionAdapter(\n     createRoutesFromFolders,\n   ) satisfies RouteConfig;\n   ```\n\n3. If you were using the `routes` option to define config-based routes, you can keep that config via `@react-router/remix-routes-option-adapter`:\n\n   ```ts filename=app/routes.ts\n   import { type RouteConfig } from \"@react-router/dev/routes\";\n   import { remixRoutesOptionAdapter } from \"@react-router/remix-routes-option-adapter\";\n\n   export default remixRoutesOptionAdapter(\n     (defineRoutes) => {\n       return defineRoutes((route) => {\n         route(\"/\", \"home/route.tsx\", { index: true });\n         route(\"about\", \"about/route.tsx\");\n         route(\"\", \"concerts/layout.tsx\", () => {\n           route(\"trending\", \"concerts/trending.tsx\");\n           route(\":city\", \"concerts/city.tsx\");\n         });\n       });\n     },\n   ) satisfies RouteConfig;\n   ```\n\n   - Be sure to also remove the `routes` option in your `vite.config.ts`:\n\n     ```diff filename=vite.config.ts\n     export default defineConfig({\n       plugins: [\n         remix({\n           ssr: true,\n     -     ignoredRouteFiles: ['**/*'],\n     -     routes(defineRoutes) {\n     -       return defineRoutes((route) => {\n     -         route(\"/somewhere/cool/*\", \"catchall.tsx\");\n     -       });\n     -     },\n         })\n         tsconfigPaths(),\n       ],\n     });\n     ```\n\n## 5. Add a React Router config\n\n**👉 Add `react-router.config.ts` your project**\n\nThe config that was previously passed to the `remix` plugin in `vite.config.ts` is now exported from `react-router.config.ts`.\n\nNote: At this point you should remove the v3 future flags you added in step 1.\n\n```shellscript nonumber\ntouch react-router.config.ts\n```\n\n```diff filename=vite.config.ts\nexport default defineConfig({\n  plugins: [\n-   remix({\n-     ssr: true,\n-     future: {/* all the v3 flags */}\n-   }),\n+   reactRouter(),\n    tsconfigPaths(),\n  ],\n});\n```\n\n```diff filename=react-router.config.ts\n+import type { Config } from \"@react-router/dev/config\";\n+export default {\n+  ssr: true,\n+} satisfies Config;\n```\n\n## 6. Add React Router plugin to `vite.config`\n\n<docs-info>\n\nIf you used the codemod you can skip this step as it was automatically completed.\n\n</docs-info>\n\n**👉 Add `reactRouter` plugin to `vite.config`**\n\nChange `vite.config.ts` to import and use the new `reactRouter` plugin from `@react-router/dev/vite`:\n\n```diff filename=vite.config.ts\n-import { vitePlugin as remix } from \"@remix-run/dev\";\n+import { reactRouter } from \"@react-router/dev/vite\";\nimport { defineConfig } from \"vite\";\nimport tsconfigPaths from \"vite-tsconfig-paths\";\n\nexport default defineConfig({\n  plugins: [\n-   remix(),\n+   reactRouter(),\n    tsconfigPaths(),\n  ],\n});\n```\n\n## 7. Enable type safety\n\n<docs-info>\n\nIf you are not using TypeScript, you can skip this step.\n\n</docs-info>\n\nReact Router automatically generates types for your route modules into a `.react-router/` directory at the root of your app. This directory is fully managed by React Router and should be gitignore'd. Learn more about the [new type safety features][type-safety].\n\n**👉 Add `.react-router/` to `.gitignore`**\n\n```txt\n.react-router/\n```\n\n**👉 Update `tsconfig.json`**\n\nUpdate the `types` field in your `tsconfig.json` to include:\n\n- `.react-router/types/**/*` path in the `include` field\n- The appropriate `@react-router/*` package in the `types` field\n- `rootDirs` for simplified relative imports\n\n```diff filename=tsconfig.json\n{\n  \"include\": [\n    /* ... */\n+   \".react-router/types/**/*\"\n  ],\n  \"compilerOptions\": {\n-   \"types\": [\"@remix-run/node\", \"vite/client\"],\n+   \"types\": [\"@react-router/node\", \"vite/client\"],\n    /* ... */\n+   \"rootDirs\": [\".\", \"./.react-router/types\"]\n  }\n}\n```\n\n## 8. Rename components in entry files\n\n<docs-info>\n\nIf you used the codemod you can skip this step as it was automatically completed.\n\n</docs-info>\n\nIf you have an `entry.server.tsx` and/or an `entry.client.tsx` file in your application, you will need to update the main components in these files:\n\n```diff filename=app/entry.server.tsx\n-import { RemixServer } from \"@remix-run/react\";\n+import { ServerRouter } from \"react-router\";\n\n-<RemixServer context={remixContext} url={request.url} />,\n+<ServerRouter context={remixContext} url={request.url} />,\n```\n\n```diff filename=app/entry.client.tsx\n-import { RemixBrowser } from \"@remix-run/react\";\n+import { HydratedRouter } from \"react-router/dom\";\n\nhydrateRoot(\n  document,\n  <StrictMode>\n-   <RemixBrowser />\n+   <HydratedRouter />\n  </StrictMode>,\n);\n```\n\n## 9. Update types for `AppLoadContext`\n\n<docs-info>\n\nIf you were using `remix-serve` you can skip this step. This is only applicable if you were using a custom server in Remix v2.\n\n</docs-info>\n\nSince React Router can be used as both a React framework _and_ a stand-alone routing library, the `context` argument for `LoaderFunctionArgs` and `ActionFunctionArgs` is now optional and typed as `any` by default. You can register types for your load context to get type safety for your loaders and actions.\n\n👉 **Register types for your load context**\n\nBefore you migrate to the new `Route.LoaderArgs` and `Route.ActionArgs` types, you can temporarily augment `LoaderFunctionArgs` and `ActionFunctionArgs` with your load context type to ease migration.\n\n```ts filename=app/env.ts\ndeclare module \"react-router\" {\n  // Your AppLoadContext used in v2\n  interface AppLoadContext {\n    whatever: string;\n  }\n\n  // TODO: remove this once we've migrated to `Route.LoaderArgs` instead for our loaders\n  interface LoaderFunctionArgs {\n    context: AppLoadContext;\n  }\n\n  // TODO: remove this once we've migrated to `Route.ActionArgs` instead for our actions\n  interface ActionFunctionArgs {\n    context: AppLoadContext;\n  }\n}\n\nexport {}; // necessary for TS to treat this as a module\n```\n\n<docs-info>\n\nUsing `declare module` to register types is a standard TypeScript technique called [module augmentation][ts-module-augmentation].\nYou can do this in any TypeScript file covered by your `tsconfig.json`'s `include` field, but we recommend a dedicated `env.ts` within your app directory.\n\n</docs-info>\n\n👉 **Use the new types**\n\nOnce you adopt the [new type generation][type-safety], you can remove the `LoaderFunctionArgs`/`ActionFunctionArgs` augmentations and use the `context` argument from [`Route.LoaderArgs`][server-loaders] and [`Route.ActionArgs`][server-actions] instead.\n\n```ts filename=app/env.ts\ndeclare module \"react-router\" {\n  // Your AppLoadContext used in v2\n  interface AppLoadContext {\n    whatever: string;\n  }\n}\n\nexport {}; // necessary for TS to treat this as a module\n```\n\n```ts filename=app/routes/my-route.tsx\nimport type { Route } from \"./+types/my-route\";\n\nexport function loader({ context }: Route.LoaderArgs) {}\n// { whatever: string }  ^^^^^^^\n\nexport function action({ context }: Route.ActionArgs) {}\n// { whatever: string }  ^^^^^^^\n```\n\nCongratulations! You are now on React Router v7. Go ahead and run your application to make sure everything is working as expected.\n\n[incremental-path-to-react-19]: https://remix.run/blog/incremental-path-to-react-19\n[v2-future-flags]: https://remix.run/docs/start/future-flags\n[routing]: ../start/framework/routing\n[fs-routing]: ../how-to/file-route-conventions\n[v7-changelog-types]: https://github.com/remix-run/react-router/blob/release-next/CHANGELOG.md#type-safety-improvements\n[server-loaders]: ../start/framework/data-loading#server-data-loading\n[server-actions]: ../start/framework/actions#server-actions\n[ts-module-augmentation]: https://www.typescriptlang.org/docs/handbook/declaration-merging.html#module-augmentation\n[type-safety]: ../explanation/type-safety\n[codemod]: https://codemod.com/registry/remix-2-react-router-upgrade\n[jrestall]: https://github.com/jrestall\n"
  },
  {
    "path": "docs/upgrading/router-provider.md",
    "content": "---\ntitle: Framework Adoption from RouterProvider\norder: 5\n---\n\n# Framework Adoption from RouterProvider\n\nIf you are not using `<RouterProvider>` please see [Framework Adoption from Component Routes][upgrade-component-routes] instead.\n\nThe React Router Vite plugin adds framework features to React Router. This guide will help you adopt the plugin in your app. If you run into any issues, please reach out for help on [Twitter](https://x.com/remix_run) or [Discord](https://rmx.as/discord).\n\n## Features\n\nThe Vite plugin adds:\n\n- Route loaders, actions, and automatic data revalidation\n- Type-safe Routes Modules\n- Automatic route code-splitting\n- Automatic scroll restoration across navigations\n- Optional Static pre-rendering\n- Optional Server rendering\n\nThe initial setup requires the most work. However, once complete, you can adopt new features incrementally.\n\n## Prerequisites\n\nTo use the Vite plugin, your project requires:\n\n- Node.js 20+ (if using Node as your runtime)\n- Vite 5+\n\n## 1. Move route definitions into route modules\n\nThe React Router Vite plugin renders its own `RouterProvider`, so you can't render an existing `RouterProvider` within it. Instead, you will need to format all of your route definitions to match the [Route Module API][route-modules].\n\nThis step will take the longest, however there are several benefits to doing this regardless of adopting the React Router Vite plugin:\n\n- Route modules will be lazy loaded, decreasing the initial bundle size of your app\n- Route definitions will be uniform, simplifying your app's architecture\n- Moving to route modules is incremental, you can migrate one route at a time\n\n**👉 Move your route definitions into route modules**\n\nExport each piece of your route definition as a separate named export, following the [Route Module API][route-modules].\n\n```tsx filename=src/routes/about.tsx\nexport async function clientLoader() {\n  return {\n    title: \"About\",\n  };\n}\n\nexport default function About() {\n  let data = useLoaderData();\n  return <div>{data.title}</div>;\n}\n\n// clientAction, ErrorBoundary, etc.\n```\n\n**👉 Create a convert function**\n\nCreate a helper function to convert route module definitions into the format expected by your data router:\n\n```tsx filename=src/main.tsx\nfunction convert(m: any) {\n  let {\n    clientLoader,\n    clientAction,\n    default: Component,\n    ...rest\n  } = m;\n  return {\n    ...rest,\n    loader: clientLoader,\n    action: clientAction,\n    Component,\n  };\n}\n```\n\n**👉 Lazy load and convert your route modules**\n\nInstead of importing your route modules directly, lazy load and convert them to the format expected by your data router.\n\nNot only does your route definition now conform to the Route Module API, but you also get the benefits of code-splitting your routes.\n\n```diff filename=src/main.tsx\nlet router = createBrowserRouter([\n  // ... other routes\n  {\n    path: \"about\",\n-   loader: aboutLoader,\n-   Component: About,\n+   lazy: () => import(\"./routes/about\").then(convert),\n  },\n  // ... other routes\n]);\n```\n\nRepeat this process for each route in your app.\n\n## 2. Install the Vite plugin\n\nOnce all of your route definitions are converted to route modules, you can adopt the React Router Vite plugin.\n\n**👉 Install the React Router Vite plugin**\n\n```shellscript nonumber\nnpm install -D @react-router/dev\n```\n\n**👉 Install a runtime adapter**\n\nWe will assume you are using Node as your runtime.\n\n```shellscript nonumber\nnpm install @react-router/node\n```\n\n**👉 Swap out the React plugin for React Router**\n\n```diff filename=vite.config.ts\n-import react from '@vitejs/plugin-react'\n+import { reactRouter } from \"@react-router/dev/vite\";\nimport { defineConfig } from \"vite\";\n\n\nexport default defineConfig({\n  plugins: [\n-    react()\n+    reactRouter()\n  ],\n});\n```\n\n## 3. Add the React Router config\n\n**👉 Create a `react-router.config.ts` file**\n\nAdd the following to the root of your project. In this config you can tell React Router about your project, like where to find the app directory and to not use SSR (server-side rendering) for now.\n\n```shellscript nonumber\ntouch react-router.config.ts\n```\n\n```ts filename=react-router.config.ts\nimport type { Config } from \"@react-router/dev/config\";\n\nexport default {\n  appDirectory: \"src\",\n  ssr: false,\n} satisfies Config;\n```\n\n## 4. Add the Root entry point\n\nIn a typical Vite app, the `index.html` file is the entry point for bundling. The React Router Vite plugin moves the entry point to a `root.tsx` file so you can use React to render the shell of your app instead of static HTML, and eventually upgrade to Server Rendering if you want.\n\n**👉 Move your existing `index.html` to `root.tsx`**\n\nFor example, if your current `index.html` looks like this:\n\n```html filename=index.html\n<!DOCTYPE html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"UTF-8\" />\n    <meta\n      name=\"viewport\"\n      content=\"width=device-width, initial-scale=1.0\"\n    />\n    <title>My App</title>\n  </head>\n  <body>\n    <div id=\"root\"></div>\n    <script type=\"module\" src=\"/src/main.tsx\"></script>\n  </body>\n</html>\n```\n\nYou would move that markup into `src/root.tsx` and delete `index.html`:\n\n```shellscript nonumber\ntouch src/root.tsx\n```\n\n```tsx filename=src/root.tsx\nimport {\n  Links,\n  Meta,\n  Outlet,\n  Scripts,\n  ScrollRestoration,\n} from \"react-router\";\n\nexport function Layout({\n  children,\n}: {\n  children: React.ReactNode;\n}) {\n  return (\n    <html lang=\"en\">\n      <head>\n        <meta charSet=\"UTF-8\" />\n        <meta\n          name=\"viewport\"\n          content=\"width=device-width, initial-scale=1.0\"\n        />\n        <title>My App</title>\n        <Meta />\n        <Links />\n      </head>\n      <body>\n        {children}\n        <ScrollRestoration />\n        <Scripts />\n      </body>\n    </html>\n  );\n}\n\nexport default function Root() {\n  return <Outlet />;\n}\n```\n\n**👉 Move everything above `RouterProvider` to `root.tsx`**\n\nAny global styles, context providers, etc. should be moved into `root.tsx` so they can be shared across all routes.\n\nFor example, if your `App.tsx` looks like this:\n\n```tsx filename=src/App.tsx\nimport \"./index.css\";\n\nexport default function App() {\n  return (\n    <OtherProviders>\n      <AppLayout>\n        <RouterProvider router={router} />\n      </AppLayout>\n    </OtherProviders>\n  );\n}\n```\n\nYou would move everything above the `RouterProvider` into `root.tsx`.\n\n```diff filename=src/root.tsx\n+import \"./index.css\";\n\n// ... other imports and Layout\n\nexport default function Root() {\n  return (\n+   <OtherProviders>\n+     <AppLayout>\n        <Outlet />\n+     </AppLayout>\n+   </OtherProviders>\n  );\n}\n```\n\n## 5. Add client entry module (optional)\n\nIn the typical Vite app the `index.html` file points to `src/main.tsx` as the client entry point. React Router uses a file named `src/entry.client.tsx` instead.\n\nIf no `entry.client.tsx` exists, the React Router Vite plugin will use a default, hidden one.\n\n**👉 Make `src/entry.client.tsx` your entry point**\n\nIf your current `src/main.tsx` looks like this:\n\n```tsx filename=src/main.tsx\nimport React from \"react\";\nimport ReactDOM from \"react-dom/client\";\nimport { BrowserRouter } from \"react-router\";\nimport App from \"./App\";\n\nconst router = createBrowserRouter([\n  // ... route definitions\n]);\n\nReactDOM.createRoot(\n  document.getElementById(\"root\")!,\n).render(\n  <React.StrictMode>\n    <RouterProvider router={router} />;\n  </React.StrictMode>,\n);\n```\n\nYou would rename it to `entry.client.tsx` and change it to this:\n\n```tsx filename=src/entry.client.tsx\nimport React from \"react\";\nimport ReactDOM from \"react-dom/client\";\nimport { HydratedRouter } from \"react-router/dom\";\n\nReactDOM.hydrateRoot(\n  document,\n  <React.StrictMode>\n    <HydratedRouter />\n  </React.StrictMode>,\n);\n```\n\n- Use `hydrateRoot` instead of `createRoot`\n- Render a `<HydratedRouter>` instead of your `<App/>` component\n- Note: We are no longer creating the routes and manually passing them to `<RouterProvider />`. We will migrate our route definitions in the next step.\n\n## 6. Migrate your routes\n\nThe React Router Vite plugin uses a `routes.ts` file to configure your routes. The format will be pretty similar to the definitions of your data router.\n\n**👉 Move definitions to a `routes.ts` file**\n\n```shellscript nonumber\ntouch src/routes.ts src/catchall.tsx\n```\n\nMove your route definitions to `routes.ts`. Note that the schemas don't match exactly, so you will get type errors; we'll fix this next.\n\n```diff filename=src/routes.ts\n+import type { RouteConfig } from \"@react-router/dev/routes\";\n\n-const router = createBrowserRouter([\n+export default [\n  {\n    path: \"/\",\n    lazy: () => import(\"./routes/layout\").then(convert),\n    children: [\n      {\n        index: true,\n        lazy: () => import(\"./routes/home\").then(convert),\n      },\n      {\n        path: \"about\",\n        lazy: () => import(\"./routes/about\").then(convert),\n      },\n      {\n        path: \"todos\",\n        lazy: () => import(\"./routes/todos\").then(convert),\n        children: [\n          {\n            path: \":id\",\n            lazy: () =>\n              import(\"./routes/todo\").then(convert),\n          },\n        ],\n      },\n    ],\n  },\n-]);\n+] satisfies RouteConfig;\n```\n\n**👉 Replace the `lazy` loader with a `file` loader**\n\n```diff filename=src/routes.ts\nexport default [\n  {\n    path: \"/\",\n-   lazy: () => import(\"./routes/layout\").then(convert),\n+   file: \"./routes/layout.tsx\",\n    children: [\n      {\n        index: true,\n-       lazy: () => import(\"./routes/home\").then(convert),\n+       file: \"./routes/home.tsx\",\n      },\n      {\n        path: \"about\",\n-       lazy: () => import(\"./routes/about\").then(convert),\n+       file: \"./routes/about.tsx\",\n      },\n      {\n        path: \"todos\",\n-       lazy: () => import(\"./routes/todos\").then(convert),\n+       file: \"./routes/todos.tsx\",\n        children: [\n          {\n            path: \":id\",\n-           lazy: () => import(\"./routes/todo\").then(convert),\n+           file: \"./routes/todo.tsx\",\n          },\n        ],\n      },\n    ],\n  },\n] satisfies RouteConfig;\n```\n\n[View our guide on configuring routes][configuring-routes] to learn more about the `routes.ts` file and helper functions to further simplify the route definitions.\n\n## 7. Boot the app\n\nAt this point you should be fully migrated to the React Router Vite plugin. Go ahead and update your `dev` script and run the app to make sure everything is working.\n\n**👉 Add `dev` script and run the app**\n\n```json filename=package.json\n\"scripts\": {\n  \"dev\": \"react-router dev\"\n}\n```\n\nNow make sure you can boot your app at this point before moving on:\n\n```shellscript\nnpm run dev\n```\n\nYou will probably want to add `.react-router/` to your `.gitignore` file to avoid tracking unnecessary files in your repository.\n\n```txt\n.react-router/\n```\n\nYou can checkout [Type Safety][type-safety] to learn how to fully setup and use autogenerated type safety for params, loader data, and more.\n\n## Enable SSR and/or Pre-rendering\n\nIf you want to enable server rendering and static pre-rendering, you can do so with the `ssr` and `prerender` options in the bundler plugin. For SSR you'll need to also deploy the server build to a server.\n\n```ts filename=react-router.config.ts\nimport type { Config } from \"@react-router/dev/config\";\n\nexport default {\n  ssr: true,\n  async prerender() {\n    return [\"/\", \"/about\", \"/contact\"];\n  },\n} satisfies Config;\n```\n\n[upgrade-component-routes]: ./component-routes\n[configuring-routes]: ../start/framework/routing\n[route-modules]: ../start/framework/route-module\n[type-safety]: ../how-to/route-module-type-safety\n"
  },
  {
    "path": "docs/upgrading/v6.md",
    "content": "---\ntitle: Upgrading from v6\norder: 2\n---\n\n# Upgrading from v6\n\n<docs-info>\n\nReact Router v7 requires the following minimum versions:\n\n- `node@20`\n- `react@18`\n- `react-dom@18`\n\n</docs-info>\n\nThe v7 upgrade has no breaking changes if you have enabled all future flags. These flags allow you to update your app one change at a time. We highly recommend you make a commit after each step and ship it instead of doing everything all at once.\n\n## Update to latest v6.x\n\nFirst update to the latest minor version of v6.x to have the latest future flags and console warnings.\n\n👉 **Update to latest v6**\n\n```shellscript nonumber\nnpm install react-router-dom@6\n```\n\n### v7_relativeSplatPath\n\n**Background**\n\nChanges the relative path matching and linking for multi-segment splats paths like `dashboard/*` (vs. just `*`). [View the CHANGELOG](https://github.com/remix-run/react-router/blob/main/CHANGELOG.md#futurev7_relativesplatpath) for more information.\n\n👉 **Enable the flag**\n\nEnabling the flag depends on the type of router:\n\n```tsx\n<BrowserRouter\n  future={{\n    v7_relativeSplatPath: true,\n  }}\n/>\n```\n\n```tsx\ncreateBrowserRouter(routes, {\n  future: {\n    v7_relativeSplatPath: true,\n  },\n});\n```\n\n**Update your Code**\n\nIf you have any routes with a path + a splat like `<Route path=\"dashboard/*\">` that have relative links like `<Link to=\"relative\">` or `<Link to=\"../relative\">` beneath them, you will need to update your code.\n\n👉 **Split the `<Route>` into two**\n\nSplit any multi-segment splat `<Route>` into a parent route with the path and a child route with the splat:\n\n```diff\n<Routes>\n  <Route path=\"/\" element={<Home />} />\n-  <Route path=\"dashboard/*\" element={<Dashboard />} />\n+  <Route path=\"dashboard\">\n+    <Route path=\"*\" element={<Dashboard />} />\n+  </Route>\n</Routes>\n\n// or\ncreateBrowserRouter([\n  { path: \"/\", element: <Home /> },\n  {\n-    path: \"dashboard/*\",\n-    element: <Dashboard />,\n+    path: \"dashboard\",\n+    children: [{ path: \"*\", element: <Dashboard /> }],\n  },\n]);\n```\n\n👉 **Update relative links**\n\nUpdate any `<Link>` elements within that route tree to include the extra `..` relative segment to continue linking to the same place:\n\n```diff\nfunction Dashboard() {\n  return (\n    <div>\n      <h2>Dashboard</h2>\n      <nav>\n         <Link to=\"/\">Dashboard Home</Link>\n-        <Link to=\"team\">Team</Link>\n-        <Link to=\"projects\">Projects</Link>\n+        <Link to=\"../team\">Team</Link>\n+        <Link to=\"../projects\">Projects</Link>\n      </nav>\n\n      <Routes>\n        <Route path=\"/\" element={<DashboardHome />} />\n        <Route path=\"team\" element={<DashboardTeam />} />\n        <Route\n          path=\"projects\"\n          element={<DashboardProjects />}\n        />\n      </Routes>\n    </div>\n  );\n}\n```\n\n### v7_startTransition\n\n**Background**\n\nThis uses `React.useTransition` instead of `React.useState` for Router state updates. View the [CHANGELOG](https://github.com/remix-run/react-router/blob/main/CHANGELOG.md#futurev7_starttransition) for more information.\n\n👉 **Enable the flag**\n\n```tsx\n<BrowserRouter\n  future={{\n    v7_startTransition: true,\n  }}\n/>\n\n// or\n<RouterProvider\n  future={{\n    v7_startTransition: true,\n  }}\n/>\n```\n\n👉 **Update your Code**\n\nYou don't need to update anything unless you are using `React.lazy` _inside_ of a component.\n\nUsing `React.lazy` inside of a component is incompatible with `React.useTransition` (or other code that makes promises inside of components). Move `React.lazy` to the module scope and stop making promises inside of components. This is not a limitation of React Router but rather incorrect usage of React.\n\n### v7_fetcherPersist\n\n<docs-warning>If you are not using a `<RouterProvider>` you can skip this</docs-warning>\n\n**Background**\n\nThe fetcher lifecycle is now based on when it returns to an idle state rather than when its owner component unmounts: [View the CHANGELOG](https://github.com/remix-run/react-router/blob/main/CHANGELOG.md#persistence-future-flag-futurev7_fetcherpersist) for more information.\n\n**Enable the Flag**\n\n```tsx\ncreateBrowserRouter(routes, {\n  future: {\n    v7_fetcherPersist: true,\n  },\n});\n```\n\n**Update your Code**\n\nIt's unlikely to affect your app. You may want to check any usage of `useFetchers` as they may persist longer than they did before. Depending on what you're doing, you may render something longer than before.\n\n### v7_normalizeFormMethod\n\n<docs-warning>If you are not using a `<RouterProvider>` you can skip this</docs-warning>\n\nThis normalizes `formMethod` fields as uppercase HTTP methods to align with the `fetch()` behavior. [View the CHANGELOG](https://github.com/remix-run/react-router/blob/main/CHANGELOG.md#futurev7_normalizeformmethod) for more information.\n\n👉 **Enable the Flag**\n\n```tsx\ncreateBrowserRouter(routes, {\n  future: {\n    v7_normalizeFormMethod: true,\n  },\n});\n```\n\n**Update your Code**\n\nIf any of your code is checking for lowercase HTTP methods, you will need to update it to check for uppercase HTTP methods (or call `toLowerCase()` on it).\n\n👉 **Compare `formMethod` to UPPERCASE**\n\n```diff\n-useNavigation().formMethod === \"post\"\n-useFetcher().formMethod === \"get\";\n+useNavigation().formMethod === \"POST\"\n+useFetcher().formMethod === \"GET\";\n```\n\n### v7_partialHydration\n\n<docs-warning>If you are not using a `<RouterProvider>` you can skip this</docs-warning>\n\nThis enables partial hydration of a data router which is primarily used for SSR frameworks, but it is also useful if you are using `lazy` to load your route modules. It's unlikely you need to worry about this, just turn the flag on. [View the CHANGELOG](https://github.com/remix-run/react-router/blob/main/CHANGELOG.md#partial-hydration) for more information.\n\n👉 **Enable the Flag**\n\n```tsx\ncreateBrowserRouter(routes, {\n  future: {\n    v7_partialHydration: true,\n  },\n});\n```\n\n**Update your Code**\n\nWith partial hydration, you need to provide a `HydrateFallback` component to render during initial hydration. Additionally, if you were using `fallbackElement` before, you need to remove it as it is now deprecated. In most cases, you will want to reuse the `fallbackElement` as the `HydrateFallback`.\n\n👉 **Replace `fallbackElement` with `HydrateFallback`**\n\n```diff\nconst router = createBrowserRouter(\n  [\n    {\n      path: \"/\",\n      Component: Layout,\n+      HydrateFallback: Fallback,\n      // or\n+      hydrateFallbackElement: <Fallback />,\n      children: [],\n    },\n  ],\n);\n\n\n<RouterProvider\n  router={router}\n-  fallbackElement={<Fallback />}\n/>\n```\n\n### v7_skipActionErrorRevalidation\n\n<docs-warning>If you are not using a `createBrowserRouter` you can skip this</docs-warning>\n\nWhen this flag is enabled, loaders will no longer revalidate by default after an action throws/returns a `Response` with a `4xx`/`5xx` status code. You may opt-into revalidation in these scenarios via `shouldRevalidate` and the `actionStatus` parameter.\n\n👉 **Enable the Flag**\n\n```tsx\ncreateBrowserRouter(routes, {\n  future: {\n    v7_skipActionErrorRevalidation: true,\n  },\n});\n```\n\n**Update your Code**\n\nIn most cases, you probably won't have to make changes to your app code. Usually, if an action errors, it's unlikely data was mutated and needs revalidation. If any of your code _does_ mutate data in action error scenarios you have 2 options:\n\n👉 **Option 1: Change the `action` to avoid mutations in error scenarios**\n\n```js\n// Before\nasync function action() {\n  await mutateSomeData();\n  if (detectError()) {\n    throw new Response(error, { status: 400 });\n  }\n  await mutateOtherData();\n  // ...\n}\n\n// After\nasync function action() {\n  if (detectError()) {\n    throw new Response(error, { status: 400 });\n  }\n  // All data is now mutated after validations\n  await mutateSomeData();\n  await mutateOtherData();\n  // ...\n}\n```\n\n👉 **Option 2: Opt-into revalidation via `shouldRevalidate` and `actionStatus`**\n\n```js\nasync function action() {\n  await mutateSomeData();\n  if (detectError()) {\n    throw new Response(error, { status: 400 });\n  }\n  await mutateOtherData();\n}\n\nasync function loader() { ... }\n\nfunction shouldRevalidate({ actionStatus, defaultShouldRevalidate }) {\n  if (actionStatus != null && actionStatus >= 400) {\n    // Revalidate this loader when actions return a 4xx/5xx status\n    return true;\n  }\n  return defaultShouldRevalidate;\n}\n```\n\n## Deprecations\n\nThe `json` and `defer` methods are deprecated in favor of returning raw objects.\n\n```diff\nasync function loader() {\n- return json({ data });\n+ return { data };\n```\n\nIf you were using `json` to serialize your data to JSON, you can use the native [Response.json()][response-json] method instead.\n\n## Upgrade to v7\n\nNow that your app is caught up, you can simply update to v7 (theoretically!) without issue.\n\n👉 **Install v7**\n\n```shellscript nonumber\nnpm install react-router-dom@latest\n```\n\n👉 **Replace react-router-dom with react-router**\n\nIn v7 we no longer need `\"react-router-dom\"` as the packages have been simplified. You can import everything from `\"react-router\"`:\n\n```shellscript nonumber\nnpm uninstall react-router-dom\nnpm install react-router@latest\n```\n\nNote you only need `\"react-router\"` in your package.json.\n\n👉 **Update imports**\n\nNow you should update your imports to use `react-router`:\n\n```diff\n-import { useLocation } from \"react-router-dom\";\n+import { useLocation } from \"react-router\";\n```\n\nInstead of manually updating imports, you can use this command. Make sure your git working tree is clean though so you can revert if it doesn't work as expected.\n\n```shellscript nonumber\nfind ./path/to/src \\( -name \"*.tsx\" -o -name \"*.ts\" -o -name \"*.js\" -o -name \"*.jsx\" \\) -type f -exec sed -i '' 's|from \"react-router-dom\"|from \"react-router\"|g' {} +\n```\n\nIf you have GNU `sed` installed (most Linux distributions), use this command instead:\n\n```shellscript nonumber\nfind ./path/to/src \\( -name \"*.tsx\" -o -name \"*.ts\" -o -name \"*.js\" -o -name \"*.jsx\" \\) -type f -exec sed -i 's|from \"react-router-dom\"|from \"react-router\"|g' {} +\n```\n\n👉 **Update DOM-specific imports**\n\n`RouterProvider` and `HydratedRouter` come from a deep import because they depend on `\"react-dom\"`:\n\n```diff\n-import { RouterProvider } from \"react-router-dom\";\n+import { RouterProvider } from \"react-router/dom\";\n```\n\nNote you should use a top-level import for non-DOM contexts, such as Jest tests:\n\n```diff\n-import { RouterProvider } from \"react-router-dom\";\n+import { RouterProvider } from \"react-router\";\n```\n\nCongratulations, you're now on v7!\n\n[react-flushsync]: https://react.dev/reference/react-dom/flushSync\n[response-json]: https://developer.mozilla.org/en-US/docs/Web/API/Response/json\n[data-util]: https://api.reactrouter.com/v7/functions/react-router.data.html\n"
  },
  {
    "path": "examples/README.md",
    "content": "---\ntitle: Examples\norder: 6\n---\n\n# React Router Examples\n\nWelcome to the examples for React Router.\n\nHere you'll find various examples of using React Router to accomplish certain tasks. Each example is a complete application including a build and even a button to preview a live instance of the app so you can play with it. You'll most often be interested in checking out the code in `src/App.tsx` (or `src/App.js`), but we included the entire source code for the app for completeness.\n\nAlso, remember to check out the README!\n\nEnjoy!\n"
  },
  {
    "path": "examples/auth/.gitignore",
    "content": "node_modules\n.DS_Store\ndist\ndist-ssr\n*.local\n"
  },
  {
    "path": "examples/auth/.stackblitzrc",
    "content": "{\n  \"installDependencies\": true,\n  \"startCommand\": \"npm run dev\"\n}\n"
  },
  {
    "path": "examples/auth/README.md",
    "content": "---\ntitle: Authentication\ntoc: false\n---\n\n# Auth Example\n\nThis example demonstrates how to restrict access to routes to authenticated users.\n\nBe sure to pay attention to the following features:\n\n- The use of the `useNavigate()` hook and the `<Navigate>` component for navigating both imperatively after the login form is submitted and declaratively when a non-authenticated user visits a particular route\n- The use of `location.state` to preserve the previous location so you can send the user there after they authenticate\n- The use of `navigate(\"...\", { replace: true })` to replace the `/login` route in the history stack so the user doesn't return to the login page when clicking the back button after logging in\n\n## Preview\n\nOpen this example on [StackBlitz](https://stackblitz.com):\n\n[![Open in StackBlitz](https://developer.stackblitz.com/img/open_in_stackblitz.svg)](https://stackblitz.com/github/remix-run/react-router/tree/main/examples/auth?file=src/App.tsx)\n"
  },
  {
    "path": "examples/auth/index.html",
    "content": "<!doctype html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"UTF-8\" />\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" />\n    <title>React Router - Auth Example</title>\n  </head>\n  <body>\n    <div id=\"root\"></div>\n    <script type=\"module\" src=\"/src/main.tsx\"></script>\n  </body>\n</html>\n"
  },
  {
    "path": "examples/auth/package.json",
    "content": "{\n  \"name\": \"auth\",\n  \"private\": true,\n  \"scripts\": {\n    \"dev\": \"vite\",\n    \"build\": \"tsc && vite build\",\n    \"serve\": \"vite preview\"\n  },\n  \"dependencies\": {\n    \"react\": \"^18.2.0\",\n    \"react-dom\": \"^18.2.0\",\n    \"react-router-dom\": \"^6.15.0\"\n  },\n  \"devDependencies\": {\n    \"@rollup/plugin-replace\": \"^5.0.2\",\n    \"@types/node\": \"^18.11.18\",\n    \"@types/react\": \"^18.0.27\",\n    \"@types/react-dom\": \"^18.0.10\",\n    \"@vitejs/plugin-react\": \"^3.0.1\",\n    \"typescript\": \"^4.9.5\",\n    \"vite\": \"^4.0.4\"\n  }\n}\n"
  },
  {
    "path": "examples/auth/src/App.tsx",
    "content": "import * as React from \"react\";\nimport {\n  Routes,\n  Route,\n  Link,\n  useNavigate,\n  useLocation,\n  Navigate,\n  Outlet,\n} from \"react-router-dom\";\nimport { fakeAuthProvider } from \"./auth\";\n\nexport default function App() {\n  return (\n    <AuthProvider>\n      <h1>Auth Example</h1>\n\n      <p>\n        This example demonstrates a simple login flow with three pages: a public\n        page, a protected page, and a login page. In order to see the protected\n        page, you must first login. Pretty standard stuff.\n      </p>\n\n      <p>\n        First, visit the public page. Then, visit the protected page. You're not\n        yet logged in, so you are redirected to the login page. After you login,\n        you are redirected back to the protected page.\n      </p>\n\n      <p>\n        Notice the URL change each time. If you click the back button at this\n        point, would you expect to go back to the login page? No! You're already\n        logged in. Try it out, and you'll see you go back to the page you\n        visited just *before* logging in, the public page.\n      </p>\n\n      <Routes>\n        <Route element={<Layout />}>\n          <Route path=\"/\" element={<PublicPage />} />\n          <Route path=\"/login\" element={<LoginPage />} />\n          <Route\n            path=\"/protected\"\n            element={\n              <RequireAuth>\n                <ProtectedPage />\n              </RequireAuth>\n            }\n          />\n        </Route>\n      </Routes>\n    </AuthProvider>\n  );\n}\n\nfunction Layout() {\n  return (\n    <div>\n      <AuthStatus />\n\n      <ul>\n        <li>\n          <Link to=\"/\">Public Page</Link>\n        </li>\n        <li>\n          <Link to=\"/protected\">Protected Page</Link>\n        </li>\n      </ul>\n\n      <Outlet />\n    </div>\n  );\n}\n\ninterface AuthContextType {\n  user: any;\n  signin: (user: string, callback: VoidFunction) => void;\n  signout: (callback: VoidFunction) => void;\n}\n\nlet AuthContext = React.createContext<AuthContextType>(null!);\n\nfunction AuthProvider({ children }: { children: React.ReactNode }) {\n  let [user, setUser] = React.useState<any>(null);\n\n  let signin = (newUser: string, callback: VoidFunction) => {\n    return fakeAuthProvider.signin(() => {\n      setUser(newUser);\n      callback();\n    });\n  };\n\n  let signout = (callback: VoidFunction) => {\n    return fakeAuthProvider.signout(() => {\n      setUser(null);\n      callback();\n    });\n  };\n\n  let value = { user, signin, signout };\n\n  return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;\n}\n\nfunction useAuth() {\n  return React.useContext(AuthContext);\n}\n\nfunction AuthStatus() {\n  let auth = useAuth();\n  let navigate = useNavigate();\n\n  if (!auth.user) {\n    return <p>You are not logged in.</p>;\n  }\n\n  return (\n    <p>\n      Welcome {auth.user}!{\" \"}\n      <button\n        onClick={() => {\n          auth.signout(() => navigate(\"/\"));\n        }}\n      >\n        Sign out\n      </button>\n    </p>\n  );\n}\n\nfunction RequireAuth({ children }: { children: JSX.Element }) {\n  let auth = useAuth();\n  let location = useLocation();\n\n  if (!auth.user) {\n    // Redirect them to the /login page, but save the current location they were\n    // trying to go to when they were redirected. This allows us to send them\n    // along to that page after they login, which is a nicer user experience\n    // than dropping them off on the home page.\n    return <Navigate to=\"/login\" state={{ from: location }} replace />;\n  }\n\n  return children;\n}\n\nfunction LoginPage() {\n  let navigate = useNavigate();\n  let location = useLocation();\n  let auth = useAuth();\n\n  let from = location.state?.from?.pathname || \"/\";\n\n  function handleSubmit(event: React.FormEvent<HTMLFormElement>) {\n    event.preventDefault();\n\n    let formData = new FormData(event.currentTarget);\n    let username = formData.get(\"username\") as string;\n\n    auth.signin(username, () => {\n      // Send them back to the page they tried to visit when they were\n      // redirected to the login page. Use { replace: true } so we don't create\n      // another entry in the history stack for the login page.  This means that\n      // when they get to the protected page and click the back button, they\n      // won't end up back on the login page, which is also really nice for the\n      // user experience.\n      navigate(from, { replace: true });\n    });\n  }\n\n  return (\n    <div>\n      <p>You must log in to view the page at {from}</p>\n\n      <form onSubmit={handleSubmit}>\n        <label>\n          Username: <input name=\"username\" type=\"text\" />\n        </label>{\" \"}\n        <button type=\"submit\">Login</button>\n      </form>\n    </div>\n  );\n}\n\nfunction PublicPage() {\n  return <h3>Public</h3>;\n}\n\nfunction ProtectedPage() {\n  return <h3>Protected</h3>;\n}\n"
  },
  {
    "path": "examples/auth/src/auth.ts",
    "content": "/**\n * This represents some generic auth provider API, like Firebase.\n */\nconst fakeAuthProvider = {\n  isAuthenticated: false,\n  signin(callback: VoidFunction) {\n    fakeAuthProvider.isAuthenticated = true;\n    setTimeout(callback, 100); // fake async\n  },\n  signout(callback: VoidFunction) {\n    fakeAuthProvider.isAuthenticated = false;\n    setTimeout(callback, 100);\n  },\n};\n\nexport { fakeAuthProvider };\n"
  },
  {
    "path": "examples/auth/src/index.css",
    "content": "body {\n  font-family:\n    -apple-system, BlinkMacSystemFont, \"Segoe UI\", \"Roboto\", \"Oxygen\", \"Ubuntu\",\n    \"Cantarell\", \"Fira Sans\", \"Droid Sans\", \"Helvetica Neue\", sans-serif;\n  -webkit-font-smoothing: antialiased;\n  -moz-osx-font-smoothing: grayscale;\n}\n\ncode {\n  font-family:\n    source-code-pro, Menlo, Monaco, Consolas, \"Courier New\", monospace;\n}\n"
  },
  {
    "path": "examples/auth/src/main.tsx",
    "content": "import * as React from \"react\";\nimport * as ReactDOM from \"react-dom/client\";\nimport { BrowserRouter } from \"react-router-dom\";\n\nimport \"./index.css\";\nimport App from \"./App\";\n\nReactDOM.createRoot(document.getElementById(\"root\")!).render(\n  <React.StrictMode>\n    <BrowserRouter>\n      <App />\n    </BrowserRouter>\n  </React.StrictMode>,\n);\n"
  },
  {
    "path": "examples/auth/src/vite-env.d.ts",
    "content": "/// <reference types=\"vite/client\" />\n"
  },
  {
    "path": "examples/auth/tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"baseUrl\": \".\",\n    \"target\": \"ESNext\",\n    \"lib\": [\"DOM\", \"DOM.Iterable\", \"ESNext\"],\n    \"allowJs\": false,\n    \"skipLibCheck\": true,\n    \"esModuleInterop\": false,\n    \"allowSyntheticDefaultImports\": true,\n    \"strict\": true,\n    \"module\": \"ESNext\",\n    \"moduleResolution\": \"Node\",\n    \"resolveJsonModule\": true,\n    \"verbatimModuleSyntax\": true,\n    \"noEmit\": true,\n    \"jsx\": \"react-jsx\",\n    \"importsNotUsedAsValues\": \"error\"\n  },\n  \"include\": [\"./src\"]\n}\n"
  },
  {
    "path": "examples/auth/vite.config.ts",
    "content": "import * as path from \"path\";\nimport { defineConfig } from \"vite\";\nimport react from \"@vitejs/plugin-react\";\nimport rollupReplace from \"@rollup/plugin-replace\";\n\n// https://vitejs.dev/config/\nexport default defineConfig({\n  server: {\n    port: 3000,\n  },\n  plugins: [\n    rollupReplace({\n      preventAssignment: true,\n      values: {\n        \"process.env.NODE_ENV\": JSON.stringify(\"development\"),\n      },\n    }),\n    react(),\n  ],\n  resolve: process.env.USE_SOURCE\n    ? {\n        alias: {\n          \"react-router\": path.resolve(\n            __dirname,\n            \"../../packages/react-router/index.ts\",\n          ),\n          \"react-router-dom\": path.resolve(\n            __dirname,\n            \"../../packages/react-router-dom/index.tsx\",\n          ),\n        },\n      }\n    : {},\n});\n"
  },
  {
    "path": "examples/auth-router-provider/.gitignore",
    "content": "node_modules\n.DS_Store\ndist\ndist-ssr\n*.local\n"
  },
  {
    "path": "examples/auth-router-provider/.stackblitzrc",
    "content": "{\n  \"installDependencies\": true,\n  \"startCommand\": \"npm run dev\"\n}\n"
  },
  {
    "path": "examples/auth-router-provider/README.md",
    "content": "---\ntitle: Authentication (using RouterProvider)\ntoc: false\n---\n\n# Auth Example (using RouterProvider)\n\nThis example demonstrates how to restrict access to routes to authenticated users when using `<RouterProvider>`.\n\nThe primary difference compared to how authentication was handled in `BrowserRouter` is that since `RouterProvider` decouples fetching from rendering, we can no longer rely on React context and/or hooks to get our user authentication status. We need access to this information outside of the React tree so we can use it in our route `loader` and `action` functions.\n\nFor some background information on this design choice, please check out the [Remixing React Router](https://remix.run/blog/remixing-react-router) blog post and Ryan's [When to Fetch](https://www.youtube.com/watch?v=95B8mnhzoCM) talk from Reactathon.\n\nBe sure to pay attention to the following features in this example:\n\n- The use of a standalone object _outside of the React tree_ that manages our authentication state\n- The use of `loader` functions to check for user authentication\n- The use of `redirect` from the `/protected` `loader` when the user is not logged in\n- The use of a `<Form>` and an `action` to perform the login\n- The use of a `from` search param and a `redirectTo` hidden input to preserve the previous location so you can send the user there after they authenticate\n- The use of `<Form replace>` to replace the `/login` route in the history stack so the user doesn't return to the login page when clicking the back button after logging in\n- The use of a `<fetcher.Form>` and an `action` to perform the logout\n\n## Preview\n\nOpen this example on [StackBlitz](https://stackblitz.com):\n\n[![Open in StackBlitz](https://developer.stackblitz.com/img/open_in_stackblitz.svg)](https://stackblitz.com/github/remix-run/react-router/tree/main/examples/auth-router-provider?file=src/App.tsx)\n"
  },
  {
    "path": "examples/auth-router-provider/index.html",
    "content": "<!doctype html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"UTF-8\" />\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" />\n    <title>React Router - Auth Example</title>\n  </head>\n  <body>\n    <div id=\"root\"></div>\n    <script type=\"module\" src=\"/src/main.tsx\"></script>\n  </body>\n</html>\n"
  },
  {
    "path": "examples/auth-router-provider/package.json",
    "content": "{\n  \"name\": \"auth\",\n  \"private\": true,\n  \"scripts\": {\n    \"dev\": \"vite\",\n    \"build\": \"tsc && vite build\",\n    \"serve\": \"vite preview\"\n  },\n  \"dependencies\": {\n    \"react\": \"^18.2.0\",\n    \"react-dom\": \"^18.2.0\",\n    \"react-router-dom\": \"^6.15.0\"\n  },\n  \"devDependencies\": {\n    \"@rollup/plugin-replace\": \"^5.0.2\",\n    \"@types/node\": \"^18.11.18\",\n    \"@types/react\": \"^18.0.27\",\n    \"@types/react-dom\": \"^18.0.10\",\n    \"@vitejs/plugin-react\": \"^3.0.1\",\n    \"typescript\": \"^4.9.5\",\n    \"vite\": \"^4.0.4\"\n  }\n}\n"
  },
  {
    "path": "examples/auth-router-provider/src/App.tsx",
    "content": "import type { LoaderFunctionArgs } from \"react-router-dom\";\nimport {\n  Form,\n  Link,\n  Outlet,\n  RouterProvider,\n  createBrowserRouter,\n  redirect,\n  useActionData,\n  useFetcher,\n  useLocation,\n  useNavigation,\n  useRouteLoaderData,\n} from \"react-router-dom\";\nimport { fakeAuthProvider } from \"./auth\";\n\nconst router = createBrowserRouter([\n  {\n    id: \"root\",\n    path: \"/\",\n    loader() {\n      // Our root route always provides the user, if logged in\n      return { user: fakeAuthProvider.username };\n    },\n    Component: Layout,\n    children: [\n      {\n        index: true,\n        Component: PublicPage,\n      },\n      {\n        path: \"login\",\n        action: loginAction,\n        loader: loginLoader,\n        Component: LoginPage,\n      },\n      {\n        path: \"protected\",\n        loader: protectedLoader,\n        Component: ProtectedPage,\n      },\n    ],\n  },\n  {\n    path: \"/logout\",\n    async action() {\n      // We signout in a \"resource route\" that we can hit from a fetcher.Form\n      await fakeAuthProvider.signout();\n      return redirect(\"/\");\n    },\n  },\n]);\n\nexport default function App() {\n  return (\n    <RouterProvider router={router} fallbackElement={<p>Initial Load...</p>} />\n  );\n}\n\nfunction Layout() {\n  return (\n    <div>\n      <h1>Auth Example using RouterProvider</h1>\n\n      <p>\n        This example demonstrates a simple login flow with three pages: a public\n        page, a protected page, and a login page. In order to see the protected\n        page, you must first login. Pretty standard stuff.\n      </p>\n\n      <p>\n        First, visit the public page. Then, visit the protected page. You're not\n        yet logged in, so you are redirected to the login page. After you login,\n        you are redirected back to the protected page.\n      </p>\n\n      <p>\n        Notice the URL change each time. If you click the back button at this\n        point, would you expect to go back to the login page? No! You're already\n        logged in. Try it out, and you'll see you go back to the page you\n        visited just *before* logging in, the public page.\n      </p>\n\n      <AuthStatus />\n\n      <ul>\n        <li>\n          <Link to=\"/\">Public Page</Link>\n        </li>\n        <li>\n          <Link to=\"/protected\">Protected Page</Link>\n        </li>\n      </ul>\n\n      <Outlet />\n    </div>\n  );\n}\n\nfunction AuthStatus() {\n  // Get our logged in user, if they exist, from the root route loader data\n  let { user } = useRouteLoaderData(\"root\") as { user: string | null };\n  let fetcher = useFetcher();\n\n  if (!user) {\n    return <p>You are not logged in.</p>;\n  }\n\n  let isLoggingOut = fetcher.formData != null;\n\n  return (\n    <div>\n      <p>Welcome {user}!</p>\n      <fetcher.Form method=\"post\" action=\"/logout\">\n        <button type=\"submit\" disabled={isLoggingOut}>\n          {isLoggingOut ? \"Signing out...\" : \"Sign out\"}\n        </button>\n      </fetcher.Form>\n    </div>\n  );\n}\n\nasync function loginAction({ request }: LoaderFunctionArgs) {\n  let formData = await request.formData();\n  let username = formData.get(\"username\") as string | null;\n\n  // Validate our form inputs and return validation errors via useActionData()\n  if (!username) {\n    return {\n      error: \"You must provide a username to log in\",\n    };\n  }\n\n  // Sign in and redirect to the proper destination if successful.\n  try {\n    await fakeAuthProvider.signin(username);\n  } catch (error) {\n    // Unused as of now but this is how you would handle invalid\n    // username/password combinations - just like validating the inputs\n    // above\n    return {\n      error: \"Invalid login attempt\",\n    };\n  }\n\n  let redirectTo = formData.get(\"redirectTo\") as string | null;\n  return redirect(redirectTo || \"/\");\n}\n\nasync function loginLoader() {\n  if (fakeAuthProvider.isAuthenticated) {\n    return redirect(\"/\");\n  }\n  return null;\n}\n\nfunction LoginPage() {\n  let location = useLocation();\n  let params = new URLSearchParams(location.search);\n  let from = params.get(\"from\") || \"/\";\n\n  let navigation = useNavigation();\n  let isLoggingIn = navigation.formData?.get(\"username\") != null;\n\n  let actionData = useActionData() as { error: string } | undefined;\n\n  return (\n    <div>\n      <p>You must log in to view the page at {from}</p>\n\n      <Form method=\"post\" replace>\n        <input type=\"hidden\" name=\"redirectTo\" value={from} />\n        <label>\n          Username: <input name=\"username\" />\n        </label>{\" \"}\n        <button type=\"submit\" disabled={isLoggingIn}>\n          {isLoggingIn ? \"Logging in...\" : \"Login\"}\n        </button>\n        {actionData && actionData.error ? (\n          <p style={{ color: \"red\" }}>{actionData.error}</p>\n        ) : null}\n      </Form>\n    </div>\n  );\n}\n\nfunction PublicPage() {\n  return <h3>Public</h3>;\n}\n\nfunction protectedLoader({ request }: LoaderFunctionArgs) {\n  // If the user is not logged in and tries to access `/protected`, we redirect\n  // them to `/login` with a `from` parameter that allows login to redirect back\n  // to this page upon successful authentication\n  if (!fakeAuthProvider.isAuthenticated) {\n    let params = new URLSearchParams();\n    params.set(\"from\", new URL(request.url).pathname);\n    return redirect(\"/login?\" + params.toString());\n  }\n  return null;\n}\n\nfunction ProtectedPage() {\n  return <h3>Protected</h3>;\n}\n"
  },
  {
    "path": "examples/auth-router-provider/src/auth.ts",
    "content": "interface AuthProvider {\n  isAuthenticated: boolean;\n  username: null | string;\n  signin(username: string): Promise<void>;\n  signout(): Promise<void>;\n}\n\n/**\n * This represents some generic auth provider API, like Firebase.\n */\nexport const fakeAuthProvider: AuthProvider = {\n  isAuthenticated: false,\n  username: null,\n  async signin(username: string) {\n    await new Promise((r) => setTimeout(r, 500)); // fake delay\n    fakeAuthProvider.isAuthenticated = true;\n    fakeAuthProvider.username = username;\n  },\n  async signout() {\n    await new Promise((r) => setTimeout(r, 500)); // fake delay\n    fakeAuthProvider.isAuthenticated = false;\n    fakeAuthProvider.username = \"\";\n  },\n};\n"
  },
  {
    "path": "examples/auth-router-provider/src/index.css",
    "content": "body {\n  font-family:\n    -apple-system, BlinkMacSystemFont, \"Segoe UI\", \"Roboto\", \"Oxygen\", \"Ubuntu\",\n    \"Cantarell\", \"Fira Sans\", \"Droid Sans\", \"Helvetica Neue\", sans-serif;\n  -webkit-font-smoothing: antialiased;\n  -moz-osx-font-smoothing: grayscale;\n}\n\ncode {\n  font-family:\n    source-code-pro, Menlo, Monaco, Consolas, \"Courier New\", monospace;\n}\n"
  },
  {
    "path": "examples/auth-router-provider/src/main.tsx",
    "content": "import * as React from \"react\";\nimport * as ReactDOM from \"react-dom/client\";\n\nimport \"./index.css\";\nimport App from \"./App\";\n\nReactDOM.createRoot(document.getElementById(\"root\")!).render(\n  <React.StrictMode>\n    <App />\n  </React.StrictMode>,\n);\n"
  },
  {
    "path": "examples/auth-router-provider/src/vite-env.d.ts",
    "content": "/// <reference types=\"vite/client\" />\n"
  },
  {
    "path": "examples/auth-router-provider/tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"baseUrl\": \".\",\n    \"target\": \"ESNext\",\n    \"lib\": [\"DOM\", \"DOM.Iterable\", \"ESNext\"],\n    \"allowJs\": false,\n    \"skipLibCheck\": true,\n    \"esModuleInterop\": false,\n    \"allowSyntheticDefaultImports\": true,\n    \"strict\": true,\n    \"module\": \"ESNext\",\n    \"moduleResolution\": \"Node\",\n    \"resolveJsonModule\": true,\n    \"verbatimModuleSyntax\": true,\n    \"noEmit\": true,\n    \"jsx\": \"react-jsx\",\n    \"importsNotUsedAsValues\": \"error\"\n  },\n  \"include\": [\"./src\"]\n}\n"
  },
  {
    "path": "examples/auth-router-provider/vite.config.ts",
    "content": "import * as path from \"path\";\nimport { defineConfig } from \"vite\";\nimport react from \"@vitejs/plugin-react\";\nimport rollupReplace from \"@rollup/plugin-replace\";\n\n// https://vitejs.dev/config/\nexport default defineConfig({\n  server: {\n    port: 3000,\n  },\n  plugins: [\n    rollupReplace({\n      preventAssignment: true,\n      values: {\n        \"process.env.NODE_ENV\": JSON.stringify(\"development\"),\n      },\n    }),\n    react(),\n  ],\n  resolve: process.env.USE_SOURCE\n    ? {\n        alias: {\n          \"react-router\": path.resolve(\n            __dirname,\n            \"../../packages/react-router/index.ts\",\n          ),\n          \"react-router-dom\": path.resolve(\n            __dirname,\n            \"../../packages/react-router-dom/index.tsx\",\n          ),\n        },\n      }\n    : {},\n});\n"
  },
  {
    "path": "examples/basic/.gitignore",
    "content": "node_modules\n.DS_Store\ndist\ndist-ssr\n*.local\n"
  },
  {
    "path": "examples/basic/.stackblitzrc",
    "content": "{\n  \"installDependencies\": true,\n  \"startCommand\": \"npm run dev\"\n}\n"
  },
  {
    "path": "examples/basic/README.md",
    "content": "---\ntitle: Basics\ntoc: false\norder: 1\n---\n\n# Basic Example\n\nThis example demonstrates some of the basic features of React Router, including:\n\n- Layouts and nested `<Route>`s\n- Index `<Route>`s\n- Catch-all `<Route>`s\n- Using `<Outlet>` as a placeholder for child routes\n- Using `<Link>`s for navigation\n\n## Preview\n\nOpen this example on [StackBlitz](https://stackblitz.com):\n\n[![Open in StackBlitz](https://developer.stackblitz.com/img/open_in_stackblitz.svg)](https://stackblitz.com/github/remix-run/react-router/tree/main/examples/basic?file=src/App.tsx)\n"
  },
  {
    "path": "examples/basic/index.html",
    "content": "<!doctype html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"UTF-8\" />\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" />\n    <title>React Router - Basic Example</title>\n  </head>\n  <body>\n    <div id=\"root\"></div>\n    <script type=\"module\" src=\"/src/main.tsx\"></script>\n  </body>\n</html>\n"
  },
  {
    "path": "examples/basic/package.json",
    "content": "{\n  \"name\": \"basic\",\n  \"private\": true,\n  \"scripts\": {\n    \"dev\": \"vite\",\n    \"build\": \"tsc && vite build\",\n    \"serve\": \"vite preview\"\n  },\n  \"dependencies\": {\n    \"react\": \"^18.2.0\",\n    \"react-dom\": \"^18.2.0\",\n    \"react-router-dom\": \"^6.15.0\"\n  },\n  \"devDependencies\": {\n    \"@rollup/plugin-replace\": \"^5.0.2\",\n    \"@types/node\": \"18.x\",\n    \"@types/react\": \"^18.0.27\",\n    \"@types/react-dom\": \"^18.0.10\",\n    \"@vitejs/plugin-react\": \"^3.0.1\",\n    \"typescript\": \"^4.9.5\",\n    \"vite\": \"^4.0.4\"\n  }\n}\n"
  },
  {
    "path": "examples/basic/src/App.tsx",
    "content": "import { Routes, Route, Outlet, Link } from \"react-router-dom\";\n\nexport default function App() {\n  return (\n    <div>\n      <h1>Basic Example</h1>\n\n      <p>\n        This example demonstrates some of the core features of React Router\n        including nested <code>&lt;Route&gt;</code>s,{\" \"}\n        <code>&lt;Outlet&gt;</code>s, <code>&lt;Link&gt;</code>s, and using a\n        \"*\" route (aka \"splat route\") to render a \"not found\" page when someone\n        visits an unrecognized URL.\n      </p>\n\n      {/* Routes nest inside one another. Nested route paths build upon\n            parent route paths, and nested route elements render inside\n            parent route elements. See the note about <Outlet> below. */}\n      <Routes>\n        <Route path=\"/\" element={<Layout />}>\n          <Route index element={<Home />} />\n          <Route path=\"about\" element={<About />} />\n          <Route path=\"dashboard\" element={<Dashboard />} />\n\n          {/* Using path=\"*\"\" means \"match anything\", so this route\n                acts like a catch-all for URLs that we don't have explicit\n                routes for. */}\n          <Route path=\"*\" element={<NoMatch />} />\n        </Route>\n      </Routes>\n    </div>\n  );\n}\n\nfunction Layout() {\n  return (\n    <div>\n      {/* A \"layout route\" is a good place to put markup you want to\n          share across all the pages on your site, like navigation. */}\n      <nav>\n        <ul>\n          <li>\n            <Link to=\"/\">Home</Link>\n          </li>\n          <li>\n            <Link to=\"/about\">About</Link>\n          </li>\n          <li>\n            <Link to=\"/dashboard\">Dashboard</Link>\n          </li>\n          <li>\n            <Link to=\"/nothing-here\">Nothing Here</Link>\n          </li>\n        </ul>\n      </nav>\n\n      <hr />\n\n      {/* An <Outlet> renders whatever child route is currently active,\n          so you can think about this <Outlet> as a placeholder for\n          the child routes we defined above. */}\n      <Outlet />\n    </div>\n  );\n}\n\nfunction Home() {\n  return (\n    <div>\n      <h2>Home</h2>\n    </div>\n  );\n}\n\nfunction About() {\n  return (\n    <div>\n      <h2>About</h2>\n    </div>\n  );\n}\n\nfunction Dashboard() {\n  return (\n    <div>\n      <h2>Dashboard</h2>\n    </div>\n  );\n}\n\nfunction NoMatch() {\n  return (\n    <div>\n      <h2>Nothing to see here!</h2>\n      <p>\n        <Link to=\"/\">Go to the home page</Link>\n      </p>\n    </div>\n  );\n}\n"
  },
  {
    "path": "examples/basic/src/index.css",
    "content": "body {\n  font-family:\n    -apple-system, BlinkMacSystemFont, \"Segoe UI\", \"Roboto\", \"Oxygen\", \"Ubuntu\",\n    \"Cantarell\", \"Fira Sans\", \"Droid Sans\", \"Helvetica Neue\", sans-serif;\n  -webkit-font-smoothing: antialiased;\n  -moz-osx-font-smoothing: grayscale;\n}\n\ncode {\n  font-family:\n    source-code-pro, Menlo, Monaco, Consolas, \"Courier New\", monospace;\n}\n"
  },
  {
    "path": "examples/basic/src/main.tsx",
    "content": "import * as React from \"react\";\nimport * as ReactDOM from \"react-dom/client\";\nimport { BrowserRouter } from \"react-router-dom\";\n\nimport \"./index.css\";\nimport App from \"./App\";\n\nReactDOM.createRoot(document.getElementById(\"root\")!).render(\n  <React.StrictMode>\n    <BrowserRouter>\n      <App />\n    </BrowserRouter>\n  </React.StrictMode>,\n);\n"
  },
  {
    "path": "examples/basic/src/vite-env.d.ts",
    "content": "/// <reference types=\"vite/client\" />\n"
  },
  {
    "path": "examples/basic/tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"baseUrl\": \".\",\n    \"target\": \"ESNext\",\n    \"lib\": [\"DOM\", \"DOM.Iterable\", \"ESNext\"],\n    \"allowJs\": false,\n    \"skipLibCheck\": true,\n    \"esModuleInterop\": false,\n    \"allowSyntheticDefaultImports\": true,\n    \"strict\": true,\n    \"module\": \"ESNext\",\n    \"moduleResolution\": \"Node\",\n    \"resolveJsonModule\": true,\n    \"verbatimModuleSyntax\": true,\n    \"noEmit\": true,\n    \"jsx\": \"react-jsx\",\n    \"importsNotUsedAsValues\": \"error\"\n  },\n  \"include\": [\"./src\"]\n}\n"
  },
  {
    "path": "examples/basic/vite.config.ts",
    "content": "import * as path from \"path\";\nimport { defineConfig } from \"vite\";\nimport react from \"@vitejs/plugin-react\";\nimport rollupReplace from \"@rollup/plugin-replace\";\n\n// https://vitejs.dev/config/\nexport default defineConfig({\n  server: {\n    port: 3000,\n  },\n  plugins: [\n    rollupReplace({\n      preventAssignment: true,\n      values: {\n        \"process.env.NODE_ENV\": JSON.stringify(\"development\"),\n      },\n    }),\n    react(),\n  ],\n  resolve: process.env.USE_SOURCE\n    ? {\n        alias: {\n          \"react-router\": path.resolve(\n            __dirname,\n            \"../../packages/react-router/index.ts\",\n          ),\n          \"react-router-dom\": path.resolve(\n            __dirname,\n            \"../../packages/react-router-dom/index.tsx\",\n          ),\n        },\n      }\n    : {},\n});\n"
  },
  {
    "path": "examples/basic-data-router/.gitignore",
    "content": "node_modules\n.DS_Store\ndist\ndist-ssr\n*.local\n"
  },
  {
    "path": "examples/basic-data-router/.stackblitzrc",
    "content": "{\n  \"installDependencies\": true,\n  \"startCommand\": \"npm run dev\"\n}\n"
  },
  {
    "path": "examples/basic-data-router/README.md",
    "content": "---\ntitle: Basic (Data Router)\ntoc: false\norder: 1\n---\n\n# Data Routers\n\nThis example demonstrates a simple usage of a Data Router, using `createBrowserRouter` and `<RouterProvider>`.\n\n## Preview\n\nOpen this example on [StackBlitz](https://stackblitz.com):\n\n[![Open in StackBlitz](https://developer.stackblitz.com/img/open_in_stackblitz.svg)](https://stackblitz.com/github/remix-run/react-router/tree/main/examples/basic-data-router?file=src/App.tsx)\n"
  },
  {
    "path": "examples/basic-data-router/index.html",
    "content": "<!doctype html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"UTF-8\" />\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" />\n    <title>React Router - Basic Data Router Example</title>\n  </head>\n  <body>\n    <div id=\"root\"></div>\n    <script type=\"module\" src=\"/src/main.tsx\"></script>\n  </body>\n</html>\n"
  },
  {
    "path": "examples/basic-data-router/package.json",
    "content": "{\n  \"name\": \"basic-data-router\",\n  \"private\": true,\n  \"scripts\": {\n    \"dev\": \"vite\",\n    \"build\": \"tsc && vite build\",\n    \"serve\": \"vite preview\"\n  },\n  \"dependencies\": {\n    \"react\": \"^18.2.0\",\n    \"react-dom\": \"^18.2.0\",\n    \"react-router-dom\": \"^6.15.0\"\n  },\n  \"devDependencies\": {\n    \"@rollup/plugin-replace\": \"5.0.2\",\n    \"@types/node\": \"18.11.18\",\n    \"@types/react\": \"18.0.27\",\n    \"@types/react-dom\": \"18.0.10\",\n    \"@vitejs/plugin-react\": \"3.0.1\",\n    \"typescript\": \"4.9.5\",\n    \"vite\": \"4.0.4\"\n  }\n}\n"
  },
  {
    "path": "examples/basic-data-router/src/app.tsx",
    "content": "import {\n  createBrowserRouter,\n  RouterProvider,\n  useLoaderData,\n} from \"react-router-dom\";\n\nimport \"./index.css\";\n\nlet router = createBrowserRouter([\n  {\n    path: \"/\",\n    loader: () => ({ message: \"Hello Data Router!\" }),\n    Component() {\n      let data = useLoaderData() as { message: string };\n      return <h1>{data.message}</h1>;\n    },\n  },\n]);\n\nexport default function App() {\n  return <RouterProvider router={router} fallbackElement={<p>Loading...</p>} />;\n}\n\nif (import.meta.hot) {\n  import.meta.hot.dispose(() => router.dispose());\n}\n"
  },
  {
    "path": "examples/basic-data-router/src/index.css",
    "content": "body {\n  font-family:\n    -apple-system, BlinkMacSystemFont, \"Segoe UI\", \"Roboto\", \"Oxygen\", \"Ubuntu\",\n    \"Cantarell\", \"Fira Sans\", \"Droid Sans\", \"Helvetica Neue\", sans-serif;\n  -webkit-font-smoothing: antialiased;\n  -moz-osx-font-smoothing: grayscale;\n}\n\ncode {\n  font-family:\n    source-code-pro, Menlo, Monaco, Consolas, \"Courier New\", monospace;\n}\n"
  },
  {
    "path": "examples/basic-data-router/src/main.tsx",
    "content": "import * as React from \"react\";\nimport * as ReactDOM from \"react-dom/client\";\nimport App from \"./app\";\n\nReactDOM.createRoot(document.getElementById(\"root\")!).render(\n  <React.StrictMode>\n    <App />\n  </React.StrictMode>,\n);\n"
  },
  {
    "path": "examples/basic-data-router/src/vite-env.d.ts",
    "content": "/// <reference types=\"vite/client\" />\n"
  },
  {
    "path": "examples/basic-data-router/tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"baseUrl\": \".\",\n    \"target\": \"ESNext\",\n    \"lib\": [\"DOM\", \"DOM.Iterable\", \"ESNext\"],\n    \"allowJs\": false,\n    \"skipLibCheck\": true,\n    \"esModuleInterop\": false,\n    \"allowSyntheticDefaultImports\": true,\n    \"strict\": true,\n    \"module\": \"ESNext\",\n    \"moduleResolution\": \"Node\",\n    \"resolveJsonModule\": true,\n    \"verbatimModuleSyntax\": true,\n    \"noEmit\": true,\n    \"jsx\": \"react-jsx\",\n    \"importsNotUsedAsValues\": \"error\"\n  },\n  \"include\": [\"./src\"]\n}\n"
  },
  {
    "path": "examples/basic-data-router/vite.config.ts",
    "content": "import * as path from \"path\";\nimport { defineConfig } from \"vite\";\nimport react from \"@vitejs/plugin-react\";\nimport rollupReplace from \"@rollup/plugin-replace\";\n\n// https://vitejs.dev/config/\nexport default defineConfig({\n  server: {\n    port: 3000,\n  },\n  plugins: [\n    rollupReplace({\n      preventAssignment: true,\n      values: {\n        \"process.env.NODE_ENV\": JSON.stringify(\"development\"),\n      },\n    }),\n    react(),\n  ],\n  resolve: process.env.USE_SOURCE\n    ? {\n        alias: {\n          \"react-router\": path.resolve(\n            __dirname,\n            \"../../packages/react-router/index.ts\",\n          ),\n          \"react-router-dom\": path.resolve(\n            __dirname,\n            \"../../packages/react-router-dom/index.tsx\",\n          ),\n        },\n      }\n    : {},\n});\n"
  },
  {
    "path": "examples/custom-filter-link/.gitignore",
    "content": "node_modules\n.DS_Store\ndist\ndist-ssr\n*.local\n"
  },
  {
    "path": "examples/custom-filter-link/.stackblitzrc",
    "content": "{\n  \"installDependencies\": true,\n  \"startCommand\": \"npm run dev\"\n}\n"
  },
  {
    "path": "examples/custom-filter-link/README.md",
    "content": "---\ntitle: Custom Filter Link\ntoc: false\n---\n\n# Custom Filter Link Example\n\nThis example demonstrates how to use a query string parameter to mark a link as \"active\" or not. This is a common technique when implementing a filter in a sidebar where you're browsing products.\n\n## Preview\n\nOpen this example on [StackBlitz](https://stackblitz.com):\n\n[![Open in StackBlitz](https://developer.stackblitz.com/img/open_in_stackblitz.svg)](https://stackblitz.com/github/remix-run/react-router/tree/main/examples/custom-filter-link?file=src/App.tsx)\n"
  },
  {
    "path": "examples/custom-filter-link/index.html",
    "content": "<!doctype html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"UTF-8\" />\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" />\n    <title>React Router - Custom Filter Link Example</title>\n  </head>\n  <body>\n    <div id=\"root\"></div>\n    <script type=\"module\" src=\"/src/main.tsx\"></script>\n  </body>\n</html>\n"
  },
  {
    "path": "examples/custom-filter-link/package.json",
    "content": "{\n  \"name\": \"custom-filter-link\",\n  \"private\": true,\n  \"scripts\": {\n    \"dev\": \"vite\",\n    \"build\": \"tsc && vite build\",\n    \"serve\": \"vite preview\"\n  },\n  \"dependencies\": {\n    \"@reach/visually-hidden\": \"0.18.0\",\n    \"react\": \"^18.2.0\",\n    \"react-dom\": \"^18.2.0\",\n    \"react-router-dom\": \"^6.15.0\"\n  },\n  \"devDependencies\": {\n    \"@rollup/plugin-replace\": \"^5.0.2\",\n    \"@types/node\": \"^18.11.18\",\n    \"@types/react\": \"^18.0.27\",\n    \"@types/react-dom\": \"^18.0.10\",\n    \"@vitejs/plugin-react\": \"^3.0.1\",\n    \"typescript\": \"^4.9.5\",\n    \"vite\": \"^4.0.4\"\n  }\n}\n"
  },
  {
    "path": "examples/custom-filter-link/src/App.tsx",
    "content": "import * as React from \"react\";\nimport {\n  Routes,\n  Route,\n  Outlet,\n  Link,\n  useSearchParams,\n  useParams,\n} from \"react-router-dom\";\nimport type { LinkProps } from \"react-router-dom\";\nimport { VisuallyHidden } from \"@reach/visually-hidden\";\n\nimport { brands, filterByBrand, getSneakerById, SNEAKERS } from \"./snkrs\";\n\nexport default function App() {\n  return (\n    <div>\n      <h1>Custom Filter Link Example</h1>\n\n      <p>\n        This example demonstrates how to create a \"filter link\" like one that is\n        commonly used to filter a list of products on an e-commerce website. The\n        <code>&lt;BrandLink&gt;</code> component is a custom{\" \"}\n        <code>&lt;Link&gt;</code> that knows whether or not it is currently\n        \"active\" by what is in the URL query string.\n      </p>\n\n      <Routes>\n        <Route path=\"/\" element={<Layout />}>\n          <Route index element={<SneakerGrid />} />\n          <Route path=\"/sneakers/:id\" element={<SneakerView />} />\n          <Route path=\"*\" element={<NoMatch />} />\n        </Route>\n      </Routes>\n    </div>\n  );\n}\n\ninterface BrandLinkProps extends Omit<LinkProps, \"to\"> {\n  brand: string;\n}\n\nfunction BrandLink({ brand, children, ...props }: BrandLinkProps) {\n  let [searchParams] = useSearchParams();\n  let isActive = searchParams.get(\"brand\") === brand;\n\n  return (\n    <Link\n      to={`/?brand=${brand}`}\n      {...props}\n      style={{\n        ...props.style,\n        color: isActive ? \"red\" : \"black\",\n      }}\n    >\n      {children}\n    </Link>\n  );\n}\n\nfunction Layout() {\n  return (\n    <div>\n      <nav>\n        <h3>Filter by brand</h3>\n        <ul>\n          <li>\n            <Link to=\"/\">All</Link>\n          </li>\n          {brands.map((brand) => (\n            <li key={brand}>\n              <BrandLink brand={brand}>{brand}</BrandLink>\n            </li>\n          ))}\n        </ul>\n      </nav>\n\n      <hr />\n\n      <Outlet />\n    </div>\n  );\n}\n\nfunction SneakerGrid() {\n  let [searchParams] = useSearchParams();\n  let brand = searchParams.get(\"brand\");\n\n  const sneakers = React.useMemo(() => {\n    if (!brand) return SNEAKERS;\n    return filterByBrand(brand);\n  }, [brand]);\n\n  return (\n    <main>\n      <h2>Sneakers</h2>\n\n      <div\n        style={{\n          display: \"grid\",\n          gridTemplateColumns: \"repeat(auto-fill, minmax(250px, 1fr))\",\n          gap: \"12px 24px\",\n        }}\n      >\n        {sneakers.map((snkr) => {\n          let name = `${snkr.brand} ${snkr.model} ${snkr.colorway}`;\n          return (\n            <div key={snkr.id} style={{ position: \"relative\" }}>\n              <img\n                width={200}\n                height={200}\n                src={snkr.imageUrl}\n                alt={name}\n                style={{\n                  borderRadius: \"8px\",\n                  width: \"100%\",\n                  height: \"auto\",\n                  aspectRatio: \"1 / 1\",\n                }}\n              />\n              <Link\n                style={{ position: \"absolute\", inset: 0 }}\n                to={`/sneakers/${snkr.id}`}\n              >\n                <VisuallyHidden>{name}</VisuallyHidden>\n              </Link>\n              <div>\n                <p>{name}</p>\n              </div>\n            </div>\n          );\n        })}\n      </div>\n    </main>\n  );\n}\n\nfunction SneakerView() {\n  let { id } = useParams<\"id\">();\n\n  if (!id) {\n    return <NoMatch />;\n  }\n\n  let snkr = getSneakerById(id);\n\n  if (!snkr) {\n    return <NoMatch />;\n  }\n\n  let name = `${snkr.brand} ${snkr.model} ${snkr.colorway}`;\n\n  return (\n    <div>\n      <h2>{name}</h2>\n      <img\n        width={400}\n        height={400}\n        style={{\n          borderRadius: \"8px\",\n          maxWidth: \"100%\",\n          aspectRatio: \"1 / 1\",\n        }}\n        src={snkr.imageUrl}\n        alt={name}\n      />\n    </div>\n  );\n}\n\nfunction NoMatch() {\n  return (\n    <div>\n      <h2>Nothing to see here!</h2>\n      <p>\n        <Link to=\"/\">Go to the home page</Link>\n      </p>\n    </div>\n  );\n}\n"
  },
  {
    "path": "examples/custom-filter-link/src/index.css",
    "content": "body {\n  font-family:\n    -apple-system, BlinkMacSystemFont, \"Segoe UI\", \"Roboto\", \"Oxygen\", \"Ubuntu\",\n    \"Cantarell\", \"Fira Sans\", \"Droid Sans\", \"Helvetica Neue\", sans-serif;\n  -webkit-font-smoothing: antialiased;\n  -moz-osx-font-smoothing: grayscale;\n}\n\ncode {\n  font-family:\n    source-code-pro, Menlo, Monaco, Consolas, \"Courier New\", monospace;\n}\n"
  },
  {
    "path": "examples/custom-filter-link/src/main.tsx",
    "content": "import * as React from \"react\";\nimport * as ReactDOM from \"react-dom/client\";\nimport { BrowserRouter } from \"react-router-dom\";\n\nimport \"./index.css\";\nimport App from \"./App\";\n\nReactDOM.createRoot(document.getElementById(\"root\")).render(\n  <React.StrictMode>\n    <BrowserRouter>\n      <App />\n    </BrowserRouter>\n  </React.StrictMode>,\n);\n"
  },
  {
    "path": "examples/custom-filter-link/src/snkrs.ts",
    "content": "interface Sneaker {\n  id: string;\n  colorway: string;\n  imageUrl: string;\n  model: string;\n  brand: string;\n}\n\nlet SNEAKERS: Sneaker[] = [\n  {\n    id: \"1\",\n    colorway: \"Pine Green\",\n    imageUrl:\n      \"https://images.mcan.sh/b_auto,c_pad,f_auto,h_400,q_auto,w_400/v1/shoes/bkkj0lqzlwlwdwtofqxs\",\n    model: \"Blazer Low 77 Vintage\",\n    brand: \"Nike\",\n  },\n  {\n    id: \"2\",\n    colorway: \"Reverse Infrared\",\n    imageUrl:\n      \"https://images.mcan.sh/b_auto,c_pad,f_auto,h_400,q_auto,w_400/v1/shoes/RPlzC_CBHjiMM4dr90gdU\",\n    model: \"Air Max 90\",\n    brand: \"Nike\",\n  },\n  {\n    id: \"3\",\n    colorway: \"White/Black/Desert\",\n    imageUrl:\n      \"https://images.mcan.sh/b_auto,c_pad,f_auto,h_400,q_auto,w_400/v1/shoes/0bf9336b-03c9-4cbd-b482-f4e80b770582\",\n    model: \"Court Legacy\",\n    brand: \"Nike\",\n  },\n  {\n    id: \"5\",\n    colorway: \"Beluga 2.0\",\n    imageUrl:\n      \"https://images.mcan.sh/b_auto,c_pad,f_auto,h_400,q_auto,w_400/v1/shoes/irxakb1ij0uzmcvn9szo\",\n    model: \"Yeezy 350 v2\",\n    brand: \"Adidas\",\n  },\n  {\n    id: \"6\",\n    colorway: \"Pumpkin Spice\",\n    imageUrl:\n      \"https://images.mcan.sh/b_auto,c_pad,f_auto,h_400,q_auto,w_400/v1/shoes/g9tjjjdn476nhou1c1dj\",\n    model: \"Grid SD\",\n    brand: \"Saucony\",\n  },\n  {\n    id: \"7\",\n    colorway: \"Golden Coast\",\n    imageUrl:\n      \"https://images.mcan.sh/b_auto,c_pad,f_auto,h_400,q_auto,w_400/v1/shoes/erg1lxa8x29h1wtbog9a\",\n    model: \"Checkerboard Slip-On\",\n    brand: \"Vans\",\n  },\n  {\n    id: \"8\",\n    colorway: \"Have a Nike Day\",\n    imageUrl:\n      \"https://images.mcan.sh/b_auto,c_pad,f_auto,h_400,q_auto,w_400/v1/shoes/u4z27k4wyzr7bxatlfgj\",\n    model: \"Air Max 1\",\n    brand: \"Nike\",\n  },\n];\n\nfunction filterByBrand(brand: string) {\n  return SNEAKERS.filter(\n    (sneaker) => sneaker.brand.toLowerCase() === brand.toLowerCase(),\n  );\n}\n\nfunction getSneakerById(id: string) {\n  return SNEAKERS.find((sneaker) => sneaker.id === id);\n}\n\nlet brands = [...new Set(SNEAKERS.map((sneaker) => sneaker.brand))];\n\nexport { brands, SNEAKERS, filterByBrand, getSneakerById };\n"
  },
  {
    "path": "examples/custom-filter-link/src/vite-env.d.ts",
    "content": "/// <reference types=\"vite/client\" />\n"
  },
  {
    "path": "examples/custom-filter-link/tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"baseUrl\": \".\",\n    \"target\": \"ESNext\",\n    \"lib\": [\"DOM\", \"DOM.Iterable\", \"ESNext\"],\n    \"allowJs\": false,\n    \"skipLibCheck\": true,\n    \"esModuleInterop\": false,\n    \"allowSyntheticDefaultImports\": true,\n    \"strict\": true,\n    \"module\": \"ESNext\",\n    \"moduleResolution\": \"Node\",\n    \"resolveJsonModule\": true,\n    \"verbatimModuleSyntax\": true,\n    \"noEmit\": true,\n    \"jsx\": \"react-jsx\",\n    \"importsNotUsedAsValues\": \"error\"\n  },\n  \"include\": [\"./src\"]\n}\n"
  },
  {
    "path": "examples/custom-filter-link/vite.config.ts",
    "content": "import * as path from \"path\";\nimport { defineConfig } from \"vite\";\nimport react from \"@vitejs/plugin-react\";\nimport rollupReplace from \"@rollup/plugin-replace\";\n\n// https://vitejs.dev/config/\nexport default defineConfig({\n  server: {\n    port: 3000,\n  },\n  plugins: [\n    rollupReplace({\n      preventAssignment: true,\n      values: {\n        \"process.env.NODE_ENV\": JSON.stringify(\"development\"),\n      },\n    }),\n    react(),\n  ],\n  resolve: process.env.USE_SOURCE\n    ? {\n        alias: {\n          \"react-router\": path.resolve(\n            __dirname,\n            \"../../packages/react-router/index.ts\",\n          ),\n          \"react-router-dom\": path.resolve(\n            __dirname,\n            \"../../packages/react-router-dom/index.tsx\",\n          ),\n        },\n      }\n    : {},\n});\n"
  },
  {
    "path": "examples/custom-link/.gitignore",
    "content": "node_modules\n.DS_Store\ndist\ndist-ssr\n*.local\n"
  },
  {
    "path": "examples/custom-link/.stackblitzrc",
    "content": "{\n  \"installDependencies\": true,\n  \"startCommand\": \"npm run dev\"\n}\n"
  },
  {
    "path": "examples/custom-link/README.md",
    "content": "---\ntitle: Custom Active Link\ntoc: false\n---\n\n# Custom Link Example\n\nThis example demonstrates how to make a custom `<Link>` component to render something different when the link is \"active\" using the `useMatch()` and `useResolvedPath()` hooks.\n\n## Preview\n\nOpen this example on [StackBlitz](https://stackblitz.com):\n\n[![Open in StackBlitz](https://developer.stackblitz.com/img/open_in_stackblitz.svg)](https://stackblitz.com/github/remix-run/react-router/tree/main/examples/custom-link?file=src/App.tsx)\n"
  },
  {
    "path": "examples/custom-link/index.html",
    "content": "<!doctype html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"UTF-8\" />\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" />\n    <title>React Router - Custom Link Example</title>\n  </head>\n  <body>\n    <div id=\"root\"></div>\n    <script type=\"module\" src=\"/src/main.tsx\"></script>\n  </body>\n</html>\n"
  },
  {
    "path": "examples/custom-link/package.json",
    "content": "{\n  \"name\": \"custom-link\",\n  \"private\": true,\n  \"scripts\": {\n    \"dev\": \"vite\",\n    \"build\": \"tsc && vite build\",\n    \"serve\": \"vite preview\"\n  },\n  \"dependencies\": {\n    \"react\": \"^18.2.0\",\n    \"react-dom\": \"^18.2.0\",\n    \"react-router-dom\": \"^6.15.0\"\n  },\n  \"devDependencies\": {\n    \"@rollup/plugin-replace\": \"^5.0.2\",\n    \"@types/node\": \"18.x\",\n    \"@types/react\": \"^18.0.27\",\n    \"@types/react-dom\": \"^18.0.10\",\n    \"@vitejs/plugin-react\": \"^3.0.1\",\n    \"typescript\": \"^4.9.5\",\n    \"vite\": \"^4.0.4\"\n  }\n}\n"
  },
  {
    "path": "examples/custom-link/src/App.tsx",
    "content": "import {\n  Routes,\n  Route,\n  Outlet,\n  Link,\n  useMatch,\n  useResolvedPath,\n} from \"react-router-dom\";\nimport type { LinkProps } from \"react-router-dom\";\n\nexport default function App() {\n  return (\n    <div>\n      <h1>Custom Link Example</h1>\n\n      <p>\n        This example demonstrates how to create a custom{\" \"}\n        <code>&lt;Link&gt;</code> component that knows whether or not it is\n        \"active\" using the low-level <code>useResolvedPath()</code> and{\" \"}\n        <code>useMatch()</code> hooks.\n      </p>\n\n      <Routes>\n        <Route path=\"/\" element={<Layout />}>\n          <Route index element={<Home />} />\n          <Route path=\"about\" element={<About />} />\n          <Route path=\"*\" element={<NoMatch />} />\n        </Route>\n      </Routes>\n    </div>\n  );\n}\n\nfunction CustomLink({ children, to, ...props }: LinkProps) {\n  let resolved = useResolvedPath(to);\n  let match = useMatch({ path: resolved.pathname, end: true });\n\n  return (\n    <div>\n      <Link\n        style={{ textDecoration: match ? \"underline\" : \"none\" }}\n        to={to}\n        {...props}\n      >\n        {children}\n      </Link>\n      {match && \" (active)\"}\n    </div>\n  );\n}\n\nfunction Layout() {\n  return (\n    <div>\n      <nav>\n        <ul>\n          <li>\n            <CustomLink to=\"/\">Home</CustomLink>\n          </li>\n          <li>\n            <CustomLink to=\"/about\">About</CustomLink>\n          </li>\n        </ul>\n      </nav>\n\n      <hr />\n\n      <Outlet />\n    </div>\n  );\n}\n\nfunction Home() {\n  return (\n    <div>\n      <h1>Home</h1>\n    </div>\n  );\n}\n\nfunction About() {\n  return (\n    <div>\n      <h1>About</h1>\n    </div>\n  );\n}\n\nfunction NoMatch() {\n  return (\n    <div>\n      <h1>Nothing to see here!</h1>\n      <p>\n        <Link to=\"/\">Go to the home page</Link>\n      </p>\n    </div>\n  );\n}\n"
  },
  {
    "path": "examples/custom-link/src/index.css",
    "content": "body {\n  font-family:\n    -apple-system, BlinkMacSystemFont, \"Segoe UI\", \"Roboto\", \"Oxygen\", \"Ubuntu\",\n    \"Cantarell\", \"Fira Sans\", \"Droid Sans\", \"Helvetica Neue\", sans-serif;\n  -webkit-font-smoothing: antialiased;\n  -moz-osx-font-smoothing: grayscale;\n}\n\ncode {\n  font-family:\n    source-code-pro, Menlo, Monaco, Consolas, \"Courier New\", monospace;\n}\n"
  },
  {
    "path": "examples/custom-link/src/main.tsx",
    "content": "import * as React from \"react\";\nimport * as ReactDOM from \"react-dom/client\";\nimport { BrowserRouter } from \"react-router-dom\";\n\nimport \"./index.css\";\nimport App from \"./App\";\n\nReactDOM.createRoot(document.getElementById(\"root\")).render(\n  <React.StrictMode>\n    <BrowserRouter>\n      <App />\n    </BrowserRouter>\n  </React.StrictMode>,\n);\n"
  },
  {
    "path": "examples/custom-link/src/vite-env.d.ts",
    "content": "/// <reference types=\"vite/client\" />\n"
  },
  {
    "path": "examples/custom-link/tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"baseUrl\": \".\",\n    \"target\": \"ESNext\",\n    \"lib\": [\"DOM\", \"DOM.Iterable\", \"ESNext\"],\n    \"allowJs\": false,\n    \"skipLibCheck\": true,\n    \"esModuleInterop\": false,\n    \"allowSyntheticDefaultImports\": true,\n    \"strict\": true,\n    \"module\": \"ESNext\",\n    \"moduleResolution\": \"Node\",\n    \"resolveJsonModule\": true,\n    \"verbatimModuleSyntax\": true,\n    \"noEmit\": true,\n    \"jsx\": \"react-jsx\",\n    \"importsNotUsedAsValues\": \"error\"\n  },\n  \"include\": [\"./src\"]\n}\n"
  },
  {
    "path": "examples/custom-link/vite.config.ts",
    "content": "import * as path from \"path\";\nimport { defineConfig } from \"vite\";\nimport react from \"@vitejs/plugin-react\";\nimport rollupReplace from \"@rollup/plugin-replace\";\n\n// https://vitejs.dev/config/\nexport default defineConfig({\n  server: {\n    port: 3000,\n  },\n  plugins: [\n    rollupReplace({\n      preventAssignment: true,\n      values: {\n        \"process.env.NODE_ENV\": JSON.stringify(\"development\"),\n      },\n    }),\n    react(),\n  ],\n  resolve: process.env.USE_SOURCE\n    ? {\n        alias: {\n          \"react-router\": path.resolve(\n            __dirname,\n            \"../../packages/react-router/index.ts\",\n          ),\n          \"react-router-dom\": path.resolve(\n            __dirname,\n            \"../../packages/react-router-dom/index.tsx\",\n          ),\n        },\n      }\n    : {},\n});\n"
  },
  {
    "path": "examples/custom-query-parsing/.gitignore",
    "content": "node_modules\n.DS_Store\ndist\ndist-ssr\n*.local\n"
  },
  {
    "path": "examples/custom-query-parsing/.stackblitzrc",
    "content": "{\n  \"installDependencies\": true,\n  \"startCommand\": \"npm run dev\"\n}\n"
  },
  {
    "path": "examples/custom-query-parsing/README.md",
    "content": "---\ntitle: Custom Query Parsing\ntoc: false\n---\n\n# Custom Query Parsing Example\n\nThis example demonstrates how to store a complex data structure in the URL query string using a custom hook.\n\nIt's a good example of how React Router's low-level hooks provide you with all the flexibility you need to create your own custom hooks that fit the needs of your app.\n\n## Preview\n\nOpen this example on [StackBlitz](https://stackblitz.com):\n\n[![Open in StackBlitz](https://developer.stackblitz.com/img/open_in_stackblitz.svg)](https://stackblitz.com/github/remix-run/react-router/tree/main/examples/custom-query-parsing?file=src/App.tsx)\n"
  },
  {
    "path": "examples/custom-query-parsing/index.html",
    "content": "<!doctype html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"UTF-8\" />\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" />\n    <title>React Router - Custom Query Parsing Example</title>\n  </head>\n  <body>\n    <div id=\"root\"></div>\n    <script type=\"module\" src=\"/src/main.tsx\"></script>\n  </body>\n</html>\n"
  },
  {
    "path": "examples/custom-query-parsing/package.json",
    "content": "{\n  \"name\": \"custom-query-parse-serialization\",\n  \"private\": true,\n  \"scripts\": {\n    \"dev\": \"vite\",\n    \"build\": \"tsc && vite build\",\n    \"serve\": \"vite preview\"\n  },\n  \"dependencies\": {\n    \"jsurl\": \"0.1.5\",\n    \"react\": \"^18.2.0\",\n    \"react-dom\": \"^18.2.0\",\n    \"react-router-dom\": \"^6.15.0\"\n  },\n  \"devDependencies\": {\n    \"@rollup/plugin-replace\": \"^5.0.2\",\n    \"@types/node\": \"18.x\",\n    \"@types/react\": \"^18.0.27\",\n    \"@types/react-dom\": \"^18.0.10\",\n    \"@vitejs/plugin-react\": \"^3.0.1\",\n    \"typescript\": \"^4.9.5\",\n    \"vite\": \"^4.0.4\"\n  }\n}\n"
  },
  {
    "path": "examples/custom-query-parsing/src/App.tsx",
    "content": "import * as React from \"react\";\nimport * as JSURL from \"jsurl\";\nimport type { NavigateOptions } from \"react-router-dom\";\nimport { Routes, Route, Link, useSearchParams } from \"react-router-dom\";\n\nexport default function App() {\n  return (\n    <div>\n      <h1>Custom Query Parsing Example</h1>\n\n      <p>\n        This example demonstrates how to store a complex data structure in a URL\n        query parameter.\n      </p>\n\n      <p>\n        Each time a field in the form below changes, the URL is updated with a\n        serialized version of the form's values. To see the effect this has,\n        manipulate some fields in the form. Then, copy the URL in the address\n        bar and paste it into a new tab in your browser to see the form in the\n        exact same state as when you left it!\n      </p>\n\n      <Routes>\n        <Route index element={<Home />} />\n        <Route path=\"*\" element={<NoMatch />} />\n      </Routes>\n    </div>\n  );\n}\n\n/**\n * This custom hook is a wrapper around `useSearchParams()` that parses and\n * serializes the search param value using the JSURL library, which permits any\n * JavaScript value to be safely URL-encoded.\n *\n * It's a good example of how React hooks offer a great deal of flexibility when\n * you compose them together!\n *\n * TODO: rethink the generic type here, users can put whatever they want in the\n * URL, probably best to use runtime validation with a type predicate:\n * https://www.typescriptlang.org/docs/handbook/2/narrowing.html#using-type-predicates\n */\nfunction useQueryParam<T>(\n  key: string,\n): [T | undefined, (newQuery: T, options?: NavigateOptions) => void] {\n  let [searchParams, setSearchParams] = useSearchParams();\n  let paramValue = searchParams.get(key);\n\n  let value = React.useMemo(() => JSURL.parse(paramValue), [paramValue]);\n\n  let setValue = React.useCallback(\n    (newValue: T, options?: NavigateOptions) => {\n      let newSearchParams = new URLSearchParams(searchParams);\n      newSearchParams.set(key, JSURL.stringify(newValue));\n      setSearchParams(newSearchParams, options);\n    },\n    [key, searchParams, setSearchParams],\n  );\n\n  return [value, setValue];\n}\n\ninterface Pizza {\n  toppings: string[];\n  crust: string;\n  extraSauce: boolean;\n}\n\nfunction Home() {\n  let [pizza, setPizza] = useQueryParam<Pizza>(\"pizza\");\n\n  if (!pizza) {\n    pizza = { toppings: [], crust: \"regular\", extraSauce: false };\n  }\n\n  function handleChange(event: React.ChangeEvent<HTMLFormElement>) {\n    let form = event.currentTarget;\n    let formData = new FormData(form);\n\n    // This complex data structure is preserved in the URL in the\n    // `pizza` query parameter each time a value in the form changes!\n    let pizza: Pizza = {\n      toppings: formData.getAll(\"toppings\") as string[],\n      crust: formData.get(\"crust\") as string,\n      extraSauce: formData.get(\"extraSauce\") === \"on\",\n    };\n\n    setPizza(pizza, { replace: true });\n  }\n\n  return (\n    <div>\n      <form onChange={handleChange}>\n        <p>What would you like on your pizza?</p>\n\n        <p>\n          <label>\n            <input\n              defaultChecked={pizza.toppings.includes(\"pepperoni\")}\n              type=\"checkbox\"\n              name=\"toppings\"\n              value=\"pepperoni\"\n            />{\" \"}\n            Pepperoni\n          </label>\n          <br />\n          <label>\n            <input\n              defaultChecked={pizza.toppings.includes(\"bell-peppers\")}\n              type=\"checkbox\"\n              name=\"toppings\"\n              value=\"bell-peppers\"\n            />{\" \"}\n            Bell Peppers\n          </label>\n          <br />\n          <label>\n            <input\n              type=\"checkbox\"\n              name=\"toppings\"\n              value=\"olives\"\n              defaultChecked={pizza.toppings.includes(\"olives\")}\n            />{\" \"}\n            Olives\n          </label>\n        </p>\n\n        <p>\n          <label>\n            <input\n              type=\"radio\"\n              name=\"crust\"\n              value=\"regular\"\n              defaultChecked={pizza.crust === \"regular\"}\n            />{\" \"}\n            Regular Crust\n          </label>\n          <br />\n          <label>\n            <input\n              type=\"radio\"\n              name=\"crust\"\n              value=\"thin\"\n              defaultChecked={pizza.crust === \"thin\"}\n            />{\" \"}\n            Thin Crust\n          </label>\n          <br />\n          <label>\n            <input\n              type=\"radio\"\n              name=\"crust\"\n              value=\"deep-dish\"\n              defaultChecked={pizza.crust === \"deep-dish\"}\n            />{\" \"}\n            Deep Dish\n          </label>\n        </p>\n\n        <p>\n          <label>\n            <input\n              type=\"checkbox\"\n              name=\"extraSauce\"\n              defaultChecked={pizza.extraSauce}\n            />{\" \"}\n            Extra Sauce\n          </label>\n        </p>\n      </form>\n\n      <hr />\n\n      <p>The current form values are:</p>\n\n      <pre>{JSON.stringify(pizza || {}, null, 2)}</pre>\n    </div>\n  );\n}\n\nfunction NoMatch() {\n  return (\n    <div>\n      <h2>Nothing to see here!</h2>\n      <p>\n        <Link to=\"/\">Go to the home page</Link>\n      </p>\n    </div>\n  );\n}\n"
  },
  {
    "path": "examples/custom-query-parsing/src/index.css",
    "content": "body {\n  font-family:\n    -apple-system, BlinkMacSystemFont, \"Segoe UI\", \"Roboto\", \"Oxygen\", \"Ubuntu\",\n    \"Cantarell\", \"Fira Sans\", \"Droid Sans\", \"Helvetica Neue\", sans-serif;\n  -webkit-font-smoothing: antialiased;\n  -moz-osx-font-smoothing: grayscale;\n}\n\ncode {\n  font-family:\n    source-code-pro, Menlo, Monaco, Consolas, \"Courier New\", monospace;\n}\n"
  },
  {
    "path": "examples/custom-query-parsing/src/main.tsx",
    "content": "import * as React from \"react\";\nimport * as ReactDOM from \"react-dom/client\";\nimport { BrowserRouter } from \"react-router-dom\";\n\nimport \"./index.css\";\nimport App from \"./App\";\n\nReactDOM.createRoot(document.getElementById(\"root\")).render(\n  <React.StrictMode>\n    <BrowserRouter>\n      <App />\n    </BrowserRouter>\n  </React.StrictMode>,\n);\n"
  },
  {
    "path": "examples/custom-query-parsing/src/vite-env.d.ts",
    "content": "/// <reference types=\"vite/client\" />\n"
  },
  {
    "path": "examples/custom-query-parsing/tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"baseUrl\": \".\",\n    \"target\": \"ESNext\",\n    \"lib\": [\"DOM\", \"DOM.Iterable\", \"ESNext\"],\n    \"allowJs\": false,\n    \"skipLibCheck\": true,\n    \"esModuleInterop\": false,\n    \"allowSyntheticDefaultImports\": true,\n    \"strict\": true,\n    \"module\": \"ESNext\",\n    \"moduleResolution\": \"Node\",\n    \"resolveJsonModule\": true,\n    \"verbatimModuleSyntax\": true,\n    \"noEmit\": true,\n    \"jsx\": \"react-jsx\",\n    \"importsNotUsedAsValues\": \"error\"\n  },\n  \"include\": [\"./src\", \"./types\"]\n}\n"
  },
  {
    "path": "examples/custom-query-parsing/types/jsurl.d.ts",
    "content": "declare module \"jsurl\" {\n  type Nullable<T> = T | null | undefined;\n  export function stringify(input: any): string;\n  export function parse(input?: Nullable<string>): Nullable<any>;\n}\n"
  },
  {
    "path": "examples/custom-query-parsing/vite.config.ts",
    "content": "import * as path from \"path\";\nimport { defineConfig } from \"vite\";\nimport react from \"@vitejs/plugin-react\";\nimport rollupReplace from \"@rollup/plugin-replace\";\n\n// https://vitejs.dev/config/\nexport default defineConfig({\n  server: {\n    port: 3000,\n  },\n  plugins: [\n    rollupReplace({\n      preventAssignment: true,\n      values: {\n        \"process.env.NODE_ENV\": JSON.stringify(\"development\"),\n      },\n    }),\n    react(),\n  ],\n  resolve: process.env.USE_SOURCE\n    ? {\n        alias: {\n          \"react-router\": path.resolve(\n            __dirname,\n            \"../../packages/react-router/index.ts\",\n          ),\n          \"react-router-dom\": path.resolve(\n            __dirname,\n            \"../../packages/react-router-dom/index.tsx\",\n          ),\n        },\n      }\n    : {},\n});\n"
  },
  {
    "path": "examples/data-router/.gitignore",
    "content": "node_modules\n.DS_Store\ndist\ndist-ssr\n*.local\n"
  },
  {
    "path": "examples/data-router/.stackblitzrc",
    "content": "{\n  \"installDependencies\": true,\n  \"startCommand\": \"npm run dev\"\n}\n"
  },
  {
    "path": "examples/data-router/README.md",
    "content": "---\ntitle: Data Router\ntoc: false\norder: 1\n---\n\n# Data Routers\n\nThis example demonstrates some of the basic features of Data Router, including:\n\n- Loader functions\n- Action functions\n- <Link> and <Form> navigations\n- <fetcher.Form> submissions\n\n## Preview\n\nOpen this example on [StackBlitz](https://stackblitz.com):\n\n[![Open in StackBlitz](https://developer.stackblitz.com/img/open_in_stackblitz.svg)](https://stackblitz.com/github/remix-run/react-router/tree/main/examples/data-router?file=src/App.tsx)\n"
  },
  {
    "path": "examples/data-router/index.html",
    "content": "<!doctype html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"UTF-8\" />\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" />\n    <title>React Router - Basic Example</title>\n  </head>\n  <body>\n    <div id=\"root\"></div>\n    <script type=\"module\" src=\"/src/main.tsx\"></script>\n  </body>\n</html>\n"
  },
  {
    "path": "examples/data-router/package.json",
    "content": "{\n  \"name\": \"data-router\",\n  \"private\": true,\n  \"scripts\": {\n    \"dev\": \"vite\",\n    \"build\": \"tsc && vite build\",\n    \"serve\": \"vite preview\"\n  },\n  \"dependencies\": {\n    \"react\": \"^18.2.0\",\n    \"react-dom\": \"^18.2.0\",\n    \"react-router-dom\": \"^6.15.0\"\n  },\n  \"devDependencies\": {\n    \"@rollup/plugin-replace\": \"5.0.2\",\n    \"@types/node\": \"18.11.18\",\n    \"@types/react\": \"18.0.27\",\n    \"@types/react-dom\": \"18.0.10\",\n    \"@vitejs/plugin-react\": \"3.0.1\",\n    \"typescript\": \"4.9.5\",\n    \"vite\": \"4.0.4\"\n  }\n}\n"
  },
  {
    "path": "examples/data-router/src/app.tsx",
    "content": "import * as React from \"react\";\nimport type { ActionFunctionArgs, LoaderFunctionArgs } from \"react-router-dom\";\nimport {\n  Await,\n  createBrowserRouter,\n  defer,\n  Form,\n  Link,\n  Outlet,\n  RouterProvider,\n  useAsyncError,\n  useAsyncValue,\n  useFetcher,\n  useFetchers,\n  useLoaderData,\n  useNavigation,\n  useParams,\n  useRevalidator,\n  useRouteError,\n} from \"react-router-dom\";\n\nimport type { Todos } from \"./todos\";\nimport { addTodo, deleteTodo, getTodos } from \"./todos\";\n\nimport \"./index.css\";\n\nlet router = createBrowserRouter([\n  {\n    path: \"/\",\n    Component: Layout,\n    children: [\n      {\n        index: true,\n        loader: homeLoader,\n        Component: Home,\n      },\n      {\n        path: \"todos\",\n        action: todosAction,\n        loader: todosLoader,\n        Component: TodosList,\n        ErrorBoundary: TodosBoundary,\n        children: [\n          {\n            path: \":id\",\n            loader: todoLoader,\n            Component: Todo,\n          },\n        ],\n      },\n      {\n        path: \"deferred\",\n        loader: deferredLoader,\n        Component: DeferredPage,\n      },\n    ],\n  },\n]);\n\nif (import.meta.hot) {\n  import.meta.hot.dispose(() => router.dispose());\n}\n\nexport default function App() {\n  return <RouterProvider router={router} fallbackElement={<Fallback />} />;\n}\n\nexport function sleep(n: number = 500) {\n  return new Promise((r) => setTimeout(r, n));\n}\n\nexport function Fallback() {\n  return <p>Performing initial data load</p>;\n}\n\n// Layout\nexport function Layout() {\n  let navigation = useNavigation();\n  let revalidator = useRevalidator();\n  let fetchers = useFetchers();\n  let fetcherInProgress = fetchers.some((f) =>\n    [\"loading\", \"submitting\"].includes(f.state),\n  );\n\n  return (\n    <>\n      <h1>Data Router Example</h1>\n\n      <p>\n        This example demonstrates some of the core features of React Router\n        including nested &lt;Route&gt;s, &lt;Outlet&gt;s, &lt;Link&gt;s, and\n        using a \"*\" route (aka \"splat route\") to render a \"not found\" page when\n        someone visits an unrecognized URL.\n      </p>\n\n      <nav>\n        <ul>\n          <li>\n            <Link to=\"/\">Home</Link>\n          </li>\n          <li>\n            <Link to=\"/todos\">Todos</Link>\n          </li>\n          <li>\n            <Link to=\"/deferred\">Deferred</Link>\n          </li>\n          <li>\n            <Link to=\"/404\">404 Link</Link>\n          </li>\n          <li>\n            <button onClick={() => revalidator.revalidate()}>\n              Revalidate Data\n            </button>\n          </li>\n        </ul>\n      </nav>\n      <div style={{ position: \"fixed\", top: 0, right: 0 }}>\n        {navigation.state !== \"idle\" && <p>Navigation in progress...</p>}\n        {revalidator.state !== \"idle\" && <p>Revalidation in progress...</p>}\n        {fetcherInProgress && <p>Fetcher in progress...</p>}\n      </div>\n      <p>\n        Click on over to <Link to=\"/todos\">/todos</Link> and check out these\n        data loading APIs!\n      </p>\n      <p>\n        Or, checkout <Link to=\"/deferred\">/deferred</Link> to see how to\n        separate critical and lazily loaded data in your loaders.\n      </p>\n      <p>\n        We've introduced some fake async-aspects of routing here, so Keep an eye\n        on the top-right hand corner to see when we're actively navigating.\n      </p>\n      <hr />\n      <Outlet />\n    </>\n  );\n}\n\n// Home\ninterface HomeLoaderData {\n  date: string;\n}\n\nexport async function homeLoader(): Promise<HomeLoaderData> {\n  await sleep();\n  return {\n    date: new Date().toISOString(),\n  };\n}\n\nexport function Home() {\n  let data = useLoaderData() as HomeLoaderData;\n  return (\n    <>\n      <h2>Home</h2>\n      <p>Date from loader: {data.date}</p>\n    </>\n  );\n}\n\n// Todos\nexport async function todosAction({ request }: ActionFunctionArgs) {\n  await sleep();\n\n  let formData = await request.formData();\n\n  // Deletion via fetcher\n  if (formData.get(\"action\") === \"delete\") {\n    let id = formData.get(\"todoId\");\n    if (typeof id === \"string\") {\n      deleteTodo(id);\n      return { ok: true };\n    }\n  }\n\n  // Addition via <Form>\n  let todo = formData.get(\"todo\");\n  if (typeof todo === \"string\") {\n    addTodo(todo);\n  }\n\n  return new Response(null, {\n    status: 302,\n    headers: { Location: \"/todos\" },\n  });\n}\n\nexport async function todosLoader(): Promise<Todos> {\n  await sleep();\n  return getTodos();\n}\n\nexport function TodosList() {\n  let todos = useLoaderData() as Todos;\n  let navigation = useNavigation();\n  let formRef = React.useRef<HTMLFormElement>(null);\n\n  // If we add and then we delete - this will keep isAdding=true until the\n  // fetcher completes it's revalidation\n  let [isAdding, setIsAdding] = React.useState(false);\n  React.useEffect(() => {\n    if (navigation.formData?.get(\"action\") === \"add\") {\n      setIsAdding(true);\n    } else if (navigation.state === \"idle\") {\n      setIsAdding(false);\n      formRef.current?.reset();\n    }\n  }, [navigation]);\n\n  return (\n    <>\n      <h2>Todos</h2>\n      <p>\n        This todo app uses a &lt;Form&gt; to submit new todos and a\n        &lt;fetcher.form&gt; to delete todos. Click on a todo item to navigate\n        to the /todos/:id route.\n      </p>\n      <ul>\n        <li>\n          <Link to=\"/todos/junk\">\n            Click this link to force an error in the loader\n          </Link>\n        </li>\n        {Object.entries(todos).map(([id, todo]) => (\n          <li key={id}>\n            <TodoItem id={id} todo={todo} />\n          </li>\n        ))}\n      </ul>\n      <Form method=\"post\" ref={formRef}>\n        <input type=\"hidden\" name=\"action\" value=\"add\" />\n        <input name=\"todo\"></input>\n        <button type=\"submit\" disabled={isAdding}>\n          {isAdding ? \"Adding...\" : \"Add\"}\n        </button>\n      </Form>\n      <Outlet />\n    </>\n  );\n}\n\nexport function TodosBoundary() {\n  let error = useRouteError() as Error;\n  return (\n    <>\n      <h2>Error 💥</h2>\n      <p>{error.message}</p>\n    </>\n  );\n}\n\ninterface TodoItemProps {\n  id: string;\n  todo: string;\n}\n\nexport function TodoItem({ id, todo }: TodoItemProps) {\n  let fetcher = useFetcher();\n\n  let isDeleting = fetcher.formData != null;\n  return (\n    <>\n      <Link to={`/todos/${id}`}>{todo}</Link>\n      &nbsp;\n      <fetcher.Form method=\"post\" style={{ display: \"inline\" }}>\n        <input type=\"hidden\" name=\"action\" value=\"delete\" />\n        <button type=\"submit\" name=\"todoId\" value={id} disabled={isDeleting}>\n          {isDeleting ? \"Deleting...\" : \"Delete\"}\n        </button>\n      </fetcher.Form>\n    </>\n  );\n}\n\n// Todo\nexport async function todoLoader({\n  params,\n}: LoaderFunctionArgs): Promise<string> {\n  await sleep();\n  let todos = getTodos();\n  if (!params.id) {\n    throw new Error(\"Expected params.id\");\n  }\n  let todo = todos[params.id];\n  if (!todo) {\n    throw new Error(`Uh oh, I couldn't find a todo with id \"${params.id}\"`);\n  }\n  return todo;\n}\n\nexport function Todo() {\n  let params = useParams();\n  let todo = useLoaderData() as string;\n  return (\n    <>\n      <h2>Nested Todo Route:</h2>\n      <p>id: {params.id}</p>\n      <p>todo: {todo}</p>\n    </>\n  );\n}\n\n// Deferred Data\ninterface DeferredRouteLoaderData {\n  critical1: string;\n  critical2: string;\n  lazyResolved: Promise<string>;\n  lazy1: Promise<string>;\n  lazy2: Promise<string>;\n  lazy3: Promise<string>;\n  lazyError: Promise<string>;\n}\n\nconst rand = () => Math.round(Math.random() * 100);\nconst resolve = (d: string, ms: number) =>\n  new Promise((r) => setTimeout(() => r(`${d} - ${rand()}`), ms));\nconst reject = (d: Error | string, ms: number) =>\n  new Promise((_, r) =>\n    setTimeout(() => {\n      if (d instanceof Error) {\n        d.message += ` - ${rand()}`;\n      } else {\n        d += ` - ${rand()}`;\n      }\n      r(d);\n    }, ms),\n  );\n\nexport async function deferredLoader() {\n  return defer({\n    critical1: await resolve(\"Critical 1\", 250),\n    critical2: await resolve(\"Critical 2\", 500),\n    lazyResolved: Promise.resolve(\"Lazy Data immediately resolved - \" + rand()),\n    lazy1: resolve(\"Lazy 1\", 1000),\n    lazy2: resolve(\"Lazy 2\", 1500),\n    lazy3: resolve(\"Lazy 3\", 2000),\n    lazyError: reject(new Error(\"Kaboom!\"), 2500),\n  });\n}\n\nexport function DeferredPage() {\n  let data = useLoaderData() as DeferredRouteLoaderData;\n  return (\n    <div>\n      {/* Critical data renders immediately */}\n      <p>{data.critical1}</p>\n      <p>{data.critical2}</p>\n\n      {/* Pre-resolved deferred data never triggers the fallback */}\n      <React.Suspense fallback={<p>should not see me!</p>}>\n        <Await resolve={data.lazyResolved}>\n          <RenderAwaitedData />\n        </Await>\n      </React.Suspense>\n\n      {/* Deferred data can be rendered using a component + the useAsyncValue() hook */}\n      <React.Suspense fallback={<p>loading 1...</p>}>\n        <Await resolve={data.lazy1}>\n          <RenderAwaitedData />\n        </Await>\n      </React.Suspense>\n\n      <React.Suspense fallback={<p>loading 2...</p>}>\n        <Await resolve={data.lazy2}>\n          <RenderAwaitedData />\n        </Await>\n      </React.Suspense>\n\n      {/* Or you can bypass the hook and use a render function */}\n      <React.Suspense fallback={<p>loading 3...</p>}>\n        <Await resolve={data.lazy3}>{(data: string) => <p>{data}</p>}</Await>\n      </React.Suspense>\n\n      {/* Deferred rejections render using the useAsyncError hook */}\n      <React.Suspense fallback={<p>loading (error)...</p>}>\n        <Await resolve={data.lazyError} errorElement={<RenderAwaitedError />}>\n          <RenderAwaitedData />\n        </Await>\n      </React.Suspense>\n    </div>\n  );\n}\n\nfunction RenderAwaitedData() {\n  let data = useAsyncValue() as string;\n  return <p>{data}</p>;\n}\n\nfunction RenderAwaitedError() {\n  let error = useAsyncError() as Error;\n  return (\n    <p style={{ color: \"red\" }}>\n      Error (errorElement)!\n      <br />\n      {error.message} {error.stack}\n    </p>\n  );\n}\n"
  },
  {
    "path": "examples/data-router/src/index.css",
    "content": "body {\n  font-family:\n    -apple-system, BlinkMacSystemFont, \"Segoe UI\", \"Roboto\", \"Oxygen\", \"Ubuntu\",\n    \"Cantarell\", \"Fira Sans\", \"Droid Sans\", \"Helvetica Neue\", sans-serif;\n  -webkit-font-smoothing: antialiased;\n  -moz-osx-font-smoothing: grayscale;\n}\n\ncode {\n  font-family:\n    source-code-pro, Menlo, Monaco, Consolas, \"Courier New\", monospace;\n}\n"
  },
  {
    "path": "examples/data-router/src/main.tsx",
    "content": "import * as React from \"react\";\nimport * as ReactDOM from \"react-dom/client\";\nimport App from \"./app\";\n\nReactDOM.createRoot(document.getElementById(\"root\")).render(\n  <React.StrictMode>\n    <App />\n  </React.StrictMode>,\n);\n"
  },
  {
    "path": "examples/data-router/src/todos.ts",
    "content": "export interface Todos {\n  [key: string]: string;\n}\n\nconst TODOS_KEY = \"todos\";\n\nexport const uuid = () => Math.random().toString(36).substr(2, 9);\n\nexport function saveTodos(todos: Todos): void {\n  return localStorage.setItem(TODOS_KEY, JSON.stringify(todos));\n}\n\nfunction initializeTodos(): Todos {\n  let todos: Todos = new Array(10)\n    .fill(null)\n    .reduce(\n      (acc, _, index) =>\n        Object.assign(acc, { [uuid()]: `Seeded Todo #${index + 1}` }),\n      {},\n    );\n  saveTodos(todos);\n  return todos;\n}\n\nexport function getTodos(): Todos {\n  let todos: Todos | null = null;\n  try {\n    // @ts-expect-error OK to throw here since we're catching\n    todos = JSON.parse(localStorage.getItem(TODOS_KEY));\n  } catch (e) {}\n  if (!todos) {\n    todos = initializeTodos();\n  }\n  return todos;\n}\n\nexport function addTodo(todo: string): void {\n  let newTodos = { ...getTodos() };\n  newTodos[uuid()] = todo;\n  saveTodos(newTodos);\n}\n\nexport function deleteTodo(id: string): void {\n  let newTodos = { ...getTodos() };\n  delete newTodos[id];\n  saveTodos(newTodos);\n}\n\nexport function resetTodos(): void {\n  localStorage.removeItem(TODOS_KEY);\n  initializeTodos();\n}\n"
  },
  {
    "path": "examples/data-router/src/vite-env.d.ts",
    "content": "/// <reference types=\"vite/client\" />\n"
  },
  {
    "path": "examples/data-router/tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"baseUrl\": \".\",\n    \"target\": \"ESNext\",\n    \"lib\": [\"DOM\", \"DOM.Iterable\", \"ESNext\"],\n    \"allowJs\": false,\n    \"skipLibCheck\": true,\n    \"esModuleInterop\": false,\n    \"allowSyntheticDefaultImports\": true,\n    \"strict\": true,\n    \"module\": \"ESNext\",\n    \"moduleResolution\": \"Node\",\n    \"resolveJsonModule\": true,\n    \"verbatimModuleSyntax\": true,\n    \"noEmit\": true,\n    \"jsx\": \"react-jsx\",\n    \"importsNotUsedAsValues\": \"error\"\n  },\n  \"include\": [\"./src\"]\n}\n"
  },
  {
    "path": "examples/data-router/vite.config.ts",
    "content": "import * as path from \"path\";\nimport { defineConfig } from \"vite\";\nimport react from \"@vitejs/plugin-react\";\nimport rollupReplace from \"@rollup/plugin-replace\";\n\n// https://vitejs.dev/config/\nexport default defineConfig({\n  server: {\n    port: 3000,\n  },\n  plugins: [\n    rollupReplace({\n      preventAssignment: true,\n      values: {\n        \"process.env.NODE_ENV\": JSON.stringify(\"development\"),\n      },\n    }),\n    react(),\n  ],\n  resolve: process.env.USE_SOURCE\n    ? {\n        alias: {\n          \"react-router\": path.resolve(\n            __dirname,\n            \"../../packages/react-router/index.ts\",\n          ),\n          \"react-router-dom\": path.resolve(\n            __dirname,\n            \"../../packages/react-router-dom/index.tsx\",\n          ),\n        },\n      }\n    : {},\n});\n"
  },
  {
    "path": "examples/error-boundaries/.gitignore",
    "content": "node_modules\n.DS_Store\ndist\ndist-ssr\n*.local\n"
  },
  {
    "path": "examples/error-boundaries/.stackblitzrc",
    "content": "{\n  \"installDependencies\": true,\n  \"startCommand\": \"npm run dev\"\n}\n"
  },
  {
    "path": "examples/error-boundaries/README.md",
    "content": "---\ntitle: Error Boundaries\ntoc: false\norder: 1\n---\n\n# Error Boundaries\n\nThis example demonstrates some of the basic features of React Router's `errorElement` boundaries, including:\n\n- Handling thrown `loader` errors\n- Handling render errors\n- Re-throwing errors from an `errorElement`\n\n## Preview\n\nOpen this example on [StackBlitz](https://stackblitz.com):\n\n[![Open in StackBlitz](https://developer.stackblitz.com/img/open_in_stackblitz.svg)](https://stackblitz.com/github/remix-run/react-router/tree/main/examples/error-boundaries?file=src/App.tsx)\n"
  },
  {
    "path": "examples/error-boundaries/index.html",
    "content": "<!doctype html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"UTF-8\" />\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" />\n    <title>React Router - Error Boundaries Example</title>\n  </head>\n  <body>\n    <div id=\"root\"></div>\n    <script type=\"module\" src=\"/src/main.tsx\"></script>\n  </body>\n</html>\n"
  },
  {
    "path": "examples/error-boundaries/package.json",
    "content": "{\n  \"name\": \"error-boundaries\",\n  \"private\": true,\n  \"scripts\": {\n    \"dev\": \"vite\",\n    \"build\": \"tsc && vite build\",\n    \"serve\": \"vite preview\"\n  },\n  \"dependencies\": {\n    \"react\": \"^18.2.0\",\n    \"react-dom\": \"^18.2.0\",\n    \"react-router-dom\": \"^6.15.0\"\n  },\n  \"devDependencies\": {\n    \"@rollup/plugin-replace\": \"5.0.2\",\n    \"@types/node\": \"18.11.18\",\n    \"@types/react\": \"^18.0.27\",\n    \"@types/react-dom\": \"^18.0.10\",\n    \"@vitejs/plugin-react\": \"3.0.1\",\n    \"typescript\": \"4.9.5\",\n    \"vite\": \"4.0.4\"\n  }\n}\n"
  },
  {
    "path": "examples/error-boundaries/src/app.tsx",
    "content": "import { createBrowserRouter, Outlet, RouterProvider } from \"react-router-dom\";\n\nimport \"./index.css\";\nimport {\n  Fallback,\n  Layout,\n  RootErrorBoundary,\n  Project,\n  ProjectErrorBoundary,\n  projectLoader,\n} from \"./routes\";\n\nlet router = createBrowserRouter([\n  {\n    path: \"/\",\n    element: <Layout />,\n    children: [\n      {\n        path: \"\",\n        element: <Outlet />,\n        errorElement: <RootErrorBoundary />,\n        children: [\n          {\n            path: \"projects/:projectId\",\n            element: <Project />,\n            errorElement: <ProjectErrorBoundary />,\n            loader: projectLoader,\n          },\n        ],\n      },\n    ],\n  },\n]);\n\nif (import.meta.hot) {\n  import.meta.hot.dispose(() => router.dispose());\n}\n\nexport default function App() {\n  return <RouterProvider router={router} fallbackElement={<Fallback />} />;\n}\n"
  },
  {
    "path": "examples/error-boundaries/src/index.css",
    "content": "body {\n  font-family:\n    -apple-system, BlinkMacSystemFont, \"Segoe UI\", \"Roboto\", \"Oxygen\", \"Ubuntu\",\n    \"Cantarell\", \"Fira Sans\", \"Droid Sans\", \"Helvetica Neue\", sans-serif;\n  -webkit-font-smoothing: antialiased;\n  -moz-osx-font-smoothing: grayscale;\n  max-width: 600px;\n}\n\ncode {\n  font-family:\n    source-code-pro, Menlo, Monaco, Consolas, \"Courier New\", monospace;\n  background-color: #eee;\n  padding: 1px 3px;\n}\n\nli {\n  padding: 10px 0;\n}\n"
  },
  {
    "path": "examples/error-boundaries/src/main.tsx",
    "content": "import * as React from \"react\";\nimport * as ReactDOM from \"react-dom/client\";\n\nimport App from \"./app\";\n\nReactDOM.createRoot(document.getElementById(\"root\")).render(\n  <React.StrictMode>\n    <App />\n  </React.StrictMode>,\n);\n"
  },
  {
    "path": "examples/error-boundaries/src/routes.tsx",
    "content": "import type { LoaderFunctionArgs } from \"react-router-dom\";\nimport {\n  isRouteErrorResponse,\n  json,\n  Link,\n  Outlet,\n  useLoaderData,\n  useRouteError,\n} from \"react-router-dom\";\n\nexport function Fallback() {\n  return <p>Performing initial data \"load\"</p>;\n}\n\nexport function Layout() {\n  return (\n    <>\n      <nav>\n        <Link to=\"/projects/authorized\">Authorized Project</Link>\n        &nbsp;|&nbsp;\n        <Link to=\"/projects/unauthorized\">Unauthorized Project</Link>\n        &nbsp;|&nbsp;\n        <Link to=\"/projects/broken\">Broken Project</Link>\n      </nav>\n      <p>\n        This example shows the flexibility of{\" \"}\n        <code>&lt;Route errorElement&gt;</code>\n      </p>\n      <ul>\n        <li>\n          Clicking the \"Authorized Project\" link will take you to the happy path\n          where we successfully load and render the details for a project.\n        </li>\n        <li>\n          Clicking the \"Unauthorized Project\" link will simulate a case where\n          the user does not have access to the given project, so our loader can\n          throw a 401 response that is handed in-context by a{\" \"}\n          <code>&lt;ProjectErrorBoundary&gt;</code>.\n        </li>\n        <li>\n          Clicking the \"Broken Project\" link will return some malformed data\n          causing a render error. This is beyond what{\" \"}\n          <code>&lt;ProjectErrorBoundary&gt;</code> can handle, so it re-throws\n          the error and it gets handled by{\" \"}\n          <code>&lt;RootErrorBoundary&gt;</code> instead.\n        </li>\n      </ul>\n      <Outlet />\n    </>\n  );\n}\n\nexport function RootErrorBoundary() {\n  let error = useRouteError() as Error;\n  return (\n    <div>\n      <h1>Uh oh, something went terribly wrong 😩</h1>\n      <pre>{error.message || JSON.stringify(error)}</pre>\n      <button onClick={() => (window.location.href = \"/\")}>\n        Click here to reload the app\n      </button>\n    </div>\n  );\n}\n\nexport function projectLoader({ params }: LoaderFunctionArgs) {\n  if (params.projectId === \"unauthorized\") {\n    throw json({ contactEmail: \"administrator@fake.com\" }, { status: 401 });\n  }\n\n  if (params.projectId === \"broken\") {\n    // Uh oh - in this flow we somehow didn't get our data nested under `project`\n    // and instead got it at the root - this will cause a render error!\n    return json({\n      id: params.projectId,\n      name: \"Break Some Stuff\",\n      owner: \"The Joker\",\n      deadline: \"June 2022\",\n      cost: \"FREE\",\n    });\n  }\n\n  return json({\n    project: {\n      id: params.projectId,\n      name: \"Build Some Stuff\",\n      owner: \"Joe\",\n      deadline: \"June 2022\",\n      cost: \"$5,000 USD\",\n    },\n  });\n}\n\nexport function Project() {\n  let { project } = useLoaderData();\n\n  return (\n    <>\n      <h1>Project Name: {project.name}</h1>\n      <p>Owner: {project.owner}</p>\n      <p>Deadline: {project.deadline}</p>\n      <p>Cost: {project.cost}</p>\n    </>\n  );\n}\n\nexport function ProjectErrorBoundary() {\n  let error = useRouteError();\n\n  // We only care to handle 401's at this level, so if this is not a 401\n  // ErrorResponse, re-throw to let the RootErrorBoundary handle it\n  if (!isRouteErrorResponse(error) || error.status !== 401) {\n    throw error;\n  }\n\n  return (\n    <>\n      <h1>You do not have access to this project</h1>\n      <p>\n        Please reach out to{\" \"}\n        <a href={`mailto:${error.data.contactEmail}`}>\n          {error.data.contactEmail}\n        </a>{\" \"}\n        to obtain access.\n      </p>\n    </>\n  );\n}\n"
  },
  {
    "path": "examples/error-boundaries/src/vite-env.d.ts",
    "content": "/// <reference types=\"vite/client\" />\n"
  },
  {
    "path": "examples/error-boundaries/tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"baseUrl\": \".\",\n    \"target\": \"ESNext\",\n    \"lib\": [\"DOM\", \"DOM.Iterable\", \"ESNext\"],\n    \"allowJs\": false,\n    \"skipLibCheck\": true,\n    \"esModuleInterop\": false,\n    \"allowSyntheticDefaultImports\": true,\n    \"strict\": true,\n    \"module\": \"ESNext\",\n    \"moduleResolution\": \"Node\",\n    \"resolveJsonModule\": true,\n    \"verbatimModuleSyntax\": true,\n    \"noEmit\": true,\n    \"jsx\": \"react-jsx\",\n    \"importsNotUsedAsValues\": \"error\"\n  },\n  \"include\": [\"./src\"]\n}\n"
  },
  {
    "path": "examples/error-boundaries/vite.config.ts",
    "content": "import * as path from \"path\";\nimport { defineConfig } from \"vite\";\nimport react from \"@vitejs/plugin-react\";\nimport rollupReplace from \"@rollup/plugin-replace\";\n\n// https://vitejs.dev/config/\nexport default defineConfig({\n  server: {\n    port: 3000,\n  },\n  plugins: [\n    rollupReplace({\n      preventAssignment: true,\n      values: {\n        \"process.env.NODE_ENV\": JSON.stringify(\"development\"),\n      },\n    }),\n    react(),\n  ],\n  resolve: process.env.USE_SOURCE\n    ? {\n        alias: {\n          \"react-router\": path.resolve(\n            __dirname,\n            \"../../packages/react-router/index.ts\",\n          ),\n          \"react-router-dom\": path.resolve(\n            __dirname,\n            \"../../packages/react-router-dom/index.tsx\",\n          ),\n        },\n      }\n    : {},\n});\n"
  },
  {
    "path": "examples/lazy-loading/.gitignore",
    "content": "node_modules\n.DS_Store\ndist\ndist-ssr\n*.local\n"
  },
  {
    "path": "examples/lazy-loading/.stackblitzrc",
    "content": "{\n  \"installDependencies\": true,\n  \"startCommand\": \"npm run dev\"\n}\n"
  },
  {
    "path": "examples/lazy-loading/README.md",
    "content": "---\ntitle: Lazy Loading\ntoc: false\n---\n\n# Lazy Loading Example\n\nThis example demonstrates how to lazily load both\n\n- individual route elements\n- entire portions of your route hierarchy\n\non demand using `React.lazy()` and dynamic `import()`. Using this technique,\npages that are not required on the home page can be split out into separate\nbundles, thereby decreasing load time on the initial page and improving\nperformance.\n\n## Preview\n\nOpen this example on [StackBlitz](https://stackblitz.com):\n\n[![Open in StackBlitz](https://developer.stackblitz.com/img/open_in_stackblitz.svg)](https://stackblitz.com/github/remix-run/react-router/tree/main/examples/lazy-loading?file=src/App.tsx)\n"
  },
  {
    "path": "examples/lazy-loading/index.html",
    "content": "<!doctype html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"UTF-8\" />\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" />\n    <title>React Router - Lazy Loading Example</title>\n  </head>\n  <body>\n    <div id=\"root\"></div>\n    <script type=\"module\" src=\"/src/main.tsx\"></script>\n  </body>\n</html>\n"
  },
  {
    "path": "examples/lazy-loading/package.json",
    "content": "{\n  \"name\": \"lazy-loading\",\n  \"private\": true,\n  \"scripts\": {\n    \"dev\": \"vite\",\n    \"build\": \"tsc && vite build\",\n    \"serve\": \"vite preview\"\n  },\n  \"dependencies\": {\n    \"react\": \"^18.2.0\",\n    \"react-dom\": \"^18.2.0\",\n    \"react-router-dom\": \"^6.15.0\"\n  },\n  \"devDependencies\": {\n    \"@rollup/plugin-replace\": \"^5.0.2\",\n    \"@types/node\": \"18.x\",\n    \"@types/react\": \"^18.0.27\",\n    \"@types/react-dom\": \"^18.0.10\",\n    \"@vitejs/plugin-react\": \"^3.0.1\",\n    \"typescript\": \"^4.9.5\",\n    \"vite\": \"^4.0.4\"\n  }\n}\n"
  },
  {
    "path": "examples/lazy-loading/src/App.tsx",
    "content": "import * as React from \"react\";\nimport { Routes, Route, Outlet, Link } from \"react-router-dom\";\n\nconst About = React.lazy(() => import(\"./pages/About\"));\nconst Dashboard = React.lazy(() => import(\"./pages/Dashboard\"));\n\nexport default function App() {\n  return (\n    <div>\n      <h1>Lazy Loading Example</h1>\n\n      <p>\n        This example demonstrates how to lazily load both route elements and\n        even entire portions of your route hierarchy on demand. To get the full\n        effect of this demo, be sure to open your Network tab and watch the new\n        bundles load dynamically as you navigate around.\n      </p>\n\n      <p>\n        The \"About\" page is not loaded until you click on the link. When you do,\n        a <code>&lt;React.Suspense fallback&gt;</code> renders while the code is\n        loaded via a dynamic <code>import()</code> statement. Once the code\n        loads, the fallback is replaced by the actual code for that page.\n      </p>\n\n      <p>\n        The \"Dashboard\" page does the same thing, but takes it even one step\n        further by <em>dynamically defining additional routes</em> once the page\n        loads! Since React Router lets you declare your routes as\n        <code>&lt;Route&gt;</code> elements, you can easily define more routes\n        by placing an additional <code>&lt;Routes&gt;</code> element anywhere\n        further down the element tree. Just be sure the parent route ends with a{\" \"}\n        <code>*</code> like <code>&lt;Route path=\"dashboard/*\"&gt;</code> in\n        this case.\n      </p>\n\n      <Routes>\n        <Route path=\"/\" element={<Layout />}>\n          <Route index element={<Home />} />\n          <Route\n            path=\"about\"\n            element={\n              <React.Suspense fallback={<>...</>}>\n                <About />\n              </React.Suspense>\n            }\n          />\n          <Route\n            path=\"dashboard/*\"\n            element={\n              <React.Suspense fallback={<>...</>}>\n                <Dashboard />\n              </React.Suspense>\n            }\n          />\n          <Route path=\"*\" element={<NoMatch />} />\n        </Route>\n      </Routes>\n    </div>\n  );\n}\n\nfunction Layout() {\n  return (\n    <div>\n      <nav>\n        <ul>\n          <li>\n            <Link to=\"/\">Home</Link>\n          </li>\n          <li>\n            <Link to=\"/about\">About</Link>\n          </li>\n          <li>\n            <Link to=\"/dashboard/messages\">Messages (Dashboard)</Link>\n          </li>\n        </ul>\n      </nav>\n\n      <hr />\n\n      <Outlet />\n    </div>\n  );\n}\n\nfunction Home() {\n  return (\n    <div>\n      <h2>Home</h2>\n    </div>\n  );\n}\n\nfunction NoMatch() {\n  return (\n    <div>\n      <h2>Nothing to see here!</h2>\n      <p>\n        <Link to=\"/\">Go to the home page</Link>\n      </p>\n    </div>\n  );\n}\n"
  },
  {
    "path": "examples/lazy-loading/src/index.css",
    "content": "body {\n  font-family:\n    -apple-system, BlinkMacSystemFont, \"Segoe UI\", \"Roboto\", \"Oxygen\", \"Ubuntu\",\n    \"Cantarell\", \"Fira Sans\", \"Droid Sans\", \"Helvetica Neue\", sans-serif;\n  -webkit-font-smoothing: antialiased;\n  -moz-osx-font-smoothing: grayscale;\n}\n\ncode {\n  font-family:\n    source-code-pro, Menlo, Monaco, Consolas, \"Courier New\", monospace;\n}\n"
  },
  {
    "path": "examples/lazy-loading/src/main.tsx",
    "content": "import * as React from \"react\";\nimport * as ReactDOM from \"react-dom/client\";\nimport { BrowserRouter } from \"react-router-dom\";\n\nimport \"./index.css\";\nimport App from \"./App\";\n\nReactDOM.createRoot(document.getElementById(\"root\")).render(\n  <React.StrictMode>\n    <BrowserRouter>\n      <App />\n    </BrowserRouter>\n  </React.StrictMode>,\n);\n"
  },
  {
    "path": "examples/lazy-loading/src/pages/About.tsx",
    "content": "function AboutPage() {\n  return (\n    <div>\n      <h2>About</h2>\n    </div>\n  );\n}\n\nexport default AboutPage;\n"
  },
  {
    "path": "examples/lazy-loading/src/pages/Dashboard.tsx",
    "content": "import { Routes, Route, Outlet, Link } from \"react-router-dom\";\n\nexport default function Dashboard() {\n  // These routes are defined when this component is loaded on demand via\n  // dynamic import() on the home page!\n  return (\n    <Routes>\n      <Route path=\"/\" element={<DashboardLayout />}>\n        <Route index element={<DashboardIndex />} />\n        <Route path=\"messages\" element={<Messages />} />\n      </Route>\n    </Routes>\n  );\n}\n\nfunction DashboardLayout() {\n  return (\n    <div>\n      <nav>\n        <ul>\n          <li>\n            <Link to=\"/dashboard\">Dashboard Home</Link>\n          </li>\n          <li>\n            <Link to=\"/dashboard/messages\">Messages</Link>\n          </li>\n        </ul>\n      </nav>\n\n      <hr />\n\n      <Outlet />\n    </div>\n  );\n}\n\nfunction DashboardIndex() {\n  return (\n    <div>\n      <h2>Dashboard Index</h2>\n    </div>\n  );\n}\n\nfunction Messages() {\n  return (\n    <div>\n      <h2>Messages</h2>\n      <ul>\n        <li>Message 1</li>\n        <li>Message 2</li>\n      </ul>\n    </div>\n  );\n}\n"
  },
  {
    "path": "examples/lazy-loading/src/vite-env.d.ts",
    "content": "/// <reference types=\"vite/client\" />\n"
  },
  {
    "path": "examples/lazy-loading/tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"baseUrl\": \".\",\n    \"target\": \"ESNext\",\n    \"lib\": [\"DOM\", \"DOM.Iterable\", \"ESNext\"],\n    \"allowJs\": false,\n    \"skipLibCheck\": true,\n    \"esModuleInterop\": false,\n    \"allowSyntheticDefaultImports\": true,\n    \"strict\": true,\n    \"module\": \"ESNext\",\n    \"moduleResolution\": \"Node\",\n    \"resolveJsonModule\": true,\n    \"verbatimModuleSyntax\": true,\n    \"noEmit\": true,\n    \"jsx\": \"react-jsx\",\n    \"importsNotUsedAsValues\": \"error\"\n  },\n  \"include\": [\"./src\"]\n}\n"
  },
  {
    "path": "examples/lazy-loading/vite.config.ts",
    "content": "import * as path from \"path\";\nimport { defineConfig } from \"vite\";\nimport react from \"@vitejs/plugin-react\";\nimport rollupReplace from \"@rollup/plugin-replace\";\n\n// https://vitejs.dev/config/\nexport default defineConfig({\n  server: {\n    port: 3000,\n  },\n  plugins: [\n    rollupReplace({\n      preventAssignment: true,\n      values: {\n        \"process.env.NODE_ENV\": JSON.stringify(\"development\"),\n      },\n    }),\n    react(),\n  ],\n  resolve: process.env.USE_SOURCE\n    ? {\n        alias: {\n          \"react-router\": path.resolve(\n            __dirname,\n            \"../../packages/react-router/index.ts\",\n          ),\n          \"react-router-dom\": path.resolve(\n            __dirname,\n            \"../../packages/react-router-dom/index.tsx\",\n          ),\n        },\n      }\n    : {},\n});\n"
  },
  {
    "path": "examples/lazy-loading-router-provider/.gitignore",
    "content": "node_modules\n.DS_Store\ndist\ndist-ssr\n*.local\n"
  },
  {
    "path": "examples/lazy-loading-router-provider/.stackblitzrc",
    "content": "{\n  \"installDependencies\": true,\n  \"startCommand\": \"npm run dev\"\n}\n"
  },
  {
    "path": "examples/lazy-loading-router-provider/README.md",
    "content": "---\ntitle: Lazy Loading with RouterProvider\ntoc: false\n---\n\n# Lazy Loading Example using `RouterProvider`\n\nThis example demonstrates how to lazily load individual route elements on demand `route.lazy()` and dynamic `import()`. Using this technique, pages that are not required on the home page can be split out into separate bundles, thereby decreasing load time on the initial page and improving performance.\n\n## Preview\n\nOpen this example on [StackBlitz](https://stackblitz.com):\n\n[![Open in StackBlitz](https://developer.stackblitz.com/img/open_in_stackblitz.svg)](https://stackblitz.com/github/remix-run/react-router/tree/main/examples/lazy-loading-router-provider?file=src/App.tsx)\n"
  },
  {
    "path": "examples/lazy-loading-router-provider/index.html",
    "content": "<!doctype html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"UTF-8\" />\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" />\n    <title>React Router - Lazy Loading Example using RouterProvider</title>\n  </head>\n  <body>\n    <div id=\"root\"></div>\n    <script type=\"module\" src=\"/src/main.tsx\"></script>\n  </body>\n</html>\n"
  },
  {
    "path": "examples/lazy-loading-router-provider/package.json",
    "content": "{\n  \"name\": \"lazy-loading-router-provider\",\n  \"private\": true,\n  \"scripts\": {\n    \"dev\": \"vite\",\n    \"build\": \"tsc && vite build\",\n    \"serve\": \"vite preview\"\n  },\n  \"dependencies\": {\n    \"react\": \"^18.2.0\",\n    \"react-dom\": \"^18.2.0\",\n    \"react-router-dom\": \"^6.15.0\"\n  },\n  \"devDependencies\": {\n    \"@rollup/plugin-replace\": \"^5.0.2\",\n    \"@types/node\": \"18.x\",\n    \"@types/react\": \"^18.0.27\",\n    \"@types/react-dom\": \"^18.0.10\",\n    \"@vitejs/plugin-react\": \"^3.0.1\",\n    \"typescript\": \"^4.9.5\",\n    \"vite\": \"^4.0.4\"\n  }\n}\n"
  },
  {
    "path": "examples/lazy-loading-router-provider/src/App.tsx",
    "content": "import {\n  Outlet,\n  Link,\n  createBrowserRouter,\n  RouterProvider,\n  useNavigation,\n} from \"react-router-dom\";\n\nconst router = createBrowserRouter([\n  {\n    path: \"/\",\n    element: <Layout />,\n    children: [\n      {\n        index: true,\n        element: <Home />,\n      },\n      {\n        path: \"about\",\n        // Single route in lazy file\n        lazy: () => import(\"./pages/About\"),\n      },\n      {\n        path: \"dashboard\",\n        async lazy() {\n          // Multiple routes in lazy file\n          let { DashboardLayout } = await import(\"./pages/Dashboard\");\n          return { Component: DashboardLayout };\n        },\n        children: [\n          {\n            index: true,\n            async lazy() {\n              let { DashboardIndex } = await import(\"./pages/Dashboard\");\n              return { Component: DashboardIndex };\n            },\n          },\n          {\n            path: \"messages\",\n            async lazy() {\n              let { dashboardMessagesLoader, DashboardMessages } = await import(\n                \"./pages/Dashboard\"\n              );\n              return {\n                loader: dashboardMessagesLoader,\n                Component: DashboardMessages,\n              };\n            },\n          },\n        ],\n      },\n      {\n        path: \"*\",\n        element: <NoMatch />,\n      },\n    ],\n  },\n]);\n\nexport default function App() {\n  return <RouterProvider router={router} fallbackElement={<p>Loading...</p>} />;\n}\n\nfunction Layout() {\n  let navigation = useNavigation();\n\n  return (\n    <div>\n      <h1>Lazy Loading Example using RouterProvider</h1>\n\n      <p>\n        This example demonstrates how to lazily load route definitions using{\" \"}\n        <code>route.lazy()</code>. To get the full effect of this demo, be sure\n        to open your Network tab and watch the new bundles load dynamically as\n        you navigate around.\n      </p>\n\n      <p>\n        The \"About\" and \"Dashboard\" pages are not loaded until you click on the\n        link. When you do, the code is loaded via a dynamic{\" \"}\n        <code>import()</code> statement during the <code>loading</code> phase of\n        the navigation. Once the code loads, the route loader executes, and then\n        the element renders with the loader-provided data.\n      </p>\n\n      <p>\n        This works for all data-loading/rendering related properties of a route,\n        including <code>action</code>, <code>loader</code>, <code>element</code>\n        , <code>errorElement</code>, and <code>shouldRevalidate</code>. You\n        cannot return path-matching properties from <code>lazy()</code> such as{\" \"}\n        <code>path</code>, <code>index</code>, <code>children</code>, and{\" \"}\n        <code>caseSensitive</code>.\n      </p>\n\n      <div style={{ position: \"fixed\", top: 0, right: 0 }}>\n        {navigation.state !== \"idle\" && <p>Navigation in progress...</p>}\n      </div>\n\n      <nav>\n        <ul>\n          <li>\n            <Link to=\"/\">Home</Link>\n          </li>\n          <li>\n            <Link to=\"/about\">About</Link>\n          </li>\n          <li>\n            <Link to=\"/dashboard/messages\">Messages (Dashboard)</Link>\n          </li>\n        </ul>\n      </nav>\n\n      <hr />\n\n      <Outlet />\n    </div>\n  );\n}\n\nfunction Home() {\n  return (\n    <div>\n      <h2>Home</h2>\n    </div>\n  );\n}\n\nfunction NoMatch() {\n  return (\n    <div>\n      <h2>Nothing to see here!</h2>\n      <p>\n        <Link to=\"/\">Go to the home page</Link>\n      </p>\n    </div>\n  );\n}\n"
  },
  {
    "path": "examples/lazy-loading-router-provider/src/index.css",
    "content": "body {\n  font-family:\n    -apple-system, BlinkMacSystemFont, \"Segoe UI\", \"Roboto\", \"Oxygen\", \"Ubuntu\",\n    \"Cantarell\", \"Fira Sans\", \"Droid Sans\", \"Helvetica Neue\", sans-serif;\n  -webkit-font-smoothing: antialiased;\n  -moz-osx-font-smoothing: grayscale;\n}\n\ncode {\n  font-family:\n    source-code-pro, Menlo, Monaco, Consolas, \"Courier New\", monospace;\n}\n"
  },
  {
    "path": "examples/lazy-loading-router-provider/src/main.tsx",
    "content": "import * as React from \"react\";\nimport * as ReactDOM from \"react-dom/client\";\n\nimport \"./index.css\";\nimport App from \"./App\";\n\nReactDOM.createRoot(document.getElementById(\"root\")).render(\n  <React.StrictMode>\n    <App />\n  </React.StrictMode>,\n);\n"
  },
  {
    "path": "examples/lazy-loading-router-provider/src/pages/About.tsx",
    "content": "import { useLoaderData } from \"react-router-dom\";\n\nexport async function loader() {\n  await new Promise((r) => setTimeout(r, 500));\n  return \"I came from the About.tsx loader function!\";\n}\n\nexport function Component() {\n  let data = useLoaderData() as string;\n\n  return (\n    <div>\n      <h2>About</h2>\n      <p>{data}</p>\n    </div>\n  );\n}\n\nComponent.displayName = \"AboutPage\";\n"
  },
  {
    "path": "examples/lazy-loading-router-provider/src/pages/Dashboard.tsx",
    "content": "import { Outlet, Link, useLoaderData } from \"react-router-dom\";\n\nexport function DashboardLayout() {\n  return (\n    <div>\n      <nav>\n        <ul>\n          <li>\n            <Link to=\"/dashboard\">Dashboard Home</Link>\n          </li>\n          <li>\n            <Link to=\"/dashboard/messages\">Messages</Link>\n          </li>\n        </ul>\n      </nav>\n\n      <hr />\n\n      <Outlet />\n    </div>\n  );\n}\n\nexport function DashboardIndex() {\n  return (\n    <div>\n      <h2>Dashboard Index</h2>\n    </div>\n  );\n}\n\ninterface MessagesData {\n  messages: string[];\n}\n\nexport async function dashboardMessagesLoader() {\n  await new Promise((r) => setTimeout(r, 500));\n  return {\n    messages: [\n      \"Message 1 from Dashboard.tsx loader\",\n      \"Message 2 from Dashboard.tsx loader\",\n      \"Message 3 from Dashboard.tsx loader\",\n    ],\n  } as MessagesData;\n}\n\nexport function DashboardMessages() {\n  let { messages } = useLoaderData() as MessagesData;\n\n  return (\n    <div>\n      <h2>Messages</h2>\n      <ul>\n        {messages.map((m) => (\n          <li key={m}>{m}</li>\n        ))}\n      </ul>\n    </div>\n  );\n}\n"
  },
  {
    "path": "examples/lazy-loading-router-provider/src/vite-env.d.ts",
    "content": "/// <reference types=\"vite/client\" />\n"
  },
  {
    "path": "examples/lazy-loading-router-provider/tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"baseUrl\": \".\",\n    \"target\": \"ESNext\",\n    \"lib\": [\"DOM\", \"DOM.Iterable\", \"ESNext\"],\n    \"allowJs\": false,\n    \"skipLibCheck\": true,\n    \"esModuleInterop\": false,\n    \"allowSyntheticDefaultImports\": true,\n    \"strict\": true,\n    \"module\": \"ESNext\",\n    \"moduleResolution\": \"Node\",\n    \"resolveJsonModule\": true,\n    \"verbatimModuleSyntax\": true,\n    \"noEmit\": true,\n    \"jsx\": \"react-jsx\",\n    \"importsNotUsedAsValues\": \"error\"\n  },\n  \"include\": [\"./src\"]\n}\n"
  },
  {
    "path": "examples/lazy-loading-router-provider/vite.config.ts",
    "content": "import * as path from \"path\";\nimport { defineConfig } from \"vite\";\nimport react from \"@vitejs/plugin-react\";\nimport rollupReplace from \"@rollup/plugin-replace\";\n\n// https://vitejs.dev/config/\nexport default defineConfig({\n  server: {\n    port: 3000,\n  },\n  plugins: [\n    rollupReplace({\n      preventAssignment: true,\n      values: {\n        \"process.env.NODE_ENV\": JSON.stringify(\"development\"),\n      },\n    }),\n    react(),\n  ],\n  resolve: process.env.USE_SOURCE\n    ? {\n        alias: {\n          \"react-router\": path.resolve(\n            __dirname,\n            \"../../packages/react-router/index.ts\",\n          ),\n          \"react-router-dom\": path.resolve(\n            __dirname,\n            \"../../packages/react-router-dom/index.tsx\",\n          ),\n        },\n      }\n    : {},\n});\n"
  },
  {
    "path": "examples/modal/.gitignore",
    "content": "node_modules\n.DS_Store\ndist\ndist-ssr\n*.local\n"
  },
  {
    "path": "examples/modal/.stackblitzrc",
    "content": "{\n  \"installDependencies\": true,\n  \"startCommand\": \"npm run dev\"\n}\n"
  },
  {
    "path": "examples/modal/README.md",
    "content": "---\ntitle: Modal\ntoc: false\n---\n\n# Modal Example\n\n## Preview\n\nOpen this example on [StackBlitz](https://stackblitz.com):\n\n[![Open in StackBlitz](https://developer.stackblitz.com/img/open_in_stackblitz.svg)](https://stackblitz.com/github/remix-run/react-router/tree/main/examples/modal?file=src/App.tsx)\n"
  },
  {
    "path": "examples/modal/index.html",
    "content": "<!doctype html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"UTF-8\" />\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" />\n    <title>React Router - Modal Example</title>\n  </head>\n  <body>\n    <div id=\"root\"></div>\n    <script type=\"module\" src=\"/src/main.tsx\"></script>\n  </body>\n</html>\n"
  },
  {
    "path": "examples/modal/package.json",
    "content": "{\n  \"name\": \"modal\",\n  \"private\": true,\n  \"scripts\": {\n    \"dev\": \"vite\",\n    \"build\": \"tsc && vite build\",\n    \"serve\": \"vite preview\"\n  },\n  \"dependencies\": {\n    \"@reach/dialog\": \"0.18.0\",\n    \"react\": \"^17.0.2\",\n    \"react-dom\": \"^17.0.2\",\n    \"react-router-dom\": \"^6.15.0\"\n  },\n  \"devDependencies\": {\n    \"@rollup/plugin-replace\": \"^5.0.2\",\n    \"@types/node\": \"18.x\",\n    \"@types/react\": \"^17.0.59\",\n    \"@types/react-dom\": \"^17.0.20\",\n    \"@vitejs/plugin-react\": \"^3.0.1\",\n    \"typescript\": \"^4.9.5\",\n    \"vite\": \"^4.0.4\"\n  }\n}\n"
  },
  {
    "path": "examples/modal/src/App.tsx",
    "content": "import * as React from \"react\";\nimport {\n  Routes,\n  Route,\n  Outlet,\n  Link,\n  useLocation,\n  useNavigate,\n  useParams,\n} from \"react-router-dom\";\nimport { Dialog } from \"@reach/dialog\";\nimport \"@reach/dialog/styles.css\";\n\nimport { IMAGES, getImageById } from \"./images\";\n\nexport default function App() {\n  let location = useLocation();\n\n  // The `backgroundLocation` state is the location that we were at when one of\n  // the gallery links was clicked. If it's there, use it as the location for\n  // the <Routes> so we show the gallery in the background, behind the modal.\n  let state = location.state as { backgroundLocation?: Location };\n\n  return (\n    <div>\n      <h1>Modal Example</h1>\n\n      <p>\n        This is an example of how to create a contextual modal navigation with\n        React Router where the navigation path the user takes determines if the\n        page is rendered in the modal or not (popularized by pinterest,\n        instagram, and others in the 2010s). This type of modal is typically\n        used as a kind of \"detail\" view to focus on a particular object in a\n        collection (like a pinterest board) while not taking you completely out\n        of context of the parent page. But, when the same URL is visited\n        directly (rather than from the collection page) it renders as it's own\n        full page instead of in a modal.\n      </p>\n\n      <p>\n        In this example, notice how the URL updates when the modal opens (if you\n        are viewing the example in StackBlitz you may need to open in a new\n        browser window). Even though the URL is updated to the specific item in\n        the modal, the background page is still showing behind it.\n      </p>\n\n      <p>\n        Next, copy and paste the URL to a new browser tab and notice that it\n        shows that specific item not in a modal, but directly on the page. This\n        is the view that someone would see if they clicked on a link that you\n        sent them when you had the modal open. They don't have the context you\n        did when you opened the modal, so they don't see it in the context of\n        the background page.\n      </p>\n\n      <Routes location={state?.backgroundLocation || location}>\n        <Route path=\"/\" element={<Layout />}>\n          <Route index element={<Home />} />\n          <Route path=\"gallery\" element={<Gallery />} />\n          <Route path=\"/img/:id\" element={<ImageView />} />\n          <Route path=\"*\" element={<NoMatch />} />\n        </Route>\n      </Routes>\n\n      {/* Show the modal when a `backgroundLocation` is set */}\n      {state?.backgroundLocation && (\n        <Routes>\n          <Route path=\"/img/:id\" element={<Modal />} />\n        </Routes>\n      )}\n    </div>\n  );\n}\n\nfunction Layout() {\n  return (\n    <div>\n      <nav>\n        <ul>\n          <li>\n            <Link to=\"/\">Home</Link>\n          </li>\n          <li>\n            <Link to=\"/gallery\">Gallery</Link>\n          </li>\n        </ul>\n      </nav>\n\n      <hr />\n\n      <Outlet />\n    </div>\n  );\n}\n\nfunction Home() {\n  return (\n    <div>\n      <h2>Home</h2>\n\n      <h3>Featured Images</h3>\n      <ul>\n        <li>\n          <Link to=\"/img/1\">Image 1</Link>\n        </li>\n        <li>\n          <Link to=\"/img/2\">Image 2</Link>\n        </li>\n      </ul>\n    </div>\n  );\n}\n\nfunction Gallery() {\n  let location = useLocation();\n\n  return (\n    <div style={{ padding: \"0 24px\" }}>\n      <h2>Gallery</h2>\n      <div\n        style={{\n          display: \"grid\",\n          gridTemplateColumns: \"repeat(auto-fit, minmax(200px, 1fr))\",\n          gap: \"24px\",\n        }}\n      >\n        {IMAGES.map((image) => (\n          <Link\n            key={image.id}\n            to={`/img/${image.id}`}\n            // This is the trick! Set the `backgroundLocation` in location state\n            // so that when we open the modal we still see the current page in\n            // the background.\n            state={{ backgroundLocation: location }}\n          >\n            <img\n              width={200}\n              height={200}\n              style={{\n                width: \"100%\",\n                aspectRatio: \"1 / 1\",\n                height: \"auto\",\n                borderRadius: \"8px\",\n              }}\n              src={image.src}\n              alt={image.title}\n            />\n          </Link>\n        ))}\n      </div>\n    </div>\n  );\n}\n\nfunction ImageView() {\n  let { id } = useParams<\"id\">();\n  let image = getImageById(Number(id));\n\n  if (!image) return <div>Image not found</div>;\n\n  return (\n    <div>\n      <h1>{image.title}</h1>\n      <img width={400} height={400} src={image.src} alt=\"\" />\n    </div>\n  );\n}\n\nfunction Modal() {\n  let navigate = useNavigate();\n  let { id } = useParams<\"id\">();\n  let image = getImageById(Number(id));\n  let buttonRef = React.useRef<HTMLButtonElement>(null);\n\n  function onDismiss() {\n    navigate(-1);\n  }\n\n  if (!image) return null;\n\n  return (\n    <Dialog\n      aria-labelledby=\"label\"\n      onDismiss={onDismiss}\n      initialFocusRef={buttonRef}\n    >\n      <div\n        style={{\n          display: \"grid\",\n          justifyContent: \"center\",\n          padding: \"8px 8px\",\n        }}\n      >\n        <h1 id=\"label\" style={{ margin: 0 }}>\n          {image.title}\n        </h1>\n        <img\n          style={{\n            margin: \"16px 0\",\n            borderRadius: \"8px\",\n            width: \"100%\",\n            height: \"auto\",\n          }}\n          width={400}\n          height={400}\n          src={image.src}\n          alt=\"\"\n        />\n        <button\n          style={{ display: \"block\" }}\n          ref={buttonRef}\n          onClick={onDismiss}\n        >\n          Close\n        </button>\n      </div>\n    </Dialog>\n  );\n}\n\nfunction NoMatch() {\n  return (\n    <div>\n      <h2>Nothing to see here!</h2>\n      <p>\n        <Link to=\"/\">Go to the home page</Link>\n      </p>\n    </div>\n  );\n}\n"
  },
  {
    "path": "examples/modal/src/images.ts",
    "content": "let IMAGES = [\n  {\n    id: 0,\n    title: \"Enjoying a cup of coffee\",\n    src: \"https://images.unsplash.com/photo-1631016800696-5ea8801b3c2a?crop=entropy&cs=tinysrgb&fit=crop&fm=jpg&h=400&ixid=MnwxfDB8MXxyYW5kb218MHx8fHx8fHx8MTYzMzM2Mzg4Ng&ixlib=rb-1.2.1&q=80&w=400\",\n  },\n  {\n    id: 1,\n    title: \"Magical winter sunrise\",\n    src: \"https://images.unsplash.com/photo-1618824834718-92f8469a4dd1?crop=entropy&cs=tinysrgb&fit=crop&fm=jpg&h=400&ixid=MnwxfDB8MXxyYW5kb218MHx8fHx8fHx8MTYzMzM2NDAzMw&ixlib=rb-1.2.1&q=80&w=400\",\n  },\n  {\n    id: 2,\n    title: \"Dalmatian and pumpkins\",\n    src: \"https://images.unsplash.com/photo-1633289944756-6295be214e16?crop=entropy&cs=tinysrgb&fit=crop&fm=jpg&h=400&ixid=MnwxfDB8MXxyYW5kb218MHx8fHx8fHx8MTYzMzM2NDA3Nw&ixlib=rb-1.2.1&q=80&w=400\",\n  },\n  {\n    id: 3,\n    title: \"Fall into Autumn 🍂🐶\",\n    src: \"https://images.unsplash.com/photo-1633172905740-2eb6730c95b4?crop=entropy&cs=tinysrgb&fit=crop&fm=jpg&h=400&ixid=MnwxfDB8MXxyYW5kb218MHx8fHx8fHx8MTYzMzM2NDEwMg&ixlib=rb-1.2.1&q=80&w=400\",\n  },\n];\n\nfunction getImageById(id: number) {\n  return IMAGES.find((image) => image.id === id);\n}\n\nexport { IMAGES, getImageById };\n"
  },
  {
    "path": "examples/modal/src/index.css",
    "content": "html {\n  box-sizing: border-box;\n}\n\n* {\n  box-sizing: inherit;\n}\n\nbody {\n  font-family:\n    -apple-system, BlinkMacSystemFont, \"Segoe UI\", \"Roboto\", \"Oxygen\", \"Ubuntu\",\n    \"Cantarell\", \"Fira Sans\", \"Droid Sans\", \"Helvetica Neue\", sans-serif;\n  -webkit-font-smoothing: antialiased;\n  -moz-osx-font-smoothing: grayscale;\n}\n\ncode {\n  font-family:\n    source-code-pro, Menlo, Monaco, Consolas, \"Courier New\", monospace;\n}\n\n[data-reach-dialog-content] {\n  width: 90%;\n  max-width: 500px;\n  border-radius: 8px;\n}\n"
  },
  {
    "path": "examples/modal/src/main.tsx",
    "content": "import * as React from \"react\";\nimport * as ReactDOM from \"react-dom\";\nimport { BrowserRouter } from \"react-router-dom\";\n\nimport App from \"./App\";\nimport \"./index.css\";\n\nReactDOM.render(\n  <React.StrictMode>\n    <BrowserRouter>\n      <App />\n    </BrowserRouter>\n  </React.StrictMode>,\n  document.getElementById(\"root\"),\n);\n"
  },
  {
    "path": "examples/modal/src/vite-env.d.ts",
    "content": "/// <reference types=\"vite/client\" />\n"
  },
  {
    "path": "examples/modal/tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"baseUrl\": \".\",\n    \"target\": \"ESNext\",\n    \"lib\": [\"DOM\", \"DOM.Iterable\", \"ESNext\"],\n    \"allowJs\": false,\n    \"skipLibCheck\": true,\n    \"esModuleInterop\": false,\n    \"allowSyntheticDefaultImports\": true,\n    \"strict\": true,\n    \"module\": \"ESNext\",\n    \"moduleResolution\": \"Node\",\n    \"resolveJsonModule\": true,\n    \"verbatimModuleSyntax\": true,\n    \"noEmit\": true,\n    \"jsx\": \"react-jsx\",\n    \"importsNotUsedAsValues\": \"error\"\n  },\n  \"include\": [\"./src\"]\n}\n"
  },
  {
    "path": "examples/modal/vite.config.ts",
    "content": "import * as path from \"path\";\nimport { defineConfig } from \"vite\";\nimport react from \"@vitejs/plugin-react\";\nimport rollupReplace from \"@rollup/plugin-replace\";\n\n// https://vitejs.dev/config/\nexport default defineConfig({\n  server: {\n    port: 3000,\n  },\n  plugins: [\n    rollupReplace({\n      preventAssignment: true,\n      values: {\n        \"process.env.NODE_ENV\": JSON.stringify(\"development\"),\n      },\n    }),\n    react(),\n  ],\n  resolve: process.env.USE_SOURCE\n    ? {\n        alias: {\n          \"react-router\": path.resolve(\n            __dirname,\n            \"../../packages/react-router/index.ts\",\n          ),\n          \"react-router-dom\": path.resolve(\n            __dirname,\n            \"../../packages/react-router-dom/index.tsx\",\n          ),\n        },\n      }\n    : {},\n});\n"
  },
  {
    "path": "examples/modal-data-router/.gitignore",
    "content": "node_modules\n.DS_Store\ndist\ndist-ssr\n*.local\n"
  },
  {
    "path": "examples/modal-data-router/.stackblitzrc",
    "content": "{\n  \"installDependencies\": true,\n  \"startCommand\": \"npm run dev\"\n}\n"
  },
  {
    "path": "examples/modal-data-router/README.md",
    "content": "---\ntitle: Modal (Data Router)\ntoc: false\n---\n\n# Data Router Modal Example\n\nSame as the [Modal](../modal/) example but using a Data Router.\n\n## Preview\n\nOpen this example on [StackBlitz](https://stackblitz.com):\n\n[![Open in StackBlitz](https://developer.stackblitz.com/img/open_in_stackblitz.svg)](https://stackblitz.com/github/remix-run/react-router/tree/main/examples/modal-data-router?file=src/App.tsx)\n"
  },
  {
    "path": "examples/modal-data-router/index.html",
    "content": "<!doctype html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"UTF-8\" />\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" />\n    <title>React Router - Data Router Modal Example</title>\n  </head>\n  <body>\n    <div id=\"root\"></div>\n    <script type=\"module\" src=\"/src/main.tsx\"></script>\n  </body>\n</html>\n"
  },
  {
    "path": "examples/modal-data-router/package.json",
    "content": "{\n  \"name\": \"modal-data-router\",\n  \"private\": true,\n  \"scripts\": {\n    \"dev\": \"vite\",\n    \"build\": \"tsc && vite build\",\n    \"serve\": \"vite preview\"\n  },\n  \"dependencies\": {\n    \"react\": \"^19\",\n    \"react-dom\": \"^19\",\n    \"react-router\": \"^0.0.0-experimental-56b72276e\"\n  },\n  \"devDependencies\": {\n    \"@types/react\": \"^19\",\n    \"@types/react-dom\": \"^19\",\n    \"@vitejs/plugin-react\": \"latest\",\n    \"typescript\": \"latest\",\n    \"vite\": \"latest\"\n  }\n}\n"
  },
  {
    "path": "examples/modal-data-router/src/App.tsx",
    "content": "import * as React from \"react\";\nimport { createPortal } from \"react-dom\";\nimport {\n  Link,\n  Outlet,\n  RouterProvider,\n  createBrowserRouter,\n  useLoaderData,\n  useNavigate,\n  useParams,\n  type LoaderFunctionArgs,\n} from \"react-router\";\n\nimport { IMAGES, getImageById, loadImage } from \"./images\";\n\nconst router = createBrowserRouter([\n  {\n    path: \"/\",\n    Component: Root,\n    children: [\n      {\n        Component: Layout,\n        children: [\n          {\n            index: true,\n            Component: Home,\n          },\n          {\n            path: \"gallery\",\n            Component: Gallery,\n            async loader({ request }: LoaderFunctionArgs) {\n              // When the gallery route has an img param, we load that image to\n              // show it in a modal\n              let searchParams = new URL(request.url).searchParams;\n              let image = searchParams.has(\"img\")\n                ? loadImage(searchParams.get(\"img\")!)\n                : null;\n              return { image };\n            },\n          },\n          {\n            path: \"img/:id\",\n            Component: ImageView,\n            async loader({ params }: LoaderFunctionArgs) {\n              return loadImage(params.id!);\n            },\n          },\n          {\n            path: \"*\",\n            Component: NoMatch,\n          },\n        ],\n      },\n    ],\n  },\n]);\n\nexport default function App() {\n  return <RouterProvider router={router} />;\n}\n\n// Root component\nfunction Root() {\n  return (\n    <div>\n      <h1>Modal Example</h1>\n\n      <p>\n        This is an example of how to create a contextual modal navigation with\n        React Router where the navigation path the user takes determines if the\n        page is rendered in the modal or not (popularized by pinterest,\n        instagram, and others in the 2010s). This type of modal is typically\n        used as a kind of \"detail\" view to focus on a particular object in a\n        collection (like a pinterest board) while not taking you completely out\n        of context of the parent page. But, when the same URL is visited\n        directly (rather than from the collection page) it renders as it's own\n        full page instead of in a modal.\n      </p>\n\n      <p>\n        In this example, notice how the URL updates when the modal opens (if you\n        are viewing the example in StackBlitz you may need to open in a new\n        browser window). Even though the URL is updated to the specific item in\n        the modal, the background page is still showing behind it.\n      </p>\n\n      <p>\n        Next, copy and paste the URL to a new browser tab and notice that it\n        shows that specific item not in a modal, but directly on the page. This\n        is the view that someone would see if they clicked on a link that you\n        sent them when you had the modal open. They don't have the context you\n        did when you opened the modal, so they don't see it in the context of\n        the background page.\n      </p>\n\n      <Outlet />\n    </div>\n  );\n}\n\nfunction Layout() {\n  return (\n    <div>\n      <nav>\n        <ul>\n          <li>\n            <Link to=\"/\">Home</Link>\n          </li>\n          <li>\n            <Link to=\"/gallery\">Gallery</Link>\n          </li>\n        </ul>\n      </nav>\n\n      <hr />\n\n      <Outlet />\n    </div>\n  );\n}\n\nfunction Home() {\n  return (\n    <div>\n      <h2>Home</h2>\n\n      <h3>Featured Images</h3>\n      <ul>\n        <li>\n          <Link to=\"/img/1\">Image 1</Link>\n        </li>\n        <li>\n          <Link to=\"/img/2\">Image 2</Link>\n        </li>\n      </ul>\n    </div>\n  );\n}\n\nfunction Gallery() {\n  let data = useLoaderData() as {\n    image: ReturnType<typeof getImageById> | null;\n  };\n  return (\n    <div style={{ padding: \"0 24px\" }}>\n      <h2>Gallery</h2>\n      <div\n        style={{\n          display: \"grid\",\n          gridTemplateColumns: \"repeat(auto-fit, minmax(200px, 1fr))\",\n          gap: \"24px\",\n        }}\n      >\n        {IMAGES.map((image) => (\n          // This is the trick! We tell the router to navigate to `/gallery?img=1`\n          // and we can use the search param to overlay a modal on the gallery.\n          // But, we also tell the router to mask the URL displayed in the href\n          // and the URL bar\n          <Link\n            key={image.id}\n            to={`?img=${image.id}`}\n            unstable_mask={`/img/${image.id}`}\n          >\n            <img\n              width={200}\n              height={200}\n              style={{\n                width: \"100%\",\n                aspectRatio: \"1 / 1\",\n                height: \"auto\",\n                borderRadius: \"8px\",\n              }}\n              src={image.src}\n              alt={image.title}\n            />\n          </Link>\n        ))}\n      </div>\n\n      {/* Display the modal if data was returned */}\n      {data.image ? <Modal image={data.image} /> : null}\n    </div>\n  );\n}\n\nfunction ImageView() {\n  let { id } = useParams<\"id\">();\n  let image = getImageById(Number(id));\n\n  if (!image) return <div>Image not found</div>;\n\n  return (\n    <div>\n      <h1>{image.title}</h1>\n      <img width={400} height={400} src={image.src} alt=\"\" />\n    </div>\n  );\n}\n\nfunction Modal({ image }: { image: ReturnType<typeof getImageById> }) {\n  let navigate = useNavigate();\n  let buttonRef = React.useRef<HTMLButtonElement>(null);\n\n  function onDismiss() {\n    navigate(-1);\n  }\n\n  if (!image) return null;\n\n  return (\n    <Dialog\n      aria-labelledby=\"label\"\n      onDismiss={onDismiss}\n      initialFocusRef={buttonRef as React.RefObject<HTMLElement>}\n    >\n      <div\n        style={{\n          display: \"grid\",\n          justifyContent: \"center\",\n          padding: \"8px 8px\",\n        }}\n      >\n        <h1 id=\"label\" style={{ margin: 0 }}>\n          {image.title}\n        </h1>\n        <img\n          style={{\n            margin: \"16px 0\",\n            borderRadius: \"8px\",\n            width: \"100%\",\n            height: \"auto\",\n          }}\n          width={400}\n          height={400}\n          src={image.src}\n          alt=\"\"\n        />\n        <button\n          style={{ display: \"block\" }}\n          ref={buttonRef}\n          onClick={onDismiss}\n        >\n          Close\n        </button>\n      </div>\n    </Dialog>\n  );\n}\n\nfunction NoMatch() {\n  return (\n    <div>\n      <h2>Nothing to see here!</h2>\n      <p>\n        <Link to=\"/\">Go to the home page</Link>\n      </p>\n    </div>\n  );\n}\n\ntype DialogProps = {\n  children: React.ReactNode;\n  onDismiss: () => void;\n  \"aria-label\"?: string;\n  \"aria-labelledby\"?: string;\n  initialFocusRef?: React.RefObject<HTMLElement>;\n};\n\nfunction Dialog({\n  children,\n  onDismiss,\n  \"aria-label\": ariaLabel,\n  \"aria-labelledby\": ariaLabelledby,\n  initialFocusRef,\n}: DialogProps) {\n  let overlayRef = React.useRef<HTMLDivElement>(null);\n  let contentRef = React.useRef<HTMLDivElement>(null);\n  let previouslyFocusedRef = React.useRef<HTMLElement | null>(null);\n\n  React.useEffect(() => {\n    previouslyFocusedRef.current = document.activeElement as HTMLElement | null;\n\n    let focusTarget = initialFocusRef?.current ?? contentRef.current;\n    if (focusTarget) {\n      focusTarget.focus();\n    }\n\n    function onKeyDown(event: KeyboardEvent) {\n      if (event.key === \"Escape\") {\n        event.stopPropagation();\n        onDismiss();\n        return;\n      }\n\n      if (event.key === \"Tab\") {\n        let container = contentRef.current;\n        if (!container) return;\n\n        let focusable = getFocusableElements(container);\n        if (focusable.length === 0) {\n          event.preventDefault();\n          container.focus();\n          return;\n        }\n\n        let activeElement = document.activeElement as HTMLElement | null;\n        let currentIndex = focusable.indexOf(activeElement ?? focusable[0]);\n\n        let nextIndex = currentIndex;\n        if (event.shiftKey) {\n          nextIndex =\n            currentIndex <= 0 ? focusable.length - 1 : currentIndex - 1;\n        } else {\n          nextIndex =\n            currentIndex === focusable.length - 1 ? 0 : currentIndex + 1;\n        }\n\n        event.preventDefault();\n        focusable[nextIndex].focus();\n      }\n    }\n\n    document.addEventListener(\"keydown\", onKeyDown);\n\n    return () => {\n      document.removeEventListener(\"keydown\", onKeyDown);\n      previouslyFocusedRef.current?.focus();\n    };\n  }, [initialFocusRef, onDismiss]);\n\n  return createPortal(\n    <div\n      ref={overlayRef}\n      onClick={(event) => {\n        if (event.target === event.currentTarget) {\n          onDismiss();\n        }\n      }}\n      style={{\n        position: \"fixed\",\n        inset: 0,\n        background: \"rgba(0, 0, 0, 0.55)\",\n        display: \"flex\",\n        alignItems: \"center\",\n        justifyContent: \"center\",\n        padding: \"24px\",\n        zIndex: 1000,\n      }}\n    >\n      <div\n        ref={contentRef}\n        role=\"dialog\"\n        aria-modal=\"true\"\n        aria-label={ariaLabel}\n        aria-labelledby={ariaLabelledby}\n        tabIndex={-1}\n        style={{\n          background: \"white\",\n          borderRadius: \"12px\",\n          maxWidth: \"min(480px, 100%)\",\n          width: \"100%\",\n          padding: \"24px\",\n          boxShadow:\n            \"0 20px 40px rgba(0, 0, 0, 0.2), 0 0 0 1px rgba(0, 0, 0, 0.06)\",\n        }}\n      >\n        {children}\n      </div>\n    </div>,\n    document.body,\n  );\n}\n\nfunction getFocusableElements(container: HTMLElement) {\n  return Array.from(\n    container.querySelectorAll<HTMLElement>(\n      'a[href], area[href], input:not([disabled]), select:not([disabled]), textarea:not([disabled]), button:not([disabled]), iframe, object, embed, [tabindex]:not([tabindex=\"-1\"]), [contenteditable=\"true\"]',\n    ),\n  ).filter((element) => !element.hasAttribute(\"disabled\"));\n}\n"
  },
  {
    "path": "examples/modal-data-router/src/images.ts",
    "content": "export const IMAGES = [\n  {\n    id: 0,\n    title: \"Enjoying a cup of coffee\",\n    src: \"https://images.unsplash.com/photo-1631016800696-5ea8801b3c2a?crop=entropy&cs=tinysrgb&fit=crop&fm=jpg&h=400&ixid=MnwxfDB8MXxyYW5kb218MHx8fHx8fHx8MTYzMzM2Mzg4Ng&ixlib=rb-1.2.1&q=80&w=400\",\n  },\n  {\n    id: 1,\n    title: \"Magical winter sunrise\",\n    src: \"https://images.unsplash.com/photo-1618824834718-92f8469a4dd1?crop=entropy&cs=tinysrgb&fit=crop&fm=jpg&h=400&ixid=MnwxfDB8MXxyYW5kb218MHx8fHx8fHx8MTYzMzM2NDAzMw&ixlib=rb-1.2.1&q=80&w=400\",\n  },\n  {\n    id: 2,\n    title: \"Dalmatian and pumpkins\",\n    src: \"https://images.unsplash.com/photo-1633289944756-6295be214e16?crop=entropy&cs=tinysrgb&fit=crop&fm=jpg&h=400&ixid=MnwxfDB8MXxyYW5kb218MHx8fHx8fHx8MTYzMzM2NDA3Nw&ixlib=rb-1.2.1&q=80&w=400\",\n  },\n  {\n    id: 3,\n    title: \"Fall into Autumn 🍂🐶\",\n    src: \"https://images.unsplash.com/photo-1633172905740-2eb6730c95b4?crop=entropy&cs=tinysrgb&fit=crop&fm=jpg&h=400&ixid=MnwxfDB8MXxyYW5kb218MHx8fHx8fHx8MTYzMzM2NDEwMg&ixlib=rb-1.2.1&q=80&w=400\",\n  },\n];\n\nexport function getImageById(id: number) {\n  return IMAGES.find((image) => image.id === id);\n}\n\nexport function loadImage(id: string | number) {\n  const image = getImageById(Number(id));\n  if (!image) {\n    throw new Response(\"Image not found\", { status: 404 });\n  }\n  return image;\n}\n"
  },
  {
    "path": "examples/modal-data-router/src/index.css",
    "content": "html {\n  box-sizing: border-box;\n}\n\n* {\n  box-sizing: inherit;\n}\n\nbody {\n  font-family:\n    -apple-system, BlinkMacSystemFont, \"Segoe UI\", \"Roboto\", \"Oxygen\", \"Ubuntu\",\n    \"Cantarell\", \"Fira Sans\", \"Droid Sans\", \"Helvetica Neue\", sans-serif;\n  -webkit-font-smoothing: antialiased;\n  -moz-osx-font-smoothing: grayscale;\n}\n\ncode {\n  font-family:\n    source-code-pro, Menlo, Monaco, Consolas, \"Courier New\", monospace;\n}\n\n[data-reach-dialog-content] {\n  width: 90%;\n  max-width: 500px;\n  border-radius: 8px;\n}\n"
  },
  {
    "path": "examples/modal-data-router/src/main.tsx",
    "content": "import * as React from \"react\";\nimport { createRoot } from \"react-dom/client\";\n\nimport App from \"./App\";\nimport \"./index.css\";\n\ncreateRoot(document.getElementById(\"root\")!).render(\n  <React.StrictMode>\n    <App />\n  </React.StrictMode>,\n);\n"
  },
  {
    "path": "examples/modal-data-router/src/vite-env.d.ts",
    "content": "/// <reference types=\"vite/client\" />\n"
  },
  {
    "path": "examples/modal-data-router/tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"baseUrl\": \".\",\n    \"target\": \"ESNext\",\n    \"lib\": [\"DOM\", \"DOM.Iterable\", \"ESNext\"],\n    \"allowJs\": false,\n    \"skipLibCheck\": true,\n    \"esModuleInterop\": false,\n    \"allowSyntheticDefaultImports\": true,\n    \"strict\": true,\n    \"module\": \"ESNext\",\n    \"moduleResolution\": \"Node\",\n    \"resolveJsonModule\": true,\n    \"noEmit\": true,\n    \"jsx\": \"react-jsx\"\n  },\n  \"include\": [\"./src\"]\n}\n"
  },
  {
    "path": "examples/modal-data-router/vite.config.ts",
    "content": "import { defineConfig } from \"vite\";\n\n// https://vitejs.dev/config/\nexport default defineConfig({\n  server: {\n    port: 3000,\n  },\n});\n"
  },
  {
    "path": "examples/modal-route-with-outlet/.gitignore",
    "content": "node_modules\n.DS_Store\ndist\ndist-ssr\n*.local\n"
  },
  {
    "path": "examples/modal-route-with-outlet/.stackblitzrc",
    "content": "{\n  \"installDependencies\": true,\n  \"startCommand\": \"npm run dev\"\n}\n"
  },
  {
    "path": "examples/modal-route-with-outlet/README.md",
    "content": "---\ntitle: Outlet Modal\ntoc: false\n---\n\n# Outlet Modal Example\n\n## Preview\n\nOpen this example on [StackBlitz](https://stackblitz.com):\n\n[![Open in StackBlitz](https://developer.stackblitz.com/img/open_in_stackblitz.svg)](https://stackblitz.com/github/remix-run/react-router/tree/main/examples/modal-route-with-outlet?file=src/App.tsx)\n"
  },
  {
    "path": "examples/modal-route-with-outlet/index.html",
    "content": "<!doctype html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"UTF-8\" />\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" />\n    <title>React Router - Modal Example</title>\n  </head>\n  <body>\n    <div id=\"root\"></div>\n    <script type=\"module\" src=\"/src/main.tsx\"></script>\n  </body>\n</html>\n"
  },
  {
    "path": "examples/modal-route-with-outlet/package.json",
    "content": "{\n  \"name\": \"modal-route-with-outlet\",\n  \"private\": true,\n  \"scripts\": {\n    \"dev\": \"vite\",\n    \"build\": \"tsc && vite build\",\n    \"serve\": \"vite preview\"\n  },\n  \"dependencies\": {\n    \"@reach/dialog\": \"0.18.0\",\n    \"react\": \"^18.2.0\",\n    \"react-dom\": \"^18.2.0\",\n    \"react-router-dom\": \"^6.15.0\"\n  },\n  \"devDependencies\": {\n    \"@rollup/plugin-replace\": \"^5.0.2\",\n    \"@types/node\": \"18.x\",\n    \"@types/react\": \"^18.0.27\",\n    \"@types/react-dom\": \"^18.0.10\",\n    \"@vitejs/plugin-react\": \"^3.0.1\",\n    \"typescript\": \"^4.9.5\",\n    \"vite\": \"^4.0.4\"\n  }\n}\n"
  },
  {
    "path": "examples/modal-route-with-outlet/src/App.tsx",
    "content": "import * as React from \"react\";\nimport {\n  Outlet,\n  Link,\n  useNavigate,\n  useParams,\n  RouterProvider,\n  createBrowserRouter,\n} from \"react-router-dom\";\nimport { Dialog } from \"@reach/dialog\";\nimport \"@reach/dialog/styles.css\";\n\nimport { IMAGES, getImageById } from \"./images\";\n\nconst router = createBrowserRouter([\n  {\n    path: \"/\",\n    Component: Layout,\n    children: [\n      {\n        path: \"/\",\n        Component: Home,\n      },\n      {\n        path: \"gallery\",\n        Component: Gallery,\n        children: [\n          {\n            path: \"img/:id\",\n            Component: ImageView,\n          },\n        ],\n      },\n    ],\n  },\n]);\n\nexport default function App() {\n  return <RouterProvider router={router} />;\n}\n\nexport function Layout() {\n  return (\n    <div>\n      <h1>Outlet Modal Example</h1>\n      <p>\n        This is a modal example using createBrowserRouter that drives modal\n        displays through URL segments. The modal is a child route of its parent\n        and renders in the Outlet.\n      </p>\n      <div>\n        <nav>\n          <ul>\n            <li>\n              <Link to=\"/\">Home</Link>\n            </li>\n            <li>\n              <Link to=\"/gallery\">Gallery</Link>\n            </li>\n          </ul>\n        </nav>\n        <hr />\n      </div>\n      <Outlet />\n    </div>\n  );\n}\n\nexport function Home() {\n  return (\n    <div>\n      <h2>Home</h2>\n      <p>\n        Click over to the <Link to=\"/gallery\">Gallery</Link> route to see the\n        modal in action\n      </p>\n      <Outlet />\n    </div>\n  );\n}\n\nexport function Gallery() {\n  return (\n    <div style={{ padding: \"0 24px\" }}>\n      <h2>Gallery</h2>\n      <p>\n        Click on an image, you'll notice that you still see this route behind\n        the modal. The URL will also change as its a child route of{\" \"}\n        <pre style={{ display: \"inline\" }}>/gallery</pre>{\" \"}\n      </p>\n      <div\n        style={{\n          display: \"grid\",\n          gridTemplateColumns: \"repeat(auto-fit, minmax(200px, 1fr))\",\n          gap: \"24px\",\n        }}\n      >\n        {IMAGES.map((image) => (\n          <Link key={image.id} to={`img/${image.id}`}>\n            <img\n              width={200}\n              height={200}\n              style={{\n                width: \"100%\",\n                aspectRatio: \"1 / 1\",\n                height: \"auto\",\n                borderRadius: \"8px\",\n              }}\n              src={image.src}\n              alt={image.title}\n            />\n          </Link>\n        ))}\n        <Outlet />\n      </div>\n    </div>\n  );\n}\n\nexport function ImageView() {\n  let navigate = useNavigate();\n  let { id } = useParams<\"id\">();\n  let image = getImageById(Number(id));\n  let buttonRef = React.useRef<HTMLButtonElement>(null);\n\n  function onDismiss() {\n    navigate(-1);\n  }\n\n  if (!image) {\n    throw new Error(`No image found with id: ${id}`);\n  }\n\n  return (\n    <Dialog\n      aria-labelledby=\"label\"\n      onDismiss={onDismiss}\n      initialFocusRef={buttonRef}\n    >\n      <div\n        style={{\n          display: \"grid\",\n          justifyContent: \"center\",\n          padding: \"8px 8px\",\n        }}\n      >\n        <h1 id=\"label\" style={{ margin: 0 }}>\n          {image.title}\n        </h1>\n        <img\n          style={{\n            margin: \"16px 0\",\n            borderRadius: \"8px\",\n            width: \"100%\",\n            height: \"auto\",\n          }}\n          width={400}\n          height={400}\n          src={image.src}\n          alt=\"\"\n        />\n        <button\n          style={{ display: \"block\" }}\n          ref={buttonRef}\n          onClick={onDismiss}\n        >\n          Close\n        </button>\n      </div>\n    </Dialog>\n  );\n}\n"
  },
  {
    "path": "examples/modal-route-with-outlet/src/images.ts",
    "content": "let IMAGES = [\n  {\n    id: 0,\n    title: \"Enjoying a cup of coffee\",\n    src: \"https://images.unsplash.com/photo-1631016800696-5ea8801b3c2a?crop=entropy&cs=tinysrgb&fit=crop&fm=jpg&h=400&ixid=MnwxfDB8MXxyYW5kb218MHx8fHx8fHx8MTYzMzM2Mzg4Ng&ixlib=rb-1.2.1&q=80&w=400\",\n  },\n  {\n    id: 1,\n    title: \"Magical winter sunrise\",\n    src: \"https://images.unsplash.com/photo-1618824834718-92f8469a4dd1?crop=entropy&cs=tinysrgb&fit=crop&fm=jpg&h=400&ixid=MnwxfDB8MXxyYW5kb218MHx8fHx8fHx8MTYzMzM2NDAzMw&ixlib=rb-1.2.1&q=80&w=400\",\n  },\n  {\n    id: 2,\n    title: \"Dalmatian and pumpkins\",\n    src: \"https://images.unsplash.com/photo-1633289944756-6295be214e16?crop=entropy&cs=tinysrgb&fit=crop&fm=jpg&h=400&ixid=MnwxfDB8MXxyYW5kb218MHx8fHx8fHx8MTYzMzM2NDA3Nw&ixlib=rb-1.2.1&q=80&w=400\",\n  },\n  {\n    id: 3,\n    title: \"Fall into Autumn 🍂🐶\",\n    src: \"https://images.unsplash.com/photo-1633172905740-2eb6730c95b4?crop=entropy&cs=tinysrgb&fit=crop&fm=jpg&h=400&ixid=MnwxfDB8MXxyYW5kb218MHx8fHx8fHx8MTYzMzM2NDEwMg&ixlib=rb-1.2.1&q=80&w=400\",\n  },\n];\n\nfunction getImageById(id: number) {\n  return IMAGES.find((image) => image.id === id);\n}\n\nexport { IMAGES, getImageById };\n"
  },
  {
    "path": "examples/modal-route-with-outlet/src/index.css",
    "content": "html {\n  box-sizing: border-box;\n}\n\n* {\n  box-sizing: inherit;\n}\n\nbody {\n  font-family:\n    -apple-system, BlinkMacSystemFont, \"Segoe UI\", \"Roboto\", \"Oxygen\", \"Ubuntu\",\n    \"Cantarell\", \"Fira Sans\", \"Droid Sans\", \"Helvetica Neue\", sans-serif;\n  -webkit-font-smoothing: antialiased;\n  -moz-osx-font-smoothing: grayscale;\n}\n\ncode {\n  font-family:\n    source-code-pro, Menlo, Monaco, Consolas, \"Courier New\", monospace;\n}\n\n[data-reach-dialog-content] {\n  width: 90%;\n  max-width: 500px;\n  border-radius: 8px;\n}\n"
  },
  {
    "path": "examples/modal-route-with-outlet/src/main.tsx",
    "content": "import * as React from \"react\";\nimport ReactDOM from \"react-dom/client\";\n\nimport App from \"./App\";\nimport \"./index.css\";\n\nconst root = document.getElementById(\"root\");\nif (!root) throw new Error(\"Root element not found\");\n\nReactDOM.createRoot(root).render(\n  <React.StrictMode>\n    <App />\n  </React.StrictMode>,\n);\n"
  },
  {
    "path": "examples/modal-route-with-outlet/src/vite-env.d.ts",
    "content": "/// <reference types=\"vite/client\" />\n"
  },
  {
    "path": "examples/modal-route-with-outlet/tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"baseUrl\": \".\",\n    \"target\": \"ESNext\",\n    \"lib\": [\"DOM\", \"DOM.Iterable\", \"ESNext\"],\n    \"allowJs\": false,\n    \"skipLibCheck\": true,\n    \"esModuleInterop\": false,\n    \"allowSyntheticDefaultImports\": true,\n    \"strict\": true,\n    \"module\": \"ESNext\",\n    \"moduleResolution\": \"Node\",\n    \"resolveJsonModule\": true,\n    \"verbatimModuleSyntax\": true,\n    \"noEmit\": true,\n    \"jsx\": \"react-jsx\",\n    \"importsNotUsedAsValues\": \"error\"\n  },\n  \"include\": [\"./src\"]\n}\n"
  },
  {
    "path": "examples/modal-route-with-outlet/vite.config.ts",
    "content": "import * as path from \"path\";\nimport { defineConfig } from \"vite\";\nimport react from \"@vitejs/plugin-react\";\nimport rollupReplace from \"@rollup/plugin-replace\";\n\n// https://vitejs.dev/config/\nexport default defineConfig({\n  server: {\n    port: 3000,\n  },\n  plugins: [\n    rollupReplace({\n      preventAssignment: true,\n      values: {\n        \"process.env.NODE_ENV\": JSON.stringify(\"development\"),\n      },\n    }),\n    react(),\n  ],\n  resolve: process.env.USE_SOURCE\n    ? {\n        alias: {\n          \"react-router\": path.resolve(\n            __dirname,\n            \"../../packages/react-router/index.ts\",\n          ),\n          \"react-router-dom\": path.resolve(\n            __dirname,\n            \"../../packages/react-router-dom/index.tsx\",\n          ),\n        },\n      }\n    : {},\n});\n"
  },
  {
    "path": "examples/multi-app/.gitignore",
    "content": "public/build\n"
  },
  {
    "path": "examples/multi-app/.stackblitzrc",
    "content": "{\n  \"installDependencies\": true,\n  \"startCommand\": \"npm run dev\"\n}\n"
  },
  {
    "path": "examples/multi-app/README.md",
    "content": "---\ntitle: Multi App\ntoc: false\n---\n\n# Multi App Example\n\nThis example demonstrates how to build a site with multiple React Router apps by mounting each at a URL pathname prefix using the `<Router basename>` prop. This essentially decouples the apps from each other and allows them to be portable and even deployed separately.\n\n## Preview\n\nOpen this example on [StackBlitz](https://stackblitz.com):\n\n[![Open in StackBlitz](https://developer.stackblitz.com/img/open_in_stackblitz.svg)](https://stackblitz.com/github/remix-run/react-router/tree/main/examples/multi-app?file=home/App.jsx)\n"
  },
  {
    "path": "examples/multi-app/home/App.jsx",
    "content": "import { Routes, Route, Outlet, Link } from \"react-router-dom\";\nimport { NoMatch } from \"../inbox/no-match\";\nimport \"./index.css\";\n\nexport default function HomeApp() {\n  return (\n    <Routes>\n      <Route path=\"/\" element={<Layout />}>\n        <Route index element={<Home />} />\n        <Route path=\"about\" element={<About />} />\n        <Route path=\"*\" element={<NoMatch />} />\n      </Route>\n    </Routes>\n  );\n}\n\nfunction Layout() {\n  return (\n    <div>\n      <h1>Welcome to the Home app!</h1>\n\n      <p>\n        This example demonstrates how you can stitch two React Router apps\n        together using the <code>`basename`</code> prop on{\" \"}\n        <code>`BrowserRouter`</code> and <code>`StaticRouter`</code>.\n      </p>\n\n      <nav>\n        <ul>\n          <li>\n            <Link to=\"/\">Home</Link>\n          </li>\n          <li>\n            <Link to=\"/about\">About</Link>\n          </li>\n          <li>\n            {/* Use a normal <a> when linking to the \"Inbox\" app so the browser\n                does a full document reload, which is what we want when exiting\n                this app and entering another so we execute its entry point in\n                inbox/main.jsx. */}\n            <a href=\"/inbox\">Inbox</a>\n          </li>\n        </ul>\n      </nav>\n\n      <hr />\n\n      <Outlet />\n    </div>\n  );\n}\n\nfunction Home() {\n  return (\n    <div>\n      <p>This is the home page.</p>\n    </div>\n  );\n}\n\nfunction About() {\n  return (\n    <div>\n      <p>This is the about page.</p>\n    </div>\n  );\n}\n"
  },
  {
    "path": "examples/multi-app/home/index.css",
    "content": "body {\n  font-family:\n    -apple-system, BlinkMacSystemFont, \"Segoe UI\", \"Roboto\", \"Oxygen\", \"Ubuntu\",\n    \"Cantarell\", \"Fira Sans\", \"Droid Sans\", \"Helvetica Neue\", sans-serif;\n  -webkit-font-smoothing: antialiased;\n  -moz-osx-font-smoothing: grayscale;\n}\n\ncode {\n  font-family:\n    source-code-pro, Menlo, Monaco, Consolas, \"Courier New\", monospace;\n}\n"
  },
  {
    "path": "examples/multi-app/home/main.jsx",
    "content": "import * as React from \"react\";\nimport * as ReactDOM from \"react-dom/client\";\nimport { BrowserRouter } from \"react-router-dom\";\n\nimport HomeApp from \"./App\";\n\nReactDOM.createRoot(document.getElementById(\"root\")).render(\n  <React.StrictMode>\n    {/* No basename for this router. This app renders at the root / URL. */}\n    <BrowserRouter>\n      <HomeApp />\n    </BrowserRouter>\n  </React.StrictMode>,\n);\n"
  },
  {
    "path": "examples/multi-app/home/no-match.jsx",
    "content": "import { Link } from \"react-router-dom\";\n\nfunction NoMatch() {\n  return (\n    <div>\n      <h2>Nothing to see here!</h2>\n      <p>\n        <Link to=\"/\">Go to the home page of the app</Link>\n      </p>\n    </div>\n  );\n}\n\nexport { NoMatch };\n"
  },
  {
    "path": "examples/multi-app/inbox/App.jsx",
    "content": "import { Routes, Route, useParams, Link, Outlet } from \"react-router-dom\";\nimport \"./index.css\";\nimport { getMessageById, messages } from \"./messages\";\nimport { NoMatch } from \"./no-match\";\n\nexport default function InboxApp() {\n  return (\n    <Routes>\n      {/* Routes in this app don't need to worry about which URL prefix they are\n          mounted at. They can just assume they are mounted at /. Then, if they\n          are moved under a different basename later on, all routes and links will\n          continue to work. */}\n      <Route path=\"/\" element={<Layout />}>\n        <Route index element={<Inbox />} />\n        <Route path=\":id\" element={<Message />} />\n        <Route path=\"*\" element={<NoMatch />} />\n      </Route>\n    </Routes>\n  );\n}\n\nfunction Layout() {\n  return (\n    <div>\n      <h1>Welcome to the Inbox app!</h1>\n      <nav>\n        <ul>\n          <li>\n            {/* Using a normal link here will cause the browser to reload the\n                document when following this link, which is exactly what we want\n                when navigating to the \"Home\" app so we execute its entry\n                point (see home/main.jsx). */}\n            <a href=\"/\">Home</a>\n          </li>\n          <li>\n            <a href=\"/about\">About</a>\n          </li>\n          <li>\n            <Link to=\"/\">Inbox</Link>\n          </li>\n        </ul>\n      </nav>\n\n      <hr />\n\n      <Outlet />\n    </div>\n  );\n}\n\nfunction Inbox() {\n  return (\n    <div>\n      <div style={{ maxWidth: 800, margin: \"0 auto\" }}>\n        {messages.map((message) => (\n          <Link\n            to={message.id}\n            key={message.id}\n            style={{\n              display: \"flex\",\n              borderBottom: \"1px solid #ccc\",\n              padding: \"10px\",\n              width: \"100%\",\n              textDecoration: \"none\",\n              color: \"#000\",\n            }}\n          >\n            <span\n              style={{\n                flexBasis: 100,\n                marginRight: \"1rem\",\n              }}\n            >\n              {message.from.name}\n            </span>\n            <div\n              style={{\n                flexGrow: 1,\n                textOverflow: \"ellipsis\",\n                width: \"100%\",\n                whiteSpace: \"nowrap\",\n                overflow: \"hidden\",\n                marginRight: \"1rem\",\n              }}\n            >\n              <span>{message.subject}</span>\n              <div style={{ color: \"#999\", display: \"inline\" }}>\n                <span>{\" — \"}</span>\n                <span>{message.body}</span>\n              </div>\n            </div>\n            <span style={{ flexShrink: 0 }}>\n              {new Date(message.date).toDateString()}\n            </span>\n          </Link>\n        ))}\n      </div>\n    </div>\n  );\n}\n\nfunction Message() {\n  let { id } = useParams();\n  let message = getMessageById(id);\n\n  if (!message) {\n    return <NoMatch />;\n  }\n\n  return (\n    <div>\n      <h2>{message.subject}</h2>\n      <div>\n        <h3 style={{ fontSize: 14 }}>\n          <span>{message.from.name}</span>{\" \"}\n          <span>&lt;{message.from.email}&gt;</span>\n        </h3>\n        <div>{message.body}</div>\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "examples/multi-app/inbox/index.css",
    "content": "body {\n  font-family:\n    -apple-system, BlinkMacSystemFont, \"Segoe UI\", \"Roboto\", \"Oxygen\", \"Ubuntu\",\n    \"Cantarell\", \"Fira Sans\", \"Droid Sans\", \"Helvetica Neue\", sans-serif;\n  -webkit-font-smoothing: antialiased;\n  -moz-osx-font-smoothing: grayscale;\n}\n\ncode {\n  font-family:\n    source-code-pro, Menlo, Monaco, Consolas, \"Courier New\", monospace;\n}\n"
  },
  {
    "path": "examples/multi-app/inbox/index.html",
    "content": "<!doctype html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"UTF-8\" />\n    <meta http-equiv=\"X-UA-Compatible\" content=\"IE=edge\" />\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" />\n    <title>React Router - Multi App Example (Inbox App)</title>\n  </head>\n  <body>\n    <div id=\"root\"></div>\n    <script type=\"module\" src=\"/inbox/main.jsx\"></script>\n  </body>\n</html>\n"
  },
  {
    "path": "examples/multi-app/inbox/main.jsx",
    "content": "import * as React from \"react\";\nimport * as ReactDOM from \"react-dom/client\";\nimport { BrowserRouter } from \"react-router-dom\";\n\nimport InboxApp from \"./App\";\n\nReactDOM.createRoot(document.getElementById(\"root\")).render(\n  <React.StrictMode>\n    {/* \"Mount\" this app under the /inbox URL pathname. All routes and links\n        are relative to this name. */}\n    <BrowserRouter basename=\"inbox\">\n      <InboxApp />\n    </BrowserRouter>\n  </React.StrictMode>,\n);\n"
  },
  {
    "path": "examples/multi-app/inbox/messages.js",
    "content": "/**\n * @typedef Message\n * @type {object}\n * @property {string} id\n * @property {string} subject\n * @property {string} body\n * @property {object} from\n * @property {string} from.name\n * @property {string} from.email\n * @property {object} to\n * @property {string} to.name\n * @property {string} to.email\n * @property {string} date\n */\n\n/**\n * @type Message[]\n */\nlet messages = [\n  {\n    id: \"1\",\n    from: {\n      name: \"GitHub\",\n      email: \"noreply@github.com\",\n    },\n    to: {\n      name: \"Logan McAnsh\",\n      email: \"logan@remix.run\",\n    },\n    subject: \"[GitHub] A personal access token has been added to your account\",\n    body: \"Hey mcansh!\\n\\nA personal access token.....................\",\n    date: \"2021-10-12T15:30:33.566Z\",\n  },\n  {\n    id: \"2\",\n    from: {\n      name: \"Remix\",\n      email: \"news@github.com\",\n    },\n    to: {\n      name: \"Logan McAnsh\",\n      email: \"logan@remix.run\",\n    },\n    subject: \"Remix Goes Open Source, Raises Seed Funding\",\n    body: \"Thanks for following our journey as we built Remix\",\n    date: \"2021-10-11T19:26:00.000Z\",\n  },\n  {\n    id: \"3\",\n    from: {\n      name: \"GitHub\",\n      email: \"noreply@github.com\",\n    },\n    to: {\n      name: \"Logan McAnsh\",\n      email: \"logan@remix.run\",\n    },\n    subject: \"[GitHub] Payment Receipt for mcansh\",\n    body: \"We received payment for your GitHub.com subscription. Thanks for your business!\",\n    date: \"2021-10-05T15:30:33.566Z\",\n  },\n];\n\n/**\n *\n * @param {string} id\n * @returns {Message | undefined}\n */\nfunction getMessageById(id) {\n  return messages.find((message) => message.id === id);\n}\n\nexport { messages, getMessageById };\n"
  },
  {
    "path": "examples/multi-app/inbox/no-match.jsx",
    "content": "import { Link } from \"react-router-dom\";\n\nfunction NoMatch() {\n  return (\n    <div>\n      <h2>Nothing to see here!</h2>\n      <p>\n        <Link to=\"/\">Go to the home page of the app</Link>\n      </p>\n    </div>\n  );\n}\n\nexport { NoMatch };\n"
  },
  {
    "path": "examples/multi-app/index.html",
    "content": "<!doctype html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"UTF-8\" />\n    <meta http-equiv=\"X-UA-Compatible\" content=\"IE=edge\" />\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" />\n    <title>React Router - Multi App Example (Home App)</title>\n  </head>\n  <body>\n    <div id=\"root\"></div>\n    <script type=\"module\" src=\"/home/main.jsx\"></script>\n  </body>\n</html>\n"
  },
  {
    "path": "examples/multi-app/package.json",
    "content": "{\n  \"name\": \"multi-app\",\n  \"private\": true,\n  \"scripts\": {\n    \"dev\": \"cross-env NODE_ENV=development node server.js\",\n    \"build\": \"vite build\",\n    \"start\": \"cross-env NODE_ENV=production node server.js\",\n    \"debug\": \"node --inspect-brk server.js\"\n  },\n  \"dependencies\": {\n    \"compression\": \"^1.7.4\",\n    \"cross-env\": \"^7.0.3\",\n    \"express\": \"^4.18.2\",\n    \"react\": \"^18.2.0\",\n    \"react-dom\": \"^18.2.0\",\n    \"react-router-dom\": \"^6.15.0\"\n  },\n  \"devDependencies\": {\n    \"@rollup/plugin-replace\": \"^5.0.2\",\n    \"@types/react\": \"^18.0.27\",\n    \"@types/react-dom\": \"^18.0.10\",\n    \"@vitejs/plugin-react\": \"^3.0.1\",\n    \"vite\": \"^4.0.4\"\n  }\n}\n"
  },
  {
    "path": "examples/multi-app/server.js",
    "content": "let path = require(\"path\");\nlet fsp = require(\"fs/promises\");\nlet express = require(\"express\");\n\nlet isProduction = process.env.NODE_ENV === \"production\";\n\nasync function createServer() {\n  let app = express();\n  /**\n   * @type {import(\"vite\").ViteDevServer}\n   */\n  let vite;\n\n  if (!isProduction) {\n    vite = await require(\"vite\").createServer({\n      root: process.cwd(),\n      server: { middlewareMode: true },\n      appType: \"custom\",\n    });\n\n    app.use(vite.middlewares);\n  } else {\n    app.use(require(\"compression\")());\n    app.use(express.static(path.join(__dirname, \"dist\")));\n  }\n\n  app.use(\"*\", async (req, res) => {\n    let url = req.originalUrl;\n\n    // Use a separate HTML file for the \"Inbox\" app.\n    let appDirectory = url.startsWith(\"/inbox\") ? \"inbox\" : \"\";\n    let htmlFileToLoad;\n\n    if (isProduction) {\n      htmlFileToLoad = path.join(\"dist\", appDirectory, \"index.html\");\n    } else {\n      htmlFileToLoad = path.join(appDirectory, \"index.html\");\n    }\n\n    try {\n      let html = await fsp.readFile(\n        path.join(__dirname, htmlFileToLoad),\n        \"utf8\",\n      );\n\n      if (!isProduction) {\n        html = await vite.transformIndexHtml(req.url, html);\n      }\n\n      res.setHeader(\"Content-Type\", \"text/html\");\n      return res.status(200).end(html);\n    } catch (error) {\n      if (!isProduction) vite.ssrFixStacktrace(error);\n      console.log(error.stack);\n      return res.status(500).end(error.stack);\n    }\n  });\n\n  return app;\n}\n\ncreateServer().then((app) => {\n  app.listen(3000, () => {\n    console.log(\"HTTP server is running at http://localhost:3000\");\n  });\n});\n"
  },
  {
    "path": "examples/multi-app/vite.config.js",
    "content": "import * as path from \"path\";\n\nimport { defineConfig } from \"vite\";\nimport react from \"@vitejs/plugin-react\";\nimport rollupReplace from \"@rollup/plugin-replace\";\n\nexport default defineConfig({\n  server: {\n    port: 3000,\n  },\n  plugins: [\n    rollupReplace({\n      preventAssignment: true,\n      values: {\n        \"process.env.NODE_ENV\": JSON.stringify(\"development\"),\n      },\n    }),\n    react(),\n  ],\n  build: {\n    rollupOptions: {\n      // Build two separate bundles, one for each app.\n      input: {\n        main: path.resolve(__dirname, \"index.html\"),\n        inbox: path.resolve(__dirname, \"inbox/index.html\"),\n      },\n    },\n  },\n  resolve: process.env.USE_SOURCE\n    ? {\n        alias: {\n          \"react-router\": path.resolve(\n            __dirname,\n            \"../../packages/react-router/index.ts\",\n          ),\n          \"react-router-dom\": path.resolve(\n            __dirname,\n            \"../../packages/react-router-dom/index.tsx\",\n          ),\n        },\n      }\n    : {},\n});\n"
  },
  {
    "path": "examples/navigation-blocking/.gitignore",
    "content": "node_modules\n.DS_Store\ndist\ndist-ssr\n*.local\n"
  },
  {
    "path": "examples/navigation-blocking/.stackblitzrc",
    "content": "{\n  \"installDependencies\": true,\n  \"startCommand\": \"npm run dev\"\n}\n"
  },
  {
    "path": "examples/navigation-blocking/README.md",
    "content": "---\ntitle: Navigation Blocking\ntoc: false\norder: 1\n---\n\n# Navigation Blocking\n\nThis example demonstrates using `useBlocker` to prevent navigating away from a page where you might lose user-entered form data. A potentially better UX for this is storing user-entered information in `sessionStorage` and pre-populating the form on return.\n\n## Preview\n\nOpen this example on [StackBlitz](https://stackblitz.com):\n\n[![Open in StackBlitz](https://developer.stackblitz.com/img/open_in_stackblitz.svg)](https://stackblitz.com/github/remix-run/react-router/tree/main/examples/navigation-blocking?file=src/App.tsx)\n"
  },
  {
    "path": "examples/navigation-blocking/index.html",
    "content": "<!doctype html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"UTF-8\" />\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" />\n    <title>React Router - Navigation Blocking</title>\n  </head>\n  <body>\n    <div id=\"root\"></div>\n    <script type=\"module\" src=\"/src/main.tsx\"></script>\n  </body>\n</html>\n"
  },
  {
    "path": "examples/navigation-blocking/package.json",
    "content": "{\n  \"name\": \"navigation-blocking\",\n  \"private\": true,\n  \"scripts\": {\n    \"dev\": \"vite\",\n    \"build\": \"tsc && vite build\",\n    \"serve\": \"vite preview\"\n  },\n  \"dependencies\": {\n    \"react\": \"^18.2.0\",\n    \"react-dom\": \"^18.2.0\",\n    \"react-router-dom\": \"^6.21.0\"\n  },\n  \"devDependencies\": {\n    \"@rollup/plugin-replace\": \"5.0.2\",\n    \"@types/node\": \"18.11.18\",\n    \"@types/react\": \"18.0.27\",\n    \"@types/react-dom\": \"18.0.10\",\n    \"@vitejs/plugin-react\": \"3.0.1\",\n    \"typescript\": \"4.9.5\",\n    \"vite\": \"4.0.4\"\n  }\n}\n"
  },
  {
    "path": "examples/navigation-blocking/src/app.tsx",
    "content": "import * as React from \"react\";\nimport type {\n  unstable_Blocker as Blocker,\n  unstable_BlockerFunction as BlockerFunction,\n} from \"react-router-dom\";\nimport { useActionData } from \"react-router-dom\";\nimport {\n  createBrowserRouter,\n  createRoutesFromElements,\n  Form,\n  json,\n  Link,\n  Outlet,\n  Route,\n  RouterProvider,\n  useBlocker,\n  useLocation,\n} from \"react-router-dom\";\n\nlet router = createBrowserRouter(\n  createRoutesFromElements(\n    <Route path=\"/\" element={<Layout />}>\n      <Route index element={<h2>Index</h2>} />\n      <Route path=\"one\" element={<h2>One</h2>} />\n      <Route path=\"two\" element={<h2>Two</h2>} />\n      <Route\n        path=\"three\"\n        action={() => json({ ok: true })}\n        element={\n          <>\n            <h2>Three</h2>\n            <ImportantForm />\n          </>\n        }\n      />\n      <Route path=\"four\" element={<h2>Four</h2>} />\n      <Route path=\"five\" element={<h2>Five</h2>} />\n    </Route>,\n  ),\n);\n\nif (import.meta.hot) {\n  import.meta.hot.dispose(() => router.dispose());\n}\n\nexport default function App() {\n  return <RouterProvider router={router} />;\n}\n\nfunction Layout() {\n  let [historyIndex, setHistoryIndex] = React.useState(\n    window.history.state?.idx,\n  );\n  let location = useLocation();\n\n  // Expose the underlying history index in the UI for debugging\n  React.useEffect(() => {\n    setHistoryIndex(window.history.state?.idx);\n  }, [location]);\n\n  // Give us meaningful document titles for popping back/forward more than 1 entry\n  React.useEffect(() => {\n    document.title = location.pathname;\n  }, [location]);\n\n  return (\n    <>\n      <h1>Navigation Blocking Example</h1>\n      <nav>\n        <Link to=\"/\">Index</Link>&nbsp;&nbsp;\n        <Link to=\"/one\">One</Link>&nbsp;&nbsp;\n        <Link to=\"/two\">Two</Link>&nbsp;&nbsp;\n        <Link to=\"/three\">Three (Form with blocker)</Link>&nbsp;&nbsp;\n        <Link to=\"/four\">Four</Link>&nbsp;&nbsp;\n        <Link to=\"/five\">Five</Link>&nbsp;&nbsp;\n      </nav>\n      <p>\n        Current location (index): {location.pathname} ({historyIndex})\n      </p>\n      <Outlet />\n    </>\n  );\n}\n\nfunction ImportantForm() {\n  let actionData = useActionData() as { ok: boolean } | undefined;\n  let [value, setValue] = React.useState(\"\");\n  // Allow the submission navigation to the same route to go through\n  let shouldBlock = React.useCallback<BlockerFunction>(\n    ({ currentLocation, nextLocation }) =>\n      value !== \"\" && currentLocation.pathname !== nextLocation.pathname,\n    [value],\n  );\n  let blocker = useBlocker(shouldBlock);\n\n  // Clean the input after a successful submission\n  React.useEffect(() => {\n    if (actionData?.ok) {\n      setValue(\"\");\n    }\n  }, [actionData]);\n\n  // Reset the blocker if the user cleans the form\n  React.useEffect(() => {\n    if (blocker.state === \"blocked\" && value === \"\") {\n      blocker.reset();\n    }\n  }, [blocker, value]);\n\n  return (\n    <>\n      <p>\n        Is the form dirty?{\" \"}\n        {value !== \"\" ? (\n          <span style={{ color: \"red\" }}>Yes</span>\n        ) : (\n          <span style={{ color: \"green\" }}>No</span>\n        )}\n      </p>\n\n      <Form method=\"post\">\n        <label>\n          Enter some important data:\n          <input\n            name=\"data\"\n            value={value}\n            onChange={(e) => setValue(e.target.value)}\n          />\n        </label>\n        <button type=\"submit\">Save</button>\n      </Form>\n\n      {blocker ? <ConfirmNavigation blocker={blocker} /> : null}\n    </>\n  );\n}\n\nfunction ConfirmNavigation({ blocker }: { blocker: Blocker }) {\n  if (blocker.state === \"blocked\") {\n    return (\n      <>\n        <p style={{ color: \"red\" }}>\n          Blocked the last navigation to {blocker.location.pathname}\n        </p>\n        <button onClick={() => blocker.proceed?.()}>Let me through</button>\n        <button onClick={() => blocker.reset?.()}>Keep me here</button>\n      </>\n    );\n  }\n\n  if (blocker.state === \"proceeding\") {\n    return (\n      <p style={{ color: \"orange\" }}>Proceeding through blocked navigation</p>\n    );\n  }\n\n  return <p style={{ color: \"green\" }}>Blocker is currently unblocked</p>;\n}\n"
  },
  {
    "path": "examples/navigation-blocking/src/main.tsx",
    "content": "import * as React from \"react\";\nimport * as ReactDOM from \"react-dom/client\";\nimport App from \"./app\";\n\nReactDOM.createRoot(document.getElementById(\"root\")).render(\n  <React.StrictMode>\n    <App />\n  </React.StrictMode>,\n);\n"
  },
  {
    "path": "examples/navigation-blocking/src/vite-env.d.ts",
    "content": "/// <reference types=\"vite/client\" />\n"
  },
  {
    "path": "examples/navigation-blocking/tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"baseUrl\": \".\",\n    \"target\": \"ESNext\",\n    \"lib\": [\"DOM\", \"DOM.Iterable\", \"ESNext\"],\n    \"allowJs\": false,\n    \"skipLibCheck\": true,\n    \"esModuleInterop\": false,\n    \"allowSyntheticDefaultImports\": true,\n    \"strict\": true,\n    \"module\": \"ESNext\",\n    \"moduleResolution\": \"Node\",\n    \"resolveJsonModule\": true,\n    \"verbatimModuleSyntax\": true,\n    \"noEmit\": true,\n    \"jsx\": \"react-jsx\",\n    \"importsNotUsedAsValues\": \"error\"\n  },\n  \"include\": [\"./src\"]\n}\n"
  },
  {
    "path": "examples/navigation-blocking/vite.config.ts",
    "content": "import * as path from \"path\";\nimport { defineConfig } from \"vite\";\nimport react from \"@vitejs/plugin-react\";\nimport rollupReplace from \"@rollup/plugin-replace\";\n\n// https://vitejs.dev/config/\nexport default defineConfig({\n  server: {\n    port: 3000,\n  },\n  plugins: [\n    rollupReplace({\n      preventAssignment: true,\n      values: {\n        \"process.env.NODE_ENV\": JSON.stringify(\"development\"),\n      },\n    }),\n    react(),\n  ],\n  resolve: process.env.USE_SOURCE\n    ? {\n        alias: {\n          \"react-router\": path.resolve(\n            __dirname,\n            \"../../packages/react-router/index.ts\",\n          ),\n          \"react-router-dom\": path.resolve(\n            __dirname,\n            \"../../packages/react-router-dom/index.tsx\",\n          ),\n        },\n      }\n    : {},\n});\n"
  },
  {
    "path": "examples/notes/.gitignore",
    "content": "node_modules\n.DS_Store\ndist\ndist-ssr\n*.local\n"
  },
  {
    "path": "examples/notes/.stackblitzrc",
    "content": "{\n  \"installDependencies\": true,\n  \"startCommand\": \"npm run dev\"\n}\n"
  },
  {
    "path": "examples/notes/README.md",
    "content": "---\ntitle: Notes CRUD\ntoc: false\norder: 1\n---\n\n# Data Routers\n\nThis example demonstrates some of the basic features of Data Router, including:\n\n- Loader functions\n- Action functions\n- <Link> and <Form> navigations\n\n## Preview\n\nOpen this example on [StackBlitz](https://stackblitz.com):\n\n[![Open in StackBlitz](https://developer.stackblitz.com/img/open_in_stackblitz.svg)](https://stackblitz.com/github/remix-run/react-router/tree/dev/examples/notes?file=src/App.jsx)\n"
  },
  {
    "path": "examples/notes/index.html",
    "content": "<!doctype html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"UTF-8\" />\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" />\n    <title>React Router - Basic Example</title>\n  </head>\n  <body>\n    <div id=\"root\"></div>\n    <script type=\"module\" src=\"/src/main.jsx\"></script>\n  </body>\n</html>\n"
  },
  {
    "path": "examples/notes/package.json",
    "content": "{\n  \"name\": \"notes\",\n  \"private\": true,\n  \"scripts\": {\n    \"dev\": \"vite\",\n    \"build\": \"tsc && vite build\",\n    \"serve\": \"vite preview\"\n  },\n  \"dependencies\": {\n    \"localforage\": \"^1.10.0\",\n    \"react\": \"^18.2.0\",\n    \"react-dom\": \"^18.2.0\",\n    \"react-router-dom\": \"^6.15.0\"\n  },\n  \"devDependencies\": {\n    \"@rollup/plugin-replace\": \"5.0.2\",\n    \"@types/node\": \"18.11.18\",\n    \"@types/react\": \"^18.0.27\",\n    \"@types/react-dom\": \"^18.0.10\",\n    \"@vitejs/plugin-react\": \"3.0.1\",\n    \"typescript\": \"4.9.5\",\n    \"vite\": \"4.0.4\"\n  }\n}\n"
  },
  {
    "path": "examples/notes/src/app.jsx",
    "content": "import \"./index.css\";\nimport { createBrowserRouter, RouterProvider } from \"react-router-dom\";\n\nimport Root, { loader as rootLoader } from \"./routes/root\";\nimport NewNote, { action as newNoteAction } from \"./routes/new\";\nimport Note, {\n  loader as noteLoader,\n  action as noteAction,\n} from \"./routes/note\";\n\nlet router = createBrowserRouter([\n  {\n    path: \"/\",\n    element: <Root />,\n    loader: rootLoader,\n    children: [\n      {\n        path: \"new\",\n        element: <NewNote />,\n        action: newNoteAction,\n      },\n      {\n        path: \"note/:noteId\",\n        element: <Note />,\n        loader: noteLoader,\n        action: noteAction,\n        errorElement: <h2>Note not found</h2>,\n      },\n    ],\n  },\n]);\n\nif (import.meta.hot) {\n  import.meta.hot.dispose(() => router.dispose());\n}\n\nexport default function App() {\n  return <RouterProvider router={router} />;\n}\n"
  },
  {
    "path": "examples/notes/src/index.css",
    "content": "body {\n  font-family:\n    -apple-system, BlinkMacSystemFont, \"Segoe UI\", \"Roboto\", \"Oxygen\", \"Ubuntu\",\n    \"Cantarell\", \"Fira Sans\", \"Droid Sans\", \"Helvetica Neue\", sans-serif;\n  -webkit-font-smoothing: antialiased;\n  -moz-osx-font-smoothing: grayscale;\n}\n\ncode {\n  font-family:\n    source-code-pro, Menlo, Monaco, Consolas, \"Courier New\", monospace;\n}\n"
  },
  {
    "path": "examples/notes/src/main.jsx",
    "content": "import * as React from \"react\";\nimport * as ReactDOM from \"react-dom/client\";\n\nimport App from \"./app\";\n\nReactDOM.createRoot(document.getElementById(\"root\")).render(\n  <React.StrictMode>\n    <App />\n  </React.StrictMode>,\n);\n"
  },
  {
    "path": "examples/notes/src/notes.js",
    "content": "import localforage from \"localforage\";\n\nexport async function getNotes() {\n  let notes = await localforage.getItem(\"notes\");\n  if (!notes) notes = [];\n  return notes;\n}\n\nexport async function createNote({ title, content }) {\n  let id = Math.random().toString(36).substring(2, 9);\n  let note = { id, title, content };\n  let notes = await getNotes();\n  notes.unshift(note);\n  await set(notes);\n  return note;\n}\n\nexport async function getNote(id) {\n  let notes = await localforage.getItem(\"notes\");\n  let note = notes.find((note) => note.id === id);\n  return note ?? null;\n}\n\nexport async function deleteNote(id) {\n  let notes = await localforage.getItem(\"notes\");\n  let index = notes.findIndex((note) => note.id === id);\n  if (index > -1) {\n    notes.splice(index, 1);\n    await set(notes);\n    return true;\n  }\n  return false;\n}\n\nfunction set(notes) {\n  return localforage.setItem(\"notes\", notes);\n}\n"
  },
  {
    "path": "examples/notes/src/routes/new.jsx",
    "content": "import { Form, redirect } from \"react-router-dom\";\nimport { createNote } from \"../notes\";\n\n////////////////////////////////////////////////////////////////////////////////\nexport default function NewNote() {\n  return (\n    <Form method=\"post\">\n      <p>\n        <label>\n          Title\n          <br />\n          <input type=\"text\" name=\"title\" />\n        </label>\n      </p>\n      <p>\n        <label htmlFor=\"content\">Content</label>\n        <br />\n        <textarea name=\"content\" rows=\"10\" cols=\"30\" id=\"content\" />\n      </p>\n      <p>\n        <button type=\"submit\">Save</button>\n      </p>\n    </Form>\n  );\n}\n\nexport async function action({ request }) {\n  const formData = await request.formData();\n  const note = await createNote({\n    title: formData.get(\"title\"),\n    content: formData.get(\"content\"),\n  });\n  return redirect(`/note/${note.id}`);\n}\n"
  },
  {
    "path": "examples/notes/src/routes/note.jsx",
    "content": "import { useLoaderData, Form, redirect } from \"react-router-dom\";\nimport { deleteNote, getNote } from \"../notes\";\n\nexport default function Note() {\n  const note = useLoaderData();\n  return (\n    <div>\n      <h2>{note.title}</h2>\n      <div>{note.content}</div>\n      <Form method=\"post\" style={{ marginTop: \"2rem\" }}>\n        <button type=\"submit\">Delete</button>\n      </Form>\n    </div>\n  );\n}\n\nexport async function loader({ params }) {\n  const note = await getNote(params.noteId);\n  if (!note) throw new Response(\"\", { status: 404 });\n  return note;\n}\n\nexport async function action({ params }) {\n  await deleteNote(params.noteId);\n  return redirect(\"/new\");\n}\n"
  },
  {
    "path": "examples/notes/src/routes/notes.jsx",
    "content": "export default function Notes() {\n  return <div>Notes</div>;\n}\n"
  },
  {
    "path": "examples/notes/src/routes/root.jsx",
    "content": "import { useLoaderData, Link, Outlet } from \"react-router-dom\";\nimport { getNotes } from \"../notes\";\n\nexport async function loader() {\n  return getNotes();\n}\n\nexport default function Root() {\n  const notes = useLoaderData();\n\n  return (\n    <div style={{ display: \"flex\" }}>\n      <div style={{ padding: \"0 2rem\", borderRight: \"solid 1px #ccc\" }}>\n        <h1>Notes!</h1>\n        <p>\n          <Link to=\"new\">Create Note</Link>\n        </p>\n        <ul>\n          {notes.map((note) => (\n            <li>\n              <Link to={`/note/${note.id}`}>{note.title}</Link>\n            </li>\n          ))}\n        </ul>\n      </div>\n\n      <div style={{ flex: 1, padding: \"0 2rem\" }}>\n        <Outlet />\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "examples/notes/src/vite-env.d.ts",
    "content": "/// <reference types=\"vite/client\" />\n"
  },
  {
    "path": "examples/notes/tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"baseUrl\": \".\",\n    \"target\": \"ESNext\",\n    \"lib\": [\"DOM\", \"DOM.Iterable\", \"ESNext\"],\n    \"allowJs\": false,\n    \"skipLibCheck\": true,\n    \"esModuleInterop\": false,\n    \"allowSyntheticDefaultImports\": true,\n    \"strict\": true,\n    \"module\": \"ESNext\",\n    \"moduleResolution\": \"Node\",\n    \"resolveJsonModule\": true,\n    \"verbatimModuleSyntax\": true,\n    \"noEmit\": true,\n    \"jsx\": \"react-jsx\",\n    \"importsNotUsedAsValues\": \"error\"\n  },\n  \"include\": [\"./src\", \"../../build/node_modules\"]\n}\n"
  },
  {
    "path": "examples/notes/vite.config.ts",
    "content": "import * as path from \"path\";\nimport { defineConfig } from \"vite\";\nimport react from \"@vitejs/plugin-react\";\nimport rollupReplace from \"@rollup/plugin-replace\";\n\n// https://vitejs.dev/config/\nexport default defineConfig({\n  server: {\n    port: 3000,\n  },\n  plugins: [\n    rollupReplace({\n      preventAssignment: true,\n      values: {\n        \"process.env.NODE_ENV\": JSON.stringify(\"development\"),\n      },\n    }),\n    react(),\n  ],\n  resolve: process.env.USE_SOURCE\n    ? {\n        alias: {\n          \"react-router\": path.resolve(\n            __dirname,\n            \"../../packages/react-router/index.ts\",\n          ),\n          \"react-router-dom\": path.resolve(\n            __dirname,\n            \"../../packages/react-router-dom/index.tsx\",\n          ),\n        },\n      }\n    : {},\n});\n"
  },
  {
    "path": "examples/route-objects/.gitignore",
    "content": "node_modules\n.DS_Store\ndist\ndist-ssr\n*.local\n"
  },
  {
    "path": "examples/route-objects/.stackblitzrc",
    "content": "{\n  \"installDependencies\": true,\n  \"startCommand\": \"npm run dev\"\n}\n"
  },
  {
    "path": "examples/route-objects/README.md",
    "content": "---\ntitle: Route Objects\ntoc: false\n---\n\n# Route Objects Example\n\nThis example demonstrates how to use the `useRoutes()` hook to define and render routes using regular JavaScript objects instead of `<Routes>` and `<Route>` elements. This is mainly a stylistic preference that may make more sense in some scenarios, depending on the data structures you're working with to define your routes.\n\nOne interesting thing to note is that even if you don't use this hook directly, `<Routes>` uses it internally. So either way you're using the exact same code path!\n\n## Preview\n\nOpen this example on [StackBlitz](https://stackblitz.com):\n\n[![Open in StackBlitz](https://developer.stackblitz.com/img/open_in_stackblitz.svg)](https://stackblitz.com/github/remix-run/react-router/tree/main/examples/route-objects?file=src/App.tsx)\n"
  },
  {
    "path": "examples/route-objects/index.html",
    "content": "<!doctype html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"UTF-8\" />\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" />\n    <title>React Router - Route Objects Example</title>\n  </head>\n  <body>\n    <div id=\"root\"></div>\n    <script type=\"module\" src=\"/src/main.tsx\"></script>\n  </body>\n</html>\n"
  },
  {
    "path": "examples/route-objects/package.json",
    "content": "{\n  \"name\": \"basic\",\n  \"private\": true,\n  \"scripts\": {\n    \"dev\": \"vite\",\n    \"build\": \"tsc && vite build\",\n    \"serve\": \"vite preview\"\n  },\n  \"dependencies\": {\n    \"react\": \"^18.2.0\",\n    \"react-dom\": \"^18.2.0\",\n    \"react-router-dom\": \"^6.15.0\"\n  },\n  \"devDependencies\": {\n    \"@rollup/plugin-replace\": \"^5.0.2\",\n    \"@types/node\": \"18.x\",\n    \"@types/react\": \"^18.0.27\",\n    \"@types/react-dom\": \"^18.0.10\",\n    \"@vitejs/plugin-react\": \"^3.0.1\",\n    \"typescript\": \"^4.9.5\",\n    \"vite\": \"^4.0.4\"\n  }\n}\n"
  },
  {
    "path": "examples/route-objects/src/App.tsx",
    "content": "import type { RouteObject } from \"react-router-dom\";\nimport { Outlet, Link, useRoutes, useParams } from \"react-router-dom\";\n\nexport default function App() {\n  let routes: RouteObject[] = [\n    {\n      path: \"/\",\n      element: <Layout />,\n      children: [\n        { index: true, element: <Home /> },\n        {\n          path: \"/courses\",\n          element: <Courses />,\n          children: [\n            { index: true, element: <CoursesIndex /> },\n            { path: \"/courses/:id\", element: <Course /> },\n          ],\n        },\n        { path: \"*\", element: <NoMatch /> },\n      ],\n    },\n  ];\n\n  // The useRoutes() hook allows you to define your routes as JavaScript objects\n  // instead of <Routes> and <Route> elements. This is really just a style\n  // preference for those who prefer to not use JSX for their routes config.\n  let element = useRoutes(routes);\n\n  return (\n    <div>\n      <h1>Route Objects Example</h1>\n\n      <p>\n        This example demonstrates how to use React Router's \"route object\" API\n        instead of the JSX API to configure your routes. Both APIs are\n        first-class. In fact, React Router actually uses the object-based API\n        internally by creating route objects from your{\" \"}\n        <code>&lt;Route&gt;</code>\n        elements.\n      </p>\n\n      <p>\n        React Router exposes a <code>useRoutes()</code> hook that allows you to\n        hook into the same matching algorithm that <code>&lt;Routes&gt;</code>{\" \"}\n        uses internally to decide which <code>&lt;Route&gt;</code> to render.\n        When you use this hook, you get back an element that will render your\n        entire route hierarchy.\n      </p>\n\n      {element}\n    </div>\n  );\n}\n\nfunction Layout() {\n  return (\n    <div>\n      <nav>\n        <ul>\n          <li>\n            <Link to=\"/\">Home</Link>\n          </li>\n          <li>\n            <Link to=\"/courses\">Courses</Link>\n          </li>\n          <li>\n            <Link to=\"/nothing-here\">Nothing Here</Link>\n          </li>\n        </ul>\n      </nav>\n\n      <hr />\n\n      <Outlet />\n    </div>\n  );\n}\n\nfunction Home() {\n  return (\n    <div>\n      <h2>Home</h2>\n    </div>\n  );\n}\n\nfunction Courses() {\n  return (\n    <div>\n      <h2>Courses</h2>\n      <Outlet />\n    </div>\n  );\n}\n\nfunction CoursesIndex() {\n  return (\n    <div>\n      <p>Please choose a course:</p>\n\n      <nav>\n        <ul>\n          <li>\n            <Link to=\"react-fundamentals\">React Fundamentals</Link>\n          </li>\n          <li>\n            <Link to=\"advanced-react\">Advanced React</Link>\n          </li>\n          <li>\n            <Link to=\"react-router\">React Router</Link>\n          </li>\n        </ul>\n      </nav>\n    </div>\n  );\n}\n\nfunction Course() {\n  let { id } = useParams<\"id\">();\n\n  return (\n    <div>\n      <h2>\n        Welcome to the {id!.split(\"-\").map(capitalizeString).join(\" \")} course!\n      </h2>\n\n      <p>This is a great course. You're gonna love it!</p>\n\n      <Link to=\"/courses\">See all courses</Link>\n    </div>\n  );\n}\n\nfunction capitalizeString(s: string): string {\n  return s.charAt(0).toUpperCase() + s.slice(1);\n}\n\nfunction NoMatch() {\n  return (\n    <div>\n      <h2>It looks like you're lost...</h2>\n      <p>\n        <Link to=\"/\">Go to the home page</Link>\n      </p>\n    </div>\n  );\n}\n"
  },
  {
    "path": "examples/route-objects/src/index.css",
    "content": "body {\n  font-family:\n    -apple-system, BlinkMacSystemFont, \"Segoe UI\", \"Roboto\", \"Oxygen\", \"Ubuntu\",\n    \"Cantarell\", \"Fira Sans\", \"Droid Sans\", \"Helvetica Neue\", sans-serif;\n  -webkit-font-smoothing: antialiased;\n  -moz-osx-font-smoothing: grayscale;\n}\n\ncode {\n  font-family:\n    source-code-pro, Menlo, Monaco, Consolas, \"Courier New\", monospace;\n}\n"
  },
  {
    "path": "examples/route-objects/src/main.tsx",
    "content": "import * as React from \"react\";\nimport * as ReactDOM from \"react-dom/client\";\nimport { BrowserRouter } from \"react-router-dom\";\n\nimport \"./index.css\";\nimport App from \"./App\";\n\nReactDOM.createRoot(document.getElementById(\"root\")).render(\n  <React.StrictMode>\n    <BrowserRouter>\n      <App />\n    </BrowserRouter>\n  </React.StrictMode>,\n);\n"
  },
  {
    "path": "examples/route-objects/src/vite-env.d.ts",
    "content": "/// <reference types=\"vite/client\" />\n"
  },
  {
    "path": "examples/route-objects/tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"baseUrl\": \".\",\n    \"target\": \"ESNext\",\n    \"lib\": [\"DOM\", \"DOM.Iterable\", \"ESNext\"],\n    \"allowJs\": false,\n    \"skipLibCheck\": false,\n    \"esModuleInterop\": false,\n    \"allowSyntheticDefaultImports\": true,\n    \"strict\": true,\n    \"module\": \"ESNext\",\n    \"moduleResolution\": \"Node\",\n    \"resolveJsonModule\": true,\n    \"verbatimModuleSyntax\": true,\n    \"noEmit\": true,\n    \"jsx\": \"react-jsx\",\n    \"importsNotUsedAsValues\": \"error\"\n  },\n  \"include\": [\"./src\"]\n}\n"
  },
  {
    "path": "examples/route-objects/vite.config.ts",
    "content": "import * as path from \"path\";\nimport { defineConfig } from \"vite\";\nimport react from \"@vitejs/plugin-react\";\nimport rollupReplace from \"@rollup/plugin-replace\";\n\n// https://vitejs.dev/config/\nexport default defineConfig({\n  server: {\n    port: 3000,\n  },\n  plugins: [\n    rollupReplace({\n      preventAssignment: true,\n      values: {\n        \"process.env.NODE_ENV\": JSON.stringify(\"development\"),\n      },\n    }),\n    react(),\n  ],\n  resolve: process.env.USE_SOURCE\n    ? {\n        alias: {\n          \"react-router\": path.resolve(\n            __dirname,\n            \"../../packages/react-router/index.ts\",\n          ),\n          \"react-router-dom\": path.resolve(\n            __dirname,\n            \"../../packages/react-router-dom/index.tsx\",\n          ),\n        },\n      }\n    : {},\n});\n"
  },
  {
    "path": "examples/scroll-restoration/.gitignore",
    "content": "node_modules\n.DS_Store\ndist\ndist-ssr\n*.local\n"
  },
  {
    "path": "examples/scroll-restoration/.stackblitzrc",
    "content": "{\n  \"installDependencies\": true,\n  \"startCommand\": \"npm run dev\"\n}\n"
  },
  {
    "path": "examples/scroll-restoration/README.md",
    "content": "---\ntitle: Scroll Restoration\ntoc: false\norder: 1\n---\n\n# Scroll Restoration\n\nThis example demonstrates the basic usage of the `<ScrollRestoration>` component, including:\n\n- Restoring scroll position via `location.key`\n- Restoring scroll position via `location.pathname`\n- Preventing scroll resetting via `<Link>`\n\n## Preview\n\nOpen this example on [StackBlitz](https://stackblitz.com):\n\n[![Open in StackBlitz](https://developer.stackblitz.com/img/open_in_stackblitz.svg)](https://stackblitz.com/github/remix-run/react-router/tree/main/examples/scroll-restoration?file=src/App.tsx)\n"
  },
  {
    "path": "examples/scroll-restoration/index.html",
    "content": "<!doctype html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"UTF-8\" />\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" />\n    <title>React Router - Basic Example</title>\n  </head>\n  <body>\n    <div id=\"root\"></div>\n    <script type=\"module\" src=\"/src/main.tsx\"></script>\n  </body>\n</html>\n"
  },
  {
    "path": "examples/scroll-restoration/package.json",
    "content": "{\n  \"name\": \"scroll-restoration\",\n  \"private\": true,\n  \"scripts\": {\n    \"dev\": \"vite\",\n    \"build\": \"tsc && vite build\",\n    \"serve\": \"vite preview\"\n  },\n  \"dependencies\": {\n    \"react\": \"^18.2.0\",\n    \"react-dom\": \"^18.2.0\",\n    \"react-router-dom\": \"^6.15.0\"\n  },\n  \"devDependencies\": {\n    \"@rollup/plugin-replace\": \"5.0.2\",\n    \"@types/node\": \"18.11.18\",\n    \"@types/react\": \"^18.0.27\",\n    \"@types/react-dom\": \"^18.0.10\",\n    \"@vitejs/plugin-react\": \"3.0.1\",\n    \"typescript\": \"4.9.5\",\n    \"vite\": \"4.0.4\"\n  }\n}\n"
  },
  {
    "path": "examples/scroll-restoration/src/app.tsx",
    "content": "import * as React from \"react\";\nimport type { Location, useMatches } from \"react-router-dom\";\nimport {\n  createBrowserRouter,\n  Link,\n  Outlet,\n  RouterProvider,\n  ScrollRestoration,\n  useLoaderData,\n  useLocation,\n  useNavigation,\n} from \"react-router-dom\";\n\nimport \"./index.css\";\n\nlet router = createBrowserRouter([\n  {\n    path: \"/\",\n    element: <Layout />,\n    children: [\n      {\n        index: true,\n        element: <h2>Home</h2>,\n      },\n      {\n        path: \"restore-by-key\",\n        loader: getArrayLoader,\n        element: <LongPage />,\n      },\n      {\n        path: \"restore-by-pathname\",\n        loader: getArrayLoader,\n        element: <LongPage />,\n        handle: { scrollMode: \"pathname\" },\n      },\n      {\n        path: \"link-to-hash\",\n        loader: getArrayLoader,\n        element: <LongPage />,\n      },\n    ],\n  },\n]);\n\nif (import.meta.hot) {\n  import.meta.hot.dispose(() => router.dispose());\n}\n\nexport default function App() {\n  return <RouterProvider router={router} fallbackElement={<p>Loading...</p>} />;\n}\n\nfunction Layout() {\n  let navigation = useNavigation();\n\n  // You can provide a custom implementation of what \"key\" should be used to\n  // cache scroll positions for a given location.  Using the location.key will\n  // provide standard browser behavior and only restore on back/forward\n  // navigations.  Using location.pathname will provide more aggressive\n  // restoration and will also restore on normal link navigations to a\n  // previously-accessed path.  Or - go nuts and lump many pages into a\n  // single key (i.e., anything /wizard/* uses the same key)!\n  let getKey = React.useCallback(\n    (location: Location, matches: ReturnType<typeof useMatches>) => {\n      let match = matches.find((m) => (m.handle as any)?.scrollMode);\n      if ((match?.handle as any)?.scrollMode === \"pathname\") {\n        return location.pathname;\n      }\n\n      return location.key;\n    },\n    [],\n  );\n\n  return (\n    <>\n      <style>{`\n        .wrapper {\n          display: grid;\n          grid-template-columns: 1fr 2fr;\n          padding: 1rem;\n        }\n\n        .fixed {\n          position: fixed;\n          max-width: 20%;\n          height: 100%;\n          padding: 1rem;\n        }\n\n        .navitem {\n          margin: 1rem 0;\n        }\n\n        .spinner {\n          position: fixed;\n          top: 0;\n          right: 0;\n          padding: 5px;\n          background-color: lightgreen;\n        }\n      `}</style>\n      <div\n        className=\"spinner\"\n        style={{\n          display: navigation.state === \"idle\" ? \"none\" : \"block\",\n        }}\n      >\n        Navigating...\n      </div>\n      <div className=\"wrapper\">\n        <div className=\"left\">\n          <div className=\"fixed\">\n            <nav>\n              <ul>\n                <li className=\"navitem\">\n                  <Link to=\"/\">Home</Link>\n                </li>\n                <li className=\"navitem\">\n                  <Link to=\"/restore-by-key\">\n                    This page restores by location.key\n                  </Link>\n                </li>\n                <li className=\"navitem\">\n                  <Link to=\"/restore-by-pathname\">\n                    {\" \"}\n                    This page restores by location.pathname\n                  </Link>\n                </li>\n                <li className=\"navitem\">\n                  <Link to=\"/link-to-hash#heading\">\n                    This link will link to a nested heading via hash\n                  </Link>\n                </li>\n                <li className=\"navitem\">\n                  <Link to=\"/restore-by-key\" preventScrollReset>\n                    This link will not scroll to the top\n                  </Link>\n                </li>\n                <li className=\"navitem\">\n                  <a href=\"https://www.google.com\">\n                    This links to an external site (google)\n                  </a>\n                </li>\n              </ul>\n            </nav>\n          </div>\n        </div>\n        <div className=\"right\">\n          <Outlet />\n        </div>\n      </div>\n      {/*\n        Including this component inside a data router component tree is what\n        enables restoration\n      */}\n      <ScrollRestoration getKey={getKey} />\n    </>\n  );\n}\n\ninterface ArrayLoaderData {\n  arr: Array<number>;\n}\n\nasync function getArrayLoader(): Promise<ArrayLoaderData> {\n  await new Promise((r) => setTimeout(r, 1000));\n  return {\n    arr: new Array(100).fill(null).map((_, i) => i),\n  };\n}\n\nfunction LongPage() {\n  let data = useLoaderData() as ArrayLoaderData;\n  let location = useLocation();\n  return (\n    <>\n      <h2>Long Page</h2>\n      {data.arr.map((n) => (\n        <p key={n}>\n          Item {n} on {location.pathname}\n        </p>\n      ))}\n      <h3 id=\"heading\">This is a linkable heading</h3>\n      {data.arr.map((n) => (\n        <p key={n}>\n          Item {n + 100} on {location.pathname}\n        </p>\n      ))}\n    </>\n  );\n}\n"
  },
  {
    "path": "examples/scroll-restoration/src/index.css",
    "content": "body {\n  font-family:\n    -apple-system, BlinkMacSystemFont, \"Segoe UI\", \"Roboto\", \"Oxygen\", \"Ubuntu\",\n    \"Cantarell\", \"Fira Sans\", \"Droid Sans\", \"Helvetica Neue\", sans-serif;\n  -webkit-font-smoothing: antialiased;\n  -moz-osx-font-smoothing: grayscale;\n}\n\ncode {\n  font-family:\n    source-code-pro, Menlo, Monaco, Consolas, \"Courier New\", monospace;\n}\n"
  },
  {
    "path": "examples/scroll-restoration/src/main.tsx",
    "content": "import * as React from \"react\";\nimport * as ReactDOM from \"react-dom/client\";\nimport App from \"./app\";\n\nReactDOM.createRoot(document.getElementById(\"root\")).render(\n  <React.StrictMode>\n    <App />\n  </React.StrictMode>,\n);\n"
  },
  {
    "path": "examples/scroll-restoration/src/vite-env.d.ts",
    "content": "/// <reference types=\"vite/client\" />\n"
  },
  {
    "path": "examples/scroll-restoration/tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"baseUrl\": \".\",\n    \"target\": \"ESNext\",\n    \"lib\": [\"DOM\", \"DOM.Iterable\", \"ESNext\"],\n    \"allowJs\": false,\n    \"skipLibCheck\": true,\n    \"esModuleInterop\": false,\n    \"allowSyntheticDefaultImports\": true,\n    \"strict\": true,\n    \"module\": \"ESNext\",\n    \"moduleResolution\": \"Node\",\n    \"resolveJsonModule\": true,\n    \"verbatimModuleSyntax\": true,\n    \"noEmit\": true,\n    \"jsx\": \"react-jsx\",\n    \"importsNotUsedAsValues\": \"error\"\n  },\n  \"include\": [\"./src\"]\n}\n"
  },
  {
    "path": "examples/scroll-restoration/vite.config.ts",
    "content": "import * as path from \"path\";\nimport { defineConfig } from \"vite\";\nimport react from \"@vitejs/plugin-react\";\nimport rollupReplace from \"@rollup/plugin-replace\";\n\n// https://vitejs.dev/config/\nexport default defineConfig({\n  server: {\n    port: 3000,\n  },\n  plugins: [\n    rollupReplace({\n      preventAssignment: true,\n      values: {\n        \"process.env.NODE_ENV\": JSON.stringify(\"development\"),\n      },\n    }),\n    react(),\n  ],\n  resolve: process.env.USE_SOURCE\n    ? {\n        alias: {\n          \"react-router\": path.resolve(\n            __dirname,\n            \"../../packages/react-router/index.ts\",\n          ),\n          \"react-router-dom\": path.resolve(\n            __dirname,\n            \"../../packages/react-router-dom/index.tsx\",\n          ),\n        },\n      }\n    : {},\n});\n"
  },
  {
    "path": "examples/search-params/.gitignore",
    "content": "node_modules\n.DS_Store\ndist\ndist-ssr\n*.local\n"
  },
  {
    "path": "examples/search-params/.stackblitzrc",
    "content": "{\n  \"installDependencies\": true,\n  \"startCommand\": \"npm run dev\"\n}\n"
  },
  {
    "path": "examples/search-params/README.md",
    "content": "---\ntitle: Search Params\ntoc: false\n---\n\n# Search Params Example\n\nThis example demonstrates how to read and write the URL query string using the `useSearchParams()` hook. This hook is similar to the `useNavigate()` hook, but just for the [`search` portion of the URL](https://developer.mozilla.org/en-US/docs/Web/API/Location/search).\n\nIn this example, we have a form to search for a user on GitHub and display their user profile.\n\n## Preview\n\nOpen this example on [StackBlitz](https://stackblitz.com):\n\n[![Open in StackBlitz](https://developer.stackblitz.com/img/open_in_stackblitz.svg)](https://stackblitz.com/github/remix-run/react-router/tree/main/examples/search-params?file=src/App.tsx)\n"
  },
  {
    "path": "examples/search-params/index.html",
    "content": "<!doctype html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"UTF-8\" />\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" />\n    <title>React Router - Search Params Example</title>\n  </head>\n  <body>\n    <div id=\"root\"></div>\n    <script type=\"module\" src=\"/src/main.tsx\"></script>\n  </body>\n</html>\n"
  },
  {
    "path": "examples/search-params/package.json",
    "content": "{\n  \"name\": \"search-params\",\n  \"private\": true,\n  \"scripts\": {\n    \"dev\": \"vite\",\n    \"build\": \"tsc && vite build\",\n    \"serve\": \"vite preview\"\n  },\n  \"dependencies\": {\n    \"react\": \"^18.2.0\",\n    \"react-dom\": \"^18.2.0\",\n    \"react-router-dom\": \"^6.15.0\"\n  },\n  \"devDependencies\": {\n    \"@rollup/plugin-replace\": \"^5.0.2\",\n    \"@types/node\": \"^18.11.18\",\n    \"@types/react\": \"^18.0.27\",\n    \"@types/react-dom\": \"^18.0.10\",\n    \"@vitejs/plugin-react\": \"^3.0.1\",\n    \"typescript\": \"^4.9.5\",\n    \"vite\": \"^4.0.4\"\n  }\n}\n"
  },
  {
    "path": "examples/search-params/src/App.tsx",
    "content": "import * as React from \"react\";\nimport { Link, Route, Routes, useSearchParams } from \"react-router-dom\";\n\nexport default function App() {\n  return (\n    <div>\n      <h1>Search Params Example</h1>\n\n      <p>\n        This example demonstrates a simple search page that makes a request for\n        user data to the GitHub API and displays information for that user on\n        the page. The example uses the <code>useSearchParams()</code> hook to\n        read and write the URL query string.\n      </p>\n\n      <Routes>\n        <Route path=\"/\" element={<Home />} />\n        <Route path=\"*\" element={<NoMatch />} />\n      </Routes>\n    </div>\n  );\n}\n\nfunction randomUser() {\n  let users = [\"chaance\", \"jacob-ebey\", \"mcansh\", \"mjackson\", \"ryanflorence\"];\n  return users[Math.floor(Math.random() * users.length)];\n}\n\nfunction Home() {\n  let [searchParams, setSearchParams] = useSearchParams();\n\n  // searchParams is a URLSearchParams object.\n  // See https://developer.mozilla.org/en-US/docs/Web/API/URLSearchParams\n  let user = searchParams.get(\"user\");\n\n  let [userData, setUserData] = React.useState<any>(null);\n\n  React.useEffect(() => {\n    let abortController = new AbortController();\n\n    async function getGitHubUser() {\n      let response = await fetch(`https://api.github.com/users/${user}`, {\n        signal: abortController.signal,\n      });\n      if (!abortController.signal.aborted) {\n        let data = await response.json();\n        setUserData(data);\n      }\n    }\n\n    if (user) {\n      getGitHubUser();\n    }\n\n    return () => {\n      abortController.abort();\n    };\n  }, [user]);\n\n  function handleSubmit(event: React.FormEvent<HTMLFormElement>) {\n    event.preventDefault();\n    let formData = new FormData(event.currentTarget);\n    let newUser = formData.get(\"user\") as string;\n    if (!newUser) return;\n    setSearchParams({ user: newUser });\n  }\n\n  function handleRandomSubmit(event: React.FormEvent<HTMLFormElement>) {\n    event.preventDefault();\n    let newUser = randomUser();\n    // our new random user is the same as our current one, let's try again\n    if (newUser === user) {\n      handleRandomSubmit(event);\n    } else {\n      setSearchParams({ user: newUser });\n    }\n  }\n\n  return (\n    <div>\n      <div style={{ display: \"flex\" }}>\n        <form onSubmit={handleSubmit}>\n          <label>\n            <input defaultValue={user ?? undefined} type=\"text\" name=\"user\" />\n          </label>\n          <button type=\"submit\">Search</button>\n        </form>\n        <form onSubmit={handleRandomSubmit}>\n          <input type=\"hidden\" name=\"random\" />\n          <button type=\"submit\">Random</button>\n        </form>\n      </div>\n\n      {userData && (\n        <div\n          style={{\n            padding: \"24px\",\n            margin: \"24px 0\",\n            borderTop: \"1px solid #eaeaea\",\n            display: \"flex\",\n            alignItems: \"center\",\n            gap: \"16px\",\n          }}\n        >\n          <img\n            style={{ borderRadius: \"50%\" }}\n            width={200}\n            height={200}\n            src={userData.avatar_url}\n            alt={userData.login}\n          />\n          <div>\n            <h2>{userData.name}</h2>\n            <p>{userData.bio}</p>\n          </div>\n        </div>\n      )}\n    </div>\n  );\n}\n\nfunction NoMatch() {\n  return (\n    <div>\n      <h2>Nothing to see here!</h2>\n      <p>\n        <Link to=\"/\">Go to the home page</Link>\n      </p>\n    </div>\n  );\n}\n"
  },
  {
    "path": "examples/search-params/src/index.css",
    "content": "body {\n  font-family:\n    -apple-system, BlinkMacSystemFont, \"Segoe UI\", \"Roboto\", \"Oxygen\", \"Ubuntu\",\n    \"Cantarell\", \"Fira Sans\", \"Droid Sans\", \"Helvetica Neue\", sans-serif;\n  -webkit-font-smoothing: antialiased;\n  -moz-osx-font-smoothing: grayscale;\n}\n\ncode {\n  font-family:\n    source-code-pro, Menlo, Monaco, Consolas, \"Courier New\", monospace;\n}\n"
  },
  {
    "path": "examples/search-params/src/main.tsx",
    "content": "import * as React from \"react\";\nimport * as ReactDOM from \"react-dom/client\";\nimport { BrowserRouter } from \"react-router-dom\";\n\nimport App from \"./App\";\nimport \"./index.css\";\n\nReactDOM.createRoot(document.getElementById(\"root\")).render(\n  <React.StrictMode>\n    <BrowserRouter>\n      <App />\n    </BrowserRouter>\n  </React.StrictMode>,\n);\n"
  },
  {
    "path": "examples/search-params/src/vite-env.d.ts",
    "content": "/// <reference types=\"vite/client\" />\n"
  },
  {
    "path": "examples/search-params/tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"baseUrl\": \".\",\n    \"target\": \"ESNext\",\n    \"lib\": [\"DOM\", \"DOM.Iterable\", \"ESNext\"],\n    \"allowJs\": false,\n    \"skipLibCheck\": true,\n    \"esModuleInterop\": false,\n    \"allowSyntheticDefaultImports\": true,\n    \"strict\": true,\n    \"module\": \"ESNext\",\n    \"moduleResolution\": \"Node\",\n    \"resolveJsonModule\": true,\n    \"verbatimModuleSyntax\": true,\n    \"noEmit\": true,\n    \"jsx\": \"react-jsx\",\n    \"importsNotUsedAsValues\": \"error\"\n  },\n  \"include\": [\"./src\"]\n}\n"
  },
  {
    "path": "examples/search-params/vite.config.ts",
    "content": "import * as path from \"path\";\nimport { defineConfig } from \"vite\";\nimport react from \"@vitejs/plugin-react\";\nimport rollupReplace from \"@rollup/plugin-replace\";\n\n// https://vitejs.dev/config/\nexport default defineConfig({\n  server: {\n    port: 3000,\n  },\n  plugins: [\n    rollupReplace({\n      preventAssignment: true,\n      values: {\n        \"process.env.NODE_ENV\": JSON.stringify(\"development\"),\n      },\n    }),\n    react(),\n  ],\n  resolve: process.env.USE_SOURCE\n    ? {\n        alias: {\n          \"react-router\": path.resolve(\n            __dirname,\n            \"../../packages/react-router/index.ts\",\n          ),\n          \"react-router-dom\": path.resolve(\n            __dirname,\n            \"../../packages/react-router-dom/index.tsx\",\n          ),\n        },\n      }\n    : {},\n});\n"
  },
  {
    "path": "examples/ssr/.stackblitzrc",
    "content": "{\n  \"installDependencies\": true,\n  \"startCommand\": \"npm run dev\"\n}\n"
  },
  {
    "path": "examples/ssr/README.md",
    "content": "---\ntitle: Server Rendering\ntoc: false\n---\n\n# Server-side Rendering Example\n\nThis example adds [server-side rendering](https://reactjs.org/docs/react-dom-server.html) (SSR) to our basic example.\n\nWith SSR, the server renders your app and sends real HTML to the browser instead of an empty HTML document with a bunch of `<script>` tags. After the browser loads the HTML and JavaScript from the server, React \"hydrates\" the HTML document using the same components it used to render the app on the server.\n\nThis example contains a server (see [server.js](server.js)) that can run in both development and production modes.\n\nIn the browser entry point (see [src/entry.client.tsx](src/entry.client.tsx)), we use React Router like we would traditionally do in a purely client-side app and render a `<BrowserRouter>` to provide routing context to the rest of the app. The main difference is that instead of using `ReactDOM.createRoot(el).render()` to render the app, since the HTML was already sent by the server, all we need is `ReactDOM.hydrateRoot()`.\n\nOn the server (see [src/entry.server.tsx](src/entry.server.tsx)), we use React Router's `<StaticRouter>` to render the app and plug in the URL we get from the incoming HTTP request.\n\n## Preview\n\nOpen this example on [StackBlitz](https://stackblitz.com):\n\n[![Open in StackBlitz](https://developer.stackblitz.com/img/open_in_stackblitz.svg)](https://stackblitz.com/github/remix-run/react-router/tree/main/examples/ssr?file=src/App.tsx)\n"
  },
  {
    "path": "examples/ssr/index.html",
    "content": "<!doctype html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"UTF-8\" />\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" />\n    <title>React Router - SSR Example</title>\n  </head>\n  <body>\n    <div id=\"app\"><!--app-html--></div>\n    <script type=\"module\" src=\"/src/entry.client.tsx\"></script>\n  </body>\n</html>\n"
  },
  {
    "path": "examples/ssr/package.json",
    "content": "{\n  \"name\": \"ssr\",\n  \"private\": true,\n  \"scripts\": {\n    \"dev\": \"cross-env NODE_ENV=development node server.js\",\n    \"build\": \"npm run build:client && npm run build:server\",\n    \"build:client\": \"vite build --outDir dist/client --ssrManifest\",\n    \"build:server\": \"vite build --ssr src/entry.server.tsx --outDir dist/server\",\n    \"start\": \"cross-env NODE_ENV=production node server.js\",\n    \"debug\": \"node --inspect-brk server.js\"\n  },\n  \"dependencies\": {\n    \"compression\": \"1.7.4\",\n    \"cross-env\": \"^7.0.3\",\n    \"express\": \"^4.18.2\",\n    \"react\": \"^18.2.0\",\n    \"react-dom\": \"^18.2.0\",\n    \"react-router-dom\": \"^6.15.0\"\n  },\n  \"devDependencies\": {\n    \"@rollup/plugin-replace\": \"^5.0.2\",\n    \"@types/node\": \"^18.11.18\",\n    \"@types/react\": \"^18.0.27\",\n    \"@types/react-dom\": \"^18.0.10\",\n    \"@vitejs/plugin-react\": \"^3.0.1\",\n    \"typescript\": \"^4.9.5\",\n    \"vite\": \"^4.0.4\"\n  }\n}\n"
  },
  {
    "path": "examples/ssr/server.js",
    "content": "let path = require(\"path\");\nlet fsp = require(\"fs/promises\");\nlet express = require(\"express\");\n\nlet root = process.cwd();\nlet isProduction = process.env.NODE_ENV === \"production\";\n\nfunction resolve(p) {\n  return path.resolve(__dirname, p);\n}\n\nasync function createServer() {\n  let app = express();\n  /**\n   * @type {import('vite').ViteDevServer}\n   */\n  let vite;\n\n  if (!isProduction) {\n    vite = await require(\"vite\").createServer({\n      root,\n      server: { middlewareMode: \"ssr\" },\n    });\n\n    app.use(vite.middlewares);\n  } else {\n    app.use(require(\"compression\")());\n    app.use(express.static(resolve(\"dist/client\")));\n  }\n\n  app.use(\"*\", async (req, res) => {\n    let url = req.originalUrl;\n\n    try {\n      let template;\n      let render;\n\n      if (!isProduction) {\n        template = await fsp.readFile(resolve(\"index.html\"), \"utf8\");\n        template = await vite.transformIndexHtml(url, template);\n        render = await vite\n          .ssrLoadModule(\"src/entry.server.tsx\")\n          .then((m) => m.render);\n      } else {\n        template = await fsp.readFile(\n          resolve(\"dist/client/index.html\"),\n          \"utf8\",\n        );\n        render = require(resolve(\"dist/server/entry.server.js\")).render;\n      }\n\n      let html = template.replace(\"<!--app-html-->\", render(url));\n      res.setHeader(\"Content-Type\", \"text/html\");\n      return res.status(200).end(html);\n    } catch (error) {\n      if (!isProduction) {\n        vite.ssrFixStacktrace(error);\n      }\n      console.log(error.stack);\n      res.status(500).end(error.stack);\n    }\n  });\n\n  return app;\n}\n\ncreateServer().then((app) => {\n  app.listen(3000, () => {\n    console.log(\"HTTP server is running at http://localhost:3000\");\n  });\n});\n"
  },
  {
    "path": "examples/ssr/src/App.tsx",
    "content": "import { Routes, Route, Outlet, Link } from \"react-router-dom\";\n\nexport default function App() {\n  return (\n    <div>\n      <h1>Server Rendering Example</h1>\n\n      <p>\n        If you check out the HTML source of this page, you'll notice that it\n        already contains the HTML markup of the app that was sent from the\n        server!\n      </p>\n\n      <p>\n        This is great for search engines that need to index this page. It's also\n        great for users because server-rendered pages tend to load more quickly\n        on mobile devices and over slow networks.\n      </p>\n\n      <p>\n        Another thing to notice is that when you click one of the links below\n        and navigate to a different URL, then hit the refresh button on your\n        browser, the server is able to generate the HTML markup for that page as\n        well because you're using React Router on the server. This creates a\n        seamless experience both for your users navigating around your site and\n        for developers on your team who get to use the same routing library in\n        both places.\n      </p>\n\n      {/* Routes nest inside one another. Nested route paths build upon\n            parent route paths, and nested route elements render inside\n            parent route elements. See the note about <Outlet> below. */}\n      <Routes>\n        <Route path=\"/\" element={<Layout />}>\n          <Route index element={<Home />} />\n          <Route path=\"about\" element={<About />} />\n          <Route path=\"dashboard\" element={<Dashboard />} />\n\n          {/* Using path=\"*\"\" means \"match anything\", so this route\n                acts like a catch-all for URLs that we don't have explicit\n                routes for. */}\n          <Route path=\"*\" element={<NoMatch />} />\n        </Route>\n      </Routes>\n    </div>\n  );\n}\n\nfunction Layout() {\n  return (\n    <div>\n      {/* A \"layout route\" is a good place to put markup you want to\n          share across all the pages on your site, like navigation. */}\n      <nav>\n        <ul>\n          <li>\n            <Link to=\"/\">Home</Link>\n          </li>\n          <li>\n            <Link to=\"/about\">About</Link>\n          </li>\n          <li>\n            <Link to=\"/dashboard\">Dashboard</Link>\n          </li>\n          <li>\n            <Link to=\"/nothing-here\">Nothing Here</Link>\n          </li>\n        </ul>\n      </nav>\n\n      <hr />\n\n      {/* An <Outlet> renders whatever child route is currently active,\n          so you can think about this <Outlet> as a placeholder for\n          the child routes we defined above. */}\n      <Outlet />\n    </div>\n  );\n}\n\nfunction Home() {\n  return (\n    <div>\n      <h2>Home</h2>\n    </div>\n  );\n}\n\nfunction About() {\n  return (\n    <div>\n      <h2>About</h2>\n    </div>\n  );\n}\n\nfunction Dashboard() {\n  return (\n    <div>\n      <h2>Dashboard</h2>\n    </div>\n  );\n}\n\nfunction NoMatch() {\n  return (\n    <div>\n      <h2>Nothing to see here!</h2>\n      <p>\n        <Link to=\"/\">Go to the home page</Link>\n      </p>\n    </div>\n  );\n}\n"
  },
  {
    "path": "examples/ssr/src/entry.client.tsx",
    "content": "import * as React from \"react\";\nimport * as ReactDOM from \"react-dom/client\";\nimport { BrowserRouter } from \"react-router-dom\";\n\nimport \"./index.css\";\nimport App from \"./App\";\n\nReactDOM.hydrateRoot(\n  document.getElementById(\"app\"),\n  <React.StrictMode>\n    <BrowserRouter>\n      <App />\n    </BrowserRouter>\n  </React.StrictMode>,\n);\n"
  },
  {
    "path": "examples/ssr/src/entry.server.tsx",
    "content": "import * as React from \"react\";\nimport ReactDOMServer from \"react-dom/server\";\nimport { StaticRouter } from \"react-router-dom/server\";\n\nimport App from \"./App\";\n\nexport function render(url: string) {\n  return ReactDOMServer.renderToString(\n    <React.StrictMode>\n      <StaticRouter location={url}>\n        <App />\n      </StaticRouter>\n    </React.StrictMode>,\n  );\n}\n"
  },
  {
    "path": "examples/ssr/src/index.css",
    "content": "body {\n  font-family:\n    -apple-system, BlinkMacSystemFont, \"Segoe UI\", \"Roboto\", \"Oxygen\", \"Ubuntu\",\n    \"Cantarell\", \"Fira Sans\", \"Droid Sans\", \"Helvetica Neue\", sans-serif;\n  -webkit-font-smoothing: antialiased;\n  -moz-osx-font-smoothing: grayscale;\n}\n\ncode {\n  font-family:\n    source-code-pro, Menlo, Monaco, Consolas, \"Courier New\", monospace;\n}\n"
  },
  {
    "path": "examples/ssr/src/vite-env.d.ts",
    "content": "/// <reference types=\"vite/client\" />\n"
  },
  {
    "path": "examples/ssr/tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"baseUrl\": \".\",\n    \"target\": \"ESNext\",\n    \"lib\": [\"DOM\", \"DOM.Iterable\", \"ESNext\"],\n    \"allowJs\": false,\n    \"skipLibCheck\": true,\n    \"esModuleInterop\": false,\n    \"allowSyntheticDefaultImports\": true,\n    \"strict\": true,\n    \"module\": \"ESNext\",\n    \"moduleResolution\": \"Node\",\n    \"resolveJsonModule\": true,\n    \"verbatimModuleSyntax\": true,\n    \"noEmit\": true,\n    \"jsx\": \"react-jsx\",\n    \"types\": [\"vite/client\"],\n    \"importsNotUsedAsValues\": \"error\"\n  },\n  \"include\": [\"./src\"]\n}\n"
  },
  {
    "path": "examples/ssr/vite.config.js",
    "content": "import * as path from \"path\";\nimport { defineConfig } from \"vite\";\nimport react from \"@vitejs/plugin-react\";\nimport rollupReplace from \"@rollup/plugin-replace\";\n\n// https://vitejs.dev/config/\nexport default defineConfig({\n  server: {\n    port: 3000,\n  },\n  plugins: [\n    rollupReplace({\n      preventAssignment: true,\n      values: {\n        \"process.env.NODE_ENV\": JSON.stringify(\"development\"),\n      },\n    }),\n    react(),\n  ],\n  resolve: process.env.USE_SOURCE\n    ? {\n        alias: {\n          \"react-router\": path.resolve(\n            __dirname,\n            \"../../packages/react-router/index.ts\",\n          ),\n          \"react-router-dom\": path.resolve(\n            __dirname,\n            \"../../packages/react-router-dom/index.tsx\",\n          ),\n        },\n      }\n    : {},\n});\n"
  },
  {
    "path": "examples/ssr-data-router/.stackblitzrc",
    "content": "{\n  \"installDependencies\": true,\n  \"startCommand\": \"npm run dev\"\n}\n"
  },
  {
    "path": "examples/ssr-data-router/README.md",
    "content": "---\ntitle: Data Router Server Rendering\ntoc: false\n---\n\n# Data Router Server-side Rendering Example\n\nThis example adds [server-side rendering](https://reactjs.org/docs/react-dom-server.html) (SSR) to our basic example using a data router.\n\nWith SSR, the server renders your app and sends real HTML to the browser instead of an empty HTML document with a bunch of `<script>` tags. After the browser loads the HTML and JavaScript from the server, React \"hydrates\" the HTML document using the same components it used to render the app on the server.\n\nThis example contains a server (see [server.js](server.js)) that can run in both development and production modes.\n\nIn the browser entry point (see [src/entry.client.tsx](src/entry.client.tsx)), we use React Router like we would traditionally do in a purely client-side app and render a `<DataBrowserRouter>` to provide routing context to the rest of the app. The main difference is that instead of using `ReactDOM.createRoot(el).render()` to render the app, since the HTML was already sent by the server, all we need is `ReactDOM.hydrateRoot()`.\n\nOn the server (see [src/entry.server.tsx](src/entry.server.tsx)), we create a static request handler using `createStaticHandler` and query for the incoming `Request` we get from Express (note that we convert the Express request to a Web Fetch Request). Once the router is finished with data loading, we use React Router's `<DataStaticRouter>` to render the app in the correct state.\n\n## Preview\n\nOpen this example on [StackBlitz](https://stackblitz.com):\n\n[![Open in StackBlitz](https://developer.stackblitz.com/img/open_in_stackblitz.svg)](https://stackblitz.com/github/remix-run/react-router/tree/main/examples/ssr-data-router?file=src/App.tsx)\n"
  },
  {
    "path": "examples/ssr-data-router/index.html",
    "content": "<!doctype html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"UTF-8\" />\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" />\n    <title>React Router - Data Router SSR Example</title>\n    <link rel=\"stylesheet\" href=\"/src/index.css\" />\n  </head>\n  <body>\n    <div id=\"app\"><!--app-html--></div>\n    <script type=\"module\" src=\"/src/entry.client.tsx\"></script>\n  </body>\n</html>\n"
  },
  {
    "path": "examples/ssr-data-router/package.json",
    "content": "{\n  \"name\": \"ssr-data-router\",\n  \"private\": true,\n  \"scripts\": {\n    \"dev\": \"cross-env NODE_ENV=development node server.js\",\n    \"build\": \"npm run build:client && npm run build:server\",\n    \"build:client\": \"vite build --outDir dist/client --ssrManifest\",\n    \"build:server\": \"vite build --ssr src/entry.server.tsx --outDir dist/server\",\n    \"start\": \"cross-env NODE_ENV=production node server.js\",\n    \"debug\": \"node --inspect-brk server.js\"\n  },\n  \"dependencies\": {\n    \"@remix-run/router\": \"^1.8.0\",\n    \"compression\": \"1.7.4\",\n    \"cross-env\": \"^7.0.3\",\n    \"express\": \"^4.18.2\",\n    \"history\": \"^5.3.0\",\n    \"react\": \"^18.2.0\",\n    \"react-dom\": \"^18.2.0\",\n    \"react-router-dom\": \"^6.15.0\"\n  },\n  \"devDependencies\": {\n    \"@rollup/plugin-replace\": \"^5.0.2\",\n    \"@types/express\": \"^4.17.16\",\n    \"@types/node\": \"^18.11.18\",\n    \"@types/react\": \"^18.0.27\",\n    \"@types/react-dom\": \"^18.0.10\",\n    \"@vitejs/plugin-react\": \"^3.0.1\",\n    \"typescript\": \"^4.9.5\",\n    \"vite\": \"^4.0.4\"\n  }\n}\n"
  },
  {
    "path": "examples/ssr-data-router/server.js",
    "content": "let path = require(\"path\");\nlet fsp = require(\"fs/promises\");\nlet express = require(\"express\");\n\nlet root = process.cwd();\nlet isProduction = process.env.NODE_ENV === \"production\";\n\nfunction resolve(p) {\n  return path.resolve(__dirname, p);\n}\n\nasync function createServer() {\n  let app = express();\n  /**\n   * @type {import('vite').ViteDevServer}\n   */\n  let vite;\n\n  if (!isProduction) {\n    vite = await require(\"vite\").createServer({\n      root,\n      server: { middlewareMode: \"ssr\" },\n    });\n\n    app.use(vite.middlewares);\n  } else {\n    app.use(require(\"compression\")());\n    app.use(express.static(resolve(\"dist/client\")));\n  }\n\n  app.use(\"*\", async (req, res) => {\n    let url = req.originalUrl;\n\n    try {\n      let template;\n      let render;\n\n      if (!isProduction) {\n        template = await fsp.readFile(resolve(\"index.html\"), \"utf8\");\n        template = await vite.transformIndexHtml(url, template);\n        render = await vite\n          .ssrLoadModule(\"src/entry.server.tsx\")\n          .then((m) => m.render);\n      } else {\n        template = await fsp.readFile(\n          resolve(\"dist/client/index.html\"),\n          \"utf8\",\n        );\n        render = require(resolve(\"dist/server/entry.server.js\")).render;\n      }\n\n      try {\n        let appHtml = await render(req, res);\n        let html = template.replace(\"<!--app-html-->\", appHtml);\n        res.setHeader(\"Content-Type\", \"text/html\");\n        return res.status(200).end(html);\n      } catch (e) {\n        if (e instanceof Response && e.status >= 300 && e.status <= 399) {\n          return res.redirect(e.status, e.headers.get(\"Location\"));\n        }\n        throw e;\n      }\n    } catch (error) {\n      if (!isProduction) {\n        vite.ssrFixStacktrace(error);\n      }\n      console.log(error.stack);\n      res.status(500).end(error.stack);\n    }\n  });\n\n  return app;\n}\n\ncreateServer().then((app) => {\n  app.listen(3000, () => {\n    console.log(\"HTTP server is running at http://localhost:3000\");\n  });\n});\n"
  },
  {
    "path": "examples/ssr-data-router/src/App.tsx",
    "content": "import type { RouteObject } from \"react-router-dom\";\nimport { Outlet, Link, useLoaderData, redirect } from \"react-router-dom\";\n\nexport const routes: RouteObject[] = [\n  {\n    path: \"/\",\n    element: <Layout />,\n    children: [\n      {\n        index: true,\n        loader: homeLoader,\n        element: <Home />,\n      },\n      {\n        path: \"about\",\n        element: <About />,\n      },\n      {\n        path: \"dashboard\",\n        loader: dashboardLoader,\n        element: <Dashboard />,\n      },\n      {\n        path: \"lazy\",\n        lazy: () => import(\"./lazy\"),\n      },\n      {\n        path: \"redirect\",\n        loader: redirectLoader,\n      },\n      {\n        path: \"*\",\n        element: <NoMatch />,\n      },\n    ],\n  },\n];\n\nfunction Layout() {\n  return (\n    <div>\n      <h1>Data Router Server Rendering Example</h1>\n\n      <p>\n        If you check out the HTML source of this page, you'll notice that it\n        already contains the HTML markup of the app that was sent from the\n        server, and all the loader data was pre-fetched!\n      </p>\n\n      <p>\n        This is great for search engines that need to index this page. It's also\n        great for users because server-rendered pages tend to load more quickly\n        on mobile devices and over slow networks.\n      </p>\n\n      <p>\n        Another thing to notice is that when you click one of the links below\n        and navigate to a different URL, then hit the refresh button on your\n        browser, the server is able to generate the HTML markup for that page as\n        well because you're using React Router on the server. This creates a\n        seamless experience both for your users navigating around your site and\n        for developers on your team who get to use the same routing library in\n        both places.\n      </p>\n\n      <nav>\n        <ul>\n          <li>\n            <Link to=\"/\">Home</Link>\n          </li>\n          <li>\n            <Link to=\"/about\">About</Link>\n          </li>\n          <li>\n            <Link to=\"/dashboard\">Dashboard</Link>\n          </li>\n          <li>\n            <Link to=\"/lazy\">Lazy</Link>\n          </li>\n          <li>\n            <Link to=\"/redirect\">Redirect to Home</Link>\n          </li>\n          <li>\n            <Link to=\"/nothing-here\">Nothing Here</Link>\n          </li>\n        </ul>\n      </nav>\n\n      <hr />\n\n      <Outlet />\n    </div>\n  );\n}\n\nconst sleep = (n = 500) => new Promise((r) => setTimeout(r, n));\nconst rand = () => Math.round(Math.random() * 100);\n\nasync function homeLoader() {\n  await sleep();\n  return { data: `Home loader - random value ${rand()}` };\n}\n\nfunction Home() {\n  let data = useLoaderData();\n  return (\n    <div>\n      <h2>Home</h2>\n      <p>Loader Data: {data.data}</p>\n    </div>\n  );\n}\n\nfunction About() {\n  return (\n    <div>\n      <h2>About</h2>\n    </div>\n  );\n}\n\nasync function dashboardLoader() {\n  await sleep();\n  return { data: `Dashboard loader - random value ${rand()}` };\n}\n\nfunction Dashboard() {\n  let data = useLoaderData();\n  return (\n    <div>\n      <h2>Dashboard</h2>\n      <p>Loader Data: {data.data}</p>\n    </div>\n  );\n}\n\nasync function redirectLoader() {\n  await sleep();\n  return redirect(\"/\");\n}\n\nfunction NoMatch() {\n  return (\n    <div>\n      <h2>Nothing to see here!</h2>\n      <p>\n        <Link to=\"/\">Go to the home page</Link>\n      </p>\n    </div>\n  );\n}\n"
  },
  {
    "path": "examples/ssr-data-router/src/entry.client.tsx",
    "content": "import * as React from \"react\";\nimport * as ReactDOM from \"react-dom/client\";\nimport {\n  createBrowserRouter,\n  matchRoutes,\n  RouterProvider,\n} from \"react-router-dom\";\n\nimport { routes } from \"./App\";\n\nhydrate();\n\nasync function hydrate() {\n  // Determine if any of the initial routes are lazy\n  let lazyMatches = matchRoutes(routes, window.location)?.filter(\n    (m) => m.route.lazy,\n  );\n\n  // Load the lazy matches and update the routes before creating your router\n  // so we can hydrate the SSR-rendered content synchronously\n  if (lazyMatches && lazyMatches?.length > 0) {\n    await Promise.all(\n      lazyMatches.map(async (m) => {\n        let routeModule = await m.route.lazy!();\n        Object.assign(m.route, { ...routeModule, lazy: undefined });\n      }),\n    );\n  }\n\n  let router = createBrowserRouter(routes);\n\n  ReactDOM.hydrateRoot(\n    document.getElementById(\"app\")!,\n    <React.StrictMode>\n      <RouterProvider router={router} fallbackElement={null} />\n    </React.StrictMode>,\n  );\n}\n"
  },
  {
    "path": "examples/ssr-data-router/src/entry.server.tsx",
    "content": "import type * as express from \"express\";\nimport * as React from \"react\";\nimport ReactDOMServer from \"react-dom/server\";\nimport {\n  createStaticHandler,\n  createStaticRouter,\n  StaticRouterProvider,\n} from \"react-router-dom/server\";\nimport { routes } from \"./App\";\n\nexport async function render(\n  request: express.Request,\n  response: express.Response,\n) {\n  let { query, dataRoutes } = createStaticHandler(routes);\n  let remixRequest = createFetchRequest(request, response);\n  let context = await query(remixRequest);\n\n  if (context instanceof Response) {\n    throw context;\n  }\n\n  let router = createStaticRouter(dataRoutes, context);\n  return ReactDOMServer.renderToString(\n    <React.StrictMode>\n      <StaticRouterProvider\n        router={router}\n        context={context}\n        nonce=\"the-nonce\"\n      />\n    </React.StrictMode>,\n  );\n}\n\nexport function createFetchRequest(\n  req: express.Request,\n  res: express.Response,\n): Request {\n  let origin = `${req.protocol}://${req.get(\"host\")}`;\n  // Note: This had to take originalUrl into account for presumably vite's proxying\n  let url = new URL(req.originalUrl || req.url, origin);\n\n  let controller = new AbortController();\n  res.on(\"close\", () => controller.abort());\n\n  let headers = new Headers();\n\n  for (let [key, values] of Object.entries(req.headers)) {\n    if (values) {\n      if (Array.isArray(values)) {\n        for (let value of values) {\n          headers.append(key, value);\n        }\n      } else {\n        headers.set(key, values);\n      }\n    }\n  }\n\n  let init: RequestInit = {\n    method: req.method,\n    headers,\n    signal: controller.signal,\n  };\n\n  if (req.method !== \"GET\" && req.method !== \"HEAD\") {\n    init.body = req.body;\n  }\n\n  return new Request(url.href, init);\n}\n"
  },
  {
    "path": "examples/ssr-data-router/src/index.css",
    "content": "body {\n  font-family:\n    -apple-system, BlinkMacSystemFont, \"Segoe UI\", \"Roboto\", \"Oxygen\", \"Ubuntu\",\n    \"Cantarell\", \"Fira Sans\", \"Droid Sans\", \"Helvetica Neue\", sans-serif;\n  -webkit-font-smoothing: antialiased;\n  -moz-osx-font-smoothing: grayscale;\n}\n\ncode {\n  font-family:\n    source-code-pro, Menlo, Monaco, Consolas, \"Courier New\", monospace;\n}\n"
  },
  {
    "path": "examples/ssr-data-router/src/lazy.tsx",
    "content": "import { useLoaderData } from \"react-router-dom\";\n\ninterface LazyLoaderData {\n  date: string;\n}\n\nexport const loader = async (): Promise<LazyLoaderData> => {\n  await new Promise((r) => setTimeout(r, 500));\n  return {\n    date: new Date().toISOString(),\n  };\n};\n\nfunction LazyPage() {\n  let data = useLoaderData() as LazyLoaderData;\n\n  return (\n    <>\n      <h2>Lazy Route</h2>\n      <p>Date from loader: {data.date}</p>\n    </>\n  );\n}\n\nexport const element = <LazyPage />;\n"
  },
  {
    "path": "examples/ssr-data-router/src/vite-env.d.ts",
    "content": "/// <reference types=\"vite/client\" />\n"
  },
  {
    "path": "examples/ssr-data-router/tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"baseUrl\": \".\",\n    \"target\": \"ESNext\",\n    \"lib\": [\"DOM\", \"DOM.Iterable\", \"ESNext\"],\n    \"allowJs\": false,\n    \"skipLibCheck\": true,\n    \"esModuleInterop\": false,\n    \"allowSyntheticDefaultImports\": true,\n    \"strict\": true,\n    \"module\": \"ESNext\",\n    \"moduleResolution\": \"Node\",\n    \"resolveJsonModule\": true,\n    \"verbatimModuleSyntax\": true,\n    \"noEmit\": true,\n    \"jsx\": \"react-jsx\",\n    \"types\": [\"vite/client\"],\n    \"importsNotUsedAsValues\": \"error\"\n  },\n  \"include\": [\"./src\"]\n}\n"
  },
  {
    "path": "examples/ssr-data-router/vite.config.js",
    "content": "import * as path from \"path\";\nimport { defineConfig } from \"vite\";\nimport react from \"@vitejs/plugin-react\";\nimport rollupReplace from \"@rollup/plugin-replace\";\n\n// https://vitejs.dev/config/\nexport default defineConfig({\n  server: {\n    port: 3000,\n  },\n  plugins: [\n    rollupReplace({\n      preventAssignment: true,\n      values: {\n        \"process.env.NODE_ENV\": JSON.stringify(\"development\"),\n      },\n    }),\n    react(),\n  ],\n  resolve: process.env.USE_SOURCE\n    ? {\n        alias: {\n          \"react-router\": path.resolve(\n            __dirname,\n            \"../../packages/react-router/index.ts\",\n          ),\n          \"react-router-dom\": path.resolve(\n            __dirname,\n            \"../../packages/react-router-dom/index.tsx\",\n          ),\n        },\n      }\n    : {},\n});\n"
  },
  {
    "path": "examples/view-transitions/.gitignore",
    "content": "node_modules\n.DS_Store\ndist\ndist-ssr\n*.local\n"
  },
  {
    "path": "examples/view-transitions/.stackblitzrc",
    "content": "{\n  \"installDependencies\": true,\n  \"startCommand\": \"npm run dev\"\n}\n"
  },
  {
    "path": "examples/view-transitions/README.md",
    "content": "---\ntitle: View Transitions\ntoc: false\n---\n\n# startViewTransition (Experimental)\n\nThis example demonstrates a simple usage of a Data Router with `document.startViewTransition` enabled.\n\n## Preview\n\nOpen this example on [StackBlitz](https://stackblitz.com):\n\n[![Open in StackBlitz](https://developer.stackblitz.com/img/open_in_stackblitz.svg)](https://stackblitz.com/github/remix-run/react-router/tree/main/examples/view-transitions?file=src/App.tsx)\n"
  },
  {
    "path": "examples/view-transitions/index.html",
    "content": "<!doctype html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"UTF-8\" />\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" />\n    <title>React Router - Basic Data Router Example</title>\n  </head>\n  <body>\n    <div id=\"root\"></div>\n    <script type=\"module\" src=\"/src/main.tsx\"></script>\n  </body>\n</html>\n"
  },
  {
    "path": "examples/view-transitions/package.json",
    "content": "{\n  \"name\": \"basic-data-router\",\n  \"private\": true,\n  \"scripts\": {\n    \"dev\": \"vite\",\n    \"build\": \"tsc && vite build\",\n    \"serve\": \"vite preview\"\n  },\n  \"dependencies\": {\n    \"react\": \"^18.2.0\",\n    \"react-dom\": \"^18.2.0\",\n    \"react-router-dom\": \"6.17.0-pre.2\"\n  },\n  \"devDependencies\": {\n    \"@rollup/plugin-replace\": \"5.0.2\",\n    \"@types/node\": \"18.11.18\",\n    \"@types/react\": \"18.0.27\",\n    \"@types/react-dom\": \"18.0.10\",\n    \"@vitejs/plugin-react\": \"3.0.1\",\n    \"typescript\": \"4.9.5\",\n    \"vite\": \"4.0.4\"\n  }\n}\n"
  },
  {
    "path": "examples/view-transitions/src/index.css",
    "content": ".is-loading {\n  position: absolute;\n  top: 0;\n  right: 0;\n  background-color: lightgrey;\n  padding: 0.5rem;\n}\n\n/* Magicawesomeness */\n@keyframes fade-in {\n  from {\n    opacity: 0;\n  }\n}\n\n@keyframes fade-out {\n  to {\n    opacity: 0;\n  }\n}\n\n@keyframes slide-from-right {\n  from {\n    transform: translateX(500px);\n  }\n}\n\n@keyframes slide-to-left {\n  to {\n    transform: translateX(-500px);\n  }\n}\n\n.content {\n  view-transition-name: content;\n}\n\n::view-transition-old(content) {\n  animation:\n    500ms cubic-bezier(0.4, 0, 1, 1) both fade-out,\n    500ms cubic-bezier(0.4, 0, 0.2, 1) both slide-to-left;\n}\n\n::view-transition-new(content) {\n  animation:\n    500ms cubic-bezier(0, 0, 0.2, 1) 90ms both fade-in,\n    500ms cubic-bezier(0.4, 0, 0.2, 1) both slide-from-right;\n}\n\n.image-list > div {\n  display: grid;\n  width: 100%;\n  grid-template-columns: repeat(4, 1fr);\n  column-gap: 10px;\n}\n\n.image-list img {\n  max-width: 100%;\n  contain: layout;\n}\n\n.image-list a.transitioning img {\n  view-transition-name: image-expand;\n}\n\n.image-detail img {\n  max-width: 100%;\n  contain: layout;\n}\n\n.image-detail img {\n  view-transition-name: image-expand;\n}\n\n::view-transition-old(image-expand):not(:only-child),\n::view-transition-new(image-expand):not(:only-child) {\n  animation: none;\n  mix-blend-mode: normal;\n}\n\n.image-list p {\n  width: fit-content;\n}\n\n.image-list a.transitioning p {\n  view-transition-name: image-title;\n}\n\n.image-detail h1 {\n  width: fit-content;\n}\n\n.image-detail h1 {\n  view-transition-name: image-title;\n}\n"
  },
  {
    "path": "examples/view-transitions/src/main.tsx",
    "content": "import * as React from \"react\";\nimport * as ReactDOMClient from \"react-dom/client\";\nimport {\n  Await,\n  createBrowserRouter,\n  defer,\n  Form,\n  json,\n  Link,\n  NavLink,\n  Outlet,\n  RouterProvider,\n  unstable_useViewTransitionState,\n  useActionData,\n  useLoaderData,\n  useLocation,\n  useNavigate,\n  useNavigation,\n  useParams,\n  useSubmit,\n} from \"react-router-dom\";\nimport \"./index.css\";\n\nconst images = [\n  \"https://remix.run/blog-images/headers/the-future-is-now.jpg\",\n  \"https://remix.run/blog-images/headers/waterfall.jpg\",\n  \"https://remix.run/blog-images/headers/webpack.png\",\n  \"https://remix.run/blog-images/headers/remix-conf.jpg\",\n];\n\nconst router = createBrowserRouter([\n  {\n    path: \"/\",\n    Component() {\n      let navigation = useNavigation();\n\n      return (\n        <>\n          {navigation.state !== \"idle\" ? (\n            <div className=\"is-loading\">Loading...</div>\n          ) : null}\n          <Nav />\n          <div className=\"content\">\n            <Outlet />\n          </div>\n        </>\n      );\n    },\n    children: [\n      {\n        index: true,\n        Component() {\n          React.useEffect(() => {\n            document.title = \"Home\";\n          }, []);\n          return <h1>Home</h1>;\n        },\n      },\n      {\n        path: \"loader\",\n        async loader() {\n          await new Promise((r) => setTimeout(r, 1000));\n          return json({ message: \"LOADER DATA\" });\n        },\n        Component() {\n          let data = useLoaderData() as { message: string };\n          React.useEffect(() => {\n            document.title = \"Loader\";\n          }, []);\n          return (\n            <>\n              <h1>Loader Page</h1>\n              <p>Loader Data: {data.message}</p>\n            </>\n          );\n        },\n      },\n      {\n        path: \"action\",\n        async action() {\n          await new Promise((r) => setTimeout(r, 1000));\n          return json({ message: \"ACTION DATA\" });\n        },\n        Component() {\n          let data = useActionData() as { message: string } | undefined;\n          React.useEffect(() => {\n            document.title = \"Action\";\n          }, []);\n          return (\n            <>\n              <h1>Action Page</h1>\n              <p>Action Data: {data?.message}</p>\n            </>\n          );\n        },\n      },\n      {\n        path: \"defer\",\n        async loader({ request }) {\n          return defer({\n            critical: \"CRITICAL PATH DATA\",\n            lazy: new Promise((r) => setTimeout(() => r(\"LAZY DATA\"), 1000)),\n          });\n        },\n        Component() {\n          let data = useLoaderData() as {\n            critical: string;\n            lazy: Promise<string>;\n          };\n          React.useEffect(() => {\n            document.title = \"Defer\";\n          }, []);\n          return (\n            <>\n              <h1>Defer</h1>\n              <p>Critical Data: {data.critical}</p>\n              <React.Suspense\n                fallback={<p>Suspense boundary in the route...</p>}\n                key={useLocation().key}\n              >\n                <Await resolve={data.lazy}>\n                  {(value) => <p>Lazy Data: {value}</p>}\n                </Await>\n              </React.Suspense>\n            </>\n          );\n        },\n      },\n      {\n        path: \"defer-no-boundary\",\n        async loader({ request }) {\n          let value = new URL(request.url).searchParams.get(\"value\") || \"\";\n          return defer({\n            value,\n            critical: \"CRITICAL PATH DATA - NO BOUNDARY \" + value,\n            lazy: new Promise((r) =>\n              setTimeout(() => r(\"LAZY DATA - NO BOUNDARY \" + value), 1000),\n            ),\n          });\n        },\n        Component() {\n          let data = useLoaderData() as {\n            value: string;\n            data: string;\n            critical: string;\n            lazy: Promise<string>;\n          };\n          React.useEffect(() => {\n            document.title = \"Defer (No Boundary)\";\n          }, []);\n          return (\n            <>\n              <h1>Defer No Boundary {data.value}</h1>\n              <p>Critical Data: {data.critical}</p>\n              <div>\n                <Await resolve={data.lazy}>\n                  {(value) => <p>Lazy Data: {value}</p>}\n                </Await>\n              </div>\n            </>\n          );\n        },\n      },\n      {\n        path: \"images\",\n        Component() {\n          React.useEffect(() => {\n            document.title = \"Images\";\n          }, []);\n          return (\n            <div className=\"image-list\">\n              <h1>Image List</h1>\n              <div>\n                {images.map((src, idx) => (\n                  // Adds 'transitioning' class to the <a> during the transition\n                  <NavLink\n                    key={src}\n                    to={`/images/${idx}`}\n                    unstable_viewTransition\n                  >\n                    <p>Image Number {idx}</p>\n                    <img src={src} alt={`Img ${idx}`} />\n                  </NavLink>\n\n                  // Render prop approach similar to isActive/isPending\n                  // <NavLink\n                  //   key={src}\n                  //   to={`/images/${idx}`}\n                  //   unstable_viewTransition\n                  // >\n                  //   {({ isTransitioning }) => (\n                  //     <div className={isTransitioning ? \"transitioning\" : \"\"}>\n                  //       <p>Image Number {idx}</p>\n                  //       <img src={src} alt={`Img ${idx}`} />\n                  //     </div>\n                  //   )}\n                  // </NavLink>\n\n                  // Manual hook based approach\n                  // <NavImage key={src} src={src} idx={idx} />\n                ))}\n              </div>\n            </div>\n          );\n        },\n      },\n      {\n        path: \"images/:id\",\n        Component() {\n          let params = useParams();\n          React.useEffect(() => {\n            document.title = \"Image \" + params.id;\n          }, [params.id]);\n          return (\n            <div className={`image-detail`}>\n              <h1>Image Number {params.id}</h1>\n              <img src={images[Number(params.id)]} alt={`${params.id}`} />\n            </div>\n          );\n        },\n      },\n    ],\n  },\n]);\n\n// eslint-disable-next-line @typescript-eslint/no-unused-vars\nfunction NavImage({ src, idx }: { src: string; idx: number }) {\n  let href = `/images/${idx}`;\n  let vt = unstable_useViewTransitionState(href);\n  return (\n    <>\n      <Link to={href} unstable_viewTransition>\n        <p style={{ viewTransitionName: vt ? \"image-title\" : \"\" }}>\n          Image Number {idx}\n        </p>\n        <img\n          src={src}\n          alt={`Img ${idx}`}\n          style={{ viewTransitionName: vt ? \"image-expand\" : \"\" }}\n        />\n      </Link>\n    </>\n  );\n}\n\nconst rootElement = document.getElementById(\"root\") as HTMLElement;\nReactDOMClient.createRoot(rootElement).render(\n  <React.StrictMode>\n    <RouterProvider router={router} />\n  </React.StrictMode>,\n);\n\nfunction Nav() {\n  let navigate = useNavigate();\n  let submit = useSubmit();\n  return (\n    <nav>\n      <ul>\n        <li>\n          <Link to=\"/\" unstable_viewTransition>\n            Home\n          </Link>\n          <ul>\n            <li>\n              The / route has no loader is should be an immediate/synchronous\n              transition\n            </li>\n          </ul>\n        </li>\n        <li>\n          <Link to=\"/loader\" unstable_viewTransition>\n            Loader with delay\n          </Link>{\" \"}\n          <button\n            style={{ display: \"inline-block\" }}\n            onClick={() =>\n              navigate(\"/loader\", { unstable_viewTransition: true })\n            }\n          >\n            via useNavigate\n          </button>\n          <ul>\n            <li>\n              The /loader route has a 1 second loader delay, and updates the DOM\n              synchronously upon completion\n            </li>\n          </ul>\n        </li>\n        <li>\n          <Form\n            method=\"post\"\n            action=\"/action\"\n            style={{ display: \"inline-block\" }}\n            unstable_viewTransition\n          >\n            <button type=\"submit\" style={{ display: \"inline-block\" }}>\n              Action with delay\n            </button>\n          </Form>{\" \"}\n          <button\n            style={{ display: \"inline-block\" }}\n            onClick={() =>\n              submit(\n                {},\n                {\n                  method: \"post\",\n                  action: \"/action\",\n                  unstable_viewTransition: true,\n                },\n              )\n            }\n          >\n            via useSubmit\n          </button>\n          <ul>\n            <li>\n              The /action route has a 1 second action delay, and updates the DOM\n              synchronously upon completion\n            </li>\n          </ul>\n        </li>\n        <li>\n          <Link to=\"/images\" unstable_viewTransition>\n            Image Gallery Example\n          </Link>\n        </li>\n        <li>\n          <Link to={`/defer`} unstable_viewTransition>\n            Deferred Data\n          </Link>\n          <ul>\n            <li>\n              The /defer route has 1s defer call that suspends and has it's own\n              Suspense boundary\n            </li>\n          </ul>\n        </li>\n        <li>\n          <Link to=\"/defer-no-boundary\" unstable_viewTransition>\n            Deferred Data (without boundary)\n          </Link>\n          <ul>\n            <li>\n              The /defer-no-boundary route has a 1s defer that suspends without\n              a Suspense boundary in the destination route. This relies on\n              React.startTransition to \"freeze\" the current UI until the\n              deferred data resolves\n            </li>\n          </ul>\n        </li>\n      </ul>\n    </nav>\n  );\n}\n"
  },
  {
    "path": "examples/view-transitions/src/vite-env.d.ts",
    "content": "/// <reference types=\"vite/client\" />\n"
  },
  {
    "path": "examples/view-transitions/tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"baseUrl\": \".\",\n    \"target\": \"ESNext\",\n    \"lib\": [\"DOM\", \"DOM.Iterable\", \"ESNext\"],\n    \"allowJs\": false,\n    \"skipLibCheck\": true,\n    \"esModuleInterop\": false,\n    \"allowSyntheticDefaultImports\": true,\n    \"strict\": true,\n    \"module\": \"ESNext\",\n    \"moduleResolution\": \"Node\",\n    \"resolveJsonModule\": true,\n    \"verbatimModuleSyntax\": true,\n    \"noEmit\": true,\n    \"jsx\": \"react-jsx\",\n    \"importsNotUsedAsValues\": \"error\"\n  },\n  \"include\": [\"./src\"]\n}\n"
  },
  {
    "path": "examples/view-transitions/vite.config.ts",
    "content": "import * as path from \"path\";\nimport { defineConfig } from \"vite\";\nimport react from \"@vitejs/plugin-react\";\nimport rollupReplace from \"@rollup/plugin-replace\";\n\n// https://vitejs.dev/config/\nexport default defineConfig({\n  server: {\n    port: 3000,\n  },\n  plugins: [\n    rollupReplace({\n      preventAssignment: true,\n      values: {\n        \"process.env.NODE_ENV\": JSON.stringify(\"development\"),\n      },\n    }),\n    react(),\n  ],\n  resolve: process.env.USE_SOURCE\n    ? {\n        alias: {\n          \"react-router\": path.resolve(\n            __dirname,\n            \"../../packages/react-router/index.ts\",\n          ),\n          \"react-router-dom\": path.resolve(\n            __dirname,\n            \"../../packages/react-router-dom/index.tsx\",\n          ),\n        },\n      }\n    : {},\n});\n"
  },
  {
    "path": "integration/CHANGELOG.md",
    "content": "# integration-tests\n\n## 0.0.0\n\n### Minor Changes\n\n- Unstable Vite support for Node-based Remix apps ([#7590](https://github.com/remix-run/remix/pull/7590))\n  - `remix build` 👉 `vite build && vite build --ssr`\n  - `remix dev` 👉 `vite dev`\n\n  Other runtimes (e.g. Deno, Cloudflare) not yet supported.\n  Custom server (e.g. Express) not yet supported.\n\n  See \"Future > Vite\" in the Remix Docs for details.\n"
  },
  {
    "path": "integration/abort-signal-test.ts",
    "content": "import { test } from \"@playwright/test\";\n\nimport { PlaywrightFixture } from \"./helpers/playwright-fixture.js\";\nimport type { Fixture, AppFixture } from \"./helpers/create-fixture.js\";\nimport {\n  createAppFixture,\n  createFixture,\n  js,\n} from \"./helpers/create-fixture.js\";\n\nlet fixture: Fixture;\nlet appFixture: AppFixture;\n\ntest.beforeAll(async () => {\n  fixture = await createFixture({\n    files: {\n      \"app/routes/_index.tsx\": js`\n        import { useActionData, useLoaderData, Form } from \"react-router\";\n\n        export async function action ({ request }) {\n          // New event loop causes express request to close\n          await new Promise(r => setTimeout(r, 0));\n          return { aborted: request.signal.aborted };\n        }\n\n        export function loader({ request }) {\n          return { aborted: request.signal.aborted };\n        }\n\n        export default function Index() {\n          let actionData = useActionData();\n          let data = useLoaderData();\n          return (\n            <div>\n              <p className=\"action\">{actionData ? String(actionData.aborted) : \"empty\"}</p>\n              <p className=\"loader\">{String(data.aborted)}</p>\n              <Form method=\"post\">\n                <button type=\"submit\">Submit</button>\n              </Form>\n            </div>\n          )\n        }\n      `,\n    },\n  });\n\n  // This creates an interactive app using playwright.\n  appFixture = await createAppFixture(fixture);\n});\n\ntest.afterAll(() => {\n  appFixture.close();\n});\n\ntest(\"should not abort the request in a new event loop\", async ({ page }) => {\n  let app = new PlaywrightFixture(appFixture, page);\n  await app.goto(\"/\");\n  await page.waitForSelector(`.action:has-text(\"empty\")`);\n  await page.waitForSelector(`.loader:has-text(\"false\")`);\n\n  await app.clickElement('button[type=\"submit\"]');\n\n  await page.waitForSelector(`.action:has-text(\"false\")`);\n  await page.waitForSelector(`.loader:has-text(\"false\")`);\n});\n"
  },
  {
    "path": "integration/action-test.ts",
    "content": "import { test, expect } from \"@playwright/test\";\n\nimport {\n  createFixture,\n  createAppFixture,\n  js,\n} from \"./helpers/create-fixture.js\";\nimport type { Fixture, AppFixture } from \"./helpers/create-fixture.js\";\nimport { PlaywrightFixture, selectHtml } from \"./helpers/playwright-fixture.js\";\nimport { type TemplateName } from \"./helpers/vite.js\";\n\nconst templateNames = [\n  \"vite-5-template\",\n  \"rsc-vite-framework\",\n] as const satisfies TemplateName[];\n\ntest.describe(\"actions\", () => {\n  for (const templateName of templateNames) {\n    test.describe(`template: ${templateName}`, () => {\n      let fixture: Fixture;\n      let appFixture: AppFixture;\n\n      let FIELD_NAME = \"message\";\n      let WAITING_VALUE = \"Waiting...\";\n      let SUBMITTED_VALUE = \"Submission\";\n      let THROWS_REDIRECT = \"redirect-throw\";\n      let REDIRECT_TARGET = \"page\";\n      let PAGE_TEXT = \"PAGE_TEXT\";\n\n      test.beforeAll(async () => {\n        fixture = await createFixture({\n          templateName,\n          files: {\n            \"app/routes/urlencoded.tsx\": js`\n              import { Form, useActionData } from \"react-router\";\n\n              export let action = async ({ request }) => {\n                let formData = await request.formData();\n                return formData.get(\"${FIELD_NAME}\");\n              };\n\n              export default function Actions() {\n                let data = useActionData()\n\n                return (\n                  <Form method=\"post\" id=\"form\">\n                    <p id=\"text\">\n                      {data ? <span id=\"action-text\">{data}</span> : \"${WAITING_VALUE}\"}\n                    </p>\n                    <p>\n                      <input type=\"text\" defaultValue=\"${SUBMITTED_VALUE}\" name=\"${FIELD_NAME}\" />\n                      <button type=\"submit\" id=\"submit\">Go</button>\n                    </p>\n                  </Form>\n                );\n              }\n            `,\n\n            \"app/routes/request-text.tsx\": js`\n              import { Form, useActionData } from \"react-router\";\n\n              export let action = async ({ request }) => {\n                let text = await request.text();\n                return text;\n              };\n\n              export default function Actions() {\n                let data = useActionData()\n\n                return (\n                  <Form method=\"post\" id=\"form\">\n                    <p id=\"text\">\n                      {data ? <span id=\"action-text\">{data}</span> : \"${WAITING_VALUE}\"}\n                    </p>\n                    <p>\n                      <input name=\"a\" defaultValue=\"1\" />\n                      <input name=\"b\" defaultValue=\"2\" />\n                      <button type=\"submit\" id=\"submit\">Go</button>\n                    </p>\n                  </Form>\n                );\n              }\n            `,\n\n            [`app/routes/${THROWS_REDIRECT}.jsx`]: js`\n              import { redirect, Form } from \"react-router\";\n\n              export function action() {\n                throw redirect(\"/${REDIRECT_TARGET}\")\n              }\n\n              export default function () {\n                return (\n                  <Form method=\"post\">\n                    <button type=\"submit\">Go</button>\n                  </Form>\n                )\n              }\n            `,\n\n            [`app/routes/${REDIRECT_TARGET}.jsx`]: js`\n              export default function () {\n                return <div id=\"${REDIRECT_TARGET}\">${PAGE_TEXT}</div>\n              }\n            `,\n\n            \"app/routes/no-action.tsx\": js`\n              import { Form } from \"react-router\";\n\n              export default function Component() {\n                return (\n                  <Form method=\"post\">\n                    <button type=\"submit\">Submit without action</button>\n                  </Form>\n                );\n              }\n            `,\n          },\n        });\n\n        appFixture = await createAppFixture(fixture);\n      });\n\n      test.afterAll(() => {\n        appFixture.close();\n      });\n\n      let logs: string[] = [];\n\n      test.beforeEach(({ page }) => {\n        page.on(\"console\", (msg) => {\n          logs.push(msg.text());\n        });\n      });\n\n      test.afterEach(() => {\n        expect(logs).toHaveLength(0);\n      });\n\n      test(\"is not called on document GET requests\", async () => {\n        let res = await fixture.requestDocument(\"/urlencoded\");\n        let html = await selectHtml(await res.text(), \"#text\");\n        expect(html).toMatch(WAITING_VALUE);\n      });\n\n      test(\"is called on document POST requests\", async () => {\n        let FIELD_VALUE = \"cheeseburger\";\n\n        let params = new URLSearchParams();\n        params.append(FIELD_NAME, FIELD_VALUE);\n\n        let res = await fixture.postDocument(\"/urlencoded\", params);\n\n        let html = await selectHtml(await res.text(), \"#text\");\n        expect(html).toMatch(FIELD_VALUE);\n      });\n\n      test(\"is called on script transition POST requests\", async ({ page }) => {\n        let app = new PlaywrightFixture(appFixture, page);\n        await app.goto(`/urlencoded`);\n        await page.waitForSelector(`#text:has-text(\"${WAITING_VALUE}\")`);\n\n        await page.click(\"button[type=submit]\");\n        await page.waitForSelector(\"#action-text\");\n        await page.waitForSelector(`#text:has-text(\"${SUBMITTED_VALUE}\")`);\n      });\n\n      test(\"throws a 405 when no action exists\", async ({ page }) => {\n        let app = new PlaywrightFixture(appFixture, page);\n        await app.goto(`/no-action`);\n        await page.click(\"button[type=submit]\");\n        await page.waitForSelector(`h1:has-text(\"405 Method Not Allowed\")`);\n        expect(logs.length).toBe(2);\n        expect(logs[0]).toMatch(\n          'Route \"routes/no-action\" does not have an action',\n        );\n        // logs[1] is the raw ErrorResponse instance from the boundary but playwright\n        // seems to just log the name of the constructor, which in the minified code\n        // is meaningless so we don't bother asserting\n\n        // The rest of the tests in this suite assert no logs, so clear this out to\n        // avoid failures in afterEach\n        logs = [];\n      });\n\n      test(\"properly encodes form data for request.text() usage\", async ({\n        page,\n      }) => {\n        let app = new PlaywrightFixture(appFixture, page);\n        await app.goto(`/request-text`);\n        await page.waitForSelector(`#text:has-text(\"${WAITING_VALUE}\")`);\n\n        await page.click(\"button[type=submit]\");\n        await page.waitForSelector(\"#action-text\");\n        expect(await app.getHtml(\"#action-text\")).toBe(\n          '<span id=\"action-text\">a=1&amp;b=2</span>',\n        );\n      });\n\n      test(\"redirects a thrown response on document requests\", async () => {\n        let params = new URLSearchParams();\n        let res = await fixture.postDocument(`/${THROWS_REDIRECT}`, params);\n        expect(res.status).toBe(302);\n        expect(res.headers.get(\"Location\")).toBe(`/${REDIRECT_TARGET}`);\n      });\n\n      test(\"redirects a thrown response on script transitions\", async ({\n        page,\n      }) => {\n        let app = new PlaywrightFixture(appFixture, page);\n        await app.goto(`/${THROWS_REDIRECT}`);\n        let responses = app.collectSingleFetchResponses();\n        await app.clickSubmitButton(`/${THROWS_REDIRECT}`);\n\n        await page.waitForSelector(`#${REDIRECT_TARGET}`);\n\n        // In RSC, every route implicitly has a loader, so we get an extra\n        // response for the page we've redirected to. To keep the rest of the\n        // test RSC-agnostic, we drop the last response.\n        if (templateName.includes(\"rsc\")) {\n          responses = responses.slice(0, -1);\n        }\n\n        expect(responses).toHaveLength(1);\n        expect(responses[0].status()).toBe(202);\n\n        expect(new URL(page.url()).pathname).toBe(`/${REDIRECT_TARGET}`);\n        expect(await app.getHtml()).toMatch(PAGE_TEXT);\n      });\n    });\n  }\n});\n"
  },
  {
    "path": "integration/assets/toupload.txt",
    "content": "Hello, World!"
  },
  {
    "path": "integration/assets/touploadtoobig.txt",
    "content": "Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!"
  },
  {
    "path": "integration/blocking-test.ts",
    "content": "import { test, expect } from \"@playwright/test\";\n\nimport type { AppFixture, Fixture } from \"./helpers/create-fixture.js\";\nimport {\n  createFixture,\n  js,\n  createAppFixture,\n} from \"./helpers/create-fixture.js\";\nimport { PlaywrightFixture } from \"./helpers/playwright-fixture.js\";\n\nlet fixture: Fixture;\nlet appFixture: AppFixture;\n\ntest.afterAll(() => appFixture.close());\n\ntest(\"handles synchronous proceeding correctly\", async ({ page }) => {\n  fixture = await createFixture({\n    files: {\n      \"app/routes/_index.tsx\": js`\n        import { Link } from \"react-router\";\n        export default function Component() {\n          return (\n            <div>\n              <h1 id=\"index\">Index</h1>\n              <Link to=\"/a\">/a</Link>\n            </div>\n          )\n        }\n      `,\n      \"app/routes/a.tsx\": js`\n        import { Link } from \"react-router\";\n        export default function Component() {\n          return (\n            <div>\n              <h1 id=\"a\">A</h1>\n              <Link to=\"/b\">/b</Link>\n            </div>\n          )\n        }\n      `,\n      \"app/routes/b.tsx\": js`\n      import * as React from \"react\";\n      import { Form, useBlocker } from \"react-router\";\n        export default function Component() {\n          return (\n            <div>\n              <h1 id=\"b\">B</h1>\n              <ImportantForm />\n            </div>\n          )\n        }\n        function ImportantForm() {\n          let [value, setValue] = React.useState(\"\");\n          let shouldBlock = React.useCallback<BlockerFunction>(\n            ({ currentLocation, nextLocation }) =>\n              value !== \"\" && currentLocation.pathname !== nextLocation.pathname,\n            [value]\n          );\n          let blocker = useBlocker(shouldBlock);\n          // Reset the blocker if the user cleans the form\n          React.useEffect(() => {\n            if (blocker.state === \"blocked\") {\n              blocker.proceed();\n            }\n          }, [blocker]);\n          return (\n            <>\n              <p>\n                Is the form dirty?{\" \"}\n                {value !== \"\" ? (\n                  <span style={{ color: \"red\" }}>Yes</span>\n                ) : (\n                  <span style={{ color: \"green\" }}>No</span>\n                )}\n              </p>\n              <Form method=\"post\">\n                <label>\n                  Enter some important data:\n                  <input\n                    name=\"data\"\n                    value={value}\n                    onChange={(e) => setValue(e.target.value)}\n                  />\n                </label>\n                <button type=\"submit\">Save</button>\n              </Form>\n            </>\n          );\n        }\n      `,\n    },\n  });\n\n  // This creates an interactive app using puppeteer.\n  appFixture = await createAppFixture(fixture);\n\n  let app = new PlaywrightFixture(appFixture, page);\n\n  await app.goto(\"/\");\n  await app.clickLink(\"/a\");\n  await page.waitForSelector(\"#a\");\n  await app.clickLink(\"/b\");\n  await page.waitForSelector(\"#b\");\n  await page.getByLabel(\"Enter some important data:\").fill(\"Hello Remix!\");\n\n  // Going back should:\n  // - block\n  // - immediately call blocker.proceed() once we enter the blocked state\n  // - and land back one history entry (/a)\n  await page.goBack();\n  await page.waitForSelector(\"#a\");\n  expect(await app.getHtml()).toContain(\"A\");\n});\n"
  },
  {
    "path": "integration/browser-entry-test.ts",
    "content": "import { test, expect } from \"@playwright/test\";\n\nimport {\n  createFixture,\n  js,\n  createAppFixture,\n} from \"./helpers/create-fixture.js\";\nimport { PlaywrightFixture } from \"./helpers/playwright-fixture.js\";\n\ntest(\n  \"expect to be able to browse backward out of a remix app, then forward \" +\n    \"twice in history and have pages render correctly\",\n  async ({ page, browserName }) => {\n    test.skip(\n      browserName === \"firefox\",\n      \"FireFox doesn't support browsing to an empty page (aka about:blank)\",\n    );\n\n    let fixture = await createFixture({\n      files: {\n        \"app/routes/_index.tsx\": js`\n          import { Link } from \"react-router\";\n\n          export default function Index() {\n            return (\n              <div>\n                <div id=\"pizza\">pizza</div>\n                <Link to=\"/burgers\">burger link</Link>\n              </div>\n            )\n          }\n        `,\n\n        \"app/routes/burgers.tsx\": js`\n          export default function Index() {\n            return <div id=\"cheeseburger\">cheeseburger</div>;\n          }\n        `,\n      },\n    });\n\n    // This creates an interactive app using puppeteer.\n    let appFixture = await createAppFixture(fixture);\n\n    let app = new PlaywrightFixture(appFixture, page);\n\n    // Slow down the entry chunk on the second load so the bug surfaces\n    let isSecondLoad = false;\n    await page.route(/entry/, async (route) => {\n      if (isSecondLoad) {\n        await new Promise((r) => setTimeout(r, 1000));\n      }\n      route.continue();\n    });\n\n    // This sets up the Remix modules cache in memory, priming the error case.\n    await app.goto(\"/\");\n    await app.clickLink(\"/burgers\");\n    expect(await page.content()).toContain(\"cheeseburger\");\n    await page.goBack();\n    await page.waitForSelector(\"#pizza\");\n    expect(await app.getHtml()).toContain(\"pizza\");\n\n    // Takes the browser out of the Remix app\n    await page.goBack();\n    expect(page.url()).toContain(\"about:blank\");\n\n    // Forward to / and immediately again to /burgers.  This will trigger the\n    // error since we'll load __routeModules for / but then try to hydrate /burgers\n    isSecondLoad = true;\n    await page.goForward();\n    await page.goForward();\n    await page.waitForSelector(\"#cheeseburger\");\n\n    // If we resolve the error, we should hard reload and eventually\n    // successfully render /burgers\n    await page.waitForSelector(\"#cheeseburger\");\n    expect(await app.getHtml()).toContain(\"cheeseburger\");\n\n    appFixture.close();\n  },\n);\n\ntest(\"allows users to pass a client side context to HydratedRouter\", async ({\n  page,\n}) => {\n  let fixture = await createFixture({\n    files: {\n      \"app/entry.client.tsx\": js`\n        import { createContext, RouterContextProvider } from \"react-router\";\n        import { HydratedRouter } from \"react-router/dom\";\n        import { startTransition, StrictMode } from \"react\";\n        import { hydrateRoot } from \"react-dom/client\";\n\n        export const myContext = new createContext('foo');\n\n        startTransition(() => {\n          hydrateRoot(\n            document,\n            <StrictMode>\n              <HydratedRouter\n                getContext={() => {\n                  return new RouterContextProvider([\n                    [myContext, 'bar']\n                  ]);\n                }}\n              />\n            </StrictMode>\n          );\n        });\n      `,\n      \"app/routes/_index.tsx\": js`\n        import { myContext } from \"../entry.client\";\n\n        export function clientLoader({ context }) {\n          return context.get(myContext);\n        }\n        export default function Index({ loaderData }) {\n          return <h1>Hello, {loaderData}</h1>\n        }\n      `,\n    },\n  });\n\n  let appFixture = await createAppFixture(fixture);\n  let app = new PlaywrightFixture(appFixture, page);\n  await app.goto(\"/\", true);\n  expect(await app.getHtml()).toContain(\"Hello, bar\");\n\n  appFixture.close();\n});\n\ntest(\"allows users to pass an onError function to HydratedRouter\", async ({\n  page,\n  browserName,\n}) => {\n  let fixture = await createFixture({\n    files: {\n      \"app/entry.client.tsx\": js`\n        import { HydratedRouter } from \"react-router/dom\";\n        import { startTransition, StrictMode } from \"react\";\n        import { hydrateRoot } from \"react-dom/client\";\n\n        startTransition(() => {\n          hydrateRoot(\n            document,\n            <StrictMode>\n              <HydratedRouter\n                onError={(error, errorInfo) => {\n                  console.log(error.message, JSON.stringify(errorInfo))\n                }}\n              />\n            </StrictMode>\n          );\n        });\n      `,\n      \"app/routes/_index.tsx\": js`\n        import { Link } from \"react-router\";\n        export default function Index() {\n          return <Link to=\"/page\">Go to Page</Link>;\n        }\n      `,\n      \"app/routes/page.tsx\": js`\n        export default function Page() {\n          throw new Error(\"Render error\");\n        }\n        export function ErrorBoundary({ error }) {\n          return <h1 data-error>Error: {error.message}</h1>\n        }\n      `,\n    },\n  });\n\n  let logs: string[] = [];\n  page.on(\"console\", (msg) => logs.push(msg.text()));\n\n  let appFixture = await createAppFixture(fixture);\n  let app = new PlaywrightFixture(appFixture, page);\n\n  await app.goto(\"/\", true);\n  await page.click('a[href=\"/page\"]');\n  await page.waitForSelector(\"[data-error]\");\n\n  expect(await app.getHtml()).toContain(\"Error: Render error\");\n  expect(logs.length).toBe(2);\n  // First one is react logging the error\n  if (browserName === \"firefox\") {\n    expect(logs[0]).toContain(\"Error\");\n  } else {\n    expect(logs[0]).toContain(\"Error: Render error\");\n  }\n  expect(logs[0]).not.toContain(\"componentStack\");\n  // Second one is ours\n  expect(logs[1]).toContain(\"Render error\");\n  expect(logs[1]).toContain('\"componentStack\":');\n\n  appFixture.close();\n});\n\ntest(\"allows users to instrument the client side router via HydratedRouter\", async ({\n  page,\n}) => {\n  let fixture = await createFixture({\n    files: {\n      \"app/entry.client.tsx\": js`\n        import { HydratedRouter } from \"react-router/dom\";\n        import { startTransition, StrictMode } from \"react\";\n        import { hydrateRoot } from \"react-dom/client\";\n\n        startTransition(() => {\n          hydrateRoot(\n            document,\n            <StrictMode>\n              <HydratedRouter\n                unstable_instrumentations={[{\n                  router(router) {\n                    router.instrument({\n                      async navigate(impl, info) {\n                        console.log(\"start navigate\", JSON.stringify(Object.entries(info).sort()));\n                        await impl();\n                        console.log(\"end navigate\", JSON.stringify(Object.entries(info).sort()));\n                      },\n                      async fetch(impl, info) {\n                        console.log(\"start fetch\", JSON.stringify(Object.entries(info).sort()));\n                        await impl();\n                        console.log(\"end fetch\", JSON.stringify(Object.entries(info).sort()));\n                      }\n                    })\n                  },\n                  route(route) {\n                    route.instrument({\n                      async loader(impl, info) {\n                        let path = new URL(info.request.url).pathname;\n                        console.log(\"start loader\", route.id, path);\n                        await impl();\n                        console.log(\"end loader\", route.id, path);\n                      },\n                      async action(impl, info) {\n                        let path = new URL(info.request.url).pathname;\n                        console.log(\"start action\", route.id, path);\n                        await impl();\n                        console.log(\"end action\", route.id, path);\n                      }\n                    })\n                  }\n                }]}\n              />\n            </StrictMode>\n          );\n        });\n      `,\n      \"app/routes/_index.tsx\": js`\n        import { Link } from \"react-router\";\n        export default function Index() {\n          return <Link to=\"/page\">Go to Page</Link>;\n        }\n      `,\n      \"app/routes/page.tsx\": js`\n        import { useFetcher } from \"react-router\";\n        export function loader() {\n          return { data: \"hello world\" };\n        }\n        export function action() {\n          return \"OK\";\n        }\n        export default function Page({ loaderData }) {\n          let fetcher = useFetcher({ key: 'a' });\n          return (\n            <>\n              <h1 data-page>{loaderData.data}</h1>;\n              <button data-fetch onClick={() => fetcher.submit({ key: 'value' }, {\n                method: 'post',\n                action: \"/page\"\n              })}>\n                Fetch\n              </button>\n              {fetcher.data ? <pre data-fetcher-data>{fetcher.data}</pre> : null}\n            </>\n          );\n        }\n      `,\n    },\n  });\n\n  let logs: string[] = [];\n  page.on(\"console\", (msg) => logs.push(msg.text()));\n\n  let appFixture = await createAppFixture(fixture);\n  let app = new PlaywrightFixture(appFixture, page);\n\n  await app.goto(\"/\", true);\n  await page.click('a[href=\"/page\"]');\n  await page.waitForSelector(\"[data-page]\");\n\n  expect(await app.getHtml()).toContain(\"hello world\");\n  expect(logs).toEqual([\n    'start navigate [[\"currentUrl\",\"/\"],[\"to\",\"/page\"]]',\n    \"start loader root /page\",\n    \"start loader routes/page /page\",\n    \"end loader root /page\",\n    \"end loader routes/page /page\",\n    'end navigate [[\"currentUrl\",\"/\"],[\"to\",\"/page\"]]',\n  ]);\n  logs.splice(0);\n\n  await page.click(\"[data-fetch]\");\n  await page.waitForSelector(\"[data-fetcher-data]\");\n  await expect(page.locator(\"[data-fetcher-data]\")).toContainText(\"OK\");\n  expect(logs).toEqual([\n    'start fetch [[\"body\",{\"key\":\"value\"}],[\"currentUrl\",\"/page\"],[\"fetcherKey\",\"a\"],[\"formData\",null],[\"formEncType\",\"application/x-www-form-urlencoded\"],[\"formMethod\",\"post\"],[\"href\",\"/page\"]]',\n    \"start action routes/page /page\",\n    \"end action routes/page /page\",\n    \"start loader root /page\",\n    \"start loader routes/page /page\",\n    \"end loader root /page\",\n    \"end loader routes/page /page\",\n    'end fetch [[\"body\",{\"key\":\"value\"}],[\"currentUrl\",\"/page\"],[\"fetcherKey\",\"a\"],[\"formData\",null],[\"formEncType\",\"application/x-www-form-urlencoded\"],[\"formMethod\",\"post\"],[\"href\",\"/page\"]]',\n  ]);\n\n  appFixture.close();\n});\n"
  },
  {
    "path": "integration/bug-report-test.ts",
    "content": "import { test, expect } from \"@playwright/test\";\n\nimport { PlaywrightFixture } from \"./helpers/playwright-fixture.js\";\nimport type { Fixture, AppFixture } from \"./helpers/create-fixture.js\";\nimport {\n  createAppFixture,\n  createFixture,\n  js,\n} from \"./helpers/create-fixture.js\";\n\nlet fixture: Fixture;\nlet appFixture: AppFixture;\n\n////////////////////////////////////////////////////////////////////////////////\n// 👋 Hola! I'm here to help you write a great bug report pull request.\n//\n// You don't need to fix the bug, this is just to report one.\n//\n// The pull request you are submitting is supposed to fail when created, to let\n// the team see the erroneous behavior, and understand what's going wrong.\n//\n// If you happen to have a fix as well, it will have to be applied in a subsequent\n// commit to this pull request, and your now-succeeding test will have to be moved\n// to the appropriate file.\n//\n// First, make sure to install dependencies and build React Router. From the root of\n// the project, run this:\n//\n//    ```\n//    pnpm install && pnpm build\n//    ```\n//\n// If you have never installed playwright on your system before, you may also need\n// to install a browser engine:\n//\n//    ```\n//    pnpm exec playwright install chromium\n//    ```\n//\n// Now try running this test:\n//\n//    ```\n//    pnpm test:integration bug-report --project chromium\n//    ```\n//\n// You can add `--watch` to the end to have it re-run on file changes:\n//\n//    ```\n//    pnpm test:integration bug-report --project chromium --watch\n//    ```\n////////////////////////////////////////////////////////////////////////////////\n\ntest.beforeEach(async ({ context }) => {\n  await context.route(/\\.data$/, async (route) => {\n    await new Promise((resolve) => setTimeout(resolve, 50));\n    route.continue();\n  });\n});\n\ntest.beforeAll(async () => {\n  fixture = await createFixture({\n    ////////////////////////////////////////////////////////////////////////////\n    // 💿 Next, add files to this object, just like files in a real app,\n    // `createFixture` will make an app and run your tests against it.\n    ////////////////////////////////////////////////////////////////////////////\n    files: {\n      \"app/routes/_index.tsx\": js`\n        import { useLoaderData, Link } from \"react-router\";\n\n        export function loader() {\n          return \"pizza\";\n        }\n\n        export default function Index() {\n          let data = useLoaderData();\n          return (\n            <div>\n              {data}\n              <Link to=\"/burgers\">Other Route</Link>\n            </div>\n          )\n        }\n      `,\n\n      \"app/routes/burgers.tsx\": js`\n        export default function Index() {\n          return <div>cheeseburger</div>;\n        }\n      `,\n    },\n  });\n\n  // This creates an interactive app using playwright.\n  appFixture = await createAppFixture(fixture);\n});\n\ntest.afterAll(() => {\n  appFixture.close();\n});\n\n////////////////////////////////////////////////////////////////////////////////\n// 💿 Almost done, now write your failing test case(s) down here Make sure to\n// add a good description for what you expect React Router to do 👇🏽\n////////////////////////////////////////////////////////////////////////////////\n\ntest(\"[description of what you expect it to do]\", async ({ page }) => {\n  let app = new PlaywrightFixture(appFixture, page);\n  // You can test any request your app might get using `fixture`.\n  let response = await fixture.requestDocument(\"/\");\n  expect(await response.text()).toMatch(\"pizza\");\n\n  // If you need to test interactivity use the `app`\n  await app.goto(\"/\");\n  await app.clickLink(\"/burgers\");\n  await page.waitForSelector(\"text=cheeseburger\");\n\n  // If you're not sure what's going on, you can \"poke\" the app, it'll\n  // automatically open up in your browser for 20 seconds, so be quick!\n  // await app.poke(20);\n\n  // Go check out the other tests to see what else you can do.\n});\n\n////////////////////////////////////////////////////////////////////////////////\n// 💿 Finally, push your changes to your fork of React Router\n// and open a pull request!\n////////////////////////////////////////////////////////////////////////////////\n"
  },
  {
    "path": "integration/catch-boundary-data-test.ts",
    "content": "import { test, expect } from \"@playwright/test\";\n\nimport {\n  createAppFixture,\n  createFixture,\n  js,\n} from \"./helpers/create-fixture.js\";\nimport type { Fixture, AppFixture } from \"./helpers/create-fixture.js\";\nimport { PlaywrightFixture } from \"./helpers/playwright-fixture.js\";\nimport { type TemplateName } from \"./helpers/vite.js\";\n\nconst templateNames = [\n  \"vite-5-template\",\n  \"rsc-vite-framework\",\n] as const satisfies TemplateName[];\n\nlet ROOT_BOUNDARY_TEXT = \"ROOT_TEXT\" as const;\nlet LAYOUT_BOUNDARY_TEXT = \"LAYOUT_BOUNDARY_TEXT\" as const;\nlet OWN_BOUNDARY_TEXT = \"OWN_BOUNDARY_TEXT\" as const;\n\nlet NO_BOUNDARY_LOADER_FILE = \"/no.loader\" as const;\nlet NO_BOUNDARY_LOADER = \"/no/loader\" as const;\n\nlet HAS_BOUNDARY_LAYOUT_NESTED_LOADER_FILE =\n  \"/yes.loader-layout-boundary\" as const;\nlet HAS_BOUNDARY_LAYOUT_NESTED_LOADER = \"/yes/loader-layout-boundary\" as const;\n\nlet HAS_BOUNDARY_NESTED_LOADER_FILE = \"/yes.loader-self-boundary\" as const;\nlet HAS_BOUNDARY_NESTED_LOADER = \"/yes/loader-self-boundary\" as const;\n\nlet ROOT_DATA = \"root data\";\nlet LAYOUT_DATA = \"root data\";\n\ntest.describe(\"ErrorBoundary (thrown responses)\", () => {\n  for (const templateName of templateNames) {\n    let fixture: Fixture;\n    let appFixture: AppFixture;\n\n    test.describe(`template: ${templateName}`, () => {\n      test.beforeEach(async ({ context }) => {\n        await context.route(/.(data|rsc)/, async (route) => {\n          await new Promise((resolve) => setTimeout(resolve, 50));\n          route.continue();\n        });\n      });\n\n      test.beforeAll(async () => {\n        fixture = await createFixture({\n          templateName,\n          files: {\n            \"app/root.tsx\": js`\n              import {\n                Links,\n                Meta,\n                Outlet,\n                Scripts,\n                useLoaderData,\n                useMatches,\n              } from \"react-router\";\n\n              export const loader = () => \"${ROOT_DATA}\";\n\n              export default function Root() {\n                const data = useLoaderData();\n\n                return (\n                  <html lang=\"en\">\n                    <head>\n                      <Meta />\n                      <Links />\n                    </head>\n                    <body>\n                      <div id=\"root-data\">{data}</div>\n                      <Outlet />\n                      <Scripts />\n                    </body>\n                  </html>\n                );\n              }\n\n              export function ErrorBoundary() {\n                let matches = useMatches();\n                let { data } = matches.find(match => match.id === \"root\");\n\n                return (\n                  <html>\n                    <head />\n                    <body>\n                      <div id=\"root-boundary\">${ROOT_BOUNDARY_TEXT}</div>\n                      <div id=\"root-boundary-data\">{data}</div>\n                      <Scripts />\n                    </body>\n                  </html>\n                );\n              }\n            `,\n\n            \"app/routes/_index.tsx\": js`\n              import { Link } from \"react-router\";\n              export default function Index() {\n                return (\n                  <div>\n                    <Link to=\"${NO_BOUNDARY_LOADER}\">${NO_BOUNDARY_LOADER}</Link>\n                    <Link to=\"${HAS_BOUNDARY_LAYOUT_NESTED_LOADER}\">${HAS_BOUNDARY_LAYOUT_NESTED_LOADER}</Link>\n                    <Link to=\"${HAS_BOUNDARY_NESTED_LOADER}\">${HAS_BOUNDARY_NESTED_LOADER}</Link>\n                  </div>\n                );\n              }\n            `,\n\n            [`app/routes${NO_BOUNDARY_LOADER_FILE}.jsx`]: js`\n              export function loader() {\n                throw new Response(\"\", { status: 401 });\n              }\n              export default function Index() {\n                return <div/>;\n              }\n            `,\n\n            [`app/routes${HAS_BOUNDARY_LAYOUT_NESTED_LOADER_FILE}.jsx`]: js`\n              import { useMatches } from \"react-router\";\n              export function loader() {\n                return \"${LAYOUT_DATA}\";\n              }\n              export default function Layout() {\n                return <div/>;\n              }\n              export function ErrorBoundary() {\n                let matches = useMatches();\n                let { data } = matches.find(match => match.id === \"routes${HAS_BOUNDARY_LAYOUT_NESTED_LOADER_FILE}\");\n\n                return (\n                  <div>\n                    <div id=\"layout-boundary\">${LAYOUT_BOUNDARY_TEXT}</div>\n                    <div id=\"layout-boundary-data\">{data}</div>\n                  </div>\n                );\n              }\n            `,\n\n            [`app/routes${HAS_BOUNDARY_LAYOUT_NESTED_LOADER_FILE}._index.jsx`]: js`\n              export function loader() {\n                throw new Response(\"\", { status: 401 });\n              }\n              export default function Index() {\n                return <div/>;\n              }\n            `,\n\n            [`app/routes${HAS_BOUNDARY_NESTED_LOADER_FILE}.jsx`]: js`\n              import { Outlet, useLoaderData } from \"react-router\";\n              export function loader() {\n                return \"${LAYOUT_DATA}\";\n              }\n              export default function Layout() {\n                let data = useLoaderData();\n                return (\n                  <div>\n                    <div id=\"layout-data\">{data}</div>\n                    <Outlet/>\n                  </div>\n                );\n              }\n            `,\n\n            [`app/routes${HAS_BOUNDARY_NESTED_LOADER_FILE}._index.jsx`]: js`\n              export function loader() {\n                throw new Response(\"\", { status: 401 });\n              }\n              export default function Index() {\n                return <div/>;\n              }\n              export function ErrorBoundary() {\n                return (\n                  <div id=\"own-boundary\">${OWN_BOUNDARY_TEXT}</div>\n                );\n              }\n            `,\n          },\n        });\n\n        appFixture = await createAppFixture(fixture);\n      });\n\n      test.afterAll(() => {\n        appFixture.close();\n      });\n\n      test(\"renders root boundary with data available\", async () => {\n        let res = await fixture.requestDocument(NO_BOUNDARY_LOADER);\n        expect(res.status).toBe(401);\n        let html = await res.text();\n        expect(html).toMatch(ROOT_BOUNDARY_TEXT);\n        expect(html).toMatch(ROOT_DATA);\n      });\n\n      test(\"renders root boundary with data available on transition\", async ({\n        page,\n      }) => {\n        let app = new PlaywrightFixture(appFixture, page);\n        await app.goto(\"/\");\n        await app.clickLink(NO_BOUNDARY_LOADER);\n        await page.waitForSelector(\"#root-boundary\");\n        await page.waitForSelector(\n          `#root-boundary-data:has-text(\"${ROOT_DATA}\")`,\n        );\n      });\n\n      test(\"renders layout boundary with data available\", async () => {\n        let res = await fixture.requestDocument(\n          HAS_BOUNDARY_LAYOUT_NESTED_LOADER,\n        );\n        expect(res.status).toBe(401);\n        let html = await res.text();\n        expect(html).toMatch(ROOT_DATA);\n        expect(html).toMatch(LAYOUT_BOUNDARY_TEXT);\n        expect(html).toMatch(LAYOUT_DATA);\n      });\n\n      test(\"renders layout boundary with data available on transition\", async ({\n        page,\n      }) => {\n        let app = new PlaywrightFixture(appFixture, page);\n        await app.goto(\"/\");\n        await app.clickLink(HAS_BOUNDARY_LAYOUT_NESTED_LOADER);\n        await page.waitForSelector(`#root-data:has-text(\"${ROOT_DATA}\")`);\n        await page.waitForSelector(\n          `#layout-boundary:has-text(\"${LAYOUT_BOUNDARY_TEXT}\")`,\n        );\n        await page.waitForSelector(\n          `#layout-boundary-data:has-text(\"${LAYOUT_DATA}\")`,\n        );\n      });\n\n      test(\"renders self boundary with layout data available\", async () => {\n        let res = await fixture.requestDocument(HAS_BOUNDARY_NESTED_LOADER);\n        expect(res.status).toBe(401);\n        let html = await res.text();\n        expect(html).toMatch(ROOT_DATA);\n        expect(html).toMatch(LAYOUT_DATA);\n        expect(html).toMatch(OWN_BOUNDARY_TEXT);\n      });\n\n      test(\"renders self boundary with layout data available on transition\", async ({\n        page,\n      }) => {\n        let app = new PlaywrightFixture(appFixture, page);\n        await app.goto(\"/\");\n        await app.clickLink(HAS_BOUNDARY_NESTED_LOADER);\n        await page.waitForSelector(`#root-data:has-text(\"${ROOT_DATA}\")`);\n        await page.waitForSelector(`#layout-data:has-text(\"${LAYOUT_DATA}\")`);\n        await page.waitForSelector(\n          `#own-boundary:has-text(\"${OWN_BOUNDARY_TEXT}\")`,\n        );\n      });\n    });\n  }\n});\n"
  },
  {
    "path": "integration/catch-boundary-test.ts",
    "content": "import { test, expect } from \"@playwright/test\";\n\nimport {\n  createAppFixture,\n  createFixture,\n  js,\n} from \"./helpers/create-fixture.js\";\nimport type { Fixture, AppFixture } from \"./helpers/create-fixture.js\";\nimport { PlaywrightFixture } from \"./helpers/playwright-fixture.js\";\n\ntest.describe(\"ErrorBoundary (thrown responses)\", () => {\n  let fixture: Fixture;\n  let appFixture: AppFixture;\n  let originalConsoleError: typeof console.error;\n  let originalConsoleWarn: typeof console.warn;\n\n  let ROOT_BOUNDARY_TEXT = \"ROOT_TEXT\" as const;\n  let OWN_BOUNDARY_TEXT = \"OWN_BOUNDARY_TEXT\" as const;\n\n  let HAS_BOUNDARY_LOADER = \"/yes/loader\" as const;\n  let HAS_BOUNDARY_LOADER_FILE = \"/yes.loader\" as const;\n  let HAS_BOUNDARY_ACTION = \"/yes/action\" as const;\n  let HAS_BOUNDARY_ACTION_FILE = \"/yes.action\" as const;\n  let NO_BOUNDARY_ACTION = \"/no/action\" as const;\n  let NO_BOUNDARY_ACTION_FILE = \"/no.action\" as const;\n  let NO_BOUNDARY_LOADER = \"/no/loader\" as const;\n  let NO_BOUNDARY_LOADER_FILE = \"/no.loader\" as const;\n\n  let NOT_FOUND_HREF = \"/not/found\";\n\n  test.beforeAll(async () => {\n    fixture = await createFixture({\n      files: {\n        \"app/root.tsx\": js`\n            import { Links, Meta, Outlet, Scripts, useMatches } from \"react-router\";\n\n            export function loader() {\n              return { data: \"ROOT LOADER\" };\n            }\n\n            export default function Root() {\n              return (\n                <html lang=\"en\">\n                  <head>\n                    <Meta />\n                    <Links />\n                  </head>\n                  <body>\n                    <Outlet />\n                    <Scripts />\n                  </body>\n                </html>\n              );\n            }\n\n            export function ErrorBoundary() {\n              let matches = useMatches()\n              return (\n                <html>\n                  <head />\n                  <body>\n                    <div id=\"root-boundary\">${ROOT_BOUNDARY_TEXT}</div>\n                    <pre id=\"matches\">{JSON.stringify(matches)}</pre>\n                    <Scripts />\n                  </body>\n                </html>\n              )\n            }\n          `,\n\n        \"app/routes/_index.tsx\": js`\n            import { Link, Form } from \"react-router\";\n            export default function() {\n              return (\n                <div>\n                  <Link to=\"${NOT_FOUND_HREF}\">${NOT_FOUND_HREF}</Link>\n\n                  <Form method=\"post\">\n                    <button formAction=\"${HAS_BOUNDARY_ACTION}\" type=\"submit\" />\n                    <button formAction=\"${NO_BOUNDARY_ACTION}\" type=\"submit\" />\n                  </Form>\n\n                  <Link to=\"${HAS_BOUNDARY_LOADER}\">\n                    ${HAS_BOUNDARY_LOADER}\n                  </Link>\n                  <Link to=\"${HAS_BOUNDARY_LOADER}/child\">\n                    ${HAS_BOUNDARY_LOADER}/child\n                  </Link>\n                  <Link to=\"${NO_BOUNDARY_LOADER}\">\n                    ${NO_BOUNDARY_LOADER}\n                  </Link>\n                </div>\n              )\n            }\n          `,\n\n        [`app/routes${HAS_BOUNDARY_ACTION_FILE}.jsx`]: js`\n            import { Form } from \"react-router\";\n            export async function action() {\n              throw new Response(\"\", { status: 401 })\n            }\n            export function ErrorBoundary() {\n              return <p id=\"action-boundary\">${OWN_BOUNDARY_TEXT}</p>\n            }\n            export default function Index() {\n              return (\n                <Form method=\"post\">\n                  <button type=\"submit\" formAction=\"${HAS_BOUNDARY_ACTION}\">\n                    Go\n                  </button>\n                </Form>\n              );\n            }\n          `,\n\n        [`app/routes${NO_BOUNDARY_ACTION_FILE}.jsx`]: js`\n            import { Form } from \"react-router\";\n            export function action() {\n              throw new Response(\"\", { status: 401 })\n            }\n            export default function Index() {\n              return (\n                <Form method=\"post\">\n                  <button type=\"submit\" formAction=\"${NO_BOUNDARY_ACTION}\">\n                    Go\n                  </button>\n                </Form>\n              )\n            }\n          `,\n\n        [`app/routes${HAS_BOUNDARY_LOADER_FILE}.jsx`]: js`\n            import { useRouteError } from \"react-router\";\n            export function loader() {\n              throw new Response(\"\", { status: 401 })\n            }\n            export function ErrorBoundary() {\n              let error = useRouteError();\n              return (\n                <>\n                  <div id=\"boundary-loader\">${OWN_BOUNDARY_TEXT}</div>\n                  <pre id=\"status\">{error.status}</pre>\n                </>\n              );\n            }\n            export default function Index() {\n              return <div/>\n            }\n          `,\n\n        [`app/routes${HAS_BOUNDARY_LOADER_FILE}.child.jsx`]: js`\n            export function loader() {\n              throw new Response(\"\", { status: 404 })\n            }\n            export default function Index() {\n              return <div/>\n            }\n          `,\n\n        [`app/routes${NO_BOUNDARY_LOADER_FILE}.jsx`]: js`\n            export function loader() {\n              throw new Response(\"\", { status: 401 })\n            }\n            export default function Index() {\n              return <div/>\n            }\n          `,\n\n        \"app/routes/action.tsx\": js`\n            import { Outlet, useLoaderData } from \"react-router\";\n\n            export function loader() {\n              return \"PARENT\";\n            }\n\n            export default function () {\n              return (\n                <div>\n                  <p id=\"parent-data\">{useLoaderData()}</p>\n                  <Outlet />\n                </div>\n              )\n            }\n          `,\n\n        \"app/routes/action.child-catch.tsx\": js`\n            import { Form, useLoaderData, useRouteError } from \"react-router\";\n\n            export function loader() {\n              return \"CHILD\";\n            }\n\n            export function action() {\n              throw new Response(\"Caught!\", { status: 400 });\n            }\n\n            export default function () {\n              return (\n                <>\n                  <p id=\"child-data\">{useLoaderData()}</p>\n                  <Form method=\"post\" reloadDocument={true}>\n                    <button type=\"submit\" name=\"key\" value=\"value\">\n                      Submit\n                    </button>\n                  </Form>\n                </>\n              )\n            }\n\n            export function ErrorBoundary() {\n              let error = useRouteError()\n              return <p id=\"child-catch\">{error.status} {error.data}</p>;\n            }\n          `,\n      },\n    });\n\n    appFixture = await createAppFixture(fixture);\n    originalConsoleError = console.error;\n    console.error = () => {};\n    originalConsoleWarn = console.warn;\n    console.warn = () => {};\n  });\n\n  test.afterAll(() => {\n    appFixture.close();\n    console.error = originalConsoleError;\n    console.warn = originalConsoleWarn;\n  });\n\n  test(\"non-matching urls on document requests\", async () => {\n    let oldConsoleError;\n    oldConsoleError = console.error;\n    console.error = () => {};\n\n    let res = await fixture.requestDocument(NOT_FOUND_HREF);\n    expect(res.status).toBe(404);\n    let html = await res.text();\n    expect(html).toMatch(ROOT_BOUNDARY_TEXT);\n\n    // There should be no loader data on the root route\n    let expected = JSON.stringify([\n      { id: \"root\", pathname: \"\", params: {} },\n    ]).replace(/\"/g, \"&quot;\");\n    expect(html).toContain(`<pre id=\"matches\">${expected}</pre>`);\n\n    console.error = oldConsoleError;\n  });\n\n  test(\"non-matching urls on client transitions\", async ({ page }) => {\n    let app = new PlaywrightFixture(appFixture, page);\n    await app.goto(\"/\");\n    await app.clickLink(NOT_FOUND_HREF, { wait: false });\n    await page.waitForSelector(\"#root-boundary\");\n\n    // Root loader data sticks around from previous load\n    let expected = JSON.stringify([\n      {\n        id: \"root\",\n        pathname: \"\",\n        params: {},\n        data: { data: \"ROOT LOADER\" },\n        loaderData: { data: \"ROOT LOADER\" },\n      },\n    ]);\n    expect(await app.getHtml(\"#matches\")).toContain(expected);\n  });\n\n  test(\"own boundary, action, document request\", async () => {\n    let params = new URLSearchParams();\n    let res = await fixture.postDocument(HAS_BOUNDARY_ACTION, params);\n    expect(res.status).toBe(401);\n    expect(await res.text()).toMatch(OWN_BOUNDARY_TEXT);\n  });\n\n  test(\"own boundary, action, client transition from other route\", async ({\n    page,\n  }) => {\n    let app = new PlaywrightFixture(appFixture, page);\n    await app.goto(\"/\");\n    await app.clickSubmitButton(HAS_BOUNDARY_ACTION);\n    await page.waitForSelector(\"#action-boundary\");\n  });\n\n  test(\"own boundary, action, client transition from itself\", async ({\n    page,\n  }) => {\n    let app = new PlaywrightFixture(appFixture, page);\n    await app.goto(HAS_BOUNDARY_ACTION);\n    await app.clickSubmitButton(HAS_BOUNDARY_ACTION);\n    await page.waitForSelector(\"#action-boundary\");\n  });\n\n  test(\"bubbles to parent in action document requests\", async () => {\n    let params = new URLSearchParams();\n    let res = await fixture.postDocument(NO_BOUNDARY_ACTION, params);\n    expect(res.status).toBe(401);\n    expect(await res.text()).toMatch(ROOT_BOUNDARY_TEXT);\n  });\n\n  test(\"bubbles to parent in action script transitions from other routes\", async ({\n    page,\n  }) => {\n    let app = new PlaywrightFixture(appFixture, page);\n    await app.goto(\"/\");\n    await app.clickSubmitButton(NO_BOUNDARY_ACTION);\n    await page.waitForSelector(\"#root-boundary\");\n  });\n\n  test(\"bubbles to parent in action script transitions from self\", async ({\n    page,\n  }) => {\n    let app = new PlaywrightFixture(appFixture, page);\n    await app.goto(NO_BOUNDARY_ACTION);\n    await app.clickSubmitButton(NO_BOUNDARY_ACTION);\n    await page.waitForSelector(\"#root-boundary\");\n  });\n\n  test(\"own boundary, loader, document request\", async () => {\n    let res = await fixture.requestDocument(HAS_BOUNDARY_LOADER);\n    expect(res.status).toBe(401);\n    expect(await res.text()).toMatch(OWN_BOUNDARY_TEXT);\n  });\n\n  test(\"own boundary, loader, client transition\", async ({ page }) => {\n    let app = new PlaywrightFixture(appFixture, page);\n    await app.goto(\"/\");\n    await app.clickLink(HAS_BOUNDARY_LOADER);\n    await page.waitForSelector(\"#boundary-loader\");\n  });\n\n  test(\"bubbles to parent in loader document requests\", async () => {\n    let res = await fixture.requestDocument(NO_BOUNDARY_LOADER);\n    expect(res.status).toBe(401);\n    expect(await res.text()).toMatch(ROOT_BOUNDARY_TEXT);\n  });\n\n  test(\"bubbles to parent in loader transitions from other routes\", async ({\n    page,\n  }) => {\n    let app = new PlaywrightFixture(appFixture, page);\n    await app.goto(\"/\");\n    await app.clickLink(NO_BOUNDARY_LOADER);\n    await page.waitForSelector(\"#root-boundary\");\n  });\n\n  test(\"uses correct catch boundary on server action errors\", async ({\n    page,\n  }) => {\n    let app = new PlaywrightFixture(appFixture, page);\n    await app.goto(`/action/child-catch`);\n    expect(await app.getHtml(\"#parent-data\")).toMatch(\"PARENT\");\n    expect(await app.getHtml(\"#child-data\")).toMatch(\"CHILD\");\n    await page.click(\"button[type=submit]\");\n    await page.waitForSelector(\"#child-catch\");\n    // Preserves parent loader data\n    expect(await app.getHtml(\"#parent-data\")).toMatch(\"PARENT\");\n    expect(await app.getHtml(\"#child-catch\")).toMatch(\"400\");\n    expect(await app.getHtml(\"#child-catch\")).toMatch(\"Caught!\");\n  });\n\n  test(\"prefers parent catch when child loader also bubbles, document request\", async () => {\n    let res = await fixture.requestDocument(`${HAS_BOUNDARY_LOADER}/child`);\n    expect(res.status).toBe(401);\n    let text = await res.text();\n    expect(text).toMatch(OWN_BOUNDARY_TEXT);\n    expect(text).toMatch('<pre id=\"status\">401</pre>');\n  });\n\n  test(\"prefers parent catch when child loader also bubbles, client transition\", async ({\n    page,\n  }) => {\n    let app = new PlaywrightFixture(appFixture, page);\n    await app.goto(\"/\");\n    await app.clickLink(`${HAS_BOUNDARY_LOADER}/child`);\n    await page.waitForSelector(\"#boundary-loader\");\n    expect(await app.getHtml(\"#boundary-loader\")).toMatch(OWN_BOUNDARY_TEXT);\n    expect(await app.getHtml(\"#status\")).toMatch(\"401\");\n  });\n});\n"
  },
  {
    "path": "integration/cli-test.ts",
    "content": "import { spawnSync } from \"node:child_process\";\nimport { existsSync, rmSync } from \"node:fs\";\nimport * as path from \"node:path\";\n\nimport { expect, test } from \"@playwright/test\";\nimport dedent from \"dedent\";\nimport semver from \"semver\";\n\nimport { createProject } from \"./helpers/vite\";\n\nconst nodeBin = process.argv[0];\nconst reactRouterBin = \"node_modules/@react-router/dev/dist/cli/index.js\";\n\nconst run = (command: string[], options: Parameters<typeof spawnSync>[2]) =>\n  spawnSync(nodeBin, [reactRouterBin, ...command], options);\n\nconst helpText = dedent`\n  react-router\n\n    Usage:\n      $ react-router build [projectDir]\n      $ react-router dev [projectDir]\n      $ react-router routes [projectDir]\n\n    Options:\n      --help, -h          Print this help message and exit\n      --version, -v       Print the CLI version and exit\n      --no-color          Disable ANSI colors in console output\n    \\`build\\` Options:\n      --assetsInlineLimit Static asset base64 inline threshold in bytes (default: 4096) (number)\n      --clearScreen       Allow/disable clear screen when logging (boolean)\n      --config, -c        Use specified config file (string)\n      --emptyOutDir       Force empty outDir when it's outside of root (boolean)\n      --logLevel, -l      Info | warn | error | silent (string)\n      --minify            Enable/disable minification, or specify minifier to use (default: \"esbuild\") (boolean | \"terser\" | \"esbuild\")\n      --mode, -m          Set env mode (string)\n      --profile           Start built-in Node.js inspector\n      --sourcemapClient   Output source maps for client build (default: false) (boolean | \"inline\" | \"hidden\")\n      --sourcemapServer   Output source maps for server build (default: false) (boolean | \"inline\" | \"hidden\")\n    \\`dev\\` Options:\n      --clearScreen       Allow/disable clear screen when logging (boolean)\n      --config, -c        Use specified config file (string)\n      --cors              Enable CORS (boolean)\n      --force             Force the optimizer to ignore the cache and re-bundle (boolean)\n      --host              Specify hostname (string)\n      --logLevel, -l      Info | warn | error | silent (string)\n      --mode, -m          Set env mode (string)\n      --open              Open browser on startup (boolean | string)\n      --port              Specify port (number)\n      --profile           Start built-in Node.js inspector\n      --strictPort        Exit if specified port is already in use (boolean)\n    \\`routes\\` Options:\n      --config, -c        Use specified Vite config file (string)\n      --json              Print the routes as JSON\n    \\`reveal\\` Options:\n      --config, -c        Use specified Vite config file (string)\n      --no-typescript     Generate plain JavaScript files\n    \\`typegen\\` Options:\n      --watch             Automatically regenerate types whenever route config (\\`routes.ts\\`) or route modules change\n\n    Build your project:\n\n      $ react-router build\n\n    Run your project locally in development:\n\n      $ react-router dev\n\n    Show all routes in your app:\n\n      $ react-router routes\n      $ react-router routes my-app\n      $ react-router routes --json\n      $ react-router routes --config vite.react-router.config.ts\n\n    Reveal the used entry point:\n\n      $ react-router reveal entry.client\n      $ react-router reveal entry.server\n      $ react-router reveal entry.client --no-typescript\n      $ react-router reveal entry.server --no-typescript\n      $ react-router reveal entry.server --config vite.react-router.config.ts\n\n    Generate types for route modules:\n\n     $ react-router typegen\n     $ react-router typegen --watch\n`;\n\ntest.describe(\"cli\", () => {\n  test(\"--help\", async () => {\n    const cwd = await createProject();\n    const { stdout, stderr, status } = run([\"--help\"], {\n      cwd,\n      env: {\n        NO_COLOR: \"1\",\n      },\n    });\n    expect(stdout.toString().trim()).toBe(helpText);\n    expect(stderr.toString()).toBe(\"\");\n    expect(status).toBe(0);\n  });\n\n  test(\"--version\", async () => {\n    const cwd = await createProject();\n    let { stdout, stderr, status } = run([\"--version\"], { cwd });\n    expect(semver.valid(stdout.toString().trim())).not.toBeNull();\n    expect(stderr.toString()).toBe(\"\");\n    expect(status).toBe(0);\n  });\n\n  test(\"routes\", async () => {\n    const cwd = await createProject();\n    let { stdout, stderr, status } = run([\"routes\"], { cwd });\n    expect(stdout.toString().trim()).toBe(dedent`\n      <Routes>\n        <Route file=\"root.tsx\">\n          <Route index file=\"routes/_index.tsx\" />\n        </Route>\n      </Routes>\n    `);\n    expect(stderr.toString()).toBe(\"\");\n    expect(status).toBe(0);\n  });\n\n  test.describe(\"reveal\", async () => {\n    test(\"generates entry.{server,client}.tsx in the app directory\", async () => {\n      const cwd = await createProject();\n      let entryClientFile = path.join(cwd, \"app\", \"entry.client.tsx\");\n      let entryServerFile = path.join(cwd, \"app\", \"entry.server.tsx\");\n\n      expect(existsSync(entryServerFile)).toBeFalsy();\n      expect(existsSync(entryClientFile)).toBeFalsy();\n\n      run([\"reveal\"], { cwd });\n\n      expect(existsSync(entryServerFile)).toBeTruthy();\n      expect(existsSync(entryClientFile)).toBeTruthy();\n    });\n\n    test(\"generates specified entries in the app directory\", async () => {\n      const cwd = await createProject();\n\n      let entryClientFile = path.join(cwd, \"app\", \"entry.client.tsx\");\n      let entryServerFile = path.join(cwd, \"app\", \"entry.server.tsx\");\n\n      expect(existsSync(entryServerFile)).toBeFalsy();\n      expect(existsSync(entryClientFile)).toBeFalsy();\n\n      run([\"reveal\", \"entry.server\"], { cwd });\n      expect(existsSync(entryServerFile)).toBeTruthy();\n      expect(existsSync(entryClientFile)).toBeFalsy();\n      rmSync(entryServerFile);\n\n      run([\"reveal\", \"entry.client\"], { cwd });\n      expect(existsSync(entryClientFile)).toBeTruthy();\n      expect(existsSync(entryServerFile)).toBeFalsy();\n    });\n\n    test(\"generates entry.{server,client}.jsx in the app directory with --no-typescript\", async () => {\n      const cwd = await createProject();\n      let entryClientFile = path.join(cwd, \"app\", \"entry.client.jsx\");\n      let entryServerFile = path.join(cwd, \"app\", \"entry.server.jsx\");\n\n      expect(existsSync(entryServerFile)).toBeFalsy();\n      expect(existsSync(entryClientFile)).toBeFalsy();\n\n      run([\"reveal\", \"--no-typescript\"], { cwd });\n\n      expect(existsSync(entryServerFile)).toBeTruthy();\n      expect(existsSync(entryClientFile)).toBeTruthy();\n    });\n  });\n});\n"
  },
  {
    "path": "integration/client-data-test.ts",
    "content": "import { test, expect } from \"@playwright/test\";\n\nimport { UNSAFE_ServerMode as ServerMode } from \"react-router\";\nimport {\n  createAppFixture,\n  createFixture,\n  js,\n} from \"./helpers/create-fixture.js\";\nimport type { AppFixture } from \"./helpers/create-fixture.js\";\nimport { PlaywrightFixture } from \"./helpers/playwright-fixture.js\";\nimport { type TemplateName, reactRouterConfig } from \"./helpers/vite.js\";\n\nconst templateNames = [\n  \"vite-5-template\",\n  \"rsc-vite-framework\",\n] as const satisfies TemplateName[];\n\ntest.describe(\"Client Data\", () => {\n  for (const templateName of templateNames) {\n    function getFiles(\n      routeBaseFileName: string,\n      {\n        parentClientLoader,\n        parentClientLoaderHydrate,\n        parentAdditions,\n        childClientLoader,\n        childClientLoaderHydrate,\n        childAdditions,\n      }: {\n        parentClientLoader: boolean;\n        parentClientLoaderHydrate: boolean;\n        parentAdditions?: string;\n        childClientLoader: boolean;\n        childClientLoaderHydrate: boolean;\n        childAdditions?: string;\n      },\n    ) {\n      return {\n        [`app/routes/${routeBaseFileName}.parent.tsx`]: js`\n          import { Outlet, useLoaderData } from \"react-router\"\n          export function loader() {\n            return { message: 'Parent Server Loader' };\n          }\n          ${\n            parentClientLoader\n              ? js`\n                  export async function clientLoader({ serverLoader }) {\n                    // Need a small delay to ensure we capture the server-rendered\n                    // fallbacks for assertions\n                    await new Promise(r => setTimeout(r, 100))\n                    let data = await serverLoader();\n                    return { message: data.message + \" (mutated by client)\" };\n                  }\n                `\n              : \"\"\n          }\n          ${\n            parentClientLoaderHydrate\n              ? js`\n                  clientLoader.hydrate = true;\n                  export function HydrateFallback() {\n                    return <p>Parent Fallback</p>\n                  }\n                `\n              : \"\"\n          }\n          ${parentAdditions || \"\"}\n          export default function Component() {\n            let data = useLoaderData();\n            return (\n              <>\n                <p id=\"parent-data\">{data.message}</p>\n                <Outlet/>\n              </>\n            );\n          }\n        `,\n        [`app/routes/${routeBaseFileName}.parent.child.tsx`]: js`\n          import { Form, Outlet, useActionData, useLoaderData } from \"react-router\"\n          export function loader() {\n            return { message: 'Child Server Loader' };\n          }\n          export function action() {\n            return { message: 'Child Server Action' };\n          }\n          ${\n            childClientLoader\n              ? js`\n                  export async function clientLoader({ serverLoader }) {\n                    // Need a small delay to ensure we capture the server-rendered\n                    // fallbacks for assertions\n                    await new Promise(r => setTimeout(r, 100))\n                    let data = await serverLoader();\n                    return { message: data.message + \" (mutated by client)\" };\n                  }\n                `\n              : \"\"\n          }\n          ${\n            childClientLoaderHydrate\n              ? js`\n                  clientLoader.hydrate = true;\n                  export function HydrateFallback() {\n                    return <p>Child Fallback</p>\n                  }\n                `\n              : \"\"\n          }\n          ${childAdditions || \"\"}\n          export default function Component() {\n            let data = useLoaderData();\n            let actionData = useActionData();\n            return (\n              <>\n                <p id=\"child-data\">{data.message}</p>\n                <Form method=\"post\">\n                  <button type=\"submit\">Submit</button>\n                  {actionData ? <p id=\"child-action-data\">{actionData.message}</p> : null}\n                </Form>\n              </>\n            );\n          }\n        `,\n      };\n    }\n\n    test.describe(`template: ${templateName}`, () => {\n      for (const v8_splitRouteModules of [true, false]) {\n        test.describe(`v8_splitRouteModules: ${v8_splitRouteModules}`, () => {\n          test.skip(\n            templateName.includes(\"rsc\") && v8_splitRouteModules,\n            \"RSC Framework Mode doesn't support splitRouteModules\",\n          );\n\n          test.skip(\n            ({ browserName }) =>\n              Boolean(process.env.CI) &&\n              v8_splitRouteModules &&\n              (browserName === \"webkit\" || process.platform === \"win32\"),\n            \"Webkit/Windows tests only run on a single worker in CI and splitRouteModules is not OS/browser-specific\",\n          );\n\n          let appFixture: AppFixture;\n\n          test.beforeAll(async () => {\n            appFixture = await createAppFixture(\n              await createFixture(\n                {\n                  templateName,\n                  files: {\n                    \"react-router.config.ts\": reactRouterConfig({\n                      future: { v8_splitRouteModules },\n                    }),\n                    \"app/root.tsx\": js`\n                      import { Form, Outlet, Scripts } from \"react-router\"\n\n                      export default function Root({ loaderData }) {\n                        return (\n                          <html>\n                            <head></head>\n                            <body>\n                              <main>\n                                <Outlet />\n                              </main>\n                              <Scripts />\n                            </body>\n                          </html>\n                        );\n                      }\n                    `,\n                    \"app/routes/_index.tsx\": js`\n                      import { Link } from \"react-router\"\n                      export default function Component() {\n                        return (\n                          <ul>\n                            <li><Link to=\"/client-loader-lazy/no-client-loaders-or-fallbacks/parent/child\">/client-loader-lazy/no-client-loaders-or-fallbacks/parent/child</Link></li>\n                            <li><Link to=\"/client-loader-lazy/parent-client-loader/parent/child\">/client-loader-lazy/parent-client-loader/parent/child</Link></li>\n                            <li><Link to=\"/client-loader-lazy/child-client-loader/parent/child\">/client-loader-lazy/child-client-loader/parent/child</Link></li>\n                            <li><Link to=\"/client-loader-lazy/parent-client-loader-child-client-loader/parent/child\">/client-loader-lazy/parent-client-loader-child-client-loader/parent/child</Link></li>\n                            <li><Link to=\"/client-loader-lazy/throws-a-400-if-you-call-serverloader-without-a-server-loader/parent/child\">/client-loader-lazy/throws-a-400-if-you-call-serverloader-without-a-server-loader/parent/child</Link></li>\n                            <li><Link prefetch=\"render\" to=\"/client-loader-lazy/does-not-prefetch-server-loader-if-a-client-loader-is-present/parent\">/client-loader-lazy/does-not-prefetch-server-loader-if-a-client-loader-is-present/parent</Link></li>\n                            <li><Link prefetch=\"render\" to=\"/client-loader-lazy/does-not-prefetch-server-loader-if-a-client-loader-is-present/parent/child\">/client-loader-lazy/does-not-prefetch-server-loader-if-a-client-loader-is-present/parent/child</Link></li>\n                            <li><Link to=\"/client-action-lazy/child-client-action/parent/child\">/client-action-lazy/child-client-action/parent/child</Link></li>\n                            <li><Link to=\"/client-action-lazy/child-client-action-parent-child-loader/parent/child\">/client-action-lazy/child-client-action-parent-child-loader/parent/child</Link></li>\n                            <li><Link to=\"/client-action-lazy/child-client-action-child-client-loader/parent/child\">/client-action-lazy/child-client-action-child-client-loader/parent/child</Link></li>\n                            <li><Link to=\"/client-action-lazy/child-client-action-parent-child-loader-child-client-loader/parent/child\">/client-action-lazy/child-client-action-parent-child-loader-child-client-loader/parent/child</Link></li>\n                            <li><Link to=\"/client-action-lazy/throws-a-400-if-you-call-serveraction-without-a-server-action/parent/child\">/client-action-lazy/throws-a-400-if-you-call-serveraction-without-a-server-action/parent/child</Link></li>\n                          </ul>\n                        );\n                      }\n                    `,\n\n                    ...getFiles(\n                      \"client-loader-critical.no-client-loaders-or-fallbacks\",\n                      {\n                        parentClientLoader: false,\n                        parentClientLoaderHydrate: false,\n                        childClientLoader: false,\n                        childClientLoaderHydrate: false,\n                      },\n                    ),\n\n                    ...getFiles(\n                      \"client-loader-critical.parent-client-loader-child-client-loader\",\n                      {\n                        parentClientLoader: true,\n                        parentClientLoaderHydrate: false,\n                        childClientLoader: true,\n                        childClientLoaderHydrate: false,\n                      },\n                    ),\n\n                    ...getFiles(\n                      \"client-loader-critical.parent-client-loader-hydrate-child-client-loader\",\n                      {\n                        parentClientLoader: true,\n                        parentClientLoaderHydrate: true,\n                        childClientLoader: true,\n                        childClientLoaderHydrate: false,\n                      },\n                    ),\n\n                    ...getFiles(\n                      \"client-loader-critical.parent-client-loader-child-client-loader-hydrate\",\n                      {\n                        parentClientLoader: true,\n                        parentClientLoaderHydrate: false,\n                        childClientLoader: true,\n                        childClientLoaderHydrate: true,\n                      },\n                    ),\n\n                    ...getFiles(\n                      \"client-loader-critical.parent-client-loader-child-client-loader-hydrate-both\",\n                      {\n                        parentClientLoader: true,\n                        parentClientLoaderHydrate: true,\n                        childClientLoader: true,\n                        childClientLoaderHydrate: true,\n                      },\n                    ),\n\n                    ...getFiles(\n                      \"client-loader-critical.handles-synchronous-client-loaders\",\n                      {\n                        parentClientLoader: false,\n                        parentClientLoaderHydrate: false,\n                        childClientLoader: false,\n                        childClientLoaderHydrate: false,\n                        parentAdditions: js`\n                          export function clientLoader() {\n                            return { message: \"Parent Client Loader\" };\n                          }\n                          clientLoader.hydrate=true\n                          export function HydrateFallback() {\n                            return <p>Parent Fallback</p>\n                          }\n                        `,\n                        childAdditions: js`\n                          export function clientLoader() {\n                            return { message: \"Child Client Loader\" };\n                          }\n                          clientLoader.hydrate=true\n                        `,\n                      },\n                    ),\n\n                    ...getFiles(\n                      \"client-loader-critical.handles-deferred-data-through-client-loaders\",\n                      {\n                        parentClientLoader: false,\n                        parentClientLoaderHydrate: false,\n                        childClientLoader: false,\n                        childClientLoaderHydrate: false,\n                      },\n                    ),\n                    \"app/routes/client-loader-critical.handles-deferred-data-through-client-loaders.parent.child.tsx\": js`\n                      import * as React from 'react';\n                      import { Await, useLoaderData } from \"react-router\"\n                      export function loader() {\n                        return {\n                          message: 'Child Server Loader',\n                          lazy: new Promise(r => setTimeout(() => r(\"Child Deferred Data\"), 1000)),\n                        };\n                      }\n                      export async function clientLoader({ serverLoader }) {\n                        let data = await serverLoader();\n                        return {\n                          ...data,\n                          message: data.message + \" (mutated by client)\",\n                        };\n                      }\n                      clientLoader.hydrate = true;\n                      export function HydrateFallback() {\n                        return <p>Child Fallback</p>\n                      }\n                      export default function Component() {\n                        let data = useLoaderData();\n                        return (\n                          <>\n                            <p id=\"child-data\">{data.message}</p>\n                            <React.Suspense fallback={<p>Loading Deferred Data...</p>}>\n                              <Await resolve={data.lazy}>\n                                {(value) => <p id=\"child-deferred-data\">{value}</p>}\n                              </Await>\n                            </React.Suspense>\n                          </>\n                        );\n                      }\n                    `,\n\n                    ...getFiles(\n                      \"client-loader-critical.allows-hydration-without-rendering-a-fallback\",\n                      {\n                        parentClientLoader: false,\n                        parentClientLoaderHydrate: false,\n                        childClientLoader: false,\n                        childClientLoaderHydrate: false,\n                        childAdditions: js`\n                          export async function clientLoader() {\n                            await new Promise(r => setTimeout(r, 100));\n                            return { message: \"Child Client Loader\" };\n                          }\n                          clientLoader.hydrate=true\n                        `,\n                      },\n                    ),\n\n                    ...getFiles(\n                      \"client-loader-critical.hydrate-fallback-not-rendered-if-not-set-with-server-loader\",\n                      {\n                        parentClientLoader: false,\n                        parentClientLoaderHydrate: false,\n                        childClientLoader: false,\n                        childClientLoaderHydrate: false,\n                      },\n                    ),\n                    \"app/routes/client-loader-critical.hydrate-fallback-not-rendered-if-not-set-with-server-loader.parent.child.tsx\": js`\n                      import * as React from 'react';\n                      import { useLoaderData } from \"react-router\";\n                      export function loader() {\n                        return { message: \"Child Server Loader Data\" };\n                      }\n                      export async function clientLoader({ serverLoader }) {\n                        await new Promise(r => setTimeout(r, 100));\n                        return { message: \"Child Client Loader Data\" };\n                      }\n                      export function HydrateFallback() {\n                        return <p>SHOULD NOT SEE ME</p>\n                      }\n                      export default function Component() {\n                        let data = useLoaderData();\n                        return <p id=\"child-data\">{data.message}</p>;\n                      }\n                    `,\n\n                    ...getFiles(\n                      \"client-loader-critical.client-loader-hydrate-is-automatically-implied-when-no-server-loader-exists-with-hydrate-fallback\",\n                      {\n                        parentClientLoader: false,\n                        parentClientLoaderHydrate: false,\n                        childClientLoader: false,\n                        childClientLoaderHydrate: false,\n                      },\n                    ),\n                    \"app/routes/client-loader-critical.client-loader-hydrate-is-automatically-implied-when-no-server-loader-exists-with-hydrate-fallback.parent.child.tsx\": js`\n                      import * as React from 'react';\n                      import { useLoaderData } from \"react-router\";\n                      // Even without setting hydrate=true, this should run on hydration\n                      export async function clientLoader({ serverLoader }) {\n                        await new Promise(r => setTimeout(r, 100));\n                        return {\n                          message: \"Loader Data (clientLoader only)\",\n                        };\n                      }\n                      export function HydrateFallback() {\n                        return <p>Child Fallback</p>\n                      }\n                      export default function Component() {\n                        let data = useLoaderData();\n                        return <p id=\"child-data\">{data.message}</p>;\n                      }\n                    `,\n\n                    ...getFiles(\n                      \"client-loader-critical.client-loader-hydrate-is-automatically-implied-when-no-server-loader-exists-without-hydrate-fallback\",\n                      {\n                        parentClientLoader: false,\n                        parentClientLoaderHydrate: false,\n                        childClientLoader: false,\n                        childClientLoaderHydrate: false,\n                      },\n                    ),\n                    \"app/routes/client-loader-critical.client-loader-hydrate-is-automatically-implied-when-no-server-loader-exists-without-hydrate-fallback.parent.child.tsx\": js`\n                      import * as React from 'react';\n                      import { useLoaderData } from \"react-router\";\n                      // Even without setting hydrate=true, this should run on hydration\n                      export async function clientLoader({ serverLoader }) {\n                        await new Promise(r => setTimeout(r, 100));\n                        return {\n                          message: \"Loader Data (clientLoader only)\",\n                        };\n                      }\n                      export default function Component() {\n                        let data = useLoaderData();\n                        return <p id=\"child-data\">{data.message}</p>;\n                      }\n                    `,\n\n                    ...getFiles(\n                      \"client-loader-critical.throws-a-400-if-you-call-serverloader-without-a-server-loader\",\n                      {\n                        parentClientLoader: false,\n                        parentClientLoaderHydrate: false,\n                        childClientLoader: false,\n                        childClientLoaderHydrate: false,\n                      },\n                    ),\n                    \"app/routes/client-loader-critical.throws-a-400-if-you-call-serverloader-without-a-server-loader.parent.child.tsx\": js`\n                      import * as React from 'react';\n                      import { useLoaderData, useRouteError } from \"react-router\";\n                      export async function clientLoader({ serverLoader }) {\n                        return await serverLoader();\n                      }\n                      export default function Component() {\n                        return <p>Child</p>;\n                      }\n                      export function HydrateFallback() {\n                        return <p>Loading...</p>;\n                      }\n                      export function ErrorBoundary() {\n                        let error = useRouteError();\n                        return <p id=\"child-error\">{error.status} {error.data}</p>;\n                      }\n                    `,\n\n                    ...getFiles(\n                      \"client-loader-critical.initial-hydration-data-check-functions-properly\",\n                      {\n                        parentClientLoader: false,\n                        parentClientLoaderHydrate: false,\n                        childClientLoader: false,\n                        childClientLoaderHydrate: false,\n                      },\n                    ),\n                    \"app/routes/client-loader-critical.initial-hydration-data-check-functions-properly.parent.child.tsx\": js`\n                      import * as React from 'react';\n                      import { useLoaderData, useRevalidator } from \"react-router\";\n                      let isFirstCall = true;\n                      export async function loader({ serverLoader }) {\n                        if (isFirstCall) {\n                          isFirstCall = false\n                          return { message: \"Child Server Loader Data (1)\" };\n                        }\n                        return { message: \"Child Server Loader Data (2+)\" };\n                      }\n                      export async function clientLoader({ serverLoader }) {\n                        await new Promise(r => setTimeout(r, 100));\n                        let serverData = await serverLoader();\n                        return {\n                          message: serverData.message + \" (mutated by client)\",\n                        };\n                      }\n                      clientLoader.hydrate=true;\n                      export default function Component() {\n                        let data = useLoaderData();\n                        let revalidator = useRevalidator();\n                        return (\n                          <>\n                            <p id=\"child-data\">{data.message}</p>\n                            <button onClick={() => revalidator.revalidate()}>Revalidate</button>\n                          </>\n                        );\n                      }\n                      export function HydrateFallback() {\n                        return <p>Loading...</p>\n                      }\n                    `,\n\n                    ...getFiles(\n                      \"client-loader-critical.initial-hydration-data-check-functions-properly-even-if-serverloader-isnt-called-on-hydration\",\n                      {\n                        parentClientLoader: false,\n                        parentClientLoaderHydrate: false,\n                        childClientLoader: false,\n                        childClientLoaderHydrate: false,\n                      },\n                    ),\n                    \"app/routes/client-loader-critical.initial-hydration-data-check-functions-properly-even-if-serverloader-isnt-called-on-hydration.parent.child.tsx\": js`\n                      import * as React from 'react';\n                      import { useLoaderData, useRevalidator } from \"react-router\";\n                      let isFirstCall = true;\n                      export async function loader({ serverLoader }) {\n                        if (isFirstCall) {\n                          isFirstCall = false\n                          return { message: \"Child Server Loader Data (1)\" };\n                        }\n                        return { message: \"Child Server Loader Data (2+)\" };\n                      }\n                      let isFirstClientCall = true;\n                      export async function clientLoader({ serverLoader }) {\n                        await new Promise(r => setTimeout(r, 100));\n                        if (isFirstClientCall) {\n                          isFirstClientCall = false;\n                          // First time through - don't even call serverLoader\n                          return {\n                            message: \"Child Client Loader Data\",\n                          };\n                        }\n                        // Only call the serverLoader on subsequent calls and this\n                        // should *not* return us the initialData any longer\n                        let serverData = await serverLoader();\n                        return {\n                          message: serverData.message + \" (mutated by client)\",\n                        };\n                      }\n                      clientLoader.hydrate=true;\n                      export default function Component() {\n                        let data = useLoaderData();\n                        let revalidator = useRevalidator();\n                        return (\n                          <>\n                            <p id=\"child-data\">{data.message}</p>\n                            <button onClick={() => revalidator.revalidate()}>Revalidate</button>\n                          </>\n                        );\n                      }\n                      export function HydrateFallback() {\n                        return <p>Loading...</p>\n                      }\n                    `,\n\n                    ...getFiles(\n                      \"client-loader-critical.server-loader-errors-are-re-thrown-from-serverloader\",\n                      {\n                        parentClientLoader: false,\n                        parentClientLoaderHydrate: false,\n                        childClientLoader: false,\n                        childClientLoaderHydrate: false,\n                      },\n                    ),\n                    \"app/routes/client-loader-critical.server-loader-errors-are-re-thrown-from-serverloader.parent.child.tsx\": js`\n                      import { useRouteError } from \"react-router\";\n\n                      export function loader() {\n                        throw new Error(\"Broken!\")\n                      }\n\n                      export async function clientLoader({ serverLoader }) {\n                        return await serverLoader();\n                      }\n                      clientLoader.hydrate = true;\n\n                      export default function Index() {\n                        return <h1>Should not see me</h1>;\n                      }\n\n                      export function ErrorBoundary() {\n                        let error = useRouteError();\n                        return <p id=\"child-error\">{error.message}</p>;\n                      }\n                    `,\n\n                    ...getFiles(\n                      \"client-loader-critical.bubbled-server-loader-errors-are-persisted-for-hydrating-routes\",\n                      {\n                        parentClientLoader: false,\n                        parentClientLoaderHydrate: false,\n                        childClientLoader: false,\n                        childClientLoaderHydrate: false,\n                      },\n                    ),\n                    \"app/routes/client-loader-critical.bubbled-server-loader-errors-are-persisted-for-hydrating-routes.parent.tsx\": js`\n                      import { Outlet, useLoaderData, useRouteLoaderData, useRouteError } from 'react-router'\n                      export function loader() {\n                        return { message: 'Parent Server Loader' };\n                      }\n                      export async function clientLoader({ serverLoader }) {\n                        console.log('running parent client loader')\n                        // Need a small delay to ensure we capture the server-rendered\n                        // fallbacks for assertions\n                        await new Promise(r => setTimeout(r, 100));\n                        let data = await serverLoader();\n                        return { message: data.message + \" (mutated by client)\" };\n                      }\n                      clientLoader.hydrate = true;\n                      export default function Component() {\n                        let data = useLoaderData();\n                        return (\n                          <>\n                            <p id=\"parent-data\">{data.message}</p>\n                            <Outlet/>\n                          </>\n                        );\n                      }\n                      export function ErrorBoundary() {\n                        let data = useRouteLoaderData(\"routes/client-loader-critical.bubbled-server-loader-errors-are-persisted-for-hydrating-routes.parent\")\n                        let error = useRouteError();\n                        return (\n                          <>\n                            <h1>Parent Error</h1>\n                            <p id=\"parent-data\">{data?.message}</p>\n                            <p id=\"parent-error\">{error?.message}</p>\n                          </>\n                        );\n                      }\n                    `,\n                    \"app/routes/client-loader-critical.bubbled-server-loader-errors-are-persisted-for-hydrating-routes.parent.child.tsx\": js`\n                      import { useRouteError, useLoaderData } from 'react-router'\n                      export function loader() {\n                        throw new Error('Child Server Error');\n                      }\n                      export function clientLoader() {\n                        console.log('running child client loader')\n                        return \"Should not see me\";\n                      }\n                      clientLoader.hydrate = true;\n                      export default function Component() {\n                        let data = useLoaderData()\n                        return (\n                          <>\n                            <p>Should not see me</p>\n                            <p>{data}</p>;\n                          </>\n                        );\n                      }\n                    `,\n\n                    ...getFiles(\n                      \"client-loader-lazy.no-client-loaders-or-fallbacks\",\n                      {\n                        parentClientLoader: false,\n                        parentClientLoaderHydrate: false,\n                        childClientLoader: false,\n                        childClientLoaderHydrate: false,\n                      },\n                    ),\n\n                    ...getFiles(\"client-loader-lazy.parent-client-loader\", {\n                      parentClientLoader: true,\n                      parentClientLoaderHydrate: false,\n                      childClientLoader: false,\n                      childClientLoaderHydrate: false,\n                    }),\n\n                    ...getFiles(\"client-loader-lazy.child-client-loader\", {\n                      parentClientLoader: false,\n                      parentClientLoaderHydrate: false,\n                      childClientLoader: true,\n                      childClientLoaderHydrate: false,\n                    }),\n\n                    ...getFiles(\n                      \"client-loader-lazy.parent-client-loader-child-client-loader\",\n                      {\n                        parentClientLoader: true,\n                        parentClientLoaderHydrate: false,\n                        childClientLoader: true,\n                        childClientLoaderHydrate: false,\n                      },\n                    ),\n\n                    ...getFiles(\n                      \"client-loader-lazy.throws-a-400-if-you-call-serverloader-without-a-server-loader\",\n                      {\n                        parentClientLoader: false,\n                        parentClientLoaderHydrate: false,\n                        childClientLoader: false,\n                        childClientLoaderHydrate: false,\n                      },\n                    ),\n                    \"app/routes/client-loader-lazy.throws-a-400-if-you-call-serverloader-without-a-server-loader.parent.child.tsx\": js`\n                      import * as React from 'react';\n                      import { useLoaderData, useRouteError } from \"react-router\";\n                      export async function clientLoader({ serverLoader }) {\n                        return await serverLoader();\n                      }\n                      export default function Component() {\n                        return <p>Child</p>;\n                      }\n                      export function HydrateFallback() {\n                        return <p>Loading...</p>;\n                      }\n                      export function ErrorBoundary() {\n                        let error = useRouteError();\n                        return <p id=\"child-error\">{error.status} {error.data}</p>;\n                      }\n                    `,\n\n                    ...getFiles(\n                      \"client-loader-lazy.does-not-prefetch-server-loader-if-a-client-loader-is-present\",\n                      {\n                        parentClientLoader: true,\n                        parentClientLoaderHydrate: false,\n                        childClientLoader: false,\n                        childClientLoaderHydrate: false,\n                      },\n                    ),\n\n                    ...getFiles(\"client-action-critical.child-client-action\", {\n                      parentClientLoader: false,\n                      parentClientLoaderHydrate: false,\n                      childClientLoader: false,\n                      childClientLoaderHydrate: false,\n                      childAdditions: js`\n                        export async function clientAction({ serverAction }) {\n                          let data = await serverAction();\n                          return {\n                            message: data.message + \" (mutated by client)\"\n                          }\n                        }\n                      `,\n                    }),\n\n                    ...getFiles(\n                      \"client-action-critical.child-client-action-parent-child-loader\",\n                      {\n                        parentClientLoader: true,\n                        parentClientLoaderHydrate: false,\n                        childClientLoader: false,\n                        childClientLoaderHydrate: false,\n                        childAdditions: js`\n                        export async function clientAction({ serverAction }) {\n                          let data = await serverAction();\n                          return {\n                            message: data.message + \" (mutated by client)\"\n                          }\n                        }\n                      `,\n                      },\n                    ),\n\n                    ...getFiles(\n                      \"client-action-critical.child-client-action-child-client-loader\",\n                      {\n                        parentClientLoader: false,\n                        parentClientLoaderHydrate: false,\n                        childClientLoader: true,\n                        childClientLoaderHydrate: false,\n                        childAdditions: js`\n                          export async function clientAction({ serverAction }) {\n                            let data = await serverAction();\n                            return {\n                              message: data.message + \" (mutated by client)\"\n                            }\n                          }\n                        `,\n                      },\n                    ),\n\n                    ...getFiles(\n                      \"client-action-critical.child-client-action-parent-child-loader-child-client-loader\",\n                      {\n                        parentClientLoader: true,\n                        parentClientLoaderHydrate: false,\n                        childClientLoader: true,\n                        childClientLoaderHydrate: false,\n                        childAdditions: js`\n                        export async function clientAction({ serverAction }) {\n                          let data = await serverAction();\n                          return {\n                            message: data.message + \" (mutated by client)\"\n                          }\n                        }\n                      `,\n                      },\n                    ),\n\n                    ...getFiles(\n                      \"client-action-critical.throws-a-400-if-you-call-serveraction-without-a-server-action\",\n                      {\n                        parentClientLoader: false,\n                        parentClientLoaderHydrate: false,\n                        childClientLoader: false,\n                        childClientLoaderHydrate: false,\n                      },\n                    ),\n                    \"app/routes/client-action-critical.throws-a-400-if-you-call-serveraction-without-a-server-action.parent.child.tsx\": js`\n                      import * as React from 'react';\n                      import { Form, useRouteError } from \"react-router\";\n                      export async function clientAction({ serverAction }) {\n                        return await serverAction();\n                      }\n                      export default function Component() {\n                        return (\n                          <Form method=\"post\">\n                            <button type=\"submit\">Submit</button>\n                          </Form>\n                        );\n                      }\n                      export function ErrorBoundary() {\n                        let error = useRouteError();\n                        return <p id=\"child-error\">{error.status} {error.data}</p>;\n                      }\n                    `,\n\n                    ...getFiles(\"client-action-lazy.child-client-action\", {\n                      parentClientLoader: false,\n                      parentClientLoaderHydrate: false,\n                      childClientLoader: false,\n                      childClientLoaderHydrate: false,\n                      childAdditions: js`\n                        export async function clientAction({ serverAction }) {\n                          let data = await serverAction();\n                          return {\n                            message: data.message + \" (mutated by client)\"\n                          }\n                        }\n                      `,\n                    }),\n\n                    ...getFiles(\n                      \"client-action-lazy.child-client-action-parent-child-loader\",\n                      {\n                        parentClientLoader: true,\n                        parentClientLoaderHydrate: false,\n                        childClientLoader: false,\n                        childClientLoaderHydrate: false,\n                        childAdditions: js`\n                        export async function clientAction({ serverAction }) {\n                          let data = await serverAction();\n                          return {\n                            message: data.message + \" (mutated by client)\"\n                          }\n                        }\n                      `,\n                      },\n                    ),\n\n                    ...getFiles(\n                      \"client-action-lazy.child-client-action-child-client-loader\",\n                      {\n                        parentClientLoader: false,\n                        parentClientLoaderHydrate: false,\n                        childClientLoader: true,\n                        childClientLoaderHydrate: false,\n                        childAdditions: js`\n                        export async function clientAction({ serverAction }) {\n                          let data = await serverAction();\n                          return {\n                            message: data.message + \" (mutated by client)\"\n                          }\n                        }\n                      `,\n                      },\n                    ),\n\n                    ...getFiles(\n                      \"client-action-lazy.child-client-action-parent-child-loader-child-client-loader\",\n                      {\n                        parentClientLoader: true,\n                        parentClientLoaderHydrate: false,\n                        childClientLoader: true,\n                        childClientLoaderHydrate: false,\n                        childAdditions: js`\n                        export async function clientAction({ serverAction }) {\n                          let data = await serverAction();\n                          return {\n                            message: data.message + \" (mutated by client)\"\n                          }\n                        }\n                      `,\n                      },\n                    ),\n\n                    ...getFiles(\n                      \"client-action-lazy.throws-a-400-if-you-call-serveraction-without-a-server-action\",\n                      {\n                        parentClientLoader: false,\n                        parentClientLoaderHydrate: false,\n                        childClientLoader: false,\n                        childClientLoaderHydrate: false,\n                      },\n                    ),\n                    \"app/routes/client-action-lazy.throws-a-400-if-you-call-serveraction-without-a-server-action.parent.child.tsx\": js`\n                      import * as React from 'react';\n                      import { Form, useRouteError } from \"react-router\";\n                      export async function clientAction({ serverAction }) {\n                        return await serverAction();\n                      }\n                      export default function Component() {\n                        return (\n                          <Form method=\"post\">\n                            <button type=\"submit\">Submit</button>\n                          </Form>\n                        );\n                      }\n                      export function ErrorBoundary() {\n                        let error = useRouteError();\n                        return <p id=\"child-error\">{error.status} {error.data}</p>;\n                      }\n                    `,\n\n                    \"app/routes/client-loader-critical.hydrating-clientloader-redirects-trigger-new-data-requests-to-the-server.tsx\": js`\n                      import { Outlet } from 'react-router'\n\n                      let count = 1;\n                      export function loader() {\n                        return count++;\n                      }\n                      export default function Component({ loaderData }) {\n                        return (\n                          <>\n                            <p id=\"parent-1-data\">{loaderData}</p>\n                            <Outlet/>\n                          </>\n                        );\n                      }\n                    `,\n\n                    \"app/routes/client-loader-critical.hydrating-clientloader-redirects-trigger-new-data-requests-to-the-server.parent.tsx\": js`\n                      import { Outlet } from 'react-router'\n                      let count = 1;\n                      export function loader() {\n                        return count++;\n                      }\n                      export default function Component({ loaderData }) {\n                        return (\n                          <>\n                            <p id=\"parent-2-data\">{loaderData}</p>\n                            <Outlet/>\n                          </>\n                        );\n                      }\n                      export function shouldRevalidate() {\n                        return false;\n                      }\n                    `,\n                    \"app/routes/client-loader-critical.hydrating-clientloader-redirects-trigger-new-data-requests-to-the-server.parent.a.tsx\": js`\n                      import { redirect } from 'react-router'\n                      export function clientLoader() {\n                        return redirect('/client-loader-critical/hydrating-clientloader-redirects-trigger-new-data-requests-to-the-server/parent/b');\n                      }\n                      clientLoader.hydrate = true;\n                      export default function Component({ loaderData }) {\n                        return <p>Should not see me</p>;\n                      }\n                    `,\n                    \"app/routes/client-loader-critical.hydrating-clientloader-redirects-trigger-new-data-requests-to-the-server.parent.b.tsx\": js`\n                      export default function Component({ loaderData }) {\n                        return <p id=\"b\">Hi!</p>;\n                      }\n                    `,\n                  },\n                },\n                ServerMode.Development, // Avoid error sanitization\n              ),\n              ServerMode.Development, // Avoid error sanitization\n            );\n          });\n\n          test.afterAll(() => {\n            appFixture?.close();\n          });\n\n          test.describe(\"clientLoader - critical route module\", () => {\n            test(\"no client loaders or fallbacks\", async ({ page }) => {\n              let app = new PlaywrightFixture(appFixture, page);\n\n              // Full SSR - normal loader behavior due to lack of clientLoader\n              await app.goto(\n                \"/client-loader-critical/no-client-loaders-or-fallbacks/parent/child\",\n              );\n              let html = await app.getHtml(\"main\");\n              expect(html).toMatch(\"Parent Server Loader\");\n              expect(html).toMatch(\"Child Server Loader\");\n            });\n\n            test(\"parent.clientLoader/child.clientLoader\", async ({ page }) => {\n              let app = new PlaywrightFixture(appFixture, page);\n\n              // Full SSR - normal loader behavior due to lack of HydrateFallback components\n              await app.goto(\n                \"/client-loader-critical/parent-client-loader-child-client-loader/parent/child\",\n              );\n              let html = await app.getHtml(\"main\");\n              expect(html).toMatch(\"Parent Server Loader\");\n              expect(html).toMatch(\"Child Server Loader\");\n            });\n\n            test(\"parent.clientLoader.hydrate/child.clientLoader\", async ({\n              page,\n            }) => {\n              let app = new PlaywrightFixture(appFixture, page);\n\n              await app.goto(\n                \"/client-loader-critical/parent-client-loader-hydrate-child-client-loader/parent/child\",\n              );\n              let html = await app.getHtml(\"main\");\n              expect(html).toMatch(\"Parent Fallback\");\n              expect(html).not.toMatch(\"Parent Server Loader\");\n              expect(html).not.toMatch(\"Child Server Loader\");\n\n              await page.waitForSelector(\"#child-data\");\n              html = await app.getHtml(\"main\");\n              expect(html).not.toMatch(\"Parent Fallback\");\n              expect(html).toMatch(\"Parent Server Loader (mutated by client)\");\n              expect(html).toMatch(\"Child Server Loader\");\n            });\n\n            test(\"parent.clientLoader/child.clientLoader.hydrate\", async ({\n              page,\n            }) => {\n              let app = new PlaywrightFixture(appFixture, page);\n\n              await app.goto(\n                \"/client-loader-critical/parent-client-loader-child-client-loader-hydrate/parent/child\",\n              );\n              let html = await app.getHtml(\"main\");\n              expect(html).toMatch(\"Parent Server Loader\");\n              expect(html).toMatch(\"Child Fallback\");\n              expect(html).not.toMatch(\"Child Server Loader\");\n\n              await page.waitForSelector(\"#child-data\");\n              html = await app.getHtml(\"main\");\n              expect(html).not.toMatch(\"Child Fallback\");\n              expect(html).toMatch(\"Parent Server Loader\");\n              expect(html).toMatch(\"Child Server Loader (mutated by client)\");\n            });\n\n            test(\"parent.clientLoader.hydrate/child.clientLoader.hydrate\", async ({\n              page,\n            }) => {\n              let app = new PlaywrightFixture(appFixture, page);\n\n              await app.goto(\n                \"/client-loader-critical/parent-client-loader-child-client-loader-hydrate-both/parent/child\",\n              );\n              let html = await app.getHtml(\"main\");\n              expect(html).toMatch(\"Parent Fallback\");\n              expect(html).not.toMatch(\"Parent Server Loader\");\n              expect(html).not.toMatch(\"Child Fallback\");\n              expect(html).not.toMatch(\"Child Server Loader\");\n\n              await page.waitForSelector(\"#child-data\");\n              html = await app.getHtml(\"main\");\n              expect(html).not.toMatch(\"Parent Fallback\");\n              expect(html).not.toMatch(\"Child Fallback\");\n              expect(html).toMatch(\"Parent Server Loader (mutated by client)\");\n              expect(html).toMatch(\"Child Server Loader (mutated by client)\");\n            });\n\n            test(\"handles synchronous client loaders\", async ({ page }) => {\n              let app = new PlaywrightFixture(appFixture, page);\n\n              // Ensure we SSR the fallbacks\n              let response = await app.goto(\n                \"/client-loader-critical/handles-synchronous-client-loaders/parent/child\",\n              );\n              let html = await response?.text();\n              expect(html).toMatch(\"Parent Fallback\");\n\n              await page.waitForSelector(\"#child-data\");\n              html = await app.getHtml(\"main\");\n              expect(html).toMatch(\"Parent Client Loader\");\n              expect(html).toMatch(\"Child Client Loader\");\n            });\n\n            test(\"handles deferred data through client loaders\", async ({\n              page,\n            }) => {\n              let app = new PlaywrightFixture(appFixture, page);\n\n              // Ensure initial document request contains the child fallback _and_ the\n              // subsequent streamed/resolved deferred data\n              let response = await app.goto(\n                \"/client-loader-critical/handles-deferred-data-through-client-loaders/parent/child\",\n              );\n              let html = await response?.text();\n              expect(html).toMatch(\"Parent Server Loader\");\n              expect(html).toMatch(\"Child Fallback\");\n              expect(html).toMatch(\"Child Deferred Data\");\n\n              await page.waitForSelector(\"#child-deferred-data\");\n              html = await app.getHtml(\"main\");\n              expect(html).toMatch(\"Parent Server Loader\");\n              // app.goto() doesn't resolve until the document finishes loading so by\n              // then the HTML has updated via the streamed suspense updates\n              expect(html).toMatch(\"Child Server Loader (mutated by client)\");\n              expect(html).toMatch(\"Child Deferred Data\");\n            });\n\n            test(\"allows hydration execution without rendering a fallback\", async ({\n              page,\n            }) => {\n              let app = new PlaywrightFixture(appFixture, page);\n              await app.goto(\n                \"/client-loader-critical/allows-hydration-without-rendering-a-fallback/parent/child\",\n              );\n              let html = await app.getHtml(\"main\");\n              expect(html).toMatch(\"Child Server Loader\");\n              await page.waitForSelector(':has-text(\"Child Client Loader\")');\n              html = await app.getHtml(\"main\");\n              expect(html).toMatch(\"Child Client Loader\");\n            });\n\n            test(\"HydrateFallback is not rendered if clientLoader.hydrate is not set (w/server loader)\", async ({\n              page,\n            }) => {\n              let app = new PlaywrightFixture(appFixture, page);\n\n              // Ensure initial document request contains the child fallback _and_ the\n              // subsequent streamed/resolved deferred data\n              let response = await app.goto(\n                \"/client-loader-critical/hydrate-fallback-not-rendered-if-not-set-with-server-loader/parent/child\",\n              );\n              let html = await response?.text();\n              expect(html).toMatch(\"Child Server Loader Data\");\n              expect(html).not.toMatch(\"SHOULD NOT SEE ME\");\n\n              await page.waitForSelector(\"#child-data\");\n              html = await app.getHtml(\"main\");\n              expect(html).toMatch(\"Child Server Loader Data\");\n            });\n\n            test(\"clientLoader.hydrate is automatically implied when no server loader exists (w HydrateFallback)\", async ({\n              page,\n            }) => {\n              let app = new PlaywrightFixture(appFixture, page);\n\n              await app.goto(\n                \"/client-loader-critical/client-loader-hydrate-is-automatically-implied-when-no-server-loader-exists-with-hydrate-fallback/parent/child\",\n              );\n              let html = await app.getHtml(\"main\");\n              expect(html).toMatch(\"Child Fallback\");\n              await page.waitForSelector(\"#child-data\");\n              html = await app.getHtml(\"main\");\n              expect(html).toMatch(\"Loader Data (clientLoader only)\");\n            });\n\n            test(\"clientLoader.hydrate is automatically implied when no server loader exists (w/o HydrateFallback)\", async ({\n              page,\n            }) => {\n              test.skip(\n                templateName.includes(\"rsc\"),\n                \"RSC Framework Mode doesn't need to provide a default root HydrateFallback since it doesn't need to ensure <Scripts /> is rendered, and you already get a console warning\",\n              );\n\n              let app = new PlaywrightFixture(appFixture, page);\n\n              await app.goto(\n                \"/client-loader-critical/client-loader-hydrate-is-automatically-implied-when-no-server-loader-exists-without-hydrate-fallback/parent/child\",\n              );\n              let html = await app.getHtml();\n              expect(html).toMatch(\n                \"💿 Hey developer 👋. You can provide a way better UX than this\",\n              );\n              expect(html).not.toMatch(\"child-data\");\n              await page.waitForSelector(\"#child-data\");\n              html = await app.getHtml(\"main\");\n              expect(html).toMatch(\"Loader Data (clientLoader only)\");\n            });\n\n            test(\"throws a 400 if you call serverLoader without a server loader\", async ({\n              page,\n            }) => {\n              let app = new PlaywrightFixture(appFixture, page);\n\n              await app.goto(\n                \"/client-loader-critical/throws-a-400-if-you-call-serverloader-without-a-server-loader/parent/child\",\n              );\n              await page.waitForSelector(\"#child-error\");\n              let html = await app.getHtml(\"#child-error\");\n              expect(html.replace(/\\n/g, \" \").replace(/ +/g, \" \")).toMatch(\n                \"400 Error: You are trying to call serverLoader() on a route that does \" +\n                  'not have a server loader (routeId: \"routes/client-loader-critical.throws-a-400-if-you-call-serverloader-without-a-server-loader.parent.child\")',\n              );\n            });\n\n            test(\"initial hydration data check functions properly\", async ({\n              page,\n            }) => {\n              let app = new PlaywrightFixture(appFixture, page);\n\n              await app.goto(\n                \"/client-loader-critical/initial-hydration-data-check-functions-properly/parent/child\",\n              );\n              await page.waitForSelector(\"#child-data\");\n              let html = await app.getHtml();\n              expect(html).toMatch(\n                \"Child Server Loader Data (1) (mutated by client)\",\n              );\n              app.clickElement(\"button\");\n              await page.waitForSelector(\n                ':has-text(\"Child Server Loader Data (2+)\")',\n              );\n              html = await app.getHtml(\"main\");\n              expect(html).toMatch(\n                \"Child Server Loader Data (2+) (mutated by client)\",\n              );\n            });\n\n            test(\"initial hydration data check functions properly even if serverLoader isn't called on hydration\", async ({\n              page,\n            }) => {\n              let app = new PlaywrightFixture(appFixture, page);\n\n              await app.goto(\n                \"/client-loader-critical/initial-hydration-data-check-functions-properly-even-if-serverloader-isnt-called-on-hydration/parent/child\",\n              );\n              await page.waitForSelector(\"#child-data\");\n              let html = await app.getHtml();\n              expect(html).toMatch(\"Child Client Loader Data\");\n              app.clickElement(\"button\");\n              await page.waitForSelector(\n                ':has-text(\"Child Server Loader Data (2+)\")',\n              );\n              html = await app.getHtml(\"main\");\n              expect(html).toMatch(\n                \"Child Server Loader Data (2+) (mutated by client)\",\n              );\n            });\n\n            test(\"server loader errors are re-thrown from serverLoader()\", async ({\n              page,\n            }) => {\n              let _consoleError = console.error;\n              console.error = () => {};\n              let app = new PlaywrightFixture(appFixture, page);\n\n              await app.goto(\n                \"/client-loader-critical/server-loader-errors-are-re-thrown-from-serverloader/parent/child\",\n              );\n              let html = await app.getHtml(\"main\");\n              expect(html).toMatch(\"Broken!\");\n              // Ensure we hydrate and remain on the boundary\n              await new Promise((r) => setTimeout(r, 100));\n              html = await app.getHtml(\"main\");\n              expect(html).toMatch(\"Broken!\");\n              expect(html).not.toMatch(\"Should not see me\");\n              console.error = _consoleError;\n            });\n\n            test(\"bubbled server loader errors are persisted for hydrating routes\", async ({\n              page,\n            }) => {\n              // test.skip(browserName === \"firefox\", \"this test fails there due to extra debug logs.\")\n              let _consoleError = console.error;\n              console.error = () => {};\n              let app = new PlaywrightFixture(appFixture, page);\n              let logs: string[] = [];\n              page.on(\"console\", (msg) => {\n                if (msg.type() === \"timeStamp\") return;\n\n                let text = msg.text();\n                if (\n                  // Chrome logs the 500 as a console error, so skip that since it's not\n                  // what we are asserting against here\n                  /500 \\(Internal Server Error\\)/.test(text) ||\n                  // Ignore any dev tools messages. This may only happen locally when dev\n                  // tools is installed and not in CI but either way we don't care\n                  /Download the React DevTools/.test(text) ||\n                  (templateName.includes(\"rsc\") &&\n                    /The <Scripts \\/> element is a no-op when using RSC and can be safely removed./.test(\n                      text,\n                    ))\n                ) {\n                  return;\n                }\n                logs.push(text);\n              });\n              await app.goto(\n                \"/client-loader-critical/bubbled-server-loader-errors-are-persisted-for-hydrating-routes/parent/child\",\n                false,\n              );\n              let html = await app.getHtml(\"main\");\n              expect(html).toMatch(\"Parent Server Loader</p>\");\n              expect(html).toMatch(\"Child Server Error\");\n              expect(html).not.toMatch(\"Should not see me\");\n              // Ensure we hydrate and remain on the boundary\n              await page.waitForSelector(\n                \":has-text('Parent Server Loader (mutated by client)')\",\n              );\n              html = await app.getHtml(\"main\");\n              expect(html).toMatch(\n                \"Parent Server Loader (mutated by client)</p>\",\n              );\n              expect(html).toMatch(\"Child Server Error\");\n              expect(html).not.toMatch(\"Should not see me\");\n              expect(logs).toEqual([\"running parent client loader\"]);\n              console.error = _consoleError;\n            });\n\n            test(\"hydrating clientLoader redirects trigger new data requests to the server\", async ({\n              page,\n            }) => {\n              let app = new PlaywrightFixture(appFixture, page);\n\n              await app.goto(\n                \"/client-loader-critical/hydrating-clientloader-redirects-trigger-new-data-requests-to-the-server/parent/a\",\n              );\n              await page.waitForSelector(\"#b\");\n              // 1st route parent re-runs\n              await expect(page.locator(\"#parent-1-data\")).toHaveText(\"2\");\n              // But 2nd parent opted out of revalidation\n              await expect(page.locator(\"#parent-2-data\")).toHaveText(\"1\");\n              await expect(page.locator(\"#b\")).toHaveText(\"Hi!\");\n            });\n          });\n\n          test.describe(\"clientLoader - lazy route module\", () => {\n            test(\"no client loaders or fallbacks\", async ({ page }) => {\n              let app = new PlaywrightFixture(appFixture, page);\n              await app.goto(\"/\");\n              await app.clickLink(\n                \"/client-loader-lazy/no-client-loaders-or-fallbacks/parent/child\",\n              );\n              await page.waitForSelector(\"#child-data\");\n\n              // Normal Remix behavior due to lack of clientLoader\n              let html = await app.getHtml(\"main\");\n              expect(html).toMatch(\"Parent Server Loader\");\n              expect(html).toMatch(\"Child Server Loader\");\n            });\n\n            test(\"parent.clientLoader\", async ({ page }) => {\n              let app = new PlaywrightFixture(appFixture, page);\n              await app.goto(\"/\");\n              await app.clickLink(\n                \"/client-loader-lazy/parent-client-loader/parent/child\",\n              );\n              await page.waitForSelector(\"#child-data\");\n\n              let html = await app.getHtml(\"main\");\n              expect(html).toMatch(\"Parent Server Loader (mutated by client)\");\n              expect(html).toMatch(\"Child Server Loader\");\n            });\n\n            test(\"child.clientLoader\", async ({ page }) => {\n              let app = new PlaywrightFixture(appFixture, page);\n              await app.goto(\"/\");\n              await app.clickLink(\n                \"/client-loader-lazy/child-client-loader/parent/child\",\n              );\n              await page.waitForSelector(\"#child-data\");\n\n              let html = await app.getHtml(\"main\");\n              expect(html).toMatch(\"Parent Server Loader\");\n              expect(html).toMatch(\"Child Server Loader (mutated by client)\");\n            });\n\n            test(\"parent.clientLoader/child.clientLoader\", async ({ page }) => {\n              let app = new PlaywrightFixture(appFixture, page);\n              await app.goto(\"/\");\n              await app.clickLink(\n                \"/client-loader-lazy/parent-client-loader-child-client-loader/parent/child\",\n              );\n              await page.waitForSelector(\"#child-data\");\n\n              let html = await app.getHtml(\"main\");\n              expect(html).toMatch(\"Parent Server Loader (mutated by client)\");\n              expect(html).toMatch(\"Child Server Loader (mutated by client\");\n            });\n\n            test(\"throws a 400 if you call serverLoader without a server loader\", async ({\n              page,\n            }) => {\n              let app = new PlaywrightFixture(appFixture, page);\n\n              await app.goto(\"/\");\n              await app.clickLink(\n                \"/client-loader-lazy/throws-a-400-if-you-call-serverloader-without-a-server-loader/parent/child\",\n              );\n              await page.waitForSelector(\"#child-error\");\n              let html = await app.getHtml(\"#child-error\");\n              expect(html.replace(/\\n/g, \" \").replace(/ +/g, \" \")).toMatch(\n                \"400 Error: You are trying to call serverLoader() on a route that does \" +\n                  'not have a server loader (routeId: \"routes/client-loader-lazy.throws-a-400-if-you-call-serverloader-without-a-server-loader.parent.child\")',\n              );\n            });\n\n            test(\"does not prefetch server loader if a client loader is present\", async ({\n              page,\n              browserName,\n            }) => {\n              test.skip(\n                templateName.includes(\"rsc\"),\n                \"This test is specific to non-RSC Framework Mode\",\n              );\n\n              let dataUrls: string[] = [];\n              page.on(\"request\", (request) => {\n                let url = request.url();\n                if (url.includes(\".data\") || url.includes(\".rsc\")) {\n                  dataUrls.push(url);\n                }\n              });\n\n              let app = new PlaywrightFixture(appFixture, page);\n              await app.goto(\"/\", true);\n\n              if (browserName === \"webkit\") {\n                // No prefetch support :/\n                expect(dataUrls).toEqual([]);\n              } else {\n                // Only prefetch child server loader since parent has a `clientLoader`\n                expect(dataUrls).toEqual([\n                  expect.stringMatching(\n                    /client-loader-lazy\\/does-not-prefetch-server-loader-if-a-client-loader-is-present\\/parent\\/child\\.data\\?_routes=routes%2Fclient-loader-lazy\\.does-not-prefetch-server-loader-if-a-client-loader-is-present\\.parent\\.child/,\n                  ),\n                ]);\n              }\n            });\n          });\n\n          test.describe(\"clientAction - critical route module\", () => {\n            test(\"child.clientAction\", async ({ page }) => {\n              let app = new PlaywrightFixture(appFixture, page);\n              await app.goto(\n                \"/client-action-critical/child-client-action/parent/child\",\n              );\n              let html = await app.getHtml(\"main\");\n              expect(html).toMatch(\"Parent Server Loader\");\n              expect(html).toMatch(\"Child Server Loader\");\n              expect(html).not.toMatch(\"Child Server Action\");\n\n              app.clickSubmitButton(\n                \"/client-action-critical/child-client-action/parent/child\",\n              );\n              await page.waitForSelector(\"#child-action-data\");\n              html = await app.getHtml(\"main\");\n              expect(html).toMatch(\"Parent Server Loader\");\n              expect(html).toMatch(\"Child Server Loader\");\n              expect(html).toMatch(\"Child Server Action (mutated by client)\");\n            });\n\n            test(\"child.clientAction/parent.childLoader\", async ({ page }) => {\n              let app = new PlaywrightFixture(appFixture, page);\n              await app.goto(\n                \"/client-action-critical/child-client-action-parent-child-loader/parent/child\",\n              );\n              let html = await app.getHtml(\"main\");\n              expect(html).toMatch(\"Parent Server Loader\");\n              expect(html).toMatch(\"Child Server Loader\");\n              expect(html).not.toMatch(\"Child Server Action\");\n\n              app.clickSubmitButton(\n                \"/client-action-critical/child-client-action-parent-child-loader/parent/child\",\n              );\n              await page.waitForSelector(\"#child-action-data\");\n              html = await app.getHtml(\"main\");\n              expect(html).toMatch(\"Parent Server Loader\"); // still revalidating\n              expect(html).toMatch(\"Child Server Loader\");\n              expect(html).toMatch(\"Child Server Action (mutated by client)\");\n\n              await page.waitForSelector(\n                ':has-text(\"Parent Server Loader (mutated by client)\")',\n              );\n              html = await app.getHtml(\"main\");\n              expect(html).toMatch(\"Parent Server Loader (mutated by client)\");\n              expect(html).toMatch(\"Child Server Loader\");\n              expect(html).toMatch(\"Child Server Action (mutated by client)\");\n            });\n\n            test(\"child.clientAction/child.clientLoader\", async ({ page }) => {\n              let app = new PlaywrightFixture(appFixture, page);\n              await app.goto(\n                \"/client-action-critical/child-client-action-child-client-loader/parent/child\",\n              );\n              let html = await app.getHtml(\"main\");\n              expect(html).toMatch(\"Parent Server Loader\");\n              expect(html).toMatch(\"Child Server Loader\");\n              expect(html).not.toMatch(\"Child Server Action\");\n\n              app.clickSubmitButton(\n                \"/client-action-critical/child-client-action-child-client-loader/parent/child\",\n              );\n              await page.waitForSelector(\"#child-action-data\");\n              html = await app.getHtml(\"main\");\n              expect(html).toMatch(\"Parent Server Loader\"); // still revalidating\n              expect(html).toMatch(\"Child Server Loader\");\n              expect(html).toMatch(\"Child Server Action (mutated by client)\");\n\n              await page.waitForSelector(\n                ':has-text(\"Child Server Loader (mutated by client)\")',\n              );\n              html = await app.getHtml(\"main\");\n              expect(html).toMatch(\"Parent Server Loader\");\n              expect(html).toMatch(\"Child Server Loader (mutated by client)\");\n              expect(html).toMatch(\"Child Server Action (mutated by client)\");\n            });\n\n            test(\"child.clientAction/parent.childLoader/child.clientLoader\", async ({\n              page,\n            }) => {\n              let app = new PlaywrightFixture(appFixture, page);\n              await app.goto(\n                \"/client-action-critical/child-client-action-parent-child-loader-child-client-loader/parent/child\",\n              );\n              let html = await app.getHtml(\"main\");\n              expect(html).toMatch(\"Parent Server Loader\");\n              expect(html).toMatch(\"Child Server Loader\");\n              expect(html).not.toMatch(\"Child Server Action\");\n\n              app.clickSubmitButton(\n                \"/client-action-critical/child-client-action-parent-child-loader-child-client-loader/parent/child\",\n              );\n              await page.waitForSelector(\"#child-action-data\");\n              html = await app.getHtml(\"main\");\n              expect(html).toMatch(\"Parent Server Loader\"); // still revalidating\n              expect(html).toMatch(\"Child Server Loader\"); // still revalidating\n              expect(html).toMatch(\"Child Server Action (mutated by client)\");\n\n              await page.waitForSelector(\n                ':has-text(\"Child Server Loader (mutated by client)\")',\n              );\n              html = await app.getHtml(\"main\");\n              expect(html).toMatch(\"Parent Server Loader (mutated by client)\");\n              expect(html).toMatch(\"Child Server Loader (mutated by client)\");\n              expect(html).toMatch(\"Child Server Action (mutated by client)\");\n            });\n\n            test(\"throws a 400 if you call serverAction without a server action\", async ({\n              page,\n            }) => {\n              let app = new PlaywrightFixture(appFixture, page);\n              await app.goto(\n                \"/client-action-critical/throws-a-400-if-you-call-serveraction-without-a-server-action/parent/child\",\n              );\n              app.clickSubmitButton(\n                \"/client-action-critical/throws-a-400-if-you-call-serveraction-without-a-server-action/parent/child\",\n              );\n              await page.waitForSelector(\"#child-error\");\n              let html = await app.getHtml(\"#child-error\");\n              expect(html.replace(/\\n/g, \" \").replace(/ +/g, \" \")).toMatch(\n                \"400 Error: You are trying to call serverAction() on a route that does \" +\n                  'not have a server action (routeId: \"routes/client-action-critical.throws-a-400-if-you-call-serveraction-without-a-server-action.parent.child\")',\n              );\n            });\n          });\n\n          test.describe(\"clientAction - lazy route module\", () => {\n            test(\"child.clientAction\", async ({ page }) => {\n              let app = new PlaywrightFixture(appFixture, page);\n              await app.goto(\"/\");\n              await app.clickLink(\n                \"/client-action-lazy/child-client-action/parent/child\",\n              );\n              await page.waitForSelector(\"#child-data\");\n              let html = await app.getHtml(\"main\");\n              expect(html).toMatch(\"Parent Server Loader\");\n              expect(html).toMatch(\"Child Server Loader\");\n              expect(html).not.toMatch(\"Child Server Action\");\n\n              app.clickSubmitButton(\n                \"/client-action-lazy/child-client-action/parent/child\",\n              );\n              await page.waitForSelector(\"#child-action-data\");\n              html = await app.getHtml(\"main\");\n              expect(html).toMatch(\"Parent Server Loader\");\n              expect(html).toMatch(\"Child Server Loader\");\n              expect(html).toMatch(\"Child Server Action (mutated by client)\");\n            });\n\n            test(\"child.clientAction/parent.childLoader\", async ({ page }) => {\n              let app = new PlaywrightFixture(appFixture, page);\n              await app.goto(\"/\");\n              await app.clickLink(\n                \"/client-action-lazy/child-client-action-parent-child-loader/parent/child\",\n              );\n              await page.waitForSelector(\"#child-data\");\n              let html = await app.getHtml(\"main\");\n              expect(html).toMatch(\"Parent Server Loader\");\n              expect(html).toMatch(\"Child Server Loader\");\n              expect(html).not.toMatch(\"Child Server Action\");\n\n              app.clickSubmitButton(\n                \"/client-action-lazy/child-client-action-parent-child-loader/parent/child\",\n              );\n              await page.waitForSelector(\"#child-action-data\");\n              html = await app.getHtml(\"main\");\n              expect(html).toMatch(\"Parent Server Loader\"); // still revalidating\n              expect(html).toMatch(\"Child Server Loader\");\n              expect(html).toMatch(\"Child Server Action (mutated by client)\");\n\n              await page.waitForSelector(\n                ':has-text(\"Parent Server Loader (mutated by client)\")',\n              );\n              html = await app.getHtml(\"main\");\n              expect(html).toMatch(\"Parent Server Loader (mutated by client)\");\n              expect(html).toMatch(\"Child Server Loader\");\n              expect(html).toMatch(\"Child Server Action (mutated by client)\");\n            });\n\n            test(\"child.clientAction/child.clientLoader\", async ({ page }) => {\n              let app = new PlaywrightFixture(appFixture, page);\n              await app.goto(\"/\");\n              await app.clickLink(\n                \"/client-action-lazy/child-client-action-child-client-loader/parent/child\",\n              );\n              await page.waitForSelector(\"#child-data\");\n              let html = await app.getHtml(\"main\");\n              expect(html).toMatch(\"Parent Server Loader\");\n              expect(html).toMatch(\"Child Server Loader\");\n              expect(html).not.toMatch(\"Child Server Action\");\n\n              app.clickSubmitButton(\n                \"/client-action-lazy/child-client-action-child-client-loader/parent/child\",\n              );\n              await page.waitForSelector(\"#child-action-data\");\n              html = await app.getHtml(\"main\");\n              expect(html).toMatch(\"Parent Server Loader\"); // still revalidating\n              expect(html).toMatch(\"Child Server Loader\");\n              expect(html).toMatch(\"Child Server Action (mutated by client)\");\n\n              await page.waitForSelector(\n                ':has-text(\"Child Server Loader (mutated by client)\")',\n              );\n              html = await app.getHtml(\"main\");\n              expect(html).toMatch(\"Parent Server Loader\");\n              expect(html).toMatch(\"Child Server Loader (mutated by client)\");\n              expect(html).toMatch(\"Child Server Action (mutated by client)\");\n            });\n\n            test(\"child.clientAction/parent.childLoader/child.clientLoader\", async ({\n              page,\n            }) => {\n              let app = new PlaywrightFixture(appFixture, page);\n              await app.goto(\"/\");\n              await app.clickLink(\n                \"/client-action-lazy/child-client-action-parent-child-loader-child-client-loader/parent/child\",\n              );\n              await page.waitForSelector(\"#child-data\");\n              let html = await app.getHtml(\"main\");\n              expect(html).toMatch(\"Parent Server Loader\");\n              expect(html).toMatch(\"Child Server Loader\");\n              expect(html).not.toMatch(\"Child Server Action\");\n\n              app.clickSubmitButton(\n                \"/client-action-lazy/child-client-action-parent-child-loader-child-client-loader/parent/child\",\n              );\n              await page.waitForSelector(\"#child-action-data\");\n              html = await app.getHtml(\"main\");\n              expect(html).toMatch(\"Parent Server Loader\"); // still revalidating\n              expect(html).toMatch(\"Child Server Loader\"); // still revalidating\n              expect(html).toMatch(\"Child Server Action (mutated by client)\");\n\n              await page.waitForSelector(\n                ':has-text(\"Child Server Loader (mutated by client)\")',\n              );\n              html = await app.getHtml(\"main\");\n              expect(html).toMatch(\"Parent Server Loader (mutated by client)\");\n              expect(html).toMatch(\"Child Server Loader (mutated by client)\");\n              expect(html).toMatch(\"Child Server Action (mutated by client)\");\n            });\n\n            test(\"throws a 400 if you call serverAction without a server action\", async ({\n              page,\n            }) => {\n              let app = new PlaywrightFixture(appFixture, page);\n              await app.goto(\"/\");\n              await app.goto(\n                \"/client-action-lazy/throws-a-400-if-you-call-serveraction-without-a-server-action/parent/child\",\n              );\n              await page.waitForSelector(\"form\");\n              app.clickSubmitButton(\n                \"/client-action-lazy/throws-a-400-if-you-call-serveraction-without-a-server-action/parent/child\",\n              );\n              await page.waitForSelector(\"#child-error\");\n              let html = await app.getHtml(\"#child-error\");\n              expect(html.replace(/\\n/g, \" \").replace(/ +/g, \" \")).toMatch(\n                \"400 Error: You are trying to call serverAction() on a route that does \" +\n                  'not have a server action (routeId: \"routes/client-action-lazy.throws-a-400-if-you-call-serveraction-without-a-server-action.parent.child\")',\n              );\n            });\n          });\n        });\n      }\n    });\n  }\n});\n"
  },
  {
    "path": "integration/custom-entry-server-test.ts",
    "content": "import { expect, test } from \"@playwright/test\";\n\nimport { PlaywrightFixture } from \"./helpers/playwright-fixture.js\";\nimport type { Fixture, AppFixture } from \"./helpers/create-fixture.js\";\nimport {\n  createAppFixture,\n  createFixture,\n  js,\n} from \"./helpers/create-fixture.js\";\n\nlet fixture: Fixture;\nlet appFixture: AppFixture;\n\ntest.beforeAll(async () => {\n  fixture = await createFixture({\n    files: {\n      \"app/entry.server.tsx\": js`\n        import * as React from \"react\";\n        import { ServerRouter } from \"react-router\";\n        import { renderToString } from \"react-dom/server\";\n\n        export default function handleRequest(\n          request,\n          responseStatusCode,\n          responseHeaders,\n          remixContext\n        ) {\n          let markup = renderToString(\n            <ServerRouter context={remixContext} url={request.url} />\n          );\n          responseHeaders.set(\"Content-Type\", \"text/html\");\n          responseHeaders.set(\"x-custom-header\", \"custom-value\");\n          return new Response('<!doctype html>' + markup, {\n            headers: responseHeaders,\n            status: responseStatusCode,\n          });\n        }\n      `,\n      \"app/routes/_index.tsx\": js`\n        export default function Index() {\n          return <h1>Hello World</h1>\n        }\n      `,\n    },\n  });\n\n  appFixture = await createAppFixture(fixture);\n});\n\ntest.afterAll(() => {\n  appFixture.close();\n});\n\ntest(\"allows user specified entry.server\", async ({ page }) => {\n  let app = new PlaywrightFixture(appFixture, page);\n  let responses = app.collectResponses((url) => url.pathname === \"/\");\n  await app.goto(\"/\");\n  let header = await responses[0].headerValues(\"x-custom-header\");\n  expect(header).toEqual([\"custom-value\"]);\n});\n"
  },
  {
    "path": "integration/deduped-route-modules-test.ts",
    "content": "import { test, expect } from \"@playwright/test\";\n\nimport { createFixture, createAppFixture } from \"./helpers/create-fixture.js\";\nimport type { Fixture, AppFixture } from \"./helpers/create-fixture.js\";\nimport { PlaywrightFixture } from \"./helpers/playwright-fixture.js\";\nimport { type TemplateName, viteConfig } from \"./helpers/vite.js\";\n\nconst templateNames = [\n  \"vite-5-template\",\n  \"rsc-vite-framework\",\n] as const satisfies TemplateName[];\n\n// This test ensures that code is not accidentally duplicated when a route is\n// imported within user code since they're not importing one of our internal\n// virtual route modules.\ntest.describe(\"Deduped route modules\", () => {\n  for (const templateName of templateNames) {\n    test.describe(`template: ${templateName}`, () => {\n      let fixture: Fixture;\n      let appFixture: AppFixture;\n\n      test.beforeAll(async () => {\n        fixture = await createFixture({\n          templateName,\n          files: {\n            \"vite.config.js\": await viteConfig.basic({\n              templateName,\n            }),\n            \"app/routes/client-first.a.tsx\": `\n              import { Link } from \"react-router\";\n\n              export const customExport = (() => {\n                globalThis.custom_export_count = (globalThis.custom_export_count || 0) + 1;\n                return () => true;\n              })();\n\n              export const loader = (() => {\n                globalThis.loader_count = (globalThis.loader_count || 0) + 1;\n                return () => ({\n                  customExportCount: globalThis.custom_export_count,\n                  loaderCount: globalThis.loader_count,\n                  componentCount: globalThis.component_count,\n                });\n              })();\n\n              export const clientLoader = (() => {\n                globalThis.client_loader_count = (globalThis.client_loader_count || 0) + 1;\n                return async ({ serverLoader }) => {\n                  const loaderData = await serverLoader();\n                  return {\n                    loaderCount: loaderData.loaderCount,\n                    clientLoaderCount: globalThis.client_loader_count,\n                    serverCustomExportCount: loaderData.customExportCount,\n                    clientCustomExportCount: globalThis.custom_export_count,\n                    serverComponentCount: loaderData.componentCount,\n                    clientComponentCount: globalThis.component_count,\n                  };\n                };\n              })();\n              clientLoader.hydrate = true;\n\n              const RouteA = (() => {\n                globalThis.component_count = (globalThis.component_count || 0) + 1;\n                return ({ loaderData }: Route.ComponentProps) => {\n                  return (\n                    <>\n                      <h1>Module Count</h1>\n                      <p>Loader count: <span data-loader-count>{loaderData.loaderCount}</span></p>\n                      <p>Client loader count: <span data-client-loader-count>{loaderData.clientLoaderCount}</span></p>\n                      <p>Server custom export count: <span data-server-custom-export-count>{loaderData.serverCustomExportCount}</span></p>\n                      <p>Client custom export count: <span data-client-custom-export-count>{loaderData.clientCustomExportCount}</span></p>\n                      <p>Server component count: <span data-server-component-count>{loaderData.serverComponentCount}</span></p>\n                      <p>Client component count: <span data-client-component-count>{loaderData.clientComponentCount}</span></p>\n                      <p><Link to=\"/client-first/b\">Go to Route B</Link></p>\n                    </>\n                  );\n                };\n              })();\n\n              export default RouteA;\n            `,\n            \"app/routes/client-first.b.tsx\": `\n              import { Link } from \"react-router\";\n\n              import { customExport } from \"./client-first.a\";\n          \n              export default function RouteB() {\n                return customExport && (\n                  <>\n                    <h1>Route B</h1>\n                    <p>This route imports the route module from Route A, so could potentially cause code duplication.</p>\n                    <p><Link to=\"/client-first/a\">Go to Route A</Link></p>\n                  </>\n                );\n              }\n            `,\n\n            ...(templateName.includes(\"rsc\")\n              ? {\n                  \"app/routes/rsc-server-first.a/route.tsx\": `\n                    import { Link } from \"react-router\";\n                    import { ModuleCounts, clientLoader } from \"./client\";\n        \n                    export const customExport = (() => {\n                      globalThis.rsc_custom_export_count = (globalThis.rsc_custom_export_count || 0) + 1;\n                      return () => true;\n                    })();\n        \n                    export const loader = (() => {\n                      globalThis.rsc_loader_count = (globalThis.rsc_loader_count || 0) + 1;\n                      return () => ({\n                        customExportCount: globalThis.rsc_custom_export_count,\n                        loaderCount: globalThis.rsc_loader_count,\n                        componentCount: globalThis.rsc_component_count,\n                      });\n                    })();\n\n                    export { clientLoader };\n        \n                    export const ServerComponent = (() => {\n                      globalThis.rsc_component_count = (globalThis.rsc_component_count || 0) + 1;\n                      return () => {\n                        return (\n                          <>\n                            <h1>RSC Server-First Module Count</h1>\n                            <ModuleCounts />\n                            <p><Link to=\"/rsc-server-first/b\">Go to RSC Route B</Link></p>\n                          </>\n                        );\n                      };\n                    })();\n                  `,\n                  \"app/routes/rsc-server-first.a/client.tsx\": `\n                    \"use client\";\n\n                    import { useLoaderData } from \"react-router\";\n\n                    export const clientLoader = (() => {\n                      globalThis.rsc_client_loader_count = (globalThis.rsc_client_loader_count || 0) + 1;\n                      return async ({ serverLoader }) => {\n                        const loaderData = await serverLoader();\n                        return {\n                          loaderCount: loaderData.loaderCount,\n                          clientLoaderCount: globalThis.rsc_client_loader_count,\n                          serverCustomExportCount: loaderData.customExportCount,\n                          clientCustomExportCount: globalThis.rsc_custom_export_count,\n                          serverComponentCount: loaderData.componentCount,\n                        };\n                      };\n                    })();\n                    clientLoader.hydrate = true;\n                  \n                    export function ModuleCounts() {\n                      const loaderData = useLoaderData();\n                      return (\n                        <>\n                          <p>Loader count: <span data-loader-count>{loaderData.loaderCount}</span></p>\n                          <p>Client loader count: <span data-client-loader-count>{loaderData.clientLoaderCount}</span></p>\n                          <p>Server custom export count: <span data-server-custom-export-count>{loaderData.serverCustomExportCount}</span></p>\n                          <p>Client custom export count: <span data-client-custom-export-count>{loaderData.clientCustomExportCount}</span></p>\n                          <p>Server component count: <span data-server-component-count>{loaderData.serverComponentCount}</span></p>\n                        </>\n                      );\n                    }\n                  `,\n                  \"app/routes/rsc-server-first.b.tsx\": `\n                    import { Link } from \"react-router\";\n        \n                    import { customExport } from \"./rsc-server-first.a/route\";\n\n                    // Ensure custom export is used in the client build in this route\n                    export const handle = customExport;\n\n                    export function ServerComponent() {\n                      return customExport && (\n                        <>\n                          <h1>RSC Route B</h1>\n                          <p>This route imports the route module from RSC Route A, so could potentially cause code duplication.</p>\n                          <p><Link to=\"/rsc-server-first/a\">Go to RSC Route A</Link></p>\n                        </>\n                      );\n                    }\n                  `,\n                }\n              : {}),\n          },\n        });\n\n        appFixture = await createAppFixture(fixture);\n      });\n\n      test.afterAll(() => {\n        appFixture.close();\n      });\n\n      let logs: string[] = [];\n\n      test.beforeEach(({ page }) => {\n        page.on(\"console\", (msg) => {\n          logs.push(msg.text());\n        });\n      });\n\n      test.afterEach(() => {\n        expect(logs).toHaveLength(0);\n      });\n\n      test(\"Client-first routes\", async ({ page }) => {\n        let app = new PlaywrightFixture(appFixture, page);\n\n        let pageErrors: unknown[] = [];\n        page.on(\"pageerror\", (error) => pageErrors.push(error));\n\n        await app.goto(`/client-first/b`, true);\n        expect(pageErrors).toEqual([]);\n\n        await app.clickLink(\"/client-first/a\");\n        await page.waitForSelector(\"[data-loader-count]\");\n        expect(await page.locator(\"[data-loader-count]\").textContent()).toBe(\n          \"1\",\n        );\n        expect(\n          await page.locator(\"[data-client-loader-count]\").textContent(),\n        ).toBe(\"1\");\n        expect(\n          await page.locator(\"[data-server-custom-export-count]\").textContent(),\n        ).toBe(\n          templateName.includes(\"rsc\")\n            ? // In RSC, custom exports are present in both the react-server and react-client\n              // environments (so they're available to be imported by both),\n              // which means the Node server actually gets 2 copies\n              \"2\"\n            : \"1\",\n        );\n        expect(\n          await page.locator(\"[data-client-custom-export-count]\").textContent(),\n        ).toBe(\"1\");\n        expect(\n          await page.locator(\"[data-server-component-count]\").textContent(),\n        ).toBe(\"1\");\n        expect(\n          await page.locator(\"[data-client-component-count]\").textContent(),\n        ).toBe(\"1\");\n        expect(pageErrors).toEqual([]);\n      });\n\n      test(\"Server-first routes\", async ({ page }) => {\n        test.skip(\n          !templateName.includes(\"rsc\"),\n          \"Server-first routes are an RSC-only feature\",\n        );\n\n        let app = new PlaywrightFixture(appFixture, page);\n\n        let pageErrors: unknown[] = [];\n        page.on(\"pageerror\", (error) => pageErrors.push(error));\n\n        await app.goto(`/rsc-server-first/b`, true);\n        expect(pageErrors).toEqual([]);\n\n        await app.clickLink(\"/rsc-server-first/a\");\n        await page.waitForSelector(\"[data-loader-count]\");\n        expect(await page.locator(\"[data-loader-count]\").textContent()).toBe(\n          \"1\",\n        );\n        expect(\n          await page.locator(\"[data-client-loader-count]\").textContent(),\n        ).toBe(\"1\");\n        expect(\n          await page.locator(\"[data-server-custom-export-count]\").textContent(),\n        ).toBe(\n          // In RSC, custom exports are present in both the react-server and react-client\n          // environments (so they're available to be imported by both),\n          // which means the Node server actually gets 2 copies\n          \"2\",\n        );\n        expect(\n          await page.locator(\"[data-client-custom-export-count]\").textContent(),\n        ).toBe(\"1\");\n        expect(\n          await page.locator(\"[data-server-component-count]\").textContent(),\n        ).toBe(\"1\");\n        expect(pageErrors).toEqual([]);\n      });\n    });\n  }\n});\n"
  },
  {
    "path": "integration/defer-loader-test.ts",
    "content": "import { test, expect } from \"@playwright/test\";\n\nimport {\n  createAppFixture,\n  createFixture,\n  js,\n} from \"./helpers/create-fixture.js\";\nimport type { Fixture, AppFixture } from \"./helpers/create-fixture.js\";\nimport { PlaywrightFixture } from \"./helpers/playwright-fixture.js\";\n\nlet fixture: Fixture;\nlet appFixture: AppFixture;\n\ntest.describe(\"deferred loaders\", () => {\n  test.beforeAll(async () => {\n    fixture = await createFixture({\n      files: {\n        \"app/routes/_index.tsx\": js`\n          import { useLoaderData, Link } from \"react-router\";\n          export default function Index() {\n            return (\n              <div>\n                <Link to=\"/redirect\">Redirect</Link>\n                <Link to=\"/direct-promise-access\">Direct Promise Access</Link>\n              </div>\n            )\n          }\n        `,\n\n        \"app/routes/redirect.tsx\": js`\n          import { data } from 'react-router';\n          export function loader() {\n            return data(\n              { food: \"pizza\" },\n              {\n                status: 301,\n                headers: {\n                  Location: \"/?redirected\"\n                }\n              }\n            );\n          }\n          export default function Redirect() {\n            return null;\n          }\n        `,\n\n        \"app/routes/direct-promise-access.tsx\": js`\n          import * as React from \"react\";\n          import { useLoaderData, Link, Await } from \"react-router\";\n          export function loader() {\n            return {\n              bar: new Promise(async (resolve, reject) => {\n                resolve(\"hamburger\");\n              }),\n            };\n          }\n          let count = 0;\n          export default function Index() {\n            let {bar} = useLoaderData();\n            React.useEffect(() => {\n              let aborted = false;\n              bar.then((data) => {\n                if (aborted) return;\n                document.getElementById(\"content\").innerHTML = data + \" \" + (++count);\n                document.getElementById(\"content\").setAttribute(\"data-done\", \"\");\n              });\n              return () => {\n                aborted = true;\n              };\n            }, [bar]);\n            return (\n              <div id=\"content\">\n                Waiting for client hydration....\n              </div>\n            )\n          }\n        `,\n      },\n    });\n\n    appFixture = await createAppFixture(fixture);\n  });\n\n  test.afterAll(async () => appFixture.close());\n\n  test(\"deferred response can redirect on document request\", async ({\n    page,\n  }) => {\n    let app = new PlaywrightFixture(appFixture, page);\n    await app.goto(\"/redirect\");\n    await page.waitForURL(/\\?redirected/);\n  });\n\n  test(\"deferred response can redirect on transition\", async ({ page }) => {\n    let app = new PlaywrightFixture(appFixture, page);\n    await app.goto(\"/\");\n    await app.clickLink(\"/redirect\");\n    await page.waitForURL(/\\?redirected/);\n  });\n\n  test(\"can directly access result from deferred promise on document request\", async ({\n    page,\n  }) => {\n    let app = new PlaywrightFixture(appFixture, page);\n    await app.goto(\"/direct-promise-access\");\n    let element = await page.waitForSelector(\"[data-done]\");\n    expect(await element.innerText()).toMatch(\"hamburger 1\");\n  });\n});\n"
  },
  {
    "path": "integration/defer-test.ts",
    "content": "import { test, expect } from \"@playwright/test\";\nimport type { ConsoleMessage, Page } from \"@playwright/test\";\n\nimport { PlaywrightFixture } from \"./helpers/playwright-fixture.js\";\nimport type { Fixture, AppFixture } from \"./helpers/create-fixture.js\";\nimport {\n  createAppFixture,\n  createFixture,\n  js,\n} from \"./helpers/create-fixture.js\";\n\nconst ROOT_ID = \"ROOT_ID\";\nconst INDEX_ID = \"INDEX_ID\";\nconst DEFERRED_ID = \"DEFERRED_ID\";\nconst RESOLVED_DEFERRED_ID = \"RESOLVED_DEFERRED_ID\";\nconst FALLBACK_ID = \"FALLBACK_ID\";\nconst ERROR_ID = \"ERROR_ID\";\nconst ERROR_BOUNDARY_ID = \"ERROR_BOUNDARY_ID\";\nconst MANUAL_RESOLVED_ID = \"MANUAL_RESOLVED_ID\";\nconst MANUAL_FALLBACK_ID = \"MANUAL_FALLBACK_ID\";\nconst MANUAL_ERROR_ID = \"MANUAL_ERROR_ID\";\n\nlet originalConsoleError: typeof console.error;\n\ndeclare global {\n  var __deferredManualResolveCache: {\n    nextId: number;\n    deferreds: Record<\n      string,\n      { resolve: (value: any) => void; reject: (error: Error) => void }\n    >;\n  };\n}\n\ntest.describe(\"non-aborted\", () => {\n  let fixture: Fixture;\n  let appFixture: AppFixture;\n\n  test.beforeEach(async ({ context }) => {\n    await context.route(/.data/, async (route) => {\n      await new Promise((resolve) => setTimeout(resolve, 50));\n      route.continue();\n    });\n  });\n\n  test.beforeAll(async () => {\n    fixture = await createFixture({\n      files: {\n        \"app/components/counter.tsx\": js`\n          import { useState } from \"react\";\n\n          export default function Counter({ id }) {\n            let [count, setCount] = useState(0);\n            return (\n              <div>\n                <button id={\"increment-\"+id} onClick={() => setCount((c) => c+1)}>Increment</button>\n                <p id={\"count-\"+id}>{count}</p>\n              </div>\n            )\n          }\n        `,\n        \"app/components/interactive.tsx\": js`\n          import { useEffect, useState } from \"react\";\n\n          export default function Interactive() {\n            let [interactive, setInteractive] = useState(false);\n            useEffect(() => {\n              setInteractive(true);\n            }, []);\n            return interactive ? (\n              <div id=\"interactive\">\n                <p>interactive</p>\n              </div>\n            ) : null;\n          }\n        `,\n        \"app/root.tsx\": js`\n          import { Links, Meta, Outlet, Scripts, useLoaderData } from \"react-router\";\n          import Counter from \"~/components/counter\";\n          import Interactive from \"~/components/interactive\";\n\n          export const meta: MetaFunction = () => {\n            return [{ title: \"New Remix App\" }];\n          };\n\n          export const loader = () => ({\n            id: \"${ROOT_ID}\",\n          });\n\n          export default function Root() {\n            let { id } = useLoaderData();\n            return (\n              <html lang=\"en\">\n                <head>\n                  <meta charSet=\"utf-8\" />\n                  <meta name=\"viewport\" content=\"width=device-width,initial-scale=1\" />\n                  <Meta />\n                  <Links />\n                </head>\n                <body>\n                  <div id={id}>\n                    <p>{id}</p>\n                    <Counter id={id} />\n                    <Outlet />\n                    <Interactive />\n                  </div>\n                  <Scripts />\n                  {/* Send arbitrary data so safari renders the initial shell before\n                      the document finishes downloading. */}\n                  {Array(10000).fill(null).map((_, i)=><p key={i}>YOOOOOOOOOO   {i}</p>)}\n                </body>\n              </html>\n            );\n          }\n        `,\n\n        \"app/routes/_index.tsx\": js`\n          import { Link, useLoaderData } from \"react-router\";\n          import Counter from \"~/components/counter\";\n\n          export function loader() {\n            return {\n              id: \"${INDEX_ID}\",\n            };\n          }\n\n          export default function Index() {\n            let { id } = useLoaderData();\n            return (\n              <div id={id}>\n                <p>{id}</p>\n                <Counter id={id} />\n\n                <ul>\n                  <li><Link to=\"/deferred-script-resolved\">deferred-script-resolved</Link></li>\n                  <li><Link to=\"/deferred-script-unresolved\">deferred-script-unresolved</Link></li>\n                  <li><Link to=\"/deferred-script-rejected\">deferred-script-rejected</Link></li>\n                  <li><Link to=\"/deferred-script-unrejected\">deferred-script-unrejected</Link></li>\n                  <li><Link to=\"/deferred-script-rejected-no-error-element\">deferred-script-rejected-no-error-element</Link></li>\n                  <li><Link to=\"/deferred-script-unrejected-no-error-element\">deferred-script-unrejected-no-error-element</Link></li>\n                </ul>\n              </div>\n            );\n          }\n        `,\n\n        \"app/routes/deferred-noscript-resolved.tsx\": js`\n          import { Suspense } from \"react\";\n          import { Await, Link, useLoaderData } from \"react-router\";\n          import Counter from \"~/components/counter\";\n\n          export function loader() {\n            return {\n              deferredId: \"${DEFERRED_ID}\",\n              resolvedId: Promise.resolve(\"${RESOLVED_DEFERRED_ID}\"),\n            };\n          }\n\n          export default function Deferred() {\n            let { deferredId, resolvedId } = useLoaderData();\n            return (\n              <div id={deferredId}>\n                <p>{deferredId}</p>\n                <Counter id={deferredId} />\n                <Suspense fallback={<div id=\"${FALLBACK_ID}\">fallback</div>}>\n                  <Await\n                    resolve={resolvedId}\n                    children={(resolvedDeferredId) => (\n                      <div id={resolvedDeferredId}>\n                        <p>{resolvedDeferredId}</p>\n                        <Counter id={resolvedDeferredId} />\n                      </div>\n                    )}\n                  />\n                </Suspense>\n              </div>\n            );\n          }\n        `,\n\n        \"app/routes/deferred-noscript-unresolved.tsx\": js`\n          import { Suspense } from \"react\";\n          import { Await, Link, useLoaderData } from \"react-router\";\n          import Counter from \"~/components/counter\";\n\n          export function loader() {\n            return {\n              deferredId: \"${DEFERRED_ID}\",\n              resolvedId: new Promise(\n                (resolve) => setTimeout(() => {\n                  resolve(\"${RESOLVED_DEFERRED_ID}\");\n                }, 10)\n              ),\n            };\n          }\n\n          export default function Deferred() {\n            let { deferredId, resolvedId } = useLoaderData();\n            return (\n              <div id={deferredId}>\n                <p>{deferredId}</p>\n                <Counter id={deferredId} />\n                <Suspense fallback={<div id=\"${FALLBACK_ID}\">fallback</div>}>\n                  <Await\n                    resolve={resolvedId}\n                    children={(resolvedDeferredId) => (\n                      <div id={resolvedDeferredId}>\n                        <p>{resolvedDeferredId}</p>\n                        <Counter id={resolvedDeferredId} />\n                      </div>\n                    )}\n                  />\n                </Suspense>\n              </div>\n            );\n          }\n        `,\n\n        \"app/routes/deferred-script-resolved.tsx\": js`\n          import { Suspense } from \"react\";\n          import { Await, Link, useLoaderData } from \"react-router\";\n          import Counter from \"~/components/counter\";\n\n          export function loader() {\n            return {\n              deferredId: \"${DEFERRED_ID}\",\n              resolvedId: Promise.resolve(\"${RESOLVED_DEFERRED_ID}\"),\n              deferredUndefined: Promise.resolve(undefined),\n            };\n          }\n\n          export default function Deferred() {\n            let { deferredId, resolvedId } = useLoaderData();\n            return (\n              <div id={deferredId}>\n                <p>{deferredId}</p>\n                <Counter id={deferredId} />\n                <Suspense fallback={<div id=\"${FALLBACK_ID}\">fallback</div>}>\n                  <Await\n                    resolve={resolvedId}\n                    children={(resolvedDeferredId) => (\n                      <div id={resolvedDeferredId}>\n                        <p>{resolvedDeferredId}</p>\n                        <Counter id={resolvedDeferredId} />\n                      </div>\n                    )}\n                  />\n                </Suspense>\n              </div>\n            );\n          }\n        `,\n\n        \"app/routes/deferred-script-unresolved.tsx\": js`\n          import { Suspense } from \"react\";\n          import { Await, Link, useLoaderData } from \"react-router\";\n          import Counter from \"~/components/counter\";\n\n          export function loader() {\n            return {\n              deferredId: \"${DEFERRED_ID}\",\n              resolvedId: new Promise(\n                (resolve) => setTimeout(() => {\n                  resolve(\"${RESOLVED_DEFERRED_ID}\");\n                }, 10)\n              ),\n              deferredUndefined: new Promise(\n                (resolve) => setTimeout(() => {\n                  resolve(undefined);\n                }, 10)\n              ),\n            };\n          }\n\n          export default function Deferred() {\n            let { deferredId, resolvedId } = useLoaderData();\n            return (\n              <div id={deferredId}>\n                <p>{deferredId}</p>\n                <Counter id={deferredId} />\n                <Suspense fallback={<div id=\"${FALLBACK_ID}\">fallback</div>}>\n                  <Await\n                    resolve={resolvedId}\n                    children={(resolvedDeferredId) => (\n                      <div id={resolvedDeferredId}>\n                        <p>{resolvedDeferredId}</p>\n                        <Counter id={resolvedDeferredId} />\n                      </div>\n                    )}\n                  />\n                </Suspense>\n              </div>\n            );\n          }\n        `,\n\n        \"app/routes/deferred-script-rejected.tsx\": js`\n          import { Suspense } from \"react\";\n          import { Await, Link, useLoaderData } from \"react-router\";\n          import Counter from \"~/components/counter\";\n\n          export function loader() {\n            return {\n              deferredId: \"${DEFERRED_ID}\",\n              resolvedId: Promise.reject(new Error(\"${RESOLVED_DEFERRED_ID}\")),\n            };\n          }\n\n          export default function Deferred() {\n            let { deferredId, resolvedId } = useLoaderData();\n            return (\n              <div id={deferredId}>\n                <p>{deferredId}</p>\n                <Counter id={deferredId} />\n                <Suspense fallback={<div id=\"${FALLBACK_ID}\">fallback</div>}>\n                  <Await\n                    resolve={resolvedId}\n                    errorElement={\n                      <div id=\"${ERROR_ID}\">\n                        error\n                        <Counter id=\"${ERROR_ID}\" />\n                      </div>\n                    }\n                    children={(resolvedDeferredId) => (\n                      <div id={resolvedDeferredId}>\n                        <p>{resolvedDeferredId}</p>\n                        <Counter id={resolvedDeferredId} />\n                      </div>\n                    )}\n                  />\n                </Suspense>\n              </div>\n            );\n          }\n        `,\n\n        \"app/routes/deferred-script-unrejected.tsx\": js`\n          import { Suspense } from \"react\";\n          import { Await, Link, useLoaderData } from \"react-router\";\n          import Counter from \"~/components/counter\";\n\n          export function loader() {\n            return {\n              deferredId: \"${DEFERRED_ID}\",\n              resolvedId: new Promise(\n                (_, reject) => setTimeout(() => {\n                  reject(new Error(\"${RESOLVED_DEFERRED_ID}\"));\n                }, 10)\n              ),\n            };\n          }\n\n          export default function Deferred() {\n            let { deferredId, resolvedId, resolvedUndefined } = useLoaderData();\n            return (\n              <div id={deferredId}>\n                <p>{deferredId}</p>\n                <Counter id={deferredId} />\n                <Suspense fallback={<div id=\"${FALLBACK_ID}\">fallback</div>}>\n                  <Await\n                    resolve={resolvedId}\n                    errorElement={\n                      <div id=\"${ERROR_ID}\">\n                        error\n                        <Counter id=\"${ERROR_ID}\" />\n                      </div>\n                    }\n                    children={(resolvedDeferredId) => (\n                      <div id={resolvedDeferredId}>\n                        <p>{resolvedDeferredId}</p>\n                        <Counter id={resolvedDeferredId} />\n                      </div>\n                    )}\n                  />\n                </Suspense>\n              </div>\n            );\n          }\n        `,\n\n        \"app/routes/deferred-script-rejected-no-error-element.tsx\": js`\n          import { Suspense } from \"react\";\n          import { Await, Link, useLoaderData } from \"react-router\";\n          import Counter from \"~/components/counter\";\n\n          export function loader() {\n            return {\n              deferredId: \"${DEFERRED_ID}\",\n              resolvedId: Promise.reject(new Error(\"${RESOLVED_DEFERRED_ID}\")),\n            };\n          }\n\n          export default function Deferred() {\n            let { deferredId, resolvedId } = useLoaderData();\n            return (\n              <div id={deferredId}>\n                <p>{deferredId}</p>\n                <Counter id={deferredId} />\n                <Suspense fallback={<div id=\"${FALLBACK_ID}\">fallback</div>}>\n                  <Await\n                    resolve={resolvedId}\n                    children={(resolvedDeferredId) => (\n                      <div id={resolvedDeferredId}>\n                        <p>{resolvedDeferredId}</p>\n                        <Counter id={resolvedDeferredId} />\n                      </div>\n                    )}\n                  />\n                </Suspense>\n              </div>\n            );\n          }\n\n          export function ErrorBoundary() {\n            return (\n              <div id=\"${ERROR_BOUNDARY_ID}\">\n                error\n                <Counter id=\"${ERROR_BOUNDARY_ID}\" />\n              </div>\n            );\n          }\n        `,\n\n        \"app/routes/deferred-script-unrejected-no-error-element.tsx\": js`\n          import { Suspense } from \"react\";\n          import { Await, Link, useLoaderData } from \"react-router\";\n          import Counter from \"~/components/counter\";\n\n          export function loader() {\n            return {\n              deferredId: \"${DEFERRED_ID}\",\n              resolvedId: new Promise(\n                (_, reject) => setTimeout(() => {\n                  reject(new Error(\"${RESOLVED_DEFERRED_ID}\"));\n                }, 10)\n              ),\n            };\n          }\n\n          export default function Deferred() {\n            let { deferredId, resolvedId } = useLoaderData();\n            return (\n              <div id={deferredId}>\n                <p>{deferredId}</p>\n                <Counter id={deferredId} />\n                <Suspense fallback={<div id=\"${FALLBACK_ID}\">fallback</div>}>\n                  <Await\n                    resolve={resolvedId}\n                    children={(resolvedDeferredId) => (\n                      <div id={resolvedDeferredId}>\n                        <p>{resolvedDeferredId}</p>\n                        <Counter id={resolvedDeferredId} />\n                      </div>\n                    )}\n                  />\n                </Suspense>\n              </div>\n            );\n          }\n\n          export function ErrorBoundary() {\n            return (\n              <div id=\"${ERROR_BOUNDARY_ID}\">\n                error\n                <Counter id=\"${ERROR_BOUNDARY_ID}\" />\n              </div>\n            );\n          }\n        `,\n\n        \"app/routes/deferred-manual-resolve.tsx\": js`\n          import { Suspense } from \"react\";\n          import { Await, Link, useLoaderData } from \"react-router\";\n          import Counter from \"~/components/counter\";\n\n          export function loader() {\n            global.__deferredManualResolveCache = global.__deferredManualResolveCache || {\n              nextId: 1,\n              deferreds: {},\n            };\n\n            let id = \"\" + global.__deferredManualResolveCache.nextId++;\n            let promise = new Promise((resolve, reject) => {\n              global.__deferredManualResolveCache.deferreds[id] = { resolve, reject };\n            });\n\n            return {\n              deferredId: \"${DEFERRED_ID}\",\n              resolvedId: new Promise(\n                (resolve) => setTimeout(() => {\n                  resolve(\"${RESOLVED_DEFERRED_ID}\");\n                }, 10)\n              ),\n              id,\n              manualValue: promise,\n            };\n          }\n\n          export default function Deferred() {\n            let { deferredId, resolvedId, id, manualValue } = useLoaderData();\n            return (\n              <div id={deferredId}>\n                <p>{deferredId}</p>\n                <Counter id={deferredId} />\n                <Suspense fallback={<div id=\"${FALLBACK_ID}\">fallback</div>}>\n                  <Await\n                    resolve={resolvedId}\n                    children={(resolvedDeferredId) => (\n                      <div>\n                        <p id={resolvedDeferredId}>{id}</p>\n                        <Counter id={resolvedDeferredId} />\n                      </div>\n                    )}\n                  />\n                </Suspense>\n                <Suspense fallback={<div id=\"${MANUAL_FALLBACK_ID}\">manual fallback</div>}>\n                  <Await\n                    resolve={manualValue}\n                    errorElement={\n                      <div id=\"${MANUAL_ERROR_ID}\">\n                        error\n                        <Counter id=\"${MANUAL_ERROR_ID}\" />\n                      </div>\n                    }\n                    children={(value) => (\n                      <div>\n                        <pre><code id=\"${MANUAL_RESOLVED_ID}\">{JSON.stringify(value)}</code></pre>\n                        <Counter id=\"${MANUAL_RESOLVED_ID}\" />\n                      </div>\n                    )}\n                  />\n                </Suspense>\n              </div>\n            );\n          }\n        `,\n      },\n    });\n\n    // This creates an interactive app using playwright.\n    appFixture = await createAppFixture(fixture);\n    originalConsoleError = console.error;\n    console.error = () => {};\n  });\n\n  test.afterAll(() => {\n    console.error = originalConsoleError;\n    appFixture.close();\n  });\n\n  function counterHtml(id: string, val: number) {\n    return `<p id=\"count-${id}\">${val}</p>`;\n  }\n\n  const deferredHTMLStartString = \"<template id=\";\n  function getCriticalHTML(html: string) {\n    return html.slice(\n      0,\n      html.indexOf(deferredHTMLStartString) + deferredHTMLStartString.length,\n    );\n  }\n  function getDeferredHTML(html: string) {\n    return html.slice(\n      html.indexOf(deferredHTMLStartString) + deferredHTMLStartString.length,\n    );\n  }\n\n  test(\"works with critical JSON like data\", async ({ page }) => {\n    let response = await fixture.requestDocument(\"/\");\n    let html = await response.text();\n    let criticalHTML = getCriticalHTML(html);\n    expect(criticalHTML).toContain(counterHtml(ROOT_ID, 0));\n    expect(criticalHTML).toContain(counterHtml(INDEX_ID, 0));\n    let deferredHTML = getDeferredHTML(html);\n    expect(deferredHTML.replace(\"</body></html>\", \"\")).not.toBe(\"\");\n    expect(deferredHTML).not.toContain('<p id=\"count-');\n\n    let app = new PlaywrightFixture(appFixture, page);\n    let assertConsole = monitorConsole(page);\n    await app.goto(\"/\");\n    await page.waitForSelector(`#${ROOT_ID}`);\n    await page.waitForSelector(`#${INDEX_ID}`);\n\n    await ensureInteractivity(page, ROOT_ID);\n    await ensureInteractivity(page, INDEX_ID);\n\n    await assertConsole();\n  });\n\n  test(\"resolved promises do not render in initial payload\", async ({\n    page,\n  }) => {\n    let response = await fixture.requestDocument(\"/deferred-noscript-resolved\");\n    let html = await response.text();\n    let criticalHTML = getCriticalHTML(html);\n    expect(criticalHTML).toContain(counterHtml(ROOT_ID, 0));\n    expect(criticalHTML).toContain(counterHtml(DEFERRED_ID, 0));\n    expect(criticalHTML).not.toContain(counterHtml(RESOLVED_DEFERRED_ID, 0));\n    let deferredHTML = getDeferredHTML(html);\n    expect(deferredHTML).toContain(FALLBACK_ID);\n    expect(deferredHTML).toContain(counterHtml(RESOLVED_DEFERRED_ID, 0));\n\n    let app = new PlaywrightFixture(appFixture, page);\n    await app.goto(\"/deferred-noscript-resolved\");\n    await page.waitForSelector(`#${ROOT_ID}`);\n    await page.waitForSelector(`#${DEFERRED_ID}`);\n    await page.waitForSelector(`#${RESOLVED_DEFERRED_ID}`);\n  });\n\n  test(\"slow promises render in subsequent payload\", async ({ page }) => {\n    let response = await fixture.requestDocument(\n      \"/deferred-noscript-unresolved\",\n    );\n    let html = await response.text();\n    let criticalHTML = getCriticalHTML(html);\n    expect(criticalHTML).toContain(counterHtml(ROOT_ID, 0));\n    expect(criticalHTML).toContain(counterHtml(DEFERRED_ID, 0));\n    expect(criticalHTML).not.toContain(RESOLVED_DEFERRED_ID);\n    let deferredHTML = getDeferredHTML(html);\n    expect(deferredHTML).toContain(`<div id=\"${FALLBACK_ID}\">`);\n    expect(deferredHTML).toContain(counterHtml(RESOLVED_DEFERRED_ID, 0));\n\n    let app = new PlaywrightFixture(appFixture, page);\n    await app.goto(\"/deferred-noscript-unresolved\");\n    await page.waitForSelector(`#${ROOT_ID}`);\n    await page.waitForSelector(`#${DEFERRED_ID}`);\n    await page.waitForSelector(`#${RESOLVED_DEFERRED_ID}`);\n  });\n\n  test(\"resolved promises render in initial payload and hydrates\", async ({\n    page,\n  }) => {\n    let response = await fixture.requestDocument(\"/deferred-script-resolved\");\n    let html = await response.text();\n    let criticalHTML = getCriticalHTML(html);\n    expect(criticalHTML).toContain(counterHtml(ROOT_ID, 0));\n    expect(criticalHTML).toContain(counterHtml(DEFERRED_ID, 0));\n    let deferredHTML = getDeferredHTML(html);\n    expect(deferredHTML).toContain(FALLBACK_ID);\n    expect(deferredHTML).toContain(counterHtml(RESOLVED_DEFERRED_ID, 0));\n\n    let app = new PlaywrightFixture(appFixture, page);\n    let assertConsole = monitorConsole(page);\n    await app.goto(\"/deferred-script-resolved\", true);\n    await page.waitForSelector(`#${ROOT_ID}`);\n    await page.waitForSelector(`#${DEFERRED_ID}`);\n    await page.waitForSelector(`#${RESOLVED_DEFERRED_ID}`);\n\n    await ensureInteractivity(page, ROOT_ID);\n    await ensureInteractivity(page, DEFERRED_ID);\n    await ensureInteractivity(page, RESOLVED_DEFERRED_ID);\n\n    await assertConsole();\n  });\n\n  test(\"slow to resolve promises render in subsequent payload and hydrates\", async ({\n    page,\n  }) => {\n    let response = await fixture.requestDocument(\"/deferred-script-unresolved\");\n    let html = await response.text();\n    let criticalHTML = getCriticalHTML(html);\n    expect(criticalHTML).toContain(counterHtml(ROOT_ID, 0));\n    expect(criticalHTML).toContain(counterHtml(DEFERRED_ID, 0));\n    expect(criticalHTML).not.toContain(RESOLVED_DEFERRED_ID);\n    let deferredHTML = getDeferredHTML(html);\n    expect(deferredHTML).toContain(`<div id=\"${FALLBACK_ID}\">`);\n    expect(deferredHTML).toContain(counterHtml(RESOLVED_DEFERRED_ID, 0));\n\n    let app = new PlaywrightFixture(appFixture, page);\n    let assertConsole = monitorConsole(page);\n    await app.goto(\"/deferred-script-unresolved\", true);\n    await page.waitForSelector(`#${ROOT_ID}`);\n    await page.waitForSelector(`#${DEFERRED_ID}`);\n    await page.waitForSelector(`#${RESOLVED_DEFERRED_ID}`);\n\n    await ensureInteractivity(page, ROOT_ID);\n    await ensureInteractivity(page, DEFERRED_ID);\n    await ensureInteractivity(page, RESOLVED_DEFERRED_ID);\n\n    await assertConsole();\n  });\n\n  test(\"rejected promises render in initial payload and hydrates\", async ({\n    page,\n  }) => {\n    let response = await fixture.requestDocument(\"/deferred-script-rejected\");\n    let html = await response.text();\n    let criticalHTML = getCriticalHTML(html);\n    expect(criticalHTML).toContain(counterHtml(ROOT_ID, 0));\n    expect(criticalHTML).toContain(counterHtml(DEFERRED_ID, 0));\n    let deferredHTML = getDeferredHTML(html);\n    expect(deferredHTML).toContain(FALLBACK_ID);\n    expect(deferredHTML).toContain(counterHtml(ERROR_ID, 0));\n\n    let app = new PlaywrightFixture(appFixture, page);\n    let assertConsole = monitorConsole(page);\n    await app.goto(\"/deferred-script-rejected\", true);\n    await page.waitForSelector(`#${ROOT_ID}`);\n    await page.waitForSelector(`#${DEFERRED_ID}`);\n    await page.waitForSelector(`#${ERROR_ID}`);\n\n    await ensureInteractivity(page, ROOT_ID);\n    await ensureInteractivity(page, DEFERRED_ID);\n    await ensureInteractivity(page, ERROR_ID);\n\n    await assertConsole();\n  });\n\n  test(\"slow to reject promises render in subsequent payload and hydrates\", async ({\n    page,\n  }) => {\n    let response = await fixture.requestDocument(\"/deferred-script-unrejected\");\n    let html = await response.text();\n    let criticalHTML = getCriticalHTML(html);\n    expect(criticalHTML).toContain(counterHtml(ROOT_ID, 0));\n    expect(criticalHTML).toContain(counterHtml(DEFERRED_ID, 0));\n    expect(criticalHTML).not.toContain(ERROR_ID);\n    let deferredHTML = getDeferredHTML(html);\n    expect(deferredHTML).toContain(`<div id=\"${FALLBACK_ID}\">`);\n    expect(deferredHTML).toContain(counterHtml(ERROR_ID, 0));\n\n    let app = new PlaywrightFixture(appFixture, page);\n    let assertConsole = monitorConsole(page);\n    await app.goto(\"/deferred-script-unrejected\", true);\n    await page.waitForSelector(`#${ROOT_ID}`);\n    await page.waitForSelector(`#${DEFERRED_ID}`);\n    await page.waitForSelector(`#${ERROR_ID}`);\n\n    await ensureInteractivity(page, ROOT_ID);\n    await ensureInteractivity(page, DEFERRED_ID);\n    await ensureInteractivity(page, ERROR_ID);\n\n    await assertConsole();\n  });\n\n  test(\"rejected promises bubble to ErrorBoundary on hydrate\", async ({\n    page,\n  }) => {\n    let app = new PlaywrightFixture(appFixture, page);\n    await app.goto(\"/deferred-script-rejected-no-error-element\", true);\n    await page.waitForSelector(`#${ROOT_ID}`);\n    await page.waitForSelector(`#${ERROR_BOUNDARY_ID}`);\n\n    await ensureInteractivity(page, ROOT_ID);\n    await ensureInteractivity(page, ERROR_BOUNDARY_ID);\n  });\n\n  test(\"slow to reject promises bubble to ErrorBoundary on hydrate\", async ({\n    page,\n  }) => {\n    let app = new PlaywrightFixture(appFixture, page);\n    await app.goto(\"/deferred-script-unrejected-no-error-element\", true);\n    await page.waitForSelector(`#${ROOT_ID}`);\n    await page.waitForSelector(`#${ERROR_BOUNDARY_ID}`);\n\n    await ensureInteractivity(page, ROOT_ID);\n    await ensureInteractivity(page, ERROR_BOUNDARY_ID);\n  });\n\n  test(\"routes are interactive when deferred promises are suspended and after resolve in subsequent payload\", async ({\n    page,\n  }) => {\n    let app = new PlaywrightFixture(appFixture, page);\n    let assertConsole = monitorConsole(page);\n    app.goto(\"/deferred-manual-resolve\", false);\n\n    await page.waitForSelector(`#${ROOT_ID}`);\n    await page.waitForSelector(`#${DEFERRED_ID}`);\n    await page.waitForSelector(`#${MANUAL_FALLBACK_ID}`);\n    let idElement = await page.waitForSelector(`#${RESOLVED_DEFERRED_ID}`);\n    let id = await idElement.innerText();\n    expect(id).toBeTruthy();\n\n    // Ensure the deferred promise is suspended\n    await page.waitForSelector(`#${MANUAL_RESOLVED_ID}`, { state: \"hidden\" });\n\n    await page.waitForSelector(\"#interactive\");\n    await ensureInteractivity(page, ROOT_ID);\n    await ensureInteractivity(page, DEFERRED_ID);\n    await ensureInteractivity(page, RESOLVED_DEFERRED_ID);\n\n    global.__deferredManualResolveCache.deferreds[id].resolve(\"value\");\n\n    await ensureInteractivity(page, MANUAL_RESOLVED_ID);\n    await ensureInteractivity(page, RESOLVED_DEFERRED_ID, 2);\n    await ensureInteractivity(page, DEFERRED_ID, 2);\n    await ensureInteractivity(page, ROOT_ID, 2);\n\n    await assertConsole();\n  });\n\n  test(\"routes are interactive when deferred promises are suspended and after rejection in subsequent payload\", async ({\n    page,\n  }) => {\n    let app = new PlaywrightFixture(appFixture, page);\n    let assertConsole = monitorConsole(page);\n    await app.goto(\"/deferred-manual-resolve\", false);\n\n    await page.waitForSelector(`#${ROOT_ID}`);\n    await page.waitForSelector(`#${DEFERRED_ID}`);\n    await page.waitForSelector(`#${MANUAL_FALLBACK_ID}`);\n    let idElement = await page.waitForSelector(`#${RESOLVED_DEFERRED_ID}`);\n    let id = await idElement.innerText();\n    expect(id).toBeTruthy();\n\n    await page.waitForSelector(\"#interactive\");\n    await ensureInteractivity(page, ROOT_ID);\n    await ensureInteractivity(page, DEFERRED_ID);\n    await ensureInteractivity(page, RESOLVED_DEFERRED_ID);\n\n    global.__deferredManualResolveCache.deferreds[id].reject(\n      new Error(\"error\"),\n    );\n\n    await ensureInteractivity(page, ROOT_ID, 2);\n    await ensureInteractivity(page, DEFERRED_ID, 2);\n    await ensureInteractivity(page, RESOLVED_DEFERRED_ID, 2);\n    await ensureInteractivity(page, MANUAL_ERROR_ID);\n\n    await assertConsole();\n  });\n\n  test(\"client transition with resolved promises work\", async ({ page }) => {\n    let app = new PlaywrightFixture(appFixture, page);\n    let assertConsole = monitorConsole(page);\n    await app.goto(\"/\");\n\n    await page.waitForSelector(\"#interactive\");\n    await ensureInteractivity(page, ROOT_ID);\n    await ensureInteractivity(page, INDEX_ID);\n\n    await app.clickLink(\"/deferred-script-resolved\");\n\n    await ensureInteractivity(page, ROOT_ID, 2);\n    await ensureInteractivity(page, DEFERRED_ID);\n    await ensureInteractivity(page, RESOLVED_DEFERRED_ID);\n\n    await assertConsole();\n  });\n\n  test(\"client transition with unresolved promises work\", async ({ page }) => {\n    let app = new PlaywrightFixture(appFixture, page);\n    let assertConsole = monitorConsole(page);\n    await app.goto(\"/\");\n\n    await page.waitForSelector(\"#interactive\");\n    await ensureInteractivity(page, ROOT_ID);\n    await ensureInteractivity(page, INDEX_ID);\n\n    await app.clickLink(\"/deferred-script-unresolved\");\n\n    await ensureInteractivity(page, ROOT_ID, 2);\n    await ensureInteractivity(page, DEFERRED_ID);\n    await ensureInteractivity(page, RESOLVED_DEFERRED_ID);\n\n    await assertConsole();\n  });\n\n  test(\"client transition with rejected promises work\", async ({ page }) => {\n    let app = new PlaywrightFixture(appFixture, page);\n    let assertConsole = monitorConsole(page);\n    await app.goto(\"/\");\n\n    await page.waitForSelector(\"#interactive\");\n    await ensureInteractivity(page, ROOT_ID);\n    await ensureInteractivity(page, INDEX_ID);\n\n    app.clickLink(\"/deferred-script-rejected\");\n\n    await ensureInteractivity(page, DEFERRED_ID);\n    await ensureInteractivity(page, ERROR_ID);\n    await ensureInteractivity(page, DEFERRED_ID, 2);\n    await ensureInteractivity(page, ROOT_ID, 2);\n\n    await assertConsole();\n  });\n\n  test(\"client transition with unrejected promises work\", async ({ page }) => {\n    let app = new PlaywrightFixture(appFixture, page);\n    let assertConsole = monitorConsole(page);\n    await app.goto(\"/\");\n\n    await page.waitForSelector(\"#interactive\");\n    await ensureInteractivity(page, ROOT_ID);\n    await ensureInteractivity(page, INDEX_ID);\n\n    await app.clickLink(\"/deferred-script-unrejected\");\n\n    await ensureInteractivity(page, DEFERRED_ID);\n    await ensureInteractivity(page, ERROR_ID);\n    await ensureInteractivity(page, DEFERRED_ID, 2);\n    await ensureInteractivity(page, ROOT_ID, 2);\n\n    await assertConsole();\n  });\n\n  test(\"client transition with rejected promises bubble to ErrorBoundary\", async ({\n    page,\n  }) => {\n    let app = new PlaywrightFixture(appFixture, page);\n    await app.goto(\"/\");\n\n    await page.waitForSelector(\"#interactive\");\n    await ensureInteractivity(page, ROOT_ID);\n    await ensureInteractivity(page, INDEX_ID);\n\n    await app.clickLink(\"/deferred-script-rejected-no-error-element\");\n\n    await ensureInteractivity(page, ERROR_BOUNDARY_ID);\n    await ensureInteractivity(page, ROOT_ID, 2);\n  });\n\n  test(\"client transition with unrejected promises bubble to ErrorBoundary\", async ({\n    page,\n  }) => {\n    let app = new PlaywrightFixture(appFixture, page);\n    await app.goto(\"/\");\n\n    await page.waitForSelector(\"#interactive\");\n    await ensureInteractivity(page, ROOT_ID);\n    await ensureInteractivity(page, INDEX_ID);\n\n    await app.clickLink(\"/deferred-script-unrejected-no-error-element\");\n\n    await ensureInteractivity(page, ERROR_BOUNDARY_ID);\n    await ensureInteractivity(page, ROOT_ID, 2);\n  });\n});\n\ntest.describe(\"aborted\", () => {\n  let fixture: Fixture;\n  let appFixture: AppFixture;\n\n  test.beforeEach(async ({ context }) => {\n    await context.route(/\\.data$/, async (route) => {\n      await new Promise((resolve) => setTimeout(resolve, 50));\n      route.continue();\n    });\n  });\n\n  test.beforeAll(async () => {\n    fixture = await createFixture({\n      files: {\n        \"app/entry.server.tsx\": js`\n          import { PassThrough } from \"node:stream\";\n          import type { AppLoadContext, EntryContext } from \"react-router\";\n          import { createReadableStreamFromReadable } from \"@react-router/node\";\n          import { ServerRouter } from \"react-router\";\n          import { isbot } from \"isbot\";\n          import { renderToPipeableStream } from \"react-dom/server\";\n\n          // Exported for use by the server runtime so we can abort the\n          // turbo-stream encode() call\n          export const streamTimeout = 250;\n          const renderTimeout = streamTimeout + 250;\n\n          export default function handleRequest(\n            request: Request,\n            responseStatusCode: number,\n            responseHeaders: Headers,\n            remixContext: EntryContext,\n            loadContext: AppLoadContext,\n          ) {\n            return isbot(request.headers.get(\"user-agent\") || \"\")\n              ? handleBotRequest(\n                  request,\n                  responseStatusCode,\n                  responseHeaders,\n                  remixContext\n                )\n              : handleBrowserRequest(\n                  request,\n                  responseStatusCode,\n                  responseHeaders,\n                  remixContext\n                );\n          }\n\n          function handleBotRequest(\n            request: Request,\n            responseStatusCode: number,\n            responseHeaders: Headers,\n            remixContext: EntryContext\n          ) {\n            return new Promise((resolve, reject) => {\n              let didError = false;\n\n              let { pipe, abort } = renderToPipeableStream(\n                <ServerRouter context={remixContext} url={request.url} />,\n                {\n                  onAllReady() {\n                    let body = new PassThrough();\n                    let stream = createReadableStreamFromReadable(body);\n\n                    responseHeaders.set(\"Content-Type\", \"text/html\");\n\n                    resolve(\n                      new Response(stream, {\n                        headers: responseHeaders,\n                        status: didError ? 500 : responseStatusCode,\n                      })\n                    );\n\n                    pipe(body);\n                  },\n                  onShellError(error: unknown) {\n                    reject(error);\n                  },\n                  onError(error: unknown) {\n                    didError = true;\n\n                    console.error(error);\n                  },\n                }\n              );\n\n              setTimeout(abort, renderTimeout);\n            });\n          }\n\n          function handleBrowserRequest(\n            request: Request,\n            responseStatusCode: number,\n            responseHeaders: Headers,\n            remixContext: EntryContext\n          ) {\n            return new Promise((resolve, reject) => {\n              let didError = false;\n\n              let { pipe, abort } = renderToPipeableStream(\n                <ServerRouter context={remixContext} url={request.url} />,\n                {\n                  onShellReady() {\n                    let body = new PassThrough();\n                    let stream = createReadableStreamFromReadable(body);\n\n                    responseHeaders.set(\"Content-Type\", \"text/html\");\n\n                    resolve(\n                      new Response(stream, {\n                        headers: responseHeaders,\n                        status: didError ? 500 : responseStatusCode,\n                      })\n                    );\n\n                    pipe(body);\n                  },\n                  onShellError(err: unknown) {\n                    reject(err);\n                  },\n                  onError(error: unknown) {\n                    didError = true;\n\n                    console.error(error);\n                  },\n                }\n              );\n\n              setTimeout(abort, renderTimeout);\n            });\n          }\n        `,\n        \"app/components/counter.tsx\": js`\n          import { useState } from \"react\";\n\n          export default function Counter({ id }) {\n            let [count, setCount] = useState(0);\n            return (\n              <div>\n                <button id={\"increment-\"+id} onClick={() => setCount((c) => c+1)}>Increment</button>\n                <p id={\"count-\"+id}>{count}</p>\n              </div>\n            )\n          }\n        `,\n        \"app/components/interactive.tsx\": js`\n          import { useEffect, useState } from \"react\";\n\n          export default function Interactive() {\n            let [interactive, setInteractive] = useState(false);\n            useEffect(() => {\n              setInteractive(true);\n            }, []);\n            return interactive ? (\n              <div id=\"interactive\">\n                <p>interactive</p>\n              </div>\n            ) : null;\n          }\n        `,\n        \"app/root.tsx\": js`\n          import { Links, Meta, Outlet, Scripts, useLoaderData } from \"react-router\";\n          import Counter from \"~/components/counter\";\n          import Interactive from \"~/components/interactive\";\n\n          export const meta: MetaFunction = () => {\n            return [{ title: \"New Remix App\" }];\n          };\n\n          export const loader = () => ({\n            id: \"${ROOT_ID}\",\n          });\n\n          export default function Root() {\n            let { id } = useLoaderData();\n            return (\n              <html lang=\"en\">\n                <head>\n                  <meta charSet=\"utf-8\" />\n                  <meta name=\"viewport\" content=\"width=device-width,initial-scale=1\" />\n                  <Meta />\n                  <Links />\n                </head>\n                <body>\n                  <div id={id}>\n                    <p>{id}</p>\n                    <Counter id={id} />\n                    <Outlet />\n                    <Interactive />\n                  </div>\n                  <Scripts />\n                  {/* Send arbitrary data so safari renders the initial shell before\n                      the document finishes downloading. */}\n                  {Array(6000).fill(null).map((_, i)=><p key={i}>YOOOOOOOOOO   {i}</p>)}\n                </body>\n              </html>\n            );\n          }\n        `,\n\n        \"app/routes/deferred-server-aborted.tsx\": js`\n          import { Suspense } from \"react\";\n          import { Await, Link, useLoaderData } from \"react-router\";\n          import Counter from \"~/components/counter\";\n\n          export function loader() {\n            return {\n              deferredId: \"${DEFERRED_ID}\",\n              resolvedId: new Promise(\n                (resolve) => setTimeout(() => {\n                  resolve(\"${RESOLVED_DEFERRED_ID}\");\n                }, 10000)\n              ),\n            };\n          }\n\n          export default function Deferred() {\n            let { deferredId, resolvedId } = useLoaderData();\n            return (\n              <div id={deferredId}>\n                <p>{deferredId}</p>\n                <Counter id={deferredId} />\n                <Suspense fallback={<div id=\"${FALLBACK_ID}\">fallback</div>}>\n                  <Await\n                    resolve={resolvedId}\n                    errorElement={\n                      <div id=\"${ERROR_ID}\">\n                        error\n                        <Counter id=\"${ERROR_ID}\" />\n                      </div>\n                    }\n                    children={(resolvedDeferredId) => (\n                      <div id={resolvedDeferredId}>\n                        <p>{resolvedDeferredId}</p>\n                        <Counter id={resolvedDeferredId} />\n                      </div>\n                    )}\n                  />\n                </Suspense>\n              </div>\n            );\n          }\n        `,\n\n        \"app/routes/deferred-server-aborted-no-error-element.tsx\": js`\n          import { Suspense } from \"react\";\n          import { Await, Link, useLoaderData } from \"react-router\";\n          import Counter from \"~/components/counter\";\n\n          export function loader() {\n            return {\n              deferredId: \"${DEFERRED_ID}\",\n              resolvedId: new Promise(\n                (resolve) => setTimeout(() => {\n                  resolve(\"${RESOLVED_DEFERRED_ID}\");\n                }, 10000)\n              ),\n            };\n          }\n\n          export default function Deferred() {\n            let { deferredId, resolvedId } = useLoaderData();\n            return (\n              <div id={deferredId}>\n                <p>{deferredId}</p>\n                <Counter id={deferredId} />\n                <Suspense fallback={<div id=\"${FALLBACK_ID}\">fallback</div>}>\n                  <Await\n                    resolve={resolvedId}\n                    children={(resolvedDeferredId) => (\n                      <div id={resolvedDeferredId}>\n                        <p>{resolvedDeferredId}</p>\n                        <Counter id={resolvedDeferredId} />\n                      </div>\n                    )}\n                  />\n                </Suspense>\n              </div>\n            );\n          }\n\n          export function ErrorBoundary() {\n            return (\n              <div id=\"${ERROR_BOUNDARY_ID}\">\n                error\n                <Counter id=\"${ERROR_BOUNDARY_ID}\" />\n              </div>\n            );\n          }\n        `,\n      },\n    });\n\n    // This creates an interactive app using playwright.\n    appFixture = await createAppFixture(fixture);\n\n    originalConsoleError = console.error;\n    console.error = () => {};\n  });\n\n  test.afterAll(() => {\n    console.error = originalConsoleError;\n    appFixture.close();\n  });\n\n  test(\"server aborts render the errorElement\", async ({ page }) => {\n    let app = new PlaywrightFixture(appFixture, page);\n    await app.goto(\"/deferred-server-aborted\");\n    await page.waitForSelector(`#${ROOT_ID}`);\n    await page.waitForSelector(`#${DEFERRED_ID}`);\n    await page.waitForSelector(`#${ERROR_ID}`);\n\n    await ensureInteractivity(page, ROOT_ID);\n    await ensureInteractivity(page, DEFERRED_ID);\n    await ensureInteractivity(page, ERROR_ID);\n  });\n\n  test(\"server aborts render the ErrorBoundary when no errorElement\", async ({\n    page,\n  }) => {\n    let app = new PlaywrightFixture(appFixture, page);\n    await app.goto(\"/deferred-server-aborted-no-error-element\");\n    await page.waitForSelector(`#${ROOT_ID}`);\n    await page.waitForSelector(`#${ERROR_BOUNDARY_ID}`);\n\n    await ensureInteractivity(page, ROOT_ID);\n    await ensureInteractivity(page, ERROR_BOUNDARY_ID);\n  });\n});\n\nasync function ensureInteractivity(page: Page, id: string, expect: number = 1) {\n  await page.waitForSelector(\"#interactive\");\n  let increment = await page.waitForSelector(\"#increment-\" + id);\n  await increment.click();\n  await page.waitForSelector(`#count-${id}:has-text('${expect}')`);\n}\n\nfunction monitorConsole(page: Page) {\n  let messages: ConsoleMessage[] = [];\n  page.on(\"console\", (message) => {\n    messages.push(message);\n  });\n\n  return async () => {\n    if (!messages.length) return;\n    let errors: string[] = [];\n    for (let message of messages) {\n      let logs = [];\n      let args = message.args();\n      if (args[0]) {\n        let arg0 = await args[0].jsonValue();\n        if (\n          typeof arg0 === \"string\" &&\n          arg0.includes(\"Download the React DevTools\")\n        ) {\n          continue;\n        }\n        logs.push(arg0);\n      }\n      errors.push(\n        `Unexpected console.log(${JSON.stringify(logs).slice(1, -1)})`,\n      );\n    }\n    if (errors.length) {\n      throw new Error(`Unexpected console.log's:\\n` + errors.join(\"\\n\") + \"\\n\");\n    }\n  };\n}\n"
  },
  {
    "path": "integration/error-boundary-test.ts",
    "content": "import { test, expect } from \"@playwright/test\";\n\nimport { UNSAFE_ServerMode as ServerMode } from \"react-router\";\nimport {\n  createAppFixture,\n  createFixture,\n  js,\n} from \"./helpers/create-fixture.js\";\nimport type { Fixture, AppFixture } from \"./helpers/create-fixture.js\";\nimport { PlaywrightFixture } from \"./helpers/playwright-fixture.js\";\n\ntest.describe(\"ErrorBoundary\", () => {\n  let fixture: Fixture;\n  let appFixture: AppFixture;\n  let _consoleError: any;\n\n  let ROOT_BOUNDARY_TEXT = \"ROOT_BOUNDARY_TEXT\";\n  let OWN_BOUNDARY_TEXT = \"OWN_BOUNDARY_TEXT\";\n\n  let HAS_BOUNDARY_LOADER = \"/yes/loader\" as const;\n  let HAS_BOUNDARY_LOADER_FILE = \"/yes.loader\" as const;\n  let HAS_BOUNDARY_ACTION = \"/yes/action\" as const;\n  let HAS_BOUNDARY_ACTION_FILE = \"/yes.action\" as const;\n  let HAS_BOUNDARY_RENDER = \"/yes/render\" as const;\n  let HAS_BOUNDARY_RENDER_FILE = \"/yes.render\" as const;\n  let HAS_BOUNDARY_NO_LOADER_OR_ACTION = \"/yes/no-loader-or-action\" as const;\n  let HAS_BOUNDARY_NO_LOADER_OR_ACTION_FILE =\n    \"/yes.no-loader-or-action\" as const;\n\n  let NO_BOUNDARY_ACTION = \"/no/action\" as const;\n  let NO_BOUNDARY_ACTION_FILE = \"/no.action\" as const;\n  let NO_BOUNDARY_LOADER = \"/no/loader\" as const;\n  let NO_BOUNDARY_LOADER_FILE = \"/no.loader\" as const;\n  let NO_BOUNDARY_RENDER = \"/no/render\" as const;\n  let NO_BOUNDARY_RENDER_FILE = \"/no.render\" as const;\n  let NO_BOUNDARY_NO_LOADER_OR_ACTION = \"/no/no-loader-or-action\" as const;\n  let NO_BOUNDARY_NO_LOADER_OR_ACTION_FILE = \"/no.no-loader-or-action\" as const;\n\n  let NOT_FOUND_HREF = \"/not/found\";\n\n  // packages/remix-react/errorBoundaries.tsx\n  let INTERNAL_ERROR_BOUNDARY_HEADING = \"Application Error\";\n\n  test.beforeAll(async () => {\n    _consoleError = console.error;\n    console.error = () => {};\n    fixture = await createFixture(\n      {\n        files: {\n          \"app/root.tsx\": js`\n              import { Links, Meta, Outlet, Scripts } from \"react-router\";\n\n              export default function Root() {\n                return (\n                  <html lang=\"en\">\n                    <head>\n                      <Meta />\n                      <Links />\n                    </head>\n                    <body>\n                      <main>\n                        <Outlet />\n                      </main>\n                      <Scripts />\n                    </body>\n                  </html>\n                );\n              }\n\n              export function ErrorBoundary() {\n                return (\n                  <html>\n                    <head />\n                    <body>\n                      <main>\n                        <div id=\"root-boundary\">${ROOT_BOUNDARY_TEXT}</div>\n                      </main>\n                      <Scripts />\n                    </body>\n                  </html>\n                )\n              }\n            `,\n\n          \"app/routes/_index.tsx\": js`\n              import { Link, Form } from \"react-router\";\n              export default function () {\n                return (\n                  <div>\n                    <Link to=\"${NOT_FOUND_HREF}\">${NOT_FOUND_HREF}</Link>\n\n                    <Form method=\"post\">\n                      <button formAction=\"${HAS_BOUNDARY_ACTION}\" type=\"submit\">\n                        Own Boundary\n                      </button>\n                      <button formAction=\"${NO_BOUNDARY_ACTION}\" type=\"submit\">\n                        No Boundary\n                      </button>\n                      <button formAction=\"${HAS_BOUNDARY_NO_LOADER_OR_ACTION}\" type=\"submit\">\n                        Has Boundary No Loader or Action\n                      </button>\n                      <button formAction=\"${NO_BOUNDARY_NO_LOADER_OR_ACTION}\" type=\"submit\">\n                        No Boundary No Loader or Action\n                      </button>\n                    </Form>\n\n                    <Link to=\"${HAS_BOUNDARY_LOADER}\">\n                      ${HAS_BOUNDARY_LOADER}\n                    </Link>\n                    <Link to=\"${NO_BOUNDARY_LOADER}\">\n                      ${NO_BOUNDARY_LOADER}\n                    </Link>\n                    <Link to=\"${HAS_BOUNDARY_RENDER}\">\n                      ${HAS_BOUNDARY_RENDER}\n                    </Link>\n                    <Link to=\"${NO_BOUNDARY_RENDER}\">\n                      ${NO_BOUNDARY_RENDER}\n                    </Link>\n                  </div>\n                )\n              }\n            `,\n\n          [`app/routes${HAS_BOUNDARY_ACTION_FILE}.jsx`]: js`\n              import { Form } from \"react-router\";\n              export async function action() {\n                throw new Error(\"Kaboom!\")\n              }\n              export function ErrorBoundary() {\n                return <p id=\"own-boundary\">${OWN_BOUNDARY_TEXT}</p>\n              }\n              export default function () {\n                return (\n                  <Form method=\"post\">\n                    <button type=\"submit\" formAction=\"${HAS_BOUNDARY_ACTION}\">\n                      Go\n                    </button>\n                  </Form>\n                );\n              }\n            `,\n\n          [`app/routes${NO_BOUNDARY_ACTION_FILE}.jsx`]: js`\n              import { Form } from \"react-router\";\n              export function action() {\n                throw new Error(\"Kaboom!\")\n              }\n              export default function () {\n                return (\n                  <Form method=\"post\">\n                    <button type=\"submit\" formAction=\"${NO_BOUNDARY_ACTION}\">\n                      Go\n                    </button>\n                  </Form>\n                )\n              }\n            `,\n\n          [`app/routes${HAS_BOUNDARY_LOADER_FILE}.jsx`]: js`\n              export function loader() {\n                throw new Error(\"Kaboom!\")\n              }\n              export function ErrorBoundary() {\n                return <div id=\"own-boundary\">${OWN_BOUNDARY_TEXT}</div>\n              }\n              export default function () {\n                return <div/>\n              }\n            `,\n\n          [`app/routes${NO_BOUNDARY_LOADER_FILE}.jsx`]: js`\n              export function loader() {\n                throw new Error(\"Kaboom!\")\n              }\n              export default function () {\n                return <div/>\n              }\n            `,\n\n          [`app/routes${NO_BOUNDARY_RENDER_FILE}.jsx`]: js`\n              export default function () {\n                throw new Error(\"Kaboom!\")\n                return <div/>\n              }\n            `,\n\n          [`app/routes${HAS_BOUNDARY_RENDER_FILE}.jsx`]: js`\n              export default function () {\n                throw new Error(\"Kaboom!\")\n                return <div/>\n              }\n\n              export function ErrorBoundary() {\n                return <div id=\"own-boundary\">${OWN_BOUNDARY_TEXT}</div>\n              }\n            `,\n\n          [`app/routes${HAS_BOUNDARY_NO_LOADER_OR_ACTION_FILE}.jsx`]: js`\n              export function ErrorBoundary() {\n                return <div id=\"boundary-no-loader-or-action\">${OWN_BOUNDARY_TEXT}</div>\n              }\n              export default function Index() {\n                return <div/>\n              }\n            `,\n\n          [`app/routes${NO_BOUNDARY_NO_LOADER_OR_ACTION_FILE}.jsx`]: js`\n              export default function Index() {\n                return <div/>\n              }\n            `,\n\n          \"app/routes/fetcher-boundary.tsx\": js`\n              import { useFetcher } from \"react-router\";\n              export function ErrorBoundary() {\n                return <p id=\"fetcher-boundary\">${OWN_BOUNDARY_TEXT}</p>\n              }\n              export default function() {\n                let fetcher = useFetcher();\n\n                return (\n                  <div>\n                    <fetcher.Form method=\"post\">\n                      <button formAction=\"${NO_BOUNDARY_NO_LOADER_OR_ACTION}\" type=\"submit\" />\n                    </fetcher.Form>\n                  </div>\n                )\n              }\n            `,\n\n          \"app/routes/fetcher-no-boundary.tsx\": js`\n              import { useFetcher } from \"react-router\";\n              export default function() {\n                let fetcher = useFetcher();\n\n                return (\n                  <div>\n                    <fetcher.Form method=\"post\">\n                      <button formAction=\"${NO_BOUNDARY_NO_LOADER_OR_ACTION}\" type=\"submit\">\n                        No Loader or Action\n                      </button>\n                    </fetcher.Form>\n                  </div>\n                )\n              }\n            `,\n\n          \"app/routes/action.tsx\": js`\n              import { Outlet, useLoaderData } from \"react-router\";\n\n              export function loader() {\n                return \"PARENT\";\n              }\n\n              export default function () {\n                return (\n                  <div>\n                    <p id=\"parent-data\">{useLoaderData()}</p>\n                    <Outlet />\n                  </div>\n                )\n              }\n            `,\n\n          \"app/routes/action.child-error.tsx\": js`\n              import { Form, useLoaderData, useRouteError } from \"react-router\";\n\n              export function loader() {\n                return \"CHILD\";\n              }\n\n              export function action() {\n                throw new Error(\"Broken!\");\n              }\n\n              export default function () {\n                return (\n                  <>\n                    <p id=\"child-data\">{useLoaderData()}</p>\n                    <Form method=\"post\" reloadDocument={true}>\n                      <button type=\"submit\" name=\"key\" value=\"value\">\n                        Submit\n                      </button>\n                    </Form>\n                  </>\n                )\n              }\n\n              export function ErrorBoundary() {\n                let error = useRouteError();\n                return <p id=\"child-error\">{error.message}</p>;\n              }\n            `,\n        },\n      },\n      ServerMode.Development,\n    );\n\n    appFixture = await createAppFixture(fixture, ServerMode.Development);\n  });\n\n  test.afterAll(() => {\n    console.error = _consoleError;\n    appFixture.close();\n  });\n\n  test(\"invalid request methods\", async () => {\n    let res = await fixture.requestDocument(\"/\", { method: \"OPTIONS\" });\n    expect(res.status).toBe(405);\n    expect(await res.text()).toMatch(ROOT_BOUNDARY_TEXT);\n  });\n\n  test(\"own boundary, action, document request\", async () => {\n    let params = new URLSearchParams();\n    let res = await fixture.postDocument(HAS_BOUNDARY_ACTION, params);\n    expect(res.status).toBe(500);\n    expect(await res.text()).toMatch(OWN_BOUNDARY_TEXT);\n  });\n\n  test(\"own boundary, action, client transition from other route\", async ({\n    page,\n  }) => {\n    let app = new PlaywrightFixture(appFixture, page);\n    await app.goto(\"/\");\n    await app.clickSubmitButton(HAS_BOUNDARY_ACTION);\n    await page.waitForSelector(`text=${OWN_BOUNDARY_TEXT}`);\n    expect(await app.getHtml(\"main\")).toMatch(OWN_BOUNDARY_TEXT);\n  });\n\n  test(\"own boundary, action, client transition from itself\", async ({\n    page,\n  }) => {\n    let app = new PlaywrightFixture(appFixture, page);\n    await app.goto(HAS_BOUNDARY_ACTION);\n    await app.clickSubmitButton(HAS_BOUNDARY_ACTION);\n    await page.waitForSelector(`text=${OWN_BOUNDARY_TEXT}`);\n    expect(await app.getHtml(\"main\")).toMatch(OWN_BOUNDARY_TEXT);\n  });\n\n  test(\"bubbles to parent in action document requests\", async () => {\n    let params = new URLSearchParams();\n    let res = await fixture.postDocument(NO_BOUNDARY_ACTION, params);\n    expect(res.status).toBe(500);\n    expect(await res.text()).toMatch(ROOT_BOUNDARY_TEXT);\n  });\n\n  test(\"bubbles to parent in action script transitions from other routes\", async ({\n    page,\n  }) => {\n    let app = new PlaywrightFixture(appFixture, page);\n    await app.goto(\"/\");\n    await app.clickSubmitButton(NO_BOUNDARY_ACTION);\n    await page.waitForSelector(`text=${ROOT_BOUNDARY_TEXT}`);\n    expect(await app.getHtml(\"main\")).toMatch(ROOT_BOUNDARY_TEXT);\n  });\n\n  test(\"bubbles to parent in action script transitions from self\", async ({\n    page,\n  }) => {\n    let app = new PlaywrightFixture(appFixture, page);\n    await app.goto(NO_BOUNDARY_ACTION);\n    await app.clickSubmitButton(NO_BOUNDARY_ACTION);\n    await page.waitForSelector(`text=${ROOT_BOUNDARY_TEXT}`);\n    expect(await app.getHtml(\"main\")).toMatch(ROOT_BOUNDARY_TEXT);\n  });\n\n  test(\"own boundary, loader, document request\", async () => {\n    let res = await fixture.requestDocument(HAS_BOUNDARY_LOADER);\n    expect(res.status).toBe(500);\n    expect(await res.text()).toMatch(OWN_BOUNDARY_TEXT);\n  });\n\n  test(\"own boundary, loader, client transition\", async ({ page }) => {\n    let app = new PlaywrightFixture(appFixture, page);\n    await app.goto(\"/\");\n    await app.clickLink(HAS_BOUNDARY_LOADER);\n    await page.waitForSelector(`text=${OWN_BOUNDARY_TEXT}`);\n    expect(await app.getHtml(\"main\")).toMatch(OWN_BOUNDARY_TEXT);\n  });\n\n  test(\"bubbles to parent in loader document requests\", async () => {\n    let res = await fixture.requestDocument(NO_BOUNDARY_LOADER);\n    expect(res.status).toBe(500);\n    expect(await res.text()).toMatch(ROOT_BOUNDARY_TEXT);\n  });\n\n  test(\"bubbles to parent in loader script transitions from other routes\", async ({\n    page,\n  }) => {\n    let app = new PlaywrightFixture(appFixture, page);\n    await app.goto(\"/\");\n    await app.clickLink(NO_BOUNDARY_LOADER);\n    await page.waitForSelector(`text=${ROOT_BOUNDARY_TEXT}`);\n    expect(await app.getHtml(\"main\")).toMatch(ROOT_BOUNDARY_TEXT);\n  });\n\n  test(\"ssr rendering errors with no boundary\", async () => {\n    let res = await fixture.requestDocument(NO_BOUNDARY_RENDER);\n    expect(res.status).toBe(500);\n    expect(await res.text()).toMatch(ROOT_BOUNDARY_TEXT);\n  });\n\n  test(\"script transition rendering errors with no boundary\", async ({\n    page,\n  }) => {\n    let app = new PlaywrightFixture(appFixture, page);\n    await app.goto(\"/\");\n    await app.clickLink(NO_BOUNDARY_RENDER);\n    await page.waitForSelector(`text=${ROOT_BOUNDARY_TEXT}`);\n    expect(await app.getHtml(\"main\")).toMatch(ROOT_BOUNDARY_TEXT);\n  });\n\n  test(\"ssr rendering errors with boundary\", async () => {\n    let res = await fixture.requestDocument(HAS_BOUNDARY_RENDER);\n    expect(res.status).toBe(500);\n    expect(await res.text()).toMatch(OWN_BOUNDARY_TEXT);\n  });\n\n  test(\"script transition rendering errors with boundary\", async ({ page }) => {\n    let app = new PlaywrightFixture(appFixture, page);\n    await app.goto(\"/\");\n    await app.clickLink(HAS_BOUNDARY_RENDER);\n    await page.waitForSelector(`text=${OWN_BOUNDARY_TEXT}`);\n    expect(await app.getHtml(\"main\")).toMatch(OWN_BOUNDARY_TEXT);\n  });\n\n  test(\"uses correct error boundary on server action errors in nested routes\", async ({\n    page,\n  }) => {\n    let app = new PlaywrightFixture(appFixture, page);\n    await app.goto(`/action/child-error`);\n    expect(await app.getHtml(\"#parent-data\")).toMatch(\"PARENT\");\n    expect(await app.getHtml(\"#child-data\")).toMatch(\"CHILD\");\n    await page.click(\"button[type=submit]\");\n    await page.waitForSelector(\"#child-error\");\n    // Preserves parent loader data\n    expect(await app.getHtml(\"#parent-data\")).toMatch(\"PARENT\");\n    expect(await app.getHtml(\"#child-error\")).toMatch(\"Broken!\");\n  });\n\n  test(\"renders own boundary in fetcher action submission without action from other routes\", async ({\n    page,\n  }) => {\n    let app = new PlaywrightFixture(appFixture, page);\n    await app.goto(\"/fetcher-boundary\");\n    await app.clickSubmitButton(NO_BOUNDARY_NO_LOADER_OR_ACTION);\n    await page.waitForSelector(\"#fetcher-boundary\");\n  });\n\n  test(\"renders root boundary in fetcher action submission without action from other routes\", async ({\n    page,\n  }) => {\n    let app = new PlaywrightFixture(appFixture, page);\n    await app.goto(\"/fetcher-no-boundary\");\n    await app.clickSubmitButton(NO_BOUNDARY_NO_LOADER_OR_ACTION);\n    await page.waitForSelector(`text=${ROOT_BOUNDARY_TEXT}`);\n  });\n\n  test(\"renders root boundary in document POST without action requests\", async () => {\n    let res = await fixture.requestDocument(NO_BOUNDARY_NO_LOADER_OR_ACTION, {\n      method: \"post\",\n    });\n    expect(res.status).toBe(405);\n    expect(await res.text()).toMatch(ROOT_BOUNDARY_TEXT);\n  });\n\n  test(\"renders root boundary in action script transitions without action from other routes\", async ({\n    page,\n  }) => {\n    let app = new PlaywrightFixture(appFixture, page);\n    await app.goto(\"/\");\n    await app.clickSubmitButton(NO_BOUNDARY_NO_LOADER_OR_ACTION);\n    await page.waitForSelector(`text=${ROOT_BOUNDARY_TEXT}`);\n  });\n\n  test(\"renders own boundary in document POST without action requests\", async () => {\n    let res = await fixture.requestDocument(HAS_BOUNDARY_NO_LOADER_OR_ACTION, {\n      method: \"post\",\n    });\n    expect(res.status).toBe(405);\n    expect(await res.text()).toMatch(OWN_BOUNDARY_TEXT);\n  });\n\n  test(\"renders own boundary in action script transitions without action from other routes\", async ({\n    page,\n  }) => {\n    let app = new PlaywrightFixture(appFixture, page);\n    await app.goto(\"/\");\n    await app.clickSubmitButton(HAS_BOUNDARY_NO_LOADER_OR_ACTION);\n    await page.waitForSelector(\"#boundary-no-loader-or-action\");\n  });\n\n  test.describe(\"if no error boundary exists in the app\", () => {\n    let NO_ROOT_BOUNDARY_LOADER = \"/loader-bad\" as const;\n    let NO_ROOT_BOUNDARY_ACTION = \"/action-bad\" as const;\n\n    test.beforeAll(async () => {\n      fixture = await createFixture({\n        files: {\n          \"app/root.tsx\": js`\n            import { Links, Meta, Outlet, Scripts } from \"react-router\";\n\n            export default function Root() {\n              return (\n                <html lang=\"en\">\n                  <head>\n                    <Meta />\n                    <Links />\n                  </head>\n                  <body>\n                    <Outlet />\n                    <Scripts />\n                  </body>\n                </html>\n              );\n            }\n          `,\n\n          \"app/routes/_index.tsx\": js`\n            import { Link, Form } from \"react-router\";\n\n            export default function () {\n              return (\n                <div>\n                  <h1>Home</h1>\n                  <Form method=\"post\">\n                    <button formAction=\"${NO_ROOT_BOUNDARY_ACTION}\" type=\"submit\">\n                      Action go boom\n                    </button>\n                  </Form>\n                </div>\n              )\n            }\n          `,\n\n          [`app/routes${NO_ROOT_BOUNDARY_LOADER}.jsx`]: js`\n            export async function loader() {\n              throw Error(\"BLARGH\");\n            }\n\n            export default function () {\n              return (\n                <div>\n                  <h1>Hello</h1>\n                </div>\n              )\n            }\n          `,\n\n          [`app/routes${NO_ROOT_BOUNDARY_ACTION}.jsx`]: js`\n            export async function action() {\n              throw Error(\"YOOOOOOOO WHAT ARE YOU DOING\");\n            }\n\n            export default function () {\n              return (\n                <div>\n                  <h1>Goodbye</h1>\n                </div>\n              )\n            }\n          `,\n        },\n      });\n      appFixture = await createAppFixture(fixture);\n    });\n\n    test(\"bubbles to internal boundary in loader document requests\", async ({\n      page,\n    }) => {\n      let app = new PlaywrightFixture(appFixture, page);\n      await app.goto(NO_ROOT_BOUNDARY_LOADER);\n      expect(await app.getHtml(\"h1\")).toMatch(INTERNAL_ERROR_BOUNDARY_HEADING);\n    });\n\n    test(\"bubbles to internal boundary in action script transitions from other routes\", async ({\n      page,\n    }) => {\n      let app = new PlaywrightFixture(appFixture, page);\n      await app.goto(\"/\");\n      await app.clickSubmitButton(NO_ROOT_BOUNDARY_ACTION);\n      await page.waitForSelector(`text=${INTERNAL_ERROR_BOUNDARY_HEADING}`);\n      expect(await app.getHtml(\"h1\")).toMatch(INTERNAL_ERROR_BOUNDARY_HEADING);\n    });\n  });\n});\n\ntest.describe(\"Default ErrorBoundary\", () => {\n  let fixture: Fixture;\n  let appFixture: AppFixture;\n  let _consoleError: any;\n\n  function getFiles({\n    includeRootErrorBoundary = false,\n    rootErrorBoundaryThrows = false,\n  } = {}) {\n    let errorBoundaryCode = !includeRootErrorBoundary\n      ? \"\"\n      : rootErrorBoundaryThrows\n        ? js`\n          export function ErrorBoundary() {\n            let error = useRouteError();\n            return (\n              <html>\n                <head />\n                <body>\n                  <main>\n                    <div>Root Error Boundary</div>\n                    <p id=\"root-error-boundary\">{error.message}</p>\n                    <p>{oh.no.what.have.i.done}</p>\n                  </main>\n                  <Scripts />\n                </body>\n              </html>\n            )\n          }\n        `\n        : js`\n        export function ErrorBoundary() {\n          let error = useRouteError();\n          return (\n            <html>\n              <head />\n              <body>\n                <main>\n                  <div>Root Error Boundary</div>\n                  <p id=\"root-error-boundary\">{error.message}</p>\n                </main>\n                <Scripts />\n              </body>\n            </html>\n          )\n        }\n      `;\n\n    return {\n      \"app/root.tsx\": js`\n          import { Links, Meta, Outlet, Scripts, useRouteError } from \"react-router\";\n\n          export default function Root() {\n            return (\n              <html lang=\"en\">\n                <head>\n                  <Meta />\n                  <Links />\n                </head>\n                <body>\n                  <main>\n                    <Outlet />\n                  </main>\n                  <Scripts />\n                </body>\n              </html>\n            );\n          }\n\n          ${errorBoundaryCode}\n        `,\n\n      \"app/routes/_index.tsx\": js`\n          import { Link } from \"react-router\";\n          export default function () {\n            return (\n              <div>\n                <h1 id=\"index\">Index</h1>\n                <Link to=\"/loader-error\">Loader Error</Link>\n                <Link to=\"/render-error\">Render Error</Link>\n              </div>\n            );\n          }\n        `,\n\n      \"app/routes/loader-error.tsx\": js`\n          export function loader() {\n            throw new Error('Loader Error');\n          }\n          export default function () {\n            return <h1 id=\"loader-error\">Loader Error</h1>\n          }\n        `,\n\n      \"app/routes/render-error.tsx\": js`\n          export default function () {\n            throw new Error(\"Render Error\")\n          }\n        `,\n    };\n  }\n\n  test.beforeAll(async () => {\n    _consoleError = console.error;\n    console.error = () => {};\n  });\n\n  test.afterAll(async () => {\n    console.error = _consoleError;\n    appFixture.close();\n  });\n\n  test.describe(\"When the root route does not have a boundary\", () => {\n    test.beforeAll(async () => {\n      fixture = await createFixture(\n        {\n          files: getFiles({ includeRootErrorBoundary: false }),\n        },\n        ServerMode.Development,\n      );\n      appFixture = await createAppFixture(fixture, ServerMode.Development);\n    });\n\n    test.afterAll(() => appFixture.close());\n\n    test.describe(\"document requests\", () => {\n      test(\"renders default boundary on loader errors\", async () => {\n        let res = await fixture.requestDocument(\"/loader-error\");\n        expect(res.status).toBe(500);\n        let text = await res.text();\n        expect(text).toMatch(\"Application Error\");\n        expect(text).toMatch(\"Loader Error\");\n        expect(text).not.toMatch(\"Root Error Boundary\");\n      });\n\n      test(\"renders default boundary on render errors\", async () => {\n        let res = await fixture.requestDocument(\"/render-error\");\n        expect(res.status).toBe(500);\n        let text = await res.text();\n        expect(text).toMatch(\"Application Error\");\n        expect(text).toMatch(\"Render Error\");\n        expect(text).not.toMatch(\"Root Error Boundary\");\n      });\n    });\n\n    test.describe(\"SPA navigations\", () => {\n      test(\"renders default boundary on loader errors\", async ({ page }) => {\n        let app = new PlaywrightFixture(appFixture, page);\n        await app.goto(\"/\");\n        await app.clickLink(\"/loader-error\");\n        await page.waitForSelector(\"pre\");\n        let html = await app.getHtml();\n        expect(html).toMatch(\"Application Error\");\n        expect(html).toMatch(\"Loader Error\");\n        expect(html).not.toMatch(\"Root Error Boundary\");\n\n        // Ensure we can click back to our prior page\n        await app.goBack();\n        await page.waitForSelector(\"h1#index\");\n      });\n\n      test(\"renders default boundary on render errors\", async ({\n        page,\n      }, workerInfo) => {\n        let app = new PlaywrightFixture(appFixture, page);\n        await app.goto(\"/\");\n        await app.clickLink(\"/render-error\");\n        await page.waitForSelector(\"pre\");\n        let html = await app.getHtml();\n        expect(html).toMatch(\"Application Error\");\n        // Chromium seems to be the only one that includes the message in the stack\n        if (workerInfo.project.name === \"chromium\") {\n          expect(html).toMatch(\"Render Error\");\n        }\n        expect(html).not.toMatch(\"Root Error Boundary\");\n\n        // Ensure we can click back to our prior page\n        await app.goBack();\n        await page.waitForSelector(\"h1#index\");\n      });\n    });\n  });\n\n  test.describe(\"When the root route has a boundary\", () => {\n    test.beforeAll(async () => {\n      fixture = await createFixture(\n        {\n          files: getFiles({ includeRootErrorBoundary: true }),\n        },\n        ServerMode.Development,\n      );\n      appFixture = await createAppFixture(fixture, ServerMode.Development);\n    });\n\n    test.afterAll(() => appFixture.close());\n\n    test.describe(\"document requests\", () => {\n      test(\"renders root boundary on loader errors\", async () => {\n        let res = await fixture.requestDocument(\"/loader-error\");\n        expect(res.status).toBe(500);\n        let text = await res.text();\n        expect(text).toMatch(\"Root Error Boundary\");\n        expect(text).toMatch(\"Loader Error\");\n        expect(text).not.toMatch(\"Application Error\");\n      });\n\n      test(\"renders root boundary on render errors\", async () => {\n        let res = await fixture.requestDocument(\"/render-error\");\n        expect(res.status).toBe(500);\n        let text = await res.text();\n        expect(text).toMatch(\"Root Error Boundary\");\n        expect(text).toMatch(\"Render Error\");\n        expect(text).not.toMatch(\"Application Error\");\n      });\n    });\n\n    test.describe(\"SPA navigations\", () => {\n      test(\"renders root boundary on loader errors\", async ({ page }) => {\n        let app = new PlaywrightFixture(appFixture, page);\n        await app.goto(\"/\");\n        await app.clickLink(\"/loader-error\");\n        await page.waitForSelector(\"#root-error-boundary\");\n        let html = await app.getHtml();\n        expect(html).toMatch(\"Root Error Boundary\");\n        expect(html).toMatch(\"Loader Error\");\n        expect(html).not.toMatch(\"Application Error\");\n\n        // Ensure we can click back to our prior page\n        await app.goBack();\n        await page.waitForSelector(\"h1#index\");\n      });\n\n      test(\"renders root boundary on render errors\", async ({ page }) => {\n        let app = new PlaywrightFixture(appFixture, page);\n        await app.goto(\"/\");\n        await app.clickLink(\"/render-error\");\n        await page.waitForSelector(\"#root-error-boundary\");\n        let html = await app.getHtml();\n        expect(html).toMatch(\"Root Error Boundary\");\n        expect(html).toMatch(\"Render Error\");\n        expect(html).not.toMatch(\"Application Error\");\n\n        // Ensure we can click back to our prior page\n        await app.goBack();\n        await page.waitForSelector(\"h1#index\");\n      });\n    });\n  });\n\n  test.describe(\"When the root route has a boundary but it also throws 😦\", () => {\n    test.beforeAll(async () => {\n      fixture = await createFixture({\n        files: getFiles({\n          includeRootErrorBoundary: true,\n          rootErrorBoundaryThrows: true,\n        }),\n      });\n      appFixture = await createAppFixture(fixture, ServerMode.Development);\n    });\n\n    test.afterAll(() => appFixture.close());\n\n    test.describe(\"document requests\", () => {\n      test(\"tries to render root boundary on loader errors but bubbles to default boundary\", async () => {\n        let res = await fixture.requestDocument(\"/loader-error\");\n        expect(res.status).toBe(500);\n        let text = await res.text();\n        expect(text).toMatch(\"Unexpected Server Error\");\n        expect(text).not.toMatch(\"Application Error\");\n        expect(text).not.toMatch(\"Loader Error\");\n        expect(text).not.toMatch(\"Root Error Boundary\");\n      });\n\n      test(\"tries to render root boundary on render errors but bubbles to default boundary\", async () => {\n        let res = await fixture.requestDocument(\"/render-error\");\n        expect(res.status).toBe(500);\n        let text = await res.text();\n        expect(text).toMatch(\"Unexpected Server Error\");\n        expect(text).not.toMatch(\"Application Error\");\n        expect(text).not.toMatch(\"Render Error\");\n        expect(text).not.toMatch(\"Root Error Boundary\");\n      });\n    });\n\n    test.describe(\"SPA navigations\", () => {\n      test(\"tries to render root boundary on loader errors but bubbles to default boundary\", async ({\n        page,\n      }, workerInfo) => {\n        let app = new PlaywrightFixture(appFixture, page);\n        await app.goto(\"/\");\n        await app.clickLink(\"/loader-error\");\n        await page.waitForSelector(\"pre\");\n        let html = await app.getHtml();\n        expect(html).toMatch(\"Application Error\");\n        if (workerInfo.project.name === \"chromium\") {\n          expect(html).toMatch(\"ReferenceError: oh is not defined\");\n        }\n        expect(html).not.toMatch(\"Loader Error\");\n        expect(html).not.toMatch(\"Root Error Boundary\");\n\n        // Ensure we can click back to our prior page\n        await app.goBack();\n        await page.waitForSelector(\"h1#index\");\n      });\n\n      test(\"tries to render root boundary on render errors but bubbles to default boundary\", async ({\n        page,\n      }, workerInfo) => {\n        let app = new PlaywrightFixture(appFixture, page);\n        await app.goto(\"/\");\n        await app.clickLink(\"/render-error\");\n        await page.waitForSelector(\"pre\");\n        let html = await app.getHtml();\n        expect(html).toMatch(\"Application Error\");\n        if (workerInfo.project.name === \"chromium\") {\n          expect(html).toMatch(\"ReferenceError: oh is not defined\");\n        }\n        expect(html).not.toMatch(\"Render Error\");\n        expect(html).not.toMatch(\"Root Error Boundary\");\n\n        // Ensure we can click back to our prior page\n        await app.goBack();\n        await page.waitForSelector(\"h1#index\");\n      });\n    });\n  });\n});\n\ntest(\"Allows back-button out of an error boundary after a hard reload\", async ({\n  page,\n  browserName,\n}) => {\n  let _consoleError = console.error;\n  console.error = () => {};\n\n  let fixture = await createFixture({\n    files: {\n      \"app/root.tsx\": js`\n          import { Links, Meta, Outlet, Scripts, useRouteError } from \"react-router\";\n\n          export default function App() {\n            return (\n              <html lang=\"en\">\n                <head>\n                  <Meta />\n                  <Links />\n                </head>\n                <body>\n                  <Outlet />\n                  <Scripts />\n                </body>\n              </html>\n            );\n          }\n\n          export function ErrorBoundary() {\n            let error = useRouteError();\n            return (\n              <html>\n                <head>\n                  <title>Oh no!</title>\n                  <Meta />\n                  <Links />\n                </head>\n                <body>\n                  <h1 id=\"error\">ERROR BOUNDARY</h1>\n                  <Scripts />\n                </body>\n              </html>\n            );\n          }\n        `,\n      \"app/routes/_index.tsx\": js`\n          import { Link } from \"react-router\";\n\n          export default function Index() {\n            return (\n              <div>\n                <h1 id=\"index\">INDEX</h1>\n                <Link to=\"/boom\">This will error</Link>\n              </div>\n            );\n          }\n        `,\n\n      \"app/routes/boom.tsx\": js`\n          export function loader() { return boom(); }\n          export default function() { return <b>my page</b>; }\n        `,\n    },\n  });\n\n  let appFixture = await createAppFixture(fixture);\n  let app = new PlaywrightFixture(appFixture, page);\n\n  await app.goto(\"/\");\n  await page.waitForSelector(\"#index\");\n  expect(app.page.url()).not.toMatch(\"/boom\");\n\n  await app.clickLink(\"/boom\");\n  await page.waitForSelector(\"#error\");\n  expect(app.page.url()).toMatch(\"/boom\");\n\n  await app.reload();\n  await page.waitForSelector(\"#error\");\n  expect(app.page.url()).toMatch(\"boom\");\n\n  await app.goBack();\n\n  // Here be dragons\n  // - Playwright sets the Firefox `fission.webContentIsolationStrategy=0` preference\n  //   for reasons having to do with out-of-process iframes:\n  //   https://github.com/microsoft/playwright/issues/22640#issuecomment-1543287282\n  // - That preference exposes a bug in firefox where a hard reload adds to the\n  //   history stack: https://bugzilla.mozilla.org/show_bug.cgi?id=1832341\n  // - Your can disable this preference via the Playwright `firefoxUserPrefs` config,\n  //   but that is broken until 1.34:\n  //   https://github.com/microsoft/playwright/issues/22640#issuecomment-1546230104\n  //   https://github.com/microsoft/playwright/issues/15405\n  // - We can't yet upgrade to 1.34 because it drops support for Node 14:\n  //   https://github.com/microsoft/playwright/releases/tag/v1.34.0\n  //\n  // So for now when in firefox we just navigate back twice to work around the issue\n  if (browserName === \"firefox\") {\n    await app.goBack();\n  }\n\n  await page.waitForSelector(\"#index\");\n  expect(app.page.url()).not.toContain(\"boom\");\n\n  appFixture.close();\n  console.error = _consoleError;\n});\n"
  },
  {
    "path": "integration/error-boundary-v2-test.ts",
    "content": "import type { Page } from \"@playwright/test\";\nimport { test, expect } from \"@playwright/test\";\n\nimport { UNSAFE_ServerMode as ServerMode } from \"react-router\";\nimport {\n  createAppFixture,\n  createFixture,\n  js,\n} from \"./helpers/create-fixture.js\";\nimport type { Fixture, AppFixture } from \"./helpers/create-fixture.js\";\nimport { PlaywrightFixture } from \"./helpers/playwright-fixture.js\";\n\ntest.describe(\"ErrorBoundary\", () => {\n  let fixture: Fixture;\n  let appFixture: AppFixture;\n  let oldConsoleError: () => void;\n\n  test.beforeAll(async () => {\n    fixture = await createFixture({\n      files: {\n        \"app/root.tsx\": js`\n          import { Links, Meta, Outlet, Scripts } from \"react-router\";\n\n          export default function Root() {\n            return (\n              <html lang=\"en\">\n                <head>\n                  <Meta />\n                  <Links />\n                </head>\n                <body>\n                  <main>\n                    <Outlet />\n                  </main>\n                  <Scripts />\n                </body>\n              </html>\n            );\n          }\n        `,\n\n        \"app/routes/parent.tsx\": js`\n          import {\n            Link,\n            Outlet,\n            isRouteErrorResponse,\n            useLoaderData,\n            useRouteError,\n          } from \"react-router\";\n\n          export function loader() {\n            return \"PARENT LOADER\";\n          }\n\n          export default function Component() {\n            return (\n              <div>\n                <nav>\n                  <ul>\n                    <li><Link to=\"/parent/child-with-boundary\">Link</Link></li>\n                    <li><Link to=\"/parent/child-with-boundary?type=error\">Link</Link></li>\n                    <li><Link to=\"/parent/child-with-boundary?type=response\">Link</Link></li>\n                    <li><Link to=\"/parent/child-with-boundary?type=render\">Link</Link></li>\n                    <li><Link to=\"/parent/child-without-boundary?type=error\">Link</Link></li>\n                    <li><Link to=\"/parent/child-without-boundary?type=response\">Link</Link></li>\n                    <li><Link to=\"/parent/child-without-boundary?type=render\">Link</Link></li>\n                  </ul>\n                </nav>\n                <p id=\"parent-data\">{useLoaderData()}</p>\n                <Outlet />\n              </div>\n            )\n          }\n\n          export function ErrorBoundary() {\n            let error = useRouteError();\n            return isRouteErrorResponse(error) ?\n              <p id=\"parent-error-response\">{error.status + ' ' + error.data}</p> :\n              <p id=\"parent-error\">{error.message}</p>;\n          }\n        `,\n\n        \"app/routes/parent.child-with-boundary.tsx\": js`\n          import {\n            isRouteErrorResponse,\n            useLoaderData,\n            useLocation,\n            useRouteError,\n          } from \"react-router\";\n\n          export function loader({ request }) {\n            let errorType = new URL(request.url).searchParams.get('type');\n            if (errorType === 'response') {\n              throw new Response('Loader Response', { status: 418 });\n            } else if (errorType === 'error') {\n              throw new Error('Loader Error');\n            }\n            return \"CHILD LOADER\";\n          }\n\n          export default function Component() {;\n            let data = useLoaderData();\n            if (new URLSearchParams(useLocation().search).get('type') === \"render\") {\n              throw new Error(\"Render Error\");\n            }\n            return <p id=\"child-data\">{data}</p>;\n          }\n\n          export function ErrorBoundary() {\n            let error = useRouteError();\n            return isRouteErrorResponse(error) ?\n              <p id=\"child-error-response\">{error.status + ' ' + error.data}</p> :\n              <p id=\"child-error\">{error.message}</p>;\n          }\n        `,\n\n        \"app/routes/parent.child-without-boundary.tsx\": js`\n          import { useLoaderData, useLocation } from \"react-router\";\n\n          export function loader({ request }) {\n            let errorType = new URL(request.url).searchParams.get('type');\n            if (errorType === 'response') {\n              throw new Response('Loader Response', { status: 418 });\n            } else if (errorType === 'error') {\n              throw new Error('Loader Error');\n            }\n            return \"CHILD LOADER\";\n          }\n\n          export default function Component() {;\n            let data = useLoaderData();\n            if (new URLSearchParams(useLocation().search).get('type') === \"render\") {\n              throw new Error(\"Render Error\");\n            }\n            return <p id=\"child-data\">{data}</p>;\n          }\n        `,\n      },\n    });\n\n    appFixture = await createAppFixture(fixture, ServerMode.Development);\n  });\n\n  test.afterAll(() => {\n    appFixture.close();\n  });\n\n  test.beforeEach(({ page }) => {\n    oldConsoleError = console.error;\n    console.error = () => {};\n  });\n\n  test.afterEach(() => {\n    console.error = oldConsoleError;\n  });\n\n  test.describe(\"without JavaScript\", () => {\n    test.use({ javaScriptEnabled: false });\n    runBoundaryTests();\n  });\n\n  test.describe(\"with JavaScript\", () => {\n    test.use({ javaScriptEnabled: true });\n    runBoundaryTests();\n\n    test(\"Network errors that never reach the Remix server\", async ({\n      page,\n    }) => {\n      // Cause a .data request to trigger an HTTP error that never reaches the\n      // Remix server, and ensure we properly handle it at the ErrorBoundary\n      await page.route(/\\/parent\\/child-with-boundary\\.data$/, (route) => {\n        route.fulfill({ status: 500, body: \"CDN Error!\" });\n      });\n      let app = new PlaywrightFixture(appFixture, page);\n      await app.goto(\"/parent\");\n      await app.clickLink(\"/parent/child-with-boundary\");\n      await waitForAndAssert(\n        page,\n        app,\n        \"#parent-error-response\",\n        \"500 CDN Error!\",\n      );\n    });\n  });\n\n  function runBoundaryTests() {\n    test(\"No errors\", async ({ page }) => {\n      let app = new PlaywrightFixture(appFixture, page);\n      await app.goto(\"/parent\");\n      await app.clickLink(\"/parent/child-with-boundary\");\n      await waitForAndAssert(page, app, \"#child-data\", \"CHILD LOADER\");\n    });\n\n    test(\"Throwing a Response to own boundary\", async ({ page }) => {\n      let app = new PlaywrightFixture(appFixture, page);\n      await app.goto(\"/parent\");\n      await app.clickLink(\"/parent/child-with-boundary?type=response\");\n      await waitForAndAssert(\n        page,\n        app,\n        \"#child-error-response\",\n        \"418 Loader Response\",\n      );\n    });\n\n    test(\"Throwing an Error to own boundary\", async ({ page }) => {\n      let app = new PlaywrightFixture(appFixture, page);\n      await app.goto(\"/parent\");\n      await app.clickLink(\"/parent/child-with-boundary?type=error\");\n      await waitForAndAssert(page, app, \"#child-error\", \"Loader Error\");\n    });\n\n    test(\"Throwing a render error to own boundary\", async ({ page }) => {\n      let app = new PlaywrightFixture(appFixture, page);\n      await app.goto(\"/parent\");\n      await app.clickLink(\"/parent/child-with-boundary?type=render\");\n      await waitForAndAssert(page, app, \"#child-error\", \"Render Error\");\n    });\n\n    test(\"Throwing a Response to parent boundary\", async ({ page }) => {\n      let app = new PlaywrightFixture(appFixture, page);\n      await app.goto(\"/parent\");\n      await app.clickLink(\"/parent/child-without-boundary?type=response\");\n      await waitForAndAssert(\n        page,\n        app,\n        \"#parent-error-response\",\n        \"418 Loader Response\",\n      );\n    });\n\n    test(\"Throwing an Error to parent boundary\", async ({ page }) => {\n      let app = new PlaywrightFixture(appFixture, page);\n      await app.goto(\"/parent\");\n      await app.clickLink(\"/parent/child-without-boundary?type=error\");\n      await waitForAndAssert(page, app, \"#parent-error\", \"Loader Error\");\n    });\n\n    test(\"Throwing a render error to parent boundary\", async ({ page }) => {\n      let app = new PlaywrightFixture(appFixture, page);\n      await app.goto(\"/parent\");\n      await app.clickLink(\"/parent/child-without-boundary?type=render\");\n      await waitForAndAssert(page, app, \"#parent-error\", \"Render Error\");\n    });\n  }\n});\n\n// Shorthand util to wait for an element to appear before asserting it\nasync function waitForAndAssert(\n  page: Page,\n  app: PlaywrightFixture,\n  selector: string,\n  match: string,\n) {\n  await page.waitForSelector(selector);\n  expect(await app.getHtml(selector)).toMatch(match);\n}\n"
  },
  {
    "path": "integration/error-data-request-test.ts",
    "content": "import { test, expect } from \"@playwright/test\";\nimport { UNSAFE_ErrorResponseImpl as ErrorResponseImpl } from \"react-router\";\n\nimport {\n  createAppFixture,\n  createFixture,\n  js,\n} from \"./helpers/create-fixture.js\";\nimport type { Fixture, AppFixture } from \"./helpers/create-fixture.js\";\n\ntest.describe(\"ErrorBoundary\", () => {\n  let fixture: Fixture;\n  let appFixture: AppFixture;\n  let _consoleError: any;\n  let errorLogs: any[];\n\n  test.beforeAll(async () => {\n    _consoleError = console.error;\n    console.error = (v) => errorLogs.push(v);\n\n    fixture = await createFixture({\n      files: {\n        \"app/root.tsx\": js`\n          import { Links, Meta, Outlet, Scripts } from \"react-router\";\n\n          export default function Root() {\n            return (\n              <html lang=\"en\">\n                <head>\n                  <Meta />\n                  <Links />\n                </head>\n                <body>\n                  <Outlet />\n                  <Scripts />\n                </body>\n              </html>\n            );\n          }\n        `,\n\n        \"app/routes/_index.tsx\": js`\n          import { Link, Form } from \"react-router\";\n\n          export default function () {\n            return <h1>Index</h1>\n          }\n        `,\n\n        [`app/routes/loader-throw-error.jsx`]: js`\n          export async function loader() {\n            throw Error(\"BLARGH\");\n          }\n\n          export default function () {\n              return <h1>Hello</h1>\n          }\n        `,\n\n        [`app/routes/loader-return-json.jsx`]: js`\n          export async function loader() {\n            return { ok: true };\n          }\n\n          export default function () {\n              return <h1>Hello</h1>\n          }\n        `,\n\n        [`app/routes/action-throw-error.jsx`]: js`\n          export async function action() {\n            throw Error(\"YOOOOOOOO WHAT ARE YOU DOING\");\n          }\n\n          export default function () {\n            return <h1>Goodbye</h1>;\n          }\n        `,\n\n        [`app/routes/action-return-json.jsx`]: js`\n          export async function action() {\n            return { ok: true };\n          }\n\n          export default function () {\n            return <h1>Hi!</h1>\n          }\n        `,\n      },\n    });\n    appFixture = await createAppFixture(fixture);\n  });\n\n  test.beforeEach(async () => {\n    errorLogs = [];\n  });\n\n  test.afterAll(() => {\n    console.error = _consoleError;\n    appFixture.close();\n  });\n\n  function assertLoggedErrorInstance(message: string) {\n    let error = errorLogs[0] as Error;\n    expect(error).toBeInstanceOf(Error);\n    expect(error.message).toEqual(message);\n  }\n\n  test(\"returns a 200 empty response on a data fetch to a path with no loaders\", async () => {\n    let { status, headers, data } =\n      await fixture.requestSingleFetchData(\"/_root.data\");\n    expect(status).toBe(200);\n    expect(headers.has(\"X-Remix-Response\")).toBe(true);\n    expect(data).toEqual({});\n  });\n\n  test(\"returns a 405 on a data fetch POST to a path with no action\", async () => {\n    let { status, headers, data } = await fixture.requestSingleFetchData(\n      \"/_root.data?index\",\n      {\n        method: \"POST\",\n      },\n    );\n    expect(status).toBe(405);\n    expect(headers.has(\"X-Remix-Response\")).toBe(true);\n    expect(data).toEqual({\n      error: new ErrorResponseImpl(\n        405,\n        \"Method Not Allowed\",\n        'Error: You made a POST request to \"/\" but did not provide an `action` for route \"routes/_index\", so there is no way to handle the request.',\n      ),\n    });\n    assertLoggedErrorInstance(\n      'You made a POST request to \"/\" but did not provide an `action` for route \"routes/_index\", so there is no way to handle the request.',\n    );\n  });\n\n  test(\"returns a 405 on a data fetch with a bad method\", async () => {\n    try {\n      await fixture.requestSingleFetchData(\"/loader-return-json.data\", {\n        method: \"TRACE\",\n      });\n      expect(false).toBe(true);\n    } catch (e) {\n      expect((e as Error).message).toMatch(\n        \"'TRACE' HTTP method is unsupported.\",\n      );\n    }\n  });\n\n  test(\"returns a 404 on a data fetch to a path with no matches\", async () => {\n    let { status, headers, data } = await fixture.requestSingleFetchData(\n      \"/i/match/nothing.data\",\n    );\n    expect(status).toBe(404);\n    expect(headers.has(\"X-Remix-Response\")).toBe(true);\n    expect(data).toEqual({\n      root: {\n        error: new ErrorResponseImpl(\n          404,\n          \"Not Found\",\n          'Error: No route matches URL \"/i/match/nothing\"',\n        ),\n      },\n    });\n    assertLoggedErrorInstance('No route matches URL \"/i/match/nothing\"');\n  });\n});\n"
  },
  {
    "path": "integration/error-sanitization-test.ts",
    "content": "import { test, expect } from \"@playwright/test\";\nimport {\n  UNSAFE_ErrorResponseImpl as ErrorResponseImpl,\n  UNSAFE_ServerMode as ServerMode,\n} from \"react-router\";\n\nimport type { Fixture } from \"./helpers/create-fixture.js\";\nimport {\n  createAppFixture,\n  createFixture,\n  js,\n} from \"./helpers/create-fixture.js\";\nimport { PlaywrightFixture } from \"./helpers/playwright-fixture.js\";\n\nconst routeFiles = {\n  \"app/root.tsx\": js`\n    import { Links, Meta, Outlet, Scripts } from \"react-router\";\n\n    export default function Root() {\n      return (\n        <html lang=\"en\">\n          <head>\n            <Meta />\n            <Links />\n          </head>\n          <body>\n            <main>\n              <Outlet />\n            </main>\n            <Scripts />\n          </body>\n        </html>\n      );\n    }\n  `,\n\n  \"app/routes/_index.tsx\": js`\n    import { useLoaderData, useLocation, useRouteError } from \"react-router\";\n\n    export function loader({ request }) {\n      if (new URL(request.url).searchParams.has('loader')) {\n        throw new Error(\"Loader Error\");\n      }\n      if (new URL(request.url).searchParams.has('subclass')) {\n        // This will throw a ReferenceError\n        console.log(thisisnotathing);\n      }\n      return \"LOADER\"\n    }\n\n    export default function Component() {\n      let data = useLoaderData();\n      let location = useLocation();\n\n      if (location.search.includes('render')) {\n        throw new Error(\"Render Error\");\n      }\n\n      return (\n        <>\n          <h1>Index Route</h1>\n          <p>{JSON.stringify(data)}</p>\n        </>\n      );\n    }\n\n    export function ErrorBoundary() {\n      let error = useRouteError();\n      return (\n        <>\n          <h1>Index Error</h1>\n          <p>{\"MESSAGE:\" + error.message}</p>\n          <p>{\"NAME:\" + error.name}</p>\n          {error.stack ? <p>{\"STACK:\" + error.stack}</p> : null}\n        </>\n      );\n    }\n  `,\n\n  \"app/routes/defer.tsx\": js`\n    import * as React from 'react';\n    import { Await, useAsyncError, useLoaderData, useRouteError } from \"react-router\";\n\n    export function loader({ request }) {\n      if (new URL(request.url).searchParams.has('loader')) {\n        return {\n          lazy: Promise.reject(new Error(\"REJECTED\")),\n        };\n      }\n      return {\n        lazy: Promise.resolve(\"RESOLVED\"),\n      };\n    }\n\n    export default function Component() {\n      let data = useLoaderData();\n\n      return (\n        <>\n          <h1>Defer Route</h1>\n          <React.Suspense fallback={<p>Loading...</p>}>\n            <Await resolve={data.lazy} errorElement={<AwaitError />}>\n              {(val) => <p>{val}</p>}\n            </Await>\n          </React.Suspense>\n        </>\n      );\n    }\n\n    function AwaitError() {\n      let error = useAsyncError();\n      return (\n        <>\n          <h2>Defer Error</h2>\n          <p>{error.message}</p>\n        </>\n      );\n    }\n\n    export function ErrorBoundary() {\n      let error = useRouteError();\n      return (\n        <>\n          <h1>Defer Error</h1>\n          <p>{\"MESSAGE:\" + error.message}</p>\n          {error.stack ? <p>{\"STACK:\" + error.stack}</p> : null}\n        </>\n      );\n    }\n  `,\n\n  \"app/routes/resource.tsx\": js`\n    export function loader({ request }) {\n      if (new URL(request.url).searchParams.has('loader')) {\n        throw new Error(\"Loader Error\");\n      }\n      return \"RESOURCE LOADER\"\n    }\n  `,\n};\n\ntest.describe(\"Error Sanitization\", () => {\n  let fixture: Fixture;\n  let oldConsoleError: () => void;\n  let errorLogs: any[] = [];\n\n  test.beforeEach(() => {\n    oldConsoleError = console.error;\n    errorLogs = [];\n    console.error = (...args) => errorLogs.push(args);\n  });\n\n  test.afterEach(() => {\n    console.error = oldConsoleError;\n  });\n\n  test.describe(\"serverMode=production\", () => {\n    test.beforeAll(async () => {\n      fixture = await createFixture(\n        {\n          files: routeFiles,\n        },\n        ServerMode.Production,\n      );\n    });\n\n    test(\"renders document without errors\", async () => {\n      let response = await fixture.requestDocument(\"/\");\n      let html = await response.text();\n      expect(html).toMatch(\"Index Route\");\n      expect(html).toMatch(\"LOADER\");\n      expect(html).not.toMatch(\"MESSAGE:\");\n      expect(html).not.toMatch(/stack/i);\n    });\n\n    test(\"sanitizes loader errors in document requests\", async () => {\n      let response = await fixture.requestDocument(\"/?loader\");\n      let html = await response.text();\n      expect(html).toMatch(\"Index Error\");\n      expect(html).not.toMatch(\"LOADER\");\n      expect(html).toMatch(\"MESSAGE:Unexpected Server Error\");\n      // This is the turbo-stream encoding - the fact that stack goes right\n      // into __type means it has no value\n      expect(html).toMatch(\n        '\\\\\"message\\\\\",\\\\\"Unexpected Server Error\\\\\",\\\\\"stack\\\\\",\\\\\"__type\\\\\",\\\\\"Error\\\\\"',\n      );\n      expect(html).not.toMatch(/ at /i);\n      expect(errorLogs.length).toBe(1);\n      expect(errorLogs[0][0].message).toMatch(\"Loader Error\");\n      expect(errorLogs[0][0].stack).toMatch(\" at \");\n    });\n\n    test(\"sanitizes render errors in document requests\", async () => {\n      let response = await fixture.requestDocument(\"/?render\");\n      let html = await response.text();\n      expect(html).toMatch(\"Index Error\");\n      expect(html).toMatch(\"MESSAGE:Unexpected Server Error\");\n      // This is the turbo-stream encoding - the fact that stack goes right\n      // into __type means it has no value\n      expect(html).toMatch(\n        '\\\\\"message\\\\\",\\\\\"Unexpected Server Error\\\\\",\\\\\"stack\\\\\",\\\\\"__type\\\\\",\\\\\"Error\\\\\"',\n      );\n      expect(html).not.toMatch(/ at /i);\n      expect(errorLogs.length).toBe(1);\n      expect(errorLogs[0][0].message).toMatch(\"Render Error\");\n      expect(errorLogs[0][0].stack).toMatch(\" at \");\n    });\n\n    test(\"renders deferred document without errors\", async () => {\n      let response = await fixture.requestDocument(\"/defer\");\n      let html = await response.text();\n      expect(html).toMatch(\"Defer Route\");\n      expect(html).toMatch(\"RESOLVED\");\n      expect(html).not.toMatch(\"MESSAGE:\");\n      // Defer errors are not part of the JSON blob but rather rejected\n      // against a pending promise and therefore are inlined JS.\n      expect(html).not.toMatch(\"x.stack=e.stack;\");\n    });\n\n    test(\"sanitizes defer errors in document requests\", async () => {\n      let response = await fixture.requestDocument(\"/defer?loader\");\n      let html = await response.text();\n      expect(html).toMatch(\"Defer Error\");\n      expect(html).not.toMatch(\"RESOLVED\");\n      expect(html).toMatch(\"Unexpected Server Error\");\n      expect(html).not.toMatch(\"stack\");\n      // defer errors are not logged to the server console since the request\n      // has \"succeeded\"\n      expect(errorLogs.length).toBe(0);\n    });\n\n    test(\"returns data without errors\", async () => {\n      let { data } = await fixture.requestSingleFetchData(\"/_root.data\");\n      expect(data).toEqual({\n        \"routes/_index\": {\n          data: \"LOADER\",\n        },\n      });\n    });\n\n    test(\"sanitizes loader errors in data requests\", async () => {\n      let { data } = await fixture.requestSingleFetchData(\"/_root.data?loader\");\n      expect(data).toEqual({\n        \"routes/_index\": {\n          error: new Error(\"Unexpected Server Error\"),\n        },\n      });\n      expect(errorLogs.length).toBe(1);\n      expect(errorLogs[0][0].message).toMatch(\"Loader Error\");\n      expect(errorLogs[0][0].stack).toMatch(\" at \");\n    });\n\n    test(\"returns deferred data without errors\", async () => {\n      let { data } = await fixture.requestSingleFetchData(\"/defer.data\");\n      // @ts-expect-error\n      expect(await data[\"routes/defer\"].data.lazy).toEqual(\"RESOLVED\");\n    });\n\n    test(\"sanitizes loader errors in deferred data requests\", async () => {\n      let { data } = await fixture.requestSingleFetchData(\"/defer.data?loader\");\n      try {\n        // @ts-expect-error\n        await data[\"routes/defer\"].data.lazy;\n        expect(true).toBe(false);\n      } catch (e) {\n        expect((e as Error).message).toBe(\"Unexpected Server Error\");\n        expect((e as Error).stack).toBeUndefined();\n      }\n      // defer errors are not logged to the server console since the request\n      // has \"succeeded\"\n      expect(errorLogs.length).toBe(0);\n    });\n\n    test(\"sanitizes loader errors in resource requests\", async () => {\n      let response = await fixture.requestResource(\"/resource?loader\");\n      let text = await response.text();\n      expect(text).toBe(\"Unexpected Server Error\");\n      expect(errorLogs.length).toBe(1);\n      expect(errorLogs[0][0].message).toMatch(\"Loader Error\");\n      expect(errorLogs[0][0].stack).toMatch(\" at \");\n    });\n\n    test(\"does not sanitize mismatched route errors in data requests\", async () => {\n      let { data } = await fixture.requestSingleFetchData(\"/not-a-route.data\");\n      expect(data).toEqual({\n        root: {\n          error: new ErrorResponseImpl(\n            404,\n            \"Not Found\",\n            'Error: No route matches URL \"/not-a-route\"',\n          ),\n        },\n      });\n      expect(errorLogs).toEqual([\n        [new Error('No route matches URL \"/not-a-route\"')],\n      ]);\n    });\n\n    test(\"does not support hydration of Error subclasses\", async ({ page }) => {\n      let response = await fixture.requestDocument(\"/?subclass\");\n      let html = await response.text();\n      expect(html).toMatch(\"<p>MESSAGE:Unexpected Server Error\");\n      expect(html).toMatch(\"<p>NAME:Error\");\n\n      // Hydration\n      let appFixture = await createAppFixture(fixture);\n      let app = new PlaywrightFixture(appFixture, page);\n      await app.goto(\"/?subclass\", true);\n      html = await app.getHtml();\n      expect(html).toMatch(\"<p>MESSAGE:Unexpected Server Error\");\n      expect(html).toMatch(\"<p>NAME:Error\");\n    });\n  });\n\n  test.describe(\"serverMode=development\", () => {\n    test.beforeAll(async () => {\n      fixture = await createFixture(\n        {\n          files: routeFiles,\n        },\n        ServerMode.Development,\n      );\n    });\n    let ogEnv = process.env.NODE_ENV;\n    test.beforeEach(() => {\n      ogEnv = process.env.NODE_ENV;\n      process.env.NODE_ENV = \"development\";\n    });\n    test.afterEach(() => {\n      process.env.NODE_ENV = ogEnv;\n    });\n\n    test(\"renders document without errors\", async () => {\n      let response = await fixture.requestDocument(\"/\");\n      let html = await response.text();\n      expect(html).toMatch(\"Index Route\");\n      expect(html).toMatch(\"LOADER\");\n      expect(html).not.toMatch(\"MESSAGE:\");\n      expect(html).not.toMatch(/stack/i);\n    });\n\n    test(\"does not sanitize loader errors in document requests\", async () => {\n      let response = await fixture.requestDocument(\"/?loader\");\n      let html = await response.text();\n      expect(html).toMatch(\"Index Error\");\n      expect(html).not.toMatch(\"LOADER\");\n      expect(html).toMatch(\"<p>MESSAGE:Loader Error\");\n      expect(html).toMatch(\"<p>STACK:Error: Loader Error\");\n      expect(errorLogs.length).toBe(1);\n      expect(errorLogs[0][0].message).toMatch(\"Loader Error\");\n      expect(errorLogs[0][0].stack).toMatch(\" at \");\n    });\n\n    test(\"does not sanitize render errors in document requests\", async () => {\n      let response = await fixture.requestDocument(\"/?render\");\n      let html = await response.text();\n      expect(html).toMatch(\"Index Error\");\n      expect(html).toMatch(\"<p>MESSAGE:Render Error\");\n      expect(html).toMatch(\"<p>STACK:Error: Render Error\");\n      expect(errorLogs.length).toBe(1);\n      expect(errorLogs[0][0].message).toMatch(\"Render Error\");\n      expect(errorLogs[0][0].stack).toMatch(\" at \");\n    });\n\n    test(\"renders deferred document without errors\", async () => {\n      let response = await fixture.requestDocument(\"/defer\");\n      let html = await response.text();\n      expect(html).toMatch(\"Defer Route\");\n      expect(html).toMatch(\"RESOLVED\");\n      expect(html).not.toMatch(\"MESSAGE:\");\n      expect(html).not.toMatch(/\"stack\":/i);\n    });\n\n    test(\"does not sanitize defer errors in document requests\", async () => {\n      let response = await fixture.requestDocument(\"/defer?loader\");\n      let html = await response.text();\n      expect(html).toMatch(\"Defer Error\");\n      expect(html).not.toMatch(\"RESOLVED\");\n      expect(html).toMatch(\"<p>REJECTED</p>\");\n      expect(html).toMatch(\"Error: REJECTED\\\\\\\\n    at \");\n      // defer errors are not logged to the server console since the request\n      // has \"succeeded\"\n      expect(errorLogs.length).toBe(0);\n    });\n\n    test(\"returns data without errors\", async () => {\n      let { data } = await fixture.requestSingleFetchData(\"/_root.data\");\n      expect(data).toEqual({\n        \"routes/_index\": {\n          data: \"LOADER\",\n        },\n      });\n    });\n\n    test(\"does not sanitize loader errors in data requests\", async () => {\n      let { data } = await fixture.requestSingleFetchData(\"/_root.data?loader\");\n      expect(data).toEqual({\n        \"routes/_index\": {\n          error: new Error(\"Loader Error\"),\n        },\n      });\n      expect(errorLogs.length).toBe(1);\n      expect(errorLogs[0][0].message).toMatch(\"Loader Error\");\n      expect(errorLogs[0][0].stack).toMatch(\" at \");\n    });\n\n    test(\"returns deferred data without errors\", async () => {\n      let { data } = await fixture.requestSingleFetchData(\"/defer.data\");\n      // @ts-expect-error\n      expect(await data[\"routes/defer\"].data.lazy).toEqual(\"RESOLVED\");\n    });\n\n    test(\"does not sanitize loader errors in deferred data requests\", async () => {\n      let { data } = await fixture.requestSingleFetchData(\"/defer.data?loader\");\n      try {\n        // @ts-expect-error\n        await data[\"routes/defer\"].data.lazy;\n        expect(true).toBe(false);\n      } catch (e) {\n        expect((e as Error).message).toBe(\"REJECTED\");\n        expect((e as Error).stack).not.toBeUndefined();\n      }\n\n      // defer errors are not logged to the server console since the request\n      // has \"succeeded\"\n      expect(errorLogs.length).toBe(0);\n    });\n\n    test(\"does not sanitize loader errors in resource requests\", async () => {\n      let response = await fixture.requestResource(\"/resource?loader\");\n      let text = await response.text();\n      expect(text).toBe(\"Unexpected Server Error\\n\\nError: Loader Error\");\n      expect(errorLogs.length).toBe(1);\n      expect(errorLogs[0][0].message).toMatch(\"Loader Error\");\n      expect(errorLogs[0][0].stack).toMatch(\" at \");\n    });\n\n    test(\"does not sanitize mismatched route errors in data requests\", async () => {\n      let { data } = await fixture.requestSingleFetchData(\"/not-a-route.data\");\n      expect(data).toEqual({\n        root: {\n          error: new ErrorResponseImpl(\n            404,\n            \"Not Found\",\n            'Error: No route matches URL \"/not-a-route\"',\n          ),\n        },\n      });\n      expect(errorLogs).toEqual([\n        [new Error('No route matches URL \"/not-a-route\"')],\n      ]);\n    });\n\n    test(\"supports hydration of Error subclasses\", async ({ page }) => {\n      let response = await fixture.requestDocument(\"/?subclass\");\n      let html = await response.text();\n      expect(html).toMatch(\"<p>MESSAGE:thisisnotathing is not defined\");\n      expect(html).toMatch(\"<p>NAME:ReferenceError\");\n      expect(html).toMatch(\n        \"<p>STACK:ReferenceError: thisisnotathing is not defined\",\n      );\n\n      // Hydration\n      let appFixture = await createAppFixture(fixture, ServerMode.Development);\n      let app = new PlaywrightFixture(appFixture, page);\n      await app.goto(\"/?subclass\", true);\n      html = await app.getHtml();\n      expect(html).toMatch(\"<p>MESSAGE:thisisnotathing is not defined\");\n      expect(html).toMatch(\"<p>NAME:ReferenceError\");\n      expect(html).toMatch(\n        \"STACK:ReferenceError: thisisnotathing is not defined\",\n      );\n    });\n  });\n\n  test.describe(\"serverMode=production (user-provided handleError)\", () => {\n    test.beforeAll(async () => {\n      fixture = await createFixture(\n        {\n          files: {\n            \"app/entry.server.tsx\": js`\n              import { PassThrough } from \"node:stream\";\n\n              import { createReadableStreamFromReadable } from \"@react-router/node\";\n              import { ServerRouter, isRouteErrorResponse } from \"react-router\";\n              import { renderToPipeableStream } from \"react-dom/server\";\n\n              export default function handleRequest(\n                request,\n                responseStatusCode,\n                responseHeaders,\n                remixContext\n              ) {\n                return new Promise((resolve, reject) => {\n                  let shellRendered = false;\n                  const { pipe, abort } = renderToPipeableStream(\n                    <ServerRouter context={remixContext} url={request.url} />,\n                    {\n                      onShellReady() {\n                        shellRendered = true;\n                        const body = new PassThrough();\n                        const stream = createReadableStreamFromReadable(body);\n\n                        responseHeaders.set(\"Content-Type\", \"text/html\");\n\n                        resolve(\n                          new Response(stream, {\n                            headers: responseHeaders,\n                            status: responseStatusCode,\n                          })\n                        );\n\n                        pipe(body);\n                      },\n                      onShellError(error) {\n                        reject(error);\n                      },\n                      onError(error) {\n                        responseStatusCode = 500;\n                        // Log streaming rendering errors from inside the shell.  Don't log\n                        // errors encountered during initial shell rendering since they'll\n                        // reject and get logged in handleDocumentRequest.\n                        if (shellRendered) {\n                          console.error(error);\n                        }\n                      },\n                    }\n                  );\n\n                  setTimeout(abort, 5000);\n                });\n              }\n\n              export function handleError(\n                error: unknown,\n                { request }: { request: Request },\n              ) {\n                console.error(\"App Specific Error Logging:\");\n                console.error(\"  Request: \" + request.method + \" \" + request.url);\n                if (isRouteErrorResponse(error)) {\n                  console.error(\"  Status: \" + error.status + \" \" + error.statusText);\n                  console.error(\"  Error: \" + error.error.message);\n                  console.error(\"  Stack: \" + error.error.stack);\n                } else if (error instanceof Error) {\n                  console.error(\"  Error: \" + error.message);\n                  console.error(\"  Stack: \" + error.stack);\n                } else {\n                  console.error(\"Dunno what this is\");\n                }\n              }\n            `,\n            ...routeFiles,\n          },\n        },\n        ServerMode.Production,\n      );\n    });\n\n    test(\"renders document without errors\", async () => {\n      let response = await fixture.requestDocument(\"/\");\n      let html = await response.text();\n      expect(html).toMatch(\"Index Route\");\n      expect(html).toMatch(\"LOADER\");\n      expect(html).not.toMatch(\"MESSAGE:\");\n      expect(html).not.toMatch(/stack/i);\n    });\n\n    test(\"sanitizes loader errors in document requests\", async () => {\n      let response = await fixture.requestDocument(\"/?loader\");\n      let html = await response.text();\n      expect(html).toMatch(\"Index Error\");\n      expect(html).not.toMatch(\"LOADER\");\n      expect(html).toMatch(\"MESSAGE:Unexpected Server Error\");\n      // This is the turbo-stream encoding - the fact that stack goes right\n      // into __type means it has no value\n      expect(html).toMatch(\n        '\\\\\"message\\\\\",\\\\\"Unexpected Server Error\\\\\",\\\\\"stack\\\\\",\\\\\"__type\\\\\",\\\\\"Error\\\\\"',\n      );\n      expect(html).not.toMatch(/ at /i);\n      expect(errorLogs[0][0]).toEqual(\"App Specific Error Logging:\");\n      expect(errorLogs[1][0]).toEqual(\"  Request: GET test://test/?loader\");\n      expect(errorLogs[2][0]).toEqual(\"  Error: Loader Error\");\n      expect(errorLogs[3][0]).toMatch(\" at \");\n      expect(errorLogs.length).toBe(4);\n    });\n\n    test(\"sanitizes render errors in document requests\", async () => {\n      let response = await fixture.requestDocument(\"/?render\");\n      let html = await response.text();\n      expect(html).toMatch(\"Index Error\");\n      expect(html).toMatch(\"MESSAGE:Unexpected Server Error\");\n      // This is the turbo-stream encoding - the fact that stack goes right\n      // into __type means it has no value\n      expect(html).toMatch(\n        '\\\\\"message\\\\\",\\\\\"Unexpected Server Error\\\\\",\\\\\"stack\\\\\",\\\\\"__type\\\\\",\\\\\"Error\\\\\"',\n      );\n      expect(html).not.toMatch(/ at /i);\n      expect(errorLogs[0][0]).toEqual(\"App Specific Error Logging:\");\n      expect(errorLogs[1][0]).toEqual(\"  Request: GET test://test/?render\");\n      expect(errorLogs[2][0]).toEqual(\"  Error: Render Error\");\n      expect(errorLogs[3][0]).toMatch(\" at \");\n      expect(errorLogs.length).toBe(4);\n    });\n\n    test(\"renders deferred document without errors\", async () => {\n      let response = await fixture.requestDocument(\"/defer\");\n      let html = await response.text();\n      expect(html).toMatch(\"Defer Route\");\n      expect(html).toMatch(\"RESOLVED\");\n      expect(html).not.toMatch(\"MESSAGE:\");\n      // Defer errors are not part of the JSON blob but rather rejected\n      // against a pending promise and therefore are inlined JS.\n      expect(html).not.toMatch(\"x.stack=e.stack;\");\n    });\n\n    test(\"sanitizes defer errors in document requests\", async () => {\n      let response = await fixture.requestDocument(\"/defer?loader\");\n      let html = await response.text();\n      expect(html).toMatch(\"Defer Error\");\n      expect(html).not.toMatch(\"RESOLVED\");\n      expect(html).toMatch(\"Unexpected Server Error\");\n      expect(html).not.toMatch(\"stack\");\n      // defer errors are not logged to the server console since the request\n      // has \"succeeded\"\n      expect(errorLogs.length).toBe(0);\n    });\n\n    test(\"returns data without errors\", async () => {\n      let { data } = await fixture.requestSingleFetchData(\"/_root.data\");\n      expect(data).toEqual({\n        \"routes/_index\": {\n          data: \"LOADER\",\n        },\n      });\n    });\n\n    test(\"sanitizes loader errors in data requests\", async () => {\n      let { data } = await fixture.requestSingleFetchData(\"/_root.data?loader\");\n      expect(data).toEqual({\n        \"routes/_index\": {\n          error: new Error(\"Unexpected Server Error\"),\n        },\n      });\n      expect(errorLogs[0][0]).toEqual(\"App Specific Error Logging:\");\n      expect(errorLogs[1][0]).toEqual(\n        \"  Request: GET test://test/_root.data?loader\",\n      );\n      expect(errorLogs[2][0]).toEqual(\"  Error: Loader Error\");\n      expect(errorLogs[3][0]).toMatch(\" at \");\n      expect(errorLogs.length).toBe(4);\n    });\n\n    test(\"returns deferred data without errors\", async () => {\n      let { data } = await fixture.requestSingleFetchData(\"/defer.data\");\n      // @ts-expect-error\n      expect(await data[\"routes/defer\"].data.lazy).toBe(\"RESOLVED\");\n    });\n\n    test(\"sanitizes loader errors in deferred data requests\", async () => {\n      let { data } = await fixture.requestSingleFetchData(\"/defer.data?loader\");\n      try {\n        // @ts-expect-error\n        await data[\"routes/defer\"].data.lazy;\n        expect(true).toBe(false);\n      } catch (e) {\n        expect((e as Error).message).toBe(\"Unexpected Server Error\");\n        expect((e as Error).stack).toBeUndefined();\n      }\n      // defer errors are not logged to the server console since the request\n      // has \"succeeded\"\n      expect(errorLogs.length).toBe(0);\n    });\n\n    test(\"sanitizes loader errors in resource requests\", async () => {\n      let response = await fixture.requestResource(\"/resource?loader\");\n      let text = await response.text();\n      expect(text).toBe(\"Unexpected Server Error\");\n      expect(errorLogs[0][0]).toEqual(\"App Specific Error Logging:\");\n      expect(errorLogs[0][0]).toEqual(\"App Specific Error Logging:\");\n      expect(errorLogs[1][0]).toEqual(\n        \"  Request: GET test://test/resource?loader\",\n      );\n      expect(errorLogs[2][0]).toEqual(\"  Error: Loader Error\");\n      expect(errorLogs[3][0]).toMatch(\" at \");\n      expect(errorLogs.length).toBe(4);\n    });\n\n    test(\"does not sanitize mismatched route errors in data requests\", async () => {\n      let { data } = await fixture.requestSingleFetchData(\"/not-a-route.data\");\n      expect(data).toEqual({\n        root: {\n          error: new ErrorResponseImpl(\n            404,\n            \"Not Found\",\n            'Error: No route matches URL \"/not-a-route\"',\n          ),\n        },\n      });\n      expect(errorLogs[0][0]).toEqual(\"App Specific Error Logging:\");\n      expect(errorLogs[1][0]).toEqual(\n        \"  Request: GET test://test/not-a-route.data\",\n      );\n      expect(errorLogs[2][0]).toEqual(\"  Status: 404 Not Found\");\n      expect(errorLogs[3][0]).toEqual(\n        '  Error: No route matches URL \"/not-a-route\"',\n      );\n      expect(errorLogs[4][0]).toMatch(\" at \");\n      expect(errorLogs.length).toBe(5);\n    });\n  });\n});\n"
  },
  {
    "path": "integration/fetch-globals-test.ts",
    "content": "import { test, expect } from \"@playwright/test\";\n\nimport type { Fixture, AppFixture } from \"./helpers/create-fixture.js\";\nimport {\n  createAppFixture,\n  createFixture,\n  js,\n} from \"./helpers/create-fixture.js\";\n\nlet fixture: Fixture;\nlet appFixture: AppFixture;\n\ntest.beforeAll(async () => {\n  fixture = await createFixture({\n    files: {\n      \"app/routes/_index.tsx\": js`\n        import { useLoaderData } from \"react-router\";\n        export async function loader() {\n          const resp = await fetch('https://reqres.in/api/users?page=2');\n          return (resp instanceof Response) ? 'is an instance of global Response' : 'is not an instance of global Response';\n        }\n        export default function Index() {\n          let data = useLoaderData();\n          return (\n            <div>\n              {data}\n            </div>\n          )\n        }\n      `,\n    },\n  });\n\n  appFixture = await createAppFixture(fixture);\n});\n\ntest.afterAll(async () => appFixture.close());\n\ntest(\"returned variable from fetch() should be instance of global Response\", async () => {\n  let response = await fixture.requestDocument(\"/\");\n  expect(await response.text()).toMatch(\"is an instance of global Response\");\n});\n"
  },
  {
    "path": "integration/fetcher-layout-test.ts",
    "content": "import { test, expect } from \"@playwright/test\";\n\nimport {\n  createAppFixture,\n  createFixture,\n  js,\n} from \"./helpers/create-fixture.js\";\nimport type { Fixture, AppFixture } from \"./helpers/create-fixture.js\";\nimport { PlaywrightFixture } from \"./helpers/playwright-fixture.js\";\n\nlet fixture: Fixture;\nlet appFixture: AppFixture;\n\ntest.beforeAll(async () => {\n  fixture = await createFixture({\n    files: {\n      \"app/routes/layout-action.tsx\": js`\n        import { Outlet, useFetcher, useFormAction } from \"react-router\";\n\n        export let action = ({ params }) => \"layout action data\";\n\n        export default function ActionLayout() {\n          let fetcher = useFetcher();\n          let action = useFormAction();\n\n          let invokeFetcher = () => {\n            fetcher.submit({}, { method: \"post\", action });\n          };\n\n          return (\n            <div>\n              <h1>Layout</h1>\n              <button id=\"layout-fetcher\" onClick={invokeFetcher}>Invoke Fetcher</button>\n              {!!fetcher.data && <p id=\"layout-fetcher-data\">{fetcher.data}</p>}\n              <Outlet />\n            </div>\n          );\n        }\n      `,\n\n      \"app/routes/layout-action._index.tsx\": js`\n        import {\n          useFetcher,\n          useFormAction,\n          useLoaderData,\n        } from \"react-router\";\n\n        export let loader = ({ params }) => \"index data\";\n\n        export let action = ({ params }) => \"index action data\";\n\n        export default function ActionLayoutIndex() {\n          let data = useLoaderData();\n          let fetcher = useFetcher();\n          let action = useFormAction();\n\n          let invokeFetcher = () => {\n            fetcher.submit({}, { method: \"post\", action })\n          };\n\n          return (\n            <>\n              <p id=\"child-data\">{data}</p>\n              <button id=\"index-fetcher\" onClick={invokeFetcher}>Invoke Index Fetcher</button>\n              {!!fetcher.data && <p id=\"index-fetcher-data\">{fetcher.data}</p>}\n            </>\n          );\n        }\n      `,\n\n      \"app/routes/layout-action.$param.tsx\": js`\n        import {\n          useFetcher,\n          useFormAction,\n          useLoaderData,\n        } from \"react-router\";\n\n        export let loader = ({ params }) => params.param;\n\n        export let action = ({ params }) => \"param action data\";\n\n        export default function ActionLayoutChild() {\n          let data = useLoaderData();\n          let fetcher = useFetcher();\n          let action = useFormAction();\n\n          let invokeFetcher = () => {\n            fetcher.submit({}, { method: \"post\", action })\n          };\n\n          return (\n            <>\n              <p id=\"child-data\">{data}</p>\n              <button id=\"param-fetcher\" onClick={invokeFetcher}>Invoke Param Fetcher</button>\n              {!!fetcher.data && <p id=\"param-fetcher-data\">{fetcher.data}</p>}\n            </>\n          );\n        }\n      `,\n\n      \"app/routes/layout-loader.tsx\": js`\n        import { Outlet, useFetcher, useFormAction } from \"react-router\";\n\n        export let loader = () => \"layout loader data\";\n\n        export default function LoaderLayout() {\n          let fetcher = useFetcher();\n          let action = useFormAction();\n\n          let invokeFetcher = () => {\n            fetcher.load(action);\n          };\n\n          return (\n            <div>\n              <h1>Layout</h1>\n              <button id=\"layout-fetcher\" onClick={invokeFetcher}>Invoke Fetcher</button>\n              {!!fetcher.data && <p id=\"layout-fetcher-data\">{fetcher.data}</p>}\n              <Outlet />\n            </div>\n          );\n        }\n      `,\n\n      \"app/routes/layout-loader._index.tsx\": js`\n        import {\n          useFetcher,\n          useFormAction,\n          useLoaderData,\n        } from \"react-router\";\n\n        export let loader = ({ params }) => \"index data\";\n\n        export default function ActionLayoutIndex() {\n          let fetcher = useFetcher();\n          let action = useFormAction();\n\n          let invokeFetcher = () => {\n            fetcher.load(action);\n          };\n\n          return (\n            <>\n              <button id=\"index-fetcher\" onClick={invokeFetcher}>Invoke Index Fetcher</button>\n              {!!fetcher.data && <p id=\"index-fetcher-data\">{fetcher.data}</p>}\n            </>\n          );\n        }\n      `,\n\n      \"app/routes/layout-loader.$param.tsx\": js`\n        import {\n          useFetcher,\n          useFormAction,\n          useLoaderData,\n        } from \"react-router\";\n\n        export let loader = ({ params }) => params.param;\n\n        export default function ActionLayoutChild() {\n          let fetcher = useFetcher();\n          let action = useFormAction();\n\n          let invokeFetcher = () => {\n            fetcher.load(action);\n          };\n\n          return (\n            <>\n              <button id=\"param-fetcher\" onClick={invokeFetcher}>Invoke Param Fetcher</button>\n              {!!fetcher.data && <p id=\"param-fetcher-data\">{fetcher.data}</p>}\n            </>\n          );\n        }\n      `,\n    },\n  });\n\n  appFixture = await createAppFixture(fixture);\n});\n\ntest.afterAll(() => {\n  appFixture.close();\n});\n\ntest(\"fetcher calls layout route action when at index route\", async ({\n  page,\n}) => {\n  let app = new PlaywrightFixture(appFixture, page);\n  await app.goto(\"/layout-action\");\n  await app.clickElement(\"#layout-fetcher\");\n  await page.waitForSelector(\"#layout-fetcher-data\");\n  let dataElement = await app.getElement(\"#layout-fetcher-data\");\n  expect(dataElement.text()).toBe(\"layout action data\");\n  dataElement = await app.getElement(\"#child-data\");\n  expect(dataElement.text()).toBe(\"index data\");\n});\n\ntest(\"fetcher calls layout route loader when at index route\", async ({\n  page,\n}) => {\n  let app = new PlaywrightFixture(appFixture, page);\n  await app.goto(\"/layout-loader\");\n  await app.clickElement(\"#layout-fetcher\");\n  await page.waitForSelector(\"#layout-fetcher-data\");\n  let dataElement = await app.getElement(\"#layout-fetcher-data\");\n  expect(dataElement.text()).toBe(\"layout loader data\");\n});\n\ntest(\"fetcher calls index route action when at index route\", async ({\n  page,\n}) => {\n  let app = new PlaywrightFixture(appFixture, page);\n  await app.goto(\"/layout-action\");\n  await app.clickElement(\"#index-fetcher\");\n  await page.waitForSelector(\"#index-fetcher-data\");\n  let dataElement = await app.getElement(\"#index-fetcher-data\");\n  expect(dataElement.text()).toBe(\"index action data\");\n  dataElement = await app.getElement(\"#child-data\");\n  expect(dataElement.text()).toBe(\"index data\");\n});\n\ntest(\"fetcher calls index route loader when at index route\", async ({\n  page,\n}) => {\n  let app = new PlaywrightFixture(appFixture, page);\n  await app.goto(\"/layout-loader\");\n  await app.clickElement(\"#index-fetcher\");\n  await page.waitForSelector(\"#index-fetcher-data\");\n  let dataElement = await app.getElement(\"#index-fetcher-data\");\n  expect(dataElement.text()).toBe(\"index data\");\n});\n\ntest(\"fetcher calls layout route action when at parameterized route\", async ({\n  page,\n}) => {\n  let app = new PlaywrightFixture(appFixture, page);\n  await app.goto(\"/layout-action/foo\");\n  await app.clickElement(\"#layout-fetcher\");\n  await page.waitForSelector(\"#layout-fetcher-data\");\n  let dataElement = await app.getElement(\"#layout-fetcher-data\");\n  expect(dataElement.text()).toBe(\"layout action data\");\n  dataElement = await app.getElement(\"#child-data\");\n  expect(dataElement.text()).toBe(\"foo\");\n});\n\ntest(\"fetcher calls layout route loader when at parameterized route\", async ({\n  page,\n}) => {\n  let app = new PlaywrightFixture(appFixture, page);\n  await app.goto(\"/layout-loader/foo\");\n  await app.clickElement(\"#layout-fetcher\");\n  await page.waitForSelector(\"#layout-fetcher-data\");\n  let dataElement = await app.getElement(\"#layout-fetcher-data\");\n  expect(dataElement.text()).toBe(\"layout loader data\");\n});\n\ntest(\"fetcher calls parameterized route route action\", async ({ page }) => {\n  let app = new PlaywrightFixture(appFixture, page);\n  await app.goto(\"/layout-action/foo\");\n  await app.clickElement(\"#param-fetcher\");\n  await page.waitForSelector(\"#param-fetcher-data\");\n  let dataElement = await app.getElement(\"#param-fetcher-data\");\n  expect(dataElement.text()).toBe(\"param action data\");\n  dataElement = await app.getElement(\"#child-data\");\n  expect(dataElement.text()).toBe(\"foo\");\n});\n\ntest(\"fetcher calls parameterized route route loader\", async ({ page }) => {\n  let app = new PlaywrightFixture(appFixture, page);\n  await app.goto(\"/layout-loader/foo\");\n  await app.clickElement(\"#param-fetcher\");\n  await page.waitForSelector(\"#param-fetcher-data\");\n  let dataElement = await app.getElement(\"#param-fetcher-data\");\n  expect(dataElement.text()).toBe(\"foo\");\n});\n"
  },
  {
    "path": "integration/fetcher-test.ts",
    "content": "import { expect, test } from \"@playwright/test\";\n\nimport {\n  createAppFixture,\n  createFixture,\n  js,\n} from \"./helpers/create-fixture.js\";\nimport type { Fixture, AppFixture } from \"./helpers/create-fixture.js\";\nimport { PlaywrightFixture } from \"./helpers/playwright-fixture.js\";\n\ntest.describe(\"useFetcher\", () => {\n  let fixture: Fixture;\n  let appFixture: AppFixture;\n\n  let CHEESESTEAK = \"CHEESESTEAK\";\n  let LUNCH = \"LUNCH\";\n  let PARENT_LAYOUT_LOADER = \"parent layout loader\";\n  let PARENT_LAYOUT_ACTION = \"parent layout action\";\n  let PARENT_INDEX_LOADER = \"parent index loader\";\n  let PARENT_INDEX_ACTION = \"parent index action\";\n\n  test.beforeAll(async () => {\n    fixture = await createFixture({\n      files: {\n        \"app/routes/resource-route-action-only.ts\": js`\n          export function action() {\n            return new Response(\"${CHEESESTEAK}\");\n          }\n        `,\n\n        \"app/routes/fetcher-action-only-call.tsx\": js`\n          import { useFetcher } from \"react-router\";\n\n          export default function FetcherActionOnlyCall() {\n            let fetcher = useFetcher();\n\n            let executeFetcher = () => {\n              fetcher.submit(new URLSearchParams(), {\n                method: 'post',\n                action: '/resource-route-action-only',\n              });\n            };\n\n            return (\n              <>\n                <button id=\"fetcher-submit\" onClick={executeFetcher}>Click Me</button>\n                {fetcher.data && <pre>{fetcher.data}</pre>}\n              </>\n            );\n          }\n        `,\n\n        \"app/routes/resource-route.tsx\": js`\n          export function loader() {\n            return new Response(\"${LUNCH}\");\n          }\n          export function action() {\n            return new Response(\"${CHEESESTEAK}\");\n          }\n        `,\n\n        \"app/routes/_index.tsx\": js`\n          import { useFetcher } from \"react-router\";\n          export default function Index() {\n            let fetcher = useFetcher();\n            return (\n              <>\n                <fetcher.Form action=\"/resource-route\">\n                  <button type=\"submit\" formMethod=\"get\">get</button>\n                  <button type=\"submit\" formMethod=\"post\">post</button>\n                </fetcher.Form>\n                <button id=\"fetcher-load\" type=\"button\" onClick={() => {\n                  fetcher.load('/resource-route');\n                }}>\n                  load\n                </button>\n                <button id=\"fetcher-submit\" type=\"button\" onClick={() => {\n                  fetcher.submit(new URLSearchParams(), {\n                    method: 'post',\n                    action: '/resource-route'\n                  });\n                }}>\n                  submit\n                </button>\n                <pre>{fetcher.data}</pre>\n              </>\n            );\n          }\n        `,\n\n        \"app/routes/parent.tsx\": js`\n          import { Outlet } from \"react-router\";\n\n          export function action() {\n            return new Response(\"${PARENT_LAYOUT_ACTION}\");\n          };\n\n          export function loader() {\n            return new Response(\"${PARENT_LAYOUT_LOADER}\");\n          };\n\n          export default function Parent() {\n            return <Outlet />;\n          }\n        `,\n\n        \"app/routes/parent._index.tsx\": js`\n          import { useFetcher } from \"react-router\";\n\n          export function action() {\n            return new Response(\"${PARENT_INDEX_ACTION}\");\n          };\n\n          export function loader() {\n            return new Response(\"${PARENT_INDEX_LOADER}\");\n          };\n\n          export default function ParentIndex() {\n            let fetcher = useFetcher();\n\n            return (\n              <>\n                <pre>{fetcher.data}</pre>\n                <button id=\"load-parent\" onClick={() => fetcher.load('/parent')}>\n                  Load parent\n                </button>\n                <button id=\"load-index\" onClick={() => fetcher.load('/parent?index')}>\n                  Load index\n                </button>\n                <button id=\"submit-empty\" onClick={() => fetcher.submit({})}>\n                  Submit empty\n                </button>\n                <button id=\"submit-parent-get\" onClick={() => fetcher.submit({}, { method: 'get', action: '/parent' })}>\n                  Submit parent\n                </button>\n                <button id=\"submit-index-get\" onClick={() => fetcher.submit({}, { method: 'get', action: '/parent?index' })}>\n                  Submit index\n                </button>\n                <button id=\"submit-parent-post\" onClick={() => fetcher.submit({}, { method: 'post', action: '/parent' })}>\n                  Submit parent\n                </button>\n                <button id=\"submit-index-post\" onClick={() => fetcher.submit({}, { method: 'post', action: '/parent?index' })}>\n                  Submit index\n                </button>\n              </>\n            );\n          }\n        `,\n\n        \"app/routes/fetcher-echo.tsx\": js`\n          import { useFetcher } from \"react-router\";\n\n          export async function action({ request }) {\n            await new Promise(r => setTimeout(r, 1000));\n            let contentType = request.headers.get('Content-Type');\n            let value;\n            if (contentType.includes('application/json')) {\n              let json = await request.json();\n              value = json === null ? json : json.value;\n            } else if (contentType.includes('text/plain')) {\n              value = await request.text();\n            } else {\n              value = (await request.formData()).get('value');\n            }\n            return { data: \"ACTION (\" + contentType + \") \" + value }\n          }\n\n          export async function loader({ request }) {\n            await new Promise(r => setTimeout(r, 1000));\n            let value = new URL(request.url).searchParams.get('value');\n            return { data: \"LOADER \" + value }\n          }\n\n          export default function Index() {\n            let fetcherValues = [];\n            if (typeof window !== 'undefined') {\n              if (!window.fetcherValues) {\n                window.fetcherValues = [];\n              }\n              fetcherValues = window.fetcherValues\n            }\n\n            let fetcher = useFetcher();\n\n            let currentValue = fetcher.state + '/' + fetcher.data?.data;\n            if (fetcherValues[fetcherValues.length - 1] !== currentValue) {\n              fetcherValues.push(currentValue)\n            }\n\n            return (\n              <>\n                <input id=\"fetcher-input\" name=\"value\" />\n                <button id=\"fetcher-load\" onClick={() => {\n                  let value = document.getElementById('fetcher-input').value;\n                  fetcher.load('/fetcher-echo?value=' + value)\n                }}>Load</button>\n                <button id=\"fetcher-submit\" onClick={() => {\n                  let value = document.getElementById('fetcher-input').value;\n                  fetcher.submit({ value }, { method: 'post', action: '/fetcher-echo' })\n                }}>Submit</button>\n                <button id=\"fetcher-submit-json\" onClick={() => {\n                  let value = document.getElementById('fetcher-input').value;\n                  fetcher.submit({ value }, { method: 'post', action: '/fetcher-echo', encType: 'application/json' })\n                }}>Submit JSON</button>\n                <button id=\"fetcher-submit-json-null\" onClick={() => {\n                  fetcher.submit(null, { method: 'post', action: '/fetcher-echo', encType: 'application/json' })\n                }}>Submit Null JSON</button>\n                <button id=\"fetcher-submit-text\" onClick={() => {\n                  let value = document.getElementById('fetcher-input').value;\n                  fetcher.submit(value, { method: 'post', action: '/fetcher-echo', encType: 'text/plain' })\n                }}>Submit Text</button>\n                <button id=\"fetcher-submit-text-empty\" onClick={() => {\n                  fetcher.submit(\"\", { method: 'post', action: '/fetcher-echo', encType: 'text/plain' })\n                }}>Submit Empty Text</button>\n\n                {fetcher.state === 'idle' ? <p id=\"fetcher-idle\">IDLE</p> : null}\n                <pre>{JSON.stringify(fetcherValues)}</pre>\n              </>\n            );\n          }\n        `,\n      },\n    });\n\n    appFixture = await createAppFixture(fixture);\n  });\n\n  test.afterAll(() => {\n    appFixture.close();\n  });\n\n  test.describe(\"No JavaScript\", () => {\n    test.use({ javaScriptEnabled: false });\n\n    test(\"Form can hit a loader\", async ({ page }) => {\n      let app = new PlaywrightFixture(appFixture, page);\n      await app.goto(\"/\");\n\n      await Promise.all([\n        page.waitForNavigation(),\n        app.clickSubmitButton(\"/resource-route\", {\n          wait: false,\n          method: \"get\",\n        }),\n      ]);\n      // Check full HTML here - Chromium/Firefox/Webkit seem to render this in\n      // a <pre> but Edge puts it in some weird code editor markup:\n      // <body data-code-mirror=\"Readonly code editor.\">\n      //   <div hidden=\"true\">\"LUNCH\"</div>\n      await page.getByText(LUNCH);\n    });\n\n    test(\"Form can hit an action\", async ({ page }) => {\n      let app = new PlaywrightFixture(appFixture, page);\n      await app.goto(\"/\");\n      await Promise.all([\n        page.waitForNavigation({ waitUntil: \"load\" }),\n        app.clickSubmitButton(\"/resource-route\", {\n          wait: false,\n          method: \"post\",\n        }),\n      ]);\n      // Check full HTML here - Chromium/Firefox/Webkit seem to render this in\n      // a <pre> but Edge puts it in some weird code editor markup:\n      // <body data-code-mirror=\"Readonly code editor.\">\n      //   <div hidden=\"true\">\"LUNCH\"</div>\n      await page.getByText(CHEESESTEAK);\n    });\n  });\n\n  test(\"load can hit a loader\", async ({ page }) => {\n    let app = new PlaywrightFixture(appFixture, page);\n    await app.goto(\"/\");\n    await app.clickElement(\"#fetcher-load\");\n    await page.waitForSelector(`pre:has-text(\"${LUNCH}\")`);\n  });\n\n  test(\"submit can hit an action\", async ({ page }) => {\n    let app = new PlaywrightFixture(appFixture, page);\n    await app.goto(\"/\");\n    await app.clickElement(\"#fetcher-submit\");\n    await page.waitForSelector(`pre:has-text(\"${CHEESESTEAK}\")`);\n  });\n\n  test(\"submit can hit an action with json\", async ({ page }) => {\n    let app = new PlaywrightFixture(appFixture, page);\n    await app.goto(\"/fetcher-echo\", true);\n    await page.fill(\"#fetcher-input\", \"input value\");\n    await app.clickElement(\"#fetcher-submit-json\");\n    await page.waitForSelector(`#fetcher-idle`);\n    await page.getByText('ACTION (application/json) input value\"');\n  });\n\n  test(\"submit can hit an action with null json\", async ({ page }) => {\n    let app = new PlaywrightFixture(appFixture, page);\n    await app.goto(\"/fetcher-echo\", true);\n    await app.clickElement(\"#fetcher-submit-json-null\");\n    await new Promise((r) => setTimeout(r, 1000));\n    await page.waitForSelector(`#fetcher-idle`);\n    await page.getByText('ACTION (application/json) null\"');\n  });\n\n  test(\"submit can hit an action with text\", async ({ page }) => {\n    let app = new PlaywrightFixture(appFixture, page);\n    await app.goto(\"/fetcher-echo\", true);\n    await page.fill(\"#fetcher-input\", \"input value\");\n    await app.clickElement(\"#fetcher-submit-text\");\n    await page.waitForSelector(`#fetcher-idle`);\n    await page.getByText('ACTION (text/plain;charset=UTF-8) input value\"');\n  });\n\n  test(\"submit can hit an action with empty text\", async ({ page }) => {\n    let app = new PlaywrightFixture(appFixture, page);\n    await app.goto(\"/fetcher-echo\", true);\n    await app.clickElement(\"#fetcher-submit-text-empty\");\n    await new Promise((r) => setTimeout(r, 1000));\n    await page.waitForSelector(`#fetcher-idle`);\n    await page.getByText('ACTION (text/plain;charset=UTF-8) \"');\n  });\n\n  test(\"submit can hit an action only route\", async ({ page }) => {\n    let app = new PlaywrightFixture(appFixture, page);\n    await app.goto(\"/fetcher-action-only-call\");\n    await app.clickElement(\"#fetcher-submit\");\n    await page.waitForSelector(`pre:has-text(\"${CHEESESTEAK}\")`);\n  });\n\n  test(\"fetchers handle ?index param correctly\", async ({ page }) => {\n    let app = new PlaywrightFixture(appFixture, page);\n    await app.goto(\"/parent\");\n\n    await app.clickElement(\"#load-parent\");\n    await page.waitForSelector(`pre:has-text(\"${PARENT_LAYOUT_LOADER}\")`);\n\n    await app.clickElement(\"#load-index\");\n    await page.waitForSelector(`pre:has-text(\"${PARENT_INDEX_LOADER}\")`);\n\n    // fetcher.submit({}) defaults to GET for the current Route\n    await app.clickElement(\"#submit-empty\");\n    await page.waitForSelector(`pre:has-text(\"${PARENT_INDEX_LOADER}\")`);\n\n    await app.clickElement(\"#submit-parent-get\");\n    await page.waitForSelector(`pre:has-text(\"${PARENT_LAYOUT_LOADER}\")`);\n\n    await app.clickElement(\"#submit-index-get\");\n    await page.waitForSelector(`pre:has-text(\"${PARENT_INDEX_LOADER}\")`);\n\n    await app.clickElement(\"#submit-parent-post\");\n    await page.waitForSelector(`pre:has-text(\"${PARENT_LAYOUT_ACTION}\")`);\n\n    await app.clickElement(\"#submit-index-post\");\n    await page.waitForSelector(`pre:has-text(\"${PARENT_INDEX_ACTION}\")`);\n  });\n\n  test(\"fetcher.load persists data through reloads\", async ({ page }) => {\n    let app = new PlaywrightFixture(appFixture, page);\n\n    await app.goto(\"/fetcher-echo\", true);\n    await page.getByText(JSON.stringify([\"idle/undefined\"]));\n\n    await page.fill(\"#fetcher-input\", \"1\");\n    await app.clickElement(\"#fetcher-load\");\n    await page.waitForSelector(\"#fetcher-idle\");\n    await page.getByText(\n      JSON.stringify([\"idle/undefined\", \"loading/undefined\", \"idle/LOADER 1\"]),\n    );\n\n    await page.fill(\"#fetcher-input\", \"2\");\n    await app.clickElement(\"#fetcher-load\");\n    await page.waitForSelector(\"#fetcher-idle\");\n    await page.getByText(\n      JSON.stringify([\n        \"idle/undefined\",\n        \"loading/undefined\",\n        \"idle/LOADER 1\",\n        \"loading/LOADER 1\", // Preserves old data during reload\n        \"idle/LOADER 2\",\n      ]),\n    );\n  });\n\n  test(\"fetcher.submit persists data through resubmissions\", async ({\n    page,\n  }) => {\n    let app = new PlaywrightFixture(appFixture, page);\n\n    await app.goto(\"/fetcher-echo\", true);\n    await page.getByText(JSON.stringify([\"idle/undefined\"]));\n\n    await page.fill(\"#fetcher-input\", \"1\");\n    await app.clickElement(\"#fetcher-submit\");\n    await page.waitForSelector(\"#fetcher-idle\");\n    await page.getByText(\n      JSON.stringify([\n        \"idle/undefined\",\n        \"submitting/undefined\",\n        \"loading/ACTION (application/x-www-form-urlencoded;charset=UTF-8) 1\",\n        \"idle/ACTION (application/x-www-form-urlencoded;charset=UTF-8) 1\",\n      ]),\n    );\n\n    await page.fill(\"#fetcher-input\", \"2\");\n    await app.clickElement(\"#fetcher-submit\");\n    await page.waitForSelector(\"#fetcher-idle\");\n    await page.getByText(\n      JSON.stringify([\n        \"idle/undefined\",\n        \"submitting/undefined\",\n        \"loading/ACTION (application/x-www-form-urlencoded;charset=UTF-8) 1\",\n        \"idle/ACTION (application/x-www-form-urlencoded;charset=UTF-8) 1\",\n        // Preserves old data during resubmissions\n        \"submitting/ACTION (application/x-www-form-urlencoded;charset=UTF-8) 1\",\n        \"loading/ACTION (application/x-www-form-urlencoded;charset=UTF-8) 2\",\n        \"idle/ACTION (application/x-www-form-urlencoded;charset=UTF-8) 2\",\n      ]),\n    );\n  });\n});\n\ntest.describe(\"fetcher aborts and adjacent forms\", () => {\n  let fixture: Fixture;\n  let appFixture: AppFixture;\n\n  test.beforeAll(async () => {\n    fixture = await createFixture({\n      files: {\n        \"app/routes/_index.tsx\": js`\n          import * as React from \"react\";\n          import {\n            Form,\n            useFetcher,\n            useLoaderData,\n            useNavigation\n          } from \"react-router\";\n\n          export async function loader({ request }) {\n            // 1 second timeout on data\n            await new Promise((r) => setTimeout(r, 1000));\n            return { foo: 'bar' };\n          }\n\n          export default function Index() {\n            const [open, setOpen] = React.useState(true);\n            const { data } = useLoaderData();\n            const navigation = useNavigation();\n\n            return (\n              <div>\n                  {navigation.state === 'idle' && <div id=\"idle\">Idle</div>}\n                  <Form id=\"main-form\">\n                    <input id=\"submit-form\" type=\"submit\" />\n                  </Form>\n\n                  <button id=\"open\" onClick={() => setOpen(true)}>Show async form</button>\n                  {open && <Child onClose={() => setOpen(false)} />}\n              </div>\n            );\n          }\n\n          function Child({ onClose }) {\n            const fetcher = useFetcher();\n\n            return (\n              <fetcher.Form method=\"get\" action=\"/api\">\n                <button id=\"submit-fetcher\" type=\"submit\">Trigger fetcher (shows a message)</button>\n                <button\n                  type=\"submit\"\n                  form=\"main-form\"\n                  id=\"submit-and-close\"\n                  onClick={() => setTimeout(onClose, 250)}\n                >\n                  Submit main form and close async form\n                </button>\n              </fetcher.Form>\n            );\n          }\n        `,\n\n        \"app/routes/api.tsx\": js`\n          export async function loader() {\n            await new Promise((resolve) => setTimeout(resolve, 500));\n            return { message: 'Hello world!' }\n          }\n        `,\n      },\n    });\n\n    appFixture = await createAppFixture(fixture);\n  });\n\n  test.afterAll(() => {\n    appFixture.close();\n  });\n\n  test(\"Unmounting a fetcher does not cancel the request of an adjacent form\", async ({\n    page,\n  }) => {\n    let app = new PlaywrightFixture(appFixture, page);\n    await app.goto(\"/\");\n\n    // Works as expected before the fetcher is loaded\n\n    // submit the main form and unmount the fetcher form\n    await app.clickElement(\"#submit-and-close\");\n    // Wait for our navigation state to be \"Idle\"\n    await page.waitForSelector(\"#idle\", { timeout: 2000 });\n\n    // Breaks after the fetcher is loaded\n\n    // re-mount the fetcher form\n    await app.clickElement(\"#open\");\n    // submit the fetcher form\n    await app.clickElement(\"#submit-fetcher\");\n    // submit the main form and unmount the fetcher form\n    await app.clickElement(\"#submit-and-close\");\n    // Wait for navigation state to be \"Idle\"\n    await page.waitForSelector(\"#idle\", { timeout: 2000 });\n  });\n});\n\ntest.describe(\"fetcher lazy route discovery\", () => {\n  let fixture: Fixture;\n  let appFixture: AppFixture;\n\n  test.afterAll(() => {\n    appFixture.close();\n  });\n\n  test(\"skips revalidation of initial load fetchers performing lazy route discovery\", async ({\n    page,\n  }) => {\n    fixture = await createFixture({\n      files: {\n        \"app/routes/parent.tsx\": js`\n          import * as React from \"react\";\n          import { useFetcher, useNavigate, Outlet } from \"react-router\";\n\n          export default function Index() {\n            const fetcher = useFetcher();\n            const navigate = useNavigate();\n\n            React.useEffect(() => {\n              fetcher.load('/api');\n            }, []);\n\n            React.useEffect(() => {\n              navigate('/parent/child');\n            }, []);\n\n            return (\n              <>\n                <h1>Parent</h1>\n                {fetcher.data ?\n                  <pre data-fetcher>{fetcher.data}</pre> :\n                  null}\n                <Outlet/>\n              </>\n            );\n          }\n        `,\n        \"app/routes/parent.child.tsx\": js`\n          export default function Index() {\n            return <h2>Child</h2>;\n          }\n        `,\n        \"app/routes/api.tsx\": js`\n          export async function loader() {\n            return \"FETCHED!\"\n          }\n        `,\n      },\n    });\n\n    // Slow down the fetcher discovery a tiny bit so it doesn't resolve prior\n    // to the navigation\n    page.route(/\\/__manifest/, async (route) => {\n      if (route.request().url().includes(encodeURIComponent(\"/api\"))) {\n        await new Promise((r) => setTimeout(r, 100));\n      }\n      route.continue();\n    });\n\n    appFixture = await createAppFixture(fixture);\n    let app = new PlaywrightFixture(appFixture, page);\n    await app.goto(\"/parent\");\n    await page.waitForSelector(\"h2\", { timeout: 3000 });\n    await expect(page.locator(\"h2\")).toHaveText(\"Child\");\n    await page.waitForSelector(\"[data-fetcher]\", { timeout: 3000 });\n    await expect(page.locator(\"[data-fetcher]\")).toHaveText(\"FETCHED!\");\n  });\n});\n"
  },
  {
    "path": "integration/fog-of-war-test.ts",
    "content": "import { test, expect } from \"@playwright/test\";\nimport { PassThrough } from \"node:stream\";\n\nimport {\n  createAppFixture,\n  createFixture,\n  js,\n} from \"./helpers/create-fixture.js\";\nimport { PlaywrightFixture } from \"./helpers/playwright-fixture.js\";\nimport { reactRouterConfig } from \"./helpers/vite.js\";\n\nfunction getFiles() {\n  return {\n    \"app/root.tsx\": js`\n      import * as React from \"react\";\n      import { Link, Links, Meta, Outlet, Scripts } from \"react-router\";\n      export default function Root() {\n        let [showLink, setShowLink] = React.useState(false);\n        return (\n          <html lang=\"en\">\n            <head>\n              <Meta />\n              <Links />\n            </head>\n            <body>\n              <Link to=\"/\">Home</Link><br/>\n              <Link to=\"/a\">/a</Link><br/>\n              <button onClick={() => setShowLink(true)}>Show Link</button>\n              {showLink ? <Link to=\"/a/b\">/a/b</Link> : null}\n              <Outlet />\n              <Scripts />\n            </body>\n          </html>\n        );\n      }\n    `,\n\n    \"app/routes/_index.tsx\": js`\n      export default function Index() {\n        return <h1 id=\"index\">Index</h1>\n      }\n    `,\n\n    \"app/routes/a.tsx\": js`\n      import { Link, Outlet, useLoaderData } from \"react-router\";\n      export function loader({ request }) {\n        return { message: \"A LOADER\" };\n      }\n      export default function Index() {\n        let data = useLoaderData();\n        return (\n          <>\n            <h1 id=\"a\">A: {data.message}</h1>\n            <Link to=\"/a/b\">/a/b</Link>\n            <Outlet/>\n          </>\n        )\n      }\n    `,\n    \"app/routes/a.b.tsx\": js`\n      import { Outlet, useLoaderData } from \"react-router\";\n      export function loader({ request }) {\n        return { message: \"B LOADER\" };\n      }\n      export default function Index() {\n        let data = useLoaderData();\n        return (\n          <>\n            <h2 id=\"b\">B: {data.message}</h2>\n            <Outlet/>\n          </>\n        )\n      }\n    `,\n    \"app/routes/a.b.c.tsx\": js`\n      import { Outlet, useLoaderData } from \"react-router\";\n      export function loader({ request }) {\n        return { message: \"C LOADER\" };\n      }\n      export default function Index() {\n        let data = useLoaderData();\n        return <h3 id=\"c\">C: {data.message}</h3>\n      }\n    `,\n  };\n}\n\ntest.describe(\"Fog of War\", () => {\n  let oldConsoleError: typeof console.error;\n\n  test.beforeEach(() => {\n    oldConsoleError = console.error;\n  });\n\n  test.afterEach(() => {\n    console.error = oldConsoleError;\n  });\n\n  test(\"loads minimal manifest on initial load\", async ({ page }) => {\n    let fixture = await createFixture({\n      files: {\n        ...getFiles(),\n        \"app/entry.client.tsx\": js`\n          import { HydratedRouter } from \"react-router/dom\";\n          import { startTransition, StrictMode } from \"react\";\n          import { hydrateRoot } from \"react-dom/client\";\n          startTransition(() => {\n            hydrateRoot(\n              document,\n              <StrictMode>\n                <HydratedRouter discover={\"none\"} />\n              </StrictMode>\n            );\n          });\n        `,\n      },\n    });\n    let appFixture = await createAppFixture(fixture);\n    let app = new PlaywrightFixture(appFixture, page);\n    let res = await fixture.requestDocument(\"/\");\n    let html = await res.text();\n\n    expect(html).toContain(\"window.__reactRouterManifest = {\");\n    expect(html).not.toContain(\n      '<link rel=\"modulepreload\" href=\"/assets/manifest-',\n    );\n    expect(html).toContain('\"root\": {');\n    expect(html).toContain('\"routes/_index\": {');\n    expect(html).not.toContain('\"routes/a\"');\n\n    // Linking to A loads A and succeeds\n    await app.goto(\"/\", true);\n    await app.clickLink(\"/a\");\n    await page.waitForSelector(\"#a\");\n    expect(await app.getHtml(\"#a\")).toBe(`<h1 id=\"a\">A: A LOADER</h1>`);\n    expect(\n      await page.evaluate(() =>\n        Object.keys((window as any).__reactRouterManifest.routes),\n      ),\n    ).toContain(\"routes/a\");\n  });\n\n  test(\"prefetches initially rendered links\", async ({ page }) => {\n    let fixture = await createFixture({\n      files: getFiles(),\n    });\n    let appFixture = await createAppFixture(fixture);\n    let app = new PlaywrightFixture(appFixture, page);\n\n    await app.goto(\"/\", true);\n    expect(\n      await page.evaluate(() =>\n        Object.keys((window as any).__reactRouterManifest.routes),\n      ),\n    ).toEqual([\"root\", \"routes/_index\", \"routes/a\"]);\n\n    await app.clickLink(\"/a\");\n    await page.waitForSelector(\"#a\");\n    expect(await app.getHtml(\"#a\")).toBe(`<h1 id=\"a\">A: A LOADER</h1>`);\n  });\n\n  test(\"prefetches links rendered via navigations\", async ({ page }) => {\n    let fixture = await createFixture({\n      files: getFiles(),\n    });\n    let appFixture = await createAppFixture(fixture);\n    let app = new PlaywrightFixture(appFixture, page);\n\n    await app.goto(\"/\", true);\n    expect(\n      await page.evaluate(() =>\n        Object.keys((window as any).__reactRouterManifest.routes),\n      ),\n    ).toEqual([\"root\", \"routes/_index\", \"routes/a\"]);\n\n    await app.clickLink(\"/a\");\n    await page.waitForSelector(\"#a\");\n\n    await page.waitForFunction(\n      () => (window as any).__reactRouterManifest.routes[\"routes/a.b\"],\n    );\n\n    expect(\n      await page.evaluate(() =>\n        Object.keys((window as any).__reactRouterManifest.routes),\n      ),\n    ).toEqual([\"root\", \"routes/_index\", \"routes/a\", \"routes/a.b\"]);\n  });\n\n  test(\"prefetches links rendered via in-page stateful updates\", async ({\n    page,\n  }) => {\n    let fixture = await createFixture({\n      files: getFiles(),\n    });\n    let appFixture = await createAppFixture(fixture);\n    let app = new PlaywrightFixture(appFixture, page);\n\n    await app.goto(\"/\", true);\n    expect(\n      await page.evaluate(() =>\n        Object.keys((window as any).__reactRouterManifest.routes),\n      ),\n    ).toEqual([\"root\", \"routes/_index\", \"routes/a\"]);\n\n    await app.clickElement(\"button\");\n    await page.waitForFunction(\n      () => (window as any).__reactRouterManifest.routes[\"routes/a.b\"],\n    );\n\n    expect(\n      await page.evaluate(() =>\n        Object.keys((window as any).__reactRouterManifest.routes),\n      ),\n    ).toEqual([\"root\", \"routes/_index\", \"routes/a\", \"routes/a.b\"]);\n  });\n\n  test(\"prefetches links who opt-into [data-discover] via an in-page stateful update\", async ({\n    page,\n  }) => {\n    let fixture = await createFixture({\n      files: {\n        \"app/root.tsx\": js`\n          import { Outlet, Scripts } from \"react-router\";\n          export default function Root() {\n            return (\n              <html lang=\"en\">\n                <head> </head>\n                <body>\n                  <Outlet />\n                  <Scripts />\n                </body>\n              </html>\n            );\n          }\n        `,\n        \"app/routes/_index.tsx\": js`\n          import * as React from 'react';\n          import { Link, Outlet, useLoaderData } from \"react-router\";\n          export default function Index() {\n            let [discover, setDiscover] = React.useState(false)\n            return (\n              <>\n                <Link to=\"/a\" discover={discover ? \"render\" : \"none\"}>/a</Link>\n                <button onClick={() => setDiscover(true)}>Toggle</button>\n              </>\n            )\n          }\n        `,\n        \"app/routes/a.tsx\": js`\n          export default function Index() {\n            return <h1 id=\"a\">A</h1>\n          }\n        `,\n      },\n    });\n    let appFixture = await createAppFixture(fixture);\n    let app = new PlaywrightFixture(appFixture, page);\n\n    await app.goto(\"/\", true);\n    expect(\n      await page.evaluate(() =>\n        Object.keys((window as any).__reactRouterManifest.routes),\n      ),\n    ).toEqual([\"root\", \"routes/_index\"]);\n\n    await app.clickElement(\"button\");\n    await page.waitForFunction(\n      () => (window as any).__reactRouterManifest.routes[\"routes/a\"],\n    );\n\n    expect(\n      await page.evaluate(() =>\n        Object.keys((window as any).__reactRouterManifest.routes),\n      ),\n    ).toEqual([\"root\", \"routes/_index\", \"routes/a\"]);\n  });\n\n  test('does not prefetch links with discover=\"none\"', async ({ page }) => {\n    let fixture = await createFixture({\n      files: {\n        ...getFiles(),\n        \"app/routes/a.tsx\": js`\n          import { Link, Outlet, useLoaderData } from \"react-router\";\n          export function loader({ request }) {\n            return { message: \"A LOADER\" };\n          }\n          export default function Index() {\n            let data = useLoaderData();\n            return (\n              <>\n                <h1 id=\"a\">A: {data.message}</h1>\n                <Link to=\"/a/b\" discover=\"none\">/a/b</Link>\n                <Outlet/>\n              </>\n            )\n          }\n        `,\n      },\n    });\n    let appFixture = await createAppFixture(fixture);\n    let app = new PlaywrightFixture(appFixture, page);\n\n    await app.goto(\"/\", true);\n    expect(\n      await page.evaluate(() =>\n        Object.keys((window as any).__reactRouterManifest.routes),\n      ),\n    ).toEqual([\"root\", \"routes/_index\", \"routes/a\"]);\n\n    await app.clickLink(\"/a\");\n    await page.waitForSelector(\"#a\");\n    await new Promise((resolve) => setTimeout(resolve, 250));\n\n    // /a/b is not discovered yet even thought it's rendered\n    expect(\n      await page.evaluate(() =>\n        Object.keys((window as any).__reactRouterManifest.routes),\n      ),\n    ).toEqual([\"root\", \"routes/_index\", \"routes/a\"]);\n\n    // /a/b gets discovered on click\n    await app.clickLink(\"/a/b\");\n    await page.waitForSelector(\"#b\");\n\n    expect(\n      await page.evaluate(() =>\n        Object.keys((window as any).__reactRouterManifest.routes),\n      ),\n    ).toEqual([\"root\", \"routes/_index\", \"routes/a\", \"routes/a.b\"]);\n  });\n\n  test(\"prefetches initially rendered forms\", async ({ page }) => {\n    let fixture = await createFixture({\n      files: {\n        ...getFiles(),\n        \"app/root.tsx\": js`\n          import * as React from \"react\";\n          import { Form, Links, Meta, Outlet, Scripts } from \"react-router\";\n          export default function Root() {\n            let [showLink, setShowLink] = React.useState(false);\n            return (\n              <html lang=\"en\">\n                <head>\n                  <Meta />\n                  <Links />\n                </head>\n                <body>\n                  <Form method=\"post\" action=\"/a\">\n                    <button type=\"submit\">Submit</button>\n                  </Form>\n                  <Outlet />\n                  <Scripts />\n                </body>\n              </html>\n            );\n          }\n        `,\n        \"app/routes/a.tsx\": js`\n          import { useActionData } from \"react-router\";\n          export function action() {\n            return { message: \"A ACTION\" };\n          }\n          export default function Index() {\n            let actionData = useActionData();\n            return <h1 id=\"a\">A: {actionData.message}</h1>\n          }\n        `,\n      },\n    });\n    let appFixture = await createAppFixture(fixture);\n    let app = new PlaywrightFixture(appFixture, page);\n\n    await app.goto(\"/\", true);\n    await page.waitForFunction(\n      () => (window as any).__reactRouterManifest.routes[\"routes/a\"],\n    );\n    expect(\n      await page.evaluate(() =>\n        Object.keys((window as any).__reactRouterManifest.routes),\n      ),\n    ).toEqual([\"root\", \"routes/_index\", \"routes/a\"]);\n\n    await app.clickSubmitButton(\"/a\");\n    await page.waitForSelector(\"#a\");\n    expect(await app.getHtml(\"#a\")).toBe(`<h1 id=\"a\">A: A ACTION</h1>`);\n  });\n\n  test(\"prefetches forms rendered via navigations\", async ({ page }) => {\n    let fixture = await createFixture({\n      files: {\n        ...getFiles(),\n        \"app/routes/a.tsx\": js`\n          import { Form } from \"react-router\";\n          export default function Component() {\n            return (\n              <Form method=\"post\" action=\"/a/b\">\n                <button type=\"submit\">Submit</button>\n              </Form>\n            );\n          }\n        `,\n      },\n    });\n    let appFixture = await createAppFixture(fixture);\n    let app = new PlaywrightFixture(appFixture, page);\n\n    await app.goto(\"/\", true);\n    expect(\n      await page.evaluate(() =>\n        Object.keys((window as any).__reactRouterManifest.routes),\n      ),\n    ).toEqual([\"root\", \"routes/_index\", \"routes/a\"]);\n\n    await app.clickLink(\"/a\");\n    await page.waitForSelector(\"form\");\n\n    await page.waitForFunction(\n      () => (window as any).__reactRouterManifest.routes[\"routes/a.b\"],\n    );\n\n    expect(\n      await page.evaluate(() =>\n        Object.keys((window as any).__reactRouterManifest.routes),\n      ),\n    ).toEqual([\"root\", \"routes/_index\", \"routes/a\", \"routes/a.b\"]);\n  });\n\n  test(\"prefetches root index child when SSR-ing a deep route\", async ({\n    page,\n  }) => {\n    let fixture = await createFixture({\n      files: {\n        \"app/root.tsx\": js`\n          import { Outlet, Scripts } from \"react-router\";\n          export default function Root() {\n            return (\n              <html lang=\"en\">\n                <head></head>\n                <body>\n                  <Outlet />\n                  <Scripts />\n                </body>\n              </html>\n            );\n          }\n        `,\n\n        \"app/routes/_index.tsx\": js`\n          export default function Index() {\n            return <h1 id=\"index\">Index</h1>\n          }\n        `,\n        \"app/routes/deep.tsx\": js`\n          import { Link } from \"react-router\";\n          export default function Component() {\n            return (\n              <>\n                <h1>Deep</h1>\n                <Link to=\"/\" discover=\"none\">Home</Link>\n              </>\n            )\n          }\n        `,\n      },\n    });\n    let appFixture = await createAppFixture(fixture);\n    let app = new PlaywrightFixture(appFixture, page);\n\n    let manifestRequests: string[] = [];\n    page.on(\"request\", (req) => {\n      if (req.url().includes(\"/__manifest\")) {\n        manifestRequests.push(req.url());\n      }\n    });\n\n    await app.goto(\"/deep\", true);\n    expect(\n      await page.evaluate(() =>\n        Object.keys((window as any).__reactRouterManifest.routes),\n      ),\n    ).toEqual([\"root\", \"routes/deep\", \"routes/_index\"]);\n\n    // Without pre-loading the index, we'd \"match\" `/` to just the root route\n    // client side and never fetch the `routes/_index` route\n    await app.clickLink(\"/\");\n    await page.waitForSelector(\"#index\");\n    expect(await app.getHtml(\"#index\")).toMatch(`Index`);\n\n    expect(manifestRequests.length).toBe(0);\n  });\n\n  test(\"prefetches ancestor index children when SSR-ing a deep route\", async ({\n    page,\n  }) => {\n    let fixture = await createFixture({\n      files: {\n        \"app/root.tsx\": js`\n          import { Link, Outlet, Scripts } from \"react-router\";\n          export default function Root() {\n            return (\n              <html lang=\"en\">\n                <head></head>\n                <body >\n                  <nav>\n                    <Link to=\"/parent\" discover=\"none\">Parent Index</Link>\n                    <Link to=\"/parent/child\" discover=\"none\">Child Index</Link>\n                  </nav>\n                  <Outlet />\n                  <Scripts />\n                  </body>\n              </html>\n            );\n          }\n        `,\n\n        \"app/routes/_index.tsx\": js`\n          export default function Index() {\n            return <h1 id=\"index\">Index</h1>\n          }\n        `,\n        \"app/routes/parent.tsx\": js`\n          import { Outlet } from \"react-router\";\n          export default function Component() {\n            return (\n              <>\n                <h1 id=\"parent\">Parent</h1>\n                <Outlet />\n              </>\n            )\n          }\n        `,\n        \"app/routes/parent._index.tsx\": js`\n          export default function Component() {\n            return <h2 id=\"parent-index\">Parent Index</h2>;\n          }\n        `,\n        \"app/routes/parent.child.tsx\": js`\n          import { Outlet } from \"react-router\";\n          export default function Component() {\n            return (\n              <>\n                <h2 id=\"child\">Child</h2>\n                <Outlet />\n              </>\n            )\n          }\n        `,\n        \"app/routes/parent.child._index.tsx\": js`\n          export default function Component() {\n            return <h3 id=\"child-index\">Child Index</h3>;\n          }\n        `,\n        \"app/routes/parent.child.grandchild.tsx\": js`\n          export default function Component() {\n            return <h3 id=\"grandchild\">Grandchild</h3>;\n          }\n        `,\n      },\n    });\n    let appFixture = await createAppFixture(fixture);\n    let app = new PlaywrightFixture(appFixture, page);\n\n    let manifestRequests: string[] = [];\n    page.on(\"request\", (req) => {\n      if (req.url().includes(\"/__manifest\")) {\n        manifestRequests.push(req.url());\n      }\n    });\n\n    await app.goto(\"/parent/child/grandchild\", true);\n    expect(\n      await page.evaluate(() =>\n        Object.keys((window as any).__reactRouterManifest.routes),\n      ),\n    ).toEqual([\n      \"root\",\n      \"routes/parent\",\n      \"routes/parent.child\",\n      \"routes/parent.child.grandchild\",\n      \"routes/_index\",\n      \"routes/parent.child._index\",\n      \"routes/parent._index\",\n    ]);\n\n    // Without pre-loading the index, we'd \"match\" `/parent/child` to just the\n    // parent and child routes client side and never fetch the\n    // `routes/parent.child._index` route\n    await app.clickLink(\"/parent/child\");\n    await page.waitForSelector(\"#child-index\");\n    expect(await app.getHtml(\"#parent\")).toMatch(\"Parent\");\n    expect(await app.getHtml(\"#child\")).toMatch(\"Child\");\n    expect(await app.getHtml(\"#child-index\")).toMatch(`Child Index`);\n\n    await app.clickLink(\"/parent\");\n    await page.waitForSelector(\"#parent-index\");\n    expect(await app.getHtml(\"#parent\")).toMatch(`Parent`);\n    expect(await app.getHtml(\"#parent-index\")).toMatch(`Parent Index`);\n\n    expect(manifestRequests.length).toBe(0);\n  });\n\n  test(\"prefetches ancestor pathless children when SSR-ing a deep route\", async ({\n    page,\n  }) => {\n    let fixture = await createFixture({\n      files: {\n        \"app/root.tsx\": js`\n          import { Link, Outlet, Scripts } from \"react-router\";\n          export default function Root() {\n            return (\n              <html lang=\"en\">\n                <head></head>\n                <body >\n                  <nav>\n                    <Link to=\"/parent\" discover=\"none\">Parent Index</Link>\n                    <Link to=\"/parent/child2\" discover=\"none\">Child2</Link>\n                  </nav>\n                  <Outlet />\n                  <Scripts />\n                  </body>\n              </html>\n            );\n          }\n        `,\n\n        \"app/routes/_index.tsx\": js`\n          export default function Index() {\n            return <h1 id=\"index\">Index</h1>\n          }\n        `,\n        \"app/routes/parent.tsx\": js`\n          import { Outlet } from \"react-router\";\n          export default function Component() {\n            return (\n              <>\n                <h1 id=\"parent\">Parent</h1>\n                <Outlet />\n              </>\n            )\n          }\n        `,\n        \"app/routes/parent.child.tsx\": js`\n          export default function Component() {\n            return <h2 id=\"child\">Child</h2>;\n          }\n        `,\n        \"app/routes/parent._a.tsx\": js`\n          import { Outlet } from 'react-router';\n          export default function Component() {\n            return <div id=\"a\"><Outlet/></div>;\n          }\n        `,\n        \"app/routes/parent._a._b._index.tsx\": js`\n          export default function Component() {\n            return <h2 id=\"parent-index\">Parent Pathless Index</h2>;\n          }\n        `,\n        \"app/routes/parent._a._b.tsx\": js`\n          import { Outlet } from 'react-router';\n          export default function Component() {\n            return <div id=\"b\"><Outlet/></div>;\n          }\n        `,\n        \"app/routes/parent._a._b.child2.tsx\": js`\n          export default function Component() {\n            return <h2 id=\"child2\">Child 2</h2>;\n          }\n        `,\n      },\n    });\n    let appFixture = await createAppFixture(fixture);\n    let app = new PlaywrightFixture(appFixture, page);\n\n    let manifestRequests: string[] = [];\n    page.on(\"request\", (req) => {\n      if (req.url().includes(\"/__manifest\")) {\n        manifestRequests.push(req.url());\n      }\n    });\n\n    await app.goto(\"/parent/child\", true);\n    expect(await app.getHtml(\"#child\")).toMatch(\"Child\");\n    expect(await page.$(\"#a\")).toBeNull();\n    expect(await page.$(\"#b\")).toBeNull();\n\n    expect(\n      await page.evaluate(() =>\n        Object.keys((window as any).__reactRouterManifest.routes),\n      ),\n    ).toEqual([\n      \"root\",\n      \"routes/parent\",\n      \"routes/parent.child\",\n      \"routes/_index\",\n      \"routes/parent._a\",\n      \"routes/parent._a._b\",\n      \"routes/parent._a._b._index\",\n    ]);\n    expect(manifestRequests).toEqual([]);\n\n    // Without pre-loading the index, we'd \"match\" `/parent` to just the\n    // parent route client side and never fetch the children pathless/index routes\n    await app.clickLink(\"/parent\");\n    await page.waitForSelector(\"#parent-index\");\n    expect(await page.$(\"#a\")).not.toBeNull();\n    expect(await page.$(\"#b\")).not.toBeNull();\n    expect(await app.getHtml(\"#parent\")).toMatch(\"Parent\");\n    expect(await app.getHtml(\"#parent-index\")).toMatch(\"Parent Pathless Index\");\n    expect(manifestRequests.length).toBe(0);\n\n    // This will require a new fetch for the child2 portion\n    await app.clickLink(\"/parent/child2\");\n    await page.waitForSelector(\"#child2\");\n    expect(await app.getHtml(\"#parent\")).toMatch(`Parent`);\n    expect(await app.getHtml(\"#child2\")).toMatch(`Child 2`);\n    expect(manifestRequests).toEqual([\n      expect.stringMatching(/\\/__manifest\\?paths=%2Fparent%2Fchild2&version=/),\n    ]);\n  });\n\n  test(\"detects higher-ranking static routes on the server when a slug match is already known by the client\", async ({\n    page,\n  }) => {\n    let fixture = await createFixture({\n      files: {\n        \"app/root.tsx\": js`\n          import { Link, Outlet, Scripts } from \"react-router\";\n          export default function Root() {\n            return (\n              <html lang=\"en\">\n                <head></head>\n                <body >\n                  <nav>\n                    <Link to=\"/something\">/something</Link>\n                  </nav>\n                  <Outlet />\n                  <Scripts />\n                  </body>\n              </html>\n            );\n          }\n        `,\n\n        \"app/routes/_index.tsx\": js`\n          export default function Index() {\n            return <h1 id=\"index\">Index</h1>\n          }\n        `,\n        \"app/routes/$slug.tsx\": js`\n          import { Link } from \"react-router\";\n          export default function Component() {\n            return (\n              <>\n                <h2 id=\"slug\">Slug</h2>;\n                <Link to=\"/static\" discover=\"none\">Go to /static</Link>\n              </>\n            );\n          }\n        `,\n        \"app/routes/static.tsx\": js`\n          export default function Component() {\n            return <h2 id=\"static\">Static</h2>;\n          }\n        `,\n      },\n    });\n    let appFixture = await createAppFixture(fixture);\n    let app = new PlaywrightFixture(appFixture, page);\n\n    let manifestRequests: string[] = [];\n    page.on(\"request\", (req) => {\n      if (req.url().includes(\"/__manifest\")) {\n        manifestRequests.push(req.url());\n      }\n    });\n\n    await app.goto(\"/\", true);\n    expect(await app.getHtml(\"#index\")).toMatch(\"Index\");\n    expect(\n      await page.evaluate(() =>\n        Object.keys((window as any).__reactRouterManifest.routes),\n      ),\n    ).toEqual([\"root\", \"routes/_index\", \"routes/$slug\"]);\n    expect(manifestRequests).toEqual([\n      expect.stringMatching(/\\/__manifest\\?paths=%2Fsomething&version=/),\n    ]);\n    manifestRequests = [];\n\n    await app.clickLink(\"/something\");\n    await page.waitForSelector(\"#slug\");\n    expect(await app.getHtml(\"#slug\")).toMatch(\"Slug\");\n    expect(manifestRequests).toEqual([]);\n\n    // This will require a new fetch for the /static route\n    await app.clickLink(\"/static\");\n    await page.waitForSelector(\"#static\");\n    expect(await app.getHtml(\"#static\")).toMatch(\"Static\");\n    expect(manifestRequests).toEqual([\n      expect.stringMatching(/\\/__manifest\\?paths=%2Fstatic&version=/),\n    ]);\n    expect(\n      await page.evaluate(() =>\n        Object.keys((window as any).__reactRouterManifest.routes),\n      ),\n    ).toEqual([\"root\", \"routes/_index\", \"routes/$slug\", \"routes/static\"]);\n  });\n\n  test(\"detects higher-ranking static routes on the server when a splat match is already known by the client\", async ({\n    page,\n  }) => {\n    let fixture = await createFixture({\n      files: {\n        \"app/root.tsx\": js`\n          import { Link, Outlet, Scripts } from \"react-router\";\n          export default function Root() {\n            return (\n              <html lang=\"en\">\n                <head></head>\n                <body >\n                  <nav>\n                    <Link to=\"/something\">/something</Link>\n                  </nav>\n                  <Outlet />\n                  <Scripts />\n                  </body>\n              </html>\n            );\n          }\n        `,\n\n        \"app/routes/_index.tsx\": js`\n          export default function Index() {\n            return <h1 id=\"index\">Index</h1>\n          }\n        `,\n        \"app/routes/$.tsx\": js`\n          import { Link } from \"react-router\";\n          export default function Component() {\n            return (\n              <>\n                <h2 id=\"splat\">Splat</h2>;\n                <Link to=\"/static\" discover=\"none\">Go to /static</Link>\n              </>\n            );\n          }\n        `,\n        \"app/routes/static.tsx\": js`\n          export default function Component() {\n            return <h2 id=\"static\">Static</h2>;\n          }\n        `,\n      },\n    });\n    let appFixture = await createAppFixture(fixture);\n    let app = new PlaywrightFixture(appFixture, page);\n\n    let manifestRequests: string[] = [];\n    page.on(\"request\", (req) => {\n      if (req.url().includes(\"/__manifest\")) {\n        manifestRequests.push(req.url());\n      }\n    });\n\n    await app.goto(\"/\", true);\n    expect(await app.getHtml(\"#index\")).toMatch(\"Index\");\n    expect(\n      await page.evaluate(() =>\n        Object.keys((window as any).__reactRouterManifest.routes),\n      ),\n    ).toEqual([\"root\", \"routes/_index\", \"routes/$\"]);\n    expect(manifestRequests).toEqual([\n      expect.stringMatching(/\\/__manifest\\?paths=%2Fsomething&version=/),\n    ]);\n    manifestRequests = [];\n\n    await app.clickLink(\"/something\");\n    await page.waitForSelector(\"#splat\");\n    expect(await app.getHtml(\"#splat\")).toMatch(\"Splat\");\n    expect(manifestRequests).toEqual([]);\n\n    // This will require a new fetch for the /static route\n    await app.clickLink(\"/static\");\n    await page.waitForSelector(\"#static\");\n    expect(await app.getHtml(\"#static\")).toMatch(\"Static\");\n    expect(manifestRequests).toEqual([\n      expect.stringMatching(/\\/__manifest\\?paths=%2Fstatic&version=/),\n    ]);\n    expect(\n      await page.evaluate(() =>\n        Object.keys((window as any).__reactRouterManifest.routes),\n      ),\n    ).toEqual([\"root\", \"routes/_index\", \"routes/$\", \"routes/static\"]);\n  });\n\n  test(\"does not re-request for previously discovered slug routes\", async ({\n    page,\n  }) => {\n    let fixture = await createFixture({\n      files: {\n        \"app/root.tsx\": js`\n          import { Link, Outlet, Scripts } from \"react-router\";\n          export default function Root() {\n            return (\n              <html lang=\"en\">\n                <head></head>\n                <body >\n                  <nav>\n                    <Link to=\"/\" discover=\"none\">Go to /</Link>\n                    <Link to=\"/a\" discover=\"none\">Go to /a</Link>\n                    <Link to=\"/b\" discover=\"none\">Go to /b</Link>\n                  </nav>\n                  <Outlet />\n                  <Scripts />\n                  </body>\n              </html>\n            );\n          }\n        `,\n\n        \"app/routes/_index.tsx\": js`\n          export default function Index() {\n            return <h1 id=\"index\">Index</h1>;\n          }\n        `,\n        \"app/routes/$slug.tsx\": js`\n          import { Link, useParams } from \"react-router\";\n          export default function Component() {\n            let params = useParams();\n            return <h2 id=\"slug\">Slug: {params.slug}</h2>;\n          }\n        `,\n      },\n    });\n    let appFixture = await createAppFixture(fixture);\n    let app = new PlaywrightFixture(appFixture, page);\n\n    let manifestRequests: string[] = [];\n    page.on(\"request\", (req) => {\n      if (req.url().includes(\"/__manifest\")) {\n        manifestRequests.push(req.url());\n      }\n    });\n\n    await app.goto(\"/\", true);\n    expect(await app.getHtml(\"#index\")).toMatch(\"Index\");\n    expect(\n      await page.evaluate(() =>\n        Object.keys((window as any).__reactRouterManifest.routes),\n      ),\n    ).toEqual([\"root\", \"routes/_index\"]);\n    expect(manifestRequests.length).toBe(0);\n\n    // Click /a which will discover via a manifest request\n    await app.clickLink(\"/a\");\n    await page.waitForSelector(\"#slug\");\n    expect(await app.getHtml(\"#slug\")).toMatch(\"Slug: a\");\n    expect(manifestRequests).toEqual([\n      expect.stringMatching(/\\/__manifest\\?paths=%2Fa&version=/),\n    ]);\n    manifestRequests = [];\n\n    // Go back home\n    await app.clickLink(\"/\");\n    await page.waitForSelector(\"#index\");\n    expect(manifestRequests).toEqual([]);\n\n    // Click /a again which will not re-discover\n    await app.clickLink(\"/a\");\n    await page.waitForSelector(\"#slug\");\n    expect(await app.getHtml(\"#slug\")).toMatch(\"Slug: a\");\n    expect(manifestRequests).toEqual([]);\n    manifestRequests = [];\n\n    // Click /b which will need to discover\n    await app.clickLink(\"/b\");\n    await page.waitForSelector(\"#slug\");\n    expect(await app.getHtml(\"#slug\")).toMatch(\"Slug: b\");\n    expect(manifestRequests).toEqual([\n      expect.stringMatching(/\\/__manifest\\?paths=%2Fb&version=/),\n    ]);\n  });\n\n  test(\"does not re-request for previously discovered splat routes\", async ({\n    page,\n  }) => {\n    let fixture = await createFixture({\n      files: {\n        \"app/root.tsx\": js`\n          import { Link, Outlet, Scripts } from \"react-router\";\n          export default function Root() {\n            return (\n              <html lang=\"en\">\n                <head></head>\n                <body >\n                  <nav>\n                    <Link to=\"/\" discover=\"none\">Go to /</Link>\n                    <Link to=\"/a\" discover=\"none\">Go to /a</Link>\n                    <Link to=\"/b/c\" discover=\"none\">Go to /b/c</Link>\n                  </nav>\n                  <Outlet />\n                  <Scripts />\n                  </body>\n              </html>\n            );\n          }\n        `,\n\n        \"app/routes/_index.tsx\": js`\n          export default function Index() {\n            return <h1 id=\"index\">Index</h1>;\n          }\n        `,\n        \"app/routes/$.tsx\": js`\n          import { Link, useParams } from \"react-router\";\n          export default function Component() {\n            let params = useParams();\n            return <h2 id=\"splat\">Splat: {params[\"*\"]}</h2>;\n          }\n        `,\n      },\n    });\n    let appFixture = await createAppFixture(fixture);\n    let app = new PlaywrightFixture(appFixture, page);\n\n    let manifestRequests: string[] = [];\n    page.on(\"request\", (req) => {\n      if (req.url().includes(\"/__manifest\")) {\n        manifestRequests.push(req.url());\n      }\n    });\n\n    await app.goto(\"/\", true);\n    expect(await app.getHtml(\"#index\")).toMatch(\"Index\");\n    expect(\n      await page.evaluate(() =>\n        Object.keys((window as any).__reactRouterManifest.routes),\n      ),\n    ).toEqual([\"root\", \"routes/_index\"]);\n    expect(manifestRequests.length).toBe(0);\n\n    // Click /a which will discover via a manifest request\n    await app.clickLink(\"/a\");\n    await page.waitForSelector(\"#splat\");\n    expect(await app.getHtml(\"#splat\")).toMatch(\"Splat: a\");\n    expect(manifestRequests).toEqual([\n      expect.stringMatching(/\\/__manifest\\?paths=%2Fa&version=/),\n    ]);\n    manifestRequests = [];\n\n    // Go back home\n    await app.clickLink(\"/\");\n    await page.waitForSelector(\"#index\");\n    expect(manifestRequests).toEqual([]);\n\n    // Click /a again which will not re-discover\n    await app.clickLink(\"/a\");\n    await page.waitForSelector(\"#splat\");\n    expect(await app.getHtml(\"#splat\")).toMatch(\"Splat: a\");\n    expect(manifestRequests).toEqual([]);\n    manifestRequests = [];\n\n    // Click /b which will need to discover\n    await app.clickLink(\"/b/c\");\n    await page.waitForSelector(\"#splat\");\n    expect(await app.getHtml(\"#splat\")).toMatch(\"Splat: b/c\");\n    expect(manifestRequests).toEqual([\n      expect.stringMatching(/\\/__manifest\\?paths=%2Fb%2Fc&version=/),\n    ]);\n  });\n\n  test(\"does not re-request for previously navigated 404 routes\", async ({\n    page,\n  }) => {\n    let fixture = await createFixture({\n      files: {\n        \"app/root.tsx\": js`\n          import { Link, Outlet, Scripts } from \"react-router\";\n          export function Layout({ children }) {\n            return (\n              <html lang=\"en\">\n                <head></head>\n                <body >\n                  <nav>\n                    <Link to=\"/\" discover=\"none\">Go to /</Link>\n                    <Link to=\"/something\" discover=\"none\">Go to /something</Link>\n                    <Link to=\"/not/a/path\" discover=\"none\">Go to /not/a/path</Link>\n                  </nav>\n                  {children}\n                  <Scripts />\n                  </body>\n              </html>\n            );\n          }\n          export default function Root() {\n            return <Outlet />;\n          }\n          export function ErrorBoundary() {\n            return <h1 id=\"error\">Error</h1>;\n          }\n        `,\n\n        \"app/routes/_index.tsx\": js`\n          export default function Index() {\n            return <h1 id=\"index\">Index</h1>;\n          }\n        `,\n        \"app/routes/$slug.tsx\": js`\n          import { Link, useParams } from \"react-router\";\n          export default function Component() {\n            let params = useParams();\n            return <h2 id=\"slug\">Slug: {params.slug}</h2>;\n          }\n        `,\n      },\n    });\n    let appFixture = await createAppFixture(fixture);\n    let app = new PlaywrightFixture(appFixture, page);\n\n    let manifestRequests: string[] = [];\n    page.on(\"request\", (req) => {\n      if (req.url().includes(\"/__manifest\")) {\n        manifestRequests.push(req.url());\n      }\n    });\n\n    await app.goto(\"/\", true);\n    expect(await app.getHtml(\"#index\")).toMatch(\"Index\");\n    expect(\n      await page.evaluate(() =>\n        Object.keys((window as any).__reactRouterManifest.routes),\n      ),\n    ).toEqual([\"root\", \"routes/_index\"]);\n    expect(manifestRequests.length).toBe(0);\n\n    // Click a 404 link which will try to discover via a manifest request\n    await app.clickLink(\"/not/a/path\");\n    await page.waitForSelector(\"#error\");\n    expect(manifestRequests).toEqual([\n      expect.stringMatching(/\\/__manifest\\?paths=%2Fnot%2Fa%2Fpath&version=/),\n    ]);\n    manifestRequests = [];\n\n    // Go to a valid slug route\n    await app.clickLink(\"/something\");\n    await page.waitForSelector(\"#slug\");\n    expect(manifestRequests).toEqual([\n      expect.stringMatching(/\\/__manifest\\?paths=%2Fsomething&version=/),\n    ]);\n    manifestRequests = [];\n\n    // Click the same 404 link again which will not re-discover\n    await app.clickLink(\"/not/a/path\");\n    await page.waitForSelector(\"#error\");\n    expect(manifestRequests).toEqual([]);\n  });\n\n  test(\"skips prefetching if the URL gets too large\", async ({ page }) => {\n    let fixture = await createFixture({\n      files: {\n        ...getFiles(),\n        \"app/routes/_index.tsx\": js`\n          import { Link } from \"react-router\";\n          export default function Index() {\n            return (\n              <>\n                <h1 id=\"index\">Index</h1>\n                {/* 400 links * ~19 chars per link > our 7198 char URL limit */}\n                {...new Array(400).fill(null).map((el, i) => (\n                  <Link to={\"/dummy-link-\" + i}>{i}</Link>\n                ))}\n              </>\n            );\n          }\n        `,\n      },\n    });\n    let appFixture = await createAppFixture(fixture);\n    let app = new PlaywrightFixture(appFixture, page);\n\n    let manifestRequests: string[] = [];\n    page.on(\"request\", (req) => {\n      if (req.url().includes(\"/__manifest\")) {\n        manifestRequests.push(req.url());\n      }\n    });\n\n    await app.goto(\"/\", true);\n    await new Promise((resolve) => setTimeout(resolve, 250));\n    expect(manifestRequests.length).toBe(0);\n\n    await app.clickLink(\"/a\");\n    await page.waitForSelector(\"#a\");\n    expect(await app.getHtml(\"#a\")).toMatch(\"A LOADER\");\n    expect(\n      await page.evaluate(() =>\n        Object.keys((window as any).__reactRouterManifest.routes),\n      ),\n    ).toEqual([\"root\", \"routes/_index\", \"routes/a\"]);\n  });\n\n  test(\"includes a version query parameter as a cachebuster\", async ({\n    page,\n  }) => {\n    let fixture = await createFixture({\n      files: {\n        ...getFiles(),\n        \"app/routes/_index.tsx\": js`\n          import { Link } from \"react-router\";\n          export default function Index() {\n            return (\n              <>\n                <h1 id=\"index\">Index</h1>\n                <Link to=\"/a\">/a</Link>\n                <Link to=\"/b\">/b</Link>\n              </>\n            );\n          }\n        `,\n      },\n    });\n    let appFixture = await createAppFixture(fixture);\n    let app = new PlaywrightFixture(appFixture, page);\n\n    let manifestRequests: string[] = [];\n    page.on(\"request\", (req) => {\n      if (req.url().includes(\"/__manifest\")) {\n        manifestRequests.push(req.url());\n      }\n    });\n\n    await app.goto(\"/\", true);\n    await new Promise((resolve) => setTimeout(resolve, 250));\n    expect(manifestRequests).toEqual([\n      expect.stringMatching(\n        /\\/__manifest\\?paths=%2F%2C%2Fa%2C%2Fb&version=[a-z0-9]{8}/,\n      ),\n    ]);\n  });\n\n  test(\"sorts url parameters\", async ({ page }) => {\n    let fixture = await createFixture({\n      files: {\n        ...getFiles(),\n        \"app/routes/_index.tsx\": js`\n          import { Link } from \"react-router\";\n          export default function Index() {\n            return (\n              <>\n                <h1 id=\"index\">Index</h1>\n                <Link to=\"/a\">/a</Link>\n                <Link to=\"/c\">/c</Link>\n                <Link to=\"/e\">/e</Link>\n                <Link to=\"/g\">/g</Link>\n                <Link to=\"/f\">/f</Link>\n                <Link to=\"/d\">/d</Link>\n                <Link to=\"/b\">/b</Link>\n              </>\n            );\n          }\n        `,\n      },\n    });\n    let appFixture = await createAppFixture(fixture);\n    let app = new PlaywrightFixture(appFixture, page);\n\n    let manifestRequests: string[] = [];\n    page.on(\"request\", (req) => {\n      if (req.url().includes(\"/__manifest\")) {\n        manifestRequests.push(req.url());\n      }\n    });\n\n    await app.goto(\"/\", true);\n    await new Promise((resolve) => setTimeout(resolve, 250));\n    expect(manifestRequests).toEqual([\n      expect.stringMatching(\n        /\\/__manifest\\?paths=%2F%2C%2Fa%2C%2Fb%2C%2Fc%2C%2Fd%2C%2Fe%2C%2Ff%2C%2Fg/,\n      ),\n    ]);\n  });\n\n  test(\"handles interruptions from back to back navigations\", async ({\n    page,\n  }) => {\n    let fixture = await createFixture({\n      files: {\n        ...getFiles(),\n        \"app/routes/a.tsx\": js`\n          import { Link, Outlet, useLoaderData, useNavigate } from \"react-router\";\n          export function loader({ request }) {\n            return { message: \"A LOADER\" };\n          }\n          export default function Index() {\n            let data = useLoaderData();\n            let navigate = useNavigate();\n            return (\n              <>\n                <h1 id=\"a\">A: {data.message}</h1>\n                <button data-link onClick={async () => {\n                  navigate('/a/b');\n                  setTimeout(() => navigate('/a/b'), 0)\n                }}>\n                  /a/b\n                </button>\n                <Outlet/>\n              </>\n            )\n          }\n        `,\n      },\n    });\n    let appFixture = await createAppFixture(fixture);\n    let app = new PlaywrightFixture(appFixture, page);\n\n    await app.goto(\"/a\", true);\n    expect(\n      await page.evaluate(() =>\n        Object.keys((window as any).__reactRouterManifest.routes),\n      ),\n    ).toEqual([\"root\", \"routes/a\", \"routes/_index\"]);\n\n    // /a/b gets discovered on click\n    await app.clickElement(\"[data-link]\");\n    await new Promise((resolve) => setTimeout(resolve, 1000));\n    expect(await (await page.$(\"body\"))?.textContent()).not.toContain(\n      \"Not Found\",\n    );\n    await page.waitForSelector(\"#b\");\n\n    expect(\n      await page.evaluate(() =>\n        Object.keys((window as any).__reactRouterManifest.routes),\n      ),\n    ).toEqual([\"root\", \"routes/a\", \"routes/_index\", \"routes/a.b\"]);\n  });\n\n  test(\"loads ancestor index routes on navigations\", async ({ page }) => {\n    let fixture = await createFixture({\n      files: {\n        ...getFiles(),\n        \"app/root.tsx\": js`\n          import * as React from \"react\";\n          import { Link, Links, Meta, Outlet, Scripts } from \"react-router\";\n          export default function Root() {\n            let [showLink, setShowLink] = React.useState(false);\n            return (\n              <html lang=\"en\">\n                <head>\n                  <Meta />\n                  <Links />\n                </head>\n                <body>\n                  <Link to=\"/\" discover=\"none\">Home</Link><br/>\n                  <Link to=\"/a\" discover=\"none\">/a</Link><br/>\n                  <Link to=\"/a/b\" discover=\"none\">/a/b</Link><br/>\n                  <Link to=\"/a/b/c\" discover=\"none\">/a/b/c</Link><br/>\n                  <Outlet />\n                  <Scripts />\n                </body>\n              </html>\n            );\n          }\n        `,\n        \"app/routes/a._index.tsx\": js`\n          export default function Index() {\n            return <h3 id=\"a-index\">A INDEX</h3>;\n          }\n        `,\n        \"app/routes/a.b._index.tsx\": js`\n          export default function Index() {\n            return <h3 id=\"b-index\">B INDEX</h3>;\n          }\n        `,\n      },\n    });\n    let appFixture = await createAppFixture(fixture);\n    let app = new PlaywrightFixture(appFixture, page);\n\n    await app.goto(\"/\", true);\n    expect(\n      await page.evaluate(() =>\n        Object.keys((window as any).__reactRouterManifest.routes),\n      ),\n    ).toEqual([\"root\", \"routes/_index\"]);\n\n    await app.clickLink(\"/a/b/c\");\n    await page.waitForSelector(\"#c\");\n\n    // /a/b is not discovered yet even thought it's rendered\n    expect(\n      await page.evaluate(() =>\n        Object.keys((window as any).__reactRouterManifest.routes),\n      ),\n    ).toEqual([\n      \"root\",\n      \"routes/_index\",\n      \"routes/a\",\n      \"routes/a._index\",\n      \"routes/a.b\",\n      \"routes/a.b._index\",\n      \"routes/a.b.c\",\n    ]);\n\n    await app.clickLink(\"/a/b\");\n    await page.waitForSelector(\"#b-index\");\n\n    await app.clickLink(\"/a\");\n    await page.waitForSelector(\"#a-index\");\n  });\n\n  test(\"allows configuration of the manifest path\", async ({ page }) => {\n    let fixture = await createFixture({\n      files: {\n        ...getFiles(),\n        \"react-router.config.ts\": reactRouterConfig({\n          routeDiscovery: { mode: \"lazy\", manifestPath: \"/custom-manifest\" },\n        }),\n      },\n    });\n    let appFixture = await createAppFixture(fixture);\n    let app = new PlaywrightFixture(appFixture, page);\n\n    let wrongManifestRequests: string[] = [];\n    let manifestRequests: string[] = [];\n    page.on(\"request\", (req) => {\n      if (req.url().includes(\"/__manifest\")) {\n        wrongManifestRequests.push(req.url());\n      }\n      if (req.url().includes(\"/custom-manifest\")) {\n        manifestRequests.push(req.url());\n      }\n    });\n\n    await app.goto(\"/\", true);\n    expect(\n      await page.evaluate(() =>\n        Object.keys((window as any).__reactRouterManifest.routes),\n      ),\n    ).toEqual([\"root\", \"routes/_index\", \"routes/a\"]);\n    expect(manifestRequests).toEqual([\n      expect.stringMatching(/\\/custom-manifest\\?paths=%2F%2C%2Fa&version=/),\n    ]);\n    manifestRequests = [];\n\n    await app.clickLink(\"/a\");\n    await page.waitForSelector(\"#a\");\n    expect(await app.getHtml(\"#a\")).toBe(`<h1 id=\"a\">A: A LOADER</h1>`);\n    // Wait for eager discovery to kick off\n    await new Promise((r) => setTimeout(r, 500));\n    expect(manifestRequests).toEqual([\n      expect.stringMatching(/\\/custom-manifest\\?paths=%2Fa%2Fb&version=/),\n    ]);\n\n    expect(wrongManifestRequests).toEqual([]);\n  });\n\n  test(\"manifest version mismatch reload should preserve query parameters and hash\", async ({\n    page,\n  }) => {\n    let fixture = await createFixture({\n      files: {\n        \"app/routes/_index.tsx\": js`\n          import { Link, useLocation } from \"react-router\";\n\n          export default function Index() {\n            const location = useLocation();\n            return (\n              <div>\n                <h1>Home</h1>\n                <p data-location>Location: {location.pathname + location.search + location.hash}</p>\n                <Link to=\"/other?token=abc123&ref=campaign#section1\">Go to Other</Link>\n              </div>\n            );\n          }\n        `,\n        \"app/routes/other.tsx\": js`\n          import { useLocation } from \"react-router\";\n\n          export default function Other() {\n            const location = useLocation();\n            return (\n              <div>\n                <h1>Other Page</h1>\n                <p data-location2>Location: {location.pathname + location.search + location.hash}</p>\n              </div>\n            );\n          }\n        `,\n      },\n    });\n\n    // Trigger mismatch + hard reload when trying to patch the /other route\n    await page.route(/\\/__manifest/, async (route) => {\n      if (route.request().url().includes(encodeURIComponent(\"/other\"))) {\n        await route.fulfill({\n          status: 204,\n          headers: {\n            \"X-Remix-Reload-Document\": \"true\",\n          },\n        });\n      } else {\n        await route.continue();\n      }\n    });\n\n    let appFixture = await createAppFixture(fixture);\n    let app = new PlaywrightFixture(appFixture, page);\n\n    // Start on home page\n    await app.goto(\"/\");\n    await page.waitForSelector(\"h1\");\n    await expect(page.locator(\"[data-location]\")).toHaveText(\"Location: /\");\n\n    // Click link to /other with query params and hash\n    // This should trigger manifest fetch -> version mismatch -> hard reload\n    await app.clickLink(\"/other?token=abc123&ref=campaign#section1\");\n\n    // Wait for the page to reload and render\n    await page.waitForSelector(\"[data-location2]\", { timeout: 5000 });\n\n    // Query parameters and hash should be preserved after reload\n    await expect(page.locator(\"[data-location2]\")).toHaveText(\n      \"Location: /other?token=abc123&ref=campaign#section1\",\n    );\n\n    // Also verify the URL in the browser\n    const currentUrl = page.url();\n    expect(currentUrl).toContain(\"token=abc123\");\n    expect(currentUrl).toContain(\"ref=campaign\");\n    expect(currentUrl).toContain(\"#section1\");\n  });\n\n  test.describe(\"routeDiscovery=initial\", () => {\n    test(\"loads full manifest on initial load\", async ({ page }) => {\n      let fixture = await createFixture({\n        files: {\n          ...getFiles(),\n          \"react-router.config.ts\": reactRouterConfig({\n            routeDiscovery: { mode: \"initial\" },\n          }),\n          \"app/entry.client.tsx\": js`\n            import { HydratedRouter } from \"react-router/dom\";\n            import { startTransition, StrictMode } from \"react\";\n            import { hydrateRoot } from \"react-dom/client\";\n            startTransition(() => {\n              hydrateRoot(\n                document,\n                <StrictMode>\n                  <HydratedRouter discover={\"none\"} />\n                </StrictMode>\n              );\n            });\n          `,\n        },\n      });\n      let appFixture = await createAppFixture(fixture);\n\n      let manifestRequests: string[] = [];\n      page.on(\"request\", (req) => {\n        if (req.url().includes(\"/__manifest\")) {\n          manifestRequests.push(req.url());\n        }\n      });\n\n      let app = new PlaywrightFixture(appFixture, page);\n      let res = await fixture.requestDocument(\"/\");\n      let html = await res.text();\n\n      expect(html).not.toContain(\"window.__reactRouterManifest = {\");\n      expect(html).toContain(\n        '<link rel=\"modulepreload\" href=\"/assets/manifest-',\n      );\n\n      // Linking to A succeeds\n      await app.goto(\"/\", true);\n      expect(\n        await page.evaluate(() =>\n          Object.keys((window as any).__reactRouterManifest.routes),\n        ),\n      ).toEqual([\n        \"root\",\n        \"routes/_index\",\n        \"routes/a\",\n        \"routes/a.b\",\n        \"routes/a.b.c\",\n      ]);\n\n      await app.clickLink(\"/a\");\n      await page.waitForSelector(\"#a\");\n      expect(await app.getHtml(\"#a\")).toBe(`<h1 id=\"a\">A: A LOADER</h1>`);\n      expect(manifestRequests).toEqual([]);\n    });\n\n    test(\"defaults to `routeDiscovery=initial` when `ssr:false` is set\", async ({\n      page,\n    }) => {\n      let fixture = await createFixture({\n        spaMode: true,\n        files: {\n          \"react-router.config.ts\": reactRouterConfig({\n            ssr: false,\n          }),\n          \"app/root.tsx\": js`\n            import * as React from \"react\";\n            import { Link, Links, Meta, Outlet, Scripts } from \"react-router\";\n            export default function Root() {\n              let [showLink, setShowLink] = React.useState(false);\n              return (\n                <html lang=\"en\">\n                  <head>\n                    <Meta />\n                    <Links />\n                  </head>\n                  <body>\n                    <Link to=\"/\">Home</Link><br/>\n                    <Link to=\"/a\">/a</Link><br/>\n                    <Outlet />\n                    <Scripts />\n                  </body>\n                </html>\n              );\n            }\n          `,\n          \"app/routes/_index.tsx\": js`\n            export default function Index() {\n              return <h1 id=\"index\">Index</h1>\n            }\n          `,\n\n          \"app/routes/a.tsx\": js`\n            export function clientLoader({ request }) {\n              return { message: \"A LOADER\" };\n            }\n            export default function Index({ loaderData }) {\n              return <h1 id=\"a\">A: {loaderData.message}</h1>\n            }\n          `,\n        },\n      });\n      let appFixture = await createAppFixture(fixture);\n\n      let manifestRequests: string[] = [];\n      page.on(\"request\", (req) => {\n        if (req.url().includes(\"/__manifest\")) {\n          manifestRequests.push(req.url());\n        }\n      });\n\n      let app = new PlaywrightFixture(appFixture, page);\n      let res = await fixture.requestDocument(\"/\");\n      let html = await res.text();\n\n      expect(html).toContain('\"routeDiscovery\":{\"mode\":\"initial\"}');\n\n      await app.goto(\"/\", true);\n      await page.waitForSelector(\"#index\");\n      await app.clickLink(\"/a\");\n      await page.waitForSelector(\"#a\");\n      expect(await app.getHtml(\"#a\")).toBe(`<h1 id=\"a\">A: A LOADER</h1>`);\n      expect(manifestRequests).toEqual([]);\n    });\n\n    test(\"Errors if you try to set routeDiscovery=lazy and ssr:false\", async () => {\n      let ogConsole = console.error;\n      console.error = () => {};\n      let buildStdio = new PassThrough();\n      let err;\n      try {\n        await createFixture({\n          buildStdio,\n          spaMode: true,\n          files: {\n            ...getFiles(),\n            \"react-router.config.ts\": reactRouterConfig({\n              ssr: false,\n              routeDiscovery: { mode: \"lazy\" },\n            }),\n          },\n        });\n      } catch (e) {\n        err = e;\n      }\n\n      let chunks: Buffer[] = [];\n      let buildOutput = await new Promise<string>((resolve, reject) => {\n        buildStdio.on(\"data\", (chunk) => chunks.push(Buffer.from(chunk)));\n        buildStdio.on(\"error\", (err) => reject(err));\n        buildStdio.on(\"end\", () =>\n          resolve(Buffer.concat(chunks).toString(\"utf8\")),\n        );\n      });\n\n      expect(err).toEqual(new Error(\"Build failed, check the output above\"));\n      expect(buildOutput).toContain(\n        'Error: The `routeDiscovery.mode` config cannot be set to \"lazy\" when setting `ssr:false`',\n      );\n      console.error = ogConsole;\n    });\n  });\n});\n"
  },
  {
    "path": "integration/form-data-test.ts",
    "content": "import { test, expect } from \"@playwright/test\";\n\nimport { createFixture, js } from \"./helpers/create-fixture.js\";\nimport type { Fixture } from \"./helpers/create-fixture.js\";\n\nlet fixture: Fixture;\n\ntest.beforeAll(async () => {\n  fixture = await createFixture({\n    files: {\n      \"app/routes/_index.tsx\": js`\n          export async function action({ request }) {\n            try {\n              await request.formData()\n            } catch {\n              return new Response(\"no pizza\");\n            }\n            return new Response(\"pizza\");\n          }\n        `,\n    },\n  });\n});\n\ntest(\"invalid content-type does not crash server\", async () => {\n  let response = await fixture.requestDocument(\"/\", {\n    method: \"post\",\n    headers: { \"content-type\": \"application/json\" },\n  });\n  expect(await response.text()).toMatch(\"no pizza\");\n});\n\ntest(\"invalid urlencoded body does not crash server\", async () => {\n  let response = await fixture.requestDocument(\"/\", {\n    method: \"post\",\n    headers: { \"content-type\": \"application/x-www-form-urlencoded\" },\n    body: \"$rofl this is totally invalid$\",\n  });\n  expect(await response.text()).toMatch(\"pizza\");\n});\n\ntest(\"invalid multipart content-type does not crash server\", async () => {\n  let response = await fixture.requestDocument(\"/\", {\n    method: \"post\",\n    headers: { \"content-type\": \"multipart/form-data\" },\n    body: \"$rofl this is totally invalid$\",\n  });\n  expect(await response.text()).toMatch(\"pizza\");\n});\n\ntest(\"invalid multipart body does not crash server\", async () => {\n  let response = await fixture.requestDocument(\"/\", {\n    method: \"post\",\n    headers: { \"content-type\": \"multipart/form-data; boundary=abc\" },\n    body: \"$rofl this is totally invalid$\",\n  });\n  expect(await response.text()).toMatch(\"pizza\");\n});\n"
  },
  {
    "path": "integration/form-test.ts",
    "content": "import { test, expect } from \"@playwright/test\";\n\nimport {\n  createAppFixture,\n  createFixture,\n  js,\n} from \"./helpers/create-fixture.js\";\nimport type { Fixture, AppFixture } from \"./helpers/create-fixture.js\";\nimport { getElement, PlaywrightFixture } from \"./helpers/playwright-fixture.js\";\n\ntest.describe(\"Forms\", () => {\n  let fixture: Fixture;\n  let appFixture: AppFixture;\n\n  let KEYBOARD_INPUT = \"KEYBOARD_INPUT\";\n  let CHECKBOX_BUTTON = \"CHECKBOX_BUTTON\";\n  let ORPHAN_BUTTON = \"ORPHAN_BUTTON\";\n  let FORM_WITH_ACTION_INPUT = \"FORM_WITH_ACTION_INPUT\";\n  let FORM_WITH_ORPHAN = \"FORM_WITH_ORPHAN\";\n  let LUNCH = \"LUNCH\";\n  let CHEESESTEAK = \"CHEESESTEAK\";\n  let LAKSA = \"LAKSA\";\n  let SQUID_INK_HOTDOG = \"SQUID_INK_HOTDOG\";\n  let ACTION = \"action\";\n  let EAT = \"EAT\";\n\n  let STATIC_ROUTE_NO_ACTION = \"static-route-none\";\n  let STATIC_ROUTE_ABSOLUTE_ACTION = \"static-route-abs\";\n  let STATIC_ROUTE_CURRENT_ACTION = \"static-route-cur\";\n  let STATIC_ROUTE_PARENT_ACTION = \"static-route-parent\";\n  let STATIC_ROUTE_TOO_MANY_DOTS_ACTION = \"static-route-too-many-dots\";\n  let INDEX_ROUTE_NO_ACTION = \"index-route-none\";\n  let INDEX_ROUTE_NO_ACTION_POST = \"index-route-none-post\";\n  let INDEX_ROUTE_ABSOLUTE_ACTION = \"index-route-abs\";\n  let INDEX_ROUTE_CURRENT_ACTION = \"index-route-cur\";\n  let INDEX_ROUTE_PARENT_ACTION = \"index-route-parent\";\n  let INDEX_ROUTE_TOO_MANY_DOTS_ACTION = \"index-route-too-many-dots\";\n  let DYNAMIC_ROUTE_NO_ACTION = \"dynamic-route-none\";\n  let DYNAMIC_ROUTE_ABSOLUTE_ACTION = \"dynamic-route-abs\";\n  let DYNAMIC_ROUTE_CURRENT_ACTION = \"dynamic-route-cur\";\n  let DYNAMIC_ROUTE_PARENT_ACTION = \"dynamic-route-parent\";\n  let DYNAMIC_ROUTE_TOO_MANY_DOTS_ACTION = \"dynamic-route-too-many-dots\";\n  let LAYOUT_ROUTE_NO_ACTION = \"layout-route-none\";\n  let LAYOUT_ROUTE_ABSOLUTE_ACTION = \"layout-route-abs\";\n  let LAYOUT_ROUTE_CURRENT_ACTION = \"layout-route-cur\";\n  let LAYOUT_ROUTE_PARENT_ACTION = \"layout-route-parent\";\n  let LAYOUT_ROUTE_TOO_MANY_DOTS_ACTION = \"layout-route-too-many-dots\";\n  let SPLAT_ROUTE_NO_ACTION = \"splat-route-none\";\n  let SPLAT_ROUTE_ABSOLUTE_ACTION = \"splat-route-abs\";\n  let SPLAT_ROUTE_CURRENT_ACTION = \"splat-route-cur\";\n  let SPLAT_ROUTE_PARENT_ACTION = \"splat-route-parent\";\n  let SPLAT_ROUTE_TOO_MANY_DOTS_ACTION = \"splat-route-too-many-dots\";\n\n  test.beforeEach(async ({ context }) => {\n    await context.route(/\\.data$/, async (route) => {\n      await new Promise((resolve) => setTimeout(resolve, 50));\n      route.continue();\n    });\n  });\n\n  test.beforeAll(async () => {\n    fixture = await createFixture({\n      files: {\n        \"app/routes/get-submission.tsx\": js`\n          import { useLoaderData, Form } from \"react-router\";\n\n          export function loader({ request }) {\n            let url = new URL(request.url);\n            return url.searchParams.toString()\n          }\n\n          export default function() {\n            let data = useLoaderData();\n            return (\n              <>\n                <Form>\n                  <input type=\"text\" name=\"${LUNCH}\" defaultValue=\"${CHEESESTEAK}\" />\n                  <input type=\"text\" name=\"${ACTION}\" defaultValue=\"${EAT}\" />\n                  <button type=\"submit\">\n                  </button>\n                </Form>\n\n                <Form id=\"${FORM_WITH_ACTION_INPUT}\">\n                  <input type=\"text\" name=\"${ACTION}\" defaultValue=\"${EAT}\" />\n                  <button type=\"submit\">\n                  </button>\n                </Form>\n\n                <Form id=\"${FORM_WITH_ORPHAN}\">\n                  <input id=\"${KEYBOARD_INPUT}\" type=\"text\" />\n                  <button\n                    id=\"buttonWithValue\"\n                    type=\"submit\"\n                    name=\"${LUNCH}\"\n                    value=\"${LAKSA}\"\n                  >\n                    <svg height=\"100\" width=\"100\">\n                      <circle id=\"svg-button-enhanced\" cx=\"50\" cy=\"50\" r=\"40\" stroke=\"black\" strokeWidth=\"3\" fill=\"red\" />\n                    </svg>\n                  </button>\n                </Form>\n\n                <button\n                  type=\"submit\"\n                  id=\"${ORPHAN_BUTTON}\"\n                  form=\"${FORM_WITH_ORPHAN}\"\n                  name=\"${LUNCH}\"\n                  value=\"${SQUID_INK_HOTDOG}\"\n                >Orphan</button>\n\n                <Form>\n                  <input\n                    defaultChecked={true}\n                    type=\"checkbox\"\n                    name=\"${LUNCH}\"\n                    defaultValue=\"${CHEESESTEAK}\"\n                  />\n                  <input\n                    defaultChecked={true}\n                    type=\"checkbox\"\n                    name=\"${LUNCH}\"\n                    defaultValue=\"${LAKSA}\"\n                  />\n\n                  <button\n                    id=\"${CHECKBOX_BUTTON}\"\n                    type=\"submit\"\n                  >Go</button>\n                </Form>\n\n                <pre>{data}</pre>\n              </>\n            )\n          }\n        `,\n\n        \"app/routes/about.tsx\": js`\n          export async function action({ request }) {\n            return { submitted: true };\n          }\n          export default function () {\n            return <h1>About</h1>;\n          }\n        `,\n\n        \"app/routes/inbox.tsx\": js`\n          import { Form } from \"react-router\";\n          export default function() {\n            return (\n              <>\n                <Form id=\"${STATIC_ROUTE_NO_ACTION}\">\n                  <button>Submit</button>\n                </Form>\n                <Form id=\"${STATIC_ROUTE_ABSOLUTE_ACTION}\" action=\"/about\">\n                  <button>Submit</button>\n                </Form>\n                <Form id=\"${STATIC_ROUTE_CURRENT_ACTION}\" action=\".\">\n                  <button>Submit</button>\n                </Form>\n                <Form id=\"${STATIC_ROUTE_PARENT_ACTION}\" action=\"..\">\n                  <button>Submit</button>\n                </Form>\n                <Form id=\"${STATIC_ROUTE_TOO_MANY_DOTS_ACTION}\" action=\"../../../about\">\n                  <button>Submit</button>\n                </Form>\n              </>\n            )\n          }\n        `,\n\n        \"app/routes/blog.tsx\": js`\n          import { Form, Outlet } from \"react-router\";\n          export default function() {\n            return (\n              <>\n                <h1>Blog</h1>\n                <Form id=\"${LAYOUT_ROUTE_NO_ACTION}\">\n                  <button>Submit</button>\n                </Form>\n                <Form id=\"${LAYOUT_ROUTE_ABSOLUTE_ACTION}\" action=\"/about\">\n                  <button>Submit</button>\n                </Form>\n                <Form id=\"${LAYOUT_ROUTE_CURRENT_ACTION}\" action=\".\">\n                  <button>Submit</button>\n                </Form>\n                <Form id=\"${LAYOUT_ROUTE_PARENT_ACTION}\" action=\"..\">\n                  <button>Submit</button>\n                </Form>\n                <Form id=\"${LAYOUT_ROUTE_TOO_MANY_DOTS_ACTION}\" action=\"../../../../about\">\n                  <button>Submit</button>\n                </Form>\n                <Outlet />\n              </>\n            )\n          }\n        `,\n\n        \"app/routes/blog._index.tsx\": js`\n          import { Form } from \"react-router\";\n\n          export function loader() {\n            return { timestamp: Date.now() }\n          }\n\n          export function action() {\n            return { ok: true };\n          }\n\n          export default function Component({ loaderData }) {\n            return (\n              <>\n                <div id=\"timestamp\">{loaderData.timestamp}</div>\n                <Form id=\"${INDEX_ROUTE_NO_ACTION}\">\n                  <input type=\"hidden\" name=\"foo\" defaultValue=\"1\" />\n                  <button>Submit</button>\n                </Form>\n                <Form id=\"${INDEX_ROUTE_ABSOLUTE_ACTION}\" action=\"/about\">\n                  <button>Submit</button>\n                </Form>\n                <Form id=\"${INDEX_ROUTE_CURRENT_ACTION}\" action=\".\">\n                  <button>Submit</button>\n                </Form>\n                <Form id=\"${INDEX_ROUTE_PARENT_ACTION}\" action=\"..\">\n                  <button>Submit</button>\n                </Form>\n                <Form id=\"${INDEX_ROUTE_TOO_MANY_DOTS_ACTION}\" action=\"../../../../about\">\n                  <button>Submit</button>\n                </Form>\n\n                <Form method=\"post\" id=\"${INDEX_ROUTE_NO_ACTION_POST}\">\n                  <input type=\"hidden\" name=\"bar\" defaultValue=\"2\" />\n                  <button>Submit</button>\n                </Form>\n              </>\n            )\n          }\n        `,\n\n        \"app/routes/blog.$postId.tsx\": js`\n          import { Form } from \"react-router\";\n          export default function() {\n            return (\n              <>\n                <Form id=\"${DYNAMIC_ROUTE_NO_ACTION}\">\n                  <button>Submit</button>\n                </Form>\n                <Form id=\"${DYNAMIC_ROUTE_ABSOLUTE_ACTION}\" action=\"/about\">\n                  <button>Submit</button>\n                </Form>\n                <Form id=\"${DYNAMIC_ROUTE_CURRENT_ACTION}\" action=\".\">\n                  <button>Submit</button>\n                </Form>\n                <Form id=\"${DYNAMIC_ROUTE_PARENT_ACTION}\" action=\"..\">\n                  <button>Submit</button>\n                </Form>\n                <Form id=\"${DYNAMIC_ROUTE_TOO_MANY_DOTS_ACTION}\" action=\"../../../../about\">\n                  <button>Submit</button>\n                </Form>\n              </>\n            )\n          }\n        `,\n\n        \"app/routes/projects.tsx\": js`\n          import { Form, Outlet } from \"react-router\";\n          export default function() {\n            return (\n              <>\n                <h1>Projects</h1>\n                <Outlet />\n              </>\n            )\n          }\n        `,\n\n        \"app/routes/projects._index.tsx\": js`\n          export default function() {\n            return <h2>All projects</h2>\n          }\n        `,\n\n        \"app/routes/projects.$.tsx\": js`\n          import { Form } from \"react-router\";\n          export default function() {\n            return (\n              <>\n                <Form id=\"${SPLAT_ROUTE_NO_ACTION}\">\n                  <button>Submit</button>\n                </Form>\n                <Form id=\"${SPLAT_ROUTE_ABSOLUTE_ACTION}\" action=\"/about\">\n                  <button>Submit</button>\n                </Form>\n                <Form id=\"${SPLAT_ROUTE_CURRENT_ACTION}\" action=\".\">\n                  <button>Submit</button>\n                </Form>\n                <Form id=\"${SPLAT_ROUTE_PARENT_ACTION}\" action=\"..\">\n                  <button>Submit</button>\n                </Form>\n                <Form id=\"${SPLAT_ROUTE_TOO_MANY_DOTS_ACTION}\" action=\"../../../../about\">\n                  <button>Submit</button>\n                </Form>\n              </>\n            )\n          }\n        `,\n\n        \"app/routes/stop-propagation.tsx\": js`\n          import { Form, useActionData } from \"react-router\";\n\n          export async function action({ request }) {\n            let formData = await request.formData();\n            return Object.fromEntries(formData);\n          }\n\n          export default function Index() {\n            let actionData = useActionData();\n            return (\n              <div onClick={(event) => event.stopPropagation()}>\n                {actionData ? <pre id=\"action-data\">{JSON.stringify(actionData)}</pre> : null}\n                <Form method=\"post\">\n                  <button type=\"submit\" name=\"intent\" value=\"add\">Add</button>\n                </Form>\n              </div>\n            )\n          }\n        `,\n\n        \"app/routes/form-method.tsx\": js`\n          import { Form, useActionData, useLoaderData, useSearchParams } from \"react-router\";\n\n          export function action({ request }) {\n            return request.method\n          }\n\n          export function loader({ request }) {\n            return request.method\n          }\n\n          export default function() {\n            let actionData = useActionData();\n            let loaderData = useLoaderData();\n            let [searchParams] = useSearchParams();\n            let formMethod = searchParams.get('method') || 'GET';\n            let submitterFormMethod = searchParams.get('submitterFormMethod') || 'GET';\n            return (\n              <>\n                <Form method={formMethod}>\n                  <button>Submit</button>\n                  <button formMethod={submitterFormMethod}>Submit with {submitterFormMethod}</button>\n                </Form>\n                {actionData ? <pre id=\"action-method\">{actionData}</pre> : null}\n                <pre id=\"loader-method\">{loaderData}</pre>\n              </>\n            )\n          }\n        `,\n\n        \"app/routes/submitter.tsx\": js`\n          import { Form } from \"react-router\";\n\n          export default function() {\n            return (\n              <>\n                <button name=\"tasks\" value=\"outside\" form=\"myform\">Outside</button>\n                <Form action=\"/outputFormData\" id=\"myform\">\n                  <input type=\"text\" name=\"tasks\" defaultValue=\"first\" />\n                  <input type=\"text\" name=\"tasks\" defaultValue=\"second\" />\n\n                  <button name=\"tasks\" value=\"\">Add Task</button>\n                  <button value=\"\">No Name</button>\n                  <input type=\"image\" name=\"tasks\" alt=\"Add Task\" />\n                  <input type=\"image\" alt=\"No Name\" />\n\n                  <input type=\"text\" name=\"tasks\" defaultValue=\"last\" />\n                </Form>\n              </>\n            )\n          }\n        `,\n\n        \"app/routes/file-upload.tsx\": js`\n          import { Form, useSearchParams } from \"react-router\";\n\n          export default function() {\n            const [params] = useSearchParams();\n            return (\n              <Form\n                action=\"/outputFormData\"\n                method={params.get(\"method\") ?? undefined}\n                encType={params.get(\"encType\") ?? undefined}\n              >\n                <input type=\"file\" name=\"filey\" />\n                <input type=\"file\" name=\"filey2\" multiple />\n                <input type=\"file\" name=\"filey3\" />\n                <button />\n              </Form>\n            )\n          }\n        `,\n\n        \"app/routes/empty-file-upload.tsx\": js`\n          import { Form, useActionData } from \"react-router\";\n\n          export async function action({ request }) {\n            let formData = await request.formData();\n            return {\n              text: formData.get('text'),\n              file: {\n                name: formData.get('file').name,\n                size: formData.get('file').size,\n              },\n              fileMultiple: formData.getAll('fileMultiple').map(f => ({\n                name: f.name,\n                size: f.size,\n              })),\n            }\n          }\n\n          export default function() {\n            const actionData = useActionData();\n            return (\n              <Form method=\"post\" encType=\"multipart/form-data\">\n                <input name=\"text\" />\n                <input type=\"file\" name=\"file\" />\n                <input type=\"file\" name=\"fileMultiple\" multiple />\n                <button type=\"submit\">Submit</button>\n                {actionData ? <p id=\"action-data\">{JSON.stringify(actionData)}</p> : null}\n              </Form>\n            )\n          }\n        `,\n\n        // Generic route for outputting url-encoded form data (either from the request body or search params)\n        //\n        // TODO: refactor other tests to use this\n        \"app/routes/outputFormData.tsx\": js`\n          import { useActionData, useSearchParams } from \"react-router\";\n\n          export async function action({ request }) {\n            const formData = await request.formData();\n            const body = new URLSearchParams();\n            for (let [key, value] of formData) {\n              body.append(\n                key,\n                value instanceof File ? await streamToString(value.stream()) : value\n              );\n            }\n            return body.toString();\n          }\n\n          export default function OutputFormData() {\n            const requestBody = useActionData();\n            const searchParams = useSearchParams()[0];\n            return <input id=\"formData\" defaultValue={requestBody ?? searchParams} />;\n          }\n        `,\n\n        \"myfile.txt\": \"stuff\",\n\n        \"app/routes/pathless-layout-parent.tsx\": js`\n          import { Form, Outlet, useActionData } from \"react-router\"\n\n          export async function action({ request }) {\n            return { submitted: true };\n          }\n          export default function () {\n            let data = useActionData();\n            return (\n              <>\n                <Form method=\"post\">\n                  <h1>Pathless Layout Parent</h1>\n                  <button type=\"submit\">Submit</button>\n                </Form>\n                <Outlet />\n                <p>{data?.submitted === true ? 'Submitted - Yes' : 'Submitted - No'}</p>\n              </>\n            );\n          }\n        `,\n\n        \"app/routes/pathless-layout-parent._pathless.nested.tsx\": js`\n          import { Outlet } from \"react-router\";\n\n          export default function () {\n            return (\n              <>\n                <h2>Pathless Layout</h2>\n                <Outlet />\n              </>\n            );\n          }\n        `,\n\n        \"app/routes/pathless-layout-parent._pathless.nested._index.tsx\": js`\n          export default function () {\n            return <h3>Pathless Layout Index</h3>\n          }\n        `,\n      },\n    });\n\n    appFixture = await createAppFixture(fixture);\n  });\n\n  test.afterAll(() => {\n    appFixture.close();\n  });\n\n  test.describe(\"without JavaScript\", () => {\n    test.use({ javaScriptEnabled: false });\n\n    runFormTests();\n  });\n\n  test.describe(\"with JavaScript\", () => {\n    test.use({ javaScriptEnabled: true }); // explicitly set so we don't have to check against undefined\n\n    runFormTests();\n  });\n\n  function runFormTests() {\n    test(\"posts to a loader\", async ({ page }) => {\n      let app = new PlaywrightFixture(appFixture, page);\n      // this indirectly tests that clicking SVG children in buttons works\n      await app.goto(\"/get-submission\");\n      await app.clickSubmitButton(\"/get-submission\", { wait: true });\n      await page.waitForSelector(`pre:has-text(\"${CHEESESTEAK}\")`);\n    });\n\n    test(\"posts to a loader with an <input name='action' />\", async ({\n      page,\n    }) => {\n      let app = new PlaywrightFixture(appFixture, page);\n      await app.goto(\"/get-submission\");\n      await page.locator(`#${FORM_WITH_ACTION_INPUT} button`).click();\n      await page.locator(`pre:has-text(\"${EAT}\")`).waitFor();\n    });\n\n    test(\"posts to a loader with button data with click\", async ({ page }) => {\n      let app = new PlaywrightFixture(appFixture, page);\n      await app.goto(\"/get-submission\");\n      await page.locator(\"#buttonWithValue\").click();\n      await page.locator(`pre:has-text(\"${LAKSA}\")`).waitFor();\n    });\n\n    test(\"posts to a loader with button data with keyboard\", async ({\n      page,\n    }) => {\n      let app = new PlaywrightFixture(appFixture, page);\n      await app.goto(\"/get-submission\");\n      await page.focus(`#${KEYBOARD_INPUT}`);\n      await app.waitForNetworkAfter(async () => {\n        await page.keyboard.press(\"Enter\");\n        // there can be a delay before the request gets kicked off (worse with JS disabled)\n        await new Promise((resolve) => setTimeout(resolve, 50));\n      });\n      await page.waitForSelector(`pre:has-text(\"${LAKSA}\")`);\n    });\n\n    test(\"posts with the correct checkbox data\", async ({ page }) => {\n      let app = new PlaywrightFixture(appFixture, page);\n      await app.goto(\"/get-submission\");\n      await page.locator(`#${CHECKBOX_BUTTON}`).click();\n      await page.locator(`pre:has-text(\"${LAKSA}\")`).waitFor();\n      await page.locator(`pre:has-text(\"${CHEESESTEAK}\")`).waitFor();\n    });\n\n    test(\"posts button data from outside the form\", async ({ page }) => {\n      let app = new PlaywrightFixture(appFixture, page);\n      await app.goto(\"/get-submission\");\n      await page.locator(`#${ORPHAN_BUTTON}`).click();\n      await page.locator(`pre:has-text(\"${SQUID_INK_HOTDOG}\")`).waitFor();\n    });\n\n    test(\n      \"when clicking on a submit button as a descendant of an element that \" +\n        \"stops propagation on click, still passes the clicked submit button's \" +\n        \"`name` and `value` props to the request payload\",\n      async ({ page }) => {\n        let app = new PlaywrightFixture(appFixture, page);\n        await app.goto(\"/stop-propagation\");\n        await app.clickSubmitButton(\"/stop-propagation\", { wait: true });\n        await page.waitForSelector(\"#action-data\");\n        expect(await app.getHtml()).toMatch('{\"intent\":\"add\"}');\n      },\n    );\n\n    test.describe(\"<Form> action\", () => {\n      test.describe(\"in a static route\", () => {\n        test(\"no action resolves relative to the closest route\", async ({\n          page,\n        }) => {\n          let app = new PlaywrightFixture(appFixture, page);\n          await app.goto(\"/inbox\");\n          let html = await app.getHtml();\n          let el = getElement(html, `#${STATIC_ROUTE_NO_ACTION}`);\n          expect(el.attr(\"action\")).toBe(\"/inbox\");\n        });\n\n        test(\"no action resolves to URL including search params\", async ({\n          page,\n        }) => {\n          let app = new PlaywrightFixture(appFixture, page);\n          await app.goto(\"/inbox?foo=bar\");\n          let html = await app.getHtml();\n          let el = getElement(html, `#${STATIC_ROUTE_NO_ACTION}`);\n          expect(el.attr(\"action\")).toBe(\"/inbox?foo=bar\");\n        });\n\n        test(\"absolute action resolves relative to the root route\", async ({\n          page,\n        }) => {\n          let app = new PlaywrightFixture(appFixture, page);\n          await app.goto(\"/inbox\");\n          let html = await app.getHtml();\n          let el = getElement(html, `#${STATIC_ROUTE_ABSOLUTE_ACTION}`);\n          expect(el.attr(\"action\")).toBe(\"/about\");\n        });\n\n        test(\"'.' action resolves relative to the closest route\", async ({\n          page,\n        }) => {\n          let app = new PlaywrightFixture(appFixture, page);\n          await app.goto(\"/inbox\");\n          let html = await app.getHtml();\n          let el = getElement(html, `#${STATIC_ROUTE_CURRENT_ACTION}`);\n          expect(el.attr(\"action\")).toBe(\"/inbox\");\n        });\n\n        test(\"'.' excludes search params\", async ({ page }) => {\n          let app = new PlaywrightFixture(appFixture, page);\n          await app.goto(\"/inbox?foo=bar\");\n          let html = await app.getHtml();\n          let el = getElement(html, `#${STATIC_ROUTE_CURRENT_ACTION}`);\n          expect(el.attr(\"action\")).toBe(\"/inbox\");\n        });\n\n        test(\"'..' action resolves relative to the parent route\", async ({\n          page,\n        }) => {\n          let app = new PlaywrightFixture(appFixture, page);\n          await app.goto(\"/inbox\");\n          let html = await app.getHtml();\n          let el = getElement(html, `#${STATIC_ROUTE_PARENT_ACTION}`);\n          expect(el.attr(\"action\")).toBe(\"/\");\n        });\n\n        test(\"'..' action with more .. segments than parent routes resolves relative to the root route\", async ({\n          page,\n        }) => {\n          let app = new PlaywrightFixture(appFixture, page);\n          await app.goto(\"/inbox\");\n          let html = await app.getHtml();\n          let el = getElement(html, `#${STATIC_ROUTE_TOO_MANY_DOTS_ACTION}`);\n          expect(el.attr(\"action\")).toBe(\"/about\");\n        });\n      });\n\n      test.describe(\"in a dynamic route\", () => {\n        test(\"no action resolves relative to the closest route\", async ({\n          page,\n        }) => {\n          let app = new PlaywrightFixture(appFixture, page);\n          await app.goto(\"/blog/abc\");\n          let html = await app.getHtml();\n          let el = getElement(html, `#${DYNAMIC_ROUTE_NO_ACTION}`);\n          expect(el.attr(\"action\")).toBe(\"/blog/abc\");\n        });\n\n        test(\"no action resolves to URL including search params\", async ({\n          page,\n        }) => {\n          let app = new PlaywrightFixture(appFixture, page);\n          await app.goto(\"/blog/abc?foo=bar\");\n          let html = await app.getHtml();\n          let el = getElement(html, `#${DYNAMIC_ROUTE_NO_ACTION}`);\n          expect(el.attr(\"action\")).toBe(\"/blog/abc?foo=bar\");\n        });\n\n        test(\"absolute action resolves relative to the root route\", async ({\n          page,\n        }) => {\n          let app = new PlaywrightFixture(appFixture, page);\n          await app.goto(\"/blog/abc\");\n          let html = await app.getHtml();\n          let el = getElement(html, `#${DYNAMIC_ROUTE_ABSOLUTE_ACTION}`);\n          expect(el.attr(\"action\")).toBe(\"/about\");\n        });\n\n        test(\"'.' action resolves relative to the closest route\", async ({\n          page,\n        }) => {\n          let app = new PlaywrightFixture(appFixture, page);\n          await app.goto(\"/blog/abc\");\n          let html = await app.getHtml();\n          let el = getElement(html, `#${DYNAMIC_ROUTE_CURRENT_ACTION}`);\n          expect(el.attr(\"action\")).toBe(\"/blog/abc\");\n        });\n\n        test(\"'.' excludes search params\", async ({ page }) => {\n          let app = new PlaywrightFixture(appFixture, page);\n          await app.goto(\"/blog/abc?foo=bar\");\n          let html = await app.getHtml();\n          let el = getElement(html, `#${DYNAMIC_ROUTE_CURRENT_ACTION}`);\n          expect(el.attr(\"action\")).toBe(\"/blog/abc\");\n        });\n\n        test(\"'..' action resolves relative to the parent route\", async ({\n          page,\n        }) => {\n          let app = new PlaywrightFixture(appFixture, page);\n          await app.goto(\"/blog/abc\");\n          let html = await app.getHtml();\n          let el = getElement(html, `#${DYNAMIC_ROUTE_PARENT_ACTION}`);\n          expect(el.attr(\"action\")).toBe(\"/blog\");\n        });\n\n        test(\"'..' action with more .. segments than parent routes resolves relative to the root route\", async ({\n          page,\n        }) => {\n          let app = new PlaywrightFixture(appFixture, page);\n          await app.goto(\"/blog/abc\");\n          let html = await app.getHtml();\n          let el = getElement(html, `#${DYNAMIC_ROUTE_TOO_MANY_DOTS_ACTION}`);\n          expect(el.attr(\"action\")).toBe(\"/about\");\n        });\n      });\n\n      test.describe(\"in an index route\", () => {\n        test(\"no action resolves relative to the closest route\", async ({\n          page,\n        }) => {\n          let app = new PlaywrightFixture(appFixture, page);\n          await app.goto(\"/blog\");\n          let html = await app.getHtml();\n          let el = getElement(html, `#${INDEX_ROUTE_NO_ACTION}`);\n          expect(el.attr(\"action\")).toBe(\"/blog?index\");\n        });\n\n        test(\"no action resolves to URL including search params\", async ({\n          page,\n        }) => {\n          let app = new PlaywrightFixture(appFixture, page);\n          await app.goto(\"/blog?foo=bar\");\n          let html = await app.getHtml();\n          let el = getElement(html, `#${INDEX_ROUTE_NO_ACTION}`);\n          expect(el.attr(\"action\")).toBe(\"/blog?index&foo=bar\");\n        });\n\n        test(\"absolute action resolves relative to the root route\", async ({\n          page,\n        }) => {\n          let app = new PlaywrightFixture(appFixture, page);\n          await app.goto(\"/blog\");\n          let html = await app.getHtml();\n          let el = getElement(html, `#${INDEX_ROUTE_ABSOLUTE_ACTION}`);\n          expect(el.attr(\"action\")).toBe(\"/about\");\n        });\n\n        test(\"'.' action resolves relative to the closest route\", async ({\n          page,\n        }) => {\n          let app = new PlaywrightFixture(appFixture, page);\n          await app.goto(\"/blog\");\n          let html = await app.getHtml();\n          let el = getElement(html, `#${INDEX_ROUTE_CURRENT_ACTION}`);\n          expect(el.attr(\"action\")).toBe(\"/blog?index\");\n        });\n\n        test(\"'.' excludes search params\", async ({ page }) => {\n          let app = new PlaywrightFixture(appFixture, page);\n          await app.goto(\"/blog?foo=bar\");\n          let html = await app.getHtml();\n          let el = getElement(html, `#${INDEX_ROUTE_CURRENT_ACTION}`);\n          expect(el.attr(\"action\")).toBe(\"/blog?index\");\n        });\n\n        test(\"'..' action resolves relative to the parent route\", async ({\n          page,\n        }) => {\n          let app = new PlaywrightFixture(appFixture, page);\n          await app.goto(\"/blog\");\n          let html = await app.getHtml();\n          let el = getElement(html, `#${INDEX_ROUTE_PARENT_ACTION}`);\n          expect(el.attr(\"action\")).toBe(\"/\");\n        });\n\n        test(\"'..' action with more .. segments than parent routes resolves relative to the root route\", async ({\n          page,\n        }) => {\n          let app = new PlaywrightFixture(appFixture, page);\n          await app.goto(\"/blog\");\n          let html = await app.getHtml();\n          let el = getElement(html, `#${INDEX_ROUTE_TOO_MANY_DOTS_ACTION}`);\n          expect(el.attr(\"action\")).toBe(\"/about\");\n        });\n\n        test(\"handles search params correctly on GET submissions\", async ({\n          page,\n        }) => {\n          let app = new PlaywrightFixture(appFixture, page);\n\n          const timestamp = page.locator(`#timestamp`);\n          const form = page.locator(`#${INDEX_ROUTE_NO_ACTION}`);\n          const submit = page.locator(`#${INDEX_ROUTE_NO_ACTION} button`);\n\n          // Start with a query param\n          await app.goto(\"/blog?junk=1\");\n          const t0 = await timestamp.innerText();\n          await expect(form).toHaveAttribute(\"action\", \"/blog?index&junk=1\");\n          expect(page.url()).toMatch(/\\/blog\\?junk=1$/);\n\n          // On submission, we replace existing parameters (reflected in the\n          // form action) with the values from the form data.  We also do not\n          // need to preserve the index param in the URL on GET submissions\n          await submit.click();\n          const t1 = await timestamp.filter({ hasNotText: t0 }).innerText();\n          await expect(form).toHaveAttribute(\"action\", \"/blog?index&foo=1\");\n          expect(page.url()).toMatch(/\\/blog\\?foo=1$/);\n\n          // Does not append duplicate params on re-submissions\n          await submit.click();\n          await timestamp.filter({ hasNotText: t1 }).innerText();\n          await expect(form).toHaveAttribute(\"action\", \"/blog?index&foo=1\");\n          expect(page.url()).toMatch(/\\/blog\\?foo=1$/);\n        });\n\n        test(\"handles search params correctly on POST submissions\", async ({\n          page,\n        }) => {\n          let app = new PlaywrightFixture(appFixture, page);\n\n          const timestamp = page.locator(`#timestamp`);\n          const form = page.locator(`#${INDEX_ROUTE_NO_ACTION_POST}`);\n          const submit = page.locator(`#${INDEX_ROUTE_NO_ACTION_POST} button`);\n\n          // Start with a query param\n          await app.goto(\"/blog?junk=1\");\n          const t0 = await timestamp.innerText();\n          await expect(form).toHaveAttribute(\"action\", \"/blog?index&junk=1\");\n          expect(page.url()).toMatch(/\\/blog\\?junk=1$/);\n\n          // Form action reflects the current params and change them on submission\n          await submit.click();\n          await timestamp.filter({ hasNotText: t0 }).innerText();\n          await expect(form).toHaveAttribute(\"action\", \"/blog?index&junk=1\");\n\n          await page.waitForURL(/\\/blog\\?index&junk=1$/);\n          expect(page.url()).toMatch(/\\/blog\\?index&junk=1$/);\n        });\n      });\n\n      test.describe(\"in a layout route\", () => {\n        test(\"no action resolves relative to the closest route\", async ({\n          page,\n        }) => {\n          let app = new PlaywrightFixture(appFixture, page);\n          await app.goto(\"/blog\");\n          let html = await app.getHtml();\n          let el = getElement(html, `#${LAYOUT_ROUTE_NO_ACTION}`);\n          expect(el.attr(\"action\")).toBe(\"/blog\");\n        });\n\n        test(\"no action resolves to URL including search params\", async ({\n          page,\n        }) => {\n          let app = new PlaywrightFixture(appFixture, page);\n          await app.goto(\"/blog?foo=bar\");\n          let html = await app.getHtml();\n          let el = getElement(html, `#${LAYOUT_ROUTE_NO_ACTION}`);\n          expect(el.attr(\"action\")).toBe(\"/blog?foo=bar\");\n        });\n\n        test(\"absolute action resolves relative to the root route\", async ({\n          page,\n        }) => {\n          let app = new PlaywrightFixture(appFixture, page);\n          await app.goto(\"/blog\");\n          let html = await app.getHtml();\n          let el = getElement(html, `#${LAYOUT_ROUTE_ABSOLUTE_ACTION}`);\n          expect(el.attr(\"action\")).toBe(\"/about\");\n        });\n\n        test(\"'.' action resolves relative to the closest route\", async ({\n          page,\n        }) => {\n          let app = new PlaywrightFixture(appFixture, page);\n          await app.goto(\"/blog\");\n          let html = await app.getHtml();\n          let el = getElement(html, `#${LAYOUT_ROUTE_CURRENT_ACTION}`);\n          expect(el.attr(\"action\")).toBe(\"/blog\");\n        });\n\n        test(\"'.' excludes search params\", async ({ page }) => {\n          let app = new PlaywrightFixture(appFixture, page);\n          await app.goto(\"/blog?foo=bar\");\n          let html = await app.getHtml();\n          let el = getElement(html, `#${LAYOUT_ROUTE_CURRENT_ACTION}`);\n          expect(el.attr(\"action\")).toBe(\"/blog\");\n        });\n\n        test(\"'..' action resolves relative to the parent route\", async ({\n          page,\n        }) => {\n          let app = new PlaywrightFixture(appFixture, page);\n          await app.goto(\"/blog\");\n          let html = await app.getHtml();\n          let el = getElement(html, `#${LAYOUT_ROUTE_PARENT_ACTION}`);\n          expect(el.attr(\"action\")).toBe(\"/\");\n        });\n\n        test(\"'..' action with more .. segments than parent routes resolves relative to the root route\", async ({\n          page,\n        }) => {\n          let app = new PlaywrightFixture(appFixture, page);\n          await app.goto(\"/blog\");\n          let html = await app.getHtml();\n          let el = getElement(html, `#${LAYOUT_ROUTE_TOO_MANY_DOTS_ACTION}`);\n          expect(el.attr(\"action\")).toBe(\"/about\");\n        });\n      });\n\n      test.describe(\"in a splat route\", () => {\n        test(\"no action resolves relative to the closest route\", async ({\n          page,\n        }) => {\n          let app = new PlaywrightFixture(appFixture, page);\n          await app.goto(\"/projects/blarg\");\n          let html = await app.getHtml();\n          let el = getElement(html, `#${SPLAT_ROUTE_NO_ACTION}`);\n          expect(el.attr(\"action\")).toBe(\"/projects/blarg\");\n        });\n\n        test(\"no action resolves to URL including search params\", async ({\n          page,\n        }) => {\n          let app = new PlaywrightFixture(appFixture, page);\n          await app.goto(\"/projects/blarg?foo=bar\");\n          let html = await app.getHtml();\n          let el = getElement(html, `#${SPLAT_ROUTE_NO_ACTION}`);\n          expect(el.attr(\"action\")).toBe(\"/projects/blarg?foo=bar\");\n        });\n\n        test(\"absolute action resolves relative to the root route\", async ({\n          page,\n        }) => {\n          let app = new PlaywrightFixture(appFixture, page);\n          await app.goto(\"/projects/blarg\");\n          let html = await app.getHtml();\n          let el = getElement(html, `#${SPLAT_ROUTE_ABSOLUTE_ACTION}`);\n          expect(el.attr(\"action\")).toBe(\"/about\");\n        });\n\n        test(\"'.' action resolves relative to the closest route\", async ({\n          page,\n        }) => {\n          let app = new PlaywrightFixture(appFixture, page);\n          await app.goto(\"/projects/blarg\");\n          let html = await app.getHtml();\n          let el = getElement(html, `#${SPLAT_ROUTE_CURRENT_ACTION}`);\n          expect(el.attr(\"action\")).toBe(\"/projects/blarg\");\n        });\n\n        test(\"'.' excludes search params\", async ({ page }) => {\n          let app = new PlaywrightFixture(appFixture, page);\n          await app.goto(\"/projects/blarg?foo=bar\");\n          let html = await app.getHtml();\n          let el = getElement(html, `#${SPLAT_ROUTE_CURRENT_ACTION}`);\n          expect(el.attr(\"action\")).toBe(\"/projects/blarg\");\n        });\n\n        test(\"'..' action resolves relative to the parent route\", async ({\n          page,\n        }) => {\n          let app = new PlaywrightFixture(appFixture, page);\n          await app.goto(\"/projects/blarg\");\n          let html = await app.getHtml();\n          let el = getElement(html, `#${SPLAT_ROUTE_PARENT_ACTION}`);\n          expect(el.attr(\"action\")).toBe(\"/projects\");\n        });\n\n        test(\"'..' action with more .. segments than parent routes resolves relative to the root route\", async ({\n          page,\n        }) => {\n          let app = new PlaywrightFixture(appFixture, page);\n          await app.goto(\"/projects/blarg\");\n          let html = await app.getHtml();\n          let el = getElement(html, `#${SPLAT_ROUTE_TOO_MANY_DOTS_ACTION}`);\n          expect(el.attr(\"action\")).toBe(\"/about\");\n        });\n      });\n    });\n\n    let FORM_METHODS = [\"GET\", \"POST\", \"PUT\", \"PATCH\", \"DELETE\"];\n    let NATIVE_FORM_METHODS = [\"GET\", \"POST\"];\n\n    test.describe(\"uses the Form `method` attribute\", () => {\n      FORM_METHODS.forEach((method) => {\n        test(`submits with ${method}`, async ({ page, javaScriptEnabled }) => {\n          test.fail(\n            !javaScriptEnabled && !NATIVE_FORM_METHODS.includes(method),\n            `Native <form> doesn't support method ${method} #4420`,\n          );\n\n          let app = new PlaywrightFixture(appFixture, page);\n          await app.goto(`/form-method?method=${method}`, true);\n          await page.getByText(\"Submit\", { exact: true }).click();\n          if (method !== \"GET\") {\n            await expect(page.locator(\"#action-method\")).toHaveText(method);\n          }\n          await expect(page.locator(\"#loader-method\")).toHaveText(\"GET\");\n        });\n      });\n    });\n\n    test.describe(\"overrides the Form `method` attribute with the submitter's `formMethod` attribute\", () => {\n      // NOTE: HTMLButtonElement only supports get/post as formMethod, which is why we don't test put/patch/delete\n      NATIVE_FORM_METHODS.forEach((overrideMethod) => {\n        // ensure the form's method is different from the submitter's\n        let method = overrideMethod === \"GET\" ? \"POST\" : \"GET\";\n        test(`submits with ${overrideMethod} instead of ${method}`, async ({\n          page,\n        }) => {\n          let app = new PlaywrightFixture(appFixture, page);\n          await app.goto(\n            `/form-method?method=${method}&submitterFormMethod=${overrideMethod}`,\n            true,\n          );\n          await page.locator(`text=Submit with ${overrideMethod}`).click();\n          if (overrideMethod !== \"GET\") {\n            await expect(page.locator(\"pre#action-method\")).toHaveText(\n              overrideMethod,\n            );\n          }\n          await expect(page.locator(\"pre#loader-method\")).toHaveText(\"GET\");\n        });\n      });\n    });\n\n    test(\"submits the submitter's value(s) in tree order in the form data\", async ({\n      page,\n    }) => {\n      let app = new PlaywrightFixture(appFixture, page);\n\n      const formData = page.locator(\"#formData\");\n\n      await app.goto(\"/submitter\");\n      await page.locator(\"text=Add Task\").click();\n      await expect(formData).toHaveValue(\n        \"tasks=first&tasks=second&tasks=&tasks=last\",\n      );\n\n      await app.goto(\"/submitter\");\n      await page.locator(\"text=No Name\").click();\n      await expect(formData).toHaveValue(\"tasks=first&tasks=second&tasks=last\");\n\n      await app.goto(\"/submitter\");\n      await page.locator(\"[alt='Add Task']\").click();\n      await expect(formData).toHaveValue(\n        /^tasks=first&tasks=second&tasks.x=\\d+&tasks.y=\\d+&tasks=last$/,\n      );\n\n      await app.goto(\"/submitter\");\n      await page.locator(\"[alt='No Name']\").click();\n      await expect(formData).toHaveValue(\n        /^tasks=first&tasks=second&x=\\d+&y=\\d+&tasks=last$/,\n      );\n\n      await app.goto(\"/submitter\");\n      await page.locator(\"text=Outside\").click();\n      await expect(formData).toHaveValue(\n        \"tasks=outside&tasks=first&tasks=second&tasks=last\",\n      );\n    });\n\n    test(\"sends file names when submitting via url encoding\", async ({\n      page,\n    }) => {\n      let app = new PlaywrightFixture(appFixture, page);\n      let myFile = fixture.projectDir + \"/myfile.txt\";\n\n      const formData = page.locator(\"#formData\");\n      const submit = page.locator(\"button\");\n\n      await app.goto(\"/file-upload\");\n      await app.uploadFile(`[name=filey]`, myFile);\n      await app.uploadFile(`[name=filey2]`, myFile, myFile);\n      await submit.click();\n      await expect(formData).toHaveValue(\n        \"filey=myfile.txt&filey2=myfile.txt&filey2=myfile.txt&filey3=\",\n      );\n\n      await app.goto(\"/file-upload?method=post\");\n      await app.uploadFile(`[name=filey]`, myFile);\n      await app.uploadFile(`[name=filey2]`, myFile, myFile);\n      await submit.click();\n\n      await expect(formData).toHaveValue(\n        \"filey=myfile.txt&filey2=myfile.txt&filey2=myfile.txt&filey3=\",\n      );\n    });\n\n    test(\"empty file inputs resolve to File objects on the server\", async ({\n      page,\n    }) => {\n      let app = new PlaywrightFixture(appFixture, page);\n\n      await app.goto(\"/empty-file-upload\");\n      await app.clickSubmitButton(\"/empty-file-upload\");\n      await page.waitForSelector(\"#action-data\");\n      expect((await app.getElement(\"#action-data\")).text()).toContain(\n        '{\"text\":\"\",\"file\":{\"name\":\"\",\"size\":0},\"fileMultiple\":[{\"name\":\"\",\"size\":0}]}',\n      );\n    });\n\n    test(\"pathless layout routes are ignored in form actions\", async ({\n      page,\n    }) => {\n      let app = new PlaywrightFixture(appFixture, page);\n      await app.goto(\"/pathless-layout-parent/nested\");\n\n      await expect(\n        page.getByText(\"Pathless Layout Parent\", { exact: true }),\n      ).toBeVisible();\n      await expect(\n        page.getByText(\"Pathless Layout\", { exact: true }),\n      ).toBeVisible();\n      await expect(\n        page.getByText(\"Pathless Layout Index\", { exact: true }),\n      ).toBeVisible();\n\n      const form = page.locator(\"form\");\n      await expect(form).toHaveAttribute(\"action\", \"/pathless-layout-parent\");\n\n      await expect(page.getByText(\"Submitted - No\")).toBeVisible();\n      // This submission should ignore the index route and the pathless layout\n      // route above it and hit the action in routes/pathless-layout-parent.jsx\n      await page.getByRole(\"button\").click();\n      await expect(page.getByText(\"Submitted - Yes\")).toBeVisible();\n    });\n  }\n});\n"
  },
  {
    "path": "integration/fs-routes-test.ts",
    "content": "import { PassThrough } from \"node:stream\";\nimport { test, expect } from \"@playwright/test\";\n\nimport { PlaywrightFixture } from \"./helpers/playwright-fixture.js\";\nimport type { Fixture, AppFixture } from \"./helpers/create-fixture.js\";\nimport { createFixtureProject } from \"./helpers/create-fixture.js\";\nimport {\n  createAppFixture,\n  createFixture,\n  js,\n} from \"./helpers/create-fixture.js\";\n\nlet fixture: Fixture;\nlet appFixture: AppFixture;\nlet originalConsoleError: typeof console.error;\nlet originalConsoleWarn: typeof console.warn;\n\ntest.describe(\"fs-routes\", () => {\n  test.beforeAll(async () => {\n    fixture = await createFixture({\n      files: {\n        \"app/routes.ts\": js`\n          import { type RouteConfig } from \"@react-router/dev/routes\";  \n          import { flatRoutes } from \"@react-router/fs-routes\";\n          import { remixRoutesOptionAdapter } from \"@react-router/remix-routes-option-adapter\";\n\n          export default [\n            ...await flatRoutes({\n              ignoredRouteFiles: [\"**/ignored-route.*\"],\n            }),\n\n            // Ensure Remix back compat layer works\n            ...await remixRoutesOptionAdapter(async (defineRoutes) => {\n              // Ensure async routes work\n              return defineRoutes((route) => {\n                route(\"/remix/config/route\", \"remix-config-route.tsx\")\n              });\n            })\n          ] satisfies RouteConfig;\n        `,\n        \"app/root.tsx\": js`\n          import { Links, Meta, Outlet, Scripts } from \"react-router\";\n\n          export default function Root() {\n            return (\n              <html lang=\"en\">\n                <head>\n                  <Meta />\n                  <Links />\n                </head>\n                <body>\n                  <div id=\"content\">\n                    <h1>Root</h1>\n                    <Outlet />\n                  </div>\n                  <Scripts />\n                </body>\n              </html>\n            );\n          }\n        `,\n\n        \"app/routes/_index.tsx\": js`\n          export default function () {\n            return <h2>Index</h2>;\n          }\n        `,\n\n        \"app/routes/folder/route.tsx\": js`\n          export default function () {\n            return <h2>Folder (Route.jsx)</h2>;\n          }\n        `,\n\n        \"app/routes/folder2/index.tsx\": js`\n          export default function () {\n            return <h2>Folder (Index.jsx)</h2>;\n          }\n        `,\n\n        \"app/routes/flat.file.tsx\": js`\n          export default function () {\n            return <h2>Flat File</h2>;\n          }\n        `,\n\n        \"app/remix-config-route.tsx\": js`\n          export default function () {\n            return <h2>Remix Config Route</h2>;\n          }\n        `,\n\n        \"app/routes/.dotfile\": `\n          DOTFILE SHOULD BE IGNORED\n        `,\n\n        \"app/routes/.route-with-unescaped-leading-dot.tsx\": js`\n          throw new Error(\"This file should be ignored as a route\");\n        `,\n\n        \"app/routes/[.]route-with-escaped-leading-dot.tsx\": js`\n          export default function () {\n            return <h2>Route With Escaped Leading Dot</h2>;\n          }\n        `,\n\n        \"app/routes/dashboard/route.tsx\": js`\n          import { Outlet } from \"react-router\";\n\n          export default function () {\n            return (\n              <>\n                <h2>Dashboard Layout</h2>\n                <Outlet />\n              </>\n            )\n          }\n        `,\n\n        \"app/routes/dashboard._index/route.tsx\": js`\n          export default function () {\n            return <h3>Dashboard Index</h3>;\n          }\n        `,\n\n        [`app/routes/ignored-route.jsx`]: js`\n          export default function () {\n            return <h2>i should 404</h2>;\n          }\n        `,\n      },\n    });\n\n    appFixture = await createAppFixture(fixture);\n    originalConsoleError = console.error;\n    console.error = () => {};\n    originalConsoleWarn = console.warn;\n    console.warn = () => {};\n  });\n\n  test.afterAll(() => {\n    appFixture.close();\n    console.error = originalConsoleError;\n    console.warn = originalConsoleWarn;\n  });\n\n  test.describe(\"without JavaScript\", () => {\n    test.use({ javaScriptEnabled: false });\n    runTests();\n  });\n\n  test.describe(\"with JavaScript\", () => {\n    test.use({ javaScriptEnabled: true });\n    runTests();\n  });\n\n  function runTests() {\n    test(\"renders matching routes (index)\", async ({ page }) => {\n      let app = new PlaywrightFixture(appFixture, page);\n      await app.goto(\"/\");\n      expect(await app.getHtml(\"#content\")).toBe(`<div id=\"content\">\n  <h1>Root</h1>\n  <h2>Index</h2>\n</div>`);\n    });\n\n    test(\"renders matching routes (folder route.jsx)\", async ({ page }) => {\n      let app = new PlaywrightFixture(appFixture, page);\n      await app.goto(\"/folder\");\n      expect(await app.getHtml(\"#content\")).toBe(`<div id=\"content\">\n  <h1>Root</h1>\n  <h2>Folder (Route.jsx)</h2>\n</div>`);\n    });\n\n    test(\"renders matching routes (folder index.jsx)\", async ({ page }) => {\n      let app = new PlaywrightFixture(appFixture, page);\n      await app.goto(\"/folder2\");\n      expect(await app.getHtml(\"#content\")).toBe(`<div id=\"content\">\n  <h1>Root</h1>\n  <h2>Folder (Index.jsx)</h2>\n</div>`);\n    });\n\n    test(\"renders matching routes (flat file)\", async ({ page }) => {\n      let app = new PlaywrightFixture(appFixture, page);\n      await app.goto(\"/flat/file\");\n      expect(await app.getHtml(\"#content\")).toBe(`<div id=\"content\">\n  <h1>Root</h1>\n  <h2>Flat File</h2>\n</div>`);\n    });\n\n    test(\"renders matching routes (Remix config route)\", async ({ page }) => {\n      let app = new PlaywrightFixture(appFixture, page);\n      await app.goto(\"/remix/config/route\");\n      expect(await app.getHtml(\"#content\")).toBe(`<div id=\"content\">\n  <h1>Root</h1>\n  <h2>Remix Config Route</h2>\n</div>`);\n    });\n\n    test(\"renders matching routes (route with escaped leading dot)\", async ({\n      page,\n    }) => {\n      let app = new PlaywrightFixture(appFixture, page);\n      await app.goto(\"/.route-with-escaped-leading-dot\");\n      expect(await app.getHtml(\"#content\")).toBe(`<div id=\"content\">\n  <h1>Root</h1>\n  <h2>Route With Escaped Leading Dot</h2>\n</div>`);\n    });\n\n    test(\"renders matching routes (nested)\", async ({ page }) => {\n      let app = new PlaywrightFixture(appFixture, page);\n      await app.goto(\"/dashboard\");\n      expect(await app.getHtml(\"#content\")).toBe(`<div id=\"content\">\n  <h1>Root</h1>\n  <h2>Dashboard Layout</h2>\n  <h3>Dashboard Index</h3>\n</div>`);\n    });\n  }\n\n  test(\"allows ignoredRouteFiles to be configured\", async () => {\n    let response = await fixture.requestDocument(\"/ignored-route\");\n\n    expect(response.status).toBe(404);\n  });\n});\n\ntest.describe(\"emits warnings for route conflicts\", async () => {\n  let buildStdio = new PassThrough();\n  let buildOutput: string;\n\n  let originalConsoleLog = console.log;\n  let originalConsoleWarn = console.warn;\n  let originalConsoleError = console.error;\n\n  test.beforeAll(async () => {\n    console.log = () => {};\n    console.warn = () => {};\n    console.error = () => {};\n    await createFixtureProject({\n      buildStdio,\n      files: {\n        \"routes/_dashboard._index.tsx\": js`\n          export default function () {\n            return <p>routes/_dashboard._index</p>;\n          }\n        `,\n        \"app/routes/_index.tsx\": js`\n          export default function () {\n            return <p>routes._index</p>;\n          }\n        `,\n        \"app/routes/_landing._index.tsx\": js`\n          export default function () {\n            return <p>routes/_landing._index</p>;\n          }\n        `,\n      },\n    });\n\n    let chunks: Buffer[] = [];\n    buildOutput = await new Promise<string>((resolve, reject) => {\n      buildStdio.on(\"data\", (chunk) => chunks.push(Buffer.from(chunk)));\n      buildStdio.on(\"error\", (err) => reject(err));\n      buildStdio.on(\"end\", () =>\n        resolve(Buffer.concat(chunks).toString(\"utf8\")),\n      );\n    });\n  });\n\n  test.afterAll(() => {\n    console.log = originalConsoleLog;\n    console.warn = originalConsoleWarn;\n    console.error = originalConsoleError;\n  });\n\n  test(\"warns about conflicting routes\", () => {\n    console.log(buildOutput);\n    expect(buildOutput).toContain(`⚠️ Route Path Collision: \"/\"`);\n  });\n});\n\ntest.describe(\"\", () => {\n  let buildStdio = new PassThrough();\n  let buildOutput: string;\n\n  let originalConsoleLog = console.log;\n  let originalConsoleWarn = console.warn;\n  let originalConsoleError = console.error;\n\n  test.beforeAll(async () => {\n    console.log = () => {};\n    console.warn = () => {};\n    console.error = () => {};\n    await createFixtureProject({\n      buildStdio,\n      files: {\n        \"app/routes/_index/route.tsx\": js``,\n        \"app/routes/_index/utils.ts\": js``,\n      },\n    });\n\n    let chunks: Buffer[] = [];\n    buildOutput = await new Promise<string>((resolve, reject) => {\n      buildStdio.on(\"data\", (chunk) => chunks.push(Buffer.from(chunk)));\n      buildStdio.on(\"error\", (err) => reject(err));\n      buildStdio.on(\"end\", () =>\n        resolve(Buffer.concat(chunks).toString(\"utf8\")),\n      );\n    });\n  });\n\n  test.afterAll(() => {\n    console.log = originalConsoleLog;\n    console.warn = originalConsoleWarn;\n    console.error = originalConsoleError;\n  });\n\n  test(\"doesn't emit a warning for nested index files with co-located files\", () => {\n    expect(buildOutput).not.toContain(`Route Path Collision`);\n  });\n});\n\ntest.describe(\"pathless routes and route collisions\", () => {\n  test.beforeAll(async () => {\n    fixture = await createFixture({\n      files: {\n        \"app/root.tsx\": js`\n          import { Link, Outlet, Scripts, useMatches } from \"react-router\";\n\n          export default function App() {\n            let matches = 'Number of matches: ' + useMatches().length;\n            return (\n              <html lang=\"en\">\n                <body>\n                  <nav>\n                    <Link to=\"/nested\">/nested</Link>\n                    <br />\n                    <Link to=\"/nested/foo\">/nested/foo</Link>\n                    <br />\n                  </nav>\n                  <p>{matches}</p>\n                  <Outlet />\n                  <Scripts />\n                </body>\n              </html>\n            );\n          }\n        `,\n        \"app/routes/nested._index.tsx\": js`\n          export default function Index() {\n            return <h1>Index</h1>;\n          }\n        `,\n        \"app/routes/nested._pathless.tsx\": js`\n          import { Outlet } from \"react-router\";\n\n          export default function Layout() {\n            return (\n              <>\n                <div>Pathless Layout</div>\n                <Outlet />\n              </>\n            );\n          }\n        `,\n        \"app/routes/nested._pathless.foo.tsx\": js`\n          export default function Foo() {\n            return <h1>Foo</h1>;\n          }\n        `,\n        \"app/routes/nested._pathless2.tsx\": js`\n          import { Outlet } from \"react-router\";\n\n          export default function Layout() {\n            return (\n              <>\n                <div>Pathless 2 Layout</div>\n                <Outlet />\n              </>\n            );\n          }\n        `,\n        \"app/routes/nested._pathless2.bar.tsx\": js`\n          export default function Bar() {\n            return <h1>Bar</h1>;\n          }\n        `,\n      },\n    });\n\n    appFixture = await createAppFixture(fixture);\n  });\n\n  test.afterAll(async () => appFixture.close());\n\n  test.describe(\"with JavaScript\", () => {\n    runTests();\n  });\n\n  test.describe(\"without JavaScript\", () => {\n    test.use({ javaScriptEnabled: false });\n    runTests();\n  });\n\n  /**\n   * Routes for this test look like this, for reference for the matches assertions:\n   *\n   * <Routes>\n   *   <Route file=\"root.jsx\">\n   *     <Route path=\"nested\" file=\"routes/nested._pathless.jsx\">\n   *       <Route path=\"foo\" file=\"routes/nested._pathless/foo.jsx\" />\n   *     </Route>\n   *     <Route path=\"nested\" index file=\"routes/nested._index.jsx\" />\n   *     <Route index file=\"routes/_index.jsx\" />\n   *   </Route>\n   * </Routes>\n   */\n\n  function runTests() {\n    test(\"displays index page and not pathless layout page\", async ({\n      page,\n    }) => {\n      let app = new PlaywrightFixture(appFixture, page);\n      await app.goto(\"/nested\");\n      expect(await app.getHtml()).toMatch(\"Index\");\n      expect(await app.getHtml()).not.toMatch(\"Pathless Layout\");\n      expect(await app.getHtml()).toMatch(\"Number of matches: 2\");\n    });\n\n    test(\"displays page inside of pathless layout\", async ({ page }) => {\n      let app = new PlaywrightFixture(appFixture, page);\n      await app.goto(\"/nested/foo\");\n      expect(await app.getHtml()).not.toMatch(\"Index\");\n      expect(await app.getHtml()).toMatch(\"Pathless Layout\");\n      expect(await app.getHtml()).toMatch(\"Foo\");\n      expect(await app.getHtml()).toMatch(\"Number of matches: 3\");\n    });\n\n    // This also asserts that we support multiple sibling pathless route layouts\n    test(\"displays page inside of second pathless layout\", async ({ page }) => {\n      let app = new PlaywrightFixture(appFixture, page);\n      await app.goto(\"/nested/bar\");\n      expect(await app.getHtml()).not.toMatch(\"Index\");\n      expect(await app.getHtml()).toMatch(\"Pathless 2 Layout\");\n      expect(await app.getHtml()).toMatch(\"Bar\");\n      expect(await app.getHtml()).toMatch(\"Number of matches: 3\");\n    });\n  }\n});\n"
  },
  {
    "path": "integration/headers-test.ts",
    "content": "import { test, expect } from \"@playwright/test\";\n\nimport { UNSAFE_ServerMode as ServerMode } from \"react-router\";\nimport { createFixture, js } from \"./helpers/create-fixture.js\";\nimport type { Fixture } from \"./helpers/create-fixture.js\";\n\ntest.describe(\"headers export\", () => {\n  let ROOT_HEADER_KEY = \"X-Test\";\n  let ROOT_HEADER_VALUE = \"SUCCESS\";\n  let ACTION_HKEY = \"X-Test-Action\";\n  let ACTION_HVALUE = \"SUCCESS\";\n\n  let appFixture: Fixture;\n\n  test.beforeAll(async () => {\n    appFixture = await createFixture(\n      {\n        files: {\n          \"app/root.tsx\": js`\n            import { Links, Meta, Outlet, Scripts } from \"react-router\";\n\n            export const loader = () => ({});\n\n            export default function Root() {\n              return (\n                <html lang=\"en\">\n                  <head>\n                    <Meta />\n                    <Links />\n                  </head>\n                  <body>\n                    <Outlet />\n                    <Scripts />\n                  </body>\n                </html>\n              );\n            }\n          `,\n\n          \"app/routes/_index.tsx\": js`\n            import { data } from \"react-router\";\n\n            export function loader() {\n              return data(null, {\n                headers: {\n                  \"${ROOT_HEADER_KEY}\": \"${ROOT_HEADER_VALUE}\"\n                }\n              })\n            }\n\n            export function headers({ loaderHeaders }) {\n              return {\n                \"${ROOT_HEADER_KEY}\": loaderHeaders.get(\"${ROOT_HEADER_KEY}\")\n              }\n            }\n\n            export default function Index() {\n              return <div>Heyo!</div>\n            }\n          `,\n\n          \"app/routes/action.tsx\": js`\n            import { data } from \"react-router\";\n\n            export function action() {\n              return data(null, {\n                headers: {\n                  \"${ACTION_HKEY}\": \"${ACTION_HVALUE}\"\n                }\n              })\n            }\n\n            export function headers({ actionHeaders }) {\n              return {\n                \"${ACTION_HKEY}\": actionHeaders.get(\"${ACTION_HKEY}\")\n              }\n            }\n\n            export default function Action() { return <div/> }\n          `,\n\n          \"app/routes/parent.tsx\": js`\n            export function headers({ actionHeaders, errorHeaders, loaderHeaders, parentHeaders }) {\n              return new Headers([\n                ...(parentHeaders ? Array.from(parentHeaders.entries()) : []),\n                ...(actionHeaders ? Array.from(actionHeaders.entries()) : []),\n                ...(loaderHeaders ? Array.from(loaderHeaders.entries()) : []),\n                ...(errorHeaders ? Array.from(errorHeaders.entries()) : []),\n              ]);\n            }\n\n            export function loader({ request }) {\n              if (new URL(request.url).searchParams.get('throw') === \"parent\") {\n                throw new Response(null, {\n                  status: 400,\n                  headers: { 'X-Parent-Loader': 'error' },\n                })\n              }\n              return new Response(null, {\n                headers: { 'X-Parent-Loader': 'success' },\n              })\n            }\n\n            export async function action({ request }) {\n              let fd = await request.formData();\n              if (fd.get('throw') === \"parent\") {\n                throw new Response(null, {\n                  status: 400,\n                  headers: { 'X-Parent-Action': 'error' },\n                })\n              }\n              return new Response(null, {\n                headers: { 'X-Parent-Action': 'success' },\n              })\n            }\n\n            export default function Component() { return <div/> }\n\n            export function ErrorBoundary() {\n              return <h1>Error!</h1>\n            }\n          `,\n\n          \"app/routes/parent.child.tsx\": js`\n            export function loader({ request }) {\n              if (new URL(request.url).searchParams.get('throw') === \"child\") {\n                throw new Response(null, {\n                  status: 400,\n                  headers: { 'X-Child-Loader': 'error' },\n                })\n              }\n              return null\n            }\n\n            export async function action({ request }) {\n              let fd = await request.formData();\n              if (fd.get('throw') === \"child\") {\n                throw new Response(null, {\n                  status: 400,\n                  headers: { 'X-Child-Action': 'error' },\n                })\n              }\n              return null\n            }\n\n            export default function Component() { return <div/> }\n          `,\n\n          \"app/routes/parent.child.grandchild.tsx\": js`\n            export function loader({ request }) {\n              throw new Response(null, {\n                status: 400,\n                headers: { 'X-Child-Grandchild': 'error' },\n              })\n            }\n\n            export default function Component() { return <div/> }\n          `,\n\n          \"app/routes/cookie.tsx\": js`\n            import { data, Outlet } from \"react-router\";\n\n            export function loader({ request }) {\n              if (new URL(request.url).searchParams.has(\"parent-throw\")) {\n                throw data(null, { headers: { \"Set-Cookie\": \"parent-thrown-cookie=true\" } });\n              }\n              return null\n            };\n\n            export default function Parent() {\n              return <Outlet />;\n            }\n\n            export function ErrorBoundary() {\n              return <h1>Caught!</h1>;\n            }\n          `,\n\n          \"app/routes/cookie.child.tsx\": js`\n            import { data } from \"react-router\";\n\n            export function loader({ request }) {\n              if (new URL(request.url).searchParams.has(\"throw\")) {\n                throw data(null, { headers: { \"Set-Cookie\": \"thrown-cookie=true\" } });\n              }\n              return data(null, {\n                headers: { \"Set-Cookie\": \"normal-cookie=true\" },\n              });\n            };\n\n            export default function Child() {\n              return <p>Child</p>;\n            }\n          `,\n        },\n      },\n      ServerMode.Test,\n    );\n  });\n\n  test(\"can use `action` headers\", async () => {\n    let response = await appFixture.postDocument(\n      \"/action\",\n      new URLSearchParams(),\n    );\n    expect(response.headers.get(ACTION_HKEY)).toBe(ACTION_HVALUE);\n  });\n\n  test(\"can use the loader headers when all routes have loaders\", async () => {\n    let response = await appFixture.requestDocument(\"/\");\n    expect(response.headers.get(ROOT_HEADER_KEY)).toBe(ROOT_HEADER_VALUE);\n  });\n\n  test(\"can use the loader headers when parents don't have loaders\", async () => {\n    let HEADER_KEY = \"X-Test\";\n    let HEADER_VALUE = \"SUCCESS\";\n\n    let fixture = await createFixture(\n      {\n        files: {\n          \"app/root.tsx\": js`\n            import { Links, Meta, Outlet, Scripts } from \"react-router\";\n\n            export default function Root() {\n              return (\n                <html lang=\"en\">\n                  <head>\n                    <Meta />\n                    <Links />\n                  </head>\n                  <body>\n                    <Outlet />\n                    <Scripts />\n                  </body>\n                </html>\n              );\n            }\n          `,\n\n          \"app/routes/_index.tsx\": js`\n            import { data } from \"react-router\";\n\n            export function loader() {\n              return data(null, {\n                headers: {\n                  \"${HEADER_KEY}\": \"${HEADER_VALUE}\"\n                }\n              })\n            }\n\n            export function headers({ loaderHeaders }) {\n              return {\n                \"${HEADER_KEY}\": loaderHeaders.get(\"${HEADER_KEY}\")\n              }\n            }\n\n            export default function Index() {\n              return <div>Heyo!</div>\n            }\n          `,\n        },\n      },\n      ServerMode.Test,\n    );\n    let response = await fixture.requestDocument(\"/\");\n    expect(response.headers.get(HEADER_KEY)).toBe(HEADER_VALUE);\n  });\n\n  test(\"returns headers from successful /parent GET requests\", async () => {\n    let response = await appFixture.requestDocument(\"/parent\");\n    expect(JSON.stringify(Array.from(response.headers.entries()))).toBe(\n      JSON.stringify([\n        [\"content-type\", \"text/html\"],\n        [\"x-parent-loader\", \"success\"],\n      ]),\n    );\n  });\n\n  test(\"returns headers from successful /parent/child GET requests\", async () => {\n    let response = await appFixture.requestDocument(\"/parent/child\");\n    expect(JSON.stringify(Array.from(response.headers.entries()))).toBe(\n      JSON.stringify([\n        [\"content-type\", \"text/html\"],\n        [\"x-parent-loader\", \"success\"],\n      ]),\n    );\n  });\n\n  test(\"returns headers from successful /parent POST requests\", async () => {\n    let response = await appFixture.postDocument(\n      \"/parent\",\n      new URLSearchParams(),\n    );\n    expect(JSON.stringify(Array.from(response.headers.entries()))).toBe(\n      JSON.stringify([\n        [\"content-type\", \"text/html\"],\n        [\"x-parent-action\", \"success\"],\n        [\"x-parent-loader\", \"success\"],\n      ]),\n    );\n  });\n\n  test(\"returns headers from successful /parent/child POST requests\", async () => {\n    let response = await appFixture.postDocument(\n      \"/parent/child\",\n      new URLSearchParams(),\n    );\n    expect(JSON.stringify(Array.from(response.headers.entries()))).toBe(\n      JSON.stringify([\n        [\"content-type\", \"text/html\"],\n        [\"x-parent-loader\", \"success\"],\n      ]),\n    );\n  });\n\n  test(\"returns headers from failed /parent GET requests\", async () => {\n    let response = await appFixture.requestDocument(\"/parent?throw=parent\");\n    expect(JSON.stringify(Array.from(response.headers.entries()))).toBe(\n      JSON.stringify([\n        [\"content-type\", \"text/html\"],\n        [\"x-parent-loader\", \"error, error\"], // Shows up in loaderHeaders and errorHeaders\n      ]),\n    );\n  });\n\n  test(\"returns bubbled headers from failed /parent/child GET requests\", async () => {\n    let response = await appFixture.requestDocument(\n      \"/parent/child?throw=child\",\n    );\n    expect(JSON.stringify(Array.from(response.headers.entries()))).toBe(\n      JSON.stringify([\n        [\"content-type\", \"text/html\"],\n        [\"x-child-loader\", \"error\"],\n        [\"x-parent-loader\", \"success\"],\n      ]),\n    );\n  });\n\n  test(\"ignores headers from successful non-rendered loaders\", async () => {\n    let response = await appFixture.requestDocument(\n      \"/parent/child?throw=parent\",\n    );\n    expect(JSON.stringify(Array.from(response.headers.entries()))).toBe(\n      JSON.stringify([\n        [\"content-type\", \"text/html\"],\n        [\"x-parent-loader\", \"error, error\"], // Shows up in loaderHeaders and errorHeaders\n      ]),\n    );\n  });\n\n  test(\"chooses higher thrown errors when multiple loaders throw\", async () => {\n    let response = await appFixture.requestDocument(\n      \"/parent/child/grandchild?throw=child\",\n    );\n    expect(JSON.stringify(Array.from(response.headers.entries()))).toBe(\n      JSON.stringify([\n        [\"content-type\", \"text/html\"],\n        [\"x-child-loader\", \"error\"],\n        [\"x-parent-loader\", \"success\"],\n      ]),\n    );\n  });\n\n  test(\"returns headers from failed /parent POST requests\", async () => {\n    let response = await appFixture.postDocument(\n      \"/parent?throw=parent\",\n      new URLSearchParams(\"throw=parent\"),\n    );\n    expect(JSON.stringify(Array.from(response.headers.entries()))).toBe(\n      JSON.stringify([\n        [\"content-type\", \"text/html\"],\n        [\"x-parent-action\", \"error, error\"], // Shows up in actionHeaders and errorHeaders\n      ]),\n    );\n  });\n\n  test(\"returns bubbled headers from failed /parent/child POST requests\", async () => {\n    let response = await appFixture.postDocument(\n      \"/parent/child\",\n      new URLSearchParams(\"throw=child\"),\n    );\n    expect(JSON.stringify(Array.from(response.headers.entries()))).toBe(\n      JSON.stringify([\n        [\"content-type\", \"text/html\"],\n        [\"x-child-action\", \"error\"],\n      ]),\n    );\n  });\n\n  test(\"automatically includes cookie headers from normal responses\", async () => {\n    let response = await appFixture.requestDocument(\"/cookie/child\");\n    expect(JSON.stringify(Array.from(response.headers.entries()))).toBe(\n      JSON.stringify([\n        [\"content-type\", \"text/html\"],\n        [\"set-cookie\", \"normal-cookie=true\"],\n      ]),\n    );\n  });\n\n  test(\"automatically includes cookie headers from thrown responses\", async () => {\n    let response = await appFixture.requestDocument(\"/cookie/child?throw\");\n    expect(JSON.stringify(Array.from(response.headers.entries()))).toBe(\n      JSON.stringify([\n        [\"content-type\", \"text/html\"],\n        [\"set-cookie\", \"thrown-cookie=true\"],\n      ]),\n    );\n  });\n\n  test(\"does not duplicate thrown cookie headers from boundary route\", async () => {\n    let response = await appFixture.requestDocument(\"/cookie?parent-throw\");\n    expect(JSON.stringify(Array.from(response.headers.entries()))).toBe(\n      JSON.stringify([\n        [\"content-type\", \"text/html\"],\n        [\"set-cookie\", \"parent-thrown-cookie=true\"],\n      ]),\n    );\n  });\n\n  test(\"does not duplicate set-cookie headers also returned via headers() function\", async () => {\n    let fixture = await createFixture(\n      {\n        files: {\n          \"app/root.tsx\": js`\n            import { Links, Meta, Outlet, Scripts } from \"react-router\";\n\n            export default function Root() {\n              return (\n                <html lang=\"en\">\n                  <head>\n                    <Meta />\n                    <Links />\n                  </head>\n                  <body>\n                    <Outlet />\n                    <Scripts />\n                  </body>\n                </html>\n              );\n            }\n          `,\n\n          \"app/routes/_index.tsx\": js`\n            export function headers({ loaderHeaders }) {\n              return loaderHeaders;\n            }\n\n            export function loader() {\n              return new Response(null, {\n                headers: {\n                  \"X-Test\": \"value\",\n                  \"Set-Cookie\": \"cookie=yum\"\n                }\n              })\n            }\n\n            export default function Index() {\n              return <div>Heyo!</div>\n            }\n          `,\n        },\n      },\n      ServerMode.Test,\n    );\n    let response = await fixture.requestDocument(\"/\");\n    expect([...response.headers.entries()]).toEqual([\n      [\"content-type\", \"text/html\"],\n      [\"set-cookie\", \"cookie=yum\"],\n      [\"x-test\", \"value\"],\n    ]);\n  });\n});\n"
  },
  {
    "path": "integration/helpers/cleanup.mjs",
    "content": "import * as path from \"node:path\";\nimport spawn from \"cross-spawn\";\n\nif (process.env.CI) {\n  console.log(\"Skipping cleanup in CI\");\n  process.exit();\n}\n\nconst pathsToRemove = [path.resolve(process.cwd(), \".tmp/integration\")];\n\nfor (let pathToRemove of pathsToRemove) {\n  console.log(`Removing ${path.relative(process.cwd(), pathToRemove)}`);\n  let childProcess;\n  if (process.platform === \"win32\") {\n    childProcess = spawn(\"rmdir\", [\"/s\", \"/q\", pathToRemove], {\n      stdio: \"inherit\",\n    });\n  } else {\n    childProcess = spawn(\"rm\", [\"-rf\", pathToRemove], {\n      stdio: \"inherit\",\n    });\n  }\n  childProcess.on(\"error\", (err) => {\n    console.error(err);\n    process.exit(1);\n  });\n}\n"
  },
  {
    "path": "integration/helpers/cloudflare-dev-proxy-template/.gitignore",
    "content": "node_modules\n\n/build\n.env\n.react-router\n"
  },
  {
    "path": "integration/helpers/cloudflare-dev-proxy-template/app/entry.server.tsx",
    "content": "import type { AppLoadContext, EntryContext } from \"react-router\";\nimport { ServerRouter } from \"react-router\";\nimport { isbot } from \"isbot\";\nimport { renderToReadableStream } from \"react-dom/server\";\n\nexport default async function handleRequest(\n  request: Request,\n  responseStatusCode: number,\n  responseHeaders: Headers,\n  routerContext: EntryContext,\n  loadContext: AppLoadContext,\n) {\n  const body = await renderToReadableStream(\n    <ServerRouter context={routerContext} url={request.url} />,\n    {\n      signal: request.signal,\n      onError(error: unknown) {\n        // Log streaming rendering errors from inside the shell\n        console.error(error);\n        responseStatusCode = 500;\n      },\n    },\n  );\n\n  const userAgent = request.headers.get(\"user-agent\");\n  if (userAgent && isbot(userAgent)) {\n    await body.allReady;\n  }\n\n  responseHeaders.set(\"Content-Type\", \"text/html\");\n  return new Response(body, {\n    headers: responseHeaders,\n    status: responseStatusCode,\n  });\n}\n"
  },
  {
    "path": "integration/helpers/cloudflare-dev-proxy-template/app/root.tsx",
    "content": "import { Links, Meta, Outlet, Scripts, ScrollRestoration } from \"react-router\";\n\nexport function Layout({ children }: { children: React.ReactNode }) {\n  return (\n    <html lang=\"en\">\n      <head>\n        <meta charSet=\"utf-8\" />\n        <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />\n        <Meta />\n        <Links />\n      </head>\n      <body>\n        {children}\n        <ScrollRestoration />\n        <Scripts />\n      </body>\n    </html>\n  );\n}\n\nexport default function App() {\n  return <Outlet />;\n}\n"
  },
  {
    "path": "integration/helpers/cloudflare-dev-proxy-template/app/routes/_index.tsx",
    "content": "import type { MetaFunction } from \"react-router\";\n\nexport const meta: MetaFunction = () => {\n  return [\n    { title: \"New React Router App\" },\n    { name: \"description\", content: \"React Router + Cloudflare\" },\n  ];\n};\n\nexport default function Index() {\n  return (\n    <div style={{ fontFamily: \"system-ui, sans-serif\", lineHeight: \"1.8\" }}>\n      <h1>Welcome to React Router + Cloudflare</h1>\n    </div>\n  );\n}\n"
  },
  {
    "path": "integration/helpers/cloudflare-dev-proxy-template/app/routes.ts",
    "content": "import { type RouteConfig } from \"@react-router/dev/routes\";\nimport { flatRoutes } from \"@react-router/fs-routes\";\n\nexport default flatRoutes() satisfies RouteConfig;\n"
  },
  {
    "path": "integration/helpers/cloudflare-dev-proxy-template/package.json",
    "content": "{\n  \"name\": \"integration-cloudflare-dev-proxy-template\",\n  \"version\": \"0.0.0\",\n  \"private\": true,\n  \"sideEffects\": false,\n  \"type\": \"module\",\n  \"scripts\": {\n    \"dev\": \"react-router dev\",\n    \"build\": \"react-router build\",\n    \"start\": \"wrangler pages dev ./build/client\",\n    \"typecheck\": \"tsc\"\n  },\n  \"dependencies\": {\n    \"@react-router/cloudflare\": \"workspace:*\",\n    \"isbot\": \"^4.1.0\",\n    \"miniflare\": \"^3.20250214.0\",\n    \"react\": \"catalog:\",\n    \"react-dom\": \"catalog:\",\n    \"react-router\": \"workspace:*\"\n  },\n  \"devDependencies\": {\n    \"@cloudflare/workers-types\": \"^4.20250803.0\",\n    \"@react-router/dev\": \"workspace:*\",\n    \"@react-router/fs-routes\": \"workspace:*\",\n    \"@react-router/remix-routes-option-adapter\": \"workspace:*\",\n    \"@types/react\": \"catalog:\",\n    \"@types/react-dom\": \"catalog:\",\n    \"typescript\": \"catalog:\",\n    \"vite\": \"^6.3.0\",\n    \"wrangler\": \"^4.23.0\"\n  },\n  \"engines\": {\n    \"node\": \">=20.0.0\"\n  }\n}\n"
  },
  {
    "path": "integration/helpers/cloudflare-dev-proxy-template/tsconfig.json",
    "content": "{\n  \"include\": [\"env.d.ts\", \"**/*.ts\", \"**/*.tsx\", \".react-router/types/**/*\"],\n  \"compilerOptions\": {\n    \"lib\": [\"DOM\", \"DOM.Iterable\", \"ES2022\"],\n    \"types\": [\"@cloudflare/workers-types\", \"vite/client\"],\n    \"verbatimModuleSyntax\": true,\n    \"esModuleInterop\": true,\n    \"jsx\": \"react-jsx\",\n    \"module\": \"ESNext\",\n    \"moduleResolution\": \"Bundler\",\n    \"resolveJsonModule\": true,\n    \"target\": \"ES2022\",\n    \"strict\": true,\n    \"allowJs\": true,\n    \"skipLibCheck\": true,\n    \"baseUrl\": \".\",\n    \"noEmit\": true\n  }\n}\n"
  },
  {
    "path": "integration/helpers/cloudflare-dev-proxy-template/vite.config.ts",
    "content": "import { reactRouter } from \"@react-router/dev/vite\";\nimport { cloudflareDevProxy } from \"@react-router/dev/vite/cloudflare\";\nimport { defineConfig } from \"vite\";\n\nexport default defineConfig({\n  plugins: [cloudflareDevProxy(), reactRouter()],\n});\n"
  },
  {
    "path": "integration/helpers/create-fixture.ts",
    "content": "import { existsSync, readFileSync } from \"node:fs\";\nimport { cp, mkdir, readFile, writeFile } from \"node:fs/promises\";\nimport path from \"node:path\";\nimport type { Writable } from \"node:stream\";\nimport { Readable } from \"node:stream\";\nimport url from \"node:url\";\nimport express from \"express\";\nimport getPort from \"get-port\";\nimport stripIndent from \"strip-indent\";\nimport { sync as spawnSync, spawn } from \"cross-spawn\";\nimport type { JsonObject } from \"type-fest\";\n\nimport {\n  type ServerBuild,\n  createRequestHandler,\n  UNSAFE_ServerMode as ServerMode,\n  UNSAFE_decodeViaTurboStream as decodeViaTurboStream,\n} from \"react-router\";\nimport { createRequestHandler as createExpressHandler } from \"@react-router/express\";\nimport { createReadableStreamFromReadable } from \"@react-router/node\";\n\nimport { type TemplateName, viteConfig, reactRouterConfig } from \"./vite.js\";\n\nconst __dirname = url.fileURLToPath(new URL(\".\", import.meta.url));\nconst root = path.join(__dirname, \"../..\");\nconst TMP_DIR = path.join(root, \".tmp\", \"integration\");\n\nexport async function spawnTestServer({\n  command,\n  regex,\n  validate,\n  env = {},\n  cwd,\n  timeout = 20000,\n}: {\n  command: string[];\n  regex: RegExp;\n  validate?: (matches: RegExpMatchArray) => void | Promise<void>;\n  env?: Record<string, string>;\n  cwd?: string;\n  timeout?: number;\n}): Promise<{ stop: VoidFunction }> {\n  return new Promise((accept, reject) => {\n    let serverProcess = spawn(command[0], command.slice(1), {\n      env: { ...process.env, ...env },\n      cwd,\n      stdio: \"pipe\",\n    });\n\n    let started = false;\n    let stdout = \"\";\n    let rejectTimeout = setTimeout(() => {\n      reject(new Error(`Timed out waiting for server to start (${timeout}ms)`));\n    }, timeout);\n\n    serverProcess.stderr.pipe(process.stderr);\n    serverProcess.stdout.on(\"data\", (chunk: Buffer) => {\n      if (started) return;\n      let newChunk = chunk.toString();\n      stdout += newChunk;\n      let match = stdout.match(regex);\n      if (match) {\n        clearTimeout(rejectTimeout);\n        started = true;\n\n        Promise.resolve(validate?.(match))\n          .then(() => {\n            accept({\n              stop: () => {\n                serverProcess.kill();\n              },\n            });\n          })\n          .catch((error: unknown) => {\n            reject(error);\n          });\n      }\n    });\n\n    serverProcess.on(\"error\", (error: unknown) => {\n      clearTimeout(rejectTimeout);\n      reject(error);\n    });\n  });\n}\n\nexport interface FixtureInit {\n  buildStdio?: Writable;\n  files?: { [filename: string]: string };\n  useReactRouterServe?: boolean;\n  spaMode?: boolean;\n  prerender?: boolean;\n  port?: number;\n  templateName?: TemplateName;\n}\n\nexport type Fixture = Awaited<ReturnType<typeof createFixture>>;\nexport type AppFixture = Awaited<ReturnType<typeof createAppFixture>>;\n\nexport const js = String.raw;\nexport const mdx = String.raw;\nexport const css = String.raw;\nexport function json(value: JsonObject) {\n  return JSON.stringify(value, null, 2);\n}\n\nconst defaultTemplateName = \"vite-5-template\" satisfies TemplateName;\n\nexport async function createFixture(init: FixtureInit, mode?: ServerMode) {\n  let templateName = init.templateName ?? defaultTemplateName;\n  let projectDir = await createFixtureProject(init, mode);\n  let buildPath = url.pathToFileURL(\n    path.join(projectDir, \"build/server/index.js\"),\n  ).href;\n\n  let getBrowserAsset = async (asset: string) => {\n    return readFile(\n      path.join(projectDir, \"public\", asset.replace(/^\\//, \"\")),\n      \"utf8\",\n    );\n  };\n\n  if (init.spaMode) {\n    return {\n      projectDir,\n      build: null,\n      isSpaMode: init.spaMode,\n      prerender: init.prerender,\n      requestDocument() {\n        let html = readFileSync(\n          path.join(projectDir, \"build/client/index.html\"),\n        );\n        return new Response(html, {\n          headers: {\n            \"Content-Type\": \"text/html\",\n          },\n        });\n      },\n      requestResource() {\n        throw new Error(\"Cannot requestResource in SPA Mode tests\");\n      },\n      requestSingleFetchData: () => {\n        throw new Error(\"Cannot requestSingleFetchData in SPA Mode tests\");\n      },\n      postDocument: () => {\n        throw new Error(\"Cannot postDocument in SPA Mode tests\");\n      },\n      getBrowserAsset,\n      useReactRouterServe: init.useReactRouterServe,\n    };\n  }\n\n  if (init.prerender) {\n    return {\n      projectDir,\n      build: null,\n      isSpaMode: init.spaMode,\n      prerender: init.prerender,\n      requestDocument(href: string) {\n        let file = new URL(href, \"test://test\").pathname + \"/index.html\";\n        let clientDir = path.join(projectDir, \"build\", \"client\");\n        let mainPath = path.join(clientDir, file);\n        let fallbackPath = path.join(clientDir, \"__spa-fallback.html\");\n        let fallbackPath2 = path.join(clientDir, \"index.html\");\n        let html = existsSync(mainPath)\n          ? readFileSync(mainPath)\n          : existsSync(fallbackPath)\n            ? readFileSync(fallbackPath)\n            : readFileSync(fallbackPath2);\n        return new Response(html, {\n          headers: {\n            \"Content-Type\": \"text/html\",\n          },\n        });\n      },\n      requestResource(href: string) {\n        let data = readFileSync(path.join(projectDir, \"build/client\", href));\n        return new Response(data);\n      },\n      async requestSingleFetchData(href: string) {\n        let data = readFileSync(path.join(projectDir, \"build/client\", href));\n        let stream = createReadableStreamFromReadable(Readable.from(data));\n        return {\n          status: 200,\n          statusText: \"OK\",\n          headers: new Headers(),\n          data: (await decodeViaTurboStream(stream, global)).value,\n        };\n      },\n      postDocument: () => {\n        throw new Error(\"Cannot postDocument in Prerender tests\");\n      },\n      getBrowserAsset,\n      useReactRouterServe: init.useReactRouterServe,\n    };\n  }\n\n  let build: ServerBuild | null = null;\n  type RequestHandler = (request: Request) => Promise<Response>;\n  let handler: RequestHandler;\n  if (templateName === \"rsc-vite-framework\") {\n    handler = (await import(buildPath))?.default?.fetch;\n    if (typeof handler !== \"function\") {\n      throw new Error(\n        \"Expected a default request handler function export in Vite RSC Framework Mode server build\",\n      );\n    }\n  } else {\n    build = (await import(buildPath)) as ServerBuild;\n    handler = createRequestHandler(build, mode || ServerMode.Production);\n  }\n\n  let requestDocument = async (href: string, init?: RequestInit) => {\n    let url = new URL(href, \"test://test\");\n    let request = new Request(url.toString(), {\n      ...init,\n      signal: init?.signal || new AbortController().signal,\n    });\n    return handler(request);\n  };\n\n  let requestResource = async (href: string, init?: RequestInit) => {\n    init = init || {};\n    init.signal = init.signal || new AbortController().signal;\n    let url = new URL(href, \"test://test\");\n    let request = new Request(url.toString(), init);\n    return handler(request);\n  };\n\n  let requestSingleFetchData = async (href: string, init?: RequestInit) => {\n    init = init || {};\n    init.signal = init.signal || new AbortController().signal;\n    let url = new URL(href, \"test://test\");\n    let request = new Request(url.toString(), init);\n    let response = await handler(request);\n    return {\n      status: response.status,\n      statusText: response.statusText,\n      headers: response.headers,\n      data: response.body\n        ? (await decodeViaTurboStream(response.body!, global)).value\n        : null,\n    };\n  };\n\n  let postDocument = async (href: string, data: URLSearchParams | FormData) => {\n    return requestDocument(href, {\n      method: \"POST\",\n      body: data,\n      headers: {\n        \"Content-Type\":\n          data instanceof URLSearchParams\n            ? \"application/x-www-form-urlencoded\"\n            : \"multipart/form-data\",\n      },\n    });\n  };\n\n  return {\n    templateName,\n    projectDir,\n    build,\n    handler,\n    isSpaMode: init.spaMode,\n    prerender: init.prerender,\n    requestDocument,\n    requestResource,\n    requestSingleFetchData,\n    postDocument,\n    getBrowserAsset,\n    useReactRouterServe: init.useReactRouterServe,\n  };\n}\n\n/**\n * @deprecated Use `integration/helpers/vite.ts`'s `test` instead\n *\n * This implementation sometimes runs a request handler in memory, forcing tests to manually manage stdout/stderr\n * which has caused many integration tests to leak noisy logs for expected errors.\n * It also means that sometimes the CLI is skipped over in those tests, missing out on code paths that should be tested.\n */\nexport async function createAppFixture(fixture: Fixture, mode?: ServerMode) {\n  let startAppServer = async (): Promise<{\n    port: number;\n    stop: VoidFunction;\n  }> => {\n    if (fixture.useReactRouterServe) {\n      let port = await getPort();\n      let { stop } = await spawnTestServer({\n        cwd: fixture.projectDir,\n        command: [\n          process.argv[0],\n          \"node_modules/@react-router/serve/dist/cli.js\",\n          \"build/server/index.js\",\n        ],\n        env: {\n          NODE_ENV: mode || \"production\",\n          PORT: port.toFixed(0),\n        },\n        regex: /\\[react-router-serve\\] http:\\/\\/localhost:(\\d+)\\s/,\n        validate: (matches) => {\n          let parsedPort = parseInt(matches[1], 10);\n          if (port !== parsedPort) {\n            throw new Error(\n              `Expected react-router-serve to start on port ${port}, but it started on port ${parsedPort}`,\n            );\n          }\n        },\n      });\n      return { stop, port };\n    }\n\n    if (fixture.isSpaMode) {\n      return new Promise(async (accept) => {\n        let port = await getPort();\n        let app = express();\n        app.use(express.static(path.join(fixture.projectDir, \"build/client\")));\n        app.get(\"*\", (_, res) =>\n          res.sendFile(\n            path.join(fixture.projectDir, \"build/client/index.html\"),\n          ),\n        );\n        let server = app.listen(port);\n        accept({ stop: server.close.bind(server), port });\n      });\n    }\n\n    if (fixture.prerender) {\n      return new Promise(async (accept) => {\n        let port = await getPort();\n        let app = express();\n        app.use(\n          express.static(path.join(fixture.projectDir, \"build\", \"client\")),\n        );\n        app.get(\"*\", (req, res, next) => {\n          let dir = path.join(fixture.projectDir, \"build\", \"client\");\n          let file;\n          if (req.path.endsWith(\".data\")) {\n            file = req.path;\n          } else {\n            let mainPath = req.path + \"/index.html\";\n            let fallbackPath = \"__spa-fallback.html\";\n            let fallbackPath2 = \"index.html\";\n            file = existsSync(mainPath)\n              ? mainPath\n              : existsSync(fallbackPath)\n                ? fallbackPath\n                : fallbackPath2;\n          }\n          let filePath = path.join(dir, file);\n          if (existsSync(filePath)) {\n            res.sendFile(filePath, next);\n          } else {\n            // Avoid a built-in console error from `sendFile` on 404's\n            res.status(404).send(\"Not found\");\n          }\n        });\n        let server = app.listen(port);\n        accept({ stop: server.close.bind(server), port });\n      });\n    }\n\n    if (fixture.templateName.includes(\"rsc\")) {\n      let port = await getPort();\n      let { stop } = await spawnTestServer({\n        cwd: fixture.projectDir,\n        command: [process.argv[0], \"start.js\"],\n        env: {\n          NODE_ENV: mode || \"production\",\n          PORT: port.toFixed(0),\n        },\n        regex: /Server listening on port (\\d+)\\s/,\n        validate: (matches) => {\n          let parsedPort = parseInt(matches[1], 10);\n          if (port !== parsedPort) {\n            throw new Error(\n              `Expected RSC Framework Mode build server to start on port ${port}, but it started on port ${parsedPort}`,\n            );\n          }\n        },\n      });\n      return { stop, port };\n    }\n\n    const build = fixture.build;\n    if (!build) {\n      return Promise.reject(\n        new Error(\"Cannot start app server without a build\"),\n      );\n    }\n\n    return new Promise(async (accept) => {\n      let port = await getPort();\n      let app = express();\n      app.use(express.static(path.join(fixture.projectDir, \"build/client\")));\n\n      app.all(\n        \"*\",\n        createExpressHandler({\n          build,\n          mode: mode || ServerMode.Production,\n        }),\n      );\n\n      let server = app.listen(port);\n\n      accept({ stop: server.close.bind(server), port });\n    });\n  };\n\n  let start = async () => {\n    let { stop, port } = await startAppServer();\n\n    let serverUrl = `http://localhost:${port}`;\n\n    return {\n      serverUrl,\n      /**\n       * Shuts down the fixture app, **you need to call this\n       * at the end of a test** or `afterAll` if the fixture is initialized in a\n       * `beforeAll` block. Also make sure to `app.close()` or else you'll\n       * have memory leaks.\n       */\n      close: () => {\n        return stop();\n      },\n    };\n  };\n\n  return start();\n}\n\n////////////////////////////////////////////////////////////////////////////////\n\nexport async function createFixtureProject(\n  init: FixtureInit = {},\n  mode?: ServerMode,\n): Promise<string> {\n  let templateName = init.templateName ?? defaultTemplateName;\n  let integrationTemplateDir = path.resolve(__dirname, templateName);\n  let projectName = `rr-${templateName}-${Math.random().toString(32).slice(2)}`;\n  let projectDir = path.join(TMP_DIR, projectName);\n  let port = init.port ?? (await getPort());\n\n  await mkdir(projectDir, { recursive: true });\n  await cp(integrationTemplateDir, projectDir, { recursive: true });\n\n  let hasViteConfig = Object.keys(init.files ?? {}).some((filename) =>\n    filename.startsWith(\"vite.config.\"),\n  );\n\n  let hasReactRouterConfig = Object.keys(init.files ?? {}).some((filename) =>\n    filename.startsWith(\"react-router.config.\"),\n  );\n\n  let { spaMode } = init;\n\n  await writeTestFiles(\n    {\n      ...(hasViteConfig\n        ? {}\n        : {\n            \"vite.config.js\": await viteConfig.basic({\n              port,\n              templateName,\n            }),\n          }),\n      ...(hasReactRouterConfig\n        ? {}\n        : {\n            \"react-router.config.ts\": reactRouterConfig({\n              ssr: !spaMode,\n            }),\n          }),\n      ...init.files,\n    },\n    projectDir,\n  );\n\n  reactRouterBuild(\n    projectDir,\n    init.buildStdio,\n    mode,\n    templateName.includes(\"rsc\"),\n  );\n\n  return projectDir;\n}\n\nfunction reactRouterBuild(\n  projectDir: string,\n  buildStdio?: Writable,\n  mode?: ServerMode,\n  isRsc?: boolean,\n) {\n  // We have a \"require\" instead of a dynamic import in readConfig gated\n  // behind mode === ServerMode.Test to make jest happy, but that doesn't\n  // work for ESM configs, those MUST be dynamic imports. So we need to\n  // force the mode to be production for ESM configs when runtime mode is\n  // tested.\n  mode = mode === ServerMode.Test ? ServerMode.Production : mode;\n\n  let reactRouterBin = \"node_modules/@react-router/dev/dist/cli/index.js\";\n  let viteBin = \"node_modules/vite/dist/node/cli.js\";\n\n  let buildArgs: string[] = [isRsc ? viteBin : reactRouterBin, \"build\"];\n\n  let buildSpawn = spawnSync(\"node\", buildArgs, {\n    cwd: projectDir,\n    env: {\n      ...process.env,\n      NODE_ENV: mode || ServerMode.Production,\n      // Ensure build can pass in Rolldown. This can be removed once\n      // \"preserveEntrySignatures\" is supported in rolldown-vite.\n      ROLLDOWN_OPTIONS_VALIDATION: \"loose\",\n    },\n  });\n\n  // These logs are helpful for debugging. Remove comments if needed.\n  // console.log(\"spawning node \" + buildArgs.join(\" \") + \":\\n\");\n  // console.log(\"  STDOUT:\");\n  // console.log(\"  \" + buildSpawn.stdout.toString(\"utf-8\"));\n  // console.log(\"  STDERR:\");\n  // console.log(\"  \" + buildSpawn.stderr.toString(\"utf-8\"));\n\n  if (buildStdio) {\n    buildStdio.write(buildSpawn.stdout.toString(\"utf-8\"));\n    buildStdio.write(buildSpawn.stderr.toString(\"utf-8\"));\n    buildStdio.end();\n  }\n\n  if (buildSpawn.error || buildSpawn.status) {\n    console.error(buildSpawn.stderr.toString(\"utf-8\"));\n    throw buildSpawn.error || new Error(`Build failed, check the output above`);\n  }\n}\n\nasync function writeTestFiles(\n  files: Record<string, string> | undefined,\n  dir: string,\n) {\n  await Promise.all(\n    Object.keys(files ?? {}).map(async (filename) => {\n      let filePath = path.join(dir, filename);\n      await mkdir(path.dirname(filePath), { recursive: true });\n      let file = files![filename];\n\n      await writeFile(filePath, stripIndent(file));\n    }),\n  );\n}\n"
  },
  {
    "path": "integration/helpers/express.ts",
    "content": "import tsx from \"dedent\";\n\nexport function server() {\n  return tsx`\n    import { createRequestHandler } from \"@react-router/express\";\n    import express from \"express\";\n\n    const port = process.env.PORT ?? 3000\n    const hmrPort = process.env.HMR_PORT ?? 3001\n\n    const app = express();\n\n    const getLoadContext = () => ({});\n\n    if (process.env.NODE_ENV === \"production\") {\n      app.use(\n        \"/assets\",\n        express.static(\"build/client/assets\", { immutable: true, maxAge: \"1y\" })\n      );\n      app.use(express.static(\"build/client\", { maxAge: \"1h\" }));\n      app.all(\"*\", createRequestHandler({\n        build: await import(\"./build/index.js\"),\n        getLoadContext,\n      }));\n    } else {\n      const viteDevServer = await import(\"vite\").then(\n        (vite) => vite.createServer({\n          server: {\n            middlewareMode: true,\n            hmr: { port: hmrPort },\n          },\n        })\n      );\n      app.use(viteDevServer.middlewares);\n      app.all(\"*\", createRequestHandler({\n        build:() => viteDevServer.ssrLoadModule(\"virtual:react-router/server-build\"),\n        getLoadContext,\n      }));\n    }\n\n    app.listen(port, () => console.log('http://localhost:' + port));\n  `;\n}\n\nexport function rsc() {\n  return tsx`\n    import { createRequestListener } from \"@mjackson/node-fetch-server\";\n    import express from \"express\";\n\n    const port = process.env.PORT ?? 3000\n    const hmrPort = process.env.HMR_PORT ?? 3001\n\n    const app = express();\n\n    if (process.env.NODE_ENV === \"production\") {\n      app.use(\n        \"/assets\",\n        express.static(\"build/client/assets\", { immutable: true, maxAge: \"1y\" })\n      );\n      app.all(\"*\", createRequestListener((await import(\"./build/server/index.js\")).default.fetch));\n    } else {\n      const viteDevServer = await import(\"vite\").then(\n        (vite) => vite.createServer({\n          server: {\n            middlewareMode: true,\n            hmr: { port: hmrPort },\n          },\n        })\n      );\n      app.use(viteDevServer.middlewares);\n    }\n\n    app.listen(port, () => console.log('http://localhost:' + port));\n  `;\n}\n"
  },
  {
    "path": "integration/helpers/fixtures.ts",
    "content": "import { ChildProcess } from \"node:child_process\";\nimport * as fs from \"node:fs/promises\";\nimport { fileURLToPath } from \"node:url\";\n\nimport { test as base } from \"@playwright/test\";\nimport {\n  execa,\n  ExecaError,\n  type Options,\n  parseCommandString,\n  type ResultPromise,\n} from \"execa\";\nimport * as Path from \"pathe\";\n\nimport type { TemplateName } from \"./vite.js\";\n\ndeclare module \"@playwright/test\" {\n  interface Page {\n    errors: Error[];\n  }\n}\n\nconst __filename = fileURLToPath(import.meta.url);\nconst ROOT = Path.join(__filename, \"../../..\");\nconst TMP = Path.join(ROOT, \".tmp/integration\");\nconst templatePath = (templateName: string) =>\n  Path.resolve(ROOT, \"integration/helpers\", templateName);\n\ntype Edits = Record<string, string | ((contents: string) => string)>;\n\nasync function applyEdits(cwd: string, edits: Edits) {\n  const promises = Object.entries(edits).map(async ([file, transform]) => {\n    const filepath = Path.join(cwd, file);\n    await fs.writeFile(\n      filepath,\n      typeof transform === \"function\"\n        ? transform(await fs.readFile(filepath, \"utf8\"))\n        : transform,\n      \"utf8\",\n    );\n    return;\n  });\n  await Promise.all(promises);\n}\n\nexport const test = base.extend<{\n  template: TemplateName;\n  files: Edits;\n  cwd: string;\n  edit: (edits: Edits) => Promise<void>;\n  $: (\n    command: string,\n    options?: Pick<Options, \"env\" | \"timeout\">,\n  ) => ResultPromise<{ reject: false }> & {\n    buffer: { stdout: string; stderr: string };\n  };\n}>({\n  template: [\"vite-6-template\", { option: true }],\n  files: [{}, { option: true }],\n  page: async ({ page }, use) => {\n    page.errors = [];\n    page.on(\"pageerror\", (error: Error) => page.errors.push(error));\n    await use(page);\n  },\n\n  cwd: async ({ template, files }, use, testInfo) => {\n    await fs.mkdir(TMP, { recursive: true });\n    const cwd = await fs.mkdtemp(Path.join(TMP, template + \"-\"));\n    testInfo.attach(\"cwd\", { body: cwd });\n\n    await fs.cp(templatePath(template), cwd, {\n      errorOnExist: true,\n      recursive: true,\n    });\n\n    await applyEdits(cwd, files);\n\n    await use(cwd);\n  },\n\n  edit: async ({ cwd }, use) => {\n    await use(async (edits) => applyEdits(cwd, edits));\n  },\n\n  $: async ({ cwd }, use) => {\n    const spawn = execa({\n      cwd,\n      env: {\n        NO_COLOR: \"1\",\n        FORCE_COLOR: \"0\",\n      },\n      reject: false,\n    });\n\n    let testHasEnded = false;\n    const processes: Array<ResultPromise> = [];\n    const unexpectedErrors: Array<Error> = [];\n\n    await use((command, options = {}) => {\n      const [file, ...args] = parseCommandString(command);\n\n      const p = spawn(file, args, options);\n      if (p instanceof ChildProcess) {\n        processes.push(p);\n      }\n\n      p.then((result) => {\n        if (!(result instanceof Error)) return result;\n\n        // Once the test has ended, this process will be killed as part of its teardown resulting in an ExecaError.\n        // We only care about surfacing errors that occurred during test execution, not during teardown.\n        const expectedError = testHasEnded && result instanceof ExecaError;\n        if (expectedError) return result;\n        unexpectedErrors.push(result);\n      });\n\n      const buffer = { stdout: \"\", stderr: \"\" };\n      p.stdout?.on(\"data\", (data) => (buffer.stdout += data.toString()));\n      p.stderr?.on(\"data\", (data) => (buffer.stderr += data.toString()));\n      return Object.assign(p, { buffer });\n    });\n\n    testHasEnded = true;\n    processes.forEach((p) => p.kill());\n\n    // Throw any unexpected errors that occurred during test execution\n    if (unexpectedErrors.length > 0) {\n      const errorMessage =\n        unexpectedErrors.length === 1\n          ? `Unexpected process error: ${unexpectedErrors[0].message}`\n          : `${unexpectedErrors.length} unexpected process errors:\\n${unexpectedErrors.map((e, i) => `${i + 1}. ${e.message}`).join(\"\\n\")}`;\n\n      const error = new Error(errorMessage);\n      error.stack = unexpectedErrors[0].stack;\n      throw error;\n    }\n  },\n});\n"
  },
  {
    "path": "integration/helpers/playwright-fixture.ts",
    "content": "import cp from \"node:child_process\";\nimport type { Page, Response, Request } from \"@playwright/test\";\nimport { test } from \"@playwright/test\";\nimport { load } from \"cheerio\";\nimport prettier from \"prettier\";\n\nlet cheerio = load(\"\");\n\nimport type { AppFixture } from \"./create-fixture.js\";\n\nexport class PlaywrightFixture {\n  readonly page: Page;\n  readonly app: AppFixture;\n\n  constructor(app: AppFixture, page: Page) {\n    this.page = page;\n    this.app = app;\n  }\n\n  /**\n   * Visits the href with a document request.\n   *\n   * @param href The href you want to visit\n   * @param waitForHydration Wait for the page to full load/hydrate?\n   *  - `undefined` to wait for the document `load` event\n   *  - `true` wait for the network to be idle, so everything should be loaded\n   *    and ready to go\n   *  - `false` to wait only until the initial doc to be returned and the document\n   *    to start loading (mostly useful for testing deferred responses)\n   */\n  async goto(href: string, waitForHydration?: boolean): Promise<Response> {\n    let response = await this.page.goto(this.app.serverUrl + href, {\n      waitUntil:\n        waitForHydration === true\n          ? \"networkidle\"\n          : waitForHydration === false\n            ? \"commit\"\n            : \"load\",\n    });\n    if (response == null)\n      throw new Error(\n        \"Unexpected null response, possible about:blank request or same-URL redirect\",\n      );\n    return response;\n  }\n\n  /**\n   * Finds a link on the page with a matching href, clicks it, and waits for\n   * the network to be idle before continuing.\n   *\n   * @param href The href of the link you want to click\n   * @param options `{ wait }` waits for the network to be idle before moving on\n   */\n  async clickLink(href: string, options: { wait: boolean } = { wait: true }) {\n    let selector = `a[href=\"${href}\"]`;\n    let el = await this.page.$(selector);\n    if (!el) {\n      throw new Error(`Could not find link for ${selector}`);\n    }\n    if (options.wait) {\n      await doAndWait(this.page, () => el!.click());\n    } else {\n      await el.click();\n    }\n  }\n\n  /**\n   * Find the input element and fill for file uploads.\n   *\n   * @param inputSelector The selector of the input you want to fill\n   * @param filePaths The paths to the files you want to upload\n   */\n  async uploadFile(inputSelector: string, ...filePaths: string[]) {\n    let el = await this.page.$(inputSelector);\n    if (!el) {\n      throw new Error(`Could not find input for: ${inputSelector}`);\n    }\n    await el.setInputFiles(filePaths);\n  }\n\n  /**\n   * Finds the first submit button with `formAction` that matches the\n   * `action` supplied, clicks it, and optionally waits for the network to\n   * be idle before continuing.\n   *\n   * @param action The formAction of the button you want to click\n   * @param options `{ wait }` waits for the network to be idle before moving on\n   */\n  async clickSubmitButton(\n    action: string,\n    options: { wait?: boolean; method?: string } = { wait: true },\n  ) {\n    let selector: string;\n    if (options.method) {\n      selector = `button[formAction=\"${action}\"][formMethod=\"${options.method}\"]`;\n    } else {\n      selector = `button[formAction=\"${action}\"]`;\n    }\n\n    let el = await this.page.$(selector);\n    if (!el) {\n      if (options.method) {\n        selector = `form[action=\"${action}\"] button[type=\"submit\"][formMethod=\"${options.method}\"]`;\n      } else {\n        selector = `form[action=\"${action}\"] button[type=\"submit\"]`;\n      }\n      el = await this.page.$(selector);\n      if (!el) {\n        throw new Error(`Can't find button for: ${action}`);\n      }\n    }\n    if (options.wait) {\n      await doAndWait(this.page, () => el!.click());\n    } else {\n      await el.click();\n    }\n  }\n\n  /**\n   * Clicks any element and waits for the network to be idle.\n   */\n  async clickElement(selector: string) {\n    let el = await this.page.$(selector);\n    if (!el) {\n      throw new Error(`Can't find element for: ${selector}`);\n    }\n    await doAndWait(this.page, () => el!.click());\n  }\n\n  /**\n   * Perform any interaction and wait for the network to be idle:\n   *\n   * ```ts\n   * await app.waitForNetworkAfter(page, () => app.page.focus(\"#el\"))\n   * ```\n   */\n  async waitForNetworkAfter(fn: () => Promise<unknown>) {\n    await doAndWait(this.page, fn);\n  }\n\n  /**\n   * \"Clicks\" the back button and optionally waits for the network to be\n   * idle (defaults to waiting).\n   */\n  async goBack(options: { wait: boolean } = { wait: true }) {\n    if (options.wait) {\n      await doAndWait(this.page, () => this.page.goBack());\n    } else {\n      await this.page.goBack();\n    }\n  }\n\n  /**\n   * \"Clicks\" the refresh button.\n   */\n  async reload(options: { wait: boolean } = { wait: true }) {\n    if (options.wait) {\n      await doAndWait(this.page, () => this.page.reload());\n    } else {\n      await this.page.reload();\n    }\n  }\n\n  /**\n   * Collects single fetch data responses from the network, usually after a\n   * link click or form submission. This is useful for asserting that specific\n   * loaders were called (or not).\n   */\n  collectSingleFetchResponses() {\n    return this.collectResponses(\n      (url) => url.pathname.endsWith(\".data\") || url.pathname.endsWith(\".rsc\"),\n    );\n  }\n\n  /**\n   * Collects all responses from the network, usually after a link click or\n   * form submission. A filter can be provided to only collect responses\n   * that meet a certain criteria.\n   */\n  collectResponses(filter?: (url: URL) => boolean) {\n    let responses: Response[] = [];\n\n    this.page.on(\"response\", (res) => {\n      if (!filter || filter(new URL(res.url()))) {\n        responses.push(res);\n      }\n    });\n\n    return responses;\n  }\n\n  /**\n   * Get HTML from the page. Useful for asserting something rendered that\n   * you expected.\n   *\n   * @param selector CSS Selector for the element's HTML you want\n   */\n  getHtml(selector?: string) {\n    return getHtml(this.page, selector);\n  }\n\n  /**\n   * Get a cheerio instance of an element from the page.\n   *\n   * @param selector CSS Selector for the element's HTML you want\n   */\n  async getElement(selector: string) {\n    return getElement(await getHtml(this.page), selector);\n  }\n\n  /**\n   * Keeps the fixture running for as many seconds as you want so you can go\n   * poke around in the browser to see what's up.\n   *\n   * @param seconds How long you want the app to stay open\n   */\n  async poke(seconds: number = 10, href: string = \"/\") {\n    let ms = seconds * 1000;\n    test.setTimeout(ms);\n    console.log(\n      `🙈 Poke around for ${seconds} seconds 👉 ${this.app.serverUrl}`,\n    );\n    cp.exec(`open ${this.app.serverUrl}${href}`);\n    return new Promise((res) => setTimeout(res, ms));\n  }\n}\n\nexport async function getHtml(page: Page, selector?: string) {\n  let html = await page.content();\n  let selectedHtml = selector\n    ? await selectHtml(html, selector)\n    : await prettyHtml(html);\n  return selectedHtml;\n}\n\nexport function getElement(source: string, selector: string) {\n  let el = cheerio(selector, source);\n  if (!el.length) {\n    throw new Error(`No element matches selector \"${selector}\"`);\n  }\n  return el;\n}\n\nexport async function selectHtml(source: string, selector: string) {\n  let el = getElement(source, selector);\n  let html = await prettyHtml(cheerio.html(el));\n  return html.trim();\n}\n\nexport async function prettyHtml(source: string) {\n  return prettier.format(source, { parser: \"html\" });\n}\n\nasync function doAndWait(\n  page: Page,\n  action: () => Promise<unknown>,\n  longPolls = 0,\n) {\n  let DEBUG = !!process.env.DEBUG;\n  let networkSettledCallback: any;\n  let networkSettledPromise = new Promise((resolve) => {\n    networkSettledCallback = resolve;\n  });\n\n  let requestCounter = 0;\n  let actionDone = false;\n  let pending = new Set<Request>();\n\n  let maybeSettle = () => {\n    if (actionDone && requestCounter <= longPolls) networkSettledCallback();\n  };\n\n  let onRequest = (request: Request) => {\n    ++requestCounter;\n    if (DEBUG) {\n      pending.add(request);\n      console.log(`+[${requestCounter}]: ${request.url()}`);\n    }\n  };\n  let onRequestDone = (request: Request) => {\n    // Let the page handle responses asynchronously (via setTimeout(0)).\n    //\n    // Note: this might be changed to use delay, e.g. setTimeout(f, 100),\n    // when the page uses delay itself.\n    let evaluate = page.evaluate(() => {\n      return new Promise((resolve) => setTimeout(resolve, 0));\n    });\n    evaluate\n      .catch(() => null)\n      .then(() => {\n        --requestCounter;\n        maybeSettle();\n        if (DEBUG) {\n          pending.delete(request);\n          console.log(`-[${requestCounter}]: ${request.url()}`);\n        }\n      });\n  };\n\n  page.on(\"request\", onRequest);\n  page.on(\"requestfinished\", onRequestDone);\n  page.on(\"requestfailed\", onRequestDone);\n  page.on(\"load\", networkSettledCallback); // e.g. navigation with javascript disabled\n\n  let timeoutId = DEBUG\n    ? setInterval(() => {\n        console.log(`${requestCounter} requests pending:`);\n        for (let request of pending) console.log(`  ${request.url()}`);\n      }, 5000)\n    : undefined;\n\n  let result = await action();\n  actionDone = true;\n  maybeSettle();\n  if (DEBUG) {\n    console.log(`action done, ${requestCounter} requests pending`);\n  }\n  await networkSettledPromise;\n\n  // I wish I knew why but Safari seems to get all screwed up without this.\n  // When you run doAndWait (via clicking a blink or submitting a form) and\n  // then waitForSelector().  It finds the selector element but thinks it's\n  // hidden for some unknown reason.  It's intermittent, but waiting for the\n  // next animation frame delaying slightly before the waitForSelector() calls\n  // seems to fix it 🤷‍♂️\n  //\n  //   Test timeout of 30000ms exceeded.\n  //\n  //   Error: page.waitForSelector: Target closed\n  //   =========================== logs ===========================\n  //   waiting for locator('text=ROOT_BOUNDARY_TEXT') to be visible\n  //     locator resolved to hidden <div id=\"root-boundary\">ROOT_BOUNDARY_TEXT</div>\n  //     locator resolved to hidden <div id=\"root-boundary\">ROOT_BOUNDARY_TEXT</div>\n  //     ... and so on until the test times out\n  let userAgent = await page.evaluate(() => navigator.userAgent);\n  if (/Safari\\//i.test(userAgent) && !/Chrome\\//i.test(userAgent)) {\n    await page.evaluate(() => new Promise((r) => requestAnimationFrame(r)));\n  }\n\n  if (DEBUG) {\n    console.log(`action done, network settled`);\n  }\n\n  page.removeListener(\"request\", onRequest);\n  page.removeListener(\"requestfinished\", onRequestDone);\n  page.removeListener(\"requestfailed\", onRequestDone);\n  page.removeListener(\"load\", networkSettledCallback);\n\n  if (DEBUG && timeoutId) {\n    clearTimeout(timeoutId);\n  }\n\n  return result;\n}\n"
  },
  {
    "path": "integration/helpers/rsc-vite/.gitignore",
    "content": "dist\nnode_modules\n"
  },
  {
    "path": "integration/helpers/rsc-vite/package.json",
    "content": "{\n  \"name\": \"integration-rsc-vite\",\n  \"version\": \"0.0.0\",\n  \"private\": true,\n  \"type\": \"module\",\n  \"scripts\": {\n    \"dev\": \"vite\",\n    \"build\": \"vite build --app\",\n    \"start\": \"cross-env NODE_ENV=production node server.js\",\n    \"typecheck\": \"tsc\"\n  },\n  \"devDependencies\": {\n    \"@vitejs/plugin-rsc\": \"catalog:\",\n    \"@types/express\": \"^5.0.0\",\n    \"@types/node\": \"^22.13.1\",\n    \"@types/react\": \"catalog:react-canary\",\n    \"@types/react-dom\": \"catalog:react-canary\",\n    \"@vitejs/plugin-react\": \"^4.5.2\",\n    \"typescript\": \"catalog:\",\n    \"vite\": \"^6.3.0\"\n  },\n  \"dependencies\": {\n    \"@mjackson/node-fetch-server\": \"0.6.1\",\n    \"compression\": \"^1.8.1\",\n    \"cross-env\": \"^7.0.3\",\n    \"express\": \"^4.21.2\",\n    \"react\": \"catalog:react-canary\",\n    \"react-dom\": \"catalog:react-canary\",\n    \"react-router\": \"workspace:*\",\n    \"react-server-dom-webpack\": \"catalog:react-canary\"\n  }\n}\n"
  },
  {
    "path": "integration/helpers/rsc-vite/server.js",
    "content": "import { parseArgs } from \"node:util\";\nimport { createRequestListener } from \"@mjackson/node-fetch-server\";\nimport express from \"express\";\n\nimport rscRequestHandler from \"./dist/rsc/index.js\";\n\nconst app = express();\n\napp.use(express.static(\"dist/client\"));\n\napp.get(\"/.well-known/appspecific/com.chrome.devtools.json\", (req, res) => {\n  res.status(404);\n  res.end();\n});\n\napp.use(createRequestListener(rscRequestHandler));\n\nconst { values } = parseArgs({\n  options: { p: { type: \"string\", default: \"3000\" } },\n  allowPositionals: true,\n});\n\nconst port = parseInt(values.p, 10);\napp.listen(port, () => {\n  console.log(`Server started on http://localhost:${port}`);\n});\n"
  },
  {
    "path": "integration/helpers/rsc-vite/src/config/basename.ts",
    "content": "// THIS FILE IS DESIGNED TO BE OVERRIDDEN IN TESTS IF NEEDED\nexport const basename = undefined;\n"
  },
  {
    "path": "integration/helpers/rsc-vite/src/config/get-context.ts",
    "content": "// THIS FILE IS DESIGNED TO BE OVERRIDDEN IN TESTS IF NEEDED\nexport const getContext = undefined;\n"
  },
  {
    "path": "integration/helpers/rsc-vite/src/config/request-context.ts",
    "content": "// THIS FILE IS DESIGNED TO BE OVERRIDDEN IN TESTS IF NEEDED\nexport const requestContext = undefined;\n"
  },
  {
    "path": "integration/helpers/rsc-vite/src/entry.browser.tsx",
    "content": "import { startTransition, StrictMode } from \"react\";\nimport { hydrateRoot } from \"react-dom/client\";\nimport {\n  createFromReadableStream,\n  createTemporaryReferenceSet,\n  encodeReply,\n  setServerCallback,\n} from \"@vitejs/plugin-rsc/browser\";\nimport {\n  unstable_createCallServer as createCallServer,\n  unstable_getRSCStream as getRSCStream,\n  unstable_RSCHydratedRouter as RSCHydratedRouter,\n  type unstable_RSCPayload as RSCPayload,\n} from \"react-router/dom\";\nimport { getContext } from \"./config/get-context\";\n\nsetServerCallback(\n  createCallServer({\n    createFromReadableStream,\n    createTemporaryReferenceSet,\n    encodeReply,\n  }),\n);\n\ncreateFromReadableStream<RSCPayload>(getRSCStream()).then((payload) => {\n  // @ts-expect-error - on 18 types, requires 19.\n  startTransition(async () => {\n    const formState =\n      payload.type === \"render\" ? await payload.formState : undefined;\n\n    hydrateRoot(\n      document,\n      <StrictMode>\n        <RSCHydratedRouter\n          payload={payload}\n          createFromReadableStream={createFromReadableStream}\n          getContext={getContext}\n        />\n      </StrictMode>,\n      {\n        // @ts-expect-error - no types for this yet\n        formState,\n      },\n    );\n  });\n});\n"
  },
  {
    "path": "integration/helpers/rsc-vite/src/entry.rsc.tsx",
    "content": "import {\n  createTemporaryReferenceSet,\n  decodeAction,\n  decodeFormState,\n  decodeReply,\n  loadServerAction,\n  renderToReadableStream,\n} from \"@vitejs/plugin-rsc/rsc\";\nimport { unstable_matchRSCServerRequest as matchRSCServerRequest } from \"react-router\";\nimport { basename } from \"./config/basename\";\n\nimport { routes } from \"./routes\";\nimport { requestContext } from \"./config/request-context\";\n\nexport async function fetchServer(request: Request) {\n  return await matchRSCServerRequest({\n    createTemporaryReferenceSet,\n    decodeReply,\n    decodeAction,\n    decodeFormState,\n    loadServerAction,\n    request,\n    requestContext,\n    routes,\n    basename,\n    generateResponse(match, options) {\n      return new Response(renderToReadableStream(match.payload, options), {\n        status: match.statusCode,\n        headers: match.headers,\n      });\n    },\n  });\n}\n\nexport default async function handler(request: Request) {\n  const ssr = await import.meta.viteRsc.loadModule<\n    // eslint-disable-next-line @typescript-eslint/consistent-type-imports\n    typeof import(\"./entry.ssr\")\n  >(\"ssr\", \"index\");\n  return ssr.default(request, await fetchServer(request));\n}\n"
  },
  {
    "path": "integration/helpers/rsc-vite/src/entry.ssr.tsx",
    "content": "import { createFromReadableStream } from \"@vitejs/plugin-rsc/ssr\";\n// @ts-expect-error\nimport * as ReactDomServer from \"react-dom/server.edge\";\nimport {\n  unstable_RSCStaticRouter as RSCStaticRouter,\n  unstable_routeRSCServerRequest as routeRSCServerRequest,\n} from \"react-router\";\n\nexport default async function handler(\n  request: Request,\n  serverResponse: Response,\n) {\n  const bootstrapScriptContent =\n    await import.meta.viteRsc.loadBootstrapScriptContent(\"index\");\n  return routeRSCServerRequest({\n    request,\n    serverResponse,\n    createFromReadableStream,\n    async renderHTML(getPayload, options) {\n      const payload = getPayload();\n\n      return ReactDomServer.renderToReadableStream(\n        <RSCStaticRouter getPayload={getPayload} />,\n        {\n          ...options,\n          bootstrapScriptContent,\n          signal: request.signal,\n          formState: await payload.formState,\n        },\n      );\n    },\n  });\n}\n"
  },
  {
    "path": "integration/helpers/rsc-vite/src/routes/home.tsx",
    "content": "export default function HomeRoute() {\n  return <h2>Home</h2>;\n}\n"
  },
  {
    "path": "integration/helpers/rsc-vite/src/routes/root.tsx",
    "content": "import { Links, Outlet, ScrollRestoration } from \"react-router\";\n\nexport function Layout({ children }: { children: React.ReactNode }) {\n  return (\n    <html lang=\"en\">\n      <head>\n        <meta charSet=\"utf-8\" />\n        <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />\n        <title>Vite (RSC)</title>\n        <Links />\n      </head>\n      <body>\n        {children}\n        <ScrollRestoration />\n      </body>\n    </html>\n  );\n}\n\nexport default function RootRoute() {\n  return <Outlet />;\n}\n"
  },
  {
    "path": "integration/helpers/rsc-vite/src/routes.ts",
    "content": "import type { unstable_RSCRouteConfig as RSCRouteConfig } from \"react-router\";\n\nexport const routes = [\n  {\n    id: \"root\",\n    path: \"\",\n    lazy: () => import(\"./routes/root\"),\n    children: [\n      {\n        id: \"home\",\n        index: true,\n        lazy: () => import(\"./routes/home\"),\n      },\n    ],\n  },\n] satisfies RSCRouteConfig;\n"
  },
  {
    "path": "integration/helpers/rsc-vite/tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"allowImportingTsExtensions\": true,\n    \"strict\": true,\n    \"noUnusedLocals\": true,\n    \"noUnusedParameters\": true,\n    \"skipLibCheck\": true,\n    \"verbatimModuleSyntax\": true,\n    \"noEmit\": true,\n    \"moduleResolution\": \"Bundler\",\n    \"module\": \"ESNext\",\n    \"target\": \"ESNext\",\n    \"lib\": [\"ESNext\", \"DOM\", \"DOM.Iterable\"],\n    \"types\": [\"vite/client\", \"@vitejs/plugin-rsc/types\"],\n    \"jsx\": \"react-jsx\"\n  }\n}\n"
  },
  {
    "path": "integration/helpers/rsc-vite/vite.config.ts",
    "content": "import rsc from \"@vitejs/plugin-rsc\";\nimport react from \"@vitejs/plugin-react\";\nimport { defineConfig } from \"vite\";\n\nexport default defineConfig({\n  plugins: [\n    react(),\n    rsc({\n      entries: {\n        client: \"src/entry.browser.tsx\",\n        rsc: \"src/entry.rsc.tsx\",\n        ssr: \"src/entry.ssr.tsx\",\n      },\n    }),\n  ],\n});\n"
  },
  {
    "path": "integration/helpers/rsc-vite-framework/.gitignore",
    "content": "build\nnode_modules\n"
  },
  {
    "path": "integration/helpers/rsc-vite-framework/app/root.tsx",
    "content": "import { Links, Meta, Outlet, ScrollRestoration } from \"react-router\";\n\nexport default function App() {\n  return (\n    <html lang=\"en\">\n      <head>\n        <meta charSet=\"utf-8\" />\n        <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />\n        <Meta />\n        <Links />\n      </head>\n      <body>\n        <Outlet />\n        <ScrollRestoration />\n      </body>\n    </html>\n  );\n}\n"
  },
  {
    "path": "integration/helpers/rsc-vite-framework/app/routes/_index.tsx",
    "content": "import type { MetaFunction } from \"react-router\";\n\nexport const meta: MetaFunction = () => {\n  return [\n    { title: \"New React Router App\" },\n    { name: \"description\", content: \"Welcome to React Router!\" },\n  ];\n};\n\nexport default function Index() {\n  return (\n    <div style={{ fontFamily: \"system-ui, sans-serif\", lineHeight: \"1.8\" }}>\n      <h1>Welcome to React Router</h1>\n    </div>\n  );\n}\n"
  },
  {
    "path": "integration/helpers/rsc-vite-framework/app/routes.ts",
    "content": "import { type RouteConfig } from \"@react-router/dev/routes\";\nimport { flatRoutes } from \"@react-router/fs-routes\";\n\nexport default flatRoutes() satisfies RouteConfig;\n"
  },
  {
    "path": "integration/helpers/rsc-vite-framework/package.json",
    "content": "{\n  \"name\": \"integration-rsc-vite-framework\",\n  \"version\": \"0.0.0\",\n  \"private\": true,\n  \"sideEffects\": false,\n  \"type\": \"module\",\n  \"scripts\": {\n    \"dev\": \"vite\",\n    \"build\": \"vite build --app\",\n    \"start\": \"cross-env NODE_ENV=production node start.js\",\n    \"typecheck\": \"react-router typegen && tsc\"\n  },\n  \"devDependencies\": {\n    \"@mdx-js/rollup\": \"^3.1.0\",\n    \"@react-router/dev\": \"workspace:*\",\n    \"@react-router/fs-routes\": \"workspace:*\",\n    \"@types/express\": \"^5.0.0\",\n    \"@types/node\": \"^22.13.1\",\n    \"@types/react\": \"catalog:react-canary\",\n    \"@types/react-dom\": \"catalog:react-canary\",\n    \"@vanilla-extract/css\": \"^1.17.4\",\n    \"@vanilla-extract/vite-plugin\": \"^5.1.1\",\n    \"@vitejs/plugin-react\": \"^4.5.2\",\n    \"@vitejs/plugin-rsc\": \"catalog:\",\n    \"cross-env\": \"^7.0.3\",\n    \"typescript\": \"catalog:\",\n    \"vite\": \"^6.3.0\",\n    \"vite-env-only\": \"^3.0.1\",\n    \"vite-tsconfig-paths\": \"^4.2.1\"\n  },\n  \"dependencies\": {\n    \"@mjackson/node-fetch-server\": \"0.6.1\",\n    \"@react-router/serve\": \"workspace:*\",\n    \"compression\": \"^1.8.1\",\n    \"express\": \"^4.21.2\",\n    \"react\": \"catalog:react-canary\",\n    \"react-dom\": \"catalog:react-canary\",\n    \"react-router\": \"workspace:*\",\n    \"react-server-dom-webpack\": \"catalog:react-canary\"\n  }\n}\n"
  },
  {
    "path": "integration/helpers/rsc-vite-framework/start.js",
    "content": "import { createRequestListener } from \"@mjackson/node-fetch-server\";\nimport express from \"express\";\nimport build from \"./build/server/index.js\";\n\nconst app = express();\n\napp.use(process.env.VITE_BASE || \"/\", express.static(\"build/client\"));\n\napp.get(\"/.well-known/appspecific/com.chrome.devtools.json\", (_, res) => {\n  res.status(404);\n  res.send(\"Not Found\");\n});\n\napp.use(createRequestListener(build.fetch));\n\nconst port = process.env.PORT || 3000;\napp.listen(port);\nconsole.log(`Server listening on port ${port} (http://localhost:${port})`);\n"
  },
  {
    "path": "integration/helpers/rsc-vite-framework/tsconfig.json",
    "content": "{\n  \"include\": [\"**/*.ts\", \"**/*.tsx\", \"./.react-router/types/**/*\"],\n  \"compilerOptions\": {\n    \"allowImportingTsExtensions\": true,\n    \"strict\": true,\n    \"noUnusedLocals\": true,\n    \"noUnusedParameters\": true,\n    \"skipLibCheck\": true,\n    \"verbatimModuleSyntax\": true,\n    \"noEmit\": true,\n    \"moduleResolution\": \"Bundler\",\n    \"module\": \"ESNext\",\n    \"target\": \"ESNext\",\n    \"lib\": [\"ESNext\", \"DOM\", \"DOM.Iterable\"],\n    \"types\": [\"vite/client\", \"@vitejs/plugin-rsc/types\"],\n    \"jsx\": \"react-jsx\",\n    \"paths\": {\n      \"~/*\": [\"./app/*\"]\n    },\n    \"rootDirs\": [\".\", \"./.react-router/types\"]\n  }\n}\n"
  },
  {
    "path": "integration/helpers/rsc-vite-framework/vite.config.ts",
    "content": "import { defineConfig } from \"vite\";\nimport { unstable_reactRouterRSC as reactRouterRSC } from \"@react-router/dev/vite\";\nimport rsc from \"@vitejs/plugin-rsc\";\n\nexport default defineConfig({\n  plugins: [\n    // @ts-ignore\n    reactRouterRSC({ __runningWithinTheReactRouterMonoRepo: true }),\n    rsc(),\n  ],\n});\n"
  },
  {
    "path": "integration/helpers/stream.ts",
    "content": "import type { Readable } from \"node:stream\";\n\nexport async function match(\n  stream: Readable,\n  pattern: string | RegExp,\n  options: {\n    /** Measured in ms */\n    timeout?: number;\n  } = {},\n): Promise<RegExpMatchArray> {\n  // Prepare error outside of promise so that stacktrace points to caller of `matchLine`\n  const timeoutError = new Error(\n    `Timed out - Could not find pattern: ${pattern}`,\n  );\n  return new Promise(async (resolve, reject) => {\n    const timeout = setTimeout(\n      () => reject(timeoutError),\n      options.timeout ?? 10_000,\n    );\n    stream.on(\"data\", (data) => {\n      const line: string = data.toString();\n      const matches = line.match(pattern);\n      if (matches) {\n        resolve(matches);\n        clearTimeout(timeout);\n      }\n    });\n  });\n}\n"
  },
  {
    "path": "integration/helpers/templates.ts",
    "content": "const templates = [\n  // Vite Major templates\n  { name: \"vite-5-template\", displayName: \"Vite 5\" },\n  { name: \"vite-6-template\", displayName: \"Vite 6\" },\n  { name: \"vite-7-beta-template\", displayName: \"Vite 7 Beta\" },\n  { name: \"vite-rolldown-template\", displayName: \"Vite Rolldown\" },\n\n  // RSC templates\n  { name: \"rsc-vite\", displayName: \"RSC (Vite)\" },\n  { name: \"rsc-vite-framework\", displayName: \"RSC Framework\" },\n\n  // Cloudflare\n  // { name: \"cloudflare-dev-proxy-template\", displayName: \"Cloudflare Dev Proxy\" },\n  { name: \"vite-plugin-cloudflare-template\", displayName: \"Cloudflare\" },\n] as const;\n\nexport type Template = (typeof templates)[number];\n\nexport function getTemplates(names?: Array<Template[\"name\"]>) {\n  if (names === undefined) return templates;\n  return templates.filter(({ name }) => names.includes(name));\n}\n\nexport const viteMajorTemplates = getTemplates([\n  \"vite-5-template\",\n  \"vite-6-template\",\n  \"vite-7-beta-template\",\n  \"vite-rolldown-template\",\n]);\n"
  },
  {
    "path": "integration/helpers/vite-5-template/.gitignore",
    "content": "node_modules\n\n/.cache\n/build\n.env\n.react-router\n"
  },
  {
    "path": "integration/helpers/vite-5-template/app/root.tsx",
    "content": "import { Links, Meta, Outlet, Scripts, ScrollRestoration } from \"react-router\";\n\nexport default function App() {\n  return (\n    <html lang=\"en\">\n      <head>\n        <meta charSet=\"utf-8\" />\n        <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />\n        <Meta />\n        <Links />\n      </head>\n      <body>\n        <Outlet />\n        <ScrollRestoration />\n        <Scripts />\n      </body>\n    </html>\n  );\n}\n"
  },
  {
    "path": "integration/helpers/vite-5-template/app/routes/_index.tsx",
    "content": "import type { MetaFunction } from \"react-router\";\n\nexport const meta: MetaFunction = () => {\n  return [\n    { title: \"New React Router App\" },\n    { name: \"description\", content: \"Welcome to React Router!\" },\n  ];\n};\n\nexport default function Index() {\n  return (\n    <div style={{ fontFamily: \"system-ui, sans-serif\", lineHeight: \"1.8\" }}>\n      <h1>Welcome to React Router</h1>\n    </div>\n  );\n}\n"
  },
  {
    "path": "integration/helpers/vite-5-template/app/routes.ts",
    "content": "import { type RouteConfig } from \"@react-router/dev/routes\";\nimport { flatRoutes } from \"@react-router/fs-routes\";\n\nexport default flatRoutes() satisfies RouteConfig;\n"
  },
  {
    "path": "integration/helpers/vite-5-template/env.d.ts",
    "content": "/// <reference types=\"@react-router/node\" />\n/// <reference types=\"vite/client\" />\n"
  },
  {
    "path": "integration/helpers/vite-5-template/package.json",
    "content": "{\n  \"name\": \"integration-vite-5-template\",\n  \"version\": \"0.0.0\",\n  \"private\": true,\n  \"sideEffects\": false,\n  \"type\": \"module\",\n  \"scripts\": {\n    \"dev\": \"react-router dev\",\n    \"build\": \"react-router build\",\n    \"start\": \"react-router-serve ./build/server/index.js\",\n    \"typecheck\": \"react-router typegen && tsc\"\n  },\n  \"dependencies\": {\n    \"@react-router/express\": \"workspace:*\",\n    \"@react-router/node\": \"workspace:*\",\n    \"@react-router/serve\": \"workspace:*\",\n    \"@vanilla-extract/css\": \"^1.17.4\",\n    \"@vanilla-extract/vite-plugin\": \"^5.1.1\",\n    \"express\": \"^4.19.2\",\n    \"isbot\": \"^5.1.11\",\n    \"react\": \"catalog:\",\n    \"react-dom\": \"catalog:\",\n    \"react-router\": \"workspace:*\",\n    \"serialize-javascript\": \"^6.0.1\"\n  },\n  \"devDependencies\": {\n    \"@react-router/dev\": \"workspace:*\",\n    \"@react-router/fs-routes\": \"workspace:*\",\n    \"@react-router/remix-routes-option-adapter\": \"workspace:*\",\n    \"@types/react\": \"catalog:\",\n    \"@types/react-dom\": \"catalog:\",\n    \"eslint\": \"^8.38.0\",\n    \"typescript\": \"catalog:\",\n    \"vite\": \"^5.1.0\",\n    \"vite-env-only\": \"^3.0.1\",\n    \"vite-tsconfig-paths\": \"^4.2.1\"\n  },\n  \"engines\": {\n    \"node\": \">=20.0.0\"\n  }\n}\n"
  },
  {
    "path": "integration/helpers/vite-5-template/tsconfig.json",
    "content": "{\n  \"include\": [\"env.d.ts\", \"**/*.ts\", \"**/*.tsx\", \".react-router/types/**/*\"],\n  \"compilerOptions\": {\n    \"lib\": [\"DOM\", \"DOM.Iterable\", \"ES2022\"],\n    \"verbatimModuleSyntax\": true,\n    \"esModuleInterop\": true,\n    \"jsx\": \"react-jsx\",\n    \"module\": \"ESNext\",\n    \"moduleResolution\": \"Bundler\",\n    \"resolveJsonModule\": true,\n    \"target\": \"ES2022\",\n    \"strict\": true,\n    \"allowJs\": true,\n    \"skipLibCheck\": true,\n    \"baseUrl\": \".\",\n    \"paths\": {\n      \"~/*\": [\"./app/*\"]\n    },\n    \"noEmit\": true,\n    \"rootDirs\": [\".\", \".react-router/types/\"]\n  }\n}\n"
  },
  {
    "path": "integration/helpers/vite-5-template/vite.config.ts",
    "content": "import { reactRouter } from \"@react-router/dev/vite\";\nimport { defineConfig } from \"vite\";\nimport tsconfigPaths from \"vite-tsconfig-paths\";\n\nexport default defineConfig({\n  plugins: [\n    // @ts-expect-error `dev` depends on Vite 6, Plugin type is mismatched.\n    reactRouter(),\n    tsconfigPaths(),\n  ],\n});\n"
  },
  {
    "path": "integration/helpers/vite-6-template/.gitignore",
    "content": "node_modules\n\n/.cache\n/build\n.env\n.react-router\n"
  },
  {
    "path": "integration/helpers/vite-6-template/app/root.tsx",
    "content": "import { Links, Meta, Outlet, Scripts, ScrollRestoration } from \"react-router\";\n\nexport default function App() {\n  return (\n    <html lang=\"en\">\n      <head>\n        <meta charSet=\"utf-8\" />\n        <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />\n        <Meta />\n        <Links />\n      </head>\n      <body>\n        <Outlet />\n        <ScrollRestoration />\n        <Scripts />\n      </body>\n    </html>\n  );\n}\n"
  },
  {
    "path": "integration/helpers/vite-6-template/app/routes/_index.tsx",
    "content": "import type { MetaFunction } from \"react-router\";\n\nexport const meta: MetaFunction = () => {\n  return [\n    { title: \"New React Router App\" },\n    { name: \"description\", content: \"Welcome to React Router!\" },\n  ];\n};\n\nexport default function Index() {\n  return (\n    <div style={{ fontFamily: \"system-ui, sans-serif\", lineHeight: \"1.8\" }}>\n      <h1>Welcome to React Router</h1>\n    </div>\n  );\n}\n"
  },
  {
    "path": "integration/helpers/vite-6-template/app/routes.ts",
    "content": "import { type RouteConfig } from \"@react-router/dev/routes\";\nimport { flatRoutes } from \"@react-router/fs-routes\";\n\nexport default flatRoutes() satisfies RouteConfig;\n"
  },
  {
    "path": "integration/helpers/vite-6-template/env.d.ts",
    "content": "/// <reference types=\"@react-router/node\" />\n/// <reference types=\"vite/client\" />\n"
  },
  {
    "path": "integration/helpers/vite-6-template/package.json",
    "content": "{\n  \"name\": \"integration-vite-6-template\",\n  \"version\": \"0.0.0\",\n  \"private\": true,\n  \"sideEffects\": false,\n  \"type\": \"module\",\n  \"scripts\": {\n    \"dev\": \"react-router dev\",\n    \"build\": \"react-router build\",\n    \"start\": \"react-router-serve ./build/server/index.js\",\n    \"typecheck\": \"react-router typegen && tsc\"\n  },\n  \"dependencies\": {\n    \"@react-router/express\": \"workspace:*\",\n    \"@react-router/node\": \"workspace:*\",\n    \"@react-router/serve\": \"workspace:*\",\n    \"@vanilla-extract/css\": \"^1.17.4\",\n    \"@vanilla-extract/vite-plugin\": \"^5.1.1\",\n    \"express\": \"^4.19.2\",\n    \"isbot\": \"^5.1.11\",\n    \"react\": \"catalog:\",\n    \"react-dom\": \"catalog:\",\n    \"react-router\": \"workspace:*\",\n    \"serialize-javascript\": \"^6.0.1\"\n  },\n  \"devDependencies\": {\n    \"@react-router/dev\": \"workspace:*\",\n    \"@react-router/fs-routes\": \"workspace:*\",\n    \"@react-router/remix-routes-option-adapter\": \"workspace:*\",\n    \"@types/react\": \"catalog:\",\n    \"@types/react-dom\": \"catalog:\",\n    \"eslint\": \"^8.38.0\",\n    \"typescript\": \"catalog:\",\n    \"vite\": \"^6.3.0\",\n    \"vite-env-only\": \"^3.0.1\",\n    \"vite-tsconfig-paths\": \"^4.2.1\"\n  },\n  \"engines\": {\n    \"node\": \">=20.0.0\"\n  }\n}\n"
  },
  {
    "path": "integration/helpers/vite-6-template/tsconfig.json",
    "content": "{\n  \"include\": [\"env.d.ts\", \"**/*.ts\", \"**/*.tsx\", \".react-router/types/**/*\"],\n  \"compilerOptions\": {\n    \"lib\": [\"DOM\", \"DOM.Iterable\", \"ES2022\"],\n    \"verbatimModuleSyntax\": true,\n    \"esModuleInterop\": true,\n    \"jsx\": \"react-jsx\",\n    \"module\": \"ESNext\",\n    \"moduleResolution\": \"Bundler\",\n    \"resolveJsonModule\": true,\n    \"target\": \"ES2022\",\n    \"strict\": true,\n    \"allowJs\": true,\n    \"skipLibCheck\": true,\n    \"baseUrl\": \".\",\n    \"paths\": {\n      \"~/*\": [\"./app/*\"]\n    },\n    \"noEmit\": true,\n    \"rootDirs\": [\".\", \".react-router/types/\"]\n  }\n}\n"
  },
  {
    "path": "integration/helpers/vite-6-template/vite.config.ts",
    "content": "import { reactRouter } from \"@react-router/dev/vite\";\nimport { defineConfig } from \"vite\";\nimport tsconfigPaths from \"vite-tsconfig-paths\";\n\nexport default defineConfig({\n  plugins: [reactRouter(), tsconfigPaths()],\n});\n"
  },
  {
    "path": "integration/helpers/vite-7-beta-template/.gitignore",
    "content": "node_modules\n\n/.cache\n/build\n.env\n.react-router\n"
  },
  {
    "path": "integration/helpers/vite-7-beta-template/app/root.tsx",
    "content": "import { Links, Meta, Outlet, Scripts, ScrollRestoration } from \"react-router\";\n\nexport default function App() {\n  return (\n    <html lang=\"en\">\n      <head>\n        <meta charSet=\"utf-8\" />\n        <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />\n        <Meta />\n        <Links />\n      </head>\n      <body>\n        <Outlet />\n        <ScrollRestoration />\n        <Scripts />\n      </body>\n    </html>\n  );\n}\n"
  },
  {
    "path": "integration/helpers/vite-7-beta-template/app/routes/_index.tsx",
    "content": "import type { MetaFunction } from \"react-router\";\n\nexport const meta: MetaFunction = () => {\n  return [\n    { title: \"New React Router App\" },\n    { name: \"description\", content: \"Welcome to React Router!\" },\n  ];\n};\n\nexport default function Index() {\n  return (\n    <div style={{ fontFamily: \"system-ui, sans-serif\", lineHeight: \"1.8\" }}>\n      <h1>Welcome to React Router</h1>\n    </div>\n  );\n}\n"
  },
  {
    "path": "integration/helpers/vite-7-beta-template/app/routes.ts",
    "content": "import { type RouteConfig } from \"@react-router/dev/routes\";\nimport { flatRoutes } from \"@react-router/fs-routes\";\n\nexport default flatRoutes() satisfies RouteConfig;\n"
  },
  {
    "path": "integration/helpers/vite-7-beta-template/env.d.ts",
    "content": "/// <reference types=\"@react-router/node\" />\n/// <reference types=\"vite/client\" />\n"
  },
  {
    "path": "integration/helpers/vite-7-beta-template/package.json",
    "content": "{\n  \"name\": \"integration-vite-7-beta-template\",\n  \"version\": \"0.0.0\",\n  \"private\": true,\n  \"sideEffects\": false,\n  \"type\": \"module\",\n  \"scripts\": {\n    \"dev\": \"react-router dev\",\n    \"build\": \"react-router build\",\n    \"start\": \"react-router-serve ./build/server/index.js\",\n    \"typecheck\": \"react-router typegen && tsc\"\n  },\n  \"dependencies\": {\n    \"@react-router/express\": \"workspace:*\",\n    \"@react-router/node\": \"workspace:*\",\n    \"@react-router/serve\": \"workspace:*\",\n    \"@vanilla-extract/css\": \"^1.17.4\",\n    \"@vanilla-extract/vite-plugin\": \"^5.1.1\",\n    \"express\": \"^4.19.2\",\n    \"isbot\": \"^5.1.11\",\n    \"react\": \"catalog:\",\n    \"react-dom\": \"catalog:\",\n    \"react-router\": \"workspace:*\",\n    \"serialize-javascript\": \"^6.0.1\"\n  },\n  \"devDependencies\": {\n    \"@react-router/dev\": \"workspace:*\",\n    \"@react-router/fs-routes\": \"workspace:*\",\n    \"@react-router/remix-routes-option-adapter\": \"workspace:*\",\n    \"@types/react\": \"catalog:\",\n    \"@types/react-dom\": \"catalog:\",\n    \"eslint\": \"^8.38.0\",\n    \"typescript\": \"catalog:\",\n    \"vite\": \"7.0.0-beta.0\",\n    \"vite-env-only\": \"^3.0.1\",\n    \"vite-tsconfig-paths\": \"^4.2.1\"\n  },\n  \"engines\": {\n    \"node\": \">=20.0.0\"\n  }\n}\n"
  },
  {
    "path": "integration/helpers/vite-7-beta-template/tsconfig.json",
    "content": "{\n  \"include\": [\"env.d.ts\", \"**/*.ts\", \"**/*.tsx\", \".react-router/types/**/*\"],\n  \"compilerOptions\": {\n    \"lib\": [\"DOM\", \"DOM.Iterable\", \"ES2022\"],\n    \"verbatimModuleSyntax\": true,\n    \"esModuleInterop\": true,\n    \"jsx\": \"react-jsx\",\n    \"module\": \"ESNext\",\n    \"moduleResolution\": \"Bundler\",\n    \"resolveJsonModule\": true,\n    \"target\": \"ES2022\",\n    \"strict\": true,\n    \"allowJs\": true,\n    \"skipLibCheck\": true,\n    \"baseUrl\": \".\",\n    \"paths\": {\n      \"~/*\": [\"./app/*\"]\n    },\n    \"noEmit\": true,\n    \"rootDirs\": [\".\", \".react-router/types/\"]\n  }\n}\n"
  },
  {
    "path": "integration/helpers/vite-7-beta-template/vite.config.ts",
    "content": "import { reactRouter } from \"@react-router/dev/vite\";\nimport { defineConfig } from \"vite\";\nimport tsconfigPaths from \"vite-tsconfig-paths\";\n\nexport default defineConfig({\n  // @ts-expect-error `dev` depends on Vite 6, Plugin type is mismatched.\n  plugins: [reactRouter(), tsconfigPaths()],\n});\n"
  },
  {
    "path": "integration/helpers/vite-plugin-cloudflare-template/.gitignore",
    "content": ".DS_Store\n/node_modules/\n*.tsbuildinfo\n\n# React Router\n/.react-router/\n/build/\n\n# Cloudflare\n.mf\n.wrangler\n"
  },
  {
    "path": "integration/helpers/vite-plugin-cloudflare-template/app/entry.server.tsx",
    "content": "import type { AppLoadContext, EntryContext } from \"react-router\";\nimport { ServerRouter } from \"react-router\";\nimport { isbot } from \"isbot\";\nimport { renderToReadableStream } from \"react-dom/server\";\n\nexport default async function handleRequest(\n  request: Request,\n  responseStatusCode: number,\n  responseHeaders: Headers,\n  routerContext: EntryContext,\n  _loadContext: AppLoadContext,\n) {\n  let shellRendered = false;\n  const userAgent = request.headers.get(\"user-agent\");\n\n  const body = await renderToReadableStream(\n    <ServerRouter context={routerContext} url={request.url} />,\n    {\n      onError(error: unknown) {\n        responseStatusCode = 500;\n        // Log streaming rendering errors from inside the shell.  Don't log\n        // errors encountered during initial shell rendering since they'll\n        // reject and get logged in handleDocumentRequest.\n        if (shellRendered) {\n          console.error(error);\n        }\n      },\n    },\n  );\n  shellRendered = true;\n\n  // Ensure requests from bots and SPA Mode renders wait for all content to load before responding\n  // https://react.dev/reference/react-dom/server/renderToPipeableStream#waiting-for-all-content-to-load-for-crawlers-and-static-generation\n  if ((userAgent && isbot(userAgent)) || routerContext.isSpaMode) {\n    await body.allReady;\n  }\n\n  responseHeaders.set(\"Content-Type\", \"text/html\");\n  return new Response(body, {\n    headers: responseHeaders,\n    status: responseStatusCode,\n  });\n}\n"
  },
  {
    "path": "integration/helpers/vite-plugin-cloudflare-template/app/root.tsx",
    "content": "import { Links, Meta, Outlet, Scripts, ScrollRestoration } from \"react-router\";\n\nexport default function App() {\n  return (\n    <html lang=\"en\">\n      <head>\n        <meta charSet=\"utf-8\" />\n        <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />\n        <Meta />\n        <Links />\n      </head>\n      <body>\n        <Outlet />\n        <ScrollRestoration />\n        <Scripts />\n      </body>\n    </html>\n  );\n}\n"
  },
  {
    "path": "integration/helpers/vite-plugin-cloudflare-template/app/routes/_index.tsx",
    "content": "import type { MetaFunction } from \"react-router\";\n\nexport const meta: MetaFunction = () => {\n  return [\n    { title: \"New React Router App\" },\n    { name: \"description\", content: \"Welcome to React Router!\" },\n  ];\n};\n\nexport default function Index() {\n  return (\n    <div style={{ fontFamily: \"system-ui, sans-serif\", lineHeight: \"1.8\" }}>\n      <h1>Welcome to React Router</h1>\n    </div>\n  );\n}\n"
  },
  {
    "path": "integration/helpers/vite-plugin-cloudflare-template/app/routes.ts",
    "content": "import { type RouteConfig } from \"@react-router/dev/routes\";\nimport { flatRoutes } from \"@react-router/fs-routes\";\n\nexport default flatRoutes() satisfies RouteConfig;\n"
  },
  {
    "path": "integration/helpers/vite-plugin-cloudflare-template/package.json",
    "content": "{\n  \"name\": \"integration-vite-plugin-cloudflare-template\",\n  \"version\": \"0.0.0\",\n  \"private\": true,\n  \"sideEffects\": false,\n  \"type\": \"module\",\n  \"scripts\": {\n    \"dev\": \"react-router dev\",\n    \"build\": \"react-router build\",\n    \"typegen\": \"wrangler types && react-router typegen\",\n    \"typecheck\": \"pnpm typegen && tsc\"\n  },\n  \"dependencies\": {\n    \"express\": \"^4.19.2\",\n    \"isbot\": \"^5.1.11\",\n    \"react\": \"catalog:\",\n    \"react-dom\": \"catalog:\",\n    \"react-router\": \"workspace:*\",\n    \"serialize-javascript\": \"^6.0.1\"\n  },\n  \"devDependencies\": {\n    \"@cloudflare/vite-plugin\": \"^1.9.0\",\n    \"@react-router/dev\": \"workspace:*\",\n    \"@react-router/fs-routes\": \"workspace:*\",\n    \"@types/node\": \"^20.0.0\",\n    \"@types/react\": \"catalog:\",\n    \"@types/react-dom\": \"catalog:\",\n    \"eslint\": \"^8.38.0\",\n    \"typescript\": \"catalog:\",\n    \"vite\": \"^6.3.0\",\n    \"vite-tsconfig-paths\": \"^4.2.1\",\n    \"wrangler\": \"^4.23.0\"\n  },\n  \"engines\": {\n    \"node\": \">=20.0.0\"\n  }\n}\n"
  },
  {
    "path": "integration/helpers/vite-plugin-cloudflare-template/react-router.config.ts",
    "content": "import type { Config } from \"@react-router/dev/config\";\n\nexport default {\n  future: {\n    v8_viteEnvironmentApi: true,\n  },\n} satisfies Config;\n"
  },
  {
    "path": "integration/helpers/vite-plugin-cloudflare-template/tsconfig.cloudflare.json",
    "content": "{\n  \"extends\": \"./tsconfig.json\",\n  \"include\": [\n    \".react-router/types/**/*\",\n    \"app/**/*\",\n    \"app/**/.server/**/*\",\n    \"app/**/.client/**/*\",\n    \"workers/**/*\",\n    \"worker-configuration.d.ts\"\n  ],\n  \"compilerOptions\": {\n    \"composite\": true,\n    \"strict\": true,\n    \"lib\": [\"DOM\", \"DOM.Iterable\", \"ES2022\"],\n    \"types\": [\"vite/client\"],\n    \"target\": \"ES2022\",\n    \"module\": \"ES2022\",\n    \"moduleResolution\": \"bundler\",\n    \"jsx\": \"react-jsx\",\n    \"baseUrl\": \".\",\n    \"rootDirs\": [\".\", \"./.react-router/types\"],\n    \"paths\": {\n      \"~/*\": [\"./app/*\"]\n    },\n    \"esModuleInterop\": true,\n    \"resolveJsonModule\": true\n  }\n}\n"
  },
  {
    "path": "integration/helpers/vite-plugin-cloudflare-template/tsconfig.json",
    "content": "{\n  \"files\": [],\n  \"references\": [\n    { \"path\": \"./tsconfig.node.json\" },\n    { \"path\": \"./tsconfig.cloudflare.json\" }\n  ],\n  \"compilerOptions\": {\n    \"checkJs\": true,\n    \"verbatimModuleSyntax\": true,\n    \"skipLibCheck\": true,\n    \"strict\": true,\n    \"noEmit\": true\n  }\n}\n"
  },
  {
    "path": "integration/helpers/vite-plugin-cloudflare-template/tsconfig.node.json",
    "content": "{\n  \"extends\": \"./tsconfig.json\",\n  \"include\": [\"react-router.config.ts\", \"vite.config.ts\"],\n  \"compilerOptions\": {\n    \"composite\": true,\n    \"strict\": true,\n    \"types\": [\"node\"],\n    \"lib\": [\"ES2022\"],\n    \"target\": \"ES2022\",\n    \"module\": \"ES2022\",\n    \"moduleResolution\": \"bundler\"\n  }\n}\n"
  },
  {
    "path": "integration/helpers/vite-plugin-cloudflare-template/vite.config.ts",
    "content": "import { reactRouter } from \"@react-router/dev/vite\";\nimport { defineConfig } from \"vite\";\nimport tsconfigPaths from \"vite-tsconfig-paths\";\nimport { cloudflare } from \"@cloudflare/vite-plugin\";\n\nexport default defineConfig({\n  plugins: [\n    cloudflare({ viteEnvironment: { name: \"ssr\" } }),\n    reactRouter(),\n    tsconfigPaths(),\n  ],\n});\n"
  },
  {
    "path": "integration/helpers/vite-plugin-cloudflare-template/workers/app.ts",
    "content": "import { createRequestHandler } from \"react-router\";\n\ndeclare global {\n  interface CloudflareEnvironment extends Env {}\n}\n\ndeclare module \"react-router\" {\n  export interface AppLoadContext {\n    cloudflare: {\n      env: CloudflareEnvironment;\n      ctx: ExecutionContext;\n    };\n  }\n}\n\nconst requestHandler = createRequestHandler(\n  () => import(\"virtual:react-router/server-build\"),\n  import.meta.env.MODE,\n);\n\nexport default {\n  async fetch(request, env, ctx) {\n    return requestHandler(request, {\n      cloudflare: { env, ctx },\n    });\n  },\n} satisfies ExportedHandler<CloudflareEnvironment>;\n"
  },
  {
    "path": "integration/helpers/vite-plugin-cloudflare-template/wrangler.toml",
    "content": "name = \"react-router-app\"\ncompatibility_date = \"2025-03-17\"\nmain = \"./workers/app.ts\"\n\nassets = {}\n\n[vars]\nVALUE_FROM_CLOUDFLARE = \"Hello from Cloudflare\"\n"
  },
  {
    "path": "integration/helpers/vite-rolldown-template/.gitignore",
    "content": "node_modules\n\n/.cache\n/build\n.env\n.react-router\n"
  },
  {
    "path": "integration/helpers/vite-rolldown-template/app/root.tsx",
    "content": "import { Links, Meta, Outlet, Scripts, ScrollRestoration } from \"react-router\";\n\nexport default function App() {\n  return (\n    <html lang=\"en\">\n      <head>\n        <meta charSet=\"utf-8\" />\n        <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />\n        <Meta />\n        <Links />\n      </head>\n      <body>\n        <Outlet />\n        <ScrollRestoration />\n        <Scripts />\n      </body>\n    </html>\n  );\n}\n"
  },
  {
    "path": "integration/helpers/vite-rolldown-template/app/routes/_index.tsx",
    "content": "import type { MetaFunction } from \"react-router\";\n\nexport const meta: MetaFunction = () => {\n  return [\n    { title: \"New React Router App\" },\n    { name: \"description\", content: \"Welcome to React Router!\" },\n  ];\n};\n\nexport default function Index() {\n  return (\n    <div style={{ fontFamily: \"system-ui, sans-serif\", lineHeight: \"1.8\" }}>\n      <h1>Welcome to React Router</h1>\n    </div>\n  );\n}\n"
  },
  {
    "path": "integration/helpers/vite-rolldown-template/app/routes.ts",
    "content": "import { type RouteConfig } from \"@react-router/dev/routes\";\nimport { flatRoutes } from \"@react-router/fs-routes\";\n\nexport default flatRoutes() satisfies RouteConfig;\n"
  },
  {
    "path": "integration/helpers/vite-rolldown-template/env.d.ts",
    "content": "/// <reference types=\"@react-router/node\" />\n/// <reference types=\"vite/client\" />\n"
  },
  {
    "path": "integration/helpers/vite-rolldown-template/package.json",
    "content": "{\n  \"name\": \"integration-vite-rolldown-template\",\n  \"version\": \"0.0.0\",\n  \"private\": true,\n  \"sideEffects\": false,\n  \"type\": \"module\",\n  \"scripts\": {\n    \"dev\": \"react-router dev\",\n    \"build\": \"cross-env ROLLDOWN_OPTIONS_VALIDATION=loose react-router build\",\n    \"start\": \"react-router-serve ./build/server/index.js\",\n    \"typecheck\": \"react-router typegen && tsc\"\n  },\n  \"dependencies\": {\n    \"@react-router/express\": \"workspace:*\",\n    \"@react-router/node\": \"workspace:*\",\n    \"@react-router/serve\": \"workspace:*\",\n    \"@vanilla-extract/css\": \"^1.17.4\",\n    \"@vanilla-extract/vite-plugin\": \"^5.1.1\",\n    \"express\": \"^4.19.2\",\n    \"isbot\": \"^5.1.11\",\n    \"react\": \"catalog:\",\n    \"react-dom\": \"catalog:\",\n    \"react-router\": \"workspace:*\",\n    \"serialize-javascript\": \"^6.0.1\"\n  },\n  \"devDependencies\": {\n    \"@react-router/dev\": \"workspace:*\",\n    \"@react-router/fs-routes\": \"workspace:*\",\n    \"@react-router/remix-routes-option-adapter\": \"workspace:*\",\n    \"@types/react\": \"catalog:\",\n    \"@types/react-dom\": \"catalog:\",\n    \"cross-env\": \"^7.0.3\",\n    \"eslint\": \"^8.38.0\",\n    \"typescript\": \"catalog:\",\n    \"vite\": \"npm:rolldown-vite@6.3.0-beta.5\",\n    \"vite-env-only\": \"^3.0.1\",\n    \"vite-tsconfig-paths\": \"^4.2.1\"\n  },\n  \"overrides\": {\n    \"vite\": \"npm:rolldown-vite@6.3.0-beta.5\"\n  },\n  \"engines\": {\n    \"node\": \">=20.0.0\"\n  }\n}\n"
  },
  {
    "path": "integration/helpers/vite-rolldown-template/tsconfig.json",
    "content": "{\n  \"include\": [\"env.d.ts\", \"**/*.ts\", \"**/*.tsx\", \".react-router/types/**/*\"],\n  \"compilerOptions\": {\n    \"lib\": [\"DOM\", \"DOM.Iterable\", \"ES2022\"],\n    \"verbatimModuleSyntax\": true,\n    \"esModuleInterop\": true,\n    \"jsx\": \"react-jsx\",\n    \"module\": \"ESNext\",\n    \"moduleResolution\": \"Bundler\",\n    \"resolveJsonModule\": true,\n    \"target\": \"ES2022\",\n    \"strict\": true,\n    \"allowJs\": true,\n    \"skipLibCheck\": true,\n    \"baseUrl\": \".\",\n    \"paths\": {\n      \"~/*\": [\"./app/*\"]\n    },\n    \"noEmit\": true,\n    \"rootDirs\": [\".\", \".react-router/types/\"]\n  }\n}\n"
  },
  {
    "path": "integration/helpers/vite-rolldown-template/vite.config.ts",
    "content": "import { reactRouter } from \"@react-router/dev/vite\";\nimport { defineConfig } from \"vite\";\nimport tsconfigPaths from \"vite-tsconfig-paths\";\n\nexport default defineConfig({\n  plugins: [\n    // @ts-expect-error `dev` depends on Vite 6, Plugin type is mismatched.\n    reactRouter(),\n    tsconfigPaths(),\n  ],\n});\n"
  },
  {
    "path": "integration/helpers/vite.ts",
    "content": "import type { ChildProcess } from \"node:child_process\";\nimport { sync as spawnSync, spawn } from \"cross-spawn\";\nimport { cp, mkdir, readFile, writeFile } from \"node:fs/promises\";\nimport { createRequire } from \"node:module\";\nimport { platform } from \"node:os\";\nimport type { Readable } from \"node:stream\";\nimport url from \"node:url\";\nimport path from \"pathe\";\nimport stripIndent from \"strip-indent\";\nimport waitOn from \"wait-on\";\nimport getPort from \"get-port\";\nimport shell from \"shelljs\";\nimport glob from \"glob\";\nimport dedent from \"dedent\";\nimport type { Page } from \"@playwright/test\";\nimport { test as base, expect } from \"@playwright/test\";\nimport type { Config } from \"@react-router/dev/config\";\n\nconst require = createRequire(import.meta.url);\n\nconst reactRouterBin = \"node_modules/@react-router/dev/bin.js\";\nconst __dirname = url.fileURLToPath(new URL(\".\", import.meta.url));\nconst root = path.resolve(__dirname, \"../..\");\nconst TMP_DIR = path.join(root, \".tmp/integration\");\n\nexport const reactRouterConfig = (\n  // Don't support function configs due to JSON.stringify()\n  config: Omit<Partial<Config>, \"buildEnd\" | \"presets\" | \"serverBundles\">,\n) => {\n  if (\n    typeof config.prerender === \"function\" ||\n    (typeof config.prerender === \"object\" &&\n      !Array.isArray(config.prerender) &&\n      typeof config.prerender.paths === \"function\")\n  ) {\n    throw new Error(\"reactRouterConfig() does not support prerender functions\");\n  }\n\n  return dedent`\n    import type { Config } from \"@react-router/dev/config\";\n\n    export default ${JSON.stringify(config, null, 2)} satisfies Config;\n  `;\n};\n\ntype ViteConfigServerArgs = {\n  port: number;\n  fsAllow?: string[];\n};\n\ntype ViteConfigBuildArgs = {\n  assetsInlineLimit?: number;\n  assetsDir?: string;\n  cssCodeSplit?: boolean;\n};\n\ntype ViteConfigBaseArgs = {\n  templateName?: TemplateName;\n  base?: string;\n  envDir?: string;\n  mdx?: boolean;\n  vanillaExtract?: boolean;\n};\n\ntype ViteConfigArgs = (\n  | ViteConfigServerArgs\n  | { [K in keyof ViteConfigServerArgs]?: never }\n) &\n  ViteConfigBuildArgs &\n  ViteConfigBaseArgs;\n\nexport const viteConfig = {\n  server: async (args: ViteConfigServerArgs) => {\n    let { port, fsAllow } = args;\n    let hmrPort = await getPort();\n    let text = dedent`\n      server: {\n        port: ${port},\n        strictPort: true,\n        hmr: { port: ${hmrPort} },\n        fs: { allow: ${fsAllow ? JSON.stringify(fsAllow) : \"undefined\"} }\n      },\n    `;\n    return text;\n  },\n  build: ({\n    assetsInlineLimit,\n    assetsDir,\n    cssCodeSplit,\n  }: ViteConfigBuildArgs = {}) => {\n    return dedent`\n      build: {\n        // Detect rolldown-vite. This should ideally use \"rolldownVersion\"\n        // but that's not exported. Once that's available, this\n        // check should be updated to use it.\n        rollupOptions: \"transformWithOxc\" in (await import(\"vite\"))\n          ? {\n              onwarn(warning, warn) {\n                // Ignore \"The built-in minifier is still under development.\" warning\n                if (warning.code === \"MINIFY_WARNING\") return;\n                warn(warning);\n              },\n            }\n          : undefined,\n        assetsInlineLimit: ${assetsInlineLimit ?? \"undefined\"},\n        assetsDir: ${assetsDir ? `\"${assetsDir}\"` : \"undefined\"},\n        cssCodeSplit: ${\n          cssCodeSplit !== undefined ? cssCodeSplit : \"undefined\"\n        },\n      },\n    `;\n  },\n  basic: async (args: ViteConfigArgs) => {\n    const isRsc = args.templateName?.includes(\"rsc\");\n    return dedent`\n      ${\n        !isRsc\n          ? \"import { reactRouter } from '@react-router/dev/vite';\"\n          : [\n              \"import { unstable_reactRouterRSC as reactRouterRSC } from '@react-router/dev/vite';\",\n              \"import rsc from '@vitejs/plugin-rsc';\",\n            ].join(\"\\n\")\n      }\n      ${args.mdx ? 'import mdx from \"@mdx-js/rollup\";' : \"\"}\n      ${args.vanillaExtract ? 'import { vanillaExtractPlugin } from \"@vanilla-extract/vite-plugin\";' : \"\"}\n      import { envOnlyMacros } from \"vite-env-only\";\n      import tsconfigPaths from \"vite-tsconfig-paths\";\n\n      export default async () => ({\n        ${args.port ? await viteConfig.server(args) : \"\"}\n        ${viteConfig.build(args)}\n        ${args.base ? `base: \"${args.base}\",` : \"\"}\n        envDir: ${args.envDir ? `\"${args.envDir}\"` : \"undefined\"},\n        plugins: [\n          ${args.mdx ? \"mdx(),\" : \"\"}\n          ${args.vanillaExtract ? \"vanillaExtractPlugin({ emitCssInSsr: true }),\" : \"\"}\n          ${isRsc ? \"    reactRouterRSC({ __runningWithinTheReactRouterMonoRepo: true }),\" : \"reactRouter(),\"}\n          ${isRsc ? \"rsc(),\" : \"\"}\n          envOnlyMacros(),\n          tsconfigPaths()\n        ],\n      });\n    `;\n  },\n};\n\nexport const EXPRESS_SERVER = (args: {\n  port: number;\n  base?: string;\n  loadContext?: Record<string, unknown>;\n  customLogic?: string;\n  templateName?: TemplateName;\n}) => {\n  if (args.templateName?.includes(\"rsc\")) {\n    return String.raw`\n      import { createRequestListener } from \"@mjackson/node-fetch-server\";\n      import express from \"express\";\n\n      const viteDevServer =\n        process.env.NODE_ENV === \"production\"\n          ? undefined\n          : await import(\"vite\").then(({ createServer }) =>\n              createServer({\n                server: {\n                  middlewareMode: true,\n                },\n              })\n            );\n      const app = express();\n\n      ${args?.customLogic || \"\"}\n\n      if (viteDevServer) {\n        app.use(viteDevServer.middlewares);\n      } else {\n        app.use(\n          \"/assets\",\n          express.static(\"build/client/assets\", { immutable: true, maxAge: \"1y\" })\n        );\n        app.all(\"*\", createRequestListener((await import(\"./build/server/index.js\")).default));\n      }\n\n      const port = ${args.port};\n      app.listen(port, () => console.log('http://localhost:' + port));\n    `;\n  }\n\n  return String.raw`\n    import { createRequestHandler } from \"@react-router/express\";\n    import express from \"express\";\n\n    let viteDevServer =\n      process.env.NODE_ENV === \"production\"\n        ? undefined\n        : await import(\"vite\").then((vite) =>\n            vite.createServer({\n              server: { middlewareMode: true },\n            })\n          );\n\n    const app = express();\n\n    if (viteDevServer) {\n      app.use(viteDevServer.middlewares);\n    } else {\n      app.use(\n        \"/assets\",\n        express.static(\"build/client/assets\", { immutable: true, maxAge: \"1y\" })\n      );\n    }\n    app.use(express.static(\"build/client\", { maxAge: \"1h\" }));\n\n    ${args?.customLogic || \"\"}\n\n    app.all(\n      \"*\",\n      createRequestHandler({\n        build: viteDevServer\n          ? () => viteDevServer.ssrLoadModule(\"virtual:react-router/server-build\")\n          : await import(\"./build/index.js\"),\n        getLoadContext: () => (${JSON.stringify(args.loadContext ?? {})}),\n      })\n    );\n\n    const port = ${args.port};\n    app.listen(port, () => console.log('http://localhost:' + port));\n  `;\n};\n\ntype FrameworkModeViteMajorTemplateName =\n  | \"vite-5-template\"\n  | \"vite-6-template\"\n  | \"vite-7-beta-template\"\n  | \"vite-plugin-cloudflare-template\"\n  | \"vite-rolldown-template\";\n\ntype FrameworkModeRscTemplateName = \"rsc-vite-framework\";\n\ntype FrameworkModeCloudflareTemplateName =\n  | \"cloudflare-dev-proxy-template\"\n  | \"vite-plugin-cloudflare-template\";\n\nexport type RscBundlerTemplateName = \"rsc-vite\";\n\nexport type TemplateName =\n  | FrameworkModeViteMajorTemplateName\n  | FrameworkModeRscTemplateName\n  | FrameworkModeCloudflareTemplateName\n  | RscBundlerTemplateName;\n\nexport const viteMajorTemplates = [\n  { templateName: \"vite-5-template\", templateDisplayName: \"Vite 5\" },\n  { templateName: \"vite-6-template\", templateDisplayName: \"Vite 6\" },\n  { templateName: \"vite-7-beta-template\", templateDisplayName: \"Vite 7 Beta\" },\n  {\n    templateName: \"vite-rolldown-template\",\n    templateDisplayName: \"Vite Rolldown\",\n  },\n] as const satisfies Array<{\n  templateName: FrameworkModeViteMajorTemplateName;\n  templateDisplayName: string;\n}>;\n\nexport const rscBundlerTemplates = [\n  { templateName: \"rsc-vite\", templateDisplayName: \"RSC (Vite)\" },\n] as const satisfies Array<{\n  templateName: RscBundlerTemplateName;\n  templateDisplayName: string;\n}>;\n\nexport async function createProject(\n  files: Record<string, string> = {},\n  templateName: TemplateName = \"vite-5-template\",\n) {\n  let projectName = `rr-${Math.random().toString(32).slice(2)}`;\n  let projectDir = path.join(TMP_DIR, projectName);\n  await mkdir(projectDir, { recursive: true });\n\n  // base template\n  let templateDir = path.resolve(__dirname, templateName);\n  await cp(templateDir, projectDir, { errorOnExist: true, recursive: true });\n\n  // user-defined files\n  await Promise.all(\n    Object.entries(files).map(async ([filename, contents]) => {\n      let filepath = path.join(projectDir, filename);\n      await mkdir(path.dirname(filepath), { recursive: true });\n      await writeFile(filepath, stripIndent(contents));\n    }),\n  );\n\n  return projectDir;\n}\n\n// Avoid \"Warning: The 'NO_COLOR' env is ignored due to the 'FORCE_COLOR' env\n// being set\" in vite-ecosystem-ci which breaks empty stderr assertions. To fix\n// this, we always ensure that only NO_COLOR is set after spreading process.env.\nconst colorEnv = {\n  FORCE_COLOR: undefined,\n  NO_COLOR: \"1\",\n} as const;\n\nexport const build = ({\n  cwd,\n  env = {},\n}: {\n  cwd: string;\n  env?: Record<string, string>;\n}) => {\n  let nodeBin = process.argv[0];\n\n  return spawnSync(nodeBin, [reactRouterBin, \"build\"], {\n    cwd,\n    env: {\n      ...process.env,\n      ...colorEnv,\n      ...env,\n      // Ensure build can pass in Rolldown. This can be removed once\n      // \"preserveEntrySignatures\" is supported in rolldown-vite.\n      ROLLDOWN_OPTIONS_VALIDATION: \"loose\",\n    },\n  });\n};\n\nexport const reactRouterServe = async ({\n  cwd,\n  port,\n  serverBundle,\n  basename,\n}: {\n  cwd: string;\n  port: number;\n  serverBundle?: string;\n  basename?: string;\n}) => {\n  let nodeBin = process.argv[0];\n  let serveProc = spawn(\n    nodeBin,\n    [\n      \"node_modules/@react-router/serve/dist/cli.js\",\n      `build/server/${serverBundle ? serverBundle + \"/\" : \"\"}index.js`,\n    ],\n    {\n      cwd,\n      stdio: \"pipe\",\n      env: { NODE_ENV: \"production\", PORT: port.toFixed(0) },\n    },\n  );\n  await waitForServer(serveProc, { port, basename });\n  return () => serveProc.kill();\n};\n\nexport const wranglerPagesDev = async ({\n  cwd,\n  port,\n}: {\n  cwd: string;\n  port: number;\n}) => {\n  let nodeBin = process.argv[0];\n  let wranglerBin = require.resolve(\"wrangler/bin/wrangler.js\", {\n    paths: [cwd],\n  });\n\n  let proc = spawn(\n    nodeBin,\n    [wranglerBin, \"pages\", \"dev\", \"./build/client\", \"--port\", String(port)],\n    {\n      cwd,\n      stdio: \"pipe\",\n      env: { NODE_ENV: \"production\" },\n    },\n  );\n  await waitForServer(proc, { port, host: \"127.0.0.1\" });\n  return () => proc.kill();\n};\n\ntype ServerArgs = {\n  cwd: string;\n  port: number;\n  env?: Record<string, string>;\n  basename?: string;\n};\n\nexport const createDev =\n  (nodeArgs: string[]) =>\n  async ({ cwd, port, env, basename }: ServerArgs): Promise<() => unknown> => {\n    let proc = node(nodeArgs, { cwd, env });\n    await waitForServer(proc, { port, basename });\n    return () => proc.kill();\n  };\n\nexport const dev = createDev([reactRouterBin, \"dev\"]);\nexport const customDev = createDev([\"./server.mjs\"]);\n\nexport const vitePreview = async ({\n  cwd,\n  port,\n}: {\n  cwd: string;\n  port: number;\n}) => {\n  let nodeBin = process.argv[0];\n  let viteBin = path.join(cwd, \"node_modules\", \"vite\", \"bin\", \"vite.js\");\n  let proc = spawn(\n    nodeBin,\n    [viteBin, \"preview\", \"--port\", String(port), \"--strict-port\"],\n    {\n      cwd,\n      stdio: \"pipe\",\n      env: { NODE_ENV: \"production\" },\n    },\n  );\n  await waitForServer(proc, { port });\n  return () => proc.kill();\n};\n\n// Used for testing errors thrown on build when we don't want to start and\n// wait for the server\nexport const viteDevCmd = ({ cwd }: { cwd: string }) => {\n  let nodeBin = process.argv[0];\n  return spawnSync(nodeBin, [reactRouterBin, \"dev\"], {\n    cwd,\n    env: { ...process.env },\n  });\n};\n\ndeclare module \"@playwright/test\" {\n  interface Page {\n    errors: Error[];\n  }\n}\n\nexport type Files = (args: { port: number }) => Promise<Record<string, string>>;\ntype Fixtures = {\n  page: Page;\n  dev: (\n    files: Files,\n    templateName?: TemplateName,\n  ) => Promise<{\n    port: number;\n    cwd: string;\n  }>;\n  customDev: (\n    files: Files,\n    templateName?: TemplateName,\n  ) => Promise<{\n    port: number;\n    cwd: string;\n  }>;\n  reactRouterServe: (files: Files) => Promise<{\n    port: number;\n    cwd: string;\n  }>;\n  vitePreview: (\n    files: Files,\n    templateName?: TemplateName,\n  ) => Promise<{\n    port: number;\n    cwd: string;\n  }>;\n  wranglerPagesDev: (files: Files) => Promise<{\n    port: number;\n    cwd: string;\n  }>;\n};\n\nexport const test = base.extend<Fixtures>({\n  page: async ({ page }, use) => {\n    page.errors = [];\n    page.on(\"pageerror\", (error: Error) => page.errors.push(error));\n    await use(page);\n  },\n  // eslint-disable-next-line no-empty-pattern\n  dev: async ({}, use) => {\n    let stop: (() => unknown) | undefined;\n    await use(async (files, template) => {\n      let port = await getPort();\n      let cwd = await createProject(await files({ port }), template);\n      stop = await dev({ cwd, port });\n      return { port, cwd };\n    });\n    stop?.();\n  },\n  // eslint-disable-next-line no-empty-pattern\n  customDev: async ({}, use) => {\n    let stop: (() => unknown) | undefined;\n    await use(async (files, template) => {\n      let port = await getPort();\n      let cwd = await createProject(await files({ port }), template);\n      stop = await customDev({ cwd, port });\n      return { port, cwd };\n    });\n    stop?.();\n  },\n  // eslint-disable-next-line no-empty-pattern\n  reactRouterServe: async ({}, use) => {\n    let stop: (() => unknown) | undefined;\n    await use(async (files) => {\n      let port = await getPort();\n      let cwd = await createProject(await files({ port }));\n      let { status } = build({ cwd });\n      expect(status).toBe(0);\n      stop = await reactRouterServe({ cwd, port });\n      return { port, cwd };\n    });\n    stop?.();\n  },\n  vitePreview: async ({}, use) => {\n    let stop: (() => unknown) | undefined;\n    await use(async (files, template) => {\n      let port = await getPort();\n      let cwd = await createProject(await files({ port }), template);\n      let { status } = build({ cwd });\n      expect(status).toBe(0);\n      stop = await vitePreview({ cwd, port });\n      return { port, cwd };\n    });\n    stop?.();\n  },\n  // eslint-disable-next-line no-empty-pattern\n  wranglerPagesDev: async ({}, use) => {\n    let stop: (() => unknown) | undefined;\n    await use(async (files) => {\n      let port = await getPort();\n      let cwd = await createProject(\n        await files({ port }),\n        \"cloudflare-dev-proxy-template\",\n      );\n      let { status } = build({ cwd });\n      expect(status).toBe(0);\n      stop = await wranglerPagesDev({ cwd, port });\n      return { port, cwd };\n    });\n    stop?.();\n  },\n});\n\nfunction node(\n  args: string[],\n  options: { cwd: string; env?: Record<string, string> },\n) {\n  let nodeBin = process.argv[0];\n\n  let proc = spawn(nodeBin, args, {\n    cwd: options.cwd,\n    env: {\n      ...process.env,\n      ...colorEnv,\n      ...options.env,\n    },\n    stdio: \"pipe\",\n  });\n  return proc;\n}\n\nasync function waitForServer(\n  proc: ChildProcess & { stdout: Readable; stderr: Readable },\n  args: { port: number; host?: string; basename?: string },\n) {\n  let devStdout = bufferize(proc.stdout);\n  let devStderr = bufferize(proc.stderr);\n\n  await waitOn({\n    resources: [\n      `http://${args.host ?? \"localhost\"}:${args.port}${\n        args.basename ?? \"/favicon.ico\"\n      }`,\n    ],\n    timeout: platform() === \"win32\" ? 20000 : 10000,\n  }).catch((err) => {\n    let stdout = devStdout();\n    let stderr = devStderr();\n    proc.kill();\n    throw new Error(\n      [\n        err.message,\n        \"\",\n        \"exit code: \" + proc.exitCode,\n        \"stdout: \" + stdout ? `\\n${stdout}\\n` : \"<empty>\",\n        \"stderr: \" + stderr ? `\\n${stderr}\\n` : \"<empty>\",\n      ].join(\"\\n\"),\n    );\n  });\n}\n\nfunction bufferize(stream: Readable): () => string {\n  let buffer = \"\";\n  stream.on(\"data\", (data) => (buffer += data.toString()));\n  return () => buffer;\n}\n\nexport function createEditor(projectDir: string) {\n  return async function edit(\n    file: string,\n    transform: (contents: string) => string,\n  ) {\n    let filepath = path.join(projectDir, file);\n    let contents = await readFile(filepath, \"utf8\");\n    await writeFile(filepath, transform(contents), \"utf8\");\n\n    return async function revert() {\n      await writeFile(filepath, contents, \"utf8\");\n    };\n  };\n}\n\nexport function grep(cwd: string, pattern: RegExp): string[] {\n  let assetFiles = glob.sync(\"**/*.@(js|jsx|ts|tsx)\", {\n    cwd,\n    absolute: true,\n  });\n\n  let lines = shell\n    .grep(\"-l\", pattern, assetFiles)\n    .stdout.trim()\n    .split(\"\\n\")\n    .filter((line) => line.length > 0);\n  return lines;\n}\n"
  },
  {
    "path": "integration/hook-useSubmit-test.ts",
    "content": "import { test, expect } from \"@playwright/test\";\n\nimport {\n  createAppFixture,\n  createFixture,\n  js,\n} from \"./helpers/create-fixture.js\";\nimport type { Fixture, AppFixture } from \"./helpers/create-fixture.js\";\nimport { PlaywrightFixture } from \"./helpers/playwright-fixture.js\";\n\ntest.describe(\"`useSubmit()` returned function\", () => {\n  let fixture: Fixture;\n  let appFixture: AppFixture;\n\n  test.beforeAll(async () => {\n    fixture = await createFixture({\n      files: {\n        \"app/routes/_index.tsx\": js`\n          import { useLoaderData, useSubmit } from \"react-router\";\n\n          export function loader({ request }) {\n            let url = new URL(request.url);\n            return url.searchParams.toString()\n          }\n\n          export default function Index() {\n            let submit = useSubmit();\n            let handleClick = event => {\n              event.preventDefault()\n              submit(event.nativeEvent.submitter || event.currentTarget)\n            }\n            let data = useLoaderData();\n            return (\n              <form>\n                <input type=\"text\" name=\"tasks\" defaultValue=\"first\" />\n                <input type=\"text\" name=\"tasks\" defaultValue=\"second\" />\n\n                <button onClick={handleClick} name=\"tasks\" value=\"third\">\n                  Prepare Third Task\n                </button>\n\n                <pre>{data}</pre>\n              </form>\n            )\n          }\n        `,\n        \"app/routes/action.tsx\": js`\n          import { useActionData, useSubmit } from \"react-router\";\n\n          export async function action({ request }) {\n            let contentType = request.headers.get('Content-Type');\n            if (contentType.includes('application/json')) {\n              return { value: await request.json() };\n            }\n            if (contentType.includes('text/plain')) {\n              return { value: await request.text() };\n            }\n            let fd = await request.formData();\n            return { value: new URLSearchParams(fd.entries()).toString() }\n          }\n\n          export default function Component() {\n            let submit = useSubmit();\n            let data = useActionData();\n            return (\n              <>\n                <button id=\"submit-json\" onClick={() => submit(\n                  { key: 'value' },\n                  { method: 'post', encType: 'application/json' },\n                )}>\n                  Submit JSON\n                </button>\n                <button id=\"submit-text\" onClick={() => submit(\n                  \"raw text\",\n                  { method: 'post', encType: 'text/plain' },\n                )}>\n                  Submit Text\n                </button>\n                <button id=\"submit-formData\" onClick={() => submit(\n                  { key: 'value' },\n                  { method: 'post' },\n                )}>\n                  Submit FrmData\n                </button>\n                {data ? <p id=\"action-data\">data: {JSON.stringify(data)}</p> : null}\n              </>\n            );\n          }\n        `,\n      },\n    });\n\n    appFixture = await createAppFixture(fixture);\n  });\n\n  test.afterAll(() => {\n    appFixture.close();\n  });\n\n  test(\"submits the submitter's value appended to the form data\", async ({\n    page,\n  }) => {\n    let app = new PlaywrightFixture(appFixture, page);\n    await app.goto(\"/\");\n    await app.clickElement(\"text=Prepare Third Task\");\n    await page.waitForLoadState(\"load\");\n    expect(await app.getHtml(\"pre\")).toBe(\n      `<pre>tasks=first&amp;tasks=second&amp;tasks=third</pre>`,\n    );\n  });\n\n  test(\"submits json data\", async ({ page }) => {\n    let app = new PlaywrightFixture(appFixture, page);\n    await app.goto(\"/action\", true);\n    await app.clickElement(\"#submit-json\");\n    await page.waitForSelector(\"#action-data\");\n    expect(await app.getHtml()).toMatch('data: {\"value\":{\"key\":\"value\"}}');\n  });\n\n  test(\"submits text data\", async ({ page }) => {\n    let app = new PlaywrightFixture(appFixture, page);\n    await app.goto(\"/action\", true);\n    await app.clickElement(\"#submit-text\");\n    await page.waitForSelector(\"#action-data\");\n    expect(await app.getHtml()).toMatch('data: {\"value\":\"raw text\"}');\n  });\n\n  test(\"submits form data\", async ({ page }) => {\n    let app = new PlaywrightFixture(appFixture, page);\n    await app.goto(\"/action\", true);\n    await app.clickElement(\"#submit-formData\");\n    await page.waitForSelector(\"#action-data\");\n    expect(await app.getHtml()).toMatch('data: {\"value\":\"key=value\"}');\n  });\n});\n"
  },
  {
    "path": "integration/http-test.ts",
    "content": "import { test, expect } from \"@playwright/test\";\n\nimport { UNSAFE_ServerMode as ServerMode } from \"react-router\";\nimport { createFixture, js } from \"./helpers/create-fixture.js\";\nimport type { Fixture } from \"./helpers/create-fixture.js\";\n\ntest.describe(\"HTTP behavior\", () => {\n  let appFixture: Fixture;\n\n  test.beforeAll(async () => {\n    appFixture = await createFixture({\n      files: {\n        \"app/root.tsx\": js`\n            import { Links, Meta, Outlet, Scripts } from \"react-router\";\n\n            export default function Root() {\n              return (\n                <html lang=\"en\">\n                  <head>\n                    <Meta />\n                    <Links />\n                  </head>\n                  <body>\n                    <Outlet />\n                    <Scripts />\n                  </body>\n                </html>\n              );\n            }\n          `,\n\n        \"app/routes/_index.tsx\": js`\n            import { data } from \"react-router\";\n\n            export function loader() {\n              return data(\"INDEX\", { status: 202 })\n            }\n\n            export default function Index() {\n              return <div>Heyo!</div>\n            }\n          `,\n\n        \"app/routes/resource.tsx\": js`\n            export function loader() {\n              return new Response(\"RESOURCE\", { status: 202 })\n            }\n          `,\n      },\n    });\n  });\n\n  test(\"includes body on GET request\", async () => {\n    let response = await appFixture.requestDocument(\"/\");\n    expect(response.status).toBe(202);\n    expect(await response.text()).toContain(\"<div>Heyo!</div>\");\n\n    response = await appFixture.requestResource(\"/resource\");\n    expect(response.status).toBe(202);\n    expect(await response.text()).toBe(\"RESOURCE\");\n\n    let singleFetchResponse =\n      await appFixture.requestSingleFetchData(\"/_root.data\");\n    expect(response.status).toBe(202);\n    expect(singleFetchResponse.data).toEqual({\n      \"routes/_index\": { data: \"INDEX\" },\n    });\n  });\n\n  test(\"does not include body on HEAD request\", async () => {\n    let response = await appFixture.requestDocument(\"/\", { method: \"HEAD\" });\n    expect(response.status).toBe(202);\n    expect(response.body).toBe(null);\n\n    response = await appFixture.requestResource(\"/resource\", {\n      method: \"HEAD\",\n    });\n    expect(response.status).toBe(202);\n    expect(response.body).toBe(null);\n\n    let singleFetchResponse = await appFixture.requestSingleFetchData(\n      \"/_root.data\",\n      { method: \"HEAD\" },\n    );\n    expect(response.status).toBe(202);\n    expect(singleFetchResponse.data).toBe(null);\n  });\n});\n"
  },
  {
    "path": "integration/layout-route-test.ts",
    "content": "import { test } from \"@playwright/test\";\n\nimport {\n  createAppFixture,\n  createFixture,\n  js,\n} from \"./helpers/create-fixture.js\";\nimport type { AppFixture } from \"./helpers/create-fixture.js\";\nimport { PlaywrightFixture } from \"./helpers/playwright-fixture.js\";\n\ntest.describe(\"pathless layout routes\", () => {\n  let appFixture: AppFixture;\n\n  test.beforeAll(async () => {\n    appFixture = await createAppFixture(\n      await createFixture({\n        files: {\n          \"app/routes/_layout.tsx\": js`\n            import { Outlet } from \"react-router\";\n\n            export default () => <div data-testid=\"layout-route\"><Outlet /></div>;\n          `,\n          \"app/routes/_layout._index.tsx\": js`\n            export default () => <div data-testid=\"layout-index\">Layout index</div>;\n          `,\n          \"app/routes/_layout.subroute.tsx\": js`\n            export default () => <div data-testid=\"layout-subroute\">Layout subroute</div>;\n          `,\n          \"app/routes/sandwiches._pathless.tsx\": js`\n            import { Outlet } from \"react-router\";\n\n            export default () => <div data-testid=\"sandwiches-pathless-route\"><Outlet /></div>;\n          `,\n          \"app/routes/sandwiches._pathless._index.tsx\": js`\n            export default () => <div data-testid=\"sandwiches-pathless-index\">Sandwiches pathless index</div>;\n          `,\n        },\n      }),\n    );\n  });\n\n  test.afterAll(() => {\n    appFixture.close();\n  });\n\n  test(\"should render pathless index route\", async ({ page }) => {\n    let app = new PlaywrightFixture(appFixture, page);\n    await app.goto(\"/\");\n    await page.waitForSelector(\"[data-testid='layout-route']\");\n    await page.waitForSelector(\"[data-testid='layout-index']\");\n  });\n\n  test(\"should render pathless sub route\", async ({ page }) => {\n    let app = new PlaywrightFixture(appFixture, page);\n    await app.goto(\"/subroute\");\n    await page.waitForSelector(\"[data-testid='layout-route']\");\n    await page.waitForSelector(\"[data-testid='layout-subroute']\");\n  });\n\n  test(\"should render pathless index as a sub route\", async ({ page }) => {\n    let app = new PlaywrightFixture(appFixture, page);\n    await app.goto(\"/sandwiches\");\n    await page.waitForSelector(\"[data-testid='sandwiches-pathless-route']\");\n    await page.waitForSelector(\"[data-testid='sandwiches-pathless-index']\");\n  });\n});\n"
  },
  {
    "path": "integration/link-test.ts",
    "content": "import { test, expect } from \"@playwright/test\";\n\nimport {\n  css,\n  js,\n  createFixture,\n  createAppFixture,\n} from \"./helpers/create-fixture.js\";\nimport type { Fixture, AppFixture } from \"./helpers/create-fixture.js\";\nimport { type TemplateName } from \"./helpers/vite.js\";\nimport { PlaywrightFixture } from \"./helpers/playwright-fixture.js\";\n\nconst templateNames = [\n  \"vite-5-template\",\n  \"rsc-vite-framework\",\n] as const satisfies TemplateName[];\n\nconst fakeGists = [\n  {\n    url: \"https://api.github.com/gists/610613b54e5b34f8122d1ba4a3da21a9\",\n    id: \"610613b54e5b34f8122d1ba4a3da21a9\",\n    files: {\n      \"remix-server.jsx\": {\n        filename: \"remix-server.jsx\",\n      },\n    },\n    owner: {\n      login: \"ryanflorence\",\n      id: 100200,\n      avatar_url: \"https://avatars0.githubusercontent.com/u/100200?v=4\",\n    },\n  },\n];\n\ntest.describe(\"route module link export\", () => {\n  for (const templateName of templateNames) {\n    let fixture: Fixture;\n    let appFixture: AppFixture;\n\n    test.describe(templateName, () => {\n      test.beforeAll(async () => {\n        fixture = await createFixture({\n          templateName,\n          files: {\n            \"app/favicon.ico\": js``,\n\n            \"app/guitar.jpg\": js``,\n\n            \"app/guitar-600.jpg\": js``,\n\n            \"app/guitar-900.jpg\": js``,\n\n            \"app/reset.css\": css`\n              * {\n                box-sizing: border-box;\n                margin: 0;\n                padding: 0;\n              }\n\n              html {\n                font-size: 16px;\n                box-sizing: border-box;\n              }\n            `,\n\n            \"app/app.css\": css`\n              body {\n                background-color: #eee;\n                color: #000;\n              }\n            `,\n\n            \"app/gists.css\": css`\n              * {\n                color: dodgerblue;\n              }\n            `,\n\n            \"app/redText.css\": css`\n              * {\n                color: red;\n              }\n            `,\n\n            \"app/blueText.css\": css`\n              * {\n                color: blue;\n              }\n            `,\n\n            \"app/root.tsx\": js`\n              import {\n                Link,\n                Links,\n                Meta,\n                Outlet,\n                Scripts,\n                useRouteError,\n                isRouteErrorResponse\n              } from \"react-router\";\n              import resetHref from \"./reset.css?url\";\n              import stylesHref from \"./app.css?url\";\n              import favicon from \"./favicon.ico\";\n\n              export function links() {\n                return [\n                  { rel: \"stylesheet\", href: resetHref },\n                  { rel: \"stylesheet\", href: stylesHref },\n                  { rel: \"stylesheet\", href: \"/resources/theme-css\" },\n                  { rel: \"shortcut icon\", href: favicon },\n                ];\n              }\n\n              export let handle = {\n                breadcrumb: () => <Link to=\"/\">Home</Link>,\n              };\n\n              export default function Root() {\n                return (\n                  <html lang=\"en\">\n                    <head>\n                      <meta charSet=\"utf-8\" />\n                      <Meta />\n                      <Links />\n                    </head>\n                    <body>\n                      <Outlet />\n                      <Scripts />\n                    </body>\n                  </html>\n                );\n              }\n\n              export function ErrorBoundary() {\n                let error = useRouteError();\n\n                if (isRouteErrorResponse()) {\n                  switch (error.status) {\n                    case 404:\n                      return (\n                        <html lang=\"en\">\n                          <head>\n                            <meta charSet=\"utf-8\" />\n                            <title>404 Not Found</title>\n                            <Links />\n                          </head>\n                          <body>\n                            <div>\n                              <h1>404 Not Found</h1>\n                            </div>\n                            <Scripts />\n                          </body>\n                        </html>\n                      );\n                    default:\n                      console.warn(\"Unexpected catch\", error);\n\n                      return (\n                        <html lang=\"en\">\n                          <head>\n                            <meta charSet=\"utf-8\" />\n                            <title>{error.status} Uh-oh!</title>\n                            <Links />\n                          </head>\n                          <body>\n                            <div>\n                              <h1>\n                                {error.status} {error.statusText}\n                              </h1>\n                              {error.data ? (\n                                <pre>\n                                  <code>{JSON.stringify(error.data, null, 2)}</code>\n                                </pre>\n                              ) : null}\n                            </div>\n                            <Scripts />\n                          </body>\n                        </html>\n                      );\n                    }\n                  } else {\n                    console.error(error);\n                    return (\n                      <html lang=\"en\">\n                        <head>\n                          <meta charSet=\"utf-8\" />\n                          <title>Oops!</title>\n                          <Links />\n                        </head>\n                        <body>\n                          <div>\n                            <h1>App Error Boundary</h1>\n                            <pre>{error.message}</pre>\n                          </div>\n                          <Scripts />\n                        </body>\n                      </html>\n                    );\n                  }\n              }\n            `,\n\n            \"app/routes/_index.tsx\": js`\n              import { useEffect } from \"react\";\n              import { Link } from \"react-router\";\n\n              export default function Index() {\n                return (\n                  <div data-test-id=\"/\">\n                    <header>\n                      <h1>Cool App</h1>\n                    </header>\n                    <nav>\n                      <ul>\n                        <li>\n                          <Link to=\"/gists\">View Some gists</Link>\n                        </li>\n                        <li>\n                          <Link to=\"/gists/mjackson\">View Michael's gists</Link>\n                        </li>\n                        <li>\n                          <Link to=\"/gists/mjijackson\">Loader Redirect</Link>\n                        </li>\n                        <li>\n                          <Link to=\"/resources\">Resource routes</Link>\n                        </li>\n                        <li>\n                          <Link to=\"/parent/child\">Errored child route</Link>\n                        </li>\n                      </ul>\n                    </nav>\n                  </div>\n                );\n              }\n            `,\n\n            \"app/routes/links.tsx\": js`\n              import { useLoaderData, Link } from \"react-router\";\n              import redTextHref from \"~/redText.css?url\";\n              import blueTextHref from \"~/blueText.css?url\";\n              import guitar from \"~/guitar.jpg\";\n              export async function loader() {\n                return [\n                  { name: \"Michael Jackson\", id: \"mjackson\" },\n                  { name: \"Ryan Florence\", id: \"ryanflorence\" },\n                ];\n              }\n              export function links()  {\n                return [\n                  { rel: \"stylesheet\", href: redTextHref },\n                  {\n                    rel: \"stylesheet\",\n                    href: blueTextHref,\n                    media: \"(prefers-color-scheme: beef)\",\n                  },\n                  { page: \"/gists/mjackson\" },\n                  {\n                    rel: \"preload\",\n                    as: \"image\",\n                    href: guitar,\n                  },\n                ];\n              }\n              export default function LinksPage() {\n                let users = useLoaderData();\n                return (\n                  <div data-test-id=\"/links\">\n                    <h2>Links Page</h2>\n                    {users.map((user) => (\n                      <li key={user.id}>\n                        <Link to={\"/gists/\" + user.id} prefetch=\"none\">\n                          {user.name}\n                        </Link>\n                      </li>\n                    ))}\n                    <hr />\n                    <p>\n                      <img alt=\"a guitar\" src={guitar} data-test-id=\"blocked\" /> Prefetched\n                      because it's a preload.\n                    </p>\n                  </div>\n                );\n              }\n            `,\n\n            \"app/routes/responsive-image-preload.tsx\": js`\n              import { Link } from \"react-router\";\n              import guitar600 from \"~/guitar-600.jpg\";\n              import guitar900 from \"~/guitar-900.jpg\";\n\n              export function links()  {\n                return [\n                  {\n                    rel: \"preload\",\n                    as: \"image\",\n                    imageSrcSet: guitar600 + \" 600w, \" + guitar900 + \" 900w\",\n                    imageSizes: \"100vw\",\n                  },\n                ];\n              }\n              export default function LinksPage() {\n                return (\n                  <div data-test-id=\"/responsive-image-preload\">\n                    <h2>Responsive Guitar</h2>\n                    <p>\n                      <img\n                        alt=\"a guitar\"\n                        srcSet={guitar600 + \" 600w, \" + guitar900 + \" 900w\"}\n                        sizes=\"100vw\"\n                        data-test-id=\"blocked\"\n                      />{\" \"}\n                      Prefetched because it's a preload.\n                    </p>\n                  </div>\n                );\n              }\n            `,\n\n            \"app/routes/gists.tsx\": js`\n              import { data, Link, Outlet, useLoaderData, useNavigation } from \"react-router\";\n              import stylesHref from \"~/gists.css?url\";\n              export function links() {\n                return [{ rel: \"stylesheet\", href: stylesHref }];\n              }\n              export async function loader() {\n                return data({\n                  users: [\n                    { id: \"ryanflorence\", name: \"Ryan Florence\" },\n                    { id: \"mjackson\", name: \"Michael Jackson\" },\n                  ],\n                }, {\n                  headers: {\n                    \"Cache-Control\": \"public, max-age=60\",\n                  },\n                });\n              }\n              export function headers({ loaderHeaders }) {\n                return {\n                  \"Cache-Control\": loaderHeaders.get(\"Cache-Control\"),\n                };\n              }\n              export let handle = {\n                breadcrumb: () => <Link to=\"/gists\">Gists</Link>,\n              };\n              export default function Gists() {\n                let locationPending = useNavigation().location;\n                let { users } = useLoaderData();\n                return (\n                  <div data-test-id=\"/gists\">\n                    <header>\n                      <h1>Gists</h1>\n                      <ul>\n                        {users.map((user) => (\n                          <li key={user.id}>\n                            <Link to={user.id}>\n                              {user.name} {locationPending ? \"...\" : null}\n                            </Link>\n                          </li>\n                        ))}\n                      </ul>\n                    </header>\n                    <Outlet />\n                  </div>\n                );\n              }\n            `,\n\n            \"app/routes/gists.$username.tsx\": js`\n              import { data, redirect, Link, useLoaderData, useParams } from \"react-router\";\n              export async function loader({ params }) {\n                let { username } = params;\n                if (username === \"mjijackson\") {\n                  return redirect(\"/gists/mjackson\", 302);\n                }\n                if (username === \"_why\") {\n                  return data(null, { status: 404 });\n                }\n                return ${JSON.stringify(fakeGists)};\n              }\n              export function headers() {\n                return {\n                  \"Cache-Control\": \"public, max-age=300\",\n                };\n              }\n              export function meta({ data, params }) {\n                let { username } = params;\n                return [\n                  {\n                    title: data\n                      ? data.length + \" gists from \" + username\n                      : \"User \" + username + \" not found\",\n                  },\n                  { name: \"description\", content: \"View all of the gists from \" + username },\n                ];\n              }\n              export let handle = {\n                breadcrumb: ({ params }) => (\n                  <Link to={\"gists/\" + params.username}>{params.username}</Link>\n                ),\n              };\n              export default function UserGists() {\n                let { username } = useParams();\n                let data = useLoaderData();\n                return (\n                  <div data-test-id=\"/gists/$username\">\n                    {data ? (\n                      <>\n                        <h2>All gists from {username}</h2>\n                        <ul>\n                          {data.map((gist) => (\n                            <li key={gist.id}>\n                              <a href={gist.html_url}>{Object.keys(gist.files)[0]}</a>\n                            </li>\n                          ))}\n                        </ul>\n                      </>\n                    ) : (\n                      <h2>No gists for {username}</h2>\n                    )}\n                  </div>\n                );\n              }\n            `,\n\n            \"app/routes/gists._index.tsx\": js`\n              import { useLoaderData } from \"react-router\";\n              export async function loader() {\n                return ${JSON.stringify(fakeGists)};\n              }\n              export function headers() {\n                return {\n                  \"Cache-Control\": \"public, max-age=60\",\n                };\n              }\n              export function meta() {\n                return [\n                  { title: \"Public Gists\" },\n                  { name: \"description\", content: \"View the latest gists from the public\" },\n                ];\n              }\n              export let handle = {\n                breadcrumb: () => <span>Public</span>,\n              };\n              export default function GistsIndex() {\n                let data = useLoaderData();\n                return (\n                  <div data-test-id=\"/gists/index\">\n                    <h2>Public Gists</h2>\n                    <ul>\n                      {data.map((gist) => (\n                        <li key={gist.id} style={{ display: \"flex\", alignItems: \"center\" }}>\n                          <img\n                            src={gist.owner.avatar_url}\n                            style={{ height: 36, margin: \"0.25rem 0.5rem 0.25rem 0\" }}\n                            alt=\"avatar\"\n                          />\n                          <a href={gist.html_url}>{Object.keys(gist.files)[0]}</a>\n                        </li>\n                      ))}\n                    </ul>\n                  </div>\n                );\n              }\n            `,\n\n            \"app/routes/resources.theme-css.tsx\": js`\n              import { redirect } from \"react-router\";\n              export async function loader({ request }) {\n                return new Response(\":root { --nc-tx-1: #ffffff; --nc-tx-2: #eeeeee; }\",\n                  {\n                    headers: {\n                      \"Content-Type\": \"text/css; charset=UTF-8\",\n                      \"x-has-custom\": \"yes\",\n                    },\n                  }\n                );\n              }\n\n            `,\n\n            \"app/routes/parent.tsx\": js`\n              import { Outlet } from \"react-router\";\n\n              export function links() {\n                return [\n                  { \"data-test-id\": \"red\" },\n                ];\n              }\n\n              export default function Component() {\n                return <div data-test-id=\"/parent\"><Outlet /></div>;\n              }\n\n              export function ErrorBoundary() {\n                return <h1 data-test-id=\"/parent:error-boundary\">Error Boundary</h1>;\n              }\n            `,\n\n            \"app/routes/parent.child.tsx\": js`\n              import { Outlet } from \"react-router\";\n\n              export function loader() {\n                throw new Response(null, { status: 404 });\n              }\n\n              export function links() {\n                return [\n                  { \"data-test-id\": \"blue\" },\n                ];\n              }\n\n              export default function Component() {\n                return <div data-test-id=\"/parent\"><Outlet /></div>;\n              }\n            `,\n          },\n        });\n        appFixture = await createAppFixture(fixture);\n      });\n\n      test.afterAll(() => {\n        appFixture.close();\n      });\n\n      test(\"adds responsive image preload links to the document\", async ({\n        page,\n      }) => {\n        let app = new PlaywrightFixture(appFixture, page);\n        await app.goto(\"/responsive-image-preload\");\n        await page.waitForSelector(\n          '[data-test-id=\"/responsive-image-preload\"]',\n        );\n        let locator = page.locator(\"link[rel=preload][as=image]\");\n        expect(await locator.getAttribute(\"imagesizes\")).toBe(\"100vw\");\n      });\n\n      test(\"waits for new styles to load before transitioning\", async ({\n        page,\n      }) => {\n        let app = new PlaywrightFixture(appFixture, page);\n        await app.goto(\"/\");\n\n        let cssResponses = app.collectResponses((url) =>\n          url.pathname.endsWith(\".css\"),\n        );\n\n        await page.click('a[href=\"/gists\"]');\n        await page.waitForSelector('[data-test-id=\"/gists/index\"]');\n\n        let stylesheetResponses = cssResponses.filter((res) => {\n          // ignore prefetches\n          return res.request().resourceType() === \"stylesheet\";\n        });\n\n        expect(stylesheetResponses.length).toEqual(1);\n      });\n\n      test(\"does not render errored child route links\", async ({ page }) => {\n        let app = new PlaywrightFixture(appFixture, page);\n        await app.goto(\"/\", true);\n        await page.click('a[href=\"/parent/child\"]');\n        await page.waitForSelector('[data-test-id=\"/parent:error-boundary\"]');\n        await page.waitForSelector('[data-test-id=\"red\"]', {\n          state: \"attached\",\n        });\n        await page.waitForSelector('[data-test-id=\"blue\"]', {\n          state: \"detached\",\n        });\n      });\n\n      test.describe(\"no js\", () => {\n        test.use({ javaScriptEnabled: false });\n\n        test(\"adds links to the document\", async ({ page }) => {\n          let app = new PlaywrightFixture(appFixture, page);\n          let responses = app.collectResponses((url) =>\n            url.pathname.endsWith(\".css\"),\n          );\n\n          await app.goto(\"/links\");\n          await page.waitForSelector('[data-test-id=\"/links\"]');\n          expect(responses.length).toEqual(4);\n        });\n\n        test(\"adds responsive image preload links to the document\", async ({\n          page,\n        }) => {\n          let app = new PlaywrightFixture(appFixture, page);\n          await app.goto(\"/responsive-image-preload\");\n          await page.waitForSelector(\n            '[data-test-id=\"/responsive-image-preload\"]',\n          );\n          let locator = page.locator(\"link[rel=preload][as=image]\");\n          expect(await locator.getAttribute(\"imagesizes\")).toBe(\"100vw\");\n        });\n\n        test(\"does not render errored child route links\", async ({ page }) => {\n          let app = new PlaywrightFixture(appFixture, page);\n          await app.goto(\"/parent/child\");\n          await page.waitForSelector('[data-test-id=\"/parent:error-boundary\"]');\n          await page.waitForSelector('[data-test-id=\"red\"]', {\n            state: \"attached\",\n          });\n          await page.waitForSelector('[data-test-id=\"blue\"]', {\n            state: \"detached\",\n          });\n        });\n      });\n\n      test.describe(\"script imports\", () => {\n        test.skip(templateName.includes(\"rsc\"), \"Not relevant for RSC\");\n\n        // Disable JS for this test since we don't want it to hydrate and remove\n        // the initial <script> tags\n        test.use({ javaScriptEnabled: false });\n\n        test(\"are added to the document\", async ({ page }) => {\n          let app = new PlaywrightFixture(appFixture, page);\n          await app.goto(\"/\");\n          let scripts = await page.$$(\"script\");\n\n          // Scripts:\n          // RR:    window.__reactRouterContext\n          // RR:    window.__reactRouterManifest/window.__reactRouterRouteModules\n          // React: requestAnimationFrame(function(){$RT=performance.now()});\n          // RR:    window.__reactRouterContext.streamController.enqueue()\n          // React: $RC=function(b,c,e){...\n          // RR:    window.__reactRouterContext.streamController.close();\n          // React: $RC(\"B:1\",\"S:1\")\n          expect(scripts.length).toEqual(7);\n\n          expect(await scripts[0].innerText()).toContain(\n            \"__reactRouterContext\",\n          );\n          let moduleScript = scripts[1];\n          expect(await moduleScript.getAttribute(\"type\")).toBe(\"module\");\n          let moduleScriptText = await moduleScript.innerText();\n          expect(\n            Array.from(\n              moduleScriptText.matchAll(/import \"\\/assets\\/manifest-/g),\n            ),\n            \"did not expect a manifest due to fog of war\",\n          ).toHaveLength(0);\n          expect(\n            Array.from(\n              moduleScriptText.matchAll(/import \\* as route0 from \"/g),\n            ),\n            \"invalid route0\",\n          ).toHaveLength(1);\n          expect(\n            Array.from(\n              moduleScriptText.matchAll(/import \\* as route1 from \"/g),\n            ),\n            \"invalid route1\",\n          ).toHaveLength(1);\n          expect(\n            Array.from(\n              moduleScriptText.matchAll(/import \\* as route2 from \"/g),\n            ),\n            \"too many routes\",\n          ).toHaveLength(0);\n        });\n      });\n    });\n  }\n});\n"
  },
  {
    "path": "integration/loader-test.ts",
    "content": "import { test, expect } from \"@playwright/test\";\n\nimport {\n  createAppFixture,\n  createFixture,\n  js,\n} from \"./helpers/create-fixture.js\";\nimport type { Fixture, AppFixture } from \"./helpers/create-fixture.js\";\nimport { PlaywrightFixture } from \"./helpers/playwright-fixture.js\";\n\ntest.describe(\"loader\", () => {\n  let fixture: Fixture;\n\n  let ROOT_DATA = \"ROOT_DATA\";\n  let INDEX_DATA = \"INDEX_DATA\";\n\n  test.beforeAll(async () => {\n    fixture = await createFixture({\n      files: {\n        \"app/root.tsx\": js`\n            import { Links, Meta, Outlet, Scripts } from \"react-router\";\n\n            export const loader = () => \"${ROOT_DATA}\";\n\n            export default function Root() {\n              return (\n                <html lang=\"en\">\n                  <head>\n                    <Meta />\n                    <Links />\n                  </head>\n                  <body>\n                    <Outlet />\n                    <Scripts />\n                  </body>\n                </html>\n              );\n            }\n          `,\n\n        \"app/routes/_index.tsx\": js`\n            export function loader() {\n              return \"${INDEX_DATA}\"\n            }\n\n            export default function Index() {\n              return <div/>\n            }\n          `,\n      },\n    });\n  });\n\n  test(\"returns responses for single fetch routes\", async () => {\n    let { data } = await fixture.requestSingleFetchData(\"/_root.data\");\n    expect(data).toEqual({\n      root: { data: ROOT_DATA },\n      \"routes/_index\": { data: INDEX_DATA },\n    });\n  });\n});\n\ntest.describe(\"loader in an app\", () => {\n  let appFixture: AppFixture;\n\n  let HOME_PAGE_TEXT = \"hello world\";\n  let REDIRECT_TARGET_TEXT = \"redirect target\";\n  let FETCH_TARGET_TEXT = \"fetch target\";\n\n  test.beforeAll(async () => {\n    appFixture = await createAppFixture(\n      await createFixture({\n        files: {\n          \"app/root.tsx\": js`\n            import { Outlet } from \"react-router\"\n\n            export default function Root() {\n              return (\n                <html>\n                  <body>\n                    ${HOME_PAGE_TEXT}\n                    <Outlet />\n                  </body>\n                </html>\n              );\n            }\n          `,\n          \"app/routes/redirect.tsx\": js`\n            import { redirect } from \"react-router\";\n            export const loader = () => redirect(\"/redirect-target\");\n            export default () => <div>Yo</div>\n          `,\n          \"app/routes/redirect-target.tsx\": js`\n            export default () => <div>${REDIRECT_TARGET_TEXT}</div>\n          `,\n          \"app/routes/fetch.tsx\": js`\n            export function loader({ request }) {\n              return fetch(new URL(request.url).origin + '/fetch-target');\n            }\n          `,\n\n          \"app/routes/fetch-target.tsx\": js`\n            export function loader() {\n              return Response.json({ message: \"${FETCH_TARGET_TEXT}\" })\n            }\n          `,\n        },\n      }),\n    );\n  });\n\n  test.afterAll(() => {\n    appFixture.close();\n  });\n\n  test(\"sends a redirect\", async ({ page }) => {\n    let app = new PlaywrightFixture(appFixture, page);\n    await app.goto(\"/redirect\");\n    expect(await app.getHtml()).toMatch(HOME_PAGE_TEXT);\n    expect(await app.getHtml()).toMatch(REDIRECT_TARGET_TEXT);\n  });\n\n  test(\"handles raw fetch responses\", async ({ page }) => {\n    let app = new PlaywrightFixture(appFixture, page);\n    let res = await app.goto(`/fetch`);\n    expect((await res.json()).message).toBe(FETCH_TARGET_TEXT);\n  });\n});\n"
  },
  {
    "path": "integration/matches-test.ts",
    "content": "import { test, expect } from \"@playwright/test\";\n\nimport {\n  createAppFixture,\n  createFixture,\n  js,\n} from \"./helpers/create-fixture.js\";\nimport type { Fixture, AppFixture } from \"./helpers/create-fixture.js\";\nimport { PlaywrightFixture } from \"./helpers/playwright-fixture.js\";\n\ntest.describe(\"useMatches\", () => {\n  let fixture: Fixture;\n  let appFixture: AppFixture;\n\n  test.beforeAll(async () => {\n    fixture = await createFixture({\n      files: {\n        \"app/root.tsx\": js`\n          import * as React from 'react';\n          import { Link, Links, Meta, Outlet, Scripts, useMatches } from \"react-router\";\n          export const handle = { stuff: \"root handle\"};\n          export const loader = () => \"ROOT\";\n          export default function Root() {\n            let matches = useMatches();\n            let [matchesCount, setMatchesCount] = React.useState(0);\n            React.useEffect(() => setMatchesCount(matchesCount + 1), [matches]);\n\n            return (\n              <html lang=\"en\">\n                <head>\n                  <Meta />\n                  <Links />\n                </head>\n                <body>\n                  <Link to=\"/about\">About</Link>\n                  <pre id=\"matches\">\n                    {JSON.stringify(matches, null, 2)}\n                  </pre>\n                  {matchesCount > 0 ? <pre id=\"matches-count-root\">{matchesCount}</pre> : null}\n                  <Outlet />\n                  <Scripts />\n                </body>\n              </html>\n            );\n          }\n        `,\n\n        \"app/routes/_index.tsx\": js`\n          export const handle = { stuff: \"index handle\"};\n          export const loader = () => \"INDEX\";\n          export default function Index() {\n            return <h1 id=\"index\">Index Page</h1>\n          }\n        `,\n\n        \"app/routes/about.tsx\": js`\n          export const handle = { stuff: \"about handle\"};\n          export const loader = async () => {\n            await new Promise(r => setTimeout(r, 100));\n            return \"ABOUT\";\n          }\n          export default function About() {\n            return <h1 id=\"about\">About Page</h1>\n          }\n        `,\n\n        \"app/routes/count.tsx\": js`\n          import * as React from 'react';\n          import { useMatches } from \"react-router\";\n          export default function Count() {\n            let matches = useMatches();\n            let [count, setCount] = React.useState(0);\n            let [matchesCount, setMatchesCount] = React.useState(0);\n            React.useEffect(() => setMatchesCount(matchesCount + 1), [matches]);\n            return (\n              <>\n                <h1>Count Page</h1>\n                <button id=\"increment\" onClick={() => setCount(count + 1)}>\n                  Increment\n                </button>\n                <pre id=\"count\">{count}</pre>\n                {matchesCount > 0 ? <pre id=\"matches-count-child\">{matchesCount}</pre> : null}\n              </>\n            );\n          }\n        `,\n      },\n    });\n\n    appFixture = await createAppFixture(fixture);\n  });\n\n  test.afterAll(() => {\n    appFixture.close();\n  });\n\n  test(\"grabs the handle from the route module cache\", async ({ page }) => {\n    let app = new PlaywrightFixture(appFixture, page);\n    await app.goto(\"/\");\n\n    // Wait for effect\n    await page.waitForSelector(\"#matches-count-root\");\n    expect(await app.getHtml(\"#matches-count-root\")).toMatch(\">1<\");\n    expect(await app.getHtml()).toMatch(\"Index Page\");\n    expect(await app.getHtml(\"#matches\")).toEqual(`<pre id=\"matches\">\n[\n  {\n    \"id\": \"root\",\n    \"pathname\": \"/\",\n    \"params\": {},\n    \"data\": \"ROOT\",\n    \"loaderData\": \"ROOT\",\n    \"handle\": {\n      \"stuff\": \"root handle\"\n    }\n  },\n  {\n    \"id\": \"routes/_index\",\n    \"pathname\": \"/\",\n    \"params\": {},\n    \"data\": \"INDEX\",\n    \"loaderData\": \"INDEX\",\n    \"handle\": {\n      \"stuff\": \"index handle\"\n    }\n  }\n]</pre\n>`);\n\n    // Click and don't wait so we can assert _during_ the navigation that we're\n    // still showing the index matches and we haven't triggered a new effect\n    await app.clickLink(\"/about\", { wait: false });\n    expect(await app.getHtml(\"#matches\")).toEqual(`<pre id=\"matches\">\n[\n  {\n    \"id\": \"root\",\n    \"pathname\": \"/\",\n    \"params\": {},\n    \"data\": \"ROOT\",\n    \"loaderData\": \"ROOT\",\n    \"handle\": {\n      \"stuff\": \"root handle\"\n    }\n  },\n  {\n    \"id\": \"routes/_index\",\n    \"pathname\": \"/\",\n    \"params\": {},\n    \"data\": \"INDEX\",\n    \"loaderData\": \"INDEX\",\n    \"handle\": {\n      \"stuff\": \"index handle\"\n    }\n  }\n]</pre\n>`);\n    expect(await app.getHtml(\"#matches-count-root\")).toMatch(\">1<\");\n\n    // Once the new page shows up we should get update dmatches and a single\n    // new effect execution\n    await page.waitForSelector(\"#about\");\n    expect(await app.getHtml()).toMatch(\"About Page\");\n    expect(await app.getHtml(\"#matches-count-root\")).toMatch(\">2<\");\n    expect(await app.getHtml(\"#matches\")).toEqual(`<pre id=\"matches\">\n[\n  {\n    \"id\": \"root\",\n    \"pathname\": \"/\",\n    \"params\": {},\n    \"data\": \"ROOT\",\n    \"loaderData\": \"ROOT\",\n    \"handle\": {\n      \"stuff\": \"root handle\"\n    }\n  },\n  {\n    \"id\": \"routes/about\",\n    \"pathname\": \"/about\",\n    \"params\": {},\n    \"data\": \"ABOUT\",\n    \"loaderData\": \"ABOUT\",\n    \"handle\": {\n      \"stuff\": \"about handle\"\n    }\n  }\n]</pre\n>`);\n  });\n\n  test(\"memoizes matches from react router\", async ({ page }) => {\n    let app = new PlaywrightFixture(appFixture, page);\n    await app.goto(\"/count\");\n    await page.waitForSelector(\"#matches-count-child\");\n    expect(await app.getHtml(\"#count\")).toMatch(\">0<\");\n    expect(await app.getHtml(\"#matches-count-child\")).toMatch(\">1<\");\n    await app.clickElement(\"#increment\");\n    expect(await app.getHtml(\"#count\")).toMatch(\">1<\");\n    expect(await app.getHtml(\"#matches-count-child\")).toMatch(\">1<\");\n    await app.clickElement(\"#increment\");\n    expect(await app.getHtml(\"#count\")).toMatch(\">2<\");\n    expect(await app.getHtml(\"#matches-count-child\")).toMatch(\">1<\");\n  });\n});\n"
  },
  {
    "path": "integration/mdx-test.ts",
    "content": "import { test, expect } from \"@playwright/test\";\n\nimport {\n  createFixture,\n  createAppFixture,\n  js,\n} from \"./helpers/create-fixture.js\";\nimport type { Fixture, AppFixture } from \"./helpers/create-fixture.js\";\nimport { PlaywrightFixture } from \"./helpers/playwright-fixture.js\";\nimport { type TemplateName, viteConfig } from \"./helpers/vite.js\";\n\nconst templateNames = [\n  \"vite-5-template\",\n  \"rsc-vite-framework\",\n] as const satisfies TemplateName[];\n\ntest.describe(\"MDX\", () => {\n  for (const templateName of templateNames) {\n    test.describe(`template: ${templateName}`, () => {\n      let fixture: Fixture;\n      let appFixture: AppFixture;\n\n      test.beforeAll(async () => {\n        fixture = await createFixture({\n          templateName,\n          files: {\n            \"vite.config.js\": await viteConfig.basic({\n              templateName,\n              mdx: true,\n            }),\n            \"app/root.tsx\": js`\n              import { Outlet, Scripts } from \"react-router\"\n        \n              export default function Root() {\n                return (\n                  <html>\n                    <head></head>\n                    <body>\n                      <main>\n                        <Outlet />\n                      </main>\n                      <Scripts />\n                    </body>\n                  </html>\n                );\n              }\n            `,\n            \"app/routes/_index.tsx\": js`\n              import { Link } from \"react-router\"\n              export default function Component() {\n                return <Link to=\"/mdx\">Go to MDX route</Link>\n              }\n            `,\n            \"app/routes/mdx.mdx\": js`\n              import { MdxComponent } from \"../components/mdx-components\";\n\n              export const loader = () => {\n                return {\n                  content: \"MDX route content from loader\",\n                }\n              }\n\n              ## MDX Route\n\n              <MdxComponent />\n            `,\n            // This needs to be a separate file to support RSC since\n            // `useLoaderData` is not available in RSC environments, and\n            // components defined within an MDX file must be exported. This\n            // means they're not removed in the RSC build.\n            \"app/components/mdx-components.tsx\": js`\n              import { useState, useEffect } from \"react\";\n              import { useLoaderData } from \"react-router\";\n\n              export function MdxComponent() {\n                const { content } = useLoaderData();\n                const [mounted, setMounted] = useState(false);\n                useEffect(() => {\n                  setMounted(true);\n                }, []);\n\n                return (\n                  <>\n                    <h3>Loader data</h3>\n                    <div data-loader-data>{content}</div>\n                    <h3>Mounted</h3>\n                    <div data-mounted>{mounted ? \"true\" : \"false\"}</div>\n                  </>\n                );\n              }\n            `,\n          },\n        });\n\n        appFixture = await createAppFixture(fixture);\n      });\n\n      test.afterAll(() => {\n        appFixture.close();\n      });\n\n      test(\"handles MDX routes\", async ({ page }) => {\n        let app = new PlaywrightFixture(appFixture, page);\n        await app.goto(\"/mdx\");\n\n        let loaderData = page.locator(\"[data-loader-data]\");\n        await expect(loaderData).toHaveText(\"MDX route content from loader\");\n\n        let mounted = page.locator(\"[data-mounted]\");\n        await expect(mounted).toHaveText(\"true\");\n      });\n    });\n  }\n});\n"
  },
  {
    "path": "integration/middleware-test.ts",
    "content": "import type {\n  Request as PlaywrightRequest,\n  Response as PlaywrightResponse,\n} from \"@playwright/test\";\nimport { test, expect } from \"@playwright/test\";\nimport {\n  UNSAFE_ErrorResponseImpl,\n  UNSAFE_ServerMode,\n  UNSAFE_SingleFetchRedirectSymbol,\n} from \"react-router\";\n\nimport {\n  type AppFixture,\n  type Fixture,\n  createAppFixture,\n  createFixture,\n  js,\n} from \"./helpers/create-fixture.js\";\nimport { PlaywrightFixture } from \"./helpers/playwright-fixture.js\";\nimport { reactRouterConfig } from \"./helpers/vite.js\";\n\ntest.describe(\"Middleware\", () => {\n  let originalConsoleError = console.error;\n  test.beforeEach(() => {\n    console.error = () => {};\n  });\n\n  test.afterEach(() => {\n    console.error = originalConsoleError;\n  });\n\n  test.describe(\"SPA Mode\", () => {\n    let fixture: Fixture;\n    let appFixture: AppFixture;\n\n    test.beforeAll(async () => {\n      fixture = await createFixture({\n        spaMode: true,\n        files: {\n          // ...existing code...\n          \"react-router.config.ts\": reactRouterConfig({\n            ssr: false,\n            future: { v8_middleware: true },\n          }),\n          \"vite.config.ts\": js`\n            import { defineConfig } from \"vite\";\n            import { reactRouter } from \"@react-router/dev/vite\";\n            export default defineConfig({ build: { manifest: true, minify: false }, plugins: [reactRouter()] });\n          `,\n          \"app/context.ts\": js`\n            import { createContext } from 'react-router'\n            export const orderContext = createContext([]);\n          `,\n          \"app/routes/loaders._index.tsx\": js`\n            import { Link } from 'react-router'\n            import { orderContext } from '../context'\n            export const clientMiddleware = [\n              ({ context }) => {\n                context.set(orderContext, [...context.get(orderContext), 'a']);\n              },\n              ({ context }) => {\n                context.set(orderContext, [...context.get(orderContext), 'b']);\n              },\n            ];\n            export async function clientLoader({ request, context }) {\n              return context.get(orderContext).join(',');\n            }\n            export default function Component({ loaderData }) {\n              return (\n                <>\n                  <h2 data-route>Index: {loaderData}</h2>\n                  <Link to=\"/loaders/about\">Go to about</Link>\n                </>\n               );\n            }\n          `,\n          \"app/routes/loaders.about.tsx\": js`\n            import { orderContext } from '../context'\n            export const clientMiddleware = [\n              ({ context }) => {\n                context.set(orderContext, [...context.get(orderContext), 'c']);\n              },\n              ({ context }) => {\n                context.set(orderContext, [...context.get(orderContext), 'd']);\n              },\n            ];\n            export async function clientLoader({ context }) {\n              return context.get(orderContext).join(',');\n            }\n            export default function Component({ loaderData }) {\n              return <h2 data-route>About: {loaderData}</h2>;\n            }\n          `,\n          \"app/routes/actions._index.tsx\": js`\n            import { Form } from 'react-router'\n            import { orderContext } from '../context';\n            export const clientMiddleware = [\n              ({ request, context }) => {\n                context.set(orderContext, ['a']);\n              },\n              ({ request, context }) => {\n                context.set(orderContext, [...context.get(orderContext), 'b']);\n              },\n            ];\n            export async function clientAction({ request, context }) {\n              return context.get(orderContext).join(',');\n            }\n            export async function clientLoader({ request, context }) {\n              return context.get(orderContext).join(',');\n            }\n            export default function Component({ loaderData, actionData }) {\n              return (\n                <>\n                  <h2 data-route>Index: {loaderData} - {actionData || 'empty'}</h2>\n                  <Form method=\"post\">\n                    <input name=\"name\" />\n                    <button type=\"submit\">Submit</button>\n                  </Form>\n                </>\n               );\n            }\n          `,\n          \"app/routes/redirect-down._index.tsx\": js`\n            import { Link } from 'react-router'\n            export default function Component({ loaderData, actionData }) {\n              return <Link to=\"/redirect-down/redirect\">Link</Link>;\n            }\n          `,\n          \"app/routes/redirect-down.redirect.tsx\": js`\n            import { Link, redirect } from 'react-router'\n            export const clientMiddleware = [\n              ({ request, context }) => { throw redirect('/redirect-down/target'); }\n            ]\n            export default function Component() {\n              return <h1>Redirect</h1>\n            }\n          `,\n          \"app/routes/redirect-down.target.tsx\": js`\n            export default function Component() {\n              return <h1>Target</h1>\n            }\n          `,\n          \"app/routes/redirect-up._index.tsx\": js`\n            import { Link } from 'react-router';\n\n            export default function Component() {\n              return <Link to=\"/redirect-up/redirect\">Link</Link>;\n            }\n          `,\n          \"app/routes/redirect-up.redirect.tsx\": js`\n            import { Link, redirect } from 'react-router';\n\n            export const clientMiddleware = [\n              async ({ request, context }, next) => {\n                await next();\n                throw redirect('/redirect-up/target');\n              }\n            ];\n\n            export default function Component() {\n              return <h1>Redirect</h1>;\n            }\n          `,\n          \"app/routes/redirect-up.target.tsx\": js`\n            export default function Component() {\n              return <h1>Target</h1>;\n            }\n          `,\n          \"app/routes/error-down._index.tsx\": js`\n            import { Link } from 'react-router';\n\n            export default function Component() {\n              return <Link to=\"/error-down/broken\">Link</Link>;\n            }\n          `,\n          \"app/routes/error-down.broken.tsx\": js`\n            export const clientMiddleware = [\n              async ({ request, context }, next) => {\n                throw new Error('broken!');\n              }\n            ];\n\n            export default function Component() {\n              return <h1>Should not see me</h1>;\n            }\n\n            export function ErrorBoundary({ error }) {\n              return <h1 data-error>{error.message}</h1>;\n            }\n          `,\n          \"app/routes/error-up._index.tsx\": js`\n            import { Link } from 'react-router';\n\n            export default function Component() {\n              return <Link to=\"/error-up/broken\">Link</Link>;\n            }\n          `,\n          \"app/routes/error-up.broken.tsx\": js`\n            export const clientMiddleware = [\n              async ({ request, context }, next) => {\n                await next();\n                throw new Error('broken!');\n              }\n            ];\n\n            export function clientLoader() {\n              return \"nope\";\n            }\n\n            export default function Component() {\n              return <h1>Should not see me</h1>;\n            }\n\n            export function ErrorBoundary({ loaderData, error }) {\n              return (\n                <>\n                  <h1 data-error>{error.message}</h1>\n                  <pre>{loaderData ?? 'empty'}</pre>\n                </>\n              );\n            }\n          `,\n          \"app/routes/middleware-chain._index.tsx\": js`\n            import { Link } from 'react-router'\n            export default function Component({ loaderData }) {\n              return <Link to=\"/middleware-chain/a/b/c\">Link</Link>;\n            }\n          `,\n          \"app/routes/middleware-chain.a.tsx\": js`\n            import { Outlet } from 'react-router'\n            import { orderContext } from '../context';\n            export const clientMiddleware = [\n              ({ context }) => {\n                context.set(orderContext, [...context.get(orderContext), 'a']);\n              },\n            ];\n\n            export function clientLoader({ context }) {\n              return context.get(orderContext).join(',');\n            }\n\n            export default function Component({ loaderData }) {\n              return <><h2>A: {loaderData}</h2><Outlet /></>;\n            }\n          `,\n          \"app/routes/middleware-chain.a.b.tsx\": js`\n            import { Outlet } from 'react-router'\n            import { orderContext } from '../context';\n            export const clientMiddleware = [\n              ({ context }) => {\n                context.set(orderContext, [...context.get(orderContext), 'b']);\n              }\n            ];\n\n            export default function Component() {\n              return <><h3>B</h3><Outlet /></>;\n            }\n          `,\n          \"app/routes/middleware-chain.a.b.c.tsx\": js`\n            import { orderContext } from '../context';\n            export function clientLoader({ context }) {\n              return context.get(orderContext).join(',');\n            }\n\n            export default function Component({ loaderData }) {\n              return <h4>C: {loaderData}</h4>;\n            }\n          `,\n        },\n      });\n      appFixture = await createAppFixture(fixture);\n    });\n\n    test.afterAll(() => {\n      appFixture.close();\n    });\n\n    test(\"calls clientMiddleware before/after loaders\", async ({ page }) => {\n      let app = new PlaywrightFixture(appFixture, page);\n      await app.goto(\"/loaders\");\n      await page.waitForSelector('[data-route]:has-text(\"Index\")');\n      expect(await page.locator(\"[data-route]\").textContent()).toBe(\n        \"Index: a,b\",\n      );\n\n      (await page.$('a[href=\"/loaders/about\"]'))?.click();\n      await page.waitForSelector('[data-route]:has-text(\"About\")');\n      expect(await page.locator(\"[data-route]\").textContent()).toBe(\n        \"About: c,d\",\n      );\n    });\n\n    test(\"calls clientMiddleware before/after actions\", async ({ page }) => {\n      let app = new PlaywrightFixture(appFixture, page);\n      await app.goto(\"/actions\");\n      await page.waitForSelector('[data-route]:has-text(\"Index\")');\n      expect(await page.locator(\"[data-route]\").textContent()).toBe(\n        \"Index: a,b - empty\",\n      );\n\n      (await page.getByRole(\"button\"))?.click();\n      await new Promise((r) => setTimeout(r, 1000));\n      await page.waitForSelector('[data-route]:has-text(\"- a,b\")');\n      expect(await page.locator(\"[data-route]\").textContent()).toBe(\n        \"Index: a,b - a,b\",\n      );\n    });\n\n    test(\"handles redirects thrown on the way down\", async ({ page }) => {\n      let app = new PlaywrightFixture(appFixture, page);\n      await app.goto(\"/redirect-down\");\n      await page.waitForSelector('a:has-text(\"Link\")');\n\n      (await page.getByRole(\"link\"))?.click();\n      await page.waitForSelector('h1:has-text(\"Target\")');\n    });\n\n    test(\"handles redirects thrown on the way up\", async ({ page }) => {\n      let app = new PlaywrightFixture(appFixture, page);\n      await app.goto(\"/redirect-up\");\n      await page.waitForSelector('a:has-text(\"Link\")');\n\n      (await page.getByRole(\"link\"))?.click();\n      await page.waitForSelector('h1:has-text(\"Target\")');\n    });\n\n    test(\"handles errors thrown on the way down\", async ({ page }) => {\n      let app = new PlaywrightFixture(appFixture, page);\n      await app.goto(\"/error-down\");\n      await page.waitForSelector('a:has-text(\"Link\")');\n\n      (await page.getByRole(\"link\"))?.click();\n      await page.waitForSelector(\"[data-error]\");\n      expect(await page.innerText(\"[data-error]\")).toBe(\"broken!\");\n    });\n\n    test(\"handles errors thrown on the way up\", async ({ page }) => {\n      let app = new PlaywrightFixture(appFixture, page);\n      await app.goto(\"/error-up\");\n      await page.waitForSelector('a:has-text(\"Link\")');\n\n      (await page.getByRole(\"link\"))?.click();\n      await page.waitForSelector(\"[data-error]\");\n      expect(await page.innerText(\"[data-error]\")).toBe(\"broken!\");\n      expect(await page.innerText(\"pre\")).toBe(\"empty\");\n    });\n\n    test(\"calls clientMiddleware for routes even without a loader\", async ({\n      page,\n    }) => {\n      let app = new PlaywrightFixture(appFixture, page);\n      await app.goto(\"/middleware-chain\", true);\n      (await page.$('a[href=\"/middleware-chain/a/b/c\"]'))?.click();\n      await page.waitForSelector(\"h4\");\n      expect(await page.innerText(\"h2\")).toBe(\"A: a,b\");\n      expect(await page.innerText(\"h3\")).toBe(\"B\");\n      expect(await page.innerText(\"h4\")).toBe(\"C: a,b\");\n    });\n  });\n\n  test.describe(\"SPA Mode + Split Route Modules\", () => {\n    let fixture: Fixture;\n    let appFixture: AppFixture;\n\n    test.beforeAll(async () => {\n      fixture = await createFixture({\n        spaMode: true,\n        files: {\n          \"react-router.config.ts\": reactRouterConfig({\n            ssr: false,\n            future: { v8_middleware: true, v8_splitRouteModules: true },\n          }),\n          \"vite.config.ts\": js`\n            import { defineConfig } from \"vite\";\n            import { reactRouter } from \"@react-router/dev/vite\";\n\n            export default defineConfig({\n              build: { manifest: true, minify: false },\n              plugins: [reactRouter()],\n            });\n          `,\n          \"app/context.ts\": js`\n            import { createContext } from 'react-router'\n            export const orderContext = createContext([]);\n          `,\n          \"app/routes/loaders._index.tsx\": js`\n            import { Link } from 'react-router'\n            import { orderContext } from '../context'\n\n            export const clientMiddleware = [\n              ({ context }) => {\n                context.set(orderContext, [...context.get(orderContext), 'a']);\n              },\n              ({ context }) => {\n                context.set(orderContext, [...context.get(orderContext), 'b']);\n              },\n            ];\n\n            export async function clientLoader({ request, context }) {\n              return context.get(orderContext).join(',');\n            }\n\n            export default function Component({ loaderData }) {\n              return (\n                <>\n                  <h2 data-route>Index: {loaderData}</h2>\n                  <Link to=\"/loaders/about\">Go to about</Link>\n                </>\n               );\n            }\n          `,\n          \"app/routes/loaders.about.tsx\": js`\n            import { orderContext } from '../context'\n\n            export const clientMiddleware = [\n              ({ context }) => {\n                context.set(orderContext, [...context.get(orderContext), 'c']);\n              },\n              ({ context }) => {\n                context.set(orderContext, [...context.get(orderContext), 'd']);\n              },\n            ];\n\n            export async function clientLoader({ context }) {\n              return context.get(orderContext).join(',');\n            }\n\n            export default function Component({ loaderData }) {\n              return <h2 data-route>About: {loaderData}</h2>;\n            }\n          `,\n        },\n      });\n\n      appFixture = await createAppFixture(fixture);\n    });\n\n    test.afterAll(() => {\n      appFixture.close();\n    });\n\n    test(\"calls clientMiddleware before/after loaders with split route modules\", async ({\n      page,\n    }) => {\n      let app = new PlaywrightFixture(appFixture, page);\n      await app.goto(\"/loaders\");\n      await page.waitForSelector('[data-route]:has-text(\"Index\")');\n      expect(await page.locator(\"[data-route]\").textContent()).toBe(\n        \"Index: a,b\",\n      );\n\n      (await page.$('a[href=\"/loaders/about\"]'))?.click();\n      await page.waitForSelector('[data-route]:has-text(\"About\")');\n      expect(await page.locator(\"[data-route]\").textContent()).toBe(\n        \"About: c,d\",\n      );\n    });\n  });\n\n  test.describe(\"Client Middleware\", () => {\n    let fixture: Fixture;\n    let appFixture: AppFixture;\n\n    test.beforeAll(async () => {\n      fixture = await createFixture({\n        files: {\n          \"react-router.config.ts\": reactRouterConfig({\n            future: { v8_middleware: true },\n          }),\n          \"vite.config.ts\": js`\n            import { defineConfig } from \"vite\";\n            import { reactRouter } from \"@react-router/dev/vite\";\n            export default defineConfig({\n              build: { manifest: true, minify: false },\n              plugins: [reactRouter()],\n            });\n          `,\n          \"app/context.ts\": js`\n            import { createContext } from 'react-router'\n            export const orderContext = createContext([]);\n            export const countContext = createContext({ parent: 0, child: 0, index: 0 });\n          `,\n          \"app/routes/loaders._index.tsx\": js`\n            import { Link } from 'react-router'\n            import { orderContext } from '../context'\n            export const clientMiddleware = [\n              ({ context }) => {\n                context.set(orderContext, [...context.get(orderContext), 'a']);\n              },\n              ({ context }) => {\n                context.set(orderContext, [...context.get(orderContext), 'b']);\n              },\n            ];\n            export async function clientLoader({ request, context }) {\n              return context.get(orderContext).join(',');\n            }\n            export default function Component({ loaderData }) {\n              return (\n                <>\n                  <h2 data-route>Index: {loaderData}</h2>\n                  <Link to=\"/loaders/about\">Go to about</Link>\n                </>\n              );\n            }\n          `,\n          \"app/routes/loaders.about.tsx\": js`\n            import { orderContext } from '../context'\n            export const clientMiddleware = [\n              ({ context }) => {\n                context.set(orderContext, ['c']);\n              },\n              ({ context }) => {\n                context.set(orderContext, [...context.get(orderContext), 'd']);\n              },\n            ];\n            export async function clientLoader({ context }) {\n              return context.get(orderContext).join(',');\n            }\n            export default function Component({ loaderData }) {\n              return <h2 data-route>About: {loaderData}</h2>;\n            }\n          `,\n          \"app/routes/actions._index.tsx\": js`\n            import { Form } from 'react-router'\n            import { orderContext } from '../context';\n            export const clientMiddleware = [\n              ({ context }) => {\n                context.set(orderContext, ['a']);\n              },\n              ({ context }) => {\n                context.set(orderContext, [...context.get(orderContext), 'b']);\n              },\n            ];\n            export async function clientAction({ request, context }) {\n              return context.get(orderContext).join(',');\n            }\n            export async function clientLoader({ request, context }) {\n              return context.get(orderContext).join(',');\n            }\n            export default function Component({ loaderData, actionData }) {\n              return (\n                <>\n                  <h2 data-route>Index: {loaderData} - {actionData || 'empty'}</h2>\n                  <Form method=\"post\">\n                    <input name=\"name\" />\n                    <button type=\"submit\">Submit</button>\n                  </Form>\n                </>\n              );\n            }\n          `,\n          \"app/routes/redirect-down._index.tsx\": js`\n            import { Link } from 'react-router'\n            export default function Component() {\n              return <Link to=\"/redirect-down/redirect\">Link</Link>;\n            }\n          `,\n          \"app/routes/redirect-down.redirect.tsx\": js`\n            import { Link, redirect } from 'react-router'\n            export const clientMiddleware = [\n              ({ request, context }) => { throw redirect('/redirect-down/target'); }\n            ];\n            export default function Component() {\n              return <h1>Redirect</h1>;\n            }\n          `,\n          \"app/routes/redirect-down.target.tsx\": js`\n            export default function Component() {\n              return <h1>Target</h1>;\n            }\n          `,\n          \"app/routes/redirect-up._index.tsx\": js`\n            import { Link } from 'react-router';\n            export default function Component() {\n              return <Link to=\"/redirect-up/redirect\">Link</Link>;\n            }\n          `,\n          \"app/routes/redirect-up.redirect.tsx\": js`\n            import { Link, redirect } from 'react-router';\n            export const clientMiddleware = [\n              async ({ request, context }, next) => {\n                await next();\n                throw redirect('/redirect-up/target');\n              }\n            ];\n            export default function Component() {\n              return <h1>Redirect</h1>;\n            }\n          `,\n          \"app/routes/redirect-up.target.tsx\": js`\n            export default function Component() {\n              return <h1>Target</h1>;\n            }\n          `,\n          \"app/routes/error-down._index.tsx\": js`\n            import { Link } from 'react-router';\n            export default function Component() {\n              return <Link to=\"/error-down/broken\">Link</Link>;\n            }\n          `,\n          \"app/routes/error-down.broken.tsx\": js`\n            export const clientMiddleware = [\n              async ({ request, context }, next) => {\n                throw new Error('broken!');\n              }\n            ];\n            export default function Component() {\n              return <h1>Should not see me</h1>;\n            }\n            export function ErrorBoundary({ error }) {\n              return <h1 data-error>{error.message}</h1>;\n            }\n          `,\n          \"app/routes/error-up._index.tsx\": js`\n            import { Link } from 'react-router';\n            export default function Component() {\n              return <Link to=\"/error-up/broken\">Link</Link>;\n            }\n          `,\n          \"app/routes/error-up.broken.tsx\": js`\n            export const clientMiddleware = [\n              async ({ request, context }, next) => {\n                await next();\n                throw new Error('broken!');\n              }\n            ];\n            export function clientLoader() {\n              return \"nope\";\n            }\n            export default function Component() {\n              return <h1>Should not see me</h1>;\n            }\n            export function ErrorBoundary({ loaderData, error }) {\n              return (\n                <>\n                  <h1 data-error>{error.message}</h1>\n                  <pre>{loaderData ?? 'empty'}</pre>\n                </>\n              );\n            }\n          `,\n          \"app/routes/middleware-chain._index.tsx\": js`\n            import { Link } from 'react-router'\n            export default function Component({ loaderData }) {\n              return <Link to=\"/middleware-chain/a/b/c\">Link</Link>;\n            }\n          `,\n          \"app/routes/middleware-chain.a.tsx\": js`\n            import { Outlet } from 'react-router'\n            import { orderContext } from '../context';\n            export const clientMiddleware = [\n              ({ context }) => {\n                context.set(orderContext, [...context.get(orderContext), 'a']);\n              },\n            ];\n            export function clientLoader({ context }) {\n              return context.get(orderContext).join(',');\n            }\n            export default function Component({ loaderData }) {\n              return (\n                <>\n                  <h2>A: {loaderData}</h2>\n                  <Outlet />\n                </>\n              );\n            }\n          `,\n          \"app/routes/middleware-chain.a.b.tsx\": js`\n            import { Outlet } from 'react-router'\n            import { orderContext } from '../context';\n            export const clientMiddleware = [\n              ({ context }) => {\n                context.set(orderContext, [...context.get(orderContext), 'b']);\n              },\n            ];\n            export default function Component() {\n              return (\n                <>\n                  <h3>B</h3>\n                  <Outlet />\n                </>\n              );\n            }\n          `,\n          \"app/routes/middleware-chain.a.b.c.tsx\": js`\n            import { orderContext } from '../context';\n            export function clientLoader({ context }) {\n              return context.get(orderContext).join(',');\n            }\n            export default function Component({ loaderData }) {\n              return <h4>C: {loaderData}</h4>;\n            }\n          `,\n        },\n      });\n      appFixture = await createAppFixture(fixture);\n    });\n\n    test.afterAll(() => {\n      appFixture.close();\n    });\n\n    test(\"calls clientMiddleware before/after loaders\", async ({ page }) => {\n      let app = new PlaywrightFixture(appFixture, page);\n      await app.goto(\"/loaders\");\n      await page.waitForSelector('[data-route]:has-text(\"Index\")');\n      expect(await page.locator(\"[data-route]\").textContent()).toBe(\n        \"Index: a,b\",\n      );\n\n      (await page.$('a[href=\"/loaders/about\"]'))?.click();\n      await page.waitForSelector('[data-route]:has-text(\"About\")');\n      expect(await page.locator(\"[data-route]\").textContent()).toBe(\n        \"About: c,d\",\n      );\n    });\n\n    test(\"handles redirects thrown on the way down\", async ({ page }) => {\n      let app = new PlaywrightFixture(appFixture, page);\n      await app.goto(\"/redirect-down\");\n      await page.waitForSelector('a:has-text(\"Link\")');\n\n      (await page.getByRole(\"link\"))?.click();\n      await page.waitForSelector('h1:has-text(\"Target\")');\n    });\n\n    test(\"handles redirects thrown on the way up\", async ({ page }) => {\n      let app = new PlaywrightFixture(appFixture, page);\n      await app.goto(\"/redirect-up\");\n      await page.waitForSelector('a:has-text(\"Link\")');\n\n      (await page.getByRole(\"link\"))?.click();\n      await page.waitForSelector('h1:has-text(\"Target\")');\n    });\n\n    test(\"handles errors thrown on the way down\", async ({ page }) => {\n      let app = new PlaywrightFixture(appFixture, page);\n      await app.goto(\"/error-down\");\n      await page.waitForSelector('a:has-text(\"Link\")');\n\n      (await page.getByRole(\"link\"))?.click();\n      await page.waitForSelector(\"[data-error]\");\n      expect(await page.innerText(\"[data-error]\")).toBe(\"broken!\");\n    });\n\n    test(\"handles errors thrown on the way up\", async ({ page }) => {\n      let app = new PlaywrightFixture(appFixture, page);\n      await app.goto(\"/error-up\");\n      await page.waitForSelector('a:has-text(\"Link\")');\n\n      (await page.getByRole(\"link\"))?.click();\n      await page.waitForSelector(\"[data-error]\");\n      expect(await page.innerText(\"[data-error]\")).toBe(\"broken!\");\n      expect(await page.innerText(\"pre\")).toBe(\"empty\");\n    });\n\n    test(\"calls clientMiddleware for routes even without a loader\", async ({\n      page,\n    }) => {\n      let app = new PlaywrightFixture(appFixture, page);\n      await app.goto(\"/middleware-chain\");\n      (await page.$('a[href=\"/middleware-chain/a/b/c\"]'))?.click();\n      await page.waitForSelector(\"h4\");\n      expect(await page.innerText(\"h2\")).toBe(\"A: a,b\");\n      expect(await page.innerText(\"h3\")).toBe(\"B\");\n      expect(await page.innerText(\"h4\")).toBe(\"C: a,b\");\n    });\n\n    test(\"calls clientMiddleware once when multiple server requests happen\", async ({\n      page,\n    }) => {\n      let fixture = await createFixture({\n        files: {\n          \"react-router.config.ts\": reactRouterConfig({\n            future: { v8_middleware: true },\n          }),\n          \"vite.config.ts\": js`\n            import { defineConfig } from \"vite\";\n            import { reactRouter } from \"@react-router/dev/vite\";\n\n            export default defineConfig({\n              build: { manifest: true, minify: false },\n              plugins: [reactRouter()],\n            });\n          `,\n          \"app/context.ts\": js`\n            import { createContext } from 'react-router'\n            export const countContext = createContext({\n              parent: 0,\n              child: 0,\n            });\n          `,\n          \"app/routes/_index.tsx\": js`\n            import { Link } from 'react-router'\n            export default function Component({ loaderData }) {\n              return <Link to=\"/parent/child\">Go to /parent/child</Link>;\n            }\n          `,\n          \"app/routes/parent.tsx\": js`\n            import { countContext } from '../context';\n            import { Outlet } from 'react-router';\n            export function loader() {\n              return 'PARENT'\n            }\n            export const clientMiddleware = [\n              ({ context }) => { context.get(countContext).parent++ },\n            ];\n\n            export async function clientLoader({ serverLoader, context }) {\n              return {\n                serverData: await serverLoader(),\n                context: context.get(countContext)\n              }\n            }\n\n            export default function Component({ loaderData }) {\n              return (\n                <>\n                  <h2 data-parent>{JSON.stringify(loaderData)}</h2>\n                  <Outlet/>\n                </>\n              );\n            }\n          `,\n          \"app/routes/parent.child.tsx\": js`\n            import { countContext } from '../context';\n            export function loader() {\n              return 'CHILD'\n            }\n            export const clientMiddleware = [\n              ({ context }) => { context.get(countContext).child++ },\n            ];\n\n            export async function clientLoader({ serverLoader, context }) {\n              return {\n                serverData: await serverLoader(),\n                context: context.get(countContext)\n              }\n            }\n\n            export default function Component({ loaderData }) {\n              return <h3 data-child>{JSON.stringify(loaderData)}</h3>;\n            }\n          `,\n        },\n      });\n\n      let appFixture = await createAppFixture(fixture);\n\n      let requests: string[] = [];\n      page.on(\"request\", (request: PlaywrightRequest) => {\n        if (request.url().includes(\".data\")) {\n          requests.push(request.url());\n        }\n      });\n\n      let app = new PlaywrightFixture(appFixture, page);\n      await app.goto(\"/\");\n      (await page.$('a[href=\"/parent/child\"]'))?.click();\n      await page.waitForSelector(\"[data-child]\");\n\n      // 2 separate server requests made\n      expect(requests.sort()).toEqual([\n        expect.stringContaining(\"/parent/child.data?_routes=routes%2Fparent\"),\n        expect.stringContaining(\n          \"/parent/child.data?_routes=routes%2Fparent.child\",\n        ),\n      ]);\n\n      // But client middlewares only ran once\n      let json = (await page.locator(\"[data-parent]\").textContent()) as string;\n      expect(JSON.parse(json)).toEqual({\n        serverData: \"PARENT\",\n        context: {\n          parent: 1,\n          child: 1,\n        },\n      });\n      json = (await page.locator(\"[data-child]\").textContent()) as string;\n      expect(JSON.parse(json)).toEqual({\n        serverData: \"CHILD\",\n        context: {\n          parent: 1,\n          child: 1,\n        },\n      });\n\n      appFixture.close();\n    });\n\n    test(\"calls clientMiddleware once when multiple server requests happen and some routes opt out\", async ({\n      page,\n    }) => {\n      let fixture = await createFixture({\n        files: {\n          \"react-router.config.ts\": reactRouterConfig({\n            future: { v8_middleware: true },\n          }),\n          \"vite.config.ts\": js`\n            import { defineConfig } from \"vite\";\n            import { reactRouter } from \"@react-router/dev/vite\";\n\n            export default defineConfig({\n              build: { manifest: true, minify: false },\n              plugins: [reactRouter()],\n            });\n          `,\n          \"app/context.ts\": js`\n            import { createContext } from 'react-router'\n            export const countContext = createContext({\n              parent: 0,\n              child: 0,\n              index: 0,\n            });\n          `,\n          \"app/routes/_index.tsx\": js`\n            import { Link } from 'react-router'\n            export default function Component({ loaderData }) {\n              return <Link to=\"/parent/child\">Go to /parent/child</Link>;\n            }\n          `,\n          \"app/routes/parent.tsx\": js`\n            import { Outlet } from 'react-router';\n            import { countContext } from '../context';\n            export function loader() {\n              return 'PARENT'\n            }\n            export const clientMiddleware = [\n              ({ context }) => { context.get(countContext).parent++ },\n            ];\n            export default function Component({ loaderData }) {\n              return (\n                <>\n                  <h2 data-parent>{loaderData}</h2>\n                  <Outlet/>\n                </>\n              );\n            }\n            export function shouldRevalidate() {\n              return false;\n            }\n          `,\n          \"app/routes/parent.child.tsx\": js`\n            import { Outlet } from 'react-router';\n            import { countContext } from '../context';\n            export function loader() {\n              return 'CHILD'\n            }\n            export const clientMiddleware = [\n              ({ context }) => { context.get(countContext).child++ },\n            ];\n            export default function Component({ loaderData }) {\n              return (\n                <>\n                  <h3 data-child>{loaderData}</h3>\n                  <Outlet/>\n                </>\n              );\n            }\n          `,\n          \"app/routes/parent.child._index.tsx\": js`\n            import { Form } from 'react-router';\n            import { countContext } from '../context';\n            export function action() {\n              return 'INDEX ACTION'\n            }\n            export function loader() {\n              return 'INDEX'\n            }\n            export const clientMiddleware = [\n              ({ context }) => { context.get(countContext).index++ },\n            ];\n            export async function clientLoader({ serverLoader, context }) {\n              return {\n                serverData: await serverLoader(),\n                context: context.get(countContext)\n              }\n            }\n            export default function Component({ loaderData, actionData }) {\n              return (\n                <>\n                  <h4 data-index>{JSON.stringify(loaderData)}</h4>\n                  <Form method=\"post\">\n                    <button type=\"submit\">Submit</button>\n                  </Form>\n                  {actionData ? <p data-action>{JSON.stringify(actionData)}</p> : null}\n                </>\n              );\n            }\n          `,\n        },\n      });\n\n      let appFixture = await createAppFixture(fixture);\n\n      let requests: string[] = [];\n      page.on(\"request\", (request: PlaywrightRequest) => {\n        if (request.method() === \"GET\" && request.url().includes(\".data\")) {\n          requests.push(request.url());\n        }\n      });\n\n      let app = new PlaywrightFixture(appFixture, page);\n      await app.goto(\"/\");\n      (await page.$('a[href=\"/parent/child\"]'))?.click();\n      await page.waitForSelector(\"[data-child]\");\n      expect(await page.locator(\"[data-parent]\").textContent()).toBe(\"PARENT\");\n      expect(await page.locator(\"[data-child]\").textContent()).toBe(\"CHILD\");\n      expect(\n        JSON.parse((await page.locator(\"[data-index]\").textContent())!),\n      ).toEqual({\n        serverData: \"INDEX\",\n        context: {\n          parent: 1,\n          child: 1,\n          index: 1,\n        },\n      });\n\n      requests = []; // clear before form submission\n      (await page.$('button[type=\"submit\"]'))?.click();\n      await page.waitForSelector(\"[data-action]\");\n\n      // 2 separate server requests made\n      expect(requests.sort()).toEqual([\n        // This is the normal request but only included parent.child because parent opted out\n        expect.stringMatching(\n          /\\/parent\\/child\\.data\\?_routes=routes%2Fparent\\.child$/,\n        ),\n        // index gets it's own due to clientLoader\n        expect.stringMatching(\n          /\\/parent\\/child\\.data\\?_routes=routes%2Fparent\\.child\\._index$/,\n        ),\n      ]);\n\n      // But client middlewares only ran once for the action and once for the revalidation\n      expect(await page.locator(\"[data-parent]\").textContent()).toBe(\"PARENT\");\n      expect(await page.locator(\"[data-child]\").textContent()).toBe(\"CHILD\");\n      expect(\n        JSON.parse((await page.locator(\"[data-index]\").textContent())!),\n      ).toEqual({\n        serverData: \"INDEX\",\n        context: {\n          parent: 3,\n          child: 3,\n          index: 3,\n        },\n      });\n\n      appFixture.close();\n    });\n  });\n\n  test.describe(\"Client Middleware + Split Route Modules\", () => {\n    let fixture: Fixture;\n    let appFixture: AppFixture;\n\n    test.beforeAll(async () => {\n      fixture = await createFixture({\n        files: {\n          \"react-router.config.ts\": reactRouterConfig({\n            future: { v8_middleware: true, v8_splitRouteModules: true },\n          }),\n          \"vite.config.ts\": js`\n            import { defineConfig } from \"vite\";\n            import { reactRouter } from \"@react-router/dev/vite\";\n\n            export default defineConfig({\n              build: { manifest: true, minify: false },\n              plugins: [reactRouter()],\n            });\n          `,\n          \"app/context.ts\": js`\n            import { createContext } from 'react-router'\n            export const orderContext = createContext([]);\n          `,\n          \"app/routes/loaders._index.tsx\": js`\n            import { Link } from 'react-router'\n            import { orderContext } from \"../context\";;\n\n            export const clientMiddleware = [\n              ({ context }) => {\n                context.set(orderContext, [...context.get(orderContext), 'a']);\n              },\n              ({ context }) => {\n                context.set(orderContext, [...context.get(orderContext), 'b']);\n              },\n            ];\n\n            export async function clientLoader({ request, context }) {\n              return context.get(orderContext).join(',');\n            }\n\n            export default function Component({ loaderData }) {\n              return (\n                <>\n                  <h2 data-route>Index: {loaderData}</h2>\n                  <Link to=\"/loaders/about\">Go to about</Link>\n                </>\n               );\n            }\n          `,\n          \"app/routes/loaders.about.tsx\": js`\n            import { orderContext } from \"../context\";;\n            export const clientMiddleware = [\n              ({ context }) => {\n                context.set(orderContext, ['c']); // reset order from hydration\n              },\n              ({ context }) => {\n                context.set(orderContext, [...context.get(orderContext), 'd']);\n              },\n            ];\n\n            export async function clientLoader({ context }) {\n              return context.get(orderContext).join(',');\n            }\n\n            export default function Component({ loaderData }) {\n              return <h2 data-route>About: {loaderData}</h2>;\n            }\n          `,\n        },\n      });\n\n      appFixture = await createAppFixture(fixture);\n    });\n\n    test.afterAll(() => {\n      appFixture.close();\n    });\n\n    test(\"calls clientMiddleware before/after loaders with split route modules\", async ({\n      page,\n    }) => {\n      let app = new PlaywrightFixture(appFixture, page);\n      await app.goto(\"/loaders\");\n      await page.waitForSelector('[data-route]:has-text(\"Index\")');\n      expect(await page.locator(\"[data-route]\").textContent()).toBe(\n        \"Index: a,b\",\n      );\n\n      (await page.$('a[href=\"/loaders/about\"]'))?.click();\n      await page.waitForSelector('[data-route]:has-text(\"About\")');\n      expect(await page.locator(\"[data-route]\").textContent()).toBe(\n        \"About: c,d\",\n      );\n    });\n  });\n\n  test.describe(\"Server Middleware\", () => {\n    let fixture: Fixture;\n    let appFixture: AppFixture;\n\n    test.beforeAll(async () => {\n      fixture = await createFixture({\n        files: {\n          \"react-router.config.ts\": reactRouterConfig({\n            future: { v8_middleware: true },\n          }),\n          \"vite.config.ts\": js`\n            import { defineConfig } from \"vite\";\n            import { reactRouter } from \"@react-router/dev/vite\";\n            export default defineConfig({ build: { manifest: true, minify: false }, plugins: [reactRouter()] });\n          `,\n          \"app/context.ts\": js`\n            import { createContext } from 'react-router'\n            export const orderContext = createContext([]);\n          `,\n          \"app/routes/loaders._index.tsx\": js`\n            import { Link } from 'react-router'\n            import { orderContext } from '../context'\n            export const middleware = [\n              ({ context }) => { context.set(orderContext, [...context.get(orderContext), 'a']); },\n              ({ context }) => { context.set(orderContext, [...context.get(orderContext), 'b']); },\n            ];\n            export async function loader({ request, context }) { return context.get(orderContext).join(','); }\n            export default function Component({ loaderData }) {\n              return (<><h2 data-route>Index: {loaderData}</h2><Link to=\"/loaders/about\">Go to about</Link></>);\n            }\n          `,\n          \"app/routes/loaders.about.tsx\": js`\n            import { orderContext } from '../context'\n            export const middleware = [\n              ({ context }) => { context.set(orderContext, ['c']); },\n              ({ context }) => { context.set(orderContext, [...context.get(orderContext), 'd']); },\n            ];\n            export async function loader({ context }) { return context.get(orderContext).join(','); }\n            export default function Component({ loaderData }) {\n              return <h2 data-route>About: {loaderData}</h2>;\n            }\n          `,\n          \"app/routes/no-loaders.parent.tsx\": js`\n            import { Link, Outlet } from 'react-router'\n\n            export const middleware = [\n              ({ request }) => {\n                console.log('Running parent middleware', new URL(request.url).pathname)\n              },\n            ];\n\n            export default function Component() {\n              return (\n                <>\n                  <h2>Parent</h2>\n                  <Link to=\"/no-loaders/parent/a\">Go to A</Link>\n                  <Link to=\"/no-loaders/parent/b\">Go to B</Link>\n                  <Outlet/>\n                </>\n               );\n            }\n          `,\n          \"app/routes/no-loaders.parent.a.tsx\": js`\n            export const middleware = [\n              ({ request }) => {\n                console.log('Running A middleware', new URL(request.url).pathname)\n              },\n            ];\n\n            export default function Component() {\n              return <h3>A</h3>;\n            }\n          `,\n          \"app/routes/no-loaders.parent.b.tsx\": js`\n            export const middleware = [\n              ({ request }) => {\n                console.log('Running B middleware', new URL(request.url).pathname)\n              },\n            ];\n\n            export default function Component() {\n              return <h3>B</h3>;\n            }\n          `,\n\n          \"app/routes/actions._index.tsx\": js`\n            import { Form } from 'react-router'\n            import { orderContext } from '../context';\n            export const middleware = [\n              ({ context }) => { context.set(orderContext, [...context.get(orderContext), 'a']); },\n              ({ context }) => { context.set(orderContext, [...context.get(orderContext), 'b']); },\n            ];\n            export async function action({ request, context }) { return context.get(orderContext).join(','); }\n            export async function loader({ request, context }) { return context.get(orderContext).join(','); }\n            export default function Component({ loaderData, actionData }) {\n              return (<><h2 data-route>Index: {loaderData} - {actionData || 'empty'}</h2><Form method=\"post\"><input name=\"name\" /><button type=\"submit\">Submit</button></Form></>);\n            }\n          `,\n          \"app/routes/redirect-down._index.tsx\": js`\n            import { Link } from 'react-router'\n            export default function Component() {\n              return <Link to=\"/redirect-down/redirect\">Link</Link>;\n            }\n          `,\n          \"app/routes/redirect-down.redirect.tsx\": js`\n            import { Link, redirect } from 'react-router'\n            export const middleware = [({ request, context }) => {\n              throw redirect('/redirect-down/target');\n            }];\n            export function loader() {\n              return null;\n            }\n            export default function Component() {\n              return <h1>Redirect</h1>;\n            }\n          `,\n          \"app/routes/redirect-down.target.tsx\": js`\n            export default function Component() {\n              return <h1>Target</h1>;\n            }\n          `,\n          \"app/routes/redirect-up._index.tsx\": js`\n            import { Link } from 'react-router';\n            export default function Component() {\n              return <Link to=\"/redirect-up/redirect\">Link</Link>;\n            }\n          `,\n          \"app/routes/redirect-up.redirect.tsx\": js`\n            import { Link, redirect } from 'react-router';\n            export const middleware = [async ({ request, context }, next) => {\n              await next(); throw redirect('/redirect-up/target');\n            }];\n            export function loader() {\n              return null;\n            }\n            export default function Component() {\n              return <h1>Redirect</h1>;\n            }\n          `,\n          \"app/routes/redirect-up.target.tsx\": js`\n            export default function Component() {\n              return <h1>Target</h1>;\n            }\n          `,\n          \"app/routes/single-fetch-serialize.redirect.tsx\": js`\n            import { Link, redirect } from 'react-router'\n            export const middleware = [\n              async ({ request, context }, next) => {\n                let res = await next();\n                // Should still be a normal redirect here, not yet encoded into\n                // a single fetch redirect\n                res.headers.set(\"X-Status\", res.status);\n                res.headers.set(\"X-Location\", res.headers.get('Location'));\n                return res;\n              }\n            ]\n            export function loader() {\n              throw redirect('/single-fetch-serialize/target');\n            }\n            export default function Component() {\n              return <h1>Redirect</h1>\n            }\n          `,\n          \"app/routes/single-fetch-serialize.target.tsx\": js`\n            export default function Component() {\n              return <h1>Target</h1>\n            }\n          `,\n          \"app/routes/throw-data-before._index.tsx\": js`\n            import { Link } from 'react-router'\n            export default function Component({ loaderData }) {\n              return <Link to=\"/throw-data-before/a/b/c\">/a/b/c</Link>;\n            }\n          `,\n          \"app/routes/throw-data-before.a.tsx\": js`\n            import { Outlet } from 'react-router'\n            export const middleware = [\n              async (_, next) => {\n                let res = await next();\n                res.headers.set('x-a', 'true');\n                return res;\n              }\n            ];\n            export default function Component() {\n              return <Outlet/>\n            }\n            export function ErrorBoundary({ error }) {\n              return (\n                <>\n                  <h1 data-error>A Error Boundary</h1>\n                  <pre>{error.data}</pre>\n                </>\n              );\n            }\n          `,\n          \"app/routes/throw-data-before.a.b.tsx\": js`\n            import { Link, Outlet } from 'react-router'\n            export const middleware = [\n              async (_, next) => {\n                let res = await next();\n                res.headers.set('x-b', 'true');\n                return res;\n              }\n            ];\n            export default function Component({ loaderData }) {\n              return <Outlet/>;\n            }\n          `,\n          \"app/routes/throw-data-before.a.b.c.tsx\": js`\n            import { data } from \"react-router\";\n            export const middleware = [(_, next) => {\n              throw data('C ERROR', { status: 418, statusText: \"I'm a teapot\" })\n            }];\n            // Force middleware to run on client side navs\n            export function loader() {\n              return null;\n            }\n            export default function Component({ loaderData }) {\n              return <h1>C</h1>\n            }\n          `,\n          \"app/routes/throw-data-after._index.tsx\": js`\n            import { Link } from 'react-router'\n            export default function Component({ loaderData }) {\n              return <Link to=\"/throw-data-after/a/b/c\">/a/b/c</Link>;\n            }\n          `,\n          \"app/routes/throw-data-after.a.tsx\": js`\n            import { Outlet } from 'react-router'\n            export const middleware = [\n              async (_, next) => {\n                let res = await next();\n                res.headers.set('x-a', 'true');\n                return res;\n              }\n            ];\n            export function loader() {\n              return \"A LOADER\";\n            }\n            export default function Component() {\n              return <Outlet/>\n            }\n            export function ErrorBoundary({ error, loaderData }) {\n              return (\n                <>\n                  <h1 data-error>A Error Boundary</h1>\n                  <pre>{error.data}</pre>\n                  <p>{loaderData}</p>\n                </>\n              );\n            }\n          `,\n          \"app/routes/throw-data-after.a.b.tsx\": js`\n            import { Link, Outlet } from 'react-router'\n            export const middleware = [\n              async (_, next) => {\n                let res = await next();\n                res.headers.set('x-b', 'true');\n                return res;\n              }\n            ];\n            export default function Component({ loaderData }) {\n              return <Outlet/>;\n            }\n          `,\n          \"app/routes/throw-data-after.a.b.c.tsx\": js`\n            import { data } from \"react-router\";\n            export const middleware = [async (_, next) => {\n              let res = await next();\n              throw data('C ERROR', { status: 418, statusText: \"I'm a teapot\" })\n            }];\n            // Force middleware to run on client side navs\n            export function loader() {\n              return null;\n            }\n            export default function Component({ loaderData }) {\n              return <h1>C</h1>\n            }\n          `,\n          \"app/routes/granular._index.tsx\": js`\n            import { Link } from 'react-router'\n            export default function Component({ loaderData }) {\n              return <Link to=\"/granular/a/b\">Link</Link>;\n            }\n          `,\n          \"app/routes/granular.a.tsx\": js`\n            import { Outlet } from 'react-router'\n            import { orderContext } from '../context';\n            export const middleware = [\n              ({ context }) => { context.set(orderContext, ['a']); },\n            ];\n\n            export async function loader({ context }) {\n              return context.get(orderContext).join(',');\n            }\n\n            // Force a granular call for this route\n            export function clientLoader({ serverLoader }) {\n              return serverLoader()\n            }\n\n            export default function Component({ loaderData }) {\n              return (\n                <>\n                  <h2 data-a>A: {loaderData}</h2>\n                  <Outlet />\n                </>\n              );\n            }\n          `,\n          \"app/routes/granular.a.b.tsx\": js`\n            import { Outlet } from 'react-router'\n            import { orderContext } from '../context';\n            export const middleware = [\n              ({ context }) => {\n                context.set(orderContext, [...context.get(orderContext), 'b']);\n              },\n            ];\n\n            export async function loader({ context }) {\n              return context.get(orderContext).join(',');\n            }\n\n            // Force a granular call for this route\n            export function clientLoader({ serverLoader }) {\n              return serverLoader()\n            }\n\n            export default function Component({ loaderData }) {\n              return <h3 data-b>B: {loaderData}</h3>;\n            }\n          `,\n          \"app/routes/revalidate.parent.tsx\": js`\n            import { Outlet } from \"react-router\";\n            import { orderContext } from '../context';\n\n            export const middleware = [\n              ({ context, params }) => {\n                context.set(orderContext, [\n                  ...context.get(orderContext),\n                  'parent middleware ' + params.child\n                ]);\n              },\n            ];\n\n            export async function loader({ context, params }) {\n              context.set(orderContext, [\n                ...context.get(orderContext),\n                'parent loader ' + params.child\n              ]);\n              await new Promise(r => setTimeout(r, 0));\n              return context.get(orderContext).join(',');\n            }\n\n            export const shouldRevalidate = () => false;\n\n            export default function Parent({ loaderData }) {\n              return (\n                <>\n                  <h1>Parent</h1>\n                  <div id=\"parent-loader-data\">{loaderData}</div>\n                  <Outlet />\n                </>\n              );\n            }\n          `,\n\n          \"app/routes/revalidate.parent.$child.tsx\": js`\n            import { href, Link } from \"react-router\";\n            import { orderContext } from '../context';\n\n            export const middleware = [\n              ({ context, params }) => {\n                context.set(orderContext, [\n                  ...context.get(orderContext),\n                  'child middleware ' + params.child\n                ]);\n              },\n            ];\n\n            export async function loader({ context, params }) {\n              context.set(orderContext, [\n                ...context.get(orderContext),\n                'child loader ' + params.child\n              ]);\n              await new Promise(r => setTimeout(r, 0));\n              return context.get(orderContext).join(',');\n            }\n\n            export default function Child({ loaderData, params }) {\n              const nextChild = String(Number(params.child) + 1);\n\n              return (\n                <>\n                  <h1>Child: {params.child}</h1>\n                  <div id=\"child-loader-data\">{loaderData}</div>\n                  <Link to={href(\"/revalidate/parent/:child\", { child: nextChild })}>\n                    Next child\n                  </Link>\n                </>\n              );\n            }\n          `,\n          \"app/routes/without-loader-document._index.tsx\": js`\n            import { Link } from 'react-router'\n            export default function Component({ loaderData }) {\n              return <Link to=\"/without-loader-document/a/b/c\">Link</Link>;\n            }\n          `,\n          \"app/routes/without-loader-document.a.tsx\": js`\n            import { Outlet } from 'react-router'\n            import { orderContext } from '../context';\n            export const middleware = [\n              ({ context }) => { context.set(orderContext, ['a']); }\n            ];\n\n            export function loader({ context }) {\n              return context.get(orderContext).join(',');\n            }\n\n            export default function Component({ loaderData }) {\n              return <><h2>A: {loaderData}</h2><Outlet /></>;\n            }\n          `,\n          \"app/routes/without-loader-document.a.b.tsx\": js`\n            import { Outlet } from 'react-router'\n            import { orderContext } from '../context';\n            export const middleware = [\n              ({ context }) => {\n                context.set(orderContext, [...context.get(orderContext), 'b']);\n              },\n            ];\n\n            export default function Component() {\n              return <><h3>B</h3><Outlet /></>;\n            }\n          `,\n          \"app/routes/without-loader-document.a.b.c.tsx\": js`\n            import { orderContext } from '../context';\n            export function loader({ context }) {\n              return context.get(orderContext).join(',');\n            }\n\n            export default function Component({ loaderData }) {\n              return <h4>C: {loaderData}</h4>;\n            }\n          `,\n          \"app/routes/without-loader-data._index.tsx\": js`\n            import { Link } from 'react-router'\n            export default function Component({ loaderData }) {\n              return <Link to=\"/without-loader-data/a/b/c\">Link</Link>;\n            }\n          `,\n          \"app/routes/without-loader-data.a.tsx\": js`\n            import { Outlet } from 'react-router'\n            import { orderContext } from '../context';\n            export const middleware = [\n              ({ context }) => { context.set(orderContext, ['a']); }\n            ];\n\n            export function loader({ context }) {\n              return context.get(orderContext).join(',');\n            }\n\n            export default function Component({ loaderData }) {\n              return <><h2>A: {loaderData}</h2><Outlet /></>;\n            }\n          `,\n          \"app/routes/without-loader-data.a.b.tsx\": js`\n            import { Outlet } from 'react-router'\n            import { orderContext } from '../context';\n            export const middleware = [\n              ({ context }) => {\n                context.set(orderContext, [...context.get(orderContext), 'b']);\n              },\n            ];\n\n            export default function Component() {\n              return <><h3>B</h3><Outlet /></>;\n            }\n          `,\n          \"app/routes/without-loader-data.a.b.c.tsx\": js`\n            import { orderContext } from '../context';\n            export function loader({ context }) {\n              return context.get(orderContext).join(',');\n            }\n\n            export default function Component({ loaderData }) {\n              return <h4>C: {loaderData}</h4>;\n            }\n          `,\n\n          \"app/routes/resource._index.tsx\": js`\n            import * as React from 'react'\n            import { useFetcher } from 'react-router'\n            export default function Component({ loaderData }) {\n              let fetcher = useFetcher();\n              let [data, setData] = React.useState();\n\n              async function rawFetch() {\n                let res = await fetch('/resource/a/b?raw');\n                let text = await res.text();\n                setData(text);\n              }\n\n              return (\n                <>\n                  <button id=\"fetcher\" onClick={() => fetcher.load('/resource/a/b')}>\n                    Load Fetcher\n                  </button>\n                  {fetcher.data ? <pre data-fetcher>{fetcher.data}</pre> : null}\n\n                  <br/>\n\n                  <button id=\"fetch\" onClick={rawFetch}>\n                    Load Raw Fetch\n                  </button>\n                  {data ? <pre data-fetch>{data}</pre> : null}\n                </>\n              );\n            }\n          `,\n          \"app/routes/resource.a.tsx\": js`\n            import { orderContext } from '../context';\n            export const middleware = [\n              async ({ context }, next) => {\n                context.set(orderContext, ['a']);\n                let res = await next();\n                res.headers.set('x-a', 'true');\n                return res;\n              },\n            ];\n          `,\n          \"app/routes/resource.a.b.tsx\": js`\n            import { orderContext } from '../context';\n            export const middleware = [\n              async ({ context }, next) => {\n                context.set(orderContext, [...context.get(orderContext), 'b']);\n                let res = await next();\n                res.headers.set('x-b', 'true');\n                return res;\n              },\n            ];\n\n            export async function loader({ request, context }) {\n              let data = context.get(orderContext).join(',');\n              let isRaw = new URL(request.url).searchParams.has('raw');\n              return isRaw ? new Response(data) : data;\n            }\n          `,\n        },\n      });\n      appFixture = await createAppFixture(fixture);\n    });\n\n    test.afterAll(() => {\n      appFixture.close();\n    });\n\n    test(\"calls middleware before/after loaders\", async ({ page }) => {\n      let app = new PlaywrightFixture(appFixture, page);\n      await app.goto(\"/loaders\");\n      await page.waitForSelector('[data-route]:has-text(\"Index\")');\n      expect(await page.locator(\"[data-route]\").textContent()).toBe(\n        \"Index: a,b\",\n      );\n\n      (await page.$('a[href=\"/loaders/about\"]'))?.click();\n      await page.waitForSelector('[data-route]:has-text(\"About\")');\n      expect(await page.locator(\"[data-route]\").textContent()).toBe(\n        \"About: c,d\",\n      );\n    });\n\n    test(\"calls middleware when no loaders exist on document, but not data requests\", async ({\n      page,\n    }) => {\n      let oldConsoleLog = console.log;\n      let logs: any[] = [];\n      console.log = (...args) => logs.push(args);\n\n      let app = new PlaywrightFixture(appFixture, page);\n      await app.goto(\"/no-loaders/parent/a\");\n      await page.waitForSelector('h2:has-text(\"Parent\")');\n      await page.waitForSelector('h3:has-text(\"A\")');\n      expect(logs).toEqual([\n        [\"Running parent middleware\", \"/no-loaders/parent/a\"],\n        [\"Running A middleware\", \"/no-loaders/parent/a\"],\n      ]);\n\n      (await page.$('a[href=\"/no-loaders/parent/b\"]'))?.click();\n      await page.waitForSelector('h3:has-text(\"B\")');\n      expect(logs).toEqual([\n        [\"Running parent middleware\", \"/no-loaders/parent/a\"],\n        [\"Running A middleware\", \"/no-loaders/parent/a\"],\n      ]);\n\n      console.log = oldConsoleLog;\n    });\n\n    test(\"calls middleware before/after actions\", async ({ page }) => {\n      let app = new PlaywrightFixture(appFixture, page);\n      await app.goto(\"/actions\");\n      await page.waitForSelector('[data-route]:has-text(\"Index\")');\n      expect(await page.locator(\"[data-route]\").textContent()).toBe(\n        \"Index: a,b - empty\",\n      );\n\n      (await page.getByRole(\"button\"))?.click();\n      await new Promise((r) => setTimeout(r, 1000));\n      await page.waitForSelector('[data-route]:has-text(\"- a,b\")');\n      expect(await page.locator(\"[data-route]\").textContent()).toBe(\n        \"Index: a,b - a,b\",\n      );\n    });\n\n    test(\"handles redirects thrown on the way down\", async ({ page }) => {\n      let res = await fixture.requestDocument(\"/redirect-down/redirect\");\n      expect(res.status).toBe(302);\n      expect(res.headers.get(\"location\")).toBe(\"/redirect-down/target\");\n      expect(res.body).toBeNull();\n\n      let app = new PlaywrightFixture(appFixture, page);\n      await app.goto(\"/redirect-down\", true);\n      await page.waitForSelector('a:has-text(\"Link\")');\n\n      (await page.$('a[href=\"/redirect-down/redirect\"]'))?.click();\n      await page.waitForSelector('h1:has-text(\"Target\")');\n    });\n\n    test(\"handles redirects thrown on the way up\", async ({ page }) => {\n      let res = await fixture.requestDocument(\"/redirect-up/redirect\");\n      expect(res.status).toBe(302);\n      expect(res.headers.get(\"location\")).toBe(\"/redirect-up/target\");\n      expect(res.body).toBeNull();\n\n      let app = new PlaywrightFixture(appFixture, page);\n      await app.goto(\"/redirect-up\", true);\n      await page.waitForSelector('a:has-text(\"Link\")');\n\n      (await page.$('a[href=\"/redirect-up/redirect\"]'))?.click();\n      await page.waitForSelector('h1:has-text(\"Target\")');\n    });\n\n    test(\"doesn't serialize single fetch redirects until after the middleware chain\", async () => {\n      let res = await fixture.requestSingleFetchData(\n        \"/single-fetch-serialize/redirect.data\",\n      );\n      expect(res.status).toBe(202);\n      expect(res.headers.get(\"location\")).toBe(null);\n      expect(res.headers.get(\"x-status\")).toBe(\"302\");\n      expect(res.headers.get(\"x-location\")).toBe(\n        \"/single-fetch-serialize/target\",\n      );\n      expect(res.data).toEqual({\n        [UNSAFE_SingleFetchRedirectSymbol]: {\n          redirect: \"/single-fetch-serialize/target\",\n          reload: false,\n          replace: false,\n          revalidate: false,\n          status: 302,\n        },\n      });\n    });\n\n    test(\"bubbles response up the chain when middleware throws data() before next\", async ({\n      page,\n    }) => {\n      let res = await fixture.requestDocument(\"/throw-data-before/a/b/c\");\n      expect(res.status).toBe(418);\n      expect(res.headers.get(\"x-a\")).toBe(\"true\");\n      expect(res.headers.get(\"x-b\")).toBe(\"true\");\n      let html = await res.text();\n      expect(html).toContain(\"A Error Boundary\");\n      expect(html).toContain(\"C ERROR\");\n\n      let data = await fixture.requestSingleFetchData(\n        \"/throw-data-before/a/b/c.data\",\n      );\n      expect(data.status).toBe(418);\n      expect(data.headers.get(\"x-a\")).toBe(\"true\");\n      expect(data.headers.get(\"x-b\")).toBe(\"true\");\n      expect((data.data as any)[\"routes/throw-data-before.a.b.c\"]).toEqual({\n        error: new UNSAFE_ErrorResponseImpl(418, \"I'm a teapot\", \"C ERROR\"),\n      });\n\n      let app = new PlaywrightFixture(appFixture, page);\n      await app.goto(\"/throw-data-before\");\n      await app.clickLink(\"/throw-data-before/a/b/c\");\n      await page.waitForSelector(\"[data-error]\");\n      expect(await page.locator(\"[data-error]\").textContent()).toBe(\n        \"A Error Boundary\",\n      );\n      expect(await page.locator(\"pre\").textContent()).toBe(\"C ERROR\");\n    });\n\n    test(\"bubbles response up the chain when middleware throws data() after next\", async ({\n      page,\n    }) => {\n      let res = await fixture.requestDocument(\"/throw-data-after/a/b/c\");\n      expect(res.status).toBe(418);\n      expect(res.headers.get(\"x-a\")).toBe(\"true\");\n      expect(res.headers.get(\"x-b\")).toBe(\"true\");\n      let html = await res.text();\n      expect(html).toContain(\"A Error Boundary\");\n      expect(html).toContain(\"C ERROR\");\n      expect(html).toContain(\"A LOADER\");\n\n      let data = await fixture.requestSingleFetchData(\n        \"/throw-data-after/a/b/c.data\",\n      );\n      expect(data.status).toBe(418);\n      expect(data.headers.get(\"x-a\")).toBe(\"true\");\n      expect(data.headers.get(\"x-b\")).toBe(\"true\");\n      expect((data.data as any)[\"routes/throw-data-after.a\"]).toEqual({\n        data: \"A LOADER\",\n      });\n      expect((data.data as any)[\"routes/throw-data-after.a.b.c\"]).toEqual({\n        error: new UNSAFE_ErrorResponseImpl(418, \"I'm a teapot\", \"C ERROR\"),\n      });\n\n      let app = new PlaywrightFixture(appFixture, page);\n      await app.goto(\"/throw-data-after\");\n      await app.clickLink(\"/throw-data-after/a/b/c\");\n      await page.waitForSelector(\"[data-error]\");\n      expect(await page.locator(\"[data-error]\").textContent()).toBe(\n        \"A Error Boundary\",\n      );\n      expect(await page.locator(\"pre\").textContent()).toBe(\"C ERROR\");\n      expect(await page.locator(\"p\").textContent()).toBe(\"A LOADER\");\n    });\n\n    test(\"still calls middleware for all matches on granular data requests\", async ({\n      page,\n    }) => {\n      let appFixture = await createAppFixture(fixture);\n\n      let app = new PlaywrightFixture(appFixture, page);\n      await app.goto(\"/granular\");\n      await page.waitForSelector('a[href=\"/granular/a/b\"]');\n\n      (await page.$('a[href=\"/granular/a/b\"]'))?.click();\n      await page.waitForSelector(\"[data-b]\");\n      expect(await page.locator(\"[data-a]\").textContent()).toBe(\"A: a,b\");\n      expect(await page.locator(\"[data-b]\").textContent()).toBe(\"B: a,b\");\n    });\n\n    test(\"Filters server loaders via shouldRevalidate\", async ({ page }) => {\n      let app = new PlaywrightFixture(appFixture, page);\n\n      await app.goto(\"/revalidate/parent/1\");\n      expect(await page.locator(\"#parent-loader-data\").textContent()).toBe(\n        \"parent middleware 1,child middleware 1,parent loader 1,child loader 1\",\n      );\n      expect(await page.locator(\"#child-loader-data\").textContent()).toBe(\n        \"parent middleware 1,child middleware 1,parent loader 1,child loader 1\",\n      );\n\n      await app.clickLink(\"/revalidate/parent/2\");\n      expect(await page.locator(\"#parent-loader-data\").textContent()).toBe(\n        \"parent middleware 1,child middleware 1,parent loader 1,child loader 1\",\n      );\n      expect(await page.locator(\"#child-loader-data\").textContent()).toBe(\n        \"parent middleware 2,child middleware 2,child loader 2\",\n      );\n\n      await app.clickLink(\"/revalidate/parent/3\");\n      expect(await page.locator(\"#parent-loader-data\").textContent()).toBe(\n        \"parent middleware 1,child middleware 1,parent loader 1,child loader 1\",\n      );\n      expect(await page.locator(\"#child-loader-data\").textContent()).toBe(\n        \"parent middleware 3,child middleware 3,child loader 3\",\n      );\n    });\n\n    test(\"calls middleware for routes even without a loader (document)\", async ({\n      page,\n    }) => {\n      let app = new PlaywrightFixture(appFixture, page);\n      await app.goto(\"/without-loader-document/a/b/c\");\n      expect(await page.innerText(\"h2\")).toBe(\"A: a,b\");\n      expect(await page.innerText(\"h3\")).toBe(\"B\");\n      expect(await page.innerText(\"h4\")).toBe(\"C: a,b\");\n    });\n\n    test(\"calls middleware for routes even without a loader (data)\", async ({\n      page,\n    }) => {\n      let app = new PlaywrightFixture(appFixture, page);\n      await app.goto(\"/without-loader-data\");\n      (await page.$('a[href=\"/without-loader-data/a/b/c\"]'))?.click();\n      await page.waitForSelector(\"h4\");\n      expect(await page.innerText(\"h2\")).toBe(\"A: a,b\");\n      expect(await page.innerText(\"h3\")).toBe(\"B\");\n      expect(await page.innerText(\"h4\")).toBe(\"C: a,b\");\n    });\n\n    test(\"calls middleware on resource routes\", async ({ page }) => {\n      let fetcherHeaders: ReturnType<PlaywrightResponse[\"headers\"]> | undefined;\n      let fetchHeaders: ReturnType<PlaywrightResponse[\"headers\"]> | undefined;\n      page.on(\"request\", async (r: PlaywrightRequest) => {\n        if (r.url().includes(\"/resource/a/b.data\")) {\n          let res = await r.response();\n          fetcherHeaders = res?.headers();\n        } else if (r.url().endsWith(\"/resource/a/b?raw\")) {\n          let res = await r.response();\n          fetchHeaders = res?.headers();\n        }\n      });\n\n      let app = new PlaywrightFixture(appFixture, page);\n      await app.goto(\"/resource\");\n\n      (await page.$(\"#fetcher\"))?.click();\n      await page.waitForSelector(\"[data-fetcher]\");\n      expect(await page.locator(\"[data-fetcher]\").textContent()).toBe(\"a,b\");\n      expect(fetcherHeaders![\"x-a\"]).toBe(\"true\");\n      expect(fetcherHeaders![\"x-b\"]).toBe(\"true\");\n\n      (await page.$(\"#fetch\"))?.click();\n      await page.waitForSelector(\"[data-fetch]\");\n      expect(await page.locator(\"[data-fetch]\").textContent()).toBe(\"a,b\");\n      expect(fetchHeaders![\"x-a\"]).toBe(\"true\");\n      expect(fetchHeaders![\"x-b\"]).toBe(\"true\");\n    });\n  });\n\n  test.describe(\"Server Middleware (dev)\", () => {\n    let fixture: Fixture;\n    let appFixture: AppFixture;\n\n    test.beforeAll(async () => {\n      fixture = await createFixture(\n        {\n          files: {\n            \"react-router.config.ts\": reactRouterConfig({\n              future: { v8_middleware: true },\n            }),\n            \"vite.config.ts\": js`\n              import { defineConfig } from \"vite\";\n              import { reactRouter } from \"@react-router/dev/vite\";\n\n              export default defineConfig({\n                build: { manifest: true },\n                plugins: [reactRouter()],\n              });\n            `,\n            \"app/entry.server.tsx\": js`\n              import { PassThrough } from \"node:stream\";\n\n              import type { AppLoadContext, EntryContext } from \"react-router\";\n              import { createReadableStreamFromReadable } from \"@react-router/node\";\n              import { ServerRouter } from \"react-router\";\n              import type { RenderToPipeableStreamOptions } from \"react-dom/server\";\n              import { renderToPipeableStream } from \"react-dom/server\";\n\n              export const streamTimeout = 5_000;\n\n              export function handleError(error, { request }) {\n                if (!request.signal.aborted) {\n                  let {pathname, search} = new URL(request.url);\n                  console.error(\"handleError\", request.method, pathname + search, error);\n                }\n              }\n\n              export default function handleRequest(\n                request: Request,\n                responseStatusCode: number,\n                responseHeaders: Headers,\n                routerContext: EntryContext,\n              ) {\n                return new Promise((resolve, reject) => {\n                  let shellRendered = false;\n\n                  const { pipe, abort } = renderToPipeableStream(\n                    <ServerRouter context={routerContext} url={request.url} />,\n                    {\n                      onShellReady() {\n                        shellRendered = true;\n                        const body = new PassThrough();\n                        const stream = createReadableStreamFromReadable(body);\n\n                        responseHeaders.set(\"Content-Type\", \"text/html\");\n\n                        resolve(\n                          new Response(stream, {\n                            headers: responseHeaders,\n                            status: responseStatusCode,\n                          }),\n                        );\n\n                        pipe(body);\n                      },\n                      onShellError(error: unknown) {\n                        reject(error);\n                      },\n                      onError(error: unknown) {\n                        responseStatusCode = 500;\n                        // Log streaming rendering errors from inside the shell.  Don't log\n                        // errors encountered during initial shell rendering since they'll\n                        // reject and get logged in handleDocumentRequest.\n                        if (shellRendered) {\n                          console.error(error);\n                        }\n                      },\n                    },\n                  );\n\n                  setTimeout(abort, streamTimeout + 1000);\n                });\n              }\n            `,\n            \"app/routes/_index.tsx\": js`\n              import { Link } from 'react-router'\n\n              export default function Component() {\n                return <Link to=\"/broken\">Link</Link>;\n              }\n            `,\n            \"app/routes/broken.tsx\": js`\n              export const middleware = [\n                async ({ request, context }, next) => {\n                  throw new Error('broken!');\n                }\n              ]\n              export default function Component() {\n                return <h1>Should not see me</h1>\n              }\n              export function ErrorBoundary({ error }) {\n                return <h1 data-error>{error.message}</h1>\n              }\n            `,\n            \"app/routes/error-down-document._index.tsx\": js`\n              import { Link } from 'react-router'\n\n              export default function Component() {\n                return <Link to=\"/error-down-document/broken\">Link</Link>;\n              }\n            `,\n            \"app/routes/error-down-document.broken.tsx\": js`\n              export const middleware = [\n                async ({ request, context }, next) => {\n                  throw new Error('broken!');\n                }\n              ]\n              export default function Component() {\n                return <h1>Should not see me</h1>\n              }\n              export function ErrorBoundary({ error }) {\n                return <h1 data-error>{error.message}</h1>\n              }\n            `,\n            \"app/routes/error-down-data._index.tsx\": js`\n              import { Link } from 'react-router'\n\n              export default function Component() {\n                return <Link to=\"/error-down-data/broken\">Link</Link>;\n              }\n            `,\n            \"app/routes/error-down-data.broken.tsx\": js`\n              export const middleware = [\n                async ({ request, context }, next) => {\n                  throw new Error('broken!');\n                }\n              ]\n              export function loader() {\n                return null\n              }\n              export default function Component() {\n                return <h1>Should not see me</h1>\n              }\n              export function ErrorBoundary({ error }) {\n                return <h1 data-error>{error.message}</h1>\n              }\n            `,\n            \"app/routes/error-up-document._index.tsx\": js`\n              import { Link } from 'react-router'\n\n              export default function Component() {\n                return <Link to=\"/error-up-document/broken\">Link</Link>;\n              }\n            `,\n            \"app/routes/error-up-document.broken.tsx\": js`\n              export const middleware = [\n                async ({ request, context }, next) => {\n                  await next();\n                  throw new Error('broken!');\n                }\n              ]\n              export function loader() {\n                return \"nope\"\n              }\n              export default function Component() {\n                return <h1>Should not see me</h1>\n              }\n              export function ErrorBoundary({ error, loaderData }) {\n                return (\n                  <>\n                    <h1 data-error>{error.message}</h1>\n                    <pre>{loaderData ?? 'empty'}</pre>\n                  </>\n                );\n              }\n            `,\n            \"app/routes/error-up-data._index.tsx\": js`\n              import { Link } from 'react-router'\n\n              export default function Component() {\n                return <Link to=\"/error-up-data/broken\">Link</Link>;\n              }\n            `,\n            \"app/routes/error-up-data.broken.tsx\": js`\n              export const middleware = [\n                async ({ request, context }, next) => {\n                  await next()\n                  throw new Error('broken!');\n                }\n              ]\n              export function loader() {\n                return \"nope\"\n              }\n              export default function Component() {\n                return <h1>Should not see me</h1>\n              }\n              export function ErrorBoundary({ error, loaderData }) {\n                return (\n                  <>\n                    <h1 data-error>{error.message}</h1>\n                    <pre>{loaderData ?? 'empty'}</pre>\n                  </>\n                );\n              }\n            `,\n\n            \"app/routes/bubble-document._index.tsx\": js`\n              import { Link } from 'react-router'\n              export default function Component({ loaderData }) {\n                return <Link to=\"/bubble-document/a/b\">Link</Link>\n              }\n            `,\n            \"app/routes/bubble-document.a.tsx\": js`\n              import { Outlet } from 'react-router'\n\n              export const middleware = [\n                async ({ context }, next) => {\n                  let res = await next();\n                  res.headers.set('x-a', 'true');\n                  return res;\n                },\n              ];\n\n              export function loader() {\n                return \"A\";\n              }\n\n              export default function Component() {\n                return <><h1>A</h1><Outlet/></>;\n              }\n\n              export function ErrorBoundary({ error }) {\n                return <><h1>A Error Boundary</h1><pre>{error.message}</pre></>\n              }\n            `,\n            \"app/routes/bubble-document.a.b.tsx\": js`\n              export const middleware = [\n                async ({ context }, next) => {\n                  let res = await next();\n                  throw new Error('broken!')\n                },\n              ];\n\n              export function loader() {\n                return \"B\";\n              }\n\n              export default function Component() {\n                return <h2>B</h2>;\n              }\n            `,\n            \"app/routes/bubble-data._index.tsx\": js`\n              import { Link } from 'react-router'\n              export default function Component({ loaderData }) {\n                return <Link to=\"/bubble-data/a/b\">Link</Link>\n              }\n            `,\n            \"app/routes/bubble-data.a.tsx\": js`\n              import { Outlet } from 'react-router'\n\n              export const middleware = [\n                async ({ context }, next) => {\n                  let res = await next();\n                  res.headers.set('x-a', 'true');\n                  return res;\n                },\n              ];\n\n              export function loader() {\n                return \"A\";\n              }\n\n              export default function Component() {\n                return <><h1>A</h1><Outlet/></>;\n              }\n\n              export function ErrorBoundary({ error }) {\n                return <><h1>A Error Boundary</h1><pre>{error.message}</pre></>\n              }\n            `,\n            \"app/routes/bubble-data.a.b.tsx\": js`\n              export const middleware = [\n                async ({ context }, next) => {\n                  let res = await next();\n                  throw new Error('broken!')\n                },\n              ];\n\n              export function loader() {\n                return \"B\";\n              }\n\n              export default function Component() {\n                return <h2>B</h2>;\n              }\n            `,\n            \"app/routes/bubble-down-a._index.tsx\": js`\n              import { Link } from 'react-router'\n              export default function Component({ loaderData }) {\n                return <Link to=\"/bubble-down-a/a/b/c/d\">Link</Link>\n              }\n            `,\n            \"app/routes/bubble-down-a.a.tsx\": js`\n              import { Outlet } from 'react-router'\n              export default function Component() {\n                return <Outlet/>\n              }\n              export function ErrorBoundary({ error }) {\n                return <><h1>A Error Boundary</h1><pre>{error.message}</pre></>\n              }\n            `,\n            \"app/routes/bubble-down-a.a.b.tsx\": js`\n              import { Outlet } from 'react-router'\n              export function loader() {\n                return null;\n              }\n              export default function Component() {\n                return <Outlet />\n              }\n            `,\n            \"app/routes/bubble-down-a.a.b.c.tsx\": js`\n              import { Outlet } from 'react-router'\n              export default function Component() {\n                return <Outlet/>\n              }\n              export function ErrorBoundary({ error }) {\n                return <><h1>C Error Boundary</h1><pre>{error.message}</pre></>\n              }\n            `,\n            \"app/routes/bubble-down-a.a.b.c.d.tsx\": js`\n              import { Outlet } from 'react-router'\n              export const middleware = [() => { throw new Error(\"broken!\") }]\n              export default function Component() {\n                return <Outlet/>\n              }\n            `,\n            \"app/routes/bubble-down-b._index.tsx\": js`\n              import { Link } from 'react-router'\n              export default function Component({ loaderData }) {\n                return (\n                  <>\n                    <Link to=\"/bubble-down-b/a/b\">/a/b</Link>\n                    <br/>\n                    <Link to=\"/bubble-down-b/a/b/c/d\">/a/b/c/d</Link>\n                  </>\n                );\n              }\n            `,\n            \"app/routes/bubble-down-b.a.tsx\": js`\n              import { Outlet } from 'react-router'\n              export default function Component() {\n                return <Outlet/>\n              }\n              export function ErrorBoundary({ error }) {\n                return <><h1 data-error-a>A Error Boundary</h1><pre>{error.message}</pre></>\n              }\n            `,\n            \"app/routes/bubble-down-b.a.b.tsx\": js`\n              import { Link, Outlet } from 'react-router'\n              export function loader() {\n                return { message: \"DATA\" };\n              }\n              export default function Component({ loaderData }) {\n                return (\n                  <>\n                    <h2 data-ab>AB: {loaderData.message}</h2>\n                    <Link to=\"/bubble-down-b/a/b/c/d\">/a/b/c/d</Link>\n                    <Outlet/>\n                  </>\n                );\n              }\n              export function shouldRevalidate() {\n                return false;\n              }\n            `,\n            \"app/routes/bubble-down-b.a.b.c.tsx\": js`\n              import { Outlet } from 'react-router'\n              export default function Component() {\n                return <Outlet/>\n              }\n              export function ErrorBoundary({ error }) {\n                return <><h1 data-error-c>C Error Boundary</h1><pre>{error.message}</pre></>\n              }\n            `,\n            \"app/routes/bubble-down-b.a.b.c.d.tsx\": js`\n              import { Outlet } from 'react-router'\n              export const middleware = [() => { throw new Error(\"broken!\") }]\n              export const loader = () => null;\n              export default function Component() {\n                return <Outlet/>\n              }\n            `,\n            \"app/routes/bubble-response-before._index.tsx\": js`\n              import { Link } from 'react-router'\n              export default function Component({ loaderData }) {\n                return <Link to=\"/bubble-response-before/a/b/c\">/a/b/c</Link>;\n              }\n            `,\n            \"app/routes/bubble-response-before.a.tsx\": js`\n              import { Outlet } from 'react-router'\n              export const middleware = [\n                async (_, next) => {\n                  let res = await next();\n                  res.headers.set('x-a', 'true');\n                  return res;\n                }\n              ];\n              export default function Component() {\n                return <Outlet/>\n              }\n              export function ErrorBoundary({ error }) {\n                return <><h1 data-error>A Error Boundary</h1><pre>{error.message}</pre></>\n              }\n            `,\n            \"app/routes/bubble-response-before.a.b.tsx\": js`\n              import { Link, Outlet } from 'react-router'\n              export const middleware = [\n                async (_, next) => {\n                  let res = await next();\n                  res.headers.set('x-b', 'true');\n                  return res;\n                }\n              ];\n              export default function Component({ loaderData }) {\n                return <Outlet/>;\n              }\n            `,\n            \"app/routes/bubble-response-before.a.b.c.tsx\": js`\n              export const middleware = [(_, next) => {\n                throw new Error('C ERROR')\n              }];\n              // Force middleware to run on client side navs\n              export function loader() {\n                return null;\n              }\n              export default function Component({ loaderData }) {\n                return <h1>C</h1>\n              }\n            `,\n            \"app/routes/bubble-response-after._index.tsx\": js`\n              import { Link } from 'react-router'\n              export default function Component({ loaderData }) {\n                return <Link to=\"/bubble-response-after/a/b/c\">/a/b/c</Link>;\n              }\n            `,\n            \"app/routes/bubble-response-after.a.tsx\": js`\n              import { Outlet } from 'react-router'\n              export const middleware = [\n                async (_, next) => {\n                  let res = await next();\n                  res.headers.set('x-a', 'true');\n                  return res;\n                }\n              ];\n              export function loader() {\n                return \"A LOADER\";\n              }\n              export default function Component() {\n                return <Outlet/>\n              }\n              export function ErrorBoundary({ error, loaderData }) {\n                return (\n                  <>\n                    <h1 data-error>A Error Boundary</h1>\n                    <pre>{error.message}</pre>\n                    <p>{loaderData}</p>\n                  </>\n                );\n              }\n            `,\n            \"app/routes/bubble-response-after.a.b.tsx\": js`\n              import { Link, Outlet } from 'react-router'\n              export const middleware = [\n                async (_, next) => {\n                  let res = await next();\n                  res.headers.set('x-b', 'true');\n                  return res;\n                }\n              ];\n              export default function Component({ loaderData }) {\n                return <Outlet/>;\n              }\n            `,\n            \"app/routes/bubble-response-after.a.b.c.tsx\": js`\n              export const middleware = [async (_, next) => {\n                let res = await next();\n                throw new Error('C ERROR')\n              }];\n              // Force middleware to run on client side navs\n              export function loader() {\n                return null;\n              }\n              export default function Component({ loaderData }) {\n                return <h1>C</h1>\n              }\n            `,\n            \"app/routes/bubble-response-multiple._index.tsx\": js`\n              import { Link } from 'react-router'\n              export default function Component({ loaderData }) {\n                return <Link to=\"/bubble-response-multiple/a/b/c\">/a/b/c</Link>;\n              }\n            `,\n            \"app/routes/bubble-response-multiple.a.tsx\": js`\n              import { Outlet } from 'react-router'\n              export const middleware = [\n                async (_, next) => {\n                  let res = await next();\n                  res.headers.set('x-a', 'true');\n                  return res;\n                }\n              ];\n              export function loader() {\n                return \"A LOADER\";\n              }\n              export default function Component() {\n                return <Outlet/>\n              }\n              export function ErrorBoundary({ error, loaderData }) {\n                return (\n                  <>\n                    <h1 data-error>A Error Boundary</h1>\n                    <pre>{error.message}</pre>\n                    <p>{loaderData}</p>\n                  </>\n                );\n              }\n            `,\n            \"app/routes/bubble-response-multiple.a.b.tsx\": js`\n              import { Link, Outlet } from 'react-router'\n              export const middleware = [async (_, next) => {\n                let res = await next();\n                throw new Error('B ERROR')\n              }];\n              export default function Component({ loaderData }) {\n                return <Outlet/>;\n              }\n            `,\n            \"app/routes/bubble-response-multiple.a.b.c.tsx\": js`\n              export const middleware = [async (_, next) => {\n                let res = await next();\n                throw new Error('C ERROR')\n              }];\n              // Force middleware to run on client side navs\n              export function loader() {\n                return null;\n              }\n              export default function Component({ loaderData }) {\n                return <h1>C</h1>\n              }\n            `,\n            \"app/routes/error-down-resource.a.tsx\": js`\n              export const middleware = [\n                async ({ context }, next) => {\n                  throw new Error(\"broken!\");\n                },\n              ];\n            `,\n            \"app/routes/error-down-resource.a.b.tsx\": js`\n              export async function loader({ request, context }) {\n                return new Response(\"ok\");\n              }\n            `,\n            \"app/routes/error-up-resource.a.tsx\": js`\n              export const middleware = [\n                async ({ context }, next) => {\n                  let res = await next()\n                  throw new Error(\"broken!\");\n                },\n              ];\n            `,\n            \"app/routes/error-up-resource.a.b.tsx\": js`\n              export async function loader({ request, context }) {\n                return new Response(\"ok\");\n              }\n            `,\n          },\n        },\n        UNSAFE_ServerMode.Development,\n      );\n      appFixture = await createAppFixture(\n        fixture,\n        UNSAFE_ServerMode.Development,\n      );\n    });\n\n    test.afterAll(() => {\n      appFixture.close();\n    });\n\n    test(\"handles errors thrown on the way down (document)\", async ({\n      page,\n    }) => {\n      let errors: any[] = [];\n      console.error = (...args) => errors.push(args);\n\n      let app = new PlaywrightFixture(appFixture, page);\n      await app.goto(\"/error-down-document/broken\");\n      expect(await page.innerText(\"[data-error]\")).toBe(\"broken!\");\n      expect(errors).toEqual([\n        [\n          \"handleError\",\n          \"GET\",\n          \"/error-down-document/broken\",\n          new Error(\"broken!\"),\n        ],\n      ]);\n    });\n\n    test(\"handles errors thrown on the way down (data)\", async ({ page }) => {\n      let errors: any[] = [];\n      console.error = (...args) => errors.push(args);\n\n      let app = new PlaywrightFixture(appFixture, page);\n      await app.goto(\"/error-down-data\");\n\n      (await page.$('a[href=\"/error-down-data/broken\"]'))?.click();\n      await page.waitForSelector(\"[data-error]\");\n      expect(await page.innerText(\"[data-error]\")).toBe(\"broken!\");\n      expect(errors).toEqual([\n        [\n          \"handleError\",\n          \"GET\",\n          \"/error-down-data/broken.data\",\n          new Error(\"broken!\"),\n        ],\n      ]);\n    });\n\n    test(\"handles errors thrown on the way up (document)\", async ({ page }) => {\n      let errors: any[] = [];\n      console.error = (...args) => errors.push(args);\n\n      let app = new PlaywrightFixture(appFixture, page);\n      await app.goto(\"/error-up-document/broken\");\n      expect(await page.innerText(\"[data-error]\")).toBe(\"broken!\");\n      expect(await page.innerText(\"pre\")).toBe(\"empty\");\n      expect(errors).toEqual([\n        [\n          \"handleError\",\n          \"GET\",\n          \"/error-up-document/broken\",\n          new Error(\"broken!\"),\n        ],\n      ]);\n    });\n\n    test(\"handles errors thrown on the way up (data)\", async ({ page }) => {\n      let errors: any[] = [];\n      console.error = (...args) => errors.push(args);\n\n      let app = new PlaywrightFixture(appFixture, page);\n      await app.goto(\"/error-up-data\");\n\n      (await page.$('a[href=\"/error-up-data/broken\"]'))?.click();\n      await page.waitForSelector(\"h1\");\n      await page.waitForSelector(\"[data-error]\");\n      expect(await page.innerText(\"[data-error]\")).toBe(\"broken!\");\n      expect(await page.innerText(\"pre\")).toBe(\"empty\");\n      expect(errors).toEqual([\n        [\n          \"handleError\",\n          \"GET\",\n          \"/error-up-data/broken.data\",\n          new Error(\"broken!\"),\n        ],\n      ]);\n    });\n\n    test(\"bubbles errors up on document requests\", async ({ page }) => {\n      let errors: any[] = [];\n      console.error = (...args) => errors.push(args);\n\n      let app = new PlaywrightFixture(appFixture, page);\n      await app.goto(\"/bubble-document/a/b\");\n      expect(await page.locator(\"h1\").textContent()).toBe(\"A Error Boundary\");\n      expect(await page.locator(\"pre\").textContent()).toBe(\"broken!\");\n      expect(errors).toEqual([\n        [\"handleError\", \"GET\", \"/bubble-document/a/b\", new Error(\"broken!\")],\n      ]);\n    });\n\n    test(\"bubbles errors up on data requests\", async ({ page }) => {\n      let errors: any[] = [];\n      console.error = (...args) => errors.push(args);\n\n      let app = new PlaywrightFixture(appFixture, page);\n      await app.goto(\"/bubble-data\");\n\n      (await page.$('a[href=\"/bubble-data/a/b\"]'))?.click();\n      await page.waitForSelector(\"pre\");\n      expect(await page.locator(\"h1\").textContent()).toBe(\"A Error Boundary\");\n      expect(await page.locator(\"pre\").textContent()).toBe(\"broken!\");\n      expect(errors).toEqual([\n        [\"handleError\", \"GET\", \"/bubble-data/a/b.data\", new Error(\"broken!\")],\n      ]);\n    });\n\n    test(\"bubbles errors on the way down up to at least the highest route with a loader\", async ({\n      page,\n    }) => {\n      let errors: any[] = [];\n      console.error = (...args) => errors.push(args);\n\n      let app = new PlaywrightFixture(appFixture, page);\n      await app.goto(\"/bubble-down-a/a/b/c/d\");\n      expect(await page.locator(\"h1\").textContent()).toBe(\"A Error Boundary\");\n      expect(await page.locator(\"pre\").textContent()).toBe(\"broken!\");\n      expect(errors).toEqual([\n        [\"handleError\", \"GET\", \"/bubble-down-a/a/b/c/d\", new Error(\"broken!\")],\n      ]);\n      errors.splice(0);\n\n      await app.goto(\"/bubble-down-a\");\n      await app.clickLink(\"/bubble-down-a/a/b/c/d\");\n      expect(await page.locator(\"h1\").textContent()).toBe(\"A Error Boundary\");\n      expect(await page.locator(\"pre\").textContent()).toBe(\"broken!\");\n      expect(errors).toEqual([\n        [\n          \"handleError\",\n          \"GET\",\n          \"/bubble-down-a/a/b/c/d.data\",\n          new Error(\"broken!\"),\n        ],\n      ]);\n    });\n\n    test(\"bubbles errors on the way down up to the deepest error boundary when loaders aren't revalidating\", async ({\n      page,\n    }) => {\n      let errors: any[] = [];\n      console.error = (...args) => errors.push(args);\n\n      let app = new PlaywrightFixture(appFixture, page);\n      await app.goto(\"/bubble-down-b\");\n      await app.clickLink(\"/bubble-down-b/a/b\");\n      await page.waitForSelector(\"[data-ab]\");\n      expect(await page.locator(\"[data-ab]\").textContent()).toBe(\"AB: DATA\");\n      expect(errors).toEqual([]);\n\n      await app.clickLink(\"/bubble-down-b/a/b/c/d\");\n      await page.waitForSelector(\"[data-error-c]\");\n      expect(await page.locator(\"h1\").textContent()).toBe(\"C Error Boundary\");\n      expect(await page.locator(\"pre\").textContent()).toBe(\"broken!\");\n      expect(errors).toEqual([\n        [\n          \"handleError\",\n          \"GET\",\n          \"/bubble-down-b/a/b/c/d.data?_routes=routes%2Fbubble-down-b.a.b.c.d\",\n          new Error(\"broken!\"),\n        ],\n      ]);\n    });\n\n    test(\"bubbles response up the chain when middleware throws before next\", async ({\n      page,\n    }) => {\n      let res = await fixture.requestDocument(\"/bubble-response-before/a/b/c\");\n      expect(res.status).toBe(500);\n      expect(res.headers.get(\"x-a\")).toBe(\"true\");\n      expect(res.headers.get(\"x-b\")).toBe(\"true\");\n      let html = await res.text();\n      expect(html).toContain(\"A Error Boundary\");\n      expect(html).toContain(\"C ERROR\");\n\n      let data = await fixture.requestSingleFetchData(\n        \"/bubble-response-before/a/b/c.data\",\n      );\n      expect(data.status).toBe(500);\n      expect(data.headers.get(\"x-a\")).toBe(\"true\");\n      expect(data.headers.get(\"x-b\")).toBe(\"true\");\n      expect((data.data as any)[\"routes/bubble-response-before.a.b.c\"]).toEqual(\n        {\n          error: new Error(\"C ERROR\"),\n        },\n      );\n\n      let app = new PlaywrightFixture(appFixture, page);\n      await app.goto(\"/bubble-response-before\");\n      await app.clickLink(\"/bubble-response-before/a/b/c\");\n      await page.waitForSelector(\"[data-error]\");\n      expect(await page.locator(\"[data-error]\").textContent()).toBe(\n        \"A Error Boundary\",\n      );\n      expect(await page.locator(\"pre\").textContent()).toBe(\"C ERROR\");\n    });\n\n    test(\"bubbles response up the chain when middleware throws after next\", async ({\n      page,\n    }) => {\n      let res = await fixture.requestDocument(\"/bubble-response-after/a/b/c\");\n      expect(res.status).toBe(500);\n      expect(res.headers.get(\"x-a\")).toBe(\"true\");\n      expect(res.headers.get(\"x-b\")).toBe(\"true\");\n      let html = await res.text();\n      expect(html).toContain(\"A Error Boundary\");\n      expect(html).toContain(\"C ERROR\");\n      expect(html).toContain(\"A LOADER\");\n\n      let data = await fixture.requestSingleFetchData(\n        \"/bubble-response-after/a/b/c.data\",\n      );\n      expect(data.status).toBe(500);\n      expect(data.headers.get(\"x-a\")).toBe(\"true\");\n      expect(data.headers.get(\"x-b\")).toBe(\"true\");\n      expect((data.data as any)[\"routes/bubble-response-after.a\"]).toEqual({\n        data: \"A LOADER\",\n      });\n      expect((data.data as any)[\"routes/bubble-response-after.a.b.c\"]).toEqual({\n        error: new Error(\"C ERROR\"),\n      });\n\n      let app = new PlaywrightFixture(appFixture, page);\n      await app.goto(\"/bubble-response-after\");\n      await app.clickLink(\"/bubble-response-after/a/b/c\");\n      await page.waitForSelector(\"[data-error]\");\n      expect(await page.locator(\"[data-error]\").textContent()).toBe(\n        \"A Error Boundary\",\n      );\n      expect(await page.locator(\"pre\").textContent()).toBe(\"C ERROR\");\n      expect(await page.locator(\"p\").textContent()).toBe(\"A LOADER\");\n    });\n\n    test(\"bubbles response up the chain when multiple middlewares throw in sequence\", async ({\n      page,\n    }) => {\n      let errors: any[] = [];\n      console.error = (...args) => errors.push(args);\n\n      let res = await fixture.requestDocument(\n        \"/bubble-response-multiple/a/b/c\",\n      );\n      expect(res.status).toBe(500);\n      expect(res.headers.get(\"x-a\")).toBe(\"true\");\n      expect(res.headers.get(\"x-b\")).toBe(null);\n      let html = await res.text();\n      expect(html).toContain(\"A Error Boundary\");\n      expect(html).toContain(\"B ERROR\");\n      expect(html).toContain(\"A LOADER\");\n      expect(errors).toEqual([\n        [\n          \"handleError\",\n          \"GET\",\n          \"/bubble-response-multiple/a/b/c\",\n          new Error(\"C ERROR\"),\n        ],\n        [\n          \"handleError\",\n          \"GET\",\n          \"/bubble-response-multiple/a/b/c\",\n          new Error(\"B ERROR\"),\n        ],\n      ]);\n      errors.splice(0);\n\n      let data = await fixture.requestSingleFetchData(\n        \"/bubble-response-multiple/a/b/c.data\",\n      );\n      expect(data.status).toBe(500);\n      expect(data.headers.get(\"x-a\")).toBe(\"true\");\n      expect(data.headers.get(\"x-b\")).toBe(null);\n      expect((data.data as any)[\"routes/bubble-response-multiple.a\"]).toEqual({\n        data: \"A LOADER\",\n      });\n      expect((data.data as any)[\"routes/bubble-response-multiple.a.b\"]).toEqual(\n        {\n          error: new Error(\"B ERROR\"),\n        },\n      );\n      expect(errors).toEqual([\n        [\n          \"handleError\",\n          \"GET\",\n          \"/bubble-response-multiple/a/b/c.data\",\n          new Error(\"C ERROR\"),\n        ],\n        [\n          \"handleError\",\n          \"GET\",\n          \"/bubble-response-multiple/a/b/c.data\",\n          new Error(\"B ERROR\"),\n        ],\n      ]);\n      errors.splice(0);\n\n      let app = new PlaywrightFixture(appFixture, page);\n      await app.goto(\"/bubble-response-multiple\");\n      await app.clickLink(\"/bubble-response-multiple/a/b/c\");\n      await page.waitForSelector(\"[data-error]\");\n      expect(await page.locator(\"[data-error]\").textContent()).toBe(\n        \"A Error Boundary\",\n      );\n      expect(await page.locator(\"pre\").textContent()).toBe(\"B ERROR\");\n      expect(await page.locator(\"p\").textContent()).toBe(\"A LOADER\");\n      expect(errors).toEqual([\n        [\n          \"handleError\",\n          \"GET\",\n          \"/bubble-response-multiple/a/b/c.data\",\n          new Error(\"C ERROR\"),\n        ],\n        [\n          \"handleError\",\n          \"GET\",\n          \"/bubble-response-multiple/a/b/c.data\",\n          new Error(\"B ERROR\"),\n        ],\n      ]);\n      errors.splice(0);\n    });\n\n    test(\"handles errors on the way down on resource routes\", async () => {\n      let errors: any[] = [];\n      console.error = (...args) => errors.push(args);\n\n      let res = await fixture.requestResource(\"/error-down-resource/a/b\");\n      expect(res.status).toBe(500);\n      await expect(res.text()).resolves.toBe(\n        \"Unexpected Server Error\\n\\nError: broken!\",\n      );\n      expect(errors).toEqual([\n        [\n          \"handleError\",\n          \"GET\",\n          \"/error-down-resource/a/b\",\n          new Error(\"broken!\"),\n        ],\n      ]);\n      errors.splice(0);\n\n      let res2 = await fixture.requestSingleFetchData(\n        \"/error-down-resource/a/b.data\",\n      );\n      expect(res2.status).toBe(500);\n      expect(res2.data).toEqual({\n        \"routes/error-down-resource.a\": { error: new Error(\"broken!\") },\n      });\n      expect(errors).toEqual([\n        [\n          \"handleError\",\n          \"GET\",\n          \"/error-down-resource/a/b.data\",\n          new Error(\"broken!\"),\n        ],\n      ]);\n      errors.splice(0);\n    });\n\n    test(\"handles errors on the way up on resource routes\", async () => {\n      let errors: any[] = [];\n      console.error = (...args) => errors.push(args);\n\n      let res = await fixture.requestResource(\"/error-up-resource/a/b\");\n      expect(res.status).toBe(500);\n      await expect(res.text()).resolves.toBe(\n        \"Unexpected Server Error\\n\\nError: broken!\",\n      );\n      expect(errors).toEqual([\n        [\"handleError\", \"GET\", \"/error-up-resource/a/b\", new Error(\"broken!\")],\n      ]);\n      errors.splice(0);\n\n      let res2 = await fixture.requestSingleFetchData(\n        \"/error-up-resource/a/b.data\",\n      );\n      expect(res2.status).toBe(500);\n      expect(res2.data).toEqual({\n        \"routes/error-up-resource.a\": { error: new Error(\"broken!\") },\n        \"routes/error-up-resource.a.b\": { data: \"ok\" },\n      });\n      expect(errors).toEqual([\n        [\n          \"handleError\",\n          \"GET\",\n          \"/error-up-resource/a/b.data\",\n          new Error(\"broken!\"),\n        ],\n      ]);\n      errors.splice(0);\n    });\n  });\n});\n"
  },
  {
    "path": "integration/multiple-cookies-test.ts",
    "content": "import { test, expect } from \"@playwright/test\";\n\nimport {\n  createAppFixture,\n  createFixture,\n  js,\n} from \"./helpers/create-fixture.js\";\nimport type { AppFixture } from \"./helpers/create-fixture.js\";\nimport { PlaywrightFixture } from \"./helpers/playwright-fixture.js\";\n\ntest.describe(\"pathless layout routes\", () => {\n  let appFixture: AppFixture;\n\n  test.beforeAll(async () => {\n    appFixture = await createAppFixture(\n      await createFixture({\n        files: {\n          \"app/routes/_index.tsx\": js`\n            import { data, redirect, Form, useActionData } from \"react-router\";\n\n            export let loader = async () => {\n              let headers = new Headers();\n              headers.append(\"Set-Cookie\", \"foo=bar\");\n              headers.append(\"Set-Cookie\", \"bar=baz\");\n              return data({}, { headers });\n            };\n\n            export let action = async () => {\n              let headers = new Headers();\n              headers.append(\"Set-Cookie\", \"another=one\");\n              headers.append(\"Set-Cookie\", \"how-about=two\");\n              return data({success: true}, { headers });\n            };\n\n            export default function MultipleSetCookiesPage() {\n              let actionData = useActionData();\n              return (\n                <>\n                  <p>👋</p>\n                  <Form method=\"post\">\n                    <button type=\"submit\">Add cookies</button>\n                  </Form>\n                  {actionData?.success && <p data-testid=\"action-success\">Success!</p>}\n                </>\n              );\n            };\n          `,\n        },\n      }),\n    );\n  });\n\n  test.afterAll(() => {\n    appFixture.close();\n  });\n\n  test(\"should get multiple cookies from the loader\", async ({ page }) => {\n    let app = new PlaywrightFixture(appFixture, page);\n    let responses = app.collectResponses((url) => url.pathname === \"/\");\n    await app.goto(\"/\");\n    let setCookies = await responses[0].headerValues(\"set-cookie\");\n    expect(setCookies).toEqual([\"foo=bar\", \"bar=baz\"]);\n    expect(responses).toHaveLength(1);\n  });\n\n  test(\"should get multiple cookies from the action\", async ({ page }) => {\n    let app = new PlaywrightFixture(appFixture, page);\n    await app.goto(\"/\");\n    // do this after the first request so that it doesnt appear in our next assertions\n    let responses = app.collectResponses(\n      (url) => url.pathname === \"/_root.data\",\n    );\n    await page.click(\"button[type=submit]\");\n    await page.waitForSelector(`[data-testid=\"action-success\"]`);\n    let setCookies = await responses[0].headerValues(\"set-cookie\");\n    expect(setCookies).toEqual([\"another=one\", \"how-about=two\"]);\n    // one for the POST and one for the GET\n    expect(responses).toHaveLength(2);\n  });\n});\n"
  },
  {
    "path": "integration/navigation-state-test.ts",
    "content": "import { test, expect } from \"@playwright/test\";\n\nimport {\n  createAppFixture,\n  createFixture,\n  js,\n} from \"./helpers/create-fixture.js\";\nimport type { Fixture, AppFixture } from \"./helpers/create-fixture.js\";\nimport { PlaywrightFixture } from \"./helpers/playwright-fixture.js\";\n\nconst STATES = {\n  NORMAL_LOAD: \"normal-load\",\n  LOADING_REDIRECT: \"loading-redirect\",\n  SUBMITTING_LOADER: \"submitting-loader\",\n  SUBMITTING_LOADER_REDIRECT: \"submitting-loader-redirect\",\n  SUBMITTING_ACTION: \"submitting-action\",\n  SUBMITTING_ACTION_REDIRECT: \"submitting-action-redirect\",\n  FETCHER_REDIRECT: \"fetcher-redirect\",\n} as const;\n\nconst IDLE_STATE = {\n  state: \"idle\",\n};\n\ntest.describe(\"navigation states\", () => {\n  let fixture: Fixture;\n  let appFixture: AppFixture;\n\n  test.beforeEach(async ({ context }) => {\n    await context.route(/\\.data$/, async (route) => {\n      await new Promise((resolve) => setTimeout(resolve, 50));\n      route.continue();\n    });\n  });\n\n  test.beforeAll(async () => {\n    fixture = await createFixture({\n      files: {\n        \"app/root.tsx\": js`\n            import { useMemo, useRef } from \"react\";\n            import { Outlet, Scripts, useNavigation } from \"react-router\";\n            export default function() {\n              const navigation = useNavigation();\n              const navigationsRef = useRef();\n              const navigations = useMemo(() => {\n                const savedNavigations = navigationsRef.current || [];\n                savedNavigations.push(navigation);\n                navigationsRef.current = savedNavigations;\n                return savedNavigations;\n              }, [navigation]);\n              return (\n                <html lang=\"en\">\n                  <head><title>Test</title></head>\n                  <body>\n                    <Outlet />\n                      {navigation.state != \"idle\" && (\n                        <p id=\"loading-indicator\">Loading...</p>\n                      )}\n                    <p>\n                      <code id=\"navigations\">\n                        {JSON.stringify(navigations, null, 2)}\n                      </code>\n                    </p>\n                    <Scripts />\n                  </body>\n                </html>\n              );\n            }\n          `,\n        \"app/routes/_index.tsx\": js`\n            import { Form, Link, useFetcher } from \"react-router\";\n            export function loader() { return null; }\n            export default function() {\n              const fetcher = useFetcher();\n              return (\n                <ul>\n                  <li>\n                    <Link to=\"/${STATES.NORMAL_LOAD}\">\n                      ${STATES.NORMAL_LOAD}\n                    </Link>\n                  </li>\n                  <li>\n                    <Link to=\"/${STATES.LOADING_REDIRECT}\">\n                      ${STATES.LOADING_REDIRECT}\n                    </Link>\n                  </li>\n                  <li>\n                    <Form action=\"/${STATES.SUBMITTING_LOADER}\" method=\"get\">\n                      <button type=\"submit\" name=\"key\" value=\"value\">\n                        ${STATES.SUBMITTING_LOADER}\n                      </button>\n                    </Form>\n                  </li>\n                  <li>\n                    <Form action=\"/${STATES.SUBMITTING_LOADER_REDIRECT}\" method=\"get\">\n                      <button type=\"submit\">\n                        ${STATES.SUBMITTING_LOADER_REDIRECT}\n                      </button>\n                    </Form>\n                  </li>\n                  <li>\n                    <Form action=\"/${STATES.SUBMITTING_ACTION}\" method=\"post\">\n                      <button type=\"submit\">\n                        ${STATES.SUBMITTING_ACTION}\n                      </button>\n                    </Form>\n                  </li>\n                  <li>\n                    <Form action=\"/${STATES.SUBMITTING_ACTION_REDIRECT}\" method=\"post\">\n                      <button type=\"submit\">\n                        ${STATES.SUBMITTING_ACTION_REDIRECT}\n                      </button>\n                    </Form>\n                  </li>\n                  <li>\n                    <fetcher.Form action=\"/${STATES.FETCHER_REDIRECT}\" method=\"post\">\n                      <button type=\"submit\">\n                        ${STATES.FETCHER_REDIRECT}\n                      </button>\n                    </fetcher.Form>\n                  </li>\n                </ul>\n              );\n            }\n          `,\n        [`app/routes/${STATES.NORMAL_LOAD}.jsx`]: js`\n            export default function() {\n              return (\n                <h2 id=\"${STATES.NORMAL_LOAD}\">\n                  ${STATES.NORMAL_LOAD}\n                </h2>\n              );\n            }\n          `,\n        [`app/routes/${STATES.LOADING_REDIRECT}.jsx`]: js`\n            import { redirect } from \"react-router\";\n            export function loader() {\n              return redirect(\"/?redirected\");\n            }\n            export default function() {\n              return (\n                <h2 id=\"${STATES.LOADING_REDIRECT}\">\n                  ${STATES.LOADING_REDIRECT}\n                </h2>\n              );\n            }\n          `,\n        [`app/routes/${STATES.SUBMITTING_LOADER}.jsx`]: js`\n            export default function() {\n              return (\n                <h2 id=\"${STATES.SUBMITTING_LOADER}\">\n                  ${STATES.SUBMITTING_LOADER}\n                </h2>\n              );\n            }\n          `,\n        [`app/routes/${STATES.SUBMITTING_LOADER_REDIRECT}.jsx`]: js`\n            import { redirect } from \"react-router\";\n            export function loader() {\n              return redirect(\"/?redirected\");\n            }\n            export default function() {\n              return (\n                <h2 id=\"${STATES.SUBMITTING_LOADER_REDIRECT}\">\n                  ${STATES.SUBMITTING_LOADER_REDIRECT}\n                </h2>\n              );\n            }\n          `,\n        [`app/routes/${STATES.SUBMITTING_ACTION}.jsx`]: js`\n            export function loader() { return null; }\n            export function action() { return null; }\n            export default function() {\n              return (\n                <h2 id=\"${STATES.SUBMITTING_ACTION}\">\n                  ${STATES.SUBMITTING_ACTION}\n                </h2>\n              );\n            }\n          `,\n        [`app/routes/${STATES.SUBMITTING_ACTION_REDIRECT}.jsx`]: js`\n            import { redirect } from \"react-router\";\n            export function action() {\n              return redirect(\"/?redirected\");\n            }\n            export default function() {\n              return (\n                <h2 id=\"${STATES.SUBMITTING_ACTION_REDIRECT}\">\n                  ${STATES.SUBMITTING_ACTION_REDIRECT}\n                </h2>\n              );\n            }\n          `,\n        [`app/routes/${STATES.FETCHER_REDIRECT}.jsx`]: js`\n            import { redirect } from \"react-router\";\n            export function action() {\n              return redirect(\"/?redirected\");\n            }\n          `,\n      },\n    });\n\n    appFixture = await createAppFixture(fixture);\n  });\n\n  test.afterAll(() => {\n    appFixture.close();\n  });\n\n  test(\"normal load (Loading)\", async ({ page }) => {\n    let app = new PlaywrightFixture(appFixture, page);\n    await app.goto(\"/\", true);\n    await app.clickLink(`/${STATES.NORMAL_LOAD}`);\n    await page.waitForSelector(`#${STATES.NORMAL_LOAD}`);\n    await page.waitForSelector(\"#loading-indicator\", { state: \"hidden\" });\n\n    let navigationsCode = await app.getElement(\"#navigations\");\n    let navigationsJson = navigationsCode.text();\n    let navigations = JSON.parse(navigationsJson);\n    expect(navigations).toEqual([\n      IDLE_STATE,\n      {\n        state: \"loading\",\n        location: {\n          pathname: `/${STATES.NORMAL_LOAD}`,\n          search: \"\",\n          hash: \"\",\n          state: null,\n          key: expect.any(String),\n        },\n      },\n      IDLE_STATE,\n    ]);\n  });\n\n  test(\"normal redirect (LoadingRedirect)\", async ({ page }) => {\n    let app = new PlaywrightFixture(appFixture, page);\n    await app.goto(\"/\", true);\n    await app.clickLink(`/${STATES.LOADING_REDIRECT}`);\n    await page.waitForURL(/\\?redirected/);\n    await page.waitForSelector(\"#loading-indicator\", { state: \"hidden\" });\n    let navigationsCode = await app.getElement(\"#navigations\");\n    let navigationsJson = navigationsCode.text();\n    let navigations = JSON.parse(navigationsJson);\n    expect(navigations).toEqual([\n      IDLE_STATE,\n      {\n        state: \"loading\",\n        location: {\n          pathname: `/${STATES.LOADING_REDIRECT}`,\n          search: \"\",\n          hash: \"\",\n          state: null,\n          key: expect.any(String),\n        },\n      },\n      {\n        state: \"loading\",\n        location: {\n          pathname: \"/\",\n          search: \"?redirected\",\n          hash: \"\",\n          state: {\n            _isRedirect: true,\n          },\n          key: expect.any(String),\n        },\n      },\n      IDLE_STATE,\n    ]);\n  });\n\n  test(\"loader submission (SubmittingLoader)\", async ({ page }) => {\n    let app = new PlaywrightFixture(appFixture, page);\n    await app.goto(\"/\", true);\n    await app.clickSubmitButton(`/${STATES.SUBMITTING_LOADER}`);\n    await page.waitForSelector(`#${STATES.SUBMITTING_LOADER}`);\n    await page.waitForSelector(\"#loading-indicator\", { state: \"hidden\" });\n    let navigationsCode = await app.getElement(\"#navigations\");\n    let navigationsJson = navigationsCode.text();\n    let navigations = JSON.parse(navigationsJson);\n    expect(navigations).toEqual([\n      IDLE_STATE,\n      {\n        state: \"loading\",\n        location: {\n          pathname: `/${STATES.SUBMITTING_LOADER}`,\n          search: \"?key=value\",\n          hash: \"\",\n          state: null,\n          key: expect.any(String),\n        },\n        formMethod: \"GET\",\n        formAction: `/${STATES.SUBMITTING_LOADER}`,\n        formEncType: \"application/x-www-form-urlencoded\",\n        formData: expect.any(Object),\n      },\n      IDLE_STATE,\n    ]);\n  });\n\n  test(\"loader submission redirect (LoadingLoaderSubmissionRedirect)\", async ({\n    page,\n  }) => {\n    let app = new PlaywrightFixture(appFixture, page);\n    await app.goto(\"/\", true);\n    await app.clickSubmitButton(`/${STATES.SUBMITTING_LOADER_REDIRECT}`);\n    await page.waitForURL(/\\?redirected/);\n    await page.waitForSelector(\"#loading-indicator\", { state: \"hidden\" });\n    let navigationsCode = await app.getElement(\"#navigations\");\n    let navigationsJson = navigationsCode.text();\n    let navigations = JSON.parse(navigationsJson);\n    expect(navigations).toEqual([\n      IDLE_STATE,\n      {\n        state: \"loading\",\n        location: {\n          pathname: `/${STATES.SUBMITTING_LOADER_REDIRECT}`,\n          search: \"\",\n          hash: \"\",\n          state: null,\n          key: expect.any(String),\n        },\n        formMethod: \"GET\",\n        formAction: `/${STATES.SUBMITTING_LOADER_REDIRECT}`,\n        formEncType: \"application/x-www-form-urlencoded\",\n        formData: expect.any(Object),\n      },\n      {\n        state: \"loading\",\n        location: {\n          pathname: \"/\",\n          search: \"?redirected\",\n          hash: \"\",\n          state: {\n            _isRedirect: true,\n          },\n          key: expect.any(String),\n        },\n        formMethod: \"GET\",\n        formAction: `/${STATES.SUBMITTING_LOADER_REDIRECT}`,\n        formEncType: \"application/x-www-form-urlencoded\",\n        formData: expect.any(Object),\n      },\n      IDLE_STATE,\n    ]);\n  });\n\n  test(\"action submission (SubmittingAction)\", async ({ page }) => {\n    let app = new PlaywrightFixture(appFixture, page);\n    await app.goto(\"/\", true);\n    await app.clickSubmitButton(`/${STATES.SUBMITTING_ACTION}`);\n    await page.waitForSelector(`#${STATES.SUBMITTING_ACTION}`);\n    await page.waitForSelector(\"#loading-indicator\", { state: \"hidden\" });\n    let navigationsCode = await app.getElement(\"#navigations\");\n    let navigationsJson = navigationsCode.text();\n    let navigations = JSON.parse(navigationsJson);\n    expect(navigations).toEqual([\n      IDLE_STATE,\n      {\n        state: \"submitting\",\n        location: {\n          pathname: `/${STATES.SUBMITTING_ACTION}`,\n          search: \"\",\n          hash: \"\",\n          state: null,\n          key: expect.any(String),\n        },\n        formMethod: \"POST\",\n        formAction: `/${STATES.SUBMITTING_ACTION}`,\n        formEncType: \"application/x-www-form-urlencoded\",\n        formData: expect.any(Object),\n      },\n      {\n        state: \"loading\",\n        location: {\n          pathname: `/${STATES.SUBMITTING_ACTION}`,\n          search: \"\",\n          hash: \"\",\n          state: null,\n          key: expect.any(String),\n        },\n        formMethod: \"POST\",\n        formAction: `/${STATES.SUBMITTING_ACTION}`,\n        formEncType: \"application/x-www-form-urlencoded\",\n        formData: expect.any(Object),\n      },\n      IDLE_STATE,\n    ]);\n  });\n\n  test(\"action submission redirect (LoadingActionRedirect)\", async ({\n    page,\n  }) => {\n    let app = new PlaywrightFixture(appFixture, page);\n    await app.goto(\"/\", true);\n    await app.clickSubmitButton(`/${STATES.SUBMITTING_ACTION_REDIRECT}`);\n    await page.waitForURL(/\\?redirected/);\n    await page.waitForSelector(\"#loading-indicator\", { state: \"hidden\" });\n    let navigationsCode = await app.getElement(\"#navigations\");\n    let navigationsJson = navigationsCode.text();\n    let navigations = JSON.parse(navigationsJson);\n    expect(navigations).toEqual([\n      IDLE_STATE,\n      {\n        state: \"submitting\",\n        location: {\n          pathname: `/${STATES.SUBMITTING_ACTION_REDIRECT}`,\n          search: \"\",\n          hash: \"\",\n          state: null,\n          key: expect.any(String),\n        },\n        formMethod: \"POST\",\n        formAction: `/${STATES.SUBMITTING_ACTION_REDIRECT}`,\n        formEncType: \"application/x-www-form-urlencoded\",\n        formData: expect.any(Object),\n      },\n      {\n        state: \"loading\",\n        location: {\n          pathname: \"/\",\n          search: \"?redirected\",\n          hash: \"\",\n          state: {\n            _isRedirect: true,\n          },\n          key: expect.any(String),\n        },\n        formMethod: \"POST\",\n        formAction: `/${STATES.SUBMITTING_ACTION_REDIRECT}`,\n        formEncType: \"application/x-www-form-urlencoded\",\n        formData: expect.any(Object),\n      },\n      IDLE_STATE,\n    ]);\n  });\n\n  test(\"fetcher action submission redirect (LoadingFetchActionRedirect)\", async ({\n    page,\n  }) => {\n    let app = new PlaywrightFixture(appFixture, page);\n    await app.goto(\"/\", true);\n    await app.clickSubmitButton(`/${STATES.FETCHER_REDIRECT}`);\n    await page.waitForURL(/\\?redirected/);\n    await page.waitForSelector(\"#loading-indicator\", { state: \"hidden\" });\n    let navigationsCode = await app.getElement(\"#navigations\");\n    let navigationsJson = navigationsCode.text();\n    let navigations = JSON.parse(navigationsJson);\n    expect(navigations).toEqual([\n      IDLE_STATE,\n      {\n        state: \"loading\",\n        location: {\n          pathname: \"/\",\n          search: \"?redirected\",\n          hash: \"\",\n          state: {\n            _isRedirect: true,\n          },\n          key: expect.any(String),\n        },\n      },\n      IDLE_STATE,\n    ]);\n  });\n});\n"
  },
  {
    "path": "integration/package.json",
    "content": "{\n  \"name\": \"integration\",\n  \"version\": \"0.0.0\",\n  \"private\": true,\n  \"description\": \"deps needed for integration tests\",\n  \"type\": \"module\",\n  \"scripts\": {\n    \"typecheck\": \"tsc\"\n  },\n  \"dependencies\": {\n    \"@mdx-js/rollup\": \"^3.1.0\",\n    \"@playwright/test\": \"^1.49.1\",\n    \"@react-router/dev\": \"workspace:*\",\n    \"@react-router/express\": \"workspace:*\",\n    \"@react-router/node\": \"workspace:*\",\n    \"@types/cross-spawn\": \"^6.0.6\",\n    \"@types/dedent\": \"^0.7.0\",\n    \"@types/express\": \"^4.17.9\",\n    \"@types/glob\": \"^8.1.0\",\n    \"@types/semver\": \"^7.7.0\",\n    \"@types/shelljs\": \"^0.8.16\",\n    \"@types/wait-on\": \"^5.3.4\",\n    \"@vanilla-extract/css\": \"^1.17.4\",\n    \"@vanilla-extract/vite-plugin\": \"^5.1.1\",\n    \"cheerio\": \"^1.0.0-rc.12\",\n    \"cross-spawn\": \"^7.0.3\",\n    \"dedent\": \"^0.7.0\",\n    \"execa\": \"^9.6.0\",\n    \"express\": \"^4.19.2\",\n    \"get-port\": \"^5.1.1\",\n    \"glob\": \"8.0.3\",\n    \"isbot\": \"^5.1.11\",\n    \"pathe\": \"^1.1.2\",\n    \"postcss\": \"^8.4.19\",\n    \"postcss-import\": \"^15.1.0\",\n    \"prettier\": \"^3.6.2\",\n    \"react\": \"catalog:\",\n    \"react-dom\": \"catalog:\",\n    \"react-router\": \"workspace:*\",\n    \"semver\": \"^7.7.2\",\n    \"serialize-javascript\": \"^6.0.1\",\n    \"shelljs\": \"^0.8.5\",\n    \"strip-ansi\": \"^6.0.1\",\n    \"strip-indent\": \"^3.0.0\",\n    \"type-fest\": \"^4.0.0\",\n    \"typescript\": \"catalog:\",\n    \"vite\": \"^6.3.0\",\n    \"vite-env-only\": \"^3.0.1\",\n    \"vite-tsconfig-paths\": \"^4.2.2\",\n    \"wait-on\": \"^7.0.1\"\n  }\n}\n"
  },
  {
    "path": "integration/playwright.config.ts",
    "content": "import * as os from \"node:os\";\n\nimport type { PlaywrightTestConfig } from \"@playwright/test\";\nimport { devices } from \"@playwright/test\";\n\n// silence expected warnings in Node 22.12 about `require(esm)`\n// when it implicitly uses `react-router`'s `module-sync` export condition\nprocess.env.NODE_OPTIONS =\n  (process.env.NODE_OPTIONS ?? \"\") + ` --no-warnings=ExperimentalWarning`;\n\nconst isWindows = process.platform === \"win32\";\nlet workers = Math.floor(os.cpus().length / 2);\nif (workers < 2) workers = 2;\n\nconst config: PlaywrightTestConfig = {\n  testDir: \".\",\n  testMatch: [\"**/*-test.ts\"],\n  // Playwright treats our workspace packages as internal by default. If we\n  // don't mark them as external, tests hang in Node 20.5.2+\n  build: {\n    external: [\"**/packages/**/*\"],\n  },\n  /* Maximum time one test can run for. */\n  timeout: isWindows ? 60_000 : 30_000,\n  fullyParallel: true,\n  workers,\n  expect: {\n    /* Maximum time expect() should wait for the condition to be met. */\n    timeout: isWindows ? 10_000 : 5_000,\n  },\n  forbidOnly: !!process.env.CI,\n  retries: process.env.CI ? 3 : 0,\n  reporter: process.env.CI ? \"dot\" : [[\"html\", { open: \"never\" }]],\n  use: { actionTimeout: 0 },\n\n  projects: [\n    {\n      name: \"chromium\",\n      use: devices[\"Desktop Chrome\"],\n    },\n    {\n      name: \"webkit\",\n      use: devices[\"Desktop Safari\"],\n    },\n    {\n      name: \"msedge\",\n      use: {\n        ...devices[\"Desktop Edge\"],\n        // Desktop Edge uses chromium by default\n        // https://github.com/microsoft/playwright/blob/main/packages/playwright-core/src/server/deviceDescriptorsSource.json#L1502\n        channel: \"msedge\",\n      },\n    },\n    {\n      name: \"firefox\",\n      use: devices[\"Desktop Firefox\"],\n    },\n  ],\n};\n\nexport default config;\n"
  },
  {
    "path": "integration/prefetch-test.ts",
    "content": "import { test, expect } from \"@playwright/test\";\n\nimport {\n  createAppFixture,\n  createFixture,\n  js,\n  css,\n} from \"./helpers/create-fixture.js\";\nimport type {\n  Fixture,\n  FixtureInit,\n  AppFixture,\n} from \"./helpers/create-fixture.js\";\nimport { PlaywrightFixture } from \"./helpers/playwright-fixture.js\";\nimport { type TemplateName, viteMajorTemplates } from \"./helpers/vite.js\";\n\ntype PrefetchType = \"intent\" | \"render\" | \"none\" | \"viewport\";\n\n// Generate the test app using the given prefetch mode\nfunction fixtureFactory(\n  mode: PrefetchType,\n  templateName: TemplateName,\n): FixtureInit {\n  return {\n    templateName,\n    files: {\n      \"app/root.tsx\": js`\n        import {\n          Link,\n          Links,\n          Meta,\n          Outlet,\n          Scripts,\n          useLoaderData,\n        } from \"react-router\";\n\n        export default function Root() {\n          const styles =\n            'a:hover { color: red; } a:hover:after { content: \" (hovered)\"; }' +\n            'a:focus { color: green; } a:focus:after { content: \" (focused)\"; }';\n\n          return (\n            <html lang=\"en\">\n              <head>\n                <Meta />\n                <Links />\n              </head>\n              <body>\n                <style>{styles}</style>\n                <h1>Root</h1>\n                <nav id=\"nav\">\n                  <Link to=\"/prefetch-with-loader\" prefetch=\"${mode}\">\n                    Loader Page\n                  </Link>\n                  <br/>\n                  <Link to=\"/prefetch-without-loader\" prefetch=\"${mode}\">\n                    Non-Loader Page\n                  </Link>\n                </nav>\n                <Outlet />\n                <Scripts />\n              </body>\n            </html>\n          );\n        }\n      `,\n\n      \"app/routes/_index.tsx\": js`\n        export default function() {\n          return <h2 className=\"index\">Index</h2>;\n        }\n      `,\n\n      \"app/routes/prefetch-with-loader.tsx\": js`\n        export function loader() {\n          return { message: 'data from the loader' };\n        }\n        export default function() {\n          return <h2 className=\"prefetch-with-loader\">With Loader</h2>;\n        }\n      `,\n\n      \"app/routes/prefetch-without-loader.tsx\": js`\n        export default function() {\n          return <h2 className=\"prefetch-without-loader\">Without Loader</h2>;\n        }\n      `,\n    },\n  };\n}\n\ntest.describe(\"prefetch\", () => {\n  viteMajorTemplates.forEach(({ templateName, templateDisplayName }) => {\n    test.describe(templateDisplayName, () => {\n      test.describe(\"prefetch=none\", () => {\n        let fixture: Fixture;\n        let appFixture: AppFixture;\n\n        test.beforeAll(async () => {\n          fixture = await createFixture(fixtureFactory(\"none\", templateName));\n          appFixture = await createAppFixture(fixture);\n        });\n\n        test.afterAll(() => {\n          appFixture.close();\n        });\n\n        test(\"does not render prefetch tags during SSR\", async ({ page }) => {\n          let res = await fixture.requestDocument(\"/\");\n          expect(res.status).toBe(200);\n          expect(\n            await page.locator(\"link[href^='/assets/prefetch-']\").count(),\n          ).toBe(0);\n        });\n\n        test(\"does not add prefetch tags on hydration\", async ({ page }) => {\n          let app = new PlaywrightFixture(appFixture, page);\n          await app.goto(\"/\");\n          expect(\n            await page.locator(\"link[href^='/assets/prefetch-']\").count(),\n          ).toBe(0);\n        });\n      });\n\n      test.describe(\"prefetch=render\", () => {\n        let fixture: Fixture;\n        let appFixture: AppFixture;\n\n        test.beforeAll(async () => {\n          fixture = await createFixture(fixtureFactory(\"render\", templateName));\n          appFixture = await createAppFixture(fixture);\n        });\n\n        test.afterAll(() => {\n          appFixture.close();\n        });\n\n        test(\"does not render prefetch tags during SSR\", async ({ page }) => {\n          let res = await fixture.requestDocument(\"/\");\n          expect(res.status).toBe(200);\n          expect(\n            await page.locator(\"link[href^='/assets/prefetch-']\").count(),\n          ).toBe(0);\n        });\n\n        test(\"adds prefetch tags on hydration\", async ({ page }) => {\n          let app = new PlaywrightFixture(appFixture, page);\n          await app.goto(\"/\");\n\n          // Both data and asset fetch for /prefetch-with-loader\n          await page.waitForSelector(\n            \"link[rel='prefetch'][as='fetch'][href='/prefetch-with-loader.data']\",\n            { state: \"attached\" },\n          );\n          await page.waitForSelector(\n            \"link[rel='modulepreload'][href^='/assets/prefetch-with-loader-']\",\n            { state: \"attached\" },\n          );\n\n          // Only asset fetch for /prefetch-without-loader\n          await page.waitForSelector(\n            \"link[rel='modulepreload'][href^='/assets/prefetch-without-loader-']\",\n            { state: \"attached\" },\n          );\n\n          // These 2 are common and duped for both - but they've already loaded on\n          // page load so they don't trigger network requests\n          await page.waitForSelector(\n            // Look for either Rollup or Rolldown chunks\n            [\n              \"link[rel='modulepreload'][href^='/assets/chunk-']\",\n              \"link[rel='modulepreload'][href^='/assets/jsx-runtime-']\",\n            ].join(\",\"),\n            { state: \"attached\" },\n          );\n\n          // Ensure no other links in the #nav element\n          expect(await page.locator(\"link\").count()).toBe(5);\n        });\n      });\n\n      test.describe(\"prefetch=intent (hover)\", () => {\n        let fixture: Fixture;\n        let appFixture: AppFixture;\n\n        test.beforeAll(async () => {\n          fixture = await createFixture(fixtureFactory(\"intent\", templateName));\n          appFixture = await createAppFixture(fixture);\n        });\n\n        test.afterAll(() => {\n          appFixture.close();\n        });\n\n        test(\"does not render prefetch tags during SSR\", async ({ page }) => {\n          let res = await fixture.requestDocument(\"/\");\n          expect(res.status).toBe(200);\n          expect(\n            await page.locator(\"link[href^='/assets/prefetch-']\").count(),\n          ).toBe(0);\n        });\n\n        test(\"does not add prefetch tags on hydration\", async ({ page }) => {\n          let app = new PlaywrightFixture(appFixture, page);\n          await app.goto(\"/\");\n          expect(\n            await page.locator(\"link[href^='/assets/prefetch-']\").count(),\n          ).toBe(0);\n        });\n\n        test(\"adds prefetch tags on hover\", async ({ page }) => {\n          let app = new PlaywrightFixture(appFixture, page);\n          await app.goto(\"/\");\n          await page.hover(\"a[href='/prefetch-with-loader']\");\n          await page.waitForSelector(\n            \"link[rel='prefetch'][as='fetch'][href='/prefetch-with-loader.data']\",\n            { state: \"attached\" },\n          );\n          // Check href prefix due to hashed filenames\n          await page.waitForSelector(\n            \"link[rel='modulepreload'][href^='/assets/prefetch-with-loader-']\",\n            { state: \"attached\" },\n          );\n          await page.waitForSelector(\n            // Look for either Rollup or Rolldown chunks\n            [\n              \"link[rel='modulepreload'][href^='/assets/chunk-']\",\n              \"link[rel='modulepreload'][href^='/assets/jsx-runtime-']\",\n            ].join(\",\"),\n            { state: \"attached\" },\n          );\n          expect(await page.locator(\"link\").count()).toBe(3);\n\n          await page.hover(\"a[href='/prefetch-without-loader']\");\n          await page.waitForSelector(\n            \"link[rel='modulepreload'][href^='/assets/prefetch-without-loader-']\",\n            { state: \"attached\" },\n          );\n          await page.waitForSelector(\n            // Look for either Rollup or Rolldown chunks\n            [\n              \"link[rel='modulepreload'][href^='/assets/chunk-']\",\n              \"link[rel='modulepreload'][href^='/assets/jsx-runtime-']\",\n            ].join(\",\"),\n            { state: \"attached\" },\n          );\n          expect(await page.locator(\"link\").count()).toBe(2);\n        });\n\n        test(\"removes prefetch tags after navigating to/from the page\", async ({\n          page,\n        }) => {\n          let app = new PlaywrightFixture(appFixture, page);\n          await app.goto(\"/\");\n\n          // Links added on hover\n          await page.hover(\"a[href='/prefetch-with-loader']\");\n          await page.waitForSelector(\"link\", { state: \"attached\" });\n          expect(await page.locator(\"link\").count()).toBe(3);\n\n          // Links removed upon navigating to the page\n          await page.click(\"a[href='/prefetch-with-loader']\");\n          await page.waitForSelector(\"h2.prefetch-with-loader\", {\n            state: \"attached\",\n          });\n          expect(\n            await page.locator(\"link[href^='/assets/prefetch-']\").count(),\n          ).toBe(0);\n\n          // Links stay removed upon navigating away from the page\n          await page.click(\"a[href='/prefetch-without-loader']\");\n          await page.waitForSelector(\"h2.prefetch-without-loader\", {\n            state: \"attached\",\n          });\n          expect(\n            await page.locator(\"link[href^='/assets/prefetch-']\").count(),\n          ).toBe(0);\n        });\n      });\n\n      test.describe(\"prefetch=intent (focus)\", () => {\n        let fixture: Fixture;\n        let appFixture: AppFixture;\n\n        test.beforeAll(async () => {\n          fixture = await createFixture(fixtureFactory(\"intent\", templateName));\n          appFixture = await createAppFixture(fixture);\n        });\n\n        test.afterAll(() => {\n          appFixture.close();\n        });\n\n        test(\"does not render prefetch tags during SSR\", async ({ page }) => {\n          let res = await fixture.requestDocument(\"/\");\n          expect(res.status).toBe(200);\n          expect(\n            await page.locator(\"link[href^='/assets/prefetch-']\").count(),\n          ).toBe(0);\n        });\n\n        test(\"does not add prefetch tags on hydration\", async ({ page }) => {\n          let app = new PlaywrightFixture(appFixture, page);\n          await app.goto(\"/\");\n          expect(\n            await page.locator(\"link[href^='/assets/prefetch-']\").count(),\n          ).toBe(0);\n        });\n\n        test(\"adds prefetch tags on focus\", async ({ page }) => {\n          let app = new PlaywrightFixture(appFixture, page);\n          await app.goto(\"/\");\n          // This click is needed to transfer focus to the main window, allowing\n          // subsequent focus events to fire\n          await page.click(\"body\");\n          await page.focus(\"a[href='/prefetch-with-loader']\");\n          await page.waitForSelector(\n            \"link[rel='prefetch'][as='fetch'][href='/prefetch-with-loader.data']\",\n            { state: \"attached\" },\n          );\n          await page.waitForSelector(\n            \"link[rel='modulepreload'][href^='/assets/prefetch-with-loader-']\",\n            { state: \"attached\" },\n          );\n          await page.waitForSelector(\n            // Look for either Rollup or Rolldown chunks\n            [\n              \"link[rel='modulepreload'][href^='/assets/chunk-']\",\n              \"link[rel='modulepreload'][href^='/assets/jsx-runtime-']\",\n            ].join(\",\"),\n            { state: \"attached\" },\n          );\n          expect(await page.locator(\"link\").count()).toBe(3);\n\n          await page.focus(\"a[href='/prefetch-without-loader']\");\n          await page.waitForSelector(\n            \"link[rel='modulepreload'][href^='/assets/prefetch-without-loader-']\",\n            { state: \"attached\" },\n          );\n          await page.waitForSelector(\n            // Look for either Rollup or Rolldown chunks\n            [\n              \"link[rel='modulepreload'][href^='/assets/chunk-']\",\n              \"link[rel='modulepreload'][href^='/assets/jsx-runtime-']\",\n            ].join(\",\"),\n            { state: \"attached\" },\n          );\n          expect(await page.locator(\"link\").count()).toBe(2);\n        });\n      });\n\n      test.describe(\"prefetch=viewport\", () => {\n        let fixture: Fixture;\n        let appFixture: AppFixture;\n\n        test.beforeAll(async () => {\n          fixture = await createFixture({\n            files: {\n              \"app/routes/_index.tsx\": js`\n                import { Link } from \"react-router\";\n\n                export default function Component() {\n                  return (\n                    <>\n                      <h1>Index Page - Scroll Down</h1>\n                      <div style={{ marginTop: \"150vh\" }}>\n                        <Link to=\"/test\" prefetch=\"viewport\">Click me!</Link>\n                      </div>\n                    </>\n                  );\n                }\n              `,\n\n              \"app/routes/test.tsx\": js`\n                export function loader() {\n                  return null;\n                }\n                export default function Component() {\n                  return <h1>Test Page</h1>;\n                }\n              `,\n            },\n          });\n\n          // This creates an interactive app using puppeteer.\n          appFixture = await createAppFixture(fixture);\n        });\n\n        test.afterAll(() => {\n          appFixture.close();\n        });\n\n        test(\"should prefetch when the link enters the viewport\", async ({\n          page,\n        }) => {\n          let app = new PlaywrightFixture(appFixture, page);\n          await app.goto(\"/\");\n\n          // No preloads to start\n          await expect(\n            page.locator(\n              [\n                \"link[rel='prefetch'][as='fetch'][href='/test.data']\",\n                \"link[rel='modulepreload'][href^='/assets/test-']\",\n              ].join(\",\"),\n            ),\n          ).toHaveCount(0);\n\n          // Preloads render on scroll down\n          await page.evaluate(() =>\n            window.scrollTo(0, document.body.scrollHeight),\n          );\n\n          await page.waitForSelector(\n            \"link[rel='prefetch'][as='fetch'][href='/test.data']\",\n            { state: \"attached\" },\n          );\n          await page.waitForSelector(\n            \"link[rel='modulepreload'][href^='/assets/test-']\",\n            { state: \"attached\" },\n          );\n\n          // Preloads removed on scroll up\n          await page.evaluate(() => window.scrollTo(0, 0));\n          await expect(\n            page.locator(\n              [\n                \"link[rel='prefetch'][as='fetch'][href='/test.data']\",\n                \"link[rel='modulepreload'][href^='/assets/test-']\",\n              ].join(\",\"),\n            ),\n          ).toHaveCount(0);\n        });\n      });\n\n      test.describe(\"other scenarios\", () => {\n        let fixture: Fixture;\n        let appFixture: AppFixture;\n\n        test.afterAll(() => {\n          appFixture?.close();\n        });\n\n        test(\"does not add prefetch links for stylesheets already in the DOM (active routes)\", async ({\n          page,\n        }) => {\n          fixture = await createFixture({\n            files: {\n              \"app/root.tsx\": js`\n                import { Links, Meta, Scripts, useFetcher } from \"react-router\";\n                import globalCss from \"./global.css?url\";\n\n                export function links() {\n                  return [{ rel: \"stylesheet\", href: globalCss }];\n                }\n\n                export async function action() {\n                  return null;\n                }\n\n                export async function loader() {\n                  return null;\n                }\n\n                export default function Root() {\n                  let fetcher = useFetcher();\n\n                  return (\n                    <html lang=\"en\">\n                      <head>\n                        <Meta />\n                        <Links />\n                      </head>\n                      <body>\n                        <button\n                          id=\"submit-fetcher\"\n                          onClick={() => fetcher.submit({}, { method: 'post' })}>\n                            Submit Fetcher\n                        </button>\n                        <p id={\"fetcher-state--\" + fetcher.state}>{fetcher.state}</p>\n                        <Scripts />\n                      </body>\n                    </html>\n                  );\n                }\n              `,\n\n              \"app/global.css\": `\n                body {\n                  background-color: black;\n                  color: white;\n                }\n              `,\n\n              \"app/routes/_index.tsx\": js`\n                export default function() {\n                  return <h2 className=\"index\">Index</h2>;\n                }\n              `,\n            },\n          });\n          appFixture = await createAppFixture(fixture);\n          let requests: { type: string; url: string }[] = [];\n\n          page.on(\"request\", (req) => {\n            requests.push({\n              type: req.resourceType(),\n              url: req.url(),\n            });\n          });\n\n          let app = new PlaywrightFixture(appFixture, page);\n          await app.goto(\"/\");\n          await page.click(\"#submit-fetcher\");\n          await page.waitForSelector(\"#fetcher-state--idle\");\n          // We should not send a second request for this root stylesheet that's\n          // already been rendered in the DOM\n          let stylesheets = requests.filter(\n            (r) =>\n              r.type === \"stylesheet\" &&\n              /\\/global-[a-z0-9-]+\\.css/i.test(r.url),\n          );\n          expect(stylesheets.length).toBe(1);\n        });\n\n        test(\"dedupes prefetch tags\", async ({ page }) => {\n          fixture = await createFixture({\n            files: {\n              \"app/root.tsx\": js`\n                import {\n                  Link,\n                  Links,\n                  Meta,\n                  Outlet,\n                  Scripts,\n                  useLoaderData,\n                } from \"react-router\";\n\n                export default function Root() {\n                  const styles =\n                    'a:hover { color: red; } a:hover:after { content: \" (hovered)\"; }' +\n                    'a:focus { color: green; } a:focus:after { content: \" (focused)\"; }';\n\n                  return (\n                    <html lang=\"en\">\n                      <head>\n                        <Meta />\n                        <Links />\n                      </head>\n                      <body>\n                        <style>{styles}</style>\n                        <h1>Root</h1>\n                        <nav id=\"nav\">\n                          <Link to=\"/with-nested-links/nested\" prefetch=\"intent\">\n                            Nested Links Page\n                          </Link>\n                        </nav>\n                        <Outlet />\n                        <Scripts />\n                      </body>\n                    </html>\n                  );\n                }\n              `,\n\n              \"app/global.css\": css`\n                .global-class {\n                  background-color: gray;\n                  color: black;\n                }\n              `,\n\n              \"app/local.css\": css`\n                .local-class {\n                  background-color: black;\n                  color: white;\n                }\n              `,\n\n              \"app/routes/_index.tsx\": js`\n                export default function() {\n                  return <h2 className=\"index\">Index</h2>;\n                }\n              `,\n\n              \"app/routes/with-nested-links.tsx\": js`\n                import { Outlet } from \"react-router\";\n                import globalCss from \"../global.css?url\";\n\n                export function links() {\n                  return [\n                    // Same links as child route but with different key order\n                    {\n                      rel: \"stylesheet\",\n                      href: globalCss,\n                    },\n                    {\n                      rel: \"preload\",\n                      as: \"image\",\n                      imageSrcSet: \"image-600.jpg 600w, image-1200.jpg 1200w\",\n                      imageSizes: \"9999px\",\n                    },\n                  ];\n                }\n                export default function() {\n                  return <Outlet />;\n                }\n              `,\n\n              \"app/routes/with-nested-links.nested.tsx\": js`\n                import globalCss from '../global.css?url';\n                import localCss from '../local.css?url';\n\n                export function links() {\n                  return [\n                    // Same links as parent route but with different key order\n                    {\n                      href: globalCss,\n                      rel: \"stylesheet\",\n                    },\n                    {\n                      imageSrcSet: \"image-600.jpg 600w, image-1200.jpg 1200w\",\n                      imageSizes: \"9999px\",\n                      rel: \"preload\",\n                      as: \"image\",\n                    },\n                    // Unique links for child route\n                    {\n                      rel: \"stylesheet\",\n                      href: localCss,\n                    },\n                    {\n                      rel: \"preload\",\n                      as: \"image\",\n                      imageSrcSet: \"image-700.jpg 700w, image-1400.jpg 1400w\",\n                      imageSizes: \"9999px\",\n                    },\n                  ];\n                }\n                export default function() {\n                  return <h2 className=\"with-nested-links\">With Nested Links</h2>;\n                }\n              `,\n            },\n          });\n          appFixture = await createAppFixture(fixture);\n\n          let app = new PlaywrightFixture(appFixture, page);\n          await app.goto(\"/\");\n          await page.hover(\"a[href='/with-nested-links/nested']\");\n          await page.waitForSelector(\"link[rel='prefetch'][as='style']\", {\n            state: \"attached\",\n          });\n          expect(\n            await page.locator(\"link[rel='prefetch'][as='style']\").count(),\n          ).toBe(2);\n          expect(\n            await page.locator(\"link[rel='prefetch'][as='image']\").count(),\n          ).toBe(2);\n        });\n      });\n    });\n  });\n});\n"
  },
  {
    "path": "integration/react-router-serve-test.ts",
    "content": "import { test, expect } from \"@playwright/test\";\n\nimport { PlaywrightFixture } from \"./helpers/playwright-fixture.js\";\nimport type { Fixture, AppFixture } from \"./helpers/create-fixture.js\";\nimport {\n  createAppFixture,\n  createFixture,\n  js,\n} from \"./helpers/create-fixture.js\";\nimport { type TemplateName } from \"./helpers/vite.js\";\n\nconst templateNames = [\n  \"vite-5-template\",\n  \"rsc-vite-framework\",\n] as const satisfies TemplateName[];\n\ntest.describe(\"react-router-serve\", () => {\n  for (const templateName of templateNames) {\n    test.describe(`template: ${templateName}`, () => {\n      let fixture: Fixture;\n      let appFixture: AppFixture;\n\n      test.beforeEach(async ({ context }) => {\n        await context.route(/\\.(data|rsc)$/, async (route) => {\n          await new Promise((resolve) => setTimeout(resolve, 50));\n          route.continue();\n        });\n      });\n\n      test.beforeAll(async () => {\n        fixture = await createFixture({\n          templateName,\n          useReactRouterServe: true,\n          files: {\n            \"app/routes/_index.tsx\": js`\n              import { useLoaderData, Link } from \"react-router\";\n\n              export function loader() {\n                return \"pizza\";\n              }\n\n              export default function Index() {\n                let data = useLoaderData();\n                return (\n                  <div>\n                    {data}\n                    <Link to=\"/burgers\">Other Route</Link>\n                  </div>\n                )\n              }\n            `,\n\n            \"app/routes/burgers.tsx\": js`\n              export default function Index() {\n                return <div>cheeseburger</div>;\n              }\n            `,\n          },\n        });\n\n        // This creates an interactive app using playwright.\n        appFixture = await createAppFixture(fixture);\n      });\n\n      test.afterAll(() => {\n        appFixture.close();\n      });\n\n      test(\"should start and perform client side navigation\", async ({\n        page,\n      }) => {\n        let app = new PlaywrightFixture(appFixture, page);\n        // You can test any request your app might get using `fixture`.\n        let response = await fixture.requestDocument(\"/\");\n        expect(await response.text()).toMatch(\"pizza\");\n\n        // If you need to test interactivity use the `app`\n        await app.goto(\"/\");\n        await app.clickLink(\"/burgers\");\n        await page.waitForSelector(\"text=cheeseburger\");\n      });\n    });\n  }\n});\n"
  },
  {
    "path": "integration/redirects-test.ts",
    "content": "import { test, expect } from \"@playwright/test\";\n\nimport {\n  createFixture,\n  createAppFixture,\n  js,\n} from \"./helpers/create-fixture.js\";\nimport type { Fixture, AppFixture } from \"./helpers/create-fixture.js\";\nimport { PlaywrightFixture } from \"./helpers/playwright-fixture.js\";\nimport type { TemplateName } from \"./helpers/vite.js\";\n\nconst templateNames = [\n  \"vite-5-template\",\n  \"rsc-vite-framework\",\n] as const satisfies TemplateName[];\n\ntest.describe(\"redirects\", () => {\n  for (const templateName of templateNames) {\n    test.describe(`template: ${templateName}`, () => {\n      let fixture: Fixture;\n      let appFixture: AppFixture;\n\n      test.beforeAll(async () => {\n        fixture = await createFixture({\n          templateName,\n          files: {\n            \"app/routes/absolute.tsx\": js`\n              import * as React from 'react';\n              import { Outlet } from \"react-router\";\n\n              export default function Component() {\n                let [count, setCount] = React.useState(0);\n                return (\n                  <>\n                    <button\n                      id=\"increment\"\n                      onClick={() => setCount(count + 1)}>\n                      {\"Count:\" + count}\n                    </button>\n                    <Outlet/>\n                  </>\n                );\n              }\n            `,\n\n            \"app/routes/absolute._index.tsx\": js`\n              import { redirect, Form } from \"react-router\";\n\n              export async function action({ request }) {\n                return redirect(new URL(request.url).origin + \"/absolute/landing\");\n              };\n\n              export default function Component() {\n                return (\n                  <Form method=\"post\">\n                    <button type=\"submit\">Submit</button>\n                  </Form>\n                );\n              }\n            `,\n\n            \"app/routes/absolute.landing.tsx\": js`\n              export default function Component() {\n                return <h1>Landing</h1>\n              }\n            `,\n\n            \"app/routes/absolute.content-length.tsx\": js`\n              import { redirect, Form } from \"react-router\";\n              export async function action({ request }) {\n                return redirect(new URL(request.url).origin + \"/absolute/landing\", {\n                  headers: { 'Content-Length': '0' }\n                });\n              };\n              export default function Component() {\n                return (\n                  <Form method=\"post\">\n                    <button type=\"submit\">Submit</button>\n                  </Form>\n                );\n              }\n            `,\n\n            \"app/routes/loader.external.ts\": js`\n              import { redirect } from \"react-router\";\n              export const loader = () => {\n                return redirect(\"https://reactrouter.com/\");\n              }\n            `,\n\n            \"app/routes/redirect-document.tsx\": js`\n              import * as React from \"react\";\n              import { Outlet } from \"react-router\";\n\n              export default function Component() {\n                let [count, setCount] = React.useState(0);\n                let countText = 'Count:' + count;\n                return (\n                  <>\n                    <button onClick={() => setCount(count+1)}>{countText}</button>\n                    <Outlet />\n                  </>\n                );\n              }\n            `,\n\n            \"app/routes/redirect-document._index.tsx\": js`\n              import { Link } from \"react-router\";\n\n              export default function Component() {\n                return <Link to=\"/redirect-document/a\">Link</Link>\n              }\n            `,\n\n            \"app/routes/redirect-document.a.tsx\": js`\n              import { redirectDocument } from \"react-router\";\n              export const loader = () =>  redirectDocument(\"/redirect-document/b\");\n            `,\n\n            \"app/routes/redirect-document.b.tsx\": js`\n              export default function Component() {\n                return <h1>Hello B!</h1>\n              }\n            `,\n\n            \"app/routes/replace.a.tsx\": js`\n              import { Link } from \"react-router\";\n              export default function () {\n                return <><h1 id=\"a\">A</h1><Link to=\"/replace/b\">Go to B</Link></>;\n              }\n            `,\n\n            \"app/routes/replace.b.tsx\": js`\n              import { Link } from \"react-router\";\n              export default function () {\n                return <><h1 id=\"b\">B</h1><Link to=\"/replace/c\">Go to C</Link></>\n              }\n            `,\n\n            \"app/routes/replace.c.tsx\": js`\n              import { replace } from \"react-router\";\n              export const loader = () => replace(\"/replace/d\");\n              export default function () {\n                return <h1 id=\"c\">C</h1>\n              }\n            `,\n\n            \"app/routes/replace.d.tsx\": js`\n              export default function () {\n                return <h1 id=\"d\">D</h1>\n              }\n            `,\n\n            \"app/routes/headers.tsx\": js`\n              import * as React from 'react';\n              import { Link, Form, redirect, useLocation } from 'react-router';\n\n              export function action() {\n                return redirect('/headers?action-redirect', {\n                  headers: { 'X-Test': 'Foo' }\n                })\n              }\n\n              export function loader({ request }) {\n                let url = new URL(request.url);\n                if (url.searchParams.has('redirect')) {\n                  return redirect('/headers?loader-redirect', {\n                    headers: { 'X-Test': 'Foo' }\n                  })\n                }\n                return null\n              }\n\n              export default function Component() {\n                let location = useLocation()\n                return (\n                  <>\n                    <Link id=\"loader-redirect\" to=\"/headers?redirect\">Redirect</Link>\n                    <Form method=\"post\">\n                      <button id=\"action-redirect\" type=\"submit\">Action Redirect</button>\n                    </Form>\n                    <p id=\"search-params\">\n                      Search Params: {location.search}\n                    </p>\n                  </>\n                );\n              }\n            `,\n          },\n        });\n\n        appFixture = await createAppFixture(fixture);\n      });\n\n      test.afterAll(() => {\n        appFixture.close();\n      });\n\n      test(\"redirects to external URLs\", async ({ page }) => {\n        let app = new PlaywrightFixture(appFixture, page);\n\n        await app.waitForNetworkAfter(() => app.goto(\"/loader/external\"));\n        expect(app.page.url()).toBe(\"https://reactrouter.com/\");\n      });\n\n      test(\"redirects to absolute URLs in the app with a SPA navigation\", async ({\n        page,\n      }) => {\n        let app = new PlaywrightFixture(appFixture, page);\n        await app.goto(`/absolute`, true);\n        await app.clickElement(\"#increment\");\n        expect(await app.getHtml(\"#increment\")).toMatch(\"Count:1\");\n        await app.waitForNetworkAfter(() =>\n          app.clickSubmitButton(\"/absolute?index\"),\n        );\n        await page.waitForSelector(`h1:has-text(\"Landing\")`);\n        // No hard reload\n        expect(await app.getHtml(\"#increment\")).toMatch(\"Count:1\");\n      });\n\n      test(\"redirects to absolute URLs in the app with a SPA navigation and Content-Length header\", async ({\n        page,\n      }) => {\n        let app = new PlaywrightFixture(appFixture, page);\n        await app.goto(`/absolute/content-length`, true);\n        await app.clickElement(\"#increment\");\n        expect(await app.getHtml(\"#increment\")).toMatch(\"Count:1\");\n        await app.waitForNetworkAfter(() =>\n          app.clickSubmitButton(\"/absolute/content-length\"),\n        );\n        await page.waitForSelector(`h1:has-text(\"Landing\")`);\n        // No hard reload\n        expect(await app.getHtml(\"#increment\")).toMatch(\"Count:1\");\n      });\n\n      test(\"supports hard redirects within the app via reloadDocument\", async ({\n        page,\n      }) => {\n        let app = new PlaywrightFixture(appFixture, page);\n        await app.goto(\"/redirect-document\", true);\n        expect(await app.getHtml(\"button\")).toMatch(\"Count:0\");\n        await app.clickElement(\"button\");\n        expect(await app.getHtml(\"button\")).toMatch(\"Count:1\");\n        await app.waitForNetworkAfter(() =>\n          app.clickLink(\"/redirect-document/a\"),\n        );\n        await page.waitForSelector('h1:has-text(\"Hello B!\")');\n        // Hard reload resets client side react state\n        expect(await app.getHtml(\"button\")).toMatch(\"Count:0\");\n      });\n\n      test(\"supports replace redirects within the app\", async ({ page }) => {\n        let app = new PlaywrightFixture(appFixture, page);\n        await app.goto(\"/replace/a\", true);\n        await page.waitForSelector(\"#a\"); // [/a]\n        await app.clickLink(\"/replace/b\");\n        await page.waitForSelector(\"#b\"); // [/a, /b]\n        await app.clickLink(\"/replace/c\");\n        await page.waitForSelector(\"#d\"); // [/a, /d]\n        await page.goBack();\n        await page.waitForSelector(\"#a\"); // [/a]\n      });\n\n      test(\"maintains custom headers on redirects\", async ({ page }) => {\n        let app = new PlaywrightFixture(appFixture, page);\n\n        let hasGetHeader = false;\n        let hasPostHeader = false;\n        page.on(\"request\", async (request) => {\n          let extension = /^rsc/.test(templateName) ? \"rsc\" : \"data\";\n          if (\n            request.method() === \"GET\" &&\n            request.url().endsWith(`headers.${extension}?redirect=`)\n          ) {\n            const headers = (await request.response())?.headers() || {};\n            hasGetHeader = headers[\"x-test\"] === \"Foo\";\n          }\n          if (\n            request.method() === \"POST\" &&\n            request.url().endsWith(`headers.${extension}`)\n          ) {\n            const headers = (await request.response())?.headers() || {};\n            hasPostHeader = headers[\"x-test\"] === \"Foo\";\n          }\n        });\n\n        await app.goto(\"/headers\", true);\n        await app.clickElement(\"#loader-redirect\");\n        await expect(page.locator(\"#search-params\")).toHaveText(\n          \"Search Params: ?loader-redirect\",\n        );\n        expect(hasGetHeader).toBe(true);\n        expect(hasPostHeader).toBe(false);\n\n        hasGetHeader = false;\n        hasPostHeader = false;\n\n        await app.goto(\"/headers\", true);\n        await app.clickElement(\"#action-redirect\");\n        await expect(page.locator(\"#search-params\")).toHaveText(\n          \"Search Params: ?action-redirect\",\n        );\n        expect(hasGetHeader).toBe(false);\n        expect(hasPostHeader).toBe(true);\n      });\n    });\n  }\n});\n"
  },
  {
    "path": "integration/rendering-test.ts",
    "content": "import { test, expect } from \"@playwright/test\";\n\nimport {\n  createAppFixture,\n  createFixture,\n  js,\n} from \"./helpers/create-fixture.js\";\nimport type { Fixture, AppFixture } from \"./helpers/create-fixture.js\";\nimport { PlaywrightFixture, selectHtml } from \"./helpers/playwright-fixture.js\";\n\ntest.describe(\"rendering\", () => {\n  let fixture: Fixture;\n  let appFixture: AppFixture;\n\n  test.beforeAll(async () => {\n    fixture = await createFixture({\n      files: {\n        \"app/root.tsx\": js`\n          import { Links, Meta, Outlet, Scripts } from \"react-router\";\n\n          export default function Root() {\n            return (\n              <html lang=\"en\">\n                <head>\n                  <Meta />\n                  <Links />\n                </head>\n                <body>\n                  <div id=\"content\">\n                    <h1>Root</h1>\n                    <Outlet />\n                  </div>\n                  <Scripts />\n                </body>\n              </html>\n            );\n          }\n        `,\n\n        \"app/routes/_index.tsx\": js`\n          export default function() {\n            return <h2>Index</h2>;\n          }\n        `,\n      },\n    });\n\n    appFixture = await createAppFixture(fixture);\n  });\n\n  test.afterAll(() => {\n    appFixture.close();\n  });\n\n  test(\"server renders matching routes\", async () => {\n    let res = await fixture.requestDocument(\"/\");\n    expect(res.status).toBe(200);\n    expect(await selectHtml(await res.text(), \"#content\"))\n      .toBe(`<div id=\"content\">\n  <h1>Root</h1>\n  <h2>Index</h2>\n</div>`);\n  });\n\n  test(\"hydrates\", async ({ page }) => {\n    let app = new PlaywrightFixture(appFixture, page);\n    await app.goto(\"/\");\n    expect(await app.getHtml(\"#content\")).toBe(`<div id=\"content\">\n  <h1>Root</h1>\n  <h2>Index</h2>\n</div>`);\n  });\n});\n"
  },
  {
    "path": "integration/request-test.ts",
    "content": "import { test, expect } from \"@playwright/test\";\n\nimport { PlaywrightFixture } from \"./helpers/playwright-fixture.js\";\nimport type { Fixture, AppFixture } from \"./helpers/create-fixture.js\";\nimport {\n  createAppFixture,\n  createFixture,\n  js,\n} from \"./helpers/create-fixture.js\";\n\nlet fixture: Fixture;\nlet appFixture: AppFixture;\n\ntest.beforeAll(async () => {\n  fixture = await createFixture({\n    files: {\n      \"app/routes/_index.tsx\": js`\n        import { Form, useLoaderData, useActionData } from \"react-router\";\n\n        async function requestToJson(request) {\n          let body = null;\n\n          if (request.body) {\n            let fd = await request.formData();\n            body = Object.fromEntries(fd.entries());\n          }\n\n          return {\n            method: request.method,\n            url: request.url,\n            headers: Object.fromEntries(request.headers.entries()),\n            body,\n          };\n        }\n        export async function loader({ request }) {\n          return requestToJson(request);\n        }\n        export function action({ request }) {\n          return requestToJson(request);\n        }\n        export default function Index() {\n          let loaderData = useLoaderData();\n          let actionData = useActionData();\n          return (\n            <div>\n              <button id=\"set-cookie\" onClick={() => {\n                document.cookie = 'cookie=nomnom; path=/';\n              }}>\n                Set Cookie\n              </button>\n              <Form method=\"get\" reloadDocument>\n                <button type=\"submit\" id=\"submit-get-ssr\" name=\"type\" value=\"ssr\">\n                  SSR GET\n                </button>\n              </Form>\n              <Form method=\"get\">\n                <button type=\"submit\" id=\"submit-get-csr\" name=\"type\" value=\"csr\">\n                  CSR GET\n                </button>\n              </Form>\n              <Form method=\"post\" reloadDocument>\n                <button type=\"submit\" id=\"submit-post-ssr\" name=\"type\" value=\"ssr\">\n                  SSR POST\n                </button>\n              </Form>\n              <Form method=\"post\">\n                <button type=\"submit\" id=\"submit-post-csr\" name=\"type\" value=\"csr\">\n                  CSR POST\n                </button>\n              </Form>\n              <pre id=\"loader-data\">{JSON.stringify(loaderData)}</pre>\n              {actionData ?\n                <pre id=\"action-data\">{JSON.stringify(actionData)}</pre> :\n                null}\n            </div>\n          )\n        }\n      `,\n    },\n  });\n\n  appFixture = await createAppFixture(fixture);\n});\n\ntest.afterAll(() => appFixture.close());\n\ntest(\"loader request on SSR GET requests\", async ({ page }) => {\n  let app = new PlaywrightFixture(appFixture, page);\n  await app.goto(\"/\");\n  await app.clickElement(\"#set-cookie\");\n\n  let loaderData = JSON.parse(await page.locator(\"#loader-data\").innerHTML());\n  expect(loaderData.method).toEqual(\"GET\");\n  expect(loaderData.url).toMatch(/^http:\\/\\/localhost:\\d+\\/$/);\n  expect(loaderData.headers.cookie).toEqual(undefined);\n  expect(loaderData.body).toEqual(null);\n\n  await app.clickElement(\"#submit-get-ssr\");\n\n  loaderData = JSON.parse(await page.locator(\"#loader-data\").innerHTML());\n  expect(loaderData.method).toEqual(\"GET\");\n  expect(loaderData.url).toMatch(/^http:\\/\\/localhost:\\d+\\/\\?type=ssr$/);\n  expect(loaderData.headers.cookie).toEqual(\"cookie=nomnom\");\n  expect(loaderData.body).toEqual(null);\n});\n\ntest(\"loader request on CSR GET requests\", async ({ page }) => {\n  let app = new PlaywrightFixture(appFixture, page);\n  await app.goto(\"/\");\n  await app.clickElement(\"#set-cookie\");\n\n  let loaderData = JSON.parse(await page.locator(\"#loader-data\").innerHTML());\n  expect(loaderData.method).toEqual(\"GET\");\n  expect(loaderData.url).toMatch(/^http:\\/\\/localhost:\\d+\\/$/);\n  expect(loaderData.headers.cookie).toEqual(undefined);\n  expect(loaderData.body).toEqual(null);\n\n  await app.clickElement(\"#submit-get-csr\");\n\n  loaderData = JSON.parse(await page.locator(\"#loader-data\").innerHTML());\n  expect(loaderData.method).toEqual(\"GET\");\n  expect(loaderData.url).toMatch(/^http:\\/\\/localhost:\\d+\\/\\?type=csr$/);\n  expect(loaderData.headers.cookie).toEqual(\"cookie=nomnom\");\n  expect(loaderData.body).toEqual(null);\n});\n\ntest(\"action + loader requests SSR POST requests\", async ({ page }) => {\n  let app = new PlaywrightFixture(appFixture, page);\n  await app.goto(\"/\");\n  await app.clickElement(\"#set-cookie\");\n\n  let loaderData = JSON.parse(await page.locator(\"#loader-data\").innerHTML());\n  expect(loaderData.method).toEqual(\"GET\");\n  expect(loaderData.url).toMatch(/^http:\\/\\/localhost:\\d+\\/$/);\n  expect(loaderData.headers.cookie).toEqual(undefined);\n  expect(loaderData.body).toEqual(null);\n\n  await app.clickElement(\"#submit-post-ssr\");\n\n  let actionData = JSON.parse(await page.locator(\"#action-data\").innerHTML());\n  expect(actionData.method).toEqual(\"POST\");\n  expect(actionData.url).toMatch(/^http:\\/\\/localhost:\\d+\\/$/);\n  expect(actionData.headers.cookie).toEqual(\"cookie=nomnom\");\n  expect(actionData.body).toEqual({ type: \"ssr\" });\n\n  loaderData = JSON.parse(await page.locator(\"#loader-data\").innerHTML());\n  expect(loaderData.method).toEqual(\"GET\");\n  expect(loaderData.url).toMatch(/^http:\\/\\/localhost:\\d+\\/$/);\n  expect(loaderData.headers.cookie).toEqual(\"cookie=nomnom\");\n  expect(loaderData.body).toEqual(null);\n});\n\ntest(\"action + loader requests on CSR POST requests\", async ({ page }) => {\n  let app = new PlaywrightFixture(appFixture, page);\n  await app.goto(\"/\");\n  await app.clickElement(\"#set-cookie\");\n\n  let loaderData = JSON.parse(await page.locator(\"#loader-data\").innerHTML());\n  expect(loaderData.method).toEqual(\"GET\");\n  expect(loaderData.url).toMatch(/^http:\\/\\/localhost:\\d+\\/$/);\n  expect(loaderData.headers.cookie).toEqual(undefined);\n  expect(loaderData.body).toEqual(null);\n\n  await app.clickElement(\"#submit-post-csr\");\n\n  let actionData = JSON.parse(await page.locator(\"#action-data\").innerHTML());\n  expect(actionData.method).toEqual(\"POST\");\n  expect(actionData.url).toMatch(/^http:\\/\\/localhost:\\d+\\/$/);\n  expect(actionData.headers.cookie).toEqual(\"cookie=nomnom\");\n  expect(actionData.body).toEqual({ type: \"csr\" });\n\n  loaderData = JSON.parse(await page.locator(\"#loader-data\").innerHTML());\n  expect(loaderData.method).toEqual(\"GET\");\n  expect(loaderData.url).toMatch(/^http:\\/\\/localhost:\\d+\\/$/);\n  expect(loaderData.headers.cookie).toEqual(\"cookie=nomnom\");\n  expect(loaderData.body).toEqual(null);\n});\n"
  },
  {
    "path": "integration/resource-routes-test.ts",
    "content": "import { test, expect } from \"@playwright/test\";\n\nimport { UNSAFE_ServerMode as ServerMode } from \"react-router\";\nimport {\n  createAppFixture,\n  createFixture,\n  js,\n} from \"./helpers/create-fixture.js\";\nimport type { AppFixture, Fixture } from \"./helpers/create-fixture.js\";\nimport { PlaywrightFixture } from \"./helpers/playwright-fixture.js\";\n\ntest.describe(\"loader in an app\", async () => {\n  let appFixture: AppFixture;\n  let fixture: Fixture;\n  let _consoleError: typeof console.error;\n  let _consoleWarn: typeof console.warn;\n\n  let SVG_CONTENTS = `<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 100 100\" fill=\"none\" stroke=\"#000\" stroke-width=\"4\" aria-label=\"Chicken\"><path d=\"M48.1 34C22.1 32 1.4 51 2.5 67.2c1.2 16.1 19.8 17 29 17.8H89c15.7-6.6 6.3-18.9.3-20.5A28 28 0 0073 41.7c-.5-7.2 3.4-11.6 6.9-15.3 8.5 2.6 8-8 .8-7.2.6-6.5-12.3-5.9-6.7 2.7l-3.7 5c-6.9 5.4-10.9 5.1-22.2 7zM48.1 34c-38 31.9 29.8 58.4 25 7.7M70.3 26.9l5.4 4.2\"/></svg>`;\n\n  test.beforeAll(async () => {\n    _consoleError = console.error;\n    console.error = () => {};\n    _consoleWarn = console.warn;\n    console.warn = () => {};\n    fixture = await createFixture({\n      files: {\n        \"app/routes/_index.tsx\": js`\n          import { Form, Link } from \"react-router\";\n\n          export default () => (\n            <>\n              <Link to=\"/redirect\">Redirect</Link>\n              <Link to=\"/some-404-path\">404 route</Link>\n              <Form action=\"/redirect-to\" method=\"post\">\n                <input name=\"destination\" defaultValue=\"/redirect-destination\" />\n                <button type=\"submit\">Redirect</button>\n              </Form>\n              <Form action=\"/no-action\" method=\"post\">\n                <button type=\"submit\">Submit to no action route</button>\n              </Form>\n            </>\n          )\n        `,\n        \"app/routes/redirected.tsx\": js`\n          export default () => <div data-testid=\"redirected\">You were redirected</div>;\n        `,\n        \"app/routes/redirect.tsx\": js`\n          import { redirect } from \"react-router\";\n\n          export let loader = () => redirect(\"/redirected\");\n        `,\n        \"app/routes/redirect-to.tsx\": js`\n          import { redirect } from \"react-router\";\n\n          export let action = async ({ request }) => {\n            let formData = await request.formData();\n            return redirect(formData.get('destination'));\n          }\n        `,\n        \"app/routes/redirect-destination.tsx\": js`\n          export default () => <div data-testid=\"redirect-destination\">You made it!</div>\n        `,\n        \"app/routes/data[.]json.tsx\": js`\n          export let loader = () => Response.json({ hello: \"world\" });\n        `,\n        \"app/assets/icon.svg\": SVG_CONTENTS,\n        \"app/routes/[manifest.webmanifest].tsx\": js`\n          import iconUrl from \"~/assets/icon.svg\";\n          export  function loader() {\n            return {\n              icons: [\n                {\n                  src: iconUrl,\n                  sizes: '48x48 72x72 96x96 128x128 192x192 256x256 512x512',\n                  type: 'image/svg+xml',\n                },\n              ],\n            };\n          }\n        `,\n        \"app/routes/throw-error.tsx\": js`\n          export let loader = () => {\n            throw new Error('Oh noes!')\n          }\n        `,\n        \"app/routes/return-response.tsx\": js`\n          export let loader = () => {\n            return new Response('Partial', { status: 207, headers: { 'X-Foo': 'Bar'} });\n          }\n        `,\n        \"app/routes/throw-response.tsx\": js`\n          export let loader = () => {\n            throw new Response('Partial', { status: 207, headers: { 'X-Foo': 'Bar' } });\n          }\n        `,\n        \"app/routes/return-data.tsx\": js`\n          import { data } from \"react-router\";\n          export let loader = () => {\n            return data('Partial', { status: 207, headers: { 'X-Foo': 'Bar'} });\n          }\n        `,\n        \"app/routes/throw-data.tsx\": js`\n          import { data } from \"react-router\";\n          export let loader = () => {\n            throw data('Partial', { status: 207, headers: { 'X-Foo': 'Bar'} });\n          }\n        `,\n        \"app/routes/return-data-through-middleware.tsx\": js`\n          import { data } from \"react-router\";\n          export const middleware = [(_, next) => next()]\n          export let loader = () => {\n            return data('Partial', { status: 207, headers: { 'X-Foo': 'Bar'} });\n          }\n        `,\n        \"app/routes/throw-data-through-middleware.tsx\": js`\n          import { data } from \"react-router\";\n          export const middleware = [(_, next) => next()]\n          export let loader = () => {\n            throw data('Partial', { status: 207, headers: { 'X-Foo': 'Bar'} });\n          }\n        `,\n        \"app/routes/return-object.tsx\": js`\n          export let loader = () => {\n            return { hello: 'world' };\n          }\n        `,\n        \"app/routes/return-string.tsx\": js`\n          export let loader = () => {\n            return 'hello world';\n          }\n        `,\n        \"app/routes/throw-object.tsx\": js`\n          export let loader = () => {\n            throw { but: 'why' };\n          }\n        `,\n        \"app/routes/no-action.tsx\": js`\n          export let loader = () => {\n            return { ok: true };\n          }\n        `,\n        \"app/routes/$.tsx\": js`\n          import { useRouteError } from \"react-router\";\n          export function loader({ request }) {\n            throw Response.json({\n              message: new URL(request.url).pathname + ' not found'\n            }, {\n              status: 404\n            });\n          }\n          export function ErrorBoundary() {\n            let error = useRouteError();\n            return <pre>{error.status + ' ' + error.data.message}</pre>;\n          }\n        `,\n      },\n    });\n    appFixture = await createAppFixture(fixture, ServerMode.Test);\n  });\n\n  test.afterAll(() => {\n    appFixture.close();\n    console.error = _consoleError;\n    console.warn = _consoleWarn;\n  });\n\n  test.describe(\"with JavaScript\", () => {\n    runTests();\n  });\n\n  test.describe(\"without JavaScript\", () => {\n    test.use({ javaScriptEnabled: false });\n    runTests();\n  });\n\n  function runTests() {\n    test(\"should redirect to redirected\", async ({ page }) => {\n      let app = new PlaywrightFixture(appFixture, page);\n      await app.goto(\"/\");\n      await page.click(\"a[href='/redirect']\");\n      await page.waitForSelector(\"[data-testid='redirected']\");\n    });\n\n    test(\"should handle post to destination\", async ({ page }) => {\n      let app = new PlaywrightFixture(appFixture, page);\n      await app.goto(\"/\");\n      await page.click(\"button[type='submit']\");\n      await page.waitForSelector(\"[data-testid='redirect-destination']\");\n    });\n\n    test(\"should handle reloadDocument to resource route\", async ({ page }) => {\n      let app = new PlaywrightFixture(appFixture, page);\n      await app.goto(\"/data.json\");\n      expect(await page.content()).toContain('{\"hello\":\"world\"}');\n    });\n\n    test(\"should handle errors thrown from resource routes\", async ({\n      page,\n    }) => {\n      let app = new PlaywrightFixture(appFixture, page);\n      let res = await app.goto(\"/throw-error\");\n      expect(res.status()).toBe(500);\n      expect(await res.text()).toEqual(\n        \"Unexpected Server Error\\n\\nError: Oh noes!\",\n      );\n    });\n\n    test(\"should let loader throw to it's own boundary without a default export\", async ({\n      page,\n    }) => {\n      let app = new PlaywrightFixture(appFixture, page);\n      await app.goto(\"/\", true);\n      await app.clickLink(\"/some-404-path\");\n      let html = await app.getHtml();\n      expect(html).toMatch(\"404 /some-404-path not found\");\n    });\n  }\n\n  test(\"should handle responses returned from resource routes\", async ({\n    page,\n  }) => {\n    let app = new PlaywrightFixture(appFixture, page);\n    let res = await app.goto(\"/return-response\");\n    expect(res.status()).toBe(207);\n    expect(res.headers()[\"x-foo\"]).toBe(\"Bar\");\n    expect(await res.text()).toEqual(\"Partial\");\n  });\n\n  test(\"should handle responses thrown from resource routes\", async ({\n    page,\n  }) => {\n    let app = new PlaywrightFixture(appFixture, page);\n    let res = await app.goto(\"/throw-response\");\n    expect(res.status()).toBe(207);\n    expect(res.headers()[\"x-foo\"]).toBe(\"Bar\");\n    expect(await res.text()).toEqual(\"Partial\");\n  });\n\n  test(\"should handle data() returned from resource routes\", async ({\n    page,\n  }) => {\n    let app = new PlaywrightFixture(appFixture, page);\n    let res = await app.goto(\"/return-data\");\n    expect(res.status()).toBe(207);\n    expect(res.headers()[\"x-foo\"]).toBe(\"Bar\");\n    expect(await res.json()).toEqual(\"Partial\");\n  });\n\n  test(\"should handle data() thrown from resource routes\", async ({ page }) => {\n    let app = new PlaywrightFixture(appFixture, page);\n    let res = await app.goto(\"/throw-data\");\n    expect(res.status()).toBe(207);\n    expect(res.headers()[\"x-foo\"]).toBe(\"Bar\");\n    expect(await res.json()).toEqual(\"Partial\");\n  });\n\n  test(\"should handle data() returned from resource routes through middleware\", async ({\n    page,\n  }) => {\n    let app = new PlaywrightFixture(appFixture, page);\n    let res = await app.goto(\"/return-data-through-middleware\");\n    expect(res.status()).toBe(207);\n    expect(res.headers()[\"x-foo\"]).toBe(\"Bar\");\n    expect(await res.json()).toEqual(\"Partial\");\n  });\n\n  test(\"should handle data() thrown from resource routes through middleware\", async ({\n    page,\n  }) => {\n    let app = new PlaywrightFixture(appFixture, page);\n    let res = await app.goto(\"/throw-data-through-middleware\");\n    expect(res.status()).toBe(207);\n    expect(res.headers()[\"x-foo\"]).toBe(\"Bar\");\n    expect(await res.json()).toEqual(\"Partial\");\n  });\n\n  test(\"should convert strings returned from resource routes to text responses\", async ({\n    page,\n  }) => {\n    let app = new PlaywrightFixture(appFixture, page);\n    let res = await app.goto(\"/return-string\");\n    expect(res.status()).toBe(200);\n    expect(await res.text()).toEqual(\"hello world\");\n  });\n\n  test(\"should convert non-strings returned from resource routes to JSON responses\", async ({\n    page,\n  }) => {\n    let app = new PlaywrightFixture(appFixture, page);\n    let res = await app.goto(\"/return-object\");\n    expect(res.status()).toBe(200);\n    expect(await res.json()).toEqual({ hello: \"world\" });\n  });\n\n  test(\"should handle objects returned from resource routes\", async ({\n    page,\n  }) => {\n    let app = new PlaywrightFixture(appFixture, page);\n    let res = await app.goto(\"/return-object\");\n    expect(res.status()).toBe(200);\n    expect(await res.json()).toEqual({ hello: \"world\" });\n  });\n\n  test(\"should handle objects thrown from resource routes\", async ({\n    page,\n  }) => {\n    let app = new PlaywrightFixture(appFixture, page);\n    let res = await app.goto(\"/throw-object\");\n    expect(res.status()).toBe(500);\n    expect(await res.text()).toEqual(\n      \"Unexpected Server Error\\n\\n[object Object]\",\n    );\n  });\n\n  test(\"should handle ErrorResponses thrown from resource routes on document requests\", async () => {\n    let res = await fixture.postDocument(\"/no-action\", new FormData());\n    expect(res.status).toBe(405);\n    expect(res.statusText).toBe(\"Method Not Allowed\");\n    expect(await res.text()).toBe('{\"message\":\"Unexpected Server Error\"}');\n  });\n\n  test(\"should handle ErrorResponses thrown from resource routes on client submissions\", async ({\n    page,\n  }) => {\n    let logs: string[] = [];\n    page.on(\"console\", (msg) => logs.push(msg.text()));\n    let app = new PlaywrightFixture(appFixture, page);\n    await app.goto(\"/\");\n    await app.clickSubmitButton(\"/no-action\");\n    let html = await app.getHtml();\n    expect(html).toMatch(\"405 Method Not Allowed\");\n    expect(logs[0]).toContain(\n      'Route \"routes/no-action\" does not have an action',\n    );\n  });\n});\n\ntest.describe(\"Development server\", async () => {\n  let appFixture: AppFixture;\n  let fixture: Fixture;\n  let _consoleError: typeof console.error;\n\n  test.beforeAll(async () => {\n    _consoleError = console.error;\n    console.error = () => {};\n\n    fixture = await createFixture(\n      {\n        files: {\n          \"app/routes/_index.tsx\": js`\n            import { Link } from \"react-router\";\n            export default () => <Link to=\"/child\">Child</Link>;\n          `,\n          \"app/routes/_main.tsx\": js`\n            import { useRouteError } from \"react-router\";\n            export function ErrorBoundary() {\n              return <pre>{useRouteError().message}</pre>;\n            }\n          `,\n          \"app/routes/_main.child.tsx\": js`\n            export default function Component() {\n              throw new Error('Error from render')\n            }\n          `,\n        },\n      },\n      ServerMode.Development,\n    );\n    appFixture = await createAppFixture(fixture, ServerMode.Development);\n  });\n\n  test.afterAll(() => {\n    appFixture.close();\n    console.error = _consoleError;\n  });\n\n  test.describe(\"with JavaScript\", () => {\n    runTests();\n  });\n\n  test.describe(\"without JavaScript\", () => {\n    test.use({ javaScriptEnabled: false });\n    runTests();\n  });\n\n  function runTests() {\n    test(\"should not treat an ErrorBoundary-only route as a resource route\", async ({\n      page,\n    }) => {\n      let app = new PlaywrightFixture(appFixture, page);\n      await app.goto(\"/child\");\n      let html = await app.getHtml();\n      expect(html).not.toMatch(\"has no component\");\n      expect(html).toMatch(\"Error from render\");\n    });\n  }\n});\n"
  },
  {
    "path": "integration/revalidate-test.ts",
    "content": "import { test, expect } from \"@playwright/test\";\n\nimport {\n  createAppFixture,\n  createFixture,\n  js,\n} from \"./helpers/create-fixture.js\";\nimport type { AppFixture } from \"./helpers/create-fixture.js\";\nimport { PlaywrightFixture } from \"./helpers/playwright-fixture.js\";\n\ntest.describe(\"Revalidation\", () => {\n  let appFixture: AppFixture;\n\n  test.beforeAll(async () => {\n    appFixture = await createAppFixture(\n      await createFixture({\n        files: {\n          \"app/root.tsx\": js`\n              import { Link, Outlet, Scripts, useNavigation } from \"react-router\";\n\n              export default function Component() {\n                let navigation = useNavigation();\n                return (\n                  <html>\n                    <head />\n                    <body>\n                      <nav>\n                        {navigation.state === 'idle' ?\n                          <p id=\"idle\">Idle</p> :\n                          <p id=\"busy\">Busy</p>}\n                        <ul>\n                          <li><Link to=\"/\">/</Link></li>\n                          <li><Link to=\"/parent\">/parent</Link></li>\n                          <li><Link to=\"/parent/child\">/parent/child</Link></li>\n                          <li><Link to=\"/parent/child?revalidate=parent\">/parent/child?revalidate=parent</Link></li>\n                          <li><Link to=\"/parent/child?revalidate=child\">/parent/child?revalidate=child</Link></li>\n                          <li><Link to=\"/parent/child?revalidate=parent,child\">/parent/child?revalidate=parent,child</Link></li>\n                        </ul>\n                      </nav>\n                      <Outlet />\n                      <Scripts />\n                    </body>\n                  </html>\n                );\n              }\n            `,\n\n          \"app/routes/parent.tsx\": js`\n              import { data, Outlet, useLoaderData } from \"react-router\";\n\n              export async function loader({ request }) {\n                let header = request.headers.get('Cookie') || '';\n                let cookie = header\n                  .split(';')\n                  .map(c => c.trim())\n                  .find(c => c.startsWith('parent='))\n                let strValue = (cookie || 'parent=0').split(\"=\")[1];\n                let value = parseInt(strValue, 10) + 1;\n                return data({ value }, {\n                  headers: {\n                    \"Set-Cookie\": \"parent=\" + value,\n                  }\n                })\n              };\n\n              export function shouldRevalidate({ nextUrl, formData }) {\n                if (nextUrl.searchParams.get('revalidate')?.split(',')?.includes('parent')) {\n                  return true;\n                }\n                if (formData?.getAll('revalidate')?.includes('parent')) {\n                  return true;\n                }\n                return false\n              }\n\n              export default function Component() {\n                let data = useLoaderData();\n                return (\n                  <>\n                    <p data-testid=\"parent-data\">{'Value:' + data.value}</p>\n                    <Outlet />\n                  </>\n                );\n              }\n            `,\n\n          \"app/routes/parent.child.tsx\": js`\n              import { data, Form, useLoaderData, useRevalidator } from \"react-router\";\n\n              export async function action() {\n                return { action: 'data' }\n              }\n\n              export async function loader({ request }) {\n                let header = request.headers.get('Cookie') || '';\n                let cookie = header\n                  .split(';')\n                  .map(c => c.trim())\n                  .find(c => c.startsWith('child='))\n                let strValue = (cookie || 'child=0').split(\"=\")[1];\n                let value = parseInt(strValue, 10) + 1;\n                return data({ value }, {\n                  headers: {\n                    \"Set-Cookie\": \"child=\" + value,\n                  }\n                })\n              };\n\n              export function shouldRevalidate({ nextUrl, formData }) {\n                let revalidate = (nextUrl.searchParams.get('revalidate') || '').split(',')\n                if (revalidate.includes('child')) {\n                  return true;\n                }\n                if (formData?.getAll('revalidate')?.includes('child')) {\n                  return true;\n                }\n                return false\n              }\n\n              export default function Component() {\n                let data = useLoaderData();\n                let revalidator = useRevalidator();\n                return (\n                  <>\n                    <p data-testid=\"child-data\">{'Value:' + data.value}</p>\n                    <Form method=\"post\" action=\".\">\n                      <input type=\"hidden\" name=\"revalidate\" value=\"\" />\n                      <button type=\"submit\" id=\"submit-neither\">Submit and revalidate neither</button>\n                    </Form>\n                    <Form method=\"post\" action=\".\">\n                      <input type=\"hidden\" name=\"revalidate\" value=\"parent\" />\n                      <button type=\"submit\" id=\"submit-parent\">Submit and revalidate parent</button>\n                    </Form>\n                    <Form method=\"post\" action=\".\">\n                      <input type=\"hidden\" name=\"revalidate\" value=\"child\" />\n                      <button type=\"submit\" id=\"submit-child\">Submit and revalidate child</button>\n                    </Form>\n                    <Form method=\"post\" action=\".\">\n                      <input type=\"hidden\" name=\"revalidate\" value=\"parent\" />\n                      <input type=\"hidden\" name=\"revalidate\" value=\"child\" />\n                      <button type=\"submit\" id=\"submit-both\">Submit and revalidate both</button>\n                    </Form>\n                    {revalidator.state === 'idle' ?\n                      <p id=\"revalidation-idle\">Revalidation idle</p> :\n                      <p id=\"revalidation-busy\">Revalidation busy</p>}\n                    <button id=\"revalidate\" onClick={() => revalidator.revalidate()}>\n                      Revalidate\n                    </button>\n                  </>\n                );\n              }\n            `,\n        },\n      }),\n    );\n  });\n\n  test.afterAll(() => {\n    appFixture.close();\n  });\n\n  test(\"Revalidates according to shouldRevalidate (loading navigations)\", async ({\n    page,\n  }) => {\n    let app = new PlaywrightFixture(appFixture, page);\n    await app.goto(\"/\", true);\n\n    // Should call parent (first load)\n    await app.clickLink(\"/parent\");\n    await page.waitForSelector(\"#idle\");\n    await expect(\n      page.getByTestId(\"parent-data\").filter({ hasText: \"Value:1\" }),\n    ).toBeAttached();\n\n    // Should call child (first load) but not parent (no param)\n    await app.clickLink(\"/parent/child\");\n    await page.waitForSelector(\"#idle\");\n    await expect(\n      page.getByTestId(\"parent-data\").filter({ hasText: \"Value:1\" }),\n    ).toBeAttached();\n    await expect(\n      page.getByTestId(\"child-data\").filter({ hasText: \"Value:1\" }),\n    ).toBeAttached();\n\n    // Should call neither\n    await app.clickLink(\"/parent/child\");\n    await page.waitForSelector(\"#idle\");\n    await expect(\n      page.getByTestId(\"parent-data\").filter({ hasText: \"Value:1\" }),\n    ).toBeAttached();\n    await expect(\n      page.getByTestId(\"child-data\").filter({ hasText: \"Value:1\" }),\n    ).toBeAttached();\n\n    // Should call both\n    await app.clickLink(\"/parent/child?revalidate=parent,child\");\n    await page.waitForSelector(\"#idle\");\n    await expect(\n      page.getByTestId(\"parent-data\").filter({ hasText: \"Value:2\" }),\n    ).toBeAttached();\n    await expect(\n      page.getByTestId(\"child-data\").filter({ hasText: \"Value:2\" }),\n    ).toBeAttached();\n\n    // Should call parent only\n    await app.clickLink(\"/parent/child?revalidate=parent\");\n    await page.waitForSelector(\"#idle\");\n    await expect(\n      page.getByTestId(\"parent-data\").filter({ hasText: \"Value:3\" }),\n    ).toBeAttached();\n    await expect(\n      page.getByTestId(\"child-data\").filter({ hasText: \"Value:2\" }),\n    ).toBeAttached();\n\n    // Should call child only\n    await app.clickLink(\"/parent/child?revalidate=child\");\n    await page.waitForSelector(\"#idle\");\n    await expect(\n      page.getByTestId(\"parent-data\").filter({ hasText: \"Value:3\" }),\n    ).toBeAttached();\n    await expect(\n      page.getByTestId(\"child-data\").filter({ hasText: \"Value:3\" }),\n    ).toBeAttached();\n  });\n\n  test(\"Revalidates according to shouldRevalidate (submission navigations)\", async ({\n    page,\n  }) => {\n    let app = new PlaywrightFixture(appFixture, page);\n    await app.goto(\"/\", true);\n\n    // Should call both (first load)\n    await app.clickLink(\"/parent/child\");\n    await page.waitForSelector(\"#idle\");\n    await expect(\n      page.getByTestId(\"parent-data\").filter({ hasText: \"Value:1\" }),\n    ).toBeAttached();\n    await expect(\n      page.getByTestId(\"child-data\").filter({ hasText: \"Value:1\" }),\n    ).toBeAttached();\n\n    // Should call neither\n    await app.clickElement(\"#submit-neither\");\n    await page.waitForSelector(\"#idle\");\n    await expect(\n      page.getByTestId(\"parent-data\").filter({ hasText: \"Value:1\" }),\n    ).toBeAttached();\n    await expect(\n      page.getByTestId(\"child-data\").filter({ hasText: \"Value:1\" }),\n    ).toBeAttached();\n\n    // Should call both\n    await app.clickElement(\"#submit-both\");\n    await page.waitForSelector(\"#idle\");\n    await expect(\n      page.getByTestId(\"parent-data\").filter({ hasText: \"Value:2\" }),\n    ).toBeAttached();\n    await expect(\n      page.getByTestId(\"child-data\").filter({ hasText: \"Value:2\" }),\n    ).toBeAttached();\n\n    // Should call parent only\n    await app.clickElement(\"#submit-parent\");\n    await page.waitForSelector(\"#idle\");\n    await expect(\n      page.getByTestId(\"parent-data\").filter({ hasText: \"Value:3\" }),\n    ).toBeAttached();\n    await expect(\n      page.getByTestId(\"child-data\").filter({ hasText: \"Value:2\" }),\n    ).toBeAttached();\n\n    // Should call child only\n    await app.clickElement(\"#submit-child\");\n    await page.waitForSelector(\"#idle\");\n    await expect(\n      page.getByTestId(\"parent-data\").filter({ hasText: \"Value:3\" }),\n    ).toBeAttached();\n    await expect(\n      page.getByTestId(\"child-data\").filter({ hasText: \"Value:3\" }),\n    ).toBeAttached();\n  });\n\n  test(\"Revalidates on demand with useRevalidator\", async ({ page }) => {\n    let app = new PlaywrightFixture(appFixture, page);\n    await app.goto(\"/\", true);\n\n    // Should call both (first load)\n    await app.clickLink(\"/parent/child\");\n    await page.waitForSelector(\"#idle\");\n    await expect(\n      page.getByTestId(\"parent-data\").filter({ hasText: \"Value:1\" }),\n    ).toBeAttached();\n    await expect(\n      page.getByTestId(\"child-data\").filter({ hasText: \"Value:1\" }),\n    ).toBeAttached();\n\n    // Should call neither on manual revalidate (no params)\n    await app.clickElement(\"#revalidate\");\n    await page.waitForSelector(\"#revalidation-idle\", { state: \"visible\" });\n    await expect(\n      page.getByTestId(\"parent-data\").filter({ hasText: \"Value:1\" }),\n    ).toBeAttached();\n    await expect(\n      page.getByTestId(\"child-data\").filter({ hasText: \"Value:1\" }),\n    ).toBeAttached();\n\n    // Should call both\n    await app.clickLink(\"/parent/child?revalidate=parent,child\");\n    await page.waitForSelector(\"#idle\");\n    await expect(\n      page.getByTestId(\"parent-data\").filter({ hasText: \"Value:2\" }),\n    ).toBeAttached();\n    await expect(\n      page.getByTestId(\"child-data\").filter({ hasText: \"Value:2\" }),\n    ).toBeAttached();\n\n    // Should call both on manual revalidate\n    await app.clickElement(\"#revalidate\");\n    await page.waitForSelector(\"#revalidation-idle\", { state: \"visible\" });\n    await expect(\n      page.getByTestId(\"parent-data\").filter({ hasText: \"Value:3\" }),\n    ).toBeAttached();\n    await expect(\n      page.getByTestId(\"child-data\").filter({ hasText: \"Value:3\" }),\n    ).toBeAttached();\n\n    // Should call parent only\n    await app.clickLink(\"/parent/child?revalidate=parent\");\n    await page.waitForSelector(\"#idle\");\n    await expect(\n      page.getByTestId(\"parent-data\").filter({ hasText: \"Value:4\" }),\n    ).toBeAttached();\n    await expect(\n      page.getByTestId(\"child-data\").filter({ hasText: \"Value:3\" }),\n    ).toBeAttached();\n\n    // Should call parent only on manual revalidate\n    await app.clickElement(\"#revalidate\");\n    await page.waitForSelector(\"#revalidation-idle\", { state: \"visible\" });\n    await expect(\n      page.getByTestId(\"parent-data\").filter({ hasText: \"Value:5\" }),\n    ).toBeAttached();\n    await expect(\n      page.getByTestId(\"child-data\").filter({ hasText: \"Value:3\" }),\n    ).toBeAttached();\n\n    // Should call child only\n    await app.clickLink(\"/parent/child?revalidate=child\");\n    await page.waitForSelector(\"#idle\");\n    await expect(\n      page.getByTestId(\"parent-data\").filter({ hasText: \"Value:5\" }),\n    ).toBeAttached();\n    await expect(\n      page.getByTestId(\"child-data\").filter({ hasText: \"Value:4\" }),\n    ).toBeAttached();\n\n    // Should call child only on manual revalidate\n    await app.clickElement(\"#revalidate\");\n    await page.waitForSelector(\"#revalidation-idle\", { state: \"visible\" });\n    await expect(\n      page.getByTestId(\"parent-data\").filter({ hasText: \"Value:5\" }),\n    ).toBeAttached();\n    await expect(\n      page.getByTestId(\"child-data\").filter({ hasText: \"Value:5\" }),\n    ).toBeAttached();\n  });\n});\n"
  },
  {
    "path": "integration/root-route-test.ts",
    "content": "import { test, expect } from \"@playwright/test\";\n\nimport { UNSAFE_ServerMode as ServerMode } from \"react-router\";\nimport {\n  createAppFixture,\n  createFixture,\n  js,\n} from \"./helpers/create-fixture.js\";\nimport type { Fixture, AppFixture } from \"./helpers/create-fixture.js\";\nimport { PlaywrightFixture } from \"./helpers/playwright-fixture.js\";\n\ntest.describe(\"root route\", () => {\n  let fixture: Fixture;\n  let appFixture: AppFixture;\n\n  test.afterAll(() => {\n    appFixture.close();\n  });\n\n  test(\"matches the sole root route on /\", async ({ page }) => {\n    fixture = await createFixture({\n      files: {\n        \"app/root.tsx\": js`\n          export default function Root() {\n            return (\n              <html>\n                <body>\n                  <h1>Hello Root!</h1>\n                </body>\n              </html>\n            );\n          }\n        `,\n      },\n    });\n    appFixture = await createAppFixture(fixture);\n    let app = new PlaywrightFixture(appFixture, page);\n    await app.goto(\"/\");\n    await page.waitForSelector(\"h1\");\n    expect(await app.getHtml(\"h1\")).toMatch(\"Hello Root!\");\n  });\n\n  test(\"renders the Layout around the component\", async ({ page }) => {\n    fixture = await createFixture({\n      files: {\n        \"app/root.tsx\": js`\n          export function Layout({ children }) {\n            return (\n              <html>\n                <head>\n                  <title>Layout Title</title>\n                </head>\n                <body>\n                  {children}\n                </body>\n              </html>\n            );\n          }\n          export default function Root() {\n            return <h1>Hello Root!</h1>;\n          }\n        `,\n      },\n    });\n    appFixture = await createAppFixture(fixture);\n    let app = new PlaywrightFixture(appFixture, page);\n    await app.goto(\"/\");\n    await page.waitForSelector(\"h1\");\n    expect(await app.getHtml(\"title\")).toMatch(\"Layout Title\");\n    expect(await app.getHtml(\"h1\")).toMatch(\"Hello Root!\");\n  });\n\n  test(\"renders the Layout around the ErrorBoundary\", async ({ page }) => {\n    let oldConsoleError;\n    oldConsoleError = console.error;\n    console.error = () => {};\n\n    fixture = await createFixture(\n      {\n        files: {\n          \"app/root.tsx\": js`\n          import { useRouteError } from \"react-router\";\n          export function Layout({ children }) {\n            return (\n              <html>\n                <head>\n                  <title>Layout Title</title>\n                </head>\n                <body>\n                  {children}\n                </body>\n              </html>\n            );\n          }\n          export default function Root() {\n            throw new Error('broken render')\n          }\n          export function ErrorBoundary() {\n            return <p>{useRouteError().message}</p>;\n          }\n        `,\n        },\n      },\n      ServerMode.Development,\n    );\n    appFixture = await createAppFixture(fixture, ServerMode.Development);\n    let app = new PlaywrightFixture(appFixture, page);\n    await app.goto(\"/\");\n    await page.waitForSelector(\"p\");\n    expect(await app.getHtml(\"title\")).toMatch(\"Layout Title\");\n    expect(await app.getHtml(\"p\")).toMatch(\"broken render\");\n\n    console.error = oldConsoleError;\n  });\n\n  test(\"renders the Layout around the default ErrorBoundary\", async ({\n    page,\n  }) => {\n    let oldConsoleError;\n    oldConsoleError = console.error;\n    console.error = () => {};\n\n    fixture = await createFixture(\n      {\n        files: {\n          \"app/root.tsx\": js`\n          export function Layout({ children }) {\n            return (\n              <html>\n                <head>\n                  <title>Layout Title</title>\n                </head>\n                <body>\n                  {children}\n                </body>\n              </html>\n            );\n          }\n          export default function Root() {\n            throw new Error('broken render')\n          }\n        `,\n        },\n      },\n      ServerMode.Development,\n    );\n    appFixture = await createAppFixture(fixture, ServerMode.Development);\n    let app = new PlaywrightFixture(appFixture, page);\n    await app.goto(\"/\");\n    await page.waitForSelector(\"h1\");\n    expect(await app.getHtml(\"title\")).toMatch(\"Layout Title\");\n    expect(await app.getHtml(\"h1\")).toMatch(\"Application Error\");\n\n    console.error = oldConsoleError;\n  });\n\n  test(\"Skip the Layout on subsequent server renders if Layout/ErrorBoundary throws (sync)\", async ({\n    page,\n  }) => {\n    let oldConsoleError;\n    oldConsoleError = console.error;\n    console.error = () => {};\n\n    fixture = await createFixture(\n      {\n        files: {\n          \"app/root.tsx\": js`\n            import * as React from \"react\";\n            import { Scripts, useRouteError, useRouteLoaderData } from \"react-router\";\n            export function Layout({ children }) {\n              let data = useRouteLoaderData(\"root\");\n              return (\n                <html>\n                  <head>\n                    <title>Layout Title</title>\n                  </head>\n                  <body id=\"layout\">\n                    <p>{data.this.should.throw}</p>\n                    {children}\n                    <Scripts />\n                  </body>\n                </html>\n              );\n            }\n            export function loader() {\n              return { ok: true };\n            }\n            export default function Root() {\n              return <p>success</p>;\n            }\n            export function ErrorBoundary() {\n              return <p>error</p>;\n            }\n          `,\n        },\n      },\n      ServerMode.Development,\n    );\n    appFixture = await createAppFixture(fixture, ServerMode.Development);\n    let app = new PlaywrightFixture(appFixture, page);\n    await app.goto(\"/\");\n    // The server should send back the fallback 500 HTML since it was unable\n    // to render the Layout/ErrorBoundary combo\n    expect(await app.page.$(\"#layout\")).toBeNull();\n    expect(await app.getHtml(\"pre\")).toMatch(\"Unexpected Server Error\");\n    expect(await app.getHtml(\"pre\")).toMatch(\n      \"Cannot read properties of undefined\",\n    );\n\n    console.error = oldConsoleError;\n  });\n\n  test(\"Skip the Layout on subsequent client renders if Layout/ErrorBoundary throws (async)\", async ({\n    page,\n    browserName,\n  }) => {\n    let oldConsoleError;\n    oldConsoleError = console.error;\n    console.error = () => {};\n\n    fixture = await createFixture(\n      {\n        files: {\n          \"app/root.tsx\": js`\n            import * as React from \"react\";\n            import { Await, Scripts, useRouteError, useRouteLoaderData } from \"react-router\";\n            export function Layout({ children }) {\n              let data = useRouteLoaderData(\"root\");\n              return (\n                <html>\n                  <head>\n                    <title>Layout Title</title>\n                  </head>\n                  <body id=\"layout\">\n                    <React.Suspense fallback={<p id=\"loading\">Loading...</p>}>\n                      <Await resolve={data.lazy}>\n                        {(v) => <p>{v.this.should.throw}</p>}\n                      </Await>\n                    </React.Suspense>\n                    {children}\n                    <Scripts />\n                  </body>\n                </html>\n              );\n            }\n            export function loader() {\n              return {\n                // this lets the app hydrate properly, then reject the promise,\n                // which should throw on the initial render _and_ the error render,\n                // resulting in us bubbling to the default error boundary and skipping\n                // our Layout component entirely to avoid a loop\n                lazy: new Promise((r) => setTimeout(() => r(null), 1000)),\n              };\n            }\n            export default function Root() {\n              return <p>success</p>;\n            }\n            export function ErrorBoundary() {\n              return <p>error</p>;\n            }\n          `,\n        },\n      },\n      ServerMode.Development,\n    );\n    appFixture = await createAppFixture(fixture, ServerMode.Development);\n    let app = new PlaywrightFixture(appFixture, page);\n    await app.goto(\"/\", false);\n    expect(await app.page.$(\"#layout\")).toBeDefined();\n    expect(await app.getHtml(\"#loading\")).toMatch(\"Loading...\");\n    await page.waitForSelector(\"h1\");\n    expect(await app.page.$(\"#layout\")).toBeNull();\n    expect(await app.getHtml(\"title\")).toMatch(\"Application Error\");\n    expect(await app.getHtml(\"h1\")).toMatch(\"Application Error\");\n    if (browserName === \"chromium\") {\n      expect(await app.getHtml(\"pre\")).toMatch(\n        \"TypeError: Cannot read properties of null\",\n      );\n    } else {\n      // Other browsers don't include the error message in the stack trace so just\n      // ensure we get the `<pre>` rendered\n      expect(await app.getHtml(\"pre\")).toMatch(\"color: red;\");\n    }\n\n    console.error = oldConsoleError;\n  });\n\n  test(\"Skip the Layout on subsequent server renders if the Layout/DefaultErrorBoundary throws (sync)\", async ({\n    page,\n  }) => {\n    let oldConsoleError;\n    oldConsoleError = console.error;\n    console.error = () => {};\n\n    fixture = await createFixture(\n      {\n        files: {\n          \"app/root.tsx\": js`\n            import * as React from \"react\";\n            import { Scripts, useRouteLoaderData } from \"react-router\";\n            export function Layout({ children }) {\n              let data = useRouteLoaderData(\"root\");\n              return (\n                <html>\n                  <head>\n                    <title>Layout Title</title>\n                  </head>\n                  <body id=\"layout\">\n                    <p>{data.this.should.throw}</p>\n                    {children}\n                    <Scripts />\n                  </body>\n                </html>\n              );\n            }\n            export function loader() {\n              return { ok: true };\n            }\n            export default function Root() {\n              return <p>success</p>;\n            }\n          `,\n        },\n      },\n      ServerMode.Development,\n    );\n    appFixture = await createAppFixture(fixture, ServerMode.Development);\n    let app = new PlaywrightFixture(appFixture, page);\n\n    await app.goto(\"/\");\n    // The server should send back the fallback 500 HTML since it was unable\n    // to render the Layout/ErrorBoundary combo\n    expect(await app.page.$(\"#layout\")).toBeNull();\n    expect(await app.getHtml(\"pre\")).toMatch(\"Unexpected Server Error\");\n    expect(await app.getHtml(\"pre\")).toMatch(\n      \"Cannot read properties of undefined\",\n    );\n\n    console.error = oldConsoleError;\n  });\n\n  test(\"Skip the Layout on subsequent client renders if the Layout/DefaultErrorBoundary throws (async)\", async ({\n    page,\n    browserName,\n  }) => {\n    let oldConsoleError;\n    oldConsoleError = console.error;\n    console.error = () => {};\n\n    fixture = await createFixture(\n      {\n        files: {\n          \"app/root.tsx\": js`\n            import * as React from \"react\";\n            import { Await, Scripts, useRouteError, useRouteLoaderData } from \"react-router\";\n            export function Layout({ children }) {\n              let data = useRouteLoaderData(\"root\");\n              return (\n                <html>\n                  <head>\n                    <title>Layout Title</title>\n                  </head>\n                  <body id=\"layout\">\n                    <React.Suspense fallback={<p id=\"loading\">Loading...</p>}>\n                      <Await resolve={data.lazy}>\n                        {(v) => <p>{v.this.should.throw}</p>}\n                      </Await>\n                    </React.Suspense>\n                    {children}\n                    <Scripts />\n                  </body>\n                </html>\n              );\n            }\n            export function loader() {\n              return {\n                // this lets the app hydrate properly, then reject the promise,\n                // which should throw on the initial render _and_ the error render,\n                // resulting in us bubbling to the default error boundary and skipping\n                // our Layout component entirely to avoid a loop\n                lazy: new Promise((r) => setTimeout(() => r(null), 1000)),\n              };\n            }\n            export default function Root() {\n              return <p>success</p>;\n            }\n          `,\n        },\n      },\n      ServerMode.Development,\n    );\n    appFixture = await createAppFixture(fixture, ServerMode.Development);\n    let app = new PlaywrightFixture(appFixture, page);\n    await app.goto(\"/\", false);\n    expect(await app.page.$(\"#layout\")).toBeDefined();\n    expect(await app.getHtml(\"#loading\")).toMatch(\"Loading...\");\n    await page.waitForSelector(\"h1\");\n    expect(await app.page.$(\"#layout\")).toBeNull();\n    expect(await app.getHtml(\"title\")).toMatch(\"Application Error\");\n    expect(await app.getHtml(\"h1\")).toMatch(\"Application Error\");\n\n    if (browserName === \"chromium\") {\n      expect(await app.getHtml(\"pre\")).toMatch(\n        \"TypeError: Cannot read properties of null\",\n      );\n    } else {\n      // Other browsers don't include the error message in the stack trace so just\n      // ensure we get the `<pre>` rendered\n      expect(await app.getHtml(\"pre\")).toMatch(\"color: red;\");\n    }\n\n    console.error = oldConsoleError;\n  });\n});\n"
  },
  {
    "path": "integration/route-collisions-test.ts",
    "content": "import { PassThrough } from \"node:stream\";\nimport { test, expect } from \"@playwright/test\";\n\nimport { createFixture, js } from \"./helpers/create-fixture.js\";\n\nlet ROOT_FILE_CONTENTS = js`\n  import { Outlet, Scripts } from \"react-router\";\n\n  export default function App() {\n    return (\n      <html lang=\"en\">\n        <body>\n          <Outlet />\n          <Scripts />\n        </body>\n      </html>\n    );\n  }\n`;\n\nlet LAYOUT_FILE_CONTENTS = js`\n  import { Outlet } from \"react-router\";\n\n  export default function Layout() {\n    return <Outlet />\n  }\n`;\n\nlet LEAF_FILE_CONTENTS = js`\n  export default function Foo() {\n    return <h1>Foo</h1>;\n  }\n`;\n\ntest.describe(\"build failures\", () => {\n  let originalConsoleLog = console.log;\n  let originalConsoleWarn = console.warn;\n  let originalConsoleError = console.error;\n\n  test.beforeAll(async () => {\n    console.log = () => {};\n    console.warn = () => {};\n    console.error = () => {};\n  });\n\n  test.afterAll(() => {\n    console.log = originalConsoleLog;\n    console.warn = originalConsoleWarn;\n    console.error = originalConsoleError;\n  });\n\n  async function setup(files: Record<string, string>) {\n    let buildStdio = new PassThrough();\n    let buildOutput: string;\n    await createFixture({\n      buildStdio,\n      files,\n    });\n    let chunks: Buffer[] = [];\n    buildOutput = await new Promise<string>((resolve, reject) => {\n      buildStdio.on(\"data\", (chunk) => chunks.push(Buffer.from(chunk)));\n      buildStdio.on(\"error\", (err) => reject(err));\n      buildStdio.on(\"end\", () =>\n        resolve(Buffer.concat(chunks).toString(\"utf8\")),\n      );\n    });\n    return buildOutput;\n  }\n\n  test(\"detects path collisions inside pathless layout routes\", async () => {\n    let buildOutput = await setup({\n      \"app/root.tsx\": ROOT_FILE_CONTENTS,\n      \"app/routes/foo.jsx\": LEAF_FILE_CONTENTS,\n      \"app/routes/_pathless.jsx\": LAYOUT_FILE_CONTENTS,\n      \"app/routes/_pathless.foo.jsx\": LEAF_FILE_CONTENTS,\n    });\n    expect(buildOutput).toContain(`⚠️ Route Path Collision: \"/foo\"`);\n    expect(buildOutput).toContain(`🟢 routes/_pathless.foo.jsx`);\n    expect(buildOutput).toContain(`⭕️️ routes/foo.jsx`);\n  });\n\n  test(\"detects path collisions across pathless layout routes\", async () => {\n    let buildOutput = await setup({\n      \"app/root.tsx\": ROOT_FILE_CONTENTS,\n      \"app/routes/_pathless.jsx\": LAYOUT_FILE_CONTENTS,\n      \"app/routes/_pathless.foo.jsx\": LEAF_FILE_CONTENTS,\n      \"app/routes/_pathless2.jsx\": LAYOUT_FILE_CONTENTS,\n      \"app/routes/_pathless2.foo.jsx\": LEAF_FILE_CONTENTS,\n    });\n    expect(buildOutput).toContain(`⚠️ Route Path Collision: \"/foo\"`);\n    expect(buildOutput).toContain(`🟢 routes/_pathless2.foo.jsx`);\n    expect(buildOutput).toContain(`⭕️️ routes/_pathless.foo.jsx`);\n  });\n\n  test(\"detects path collisions inside multiple pathless layout routes\", async () => {\n    let buildOutput = await setup({\n      \"app/root.tsx\": ROOT_FILE_CONTENTS,\n      \"app/routes/foo.jsx\": LEAF_FILE_CONTENTS,\n      \"app/routes/_pathless.jsx\": LAYOUT_FILE_CONTENTS,\n      \"app/routes/_pathless._again.jsx\": LAYOUT_FILE_CONTENTS,\n      \"app/routes/_pathless._again.foo.jsx\": LEAF_FILE_CONTENTS,\n    });\n    expect(buildOutput).toContain(`⚠️ Route Path Collision: \"/foo\"`);\n    expect(buildOutput).toContain(`🟢 routes/_pathless._again.foo.jsx`);\n    expect(buildOutput).toContain(`⭕️️ routes/foo.jsx`);\n  });\n\n  test(\"detects path collisions of index files inside pathless layouts\", async () => {\n    let buildOutput = await setup({\n      \"app/root.tsx\": ROOT_FILE_CONTENTS,\n      \"app/routes/_index.jsx\": LEAF_FILE_CONTENTS,\n      \"app/routes/_pathless.jsx\": LAYOUT_FILE_CONTENTS,\n      \"app/routes/_pathless._index.jsx\": LEAF_FILE_CONTENTS,\n    });\n    expect(buildOutput).toContain(`⚠️ Route Path Collision: \"/\"`);\n    expect(buildOutput).toContain(`🟢 routes/_pathless._index.jsx`);\n    expect(buildOutput).toContain(`⭕️️ routes/_index.jsx`);\n  });\n\n  test(\"detects path collisions of index files across multiple pathless layouts\", async () => {\n    let buildOutput = await setup({\n      \"app/root.tsx\": ROOT_FILE_CONTENTS,\n      \"app/routes/nested._pathless.jsx\": LAYOUT_FILE_CONTENTS,\n      \"app/routes/nested._pathless._index.jsx\": LEAF_FILE_CONTENTS,\n      \"app/routes/nested._oops.jsx\": LAYOUT_FILE_CONTENTS,\n      \"app/routes/nested._oops._index.jsx\": LEAF_FILE_CONTENTS,\n    });\n    expect(buildOutput).toContain(`⚠️ Route Path Collision: \"/nested\"`);\n    expect(buildOutput).toContain(`🟢 routes/nested._pathless._index.jsx`);\n    expect(buildOutput).toContain(`⭕️️ routes/nested._oops._index.jsx`);\n  });\n\n  test(\"detects path collisions of param routes inside pathless layouts\", async () => {\n    let buildOutput = await setup({\n      \"app/root.tsx\": ROOT_FILE_CONTENTS,\n      \"app/routes/$param.jsx\": LEAF_FILE_CONTENTS,\n      \"app/routes/_pathless.jsx\": LAYOUT_FILE_CONTENTS,\n      \"app/routes/_pathless.$param.jsx\": LEAF_FILE_CONTENTS,\n    });\n    expect(buildOutput).toContain(`⚠️ Route Path Collision: \"/:param\"`);\n    expect(buildOutput).toContain(`🟢 routes/_pathless.$param.jsx`);\n    expect(buildOutput).toContain(`⭕️️ routes/$param.jsx`);\n  });\n});\n"
  },
  {
    "path": "integration/route-config-test.ts",
    "content": "import fs from \"node:fs/promises\";\nimport path from \"node:path\";\nimport { expect } from \"@playwright/test\";\n\nimport {\n  type Files,\n  createProject,\n  build,\n  test,\n  viteConfig,\n  createEditor,\n  viteMajorTemplates,\n} from \"./helpers/vite.js\";\n\nconst js = String.raw;\n\ntest.describe(\"route config\", () => {\n  viteMajorTemplates.forEach(({ templateName, templateDisplayName }) => {\n    test.describe(templateDisplayName, () => {\n      test(\"fails the build if route config is missing\", async () => {\n        let cwd = await createProject({}, templateName);\n        await fs.rm(path.join(cwd, \"app/routes.ts\"));\n        let buildResult = build({ cwd });\n        expect(buildResult.status).toBe(1);\n        expect(buildResult.stderr.toString()).toContain(\n          'Route config file not found at \"app/routes.ts\"',\n        );\n      });\n\n      test(\"fails the build if route config is invalid\", async () => {\n        let cwd = await createProject(\n          { \"app/routes.ts\": `export default INVALID(` },\n          templateName,\n        );\n        let buildResult = build({ cwd });\n        expect(buildResult.status).toBe(1);\n        expect(buildResult.stderr.toString()).toContain(\n          'Route config in \"routes.ts\" is invalid.',\n        );\n      });\n\n      test(\"fails the dev process if route config is initially invalid\", async ({\n        dev,\n      }) => {\n        let files: Files = async ({ port }) => ({\n          \"vite.config.js\": await viteConfig.basic({ port }),\n          \"app/routes.ts\": `export default INVALID(`,\n        });\n        let devError: Error | undefined;\n        try {\n          await dev(files);\n        } catch (error: any) {\n          devError = error;\n        }\n        expect(devError?.toString()).toContain(\n          'Route config in \"routes.ts\" is invalid.',\n        );\n      });\n\n      test(\"supports correcting an invalid route config\", async ({\n        page,\n        dev,\n      }) => {\n        let files: Files = async ({ port }) => ({\n          \"vite.config.js\": await viteConfig.basic({ port }),\n          \"app/routes.ts\": js`\n            import { type RouteConfig, index } from \"@react-router/dev/routes\";\n\n            export default [\n              index(\"test-route-1.tsx\"),\n            ] satisfies RouteConfig;\n          `,\n          \"app/test-route-1.tsx\": `\n            export default () => <div data-test-route>Test route 1</div>\n          `,\n          \"app/test-route-2.tsx\": `\n            export default () => <div data-test-route>Test route 2</div>\n          `,\n        });\n        let { cwd, port } = await dev(files);\n\n        await page.goto(`http://localhost:${port}/`, {\n          waitUntil: \"networkidle\",\n        });\n        await expect(page.locator(\"[data-test-route]\")).toHaveText(\n          \"Test route 1\",\n        );\n\n        let edit = createEditor(cwd);\n\n        // Make config invalid\n        await edit(\"app/routes.ts\", (contents) => contents + \"INVALID\");\n\n        // Ensure dev server is still running with old config + HMR\n        await edit(\"app/test-route-1.tsx\", (contents) =>\n          contents.replace(\"Test route 1\", \"Test route 1 updated\"),\n        );\n        await expect(page.locator(\"[data-test-route]\")).toHaveText(\n          \"Test route 1 updated\",\n        );\n\n        // Fix config with new route\n        await edit(\"app/routes.ts\", (contents) =>\n          contents\n            .replace(\"INVALID\", \"\")\n            .replace(\"test-route-1\", \"test-route-2\"),\n        );\n\n        await expect(async () => {\n          // Reload to pick up new route for current path\n          await page.reload();\n          await expect(page.locator(\"[data-test-route]\")).toHaveText(\n            \"Test route 2\",\n          );\n        }).toPass();\n      });\n\n      test(\"supports correcting an invalid route config module graph\", async ({\n        page,\n        dev,\n      }) => {\n        let files: Files = async ({ port }) => ({\n          \"vite.config.js\": await viteConfig.basic({ port }),\n          \"app/routes.ts\": js`\n            export { default } from \"./actual-routes\";\n          `,\n          \"app/actual-routes.ts\": js`\n            import { type RouteConfig, index } from \"@react-router/dev/routes\";\n\n            export default [\n              index(\"test-route-1.tsx\"),\n            ] satisfies RouteConfig;\n          `,\n          \"app/test-route-1.tsx\": `\n            export default () => <div data-test-route>Test route 1</div>\n          `,\n          \"app/test-route-2.tsx\": `\n            export default () => <div data-test-route>Test route 2</div>\n          `,\n        });\n        let { cwd, port } = await dev(files);\n\n        await page.goto(`http://localhost:${port}/`, {\n          waitUntil: \"networkidle\",\n        });\n        await expect(page.locator(\"[data-test-route]\")).toHaveText(\n          \"Test route 1\",\n        );\n\n        let edit = createEditor(cwd);\n\n        // Make config invalid\n        await edit(\"app/actual-routes.ts\", (contents) => contents + \"INVALID\");\n\n        // Ensure dev server is still running with old config + HMR\n        await edit(\"app/test-route-1.tsx\", (contents) =>\n          contents.replace(\"Test route 1\", \"Test route 1 updated\"),\n        );\n        await expect(page.locator(\"[data-test-route]\")).toHaveText(\n          \"Test route 1 updated\",\n        );\n\n        // Fix config with new route\n        await edit(\"app/actual-routes.ts\", (contents) =>\n          contents\n            .replace(\"INVALID\", \"\")\n            .replace(\"test-route-1\", \"test-route-2\"),\n        );\n\n        await expect(async () => {\n          // Reload to pick up new route for current path\n          await page.reload();\n          await expect(page.locator(\"[data-test-route]\")).toHaveText(\n            \"Test route 2\",\n          );\n        }).toPass();\n      });\n\n      test(\"supports correcting a missing route config\", async ({\n        page,\n        dev,\n      }) => {\n        let files: Files = async ({ port }) => ({\n          \"vite.config.js\": await viteConfig.basic({ port }),\n          \"app/routes.ts\": js`\n            import { type RouteConfig, index } from \"@react-router/dev/routes\";\n\n            export default [\n              index(\"test-route-1.tsx\"),\n            ] satisfies RouteConfig;\n          `,\n          \"app/test-route-1.tsx\": js`\n            export default () => <div data-test-route>Test route 1</div>\n          `,\n          \"app/test-route-2.tsx\": js`\n            export default () => <div data-test-route>Test route 2</div>\n          `,\n        });\n        let { cwd, port } = await dev(files);\n\n        await page.goto(`http://localhost:${port}/`, {\n          waitUntil: \"networkidle\",\n        });\n        await expect(page.locator(\"[data-test-route]\")).toHaveText(\n          \"Test route 1\",\n        );\n\n        let edit = createEditor(cwd);\n\n        let INVALID_FILENAME = \"app/routes.ts.oops\";\n\n        // Rename config to make it missing\n        await fs.rename(\n          path.join(cwd, \"app/routes.ts\"),\n          path.join(cwd, INVALID_FILENAME),\n        );\n\n        // Ensure dev server is still running with old config + HMR\n        await edit(\"app/test-route-1.tsx\", (contents) =>\n          contents.replace(\"Test route 1\", \"Test route 1 updated\"),\n        );\n        await expect(page.locator(\"[data-test-route]\")).toHaveText(\n          \"Test route 1 updated\",\n        );\n\n        // Add new route\n        await edit(INVALID_FILENAME, (contents) =>\n          contents.replace(\"test-route-1\", \"test-route-2\"),\n        );\n\n        // Rename config to bring it back\n        await fs.rename(\n          path.join(cwd, INVALID_FILENAME),\n          path.join(cwd, \"app/routes.ts\"),\n        );\n\n        await expect(async () => {\n          // Reload to pick up new route for current path\n          await page.reload();\n          await expect(page.locator(\"[data-test-route]\")).toHaveText(\n            \"Test route 2\",\n          );\n        }).toPass();\n      });\n\n      test(\"supports absolute route file paths\", async ({ page, dev }) => {\n        let files: Files = async ({ port }) => ({\n          \"vite.config.js\": await viteConfig.basic({ port }),\n          \"app/routes.ts\": js`\n            import path from \"node:path\";\n            import { type RouteConfig, index } from \"@react-router/dev/routes\";\n\n            export default [\n              index(path.resolve(import.meta.dirname, \"test-route.tsx\")),\n            ] satisfies RouteConfig;\n          `,\n          \"app/test-route.tsx\": `\n            export default () => <div data-test-route>Test route</div>\n          `,\n        });\n        let { port } = await dev(files);\n\n        await page.goto(`http://localhost:${port}/`, {\n          waitUntil: \"networkidle\",\n        });\n        await expect(page.locator(\"[data-test-route]\")).toHaveText(\n          \"Test route\",\n        );\n      });\n    });\n  });\n});\n"
  },
  {
    "path": "integration/rsc/rsc-nojs-test.ts",
    "content": "import { test, expect } from \"@playwright/test\";\nimport getPort from \"get-port\";\n\nimport { implementations, js, setupRscTest, validateRSCHtml } from \"./utils\";\n\nimplementations.forEach((implementation) => {\n  test.describe(`RSC nojs (${implementation.name})`, () => {\n    let port: number;\n    let stop: () => void;\n\n    test.afterAll(() => {\n      stop?.();\n    });\n\n    test.beforeAll(async () => {\n      port = await getPort();\n      stop = await setupRscTest({\n        implementation,\n        port,\n        files: {\n          \"src/routes.ts\": js`\n            import type { unstable_RSCRouteConfig as RSCRouteConfig } from \"react-router\";\n\n            export const routes = [\n              {\n                id: \"root\",\n                path: \"\",\n                lazy: () => import(\"./routes/root\"),\n                children: [\n                  {\n                    id: \"home\",\n                    index: true,\n                    lazy: () => import(\"./routes/home\"),\n                  },\n                  {\n                    id: \"render-redirect-lazy\",\n                    path: \"/render-redirect/lazy/:id?\",\n                    lazy: () => import(\"./routes/render-redirect/lazy\"),\n                  },\n                  {\n                    id: \"render-redirect\",\n                    path: \"/render-redirect/:id?\",\n                    lazy: () => import(\"./routes/render-redirect/home\"),\n                  },\n                ],\n              },\n            ] satisfies RSCRouteConfig;\n          `,\n          \"src/routes/home.actions.ts\": js`\n            \"use server\";\n            import { redirect } from \"react-router\";\n\n            export async function redirectAction() {\n              redirect(\"/?redirected=true\", { headers: { \"x-test\": \"test\" } });\n              return \"redirected\";\n            }\n\n            export async function incrementAction(prev) {\n              return prev + 1;\n            }\n          `,\n          \"src/routes/home.client.tsx\": js`\n            \"use client\";\n            import { useState } from \"react\";\n\n            export function Counter() {\n              const [count, setCount] = useState(0);\n              return <button type=\"button\" onClick={() => setCount(c => c + 1)} data-count>Count: {count}</button>;\n            }\n          `,\n          \"src/routes/home.tsx\": js`\n            \"use client\";\n            import {useActionState} from \"react\";\n            import { redirectAction, incrementAction } from \"./home.actions\";\n            import { Counter } from \"./home.client\";\n\n            export default function HomeRoute(props) {\n              const [state, action] = useActionState(redirectAction, null);\n              return (\n                <div>\n                  <form action={action}>\n                    <button type=\"submit\" data-submit>\n                      Redirect via Server Function\n                    </button>\n                  </form>\n                  {state && <div data-testid=\"state\">{state}</div>}\n                  <Counter />\n                  <TestActionState />\n                </div>\n              );\n            }\n\n            function TestActionState() {\n              const [state, action] = useActionState(incrementAction, 0);\n              return (\n                <form action={action}>\n                  <button type=\"submit\" data-action-state-increment-submit>\n                    action-state-increment\n                  </button>\n                  <div data-action-state-increment-result>{state}</div>\n                </form>\n              );\n            }\n          `,\n\n          \"src/routes/render-redirect/home.tsx\": js`\n            import { Link, redirect } from \"react-router\";\n\n            export default function RenderRedirect({ params: { id } }) {\n              if (id === \"redirect\") {\n                throw redirect(\"/render-redirect/redirected\");\n              }\n\n              if (id === \"external\") {\n                throw redirect(\"https://example.com/\");\n              }\n\n              return (\n                <>\n                  <h1>{id || \"home\"}</h1>\n                  <Link to=\"/render-redirect/redirect\">Redirect</Link>\n                  <Link to=\"/render-redirect/external\">External</Link>\n                </>\n              )\n            }\n          `,\n          \"src/routes/render-redirect/lazy.tsx\": js`\n            import { Suspense } from \"react\";\n            import { Link, redirect } from \"react-router\";\n\n            export default function RenderRedirect({ params: { id } }) {\n              return (\n                <Suspense fallback={<p>Loading...</p>}>\n                  <Lazy id={id} />\n                </Suspense>\n              );\n            }\n\n            async function Lazy({ id }) {\n              await new Promise((r) => setTimeout(r, 0));\n\n              if (id === \"redirect\") {\n                throw redirect(\"/render-redirect/lazy/redirected\");\n              }\n\n              if (id === \"external\") {\n                throw redirect(\"https://example.com/\");\n              }\n\n              return (\n                <>\n                  <h1>{id || \"home\"}</h1>\n                  <Link to=\"/render-redirect/lazy/redirect\">Redirect</Link>\n                  <Link to=\"/render-redirect/external\">External</Link>\n                </>\n              );\n            }\n          `,\n        },\n      });\n    });\n\n    test(\"Supports React Server Functions side-effect redirect headers for document requests\", async ({\n      page,\n    }, { project }) => {\n      test.skip(project.name !== \"chromium\");\n\n      await page.goto(`http://localhost:${port}/`);\n\n      const responseHeadersPromise = new Promise<Record<string, string>>(\n        (resolve) => {\n          page.addListener(\"response\", (response) => {\n            if (response.request().method() === \"POST\") {\n              resolve(response.headers());\n            }\n          });\n        },\n      );\n\n      await page.click(\"[data-submit]\");\n\n      await page.waitForURL(`http://localhost:${port}/?redirected=true`);\n\n      expect((await responseHeadersPromise)[\"x-test\"]).toBe(\"test\");\n\n      // Ensure this is using RSC\n      validateRSCHtml(await page.content());\n    });\n\n    test(\"Supports form state without JS\", async ({ page }, { project }) => {\n      test.skip(project.name !== \"chromium\");\n\n      await page.goto(`http://localhost:${port}/`);\n\n      await expect(\n        page.locator(\"[data-action-state-increment-result]\"),\n      ).toHaveText(\"0\");\n      await page.click(\"[data-action-state-increment-submit]\");\n      await expect(\n        page.locator(\"[data-action-state-increment-result]\"),\n      ).toHaveText(\"1\");\n\n      // Ensure this is using RSC\n      validateRSCHtml(await page.content());\n    });\n\n    test(\"Suppport throwing redirect Response from render\", async ({\n      page,\n    }) => {\n      await page.goto(`http://localhost:${port}/render-redirect`);\n      await expect(page.getByText(\"home\")).toBeAttached();\n      await page.getByText(\"Redirect\").click();\n      await page.waitForURL(\n        `http://localhost:${port}/render-redirect/redirected`,\n      );\n      await expect(page.getByText(\"redirected\")).toBeAttached();\n    });\n\n    test(\"Suppport throwing external redirect Response from render\", async ({\n      browserName,\n      page,\n    }) => {\n      test.skip(\n        browserName === \"firefox\",\n        \"Playwright doesn't like external redirects for tests. It times out waiting for the URL even though it navigates.\",\n      );\n      await page.goto(`http://localhost:${port}/render-redirect`);\n      await expect(page.getByText(\"home\")).toBeAttached();\n      await page.getByText(\"External\").click();\n      await page.waitForURL(`https://example.com/`);\n      await expect(page.getByText(\"Example Domain\")).toBeAttached();\n    });\n\n    test(\"Suppport throwing redirect Response from suspended render\", async ({\n      page,\n    }) => {\n      await page.goto(`http://localhost:${port}/render-redirect/lazy/redirect`);\n      await page.waitForURL(\n        `http://localhost:${port}/render-redirect/lazy/redirected`,\n      );\n      await expect(page.getByText(\"redirected\")).toBeAttached();\n    });\n\n    test(\"Suppport throwing external redirect Response from suspended render\", async ({\n      page,\n      browserName,\n    }) => {\n      test.skip(\n        browserName === \"firefox\",\n        \"Playwright doesn't like external redirects for tests. It times out waiting for the URL even though it navigates.\",\n      );\n      await page.goto(`http://localhost:${port}/render-redirect/lazy/external`);\n      await page.waitForURL(`https://example.com/`);\n      await expect(page.getByText(\"Example Domain\")).toBeAttached();\n    });\n  });\n});\n"
  },
  {
    "path": "integration/rsc/rsc-test.ts",
    "content": "import {\n  test,\n  expect,\n  type Response as PlaywrightResponse,\n} from \"@playwright/test\";\nimport getPort from \"get-port\";\n\nimport { implementations, js, setupRscTest, validateRSCHtml } from \"./utils\";\n\nimplementations.forEach((implementation) => {\n  test.describe(`RSC (${implementation.name})`, () => {\n    test.describe(\"Development\", () => {\n      let port: number;\n      let stopAfterAll: () => void;\n\n      test.afterAll(() => {\n        stopAfterAll?.();\n      });\n\n      test.beforeAll(async () => {\n        port = await getPort();\n        stopAfterAll = await setupRscTest({\n          implementation,\n          port,\n          dev: true,\n          files: {\n            \"src/routes.ts\": js`\n              import type { unstable_RSCRouteConfig as RSCRouteConfig } from \"react-router\";\n\n              export const routes = [\n                {\n                  id: \"root\",\n                  path: \"\",\n                  lazy: () => import(\"./routes/root\"),\n                  children: [\n                    {\n                      id: \"loader-error-server-boundary\",\n                      path: \"loader-error-server-boundary\",\n                      lazy: () => import(\"./routes/loader-error-server-boundary/home\"),\n                    },\n                    {\n                      id: \"errors-force-revalidation\",\n                      path: \"errors-force-revalidation\",\n                      lazy: () => import(\"./routes/errors-force-revalidation/root\"),\n                      children: [\n                        {\n                          id: \"errors-force-revalidation.index\",\n                          index: true,\n                          lazy: () => import(\"./routes/errors-force-revalidation/index\"),\n                        },\n                        {\n                          id: \"errors-force-revalidation.other\",\n                          path: \"other\",\n                          lazy: () => import(\"./routes/errors-force-revalidation/other\"),\n                        },\n                      ],\n                    },\n                    {\n                      id: \"client-error-boundary-props-server-loader\",\n                      path: \"client-error-boundary-props-server-loader\",\n                      lazy: () => import(\"./routes/client-error-boundary-props-server-loader/home\"),\n                    },\n                  ],\n                },\n              ] satisfies RSCRouteConfig;\n            `,\n\n            \"src/routes/loader-error-server-boundary/home.tsx\": js`\n              export function loader() {\n                throw new Error(\"Intentional error from loader\");\n              }\n\n              export default function HomeRoute() {\n                return <h2>This should not be rendered</h2>;\n              }\n\n              export { ErrorBoundary } from \"./home.client\";\n            `,\n            \"src/routes/loader-error-server-boundary/home.client.tsx\": js`\n              \"use client\"\n              import { useRouteError } from \"react-router\";\n\n              export function ErrorBoundary() {\n                let error = useRouteError();\n                return (\n                  <>\n                    <h2 data-error-title>Error Caught!</h2>\n                    <p data-error-message>{error.message}</p>\n                  </>\n                );\n              }\n            `,\n\n            \"src/routes/errors-force-revalidation/root.tsx\": js`\n              import { Outlet, Link } from \"react-router\";\n\n              export { shouldRevalidate } from \"./root.client\";\n\n              let loaderCallCount = 0;\n\n              export function loader() {\n                loaderCallCount++;\n                throw new Error(\"Root loader error (call #\" + loaderCallCount + \")\");\n              }\n\n              export function Layout({ children }: { children: React.ReactNode }) {\n                return (\n                  <html>\n                    <body>\n                      <ul>\n                        <li><Link to=\"/errors-force-revalidation\" data-link-index>Index route</Link></li>\n                        <li><Link to=\"/errors-force-revalidation/other\" data-link-other>Other route</Link></li>\n                      </ul>\n                      {children}\n                    </body>\n                  </html>\n                );\n              }\n\n              export function ErrorBoundary({ error }) {\n                return (\n                  <div>\n                    <h1 data-error-boundary-loader-call-count={loaderCallCount}>\n                      Root ErrorBoundary (loaderCallCount: {loaderCallCount})\n                    </h1>\n                  </div>\n                );\n              }\n\n              export default function RootRoute() {\n                return (\n                  <div>\n                    <h1>Root Route</h1>\n                    <p>This should never be rendered since the root loader always throws</p>\n                    <Outlet />\n                  </div>\n                );\n              }\n            `,\n            \"src/routes/errors-force-revalidation/root.client.tsx\": js`\n              \"use client\";\n\n              export function shouldRevalidate() {\n                // This should be ignored since this route always throws an error\n                return false;\n              }\n            `,\n            \"src/routes/errors-force-revalidation/index.tsx\": js`\n              export default function IndexRoute() {\n                return (\n                  <div>\n                    <h2>Index Route</h2>\n                    <p>This should never be rendered since the root loader always throws</p>\n                  </div>\n                );\n              }\n            `,\n            \"src/routes/errors-force-revalidation/other.tsx\": js`\n              export default function OtherRoute() {\n                return (\n                  <div>\n                    <h2>Other Route</h2>\n                    <p>This should never be rendered since the root loader always throws</p>\n                  </div>\n                );\n              }\n            `,\n\n            \"src/routes/client-error-boundary-props-server-loader/home.tsx\": js`\n              export function loader() {\n                throw new Error(\"Intentional error from server loader\");\n              }\n\n              export default function HomeRoute() {\n                return <h2>This should not be rendered</h2>;\n              }\n\n              export { ErrorBoundary } from \"./home.client\";\n            `,\n            \"src/routes/client-error-boundary-props-server-loader/home.client.tsx\": js`\n              \"use client\";\n\n              export function ErrorBoundary({ error, params }) {\n                return (\n                  <div>\n                    <h2 data-error-title>Error Caught!</h2>\n                    <p data-error-message>{error.message}</p>\n                    {params && (\n                      <div data-error-params>\n                        <p data-error-params-type>typeof params: {typeof params}</p>\n                        <p data-error-params-count>params count: {Object.keys(params).length}</p>\n                      </div>\n                    )}\n                  </div>\n                );\n              }\n            `,\n          },\n        });\n      });\n\n      test.describe(\"Errors\", () => {\n        test(\"Handles errors from loaders with server component boundary\", async ({\n          page,\n        }) => {\n          await page.goto(\n            `http://localhost:${port}/loader-error-server-boundary`,\n          );\n\n          // Verify error boundary is shown\n          await page.waitForSelector(\"[data-error-title]\");\n          await page.waitForSelector(\"[data-error-message]\");\n          expect(await page.locator(\"[data-error-message]\").textContent()).toBe(\n            \"Intentional error from loader\",\n          );\n\n          // Ensure this is using RSC\n          validateRSCHtml(await page.content());\n        });\n\n        test(\"Forces revalidation of routes with errors\", async ({ page }) => {\n          await page.goto(\n            `http://localhost:${port}/errors-force-revalidation`,\n            {\n              waitUntil: \"networkidle\",\n            },\n          );\n\n          // Verify that the root error boundary is re-rendered as we navigate around\n          await page.waitForSelector(\n            \"[data-error-boundary-loader-call-count='1']\",\n          );\n          await page.click(\"[data-link-other]\");\n          await page.waitForSelector(\n            \"[data-error-boundary-loader-call-count='2']\",\n          );\n          await page.click(\"[data-link-index]\");\n          await page.waitForSelector(\n            \"[data-error-boundary-loader-call-count='3']\",\n          );\n\n          // Ensure this is using RSC\n          validateRSCHtml(await page.content());\n        });\n\n        test(\"Passes props to client ErrorBoundary when error is thrown in server loader\", async ({\n          page,\n        }) => {\n          await page.goto(\n            `http://localhost:${port}/client-error-boundary-props-server-loader`,\n          );\n\n          // Verify error boundary is shown\n          await page.waitForSelector(\"[data-error-title]\");\n          await page.waitForSelector(\"[data-error-message]\");\n          expect(await page.locator(\"[data-error-title]\").textContent()).toBe(\n            \"Error Caught!\",\n          );\n          expect(await page.locator(\"[data-error-message]\").textContent()).toBe(\n            \"Intentional error from server loader\",\n          );\n\n          // Verify params are passed to error boundary\n          await page.waitForSelector(\"[data-error-params]\");\n          await page.waitForSelector(\"[data-error-params-type]\");\n          await page.waitForSelector(\"[data-error-params-count]\");\n          expect(\n            await page.locator(\"[data-error-params-type]\").textContent(),\n          ).toBe(\"typeof params: object\");\n          expect(\n            await page.locator(\"[data-error-params-count]\").textContent(),\n          ).toBe(\"params count: 0\");\n\n          // Ensure this is using RSC\n          validateRSCHtml(await page.content());\n        });\n      });\n    });\n\n    test.describe(\"Production\", () => {\n      let port: number;\n      let stopAfterAll: () => void;\n\n      test.afterAll(() => {\n        stopAfterAll?.();\n      });\n\n      test.beforeAll(async () => {\n        port = await getPort();\n        stopAfterAll = await setupRscTest({\n          implementation,\n          port,\n          files: {\n            \"src/routes.ts\": js`\n              import type { unstable_RSCRouteConfig as RSCRouteConfig } from \"react-router\";\n\n              export const routes = [\n                {\n                  id: \"root\",\n                  path: \"\",\n                  lazy: () => import(\"./routes/root\"),\n                  children: [\n                    {\n                      id: \"soft-navigation\",\n                      path: \"soft-navigation\",\n                      children: [\n                        {\n                          id: \"soft-navigation.home\",\n                          index: true,\n                          lazy: () => import(\"./routes/soft-navigation/home\"),\n                        },\n                        {\n                          id: \"soft-navigation.dashboard\",\n                          path: \"dashboard\",\n                          lazy: () => import(\"./routes/soft-navigation/dashboard\"),\n                        },\n                      ]\n                    },\n                    {\n                      id: \"request-context\",\n                      path: \"request-context\",\n                      lazy: () => import(\"./routes/request-context/home\"),\n                    },\n                    {\n                      id: \"resource-request-context\",\n                      path: \"resource-request-context\",\n                      children: [\n                        {\n                          id: \"resource-request-context.home\",\n                          index: true,\n                          lazy: () => import(\"./routes/resource-request-context/home\"),\n                        },\n                        {\n                          id: \"resource-request-context.resource\",\n                          path: \"resource\",\n                          lazy: () => import(\"./routes/resource-request-context/resource\"),\n                        },\n                      ]\n                    },\n                    {\n                      id: \"middleware-request-context\",\n                      path: \"middleware-request-context\",\n                      lazy: () => import(\"./routes/middleware-request-context/root\"),\n                      children: [\n                        {\n                          id: \"middleware-request-context.home\",\n                          index: true,\n                          lazy: () => import(\"./routes/middleware-request-context/home\"),\n                        },\n                      ]\n                    },\n                    {\n                      id: \"get-context\",\n                      path: \"get-context\",\n                      lazy: () => import(\"./routes/get-context/root\"),\n                      children: [\n                        {\n                          id: \"get-context.home\",\n                          index: true,\n                          lazy: () => import(\"./routes/get-context/home\"),\n                        },\n                      ]\n                    },\n                    {\n                      id: \"resource-url-and-fetchers\",\n                      path: \"resource-url-and-fetchers\",\n                      children: [\n                        {\n                          id: \"resource-url-and-fetchers.home\",\n                          index: true,\n                          lazy: () => import(\"./routes/resource-url-and-fetchers/home\"),\n                        },\n                        {\n                          id: \"resource-url-and-fetchers.resource\",\n                          path: \"resource\",\n                          lazy: () => import(\"./routes/resource-url-and-fetchers/resource\"),\n                        }\n                      ]\n                    },\n                    {\n                      id: \"resource-error-handling\",\n                      path: \"resource-error-handling\",\n                      children: [\n                        {\n                          id: \"resource-error-handling.home\",\n                          index: true,\n                          lazy: () => import(\"./routes/resource-error-handling/home\"),\n                        },\n                        {\n                          id: \"resource-error-handling.no-loader-resource\",\n                          path: \"no-loader-resource\",\n                          lazy: () => import(\"./routes/resource-error-handling/no-loader-resource\"),\n                        },\n                        {\n                          id: \"resource-error-handling.no-action-resource\",\n                          path: \"no-action-resource\",\n                          lazy: () => import(\"./routes/resource-error-handling/no-action-resource\"),\n                        }\n                      ]\n                    },\n                    {\n                      id: \"server-action\",\n                      path: \"server-action\",\n                      children: [\n                        {\n                          id: \"server-action.home\",\n                          index: true,\n                          lazy: () => import(\"./routes/server-action/home\"),\n                        },\n                      ]\n                    },\n                    {\n                      id: \"inline-server-action\",\n                      path: \"inline-server-action\",\n                      children: [\n                        {\n                          id: \"inline-server-action.home\",\n                          index: true,\n                          lazy: () => import(\"./routes/inline-server-action/home\"),\n                        }\n                      ]\n                    },\n                    {\n                      id: \"throw-redirect-server-action\",\n                      path: \"throw-redirect-server-action\",\n                      children: [\n                        {\n                          id: \"throw-redirect-server-action.home\",\n                          index: true,\n                          lazy: () => import(\"./routes/throw-redirect-server-action/home\"),\n                        }\n                      ]\n                    },\n                    {\n                      id: \"throw-external-redirect-server-action\",\n                      path: \"throw-external-redirect-server-action\",\n                      children: [\n                        {\n                          id: \"throw-external-redirect-server-action.home\",\n                          index: true,\n                          lazy: () => import(\"./routes/throw-external-redirect-server-action/home\"),\n                        }\n                      ]\n                    },\n                    {\n                      id: \"side-effect-redirect-server-action\",\n                      path: \"side-effect-redirect-server-action\",\n                      children: [\n                        {\n                          id: \"side-effect-redirect-server-action.home\",\n                          index: true,\n                          lazy: () => import(\"./routes/side-effect-redirect-server-action/home\"),\n                        }\n                      ]\n                    },\n                    {\n                      id: \"side-effect-external-redirect-server-action\",\n                      path: \"side-effect-external-redirect-server-action\",\n                      children: [\n                        {\n                          id: \"side-effect-external-redirect-server-action.home\",\n                          index: true,\n                          lazy: () => import(\"./routes/side-effect-external-redirect-server-action/home\"),\n                        }\n                      ]\n                    },\n                    {\n                      id: \"server-function-reference\",\n                      path: \"server-function-reference\",\n                      children: [\n                        {\n                          id: \"server-function-reference.home\",\n                          index: true,\n                          lazy: () => import(\"./routes/server-function-reference/home\"),\n                        }\n                      ]\n                    },\n                    {\n                      id: \"sanitized-errors\",\n                      path: \"sanitized-errors\",\n                      lazy: () => import(\"./routes/sanitized-errors/home\"),\n                    },\n                    {\n                      id: \"client-route-component-props\",\n                      path: \"client-route-component-props\",\n                      lazy: () => import(\"./routes/client-route-component-props/home\"),\n                    },\n                    {\n                      id: \"client-error-boundary-props-client-loader\",\n                      path: \"client-error-boundary-props-client-loader\",\n                      lazy: () => import(\"./routes/client-error-boundary-props-client-loader/home\"),\n                    },\n                    {\n                      id: \"hydrate-fallback-props\",\n                      path: \"hydrate-fallback-props\",\n                      lazy: () => import(\"./routes/hydrate-fallback-props/home\"),\n                    },\n                    {\n                      id: \"no-revalidate-server-action\",\n                      path: \"no-revalidate-server-action\",\n                      lazy: () => import(\"./routes/no-revalidate-server-action/home\"),\n                    },\n                    {\n                      id: \"await-component\",\n                      path: \"await-component\",\n                      children: [\n                        {\n                          id: \"await-component.home\",\n                          index: true,\n                          lazy: () => import(\"./routes/await-component/home\"),\n                        },\n                        {\n                          id: \"await-component.reject\",\n                          path: \"reject\",\n                          lazy: () => import(\"./routes/await-component/reject\"),\n                        },\n                        {\n                          id: \"await-component.api\",\n                          path: \"api\",\n                          lazy: () => import(\"./routes/await-component/api\"),\n                        }\n                      ]\n                    },\n                    {\n                      id: \"ssr-error\",\n                      path: \"ssr-error\",\n                      lazy: () => import(\"./routes/ssr-error/ssr-error\"),\n                    },\n                    {\n                      id: \"action-transition-state\",\n                      path: \"action-transition-state\",\n                      lazy: () => import(\"./routes/action-transition-state/home\"),\n                    },\n                    {\n                      id: \"render-redirect-lazy\",\n                      path: \"/render-redirect/lazy/:id?\",\n                      lazy: () => import(\"./routes/render-redirect/lazy\"),\n                    },\n                    {\n                      id: \"render-redirect\",\n                      path: \"/render-redirect/:id?\",\n                      lazy: () => import(\"./routes/render-redirect/home\"),\n                    },\n                    {\n                      id: \"render-route-error-response\",\n                      path: \"render-route-error-response/:id?\",\n                      lazy: () => import(\"./routes/render-route-error-response/home\"),\n                    },\n                    {\n                      id: \"get-request\",\n                      path: \"/get-request\",\n                      lazy: () => import(\"./routes/get-request/get-request\"),\n                    },\n                  ],\n                },\n              ] satisfies RSCRouteConfig;\n            `,\n\n            \"src/routes/root.tsx\": js`\n              import { Links, Outlet, ScrollRestoration } from \"react-router\";\n\n              export const middleware = [\n                async (_, next) => {\n                  const response = await next();\n                  return response.headers.set(\"x-test\", \"test\");\n                }\n              ];\n\n              export function Layout({ children }: { children: React.ReactNode }) {\n                return (\n                  <html lang=\"en\">\n                    <head>\n                      <meta charSet=\"utf-8\" />\n                      <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />\n                      <title>Vite (RSC)</title>\n                      <Links />\n                    </head>\n                    <body>\n                      {children}\n                      <ScrollRestoration />\n                    </body>\n                  </html>\n                );\n              }\n\n              export default function RootRoute() {\n                return <Outlet />;\n              }\n            `,\n\n            \"src/config/request-context.ts\": js`\n              import { createContext, RouterContextProvider } from \"react-router\";\n\n              export const testContext = createContext<string>(\"default-value\");\n\n              export const requestContext = new RouterContextProvider(\n                new Map([[testContext, \"test-context-value\"]])\n              );\n            `,\n            \"src/config/get-context.ts\": js`\n              // THIS FILE OVERRIDES THE DEFAULT IMPLEMENTATION\n              import { createContext } from \"react-router\";\n\n              export const testContext = createContext<string>(\"default-value\");\n\n              export function getContext() {\n                return new Map([[testContext, \"client-context-value\"]]);\n              }\n            `,\n\n            \"src/routes/soft-navigation/home.tsx\": js`\n              import { Link } from \"react-router\";\n\n              export function loader() {\n                return { message: \"Home Page Data\" };\n              }\n\n              export default function HomeRoute({ loaderData }) {\n                return (\n                  <div>\n                    <h1 data-page=\"home\">Home Page</h1>\n                    <p data-content>{loaderData.message}</p>\n                    <Link to=\"/soft-navigation/dashboard\">Dashboard</Link>\n                  </div>\n                );\n              }\n            `,\n            \"src/routes/soft-navigation/dashboard.tsx\": js`\n              export function loader() {\n                return { count: 1 };\n              }\n\n              export { default } from \"./dashboard.client\";\n            `,\n            \"src/routes/soft-navigation/dashboard.client.tsx\": js`\n              \"use client\";\n\n              import { useState } from \"react\";\n              import { Link } from \"react-router\";\n\n              // Export the entire route as a client component\n              export default function DashboardRoute({ loaderData }) {\n                const [count, setCount] = useState(loaderData.count);\n\n                return (\n                  <div>\n                    <h1 data-page=\"dashboard\">Dashboard</h1>\n\n                    {/* Server data rendered in client component */}\n                    <p data-server-count>\n                      Server count: {loaderData.count}\n                    </p>\n\n                    {/* Client interactive elements */}\n                    <p data-client-count>\n                      Client count: {count}\n                    </p>\n\n                    <button data-increment onClick={() => setCount(count + 1)}>\n                      Increment\n                    </button>\n\n                    <Link to=\"/soft-navigation\">Home</Link>\n                  </div>\n                );\n              }\n            `,\n\n            \"src/routes/request-context/home.tsx\": js`\n              import { testContext } from \"../../config/request-context\";\n\n              export function loader({ context }) {\n                return { contextValue: context.get(testContext) };\n              }\n\n              export default function HomeRoute({ loaderData }) {\n                return (\n                  <div>\n                    <h2 data-home>Home: {loaderData.contextValue}</h2>\n                  </div>\n                );\n              }\n            `,\n\n            \"src/routes/resource-request-context/home.client.tsx\": js`\n              \"use client\";\n\n              import { useFetcher } from \"react-router\";\n\n              export function ResourceFetcher() {\n                const fetcher = useFetcher();\n\n                const loadResource = () => {\n                  fetcher.submit({ hello: \"world\" }, { method: \"post\", action: \"/resource-request-context/resource\" });\n                };\n\n                return (\n                  <div>\n                    <button type=\"button\" onClick={loadResource}>\n                      Load Resource\n                    </button>\n                    {!!fetcher.data && (\n                      <pre data-testid=\"resource-data\">\n                        {JSON.stringify(fetcher.data)}\n                      </pre>\n                    )}\n                  </div>\n                );\n              }\n            `,\n            \"src/routes/resource-request-context/home.tsx\": js`\n              import { ResourceFetcher } from \"./home.client\";\n\n              export default function HomeRoute() {\n                return <ResourceFetcher />;\n              }\n            `,\n            \"src/routes/resource-request-context/resource.tsx\": js`\n              import { testContext } from \"../../config/request-context\";\n\n              export function loader({ context }) {\n                return Response.json({\n                  message: \"Hello from resource route!\",\n                  contextValue: context.get(testContext)\n                });\n              }\n\n              export async function action({ context }) {\n                return Response.json({\n                  message: \"Hello from resource route!\",\n                  contextValue: context.get(testContext),\n                });\n              }\n            `,\n\n            \"src/routes/middleware-request-context/root.tsx\": js`\n              import type { MiddlewareFunction } from \"react-router\";\n              import { Outlet } from \"react-router\";\n              import { testContext } from \"../../config/request-context\";\n\n              export const middleware: MiddlewareFunction<Response>[] = [\n                async ({ request, context }, next) => {\n                  const contextValue = context.get(testContext);\n                  request.headers.set(\"x-middleware-context\", contextValue);\n                  return await next();\n                },\n              ];\n\n              export default function RootRoute() {\n                return (\n                  <div>\n                    <h1>Root Route</h1>\n                    <Outlet />\n                  </div>\n                );\n              }\n            `,\n            \"src/routes/middleware-request-context/home.tsx\": js`\n              export function loader({ request }) {\n                const contextValue = request.headers.get(\"x-middleware-context\");\n                return { contextValue };\n              }\n\n              export default function HomeRoute({ loaderData }) {\n                return (\n                  <div>\n                    <h2 data-context-value>Context value: {loaderData.contextValue}</h2>\n                  </div>\n                );\n              }\n            `,\n\n            \"src/routes/get-context/root.tsx\": js`\n              \"use client\";\n\n              import { Outlet } from \"react-router\";\n              import type { ClientMiddlewareFunction } from \"react-router\";\n              import { testContext } from \"../../config/get-context\";\n\n              export const clientMiddleware = [\n                async ({ context }, next) => {\n                  context.set(testContext, \"client-context-value\");\n                  return await next();\n                },\n              ];\n\n              export function HydrateFallback() {\n                return <div>Loading...</div>;\n              }\n\n              export default function RootRoute() {\n                return (\n                  <div>\n                    <h1>Root Route</h1>\n                    <Outlet />\n                  </div>\n                );\n              }\n            `,\n            \"src/routes/get-context/home.tsx\": js`\n              \"use client\";\n\n              import { useLoaderData } from \"react-router\";\n              import { testContext } from \"../../config/get-context\";\n\n              export function clientLoader({ context }) {\n                const contextValue = context.get(testContext);\n                return { contextValue };\n              }\n\n              clientLoader.hydrate = true;\n\n              export default function HomeRoute() {\n                const loaderData = useLoaderData();\n                return (\n                  <div>\n                    <h2 data-client-context>Client context value: {loaderData.contextValue}</h2>\n                  </div>\n                );\n              }\n            `,\n\n            \"src/routes/resource-url-and-fetchers/resource.ts\": js`\n              export function loader() {\n                return Response.json({ message: \"Hello from resource route!\" });\n              }\n\n              export async function action({ request }) {\n                return {\n                  message: \"Hello from resource route!\",\n                  echo: await request.text(),\n                };\n              }\n            `,\n            \"src/routes/resource-url-and-fetchers/home.client.tsx\": js`\n              \"use client\";\n\n              import { useFetcher } from \"react-router\";\n\n              export function ResourceFetcher() {\n                const fetcher = useFetcher();\n\n                const loadResource = () => {\n                  fetcher.submit({ hello: \"world\" }, { method: \"post\", action: \"/resource-url-and-fetchers/resource\" });\n                };\n\n                return (\n                  <div>\n                    <button type=\"button\" onClick={loadResource}>\n                      Load Resource\n                    </button>\n                    {!!fetcher.data && (\n                      <pre data-testid=\"resource-data\">\n                        {JSON.stringify(fetcher.data)}\n                      </pre>\n                    )}\n                  </div>\n                );\n              }\n            `,\n            \"src/routes/resource-url-and-fetchers/home.tsx\": js`\n              import { ResourceFetcher } from \"./home.client\";\n\n              export default function HomeRoute() {\n                return <ResourceFetcher />;\n              }\n            `,\n\n            \"src/routes/resource-error-handling/home.tsx\": js`\n              export default function HomeRoute() {\n                return (\n                  <div>\n                    <h2 data-home>Home Route</h2>\n                  </div>\n                );\n              }\n            `,\n            \"src/routes/resource-error-handling/no-loader-resource.tsx\": js`\n              // This resource route has no loader, so GET requests should fail\n              export async function action() {\n                return { message: \"no-loader-resource action works\" };\n              }\n            `,\n            \"src/routes/resource-error-handling/no-action-resource.tsx\": js`\n              // This resource route has no action, so POST requests should fail\n              export function loader() {\n                return { message: \"no-action-resource loader works\" };\n              }\n            `,\n\n            \"src/routes/server-action/home.actions.ts\": js`\n              \"use server\";\n\n              export async function incrementCounter(count: number, formData: FormData) {\n                return count + parseInt(formData.get(\"by\") as string || \"1\", 10);\n              }\n            `,\n            \"src/routes/server-action/home.tsx\": js`\n              export { default } from \"./home.client\";\n            `,\n            \"src/routes/server-action/home.client.tsx\": js`\n              \"use client\";\n\n              import { useActionState } from \"react\";\n\n              import { incrementCounter } from \"./home.actions\";\n\n              export default function HomeRoute() {\n                const [count, incrementCounterAction, incrementing] = useActionState(incrementCounter, 0);\n\n                return (\n                  <div>\n                    <h2 data-home>Home: ({count})</h2>\n                    <form action={incrementCounterAction}>\n                      <input type=\"hidden\" name=\"name\" value=\"Updated\" />\n                      <button type=\"submit\" data-submit>\n                        {incrementing ? \"Updating via Server Function\" : \"Update via Server Function\"}\n                      </button>\n                    </form>\n                  </div>\n                );\n              }\n            `,\n\n            \"src/routes/inline-server-action/home.tsx\": js`\n              let count = 0;\n              let name = \"Default\";\n\n              export function loader() {\n                return { name, count };\n              }\n\n              export default function HomeRoute({ loaderData }) {\n                const updateCounter = async (formData: FormData) => {\n                  \"use server\";\n                  name = formData.get(\"name\");\n                  ++count\n                  return { name, count };\n                }\n\n                return (\n                  <div>\n                    <h2 data-home>Home: {loaderData.name} ({loaderData.count})</h2>\n                    <form action={updateCounter}>\n                      <input type=\"hidden\" name=\"name\" value=\"Updated\" />\n                      <button type=\"submit\" data-submit>Update via Server Function</button>\n                    </form>\n                  </div>\n                );\n              }\n            `,\n\n            \"src/routes/throw-redirect-server-action/home.actions.ts\": js`\n              \"use server\";\n              import { redirect } from \"react-router\";\n\n              export async function redirectAction(formData: FormData) {\n                throw redirect(\"/throw-redirect-server-action?redirected=true\");\n              }\n            `,\n            \"src/routes/throw-redirect-server-action/home.client.tsx\": js`\n              \"use client\";\n              import { useState } from \"react\";\n\n              export function Counter() {\n                const [count, setCount] = useState(0);\n                return <button type=\"button\" onClick={() => setCount(c => c + 1)} data-count>Count: {count}</button>;\n              }\n            `,\n            \"src/routes/throw-redirect-server-action/home.tsx\": js`\n              import { redirectAction } from \"./home.actions\";\n              import { Counter } from \"./home.client\";\n\n              export default function HomeRoute(props) {\n                return (\n                  <div>\n                    <form action={redirectAction}>\n                      <button type=\"submit\" data-submit>\n                        Redirect via Server Function\n                      </button>\n                    </form>\n                    <Counter />\n                  </div>\n                );\n              }\n            `,\n\n            \"src/routes/side-effect-redirect-server-action/home.actions.ts\": js`\n              \"use server\";\n              import { redirect } from \"react-router\";\n\n              export async function redirectAction() {\n                redirect(\"/side-effect-redirect-server-action?redirected=true\", { headers: { \"x-test\": \"test\" } });\n                return \"redirected\";\n              }\n            `,\n            \"src/routes/side-effect-redirect-server-action/home.client.tsx\": js`\n              \"use client\";\n              import { useState } from \"react\";\n\n              export function Counter() {\n                const [count, setCount] = useState(0);\n                return <button type=\"button\" onClick={() => setCount(c => c + 1)} data-count>Count: {count}</button>;\n              }\n            `,\n            \"src/routes/side-effect-redirect-server-action/home.tsx\": js`\n              \"use client\";\n              import {useActionState} from \"react\";\n              import { redirectAction } from \"./home.actions\";\n              import { Counter } from \"./home.client\";\n\n              export default function HomeRoute(props) {\n                const [state, action] = useActionState(redirectAction, null);\n                return (\n                  <div>\n                    <form action={action}>\n                      <button type=\"submit\" data-submit>\n                        Redirect via Server Function\n                      </button>\n                    </form>\n                    {state && <div data-testid=\"state\">{state}</div>}\n                    <Counter />\n                  </div>\n                );\n              }\n            `,\n            \"src/routes/throw-external-redirect-server-action/home.actions.ts\": js`\n              \"use server\";\n              import { redirect } from \"react-router\";\n\n              export async function redirectAction(formData: FormData) {\n                // Throw a redirect to an external URL\n                throw redirect(\"https://example.com/\");\n              }\n            `,\n            \"src/routes/throw-external-redirect-server-action/home.client.tsx\": js`\n              \"use client\";\n\n              import { useState } from \"react\";\n\n              export function Counter() {\n                const [count, setCount] = useState(0);\n                return <button type=\"button\" onClick={() => setCount(c => c + 1)} data-count>Count: {count}</button>;\n              }\n            `,\n            \"src/routes/throw-external-redirect-server-action/home.tsx\": js`\n              import { redirectAction } from \"./home.actions\";\n              import { Counter } from \"./home.client\";\n\n              export default function HomeRoute(props) {\n                return (\n                  <div>\n                    <form action={redirectAction}>\n                      <button type=\"submit\" data-submit>\n                        Redirect via Server Function\n                      </button>\n                    </form>\n                    <Counter />\n                  </div>\n                );\n              }\n            `,\n            \"src/routes/side-effect-external-redirect-server-action/home.actions.ts\": js`\n              \"use server\";\n              import { redirect } from \"react-router\";\n\n              export async function redirectAction() {\n                // Perform a side-effect redirect to an external URL\n                redirect(\"https://example.com/\", { headers: { \"x-test\": \"test\" } });\n                return \"redirected\";\n              }\n            `,\n            \"src/routes/side-effect-external-redirect-server-action/home.client.tsx\": js`\n              \"use client\";\n              import { useState } from \"react\";\n\n              export function Counter() {\n                const [count, setCount] = useState(0);\n                return <button type=\"button\" onClick={() => setCount(c => c + 1)} data-count>Count: {count}</button>;\n              }\n            `,\n            \"src/routes/side-effect-external-redirect-server-action/home.tsx\": js`\n              \"use client\";\n              import {useActionState} from \"react\";\n              import { redirectAction } from \"./home.actions\";\n              import { Counter } from \"./home.client\";\n\n              export default function HomeRoute(props) {\n                const [state, action] = useActionState(redirectAction, null);\n                return (\n                  <div>\n                    <form action={action}>\n                      <button type=\"submit\" data-submit>\n                        Redirect via Server Function\n                      </button>\n                    </form>\n                    {state && <div data-testid=\"state\">{state}</div>}\n                    <Counter />\n                  </div>\n                );\n              }\n            `,\n\n            \"src/routes/server-function-reference/home.actions.ts\": js`\n              \"use server\";\n\n              export async function incrementCounter({count, ref}: {count: number; ref: unknown}, formData: FormData) {\n                return {count: count + parseInt(formData.get(\"by\") as string || \"1\", 10), ref};\n              }\n            `,\n            \"src/routes/server-function-reference/home.tsx\": js`\n              export { default } from \"./home.client\";\n            `,\n            \"src/routes/server-function-reference/home.client.tsx\": js`\n              \"use client\";\n\n              import { useActionState } from \"react\";\n\n              import { incrementCounter } from \"./home.actions\";\n\n              const ogRef = {};\n              export default function HomeRoute() {\n                const [{count,ref}, incrementCounterAction, incrementing] = useActionState(incrementCounter, {count: 0, ref: ogRef});\n\n                return (\n                  <div>\n                    <h2 data-home>Home: ({count})</h2>\n                    <h2 data-home-ref>{ref === ogRef ? \"good\" : \"bad\"}</h2>\n                    <form action={incrementCounterAction}>\n                      <button type=\"submit\" data-submit>\n                        {incrementing ? \"Updating via Server Function\" : \"Update via Server Function\"}\n                      </button>\n                    </form>\n                  </div>\n                );\n              }\n            `,\n\n            \"src/routes/sanitized-errors/home.tsx\": js`\n              export function loader() {\n                throw new Error(\"This error should be sanitized\");\n              }\n\n              export default function HomeRoute() {\n                return <h2>This should not be rendered</h2>;\n              }\n\n              export { ErrorBoundary } from \"./home.client\";\n            `,\n            \"src/routes/sanitized-errors/home.client.tsx\": js`\n              \"use client\"\n              import { useRouteError } from \"react-router\";\n\n              export function ErrorBoundary() {\n                let error = useRouteError();\n                return (\n                  <>\n                    <h2 data-error-title>Error Caught!</h2>\n                    <p data-error-message>{error.message}</p>\n                  </>\n                );\n              }\n            `,\n\n            \"src/routes/client-route-component-props/home.tsx\": js`\n              export { default, clientLoader, clientAction } from \"./home.client\";\n            `,\n            \"src/routes/client-route-component-props/home.client.tsx\": js`\n              \"use client\";\n\n              import { Form } from \"react-router\";\n\n              export async function clientLoader() {\n                return { message: \"Hello from client loader!\" };\n              }\n\n              export async function clientAction({ request }) {\n                const formData = await request.formData();\n                const name = formData.get(\"name\") as string;\n                return { actionResult: \"Hello \" + name + \" from client action!\" };\n              }\n\n              export default function HomeRoute({ loaderData, actionData, matches, params }) {\n                return (\n                  <div>\n                    <h2 data-home>Home Route</h2>\n                    {loaderData && (\n                      <p data-loader-data>{loaderData.message}</p>\n                    )}\n                    {actionData && (\n                      <p data-action-data>{actionData.actionResult}</p>\n                    )}\n                    {matches && (\n                      <div data-matches>\n                        <p data-matches-ids>matches ids: {matches.map(match => match.id).join(\", \")}</p>\n                      </div>\n                    )}\n                    {params && (\n                      <div data-params>\n                        <p data-params-type>typeof params: {typeof params}</p>\n                        <p data-params-count>params count: {Object.keys(params).length}</p>\n                      </div>\n                    )}\n                    <Form method=\"post\">\n                      <input name=\"name\" data-name-input />\n                      <button type=\"submit\" data-submit-button>\n                        Submit Action\n                      </button>\n                    </Form>\n                  </div>\n                );\n              }\n            `,\n\n            \"src/routes/client-error-boundary-props-client-loader/home.tsx\": js`\n              export { default, clientLoader, ErrorBoundary } from \"./home.client\";\n            `,\n            \"src/routes/client-error-boundary-props-client-loader/home.client.tsx\": js`\n              \"use client\";\n\n              export async function clientLoader() {\n                throw new Error(\"Intentional error from client loader\");\n              }\n\n              export function ErrorBoundary({ error, params }) {\n                return (\n                  <div>\n                    <h2 data-error-title>Error Caught!</h2>\n                    <p data-error-message>{error.message}</p>\n                    {params && (\n                      <div data-error-params>\n                        <p data-error-params-type>typeof params: {typeof params}</p>\n                        <p data-error-params-count>params count: {Object.keys(params).length}</p>\n                      </div>\n                    )}\n                  </div>\n                );\n              }\n\n              export default function HomeRoute() {\n                return (\n                  <div>\n                    <h2>Home Route</h2>\n                  </div>\n                );\n              }\n            `,\n\n            \"src/routes/hydrate-fallback-props/home.tsx\": js`\n              export { default, clientLoader, HydrateFallback } from \"./home.client\";\n            `,\n            \"src/routes/hydrate-fallback-props/home.client.tsx\": js`\n              \"use client\";\n\n              export async function clientLoader() {\n                const pollingPromise = (async () => {\n                  while (globalThis.unblockClientLoader !== true) {\n                    await new Promise((resolve) => setTimeout(resolve, 0));\n                  }\n                })();\n                const timeoutPromise = new Promise((_, reject) => {\n                  setTimeout(() => reject(new Error(\"Client loader wasn't unblocked after 5s\")), 5000);\n                });\n                await Promise.race([pollingPromise, timeoutPromise]);\n                return { message: \"Hello from client loader!\" };\n              }\n\n              export function HydrateFallback({ params }) {\n                return (\n                  <div>\n                    <h2 data-hydrate-fallback>Hydrate Fallback</h2>\n                    {params && (\n                      <div data-hydrate-params>\n                        <p data-hydrate-params-type>typeof params: {typeof params}</p>\n                        <p data-hydrate-params-count>params count: {Object.keys(params).length}</p>\n                      </div>\n                    )}\n                  </div>\n                );\n              }\n\n              export default function HomeRoute() {\n                return (\n                  <div>\n                    <h2 data-home>Home Route</h2>\n                  </div>\n                );\n              }\n            `,\n\n            \"src/routes/no-revalidate-server-action/home.actions.ts\": js`\n              \"use server\";\n\n              export async function noRevalidateAction() {\n                return \"no revalidate\";\n              }\n            `,\n            \"src/routes/no-revalidate-server-action/home.tsx\": js`\n              import ClientHomeRoute from \"./home.client\";\n\n              export function loader() {\n                console.log(\"THIS SHOULD NOT BE LOGGED!!!\");\n              }\n\n              export default function HomeRoute() {\n                return <ClientHomeRoute identity={{}} />;\n              }\n            `,\n            \"src/routes/no-revalidate-server-action/home.client.tsx\": js`\n              \"use client\";\n\n              import { useActionState, useState } from \"react\";\n              import { noRevalidateAction } from \"./home.actions\";\n\n              export default function HomeRoute({ identity }) {\n                const [initialIdentity] = useState(identity);\n                const [state, action, pending] = useActionState(noRevalidateAction, null);\n                return (\n                  <div>\n                    <form action={action}>\n                      <input name=\"$SKIP_REVALIDATION\" type=\"hidden\" />\n                      <button type=\"submit\" data-submit>No Revalidate</button>\n                    </form>\n                    {state && <div data-state>{state}</div>}\n                    {pending && <div data-pending>Pending</div>}\n                    {initialIdentity !== identity && <div data-revalidated>Revalidated</div>}\n                  </div>\n                );\n              }\n            `,\n\n            \"src/routes/await-component/events.ts\": js`\n              import EventEmitter from 'node:events'\n\n              export const events = new EventEmitter();\n            `,\n            \"src/routes/await-component/api.ts\": js`\n              import { events } from \"./events\";\n              export async function action({ request }) {\n                const event = await request.text()\n                events.emit(event);\n                return Response.json(event);\n              }\n            `,\n            \"src/routes/await-component/client.tsx\": js`\n              \"use client\";\n              import { useAsyncError, useAsyncValue } from \"react-router\";\n\n              export function ClientValue() {\n                const value = useAsyncValue();\n                return <div data-resolved>{value}</div>;\n              }\n\n              export function ClientError() {\n                const error = useAsyncError();\n                return <div data-rejected>{error.message}</div>;\n              }\n            `,\n            \"src/routes/await-component/home.tsx\": js`\n              import { Suspense } from \"react\";\n              import { Await } from \"react-router\";\n\n              import { ClientValue } from \"./client\";\n              import { events } from \"./events\";\n\n              export default function AwaitResolveTest() {\n                const promise = new Promise(resolve => {\n                  events.on(\"resolve\", () => {\n                    resolve(\"Async Data\");\n                  });\n                });\n\n                return (\n                  <>\n                    <Suspense fallback={<p data-fallback>Loading...</p>}>\n                      <Await resolve={promise}>\n                        <ClientValue />\n                      </Await>\n                    </Suspense>\n                    {Array.from({ length: 100 }, (_, i) => (\n                      <p key={i}>Item {i}</p>\n                    ))}\n                  </>\n                );\n              }\n            `,\n            \"src/routes/await-component/reject.tsx\": js`\n              import { Suspense } from \"react\";\n              import { Await } from \"react-router\";\n\n              import { ClientError } from \"./client\";\n              import { events } from \"./events\";\n\n              export default function AwaitRejectTest() {\n                const promise = new Promise((_, reject) => {\n                  events.on(\"reject\", () => {\n                    reject(new Error(\"Async Error\"));\n                  });\n                });\n\n                return (\n                  <>\n                    <Suspense fallback={<p data-fallback>Loading...</p>}>\n                      <Await resolve={promise} errorElement={<ClientError />}>\n                        {(data) => (<p data-resolved>{data}</p>)}\n                      </Await>\n                    </Suspense>\n                    {Array.from({ length: 100 }, (_, i) => (\n                      <p key={i}>Item {i}</p>\n                    ))}\n                  </>\n                );\n              }\n            `,\n            \"src/routes/ssr-error/ssr-error.tsx\": js`\n              \"use client\";\n              import { useState } from \"react\";\n\n              export function ErrorBoundary() {\n                const [count, setCount] = useState(0);\n\n                return (\n                  <div>\n                    <div data-error-boundary>Client Error Boundary</div>\n                    <button data-increment onClick={() => setCount(c => c + 1)}>\n                      Increment {count}\n                    </button>\n                  </div>\n                );\n              }\n\n              export default function SSRError() {\n                throw new Error(\"Error from SSR component\");\n              }\n            `,\n\n            \"src/routes/action-transition-state/home.tsx\": js`\n              import { Suspense } from \"react\";\n              import { IncrementButton } from \"./client\";\n              let count = 0;\n\n              export default function ActionTransitionState() {\n                return (\n                  <div>\n                    <form\n                      action={async () => {\n                        \"use server\";\n                        await new Promise((r) => setTimeout(r, 1000));\n                        count++;\n                      }}\n                    >\n                      <IncrementButton count={count} />\n                    </form>\n                    <Suspense>\n                      <AsyncComponent count={count} />\n                    </Suspense>\n                  </div>\n                );\n              }\n\n              async function AsyncComponent({ count }) {\n                await new Promise((r) => setTimeout(r, 1000));\n                return <div data-testid=\"async-count\">AsyncCount: {count}</div>;\n              }\n            `,\n            \"src/routes/action-transition-state/client.tsx\": js`\n              \"use client\";\n              import { useFormStatus } from \"react-dom\";\n\n              export function IncrementButton({ count }: { count: number }) {\n                const { pending } = useFormStatus();\n                return (\n                  <button data-testid=\"increment-button\" type=\"submit\" disabled={pending}>\n                    IncrementCount: {pending ? count + 1 : count}\n                  </button>\n                );\n              }\n            `,\n\n            \"src/routes/render-redirect/home.tsx\": js`\n              import { Link, redirect } from \"react-router\";\n\n              export default function RenderRedirect({ params: { id } }) {\n                if (id === \"redirect\") {\n                  throw redirect(\"/render-redirect/redirected\");\n                }\n\n                if (id === \"external\") {\n                  throw redirect(\"https://example.com/\")\n                }\n\n                return (\n                  <>\n                    <h1>{id || \"home\"}</h1>\n                    <Link to=\"/render-redirect/redirect\">Redirect</Link>\n                    <Link to=\"/render-redirect/external\">External</Link>\n                  </>\n                )\n              }\n            `,\n            \"src/routes/render-redirect/lazy.tsx\": js`\n              import { Suspense } from \"react\";\n              import { Link, redirect } from \"react-router\";\n\n              export default function RenderRedirect({ params: { id } }) {\n                return (\n                  <Suspense fallback={<p>Loading...</p>}>\n                    <Lazy id={id} />\n                  </Suspense>\n                );\n              }\n\n              async function Lazy({ id }) {\n                await new Promise((r) => setTimeout(r, 0));\n\n                if (id === \"redirect\") {\n                  throw redirect(\"/render-redirect/lazy/redirected\");\n                }\n\n                if (id === \"external\") {\n                  throw redirect(\"https://example.com/\")\n                }\n\n                return (\n                  <>\n                    <h1>{id || \"home\"}</h1>\n                    <Link to=\"/render-redirect/lazy/redirect\">Redirect</Link>\n                    <Link to=\"/render-redirect/external\">External</Link>\n                  </>\n                );\n              }\n            `,\n\n            \"src/routes/render-route-error-response/home.tsx\": js`\n              import { data } from \"react-router\";\n\n              export { ErrorBoundary } from \"./home.client\";\n\n              export default function RenderRouteErrorResponse({ params: { id } }) {\n                if (!id) throw new Response(null, { status: 400, statusText: \"Oh no!\" });\n\n                throw data({ message: id }, { status: 400, statusText: \"Oh no!\" });\n              }\n            `,\n            \"src/routes/render-route-error-response/home.client.tsx\": js`\n              \"use client\";\n              import { useRouteError, isRouteErrorResponse } from \"react-router\";\n\n              export function ErrorBoundary() {\n                const error = useRouteError();\n                if (isRouteErrorResponse(error)) {\n                  return <p>{error.status} {error.statusText} {error.data?.message || \"no\"}</p>;\n                }\n                return <p>Oh no D:</p>;\n              }\n            `,\n\n            \"src/routes/get-request/get-request.tsx\": js`\n              import { unstable_getRequest as getRequest } from \"react-router\";\n\n              export function action() { return null; }\n\n              export default function GetRequest() {\n                const request = getRequest();\n                return <p>{request.method}</p>;\n              }\n            `,\n          },\n        });\n      });\n\n      test.describe(\"Basic functionality\", () => {\n        test(\"Supports navigating between server-first/client-first routes starting on a server route\", async ({\n          page,\n        }) => {\n          await page.goto(`http://localhost:${port}/soft-navigation`);\n\n          // Load a server route\n          await page.waitForSelector(\"[data-page=home]\");\n          expect(await page.locator(\"[data-content]\").textContent()).toBe(\n            \"Home Page Data\",\n          );\n\n          // Navigate to a client route\n          await page.click(\"a[href='/soft-navigation/dashboard']\");\n          await page.waitForSelector(\"[data-page=dashboard]\");\n\n          // Verify server data\n          expect(await page.locator(\"[data-server-count]\").textContent()).toBe(\n            \"Server count: 1\",\n          );\n          expect(await page.locator(\"[data-client-count]\").textContent()).toBe(\n            \"Client count: 1\",\n          );\n\n          // Increment via the client component\n          await page.click(\"[data-increment]\");\n          expect(await page.locator(\"[data-server-count]\").textContent()).toBe(\n            \"Server count: 1\",\n          );\n          expect(await page.locator(\"[data-client-count]\").textContent()).toBe(\n            \"Client count: 2\",\n          );\n\n          // Navigate back to a server route\n          await page.click(\"a[href='/soft-navigation']\");\n          await page.waitForSelector(\"[data-page=home]\");\n          expect(await page.locator(\"[data-content]\").textContent()).toBe(\n            \"Home Page Data\",\n          );\n\n          // Ensure this is using RSC\n          validateRSCHtml(await page.content());\n        });\n\n        test(\"Supports navigating between server-first/client-first routes starting on a client route\", async ({\n          page,\n        }) => {\n          await page.goto(`http://localhost:${port}/soft-navigation/dashboard`);\n          await page.waitForSelector(\"[data-page=dashboard]\");\n\n          // Verify server data\n          expect(await page.locator(\"[data-server-count]\").textContent()).toBe(\n            \"Server count: 1\",\n          );\n          expect(await page.locator(\"[data-client-count]\").textContent()).toBe(\n            \"Client count: 1\",\n          );\n\n          // Increment via the client component\n          await page.click(\"[data-increment]\");\n          expect(await page.locator(\"[data-server-count]\").textContent()).toBe(\n            \"Server count: 1\",\n          );\n          expect(await page.locator(\"[data-client-count]\").textContent()).toBe(\n            \"Client count: 2\",\n          );\n\n          // Navigate to a server route\n          await page.click(\"a[href='/soft-navigation']\");\n          await page.waitForSelector(\"[data-page=home]\");\n          expect(await page.locator(\"[data-content]\").textContent()).toBe(\n            \"Home Page Data\",\n          );\n\n          // Navigate back to a client route\n          await page.click(\"a[href='/soft-navigation/dashboard']\");\n          await page.waitForSelector(\"[data-page=dashboard]\");\n          expect(await page.locator(\"[data-server-count]\").textContent()).toBe(\n            \"Server count: 1\",\n          );\n          expect(await page.locator(\"[data-client-count]\").textContent()).toBe(\n            \"Client count: 1\",\n          );\n\n          // Ensure this is using RSC\n          validateRSCHtml(await page.content());\n        });\n\n        test(\"Supports request context using the RouterContextProvider API\", async ({\n          page,\n        }) => {\n          await page.goto(`http://localhost:${port}/request-context`);\n          await page.waitForSelector(\"[data-home]\");\n          expect(await page.locator(\"[data-home]\").textContent()).toBe(\n            \"Home: test-context-value\",\n          );\n\n          // Ensure this is actually using RSC\n          validateRSCHtml(await page.content());\n        });\n\n        test(\"Supports request context in resource routes using the RouterContextProvider API\", async ({\n          page,\n          request,\n        }) => {\n          const getResponse = await request.get(\n            `http://localhost:${port}/resource-request-context/resource`,\n          );\n          expect(getResponse?.status()).toBe(200);\n          expect((await getResponse?.json()).contextValue).toBe(\n            \"test-context-value\",\n          );\n\n          const postResponse = await request.post(\n            `http://localhost:${port}/resource-request-context/resource`,\n          );\n          expect(postResponse?.status()).toBe(200);\n          expect((await postResponse?.json()).contextValue).toBe(\n            \"test-context-value\",\n          );\n\n          await page.goto(`http://localhost:${port}/resource-request-context`);\n          await page.click(\"button\");\n\n          await page.waitForSelector(\"[data-testid=resource-data]\");\n          const fetcherData = JSON.parse(\n            (await page.locator(\"[data-testid=resource-data]\").textContent()) ||\n              \"{}\",\n          );\n          expect(fetcherData.contextValue).toBe(\"test-context-value\");\n\n          // Ensure this is using RSC\n          validateRSCHtml(await page.content());\n        });\n\n        test(\"Supports request context in middleware using the RouterContextProvider API\", async ({\n          page,\n        }) => {\n          await page.goto(\n            `http://localhost:${port}/middleware-request-context/`,\n          );\n          await page.waitForSelector(\"[data-context-value]\");\n          expect(await page.locator(\"[data-context-value]\").textContent()).toBe(\n            \"Context value: test-context-value\",\n          );\n\n          // Ensure this is using RSC\n          validateRSCHtml(await page.content());\n        });\n\n        test(\"Supports client context using getContext\", async ({ page }) => {\n          await page.goto(`http://localhost:${port}/get-context`);\n          await page.waitForSelector(\"[data-client-context]\");\n          expect(\n            await page.locator(\"[data-client-context]\").textContent(),\n          ).toBe(\"Client context value: client-context-value\");\n\n          // Ensure this is using RSC\n          validateRSCHtml(await page.content());\n        });\n\n        test(\"Supports resource routes as URL and fetchers\", async ({\n          page,\n          request,\n        }) => {\n          const getResponse = await request.get(\n            `http://localhost:${port}/resource-url-and-fetchers/resource`,\n          );\n          expect(getResponse?.status()).toBe(200);\n          expect(await getResponse?.json()).toEqual({\n            message: \"Hello from resource route!\",\n          });\n\n          const postResponse = await request.post(\n            `http://localhost:${port}/resource-url-and-fetchers/resource`,\n            {\n              data: { hello: \"world\" },\n            },\n          );\n          expect(postResponse?.status()).toBe(200);\n          expect(await postResponse?.json()).toEqual({\n            message: \"Hello from resource route!\",\n            echo: JSON.stringify({ hello: \"world\" }),\n          });\n\n          await page.goto(`http://localhost:${port}/resource-url-and-fetchers`);\n          await page.click(\"button\");\n\n          await page.waitForSelector(\"[data-testid=resource-data]\");\n          expect(\n            await page.locator(\"[data-testid=resource-data]\").textContent(),\n          ).toBe(\n            JSON.stringify({\n              message: \"Hello from resource route!\",\n              echo: \"hello=world\",\n            }),\n          );\n        });\n\n        test(\"Handles error responses from resource routes missing loaders/actions\", async ({\n          page,\n          request,\n        }) => {\n          const getResponse = await request.get(\n            `http://localhost:${port}/resource-error-handling/no-loader-resource`,\n          );\n          expect(getResponse?.status()).toBe(400);\n          expect(await getResponse?.text()).toBe(\n            'Error: You made a GET request to \"/resource-error-handling/no-loader-resource\" but did not provide a `loader` for route \"resource-error-handling.no-loader-resource\", so there is no way to handle the request.',\n          );\n\n          const postResponse = await request.post(\n            `http://localhost:${port}/resource-error-handling/no-action-resource`,\n          );\n          expect(postResponse?.status()).toBe(405);\n          expect(await postResponse?.text()).toBe(\n            'Error: You made a POST request to \"/resource-error-handling/no-action-resource\" but did not provide an `action` for route \"resource-error-handling.no-action-resource\", so there is no way to handle the request.',\n          );\n\n          const postWithActionResponse = await request.post(\n            `http://localhost:${port}/resource-error-handling/no-loader-resource`,\n          );\n          expect(postWithActionResponse?.status()).toBe(200);\n          expect(await postWithActionResponse?.json()).toEqual({\n            message: \"no-loader-resource action works\",\n          });\n\n          const getWithLoaderResponse = await request.get(\n            `http://localhost:${port}/resource-error-handling/no-action-resource`,\n          );\n          expect(getWithLoaderResponse?.status()).toBe(200);\n          expect(await getWithLoaderResponse?.json()).toEqual({\n            message: \"no-action-resource loader works\",\n          });\n\n          // Ensure this is using RSC\n          await page.goto(`http://localhost:${port}/resource-error-handling/`);\n          validateRSCHtml(await page.content());\n        });\n\n        test(\"Supports Await component resolve\", async ({ page }) => {\n          await page.goto(`http://localhost:${port}/await-component`, {\n            waitUntil: \"commit\",\n          });\n          await page.waitForSelector(\"[data-fallback]\");\n          await fetch(`http://localhost:${port}/await-component/api`, {\n            method: \"POST\",\n            headers: { \"Content-Type\": \"text/plain\" },\n            body: \"resolve\",\n          });\n          const resolved = await page.waitForSelector(\"[data-resolved]\");\n          expect(await resolved.innerText()).toContain(\"Async Data\");\n        });\n\n        test(\"Supports Await component rejection\", async ({ page }) => {\n          await page.goto(`http://localhost:${port}/await-component/reject`, {\n            waitUntil: \"commit\",\n          });\n          await page.waitForSelector(\"[data-fallback]\");\n          await fetch(`http://localhost:${port}/await-component/api`, {\n            method: \"POST\",\n            headers: { \"Content-Type\": \"text/plain\" },\n            body: \"reject\",\n          });\n          const rejected = await page.waitForSelector(\"[data-rejected]\");\n          expect(await rejected.innerText()).toContain(\n            \"An error occurred in the Server Components render.\",\n          );\n        });\n\n        test(\"Suppport throwing redirect Response from render\", async ({\n          page,\n        }) => {\n          await page.goto(`http://localhost:${port}/render-redirect`);\n          await expect(page.getByText(\"home\")).toBeAttached();\n          await page.getByText(\"Redirect\").click();\n          await page.waitForURL(\n            `http://localhost:${port}/render-redirect/redirected`,\n          );\n          await expect(page.getByText(\"redirected\")).toBeAttached();\n        });\n\n        test(\"Suppport throwing external redirect Response from render\", async ({\n          browserName,\n          page,\n        }) => {\n          test.skip(\n            browserName === \"firefox\",\n            \"Playwright doesn't like external redirects for tests. It times out waiting for the URL even though it navigates.\",\n          );\n          await page.goto(`http://localhost:${port}/render-redirect`);\n          await expect(page.getByText(\"home\")).toBeAttached();\n          await page.getByText(\"External\").click();\n          await page.waitForURL(`https://example.com/`);\n          await expect(page.getByText(\"Example Domain\")).toBeAttached();\n        });\n\n        test(\"Suppport throwing redirect Response from suspended render\", async ({\n          page,\n        }) => {\n          await page.goto(`http://localhost:${port}/render-redirect/lazy`);\n          await expect(page.getByText(\"home\")).toBeAttached();\n          await page.getByText(\"Redirect\").click();\n          await page.waitForURL(\n            `http://localhost:${port}/render-redirect/lazy/redirected`,\n          );\n          await expect(page.getByText(\"redirected\")).toBeAttached();\n        });\n\n        test(\"Suppport throwing external redirect Response from suspended render\", async ({\n          browserName,\n          page,\n        }) => {\n          test.skip(\n            browserName === \"firefox\",\n            \"Playwright doesn't like external redirects for tests. It times out waiting for the URL even though it navigates.\",\n          );\n          await page.goto(`http://localhost:${port}/render-redirect/lazy`);\n          await expect(page.getByText(\"home\")).toBeAttached();\n          await page.getByText(\"External\").click();\n          await page.waitForURL(`https://example.com/`);\n          await expect(page.getByText(\"Example Domain\")).toBeAttached();\n        });\n\n        test(\"Support throwing Responses\", async ({ page }) => {\n          await page.goto(\n            `http://localhost:${port}/render-route-error-response`,\n          );\n          await expect(page.getByText(\"400 Oh no! no\")).toBeAttached();\n        });\n\n        test(\"Support throwing data() responses with data\", async ({\n          page,\n        }) => {\n          await page.goto(\n            `http://localhost:${port}/render-route-error-response/Test`,\n          );\n          await expect(page.getByText(\"400 Oh no! Test\")).toBeAttached();\n        });\n\n        test(\"Supports getRequest in server components\", async ({ page }) => {\n          await page.goto(`http://localhost:${port}/get-request`);\n          await expect(page.getByText(\"GET\")).toBeAttached();\n\n          const response = await page.request.fetch(\n            `http://localhost:${port}/get-request`,\n            {\n              method: \"POST\",\n            },\n          );\n          const body = await response.text();\n          expect(body).toContain(\"<p>POST</p>\");\n        });\n      });\n\n      test.describe(\"Server Actions\", () => {\n        test(\"Supports React Server Functions\", async ({ page }) => {\n          await page.goto(`http://localhost:${port}/server-action`);\n\n          // Verify initial server render\n          await page.waitForSelector(\"[data-home]\");\n          expect(await page.locator(\"[data-home]\").textContent()).toBe(\n            \"Home: (0)\",\n          );\n\n          // Submit the form to trigger server function\n          await page.click(\"[data-submit]\");\n\n          // Verify server function updated the UI\n          await expect(page.locator(\"[data-home]\")).toHaveText(\"Home: (1)\");\n\n          // Submit again to ensure server functions work repeatedly\n          await page.click(\"[data-submit]\");\n          await expect(page.locator(\"[data-home]\")).toHaveText(\"Home: (2)\");\n\n          // Ensure this is using RSC\n          validateRSCHtml(await page.content());\n        });\n\n        test(\"Supports Inline React Server Functions\", async ({ page }) => {\n          await page.goto(`http://localhost:${port}/inline-server-action/`);\n\n          // Verify initial server render\n          await page.waitForSelector(\"[data-home]\");\n          expect(await page.locator(\"[data-home]\").textContent()).toBe(\n            \"Home: Default (0)\",\n          );\n\n          // Submit the form to trigger server function\n          await page.click(\"[data-submit]\");\n\n          // Verify server function updated the UI\n          await expect(page.locator(\"[data-home]\")).toHaveText(\n            \"Home: Updated (1)\",\n          );\n\n          // Submit again to ensure server functions work repeatedly\n          await page.click(\"[data-submit]\");\n          await expect(page.locator(\"[data-home]\")).toHaveText(\n            \"Home: Updated (2)\",\n          );\n\n          // Ensure this is using RSC\n          validateRSCHtml(await page.content());\n        });\n\n        test(\"Supports React Server Functions thrown redirects\", async ({\n          page,\n        }) => {\n          await page.goto(\n            `http://localhost:${port}/throw-redirect-server-action/`,\n          );\n\n          // Verify initial server render\n          await page.waitForSelector(\"[data-count]\");\n          expect(await page.locator(\"[data-count]\").textContent()).toBe(\n            \"Count: 0\",\n          );\n          await page.click(\"[data-count]\");\n          expect(await page.locator(\"[data-count]\").textContent()).toBe(\n            \"Count: 1\",\n          );\n\n          // Submit the form to trigger server function redirect\n          await page.click(\"[data-submit]\");\n\n          await expect(page).toHaveURL(\n            `http://localhost:${port}/throw-redirect-server-action?redirected=true`,\n          );\n\n          expect(await page.locator(\"[data-count]\").textContent()).toBe(\n            \"Count: 1\",\n          );\n          // Validate things are still interactive after redirect\n          await page.click(\"[data-count]\");\n          expect(await page.locator(\"[data-count]\").textContent()).toBe(\n            \"Count: 2\",\n          );\n\n          // Ensure this is using RSC\n          validateRSCHtml(await page.content());\n        });\n\n        test(\"Supports React Server Functions thrown external redirects\", async ({\n          page,\n        }) => {\n          await page.goto(\n            `http://localhost:${port}/throw-external-redirect-server-action/`,\n          );\n\n          // Verify initial server render\n          await page.waitForSelector(\"[data-count]\");\n          expect(await page.locator(\"[data-count]\").textContent()).toBe(\n            \"Count: 0\",\n          );\n          await page.click(\"[data-count]\");\n          expect(await page.locator(\"[data-count]\").textContent()).toBe(\n            \"Count: 1\",\n          );\n\n          // Submit the form to trigger server function redirect to external URL\n          await page.click(\"[data-submit]\");\n\n          // We expect the browser to navigate to the external site (example.com)\n          await expect(page).toHaveURL(`https://example.com/`);\n        });\n\n        test(\"Supports React Server Functions side-effect redirects\", async ({\n          page,\n        }) => {\n          await page.goto(\n            `http://localhost:${port}/side-effect-redirect-server-action`,\n          );\n\n          // Verify initial server render\n          await page.waitForSelector(\"[data-count]\");\n          expect(await page.locator(\"[data-count]\").textContent()).toBe(\n            \"Count: 0\",\n          );\n          await page.click(\"[data-count]\");\n          expect(await page.locator(\"[data-count]\").textContent()).toBe(\n            \"Count: 1\",\n          );\n\n          const responseHeadersPromise = new Promise<Record<string, string>>(\n            (resolve) => {\n              page.addListener(\"response\", (response) => {\n                if (response.request().method() === \"POST\") {\n                  resolve(response.headers());\n                }\n              });\n            },\n          );\n\n          // Submit the form to trigger server function redirect\n          await page.click(\"[data-submit]\");\n\n          await expect(page.getByTestId(\"state\")).toHaveText(\"redirected\");\n\n          await page.waitForURL(\n            `http://localhost:${port}/side-effect-redirect-server-action?redirected=true`,\n          );\n\n          expect((await responseHeadersPromise)[\"x-test\"]).toBe(\"test\");\n\n          expect(await page.locator(\"[data-count]\").textContent()).toBe(\n            \"Count: 1\",\n          );\n          // Validate things are still interactive after redirect\n          await page.click(\"[data-count]\");\n          expect(await page.locator(\"[data-count]\").textContent()).toBe(\n            \"Count: 2\",\n          );\n\n          // Ensure this is using RSC\n          validateRSCHtml(await page.content());\n        });\n\n        test(\"Supports React Server Functions side-effect external redirects\", async ({\n          page,\n        }) => {\n          await page.goto(\n            `http://localhost:${port}/side-effect-external-redirect-server-action`,\n          );\n\n          // Verify initial server render\n          await page.waitForSelector(\"[data-count]\");\n          expect(await page.locator(\"[data-count]\").textContent()).toBe(\n            \"Count: 0\",\n          );\n          await page.click(\"[data-count]\");\n          expect(await page.locator(\"[data-count]\").textContent()).toBe(\n            \"Count: 1\",\n          );\n\n          const responseHeadersPromise = new Promise<Record<string, string>>(\n            (resolve) => {\n              page.addListener(\"response\", (response) => {\n                if (response.request().method() === \"POST\") {\n                  resolve(response.headers());\n                }\n              });\n            },\n          );\n\n          // Submit the form to trigger server function redirect to external URL\n          await page.click(\"[data-submit]\");\n\n          // We expect the browser to navigate to the external site (example.com)\n          await expect(page).toHaveURL(`https://example.com/`);\n\n          // Optionally assert that the server sent the header\n          expect((await responseHeadersPromise)[\"x-test\"]).toBe(\"test\");\n        });\n\n        test(\"Supports React Server Function References\", async ({ page }) => {\n          await page.goto(`http://localhost:${port}/server-function-reference`);\n\n          // Verify initial server render\n          await page.waitForSelector(\"[data-home]\");\n          expect(await page.locator(\"[data-home]\").textContent()).toBe(\n            \"Home: (0)\",\n          );\n          await expect(page.locator(\"[data-home-ref]\")).toHaveText(\"good\");\n\n          // Submit the form to trigger server function\n          await page.click(\"[data-submit]\");\n\n          // Verify server function updated the UI\n          await expect(page.locator(\"[data-home]\")).toHaveText(\"Home: (1)\");\n          await expect(page.locator(\"[data-home-ref]\")).toHaveText(\"good\");\n\n          // Submit again to ensure server functions work repeatedly\n          await page.click(\"[data-submit]\");\n          await expect(page.locator(\"[data-home]\")).toHaveText(\"Home: (2)\");\n          await expect(page.locator(\"[data-home-ref]\")).toHaveText(\"good\");\n\n          // Ensure this is using RSC\n          validateRSCHtml(await page.content());\n        });\n\n        test(\"Supports server actions that disable revalidation\", async ({\n          page,\n        }) => {\n          await page.goto(\n            `http://localhost:${port}/no-revalidate-server-action`,\n            { waitUntil: \"networkidle\" },\n          );\n\n          const actionResponsePromise = new Promise<PlaywrightResponse>(\n            (resolve) => {\n              page.on(\"response\", async (response) => {\n                if (!!(await response.request().headerValue(\"rsc-action-id\"))) {\n                  resolve(response);\n                }\n              });\n            },\n          );\n\n          await page.click(\"[data-submit]\");\n          await page.waitForSelector(\"[data-state]\");\n          await page.waitForSelector(\"[data-pending]\", { state: \"hidden\" });\n          await page.waitForSelector(\"[data-revalidated]\", {\n            state: \"hidden\",\n          });\n          expect(await page.locator(\"[data-state]\").textContent()).toBe(\n            \"no revalidate\",\n          );\n\n          const actionResponse = await actionResponsePromise;\n          expect(await actionResponse.headerValue(\"x-test\")).toBe(\"test\");\n        });\n\n        test(\"Supports transition state throughout the revalidation lifecycle\", async ({\n          page,\n        }) => {\n          await page.goto(`http://localhost:${port}/action-transition-state`, {\n            waitUntil: \"networkidle\",\n          });\n\n          const count0Button = page.getByText(\"IncrementCount: 0\");\n          await expect(count0Button).toBeEnabled();\n          await count0Button.click();\n\n          const count1Button = page.getByText(\"IncrementCount: 1\");\n          await expect(count1Button).toBeDisabled();\n\n          expect(await page.getByTestId(\"async-count\").textContent()).toBe(\n            \"AsyncCount: 0\",\n          );\n\n          await page.waitForFunction(\n            () =>\n              !(\n                document.querySelector(\n                  '[data-testid=\"increment-button\"]',\n                ) as HTMLButtonElement\n              )?.disabled,\n          );\n          await expect(count1Button).toBeEnabled();\n\n          await expect(page.getByTestId(\"async-count\")).toHaveText(\n            \"AsyncCount: 1\",\n          );\n        });\n      });\n\n      test.describe(\"Errors\", () => {\n        test(\"Handles sanitized production errors in server components correctly\", async ({\n          page,\n        }) => {\n          await page.goto(`http://localhost:${port}/sanitized-errors`);\n\n          // Verify error boundary is shown\n          await page.waitForSelector(\"[data-error-title]\");\n          await page.waitForSelector(\"[data-error-message]\");\n          expect(await page.locator(\"[data-error-message]\").textContent()).toBe(\n            \"An error occurred in the Server Components render. The specific message is omitted in production builds to avoid leaking sensitive details. A digest property is included on this error instance which may provide additional details about the nature of the error.\",\n          );\n\n          // Ensure this is using RSC\n          validateRSCHtml(await page.content());\n        });\n\n        test(\"Handles errors thrown in SSR components correctly\", async ({\n          page,\n        }) => {\n          await page.goto(`http://localhost:${port}/ssr-error`);\n\n          // Verify error boundary is shown\n          await page.waitForSelector(\"[data-error-boundary]\");\n          expect(\n            await page.locator(\"[data-error-boundary]\").textContent(),\n          ).toBe(\"Client Error Boundary\");\n\n          // Ensure this is using RSC\n          validateRSCHtml(await page.content());\n        });\n      });\n\n      test.describe(\"Route Client Component Props\", () => {\n        test(\"Passes props to client route component\", async ({ page }) => {\n          await page.goto(\n            `http://localhost:${port}/client-route-component-props`,\n          );\n\n          // Verify loader data is passed\n          await page.waitForSelector(\"[data-loader-data]\");\n          expect(await page.locator(\"[data-loader-data]\").textContent()).toBe(\n            \"Hello from client loader!\",\n          );\n\n          // Verify params are passed (empty for home route)\n          await page.waitForSelector(\"[data-params]\");\n          await page.waitForSelector(\"[data-params-type]\");\n          await page.waitForSelector(\"[data-params-count]\");\n          expect(await page.locator(\"[data-params-type]\").textContent()).toBe(\n            \"typeof params: object\",\n          );\n          expect(await page.locator(\"[data-params-count]\").textContent()).toBe(\n            \"params count: 0\",\n          );\n\n          // Verify matches are passed\n          await page.waitForSelector(\"[data-matches]\");\n          await page.waitForSelector(\"[data-matches-ids]\");\n          expect(await page.locator(\"[data-matches-ids]\").textContent()).toBe(\n            \"matches ids: root, client-route-component-props\",\n          );\n\n          // Submit the form to trigger the client action\n          await page.fill(\"[data-name-input]\", \"World\");\n          await page.click(\"[data-submit-button]\");\n\n          // Verify the action data is displayed\n          await page.waitForSelector(\"[data-action-data]\");\n          expect(await page.locator(\"[data-action-data]\").textContent()).toBe(\n            \"Hello World from client action!\",\n          );\n\n          // Ensure this is using RSC\n          validateRSCHtml(await page.content());\n        });\n\n        test(\"Passes props to client ErrorBoundary when error is thrown in client loader\", async ({\n          page,\n        }) => {\n          await page.goto(\n            `http://localhost:${port}/client-error-boundary-props-client-loader`,\n          );\n\n          // Verify error boundary is shown\n          await page.waitForSelector(\"[data-error-title]\");\n          await page.waitForSelector(\"[data-error-message]\");\n          expect(await page.locator(\"[data-error-title]\").textContent()).toBe(\n            \"Error Caught!\",\n          );\n          expect(await page.locator(\"[data-error-message]\").textContent()).toBe(\n            \"Intentional error from client loader\",\n          );\n\n          // Verify params are passed to error boundary\n          await page.waitForSelector(\"[data-error-params]\");\n          await page.waitForSelector(\"[data-error-params-type]\");\n          await page.waitForSelector(\"[data-error-params-count]\");\n          expect(\n            await page.locator(\"[data-error-params-type]\").textContent(),\n          ).toBe(\"typeof params: object\");\n          expect(\n            await page.locator(\"[data-error-params-count]\").textContent(),\n          ).toBe(\"params count: 0\");\n\n          // Ensure this is using RSC\n          validateRSCHtml(await page.content());\n        });\n\n        test(\"Passes props to client HydrateFallback\", async ({ page }) => {\n          await page.goto(`http://localhost:${port}/hydrate-fallback-props`);\n\n          // Verify the hydrate fallback is shown initially\n          await page.waitForSelector(\"[data-hydrate-fallback]\");\n          expect(\n            await page.locator(\"[data-hydrate-fallback]\").textContent(),\n          ).toBe(\"Hydrate Fallback\");\n\n          // Verify params are passed to hydrate fallback\n          await page.waitForSelector(\"[data-hydrate-params]\");\n          await page.waitForSelector(\"[data-hydrate-params-type]\");\n          await page.waitForSelector(\"[data-hydrate-params-count]\");\n          expect(\n            await page.locator(\"[data-hydrate-params-type]\").textContent(),\n          ).toBe(\"typeof params: object\");\n          expect(\n            await page.locator(\"[data-hydrate-params-count]\").textContent(),\n          ).toBe(\"params count: 0\");\n\n          // Unblock the client loader to allow it to complete\n          await page.evaluate(() => {\n            (globalThis as any).unblockClientLoader = true;\n          });\n\n          await page.waitForSelector(\"[data-home]\");\n\n          // Ensure this is using RSC\n          validateRSCHtml(await page.content());\n        });\n      });\n    });\n\n    test.describe(\"Basename\", () => {\n      let basename = \"/custom/basename\" as const;\n\n      let port: number;\n      let stopAfterAll: () => void;\n\n      test.afterAll(() => {\n        stopAfterAll?.();\n      });\n\n      test.beforeAll(async () => {\n        port = await getPort();\n        stopAfterAll = await setupRscTest({\n          implementation,\n          port,\n          files: {\n            \"src/config/basename.ts\": js`\n              // THIS FILE OVERRIDES THE DEFAULT IMPLEMENTATION\n              export const basename = ${JSON.stringify(basename)};\n            `,\n            \"src/routes.ts\": js`\n              import type { unstable_RSCRouteConfig as RSCRouteConfig } from \"react-router\";\n\n              export const routes = [\n                {\n                  id: \"root\",\n                  path: \"\",\n                  lazy: () => import(\"./routes/root\"),\n                  children: [\n                    {\n                      id: \"basic\",\n                      path: \"basic\",\n                      lazy: () => import(\"./routes/basic/home\"),\n                    },\n                    {\n                      id: \"server-redirects\",\n                      path: \"server-redirects\",\n                      children: [\n                        {\n                          id: \"server-redirects.home\",\n                          path: \"home\",\n                          lazy: () => import(\"./routes/server-redirects/home\"),\n                        },\n                        {\n                          id: \"server-redirects.redirect\",\n                          path: \"redirect\",\n                          lazy: () => import(\"./routes/server-redirects/redirect\"),\n                        },\n                        {\n                          id: \"server-redirects.target\",\n                          path: \"target\",\n                          lazy: () => import(\"./routes/server-redirects/target\"),\n                        },\n                      ]\n                    },\n                    {\n                      id: \"action-redirects\",\n                      path: \"action-redirects\",\n                      children: [\n                        {\n                          id: \"action-redirects.action-redirect\",\n                          path: \"action-redirect\",\n                          lazy: () => import(\"./routes/action-redirects/action-redirect\"),\n                        },\n                        {\n                          id: \"action-redirects.target\",\n                          path: \"target\",\n                          lazy: () => import(\"./routes/action-redirects/target\"),\n                        },\n                      ]\n                    },\n                    {\n                      id: \"server-action-redirects\",\n                      path: \"server-action-redirects\",\n                      children: [\n                        {\n                          id: \"server-action-redirects.home\",\n                          index: true,\n                          lazy: () => import(\"./routes/server-action-redirects/home\"),\n                        },\n                        {\n                          id: \"server-action-redirects.redirect\",\n                          path: \"redirect\",\n                          lazy: () => import(\"./routes/server-action-redirects/redirect\"),\n                        },\n                        {\n                          id: \"server-action-redirects.target\",\n                          path: \"target\",\n                          lazy: () => import(\"./routes/server-action-redirects/target\"),\n                        },\n                      ]\n                    }\n                  ],\n                },\n              ] satisfies RSCRouteConfig;\n            `,\n\n            \"src/routes/basic/home.tsx\": js`\n              export function loader() {\n                return { message: \"Loader Data\" };\n              }\n              export default function HomeRoute({ loaderData }) {\n                return <h2 data-home>Home: {loaderData.message}</h2>;\n              }\n            `,\n\n            \"src/routes/server-redirects/home.tsx\": js`\n              import { Link } from \"react-router\";\n\n              export default function HomeRoute() {\n                return (\n                  <div>\n                    <h2 data-home>Home Route</h2>\n                    <Link to=\"/server-redirects/redirect\" data-link-to-redirect>\n                      Go to redirect route\n                    </Link>\n                  </div>\n                );\n              }\n            `,\n            \"src/routes/server-redirects/redirect.tsx\": js`\n              import { redirect } from \"react-router\";\n\n              export function loader() {\n                throw redirect(\"/server-redirects/target\");\n              }\n\n              export default function RedirectRoute() {\n                return <h2>This should not be rendered</h2>;\n              }\n            `,\n            \"src/routes/server-redirects/target.tsx\": js`\n              export default function TargetRoute() {\n                return <h2 data-target>Target Route</h2>;\n              }\n            `,\n\n            \"src/routes/action-redirects/action-redirect.tsx\": js`\n              import { redirect } from \"react-router\";\n\n              export async function action({ request }) {\n                // Redirect to target when form is submitted\n                throw redirect(\"/action-redirects/target\");\n              }\n\n              export default function ActionRedirectRoute() {\n                return (\n                  <div>\n                    <h2 data-action-redirect>Action Redirect Route</h2>\n                    <form method=\"post\">\n                      <button type=\"submit\" data-submit-action>\n                        Submit to trigger redirect\n                      </button>\n                    </form>\n                  </div>\n                );\n              }\n            `,\n            \"src/routes/action-redirects/target.tsx\": js`\n              export default function TargetRoute() {\n                return <h2 data-target>Target Route</h2>;\n              }\n            `,\n\n            \"src/routes/server-action-redirects/home.tsx\": js`\n              import { Link } from \"react-router\";\n\n              export default function HomeRoute() {\n                return (\n                  <div>\n                    <h2 data-home>Home Route</h2>\n                    <Link to=\"/server-action-redirects/redirect\" data-link-to-redirect>\n                      Go to server action redirect route\n                    </Link>\n                  </div>\n                );\n              }\n            `,\n            \"src/routes/server-action-redirects/redirect.actions.ts\": js`\n              \"use server\";\n              import { redirect } from \"react-router\";\n\n              export async function redirectAction(formData: FormData) {\n                throw redirect(\"/server-action-redirects/target\");\n              }\n            `,\n            \"src/routes/server-action-redirects/redirect.tsx\": js`\n              export { default } from \"./redirect.client\";\n            `,\n            \"src/routes/server-action-redirects/redirect.client.tsx\": js`\n              \"use client\";\n\n              import { useActionState } from \"react\";\n              import { redirectAction } from \"./redirect.actions\";\n\n              export default function RedirectRoute() {\n                const [state, formAction, isPending] = useActionState(redirectAction, null);\n\n                return (\n                  <div>\n                    <h2 data-redirect>Server Action Redirect Route</h2>\n                    <form action={formAction}>\n                      <button type=\"submit\" data-submit-action disabled={isPending}>\n                        {isPending ? \"Redirecting...\" : \"Redirect to Target\"}\n                      </button>\n                    </form>\n                  </div>\n                );\n              }\n            `,\n            \"src/routes/server-action-redirects/target.tsx\": js`\n              export default function TargetRoute() {\n                return <h2 data-target>Target Route</h2>;\n              }\n            `,\n          },\n        });\n      });\n\n      test(\"Renders a page with a custom basename\", async ({ page }) => {\n        await page.goto(`http://localhost:${port}${basename}/basic`);\n        await page.waitForSelector(\"[data-home]\");\n        expect(await page.locator(\"[data-home]\").textContent()).toBe(\n          \"Home: Loader Data\",\n        );\n\n        // Ensure this is using RSC\n        validateRSCHtml(await page.content());\n      });\n\n      test(\"Handles server-side redirects with basename\", async ({ page }) => {\n        // Navigate directly to redirect route with basename\n        await page.goto(\n          `http://localhost:${port}${basename}/server-redirects/redirect`,\n        );\n\n        // Should be redirected to target route\n        await page.waitForURL(\n          `http://localhost:${port}${basename}/server-redirects/target`,\n        );\n        await page.waitForSelector(\"[data-target]\");\n        expect(await page.locator(\"[data-target]\").textContent()).toBe(\n          \"Target Route\",\n        );\n\n        // Ensure this is using RSC\n        validateRSCHtml(await page.content());\n      });\n\n      test(\"Handles server-side redirects in route actions with basename\", async ({\n        page,\n      }) => {\n        // Navigate to action redirect route with basename\n        await page.goto(\n          `http://localhost:${port}${basename}/action-redirects/action-redirect`,\n        );\n        await page.waitForSelector(\"[data-action-redirect]\");\n        expect(await page.locator(\"[data-action-redirect]\").textContent()).toBe(\n          \"Action Redirect Route\",\n        );\n\n        // Mutate the window object so we can check if the navigation occurred\n        // within the same browser context\n        await page.evaluate(() => {\n          // @ts-expect-error\n          window.__isWithinSameBrowserContext = true;\n        });\n\n        // Submit the form to trigger the action redirect\n        await page.click(\"[data-submit-action]\");\n\n        // Should be redirected to target route\n        await page.waitForURL(\n          `http://localhost:${port}${basename}/action-redirects/target`,\n        );\n        await page.waitForSelector(\"[data-target]\");\n        expect(await page.locator(\"[data-target]\").textContent()).toBe(\n          \"Target Route\",\n        );\n\n        // Ensure a document navigation occurred\n        expect(\n          await page.evaluate(() => {\n            // @ts-expect-error\n            return window.__isWithinSameBrowserContext;\n          }),\n        ).not.toBe(true);\n\n        // Ensure this is using RSC\n        validateRSCHtml(await page.content());\n      });\n\n      test(\"Supports redirects in server actions with basename\", async ({\n        page,\n      }) => {\n        // Start on home route\n        await page.goto(\n          `http://localhost:${port}${basename}/server-action-redirects`,\n        );\n        await page.waitForSelector(\"[data-home]\");\n        expect(await page.locator(\"[data-home]\").textContent()).toBe(\n          \"Home Route\",\n        );\n\n        // Navigate to redirect route via client navigation\n        await page.click(\"[data-link-to-redirect]\");\n        await page.waitForSelector(\"[data-redirect]\");\n        expect(await page.locator(\"[data-redirect]\").textContent()).toBe(\n          \"Server Action Redirect Route\",\n        );\n\n        // Submit the form to trigger server action redirect\n        await page.click(\"[data-submit-action]\");\n\n        // Should be redirected to target route\n        await page.waitForURL(\n          `http://localhost:${port}${basename}/server-action-redirects/target`,\n        );\n        await page.waitForSelector(\"[data-target]\");\n        expect(await page.locator(\"[data-target]\").textContent()).toBe(\n          \"Target Route\",\n        );\n\n        // Ensure this is using RSC\n        validateRSCHtml(await page.content());\n      });\n\n      test.describe(\"Without JavaScript\", () => {\n        test.use({ javaScriptEnabled: false });\n        test(\"Supports redirects in server actions without JavaScript with basename\", async ({\n          page,\n        }) => {\n          // Start on home route\n          await page.goto(\n            `http://localhost:${port}${basename}/server-action-redirects`,\n          );\n          await page.waitForSelector(\"[data-home]\");\n          expect(await page.locator(\"[data-home]\").textContent()).toBe(\n            \"Home Route\",\n          );\n\n          // Navigate to redirect route via client navigation\n          await page.click(\"[data-link-to-redirect]\");\n          await page.waitForSelector(\"[data-redirect]\");\n          expect(await page.locator(\"[data-redirect]\").textContent()).toBe(\n            \"Server Action Redirect Route\",\n          );\n\n          // Submit the form to trigger server action redirect\n          await page.click(\"[data-submit-action]\");\n\n          // Should be redirected to target route\n          await page.waitForURL(\n            `http://localhost:${port}${basename}/server-action-redirects/target`,\n          );\n          await page.waitForSelector(\"[data-target]\");\n          expect(await page.locator(\"[data-target]\").textContent()).toBe(\n            \"Target Route\",\n          );\n\n          // Ensure this is using RSC\n          validateRSCHtml(await page.content());\n        });\n      });\n    });\n  });\n});\n"
  },
  {
    "path": "integration/rsc/utils.ts",
    "content": "import { expect } from \"@playwright/test\";\nimport { sync as spawnSync } from \"cross-spawn\";\n\nimport {\n  type TemplateName,\n  createDev,\n  createProject,\n} from \"../helpers/vite.js\";\n\nexport const js = String.raw;\n\nexport type Implementation = {\n  name: string;\n  template: TemplateName;\n  /** Build a production app */\n  build: ({ cwd }: { cwd: string }) => ReturnType<typeof spawnSync>;\n  /** Run a production app */\n  run: ({ cwd, port }: { cwd: string; port: number }) => Promise<() => void>;\n  /** Run the dev server */\n  dev: ({ cwd, port }: { cwd: string; port: number }) => Promise<() => void>;\n};\n\nexport const implementations: Implementation[] = [\n  {\n    name: \"vite\",\n    template: \"rsc-vite\",\n    build: ({ cwd }: { cwd: string }) => spawnSync(\"pnpm\", [\"build\"], { cwd }),\n    run: ({ cwd, port }) =>\n      createDev([\"server.js\", \"-p\", String(port)])({\n        cwd,\n        port,\n        env: {\n          NODE_ENV: \"production\",\n        },\n      }),\n    dev: ({ cwd, port }) =>\n      createDev([\"node_modules/vite/bin/vite.js\", \"--port\", String(port)])({\n        cwd,\n        port,\n      }),\n  },\n] as Implementation[];\n\nexport async function setupRscTest({\n  implementation,\n  port,\n  dev,\n  files,\n}: {\n  implementation: Implementation;\n  port: number;\n  dev?: boolean;\n  files: Record<string, string>;\n}) {\n  let cwd = await createProject(files, implementation.template);\n\n  let { error, status, stderr, stdout } = implementation.build({ cwd });\n  if (status !== 0) {\n    console.error(\"Error building project\", {\n      status,\n      error,\n      stdout: stdout?.toString(),\n      stderr: stderr?.toString(),\n    });\n    throw new Error(\"Error building project\");\n  }\n  return dev\n    ? implementation.dev({ cwd, port })\n    : implementation.run({ cwd, port });\n}\n\nexport const validateRSCHtml = (html: string) =>\n  expect(html).toMatch(/\\(self\\.__FLIGHT_DATA\\|\\|=\\[\\]\\)\\.push\\(/);\n"
  },
  {
    "path": "integration/scroll-test.ts",
    "content": "import { test, expect } from \"@playwright/test\";\n\nimport { PlaywrightFixture } from \"./helpers/playwright-fixture.js\";\nimport type { Fixture, AppFixture } from \"./helpers/create-fixture.js\";\nimport {\n  createAppFixture,\n  createFixture,\n  js,\n} from \"./helpers/create-fixture.js\";\n\nlet fixture: Fixture;\nlet appFixture: AppFixture;\n\ntest.beforeAll(async () => {\n  fixture = await createFixture({\n    files: {\n      \"app/routes/_index.tsx\": js`\n        import { redirect, Form } from \"react-router\";\n\n        export function action() {\n          return redirect(\"/test\");\n        };\n\n        export default function Component() {\n          return (\n            <>\n              <h1>Index Page - Scroll Down</h1>\n              <Form method=\"post\" style={{ marginTop: \"150vh\" }}>\n                <button type=\"submit\">Submit</button>\n              </Form>\n            </>\n          );\n        }\n      `,\n\n      \"app/routes/test.tsx\": js`\n        export default function Component() {\n          return (\n            <>\n              <h1 id=\"redirected\">Redirected!</h1>\n              <p style={{ marginTop: \"150vh\" }}>I should not be visible!!</p>\n            </>\n          );\n        }\n      `,\n\n      \"app/routes/hash.tsx\": js`\n        import { Link } from \"react-router\";\n\n        export default function Component() {\n          return (\n            <>\n              <h1>Hash Scrolling</h1>\n              <Link to=\"#hello-world\">hash link to hello-world</Link>\n              <Link to=\"#hello 🌎\">hash link to hello 🌎</Link>\n              <div style={{ height: '3000px' }}>Spacer Div</div>\n              <p id=\"hello-world\">hello-world scroll target</p>\n              <p id=\"hello 🌎\">hello 🌎 scroll target</p>\n            </>\n          );\n        }\n      `,\n    },\n  });\n\n  // This creates an interactive app using playwright.\n  appFixture = await createAppFixture(fixture);\n});\n\ntest.afterAll(() => {\n  appFixture.close();\n});\n\ntest.describe(\"without JavaScript\", () => {\n  test.use({ javaScriptEnabled: false });\n  runTests();\n});\n\ntest.describe(\"with JavaScript\", () => {\n  test.use({ javaScriptEnabled: true });\n  runTests();\n});\n\nfunction runTests() {\n  test(\"page scroll should be at the top on the new page\", async ({ page }) => {\n    let app = new PlaywrightFixture(appFixture, page);\n    await app.goto(\"/\");\n\n    // Scroll to the bottom and submit\n    await page.evaluate(() => window.scrollTo(0, document.body.scrollHeight));\n    let scroll = await page.evaluate(() => window.scrollY);\n    expect(scroll).toBeGreaterThan(0);\n    await app.clickSubmitButton(\"/?index\");\n    await page.waitForSelector(\"#redirected\");\n\n    // Ensure we scrolled back to the top\n    scroll = await page.evaluate(() => window.scrollY);\n    expect(scroll).toBe(0);\n  });\n\n  test(\"should scroll to hash locations\", async ({ page }) => {\n    let app = new PlaywrightFixture(appFixture, page);\n    await app.goto(\"/hash\");\n    let scroll = await page.evaluate(() => window.scrollY);\n    expect(scroll).toBe(0);\n    await app.clickLink(\"/hash#hello-world\");\n    await new Promise((r) => setTimeout(r, 0));\n    scroll = await page.evaluate(() => window.scrollY);\n    expect(scroll).toBeGreaterThan(0);\n  });\n\n  test(\"should scroll to hash locations with URL encoded characters\", async ({\n    page,\n  }) => {\n    let app = new PlaywrightFixture(appFixture, page);\n    await app.goto(\"/hash\");\n    let scroll = await page.evaluate(() => window.scrollY);\n    expect(scroll).toBe(0);\n    await app.clickLink(\"/hash#hello 🌎\");\n    await new Promise((r) => setTimeout(r, 0));\n    scroll = await page.evaluate(() => window.scrollY);\n    expect(scroll).toBeGreaterThan(0);\n  });\n}\n"
  },
  {
    "path": "integration/server-entry-test.ts",
    "content": "import { test, expect } from \"@playwright/test\";\n\nimport { createFixture, js } from \"./helpers/create-fixture.js\";\nimport type { Fixture } from \"./helpers/create-fixture.js\";\nimport { selectHtml } from \"./helpers/playwright-fixture.js\";\n\ntest.describe(\"Custom Server Entry\", () => {\n  let fixture: Fixture;\n\n  let DATA_HEADER_NAME = \"X-Macaroni-Salad\";\n  let DATA_HEADER_VALUE = \"Smoked Mozarella\";\n\n  test.beforeAll(async () => {\n    fixture = await createFixture({\n      files: {\n        \"app/entry.server.tsx\": js`\n          export default function handleRequest() {\n            return new Response(\"\");\n          }\n\n          export function handleDataRequest(response) {\n            response.headers.set(\"${DATA_HEADER_NAME}\", \"${DATA_HEADER_VALUE}\");\n            return response;\n          }\n        `,\n\n        \"app/routes/_index.tsx\": js`\n          export function loader() {\n            return \"\"\n          }\n          export default function () {\n            return <div/>\n          }\n        `,\n      },\n    });\n  });\n\n  test(\"can manipulate a data response\", async () => {\n    let response = await fixture.requestSingleFetchData(\"/.data\");\n    expect(response.headers.get(DATA_HEADER_NAME)).toBe(DATA_HEADER_VALUE);\n  });\n});\n\ntest.describe(\"Default Server Entry\", () => {\n  let fixture: Fixture;\n\n  test.beforeAll(async () => {\n    fixture = await createFixture({\n      files: {\n        \"app/routes/_index.tsx\": js`\n          export default function () {\n            return <p>Hello World</p>\n          }\n        `,\n      },\n    });\n  });\n\n  test(\"renders\", async () => {\n    let response = await fixture.requestDocument(\"/\");\n    expect(await selectHtml(await response.text(), \"p\")).toBe(\n      \"<p>Hello World</p>\",\n    );\n  });\n});\n"
  },
  {
    "path": "integration/session-storage-denied-test.ts",
    "content": "import { test, expect } from \"@playwright/test\";\nimport {\n  createAppFixture,\n  createFixture,\n  js,\n  type AppFixture,\n  type Fixture,\n} from \"./helpers/create-fixture.js\";\nimport { PlaywrightFixture } from \"./helpers/playwright-fixture.js\";\n\ntest.describe(\"sessionStorage denied\", () => {\n  let fixture: Fixture;\n  let appFixture: AppFixture;\n\n  test.beforeAll(async () => {\n    fixture = await createFixture({\n      files: {\n        \"app/root.tsx\": js`\n          import * as React from \"react\";\n          import { Link, Links, Meta, Outlet, Scripts, useRouteError } from \"react-router\";\n          \n          export function ErrorBoundary() {\n            const error = useRouteError();\n            console.error(\"ErrorBoundary caught:\", error);\n            return (\n              <html>\n                <head>\n                  <title>Error</title>\n                  <Meta />\n                  <Links />\n                </head>\n                <body>\n                  <h1>Application Error</h1>\n                  <pre>{error?.message || \"Unknown error\"}</pre>\n                  <Scripts />\n                </body>\n              </html>\n            );\n          }\n          \n          export default function Root() {\n            return (\n              <html lang=\"en\">\n                <head>\n                  <Meta />\n                  <Links />\n                </head>\n                <body>\n                  <nav>\n                    <Link to=\"/\">Home</Link>{\" | \"}\n                    <Link to=\"/docs\">Docs</Link>\n                  </nav>\n                  <Outlet />\n                  <Scripts />\n                </body>\n              </html>\n            );\n          }\n        `,\n        \"app/routes/_index.tsx\": js`\n          export default function Index() {\n            return <h1>Home</h1>;\n          }\n        `,\n        \"app/routes/docs.tsx\": js`\n          export default function Docs() {\n            return <h1>Documentation</h1>;\n          }\n        `,\n      },\n    });\n    appFixture = await createAppFixture(fixture);\n  });\n\n  test(\"should handle navigation gracefully when storage is blocked\", async ({\n    page,\n    context,\n  }) => {\n    await context.addInitScript(() => {\n      const storageError = new DOMException(\n        \"Failed to read the 'sessionStorage' property from 'Window': Access is denied for this document.\",\n        \"SecurityError\",\n      );\n\n      [\"sessionStorage\", \"localStorage\"].forEach((storage) => {\n        Object.defineProperty(window, storage, {\n          get() {\n            throw storageError;\n          },\n          set() {\n            throw storageError;\n          },\n          configurable: false,\n        });\n      });\n    });\n\n    let app = new PlaywrightFixture(appFixture, page);\n\n    await app.goto(\"/\");\n    await expect(page.locator(\"h1\")).toContainText(\"Home\");\n\n    await app.clickLink(\"/docs\");\n    await expect(page).toHaveURL(/\\/docs$/);\n    await expect(page.locator(\"h1\")).toContainText(\"Documentation\");\n\n    await page.goBack();\n    await expect(page).toHaveURL(/\\/$/);\n    await expect(page.locator(\"h1\")).toContainText(\"Home\");\n\n    await page.goForward();\n    await expect(page).toHaveURL(/\\/docs$/);\n    await expect(page.locator(\"h1\")).toContainText(\"Documentation\");\n  });\n});\n"
  },
  {
    "path": "integration/set-cookie-revalidation-test.ts",
    "content": "import { test, expect } from \"@playwright/test\";\n\nimport {\n  createAppFixture,\n  createFixture,\n  js,\n} from \"./helpers/create-fixture.js\";\nimport type { Fixture, AppFixture } from \"./helpers/create-fixture.js\";\nimport { PlaywrightFixture } from \"./helpers/playwright-fixture.js\";\n\nlet fixture: Fixture;\nlet appFixture: AppFixture;\n\nlet BANNER_MESSAGE = \"you do not have permission to view /protected\";\n\ntest.describe(\"set-cookie revalidation\", () => {\n  test.beforeAll(async () => {\n    fixture = await createFixture({\n      files: {\n        \"app/session.server.ts\": js`\n            import { createCookieSessionStorage } from \"react-router\";\n\n            export let MESSAGE_KEY = \"message\";\n\n            export let sessionStorage = createCookieSessionStorage({\n              cookie: {\n                httpOnly: true,\n                path: \"/\",\n                sameSite: \"lax\",\n                secrets: [\"cookie-secret\"],\n              }\n            })\n          `,\n\n        \"app/root.tsx\": js`\n            import {\n              data,\n              Links,\n              Meta,\n              Outlet,\n              Scripts,\n              useLoaderData,\n            } from \"react-router\";\n\n            import { sessionStorage, MESSAGE_KEY } from \"~/session.server\";\n\n            export const loader = async ({ request }) => {\n              let session = await sessionStorage.getSession(request.headers.get(\"Cookie\"));\n              let message = session.get(MESSAGE_KEY) || null;\n\n              return data(message, {\n                headers: {\n                  \"Set-Cookie\": await sessionStorage.commitSession(session),\n                },\n              });\n            };\n\n            export default function Root() {\n              const message = useLoaderData();\n\n              return (\n                <html lang=\"en\">\n                  <head>\n                    <Meta />\n                    <Links />\n                  </head>\n                  <body>\n                    {!!message && <p id=\"message\">{message}</p>}\n                    <Outlet />\n                    <Scripts />\n                  </body>\n                </html>\n              );\n            }\n          `,\n\n        \"app/routes/_index.tsx\": js`\n            import { Link } from \"react-router\";\n\n            export default function Index() {\n              return (\n                <p>\n                  <Link to=\"/protected\">protected</Link>\n                </p>\n              );\n            }\n          `,\n\n        \"app/routes/login.tsx\": js`\n            export default function Login() {\n              return <p>login</p>;\n            }\n          `,\n\n        \"app/routes/protected.tsx\": js`\n            import { redirect } from \"react-router\";\n\n            import { sessionStorage, MESSAGE_KEY } from \"~/session.server\";\n\n            export let loader = async ({ request }) => {\n              let session = await sessionStorage.getSession(request.headers.get(\"Cookie\"));\n\n              session.flash(MESSAGE_KEY, \"${BANNER_MESSAGE}\");\n\n              return redirect(\"/login\", {\n                headers: {\n                  \"Set-Cookie\": await sessionStorage.commitSession(session),\n                },\n              });\n            };\n\n            export default function Protected() {\n              return <p>protected</p>;\n            }\n          `,\n      },\n    });\n\n    // This creates an interactive app using playwright.\n    appFixture = await createAppFixture(fixture);\n  });\n\n  test.afterAll(() => {\n    appFixture.close();\n  });\n\n  test(\"should revalidate when cookie is set on redirect from loader\", async ({\n    page,\n  }) => {\n    let app = new PlaywrightFixture(appFixture, page);\n    await app.goto(\"/\");\n    await app.clickLink(\"/protected\");\n    await page.waitForSelector(`#message:has-text(\"${BANNER_MESSAGE}\")`);\n    expect(await app.getHtml()).toMatch(BANNER_MESSAGE);\n  });\n});\n"
  },
  {
    "path": "integration/single-fetch-test.ts",
    "content": "import { test, expect } from \"@playwright/test\";\nimport {\n  UNSAFE_ServerMode as ServerMode,\n  UNSAFE_SingleFetchRedirectSymbol as SingleFetchRedirectSymbol,\n} from \"react-router\";\n\nimport {\n  createAppFixture,\n  createFixture,\n  js,\n} from \"./helpers/create-fixture.js\";\nimport { PlaywrightFixture } from \"./helpers/playwright-fixture.js\";\nimport {\n  EXPRESS_SERVER,\n  createProject,\n  customDev,\n  reactRouterConfig,\n  viteConfig,\n} from \"./helpers/vite.js\";\nimport getPort from \"get-port\";\n\nconst ISO_DATE = \"2024-03-12T12:00:00.000Z\";\n\nconst files = {\n  \"app/root.tsx\": js`\n    import { Form, Link, Links, Meta, Outlet, Scripts } from \"react-router\";\n\n    export function headers ({ actionHeaders, loaderHeaders, errorHeaders }) {\n      if (errorHeaders) {\n        return errorHeaders;\n      } else if ([...actionHeaders].length > 0) {\n        return actionHeaders;\n      } else {\n        return loaderHeaders;\n      }\n    }\n\n    export function loader() {\n      return {\n        message: \"ROOT\",\n      };\n    }\n\n    export default function Root() {\n      return (\n        <html lang=\"en\">\n          <head>\n            <Meta />\n            <Links />\n          </head>\n          <body>\n            <Link to=\"/\">Go to Home</Link><br/>\n            <Link to=\"/data\">Go to Data</Link><br/>\n            <Link to=\"/a/b/c\">Go to /a/b/c</Link><br/>\n            <Form method=\"post\" action=\"/data\">\n              <button type=\"submit\" name=\"key\" value=\"value\">\n                Submit\n              </button>\n            </Form>\n            <Outlet />\n            <Scripts />\n          </body>\n        </html>\n      );\n    }\n  `,\n\n  \"app/routes/_index.tsx\": js`\n    export default function Index() {\n      return <h1>Index</h1>\n    }\n  `,\n\n  \"app/routes/data.tsx\": js`\n    import { useActionData, useLoaderData } from \"react-router\";\n\n    export async function action({ request }) {\n      let formData = await request.formData();\n      return {\n        key: formData.get('key'),\n      };\n    }\n\n    class MyClass {\n      a: string\n      b: bigint\n      constructor(a: string, b: bigint) {\n        this.a = a\n        this.b = b\n      }\n      c() {}\n    }\n\n    export function loader({ request }) {\n      if (new URL(request.url).searchParams.has(\"error\")) {\n        throw new Error(\"Loader Error\");\n      }\n      return {\n        message: \"DATA\",\n        date: new Date(\"${ISO_DATE}\"),\n        function: () => {},\n        class: new MyClass(\"hello\", BigInt(1)),\n      };\n    }\n\n    export default function Index() {\n      let data = useLoaderData();\n      let actionData = useActionData();\n      return (\n        <>\n          <h1 id=\"heading\">Data Route</h1>\n          <p id=\"message\">{data.message}</p>\n          <p id=\"date\">{data.date.toISOString()}</p>\n          {actionData ? <p id=\"action-data\">{actionData.key}</p> : null}\n        </>\n      )\n    }\n  `,\n\n  \"app/routes/data-with-response.tsx\": js`\n    import { useActionData, useLoaderData, data } from \"react-router\";\n\n    export function headers ({ actionHeaders, loaderHeaders, errorHeaders }) {\n      if ([...actionHeaders].length > 0) {\n        return actionHeaders;\n      } else {\n        return loaderHeaders;\n      }\n    }\n\n    export async function action({ request }) {\n      let formData = await request.formData();\n      return data({\n        key: formData.get('key'),\n      }, { status: 201, headers: { 'X-Action': 'yes' }});\n    }\n\n    class MyClass {\n      a: string\n      b: Date\n      constructor(a: string, b: Date) {\n        this.a = a\n        this.b = b\n      }\n      c() {}\n    }\n\n    export function loader({ request }) {\n      if (new URL(request.url).searchParams.has(\"error\")) {\n        throw new Error(\"Loader Error\");\n      }\n      return data({\n        message: \"DATA\",\n        date: new Date(\"${ISO_DATE}\"),\n        function: () => {},\n        class: new MyClass(\"hello\", BigInt(1)),\n      }, { status: 206, headers: { 'X-Loader': 'yes' }});\n    }\n\n    export default function DataWithResponse() {\n      let data = useLoaderData();\n      let actionData = useActionData();\n      return (\n        <>\n          <h1 id=\"heading\">Data</h1>\n          <p id=\"message\">{data.message}</p>\n          <p id=\"date\">{data.date.toISOString()}</p>\n          {actionData ? <p id=\"action-data\">{actionData.key}</p> : null}\n        </>\n      )\n    }\n  `,\n\n  \"app/routes/invalid-date.tsx\": js`\n    import { useLoaderData, data } from \"react-router\";\n\n    export function loader({ request }) {\n      return data({ invalidDate: new Date(\"invalid\") });\n    }\n\n    export default function InvalidDate() {\n      let data = useLoaderData();\n      return (\n        <>\n          <h1 id=\"heading\">Invalid Date</h1>\n          <p id=\"date\">{data.invalidDate.toISOString()}</p>\n        </>\n      )\n    }\n  `,\n};\n\ntest.describe(\"single-fetch\", () => {\n  let oldConsoleError: typeof console.error;\n\n  test.beforeEach(() => {\n    oldConsoleError = console.error;\n  });\n\n  test.afterEach(() => {\n    console.error = oldConsoleError;\n  });\n\n  test(\"loads proper data on single fetch loader requests\", async () => {\n    let fixture = await createFixture({\n      files,\n    });\n    let res = await fixture.requestSingleFetchData(\"/_root.data\");\n    expect(res.data).toEqual({\n      root: {\n        data: {\n          message: \"ROOT\",\n        },\n      },\n    });\n    expect(res.headers.get(\"Content-Type\")).toBe(\"text/x-script\");\n\n    res = await fixture.requestSingleFetchData(\"/data.data\");\n    expect(res.data).toStrictEqual({\n      root: {\n        data: {\n          message: \"ROOT\",\n        },\n      },\n      \"routes/data\": {\n        data: {\n          message: \"DATA\",\n          date: new Date(ISO_DATE),\n          function: undefined,\n          class: {\n            a: \"hello\",\n            b: BigInt(1),\n          },\n        },\n      },\n    });\n\n    res = await fixture.requestSingleFetchData(\"/invalid-date.data\");\n    expect(res.data).toEqual({\n      root: {\n        data: {\n          message: \"ROOT\",\n        },\n      },\n      \"routes/invalid-date\": {\n        data: {\n          invalidDate: expect.any(Date),\n        },\n      },\n    });\n\n    let date = (\n      res.data as { [\"routes/invalid-date\"]: { data: { invalidDate: Date } } }\n    )[\"routes/invalid-date\"].data.invalidDate;\n    expect(isNaN(date.getTime())).toBe(true);\n  });\n\n  test(\"loads proper errors on single fetch loader requests\", async () => {\n    console.error = () => {};\n\n    let fixture = await createFixture(\n      {\n        files,\n      },\n      ServerMode.Development,\n    );\n\n    let res = await fixture.requestSingleFetchData(\"/data.data?error=true\");\n    expect(res.data).toEqual({\n      root: {\n        data: {\n          message: \"ROOT\",\n        },\n      },\n      \"routes/data\": {\n        error: new Error(\"Loader Error\"),\n      },\n    });\n  });\n\n  test(\"loads proper data on single fetch action requests\", async () => {\n    let fixture = await createFixture({\n      files,\n    });\n    let postBody = new URLSearchParams();\n    postBody.set(\"key\", \"value\");\n    let res = await fixture.requestSingleFetchData(\"/data.data\", {\n      method: \"post\",\n      body: postBody,\n    });\n    expect(res.data).toEqual({\n      data: {\n        key: \"value\",\n      },\n    });\n  });\n\n  test(\"loads proper data (via data()) on single fetch loader requests\", async () => {\n    let fixture = await createFixture({\n      files,\n    });\n    let res = await fixture.requestSingleFetchData(\"/data-with-response.data\");\n    expect(res.status).toEqual(206);\n    expect(res.headers.get(\"X-Loader\")).toEqual(\"yes\");\n    expect(res.data).toStrictEqual({\n      root: {\n        data: {\n          message: \"ROOT\",\n        },\n      },\n      \"routes/data-with-response\": {\n        data: {\n          message: \"DATA\",\n          date: new Date(ISO_DATE),\n          function: undefined,\n          class: {\n            a: \"hello\",\n            b: BigInt(1),\n          },\n        },\n      },\n    });\n  });\n\n  test(\"loads proper data (via data()) on single fetch action requests\", async () => {\n    let fixture = await createFixture({\n      files,\n    });\n    let postBody = new URLSearchParams();\n    postBody.set(\"key\", \"value\");\n    let res = await fixture.requestSingleFetchData(\"/data-with-response.data\", {\n      method: \"post\",\n      body: postBody,\n    });\n    expect(res.status).toEqual(201);\n    expect(res.headers.get(\"X-Action\")).toEqual(\"yes\");\n    expect(res.data).toEqual({\n      data: {\n        key: \"value\",\n      },\n    });\n  });\n\n  test(\"loads proper data on document request\", async ({ page }) => {\n    let fixture = await createFixture({\n      files,\n    });\n    let appFixture = await createAppFixture(fixture);\n    let app = new PlaywrightFixture(appFixture, page);\n    await app.goto(\"/data\");\n    expect(await app.getHtml(\"#heading\")).toContain(\"Data\");\n    expect(await app.getHtml(\"#message\")).toContain(\"DATA\");\n    expect(await app.getHtml(\"#date\")).toContain(ISO_DATE);\n  });\n\n  test(\"allows SSR loaders to return undefined\", async ({ page }) => {\n    let fixture = await createFixture({\n      files: {\n        ...files,\n        \"app/routes/_index.tsx\": js`\n          export function loader() {}\n          export default function Index() {\n            return <h1>Index</h1>\n          }\n        `,\n      },\n    });\n    let appFixture = await createAppFixture(fixture);\n    let app = new PlaywrightFixture(appFixture, page);\n    await app.goto(\"/\", true);\n    expect(await app.getHtml(\"h1\")).toContain(\"Index\");\n  });\n\n  test(\"loads proper data on client side navigation\", async ({ page }) => {\n    let fixture = await createFixture({\n      files,\n    });\n    let appFixture = await createAppFixture(fixture);\n    let app = new PlaywrightFixture(appFixture, page);\n    await app.goto(\"/\");\n    await app.clickLink(\"/data\");\n    await page.waitForSelector(\"#message\");\n    expect(await app.getHtml(\"#heading\")).toContain(\"Data\");\n    expect(await app.getHtml(\"#message\")).toContain(\"DATA\");\n    expect(await app.getHtml(\"#date\")).toContain(ISO_DATE);\n  });\n\n  test(\"loads proper data on client side action navigation\", async ({\n    page,\n  }) => {\n    let fixture = await createFixture({\n      files,\n    });\n    let appFixture = await createAppFixture(fixture);\n    let app = new PlaywrightFixture(appFixture, page);\n    await app.goto(\"/\");\n    await app.clickSubmitButton(\"/data\");\n    await page.waitForSelector(\"#message\");\n    expect(await app.getHtml(\"#heading\")).toContain(\"Data\");\n    expect(await app.getHtml(\"#message\")).toContain(\"DATA\");\n    expect(await app.getHtml(\"#date\")).toContain(ISO_DATE);\n    expect(await app.getHtml(\"#action-data\")).toContain(\"value\");\n  });\n\n  test(\"allows fine-grained revalidation\", async ({ page }) => {\n    let fixture = await createFixture({\n      files: {\n        ...files,\n        \"app/routes/no-revalidate.tsx\": js`\n          import { Form, useActionData, useLoaderData, useNavigation } from 'react-router';\n\n          export async function action({ request }) {\n            let fd = await request.formData();\n            return { shouldRevalidate: fd.get('revalidate') === \"yes\" }\n          }\n\n          let count = 0;\n          export function loader() {\n            return { count: ++count };\n          }\n\n          export default function Comp() {\n            let navigation = useNavigation();\n            let data = useLoaderData();\n            let actionData = useActionData();\n            return (\n              <Form method=\"post\">\n                <button type=\"submit\" name=\"revalidate\" value=\"yes\">Submit w/Revalidation</button>\n                <button type=\"submit\" name=\"revalidate\" value=\"no\">Submit w/o Revalidation</button>\n                <p id=\"data\">{data.count}</p>\n                {navigation.state === \"idle\" ? <p id=\"idle\">idle</p> : null}\n                {actionData ? <p id=\"action-data\">yes</p> : null}\n              </Form>\n            );\n          }\n\n          export function shouldRevalidate({ actionResult }) {\n            return actionResult.shouldRevalidate === true;\n          }\n        `,\n      },\n    });\n\n    let urls: string[] = [];\n    page.on(\"request\", (req) => {\n      if (req.method() === \"GET\" && req.url().includes(\".data\")) {\n        urls.push(req.url());\n      }\n    });\n\n    let appFixture = await createAppFixture(fixture);\n    let app = new PlaywrightFixture(appFixture, page);\n    await app.goto(\"/no-revalidate\");\n    expect(await app.getHtml(\"#data\")).toContain(\"1\");\n    expect(urls).toEqual([]);\n\n    await page.click('button[name=\"revalidate\"][value=\"yes\"]');\n    await page.waitForSelector(\"#action-data\");\n    await page.waitForSelector(\"#idle\");\n    expect(await app.getHtml(\"#data\")).toContain(\"2\");\n    expect(urls).toEqual([expect.stringMatching(/\\/no-revalidate\\.data$/)]);\n\n    await page.click('button[name=\"revalidate\"][value=\"no\"]');\n    await page.waitForSelector(\"#action-data\");\n    await page.waitForSelector(\"#idle\");\n    expect(await app.getHtml(\"#data\")).toContain(\"2\");\n    expect(urls).toEqual([\n      expect.stringMatching(/\\/no-revalidate\\.data$/),\n      expect.stringMatching(/\\/no-revalidate\\.data\\?_routes=root$/),\n    ]);\n  });\n\n  test(\"revalidates on reused routes by default\", async ({ page }) => {\n    let fixture = await createFixture({\n      files: {\n        ...files,\n        \"app/routes/_index.tsx\": js`\n          import { Link } from \"react-router\";\n          export default function Index() {\n            return <Link to=\"/parent\">Go to Parent</Link>\n          }\n        `,\n        \"app/routes/parent.tsx\": js`\n          import { Link, Outlet } from \"react-router\";\n          import type { Route } from \"./+types/parent\";\n\n          let count = 0;\n          export function loader() {\n            return ++count;\n          }\n\n          export default function Parent({ loaderData }: Route.ComponentProps) {\n            return (\n              <>\n                <h1 data-parent={loaderData}>PARENT:{loaderData}</h1>\n                <Link to=\"/parent\">Go to Parent</Link><br/>\n                <Link to=\"/parent/child\">Go to Child</Link>\n                <Outlet />\n              </>\n            );\n          }\n        `,\n        \"app/routes/parent.child.tsx\": js`\n          import { Outlet } from \"react-router\";\n          import type { Route } from \"./+types/parent\";\n\n          export function loader() {\n            return \"CHILD\"\n          }\n\n          export default function Parent({ loaderData }: Route.ComponentProps) {\n            return <h2 data-child>{loaderData}</h2>\n          }\n        `,\n      },\n    });\n\n    let urls: string[] = [];\n    page.on(\"request\", (req) => {\n      let url = new URL(req.url());\n      if (req.method() === \"GET\" && url.pathname.endsWith(\".data\")) {\n        urls.push(url.pathname + url.search);\n      }\n    });\n\n    let appFixture = await createAppFixture(fixture);\n    let app = new PlaywrightFixture(appFixture, page);\n    await app.goto(\"/\", true);\n\n    await app.clickLink(\"/parent\");\n    await page.waitForSelector('[data-parent=\"1\"]');\n    expect(urls).toEqual([\"/parent.data\"]);\n    urls.length = 0;\n\n    await app.clickLink(\"/parent/child\");\n    await page.waitForSelector(\"[data-child]\");\n    await expect(page.locator('[data-parent=\"2\"]')).toBeDefined();\n    expect(urls).toEqual([\"/parent/child.data\"]);\n    urls.length = 0;\n\n    await app.clickLink(\"/parent\");\n    await page.waitForSelector('[data-parent=\"3\"]');\n    expect(urls).toEqual([\"/parent.data\"]);\n    urls.length = 0;\n  });\n\n  test(\"does not revalidate on 4xx/5xx action responses\", async ({ page }) => {\n    let fixture = await createFixture({\n      files: {\n        ...files,\n        \"app/routes/action.tsx\": js`\n          import { Form, Link, useActionData, useLoaderData, useNavigation } from 'react-router';\n\n          export async function action({ request }) {\n            let fd = await request.formData();\n            if (fd.get('throw') === \"5xx\") {\n              throw new Response(\"Thrown 500\", { status: 500 });\n            }\n            if (fd.get('throw') === \"4xx\") {\n              throw new Response(\"Thrown 400\", { status: 400 });\n            }\n            if (fd.get('return') === \"5xx\") {\n              return new Response(\"Returned 500\", { status: 500 });\n            }\n            if (fd.get('return') === \"4xx\") {\n              return  new Response(\"Returned 400\", { status: 400 });\n            }\n            return null;\n          }\n\n          let count = 0;\n          export function loader() {\n            return { count: ++count };\n          }\n\n          export default function Comp() {\n            let navigation = useNavigation();\n            let data = useLoaderData();\n            return (\n              <Form method=\"post\">\n                <button type=\"submit\" name=\"throw\" value=\"5xx\">Throw 5xx</button>\n                <button type=\"submit\" name=\"throw\" value=\"4xx\">Throw 4xx</button>\n                <button type=\"submit\" name=\"return\" value=\"5xx\">Return 5xx</button>\n                <button type=\"submit\" name=\"return\" value=\"4xx\">Return 4xx</button>\n                <p id=\"data\">{data.count}</p>\n                {navigation.state === \"idle\" ? <p id=\"idle\">idle</p> : null}\n              </Form>\n            );\n          }\n\n          export function ErrorBoundary() {\n            return (\n              <div>\n                <h1 id=\"error\">Error</h1>\n                <Link to=\"/action\">Back</Link>\n              </div>\n            );\n          }\n        `,\n      },\n    });\n\n    let urls: string[] = [];\n    page.on(\"request\", (req) => {\n      if (req.method() === \"GET\" && req.url().includes(\".data\")) {\n        urls.push(req.url());\n      }\n    });\n\n    console.error = () => {};\n\n    let appFixture = await createAppFixture(fixture);\n    let app = new PlaywrightFixture(appFixture, page);\n    await app.goto(\"/action\");\n    expect(await app.getHtml(\"#data\")).toContain(\"1\");\n    expect(urls).toEqual([]);\n\n    await page.click('button[name=\"return\"][value=\"5xx\"]');\n    await page.waitForSelector(\"#idle\");\n    expect(await app.getHtml(\"#data\")).toContain(\"1\");\n    expect(urls).toEqual([]);\n\n    await page.click('button[name=\"return\"][value=\"4xx\"]');\n    await page.waitForSelector(\"#idle\");\n    expect(await app.getHtml(\"#data\")).toContain(\"1\");\n    expect(urls).toEqual([]);\n\n    await page.click('button[name=\"throw\"][value=\"5xx\"]');\n    await page.waitForSelector(\"#error\");\n    expect(urls).toEqual([]);\n\n    await app.clickLink(\"/action\");\n    await page.waitForSelector(\"#data\");\n    expect(await app.getHtml(\"#data\")).toContain(\"2\");\n    urls = [];\n\n    await page.click('button[name=\"throw\"][value=\"4xx\"]');\n    await page.waitForSelector(\"#error\");\n    expect(urls).toEqual([]);\n  });\n\n  test(\"does not revalidate on 4xx/5xx action responses (via data())\", async ({\n    page,\n  }) => {\n    let fixture = await createFixture({\n      files: {\n        ...files,\n        \"app/routes/action.tsx\": js`\n          import { Form, Link, useActionData, useLoaderData, useNavigation, data } from 'react-router';\n\n          export async function action({ request }) {\n            let fd = await request.formData();\n            if (fd.get('throw') === \"5xx\") {\n              throw data(\"Thrown 500\", { status: 500 });\n            }\n            if (fd.get('throw') === \"4xx\") {\n              throw data(\"Thrown 400\", { status: 400 });\n            }\n            if (fd.get('return') === \"5xx\") {\n              return data(\"Returned 500\", { status: 500 });\n            }\n            if (fd.get('return') === \"4xx\") {\n              return data(\"Returned 400\", { status: 400 });\n            }\n            return null;\n          }\n\n          let count = 0;\n          export function loader() {\n            return { count: ++count };\n          }\n\n          export default function Comp() {\n            let navigation = useNavigation();\n            let data = useLoaderData();\n            return (\n              <Form method=\"post\">\n                <button type=\"submit\" name=\"throw\" value=\"5xx\">Throw 5xx</button>\n                <button type=\"submit\" name=\"throw\" value=\"4xx\">Throw 4xx</button>\n                <button type=\"submit\" name=\"return\" value=\"5xx\">Return 5xx</button>\n                <button type=\"submit\" name=\"return\" value=\"4xx\">Return 4xx</button>\n                <p id=\"data\">{data.count}</p>\n                {navigation.state === \"idle\" ? <p id=\"idle\">idle</p> : null}\n              </Form>\n            );\n          }\n\n          export function ErrorBoundary() {\n            return (\n              <div>\n                <h1 id=\"error\">Error</h1>\n                <Link to=\"/action\">Back</Link>\n              </div>\n            );\n          }\n        `,\n      },\n    });\n\n    let urls: string[] = [];\n    page.on(\"request\", (req) => {\n      if (req.method() === \"GET\" && req.url().includes(\".data\")) {\n        urls.push(req.url());\n      }\n    });\n\n    console.error = () => {};\n\n    let appFixture = await createAppFixture(fixture);\n    let app = new PlaywrightFixture(appFixture, page);\n    await app.goto(\"/action\");\n    expect(await app.getHtml(\"#data\")).toContain(\"1\");\n    expect(urls).toEqual([]);\n\n    await page.click('button[name=\"return\"][value=\"5xx\"]');\n    await page.waitForSelector(\"#idle\");\n    expect(await app.getHtml(\"#data\")).toContain(\"1\");\n    expect(urls).toEqual([]);\n\n    await page.click('button[name=\"return\"][value=\"4xx\"]');\n    await page.waitForSelector(\"#idle\");\n    expect(await app.getHtml(\"#data\")).toContain(\"1\");\n    expect(urls).toEqual([]);\n\n    await page.click('button[name=\"throw\"][value=\"5xx\"]');\n    await page.waitForSelector(\"#error\");\n    expect(urls).toEqual([]);\n\n    await app.clickLink(\"/action\");\n    await page.waitForSelector(\"#data\");\n    expect(await app.getHtml(\"#data\")).toContain(\"2\");\n    urls = [];\n\n    await page.click('button[name=\"throw\"][value=\"4xx\"]');\n    await page.waitForSelector(\"#error\");\n    expect(urls).toEqual([]);\n  });\n\n  test(\"provides proper defaultShouldRevalidate value on 4xx/5xx action responses\", async ({\n    page,\n  }) => {\n    let fixture = await createFixture({\n      files: {\n        ...files,\n        \"app/routes/action.tsx\": js`\n          import { Form, Link, useActionData, useLoaderData, useNavigation, data } from 'react-router';\n\n          export async function action({ request }) {\n            let fd = await request.formData();\n            if (fd.get('throw') === \"5xx\") {\n              throw data(\"Thrown 500\", { status: 500 });\n            }\n            if (fd.get('throw') === \"4xx\") {\n              throw data(\"Thrown 400\", { status: 400 });\n            }\n            if (fd.get('return') === \"5xx\") {\n              return data(\"Returned 500\", { status: 500 });\n            }\n            if (fd.get('return') === \"4xx\") {\n              return data(\"Returned 400\", { status: 400 });\n            }\n            return null;\n          }\n\n          let count = 0;\n          export function loader() {\n            return { count: ++count };\n          }\n\n          export function shouldRevalidate({ defaultShouldRevalidate }) {\n            return defaultShouldRevalidate;\n          }\n\n          export default function Comp() {\n            let navigation = useNavigation();\n            let data = useLoaderData();\n            return (\n              <Form method=\"post\">\n                <button type=\"submit\" name=\"throw\" value=\"5xx\">Throw 5xx</button>\n                <button type=\"submit\" name=\"throw\" value=\"4xx\">Throw 4xx</button>\n                <button type=\"submit\" name=\"return\" value=\"5xx\">Return 5xx</button>\n                <button type=\"submit\" name=\"return\" value=\"4xx\">Return 4xx</button>\n                <p id=\"data\">{data.count}</p>\n                {navigation.state === \"idle\" ? <p id=\"idle\">idle</p> : null}\n              </Form>\n            );\n          }\n\n          export function ErrorBoundary() {\n            return (\n              <div>\n                <h1 id=\"error\">Error</h1>\n                <Link to=\"/action\">Back</Link>\n              </div>\n            );\n          }\n        `,\n      },\n    });\n\n    let urls: string[] = [];\n    page.on(\"request\", (req) => {\n      if (req.method() === \"GET\" && req.url().includes(\".data\")) {\n        urls.push(req.url());\n      }\n    });\n\n    console.error = () => {};\n\n    let appFixture = await createAppFixture(fixture);\n    let app = new PlaywrightFixture(appFixture, page);\n    await app.goto(\"/action\");\n    expect(await app.getHtml(\"#data\")).toContain(\"1\");\n    expect(urls).toEqual([]);\n\n    await page.click('button[name=\"return\"][value=\"5xx\"]');\n    await page.waitForSelector(\"#idle\");\n    expect(await app.getHtml(\"#data\")).toContain(\"1\");\n    expect(urls).toEqual([]);\n\n    await page.click('button[name=\"return\"][value=\"4xx\"]');\n    await page.waitForSelector(\"#idle\");\n    expect(await app.getHtml(\"#data\")).toContain(\"1\");\n    expect(urls).toEqual([]);\n\n    await page.click('button[name=\"throw\"][value=\"5xx\"]');\n    await page.waitForSelector(\"#error\");\n    expect(urls).toEqual([]);\n\n    await app.clickLink(\"/action\");\n    await page.waitForSelector(\"#data\");\n    expect(await app.getHtml(\"#data\")).toContain(\"2\");\n    urls = [];\n\n    await page.click('button[name=\"throw\"][value=\"4xx\"]');\n    await page.waitForSelector(\"#error\");\n    expect(urls).toEqual([]);\n  });\n\n  test(\"supports call-site revalidation opt-out on submissions (w/o shouldRevalidate)\", async ({\n    page,\n  }) => {\n    let fixture = await createFixture({\n      files: {\n        ...files,\n        \"app/routes/action.tsx\": js`\n          import { Form } from 'react-router';\n\n          let count = 0;\n          export function loader() {\n            return { count: ++count };\n          }\n\n          export function action() {\n            return { count: ++count };\n          }\n\n          export default function Comp({ loaderData, actionData }) {\n            return (\n              <Form method=\"post\" unstable_defaultShouldRevalidate={false}>\n                <button type=\"submit\" name=\"name\" value=\"value\">Submit</button>\n                <p id=\"data\">{loaderData.count}</p>\n                {actionData ? <p id=\"action-data\">{actionData.count}</p> : null}\n              </Form>\n            );\n          }\n        `,\n      },\n    });\n\n    let urls: string[] = [];\n    page.on(\"request\", (req) => {\n      if (req.method() === \"GET\" && req.url().includes(\".data\")) {\n        urls.push(req.url());\n      }\n    });\n\n    console.error = () => {};\n\n    let appFixture = await createAppFixture(fixture);\n    let app = new PlaywrightFixture(appFixture, page);\n    await app.goto(\"/action\");\n    expect(await app.getHtml(\"#data\")).toContain(\"1\");\n    expect(urls).toEqual([]);\n\n    await page.click('button[name=\"name\"][value=\"value\"]');\n    await page.waitForSelector(\"#action-data\");\n    expect(await app.getHtml(\"#action-data\")).toContain(\"2\");\n    expect(await app.getHtml(\"#data\")).toContain(\"1\");\n    expect(urls).toEqual([]);\n  });\n\n  test(\"supports call-site revalidation opt-in on 4xx/5xx action responses (w/o shouldRevalidate)\", async ({\n    page,\n  }) => {\n    let fixture = await createFixture({\n      files: {\n        ...files,\n        \"app/routes/action.tsx\": js`\n          import { Form, Link, useNavigation, data } from 'react-router';\n\n          export async function action({ request }) {\n            throw data(\"Thrown 500\", { status: 500 });\n          }\n\n          let count = 0;\n          export function loader() {\n            return { count: ++count };\n          }\n\n          export default function Comp({ loaderData }) {\n            let navigation = useNavigation();\n            return (\n              <Form method=\"post\" unstable_defaultShouldRevalidate={true}>\n                <button type=\"submit\" name=\"throw\" value=\"5xx\">Throw 5xx</button>\n                <p id=\"data\">{loaderData.count}</p>\n                {navigation.state === \"idle\" ? <p id=\"idle\">idle</p> : null}\n              </Form>\n            );\n          }\n\n          export function ErrorBoundary() {\n            return <h1 id=\"error\">Error</h1>\n          }\n        `,\n      },\n    });\n\n    let urls: string[] = [];\n    page.on(\"request\", (req) => {\n      if (req.method() === \"GET\" && req.url().includes(\".data\")) {\n        urls.push(req.url());\n      }\n    });\n\n    console.error = () => {};\n\n    let appFixture = await createAppFixture(fixture);\n    let app = new PlaywrightFixture(appFixture, page);\n    await app.goto(\"/action\");\n    expect(await app.getHtml(\"#data\")).toContain(\"1\");\n    expect(urls).toEqual([]);\n\n    await page.click('button[name=\"throw\"][value=\"5xx\"]');\n    await page.waitForSelector(\"#error\");\n    expect(urls).toEqual([expect.stringMatching(/\\/action\\.data$/)]);\n  });\n\n  test(\"supports call-site revalidation opt-out on submissions (w/ shouldRevalidate)\", async ({\n    page,\n  }) => {\n    let fixture = await createFixture({\n      files: {\n        ...files,\n        \"app/routes/action.tsx\": js`\n          import { Form } from 'react-router';\n\n          let count = 0;\n          export function loader() {\n            return { count: ++count };\n          }\n\n          export function action() {\n            return { count: ++count };\n          }\n\n          export function shouldRevalidate({ defaultShouldRevalidate }) {\n            return defaultShouldRevalidate;\n          }\n\n          export default function Comp({ loaderData, actionData }) {\n            return (\n              <Form method=\"post\" unstable_defaultShouldRevalidate={false}>\n                <button type=\"submit\" name=\"name\" value=\"value\">Submit</button>\n                <p id=\"data\">{loaderData.count}</p>\n                {actionData ? <p id=\"action-data\">{actionData.count}</p> : null}\n              </Form>\n            );\n          }\n        `,\n      },\n    });\n\n    let urls: string[] = [];\n    page.on(\"request\", (req) => {\n      if (req.method() === \"GET\" && req.url().includes(\".data\")) {\n        urls.push(req.url());\n      }\n    });\n\n    console.error = () => {};\n\n    let appFixture = await createAppFixture(fixture);\n    let app = new PlaywrightFixture(appFixture, page);\n    await app.goto(\"/action\");\n    expect(await app.getHtml(\"#data\")).toContain(\"1\");\n    expect(urls).toEqual([]);\n\n    await page.click('button[name=\"name\"][value=\"value\"]');\n    await page.waitForSelector(\"#action-data\");\n    expect(await app.getHtml(\"#action-data\")).toContain(\"2\");\n    expect(await app.getHtml(\"#data\")).toContain(\"1\");\n    expect(urls).toEqual([]);\n  });\n\n  test(\"supports call-site revalidation opt-in on 4xx/5xx action responses (w shouldRevalidate)\", async ({\n    page,\n  }) => {\n    let fixture = await createFixture({\n      files: {\n        ...files,\n        \"app/routes/action.tsx\": js`\n          import { Form, Link, useNavigation, data } from 'react-router';\n\n          export async function action({ request }) {\n            throw data(\"Thrown 500\", { status: 500 });\n          }\n\n          let count = 0;\n          export function loader() {\n            return { count: ++count };\n          }\n\n          export function shouldRevalidate({ defaultShouldRevalidate }) {\n            return defaultShouldRevalidate;\n          }\n\n          export default function Comp({ loaderData }) {\n            let navigation = useNavigation();\n            return (\n              <Form method=\"post\" unstable_defaultShouldRevalidate={true}>\n                <button type=\"submit\" name=\"throw\" value=\"5xx\">Throw 5xx</button>\n                <p id=\"data\">{loaderData.count}</p>\n                {navigation.state === \"idle\" ? <p id=\"idle\">idle</p> : null}\n              </Form>\n            );\n          }\n\n          export function ErrorBoundary() {\n            return <h1 id=\"error\">Error</h1>\n          }\n        `,\n      },\n    });\n\n    let urls: string[] = [];\n    page.on(\"request\", (req) => {\n      if (req.method() === \"GET\" && req.url().includes(\".data\")) {\n        urls.push(req.url());\n      }\n    });\n\n    console.error = () => {};\n\n    let appFixture = await createAppFixture(fixture);\n    let app = new PlaywrightFixture(appFixture, page);\n    await app.goto(\"/action\");\n    expect(await app.getHtml(\"#data\")).toContain(\"1\");\n    expect(urls).toEqual([]);\n\n    await page.click('button[name=\"throw\"][value=\"5xx\"]');\n    await page.waitForSelector(\"#error\");\n    expect(urls).toEqual([expect.stringMatching(/\\/action\\.data$/)]);\n  });\n\n  test(\"returns headers correctly for singular loader and action calls\", async () => {\n    let fixture = await createFixture({\n      files: {\n        ...files,\n        \"app/routes/headers.tsx\": js`\n          export function headers ({ actionHeaders, loaderHeaders, errorHeaders }) {\n            if (errorHeaders) {\n              return errorHeaders;\n            } else if ([...actionHeaders].length > 0) {\n              return actionHeaders;\n            } else {\n              return loaderHeaders;\n            }\n          }\n\n          export function action({ request }) {\n            if (new URL(request.url).searchParams.has(\"error\")) {\n              throw new Response(null, { headers: { \"x-action-error\": \"true\" } });\n            }\n            return new Response(null, { headers: { \"x-action\": \"true\" } });\n          }\n\n          export function loader({ request }) {\n            if (new URL(request.url).searchParams.has(\"error\")) {\n              throw new Response(null, { headers: { \"x-loader-error\": \"true\" } });\n            }\n            return new Response(null, { headers: { \"x-loader\": \"true\" } });\n          }\n\n          export default function Comp() {\n            return null;\n          }\n        `,\n      },\n    });\n\n    // Loader\n    let docResponse = await fixture.requestDocument(\"/headers\");\n    let dataResponse = await fixture.requestSingleFetchData(\"/headers.data\");\n    expect(docResponse.headers.get(\"x-loader\")).toEqual(\"true\");\n    expect(dataResponse.headers.get(\"x-loader\")).toEqual(\"true\");\n\n    // Action\n    docResponse = await fixture.requestDocument(\"/headers\", {\n      method: \"post\",\n      body: null,\n    });\n    dataResponse = await fixture.requestSingleFetchData(\"/headers.data\", {\n      method: \"post\",\n      body: null,\n    });\n    expect(docResponse.headers.get(\"x-action\")).toEqual(\"true\");\n    expect(dataResponse.headers.get(\"x-action\")).toEqual(\"true\");\n\n    console.error = () => {};\n\n    // Loader Error\n    docResponse = await fixture.requestDocument(\"/headers?error\");\n    dataResponse = await fixture.requestSingleFetchData(\"/headers.data?error\");\n    expect(docResponse.headers.get(\"x-loader-error\")).toEqual(\"true\");\n    expect(dataResponse.headers.get(\"x-loader-error\")).toEqual(\"true\");\n\n    // Action Error\n    docResponse = await fixture.requestDocument(\"/headers?error\", {\n      method: \"post\",\n      body: null,\n    });\n    dataResponse = await fixture.requestSingleFetchData(\"/headers.data?error\", {\n      method: \"post\",\n      body: null,\n    });\n    expect(docResponse.headers.get(\"x-action-error\")).toEqual(\"true\");\n    expect(dataResponse.headers.get(\"x-action-error\")).toEqual(\"true\");\n  });\n\n  test(\"merges headers from nested routes when raw Responses are returned\", async () => {\n    let fixture = await createFixture({\n      files: {\n        ...files,\n        \"app/routes/a.tsx\": js`\n          export function headers({ loaderHeaders }) {\n            return loaderHeaders;\n          }\n\n          export function loader({ request }) {\n            let headers = new Headers();\n            headers.set('x-one', 'a set');\n            headers.set('x-two', 'a set');\n            return new Response(null, { headers });\n          }\n\n          export default function Comp() {\n            return null;\n          }\n        `,\n        \"app/routes/a.b.tsx\": js`\n          export function headers({ actionHeaders, loaderHeaders, parentHeaders }) {\n            let h = new Headers();\n            for (let [name, value] of parentHeaders) {\n              h.set(name, value);\n            }\n            for (let [name, value] of loaderHeaders) {\n              h.set(name, value);\n            }\n            for (let [name, value] of actionHeaders) {\n              h.set(name, value);\n            }\n            return h;\n          }\n\n          export function action({ request }) {\n            let headers = new Headers();\n            headers.set('x-two', 'b action set');\n            return new Response(null, { headers });\n          }\n\n          export function loader({ request }) {\n            let headers = new Headers();\n            headers.set('x-two', 'b set');\n            return new Response(null, { headers });\n          }\n\n          export default function Comp() {\n            return null;\n          }\n        `,\n      },\n    });\n\n    // x-one uses both set and append\n    // x-two only uses set\n    // x-three only uses append\n    // x-four deletes\n    let res: Awaited<\n      ReturnType<\n        typeof fixture.requestDocument | typeof fixture.requestSingleFetchData\n      >\n    >;\n    res = await fixture.requestDocument(\"/a\");\n    expect(res.headers.get(\"x-one\")).toEqual(\"a set\");\n    expect(res.headers.get(\"x-two\")).toEqual(\"a set\");\n\n    res = await fixture.requestSingleFetchData(\"/a.data\");\n    expect(res.headers.get(\"x-one\")).toEqual(\"a set\");\n    expect(res.headers.get(\"x-two\")).toEqual(\"a set\");\n\n    res = await fixture.requestDocument(\"/a/b\");\n    expect(res.headers.get(\"x-one\")).toEqual(\"a set\");\n    expect(res.headers.get(\"x-two\")).toEqual(\"b set\");\n\n    res = await fixture.requestSingleFetchData(\"/a/b.data\");\n    expect(res.headers.get(\"x-one\")).toEqual(\"a set\");\n    expect(res.headers.get(\"x-two\")).toEqual(\"b set\");\n\n    // Action only - single fetch request\n    res = await fixture.requestSingleFetchData(\"/a/b.data\", {\n      method: \"post\",\n      body: null,\n    });\n    expect(res.headers.get(\"x-one\")).toEqual(null);\n    expect(res.headers.get(\"x-two\")).toEqual(\"b action set\");\n\n    // Actions and Loaders - Document request\n    res = await fixture.requestDocument(\"/a/b\", {\n      method: \"post\",\n      body: null,\n    });\n    expect(res.headers.get(\"x-one\")).toEqual(\"a set\");\n    expect(res.headers.get(\"x-two\")).toEqual(\"b action set\");\n  });\n\n  test(\"merges status codes from nested routes when raw Responses are used\", async () => {\n    let fixture = await createFixture({\n      files: {\n        ...files,\n        \"app/routes/a.tsx\": js`\n          export function loader({ request }) {\n            if (new URL(request.url).searchParams.has(\"error\")) {\n              throw new Response(null, { status: 401 });\n            } else {\n              return new Response(null, { status: 201 });\n            }\n          }\n\n          export default function Comp() {\n            return null;\n          }\n        `,\n        \"app/routes/a.b.tsx\": js`\n          export function loader({ request }) {\n            return new Response(null, { status: 202 });\n          }\n\n          export default function Comp() {\n            return null;\n          }\n        `,\n        \"app/routes/a.b.c.tsx\": js`\n          export function action({ request }) {\n            return new Response(null, { status: 206 });\n          }\n\n          export function loader({ request }) {\n            return new Response(null, { status: 203 });\n          }\n\n          export default function Comp() {\n            return null;\n          }\n        `,\n      },\n    });\n\n    console.error = () => {};\n\n    // Loaders\n    let res: Awaited<\n      ReturnType<\n        typeof fixture.requestDocument | typeof fixture.requestSingleFetchData\n      >\n    >;\n    res = await fixture.requestDocument(\"/a\");\n    expect(res.status).toEqual(201);\n\n    res = await fixture.requestSingleFetchData(\"/a.data\");\n    expect(res.status).toEqual(201);\n\n    res = await fixture.requestDocument(\"/a/b\");\n    expect(res.status).toEqual(202);\n\n    res = await fixture.requestSingleFetchData(\"/a/b.data\");\n    expect(res.status).toEqual(202);\n\n    res = await fixture.requestDocument(\"/a/b/c\");\n    expect(res.status).toEqual(203);\n\n    res = await fixture.requestSingleFetchData(\"/a/b/c.data\");\n    expect(res.status).toEqual(203);\n\n    // Errors\n    res = await fixture.requestDocument(\"/a?error\");\n    expect(res.status).toEqual(401);\n\n    res = await fixture.requestSingleFetchData(\"/a.data?error\");\n    expect(res.status).toEqual(401);\n\n    res = await fixture.requestDocument(\"/a/b?error\");\n    expect(res.status).toEqual(401);\n\n    res = await fixture.requestSingleFetchData(\"/a/b.data?error\");\n    expect(res.status).toEqual(401);\n\n    // Actions\n    res = await fixture.requestDocument(\"/a/b/c\", {\n      method: \"post\",\n      body: null,\n    });\n    expect(res.status).toEqual(206);\n\n    res = await fixture.requestSingleFetchData(\"/a/b/c.data\", {\n      method: \"post\",\n      body: null,\n    });\n    expect(res.status).toEqual(206);\n  });\n\n  test(\"processes thrown loader redirects via Response\", async ({ page }) => {\n    let fixture = await createFixture({\n      files: {\n        ...files,\n        \"app/routes/data.tsx\": js`\n          import { redirect } from 'react-router';\n          export function loader() {\n            throw redirect('/target');\n          }\n          export default function Component() {\n            return null\n          }\n        `,\n        \"app/routes/target.tsx\": js`\n          export default function Component() {\n            return <h1 id=\"target\">Target</h1>\n          }\n        `,\n      },\n    });\n\n    console.error = () => {};\n\n    let res = await fixture.requestDocument(\"/data\");\n    expect(res.status).toBe(302);\n    expect(res.headers.get(\"Location\")).toBe(\"/target\");\n    expect(await res.text()).toBe(\"\");\n\n    let { status, data } = await fixture.requestSingleFetchData(\"/data.data\");\n    expect(data).toEqual({\n      [SingleFetchRedirectSymbol]: {\n        status: 302,\n        redirect: \"/target\",\n        reload: false,\n        replace: false,\n        revalidate: false,\n      },\n    });\n    expect(status).toBe(202);\n\n    let appFixture = await createAppFixture(fixture);\n    let app = new PlaywrightFixture(appFixture, page);\n    await app.goto(\"/\");\n    await app.clickLink(\"/data\");\n    await page.waitForSelector(\"#target\");\n    expect(await app.getHtml(\"#target\")).toContain(\"Target\");\n  });\n\n  test(\"processes returned loader redirects via Response\", async ({ page }) => {\n    let fixture = await createFixture({\n      files: {\n        ...files,\n        \"app/routes/data.tsx\": js`\n          import { redirect } from 'react-router';\n          export function loader() {\n            return redirect('/target');\n          }\n          export default function Component() {\n            return null\n          }\n        `,\n        \"app/routes/target.tsx\": js`\n          export default function Component() {\n            return <h1 id=\"target\">Target</h1>\n          }\n        `,\n      },\n    });\n    let res = await fixture.requestDocument(\"/data\");\n    expect(res.status).toBe(302);\n    expect(res.headers.get(\"Location\")).toBe(\"/target\");\n    expect(await res.text()).toBe(\"\");\n\n    let { status, data } = await fixture.requestSingleFetchData(\"/data.data\");\n    expect(data).toEqual({\n      [SingleFetchRedirectSymbol]: {\n        status: 302,\n        redirect: \"/target\",\n        reload: false,\n        replace: false,\n        revalidate: false,\n      },\n    });\n    expect(status).toBe(202);\n\n    let appFixture = await createAppFixture(fixture);\n    let app = new PlaywrightFixture(appFixture, page);\n    await app.goto(\"/\");\n    await app.clickLink(\"/data\");\n    await page.waitForSelector(\"#target\");\n    expect(await app.getHtml(\"#target\")).toContain(\"Target\");\n  });\n\n  test(\"processes thrown loader replace redirects via Response\", async ({\n    page,\n  }) => {\n    let fixture = await createFixture({\n      files: {\n        ...files,\n        \"app/routes/data.tsx\": js`\n          import { replace } from 'react-router';\n          export function loader() {\n            throw replace('/target');\n          }\n          export default function Component() {\n            return null\n          }\n        `,\n        \"app/routes/target.tsx\": js`\n          export default function Component() {\n            return <h1 id=\"target\">Target</h1>\n          }\n        `,\n      },\n    });\n\n    console.error = () => {};\n\n    let res = await fixture.requestDocument(\"/data\");\n    expect(res.status).toBe(302);\n    expect(res.headers.get(\"Location\")).toBe(\"/target\");\n    expect(await res.text()).toBe(\"\");\n\n    let { status, data } = await fixture.requestSingleFetchData(\"/data.data\");\n    expect(data).toEqual({\n      [SingleFetchRedirectSymbol]: {\n        status: 302,\n        redirect: \"/target\",\n        reload: false,\n        replace: true,\n        revalidate: false,\n      },\n    });\n    expect(status).toBe(202);\n\n    let appFixture = await createAppFixture(fixture);\n    let app = new PlaywrightFixture(appFixture, page);\n    await app.goto(\"/\");\n    await app.clickLink(\"/data\");\n    await page.waitForSelector(\"#target\");\n    expect(await app.getHtml(\"#target\")).toContain(\"Target\");\n  });\n\n  test(\"processes thrown action redirects via Response\", async ({ page }) => {\n    let fixture = await createFixture({\n      files: {\n        ...files,\n        \"app/routes/data.tsx\": js`\n          import { redirect } from 'react-router';\n          export function action() {\n            throw redirect('/target');\n          }\n          export default function Component() {\n            return null\n          }\n        `,\n        \"app/routes/target.tsx\": js`\n          export default function Component() {\n            return <h1 id=\"target\">Target</h1>\n          }\n        `,\n      },\n    });\n\n    console.error = () => {};\n\n    let res = await fixture.requestDocument(\"/data\", {\n      method: \"post\",\n      body: null,\n    });\n    expect(res.status).toBe(302);\n    expect(res.headers.get(\"Location\")).toBe(\"/target\");\n    expect(await res.text()).toBe(\"\");\n\n    let { status, data } = await fixture.requestSingleFetchData(\"/data.data\", {\n      method: \"post\",\n      body: null,\n    });\n    expect(data).toEqual({\n      status: 302,\n      redirect: \"/target\",\n      reload: false,\n      replace: false,\n      revalidate: false,\n    });\n    expect(status).toBe(202);\n\n    let appFixture = await createAppFixture(fixture);\n    let app = new PlaywrightFixture(appFixture, page);\n    await app.goto(\"/\");\n    await app.clickSubmitButton(\"/data\");\n    await page.waitForSelector(\"#target\");\n    expect(await app.getHtml(\"#target\")).toContain(\"Target\");\n  });\n\n  test(\"processes returned action redirects via Response\", async ({ page }) => {\n    let fixture = await createFixture({\n      files: {\n        ...files,\n        \"app/routes/data.tsx\": js`\n          import { redirect } from 'react-router';\n          export function action() {\n            return redirect('/target');\n          }\n          export default function Component() {\n            return null\n          }\n        `,\n        \"app/routes/target.tsx\": js`\n          export default function Component() {\n            return <h1 id=\"target\">Target</h1>\n          }\n        `,\n      },\n    });\n\n    let res = await fixture.requestDocument(\"/data\", {\n      method: \"post\",\n      body: null,\n    });\n    expect(res.status).toBe(302);\n    expect(res.headers.get(\"Location\")).toBe(\"/target\");\n    expect(await res.text()).toBe(\"\");\n\n    let { status, data } = await fixture.requestSingleFetchData(\"/data.data\", {\n      method: \"post\",\n      body: null,\n    });\n    expect(data).toEqual({\n      status: 302,\n      redirect: \"/target\",\n      reload: false,\n      replace: false,\n      revalidate: false,\n    });\n    expect(status).toBe(202);\n\n    let appFixture = await createAppFixture(fixture);\n    let app = new PlaywrightFixture(appFixture, page);\n    await app.goto(\"/\");\n    await app.clickSubmitButton(\"/data\");\n    await page.waitForSelector(\"#target\");\n    expect(await app.getHtml(\"#target\")).toContain(\"Target\");\n  });\n\n  test(\"processes thrown loader redirects (resource route)\", async ({\n    page,\n  }) => {\n    let fixture = await createFixture({\n      files: {\n        ...files,\n        \"app/routes/data.tsx\": js`\n          import { redirect } from 'react-router';\n          export function loader({ request }) {\n            throw redirect('/target');\n          }\n        `,\n        \"app/routes/target.tsx\": js`\n          export default function Component() {\n            return <h1 id=\"target\">Target</h1>\n          }\n        `,\n      },\n    });\n\n    console.error = () => {};\n\n    let res = await fixture.requestDocument(\"/data\");\n    expect(res.status).toBe(302);\n    expect(res.headers.get(\"Location\")).toBe(\"/target\");\n    expect(await res.text()).toBe(\"\");\n\n    let appFixture = await createAppFixture(fixture);\n    let app = new PlaywrightFixture(appFixture, page);\n    await app.goto(\"/\");\n    await app.clickLink(\"/data\");\n    await page.waitForSelector(\"#target\");\n    expect(await app.getHtml(\"#target\")).toContain(\"Target\");\n  });\n\n  test(\"processes returned loader redirects (resource route)\", async ({\n    page,\n  }) => {\n    let fixture = await createFixture({\n      files: {\n        ...files,\n        \"app/routes/data.tsx\": js`\n          import { redirect } from 'react-router';\n          export function loader({ request }) {\n            return redirect('/target');\n          }\n        `,\n        \"app/routes/target.tsx\": js`\n          export default function Component() {\n            return <h1 id=\"target\">Target</h1>\n          }\n        `,\n      },\n    });\n\n    let res = await fixture.requestDocument(\"/data\");\n    expect(res.status).toBe(302);\n    expect(res.headers.get(\"Location\")).toBe(\"/target\");\n    expect(await res.text()).toBe(\"\");\n\n    let appFixture = await createAppFixture(fixture);\n    let app = new PlaywrightFixture(appFixture, page);\n    await app.goto(\"/\");\n    await app.clickLink(\"/data\");\n    await page.waitForSelector(\"#target\");\n    expect(await app.getHtml(\"#target\")).toContain(\"Target\");\n  });\n\n  test(\"processes redirects from handleDataRequest (after loaders)\", async ({\n    page,\n  }) => {\n    let fixture = await createFixture({\n      files: {\n        ...files,\n        \"app/entry.server.tsx\": js`\n          import { PassThrough } from \"node:stream\";\n\n          import type { EntryContext } from \"react-router\";\n          import { createReadableStreamFromReadable } from \"@react-router/node\";\n          import { ServerRouter } from \"react-router\";\n          import { renderToPipeableStream } from \"react-dom/server\";\n\n          export default function handleRequest(\n            request: Request,\n            responseStatusCode: number,\n            responseHeaders: Headers,\n            remixContext: EntryContext\n          ) {\n            return new Promise((resolve, reject) => {\n              const { pipe } = renderToPipeableStream(\n                <ServerRouter context={remixContext} url={request.url} />,\n                {\n                  onShellReady() {\n                    const body = new PassThrough();\n                    const stream = createReadableStreamFromReadable(body);\n                    responseHeaders.set(\"Content-Type\", \"text/html\");\n                    resolve(\n                      new Response(stream, {\n                        headers: responseHeaders,\n                        status: responseStatusCode,\n                      })\n                    );\n                    pipe(body);\n                  },\n                  onShellError(error: unknown) {\n                    reject(error);\n                  },\n                  onError(error: unknown) {\n                    responseStatusCode = 500;\n                  },\n                }\n              );\n            });\n          }\n\n          export function handleDataRequest(response, { request }) {\n            if (request.url.endsWith(\"/data.data\")) {\n              return new Response(null, {\n                status: 302,\n                headers: {\n                  Location: \"/target\",\n                },\n              });\n            }\n            return response;\n          }\n        `,\n        \"app/routes/data.tsx\": js`\n          import { redirect } from 'react-router';\n          export function loader() {\n            return redirect('/target');\n          }\n          export default function Component() {\n            return null\n          }\n        `,\n        \"app/routes/target.tsx\": js`\n          export default function Component() {\n            return <h1 id=\"target\">Target</h1>\n          }\n        `,\n      },\n    });\n\n    let { status, data } = await fixture.requestSingleFetchData(\"/data.data\");\n    expect(data).toEqual({\n      [SingleFetchRedirectSymbol]: {\n        status: 302,\n        redirect: \"/target\",\n        reload: false,\n        replace: false,\n        revalidate: false,\n      },\n    });\n    expect(status).toBe(202);\n\n    let appFixture = await createAppFixture(fixture);\n    let app = new PlaywrightFixture(appFixture, page);\n    await app.goto(\"/\");\n    await app.clickLink(\"/data\");\n    await page.waitForSelector(\"#target\");\n    expect(await app.getHtml(\"#target\")).toContain(\"Target\");\n  });\n\n  test(\"processes redirects from handleDataRequest (after actions)\", async ({\n    page,\n  }) => {\n    let fixture = await createFixture({\n      files: {\n        ...files,\n        \"app/entry.server.tsx\": js`\n          import { PassThrough } from \"node:stream\";\n\n          import type { EntryContext } from \"react-router\";\n          import { createReadableStreamFromReadable } from \"@react-router/node\";\n          import { ServerRouter } from \"react-router\";\n          import { renderToPipeableStream } from \"react-dom/server\";\n\n          export default function handleRequest(\n            request: Request,\n            responseStatusCode: number,\n            responseHeaders: Headers,\n            remixContext: EntryContext\n          ) {\n            return new Promise((resolve, reject) => {\n              const { pipe } = renderToPipeableStream(\n                <ServerRouter context={remixContext} url={request.url} />,\n                {\n                  onShellReady() {\n                    const body = new PassThrough();\n                    const stream = createReadableStreamFromReadable(body);\n                    responseHeaders.set(\"Content-Type\", \"text/html\");\n                    resolve(\n                      new Response(stream, {\n                        headers: responseHeaders,\n                        status: responseStatusCode,\n                      })\n                    );\n                    pipe(body);\n                  },\n                  onShellError(error: unknown) {\n                    reject(error);\n                  },\n                  onError(error: unknown) {\n                    responseStatusCode = 500;\n                  },\n                }\n              );\n            });\n          }\n\n          export function handleDataRequest(response, { request }) {\n            if (request.url.endsWith(\"/data.data\")) {\n              return new Response(null, {\n                status: 302,\n                headers: {\n                  Location: \"/target\",\n                },\n              });\n            }\n            return response;\n          }\n        `,\n        \"app/routes/data.tsx\": js`\n          import { redirect } from 'react-router';\n          export function action() {\n            return redirect('/target');\n          }\n          export default function Component() {\n            return null\n          }\n        `,\n        \"app/routes/target.tsx\": js`\n          export default function Component() {\n            return <h1 id=\"target\">Target</h1>\n          }\n        `,\n      },\n    });\n\n    let { status, data } = await fixture.requestSingleFetchData(\"/data.data\", {\n      method: \"post\",\n      body: null,\n    });\n    expect(data).toEqual({\n      status: 302,\n      redirect: \"/target\",\n      reload: false,\n      replace: false,\n      revalidate: false,\n    });\n    expect(status).toBe(202);\n\n    let appFixture = await createAppFixture(fixture);\n    let app = new PlaywrightFixture(appFixture, page);\n    await app.goto(\"/\");\n    await app.clickSubmitButton(\"/data\");\n    await page.waitForSelector(\"#target\");\n    expect(await app.getHtml(\"#target\")).toContain(\"Target\");\n  });\n\n  test(\"supports a basename\", async ({ page }) => {\n    let fixture = await createFixture({\n      files: {\n        \"vite.config.ts\": js`\n          import { reactRouter } from \"@react-router/dev/vite\";\n\n          export default {\n            base: \"/base/\",\n            plugins: [reactRouter()]\n          }\n        `,\n        \"react-router.config.ts\": reactRouterConfig({\n          basename: \"/base/\",\n        }),\n        ...files,\n      },\n      useReactRouterServe: true,\n    });\n\n    let appFixture = await createAppFixture(fixture);\n\n    let requests: string[] = [];\n    page.on(\"request\", (req) => {\n      let url = new URL(req.url());\n      if (url.pathname.endsWith(\".data\")) {\n        requests.push(url.pathname + url.search);\n      }\n    });\n\n    let app = new PlaywrightFixture(appFixture, page);\n    await app.goto(\"/base/\");\n    await app.clickLink(\"/base/data\");\n    await expect(page.getByText(\"Data Route\")).toBeVisible();\n    await app.clickLink(\"/base/\");\n    await expect(page.getByText(\"Index\")).toBeVisible();\n\n    expect(requests).toEqual([\"/base/data.data\", \"/base/_root.data\"]);\n  });\n\n  test(\"processes redirects when a basename is present\", async ({ page }) => {\n    let fixture = await createFixture({\n      files: {\n        ...files,\n        \"react-router.config.ts\": reactRouterConfig({\n          basename: \"/base\",\n        }),\n        \"app/routes/data.tsx\": js`\n          import { redirect } from 'react-router';\n          export function loader() {\n            throw redirect('/target');\n          }\n          export default function Component() {\n            return null\n          }\n        `,\n        \"app/routes/target.tsx\": js`\n          export default function Component() {\n            return <h1 id=\"target\">Target</h1>\n          }\n        `,\n      },\n    });\n\n    console.error = () => {};\n\n    let res = await fixture.requestDocument(\"/base/data\");\n    expect(res.status).toBe(302);\n    expect(res.headers.get(\"Location\")).toBe(\"/base/target\");\n    expect(await res.text()).toBe(\"\");\n\n    let { status, data } =\n      await fixture.requestSingleFetchData(\"/base/data.data\");\n    expect(data).toEqual({\n      [SingleFetchRedirectSymbol]: {\n        status: 302,\n        redirect: \"/target\",\n        reload: false,\n        replace: false,\n        revalidate: false,\n      },\n    });\n    expect(status).toBe(202);\n\n    let appFixture = await createAppFixture(fixture);\n    let app = new PlaywrightFixture(appFixture, page);\n    await app.goto(\"/base/\");\n    await app.clickLink(\"/base/data\");\n    await page.waitForSelector(\"#target\");\n    expect(await app.getHtml(\"#target\")).toContain(\"Target\");\n  });\n\n  test(\"processes redirects returned outside of react router\", async ({\n    page,\n  }) => {\n    let port = await getPort();\n    let cwd = await createProject({\n      \"vite.config.js\": await viteConfig.basic({ port }),\n      \"server.mjs\": EXPRESS_SERVER({\n        port,\n        customLogic: js`\n          app.use(async (req, res, next) => {\n            if (req.url === \"/page.data\") {\n              res.status(204);\n              res.append('X-Remix-Status', '302');\n              res.append('X-Remix-Redirect', '/target');\n              res.end();\n            } else {\n              next();\n            }\n          });\n        `,\n      }),\n      \"app/routes/_index.tsx\": js`\n        import { Link, Form } from \"react-router\";\n        export default function Component() {\n          return (\n            <div id=\"index\">\n              <Link to=\"/page\">Go to /page</Link>\n              <Form method=\"post\" action=\"/page\">\n                <button type=\"submit\" name=\"key\" value=\"value\">Submit</button>\n              </Form>\n            </div>\n          );\n        }\n      `,\n      \"app/routes/page.tsx\": js`\n        export function action() {\n          return null\n        }\n        export function loader() {\n            return null\n        }\n        export default function Component() {\n          return <p>Should not see me</p>\n        }\n      `,\n      \"app/routes/target.tsx\": js`\n        import { Link } from \"react-router\";\n        export default function Component() {\n          return (\n            <>\n              <h1 id=\"target\">Target</h1>\n              <Link to=\"/\">Go home</Link>\n            </>\n          );\n        }\n      `,\n    });\n    let stop = await customDev({ cwd, port });\n\n    try {\n      await page.goto(`http://localhost:${port}/`, {\n        waitUntil: \"networkidle\",\n      });\n\n      await page.locator('a[href=\"/page\"]').click();\n      await page.waitForSelector(\"#target\");\n      await expect(page.locator(\"#target\")).toHaveText(\"Target\");\n\n      await page.locator('a[href=\"/\"]').click();\n      await page.waitForSelector(\"#index\");\n\n      await page.locator('button[type=\"submit\"]').click();\n      await page.waitForSelector(\"#target\");\n      await expect(page.locator(\"#target\")).toHaveText(\"Target\");\n    } finally {\n      stop();\n    }\n  });\n\n  test(\"processes thrown loader errors\", async ({ page }) => {\n    let fixture = await createFixture({\n      files: {\n        ...files,\n        \"app/routes/data.tsx\": js`\n          import { redirect } from 'react-router';\n          import { isRouteErrorResponse, useRouteError } from 'react-router';\n          export function headers({ errorHeaders }) {\n            return errorHeaders;\n          }\n          export function loader({ request }) {\n            throw new Response(null, { status: 404, headers: { 'X-Foo': 'Bar' } });\n          }\n          export default function Component() {\n            return null\n          }\n          export function ErrorBoundary() {\n            let error = useRouteError();\n            if (isRouteErrorResponse(error)) {\n              return <p id=\"error\">{error.status}</p>\n            }\n            throw new Error('Nope')\n          }\n        `,\n      },\n    });\n\n    console.error = () => {};\n\n    let res = await fixture.requestDocument(\"/data\");\n    expect(res.status).toBe(404);\n    expect(res.headers.get(\"X-Foo\")).toBe(\"Bar\");\n    expect(await res.text()).toContain('<p id=\"error\">404</p>');\n\n    let appFixture = await createAppFixture(fixture);\n    let app = new PlaywrightFixture(appFixture, page);\n    await app.goto(\"/\");\n    await app.clickLink(\"/data\");\n    await page.waitForSelector(\"#error\");\n    expect(await app.getHtml(\"#error\")).toContain(\"404\");\n  });\n\n  test(\"processes thrown action errors via responseStub\", async ({ page }) => {\n    let fixture = await createFixture({\n      files: {\n        ...files,\n        \"app/routes/data.tsx\": js`\n          import { redirect } from 'react-router';\n          import { isRouteErrorResponse, useRouteError } from 'react-router';\n          export function headers({ errorHeaders }) {\n            return errorHeaders;\n          }\n          export function action({ request }) {\n            throw new Response(null, { status: 404, headers: { 'X-Foo': 'Bar' } });\n          }\n          export default function Component() {\n            return null\n          }\n          export function ErrorBoundary() {\n            let error = useRouteError();\n            if (isRouteErrorResponse(error)) {\n              return <p id=\"error\">{error.status}</p>\n            }\n            throw new Error('Nope')\n          }\n        `,\n      },\n    });\n\n    console.error = () => {};\n\n    let res = await fixture.requestDocument(\"/data\", {\n      method: \"post\",\n      body: null,\n    });\n    expect(res.status).toBe(404);\n    expect(res.headers.get(\"X-Foo\")).toBe(\"Bar\");\n    expect(await res.text()).toContain('<p id=\"error\">404</p>');\n\n    let appFixture = await createAppFixture(fixture);\n    let app = new PlaywrightFixture(appFixture, page);\n    await app.goto(\"/\");\n    await app.clickSubmitButton(\"/data\");\n    await page.waitForSelector(\"#error\");\n    expect(await app.getHtml(\"#error\")).toContain(\"404\");\n  });\n\n  test(\"allows fetcher to hit resource route and return via turbo stream\", async ({\n    page,\n  }) => {\n    let fixture = await createFixture({\n      files: {\n        ...files,\n        \"app/routes/_index.tsx\": js`\n          import { useFetcher } from \"react-router\";\n\n          export default function Component() {\n            let fetcher = useFetcher();\n            return (\n              <div>\n                <button id=\"load\" onClick={() => fetcher.load('/resource')}>\n                  Load\n                </button>\n                {fetcher.data ? <pre id=\"fetcher-data\">{fetcher.data.message} {fetcher.data.date.toISOString()}</pre> : null}\n              </div>\n            );\n          }\n        `,\n        \"app/routes/resource.tsx\": js`\n          export function loader() {\n            // Fetcher calls to resource routes will append \".data\" and we'll go through\n            // the turbo-stream flow.  If a user were to curl this endpoint they'd go\n            // through \"handleResourceRoute\" and it would be returned as \"json()\"\n            return {\n              message: \"RESOURCE\",\n              date: new Date(\"${ISO_DATE}\"),\n            };\n          }\n        `,\n      },\n    });\n    let appFixture = await createAppFixture(fixture);\n    let app = new PlaywrightFixture(appFixture, page);\n    await app.goto(\"/\");\n    await app.clickElement(\"#load\");\n    await page.waitForSelector(\"#fetcher-data\");\n    expect(await app.getHtml(\"#fetcher-data\")).toContain(\n      \"RESOURCE 2024-03-12T12:00:00.000Z\",\n    );\n  });\n\n  test(\"Strips ?_routes query param from loader/action requests\", async ({\n    page,\n  }) => {\n    let fixture = await createFixture({\n      files: {\n        ...files,\n        \"app/routes/_index.tsx\": js`\n          import { Link } from \"react-router\";\n          export default function Component() {\n            return <Link to=\"/parent/a\">Go to /parent/a</Link>;\n          }\n        `,\n        \"app/routes/parent.tsx\": js`\n          import { Link, Outlet, useLoaderData } from \"react-router\";\n          export function loader({ request }) {\n            return { url: request.url };\n          }\n          export default function Component() {\n            return (\n              <>\n                <p id=\"parent\">Parent loader URL: {useLoaderData().url}</p>\n                <Outlet/>\n              </>\n            );\n          }\n        `,\n        \"app/routes/parent.a.tsx\": js`\n          import { useLoaderData } from \"react-router\";\n          export function loader({ request }) {\n            return { url: request.url };\n          }\n          export async function clientLoader({ request, serverLoader }) {\n            let serverData = await serverLoader();\n            return {\n              serverUrl: serverData.url,\n              clientUrl: request.url\n            }\n          }\n          export default function Component() {\n            let data = useLoaderData();\n            return (\n              <>\n                <p id=\"a-server\">A server loader URL: {data.serverUrl}</p>\n                <p id=\"a-client\">A client loader URL: {data.clientUrl}</p>\n              </>\n            );\n          }\n        `,\n      },\n    });\n    let appFixture = await createAppFixture(fixture);\n    let app = new PlaywrightFixture(appFixture, page);\n\n    let urls: string[] = [];\n    page.on(\"request\", (req) => {\n      if (req.url().includes(\".data\")) {\n        urls.push(req.url());\n      }\n    });\n\n    await app.goto(\"/\");\n\n    await app.clickLink(\"/parent/a\");\n    await page.waitForSelector(\"#a-server\");\n\n    // HTTP Requests contained routes params\n    expect(urls.length).toBe(2);\n    expect(urls[0].endsWith(\"/parent/a.data?_routes=routes%2Fparent.a\")).toBe(\n      true,\n    );\n    expect(\n      urls[1].endsWith(\"/parent/a.data?_routes=root%2Croutes%2Fparent\"),\n    ).toBe(true);\n\n    // But loaders don't receive any routes params\n    expect(await app.getHtml(\"#parent\")).toMatch(\n      />Parent loader URL: http:\\/\\/localhost:\\d+\\/parent\\/a</,\n    );\n    expect(await app.getHtml(\"#a-server\")).toMatch(\n      />A server loader URL: http:\\/\\/localhost:\\d+\\/parent\\/a</,\n    );\n    expect(await app.getHtml(\"#a-client\")).toMatch(\n      />A client loader URL: http:\\/\\/localhost:\\d+\\/parent\\/a</,\n    );\n  });\n\n  test(\"Strips Content-Length header from loader/action responses\", async () => {\n    let fixture = await createFixture({\n      files: {\n        ...files,\n        \"app/routes/data-with-response.tsx\": js`\n          import { useActionData, useLoaderData, data } from \"react-router\";\n\n          export function headers ({ actionHeaders, loaderHeaders, errorHeaders }) {\n            if ([...actionHeaders].length > 0) {\n              return actionHeaders;\n            } else {\n              return loaderHeaders;\n            }\n          }\n\n          export async function action({ request }) {\n            let formData = await request.formData();\n            return data({\n              key: formData.get('key'),\n            }, { headers: { 'Content-Length': '0' }});\n          }\n\n          export function loader({ request }) {\n            return data({\n              message: \"DATA\",\n            }, { headers: { 'Content-Length': '0' }});\n          }\n\n          export default function DataWithResponse() {\n            let data = useLoaderData();\n            let actionData = useActionData();\n            return (\n              <>\n                <h1 id=\"heading\">Data</h1>\n                <p id=\"message\">{data.message}</p>\n                <p id=\"date\">{data.date.toISOString()}</p>\n                {actionData ? <p id=\"action-data\">{actionData.key}</p> : null}\n              </>\n            )\n          }\n        `,\n      },\n    });\n\n    let res = await fixture.requestSingleFetchData(\"/data-with-response.data\");\n    expect(res.headers.get(\"Content-Length\")).toEqual(null);\n    expect(res.data).toStrictEqual({\n      root: {\n        data: {\n          message: \"ROOT\",\n        },\n      },\n      \"routes/data-with-response\": {\n        data: {\n          message: \"DATA\",\n        },\n      },\n    });\n\n    let postBody = new URLSearchParams();\n    postBody.set(\"key\", \"value\");\n    res = await fixture.requestSingleFetchData(\"/data-with-response.data\", {\n      method: \"post\",\n      body: postBody,\n    });\n    expect(res.headers.get(\"Content-Length\")).toEqual(null);\n    expect(res.data).toEqual({\n      data: {\n        key: \"value\",\n      },\n    });\n  });\n\n  test(\"Action requests do not use _routes and do not call loaders on the server\", async ({\n    page,\n  }) => {\n    let fixture = await createFixture({\n      files: {\n        ...files,\n        \"app/routes/page.tsx\": js`\n          import { Form, useActionData, useLoaderData } from \"react-router\";\n          let count = 0;\n          export function loader({ request }) {\n            return { count: ++count };\n          }\n          export function action({ request }) {\n            return { message: \"ACTION\" };\n          }\n          export default function Component() {\n            let data = useLoaderData();\n            let actionData = useActionData();\n            return (\n              <>\n                <p id=\"data\">{\"Count:\" + data.count}</p>\n                <Form method=\"post\">\n                  <button type=\"submit\">Submit</button>\n                  {actionData ? <p id=\"action\">{actionData.message}</p> : null}\n                </Form>\n              </>\n            )\n          }\n        `,\n      },\n    });\n    let appFixture = await createAppFixture(fixture);\n    let app = new PlaywrightFixture(appFixture, page);\n\n    let urls: string[] = [];\n    page.on(\"request\", (req) => {\n      if (req.url().includes(\".data\")) {\n        urls.push(req.method() + \" \" + req.url());\n      }\n    });\n\n    await app.goto(\"/page\");\n    expect(await app.getHtml(\"#data\")).toContain(\"Count:1\");\n\n    await app.clickSubmitButton(\"/page\");\n    await page.waitForSelector(\"#action\");\n    expect(await app.getHtml(\"#data\")).toContain(\"Count:2\");\n\n    // HTTP Requests contained routes params\n    expect(urls).toEqual([\n      expect.stringMatching(/POST .*\\/page.data$/),\n      expect.stringMatching(/GET .*\\/page.data$/),\n    ]);\n  });\n\n  test(\"does not try to encode a turbo-stream body into 204 responses\", async ({\n    page,\n  }) => {\n    let fixture = await createFixture({\n      files: {\n        ...files,\n        \"app/routes/_index.tsx\": js`\n          import { data, Form, useActionData, useNavigation } from \"react-router\";\n\n          export async function action({ request }) {\n            await new Promise(r => setTimeout(r, 500));\n            return data(null, { status: 204 });\n          };\n\n          export default function Index() {\n            const navigation = useNavigation();\n            const actionData = useActionData();\n            return (\n              <Form method=\"post\">\n                {navigation.state === \"idle\" ? <p data-idle>idle</p> : <p data-active>active</p>}\n                <button data-submit type=\"submit\">{actionData ?? 'no content!'}</button>\n              </Form>\n            );\n          }\n        `,\n      },\n    });\n    let appFixture = await createAppFixture(fixture);\n\n    let app = new PlaywrightFixture(appFixture, page);\n\n    let requests: [string, number, string][] = [];\n    page.on(\"request\", async (req) => {\n      if (req.url().includes(\".data\")) {\n        let url = new URL(req.url());\n        requests.push([\n          req.method(),\n          (await req.response())!.status(),\n          url.pathname + url.search,\n        ]);\n      }\n    });\n\n    // Document requests\n    let documentRes = await fixture.requestDocument(\"/?index\", {\n      method: \"post\",\n    });\n    expect(documentRes.status).toBe(204);\n    expect(await documentRes.text()).toBe(\"\");\n\n    // Data requests\n    await app.goto(\"/\");\n    (await page.$(\"[data-submit]\"))?.click();\n    await page.waitForSelector(\"[data-active]\");\n    await page.waitForSelector(\"[data-idle]\");\n\n    expect(await page.innerText(\"[data-submit]\")).toEqual(\"no content!\");\n    expect(requests).toEqual([\n      [\"POST\", 204, \"/_root.data?index\"],\n      [\"GET\", 200, \"/_root.data\"],\n    ]);\n  });\n\n  test(\"does not try to encode a turbo-stream body into 304 responses\", async () => {\n    let fixture = await createFixture({\n      files: {\n        ...files,\n        \"app/routes/_index.tsx\": js`\n          import { useLoaderData } from \"react-router\";\n\n          const eTag = \"1234\";\n          export function loader({ request }) {\n            if (request.headers.get(\"If-None-Match\") === eTag) {\n              throw new Response(null, { status: 304 });\n            }\n            return { message: \"Hello from the loader!\" };\n          };\n          export default function Index() {\n            const { message } = useLoaderData<typeof loader>();\n            return <h1>{message}</h1>\n          }\n        `,\n      },\n    });\n\n    // Document requests\n    let documentRes = await fixture.requestDocument(\"/\");\n    let html = await documentRes.text();\n    expect(html).toContain(\"<body>\");\n    expect(html).toContain(\"<h1>Hello from the loader!</h1>\");\n    documentRes = await fixture.requestDocument(\"/\", {\n      headers: {\n        \"If-None-Match\": \"1234\",\n      },\n    });\n    expect(documentRes.status).toBe(304);\n    expect(await documentRes.text()).toBe(\"\");\n\n    // Data requests\n    let dataRes = await fixture.requestSingleFetchData(\"/_root.data\");\n    expect(dataRes.data).toEqual({\n      root: {\n        data: {\n          message: \"ROOT\",\n        },\n      },\n      \"routes/_index\": {\n        data: {\n          message: \"Hello from the loader!\",\n        },\n      },\n    });\n    dataRes = await fixture.requestSingleFetchData(\"/_root.data\", {\n      headers: {\n        \"If-None-Match\": \"1234\",\n      },\n    });\n    expect(dataRes.status).toBe(304);\n    expect(dataRes.data).toBeNull();\n  });\n\n  test.describe(\"revalidations/_routes param\", () => {\n    test(\"does not make a server call if no loaders need to run\", async ({\n      page,\n    }) => {\n      let fixture = await createFixture({\n        files: {\n          \"app/root.tsx\": js`\n            import { Link, Links, Meta, Outlet, Scripts } from \"react-router\";\n            export default function Root() {\n              return (\n                <html lang=\"en\">\n                  <head>\n                    <Meta />\n                    <Links />\n                  </head>\n                  <body>\n                    <Link to=\"/\">Home</Link><br/>\n                    <Link to=\"/a/b\">/a/b</Link><br/>\n                    <Outlet />\n                    <Scripts />\n                  </body>\n                </html>\n              );\n            }\n          `,\n          \"app/routes/a.tsx\": js`\n            import { Outlet } from \"react-router\";\n            export default function Root() {\n              return <Outlet />;\n            }\n          `,\n          \"app/routes/a.b.tsx\": js`\n            export default function Root() {\n              return <h1>B</h1>;\n            }\n          `,\n        },\n      });\n\n      let urls: string[] = [];\n      page.on(\"request\", (req) => {\n        if (req.method() === \"GET\" && req.url().includes(\".data\")) {\n          urls.push(req.url());\n        }\n      });\n\n      let appFixture = await createAppFixture(fixture);\n      let app = new PlaywrightFixture(appFixture, page);\n      await app.goto(\"/\");\n\n      await app.clickLink(\"/a/b\");\n      await page.waitForSelector(\"h1\");\n      expect(await app.getHtml(\"h1\")).toBe(\"<h1>B</h1>\");\n      expect(urls.length).toBe(0);\n    });\n\n    test(\"calls reused parent routes by default\", async ({ page }) => {\n      let fixture = await createFixture({\n        files: {\n          ...files,\n          \"app/routes/_index.tsx\": js`\n            import { Link } from \"react-router\";\n            export default function Component() {\n              return <Link to=\"/parent/a\">Go to /parent/a</Link>;\n            }\n          `,\n          \"app/routes/parent.tsx\": js`\n            import { Link, Outlet, useLoaderData } from \"react-router\";\n            let count = 0;\n            export function loader({ request }) {\n              return { count: ++count };\n            }\n            export default function Component() {\n              return (\n                <>\n                  <p id=\"parent\">Parent Count: {useLoaderData().count}</p>\n                  <Link to=\"/parent/a\">Go to /parent/a</Link>\n                  <Link to=\"/parent/b\">Go to /parent/b</Link>\n                  <Outlet/>\n                </>\n              );\n            }\n          `,\n          \"app/routes/parent.a.tsx\": js`\n            import { useLoaderData } from \"react-router\";\n            let count = 0;\n            export function loader({ request }) {\n              return { count: ++count };\n            }\n            export default function Component() {\n              return <p id=\"a\">A Count: {useLoaderData().count}</p>;\n            }\n          `,\n          \"app/routes/parent.b.tsx\": js`\n            import { useLoaderData } from \"react-router\";\n            let count = 0;\n            export function loader({ request }) {\n              return { count: ++count };\n            }\n            export default function Component() {\n              return <p id=\"b\">B Count: {useLoaderData().count}</p>;\n            }\n          `,\n        },\n      });\n      let appFixture = await createAppFixture(fixture);\n      let app = new PlaywrightFixture(appFixture, page);\n\n      let urls: string[] = [];\n      page.on(\"request\", (req) => {\n        if (req.url().includes(\".data\")) {\n          urls.push(req.url());\n        }\n      });\n\n      await app.goto(\"/\");\n\n      await app.clickLink(\"/parent/a\");\n      await page.waitForSelector(\"#a\");\n      expect(await app.getHtml(\"#parent\")).toContain(\"Parent Count: 1\");\n      expect(await app.getHtml(\"#a\")).toContain(\"A Count: 1\");\n      expect(urls.length).toBe(1);\n      expect(urls[0].endsWith(\"/parent/a.data\")).toBe(true);\n      urls = [];\n\n      await app.clickLink(\"/parent/b\");\n      await page.waitForSelector(\"#b\");\n      expect(await app.getHtml(\"#parent\")).toContain(\"Parent Count: 2\");\n      expect(await app.getHtml(\"#b\")).toContain(\"B Count: 1\");\n      expect(urls.length).toBe(1);\n      expect(urls[0].endsWith(\"/parent/b.data\")).toBe(true);\n      urls = [];\n\n      await app.clickLink(\"/parent/a\");\n      await page.waitForSelector(\"#a\");\n      expect(await app.getHtml(\"#parent\")).toContain(\"Parent Count: 3\");\n      expect(await app.getHtml(\"#a\")).toContain(\"A Count: 2\");\n      expect(urls.length).toBe(1);\n      expect(urls[0].endsWith(\"/parent/a.data\")).toBe(true);\n    });\n\n    test(\"allows reused routes to opt out via shouldRevalidate\", async ({\n      page,\n    }) => {\n      let fixture = await createFixture({\n        files: {\n          ...files,\n          \"app/routes/_index.tsx\": js`\n            import { Link } from \"react-router\";\n            export default function Component() {\n              return <Link to=\"/parent/a\">Go to /parent/a</Link>;\n            }\n          `,\n          \"app/routes/parent.tsx\": js`\n            import { Link, Outlet, useLoaderData } from \"react-router\";\n            let count = 0;\n            export function loader({ request }) {\n              return { count: ++count };\n            }\n            export function shouldRevalidate() {\n              return false;\n            }\n            export default function Component() {\n              return (\n                <>\n                  <p id=\"parent\">Parent Count: {useLoaderData().count}</p>\n                  <Link to=\"/parent/a\">Go to /parent/a</Link>\n                  <Link to=\"/parent/b\">Go to /parent/b</Link>\n                  <Outlet/>\n                </>\n              );\n            }\n          `,\n          \"app/routes/parent.a.tsx\": js`\n            import { useLoaderData } from \"react-router\";\n            let count = 0;\n            export function loader({ request }) {\n              return { count: ++count };\n            }\n            export default function Component() {\n              return <p id=\"a\">A Count: {useLoaderData().count}</p>;\n            }\n          `,\n          \"app/routes/parent.b.tsx\": js`\n            import { useLoaderData } from \"react-router\";\n            let count = 0;\n            export function loader({ request }) {\n              return { count: ++count };\n            }\n            export default function Component() {\n              return <p id=\"b\">B Count: {useLoaderData().count}</p>;\n            }\n          `,\n        },\n      });\n      let appFixture = await createAppFixture(fixture);\n      let app = new PlaywrightFixture(appFixture, page);\n\n      let urls: string[] = [];\n      page.on(\"request\", (req) => {\n        if (req.url().includes(\".data\")) {\n          urls.push(req.url());\n        }\n      });\n\n      await app.goto(\"/\");\n\n      await app.clickLink(\"/parent/a\");\n      await page.waitForSelector(\"#a\");\n      expect(await app.getHtml(\"#parent\")).toContain(\"Parent Count: 1\");\n      expect(await app.getHtml(\"#a\")).toContain(\"A Count: 1\");\n      expect(urls.length).toBe(1);\n      // Not a revalidation on the first navigation so no params\n      expect(urls[0].endsWith(\"/parent/a.data\")).toBe(true);\n      urls = [];\n\n      await app.clickLink(\"/parent/b\");\n      await page.waitForSelector(\"#b\");\n      expect(await app.getHtml(\"#parent\")).toContain(\"Parent Count: 1\");\n      expect(await app.getHtml(\"#b\")).toContain(\"B Count: 1\");\n      expect(urls.length).toBe(1);\n      // Don't reload the parent route\n      expect(\n        urls[0].endsWith(\"/parent/b.data?_routes=root%2Croutes%2Fparent.b\"),\n      ).toBe(true);\n      urls = [];\n\n      await app.clickLink(\"/parent/a\");\n      await page.waitForSelector(\"#a\");\n      expect(await app.getHtml(\"#parent\")).toContain(\"Parent Count: 1\");\n      expect(await app.getHtml(\"#a\")).toContain(\"A Count: 2\");\n      expect(urls.length).toBe(1);\n      // Don't reload the parent route\n      expect(\n        urls[0].endsWith(\"/parent/a.data?_routes=root%2Croutes%2Fparent.a\"),\n      ).toBe(true);\n    });\n\n    test(\"allows reused routes to opt out via shouldRevalidate (w/clientLoader)\", async ({\n      page,\n    }) => {\n      let fixture = await createFixture({\n        files: {\n          ...files,\n          \"app/routes/_index.tsx\": js`\n            import { Link } from \"react-router\";\n            export default function Component() {\n              return <Link to=\"/parent/a\">Go to /parent/a</Link>;\n            }\n          `,\n          \"app/routes/parent.tsx\": js`\n            import { Link, Outlet, useLoaderData } from \"react-router\";\n            let count = 0;\n            export function loader({ request }) {\n              return { count: ++count };\n            }\n            export function shouldRevalidate() {\n              return false;\n            }\n            export function clientLoader({ serverLoader }) {\n              return serverLoader()\n            }\n            export default function Component() {\n              return (\n                <>\n                  <p id=\"parent\">Parent Count: {useLoaderData().count}</p>\n                  <Link to=\"/parent/a\">Go to /parent/a</Link>\n                  <Link to=\"/parent/b\">Go to /parent/b</Link>\n                  <Outlet/>\n                </>\n              );\n            }\n          `,\n          \"app/routes/parent.a.tsx\": js`\n            import { useLoaderData } from \"react-router\";\n            let count = 0;\n            export function loader({ request }) {\n              return { count: ++count };\n            }\n            export default function Component() {\n              return <p id=\"a\">A Count: {useLoaderData().count}</p>;\n            }\n          `,\n          \"app/routes/parent.b.tsx\": js`\n            import { useLoaderData } from \"react-router\";\n            let count = 0;\n            export function loader({ request }) {\n              return { count: ++count };\n            }\n            export default function Component() {\n              return <p id=\"b\">B Count: {useLoaderData().count}</p>;\n            }\n          `,\n        },\n      });\n      let appFixture = await createAppFixture(fixture);\n      let app = new PlaywrightFixture(appFixture, page);\n\n      let urls: string[] = [];\n      page.on(\"request\", (req) => {\n        if (req.url().includes(\".data\")) {\n          urls.push(req.url());\n        }\n      });\n\n      await app.goto(\"/\");\n\n      await app.clickLink(\"/parent/a\");\n      await page.waitForSelector(\"#a\");\n      expect(await app.getHtml(\"#parent\")).toContain(\"Parent Count: 1\");\n      expect(await app.getHtml(\"#a\")).toContain(\"A Count: 1\");\n      expect(urls.length).toBe(2);\n      // Client loader triggers 2 requests on the first navigation\n      expect(urls[0].endsWith(\"/parent/a.data?_routes=routes%2Fparent\")).toBe(\n        true,\n      );\n      expect(\n        urls[1].endsWith(\"/parent/a.data?_routes=root%2Croutes%2Fparent.a\"),\n      ).toBe(true);\n      urls = [];\n\n      await app.clickLink(\"/parent/b\");\n      await page.waitForSelector(\"#b\");\n      expect(await app.getHtml(\"#parent\")).toContain(\"Parent Count: 1\");\n      expect(await app.getHtml(\"#b\")).toContain(\"B Count: 1\");\n      expect(urls.length).toBe(1);\n      // Don't reload the parent route\n      expect(\n        urls[0].endsWith(\"/parent/b.data?_routes=root%2Croutes%2Fparent.b\"),\n      ).toBe(true);\n      urls = [];\n\n      await app.clickLink(\"/parent/a\");\n      await page.waitForSelector(\"#a\");\n      expect(await app.getHtml(\"#parent\")).toContain(\"Parent Count: 1\");\n      expect(await app.getHtml(\"#a\")).toContain(\"A Count: 2\");\n      expect(urls.length).toBe(1);\n      // Don't reload the parent route\n      expect(\n        urls[0].endsWith(\"/parent/a.data?_routes=root%2Croutes%2Fparent.a\"),\n      ).toBe(true);\n    });\n\n    test(\"allows reused routes to opt out via shouldRevalidate (w/only clientLoader)\", async ({\n      page,\n    }) => {\n      let fixture = await createFixture({\n        files: {\n          ...files,\n          \"app/routes/_index.tsx\": js`\n            import { Link } from \"react-router\";\n            export default function Component() {\n              return <Link to=\"/parent/a\">Go to /parent/a</Link>;\n            }\n          `,\n          \"app/routes/parent.tsx\": js`\n            import { Link, Outlet, useLoaderData } from \"react-router\";\n            let count = 0;\n            export function clientLoader({ request }) {\n              return { count: ++count };\n            }\n            export function shouldRevalidate() {\n              return false;\n            }\n            export default function Component() {\n              return (\n                <>\n                  <p id=\"parent\">Parent Count: {useLoaderData().count}</p>\n                  <Link to=\"/parent/a\">Go to /parent/a</Link>\n                  <Link to=\"/parent/b\">Go to /parent/b</Link>\n                  <Outlet/>\n                </>\n              );\n            }\n          `,\n          \"app/routes/parent.a.tsx\": js`\n            import { useLoaderData } from \"react-router\";\n            let count = 0;\n            export function loader({ request }) {\n              return { count: ++count };\n            }\n            export default function Component() {\n              return <p id=\"a\">A Count: {useLoaderData().count}</p>;\n            }\n          `,\n          \"app/routes/parent.b.tsx\": js`\n            import { useLoaderData } from \"react-router\";\n            let count = 0;\n            export function loader({ request }) {\n              return { count: ++count };\n            }\n            export default function Component() {\n              return <p id=\"b\">B Count: {useLoaderData().count}</p>;\n            }\n          `,\n        },\n      });\n      let appFixture = await createAppFixture(fixture);\n      let app = new PlaywrightFixture(appFixture, page);\n\n      let urls: string[] = [];\n      page.on(\"request\", (req) => {\n        if (req.url().includes(\".data\")) {\n          urls.push(req.url());\n        }\n      });\n\n      await app.goto(\"/\");\n\n      await app.clickLink(\"/parent/a\");\n      await page.waitForSelector(\"#a\");\n      expect(await app.getHtml(\"#parent\")).toContain(\"Parent Count: 1\");\n      expect(await app.getHtml(\"#a\")).toContain(\"A Count: 1\");\n      expect(urls.length).toBe(1);\n      // Client loader triggers 2 requests on the first navigation\n      expect(urls[0].endsWith(\"/parent/a.data\")).toBe(true);\n      urls = [];\n\n      await app.clickLink(\"/parent/b\");\n      await page.waitForSelector(\"#b\");\n      expect(await app.getHtml(\"#parent\")).toContain(\"Parent Count: 1\");\n      expect(await app.getHtml(\"#b\")).toContain(\"B Count: 1\");\n      expect(urls.length).toBe(1);\n      expect(urls[0].endsWith(\"/parent/b.data\")).toBe(true);\n      urls = [];\n\n      await app.clickLink(\"/parent/a\");\n      await page.waitForSelector(\"#a\");\n      expect(await app.getHtml(\"#parent\")).toContain(\"Parent Count: 1\");\n      expect(await app.getHtml(\"#a\")).toContain(\"A Count: 2\");\n      expect(urls.length).toBe(1);\n      expect(urls[0].endsWith(\"/parent/a.data\")).toBe(true);\n    });\n\n    test(\"provides the proper defaultShouldRevalidate value\", async ({\n      page,\n    }) => {\n      let fixture = await createFixture({\n        files: {\n          ...files,\n          \"app/routes/_index.tsx\": js`\n            import { Link } from 'react-router';\n            export default function Component() {\n              return <Link to=\"/parent/a\">Go to /parent/a</Link>;\n            }\n          `,\n          \"app/routes/parent.tsx\": js`\n            import { Link, Outlet, useLoaderData } from 'react-router';\n            let count = 0;\n            export function loader({ request }) {\n              return { count: ++count };\n            }\n            export function shouldRevalidate({ defaultShouldRevalidate }) {\n              return defaultShouldRevalidate;\n            }\n            export default function Component() {\n              return (\n                <>\n                  <p id=\"parent\">Parent Count: {useLoaderData().count}</p>\n                  <Link to=\"/parent/a\">Go to /parent/a</Link>\n                  <Link to=\"/parent/b\">Go to /parent/b</Link>\n                  <Outlet/>\n                </>\n              );\n            }\n          `,\n          \"app/routes/parent.a.tsx\": js`\n            import { useLoaderData } from 'react-router';\n            let count = 0;\n            export function loader({ request }) {\n              return { count: ++count };\n            }\n            export default function Component() {\n              return <p id=\"a\">A Count: {useLoaderData().count}</p>;\n            }\n          `,\n          \"app/routes/parent.b.tsx\": js`\n            import { useLoaderData } from 'react-router';\n            let count = 0;\n            export function loader({ request }) {\n              return { count: ++count };\n            }\n            export default function Component() {\n              return <p id=\"b\">B Count: {useLoaderData().count}</p>;\n            }\n          `,\n        },\n      });\n      let appFixture = await createAppFixture(fixture);\n      let app = new PlaywrightFixture(appFixture, page);\n\n      let urls: string[] = [];\n      page.on(\"request\", (req) => {\n        if (req.url().includes(\".data\")) {\n          urls.push(req.url());\n        }\n      });\n\n      await app.goto(\"/\");\n\n      await app.clickLink(\"/parent/a\");\n      await page.waitForSelector(\"#a\");\n      expect(await app.getHtml(\"#parent\")).toContain(\"Parent Count: 1\");\n      expect(await app.getHtml(\"#a\")).toContain(\"A Count: 1\");\n      expect(urls.length).toBe(1);\n      expect(urls[0].endsWith(\"/parent/a.data\")).toBe(true);\n      urls = [];\n\n      await app.clickLink(\"/parent/b\");\n      await page.waitForSelector(\"#b\");\n      expect(await app.getHtml(\"#parent\")).toContain(\"Parent Count: 2\");\n      expect(await app.getHtml(\"#b\")).toContain(\"B Count: 1\");\n      expect(urls.length).toBe(1);\n      // Reload the parent route\n      expect(urls[0].endsWith(\"/parent/b.data\")).toBe(true);\n      urls = [];\n\n      await app.clickLink(\"/parent/a\");\n      await page.waitForSelector(\"#a\");\n      expect(await app.getHtml(\"#parent\")).toContain(\"Parent Count: 3\");\n      expect(await app.getHtml(\"#a\")).toContain(\"A Count: 2\");\n      expect(urls.length).toBe(1);\n      // Reload the parent route\n      expect(urls[0].endsWith(\"/parent/a.data\")).toBe(true);\n    });\n\n    test(\"does not add a _routes param for routes without loaders\", async ({\n      page,\n    }) => {\n      let fixture = await createFixture({\n        files: {\n          ...files,\n          \"app/routes/_index.tsx\": js`\n            import { Link } from \"react-router\";\n            export default function Component() {\n              return <Link to=\"/parent/a\">Go to /parent/a</Link>;\n            }\n          `,\n          \"app/routes/parent.tsx\": js`\n            import { Link, Outlet, useLoaderData } from \"react-router\";\n            let count = 0;\n            export function loader({ request }) {\n              return { count: ++count };\n            }\n            export function shouldRevalidate() {\n              return false;\n            }\n            export default function Component() {\n              return (\n                <>\n                  <p id=\"parent\">Parent Count: {useLoaderData().count}</p>\n                  <Link to=\"/parent/a\">Go to /parent/a</Link>\n                  <Link to=\"/parent/b\">Go to /parent/b</Link>\n                  <Outlet/>\n                </>\n              );\n            }\n          `,\n          \"app/routes/parent.a.tsx\": js`\n            import { useLoaderData } from \"react-router\";\n            let count = 0;\n            export function loader({ request }) {\n              return { count: ++count };\n            }\n            export default function Component() {\n              return <p id=\"a\">A Count: {useLoaderData().count}</p>;\n            }\n          `,\n          \"app/routes/parent.b.tsx\": js`\n            export default function Component() {\n              return <p id=\"b\">B</p>;\n            }\n          `,\n        },\n      });\n      let appFixture = await createAppFixture(fixture);\n      let app = new PlaywrightFixture(appFixture, page);\n\n      let urls: string[] = [];\n      page.on(\"request\", (req) => {\n        if (req.url().includes(\".data\")) {\n          urls.push(req.url());\n        }\n      });\n\n      await app.goto(\"/\");\n\n      await app.clickLink(\"/parent/a\");\n      await page.waitForSelector(\"#a\");\n      expect(await app.getHtml(\"#parent\")).toContain(\"Parent Count: 1\");\n      expect(await app.getHtml(\"#a\")).toContain(\"A Count: 1\");\n      expect(urls.length).toBe(1);\n      // Not a revalidation on the first navigation so no params\n      expect(urls[0].endsWith(\"/parent/a.data\")).toBe(true);\n      urls = [];\n\n      await app.clickLink(\"/parent/b\");\n      await page.waitForSelector(\"#b\");\n      expect(await app.getHtml(\"#parent\")).toContain(\"Parent Count: 1\");\n      expect(await app.getHtml(\"#b\")).toContain(\"B\");\n      expect(urls.length).toBe(1);\n      // Don't reload the parent route\n      expect(urls[0].endsWith(\"/parent/b.data?_routes=root\")).toBe(true);\n      urls = [];\n    });\n  });\n\n  test.describe(\"client loaders\", () => {\n    test(\"when no routes have client loaders\", async ({ page }) => {\n      let fixture = await createFixture({\n        files: {\n          ...files,\n          \"app/routes/a.tsx\": js`\n            import { Outlet, useLoaderData } from 'react-router';\n\n            export function loader() {\n              return { message: \"A server loader\" };\n            }\n\n            export default function Comp() {\n              let data = useLoaderData();\n              return (\n                <>\n                  <h1>A</h1>\n                  <p id=\"a-data\">{data.message}</p>\n                  <Outlet/>\n                </>\n              );\n            }\n          `,\n          \"app/routes/a.b.tsx\": js`\n            import { Outlet, useLoaderData } from 'react-router';\n\n            export function loader() {\n              return { message: \"B server loader\" };\n            }\n\n            export default function Comp() {\n              let data = useLoaderData();\n              return (\n                <>\n                  <h1>B</h1>\n                  <p id=\"b-data\">{data.message}</p>\n                  <Outlet/>\n                </>\n              );\n            }\n          `,\n          \"app/routes/a.b.c.tsx\": js`\n            import { useLoaderData } from 'react-router';\n\n            export function  loader() {\n              return { message: \"C server loader\" };\n            }\n\n            export default function Comp() {\n              let data = useLoaderData();\n              return (\n                <>\n                  <h1>C</h1>\n                  <p id=\"c-data\">{data.message}</p>\n                </>\n              );\n            }\n          `,\n        },\n      });\n\n      let urls: string[] = [];\n      page.on(\"request\", (req) => {\n        if (req.method() === \"GET\" && req.url().includes(\".data\")) {\n          urls.push(req.url());\n        }\n      });\n\n      let appFixture = await createAppFixture(fixture);\n      let app = new PlaywrightFixture(appFixture, page);\n      await app.goto(\"/\");\n      await app.clickLink(\"/a/b/c\");\n      await page.waitForSelector(\"#c-data\");\n      expect(await app.getHtml(\"#a-data\")).toContain(\"A server loader\");\n      expect(await app.getHtml(\"#b-data\")).toContain(\"B server loader\");\n      expect(await app.getHtml(\"#c-data\")).toContain(\"C server loader\");\n\n      // No clientLoaders so we can make a single parameter-less fetch\n      expect(urls).toEqual([expect.stringMatching(/\\/a\\/b\\/c\\.data$/)]);\n    });\n\n    test(\"when one route has a client loader\", async ({ page }) => {\n      let fixture = await createFixture({\n        files: {\n          ...files,\n          \"app/routes/a.tsx\": js`\n            import { Outlet, useLoaderData } from 'react-router';\n\n            export function loader() {\n              return { message: \"A server loader\" };\n            }\n\n            export default function Comp() {\n              let data = useLoaderData();\n              return (\n                <>\n                  <h1>A</h1>\n                  <p id=\"a-data\">{data.message}</p>\n                  <Outlet/>\n                </>\n              );\n            }\n          `,\n          \"app/routes/a.b.tsx\": js`\n            import { Outlet, useLoaderData } from 'react-router';\n\n            export function loader() {\n              return { message: \"B server loader\" };\n            }\n\n            export default function Comp() {\n              let data = useLoaderData();\n              return (\n                <>\n                  <h1>B</h1>\n                  <p id=\"b-data\">{data.message}</p>\n                  <Outlet/>\n                </>\n              );\n            }\n          `,\n          \"app/routes/a.b.c.tsx\": js`\n            import { useLoaderData } from 'react-router';\n\n            export function  loader() {\n              return { message: \"C server loader\" };\n            }\n\n            export async function clientLoader({ serverLoader }) {\n              let data = await serverLoader();\n              return { message: data.message + \" (C client loader)\" };\n            }\n\n            export default function Comp() {\n              let data = useLoaderData();\n              return (\n                <>\n                  <h1>C</h1>\n                  <p id=\"c-data\">{data.message}</p>\n                </>\n              );\n            }\n          `,\n        },\n      });\n\n      let urls: string[] = [];\n      page.on(\"request\", (req) => {\n        if (req.method() === \"GET\" && req.url().includes(\".data\")) {\n          urls.push(req.url());\n        }\n      });\n\n      let appFixture = await createAppFixture(fixture);\n      let app = new PlaywrightFixture(appFixture, page);\n      await app.goto(\"/\");\n      await app.clickLink(\"/a/b/c\");\n      await page.waitForSelector(\"#c-data\");\n      expect(await app.getHtml(\"#a-data\")).toContain(\"A server loader\");\n      expect(await app.getHtml(\"#b-data\")).toContain(\"B server loader\");\n      expect(await app.getHtml(\"#c-data\")).toContain(\n        \"C server loader (C client loader)\",\n      );\n\n      // root/A/B can be loaded together, C needs it's own call due to it's clientLoader\n      expect(urls.sort()).toEqual([\n        expect.stringMatching(\n          /\\/a\\/b\\/c\\.data\\?_routes=root%2Croutes%2Fa%2Croutes%2Fa\\.b$/,\n        ),\n        expect.stringMatching(/\\/a\\/b\\/c\\.data\\?_routes=routes%2Fa\\.b\\.c$/),\n      ]);\n    });\n\n    test(\"when multiple routes have client loaders\", async ({ page }) => {\n      let fixture = await createFixture({\n        files: {\n          ...files,\n          \"app/routes/a.tsx\": js`\n            import { Outlet, useLoaderData } from 'react-router';\n\n            export function loader() {\n              return { message: \"A server loader\" };\n            }\n\n            export default function Comp() {\n              let data = useLoaderData();\n              return (\n                <>\n                  <h1>A</h1>\n                  <p id=\"a-data\">{data.message}</p>\n                  <Outlet/>\n                </>\n              );\n            }\n          `,\n          \"app/routes/a.b.tsx\": js`\n            import { Outlet, useLoaderData } from 'react-router';\n\n            export function loader() {\n              return { message: \"B server loader\" };\n            }\n\n            export async function clientLoader({ serverLoader }) {\n              let data = await serverLoader();\n              return { message: data.message + \" (B client loader)\" };\n            }\n\n            export default function Comp() {\n              let data = useLoaderData();\n              return (\n                <>\n                  <h1>B</h1>\n                  <p id=\"b-data\">{data.message}</p>\n                  <Outlet/>\n                </>\n              );\n            }\n          `,\n          \"app/routes/a.b.c.tsx\": js`\n            import { useLoaderData } from 'react-router';\n\n            export function  loader() {\n              return { message: \"C server loader\" };\n            }\n\n            export async function clientLoader({ serverLoader }) {\n              let data = await serverLoader();\n              return { message: data.message + \" (C client loader)\" };\n            }\n\n            export default function Comp() {\n              let data = useLoaderData();\n              return (\n                <>\n                  <h1>C</h1>\n                  <p id=\"c-data\">{data.message}</p>\n                </>\n              );\n            }\n          `,\n        },\n      });\n\n      let urls: string[] = [];\n      page.on(\"request\", (req) => {\n        if (req.method() === \"GET\" && req.url().includes(\".data\")) {\n          urls.push(req.url());\n        }\n      });\n\n      let appFixture = await createAppFixture(fixture);\n      let app = new PlaywrightFixture(appFixture, page);\n      await app.goto(\"/\");\n      await app.clickLink(\"/a/b/c\");\n      await page.waitForSelector(\"#c-data\");\n      expect(await app.getHtml(\"#a-data\")).toContain(\"A server loader\");\n      expect(await app.getHtml(\"#b-data\")).toContain(\n        \"B server loader (B client loader)\",\n      );\n      expect(await app.getHtml(\"#c-data\")).toContain(\n        \"C server loader (C client loader)\",\n      );\n\n      // B/C have client loaders so they get individual calls, root/A go together\n      expect(urls.sort()).toEqual([\n        expect.stringMatching(/\\/a\\/b\\/c\\.data\\?_routes=root%2Croutes%2Fa$/),\n        expect.stringMatching(/\\/a\\/b\\/c\\.data\\?_routes=routes%2Fa\\.b$/),\n        expect.stringMatching(/\\/a\\/b\\/c\\.data\\?_routes=routes%2Fa\\.b\\.c$/),\n      ]);\n    });\n\n    test(\"when all routes have client loaders\", async ({ page }) => {\n      let fixture = await createFixture({\n        files: {\n          ...files,\n          \"app/routes/a.tsx\": js`\n            import { Outlet, useLoaderData } from 'react-router';\n\n            export function loader() {\n              return { message: \"A server loader\" };\n            }\n\n            export async function clientLoader({ serverLoader }) {\n              let data = await serverLoader();\n              return { message: data.message + \" (A client loader)\" };\n            }\n\n            export default function Comp() {\n              let data = useLoaderData();\n              return (\n                <>\n                  <h1>A</h1>\n                  <p id=\"a-data\">{data.message}</p>\n                  <Outlet/>\n                </>\n              );\n            }\n          `,\n          \"app/routes/a.b.tsx\": js`\n            import { Outlet, useLoaderData } from 'react-router';\n\n            export function loader() {\n              return { message: \"B server loader\" };\n            }\n\n            export async function clientLoader({ serverLoader }) {\n              let data = await serverLoader();\n              return { message: data.message + \" (B client loader)\" };\n            }\n\n            export default function Comp() {\n              let data = useLoaderData();\n              return (\n                <>\n                  <h1>B</h1>\n                  <p id=\"b-data\">{data.message}</p>\n                  <Outlet/>\n                </>\n              );\n            }\n          `,\n          \"app/routes/a.b.c.tsx\": js`\n            import { useLoaderData } from 'react-router';\n\n            export function  loader() {\n              return { message: \"C server loader\" };\n            }\n\n            export async function clientLoader({ serverLoader }) {\n              let data = await serverLoader();\n              return { message: data.message + \" (C client loader)\" };\n            }\n\n            export default function Comp() {\n              let data = useLoaderData();\n              return (\n                <>\n                  <h1>C</h1>\n                  <p id=\"c-data\">{data.message}</p>\n                </>\n              );\n            }\n          `,\n        },\n      });\n\n      let urls: string[] = [];\n      page.on(\"request\", (req) => {\n        if (req.method() === \"GET\" && req.url().includes(\".data\")) {\n          urls.push(req.url());\n        }\n      });\n\n      let appFixture = await createAppFixture(fixture);\n      let app = new PlaywrightFixture(appFixture, page);\n      await app.goto(\"/\");\n      await app.clickLink(\"/a/b/c\");\n      await page.waitForSelector(\"#c-data\");\n      expect(await app.getHtml(\"#a-data\")).toContain(\n        \"A server loader (A client loader)\",\n      );\n      expect(await app.getHtml(\"#b-data\")).toContain(\n        \"B server loader (B client loader)\",\n      );\n      expect(await app.getHtml(\"#c-data\")).toContain(\n        \"C server loader (C client loader)\",\n      );\n\n      // root/A/B/C all have client loaders so they get individual calls\n      expect(urls.sort()).toEqual([\n        expect.stringMatching(/\\/a\\/b\\/c.data\\?_routes=root$/),\n        expect.stringMatching(/\\/a\\/b\\/c.data\\?_routes=routes%2Fa$/),\n        expect.stringMatching(/\\/a\\/b\\/c.data\\?_routes=routes%2Fa.b$/),\n        expect.stringMatching(/\\/a\\/b\\/c.data\\?_routes=routes%2Fa.b.c$/),\n      ]);\n    });\n  });\n\n  test.describe(\"fetchers\", () => {\n    test(\"Fetcher loaders call singular routes\", async ({ page }) => {\n      let fixture = await createFixture({\n        files: {\n          ...files,\n          \"app/routes/a.tsx\": js`\n            import { Outlet } from \"react-router\";\n            export default function Comp() {\n              return <Outlet />;\n            }\n          `,\n          \"app/routes/a.b.tsx\": js`\n            import { useFetcher } from \"react-router\";\n            export function loader() {\n              return { message: 'LOADER' };\n            }\n            export default function Comp() {\n              let fetcher = useFetcher();\n              return (\n                <>\n                  <button id=\"load\" onClick={() => fetcher.load('/a/b')}>Load</button>\n                  {fetcher.data ? <p id=\"data\">{fetcher.data.message}</p> : null}\n                </>\n              );\n            }\n          `,\n        },\n      });\n\n      let urls: string[] = [];\n      page.on(\"request\", (req) => {\n        if (req.method() === \"GET\" && req.url().includes(\".data\")) {\n          urls.push(req.url());\n        }\n      });\n\n      let appFixture = await createAppFixture(fixture);\n      let app = new PlaywrightFixture(appFixture, page);\n      await app.goto(\"/a/b\");\n      await app.clickElement(\"#load\");\n      await page.waitForSelector(\"#data\");\n      expect(await app.getHtml(\"#data\")).toContain(\"LOADER\");\n\n      // No clientLoaders so we can make a single parameter-less fetch\n      expect(urls.length).toBe(1);\n      expect(urls[0].endsWith(\"/a/b.data?_routes=routes%2Fa.b\")).toBe(true);\n    });\n\n    test(\"Fetcher actions call singular routes\", async ({ page }) => {\n      let fixture = await createFixture({\n        files: {\n          ...files,\n          \"app/routes/a.tsx\": js`\n            import { Outlet } from \"react-router\";\n            export default function Comp() {\n              return <Outlet />;\n            }\n          `,\n          \"app/routes/a.b.tsx\": js`\n            import { useFetcher } from \"react-router\";\n            export function action() {\n              return { message: 'ACTION' };\n            }\n            export default function Comp() {\n              let fetcher = useFetcher();\n              return (\n                <>\n                  <button id=\"submit\" onClick={() => {\n                    fetcher.submit({}, {\n                      method: 'post',\n                      action: '/a/b'\n                    });\n                  }}>\n                    Load\n                  </button>\n                  {fetcher.data ? <p id=\"data\">{fetcher.data.message}</p> : null}\n                </>\n              );\n            }\n          `,\n        },\n      });\n\n      let urls: string[] = [];\n      page.on(\"request\", (req) => {\n        if (req.method() === \"GET\" && req.url().includes(\".data\")) {\n          urls.push(req.url());\n        }\n      });\n\n      let appFixture = await createAppFixture(fixture);\n      let app = new PlaywrightFixture(appFixture, page);\n      await app.goto(\"/a/b\");\n      await app.clickElement(\"#submit\");\n      await page.waitForSelector(\"#data\");\n      expect(await app.getHtml(\"#data\")).toContain(\"ACTION\");\n\n      // No clientLoaders so we can make a single parameter-less fetch\n      expect(urls.length).toBe(1);\n      expect(urls[0].endsWith(\"/a/b.data\")).toBe(true);\n    });\n\n    test(\"Fetcher loads do not revalidate on GET navigations by default\", async ({\n      page,\n    }) => {\n      let fixture = await createFixture({\n        files: {\n          ...files,\n          \"app/routes/parent.tsx\": js`\n            import { Link, Outlet, useFetcher } from \"react-router\";\n            export default function Component() {\n              let fetcher = useFetcher();\n              return (\n                <>\n                  <Link to=\"/parent/a\">Go to /parent/a</Link>\n                  <Link to=\"/parent/b\">Go to /parent/b</Link>\n                  <button id=\"load\" onClick={() => fetcher.load('/fetch')}>\n                    Load Fetcher\n                  </button>\n                  {fetcher.data ? <p id=\"fetch\">Fetch Count: {fetcher.data.count}</p> : null}\n                  <Outlet/>\n                </>\n              );\n            }\n          `,\n          \"app/routes/parent.a.tsx\": js`\n            export default function Component() {\n              return <p id=\"a\">A</p>;\n            }\n          `,\n          \"app/routes/parent.b.tsx\": js`\n            export default function Component() {\n              return <p id=\"b\">B</p>;\n            }\n          `,\n          \"app/routes/fetch.tsx\": js`\n            let count = 0;\n            export function loader({ request }) {\n              return { count: ++count };\n            }\n            export default function Component() {\n              return <h1>Fetch</h1>;\n            }\n          `,\n        },\n      });\n      let appFixture = await createAppFixture(fixture);\n      let app = new PlaywrightFixture(appFixture, page);\n\n      let urls: string[] = [];\n      page.on(\"request\", (req) => {\n        if (req.url().includes(\".data\")) {\n          urls.push(req.url());\n        }\n      });\n\n      await app.goto(\"/parent/a\");\n      await app.clickElement(\"#load\");\n      await page.waitForSelector(\"#fetch\");\n      expect(await app.getHtml(\"#fetch\")).toContain(\"Fetch Count: 1\");\n      expect(urls.length).toBe(1);\n      expect(urls[0].endsWith(\"/fetch.data?_routes=routes%2Ffetch\")).toBe(true);\n      urls = [];\n\n      await app.clickLink(\"/parent/b\");\n      await page.waitForSelector(\"#b\");\n      expect(await app.getHtml(\"#fetch\")).toContain(\"Fetch Count: 1\");\n      expect(urls.length).toBe(1);\n      expect(urls[0].endsWith(\"/parent/b.data\")).toBe(true);\n    });\n\n    test(\"Fetcher loads can opt into revalidation on GET navigations\", async ({\n      page,\n    }) => {\n      let fixture = await createFixture({\n        files: {\n          ...files,\n          \"app/routes/parent.tsx\": js`\n            import { Link, Outlet, useFetcher } from \"react-router\";\n            export default function Component() {\n              let fetcher = useFetcher();\n              return (\n                <>\n                  <Link to=\"/parent/a\">Go to /parent/a</Link>\n                  <Link to=\"/parent/b\">Go to /parent/b</Link>\n                  <button id=\"load\" onClick={() => fetcher.load('/fetch')}>\n                    Load Fetcher\n                  </button>\n                  {fetcher.data ? <p id=\"fetch\">Fetch Count: {fetcher.data.count}</p> : null}\n                  <Outlet/>\n                </>\n              );\n            }\n          `,\n          \"app/routes/parent.a.tsx\": js`\n            export default function Component() {\n              return <p id=\"a\">A</p>;\n            }\n          `,\n          \"app/routes/parent.b.tsx\": js`\n            export default function Component() {\n              return <p id=\"b\">B</p>;\n            }\n          `,\n          \"app/routes/fetch.tsx\": js`\n            let count = 0;\n            export function loader({ request }) {\n              return { count: ++count };\n            }\n            export function shouldRevalidate() {\n              return true;\n            }\n            export default function Component() {\n              return <h1>Fetch</h1>;\n            }\n          `,\n        },\n      });\n      let appFixture = await createAppFixture(fixture);\n      let app = new PlaywrightFixture(appFixture, page);\n\n      let urls: string[] = [];\n      page.on(\"request\", (req) => {\n        if (req.url().includes(\".data\")) {\n          urls.push(req.url());\n        }\n      });\n\n      await app.goto(\"/parent/a\");\n      await app.clickElement(\"#load\");\n      await page.waitForSelector(\"#fetch\");\n      expect(await app.getHtml(\"#fetch\")).toContain(\"Fetch Count: 1\");\n      expect(urls.length).toBe(1);\n      expect(urls[0].endsWith(\"/fetch.data?_routes=routes%2Ffetch\")).toBe(true);\n      urls = [];\n\n      await app.clickLink(\"/parent/b\");\n      await page.waitForSelector(\"#b\");\n      expect(await app.getHtml(\"#fetch\")).toContain(\"Fetch Count: 2\");\n      expect(urls.length).toBe(2);\n      expect(urls[0].endsWith(\"/fetch.data?_routes=routes%2Ffetch\")).toBe(true);\n      expect(urls[1].endsWith(\"/parent/b.data\")).toBe(true);\n    });\n\n    test(\"Aborted fetcher loads don't cause console errors\", async ({\n      page,\n    }) => {\n      let fixture = await createFixture({\n        files: {\n          ...files,\n          \"app/routes/_index.tsx\": js`\n            import { Form, redirect, useFetcher } from \"react-router\";\n\n            export function action() {\n              return redirect(\"/other\");\n            }\n\n            export default function Page() {\n              const fetcher = useFetcher();\n              const isPending = fetcher.state !== \"idle\";\n\n              return (\n                <>\n                  <button id=\"fetch\" onClick={() => fetcher.load(\"/fetch\")}>\n                    {isPending ? \"Loading...\" : \"First load data\"}\n                  </button>\n                  <Form method=\"POST\">\n                    <button type=\"submit\">Then submit before load ends</button>\n                  </Form>\n                </>\n              );\n            }\n          `,\n          \"app/routes/other.tsx\": js`\n            export default function Component() {\n              return <p id=\"other\">Other</p>;\n            }\n          `,\n          \"app/routes/fetch.tsx\": js`\n            export async function loader() {\n              await new Promise((r) => setTimeout(r, 10000));\n              return 'nope';\n            }\n          `,\n        },\n      });\n      let appFixture = await createAppFixture(fixture);\n      let app = new PlaywrightFixture(appFixture, page);\n\n      // Capture console logs and uncaught errors\n      let msgs: string[] = [];\n      page.on(\"console\", (msg) => msgs.push(msg.text()));\n      page.on(\"pageerror\", (error) => msgs.push(error.message));\n\n      await app.goto(\"/\", true);\n      app.clickElement(\"#fetch\");\n      await app.clickSubmitButton(\"/?index\");\n      await page.waitForSelector(\"#other\");\n      expect(msgs).toEqual([]);\n    });\n  });\n\n  test.describe(\"prefetching\", () => {\n    test(\"when no routes have client loaders\", async ({ page }) => {\n      let fixture = await createFixture({\n        files: {\n          ...files,\n          \"app/routes/_index.tsx\": js`\n            import {  Link } from \"react-router\";\n\n            export default function Index() {\n              return (\n                <nav>\n                  <Link to=\"/a/b/c\" prefetch=\"render\">/a/b/c</Link>\n                </nav>\n              );\n            }\n          `,\n          \"app/routes/a.tsx\": js`\n            import { Outlet, useLoaderData } from 'react-router';\n\n            export function loader() {\n              return { message: \"A server loader\" };\n            }\n\n            export default function Comp() {\n              let data = useLoaderData();\n              return (\n                <>\n                  <h1>A</h1>\n                  <p id=\"a-data\">{data.message}</p>\n                  <Outlet/>\n                </>\n              );\n            }\n          `,\n          \"app/routes/a.b.tsx\": js`\n            import { Outlet, useLoaderData } from 'react-router';\n\n            export function loader() {\n              return { message: \"B server loader\" };\n            }\n\n            export default function Comp() {\n              let data = useLoaderData();\n              return (\n                <>\n                  <h1>B</h1>\n                  <p id=\"b-data\">{data.message}</p>\n                  <Outlet/>\n                </>\n              );\n            }\n          `,\n          \"app/routes/a.b.c.tsx\": js`\n            import { useLoaderData } from 'react-router';\n\n            export function  loader() {\n              return { message: \"C server loader\" };\n            }\n\n            export default function Comp() {\n              let data = useLoaderData();\n              return (\n                <>\n                  <h1>C</h1>\n                  <p id=\"c-data\">{data.message}</p>\n                </>\n              );\n            }\n          `,\n        },\n      });\n\n      let appFixture = await createAppFixture(fixture);\n      let app = new PlaywrightFixture(appFixture, page);\n      await app.goto(\"/\", true);\n      // No clientLoaders so we can make a single parameter-less fetch\n      await page.waitForSelector(\n        \"link[rel='prefetch'][as='fetch'][href='/a/b/c.data']\",\n        { state: \"attached\" },\n      );\n      expect(await app.page.locator(\"link[as='fetch']\").count()).toEqual(1);\n    });\n\n    test(\"when one route has a client loader\", async ({ page }) => {\n      let fixture = await createFixture({\n        files: {\n          ...files,\n          \"app/routes/_index.tsx\": js`\n            import {  Link } from \"react-router\";\n\n            export default function Index() {\n              return (\n                <nav>\n                  <Link to=\"/a/b/c\" prefetch=\"render\">/a/b/c</Link>\n                </nav>\n              );\n            }\n          `,\n          \"app/routes/a.tsx\": js`\n            import { Outlet, useLoaderData } from 'react-router';\n\n            export function loader() {\n              return { message: \"A server loader\" };\n            }\n\n            export default function Comp() {\n              let data = useLoaderData();\n              return (\n                <>\n                  <h1>A</h1>\n                  <p id=\"a-data\">{data.message}</p>\n                  <Outlet/>\n                </>\n              );\n            }\n          `,\n          \"app/routes/a.b.tsx\": js`\n            import { Outlet, useLoaderData } from 'react-router';\n\n            export function loader() {\n              return { message: \"B server loader\" };\n            }\n\n            export default function Comp() {\n              let data = useLoaderData();\n              return (\n                <>\n                  <h1>B</h1>\n                  <p id=\"b-data\">{data.message}</p>\n                  <Outlet/>\n                </>\n              );\n            }\n          `,\n          \"app/routes/a.b.c.tsx\": js`\n            import { useLoaderData } from 'react-router';\n\n            export function  loader() {\n              return { message: \"C server loader\" };\n            }\n\n            export async function clientLoader({ serverLoader }) {\n              let data = await serverLoader();\n              return { message: data.message + \" (C client loader)\" };\n            }\n\n            export default function Comp() {\n              let data = useLoaderData();\n              return (\n                <>\n                  <h1>C</h1>\n                  <p id=\"c-data\">{data.message}</p>\n                </>\n              );\n            }\n          `,\n        },\n      });\n\n      let appFixture = await createAppFixture(fixture);\n      let app = new PlaywrightFixture(appFixture, page);\n      await app.goto(\"/\", true);\n\n      // root/A/B can be prefetched, C doesn't get prefetched due to its `clientLoader`\n      await page.waitForSelector(\n        \"link[rel='prefetch'][as='fetch'][href='/a/b/c.data?_routes=root%2Croutes%2Fa%2Croutes%2Fa.b']\",\n        { state: \"attached\" },\n      );\n      expect(await app.page.locator(\"link[as='fetch']\").count()).toEqual(1);\n    });\n\n    test(\"when multiple routes have client loaders\", async ({ page }) => {\n      let fixture = await createFixture({\n        files: {\n          ...files,\n          \"app/routes/_index.tsx\": js`\n            import {  Link } from \"react-router\";\n\n            export default function Index() {\n              return (\n                <nav>\n                  <Link to=\"/a/b/c\" prefetch=\"render\">/a/b/c</Link>\n                </nav>\n              );\n            }\n          `,\n          \"app/routes/a.tsx\": js`\n            import { Outlet, useLoaderData } from 'react-router';\n\n            export function loader() {\n              return { message: \"A server loader\" };\n            }\n\n            export default function Comp() {\n              let data = useLoaderData();\n              return (\n                <>\n                  <h1>A</h1>\n                  <p id=\"a-data\">{data.message}</p>\n                  <Outlet/>\n                </>\n              );\n            }\n          `,\n          \"app/routes/a.b.tsx\": js`\n            import { Outlet, useLoaderData } from 'react-router';\n\n            export function loader() {\n              return { message: \"B server loader\" };\n            }\n\n            export async function clientLoader({ serverLoader }) {\n              let data = await serverLoader();\n              return { message: data.message + \" (B client loader)\" };\n            }\n\n            export default function Comp() {\n              let data = useLoaderData();\n              return (\n                <>\n                  <h1>B</h1>\n                  <p id=\"b-data\">{data.message}</p>\n                  <Outlet/>\n                </>\n              );\n            }\n          `,\n          \"app/routes/a.b.c.tsx\": js`\n            import { useLoaderData } from 'react-router';\n\n            export function  loader() {\n              return { message: \"C server loader\" };\n            }\n\n            export async function clientLoader({ serverLoader }) {\n              let data = await serverLoader();\n              return { message: data.message + \" (C client loader)\" };\n            }\n\n            export default function Comp() {\n              let data = useLoaderData();\n              return (\n                <>\n                  <h1>C</h1>\n                  <p id=\"c-data\">{data.message}</p>\n                </>\n              );\n            }\n          `,\n        },\n      });\n\n      let appFixture = await createAppFixture(fixture);\n      let app = new PlaywrightFixture(appFixture, page);\n      await app.goto(\"/\", true);\n\n      // root/A can get prefetched, B/C can't due to `clientLoader`\n      await page.waitForSelector(\n        \"link[rel='prefetch'][as='fetch'][href='/a/b/c.data?_routes=root%2Croutes%2Fa']\",\n        { state: \"attached\" },\n      );\n      expect(await app.page.locator(\"link[as='fetch']\").count()).toEqual(1);\n    });\n\n    test(\"when all routes have client loaders\", async ({ page }) => {\n      let fixture = await createFixture({\n        files: {\n          ...files,\n          \"app/root.tsx\": js`\n            import { Links, Meta, Outlet, Scripts } from \"react-router\";\n            export function loader() {\n              return {\n                message: \"ROOT\",\n              };\n            }\n            export async function clientLoader({ serverLoader }) {\n              let data = await serverLoader();\n              return { message: data.message + \" (root client loader)\" };\n            }\n            export default function Root() {\n              return (\n                <html lang=\"en\">\n                  <head>\n                    <Meta />\n                    <Links />\n                  </head>\n                  <body>\n                    <Outlet />\n                    <Scripts />\n                  </body>\n                </html>\n              );\n            }\n          `,\n          \"app/routes/_index.tsx\": js`\n            import {  Link } from \"react-router\";\n\n            export default function Index() {\n              return (\n                <nav>\n                  <Link to=\"/a/b/c\" prefetch=\"render\">/a/b/c</Link>\n                </nav>\n              );\n            }\n          `,\n          \"app/routes/a.tsx\": js`\n            import { Outlet, useLoaderData } from 'react-router';\n\n            export function loader() {\n              return { message: \"A server loader\" };\n            }\n\n            export async function clientLoader({ serverLoader }) {\n              let data = await serverLoader();\n              return { message: data.message + \" (A client loader)\" };\n            }\n\n            export default function Comp() {\n              let data = useLoaderData();\n              return (\n                <>\n                  <h1>A</h1>\n                  <p id=\"a-data\">{data.message}</p>\n                  <Outlet/>\n                </>\n              );\n            }\n          `,\n          \"app/routes/a.b.tsx\": js`\n            import { Outlet, useLoaderData } from 'react-router';\n\n            export function loader() {\n              return { message: \"B server loader\" };\n            }\n\n            export async function clientLoader({ serverLoader }) {\n              let data = await serverLoader();\n              return { message: data.message + \" (B client loader)\" };\n            }\n\n            export default function Comp() {\n              let data = useLoaderData();\n              return (\n                <>\n                  <h1>B</h1>\n                  <p id=\"b-data\">{data.message}</p>\n                  <Outlet/>\n                </>\n              );\n            }\n          `,\n          \"app/routes/a.b.c.tsx\": js`\n            import { useLoaderData } from 'react-router';\n\n            export function  loader() {\n              return { message: \"C server loader\" };\n            }\n\n            export async function clientLoader({ serverLoader }) {\n              let data = await serverLoader();\n              return { message: data.message + \" (C client loader)\" };\n            }\n\n            export default function Comp() {\n              let data = useLoaderData();\n              return (\n                <>\n                  <h1>C</h1>\n                  <p id=\"c-data\">{data.message}</p>\n                </>\n              );\n            }\n          `,\n        },\n      });\n\n      let appFixture = await createAppFixture(fixture);\n      let app = new PlaywrightFixture(appFixture, page);\n      await app.goto(\"/\", true);\n\n      // No prefetching due to clientLoaders\n      expect(await app.page.locator(\"link[as='fetch']\").count()).toEqual(0);\n    });\n\n    test(\"when a reused route opts out of revalidation\", async ({ page }) => {\n      let fixture = await createFixture({\n        files: {\n          ...files,\n          \"app/routes/a.tsx\": js`\n            import { Link, Outlet, useLoaderData } from 'react-router';\n            export function loader() {\n              return { message: \"A server loader\" };\n            }\n            export function shouldRevalidate() {\n              return false;\n            }\n            export default function Comp() {\n              let data = useLoaderData();\n              return (\n                <>\n                  <h1>A</h1>\n                  <p id=\"a-data\">{data.message}</p>\n                  <nav>\n                    <Link to=\"/a/b/c\" prefetch=\"render\">/a/b/c</Link>\n                  </nav>\n                  <Outlet/>\n                </>\n              );\n            }\n          `,\n          \"app/routes/a.b.tsx\": js`\n            import { Outlet, useLoaderData } from 'react-router';\n            export function loader() {\n              return { message: \"B server loader\" };\n            }\n            export default function Comp() {\n              let data = useLoaderData();\n              return (\n                <>\n                  <h1>B</h1>\n                  <p id=\"b-data\">{data.message}</p>\n                  <Outlet/>\n                </>\n              );\n            }\n          `,\n          \"app/routes/a.b.c.tsx\": js`\n            import { useLoaderData } from 'react-router';\n            export function  loader() {\n              return { message: \"C server loader\" };\n            }\n            export default function Comp() {\n              let data = useLoaderData();\n              return (\n                <>\n                  <h1>C</h1>\n                  <p id=\"c-data\">{data.message}</p>\n                </>\n              );\n            }\n          `,\n        },\n      });\n\n      let appFixture = await createAppFixture(fixture);\n      let app = new PlaywrightFixture(appFixture, page);\n      await app.goto(\"/a\", true);\n\n      // A opted out of revalidation\n      await page.waitForSelector(\n        \"link[rel='prefetch'][as='fetch'][href='/a/b/c.data?_routes=root%2Croutes%2Fa.b%2Croutes%2Fa.b.c']\",\n        { state: \"attached\" },\n      );\n      expect(await app.page.locator(\"link[as='fetch']\").count()).toEqual(1);\n    });\n\n    test(\"when a reused route opts out of revalidation and another route has a clientLoader\", async ({\n      page,\n    }) => {\n      let fixture = await createFixture({\n        files: {\n          ...files,\n          \"app/routes/a.tsx\": js`\n            import { Link, Outlet, useLoaderData } from 'react-router';\n            export function loader() {\n              return { message: \"A server loader\" };\n            }\n            export function shouldRevalidate() {\n              return false;\n            }\n            export default function Comp() {\n              let data = useLoaderData();\n              return (\n                <>\n                  <h1>A</h1>\n                  <p id=\"a-data\">{data.message}</p>\n                  <nav>\n                    <Link to=\"/a/b/c\" prefetch=\"render\">/a/b/c</Link>\n                  </nav>\n                  <Outlet/>\n                </>\n              );\n            }\n          `,\n          \"app/routes/a.b.tsx\": js`\n            import { Outlet, useLoaderData } from 'react-router';\n            export function loader() {\n              return { message: \"B server loader\" };\n            }\n            export default function Comp() {\n              let data = useLoaderData();\n              return (\n                <>\n                  <h1>B</h1>\n                  <p id=\"b-data\">{data.message}</p>\n                  <Outlet/>\n                </>\n              );\n            }\n          `,\n          \"app/routes/a.b.c.tsx\": js`\n            import { useLoaderData } from 'react-router';\n            export function  loader() {\n              return { message: \"C server loader\" };\n            }\n            export async function clientLoader({ serverLoader }) {\n              let data = await serverLoader();\n              return { message: data.message + \" (C client loader)\" };\n            }\n            export default function Comp() {\n              let data = useLoaderData();\n              return (\n                <>\n                  <h1>C</h1>\n                  <p id=\"c-data\">{data.message}</p>\n                </>\n              );\n            }\n          `,\n        },\n      });\n\n      let appFixture = await createAppFixture(fixture);\n      let app = new PlaywrightFixture(appFixture, page);\n      await app.goto(\"/a\", true);\n\n      // A opted out of revalidation\n      await page.waitForSelector(\n        \"link[rel='prefetch'][as='fetch'][href='/a/b/c.data?_routes=root%2Croutes%2Fa.b']\",\n        { state: \"attached\" },\n      );\n      expect(await app.page.locator(\"link[as='fetch']\").count()).toEqual(1);\n    });\n  });\n\n  test(\"supports nonce on streaming script tags\", async ({ page }) => {\n    let fixture = await createFixture({\n      files: {\n        ...files,\n        \"app/root.tsx\": js`\n          import { Links, Meta, Outlet, Scripts } from \"react-router\";\n\n          export function loader() {\n            return {\n              message: \"ROOT\",\n            };\n          }\n\n          export default function Root() {\n            return (\n              <html lang=\"en\">\n                <head>\n                  <Meta />\n                  <Links />\n                </head>\n                <body>\n                  <Outlet />\n                  <Scripts nonce=\"the-nonce\" />\n                </body>\n              </html>\n            );\n          }\n        `,\n        \"app/entry.server.tsx\": js`\n          import { PassThrough } from \"node:stream\";\n\n          import type { EntryContext } from \"react-router\";\n          import { createReadableStreamFromReadable } from \"@react-router/node\";\n          import { ServerRouter } from \"react-router\";\n          import { renderToPipeableStream } from \"react-dom/server\";\n\n          export default function handleRequest(\n            request: Request,\n            responseStatusCode: number,\n            responseHeaders: Headers,\n            remixContext: EntryContext\n          ) {\n            return new Promise((resolve, reject) => {\n              const { pipe } = renderToPipeableStream(\n                <ServerRouter context={remixContext} url={request.url} nonce=\"the-nonce\" />,\n                {\n                  onShellReady() {\n                    const body = new PassThrough();\n                    const stream = createReadableStreamFromReadable(body);\n                    responseHeaders.set(\"Content-Type\", \"text/html\");\n                    resolve(\n                      new Response(stream, {\n                        headers: responseHeaders,\n                        status: responseStatusCode,\n                      })\n                    );\n                    pipe(body);\n                  },\n                  onShellError(error: unknown) {\n                    reject(error);\n                  },\n                  onError(error: unknown) {\n                    responseStatusCode = 500;\n                  },\n                }\n              );\n            });\n          }\n        `,\n      },\n    });\n    let appFixture = await createAppFixture(fixture);\n    let app = new PlaywrightFixture(appFixture, page);\n    await app.goto(\"/data\", true);\n    let scripts = await page.$$(\"script\");\n\n    // Scripts:\n    // RR:    window.__reactRouterContext\n    // RR:    window.__reactRouterManifest/window.__reactRouterRouteModules\n    // React: requestAnimationFrame(function(){$RT=performance.now()});\n    // RR:    window.__reactRouterContext.streamController.enqueue()\n    // React: $RC=function(b,c,e){...\n    // RR:    window.__reactRouterContext.streamController.close();\n    // React: $RC(\"B:1\",\"S:1\")\n    expect(scripts.length).toBe(7);\n\n    let remixScriptsCount = 0;\n    for (let script of scripts) {\n      let content = await script.innerHTML();\n      if (content.includes(\"window.__reactRouter\")) {\n        remixScriptsCount++;\n        expect(await script.getAttribute(\"nonce\")).toEqual(\"the-nonce\");\n      }\n    }\n    expect(remixScriptsCount).toBe(4);\n  });\n\n  test(\"supports loaders that return undefined\", async ({ page }) => {\n    let fixture = await createFixture(\n      {\n        files: {\n          ...files,\n          \"app/routes/_index.tsx\": js`\n            import { Link } from \"react-router\";\n\n            export default function () {\n              return <Link to=\"/loader\">Go to /loader</Link>;\n            }\n          `,\n          \"app/routes/loader.tsx\": js`\n            import { useLoaderData } from \"react-router\";\n\n            export async function loader() {}\n\n            export default function () {\n              let data = useLoaderData();\n              return <h1>{data === undefined? 'It worked!' : 'Error'}</h1>;\n            }\n          `,\n        },\n      },\n      ServerMode.Development,\n    );\n\n    // Document requests\n    let res = await fixture.requestDocument(\"/loader\");\n    expect(res.status).toBe(200);\n    expect(await res.text()).toMatch(\"It worked!\");\n\n    // SPA navigations\n    let appFixture = await createAppFixture(fixture, ServerMode.Development);\n    let app = new PlaywrightFixture(appFixture, page);\n    await app.goto(\"/\");\n    await app.clickLink(\"/loader\");\n    await page.waitForSelector(\"h1\");\n    expect(await app.getHtml(\"h1\")).toMatch(\"It worked!\");\n  });\n\n  test(\"supports actions that return undefined\", async ({ page }) => {\n    let fixture = await createFixture(\n      {\n        files: {\n          ...files,\n          \"app/routes/_index.tsx\": js`\n            import { Form } from \"react-router\";\n\n            export default function () {\n              return (\n                <Form method=\"post\" action=\"/action\">\n                  <input name=\"test\" />\n                  <button type=\"submit\">Submit</button>\n                </Form>\n              );\n            }\n          `,\n          \"app/routes/action.tsx\": js`\n            import { useActionData } from \"react-router\";\n\n            export async function action() {}\n\n            export default function () {\n              let data = useActionData();\n              return <h1>{data === undefined? 'It worked!' : 'Error'}</h1>;\n            }\n          `,\n        },\n      },\n      ServerMode.Development,\n    );\n\n    // Document requests\n    let res = await fixture.requestDocument(\"/action\");\n    expect(res.status).toBe(200);\n    expect(await res.text()).toMatch(\"It worked!\");\n\n    // SPA navigations\n    let appFixture = await createAppFixture(fixture, ServerMode.Development);\n    let app = new PlaywrightFixture(appFixture, page);\n    await app.goto(\"/\");\n    await app.clickSubmitButton(\"/action\");\n    await page.waitForSelector(\"h1\");\n    expect(await app.getHtml(\"h1\")).toMatch(\"It worked!\");\n  });\n\n  test(\"always uses /{path}.data without future.unstable_trailingSlashAwareDataRequests flag\", async ({\n    page,\n  }) => {\n    let fixture = await createFixture({\n      files: {\n        ...files,\n        \"app/routes/_index.tsx\": js`\n          import { Link } from \"react-router\";\n\n          export default function Index() {\n            return (\n              <div>\n                <h1>Home</h1>\n                <Link to=\"/about/\">Go to About (with trailing slash)</Link>\n                <Link to=\"/about\">Go to About (without trailing slash)</Link>\n              </div>\n            );\n          }\n        `,\n        \"app/routes/about.tsx\": js`\n          import { Link, useLoaderData } from \"react-router\";\n\n          export function loader({ request }) {\n            let url = new URL(request.url);\n            return {\n              pathname: url.pathname,\n              hasTrailingSlash: url.pathname.endsWith(\"/\"),\n            };\n          }\n\n          export default function About() {\n            let { pathname, hasTrailingSlash } = useLoaderData();\n            return (\n              <div>\n                <h1>About</h1>\n                <p id=\"pathname\">{pathname}</p>\n                <p id=\"trailing-slash\">{String(hasTrailingSlash)}</p>\n                <Link to=\"/\">Go back home</Link>\n              </div>\n            );\n          }\n        `,\n      },\n    });\n    let appFixture = await createAppFixture(fixture);\n    let app = new PlaywrightFixture(appFixture, page);\n\n    let requests: string[] = [];\n    page.on(\"request\", (req) => {\n      let url = new URL(req.url());\n      if (url.pathname.endsWith(\".data\")) {\n        requests.push(url.pathname + url.search);\n      }\n    });\n\n    // Document load without trailing slash\n    await app.goto(\"/about\");\n    await page.waitForSelector(\"#pathname\");\n    expect(await page.locator(\"#pathname\").innerText()).toEqual(\"/about\");\n    expect(await page.locator(\"#trailing-slash\").innerText()).toEqual(\"false\");\n\n    // Client-side navigation without trailing slash\n    await app.goto(\"/\");\n    await app.clickLink(\"/about\");\n    await page.waitForSelector(\"#pathname\");\n    expect(await page.locator(\"#pathname\").innerText()).toEqual(\"/about\");\n    expect(await page.locator(\"#trailing-slash\").innerText()).toEqual(\"false\");\n    expect(requests).toEqual([\"/about.data\"]);\n    requests = [];\n\n    // Document load with trailing slash\n    await app.goto(\"/about/\");\n    await page.waitForSelector(\"#pathname\");\n    expect(await page.locator(\"#pathname\").innerText()).toEqual(\"/about/\");\n    expect(await page.locator(\"#trailing-slash\").innerText()).toEqual(\"true\");\n\n    // Client-side navigation with trailing slash\n    await app.goto(\"/\");\n    await app.clickLink(\"/about/\");\n    await page.waitForSelector(\"#pathname\");\n    expect(await page.locator(\"#pathname\").innerText()).toEqual(\"/about\");\n    expect(await page.locator(\"#trailing-slash\").innerText()).toEqual(\"false\");\n    expect(requests).toEqual([\"/about.data\"]);\n    requests = [];\n\n    // Client-side navigation back to /\n    await app.clickLink(\"/\");\n    await page.waitForSelector(\"h1:has-text('Home')\");\n    expect(requests).toEqual([\"/_root.data\"]);\n    requests = [];\n  });\n\n  test(\"uses {path}.data or {path}/_.data depending on trailing slash with future.unstable_trailingSlashAwareDataRequests flag\", async ({\n    page,\n  }) => {\n    let fixture = await createFixture({\n      files: {\n        ...files,\n        \"react-router.config.ts\": reactRouterConfig({\n          future: {\n            unstable_trailingSlashAwareDataRequests: true,\n          },\n        }),\n        \"app/routes/_index.tsx\": js`\n          import { Link } from \"react-router\";\n\n          export default function Index() {\n            return (\n              <div>\n                <h1>Home</h1>\n                <Link to=\"/about/\">Go to About (with trailing slash)</Link>\n                <Link to=\"/about\">Go to About (without trailing slash)</Link>\n              </div>\n            );\n          }\n        `,\n        \"app/routes/about.tsx\": js`\n          import { Link, useLoaderData } from \"react-router\";\n\n          export function loader({ request }) {\n            let url = new URL(request.url);\n            return {\n              pathname: url.pathname,\n              hasTrailingSlash: url.pathname.endsWith(\"/\"),\n            };\n          }\n\n          export default function About() {\n            let { pathname, hasTrailingSlash } = useLoaderData();\n            return (\n              <div>\n                <h1>About</h1>\n                <p id=\"pathname\">{pathname}</p>\n                <p id=\"trailing-slash\">{String(hasTrailingSlash)}</p>\n                <Link to=\"/\">Go back home</Link>\n              </div>\n            );\n          }\n        `,\n      },\n    });\n    let appFixture = await createAppFixture(fixture);\n    let app = new PlaywrightFixture(appFixture, page);\n\n    let requests: string[] = [];\n    page.on(\"request\", (req) => {\n      let url = new URL(req.url());\n      if (url.pathname.endsWith(\".data\")) {\n        requests.push(url.pathname + url.search);\n      }\n    });\n\n    // Document load without trailing slash\n    await app.goto(\"/about\");\n    await page.waitForSelector(\"#pathname\");\n    expect(await page.locator(\"#pathname\").innerText()).toEqual(\"/about\");\n    expect(await page.locator(\"#trailing-slash\").innerText()).toEqual(\"false\");\n\n    // Client-side navigation without trailing slash\n    await app.goto(\"/\");\n    await app.clickLink(\"/about\");\n    await page.waitForSelector(\"#pathname\");\n    expect(await page.locator(\"#pathname\").innerText()).toEqual(\"/about\");\n    expect(await page.locator(\"#trailing-slash\").innerText()).toEqual(\"false\");\n    expect(requests).toEqual([\"/about.data\"]);\n    requests = [];\n\n    // Document load with trailing slash\n    await app.goto(\"/about/\");\n    await page.waitForSelector(\"#pathname\");\n    expect(await page.locator(\"#pathname\").innerText()).toEqual(\"/about/\");\n    expect(await page.locator(\"#trailing-slash\").innerText()).toEqual(\"true\");\n\n    // Client-side navigation with trailing slash\n    await app.goto(\"/\");\n    await app.clickLink(\"/about/\");\n    await page.waitForSelector(\"#pathname\");\n    expect(await page.locator(\"#pathname\").innerText()).toEqual(\"/about/\");\n    expect(await page.locator(\"#trailing-slash\").innerText()).toEqual(\"true\");\n    expect(requests).toEqual([\"/about/_.data\"]);\n    requests = [];\n\n    // Client-side navigation back to /\n    await app.clickLink(\"/\");\n    await page.waitForSelector(\"h1:has-text('Home')\");\n    expect(requests).toEqual([\"/_.data\"]);\n    requests = [];\n  });\n});\n"
  },
  {
    "path": "integration/splat-routes-test.ts",
    "content": "import { test, expect } from \"@playwright/test\";\n\nimport { createFixture, js } from \"./helpers/create-fixture.js\";\nimport type { Fixture } from \"./helpers/create-fixture.js\";\n\ntest.describe(\"rendering\", () => {\n  let fixture: Fixture;\n\n  let ROOT_$ = \"FLAT\";\n  let ROOT_INDEX = \"ROOT_INDEX\";\n  let FLAT_$ = \"FLAT\";\n  let PARENT = \"PARENT\";\n  let NESTED_$ = \"NESTED_$\";\n  let NESTED_INDEX = \"NESTED_INDEX\";\n  let PARENTLESS_$ = \"PARENTLESS_$\";\n\n  test.beforeAll(async () => {\n    fixture = await createFixture({\n      files: {\n        \"app/root.tsx\": js`\n          import { Links, Meta, Outlet, Scripts } from \"react-router\";\n\n          export default function Root() {\n            return (\n              <html lang=\"en\">\n                <head>\n                  <Meta />\n                  <Links />\n                </head>\n                <body>\n                  <Outlet />\n                  <Scripts />\n                </body>\n              </html>\n            );\n          }\n        `,\n\n        \"app/routes/_index.tsx\": js`\n          export default function() {\n            return <h2>${ROOT_INDEX}</h2>;\n          }\n        `,\n\n        \"app/routes/$.tsx\": js`\n          export default function() {\n            return <h2>${ROOT_$}</h2>;\n          }\n        `,\n\n        \"app/routes/flat.$.tsx\": js`\n          export default function() {\n            return <h2>${FLAT_$}</h2>\n          }\n        `,\n\n        \"app/routes/nested.tsx\": js`\n          import { Outlet } from \"react-router\";\n          export default function() {\n            return (\n              <div>\n                <h2>${PARENT}</h2>\n                <Outlet/>\n              </div>\n            )\n          }\n        `,\n\n        \"app/routes/nested.$.tsx\": js`\n          export default function() {\n            return <h2>${NESTED_$}</h2>\n          }\n        `,\n\n        \"app/routes/nested._index.tsx\": js`\n          export default function() {\n            return <h2>${NESTED_INDEX}</h2>\n          }\n        `,\n\n        \"app/routes/parentless.$.tsx\": js`\n          export default function() {\n            return <h2>${PARENTLESS_$}</h2>\n          }\n        `,\n      },\n    });\n  });\n\n  test(\"flat exact match\", async () => {\n    let res = await fixture.requestDocument(\"/flat\");\n    expect(await res.text()).toMatch(FLAT_$);\n  });\n\n  test(\"flat deep match\", async () => {\n    let res = await fixture.requestDocument(\"/flat/swig\");\n    expect(await res.text()).toMatch(FLAT_$);\n  });\n\n  test(\"prioritizes index over root splat\", async () => {\n    let res = await fixture.requestDocument(\"/\");\n    expect(await res.text()).toMatch(ROOT_INDEX);\n  });\n\n  test(\"matches root splat\", async () => {\n    let res = await fixture.requestDocument(\"/twisted/sugar\");\n    expect(await res.text()).toMatch(ROOT_$);\n  });\n\n  test(\"prioritizes index over splat for parent route match\", async () => {\n    let res = await fixture.requestDocument(\"/nested\");\n    expect(await res.text()).toMatch(NESTED_INDEX);\n  });\n\n  test(\"nested child\", async () => {\n    let res = await fixture.requestDocument(\"/nested/sodalicious\");\n    expect(await res.text()).toMatch(NESTED_$);\n  });\n\n  test(\"parentless exact match\", async () => {\n    let res = await fixture.requestDocument(\"/parentless\");\n    expect(await res.text()).toMatch(PARENTLESS_$);\n  });\n\n  test(\"parentless deep match\", async () => {\n    let res = await fixture.requestDocument(\"/parentless/chip\");\n    expect(await res.text()).toMatch(PARENTLESS_$);\n  });\n});\n"
  },
  {
    "path": "integration/split-route-modules-test.ts",
    "content": "import { test, expect, type Page } from \"@playwright/test\";\nimport getPort from \"get-port\";\nimport dedent from \"dedent\";\n\nimport {\n  createProject,\n  build,\n  reactRouterServe,\n  viteConfig,\n  reactRouterConfig,\n} from \"./helpers/vite.js\";\n\nconst js = String.raw;\n\nconst files = {\n  \"app/routes/_index.tsx\": js`\n    import { useState, useEffect } from \"react\";\n    import { Link } from \"react-router\";\n\n    export default function IndexRoute() {\n      return (\n        <ul>\n          <li>\n            <Link to=\"/splittable\">/splittable</Link>\n          </li>\n          <li>\n            <Link to=\"/unsplittable\">/unsplittable</Link>\n          </li>\n          <li>\n            <Link to=\"/mixed\">/mixed</Link>\n          </li>\n        </ul>\n      );\n    }\n  `,\n  \"app/routes/splittable/route.tsx\": js`\n    import type { Route } from \"./+types/splittable/route\";\n    import { Form } from \"react-router\";\n    \n    // Ensure these style imports are still included in the page even though\n    // they're not used in the main chunk\n    import clientLoaderStyles from \"./clientLoader.module.css\";\n    import clientActionStyles from \"./clientAction.module.css\";\n    import hydrateFallbackStyles from \"./hydrateFallback.module.css\";\n\n    // Usage of this exported function forces any consuming code into the main\n    // chunk. The variable name is globally unique to prevent name mangling,\n    // e.g. inSplittableMainChunk$1. The use of console.log prevents dead code\n    // elimination in the build by introducing a side effect\n    export const inSplittableMainChunk = () => console.log() || true;\n\n    export const clientLoader = async () => {\n      const pollingPromise = (async () => {\n        while (globalThis.blockClientLoader !== false) {\n          await new Promise((resolve) => setTimeout(resolve, 0));\n        }\n      })();\n      const timeoutPromise = new Promise((_, reject) => {\n        setTimeout(() => reject(new Error(\"Client loader wasn't unblocked after 5s\")), 5000);\n      });\n      await Promise.race([pollingPromise, timeoutPromise]);\n      return {\n        message: \"clientLoader in main chunk: \" + eval(\"typeof inSplittableMainChunk === 'function'\"),\n        className: clientLoaderStyles.root,\n      };\n    };\n\n    export const clientAction = () => ({\n      message: \"clientAction in main chunk: \" + eval(\"typeof inSplittableMainChunk === 'function'\"),\n      className: clientActionStyles.root,\n    });\n\n    export const HydrateFallback = (function() {\n      (globalThis as any).splittableHydrateFallbackDownloaded = true;\n      return () => <div data-hydrate-fallback className={hydrateFallbackStyles.root}>Loading...</div>;\n    })();\n\n    export default function SplittableRoute({\n      loaderData,\n      actionData,\n    }: Route.ComponentProps) {\n      inSplittableMainChunk();\n      return (\n        <>\n          <h1>Splittable Route</h1>\n          <div\n            data-loader-data\n            className={loaderData.className}>\n            loaderData = {JSON.stringify(loaderData.message)}\n          </div>\n          {actionData ? (\n            <div\n              data-action-data\n              className={actionData.className}>\n              actionData = {JSON.stringify(actionData.message)}\n            </div>\n          ) : null}\n          <input type=\"text\" />\n          <Form method=\"post\">\n            <button>Submit</button>\n          </Form>\n        </>\n      );\n    }\n  `,\n  \"app/routes/splittable/clientLoader.module.css\": `\n    .root { padding: 20px; }\n  `,\n  \"app/routes/splittable/clientAction.module.css\": `\n    .root { padding: 20px; }\n  `,\n  \"app/routes/splittable/hydrateFallback.module.css\": `\n    .root { padding: 20px; }\n  `,\n\n  \"app/routes/unsplittable.tsx\": js`\n    import type { Route } from \"./+types/unsplittable\";\n    import { Form } from \"react-router\";\n\n    // Usage of this exported function forces any consuming code into the main\n    // chunk. The variable name is globally unique to prevent name mangling,\n    // e.g. inUnsplittableMainChunk$1. The use of console.log prevents dead code\n    // elimination in the build by introducing a side effect\n    export const inUnsplittableMainChunk = () => console.log() || true;\n\n    export const clientLoader = async () => {\n      inUnsplittableMainChunk();\n      const pollingPromise = (async () => {\n        while (globalThis.blockClientLoader !== false) {\n          await new Promise((resolve) => setTimeout(resolve, 0));\n        }\n      })();\n      const timeoutPromise = new Promise((_, reject) => {\n        setTimeout(() => reject(new Error(\"Client loader wasn't unblocked after 5s\")), 5000);\n      });\n      await Promise.race([pollingPromise, timeoutPromise]);\n      return \"clientLoader in main chunk: \" + eval(\"typeof inUnsplittableMainChunk === 'function'\");\n    };\n \n    export const clientAction = () => {\n      inUnsplittableMainChunk();\n      return \"clientAction in main chunk: \" + eval(\"typeof inUnsplittableMainChunk === 'function'\");\n    }\n\n    export const HydrateFallback = (function() {\n      inUnsplittableMainChunk();\n      (globalThis as any).unsplittableHydrateFallbackDownloaded = true;\n      return () => <div data-hydrate-fallback>Loading...</div>;\n    })();\n\n    export default function UnsplittableRoute({\n      loaderData,\n      actionData,\n    }: Route.ComponentProps) {\n      inUnsplittableMainChunk();\n      return (\n        <>\n          <h1>Unsplittable Route</h1>\n          <div data-loader-data>loaderData = {JSON.stringify(loaderData)}</div>\n          {actionData ? (\n            <div data-action-data>actionData = {JSON.stringify(actionData)}</div>\n          ) : null}\n          <input type=\"text\" />\n          <Form method=\"post\">\n            <button>Submit</button>\n          </Form>\n        </>\n      );\n    }\n  `,\n\n  \"app/routes/mixed.tsx\": js`\n    import type { Route } from \"./+types/mixed\";\n    import { Form } from \"react-router\";\n\n    // Usage of this exported function forces any consuming code into the main\n    // chunk. The variable name is globally unique to prevent name mangling,\n    // e.g. inMixedMainChunk$1. The use of console.log prevents dead code\n    // elimination in the build by introducing a side effect\n    export const inMixedMainChunk = () => console.log() || true;\n\n    export const clientLoader = async () => {\n      inMixedMainChunk();\n      const pollingPromise = (async () => {\n        while (globalThis.blockClientLoader !== false) {\n          await new Promise((resolve) => setTimeout(resolve, 0));\n        }\n      })();\n      const timeoutPromise = new Promise((_, reject) => {\n        setTimeout(() => reject(new Error(\"Client loader wasn't unblocked after 2s\")), 2000);\n      });\n      await Promise.race([pollingPromise, timeoutPromise]);\n      return \"clientLoader in main chunk: \" + eval(\"typeof inMixedMainChunk === 'function'\");\n    };\n \n    export const clientAction = () => {\n      return \"clientAction in main chunk: \" + eval(\"typeof inMixedMainChunk === 'function'\");\n    };\n\n    export const HydrateFallback = (function() {\n      inMixedMainChunk();\n      (globalThis as any).mixedHydrateFallbackDownloaded = true;\n      return () => <div data-hydrate-fallback>Loading...</div>;\n    })();\n\n    export default function MixedRoute({\n      loaderData,\n      actionData,\n    }: Route.ComponentProps) {\n      inMixedMainChunk();\n      return (\n        <>\n          <h1>Mixed Route</h1>\n          <div data-loader-data>loaderData = {JSON.stringify(loaderData)}</div>\n          {actionData ? (\n            <div data-action-data>actionData = {JSON.stringify(actionData)}</div>\n          ) : null}\n          <input type=\"text\" />\n          <Form method=\"post\">\n            <button>Submit</button>\n          </Form>\n        </>\n      );\n    }\n  `,\n};\n\nasync function splittableHydrateFallbackDownloaded(page: Page) {\n  return await page.evaluate(() =>\n    Boolean((globalThis as any).splittableHydrateFallbackDownloaded),\n  );\n}\n\nasync function unsplittableHydrateFallbackDownloaded(page: Page) {\n  return await page.evaluate(() =>\n    Boolean((globalThis as any).unsplittableHydrateFallbackDownloaded),\n  );\n}\nasync function mixedHydrateFallbackDownloaded(page: Page) {\n  return await page.evaluate(() =>\n    Boolean((globalThis as any).mixedHydrateFallbackDownloaded),\n  );\n}\n\nasync function unblockClientLoader(page: Page) {\n  await page.evaluate(() => {\n    (globalThis as any).blockClientLoader = false;\n  });\n}\n\ntest.describe(\"Split route modules\", async () => {\n  test.describe(\"enabled\", () => {\n    let v8_splitRouteModules = true;\n    let port: number;\n    let cwd: string;\n    let stop: Awaited<ReturnType<typeof reactRouterServe>>;\n\n    test.beforeAll(async () => {\n      port = await getPort();\n      cwd = await createProject({\n        \"react-router.config.ts\": reactRouterConfig({\n          future: { v8_splitRouteModules },\n        }),\n        \"vite.config.js\": await viteConfig.basic({ port }),\n        ...files,\n      });\n      build({ cwd });\n      stop = await reactRouterServe({ cwd, port });\n    });\n\n    test.afterAll(() => {\n      stop();\n    });\n\n    test(\"supports splitting route modules\", async ({ page }) => {\n      let pageErrors: Error[] = [];\n      page.on(\"pageerror\", (error) => pageErrors.push(error));\n\n      await page.goto(`http://localhost:${port}`, { waitUntil: \"networkidle\" });\n      await unblockClientLoader(page);\n      expect(pageErrors).toEqual([]);\n\n      // Ensure splittable exports are not in main chunk\n      await page.getByRole(\"link\", { name: \"/splittable\" }).click();\n      await expect(page.getByText(\"Splittable Route\")).toBeVisible();\n      expect(await splittableHydrateFallbackDownloaded(page)).toBe(false);\n      await expect(page.locator(\"[data-loader-data]\")).toHaveText(\n        `loaderData = \"clientLoader in main chunk: false\"`,\n      );\n      expect(await splittableHydrateFallbackDownloaded(page)).toBe(false);\n      expect(page.locator(\"[data-loader-data]\")).toHaveCSS(\"padding\", \"20px\");\n      await page.getByRole(\"button\").click();\n      await expect(page.locator(\"[data-action-data]\")).toHaveText(\n        'actionData = \"clientAction in main chunk: false\"',\n      );\n      expect(page.locator(\"[data-action-data]\")).toHaveCSS(\"padding\", \"20px\");\n\n      await page.goBack();\n\n      // Ensure unsplittable exports are in main chunk\n      await page.getByRole(\"link\", { name: \"/unsplittable\" }).click();\n      await expect(page.getByText(\"Unsplittable Route\")).toBeVisible();\n      expect(await unsplittableHydrateFallbackDownloaded(page)).toBe(true);\n      await expect(page.locator(\"[data-loader-data]\")).toHaveText(\n        'loaderData = \"clientLoader in main chunk: true\"',\n      );\n      await page.getByRole(\"button\").click();\n      await expect(page.locator(\"[data-action-data]\")).toHaveText(\n        'actionData = \"clientAction in main chunk: true\"',\n      );\n\n      await page.goBack();\n\n      // Ensure mix of splittable and unsplittable exports are handled correctly.\n      // Note that only the client action is in its own chunk.\n      await page.getByRole(\"link\", { name: \"/mixed\" }).click();\n      await expect(page.getByText(\"Mixed Route\")).toBeVisible();\n      await expect(page.locator(\"[data-loader-data]\")).toHaveText(\n        'loaderData = \"clientLoader in main chunk: true\"',\n      );\n      expect(await mixedHydrateFallbackDownloaded(page)).toBe(true);\n      await page.getByRole(\"button\").click();\n      await expect(page.locator(\"[data-action-data]\")).toHaveText(\n        'actionData = \"clientAction in main chunk: false\"',\n      );\n\n      // Ensure splittable HydrateFallback and client loader work during SSR\n      await page.goto(`http://localhost:${port}/splittable`);\n      await expect(page.locator(\"[data-hydrate-fallback]\")).toHaveText(\n        \"Loading...\",\n      );\n      await expect(page.locator(\"[data-hydrate-fallback]\")).toHaveCSS(\n        \"padding\",\n        \"20px\",\n      );\n      expect(await splittableHydrateFallbackDownloaded(page)).toBe(true);\n      await unblockClientLoader(page);\n      await expect(page.locator(\"[data-loader-data]\")).toHaveText(\n        `loaderData = \"clientLoader in main chunk: false\"`,\n      );\n      await expect(page.locator(\"[data-loader-data]\")).toHaveCSS(\n        \"padding\",\n        \"20px\",\n      );\n\n      // Ensure unsplittable HydrateFallback and client loader work during SSR\n      await page.goto(`http://localhost:${port}/unsplittable`);\n      await expect(page.locator(\"[data-hydrate-fallback]\")).toHaveText(\n        \"Loading...\",\n      );\n      expect(await unsplittableHydrateFallbackDownloaded(page)).toBe(true);\n      await unblockClientLoader(page);\n      await expect(page.locator(\"[data-loader-data]\")).toHaveText(\n        `loaderData = \"clientLoader in main chunk: true\"`,\n      );\n    });\n  });\n\n  test.describe(\"disabled\", () => {\n    let v8_splitRouteModules = false;\n    let port: number;\n    let cwd: string;\n    let stop: Awaited<ReturnType<typeof reactRouterServe>>;\n\n    test.beforeAll(async () => {\n      port = await getPort();\n      cwd = await createProject({\n        \"react-router.config.ts\": reactRouterConfig({\n          future: { v8_splitRouteModules },\n        }),\n        \"vite.config.js\": await viteConfig.basic({ port }),\n        ...files,\n      });\n      build({ cwd });\n      stop = await reactRouterServe({ cwd, port });\n    });\n\n    test.afterAll(() => {\n      stop();\n    });\n\n    test(\"keeps route module in a single chunk\", async ({ page }) => {\n      let pageErrors: Error[] = [];\n      page.on(\"pageerror\", (error) => pageErrors.push(error));\n\n      await page.goto(`http://localhost:${port}`, { waitUntil: \"networkidle\" });\n      await unblockClientLoader(page);\n      expect(pageErrors).toEqual([]);\n\n      // Ensure splittable exports are kept in main chunk\n      await page.getByRole(\"link\", { name: \"/splittable\" }).click();\n      await expect(page.getByText(\"Splittable Route\")).toBeVisible();\n      expect(await splittableHydrateFallbackDownloaded(page)).toBe(true);\n      await expect(page.locator(\"[data-loader-data]\")).toHaveText(\n        `loaderData = \"clientLoader in main chunk: true\"`,\n      );\n      await expect(page.locator(\"[data-loader-data]\")).toHaveCSS(\n        \"padding\",\n        \"20px\",\n      );\n      await page.getByRole(\"button\").click();\n      await expect(page.locator(\"[data-action-data]\")).toHaveText(\n        'actionData = \"clientAction in main chunk: true\"',\n      );\n      await expect(page.locator(\"[data-action-data]\")).toHaveCSS(\n        \"padding\",\n        \"20px\",\n      );\n\n      await page.goBack();\n\n      // Ensure unsplittable exports are kept in main chunk\n      await page.getByRole(\"link\", { name: \"/unsplittable\" }).click();\n      await expect(page.getByText(\"Unsplittable Route\")).toBeVisible();\n      expect(await unsplittableHydrateFallbackDownloaded(page)).toBe(true);\n      await expect(page.locator(\"[data-loader-data]\")).toHaveText(\n        'loaderData = \"clientLoader in main chunk: true\"',\n      );\n      await page.getByRole(\"button\").click();\n      await expect(page.locator(\"[data-action-data]\")).toHaveText(\n        'actionData = \"clientAction in main chunk: true\"',\n      );\n\n      // Ensure splittable client loader works during SSR\n      await page.goto(`http://localhost:${port}/splittable`);\n      await expect(page.locator(\"[data-hydrate-fallback]\")).toHaveText(\n        \"Loading...\",\n      );\n      await expect(page.locator(\"[data-hydrate-fallback]\")).toHaveCSS(\n        \"padding\",\n        \"20px\",\n      );\n      await unblockClientLoader(page);\n      await expect(page.locator(\"[data-loader-data]\")).toHaveText(\n        `loaderData = \"clientLoader in main chunk: true\"`,\n      );\n\n      // Ensure unsplittable client loader works during SSR\n      await page.goto(`http://localhost:${port}/unsplittable`);\n      await expect(page.locator(\"[data-hydrate-fallback]\")).toHaveText(\n        \"Loading...\",\n      );\n      await unblockClientLoader(page);\n      await expect(page.locator(\"[data-loader-data]\")).toHaveText(\n        `loaderData = \"clientLoader in main chunk: true\"`,\n      );\n    });\n  });\n\n  test.describe(\"enforce\", () => {\n    let v8_splitRouteModules = \"enforce\" as const;\n    let port: number;\n    let cwd: string;\n\n    test.describe(\"splittable routes\", () => {\n      test.beforeAll(async () => {\n        port = await getPort();\n        cwd = await createProject({\n          \"react-router.config.ts\": reactRouterConfig({\n            future: { v8_splitRouteModules },\n          }),\n          \"vite.config.js\": await viteConfig.basic({ port }),\n          // Make unsplittable routes valid so the build can pass\n          \"app/routes/unsplittable.tsx\": \"export default function(){}\",\n          \"app/routes/mixed.tsx\": \"export default function(){}\",\n        });\n      });\n\n      test(\"build passes\", async () => {\n        let { status } = build({ cwd });\n        expect(status).toBe(0);\n      });\n    });\n\n    test.describe(\"splittable routes with splittable root route exports\", () => {\n      test.beforeAll(async () => {\n        port = await getPort();\n        cwd = await createProject({\n          \"react-router.config.ts\": reactRouterConfig({\n            future: { v8_splitRouteModules },\n          }),\n          \"vite.config.js\": await viteConfig.basic({ port }),\n          \"app/root.tsx\": js`\n            import { Outlet } from \"react-router\";\n            export const clientLoader = () => null;\n            export const clientAction = () => null;\n            export default function() {\n              return <Outlet />;\n            }\n          `,\n          // Make unsplittable routes valid so the build can pass\n          \"app/routes/unsplittable.tsx\": \"export default function(){}\",\n          \"app/routes/mixed.tsx\": \"export default function(){}\",\n        });\n      });\n\n      test(\"build passes\", async () => {\n        let { status } = build({ cwd });\n        expect(status).toBe(0);\n      });\n    });\n\n    test.describe(\"splittable routes with unsplittable root route exports\", () => {\n      test.beforeAll(async () => {\n        port = await getPort();\n        cwd = await createProject({\n          \"react-router.config.ts\": reactRouterConfig({\n            future: { v8_splitRouteModules },\n          }),\n          \"vite.config.js\": await viteConfig.basic({ port }),\n          \"app/root.tsx\": js`\n            import { Outlet } from \"react-router\";\n            const shared = null;\n            export const clientLoader = () => shared;\n            export const clientAction = () => shared;\n            export default function() {\n              return <Outlet />;\n            }\n          `,\n          // Make unsplittable routes valid so the build can pass\n          \"app/routes/unsplittable.tsx\": \"export default function(){}\",\n          \"app/routes/mixed.tsx\": \"export default function(){}\",\n        });\n      });\n\n      test(\"build passes\", async () => {\n        let { status } = build({ cwd });\n        expect(status).toBe(0);\n      });\n    });\n\n    test.describe(\"unsplittable routes\", () => {\n      test.beforeAll(async () => {\n        port = await getPort();\n        cwd = await createProject({\n          \"react-router.config.ts\": reactRouterConfig({\n            future: { v8_splitRouteModules },\n          }),\n          \"vite.config.js\": await viteConfig.basic({ port }),\n          ...files,\n          // Ensure we're only testing the mixed route\n          \"app/routes/unsplittable.tsx\": \"export default function(){}\",\n        });\n      });\n\n      test(\"build fails\", async () => {\n        let { stderr, status } = build({ cwd });\n        expect(status).toBe(1);\n        expect(stderr.toString()).toMatch(\n          dedent`\n            Error splitting route module: routes/mixed.tsx\n            \n            - clientLoader\n            - HydrateFallback\n\n            These exports could not be split into their own chunks because they share code with other exports. You should extract any shared code into its own module and then import it within the route module.\n          `,\n        );\n      });\n    });\n  });\n});\n"
  },
  {
    "path": "integration/sri-test.ts",
    "content": "import { test, expect } from \"@playwright/test\";\n\nimport {\n  createAppFixture,\n  createFixture,\n  js,\n} from \"./helpers/create-fixture.js\";\nimport type { AppFixture, Fixture } from \"./helpers/create-fixture.js\";\nimport { reactRouterConfig } from \"./helpers/vite.js\";\nimport { PlaywrightFixture } from \"./helpers/playwright-fixture.js\";\n\ntest.describe(\"CSub-Resource Integrity\", () => {\n  test.use({ javaScriptEnabled: false });\n\n  let fixture: Fixture;\n  let appFixture: AppFixture;\n\n  test.beforeAll(async () => {\n    fixture = await createFixture({\n      files: {\n        \"react-router.config.ts\": reactRouterConfig({\n          future: { unstable_subResourceIntegrity: true },\n        }),\n        \"app/root.tsx\": js`\n          import { Links, Meta, Outlet, Scripts } from \"react-router\";\n\n          export default function Root() {\n            return (\n              <html>\n                <head>\n                  <Meta />\n                  <Links />\n                </head>\n                <body>\n                  <Outlet />\n                  <Scripts nonce=\"test-nonce-123\" />\n                </body>\n              </html>\n            );\n          }\n        `,\n      },\n    });\n    appFixture = await createAppFixture(fixture);\n  });\n\n  test(\"includes an importmap\", async ({ page }) => {\n    let app = new PlaywrightFixture(appFixture, page);\n    await app.goto(\"/\");\n    let json = await page.locator('script[type=\"importmap\"]').innerText();\n    let importMap = JSON.parse(json);\n    expect(Object.keys(importMap.integrity).length).toBeGreaterThan(0);\n    for (let key in importMap.integrity) {\n      if (key.includes(\"manifest\")) continue;\n      let value = importMap.integrity[key];\n      expect(value).toMatch(/^sha384-/);\n\n      let linkEl = page.locator(`link[rel=\"modulepreload\"][href=\"${key}\"]`);\n      expect(await linkEl.getAttribute(\"href\")).toBe(key);\n      expect(await linkEl.getAttribute(\"integrity\")).toBe(value);\n\n      let scriptEl = page.locator(`script[type=\"module\"]`);\n      expect(await scriptEl.innerText()).toContain(key);\n    }\n  });\n\n  test(\"includes a nonce on the importmap script\", async ({ page }) => {\n    let app = new PlaywrightFixture(appFixture, page);\n    await app.goto(\"/\");\n    expect(\n      await page.locator('script[type=\"importmap\"]').getAttribute(\"nonce\"),\n    ).toBe(\"test-nonce-123\");\n  });\n});\n"
  },
  {
    "path": "integration/transition-test.ts",
    "content": "import { test, expect } from \"@playwright/test\";\n\nimport { UNSAFE_decodeViaTurboStream as decodeViaTurboStream } from \"react-router\";\n\nimport {\n  createAppFixture,\n  createFixture,\n  js,\n} from \"./helpers/create-fixture.js\";\nimport type { Fixture, AppFixture } from \"./helpers/create-fixture.js\";\nimport { PlaywrightFixture } from \"./helpers/playwright-fixture.js\";\n\ntest.describe(\"rendering\", () => {\n  let fixture: Fixture;\n  let appFixture: AppFixture;\n\n  let PAGE = \"page\";\n  let PAGE_TEXT = \"PAGE_TEXT\";\n  let PAGE_INDEX_TEXT = \"PAGE_INDEX_TEXT\";\n  let CHILD = \"child\";\n  let CHILD_TEXT = \"CHILD_TEXT\";\n  let REDIRECT = \"redirect\";\n  let REDIRECT_HASH = \"redirect-hash\";\n  let REDIRECT_TARGET = \"page\";\n\n  test.beforeAll(async () => {\n    fixture = await createFixture({\n      files: {\n        \"app/root.tsx\": js`\n          import { Links, Meta, Outlet, Scripts } from \"react-router\";\n\n          export function shouldRevalidate() {\n            return false;\n          }\n\n          export default function Root() {\n            return (\n              <html lang=\"en\">\n                <head>\n                  <Meta />\n                  <Links />\n                </head>\n                <body>\n                  <main>\n                    <Outlet />\n                  </main>\n                  <Scripts />\n                </body>\n              </html>\n            );\n          }\n        `,\n\n        \"app/routes/_index.tsx\": js`\n          import { Link } from \"react-router\";\n          export default function() {\n            return (\n              <div>\n                <h2>Index</h2>\n                <Link to=\"/${PAGE}\">${PAGE}</Link>\n                <Link to=\"/${REDIRECT}\">${REDIRECT}</Link>\n                <Link to=\"/${REDIRECT_HASH}\">${REDIRECT_HASH}</Link>\n              </div>\n            );\n          }\n        `,\n\n        [`app/routes/${PAGE}.jsx`]: js`\n          import { Outlet, useLoaderData } from \"react-router\";\n\n          export function loader() {\n            return \"${PAGE_TEXT}\"\n          }\n\n          export function shouldRevalidate() {\n            return false;\n          }\n\n          export default function() {\n            let text = useLoaderData();\n            return (\n              <>\n                <h2>{text}</h2>\n                <Outlet />\n              </>\n            );\n          }\n        `,\n\n        [`app/routes/${PAGE}._index.jsx`]: js`\n          import { useLoaderData, Link } from \"react-router\";\n\n          export function loader() {\n            return \"${PAGE_INDEX_TEXT}\"\n          }\n\n          export default function() {\n            let text = useLoaderData();\n            return (\n              <>\n                <h3>{text}</h3>\n                <Link to=\"/${PAGE}/${CHILD}\">${CHILD}</Link>\n              </>\n            );\n          }\n        `,\n\n        [`app/routes/${PAGE}.${CHILD}.jsx`]: js`\n          import { useLoaderData } from \"react-router\";\n\n          export function loader() {\n            return \"${CHILD_TEXT}\"\n          }\n\n          export default function() {\n            let text = useLoaderData();\n            return <h3>{text}</h3>;\n          }\n        `,\n\n        [`app/routes/${REDIRECT}.jsx`]: js`\n          import { redirect } from \"react-router\";\n          export function loader() {\n            return redirect(\"/${REDIRECT_TARGET}\")\n          }\n          export default function() {\n            return null;\n          }\n        `,\n\n        [`app/routes/${REDIRECT_HASH}.jsx`]: js`\n          import { redirect } from \"react-router\";\n          export function loader() {\n            return redirect(\"/${REDIRECT_TARGET}#my-hash\")\n          }\n          export default function() {\n            return null;\n          }\n        `,\n\n        \"app/routes/gh-1691.tsx\": js`\n          import { redirect, useFetcher } from \"react-router\";\n\n          export const action = async ( ) => {\n            return redirect(\"/gh-1691\");\n          };\n\n          export const loader = async () => {\n            return {};\n          };\n\n          export default function GitHubIssue1691() {\n            const fetcher = useFetcher();\n\n            return (\n              <div style={{ fontFamily: \"system-ui, sans-serif\", lineHeight: \"1.4\" }}>\n                <span>{fetcher.state}</span>\n                <fetcher.Form method=\"post\">\n                  <input type=\"hidden\" name=\"source\" value=\"fetcher\" />\n                  <button type=\"submit\" name=\"intent\" value=\"add\">\n                    Submit\n                  </button>\n                </fetcher.Form>\n              </div>\n            );\n          }\n        `,\n\n        \"app/routes/parent.tsx\": js`\n          import { Outlet, useLoaderData } from \"react-router\";\n\n          if (!global.counts) {\n            global.count = 0;\n            global.counts = new Set();\n          }\n          export const loader = async ({ request, context }) => {\n            let count = global.count;\n            if (!global.counts.has(context)) {\n              counts.add(context);\n              count = ++global.count;\n            }\n            return { count };\n          };\n\n          export default function Parent() {\n            const data = useLoaderData();\n            return (\n              <div>\n                <div id=\"parent\">{data.count}</div>\n                <Outlet />\n              </div>\n            );\n          }\n        `,\n\n        \"app/routes/parent.child.tsx\": js`\n          import { redirect, useFetcher } from \"react-router\";\n\n          export const action = async ({ request }) => {\n            return redirect(\"/parent\");\n          };\n\n          export default function Child() {\n            const fetcher = useFetcher();\n\n            return (\n              <fetcher.Form method=\"post\">\n                <button id=\"fetcher-submit-redirect\" type=\"submit\">Submit</button>\n              </fetcher.Form>\n            );\n          }\n        `,\n      },\n    });\n\n    appFixture = await createAppFixture(fixture);\n  });\n\n  test.afterAll(() => {\n    appFixture.close();\n  });\n\n  test(\"calls all loaders for new routes\", async ({ page }) => {\n    let app = new PlaywrightFixture(appFixture, page);\n    await app.goto(\"/\");\n    let responses = app.collectSingleFetchResponses();\n\n    await app.clickLink(`/${PAGE}`);\n    await page.waitForLoadState(\"networkidle\");\n\n    expect(responses.map((res) => new URL(res.url()).pathname)).toEqual([\n      `/${PAGE}.data`,\n    ]);\n\n    await page.waitForSelector(`h2:has-text(\"${PAGE_TEXT}\")`);\n    await page.waitForSelector(`h3:has-text(\"${PAGE_INDEX_TEXT}\")`);\n  });\n\n  test(\"calls only loaders for changing routes\", async ({ page }) => {\n    let app = new PlaywrightFixture(appFixture, page);\n    await app.goto(`/${PAGE}`);\n    let responses = app.collectSingleFetchResponses();\n    await app.clickLink(`/${PAGE}/${CHILD}`);\n    await page.waitForLoadState(\"networkidle\");\n\n    expect(responses.map((res) => new URL(res.url()).pathname)).toEqual([\n      `/${PAGE}/${CHILD}.data`,\n    ]);\n\n    let body = new ReadableStream<Uint8Array>({\n      async start(controller) {\n        let buffer = await responses[0].body();\n        controller.enqueue(new Uint8Array(buffer));\n      },\n    });\n    const decoded = await decodeViaTurboStream(body, global);\n\n    expect(Object.keys(decoded.value as Record<string, unknown>)).toEqual([\n      \"routes/page.child\",\n    ]);\n\n    await page.waitForSelector(`h2:has-text(\"${PAGE_TEXT}\")`);\n    await page.waitForSelector(`h3:has-text(\"${CHILD_TEXT}\")`);\n  });\n\n  test(\"loader redirect\", async ({ page }) => {\n    let app = new PlaywrightFixture(appFixture, page);\n    await app.goto(\"/\");\n\n    let responses = app.collectSingleFetchResponses();\n\n    await app.clickLink(`/${REDIRECT}`);\n    await page.waitForURL(/\\/page/);\n    await page.waitForLoadState(\"networkidle\");\n\n    expect(responses.map((res) => new URL(res.url()).pathname)).toEqual([\n      `/${REDIRECT}.data`,\n      `/${PAGE}.data`,\n    ]);\n\n    await page.waitForSelector(`h2:has-text(\"${PAGE_TEXT}\")`);\n    await page.waitForSelector(`h3:has-text(\"${PAGE_INDEX_TEXT}\")`);\n  });\n\n  test(\"loader redirect with hash\", async ({ page }) => {\n    let app = new PlaywrightFixture(appFixture, page);\n    await app.goto(\"/\");\n\n    await app.clickLink(`/${REDIRECT_HASH}`);\n\n    await page.waitForURL(/\\/page#my-hash/);\n    let url = new URL(page.url());\n    expect(url.pathname).toBe(`/${REDIRECT_TARGET}`);\n    expect(url.hash).toBe(`#my-hash`);\n  });\n\n  test(\"calls changing routes on POP\", async ({ page }) => {\n    let app = new PlaywrightFixture(appFixture, page);\n    await app.goto(`/${PAGE}`);\n    await app.clickLink(`/${PAGE}/${CHILD}`);\n\n    let responses = app.collectSingleFetchResponses();\n    await app.goBack();\n    await page.waitForLoadState(\"networkidle\");\n\n    expect(responses.map((res) => new URL(res.url()).pathname)).toEqual([\n      `/${PAGE}.data`,\n    ]);\n    // expect(\n    //   responses.map((res) => new URL(res.url()).searchParams.get(\"_data\"))\n    // ).toEqual([`routes/${PAGE}._index`]);\n\n    await page.waitForSelector(`h2:has-text(\"${PAGE_TEXT}\")`);\n    await page.waitForSelector(`h3:has-text(\"${PAGE_INDEX_TEXT}\")`);\n  });\n\n  test(\"useFetcher state should return to the idle when redirect from an action\", async ({\n    page,\n  }) => {\n    let app = new PlaywrightFixture(appFixture, page);\n    await app.goto(\"/gh-1691\");\n    expect(await app.getHtml(\"span\")).toMatch(\"idle\");\n\n    await app.waitForNetworkAfter(async () => {\n      await app.clickSubmitButton(\"/gh-1691\");\n    });\n    await page.waitForSelector(`span:has-text(\"idle\")`);\n  });\n\n  test(\"fetcher action redirects re-call parent loaders\", async ({ page }) => {\n    let app = new PlaywrightFixture(appFixture, page);\n    await app.goto(\"/parent/child\");\n    await page.waitForSelector(`#parent:has-text(\"1\")`);\n\n    await app.clickElement(\"#fetcher-submit-redirect\");\n    await page.waitForSelector(`#parent:has-text(\"2\")`);\n  });\n});\n"
  },
  {
    "path": "integration/tsconfig.json",
    "content": "{\n  \"include\": [\"**/*.ts\"],\n  \"exclude\": [\"helpers/*\"],\n  \"compilerOptions\": {\n    \"lib\": [\"DOM\", \"DOM.Iterable\", \"ES2022\"],\n    \"target\": \"ES2022\",\n    \"module\": \"ES2022\",\n    \"skipLibCheck\": true,\n\n    \"moduleResolution\": \"Bundler\",\n    \"allowSyntheticDefaultImports\": true,\n    \"strict\": true,\n    \"resolveJsonModule\": true,\n\n    \"noEmit\": true,\n\n    \"rootDir\": \".\"\n  }\n}\n"
  },
  {
    "path": "integration/typegen-test.ts",
    "content": "import fs from \"node:fs/promises\";\n\nimport tsx from \"dedent\";\nimport * as Path from \"pathe\";\n\nimport { test } from \"./helpers/fixtures\";\nimport { reactRouterConfig } from \"./helpers/vite\";\n\nconst viteConfig = ({ rsc }: { rsc: boolean }) => {\n  const reactRouterImportSpecifier = rsc\n    ? \"unstable_reactRouterRSC as reactRouter\"\n    : \"reactRouter\";\n  return tsx`\n    import { ${reactRouterImportSpecifier} } from \"@react-router/dev/vite\";\n\n    export default {\n      plugins: [reactRouter()],\n    };\n  `;\n};\n\ntest.use({\n  files: {\n    \"vite.config.ts\": viteConfig({ rsc: false }),\n    \"app/expect-type.ts\": tsx`\n      export type Expect<T extends true> = T\n\n      export type Equal<X, Y> =\n        (<T>() => T extends X ? 1 : 2) extends\n        (<T>() => T extends Y ? 1 : 2) ? true : false\n    `,\n  },\n});\n\ntest.describe(\"typegen\", () => {\n  test(\"basic\", async ({ edit, $ }) => {\n    await edit({\n      \"app/routes.ts\": tsx`\n        import { type RouteConfig, route } from \"@react-router/dev/routes\";\n\n        export default [\n          route(\"products/:id\", \"routes/product.tsx\")\n        ] satisfies RouteConfig;\n      `,\n      \"app/routes/product.tsx\": tsx`\n        import type { Expect, Equal } from \"../expect-type\"\n        import type { Route } from \"./+types/product\"\n\n        export function loader({ params }: Route.LoaderArgs) {\n          type Test = Expect<Equal<typeof params, { id: string} >>\n          return { planet: \"world\" }\n        }\n\n        export default function Component({ loaderData }: Route.ComponentProps) {\n          type Test = Expect<Equal<typeof loaderData.planet, string>>\n          return <h1>Hello, {loaderData.planet}!</h1>\n        }\n      `,\n    });\n    await $(\"pnpm typecheck\");\n  });\n\n  test(\"repeated params\", async ({ edit, $ }) => {\n    await edit({\n      \"app/routes.ts\": tsx`\n        import { type RouteConfig, route } from \"@react-router/dev/routes\";\n\n        export default [\n          route(\"only-required/:id/:id\", \"routes/only-required.tsx\"),\n          route(\"only-optional/:id?/:id?\", \"routes/only-optional.tsx\"),\n          route(\"optional-then-required/:id?/:id\", \"routes/optional-then-required.tsx\"),\n          route(\"required-then-optional/:id/:id?\", \"routes/required-then-optional.tsx\"),\n        ] satisfies RouteConfig;\n      `,\n      \"app/routes/only-required.tsx\": tsx`\n        import type { Expect, Equal } from \"../expect-type\"\n        import type { Route } from \"./+types/only-required\"\n        export function loader({ params }: Route.LoaderArgs) {\n          type Test = Expect<Equal<typeof params, { id: string }>>\n          return null\n        }\n      `,\n      \"app/routes/only-optional.tsx\": tsx`\n        import type { Expect, Equal } from \"../expect-type\"\n        import type { Route } from \"./+types/only-optional\"\n        export function loader({ params }: Route.LoaderArgs) {\n          type Test = Expect<Equal<typeof params.id, string | undefined>>\n          return null\n        }\n      `,\n      \"app/routes/optional-then-required.tsx\": tsx`\n        import type { Expect, Equal } from \"../expect-type\"\n        import type { Route } from \"./+types/optional-then-required\"\n        export function loader({ params }: Route.LoaderArgs) {\n          type Test = Expect<Equal<typeof params.id,  string>>\n          return null\n        }\n      `,\n      \"app/routes/required-then-optional.tsx\": tsx`\n        import type { Expect, Equal } from \"../expect-type\"\n        import type { Route } from \"./+types/required-then-optional\"\n\n        export function loader({ params }: Route.LoaderArgs) {\n          type Test = Expect<Equal<typeof params.id, string>>\n          return null\n        }\n      `,\n    });\n    await $(\"pnpm typecheck\");\n  });\n\n  test(\"params with extension\", async ({ edit, $ }) => {\n    await edit({\n      \"app/routes.ts\": tsx`\n        import { type RouteConfig, route } from \"@react-router/dev/routes\";\n\n        export default [\n          route(\":lang.xml\", \"routes/param-with-ext.tsx\"),\n          route(\":user?.pdf\", \"routes/optional-param-with-ext.tsx\"),\n        ] satisfies RouteConfig;\n      `,\n      \"app/routes/param-with-ext.tsx\": tsx`\n        import type { Expect, Equal } from \"../expect-type\"\n        import type { Route } from \"./+types/param-with-ext\"\n\n        export function loader({ params }: Route.LoaderArgs) {\n          type Test = Expect<Equal<typeof params, { lang: string }>>\n          return null\n        }\n      `,\n      \"app/routes/optional-param-with-ext.tsx\": tsx`\n        import type { Expect, Equal } from \"../expect-type\"\n        import type { Route } from \"./+types/optional-param-with-ext\"\n\n        export function loader({ params }: Route.LoaderArgs) {\n          type Test = Expect<Equal<typeof params, { user?: string }>>\n          return null\n        }\n      `,\n    });\n    await $(\"pnpm typecheck\");\n  });\n\n  test(\"normalized param\", async ({ edit, $ }) => {\n    await edit({\n      \"app/routes.ts\": tsx`\n        import { type RouteConfig, route, layout } from \"@react-router/dev/routes\";\n\n        export default [\n          route(\"parent/:p\", \"routes/parent.tsx\", [\n            route(\"route/:r\", \"routes/route.tsx\", [\n              route(\"child1/:c1a/:c1b\", \"routes/child1.tsx\"),\n              route(\"child2/:c2a/:c2b\", \"routes/child2.tsx\")\n            ]),\n          ]),\n          layout(\"routes/layout.tsx\", [\n            route(\"in-layout1/:id\", \"routes/in-layout1.tsx\"),\n            route(\"in-layout2/:id/:other\", \"routes/in-layout2.tsx\")\n          ])\n        ] satisfies RouteConfig;\n      `,\n      \"app/routes/parent.tsx\": tsx`\n        import type { Expect, Equal } from \"../expect-type\"\n        import type { Route } from \"./+types/parent\"\n\n        export function loader({ params }: Route.LoaderArgs) {\n          type Test = Expect<Equal<typeof params,\n            | { p: string, r?: undefined, c1a?: undefined,  c1b?: undefined, c2a?: undefined, c2b?: undefined }\n            | { p: string, r: string, c1a?: undefined,  c1b?: undefined, c2a?: undefined, c2b?: undefined }\n            | { p: string, r: string, c1a: string,  c1b: string, c2a?: undefined, c2b?: undefined }\n            | { p: string, r: string, c1a?: undefined,  c1b?: undefined, c2a: string, c2b: string }\n          >>\n          return null\n        }\n      `,\n      \"app/routes/route.tsx\": tsx`\n        import type { Expect, Equal } from \"../expect-type\"\n        import type { Route } from \"./+types/route\"\n\n        export function loader({ params }: Route.LoaderArgs) {\n          type Test = Expect<Equal<typeof params,\n            | { p: string, r: string, c1a?: undefined,  c1b?: undefined, c2a?: undefined, c2b?: undefined }\n            | { p: string, r: string, c1a: string,  c1b: string, c2a?: undefined, c2b?: undefined }\n            | { p: string, r: string, c1a?: undefined,  c1b?: undefined, c2a: string, c2b: string }\n          >>\n          return null\n        }\n      `,\n      \"app/routes/child1.tsx\": tsx`\n        import type { Expect, Equal } from \"../expect-type\"\n        import type { Route } from \"./+types/child1\"\n\n        export function loader({ params }: Route.LoaderArgs) {\n          type Test = Expect<Equal<typeof params, { p: string, r: string, c1a: string,  c1b: string }>>\n          return null\n        }\n      `,\n      \"app/routes/child2.tsx\": tsx`\n        import type { Expect, Equal } from \"../expect-type\"\n        import type { Route } from \"./+types/child2\"\n\n        export function loader({ params }: Route.LoaderArgs) {\n          type Test = Expect<Equal<typeof params, { p: string, r: string, c2a: string, c2b: string }>>\n          return null\n        }\n      `,\n      \"app/routes/layout.tsx\": tsx`\n        import type { Expect, Equal } from \"../expect-type\"\n        import type { Route } from \"./+types/layout\"\n\n        export function loader({ params }: Route.LoaderArgs) {\n          type Test = Expect<Equal<typeof params, { id: string, other?: undefined } | { id: string, other: string } >>\n          return null\n        }\n      `,\n      \"app/routes/in-layout1.tsx\": tsx`\n        import type { Expect, Equal } from \"../expect-type\"\n        import type { Route } from \"./+types/in-layout1\"\n\n        export function loader({ params }: Route.LoaderArgs) {\n          type Test = Expect<Equal<typeof params, { id: string }>>\n          return null\n        }\n      `,\n      \"app/routes/in-layout2.tsx\": tsx`\n        import type { Expect, Equal } from \"../expect-type\"\n        import type { Route } from \"./+types/in-layout2\"\n\n        export function loader({ params }: Route.LoaderArgs) {\n          type Test = Expect<Equal<typeof params, { id: string, other: string }>>\n          return null\n        }\n      `,\n    });\n    await $(\"pnpm typecheck\");\n  });\n\n  test(\"splat\", async ({ edit, $ }) => {\n    await edit({\n      \"app/routes.ts\": tsx`\n        import { type RouteConfig, route } from \"@react-router/dev/routes\";\n\n        export default [\n          route(\"splat/*\", \"routes/splat.tsx\")\n        ] satisfies RouteConfig;\n      `,\n      \"app/routes/splat.tsx\": tsx`\n        import type { Expect, Equal } from \"../expect-type\"\n        import type { Route } from \"./+types/splat\"\n\n        export function loader({ params }: Route.LoaderArgs) {\n          type Test = Expect<Equal<typeof params, { \"*\": string }>>\n          return null\n        }\n      `,\n    });\n    await $(\"pnpm typecheck\");\n  });\n\n  test(\"clientLoader.hydrate = true\", async ({ edit, $ }) => {\n    await edit({\n      \"app/routes/_index.tsx\": tsx`\n        import type { Expect, Equal } from \"../expect-type\"\n        import type { Route } from \"./+types/_index\"\n\n        export function loader() {\n          return { server: \"server\" }\n        }\n\n        export function clientLoader() {\n          return { client: \"client\" }\n        }\n        clientLoader.hydrate = true as const\n\n        export function HydrateFallback() {\n          return <h1>Loading...</h1>\n        }\n\n        export default function Component({ loaderData }: Route.ComponentProps) {\n          type Test = Expect<Equal<typeof loaderData, { client: string }>>\n          return <h1>Hello from {loaderData.client}!</h1>\n        }\n      `,\n    });\n    await $(\"pnpm typecheck\");\n  });\n\n  test(\"clientLoader data should not be serialized\", async ({ edit, $ }) => {\n    await edit({\n      \"app/routes/_index.tsx\": tsx`\n        import { useRouteLoaderData } from \"react-router\"\n\n        import type { Expect, Equal } from \"../expect-type\"\n        import type { Route } from \"./+types/_index\"\n\n        export function clientLoader({}: Route.ClientLoaderArgs) {\n          return { fn: () => 0 }\n        }\n\n        export default function Component({ loaderData }: Route.ComponentProps) {\n          type Test1 = Expect<Equal<typeof loaderData, { fn: () => number }>>\n\n          const routeLoaderData = useRouteLoaderData<typeof clientLoader>(\"routes/_index\")\n          type Test2 = Expect<Equal<typeof routeLoaderData, { fn: () => number} | undefined>>\n\n          return <h1>Hello, world!</h1>\n        }\n      `,\n    });\n    await $(\"pnpm typecheck\");\n  });\n\n  test(\"custom app dir\", async ({ cwd, edit, $ }) => {\n    await edit({\n      \"react-router.config.ts\": reactRouterConfig({\n        appDirectory: \"src/myapp\",\n      }),\n      \"app/routes/products.$id.tsx\": tsx`\n        import type { Expect, Equal } from \"../expect-type\"\n        import type { Route } from \"./+types/products.$id\"\n\n        export function loader({ params }: Route.LoaderArgs) {\n          type Test = Expect<Equal<typeof params, { id: string }>>\n          return { planet: \"world\" }\n        }\n\n        export default function Component({ loaderData }: Route.ComponentProps) {\n          type Test = Expect<Equal<typeof loaderData.planet, string>>\n          return <h1>Hello, {loaderData.planet}!</h1>\n        }\n      `,\n    });\n    await fs.mkdir(Path.join(cwd, \"src\"));\n    await fs.rename(Path.join(cwd, \"app\"), Path.join(cwd, \"src/myapp\"));\n    await $(\"pnpm typecheck\");\n  });\n\n  test(\"matches\", async ({ edit, $ }) => {\n    await edit({\n      \"app/routes.ts\": tsx`\n        import { type RouteConfig, route } from \"@react-router/dev/routes\";\n\n        export default [\n          route(\"parent1/:parent1\", \"routes/parent1.tsx\", [\n            route(\"parent2/:parent2\", \"routes/parent2.tsx\", [\n              route(\"current\", \"routes/current.tsx\")\n            ])\n          ])\n        ] satisfies RouteConfig;\n      `,\n      \"app/routes/parent1.tsx\": tsx`\n        import { Outlet } from \"react-router\"\n\n        export function loader() {\n          return { parent1: 1 }\n        }\n\n        export default function Component() {\n          return (\n            <section>\n              <h1>Parent1</h1>\n              <Outlet />\n            </section>\n          )\n        }\n      `,\n      \"app/routes/parent2.tsx\": tsx`\n        import { Outlet } from \"react-router\"\n\n        export function loader() {\n          return { parent2: 2 }\n        }\n\n        export default function Component() {\n          return (\n            <section>\n              <h2>Parent2</h2>\n              <Outlet />\n            </section>\n          )\n        }\n      `,\n      \"app/routes/current.tsx\": tsx`\n        import type { Expect, Equal } from \"../expect-type\"\n        import type { Route } from \"./+types/current\"\n\n        export function loader() {\n          return { current: 3 }\n        }\n\n        export function meta({ matches }: Route.MetaArgs) {\n          const parent1 = matches[1]\n          type Test1 = Expect<Equal<typeof parent1.data, { parent1: number }>>\n\n          const parent2 = matches[2]\n          type Test2 = Expect<Equal<typeof parent2.data, { parent2: number }>>\n\n          const current = matches[3]\n          type Test3 = Expect<Equal<typeof current.data, { current: number }>>\n\n          const child1 = matches[4]\n          type Test4a = Expect<undefined extends typeof child1 ? true : false>\n          if (child1) {\n            type Test4b = Expect<Equal<typeof child1.data, unknown>>\n          }\n          return []\n        }\n\n        export default function Component({ matches }: Route.ComponentProps) {\n          const parent1 = matches[1]\n          type Test1 = Expect<Equal<typeof parent1.data, { parent1: number }>>\n\n          const parent2 = matches[2]\n          type Test2 = Expect<Equal<typeof parent2.data, { parent2: number }>>\n\n          const current = matches[3]\n          type Test3 = Expect<Equal<typeof current.data, { current: number }>>\n\n          const child1 = matches[4]\n          type Test4a = Expect<undefined extends typeof child1 ? true : false>\n          if (child1) {\n            type Test4b = Expect<Equal<typeof child1.data, unknown>>\n          }\n        }\n      `,\n    });\n    await $(\"pnpm typecheck\");\n  });\n\n  test(\"route files with absolute paths\", async ({ edit, $ }) => {\n    await edit({\n      \"app/routes.ts\": tsx`\n        import path from \"node:path\";\n        import { type RouteConfig, route } from \"@react-router/dev/routes\";\n\n        export default [\n          route(\"absolute/:id\", path.resolve(__dirname, \"routes/absolute.tsx\")),\n        ] satisfies RouteConfig;\n      `,\n      \"app/routes/absolute.tsx\": tsx`\n        import type { Expect, Equal } from \"../expect-type\"\n        import type { Route } from \"./+types/absolute\"\n\n        export function loader({ params }: Route.LoaderArgs) {\n          type Test = Expect<Equal<typeof params, { id: string }>>\n          return { planet: \"world\" }\n        }\n\n        export default function Component({ loaderData }: Route.ComponentProps) {\n          type Test = Expect<Equal<typeof loaderData.planet, string>>\n          return <h1>Hello, {loaderData.planet}!</h1>\n        }\n      `,\n    });\n    await $(\"pnpm typecheck\");\n  });\n\n  test(\"route files with paths outside of app directory\", async ({\n    cwd,\n    edit,\n    $,\n  }) => {\n    await fs.mkdir(Path.join(cwd, \"node_modules/external_dependency\"), {\n      recursive: true,\n    });\n    await edit({\n      \"app/routes.ts\": tsx`\n        import { type RouteConfig, route } from \"@react-router/dev/routes\";\n\n        export default [\n          route(\"outside/:id\", \"../node_modules/external_dependency/index.js\"),\n        ] satisfies RouteConfig;\n      `,\n      \"node_modules/external_dependency/index.js\": tsx`\n        export default function Component() {\n          return <div>External Dependency</div>\n        }\n      `,\n    });\n    await $(\"pnpm typecheck\");\n  });\n\n  test(\"href\", async ({ edit, $ }) => {\n    await edit({\n      \"app/routes.ts\": tsx`\n        import path from \"node:path\";\n        import { type RouteConfig, route } from \"@react-router/dev/routes\";\n\n        export default [\n          route(\"optional-static/opt?\", \"routes/optional-static.tsx\"),\n          route(\"no-params\", \"routes/no-params.tsx\"),\n          route(\"required-param/:req\", \"routes/required-param.tsx\"),\n          route(\"optional-param/:opt?\", \"routes/optional-param.tsx\"),\n          route(\"/leading-and-trailing-slash/\", \"routes/leading-and-trailing-slash.tsx\"),\n          route(\"some-other-route\", \"routes/some-other-route.tsx\"),\n        ] satisfies RouteConfig;\n      `,\n      \"app/routes/optional-static.tsx\": tsx`\n        export default function Component() {}\n      `,\n      \"app/routes/no-params.tsx\": tsx`\n        export default function Component() {}\n      `,\n      \"app/routes/required-param.tsx\": tsx`\n        export default function Component() {}\n      `,\n      \"app/routes/optional-param.tsx\": tsx`\n        export default function Component() {}\n      `,\n      \"app/routes/leading-and-trailing-slash.tsx\": tsx`\n        export default function Component() {}\n      `,\n      \"app/routes/some-other-route.tsx\": tsx`\n        import { href } from \"react-router\"\n\n        // @ts-expect-error\n        href(\"/does-not-exist\")\n\n        href(\"/optional-static\")\n        href(\"/optional-static/opt\")\n        // @ts-expect-error\n        href(\"/optional-static/opt?\")\n\n        href(\"/no-params\")\n\n        // @ts-expect-error\n        href(\"/required-param/:req\")\n        href(\"/required-param/:req\", { req: \"hello\" })\n\n        // @ts-expect-error\n        href(\"/optional-param\")\n        // @ts-expect-error\n        href(\"/optional-param/:opt\", { opt: \"hello\" })\n        href(\"/optional-param/:opt?\")\n        href(\"/optional-param/:opt?\", { opt: \"hello\" })\n\n        href(\"/leading-and-trailing-slash\")\n        // @ts-expect-error\n        href(\"/leading-and-trailing-slash/\")\n\n        export default function Component() {}\n      `,\n    });\n    await $(\"pnpm typecheck\");\n  });\n\n  test(\"reuse route file at multiple paths\", async ({ edit, $ }) => {\n    await edit({\n      \"app/routes.ts\": tsx`\n        import { type RouteConfig, route } from \"@react-router/dev/routes\";\n        export default [\n          route(\"base/:base\", \"routes/base.tsx\", [\n            route(\"home/:home\", \"routes/route.tsx\", { id: \"home\" }),\n            route(\"changelog/:changelog\", \"routes/route.tsx\", { id: \"changelog\" }),\n            route(\"splat/*\", \"routes/route.tsx\", { id: \"splat\" }),\n          ]),\n          route(\"other/:other\", \"routes/route.tsx\", { id: \"other\" })\n        ] satisfies RouteConfig;\n      `,\n      \"app/routes/base.tsx\": tsx`\n        import { Outlet } from \"react-router\"\n        import type { Route } from \"./+types/base\"\n\n        export function loader() {\n          return { base: \"hello\" }\n        }\n\n        export default function Component() {\n          return (\n            <>\n              <h1>Layout</h1>\n              <Outlet/>\n            </>\n          )\n        }\n      `,\n      \"app/routes/route.tsx\": tsx`\n        import type { Expect, Equal } from \"../expect-type\"\n        import type { Route } from \"./+types/route\"\n\n        export function loader() {\n          return { route: \"world\" }\n        }\n\n        export default function Component({ params, matches }: Route.ComponentProps) {\n          type Test = Expect<Equal<typeof params,\n            | {\n                  base: string;\n                  home: string;\n                  changelog?: undefined;\n                  \"*\"?: undefined;\n                  other?: undefined;\n                }\n              | {\n                  base: string;\n                  home?: undefined;\n                  changelog: string;\n                  \"*\"?: undefined;\n                  other?: undefined;\n                }\n              | {\n                  base: string;\n                  home?: undefined;\n                  changelog?: undefined;\n                  \"*\": string;\n                  other?: undefined;\n                }\n              | {\n                  base?: undefined;\n                  home?: undefined;\n                  changelog?: undefined;\n                  \"*\"?: undefined;\n                  other: string;\n                }\n          >>\n          return <h1>Hello, world!</h1>\n        }\n      `,\n    });\n    await $(\"pnpm typecheck\");\n  });\n\n  test.describe(\"virtual:react-router/server-build\", async () => {\n    test(\"static import matches 'createRequestHandler' argument type\", async ({\n      edit,\n      $,\n    }) => {\n      await edit({\n        \"app/routes.ts\": tsx`\n          import { type RouteConfig } from \"@react-router/dev/routes\";\n          export default [] satisfies RouteConfig;\n        `,\n        \"app/handler.ts\": tsx`\n          import { createRequestHandler } from \"react-router\";\n          import * as serverBuild from \"virtual:react-router/server-build\";\n          export default createRequestHandler(serverBuild);\n        `,\n      });\n      await $(\"pnpm typecheck\");\n    });\n\n    test(\"works with tsconfig 'moduleDetection' set to 'force'\", async ({\n      edit,\n      $,\n    }) => {\n      await edit({\n        \"app/routes.ts\": tsx`\n          import { type RouteConfig } from \"@react-router/dev/routes\";\n          export default [] satisfies RouteConfig;\n        `,\n        \"app/handler.ts\": tsx`\n          import { createRequestHandler } from \"react-router\";\n          import * as serverBuild from \"virtual:react-router/server-build\";\n          export default createRequestHandler(serverBuild);\n        `,\n        \"tsconfig.json\": (contents) => {\n          const tsconfig = JSON.parse(contents);\n          tsconfig.compilerOptions.moduleDetection = \"force\";\n          return JSON.stringify(tsconfig, null, 2);\n        },\n      });\n      await $(\"pnpm typecheck\");\n    });\n\n    test(\"dynamic import matches 'createRequestHandler' function argument type\", async ({\n      edit,\n      $,\n    }) => {\n      await edit({\n        \"app/routes.ts\": tsx`\n          import { type RouteConfig } from \"@react-router/dev/routes\";\n          export default [] satisfies RouteConfig;\n        `,\n        \"app/handler.ts\": tsx`\n          import { createRequestHandler } from \"react-router\";\n          export default createRequestHandler(\n            () => import(\"virtual:react-router/server-build\")\n          );\n        `,\n      });\n      await $(\"pnpm typecheck\");\n    });\n  });\n\n  test.describe(\"server-first route component detection\", () => {\n    test.describe(\"ServerComponent export\", () => {\n      test(\"when RSC Framework Mode plugin is present\", async ({ edit, $ }) => {\n        await edit({\n          \"vite.config.ts\": viteConfig({ rsc: true }),\n          \"app/routes.ts\": tsx`\n            import { type RouteConfig, route } from \"@react-router/dev/routes\";\n\n            export default [\n              route(\"server-component/:id\", \"routes/server-component.tsx\")\n            ] satisfies RouteConfig;\n          `,\n          \"app/routes/server-component.tsx\": tsx`\n            import type { Expect, Equal } from \"../expect-type\"\n            import type { Route } from \"./+types/server-component\"\n\n            export function loader({ params }: Route.LoaderArgs) {\n              type Test = Expect<Equal<typeof params, { id: string} >>\n              return { server: \"server\" }\n            }\n\n            export function clientLoader() {\n              return { client: \"client\" }\n            }\n\n            export function action() {\n              return { server: \"server\" }\n            }\n\n            export function clientAction() {\n              return { client: \"client\" }\n            }\n\n            export function ServerComponent({\n              loaderData,\n              actionData\n            }: Route.ComponentProps) {\n              type TestLoaderData = Expect<Equal<typeof loaderData, { server: string }>>\n              type TestActionData = Expect<Equal<typeof actionData, { server: string } | undefined>>\n\n              return (\n                <>\n                  <h1>ServerComponent</h1>\n                  <p>Loader data: {loaderData.server}</p>\n                  <p>Action data: {actionData?.server}</p>\n                </>\n              )\n            }\n\n            export function ErrorBoundary({\n              loaderData,\n              actionData\n            }: Route.ErrorBoundaryProps) {\n              type TestLoaderData = Expect<Equal<typeof loaderData, { server: string } | undefined>>\n              type TestActionData = Expect<Equal<typeof actionData, { server: string } | undefined>>\n\n              return (\n                <>\n                  <h1>ErrorBoundary</h1>\n                  <p>Loader data: {loaderData?.server}</p>\n                  <p>Action data: {actionData?.server}</p>\n                </>\n              )\n            }\n\n            export function HydrateFallback({\n              loaderData,\n              actionData\n            }: Route.HydrateFallbackProps) {\n              type TestLoaderData = Expect<Equal<typeof loaderData, { server: string } | undefined>>\n              type TestActionData = Expect<Equal<typeof actionData, { server: string } | undefined>>\n\n              return (\n                <>\n                  <h1>HydrateFallback</h1>\n                  <p>Loader data: {loaderData?.server}</p>\n                  <p>Action data: {actionData?.server}</p>\n                </>\n              )\n            }\n          `,\n        });\n        await $(\"pnpm typecheck\");\n      });\n\n      test(\"when RSC Framework Mode plugin is not present\", async ({\n        edit,\n        $,\n      }) => {\n        await edit({\n          \"vite.config.ts\": viteConfig({ rsc: false }),\n          \"app/routes.ts\": tsx`\n            import { type RouteConfig, route } from \"@react-router/dev/routes\";\n\n            export default [\n              route(\"server-component/:id\", \"routes/server-component.tsx\")\n            ] satisfies RouteConfig;\n          `,\n          \"app/routes/server-component.tsx\": tsx`\n            import type { Expect, Equal } from \"../expect-type\"\n            import type { Route } from \"./+types/server-component\"\n\n            export function loader({ params }: Route.LoaderArgs) {\n              type Test = Expect<Equal<typeof params, { id: string} >>\n              return { server: \"server\" }\n            }\n\n            export function clientLoader() {\n              return { client: \"client\" }\n            }\n\n            export function action() {\n              return { server: \"server\" }\n            }\n\n            export function clientAction() {\n              return { client: \"client\" }\n            }\n\n            // This export is not used in standard Framework Mode. This is just\n            // to test that the typegen is unaffected by this export outside of\n            // RSC Framework Mode.\n            export function ServerComponent({\n              loaderData,\n              actionData\n            }: Route.ComponentProps) {\n              type TestLoaderData = Expect<Equal<typeof loaderData, { server: string } | { client: string }>>\n              type TestActionData = Expect<Equal<typeof actionData, { server: string } | { client: string } | undefined>>\n\n              return (\n                <>\n                  <h1>ServerComponent (unused)</h1>\n                  <p>Loader data: {\"server\" in loaderData ? loaderData.server : loaderData.client}</p>\n                  {actionData && <p>Action data: {\"server\" in actionData ? actionData.server : actionData.client}</p>}\n                </>\n              )\n            }\n\n            export function ErrorBoundary({\n              loaderData,\n              actionData\n            }: Route.ErrorBoundaryProps) {\n              type TestLoaderData = Expect<Equal<typeof loaderData, { server: string } | { client: string } | undefined>>\n              type TestActionData = Expect<Equal<typeof actionData, { server: string } | { client: string } | undefined>>\n\n              return (\n                <>\n                  <h1>ErrorBoundary</h1>\n                  {loaderData && <p>Loader data: {\"server\" in loaderData ? loaderData.server : loaderData.client}</p>}\n                  {actionData && <p>Action data: {\"server\" in actionData ? actionData.server : actionData.client}</p>}\n                </>\n              )\n            }\n\n            export function HydrateFallback({\n              loaderData,\n              actionData\n            }: Route.HydrateFallbackProps) {\n              type TestLoaderData = Expect<Equal<typeof loaderData, { server: string } | { client: string } | undefined>>\n              type TestActionData = Expect<Equal<typeof actionData, { server: string } | { client: string } | undefined>>\n\n              return (\n                <>\n                  <h1>HydrateFallback</h1>\n                  {loaderData && <p>Loader data: {\"server\" in loaderData ? loaderData.server : loaderData.client}</p>}\n                  {actionData && <p>Action data: {\"server\" in actionData ? actionData.server : actionData.client}</p>}\n                </>\n              )\n            }\n          `,\n        });\n        await $(\"pnpm typecheck\");\n      });\n    });\n\n    test.describe(\"default export\", () => {\n      const clientFirstRouteFiles = {\n        \"app/routes.ts\": tsx`\n          import { type RouteConfig, route } from \"@react-router/dev/routes\";\n\n          export default [\n            route(\"client-component/:id\", \"routes/client-component.tsx\")\n          ] satisfies RouteConfig;\n        `,\n        \"app/routes/client-component.tsx\": tsx`\n          import type { Expect, Equal } from \"../expect-type\"\n          import type { Route } from \"./+types/client-component\"\n\n          export function loader({ params }: Route.LoaderArgs) {\n            type Test = Expect<Equal<typeof params, { id: string} >>\n            return { server: \"server\" }\n          }\n\n          export function clientLoader() {\n            return { client: \"client\" }\n          }\n\n          export function action() {\n            return { server: \"server\" }\n          }\n\n          export function clientAction() {\n            return { client: \"client\" }\n          }\n\n          export default function ClientComponent({\n            loaderData,\n            actionData\n          }: Route.ComponentProps) {\n            type TestLoaderData = Expect<Equal<typeof loaderData, { server: string } | { client: string }>>\n            type TestActionData = Expect<Equal<typeof actionData, { server: string } | { client: string } | undefined>>\n\n            return (\n              <>\n                <h1>default (Component)</h1>\n                <p>Loader data: {\"server\" in loaderData ? loaderData.server : loaderData.client}</p>\n                {actionData && <p>Action data: {\"server\" in actionData ? actionData.server : actionData.client}</p>}\n              </>\n            )\n          }\n\n          export function ErrorBoundary({\n            loaderData,\n            actionData\n          }: Route.ErrorBoundaryProps) {\n            type TestLoaderData = Expect<Equal<typeof loaderData, { server: string } | { client: string } | undefined>>\n            type TestActionData = Expect<Equal<typeof actionData, { server: string } | { client: string } | undefined>>\n\n            return (\n              <>\n                <h1>ErrorBoundary</h1>\n                {loaderData && <p>Loader data: {\"server\" in loaderData ? loaderData.server : loaderData.client}</p>}\n                {actionData && <p>Action data: {\"server\" in actionData ? actionData.server : actionData.client}</p>}\n              </>\n            )\n          }\n\n          export function HydrateFallback({\n            loaderData,\n            actionData\n          }: Route.HydrateFallbackProps) {\n            type TestLoaderData = Expect<Equal<typeof loaderData, { server: string } | { client: string } | undefined>>\n            type TestActionData = Expect<Equal<typeof actionData, { server: string } | { client: string } | undefined>>\n\n            return (\n              <>\n                <h1>HydrateFallback</h1>\n                {loaderData && <p>Loader data: {\"server\" in loaderData ? loaderData.server : loaderData.client}</p>}\n                {actionData && <p>Action data: {\"server\" in actionData ? actionData.server : actionData.client}</p>}\n              </>\n            )\n          }\n        `,\n      };\n\n      test(\"when RSC Framework Mode plugin is present\", async ({ edit, $ }) => {\n        await edit({\n          \"vite.config.ts\": viteConfig({ rsc: true }),\n          ...clientFirstRouteFiles,\n        });\n        await $(\"pnpm typecheck\");\n      });\n\n      test(\"when RSC Framework Mode plugin is not present\", async ({\n        edit,\n        $,\n      }) => {\n        await edit({\n          \"vite.config.ts\": viteConfig({ rsc: false }),\n          ...clientFirstRouteFiles,\n        });\n        await $(\"pnpm typecheck\");\n      });\n    });\n  });\n});\n"
  },
  {
    "path": "integration/use-route-test.ts",
    "content": "import tsx from \"dedent\";\nimport { expect } from \"@playwright/test\";\n\nimport { test } from \"./helpers/fixtures\";\nimport * as Stream from \"./helpers/stream\";\nimport getPort from \"get-port\";\n\ntest.use({\n  files: {\n    \"app/expect-type.ts\": tsx`\n      export type Expect<T extends true> = T\n\n      export type Equal<X, Y> =\n        (<T>() => T extends X ? 1 : 2) extends\n        (<T>() => T extends Y ? 1 : 2) ? true : false\n    `,\n    \"app/routes.ts\": tsx`\n      import { type RouteConfig, route } from \"@react-router/dev/routes\"\n\n      export default [\n        route(\"parent\", \"routes/parent.tsx\", [\n          route(\"current\", \"routes/current.tsx\")\n        ]),\n        route(\"other\", \"routes/other.tsx\"),\n      ] satisfies RouteConfig\n    `,\n    \"app/root.tsx\": tsx`\n      import { Outlet } from \"react-router\"\n\n      export const handle = { rootHandle: \"root/handle\" }\n      export const loader = () => ({ rootLoader: \"root/loader\" })\n      export const action = () => ({ rootAction: \"root/action\" })\n\n      export default function Component() {\n        return (\n          <>\n            <h1>Root</h1>\n            <Outlet />\n          </>\n        )\n      }\n    `,\n    \"app/routes/parent.tsx\": tsx`\n      import { Outlet } from \"react-router\"\n\n      export const handle = { parentHandle: \"parent/handle\" }\n      export const loader = () => ({ parentLoader: \"parent/loader\" })\n      export const action = () => ({ parentAction: \"parent/action\" })\n\n      export default function Component() {\n        return (\n          <>\n            <h2>Parent</h2>\n            <Outlet />\n          </>\n        )\n      }\n    `,\n    \"app/routes/current.tsx\": tsx`\n      import { unstable_useRoute as useRoute } from \"react-router\"\n\n      import type { Expect, Equal } from \"../expect-type\"\n\n      export const handle = { currentHandle: \"current/handle\" }\n      export const loader = () => ({ currentLoader: \"current/loader\" })\n      export const action = () => ({ currentAction: \"current/action\" })\n\n      export default function Component() {\n        const current = useRoute()\n        type Test1 = Expect<Equal<typeof current, {\n          handle: unknown,\n          loaderData: unknown,\n          actionData: unknown,\n        }>>\n\n        const root = useRoute(\"root\")\n        type Test2 = Expect<Equal<typeof root, {\n          handle: { rootHandle: string },\n          loaderData: { rootLoader: string } | undefined,\n          actionData: { rootAction: string } | undefined,\n        }>>\n\n        const parent = useRoute(\"routes/parent\")\n        type Test3 = Expect<Equal<typeof parent, {\n          handle: { parentHandle: string },\n          loaderData: { parentLoader: string } | undefined,\n          actionData: { parentAction: string } | undefined\n        } | undefined>>\n\n        const other = useRoute(\"routes/other\")\n        type Test4 = Expect<Equal<typeof other, {\n          handle: { otherHandle: string },\n          loaderData: { otherLoader: string } | undefined,\n          actionData: { otherAction: string } | undefined,\n        } | undefined>>\n\n        return (\n          <>\n            <pre data-root>{root.loaderData?.rootLoader}</pre>\n            <pre data-parent>{parent?.loaderData?.parentLoader}</pre>\n            {/* @ts-expect-error */}\n            <pre data-current>{current?.loaderData?.currentLoader}</pre>\n            <pre data-other>{other === undefined ? \"undefined\" : \"something else\"}</pre>\n          </>\n        )\n      }\n    `,\n    \"app/routes/other.tsx\": tsx`\n      export const handle = { otherHandle: \"other/handle\" }\n      export const loader = () => ({ otherLoader: \"other/loader\" })\n      export const action = () => ({ otherAction: \"other/action\" })\n\n      export default function Component() {\n        return <h2>Other</h2>\n      }\n    `,\n  },\n});\n\ntest(\"useRoute\", async ({ $, page }) => {\n  await $(\"pnpm typecheck\");\n\n  const port = await getPort();\n  const url = `http://localhost:${port}`;\n\n  const dev = $(`pnpm dev --port ${port}`);\n  await Stream.match(dev.stdout, url);\n\n  await page.goto(url + \"/parent/current\", { waitUntil: \"networkidle\" });\n\n  await expect(page.locator(\"[data-root]\")).toHaveText(\"root/loader\");\n\n  await expect(page.locator(\"[data-parent]\")).toHaveText(\"parent/loader\");\n\n  await expect(page.locator(\"[data-current]\")).toHaveText(\"current/loader\");\n\n  await expect(page.locator(\"[data-other]\")).toHaveText(\"undefined\");\n});\n"
  },
  {
    "path": "integration/vite-absolute-base-test.ts",
    "content": "import { expect } from \"@playwright/test\";\nimport dedent from \"dedent\";\n\nimport type { Files } from \"./helpers/vite.js\";\nimport { test, viteConfig } from \"./helpers/vite.js\";\n\nlet files: Files = async ({ port }) => ({\n  \"vite.config.ts\": dedent`\n    import { reactRouter } from \"@react-router/dev/vite\";\n\n    export default {\n      base: \"http://localhost:${port}/\",\n      ${await viteConfig.server({ port })}\n      plugins: [reactRouter()],\n    }\n  `,\n  \"app/routes/_index.tsx\": `\n    export default () => <h1 data-title>This should work</h1>;\n  `,\n});\n\ntest(\"Vite absolute base / dev\", async ({ page, dev }) => {\n  let { port } = await dev(files);\n\n  await page.goto(`http://localhost:${port}/`, {\n    waitUntil: \"networkidle\",\n  });\n  await expect(page.locator(\"[data-title]\")).toHaveText(\"This should work\");\n  expect(page.errors).toEqual([]);\n});\n\ntest(\"Vite absolute base / build\", async ({ page, reactRouterServe }) => {\n  let { port } = await reactRouterServe(files);\n\n  await page.goto(`http://localhost:${port}/`, {\n    waitUntil: \"networkidle\",\n  });\n  await expect(page.locator(\"[data-title]\")).toHaveText(\"This should work\");\n  expect(page.errors).toEqual([]);\n});\n"
  },
  {
    "path": "integration/vite-basename-test.ts",
    "content": "import type { Page } from \"@playwright/test\";\nimport { test, expect } from \"@playwright/test\";\nimport getPort from \"get-port\";\n\nimport {\n  createEditor,\n  createProject,\n  customDev,\n  build,\n  viteConfig,\n  dev,\n  viteDevCmd,\n  reactRouterServe,\n  reactRouterConfig,\n  type TemplateName,\n} from \"./helpers/vite.js\";\nimport { js } from \"./helpers/create-fixture.js\";\n\nconst templateNames = [\n  \"vite-5-template\",\n  \"rsc-vite-framework\",\n] as const satisfies TemplateName[];\n\nconst sharedFiles = {\n  \"app/routes/_index.tsx\": js`\n    import { useState, useEffect } from \"react\";\n    import { Link } from \"react-router\"\n\n    export default function IndexRoute() {\n      const [mounted, setMounted] = useState(false);\n      useEffect(() => {\n        setMounted(true);\n      }, []);\n\n      return (\n        <div id=\"index\">\n          <h2 data-title>Index</h2>\n          <input />\n          <p data-mounted>Mounted: {mounted ? \"yes\" : \"no\"}</p>\n          <p data-hmr>HMR updated: 0</p>\n          <Link to=\"/other\">other</Link>\n        </div>\n      );\n    }\n  `,\n  \"app/routes/other.tsx\": js`\n    import { useLoaderData } from \"react-router\";\n\n    export const loader = () => {\n      return \"other-loader\";\n    };\n\n    export default function OtherRoute() {\n      const loaderData = useLoaderData()\n\n      return (\n        <div id=\"other\">\n          <p>{loaderData}</p>\n        </div>\n      );\n    }\n  `,\n};\n\nasync function configFiles({\n  port,\n  base,\n  basename,\n  templateName,\n}: {\n  port: number;\n  base?: string;\n  basename?: string;\n  templateName: TemplateName;\n}) {\n  return {\n    \"react-router.config.ts\": reactRouterConfig({\n      basename: basename !== \"/\" ? basename : undefined,\n    }),\n    \"vite.config.ts\": await viteConfig.basic({\n      port,\n      base,\n      templateName,\n    }),\n  };\n}\n\nconst customServerFile = ({\n  port,\n  base,\n  basename,\n  templateName,\n}: {\n  port: number;\n  base?: string;\n  basename?: string;\n  templateName: TemplateName;\n}) => {\n  base = base ?? \"/mybase/\";\n  basename = basename ?? base;\n\n  if (templateName.includes(\"rsc\")) {\n    return js`\n      import { createRequestListener } from \"@mjackson/node-fetch-server\";\n      import express from \"express\";\n\n      const viteDevServer =\n        process.env.NODE_ENV === \"production\"\n          ? undefined\n          : await import(\"vite\").then(({ createServer }) =>\n              createServer({\n                server: {\n                  middlewareMode: true,\n                },\n              })\n            );\n\n      const app = express();\n      if (viteDevServer) {\n        app.use(viteDevServer.middlewares);\n      } else {\n        app.use(\"${base}\", express.static(\"build/client\"));\n        app.all(\n          \"${basename}*\",\n          createRequestListener((await import(\"./build/server/index.js\")).default.fetch),\n        );\n      }\n      app.get(\"*\", (_req, res) => {\n        res.setHeader(\"content-type\", \"text/html\")\n        res.end('React Router app is at <a href=\"${basename}\">${basename}</a>');\n      });\n\n      const port = ${port};\n      app.listen(port, () => console.log('http://localhost:' + port));\n    `;\n  } else {\n    return js`\n      import { createRequestHandler } from \"@react-router/express\";\n      import express from \"express\";\n\n      const viteDevServer =\n        process.env.NODE_ENV === \"production\"\n          ? undefined\n          : await import(\"vite\").then(({ createServer }) =>\n              createServer({\n                server: {\n                  middlewareMode: true,\n                },\n              })\n            );\n\n      const app = express();\n      app.use(\"${base}\", viteDevServer?.middlewares || express.static(\"build/client\"));\n      app.all(\n        \"${basename}*\",\n        createRequestHandler({\n          build: viteDevServer\n            ? () => viteDevServer.ssrLoadModule(\"virtual:react-router/server-build\")\n            : await import(\"./build/server/index.js\"),\n        })\n      );\n      app.get(\"*\", (_req, res) => {\n        res.setHeader(\"content-type\", \"text/html\")\n        res.end('React Router app is at <a href=\"${basename}\">${basename}</a>');\n      });\n\n      const port = ${port};\n      app.listen(port, () => console.log('http://localhost:' + port));\n    `;\n  }\n};\n\ntest.describe(\"Vite base + React Router basename\", () => {\n  for (const templateName of templateNames) {\n    test.describe(`template: ${templateName}`, () => {\n      test.describe(\"Vite dev\", () => {\n        let port: number;\n        let cwd: string;\n        let stop: () => unknown;\n\n        async function setup({\n          base,\n          basename,\n          startServer,\n          files,\n        }: {\n          base: string;\n          basename: string;\n          startServer?: boolean;\n          files?: Record<string, string>;\n        }) {\n          port = await getPort();\n          cwd = await createProject(\n            {\n              ...(await configFiles({ port, base, basename, templateName })),\n              ...(files || sharedFiles),\n            },\n            templateName,\n          );\n          if (startServer !== false) {\n            stop = await dev({ cwd, port, basename });\n          }\n        }\n\n        test.afterAll(async () => await stop?.());\n\n        test(\"works when the base and basename are the same\", async ({\n          page,\n        }) => {\n          await setup({ base: \"/mybase/\", basename: \"/mybase/\" });\n          await workflowDev({ page, cwd, port });\n        });\n\n        test(\"works when the base and basename are different\", async ({\n          page,\n        }) => {\n          await setup({\n            base: \"/mybase/\",\n            basename: \"/mybase/app/\",\n          });\n          await workflowDev({ page, cwd, port, basename: \"/mybase/app/\" });\n        });\n\n        test(\"errors if basename does not start with base\", async ({\n          page,\n        }) => {\n          await setup({\n            base: \"/mybase/\",\n            basename: \"/notmybase/\",\n            startServer: false,\n          });\n          let proc = await viteDevCmd({ cwd });\n          expect(proc.stderr.toString()).toMatch(\n            \"When using the React Router `basename` and the Vite `base` config, the \" +\n              \"`basename` config must begin with `base` for the default Vite dev server.\",\n          );\n        });\n\n        test(\"works with child routes using client loaders\", async ({\n          page,\n        }) => {\n          let basename = \"/mybase/\";\n          await setup({\n            base: basename,\n            basename,\n            files: {\n              ...sharedFiles,\n              \"app/routes/parent.tsx\": js`\n                import { Outlet } from 'react-router'\n                export default function Parent() {\n                  return <div id=\"parent\"><Outlet /></div>;\n                }\n              `,\n              \"app/routes/parent.child.tsx\": js`\n                import { useState, useEffect } from \"react\";\n                import { useLoaderData } from 'react-router'\n                export async function clientLoader() {\n                  await new Promise(resolve => setTimeout(resolve, 500))\n                  return \"CHILD\"\n                }\n                export function HydrateFallback() {\n                  const [mounted, setMounted] = useState(false);\n                  useEffect(() => setMounted(true), []);\n                  return (\n                    <>\n                      <p id=\"loading\">Loading...</p>\n                      <p data-mounted>Mounted: {mounted ? \"yes\" : \"no\"}</p>\n                    </>\n                  );\n                }\n                export default function Child() {\n                  const data = useLoaderData()\n                  const [mounted, setMounted] = useState(false);\n                  useEffect(() => setMounted(true), []);\n                  return (\n                    <>\n                      <p id=\"child\">{data}</p>\n                      <p data-mounted>Mounted: {mounted ? \"yes\" : \"no\"}</p>\n                    </>\n                  );\n                }\n              `,\n            },\n          });\n\n          let hydrationErrors: Error[] = [];\n          page.on(\"pageerror\", (error) => {\n            if (\n              error.message.includes(\"Hydration failed\") ||\n              error.message.includes(\"error while hydrating\") ||\n              error.message.includes(\"does not match server-rendered HTML\")\n            ) {\n              hydrationErrors.push(error);\n            }\n          });\n\n          // setup: initial render at basename\n          await page.goto(`http://localhost:${port}${basename}parent/child`, {\n            waitUntil: \"domcontentloaded\",\n          });\n\n          await expect(page.locator(\"#parent\")).toBeDefined();\n          await expect(page.locator(\"#loading\")).toContainText(\"Loading...\");\n          await expect(page.locator(\"[data-mounted]\")).toHaveText(\n            \"Mounted: yes\",\n          );\n\n          expect(hydrationErrors).toEqual([]);\n\n          await page.waitForSelector(\"#child\");\n          await expect(page.locator(\"#child\")).toHaveText(\"CHILD\");\n          await expect(page.locator(\"[data-mounted]\")).toHaveText(\n            \"Mounted: yes\",\n          );\n        });\n      });\n\n      test.describe(\"express dev\", async () => {\n        let port: number;\n        let cwd: string;\n        let stop: () => void;\n\n        async function setup({\n          base,\n          basename,\n          startServer,\n        }: {\n          base: string;\n          basename: string;\n          startServer?: boolean;\n        }) {\n          port = await getPort();\n          cwd = await createProject(\n            {\n              ...(await configFiles({ port, base, basename, templateName })),\n              \"server.mjs\": customServerFile({ port, basename, templateName }),\n              ...sharedFiles,\n            },\n            templateName,\n          );\n          if (startServer !== false) {\n            stop = await customDev({ cwd, port, basename });\n          }\n        }\n\n        test.afterAll(() => stop());\n\n        test(\"works when base and basename are the same\", async ({ page }) => {\n          await setup({ base: \"/mybase/\", basename: \"/mybase/\" });\n          await workflowDev({ page, cwd, port });\n        });\n\n        test(\"works when base and basename are different\", async ({ page }) => {\n          await setup({\n            base: \"/mybase/\",\n            basename: \"/mybase/dashboard/\",\n          });\n          await workflowDev({\n            page,\n            cwd,\n            port,\n            basename: \"/mybase/dashboard/\",\n          });\n        });\n\n        test(\"works when basename does not start with base\", async ({\n          page,\n        }) => {\n          await setup({\n            base: \"/mybase/\",\n            basename: \"/notmybase/\",\n          });\n          await workflowDev({ page, cwd, port, basename: \"/notmybase/\" });\n        });\n      });\n\n      test.describe(\"vite build\", () => {\n        let port: number;\n        let cwd: string;\n        let stop: () => unknown;\n\n        async function setup({\n          base,\n          basename,\n          startServer,\n        }: {\n          base: string;\n          basename: string;\n          startServer?: boolean;\n        }) {\n          port = await getPort();\n          cwd = await createProject(\n            {\n              ...(await configFiles({ port, base, basename, templateName })),\n              ...sharedFiles,\n            },\n            templateName,\n          );\n          build({ cwd });\n          if (startServer !== false) {\n            stop = await reactRouterServe({ cwd, port, basename });\n          }\n        }\n\n        test.afterAll(() => stop());\n\n        test(\"works when base and basename are the same\", async ({ page }) => {\n          await setup({ base: \"/mybase/\", basename: \"/mybase/\" });\n          await workflowBuild({ page, port });\n        });\n\n        test(\"works when base and basename are different\", async ({ page }) => {\n          await setup({\n            base: \"/mybase/\",\n            basename: \"/mybase/dashboard/\",\n          });\n          await workflowBuild({\n            page,\n            port,\n            basename: \"/mybase/dashboard/\",\n          });\n        });\n\n        test(\"works when basename does not start with base\", async ({\n          page,\n        }) => {\n          await setup({\n            base: \"/mybase/\",\n            basename: \"/notmybase/\",\n          });\n          await workflowBuild({\n            page,\n            port,\n            base: \"/mybase/\",\n            basename: \"/notmybase/\",\n          });\n        });\n      });\n\n      test.describe(\"express build\", async () => {\n        let port: number;\n        let cwd: string;\n        let stop: () => void;\n\n        async function setup({\n          base,\n          basename,\n          startServer,\n        }: {\n          base: string;\n          basename: string;\n          startServer?: boolean;\n        }) {\n          port = await getPort();\n          cwd = await createProject(\n            {\n              ...(await configFiles({ port, base, basename, templateName })),\n              \"server.mjs\": customServerFile({\n                port,\n                base,\n                basename,\n                templateName,\n              }),\n              ...sharedFiles,\n            },\n            templateName,\n          );\n          build({ cwd });\n          if (startServer !== false) {\n            stop = await customDev({\n              cwd,\n              port,\n              basename,\n              env: { NODE_ENV: \"production\" },\n            });\n          }\n        }\n\n        test.afterAll(() => stop?.());\n\n        test(\"works when base and basename are the same\", async ({ page }) => {\n          await setup({ base: \"/mybase/\", basename: \"/mybase/\" });\n          await workflowBuild({ page, port });\n        });\n\n        test(\"works when base and basename are different\", async ({ page }) => {\n          await setup({\n            base: \"/mybase/\",\n            basename: \"/mybase/app/\",\n          });\n          await workflowBuild({ page, port, basename: \"/mybase/app/\" });\n        });\n\n        test(\"works when basename does not start with base\", async ({\n          page,\n        }) => {\n          await setup({\n            base: \"/mybase/\",\n            basename: \"/notmybase/\",\n          });\n          await workflowBuild({ page, port, basename: \"/notmybase/\" });\n        });\n\n        test(\"works when when base is an absolute external URL\", async ({\n          page,\n        }) => {\n          test.skip(\n            templateName === \"rsc-vite-framework\",\n            \"I'm not sure the use-case for this and can't find anything. If you ever need this file an issue and we will revisit.\",\n          );\n          port = await getPort();\n          cwd = await createProject(\n            {\n              ...(await configFiles({\n                templateName,\n                port,\n                base: \"https://cdn.example.com/assets/\",\n                basename: \"/app/\",\n              })),\n              // Slim server that only serves basename (route) requests from the React Router handler\n              \"server.mjs\": templateName.includes(\"rsc\")\n                ? String.raw`\n                  import { createRequestListener } from \"@mjackson/node-fetch-server\";\n                  import express from \"express\";\n\n                  const app = express();\n                  app.all(\n                    \"/app/*\",\n                    createRequestListener((await import(\"./build/server/index.js\")).default)\n                  );\n\n                  const port = ${port};\n                  app.listen(port, () => console.log('http://localhost:' + port));\n                `\n                : String.raw`\n                  import { createRequestHandler } from \"@react-router/express\";\n                  import express from \"express\";\n\n                  const app = express();\n                  app.all(\n                    \"/app/*\",\n                    createRequestHandler({ build: await import(\"./build/server/index.js\") })\n                  );\n\n                  const port = ${port};\n                  app.listen(port, () => console.log('http://localhost:' + port));\n                `,\n              ...sharedFiles,\n            },\n            templateName,\n          );\n\n          build({ cwd });\n          stop = await customDev({\n            cwd,\n            port,\n            basename: \"/app/\",\n            env: { NODE_ENV: \"production\" },\n          });\n\n          // Intercept and make all CDN requests 404\n          let requestUrls: string[] = [];\n          await page.route(\"**/*.js\", (route) => {\n            requestUrls.push(route.request().url());\n            route.fulfill({ status: 404 });\n          });\n\n          // setup: initial render\n          await page.goto(`http://localhost:${port}/app/`, {\n            waitUntil: \"networkidle\",\n          });\n          await expect(page.locator(\"#index [data-title]\")).toHaveText(\"Index\");\n\n          // Can't validate hydration here due to 404s, but we can ensure assets are\n          // attempting to load from the CDN\n          expect(\n            requestUrls.length > 0 &&\n              requestUrls.every((url) =>\n                url.startsWith(\"https://cdn.example.com/assets/\"),\n              ),\n          ).toBe(true);\n        });\n      });\n    });\n  }\n});\n\nasync function workflowDev({\n  page,\n  cwd,\n  port,\n  base,\n  basename,\n}: {\n  page: Page;\n  cwd: string;\n  port: number;\n  base?: string;\n  basename?: string;\n}) {\n  base = base ?? \"/mybase/\";\n  basename = basename ?? base;\n\n  let pageErrors: Error[] = [];\n  page.on(\"pageerror\", (error) => pageErrors.push(error));\n  let edit = createEditor(cwd);\n\n  let requestUrls: string[] = [];\n  page.on(\"request\", (request) => {\n    requestUrls.push(request.url());\n  });\n\n  // setup: initial render at basename\n  await page.goto(`http://localhost:${port}${basename}`, {\n    waitUntil: \"networkidle\",\n  });\n  await expect(page.locator(\"#index [data-title]\")).toHaveText(\"Index\");\n\n  // setup: hydration\n  await expect(page.locator(\"#index [data-mounted]\")).toHaveText(\n    \"Mounted: yes\",\n  );\n\n  // setup: browser state\n  let hmrStatus = page.locator(\"#index [data-hmr]\");\n  await expect(hmrStatus).toHaveText(\"HMR updated: 0\");\n  let input = page.locator(\"#index input\");\n  await expect(input).toBeVisible();\n  await input.type(\"stateful\");\n  expect(pageErrors).toEqual([]);\n\n  // route: HMR\n  await edit(\"app/routes/_index.tsx\", (contents) =>\n    contents.replace(\"HMR updated: 0\", \"HMR updated: 1\"),\n  );\n  await page.waitForLoadState(\"networkidle\");\n  await expect(hmrStatus).toHaveText(\"HMR updated: 1\");\n  await expect(input).toHaveValue(\"stateful\");\n  expect(pageErrors).toEqual([]);\n\n  // client side navigation\n  await page.getByRole(\"link\", { name: \"other\" }).click();\n  await page.waitForURL(`http://localhost:${port}${basename}other`);\n  await page.getByText(\"other-loader\").click();\n  expect(pageErrors).toEqual([]);\n\n  let isAssetRequest = (url: string) =>\n    /\\.[jt]sx?/.test(url) ||\n    /\\/@id\\/__x00__virtual:/.test(url) ||\n    /\\/@vite\\/client/.test(url) ||\n    /\\/@fs\\//.test(url);\n\n  // verify client asset requests are all under base\n  expect(\n    requestUrls\n      .filter((url) => isAssetRequest(url))\n      .every((url) => url.startsWith(`http://localhost:${port}${base}`)),\n  ).toBe(true);\n\n  // verify client route requests are all under basename\n  expect(\n    requestUrls\n      .filter((url) => !isAssetRequest(url))\n      .every((url) => url.startsWith(`http://localhost:${port}${basename}`)),\n  ).toBe(true);\n}\n\nasync function workflowBuild({\n  page,\n  port,\n  base,\n  basename,\n}: {\n  page: Page;\n  port: number;\n  base?: string;\n  basename?: string;\n}) {\n  base = base ?? \"/mybase/\";\n  basename = basename ?? base;\n\n  let pageErrors: Error[] = [];\n  page.on(\"pageerror\", (error) => pageErrors.push(error));\n\n  let requestUrls: string[] = [];\n  page.on(\"request\", (request) => {\n    requestUrls.push(request.url());\n  });\n\n  // setup: initial render\n  await page.goto(`http://localhost:${port}${basename}`, {\n    waitUntil: \"networkidle\",\n  });\n  await expect(page.locator(\"#index [data-title]\")).toHaveText(\"Index\");\n\n  // setup: hydration\n  await expect(page.locator(\"#index [data-mounted]\")).toHaveText(\n    \"Mounted: yes\",\n  );\n\n  // client side navigation\n  await page.getByRole(\"link\", { name: \"other\" }).click();\n  await page.waitForURL(`http://localhost:${port}${basename}other`);\n  await page.getByText(\"other-loader\").click();\n  expect(pageErrors).toEqual([]);\n\n  let isAssetRequest = (url: string) => /\\.js/.test(url);\n\n  // verify client asset requests are all under base\n  expect(\n    requestUrls\n      .filter((url) => isAssetRequest(url))\n      .every((url) => url.startsWith(`http://localhost:${port}${base}`)),\n  ).toBe(true);\n\n  // verify client route requests are all under basename\n  expect(\n    requestUrls\n      .filter((url) => !isAssetRequest(url))\n      .every((url) => url.startsWith(`http://localhost:${port}${basename}`)),\n  ).toBe(true);\n}\n"
  },
  {
    "path": "integration/vite-build-test.ts",
    "content": "import * as path from \"node:path\";\nimport { test, expect } from \"@playwright/test\";\nimport getPort from \"get-port\";\nimport glob from \"glob\";\n\nimport {\n  createProject,\n  build,\n  reactRouterServe,\n  reactRouterConfig,\n  viteConfig,\n  grep,\n  viteMajorTemplates,\n} from \"./helpers/vite.js\";\n\nlet port: number;\nlet cwd: string;\nlet stop: () => void;\n\nconst js = String.raw;\n\ntest.describe(\"Build\", () => {\n  [false, true].forEach((v8_viteEnvironmentApi) => {\n    viteMajorTemplates.forEach(({ templateName, templateDisplayName }) => {\n      // Vite 5 doesn't support the Environment API\n      if (templateName === \"vite-5-template\" && v8_viteEnvironmentApi) {\n        return;\n      }\n\n      test.describe(`${templateDisplayName}${\n        v8_viteEnvironmentApi ? \" with Vite Environment API\" : \"\"\n      }`, () => {\n        test.beforeAll(async () => {\n          port = await getPort();\n          cwd = await createProject(\n            {\n              \".env\": `\n                ENV_VAR_FROM_DOTENV_FILE=true\n              `,\n              \"react-router.config.ts\": reactRouterConfig({\n                future: { v8_viteEnvironmentApi },\n              }),\n              \"vite.config.ts\": js`\n                import { defineConfig } from \"vite\";\n                import { reactRouter } from \"@react-router/dev/vite\";\n                import mdx from \"@mdx-js/rollup\";\n\n                export default defineConfig({\n                  ${await viteConfig.server({ port })}\n                  ${viteConfig.build({\n                    assetsInlineLimit: 0,\n                  })}\n                  plugins: [\n                    mdx(),\n                    reactRouter(),\n                  ],\n                });\n              `,\n              \"app/root.tsx\": js`\n                import { Links, Meta, Outlet, Scripts } from \"react-router\";\n\n                export default function Root() {\n                  return (\n                    <html lang=\"en\">\n                      <head>\n                        <Meta />\n                        <Links />\n                      </head>\n                      <body>\n                        <div id=\"content\">\n                          <h1>Root</h1>\n                          <Outlet />\n                        </div>\n                        <Scripts />\n                      </body>\n                    </html>\n                  );\n                }\n              `,\n              \"app/routes/_index.tsx\": js`\n                import { useState, useEffect } from \"react\";\n\n                import { serverOnly1, serverOnly2 } from \"../utils.server\";\n\n                export const loader = () => {\n                  return { serverOnly1 }\n                }\n\n                export const action = () => {\n                  console.log(serverOnly2)\n                  return null\n                }\n\n                export default function() {\n                  const [mounted, setMounted] = useState(false);\n                  useEffect(() => {\n                    setMounted(true);\n                  }, []);\n\n                  return (\n                    <>\n                      <h2>Index</h2>\n                      {!mounted ? <h3>Loading...</h3> : <h3 data-mounted>Mounted</h3>}\n                    </>\n                  );\n                }\n              `,\n              \"app/utils.server.ts\": js`\n                export const serverOnly1 = \"SERVER_ONLY_1\"\n                export const serverOnly2 = \"SERVER_ONLY_2\"\n              `,\n              \"app/routes/resource.ts\": js`\n                import { serverOnly1, serverOnly2 } from \"../utils.server\";\n\n                export const loader = () => {\n                  return { serverOnly1 }\n                }\n\n                export const action = () => {\n                  console.log(serverOnly2)\n                  return null\n                }\n              `,\n              \"app/routes/mdx.mdx\": js`\n                import { useEffect, useState } from \"react\";\n                import { useLoaderData } from \"react-router\";\n\n                import { serverOnly1, serverOnly2 } from \"../utils.server\";\n\n                export const loader = () => {\n                  return {\n                    serverOnly1,\n                    content: \"MDX route content from loader\",\n                  }\n                }\n\n                export const action = () => {\n                  console.log(serverOnly2)\n                  return null\n                }\n\n                export function MdxComponent() {\n                  const [mounted, setMounted] = useState(false);\n                  useEffect(() => {\n                    setMounted(true);\n                  }, []);\n                  const { content } = useLoaderData();\n                  const text = content + (mounted\n                    ? \": mounted\"\n                    : \": not mounted\");\n                  return <div data-mdx-route>{text}</div>\n                }\n\n                ## MDX Route\n\n                <MdxComponent />\n              `,\n              \"app/routes/code-split1.tsx\": js`\n                import { CodeSplitComponent } from \"../code-split-component\";\n\n                export default function CodeSplit1Route() {\n                  return <div id=\"code-split1\"><CodeSplitComponent /></div>;\n                }\n              `,\n              \"app/routes/code-split2.tsx\": js`\n                import { CodeSplitComponent } from \"../code-split-component\";\n\n                export default function CodeSplit2Route() {\n                  return <div id=\"code-split2\"><CodeSplitComponent /></div>;\n                }\n              `,\n              \"app/code-split-component.tsx\": js`\n                import classes from \"./code-split.module.css\";\n\n                export function CodeSplitComponent() {\n                  return <span className={classes.test}>ok</span>\n                }\n              `,\n              \"app/code-split.module.css\": js`\n                .test {\n                  background-color: rgb(255, 170, 0);\n                }\n              `,\n              \"app/routes/dotenv.tsx\": js`\n                import { useLoaderData } from \"react-router\";\n\n                export const loader = () => {\n                  return {\n                    loaderContent: process.env.ENV_VAR_FROM_DOTENV_FILE ?? '.env file was NOT loaded, which is a good thing',\n                  }\n                }\n\n                export default function DotenvRoute() {\n                  const { loaderContent } = useLoaderData();\n\n                  return <div data-dotenv-route-loader-content>{loaderContent}</div>;\n                }\n              `,\n\n              \"app/assets/test.txt\": \"test\",\n              \"app/routes/ssr-only-assets.tsx\": js`\n                import txtUrl from \"../assets/test.txt?url\";\n                import { useLoaderData } from \"react-router\"\n\n                export const loader: LoaderFunction = () => {\n                  return { txtUrl };\n                };\n\n                export default function SsrOnlyAssetsRoute() {\n                  const loaderData = useLoaderData();\n                  return (\n                    <div>\n                      <a href={loaderData.txtUrl}>txtUrl</a>\n                    </div>\n                  );\n                }\n              `,\n\n              \"app/assets/test.css\": \".test{color:red}\",\n              \"app/routes/ssr-only-css-url-files.tsx\": js`\n                import cssUrl from \"../assets/test.css?url\";\n                import { useLoaderData } from \"react-router\"\n\n                export const loader: LoaderFunction = () => {\n                  return { cssUrl };\n                };\n\n                export default function SsrOnlyCssUrlFilesRoute() {\n                  const loaderData = useLoaderData();\n                  return (\n                    <div>\n                      <a href={loaderData.cssUrl}>cssUrl</a>\n                    </div>\n                  );\n                }\n              `,\n\n              \"app/routes/ssr-code-split.tsx\": js`\n                import { useLoaderData } from \"react-router\"\n\n                export const loader: LoaderFunction = async () => {\n                  const lib = await import(\"../ssr-code-split-lib\");\n                  return lib.ssrCodeSplitTest();\n                };\n\n                export default function SsrCodeSplitRoute() {\n                  const loaderData = useLoaderData();\n                  return (\n                    <div data-ssr-code-split>{loaderData}</div>\n                  );\n                }\n              `,\n\n              \"app/ssr-code-split-lib.ts\": js`\n                export function ssrCodeSplitTest() {\n                  return \"ssrCodeSplitTest\";\n                }\n              `,\n            },\n            templateName,\n          );\n\n          let { stderr, status } = build({ cwd });\n          expect(\n            stderr\n              .toString()\n              // This can be removed when this issue is fixed: https://github.com/remix-run/remix/issues/9055\n              .replace('Generated an empty chunk: \"resource\".', \"\")\n              .trim(),\n          ).toBeFalsy();\n          expect(status).toBe(0);\n          stop = await reactRouterServe({ cwd, port });\n        });\n        test.afterAll(() => stop());\n\n        test(\"server code is removed from client build\", async () => {\n          expect(\n            grep(path.join(cwd, \"build/client\"), /SERVER_ONLY_1/).length,\n          ).toBe(0);\n          expect(\n            grep(path.join(cwd, \"build/client\"), /SERVER_ONLY_2/).length,\n          ).toBe(0);\n        });\n\n        test(\"renders matching MDX routes\", async ({ page }) => {\n          let pageErrors: Error[] = [];\n          page.on(\"pageerror\", (error) => pageErrors.push(error));\n\n          await page.goto(`http://localhost:${port}/mdx`, {\n            waitUntil: \"networkidle\",\n          });\n          await expect(page.locator(\"[data-mdx-route]\")).toHaveText(\n            \"MDX route content from loader: mounted\",\n          );\n          expect(pageErrors).toEqual([]);\n        });\n\n        test(\"emits SSR-only assets to the client assets directory\", async ({\n          page,\n        }) => {\n          let pageErrors: Error[] = [];\n          page.on(\"pageerror\", (error) => pageErrors.push(error));\n\n          await page.goto(`http://localhost:${port}/ssr-only-assets`, {\n            waitUntil: \"networkidle\",\n          });\n\n          await page.getByRole(\"link\", { name: \"txtUrl\" }).click();\n          await page.waitForURL(\"**/assets/test-*.txt\");\n          await expect(page.getByText(\"test\")).toBeVisible();\n          expect(pageErrors).toEqual([]);\n        });\n\n        test(\"emits SSR-only .css?url files to the client assets directory\", async ({\n          page,\n        }) => {\n          let pageErrors: Error[] = [];\n          page.on(\"pageerror\", (error) => pageErrors.push(error));\n\n          await page.goto(`http://localhost:${port}/ssr-only-css-url-files`, {\n            waitUntil: \"networkidle\",\n          });\n\n          await page.getByRole(\"link\", { name: \"cssUrl\" }).click();\n          await page.waitForURL(\"**/assets/test-*.css\");\n          await expect(page.getByText(\".test{\")).toBeVisible();\n          expect(pageErrors).toEqual([]);\n        });\n\n        test(\"supports code-split JS from SSR build\", async ({ page }) => {\n          let pageErrors: Error[] = [];\n          page.on(\"pageerror\", (error) => pageErrors.push(error));\n\n          await page.goto(`http://localhost:${port}/ssr-code-split`, {\n            waitUntil: \"networkidle\",\n          });\n\n          await expect(page.locator(\"[data-ssr-code-split]\")).toHaveText(\n            \"ssrCodeSplitTest\",\n          );\n          expect(pageErrors).toEqual([]);\n        });\n\n        test(\"removes assets (other than code-split JS) and CSS files from SSR build\", async () => {\n          let assetFiles = glob.sync(\"build/server/assets/**/*\", { cwd });\n          let [asset, ...rest] = assetFiles;\n          expect(rest).toEqual([]); // Provide more useful test output if this fails\n          expect(asset).toMatch(/ssr-code-split-lib-.*\\.js/);\n        });\n\n        test(\"supports code-split CSS\", async ({ page }) => {\n          let pageErrors: unknown[] = [];\n          page.on(\"pageerror\", (error) => pageErrors.push(error));\n\n          await page.goto(`http://localhost:${port}/code-split1`, {\n            waitUntil: \"networkidle\",\n          });\n          expect(\n            await page\n              .locator(\"#code-split1 span\")\n              .evaluate((e) => window.getComputedStyle(e).backgroundColor),\n          ).toBe(\"rgb(255, 170, 0)\");\n\n          await page.goto(`http://localhost:${port}/code-split2`, {\n            waitUntil: \"networkidle\",\n          });\n          expect(\n            await page\n              .locator(\"#code-split2 span\")\n              .evaluate((e) => window.getComputedStyle(e).backgroundColor),\n          ).toBe(\"rgb(255, 170, 0)\");\n\n          expect(pageErrors).toEqual([]);\n        });\n\n        test(\"doesn't load .env file\", async ({ page }) => {\n          let pageErrors: unknown[] = [];\n          page.on(\"pageerror\", (error) => pageErrors.push(error));\n\n          await page.goto(`http://localhost:${port}/dotenv`, {\n            waitUntil: \"networkidle\",\n          });\n          expect(pageErrors).toEqual([]);\n\n          let loaderContent = page.locator(\n            \"[data-dotenv-route-loader-content]\",\n          );\n          await expect(loaderContent).toHaveText(\n            \".env file was NOT loaded, which is a good thing\",\n          );\n\n          expect(pageErrors).toEqual([]);\n        });\n      });\n    });\n  });\n});\n"
  },
  {
    "path": "integration/vite-cloudflare-test.ts",
    "content": "import type { Page } from \"@playwright/test\";\nimport { expect } from \"@playwright/test\";\n\nimport type { Files } from \"./helpers/vite.js\";\nimport { reactRouterConfig, test, viteConfig } from \"./helpers/vite.js\";\n\nfunction getFiles({\n  v8_viteEnvironmentApi,\n}: {\n  v8_viteEnvironmentApi: boolean;\n}): Files {\n  return async ({ port }) => ({\n    \"react-router.config.ts\": reactRouterConfig({\n      future: { v8_viteEnvironmentApi },\n    }),\n    \"vite.config.ts\": `\n      import { reactRouter } from \"@react-router/dev/vite\";\n      import { cloudflareDevProxy } from \"@react-router/dev/vite/cloudflare\";\n      import { getLoadContext } from \"./load-context\";\n\n      export default {\n        ${await viteConfig.server({ port })}\n        plugins: [\n          cloudflareDevProxy({ getLoadContext }),\n          reactRouter(),\n        ],\n      }\n    `,\n    \"load-context.ts\": `\n      import type { ExecutionContext } from \"@cloudflare/workers-types\";\n      import type { AppLoadContext } from \"react-router\";\n      \n      type Env = {\n        MY_KV: KVNamespace;\n      }\n      \n      declare global {\n        interface CloudflareEnvironment extends Env {}\n      }\n      \n      declare module \"react-router\" {\n        export interface AppLoadContext {\n          cloudflare: {\n            env: CloudflareEnvironment;\n            ctx: Omit<ExecutionContext, \"props\">;\n          };\n          env2: CloudflareEnvironment;\n          extra: string;\n        }\n      }\n      \n      type GetLoadContextArgs = {\n        request: Request;\n        context: Pick<AppLoadContext, \"cloudflare\">;\n      };\n      \n      export const getLoadContext = ({ context }: GetLoadContextArgs) => {\n        return {\n          ...context,\n          env2: context.cloudflare.env,\n          extra: \"stuff\",\n        };\n      };\n    `,\n    \"functions/[[page]].ts\": `\n      import { createPagesFunctionHandler } from \"@react-router/cloudflare\";\n\n      // @ts-ignore - the server build file is generated by \\`react-router build\\`\n      import * as build from \"../build/server\";\n      import { getLoadContext } from \"../load-context\";\n\n      export const onRequest = createPagesFunctionHandler({\n        build,\n        getLoadContext,\n      });\n    `,\n    \"wrangler.toml\": `\n      compatibility_date = \"2025-03-17\"\n      kv_namespaces = [\n        { id = \"abc123\", binding=\"MY_KV\" }\n      ]\n    `,\n    \"app/routes/_index.tsx\": `\n      import {\n        type LoaderFunctionArgs,\n        type ActionFunctionArgs,\n        Form,\n        useLoaderData,\n      } from \"react-router\";\n\n      const key = \"__my-key__\";\n\n      export async function loader({ context }: LoaderFunctionArgs) {\n        const { MY_KV } = context.cloudflare.env;\n        const value = await MY_KV.get(key);\n        return { value, extra: context.extra };\n      }\n\n      export async function action({ request, context }: ActionFunctionArgs) {\n        const { MY_KV } = context.env2;\n\n        if (request.method === \"POST\") {\n          const formData = await request.formData();\n          const value = formData.get(\"value\") as string;\n          await MY_KV.put(key, value);\n          return null;\n        }\n\n        if (request.method === \"DELETE\") {\n          await MY_KV.delete(key);\n          return null;\n        }\n\n        throw new Error(\\`Method not supported: \"\\${request.method}\"\\`);\n      }\n\n      export default function Index() {\n        const { value, extra } = useLoaderData<typeof loader>();\n        return (\n          <div>\n            <h1>Welcome to React Router + Cloudflare</h1>\n            <p data-extra>Extra: {extra}</p>\n            {value ? (\n              <>\n                <p data-text>Value: {value}</p>\n                <Form method=\"DELETE\">\n                  <button>Delete</button>\n                </Form>\n              </>\n            ) : (\n              <>\n                <p data-text>No value</p>\n                <Form method=\"POST\">\n                  <label htmlFor=\"value\">Set value:</label>\n                  <input type=\"text\" name=\"value\" id=\"value\" required />\n                  <br />\n                  <button>Save</button>\n                </Form>\n              </>\n            )}\n          </div>\n        );\n      }\n    `,\n  });\n}\n\ntest.describe(\"Cloudflare Dev Proxy\", () => {\n  [false, true].forEach((v8_viteEnvironmentApi) => {\n    test.describe(`viteEnvironmentApi enabled: ${v8_viteEnvironmentApi}`, () => {\n      const files = getFiles({ v8_viteEnvironmentApi });\n\n      test(\"vite dev\", async ({ page, dev }) => {\n        let { port } = await dev(files, \"cloudflare-dev-proxy-template\");\n        await workflow({ page, port });\n      });\n\n      test(\"wrangler pages dev\", async ({ page, wranglerPagesDev }) => {\n        let { port } = await wranglerPagesDev(files);\n        await workflow({ page, port });\n      });\n    });\n  });\n});\n\nasync function workflow({ page, port }: { page: Page; port: number }) {\n  await page.goto(`http://localhost:${port}/`, {\n    waitUntil: \"networkidle\",\n  });\n  await expect(page.locator(\"[data-extra]\")).toHaveText(\"Extra: stuff\");\n  await expect(page.locator(\"[data-text]\")).toHaveText(\"No value\");\n\n  await page.getByLabel(\"Set value:\").fill(\"my-value\");\n  await page.getByRole(\"button\").click();\n  await expect(page.locator(\"[data-text]\")).toHaveText(\"Value: my-value\");\n  expect(page.errors).toEqual([]);\n}\n"
  },
  {
    "path": "integration/vite-css-lazy-loading-test.ts",
    "content": "import { type Page, test, expect } from \"@playwright/test\";\n\nimport { PlaywrightFixture } from \"./helpers/playwright-fixture.js\";\nimport {\n  type Fixture,\n  type AppFixture,\n  createAppFixture,\n  createFixture,\n  css,\n  js,\n} from \"./helpers/create-fixture.js\";\n\n// Link hrefs with a trailing hash are only ever managed by React Router, to\n// ensure they're forcibly unique from the Vite-injected links\nconst FORCIBLY_UNIQUE_HREF_SELECTOR = \"[href$='#']\";\nconst CSS_LINK_SELECTOR = \"link[rel='stylesheet']\";\nconst ANY_FORCIBLY_UNIQUE_CSS_LINK_SELECTOR = `link[rel='stylesheet']${FORCIBLY_UNIQUE_HREF_SELECTOR}`;\nconst CSS_COMPONENT_LINK_SELECTOR =\n  \"link[rel='stylesheet'][href*='css-component']\";\nconst CSS_COMPONENT_FORCIBLY_UNIQUE_LINK_SELECTOR = `link[rel='stylesheet'][href*='css-component']${FORCIBLY_UNIQUE_HREF_SELECTOR}`;\n\nfunction getColor(page: Page, selector: string) {\n  return page\n    .locator(selector)\n    .first()\n    .evaluate((el) => window.getComputedStyle(el).color);\n}\n\ntest.describe(\"Vite CSS lazy loading\", () => {\n  let fixture: Fixture;\n  let appFixture: AppFixture;\n\n  test.beforeAll(async () => {\n    fixture = await createFixture({\n      files: {\n        \"app/components/css-component.css\": css`\n          .css-component {\n            color: rgb(0, 128, 0);\n            font-family: sans-serif;\n            font-weight: bold;\n          }\n        `,\n\n        \"app/components/css-component.tsx\": js`\n          import \"./css-component.css\";\n          export default function CssComponent() {\n            return <p data-css-component className=\"css-component\">This text should be green.</p>;\n          }\n        `,\n\n        \"app/components/static-only-css-component.css\": css`\n          .static-only-css-component {\n            color: rgb(128, 128, 0);\n            font-family: sans-serif;\n            font-weight: bold;\n          }\n        `,\n\n        \"app/components/static-only-css-component.tsx\": js`\n          import \"./static-only-css-component.css\";\n          export default function StaticOnlyCssComponent() {\n            return <p data-static-only-css-component className=\"static-only-css-component\">This text should be olive.</p>;\n          }\n        `,\n\n        \"app/components/load-lazy-css-component.tsx\": js`\n          import { lazy, useState } from \"react\";\n          export const LazyCssComponent = lazy(() => import(\"./css-component\"));\n          export function LoadLazyCssComponent() {\n            const [show, setShow] = useState(false);\n            return (\n              <>\n                <button data-load-lazy-css-component onClick={() => setShow(true)}>Load Lazy CSS Component</button>\n                {show && <LazyCssComponent />}\n              </>\n            );\n          }\n        `,\n\n        \"app/routes/_layout.tsx\": js`\n          import { Link, Outlet } from \"react-router\";\n          import { LoadLazyCssComponent } from \"../components/load-lazy-css-component\";\n          export default function Layout() {\n            return (\n              <>\n                <nav>\n                  <ul>\n                    <li>\n                      <Link to=\"/\">Home</Link>\n                    </li>\n                    <li>\n                      <Link to=\"/parent-child/with-css-component\">Parent + Child / Route with CSS Component</Link>\n                    </li>\n                    <li>\n                      <Link to=\"/parent-child/without-css-component\">Parent + Child / Route Without CSS Component</Link>\n                    </li>\n                    <li>\n                      <Link to=\"/siblings/with-css-component\">Siblings / Route with CSS Component</Link>\n                    </li>\n                    <li>\n                      <Link to=\"/siblings/with-lazy-css-component\">Siblings / Route with Lazy CSS Component</Link>\n                    </li>\n                    <li>\n                      <Link to=\"/with-static-only-css-component\">Route with Static Only CSS Component</Link>\n                    </li>\n                  </ul>\n                </nav>\n                <Outlet />\n              </>\n            );\n          }\n        `,\n\n        \"app/routes/_layout._index.tsx\": js`\n          export default function Index() {\n            return <h2 data-route-home>Home</h2>;\n          }\n        `,\n\n        \"app/routes/_layout.parent-child.tsx\": js`\n          import { Outlet } from \"react-router\";\n          import { LoadLazyCssComponent } from \"../components/load-lazy-css-component\";\n          export default function ParentChild() {\n            return (\n              <>\n                <h2 data-route-parent>Parent + Child</h2>\n                <LoadLazyCssComponent />\n                <Outlet />\n              </>\n            );\n          }\n        `,\n\n        \"app/routes/_layout.parent-child.with-css-component.tsx\": js`\n          import CssComponent from \"../components/css-component\";\n          export default function RouteWithCssComponent() {\n            return (\n              <>\n                <h2 data-child-route-with-css-component>Route with CSS Component</h2>\n                <CssComponent />\n              </>\n            );\n          }\n        `,\n\n        \"app/routes/_layout.parent-child.without-css-component.tsx\": js`\n          export default function RouteWithoutCssComponent() {\n            return <h2 data-child-route-without-css-component>Route Without CSS Component</h2>;\n          }\n        `,\n\n        \"app/routes/_layout.siblings.tsx\": js`\n          import { Outlet } from \"react-router\";\n          export default function Siblings() {\n            return (\n              <>\n                <h2 data-sibling-route>Siblings</h2>\n                <Outlet />\n              </>\n            );\n          }\n        `,\n\n        \"app/routes/_layout.siblings.with-css-component.tsx\": js`\n          import CssComponent from \"../components/css-component\";\n          export default function SiblingsWithCssComponent() {\n            return (\n              <>\n                <h2 data-sibling-route-with-css-component>Route with CSS Component</h2>\n                <CssComponent />\n              </>\n            );\n          }\n        `,\n\n        \"app/routes/_layout.siblings.with-lazy-css-component.tsx\": js`\n          import { LazyCssComponent } from \"../components/load-lazy-css-component\";\n          export default function SiblingsWithLazyCssComponent() {\n            return (\n              <>\n                <h2 data-sibling-route-with-lazy-css-component>Route with Lazy CSS Component</h2>\n                <LazyCssComponent />\n              </>\n            );\n          }\n        `,\n\n        \"app/routes/_layout.with-static-only-css-component.tsx\": js`\n          import StaticOnlyCssComponent from \"../components/static-only-css-component\";\n          export default function WithStaticOnlyCssComponent() {\n            return (\n              <>\n                <h2 data-route-with-static-only-css-component>Route with Static Only CSS Component</h2>\n                <StaticOnlyCssComponent />\n              </>\n            );\n          }\n        `,\n      },\n    });\n\n    appFixture = await createAppFixture(fixture);\n  });\n\n  test.afterAll(() => {\n    appFixture.close();\n  });\n\n  test(\"retains CSS from dynamic imports in a parent route on navigation if the same CSS is a static dependency of a child route\", async ({\n    page,\n  }) => {\n    let app = new PlaywrightFixture(appFixture, page);\n\n    await app.goto(\"/parent-child/with-css-component\");\n    await page.waitForSelector(\"[data-child-route-with-css-component]\");\n    expect(await page.locator(\"[data-css-component]\").count()).toBe(1);\n    expect(await page.locator(CSS_COMPONENT_LINK_SELECTOR).count()).toBe(1);\n    expect(\n      await page.locator(CSS_COMPONENT_FORCIBLY_UNIQUE_LINK_SELECTOR).count(),\n    ).toBe(1);\n    expect(await getColor(page, \"[data-css-component]\")).toBe(\"rgb(0, 128, 0)\");\n\n    await page.locator(\"[data-load-lazy-css-component]\").click();\n    await page.waitForSelector(\"[data-css-component]\");\n    expect(await page.locator(CSS_COMPONENT_LINK_SELECTOR).count()).toBe(2);\n    expect(\n      await page.locator(CSS_COMPONENT_FORCIBLY_UNIQUE_LINK_SELECTOR).count(),\n    ).toBe(1);\n    expect(await getColor(page, \"[data-css-component]\")).toBe(\"rgb(0, 128, 0)\");\n\n    await app.clickLink(\"/parent-child/without-css-component\");\n    await page.waitForSelector(\"[data-child-route-without-css-component]\");\n    expect(await page.locator(\"[data-css-component]\").count()).toBe(1);\n    expect(await page.locator(CSS_COMPONENT_LINK_SELECTOR).count()).toBe(1);\n    expect(\n      await page.locator(CSS_COMPONENT_FORCIBLY_UNIQUE_LINK_SELECTOR).count(),\n    ).toBe(0);\n    expect(await getColor(page, \"[data-css-component]\")).toBe(\"rgb(0, 128, 0)\");\n  });\n\n  test(\"supports CSS lazy loading when navigating to a sibling route if the current route has a static dependency on the same CSS\", async ({\n    page,\n  }) => {\n    let app = new PlaywrightFixture(appFixture, page);\n\n    await app.goto(\"/siblings/with-css-component\");\n    await page.waitForSelector(\"[data-sibling-route-with-css-component]\");\n    expect(await page.locator(\"[data-css-component]\").count()).toBe(1);\n    expect(await page.locator(CSS_COMPONENT_LINK_SELECTOR).count()).toBe(1);\n    expect(\n      await page.locator(CSS_COMPONENT_FORCIBLY_UNIQUE_LINK_SELECTOR).count(),\n    ).toBe(1);\n    expect(await getColor(page, \"[data-css-component]\")).toBe(\"rgb(0, 128, 0)\");\n\n    await app.clickLink(\"/siblings/with-lazy-css-component\");\n    await page.waitForSelector(\"[data-sibling-route-with-lazy-css-component]\");\n    expect(await page.locator(\"[data-css-component]\").count()).toBe(1);\n    expect(await page.locator(CSS_COMPONENT_LINK_SELECTOR).count()).toBe(1);\n    expect(\n      await page.locator(CSS_COMPONENT_FORCIBLY_UNIQUE_LINK_SELECTOR).count(),\n    ).toBe(0);\n    expect(await getColor(page, \"[data-css-component]\")).toBe(\"rgb(0, 128, 0)\");\n  });\n\n  test(\"does not add a hash to the CSS link if the CSS is only ever statically imported\", async ({\n    page,\n  }) => {\n    let app = new PlaywrightFixture(appFixture, page);\n\n    await app.goto(\"/with-static-only-css-component\");\n    await page.waitForSelector(\"[data-route-with-static-only-css-component]\");\n    expect(await page.locator(CSS_LINK_SELECTOR).count()).toBe(1);\n    expect(\n      await page.locator(ANY_FORCIBLY_UNIQUE_CSS_LINK_SELECTOR).count(),\n    ).toBe(0);\n    expect(await getColor(page, \"[data-static-only-css-component]\")).toBe(\n      \"rgb(128, 128, 0)\",\n    );\n  });\n});\n"
  },
  {
    "path": "integration/vite-css-test.ts",
    "content": "import type { Page } from \"@playwright/test\";\nimport { test, expect } from \"@playwright/test\";\nimport getPort from \"get-port\";\n\nimport {\n  createProject,\n  createEditor,\n  dev,\n  build,\n  reactRouterServe,\n  customDev,\n  EXPRESS_SERVER,\n  reactRouterConfig,\n  viteConfig,\n  viteMajorTemplates,\n  type TemplateName,\n} from \"./helpers/vite.js\";\n\nconst js = String.raw;\nconst css = String.raw;\n\nconst PADDING = \"20px\";\nconst NEW_PADDING = \"30px\";\n\nconst fixtures = [\n  ...viteMajorTemplates,\n  // TODO: Figure out why this is failing. It works outside the integration tests.\n  // {\n  //   templateName: \"rsc-vite-framework\",\n  //   templateDisplayName: \"RSC Vite Framework\",\n  // },\n] as const satisfies ReadonlyArray<{\n  templateName: TemplateName;\n  templateDisplayName: string;\n}>;\n\ntype RouteBasePath =\n  | \"css-with-links-export\"\n  | \"css-with-floated-link\"\n  | \"rsc-server-first-route\";\nconst getRouteBasePaths = (templateName: TemplateName) => {\n  return [\n    \"css-with-links-export\",\n    \"css-with-floated-link\",\n    ...(templateName.includes(\"rsc\")\n      ? ([\"rsc-server-first-route\"] as const)\n      : []),\n  ] as const satisfies RouteBasePath[];\n};\n\nconst files = ({ templateName }: { templateName: TemplateName }) => ({\n  \"postcss.config.js\": js`\n    export default ({\n      plugins: [\n        {\n          // Minimal PostCSS plugin to test that it's being used\n          postcssPlugin: 'replace',\n          Declaration (decl) {\n            decl.value = decl.value\n              .replace(\n                /NEW_PADDING_INJECTED_VIA_POSTCSS/g,\n                ${JSON.stringify(NEW_PADDING)},\n              )\n              .replace(\n                /PADDING_INJECTED_VIA_POSTCSS/g,\n                ${JSON.stringify(PADDING)},\n              );\n          },\n        },\n      ],\n    });\n  `,\n  // RSC Framework mode doesn't support custom entries yet\n  ...(!templateName.includes(\"rsc\")\n    ? {\n        \"app/entry.client.tsx\": js`\n          import \"./entry.client.css\";\n\n          import { HydratedRouter } from \"react-router/dom\";\n          import { startTransition, StrictMode } from \"react\";\n          import { hydrateRoot } from \"react-dom/client\";\n\n          startTransition(() => {\n            hydrateRoot(\n              document,\n              <StrictMode>\n                <HydratedRouter />\n              </StrictMode>\n            );\n          });\n        `,\n        \"app/entry.client.css\": css`\n          .entry-client {\n            background: pink;\n            padding: ${PADDING};\n          }\n        `,\n      }\n    : {}),\n  \"app/root.tsx\": js`\n    import { Links, Meta, Outlet, Scripts } from \"react-router\";\n\n    export default function Root() {\n      return (\n        <html lang=\"en\">\n          <head>\n            <Meta />\n            <Links />\n          </head>\n          <body>\n            <Outlet />\n            <Scripts />\n          </body>\n        </html>\n      );\n    }\n  `,\n  ...Object.assign(\n    {},\n    ...getRouteBasePaths(templateName).map((routeBasePath) => {\n      const isServerFirstRoute = routeBasePath === \"rsc-server-first-route\";\n      const exportName = isServerFirstRoute ? \"ServerComponent\" : \"default\";\n\n      return {\n        [`app/routes/${routeBasePath}/styles-bundled.css`]: css`\n          .${routeBasePath}-bundled {\n            background: papayawhip;\n            padding: ${PADDING};\n          }\n        `,\n        [`app/routes/${routeBasePath}/styles-postcss-linked.css`]: css`\n          .${routeBasePath}-postcss-linked {\n            background: salmon;\n            padding: PADDING_INJECTED_VIA_POSTCSS;\n          }\n        `,\n        [`app/routes/${routeBasePath}/styles.module.css`]: css`\n          .index {\n            background: peachpuff;\n            padding: ${PADDING};\n          }\n        `,\n        [`app/routes/${routeBasePath}/styles-vanilla-global.css.ts`]: js`\n          import { createVar, globalStyle } from \"@vanilla-extract/css\";\n\n          globalStyle(\".${routeBasePath}-vanilla-global\", {\n            background: \"lightgreen\",\n            padding: \"${PADDING}\",\n          });\n        `,\n        [`app/routes/${routeBasePath}/styles-vanilla-local.css.ts`]: js`\n          import { style } from \"@vanilla-extract/css\";\n\n          export const index = style({\n            background: \"lightblue\",\n            padding: \"${PADDING}\",\n          });\n        `,\n        [`app/routes/${routeBasePath}/route.tsx`]: js`\n          import \"./styles-bundled.css\";\n          import postcssLinkedStyles from \"./styles-postcss-linked.css?url\";\n          import cssModulesStyles from \"./styles.module.css\";\n          import \"./styles-vanilla-global.css\";\n          import * as stylesVanillaLocal from \"./styles-vanilla-local.css\";\n\n          // Workaround for \"Generated an empty chunk\" warnings in RSC Framework Mode\n          export function loader() {\n            return null;\n          }\n\n          ${\n            routeBasePath === \"css-with-links-export\"\n              ? `export function links() { return [{ rel: \"stylesheet\", href: postcssLinkedStyles }]; }`\n              : \"\"\n          }\n\n          function TestRoute() {\n            return (\n              <>\n                <input />\n                ${routeBasePath !== \"css-with-links-export\" ? `<link rel=\"stylesheet\" href={postcssLinkedStyles} precedence=\"default\" />` : \"\"}\n                <div id=\"entry-client\" className=\"entry-client\">\n                  <div id=\"css-modules\" className={cssModulesStyles.index}>\n                    <div id=\"css-postcss-linked\" className=\"${routeBasePath}-postcss-linked\">\n                      <div id=\"css-bundled\" className=\"${routeBasePath}-bundled\">\n                        <div id=\"css-vanilla-global\" className=\"${routeBasePath}-vanilla-global\">\n                          <div id=\"css-vanilla-local\" className={stylesVanillaLocal.index}>\n                            <h2>CSS test</h2>\n                          </div>\n                        </div>\n                      </div>\n                    </div>\n                  </div>\n                </div>\n              </>\n            );\n          }\n\n          export ${exportName === \"default\" ? \"default\" : `const ${exportName} =`} TestRoute;\n        `,\n      };\n    }),\n  ),\n});\n\ntest.describe(\"Vite CSS\", () => {\n  fixtures.forEach(({ templateName, templateDisplayName }) => {\n    test.describe(templateDisplayName, () => {\n      test.describe(\"vite dev\", async () => {\n        let port: number;\n        let cwd: string;\n        let stop: () => void;\n\n        test.beforeAll(async () => {\n          port = await getPort();\n          cwd = await createProject(\n            {\n              \"react-router.config.ts\": reactRouterConfig({\n                future: {\n                  v8_viteEnvironmentApi: templateName !== \"vite-5-template\",\n                },\n              }),\n              \"vite.config.ts\": await viteConfig.basic({\n                port,\n                templateName,\n                vanillaExtract: true,\n              }),\n              ...files({ templateName }),\n            },\n            templateName,\n          );\n          stop = await dev({ cwd, port });\n        });\n        test.afterAll(() => stop());\n\n        test.describe(() => {\n          test.use({ javaScriptEnabled: false });\n          test(\"without JS\", async ({ page }) => {\n            await pageLoadWorkflow({ page, port, templateName });\n          });\n        });\n\n        test.describe(() => {\n          test.use({ javaScriptEnabled: true });\n          test(\"with JS\", async ({ page }) => {\n            await pageLoadWorkflow({ page, port, templateName });\n            await hmrWorkflow({ page, port, cwd, templateName });\n          });\n        });\n      });\n\n      test.describe(\"vite dev with custom base\", async () => {\n        let port: number;\n        let cwd: string;\n        let stop: () => void;\n        let base = \"/custom/base/\";\n\n        test.beforeAll(async () => {\n          port = await getPort();\n          cwd = await createProject(\n            {\n              \"react-router.config.ts\": reactRouterConfig({\n                future: {\n                  v8_viteEnvironmentApi: templateName !== \"vite-5-template\",\n                },\n                basename: base,\n              }),\n              \"vite.config.ts\": await viteConfig.basic({\n                port,\n                base,\n                templateName,\n                vanillaExtract: true,\n              }),\n              ...files({ templateName }),\n            },\n            templateName,\n          );\n          stop = await dev({ cwd, port, basename: base });\n        });\n        test.afterAll(() => stop());\n\n        test.describe(() => {\n          test.use({ javaScriptEnabled: false });\n          test(\"without JS\", async ({ page }) => {\n            await pageLoadWorkflow({ page, port, base, templateName });\n          });\n        });\n\n        test.describe(() => {\n          test.use({ javaScriptEnabled: true });\n          test(\"with JS\", async ({ page }) => {\n            await pageLoadWorkflow({ page, port, base, templateName });\n            await hmrWorkflow({ page, port, cwd, base, templateName });\n          });\n        });\n      });\n\n      test.describe(\"express\", async () => {\n        let port: number;\n        let cwd: string;\n        let stop: () => void;\n\n        test.beforeAll(async () => {\n          port = await getPort();\n          cwd = await createProject(\n            {\n              \"react-router.config.ts\": reactRouterConfig({\n                future: {\n                  v8_viteEnvironmentApi: templateName !== \"vite-5-template\",\n                },\n              }),\n              \"vite.config.ts\": await viteConfig.basic({\n                port,\n                templateName,\n                vanillaExtract: true,\n              }),\n              \"server.mjs\": EXPRESS_SERVER({ port, templateName }),\n              ...files({ templateName }),\n            },\n            templateName,\n          );\n          stop = await customDev({ cwd, port });\n        });\n        test.afterAll(() => stop());\n\n        test.describe(() => {\n          test.use({ javaScriptEnabled: false });\n          test(\"without JS\", async ({ page }) => {\n            await pageLoadWorkflow({ page, port, templateName });\n          });\n        });\n\n        test.describe(() => {\n          test.use({ javaScriptEnabled: true });\n          test(\"with JS\", async ({ page }) => {\n            await pageLoadWorkflow({ page, port, templateName });\n            await hmrWorkflow({ page, port, cwd, templateName });\n          });\n        });\n      });\n\n      test.describe(\"vite build\", async () => {\n        let port: number;\n        let cwd: string;\n        let stop: () => void;\n\n        test.beforeAll(async () => {\n          port = await getPort();\n          cwd = await createProject(\n            {\n              \"react-router.config.ts\": reactRouterConfig({\n                future: {\n                  v8_viteEnvironmentApi: templateName !== \"vite-5-template\",\n                },\n              }),\n              \"vite.config.ts\": await viteConfig.basic({\n                port,\n                templateName,\n                vanillaExtract: true,\n              }),\n              ...files({ templateName }),\n            },\n            templateName,\n          );\n\n          let edit = createEditor(cwd);\n          await edit(\"package.json\", (contents) =>\n            contents.replace(\n              '\"sideEffects\": false',\n              '\"sideEffects\": [\"*.css.ts\"]',\n            ),\n          );\n\n          let { stderr, status } = build({\n            cwd,\n            env: {\n              // Vanilla Extract uses Vite's CJS build which emits a warning to stderr\n              VITE_CJS_IGNORE_WARNING: \"true\",\n            },\n          });\n          let stderrString = stderr.toString();\n          if (templateName.includes(\"rsc\")) {\n            // In RSC builds, the same assets can be generated multiple times\n            stderrString = stderrString.replace(\n              /The emitted file \".*?\" overwrites a previously emitted file of the same name\\.\\n?/g,\n              \"\",\n            );\n          }\n          expect(stderrString).toBeFalsy();\n          expect(status).toBe(0);\n          stop = await reactRouterServe({ cwd, port });\n        });\n        test.afterAll(() => stop());\n\n        test.describe(() => {\n          test.use({ javaScriptEnabled: false });\n          test(\"without JS\", async ({ page }) => {\n            await pageLoadWorkflow({ page, port, templateName });\n          });\n        });\n\n        test.describe(() => {\n          test.use({ javaScriptEnabled: true });\n          test(\"with JS\", async ({ page }) => {\n            await pageLoadWorkflow({ page, port, templateName });\n          });\n        });\n      });\n\n      test.describe(\"vite build with CSS code splitting disabled\", async () => {\n        test.fixme(\n          templateName.includes(\"rsc\"),\n          \"RSC Framework mode doesn't support disabling CSS code splitting yet (likely due to @vitejs/plugin-rsc)\",\n        );\n\n        let port: number;\n        let cwd: string;\n        let stop: () => void;\n\n        test.beforeAll(async () => {\n          port = await getPort();\n          cwd = await createProject(\n            {\n              \"react-router.config.ts\": reactRouterConfig({\n                future: {\n                  v8_viteEnvironmentApi: templateName !== \"vite-5-template\",\n                },\n              }),\n              \"vite.config.ts\": await viteConfig.basic({\n                port,\n                templateName,\n                cssCodeSplit: false,\n                vanillaExtract: true,\n              }),\n              ...files({ templateName }),\n            },\n            templateName,\n          );\n\n          let edit = createEditor(cwd);\n          await edit(\"package.json\", (contents) =>\n            contents.replace(\n              '\"sideEffects\": false',\n              '\"sideEffects\": [\"*.css.ts\"]',\n            ),\n          );\n\n          let { stderr, status } = build({\n            cwd,\n            env: {\n              // Vanilla Extract uses Vite's CJS build which emits a warning to stderr\n              VITE_CJS_IGNORE_WARNING: \"true\",\n            },\n          });\n          expect(stderr.toString()).toBeFalsy();\n          expect(status).toBe(0);\n          stop = await reactRouterServe({ cwd, port });\n        });\n        test.afterAll(() => stop());\n\n        test.describe(() => {\n          test.use({ javaScriptEnabled: false });\n          test(\"without JS\", async ({ page }) => {\n            await pageLoadWorkflow({ page, port, templateName });\n          });\n        });\n\n        test.describe(() => {\n          test.use({ javaScriptEnabled: true });\n          test(\"with JS\", async ({ page }) => {\n            await pageLoadWorkflow({ page, port, templateName });\n          });\n        });\n      });\n    });\n  });\n});\n\nasync function pageLoadWorkflow({\n  page,\n  port,\n  base,\n  templateName,\n}: {\n  page: Page;\n  port: number;\n  base?: string;\n  templateName: TemplateName;\n}) {\n  for (const routeBase of getRouteBasePaths(templateName)) {\n    let pageErrors: Error[] = [];\n    page.on(\"pageerror\", (error) => pageErrors.push(error));\n\n    await page.goto(`http://localhost:${port}${base ?? \"/\"}${routeBase}`, {\n      waitUntil: \"networkidle\",\n    });\n\n    await Promise.all(\n      [\n        \"#css-bundled\",\n        \"#css-postcss-linked\",\n        \"#css-modules\",\n        \"#css-vanilla-global\",\n        \"#css-vanilla-local\",\n      ].map(\n        async (selector) =>\n          await expect(page.locator(selector)).toHaveCSS(\"padding\", PADDING),\n      ),\n    );\n  }\n}\n\nasync function hmrWorkflow({\n  page,\n  cwd,\n  port,\n  base,\n  templateName,\n}: {\n  page: Page;\n  cwd: string;\n  port: number;\n  base?: string;\n  templateName: TemplateName;\n}) {\n  for (const routeBase of getRouteBasePaths(templateName)) {\n    let pageErrors: Error[] = [];\n    page.on(\"pageerror\", (error) => pageErrors.push(error));\n\n    await page.goto(`http://localhost:${port}${base ?? \"/\"}${routeBase}`, {\n      waitUntil: \"networkidle\",\n    });\n\n    let input = page.locator(\"input\");\n    await expect(input).toBeVisible();\n\n    let edit = createEditor(cwd);\n    let modifyCss = (contents: string) =>\n      contents\n        .replace(PADDING, NEW_PADDING)\n        .replace(\n          \"PADDING_INJECTED_VIA_POSTCSS\",\n          \"NEW_PADDING_INJECTED_VIA_POSTCSS\",\n        );\n\n    const testCases = [\n      {\n        file: \"styles-bundled.css\",\n        selector: \"#css-bundled\",\n      },\n      {\n        file: \"styles.module.css\",\n        selector: \"#css-modules\",\n      },\n      {\n        file: \"styles-postcss-linked.css\",\n        selector: \"#css-postcss-linked\",\n      },\n      {\n        file: \"styles-vanilla-global.css.ts\",\n        selector: \"#css-vanilla-global\",\n      },\n      // Vanilla Extract's HMR isn't working for RSC server-first routes\n      ...(routeBase === \"rsc-server-first-route\"\n        ? []\n        : ([\n            {\n              file: \"styles-vanilla-local.css.ts\",\n              selector: \"#css-vanilla-local\",\n            },\n          ] as const)),\n    ] as const satisfies Array<{\n      file: string;\n      selector: string;\n    }>;\n\n    for (const { file, selector } of testCases) {\n      const routeFile = `app/routes/${routeBase}/${file}`;\n      await input.fill(routeFile);\n      await edit(routeFile, modifyCss);\n      await expect(\n        page.locator(selector),\n        `${file}: CSS update for ${routeFile}`,\n      ).toHaveCSS(\"padding\", NEW_PADDING);\n\n      // Ensure CSS updates were handled by HMR\n      await expect(input, `State preservation for ${routeFile}`).toHaveValue(\n        routeFile,\n      );\n    }\n\n    // RSC Framework mode doesn't support custom entries yet\n    if (!templateName.includes(\"rsc\")) {\n      // The following change triggers a full page reload, so we check it after all the checks for HMR state preservation\n      await edit(\"app/entry.client.css\", modifyCss);\n      await expect(page.locator(\"#entry-client\")).toHaveCSS(\n        \"padding\",\n        NEW_PADDING,\n      );\n    }\n\n    expect(pageErrors).toEqual([]);\n  }\n}\n"
  },
  {
    "path": "integration/vite-dev-custom-entry-test.ts",
    "content": "import { expect } from \"@playwright/test\";\nimport type { Files } from \"./helpers/vite.js\";\nimport { test, viteConfig } from \"./helpers/vite.js\";\n\nconst js = String.raw;\n\nconst files: Files = async ({ port }) => ({\n  \"vite.config.ts\": await viteConfig.basic({ port }),\n  \"app/entry.server.tsx\": js`\n    import { PassThrough } from \"node:stream\";\n\n    import type { EntryContext } from \"react-router\";\n    import { createReadableStreamFromReadable } from \"@react-router/node\";\n    import { ServerRouter } from \"react-router\";\n    import { renderToPipeableStream } from \"react-dom/server\";\n\n    export default function handleRequest(\n      request: Request,\n      responseStatusCode: number,\n      responseHeaders: Headers,\n      remixContext: EntryContext\n    ) {\n      return new Promise((resolve, reject) => {\n        let shellRendered = false;\n        const { pipe, abort } = renderToPipeableStream(\n          <ServerRouter context={remixContext} url={request.url} />,\n          {\n            onShellReady() {\n              shellRendered = true;\n              const body = new PassThrough();\n              const stream = createReadableStreamFromReadable(body);\n\n              responseHeaders.set(\"Content-Type\", \"text/html\");\n\n              // Used to test that the request object is an instance of the global Request constructor\n              responseHeaders.set(\"x-test-request-instanceof-request\", String(request instanceof Request));\n\n              resolve(\n                new Response(stream, {\n                  headers: responseHeaders,\n                  status: responseStatusCode,\n                })\n              );\n\n              pipe(body);\n            },\n            onShellError(error: unknown) {\n              reject(error);\n            },\n            onError(error: unknown) {\n              responseStatusCode = 500;\n              // Log streaming rendering errors from inside the shell.  Don't log\n              // errors encountered during initial shell rendering since they'll\n              // reject and get logged in handleDocumentRequest.\n              if (shellRendered) {\n                console.error(error);\n              }\n            },\n          }\n        );\n\n        setTimeout(abort, 5000);\n      });\n    }\n  `,\n  \"app/root.tsx\": js`\n    import { Links, Meta, Outlet, Scripts } from \"react-router\";\n\n    export default function Root() {\n      return (\n        <html lang=\"en\">\n          <head>\n            <Meta />\n            <Links />\n          </head>\n          <body>\n            <div id=\"content\">\n              <h1>Root</h1>\n              <Outlet />\n            </div>\n            <Scripts />\n          </body>\n        </html>\n      );\n    }\n  `,\n  \"app/routes/_index.tsx\": js`\n    export default function IndexRoute() {\n      return <div>IndexRoute</div>\n    }\n  `,\n});\n\ntest.describe(\"Vite custom entry dev\", () => {\n  // Ensure libraries/consumers can perform an instanceof check on the request\n  test(\"request instanceof Request\", async ({ request, dev }) => {\n    let { port } = await dev(files);\n    let res = await request.get(`http://localhost:${port}/`);\n    expect(res.headers()).toMatchObject({\n      \"x-test-request-instanceof-request\": \"true\",\n    });\n  });\n});\n"
  },
  {
    "path": "integration/vite-dev-test.ts",
    "content": "import fs from \"node:fs/promises\";\nimport path from \"node:path\";\n\nimport { expect } from \"@playwright/test\";\nimport dedent from \"dedent\";\n\nimport {\n  reactRouterConfig,\n  viteConfig,\n  test,\n  type TemplateName,\n  type Files,\n} from \"./helpers/vite.js\";\n\nconst tsx = dedent;\n\nconst fixtures = [\n  {\n    templateName: \"vite-5-template\",\n    v8_viteEnvironmentApi: false,\n  },\n  {\n    templateName: \"vite-6-template\",\n    v8_viteEnvironmentApi: true,\n  },\n  {\n    templateName: \"rsc-vite-framework\",\n    v8_viteEnvironmentApi: true,\n  },\n] as const satisfies ReadonlyArray<{\n  templateName: TemplateName;\n  v8_viteEnvironmentApi: boolean;\n}>;\n\ntest.describe(\"Vite dev\", () => {\n  for (const { templateName, v8_viteEnvironmentApi } of fixtures) {\n    test.describe(`template: ${templateName} viteEnvironmentApi: ${v8_viteEnvironmentApi}`, () => {\n      const files: Files = async ({ port }) => ({\n        \"react-router.config.ts\": reactRouterConfig({\n          future: { v8_viteEnvironmentApi },\n        }),\n        \"vite.config.ts\": await viteConfig.basic({\n          port,\n          templateName,\n          mdx: true,\n        }),\n        \"app/root.tsx\": tsx`\n          import { Links, Meta, Outlet, Scripts } from \"react-router\";\n\n          export default function Root() {\n            return (\n              <html lang=\"en\">\n                <head>\n                  <Meta />\n                  <Links />\n                </head>\n                <body>\n                  <div id=\"content\">\n                    <h1>Root</h1>\n                    <Outlet />\n                  </div>\n                  <Scripts />\n                </body>\n              </html>\n            );\n          }\n        `,\n        \"app/routes/_index.tsx\": tsx`\n          export default function IndexRoute() {\n            return (\n              <div id=\"index\">\n                <h2 data-title>Index</h2>\n                <input />\n                <p data-hmr>HMR updated: no</p>\n              </div>\n            );\n          }\n        `,\n        \"app/routes/deferred-loader-data.tsx\": tsx`\n          import { Suspense } from \"react\";\n          import { Await, useLoaderData } from \"react-router\";\n\n          export function loader() {\n            let deferred = new Promise((resolve) => {\n              setTimeout(() => resolve(true), 1000)\n            });\n            return { deferred };\n          }\n\n          export default function IndexRoute() {\n            const { deferred } = useLoaderData<typeof loader>();\n\n            return (\n              <div id=\"index\">\n                <Suspense fallback={<p data-defer>Defer finished: no</p>}>\n                  <Await resolve={deferred}>{() => <p data-defer>Defer finished: yes</p>}</Await>\n                </Suspense>\n              </div>\n            );\n          }\n        `,\n        \"app/routes/set-cookies.tsx\": tsx`\n          import type { LoaderFunction } from \"react-router\";\n\n          export const loader: LoaderFunction = () => {\n            const headers = new Headers();\n\n            headers.append(\n              \"Set-Cookie\",\n              \"first=one; Domain=localhost; Path=/; SameSite=Lax\"\n            );\n\n            headers.append(\n              \"Set-Cookie\",\n              \"second=two; Domain=localhost; Path=/; SameSite=Lax\"\n            );\n\n            headers.append(\n              \"Set-Cookie\",\n              \"third=three; Domain=localhost; Path=/; SameSite=Lax\"\n            );\n\n            headers.set(\"location\", \"http://localhost:${port}/get-cookies\");\n\n            const response = new Response(null, {\n              headers,\n              status: 302,\n            });\n\n            return response;\n          };\n        `,\n        \"app/routes/get-cookies.tsx\": tsx`\n          import { useLoaderData, type LoaderFunctionArgs } from \"react-router\";\n\n          export const loader = ({ request }: LoaderFunctionArgs) => ({\n            cookies: request.headers.get(\"Cookie\")\n          });\n\n          export default function IndexRoute() {\n            const { cookies } = useLoaderData<typeof loader>();\n\n            return (\n              <div id=\"get-cookies\">\n                <h2 data-title>Get Cookies</h2>\n                <p data-cookies>{cookies}</p>\n              </div>\n            );\n          }\n        `,\n        \"app/routes/jsx.jsx\": tsx`\n          export default function JsxRoute() {\n            return (\n              <div id=\"jsx\">\n                <p data-hmr>HMR updated: no</p>\n              </div>\n            );\n          }\n        `,\n        \"app/routes/mdx.mdx\": tsx`\n          import { useLoaderData } from \"react-router\";\n\n          export const loader = () => {\n            return {\n              content: \"MDX route content from loader\",\n            }\n          }\n\n          export function MdxComponent() {\n            const { content } = useLoaderData();\n            return <div data-mdx-route>{content}</div>\n          }\n\n          ## MDX Route\n\n          <MdxComponent />\n        `,\n        ...(!templateName.includes(\"rsc\")\n          ? {\n              \".env\": `\n                ENV_VAR_FROM_DOTENV_FILE=Content from .env file\n              `,\n              \"app/routes/dotenv.tsx\": tsx`\n                import { useState, useEffect } from \"react\";\n                import { useLoaderData } from \"react-router\";\n\n                export const loader = () => {\n                  return {\n                    loaderContent: process.env.ENV_VAR_FROM_DOTENV_FILE,\n                  }\n                }\n\n                export default function DotenvRoute() {\n                  const { loaderContent } = useLoaderData();\n\n                  const [clientContent, setClientContent] = useState('');\n                  useEffect(() => {\n                    try {\n                      setClientContent(\"process.env.ENV_VAR_FROM_DOTENV_FILE shouldn't be available on the client, found: \" + process.env.ENV_VAR_FROM_DOTENV_FILE);\n                    } catch (err) {\n                      setClientContent(\"process.env.ENV_VAR_FROM_DOTENV_FILE not available on the client, which is a good thing\");\n                    }\n                  }, []);\n\n                  return <>\n                    <div data-dotenv-route-loader-content>{loaderContent}</div>\n                    <div data-dotenv-route-client-content>{clientContent}</div>\n                  </>\n                }\n              `,\n            }\n          : {}),\n        \"app/routes/error-stacktrace.tsx\": tsx`\n          import { Link, useLocation, type LoaderFunction, type MetaFunction } from \"react-router\";\n\n          export const loader: LoaderFunction = ({ request }) => {\n            if (request.url.includes(\"crash-loader\")) {\n              throw new Error(\"crash-loader\");\n            }\n            return null;\n          };\n\n          export default function TestRoute() {\n            const location = useLocation();\n\n            if (import.meta.env.SSR && location.search.includes(\"crash-server-render\")) {\n              throw new Error(\"crash-server-render\");\n            }\n\n            return (\n              <div>\n                <ul>\n                  {[\"crash-loader\", \"crash-server-render\"].map(\n                    (v) => (\n                      <li key={v}>\n                        <Link to={\"/?\" + v}>{v}</Link>\n                      </li>\n                    )\n                  )}\n                </ul>\n              </div>\n            );\n          }\n        `,\n        \"app/routes/known-route-exports.tsx\": tsx`\n          import { useMatches } from \"react-router\";\n\n          export const meta = () => [{\n            title: \"HMR meta: 0\"\n          }]\n\n          export const links = () => [{\n            rel: \"icon\",\n            href: \"/favicon.ico\",\n            type: \"image/png\",\n            \"data-link\": \"HMR links: 0\",\n          }]\n\n          export const handle = {\n            data: \"HMR handle: 0\"\n          };\n\n          export default function TestRoute() {\n            const matches = useMatches();\n\n            return (\n              <div id=\"known-route-export-hmr\">\n                <input />\n                <p data-hmr>HMR component: 0</p>\n                <p data-handle>{matches[1].handle.data}</p>\n              </div>\n            );\n          }\n        `,\n      });\n\n      test(\"renders matching routes with HMR\", async ({ dev, page }) => {\n        const { cwd, port } = await dev(files, templateName);\n\n        await page.goto(`http://localhost:${port}/`, {\n          waitUntil: \"networkidle\",\n        });\n\n        // Ensure no errors on page load\n        expect(page.errors).toEqual([]);\n\n        await expect(page.locator(\"#index [data-title]\")).toHaveText(\"Index\");\n\n        let hmrStatus = page.locator(\"#index [data-hmr]\");\n        await expect(hmrStatus).toHaveText(\"HMR updated: no\");\n\n        let input = page.locator(\"#index input\");\n        await expect(input).toBeVisible();\n        await input.type(\"stateful\");\n\n        let indexRouteContents = await fs.readFile(\n          path.join(cwd, \"app/routes/_index.tsx\"),\n          \"utf8\",\n        );\n        await fs.writeFile(\n          path.join(cwd, \"app/routes/_index.tsx\"),\n          indexRouteContents.replace(\"HMR updated: no\", \"HMR updated: yes\"),\n          \"utf8\",\n        );\n        await page.waitForLoadState(\"networkidle\");\n        await expect(hmrStatus).toHaveText(\"HMR updated: yes\");\n        await expect(input).toHaveValue(\"stateful\");\n\n        // Ensure no errors after HMR\n        expect(page.errors).toEqual([]);\n      });\n\n      test(\"deferred loader data\", async ({ dev, page }) => {\n        test.fixme(\n          templateName.includes(\"rsc\"),\n          \"RSC doesn't support Await component\",\n        );\n\n        const { port } = await dev(files, templateName);\n        await page.goto(`http://localhost:${port}/deferred-loader-data`, {\n          waitUntil: \"networkidle\",\n        });\n\n        // Ensure no errors on page load\n        expect(page.errors).toEqual([]);\n\n        await expect(page.locator(\"#index [data-defer]\")).toHaveText(\n          \"Defer finished: yes\",\n        );\n        // Ensure no errors after deferred rendering\n        expect(page.errors).toEqual([]);\n      });\n\n      test(\"handles multiple set-cookie headers\", async ({ dev, page }) => {\n        const { port } = await dev(files, templateName);\n\n        await page.goto(`http://localhost:${port}/set-cookies`, {\n          waitUntil: \"networkidle\",\n        });\n\n        expect(page.errors).toEqual([]);\n\n        // Ensure we redirected\n        expect(new URL(page.url()).pathname).toBe(\"/get-cookies\");\n\n        await expect(page.locator(\"#get-cookies [data-cookies]\")).toHaveText(\n          \"first=one; second=two; third=three\",\n        );\n      });\n\n      test(\"handles JSX in .jsx file without React import\", async ({\n        dev,\n        page,\n      }) => {\n        const { cwd, port } = await dev(files, templateName);\n\n        await page.goto(`http://localhost:${port}/jsx`, {\n          waitUntil: \"networkidle\",\n        });\n        expect(page.errors).toEqual([]);\n\n        let hmrStatus = page.locator(\"#jsx [data-hmr]\");\n        await expect(hmrStatus).toHaveText(\"HMR updated: no\");\n\n        let indexRouteContents = await fs.readFile(\n          path.join(cwd, \"app/routes/jsx.jsx\"),\n          \"utf8\",\n        );\n        await fs.writeFile(\n          path.join(cwd, \"app/routes/jsx.jsx\"),\n          indexRouteContents.replace(\"HMR updated: no\", \"HMR updated: yes\"),\n          \"utf8\",\n        );\n        await page.waitForLoadState(\"networkidle\");\n        await expect(hmrStatus).toHaveText(\"HMR updated: yes\");\n\n        expect(page.errors).toEqual([]);\n      });\n\n      test(\"handles MDX routes\", async ({ dev, page }) => {\n        const { port } = await dev(files, templateName);\n        await page.goto(`http://localhost:${port}/mdx`, {\n          waitUntil: \"networkidle\",\n        });\n        expect(page.errors).toEqual([]);\n\n        let mdxContent = page.locator(\"[data-mdx-route]\");\n        await expect(mdxContent).toHaveText(\"MDX route content from loader\");\n\n        expect(page.errors).toEqual([]);\n      });\n\n      test(\"loads .env file\", async ({ dev, page }) => {\n        test.fixme(\n          templateName.includes(\"rsc\"),\n          \"RSC Framework Mode doesn't load .env files\",\n        );\n\n        const { port } = await dev(files, templateName);\n\n        await page.goto(`http://localhost:${port}/dotenv`, {\n          waitUntil: \"networkidle\",\n        });\n        expect(page.errors).toEqual([]);\n\n        let loaderContent = page.locator(\"[data-dotenv-route-loader-content]\");\n        await expect(loaderContent).toHaveText(\"Content from .env file\");\n\n        let clientContent = page.locator(\"[data-dotenv-route-client-content]\");\n        await expect(clientContent).toHaveText(\n          \"process.env.ENV_VAR_FROM_DOTENV_FILE not available on the client, which is a good thing\",\n        );\n\n        expect(page.errors).toEqual([]);\n      });\n\n      test(\"request errors map to original source code\", async ({\n        dev,\n        page,\n      }) => {\n        test.fixme(\n          templateName.includes(\"rsc\"),\n          \"Investigate this for RSC Framework Mode\",\n        );\n\n        const { port } = await dev(files, templateName);\n\n        await page.goto(\n          `http://localhost:${port}/error-stacktrace?crash-server-render`,\n        );\n        await expect(page.locator(\"main\")).toContainText(\n          \"Error: crash-server-render\",\n        );\n        await expect(page.locator(\"main\")).toContainText(\n          \"error-stacktrace.tsx:14:11\",\n        );\n\n        await page.goto(\n          `http://localhost:${port}/error-stacktrace?crash-loader`,\n        );\n        await expect(page.locator(\"main\")).toContainText(\"Error: crash-loader\");\n        await expect(page.locator(\"main\")).toContainText(\n          \"error-stacktrace.tsx:5:11\",\n        );\n      });\n\n      test(\"handle known route exports with HMR\", async ({ dev, page }) => {\n        test.fixme(\n          templateName.includes(\"rsc\"),\n          \"Investigate why this is failing in RSC Framework Mode\",\n        );\n\n        const { cwd, port } = await dev(files, templateName);\n\n        await page.goto(`http://localhost:${port}/known-route-exports`, {\n          waitUntil: \"networkidle\",\n        });\n        expect(page.errors).toEqual([]);\n\n        // file editing utils\n        let filepath = path.join(cwd, \"app/routes/known-route-exports.tsx\");\n        let filedata = await fs.readFile(filepath, \"utf8\");\n        async function editFile(edit: (data: string) => string) {\n          filedata = edit(filedata);\n          await fs.writeFile(filepath, filedata, \"utf8\");\n        }\n\n        // verify input state is preserved after each update\n        let input = page.locator(\"input\");\n        await input.type(\"stateful\");\n        await expect(input).toHaveValue(\"stateful\");\n\n        // component\n        await editFile((data) =>\n          data.replace(\"HMR component: 0\", \"HMR component: 1\"),\n        );\n        await expect(page.locator(\"[data-hmr]\")).toHaveText(\"HMR component: 1\");\n        await expect(input).toHaveValue(\"stateful\");\n\n        // handle\n        await editFile((data) =>\n          data.replace(\"HMR handle: 0\", \"HMR handle: 1\"),\n        );\n        await expect(page.locator(\"[data-handle]\")).toHaveText(\"HMR handle: 1\");\n        await expect(input).toHaveValue(\"stateful\");\n\n        // meta\n        await editFile((data) => data.replace(\"HMR meta: 0\", \"HMR meta: 1\"));\n        await expect(page).toHaveTitle(\"HMR meta: 1\");\n        await expect(input).toHaveValue(\"stateful\");\n\n        // links\n        await editFile((data) => data.replace(\"HMR links: 0\", \"HMR links: 1\"));\n        await expect(page.locator(\"[data-link]\")).toHaveAttribute(\n          \"data-link\",\n          \"HMR links: 1\",\n        );\n\n        expect(page.errors).toEqual([]);\n      });\n    });\n  }\n});\n"
  },
  {
    "path": "integration/vite-dot-client-test.ts",
    "content": "import * as path from \"node:path\";\nimport { test, expect } from \"@playwright/test\";\n\nimport { createProject, grep, build } from \"./helpers/vite.js\";\n\nlet files = {\n  \"app/utils.client.ts\": String.raw`\n    export const dotClientFile = \"CLIENT_ONLY_FILE\";\n    export default dotClientFile;\n  `,\n  \"app/.client/utils.ts\": String.raw`\n    export const dotClientDir = \"CLIENT_ONLY_DIR\";\n    export default dotClientDir;\n  `,\n};\n\ntest(\"Vite / client code excluded from server bundle\", async () => {\n  let cwd = await createProject({\n    ...files,\n    \"app/routes/dot-client-imports.tsx\": String.raw`\n      import { dotClientFile } from \"../utils.client\";\n      import { dotClientDir } from \"../.client/utils\";\n\n      export default function() {\n        const [mounted, setMounted] = useState(false);\n\n        useEffect(() => {\n          setMounted(true);\n        }, []);\n\n        return (\n          <>\n            <h2>Index</h2>\n            <p>{mounted ? dotClientFile + dotClientDir : \"\"}</p>\n          </>\n        );\n      }\n    `,\n  });\n  let { status } = build({ cwd });\n  expect(status).toBe(0);\n  let lines = grep(\n    path.join(cwd, \"build/server\"),\n    /CLIENT_ONLY_FILE|CLIENT_ONLY_DIR/,\n  );\n  expect(lines).toHaveLength(0);\n});\n"
  },
  {
    "path": "integration/vite-dot-server-test.ts",
    "content": "import * as path from \"node:path\";\nimport { expect } from \"@playwright/test\";\nimport stripAnsi from \"strip-ansi\";\nimport dedent from \"dedent\";\n\nimport type { Files } from \"./helpers/vite.js\";\nimport {\n  test,\n  createProject,\n  grep,\n  build,\n  viteConfig,\n} from \"./helpers/vite.js\";\n\nlet serverOnlyModule = `\n  export const serverOnly = \"SERVER_ONLY\";\n  export default serverOnly;\n`;\n\nlet tsconfig = (aliases: Record<string, string[]>) => `\n  {\n    \"include\": [\"env.d.ts\", \"**/*.ts\", \"**/*.tsx\"],\n    \"compilerOptions\": {\n      \"lib\": [\"DOM\", \"DOM.Iterable\", \"ES2022\"],\n      \"verbatimModuleSyntax\": true,\n      \"esModuleInterop\": true,\n      \"jsx\": \"react-jsx\",\n      \"module\": \"ESNext\",\n      \"moduleResolution\": \"Bundler\",\n      \"resolveJsonModule\": true,\n      \"target\": \"ES2022\",\n      \"strict\": true,\n      \"allowJs\": true,\n      \"baseUrl\": \".\",\n      \"paths\": ${JSON.stringify(aliases)},\n      \"noEmit\": true\n    }\n  }\n`;\n\ntest(\"Vite / dead-code elimination for server exports\", async () => {\n  let cwd = await createProject({\n    \"app/utils.server.ts\": serverOnlyModule,\n    \"app/.server/utils.ts\": serverOnlyModule,\n    \"app/routes/remove-server-exports-and-dce.tsx\": `\n      import fs from \"node:fs\";\n      import { useLoaderData } from \"react-router\";\n\n      import { serverOnly as serverOnlyFile } from \"../utils.server\";\n      import { serverOnly as serverOnlyDir } from \"../.server/utils\";\n\n      export const loader = () => {\n        let contents = fs.readFileSync(\"server_only.txt\");\n        return { serverOnlyFile, serverOnlyDir, contents }\n      }\n\n      export const action = () => {\n        let contents = fs.readFileSync(\"server_only.txt\");\n        console.log({ serverOnlyFile, serverOnlyDir, contents });\n        return null;\n      }\n\n      export default function() {\n        let { data } = useLoaderData<typeof loader>();\n        return <pre>{JSON.stringify(data)}</pre>;\n      }\n    `,\n  });\n  let { status } = build({ cwd });\n  expect(status).toBe(0);\n\n  let lines = grep(\n    path.join(cwd, \"build/client\"),\n    /SERVER_ONLY|SERVER_ONLY|node:fs/,\n  );\n  expect(lines).toHaveLength(0);\n});\n\ntest.describe(\"Vite / route / server-only module referenced by client\", () => {\n  let matrix = [\n    { type: \"file\", path: \"app/utils.server.ts\", specifier: `~/utils.server` },\n    { type: \"dir\", path: \"app/.server/utils.ts\", specifier: `~/.server/utils` },\n\n    {\n      type: \"file alias\",\n      path: \"app/utils.server.ts\",\n      specifier: `#dot-server-file`,\n    },\n    {\n      type: \"dir alias\",\n      path: \"app/.server/utils.ts\",\n      specifier: `#dot-server-dir/utils`,\n    },\n  ];\n\n  let cases = matrix.flatMap(({ type, path, specifier }) => [\n    {\n      name: `default import / .server ${type}`,\n      path,\n      specifier,\n      route: `\n        import serverOnly from \"${specifier}\";\n        export default () => <h1>{serverOnly}</h1>;\n      `,\n    },\n    {\n      name: `named import / .server ${type}`,\n      path,\n      specifier,\n      route: `\n        import { serverOnly } from \"${specifier}\"\n        export default () => <h1>{serverOnly}</h1>;\n      `,\n    },\n    {\n      name: `namespace import / .server ${type}`,\n      path,\n      specifier,\n      route: `\n        import * as utils from \"${specifier}\"\n        export default () => <h1>{utils.serverOnly}</h1>;\n      `,\n    },\n  ]);\n\n  for (let { name, path, specifier, route } of cases) {\n    test(name, async () => {\n      let cwd = await createProject({\n        \"tsconfig.json\": tsconfig({\n          \"~/*\": [\"app/*\"],\n          \"#dot-server-file\": [\"app/utils.server.ts\"],\n          \"#dot-server-dir/*\": [\"app/.server/*\"],\n        }),\n        [path]: serverOnlyModule,\n        \"app/routes/_index.tsx\": route,\n      });\n      let result = build({ cwd });\n      let stderr = result.stderr.toString(\"utf8\");\n      [\n        \"Server-only module referenced by client\",\n\n        `    '${specifier}' imported by route 'app/routes/_index.tsx'`,\n\n        \"  React Router automatically removes server-code from these exports:\",\n        \"    `loader`, `action`, `middleware`, `headers`\",\n\n        `  But other route exports in 'app/routes/_index.tsx' depend on '${specifier}'.`,\n\n        \"  See https://reactrouter.com/explanation/code-splitting#removal-of-server-code\",\n      ].forEach(expect(stderr).toMatch);\n    });\n  }\n});\n\ntest.describe(\"Vite / non-route / server-only module referenced by client\", () => {\n  let matrix = [\n    { type: \"file\", path: \"app/utils.server.ts\", specifier: `~/utils.server` },\n    { type: \"dir\", path: \"app/.server/utils.ts\", specifier: `~/.server/utils` },\n  ];\n\n  let cases = matrix.flatMap(({ type, path, specifier }) => [\n    {\n      name: `default import / .server ${type}`,\n      path,\n      specifier,\n      nonroute: `\n        import serverOnly from \"${specifier}\";\n        export const getServerOnly = () => serverOnly;\n      `,\n    },\n    {\n      name: `named import / .server ${type}`,\n      path,\n      specifier,\n      nonroute: `\n        import { serverOnly } from \"${specifier}\";\n        export const getServerOnly = () => serverOnly;\n      `,\n    },\n    {\n      name: `namespace import / .server ${type}`,\n      path,\n      specifier,\n      nonroute: `\n        import * as utils from \"${specifier}\";\n        export const getServerOnly = () => utils.serverOnly;\n      `,\n    },\n  ]);\n\n  for (let { name, path, specifier, nonroute } of cases) {\n    test(name, async () => {\n      let cwd = await createProject({\n        [path]: serverOnlyModule,\n        \"app/reexport-server-only.ts\": nonroute,\n        \"app/routes/_index.tsx\": `\n          import { serverOnly } from \"~/reexport-server-only\"\n          export default () => <h1>{serverOnly}</h1>;\n        `,\n      });\n      let result = build({ cwd });\n      let stderr = stripAnsi(result.stderr.toString(\"utf8\"));\n\n      [\n        `Server-only module referenced by client`,\n\n        `    '${specifier}' imported by 'app/reexport-server-only.ts'`,\n\n        \"  See https://reactrouter.com/explanation/code-splitting#removal-of-server-code\",\n      ].forEach(expect(stderr).toMatch);\n    });\n  }\n});\n\ntest.describe(\"Vite / server-only escape hatch\", async () => {\n  let files: Files = async ({ port }) => ({\n    \"vite.config.ts\": dedent`\n      import { reactRouter } from \"@react-router/dev/vite\";\n      import { envOnlyMacros } from \"vite-env-only\";\n      import tsconfigPaths from \"vite-tsconfig-paths\";\n\n      export default {\n        ${await viteConfig.server({ port })}\n        plugins: [reactRouter(), envOnlyMacros(), tsconfigPaths()],\n      }\n    `,\n    \"app/utils.server.ts\": serverOnlyModule,\n    \"app/.server/utils.ts\": serverOnlyModule,\n    \"app/routes/_index.tsx\": `\n      import { serverOnly$ } from \"vite-env-only/macros\";\n\n      import { serverOnly as serverOnlyFile } from \"~/utils.server\";\n      import serverOnlyDir from \"~/.server/utils\";\n\n      export const handle = {\n        escapeHatch: serverOnly$(async () => {\n          return { serverOnlyFile, serverOnlyDir };\n        })\n      }\n\n      export default () => <h1 data-title>This should work</h1>;\n    `,\n  });\n\n  test(\"vite dev\", async ({ page, dev }) => {\n    let { port } = await dev(files);\n\n    await page.goto(`http://localhost:${port}/`, {\n      waitUntil: \"networkidle\",\n    });\n    await expect(page.locator(\"[data-title]\")).toHaveText(\"This should work\");\n    expect(page.errors).toEqual([]);\n  });\n\n  test(\"vite build + react-router-serve\", async ({\n    page,\n    reactRouterServe,\n  }) => {\n    let { port, cwd } = await reactRouterServe(files);\n\n    let lines = grep(path.join(cwd, \"build/client\"), /SERVER_ONLY/);\n    expect(lines).toHaveLength(0);\n\n    await page.goto(`http://localhost:${port}/`, {\n      waitUntil: \"networkidle\",\n    });\n    await expect(page.locator(\"[data-title]\")).toHaveText(\"This should work\");\n    expect(page.errors).toEqual([]);\n  });\n});\n"
  },
  {
    "path": "integration/vite-dotenv-test.ts",
    "content": "import { expect } from \"@playwright/test\";\nimport tsx from \"dedent\";\nimport getPort from \"get-port\";\n\nimport * as Express from \"./helpers/express\";\nimport { test } from \"./helpers/fixtures\";\nimport * as Stream from \"./helpers/stream\";\nimport { viteMajorTemplates, getTemplates } from \"./helpers/templates\";\n\nconst templates = [\n  ...viteMajorTemplates,\n  ...getTemplates([\"rsc-vite-framework\"]),\n];\n\ntest.use({\n  files: {\n    \"app/routes/dotenv.tsx\": tsx`\n      import { useState, useEffect } from \"react\";\n      import { useLoaderData } from \"react-router\";\n\n      export const loader = () => {\n        return {\n          loaderContent: process.env.ENV_VAR_FROM_DOTENV_FILE,\n        }\n      }\n\n      export default function DotenvRoute() {\n        const { loaderContent } = useLoaderData();\n\n        const [clientContent, setClientContent] = useState('');\n        useEffect(() => {\n          try {\n            setClientContent(\"process.env.ENV_VAR_FROM_DOTENV_FILE shouldn't be available on the client, found: \" + process.env.ENV_VAR_FROM_DOTENV_FILE);\n          } catch (err) {\n            setClientContent(\"process.env.ENV_VAR_FROM_DOTENV_FILE not available on the client, which is a good thing\");\n          }\n        }, []);\n\n        return <>\n          <div data-dotenv-route-loader-content>{loaderContent}</div>\n          <div data-dotenv-route-client-content>{clientContent}</div>\n        </>\n      }\n    `,\n  },\n});\n\nconst envs = [\n  { name: \"default\", path: \".env\" },\n  { name: \"custom env dir\", path: \"custom-env-dir/.env\" },\n];\n\ntest.describe(\"Vite .env\", () => {\n  templates.forEach((template) => {\n    test.describe(`template: ${template.displayName}`, () => {\n      const isRsc = template.name.startsWith(\"rsc-\");\n      test.use({ template: template.name });\n      envs.forEach((env) => {\n        test(env.name, async ({ edit, $, page }) => {\n          await edit({\n            \"server.mjs\": isRsc ? Express.rsc() : Express.server(),\n            \".env\": `\n              VITE_ENV_ROUTE=dotenv\n              ENV_VAR_FROM_DOTENV_FILE=Content from ${env.path} file\n            `,\n            \"app/routes.ts\": (contents) => {\n              if (template.name === \"vite-5-template\") return contents;\n              return tsx`\n                import { type RouteConfig, route } from \"@react-router/dev/routes\";\n\n                const routes: RouteConfig = [];\n                if (import.meta.env.VITE_ENV_ROUTE === \"dotenv\") {\n                  routes.push(route(\"dotenv\", \"routes/dotenv.tsx\"));\n                }\n\n                export default routes\n              `;\n            },\n          });\n          await $(\"pnpm build\");\n\n          const port = await getPort();\n          const url = `http://localhost:${port}`;\n\n          const server = $(\"node server.mjs\", {\n            env: {\n              PORT: String(port),\n              HMR_PORT: String(await getPort()),\n            },\n          });\n          await Stream.match(server.stdout, url);\n\n          await page.goto(`${url}/dotenv`, { waitUntil: \"networkidle\" });\n          expect(page.errors).toEqual([]);\n\n          let loaderContent = page.locator(\n            \"[data-dotenv-route-loader-content]\",\n          );\n          await expect(loaderContent).toHaveText(\n            `Content from ${env.path} file`,\n          );\n\n          let clientContent = page.locator(\n            \"[data-dotenv-route-client-content]\",\n          );\n          await expect(clientContent).toHaveText(\n            \"process.env.ENV_VAR_FROM_DOTENV_FILE not available on the client, which is a good thing\",\n          );\n\n          expect(page.errors).toEqual([]);\n        });\n      });\n    });\n  });\n});\n"
  },
  {
    "path": "integration/vite-hmr-hdr-rsc-test.ts",
    "content": "import fs from \"node:fs/promises\";\nimport path from \"node:path\";\nimport { expect } from \"@playwright/test\";\n\nimport type { Files, TemplateName } from \"./helpers/vite.js\";\nimport { test, createEditor, viteConfig } from \"./helpers/vite.js\";\n\nconst templateName = \"rsc-vite-framework\" as const satisfies TemplateName;\n\ntest.describe(\"Vite HMR & HDR (RSC)\", () => {\n  test(\"vite dev\", async ({ page, dev }) => {\n    let files: Files = async ({ port }) => ({\n      \"vite.config.js\": await viteConfig.basic({ port, templateName }),\n      \"app/routes/hmr/route.tsx\": `\n        // imports\n        import { Mounted } from \"./route.client\";\n\n        // loader\n\n        export function ServerComponent() {\n          return (\n            <div id=\"index\">\n              <h2 data-title>Index</h2>\n              <input />\n              <Mounted />\n              <p data-hmr>HMR updated: 0</p>\n              {/* elements */}\n            </div>\n          );\n        }\n      `,\n      \"app/routes/hmr/route.client.tsx\": `\n        \"use client\";\n        \n        import { useState, useEffect } from \"react\";\n\n        export function Mounted() {\n          const [mounted, setMounted] = useState(false);\n          useEffect(() => {\n            setMounted(true);\n          }, []);\n\n          return <p data-mounted>Mounted: {mounted ? \"yes\" : \"no\"}</p>;\n        }\n      `,\n    });\n    let { cwd, port } = await dev(files, templateName);\n    let edit = createEditor(cwd);\n\n    // setup: initial render\n    await page.goto(`http://localhost:${port}/hmr`, {\n      waitUntil: \"networkidle\",\n    });\n    await expect(page.locator(\"#index [data-title]\")).toHaveText(\"Index\");\n\n    // setup: hydration\n    await expect(page.locator(\"#index [data-mounted]\")).toHaveText(\n      \"Mounted: yes\",\n    );\n\n    // setup: browser state\n    let hmrStatus = page.locator(\"#index [data-hmr]\");\n\n    await expect(hmrStatus).toHaveText(\"HMR updated: 0\");\n    let input = page.locator(\"#index input\");\n    await expect(input).toBeVisible();\n    await input.type(\"stateful\");\n    expect(page.errors).toEqual([]);\n\n    // route: HMR\n    await edit(\"app/routes/hmr/route.tsx\", (contents) =>\n      contents.replace(\"HMR updated: 0\", \"HMR updated: 1\"),\n    );\n    await page.waitForLoadState(\"networkidle\");\n\n    await expect(hmrStatus).toHaveText(\"HMR updated: 1\");\n    await expect(input).toHaveValue(\"stateful\");\n    expect(page.errors).toEqual([]);\n\n    // route: add loader\n    await edit(\"app/routes/hmr/route.tsx\", (contents) =>\n      contents\n        .replace(\n          \"// loader\",\n          `// loader\\nexport const loader = () => ({ message: \"HDR updated: 0\" });`,\n        )\n        .replace(\n          \"export function ServerComponent() {\",\n          `export function ServerComponent({ loaderData }: { loaderData: { message: string } }) {`,\n        )\n        .replace(\n          \"{/* elements */}\",\n          `{/* elements */}\\n<p data-hdr>{loaderData.message}</p>`,\n        ),\n    );\n    await page.waitForLoadState(\"networkidle\");\n    let hdrStatus = page.locator(\"#index [data-hdr]\");\n    await expect(hdrStatus).toHaveText(\"HDR updated: 0\");\n    await expect(input).toHaveValue(\"stateful\");\n    expect(page.errors).toEqual([]);\n\n    // route: HDR\n    await edit(\"app/routes/hmr/route.tsx\", (contents) =>\n      contents.replace(\"HDR updated: 0\", \"HDR updated: 1\"),\n    );\n    await page.waitForLoadState(\"networkidle\");\n    await expect(hdrStatus).toHaveText(\"HDR updated: 1\");\n    await expect(input).toHaveValue(\"stateful\");\n    expect(page.errors).toEqual([]);\n\n    // route: HMR + HDR\n    await edit(\"app/routes/hmr/route.tsx\", (contents) =>\n      contents\n        .replace(\"HMR updated: 1\", \"HMR updated: 2\")\n        .replace(\"HDR updated: 1\", \"HDR updated: 2\"),\n    );\n    await page.waitForLoadState(\"networkidle\");\n    await expect(hmrStatus).toHaveText(\"HMR updated: 2\");\n    await expect(hdrStatus).toHaveText(\"HDR updated: 2\");\n    await expect(input).toHaveValue(\"stateful\");\n    expect(page.errors).toEqual([]);\n\n    // create new non-route imported server component\n    await fs.writeFile(\n      path.join(cwd, \"app/imported-server-component.tsx\"),\n      `\n        import { ImportedServerComponentClientMounted } from \"./imported-server-component-client\";\n\n        export function ImportedServerComponent() {\n          return (\n            <div>\n              <p data-imported-server-component>Imported Server Component HMR: 0</p>\n              <ImportedServerComponentClientMounted />\n            </div>\n          );\n        }\n     `,\n      \"utf8\",\n    );\n    await fs.writeFile(\n      path.join(cwd, \"app/imported-server-component-client.tsx\"),\n      `\n        \"use client\";\n        \n        import { useState, useEffect } from \"react\";\n\n        export function ImportedServerComponentClientMounted() {\n          const [mounted, setMounted] = useState(false);\n          useEffect(() => {\n            setMounted(true);\n          }, []);\n\n          return (\n            <p data-imported-server-component-client-mounted>\n              Imported Server Component Client Mounted: {mounted ? \"yes\" : \"no\"}\n            </p>\n          );\n        }\n      `,\n      \"utf8\",\n    );\n    await edit(\"app/routes/hmr/route.tsx\", (contents) =>\n      contents\n        .replace(\n          \"// imports\",\n          `// imports\\nimport { ImportedServerComponent } from \"../../imported-server-component\";`,\n        )\n        .replace(\n          \"{/* elements */}\",\n          \"{/* elements */}\\n<ImportedServerComponent />\",\n        ),\n    );\n    await page.waitForLoadState(\"networkidle\");\n    let serverComponent = page.locator(\n      \"#index [data-imported-server-component]\",\n    );\n    let importedServerComponentClientMounted = page.locator(\n      \"#index [data-imported-server-component-client-mounted]\",\n    );\n    await expect(serverComponent).toBeVisible();\n    await expect(serverComponent).toHaveText(\n      \"Imported Server Component HMR: 0\",\n    );\n    await expect(importedServerComponentClientMounted).toBeVisible();\n    await expect(importedServerComponentClientMounted).toHaveText(\n      \"Imported Server Component Client Mounted: yes\",\n    );\n    await expect(input).toHaveValue(\"stateful\");\n    expect(page.errors).toEqual([]);\n\n    // non-route imported server component: HMR\n    await edit(\"app/imported-server-component.tsx\", (contents) =>\n      contents.replace(\n        \"Imported Server Component HMR: 0\",\n        \"Imported Server Component HMR: 1\",\n      ),\n    );\n    await page.waitForLoadState(\"networkidle\");\n    await expect(serverComponent).toHaveText(\n      \"Imported Server Component HMR: 1\",\n    );\n    await expect(input).toHaveValue(\"stateful\");\n    expect(page.errors).toEqual([]);\n\n    // create new non-route imported client component\n    await fs.writeFile(\n      path.join(cwd, \"app/imported-client-component.tsx\"),\n      `\n        \"use client\";\n        \n        import { useState } from \"react\";\n\n        export function ImportedClientComponent() {\n          const [count, setCount] = useState(0);\n          return (\n            <div>\n              <p data-imported-client-component>Imported Client Component HMR: 0</p>\n              <button data-imported-client-component-button onClick={() => setCount(count + 1)}>\n                Count: {count}\n              </button>\n            </div>\n          );\n        }\n      `,\n      \"utf8\",\n    );\n    await edit(\"app/routes/hmr/route.tsx\", (contents) =>\n      contents\n        .replace(\n          \"// imports\",\n          `// imports\\nimport { ImportedClientComponent } from \"../../imported-client-component\";`,\n        )\n        .replace(\n          \"{/* elements */}\",\n          \"{/* elements */}\\n<ImportedClientComponent />\",\n        ),\n    );\n    await page.waitForLoadState(\"networkidle\");\n    let clientComponent = page.locator(\n      \"#index [data-imported-client-component]\",\n    );\n    let clientButton = page.locator(\n      \"#index [data-imported-client-component-button]\",\n    );\n    await expect(clientComponent).toBeVisible();\n    await expect(clientComponent).toHaveText(\n      \"Imported Client Component HMR: 0\",\n    );\n    await expect(clientButton).toHaveText(\"Count: 0\");\n    await expect(input).toHaveValue(\"stateful\");\n    expect(page.errors).toEqual([]);\n\n    // non-route imported client component: HMR\n    await edit(\"app/imported-client-component.tsx\", (contents) =>\n      contents.replace(\n        \"Imported Client Component HMR: 0\",\n        \"Imported Client Component HMR: 1\",\n      ),\n    );\n    await page.waitForLoadState(\"networkidle\");\n    await expect(clientComponent).toHaveText(\n      \"Imported Client Component HMR: 1\",\n    );\n    await expect(clientButton).toHaveText(\"Count: 0\");\n    await expect(input).toHaveValue(\"stateful\");\n    expect(page.errors).toEqual([]);\n\n    // non-route imported client component: state preservation\n    await clientButton.click();\n    await expect(clientButton).toHaveText(\"Count: 1\");\n    await edit(\"app/imported-client-component.tsx\", (contents) =>\n      contents.replace(\n        \"Imported Client Component HMR: 1\",\n        \"Imported Client Component HMR: 2\",\n      ),\n    );\n    await page.waitForLoadState(\"networkidle\");\n    await expect(clientComponent).toHaveText(\n      \"Imported Client Component HMR: 2\",\n    );\n    await expect(clientButton).toHaveText(\"Count: 1\");\n    await expect(input).toHaveValue(\"stateful\");\n    expect(page.errors).toEqual([]);\n\n    // create new non-route server module\n    await fs.writeFile(\n      path.join(cwd, \"app/indirect-hdr-dep.ts\"),\n      `export const indirect = \"indirect 0\"`,\n      \"utf8\",\n    );\n    await fs.writeFile(\n      path.join(cwd, \"app/direct-hdr-dep.ts\"),\n      `\n        import { indirect } from \"./indirect-hdr-dep\"\n        export const direct = \"direct 0 & \" + indirect\n      `,\n      \"utf8\",\n    );\n    await edit(\"app/routes/hmr/route.tsx\", (contents) =>\n      contents\n        .replace(\n          \"// imports\",\n          `// imports\\nimport { direct } from \"../../direct-hdr-dep\"`,\n        )\n        .replace(\n          `{ message: \"HDR updated: 2\" }`,\n          `{ message: \"HDR updated: \" + direct }`,\n        )\n        .replace(`HDR updated: 2`, `HDR updated: direct 0 & indirect 0`),\n    );\n    await page.waitForLoadState(\"networkidle\");\n    await expect(hdrStatus).toHaveText(\"HDR updated: direct 0 & indirect 0\");\n    await expect(input).toHaveValue(\"stateful\");\n    expect(page.errors).toEqual([]);\n\n    // non-route: HDR for direct dependency\n    await edit(\"app/direct-hdr-dep.ts\", (contents) =>\n      contents.replace(\"direct 0 &\", \"direct 1 &\"),\n    );\n    await page.waitForLoadState(\"networkidle\");\n    await expect(hdrStatus).toHaveText(\"HDR updated: direct 1 & indirect 0\");\n    await expect(input).toHaveValue(\"stateful\");\n    expect(page.errors).toEqual([]);\n\n    // non-route: HDR for indirect dependency\n    await edit(\"app/indirect-hdr-dep.ts\", (contents) =>\n      contents.replace(\"indirect 0\", \"indirect 1\"),\n    );\n    await page.waitForLoadState(\"networkidle\");\n    await expect(hdrStatus).toHaveText(\"HDR updated: direct 1 & indirect 1\");\n    await expect(input).toHaveValue(\"stateful\");\n    expect(page.errors).toEqual([]);\n\n    // everything everywhere all at once\n    await Promise.all([\n      edit(\"app/routes/hmr/route.tsx\", (contents) =>\n        contents\n          .replace(\"HMR updated: 2\", \"HMR updated: 3\")\n          .replace(\"HDR updated: \", \"HDR updated: route & \"),\n      ),\n      edit(\"app/imported-server-component.tsx\", (contents) =>\n        contents.replace(\n          \"Imported Server Component HMR: 1\",\n          \"Imported Server Component HMR: 2\",\n        ),\n      ),\n      edit(\"app/imported-client-component.tsx\", (contents) =>\n        contents.replace(\n          \"Imported Client Component HMR: 2\",\n          \"Imported Client Component HMR: 3\",\n        ),\n      ),\n      edit(\"app/direct-hdr-dep.ts\", (contents) =>\n        contents.replace(\"direct 1 &\", \"direct 2 &\"),\n      ),\n      edit(\"app/indirect-hdr-dep.ts\", (contents) =>\n        contents.replace(\"indirect 1\", \"indirect 2\"),\n      ),\n    ]);\n    await page.waitForLoadState(\"networkidle\");\n    await expect(hmrStatus).toHaveText(\"HMR updated: 3\");\n    await expect(serverComponent).toHaveText(\n      \"Imported Server Component HMR: 2\",\n    );\n    await expect(clientComponent).toHaveText(\n      \"Imported Client Component HMR: 3\",\n    );\n    await expect(clientButton).toHaveText(\"Count: 1\");\n    await expect(hdrStatus).toHaveText(\n      \"HDR updated: route & direct 2 & indirect 2\",\n    );\n    await expect(input).toHaveValue(\"stateful\");\n    expect(page.errors).toEqual([]);\n\n    // switch from server-first to client route\n    await edit(\"app/routes/hmr/route.tsx\", (contents) =>\n      contents\n        .replace(\n          \"export function ServerComponent\",\n          \"export default function ClientComponent\",\n        )\n        .replace(\"HMR updated: 3\", \"Client Route HMR: 0\"),\n    );\n    await page.waitForLoadState(\"networkidle\");\n    await expect(hmrStatus).toHaveText(\"Client Route HMR: 0\");\n    // adding/removing client component exports causes an HMR invalidation and a\n    // page reload. some browsers maintain input state, so we forcibly clear\n    await input.clear();\n    await input.type(\"client stateful\");\n    expect(page.errors).toEqual([]);\n    await edit(\"app/routes/hmr/route.tsx\", (contents) =>\n      contents.replace(\"Client Route HMR: 0\", \"Client Route HMR: 1\"),\n    );\n    await page.waitForLoadState(\"networkidle\");\n    await expect(hmrStatus).toHaveText(\"Client Route HMR: 1\");\n    await expect(input).toHaveValue(\"client stateful\");\n    expect(page.errors).toEqual([]);\n\n    // switch from client route back to server-first route\n    await edit(\"app/routes/hmr/route.tsx\", (contents) =>\n      contents\n        .replace(\n          \"export default function ClientComponent\",\n          \"export function ServerComponent\",\n        )\n        .replace(\"Client Route HMR: 1\", \"Server Route HMR: 0\"),\n    );\n    await page.waitForLoadState(\"networkidle\");\n    await expect(hmrStatus).toHaveText(\"Server Route HMR: 0\");\n    // adding/removing client component exports causes an HMR invalidation and a\n    // page reload. some browsers maintain input state, so we forcibly clear\n    await input.clear();\n    await input.type(\"server stateful\");\n    expect(page.errors).toEqual([]);\n    await edit(\"app/routes/hmr/route.tsx\", (contents) =>\n      contents.replace(\"Server Route HMR: 0\", \"Server Route HMR: 1\"),\n    );\n    await page.waitForLoadState(\"networkidle\");\n    await expect(hmrStatus).toHaveText(\"Server Route HMR: 1\");\n    await expect(input).toHaveValue(\"server stateful\");\n    expect(page.errors).toEqual([]);\n  });\n});\n"
  },
  {
    "path": "integration/vite-hmr-hdr-test.ts",
    "content": "import type { Page } from \"@playwright/test\";\nimport { expect } from \"@playwright/test\";\nimport getPort from \"get-port\";\nimport dedent from \"dedent\";\n\nimport * as Express from \"./helpers/express\";\nimport { test } from \"./helpers/fixtures\";\nimport * as Stream from \"./helpers/stream\";\nimport { viteMajorTemplates, getTemplates } from \"./helpers/templates\";\n\nconst tsx = dedent;\nconst mdx = dedent;\n\nconst templates = [\n  ...viteMajorTemplates,\n  ...getTemplates([\"rsc-vite-framework\"]),\n];\n\ntemplates.forEach((template) => {\n  const isRsc = template.name.startsWith(\"rsc-\");\n\n  test.describe(`${template.displayName} - HMR & HDR`, () => {\n    test.use({\n      template: template.name,\n      files: {\n        \"app/routes/_index.tsx\": tsx`\n          // imports\n          import { useState, useEffect } from \"react\";\n\n          export const meta = () => [{ title: \"HMR updated title: 0\" }]\n\n          // loader\n\n          export default function IndexRoute() {\n            // hooks\n            const [mounted, setMounted] = useState(false);\n            useEffect(() => {\n              setMounted(true);\n            }, []);\n\n            return (\n              <div id=\"index\">\n                <h2 data-title>Index</h2>\n                <input />\n                <p data-mounted>Mounted: {mounted ? \"yes\" : \"no\"}</p>\n                <p data-hmr>HMR updated: 0</p>\n                {/* elements */}\n              </div>\n            );\n          }\n        `,\n      },\n    });\n\n    test(\"vite dev\", async ({ page, edit, $ }) => {\n      const port = await getPort();\n      const url = `http://localhost:${port}`;\n\n      const dev = $(`pnpm dev --port ${port}`);\n      await Stream.match(dev.stdout, url);\n\n      await workflow({ isRsc, page, edit, url });\n    });\n\n    test(\"express\", async ({ page, edit, $ }) => {\n      await edit({\n        \"server.mjs\": isRsc ? Express.rsc() : Express.server(),\n      });\n\n      await $(\"pnpm build\");\n\n      const port = await getPort();\n      const url = `http://localhost:${port}`;\n\n      const server = $(\"node server.mjs\", {\n        env: {\n          PORT: String(port),\n          HMR_PORT: String(await getPort()),\n        },\n      });\n      await Stream.match(server.stdout, url);\n\n      await workflow({ isRsc, page, edit, url });\n    });\n\n    test(\"mdx\", async ({ page, edit, $ }) => {\n      test.skip(template.name.includes(\"rsc\"), \"RSC is not supported\");\n\n      await edit({\n        \"vite.config.ts\": tsx`\n          import { defineConfig } from \"vite\";\n          import { reactRouter } from \"@react-router/dev/vite\";\n          import mdx from \"@mdx-js/rollup\";\n\n          export default defineConfig({\n            plugins: [\n              mdx(),\n              reactRouter(),\n            ],\n          });\n        `,\n        \"app/component.tsx\": tsx`\n          import {useState} from \"react\";\n\n          export const Counter = () => {\n            const [count, setCount] = useState(0);\n            return <button onClick={() => setCount(count => count + 1)}>Count: {count}</button>\n          }\n        `,\n        \"app/routes/mdx.mdx\": mdx`\n          import { Counter } from \"../component\";\n\n          # MDX Title (HMR: 0)\n\n          <Counter />\n        `,\n      });\n\n      const port = await getPort();\n      const url = `http://localhost:${port}`;\n\n      const dev = $(`pnpm dev --port ${port}`);\n      await Stream.match(dev.stdout, url);\n\n      await page.goto(url + \"/mdx\", { waitUntil: \"networkidle\" });\n\n      await expect(page.locator(\"h1\")).toHaveText(\"MDX Title (HMR: 0)\");\n      let button = page.locator(\"button\");\n      await expect(button).toHaveText(\"Count: 0\");\n      await button.click();\n      await expect(button).toHaveText(\"Count: 1\");\n\n      await edit({\n        \"app/routes/mdx.mdx\": (contents) =>\n          contents.replace(\"(HMR: 0)\", \"(HMR: 1)\"),\n      });\n      await page.waitForLoadState(\"networkidle\");\n\n      await expect(page.locator(\"h1\")).toHaveText(\"MDX Title (HMR: 1)\");\n      await expect(page.locator(\"button\")).toHaveText(\"Count: 1\");\n\n      expect(page.errors).toEqual([]);\n    });\n  });\n});\n\nasync function workflow({\n  isRsc,\n  page,\n  edit,\n  url,\n}: {\n  isRsc: boolean;\n  page: Page;\n  edit: (\n    edits: Record<string, string | ((contents: string) => string)>,\n  ) => Promise<void>;\n  url: string;\n}) {\n  // setup: initial render\n  await page.goto(url, { waitUntil: \"networkidle\" });\n  await expect(page.locator(\"#index [data-title]\")).toHaveText(\"Index\");\n\n  // setup: hydration\n  await expect(page.locator(\"#index [data-mounted]\")).toHaveText(\n    \"Mounted: yes\",\n  );\n\n  // setup: browser state\n  let hmrStatus = page.locator(\"#index [data-hmr]\");\n\n  await expect(page).toHaveTitle(\"HMR updated title: 0\");\n  await expect(hmrStatus).toHaveText(\"HMR updated: 0\");\n  let input = page.locator(\"#index input\");\n  await expect(input).toBeVisible();\n  await input.fill(\"stateful\");\n  expect(page.errors).toEqual([]);\n\n  // route: HMR\n  await edit({\n    \"app/routes/_index.tsx\": (contents) =>\n      contents\n        .replace(\"HMR updated title: 0\", \"HMR updated title: 1\")\n        .replace(\"HMR updated: 0\", \"HMR updated: 1\"),\n  });\n  await page.waitForLoadState(\"networkidle\");\n\n  await expect(page).toHaveTitle(\"HMR updated title: 1\");\n  await expect(hmrStatus).toHaveText(\"HMR updated: 1\");\n  await expect(input).toHaveValue(\"stateful\");\n  expect(page.errors).toEqual([]);\n\n  // route: add loader\n  await edit({\n    \"app/routes/_index.tsx\": (contents) =>\n      contents\n        .replace(\n          \"// imports\",\n          `// imports\\nimport { useLoaderData } from \"react-router\"`,\n        )\n        .replace(\n          \"// loader\",\n          `// loader\\nexport const loader = () => ({ message: \"HDR updated: 0\" });`,\n        )\n        .replace(\n          \"// hooks\",\n          \"// hooks\\nconst { message } = useLoaderData<typeof loader>();\",\n        )\n        .replace(\n          \"{/* elements */}\",\n          `{/* elements */}\\n<p data-hdr>{message}</p>`,\n        ),\n  });\n  await page.waitForLoadState(\"networkidle\");\n  let hdrStatus = page.locator(\"#index [data-hdr]\");\n  await expect(hdrStatus).toHaveText(\"HDR updated: 0\");\n\n  // React Fast Refresh cannot preserve state for a component when hooks are added or removed\n  await expect(input).toHaveValue(\"\");\n  await input.fill(\"stateful\");\n  expect(page.errors.length).toBeGreaterThan(0);\n  expect(\n    // When adding a loader, a harmless error is logged to the browser console.\n    // HMR works as intended, so this seems like a React Fast Refresh bug caused by off-screen rendering with old server data or something like that 🤷\n    page.errors.filter(\n      (error) =>\n        !error.message.includes(\n          \"There was an error during concurrent rendering but React was able to recover by instead synchronously rendering the entire root.\",\n        ),\n    ),\n  ).toEqual([]);\n  page.errors = [];\n\n  // route: HDR\n  await edit({\n    \"app/routes/_index.tsx\": (contents) =>\n      contents.replace(\"HDR updated: 0\", \"HDR updated: 1\"),\n  });\n  await page.waitForLoadState(\"networkidle\");\n  await expect(hdrStatus).toHaveText(\"HDR updated: 1\");\n  await expect(input).toHaveValue(\"stateful\");\n\n  // route: HMR + HDR\n  await edit({\n    \"app/routes/_index.tsx\": (contents) =>\n      contents\n        .replace(\"HMR updated: 1\", \"HMR updated: 2\")\n        .replace(\"HDR updated: 1\", \"HDR updated: 2\"),\n  });\n  await page.waitForLoadState(\"networkidle\");\n  await expect(hmrStatus).toHaveText(\"HMR updated: 2\");\n  await expect(hdrStatus).toHaveText(\"HDR updated: 2\");\n  await expect(input).toHaveValue(\"stateful\");\n  expect(page.errors).toEqual([]);\n\n  // create new non-route component module\n  await edit({\n    \"app/component.tsx\": tsx`\n      export function MyComponent() {\n        return <p data-component>Component HMR: 0</p>;\n      }\n    `,\n    \"app/routes/_index.tsx\": (contents) =>\n      contents\n        .replace(\n          \"// imports\",\n          `// imports\\nimport { MyComponent } from \"../component\";`,\n        )\n        .replace(\"{/* elements */}\", \"{/* elements */}\\n<MyComponent />\"),\n  });\n  await page.waitForLoadState(\"networkidle\");\n  let component = page.locator(\"#index [data-component]\");\n  await expect(component).toBeVisible();\n  await expect(component).toHaveText(\"Component HMR: 0\");\n  await expect(input).toHaveValue(\"stateful\");\n  expect(page.errors).toEqual([]);\n\n  // non-route: HMR\n  await edit({\n    \"app/component.tsx\": (contents) =>\n      contents.replace(\"Component HMR: 0\", \"Component HMR: 1\"),\n  });\n  await page.waitForLoadState(\"networkidle\");\n  await expect(component).toHaveText(\"Component HMR: 1\");\n  await expect(input).toHaveValue(\"stateful\");\n  expect(page.errors).toEqual([]);\n\n  // create new non-route server module\n  await edit({\n    \"app/indirect-hdr-dep.ts\": tsx`export const indirect = \"indirect 0\"`,\n    \"app/direct-hdr-dep.ts\": tsx`\n      import { indirect } from \"./indirect-hdr-dep\"\n      export const direct = \"direct 0 & \" + indirect\n    `,\n    \"app/routes/_index.tsx\": (contents) =>\n      contents\n        .replace(\n          \"// imports\",\n          `// imports\\nimport { direct } from \"../direct-hdr-dep\"`,\n        )\n        .replace(\n          `{ message: \"HDR updated: 2\" }`,\n          `{ message: \"HDR updated: \" + direct }`,\n        ),\n  });\n  await page.waitForLoadState(\"networkidle\");\n  await expect(hdrStatus).toHaveText(\"HDR updated: direct 0 & indirect 0\");\n  await expect(input).toHaveValue(\"stateful\");\n  expect(page.errors).toEqual([]);\n\n  // non-route: HDR for direct dependency\n  await edit({\n    \"app/direct-hdr-dep.ts\": (contents) =>\n      contents.replace(\"direct 0 &\", \"direct 1 &\"),\n  });\n  await page.waitForLoadState(\"networkidle\");\n  await expect(hdrStatus).toHaveText(\"HDR updated: direct 1 & indirect 0\");\n  await expect(input).toHaveValue(\"stateful\");\n  expect(page.errors).toEqual([]);\n\n  // non-route: HDR for indirect dependency\n  await edit({\n    \"app/indirect-hdr-dep.ts\": (contents) =>\n      contents.replace(\"indirect 0\", \"indirect 1\"),\n  });\n  await page.waitForLoadState(\"networkidle\");\n  await expect(hdrStatus).toHaveText(\"HDR updated: direct 1 & indirect 1\");\n  await expect(input).toHaveValue(\"stateful\");\n  expect(page.errors).toEqual([]);\n\n  // everything everywhere all at once\n  await Promise.all([\n    edit({\n      \"app/routes/_index.tsx\": (contents) =>\n        contents\n          .replace(\"HMR updated: 2\", \"HMR updated: 3\")\n          .replace(\"HDR updated: \", \"HDR updated: route & \"),\n    }),\n    edit({\n      \"app/component.tsx\": (contents) =>\n        contents.replace(\"Component HMR: 1\", \"Component HMR: 2\"),\n    }),\n    edit({\n      \"app/direct-hdr-dep.ts\": (contents) =>\n        contents.replace(\"direct 1 &\", \"direct 2 &\"),\n    }),\n    edit({\n      \"app/indirect-hdr-dep.ts\": (contents) =>\n        contents.replace(\"indirect 1\", \"indirect 2\"),\n    }),\n  ]);\n  await page.waitForLoadState(\"networkidle\");\n  await expect(hmrStatus).toHaveText(\"HMR updated: 3\");\n  await expect(component).toHaveText(\"Component HMR: 2\");\n  await expect(hdrStatus).toHaveText(\n    \"HDR updated: route & direct 2 & indirect 2\",\n  );\n  // TODO: Investigate why this is flaky in CI for RSC Framework Mode\n  if (isRsc) {\n    await expect(input).toHaveValue(\"stateful\");\n  }\n\n  expect(page.errors).toEqual([]);\n}\n"
  },
  {
    "path": "integration/vite-loader-context-test.ts",
    "content": "import { test, expect } from \"@playwright/test\";\nimport getPort from \"get-port\";\n\nimport {\n  createProject,\n  customDev,\n  EXPRESS_SERVER,\n  viteConfig,\n} from \"./helpers/vite.js\";\n\nlet port: number;\nlet cwd: string;\nlet stop: () => void;\n\ntest.beforeAll(async () => {\n  port = await getPort();\n  cwd = await createProject({\n    \"vite.config.js\": await viteConfig.basic({ port }),\n    \"server.mjs\": EXPRESS_SERVER({ port, loadContext: { value: \"value\" } }),\n    \"app/routes/_index.tsx\": String.raw`\n      import { useLoaderData } from \"react-router\";\n\n      export const loader = ({ context }) => {\n        return { context }\n      }\n\n      export default function IndexRoute() {\n        let { context } = useLoaderData<typeof loader>();\n        return (\n          <div id=\"index\">\n            <p data-context>Context: {context.value}</p>\n          </div>\n        );\n      }\n    `,\n  });\n  stop = await customDev({ cwd, port });\n});\ntest.afterAll(() => stop());\n\ntest(\"Vite / Load context / express\", async ({ page }) => {\n  let pageErrors: Error[] = [];\n  page.on(\"pageerror\", (error) => pageErrors.push(error));\n\n  await page.goto(`http://localhost:${port}/`, {\n    waitUntil: \"networkidle\",\n  });\n  await expect(page.locator(\"#index [data-context]\")).toHaveText(\n    \"Context: value\",\n  );\n  expect(pageErrors).toEqual([]);\n});\n"
  },
  {
    "path": "integration/vite-manifests-test.ts",
    "content": "import fs from \"node:fs\";\nimport path from \"node:path\";\nimport { test, expect } from \"@playwright/test\";\nimport getPort from \"get-port\";\nimport dedent from \"dedent\";\n\nimport { createProject, build, viteConfig } from \"./helpers/vite.js\";\n\nconst js = String.raw;\n\nfunction createRoute(path: string) {\n  return {\n    [`app/routes/${path}`]: js`\n      export default function Route() {\n        return <p>Path: ${path}</p>;\n      }\n    `,\n  };\n}\n\nconst TEST_ROUTES = [\n  \"_index.tsx\",\n  \"parent-route.tsx\",\n  \"parent-route.child-route.tsx\",\n];\n\nconst files = {\n  \"app/root.tsx\": js`\n    import { Links, Meta, Outlet, Scripts } from \"react-router\";\n\n    export default function Root() {\n      return (\n        <html lang=\"en\">\n          <head>\n            <Meta />\n            <Links />\n          </head>\n          <body>\n            <Outlet />\n            <Scripts />\n          </body>\n        </html>\n      );\n    }\n  `,\n  ...Object.assign({}, ...TEST_ROUTES.map(createRoute)),\n};\n\ntest.describe(() => {\n  let cwd: string;\n\n  test.beforeAll(async () => {\n    cwd = await createProject({\n      \"react-router.config.ts\": dedent(js`\n        export default {\n          buildEnd: async ({ buildManifest }) => {\n            let fs = await import(\"node:fs\");\n            await fs.promises.writeFile(\n              \"build/test-manifest.json\",\n              JSON.stringify(buildManifest, null, 2),\n              \"utf-8\",\n            );\n          },\n        }\n      `),\n      \"vite.config.ts\": dedent(js`\n        import { reactRouter } from \"@react-router/dev/vite\";\n\n        export default {\n          build: { manifest: true },\n          plugins: [reactRouter()],\n        }\n      `),\n      ...files,\n    });\n\n    build({ cwd });\n  });\n\n  test(\"Vite / manifests enabled / Vite manifests\", () => {\n    let viteManifestFilesClient = fs.readdirSync(\n      path.join(cwd, \"build\", \"client\", \".vite\"),\n    );\n    expect(viteManifestFilesClient).toEqual([\"manifest.json\"]);\n\n    let viteManifestFilesServer = fs.readdirSync(\n      path.join(cwd, \"build\", \"server\", \".vite\"),\n    );\n    expect(viteManifestFilesServer).toEqual([\"manifest.json\"]);\n  });\n\n  test(\"Vite / manifests enabled / React Router build manifest\", async () => {\n    let manifestPath = path.join(cwd, \"build\", \"test-manifest.json\");\n    expect(JSON.parse(fs.readFileSync(manifestPath, \"utf8\"))).toEqual({\n      routes: {\n        root: {\n          file: \"root.tsx\",\n          id: \"root\",\n          path: \"\",\n        },\n        \"routes/_index\": {\n          file: \"routes/_index.tsx\",\n          id: \"routes/_index\",\n          index: true,\n          parentId: \"root\",\n        },\n        \"routes/parent-route\": {\n          file: \"routes/parent-route.tsx\",\n          id: \"routes/parent-route\",\n          parentId: \"root\",\n          path: \"parent-route\",\n        },\n        \"routes/parent-route.child-route\": {\n          file: \"routes/parent-route.child-route.tsx\",\n          id: \"routes/parent-route.child-route\",\n          parentId: \"routes/parent-route\",\n          path: \"child-route\",\n        },\n      },\n    });\n  });\n});\n\ntest.describe(() => {\n  let cwd: string;\n\n  test.beforeAll(async () => {\n    cwd = await createProject({\n      \"vite.config.ts\": await viteConfig.basic({ port: await getPort() }),\n      ...files,\n    });\n\n    build({ cwd });\n  });\n\n  test(\"Vite / manifest disabled / Vite manifests\", () => {\n    let manifestDirClient = path.join(cwd, \"build\", \"client\", \".vite\");\n    expect(fs.existsSync(manifestDirClient)).toBe(false);\n\n    let manifestDirServer = path.join(cwd, \"build\", \"server\", \".vite\");\n    expect(fs.existsSync(manifestDirServer)).toBe(false);\n  });\n});\n"
  },
  {
    "path": "integration/vite-node-env-test.ts",
    "content": "import { test, expect } from \"@playwright/test\";\nimport getPort from \"get-port\";\n\nimport {\n  createProject,\n  dev,\n  build,\n  reactRouterServe,\n  viteConfig,\n} from \"./helpers/vite.js\";\n\nlet files = {\n  \"app/routes/node_env.tsx\": String.raw`\n    export default function NodeEnvRoute() {\n      return <div data-node-env>{process.env.NODE_ENV}</div>\n    }\n  `,\n};\n\ntest.describe(async () => {\n  let port: number;\n  let cwd: string;\n  let stop: () => void;\n\n  test.beforeAll(async () => {\n    port = await getPort();\n    cwd = await createProject({\n      \"vite.config.js\": await viteConfig.basic({ port: port }),\n      ...files,\n    });\n  });\n\n  test.describe(() => {\n    test.beforeAll(async () => {\n      stop = await dev({ cwd, port });\n    });\n    test.afterAll(() => stop());\n\n    test(\"Vite / NODE_ENV / dev\", async ({ page }) => {\n      let pageErrors: unknown[] = [];\n      page.on(\"pageerror\", (error) => pageErrors.push(error));\n\n      await page.goto(`http://localhost:${port}/node_env`, {\n        waitUntil: \"networkidle\",\n      });\n      expect(pageErrors).toEqual([]);\n\n      let nodeEnvContent = page.locator(\"[data-node-env]\");\n      await expect(nodeEnvContent).toHaveText(\"development\");\n\n      expect(pageErrors).toEqual([]);\n    });\n  });\n\n  test.describe(() => {\n    let buildPort: number;\n    test.beforeAll(async () => {\n      build({ cwd });\n      buildPort = await getPort();\n      stop = await reactRouterServe({ cwd, port: buildPort });\n    });\n    test.afterAll(() => stop());\n\n    test(\"Vite / NODE_ENV / build\", async ({ page }) => {\n      let pageErrors: unknown[] = [];\n      page.on(\"pageerror\", (error) => pageErrors.push(error));\n\n      await page.goto(`http://localhost:${buildPort}/node_env`, {\n        waitUntil: \"networkidle\",\n      });\n      expect(pageErrors).toEqual([]);\n\n      let nodeEnvContent = page.locator(\"[data-node-env]\");\n      await expect(nodeEnvContent).toHaveText(\"production\");\n\n      expect(pageErrors).toEqual([]);\n    });\n  });\n});\n"
  },
  {
    "path": "integration/vite-plugin-cloudflare-test.ts",
    "content": "import { expect } from \"@playwright/test\";\nimport dedent from \"dedent\";\n\nimport { type Files, test, viteConfig } from \"./helpers/vite.js\";\n\nconst tsx = dedent;\nconst css = dedent;\n\nfunction defineFiles({\n  reversePlugins = false,\n}: { reversePlugins?: boolean } = {}): Files {\n  const files: Files = async ({ port }) => ({\n    \"vite.config.ts\": tsx`\n    import { defineConfig } from \"vite\";\n    import { cloudflare } from \"@cloudflare/vite-plugin\";\n    import { reactRouter } from \"@react-router/dev/vite\";\n\n    export default defineConfig({\n      ${await viteConfig.server({ port })}\n      plugins: [\n        cloudflare({ viteEnvironment: { name: \"ssr\" } }),\n        reactRouter(),\n      ]${reversePlugins ? \".reverse()\" : \"\"},\n    });\n  `,\n    \"app/routes/env.tsx\": tsx`\n    import type { Route } from \"./+types/env\";\n    export function loader({ context }: Route.LoaderArgs) {\n      return { message: context.cloudflare.env.VALUE_FROM_CLOUDFLARE };\n    }\n    export default function EnvRoute({ loaderData }: Route.RouteComponentProps) {\n      return <div data-loader-message>{loaderData.message}</div>;\n    }\n  `,\n    \"app/routes/css-side-effect/route.tsx\": tsx`\n    import \"./styles.css\";\n    \n    export default function CssSideEffectRoute() {\n      return <div className=\"css-side-effect\" data-css-side-effect>CSS Side Effect</div>;\n    }\n  `,\n    \"app/routes/css-side-effect/styles.css\": css`\n      .css-side-effect {\n        padding: 20px;\n      }\n    `,\n  });\n  return files;\n}\n\ntest.describe(\"vite-plugin-cloudflare\", () => {\n  test(\"handles Cloudflare env\", async ({ dev, page }) => {\n    const files = defineFiles();\n    const { port } = await dev(files, \"vite-plugin-cloudflare-template\");\n\n    await page.goto(`http://localhost:${port}/env`, {\n      waitUntil: \"networkidle\",\n    });\n\n    // Ensure no errors on page load\n    expect(page.errors).toEqual([]);\n\n    await expect(page.locator(\"[data-loader-message]\")).toHaveText(\n      \"Hello from Cloudflare\",\n    );\n  });\n\n  test(\"handles Cloudflare env with plugin order reversed\", async ({\n    dev,\n    page,\n  }) => {\n    const files = defineFiles({ reversePlugins: true });\n    const { port } = await dev(files, \"vite-plugin-cloudflare-template\");\n\n    await page.goto(`http://localhost:${port}/env`, {\n      waitUntil: \"networkidle\",\n    });\n\n    // Ensure no errors on page load\n    expect(page.errors).toEqual([]);\n\n    await expect(page.locator(\"[data-loader-message]\")).toHaveText(\n      \"Hello from Cloudflare\",\n    );\n  });\n\n  test.describe(\"without JavaScript\", () => {\n    test.use({ javaScriptEnabled: false });\n\n    test(\"handles CSS side effects during SSR in dev\", async ({\n      dev,\n      page,\n    }) => {\n      const files = defineFiles();\n      const { port } = await dev(files, \"vite-plugin-cloudflare-template\");\n\n      await page.goto(`http://localhost:${port}/css-side-effect`, {\n        waitUntil: \"networkidle\",\n      });\n\n      await expect(page.locator(\"[data-css-side-effect]\")).toHaveCSS(\n        \"padding\",\n        \"20px\",\n      );\n    });\n  });\n\n  test(\"handles CSS side effects during SSR in dev with plugin order reversed\", async ({\n    dev,\n    page,\n  }) => {\n    const files = defineFiles({ reversePlugins: true });\n    const { port } = await dev(files, \"vite-plugin-cloudflare-template\");\n\n    await page.goto(`http://localhost:${port}/css-side-effect`, {\n      waitUntil: \"networkidle\",\n    });\n\n    await expect(page.locator(\"[data-css-side-effect]\")).toHaveCSS(\n      \"padding\",\n      \"20px\",\n    );\n  });\n});\n"
  },
  {
    "path": "integration/vite-plugin-order-validation-test.ts",
    "content": "import { test, expect } from \"@playwright/test\";\nimport dedent from \"dedent\";\n\nimport { createProject, build, reactRouterConfig } from \"./helpers/vite.js\";\n\ntest.describe(\"Vite plugin order validation\", () => {\n  test(\"Framework Mode with MDX plugin after React Router plugin\", async () => {\n    let cwd = await createProject({\n      \"vite.config.ts\": dedent`\n        import { reactRouter } from \"@react-router/dev/vite\";\n        import mdx from \"@mdx-js/rollup\";\n\n        export default {\n          plugins: [\n            reactRouter(),\n            mdx(),\n          ],\n        }\n      `,\n    });\n\n    let buildResult = build({ cwd });\n    expect(buildResult.stderr.toString()).toContain(\n      'Error: The \"@mdx-js/rollup\" plugin should be placed before the React Router plugin in your Vite config',\n    );\n  });\n\n  test(\"RSC Framework Mode with MDX plugin after React Router plugin\", async () => {\n    let cwd = await createProject(\n      {\n        \"vite.config.js\": dedent`\n          import { defineConfig } from \"vite\";\n          import { unstable_reactRouterRSC as reactRouterRSC } from \"@react-router/dev/vite\";\n          import rsc from \"@vitejs/plugin-rsc\";\n          import mdx from \"@mdx-js/rollup\";\n\n          export default defineConfig({\n            plugins: [\n              reactRouterRSC(),\n              rsc(),\n              mdx(),\n            ],\n          });\n        `,\n        \"react-router.config.ts\": reactRouterConfig({\n          future: { v8_viteEnvironmentApi: true },\n        }),\n      },\n      \"rsc-vite-framework\",\n    );\n\n    let buildResult = build({ cwd });\n    expect(buildResult.stderr.toString()).toContain(\n      'Error: The \"@mdx-js/rollup\" plugin should be placed before the React Router plugin in your Vite config',\n    );\n  });\n\n  test(\"RSC Framework Mode with @vitejs/plugin-rsc before React Router plugin\", async () => {\n    let cwd = await createProject(\n      {\n        \"vite.config.js\": dedent`\n          import { defineConfig } from \"vite\";\n          import { unstable_reactRouterRSC as reactRouterRSC } from \"@react-router/dev/vite\";\n          import rsc from \"@vitejs/plugin-rsc\";\n          import mdx from \"@mdx-js/rollup\";\n\n          export default defineConfig({\n            plugins: [\n              rsc(),\n              reactRouterRSC(),\n            ],\n          });\n        `,\n        \"react-router.config.ts\": reactRouterConfig({\n          future: { v8_viteEnvironmentApi: true },\n        }),\n      },\n      \"rsc-vite-framework\",\n    );\n\n    let buildResult = build({ cwd });\n    expect(buildResult.stderr.toString()).toContain(\n      'Error: The \"@vitejs/plugin-rsc\" plugin should be placed after the React Router RSC plugin in your Vite config',\n    );\n  });\n});\n"
  },
  {
    "path": "integration/vite-prerender-test.ts",
    "content": "import fs from \"node:fs\";\nimport path from \"node:path\";\nimport { PassThrough } from \"node:stream\";\nimport type { Page } from \"@playwright/test\";\nimport { test, expect } from \"@playwright/test\";\n\nimport {\n  createAppFixture,\n  createFixture,\n  js,\n} from \"./helpers/create-fixture.js\";\nimport type { Fixture, AppFixture } from \"./helpers/create-fixture.js\";\nimport { PlaywrightFixture } from \"./helpers/playwright-fixture.js\";\nimport { build, createProject, reactRouterConfig } from \"./helpers/vite.js\";\n\nfor (let previewServerPrerendering of [false, true]) {\n  let files = {\n    \"react-router.config.ts\": reactRouterConfig({\n      prerender: true,\n      future: {\n        unstable_previewServerPrerendering: previewServerPrerendering,\n      },\n    }),\n    \"vite.config.ts\": js`\n    import { defineConfig } from \"vite\";\n    import { reactRouter } from \"@react-router/dev/vite\";\n\n    export default defineConfig({\n      build: { manifest: true },\n      plugins: [\n        reactRouter()\n      ],\n    });\n  `,\n    \"app/root.tsx\": js`\n    import * as React from \"react\";\n    import { Link, Links, Meta, Outlet, Scripts, useRouteError } from \"react-router\";\n\n    export function meta({ data }) {\n      return [{\n        title: \"Root Title\"\n      }];\n    }\n\n    export function Layout({ children }) {\n      const [mounted, setMounted] = React.useState(false);\n      React.useEffect(() => setMounted(true), []);\n      return (\n        <html lang=\"en\">\n          <head>\n            <Meta />\n            <Links />\n          </head>\n          <body>\n            <h1>Root</h1>\n            {!mounted ? <h3>Unmounted</h3> : <h3 data-mounted>Mounted</h3>}\n            <nav>\n            <Link to=\"/\">Home</Link><br/>\n            <Link to=\"/about\">About</Link><br/>\n            <Link to=\"/not-found\">Not Found</Link><br/>\n            <Link to=\"/redirect\">Redirect</Link><br/>\n            </nav>\n            {children}\n            <Scripts />\n          </body>\n        </html>\n      );\n    }\n\n    export default function Root() {\n      return <Outlet />\n    }\n\n    export function ErrorBoundary() {\n      let error = useRouteError();\n      let msg = 'status' in error ?\n        error.status + \" \" + error.statusText :\n        error.message;\n      return <p data-error>{msg}</p>;\n    }\n\n    export function HydrateFallback() {\n      return <p>Loading...</p>;\n    }\n  `,\n    \"app/routes/_index.tsx\": js`\n    import * as React  from \"react\";\n    import { useLoaderData } from \"react-router\";\n\n    export function meta({ data }) {\n      return [{\n        title: \"Index Title: \" + data\n      }];\n    }\n\n    export async function loader() {\n      return \"Index Loader Data\";\n    }\n\n    export default function Component() {\n      let data = useLoaderData();\n\n      return (\n        <>\n          <h2 data-route>Index</h2>\n          <p data-loader-data>{data}</p>\n        </>\n      );\n    }\n  `,\n    \"app/routes/about.tsx\": js`\n    import { useActionData, useLoaderData } from \"react-router\";\n\n    export function meta({ data }) {\n      return [{\n        title: \"About Title: \" + data\n      }];\n    }\n\n    export async function loader() {\n      return \"About Loader Data\";\n    }\n\n    export default function Component() {\n      let data = useLoaderData();\n\n      return (\n        <>\n          <h2 data-route>About</h2>\n          <p data-loader-data>{data}</p>\n        </>\n      );\n    }\n  `,\n  };\n\n  function listAllFiles(_dir: string) {\n    let files: string[] = [];\n\n    function recurse(dir: string) {\n      fs.readdirSync(dir).forEach((file) => {\n        // Join with posix separator for consistency\n        const absolute = dir + \"/\" + file;\n        if (fs.statSync(absolute).isDirectory()) {\n          if (![\".vite\", \"assets\"].includes(file)) {\n            return recurse(absolute);\n          }\n        } else {\n          return files.push(absolute);\n        }\n      });\n    }\n\n    recurse(_dir);\n\n    // Normalize *nix/windows paths\n    return files.map((f) => f.replace(_dir, \"\").replace(/^\\//, \"\"));\n  }\n\n  test.describe(`Prerendering (unstable_previewServerPrerendering: ${JSON.stringify(previewServerPrerendering)})`, () => {\n    let fixture: Fixture;\n    let appFixture: AppFixture;\n\n    test.afterAll(() => {\n      appFixture?.close();\n    });\n\n    test.describe(\"prerendered file behavior (agnostic of ssr flag)\", () => {\n      test(\"Prerenders known static routes when true is specified\", async () => {\n        let buildStdio = new PassThrough();\n        fixture = await createFixture({\n          buildStdio,\n          prerender: true,\n          files: {\n            ...files,\n            \"app/routes/parent.tsx\": js`\n            import { Outlet } from 'react-router'\n            export default function Component() {\n              return <Outlet/>\n            }\n          `,\n            \"app/routes/parent.child.tsx\": js`\n            import { Outlet } from 'react-router'\n            export function loader() {\n              return null;\n            }\n            export default function Component() {\n              return <Outlet/>\n            }\n          `,\n            \"app/routes/$slug.tsx\": js`\n            import { Outlet } from 'react-router'\n            export function loader() {\n              return null;\n            }\n            export default function Component() {\n              return <Outlet/>\n            }\n          `,\n            \"app/routes/$.tsx\": js`\n            import { Outlet } from 'react-router'\n            export function loader() {\n              return null;\n            }\n            export default function Component() {\n              return <Outlet/>\n            }\n          `,\n          },\n        });\n\n        appFixture = await createAppFixture(fixture);\n\n        let clientDir = path.join(fixture.projectDir, \"build\", \"client\");\n        expect(listAllFiles(clientDir).sort()).toEqual([\n          \"_root.data\",\n          \"about.data\",\n          \"about/index.html\",\n          \"favicon.ico\",\n          \"index.html\",\n          \"parent/child.data\",\n          \"parent/child/index.html\",\n          \"parent/index.html\",\n        ]);\n\n        let res = await fixture.requestDocument(\"/\");\n        let html = await res.text();\n        expect(html).toMatch(\"<title>Index Title: Index Loader Data</title>\");\n        expect(html).toMatch(\"<h1>Root</h1>\");\n        expect(html).toMatch('<h2 data-route=\"true\">Index</h2>');\n        expect(html).toMatch(\n          '<p data-loader-data=\"true\">Index Loader Data</p>',\n        );\n\n        res = await fixture.requestDocument(\"/about\");\n        html = await res.text();\n        expect(html).toMatch(\"<title>About Title: About Loader Data</title>\");\n        expect(html).toMatch(\"<h1>Root</h1>\");\n        expect(html).toMatch('<h2 data-route=\"true\">About</h2>');\n        expect(html).toMatch(\n          '<p data-loader-data=\"true\">About Loader Data</p>',\n        );\n      });\n\n      test(\"Prerenders a static array of routes\", async () => {\n        fixture = await createFixture({\n          prerender: true,\n          files: {\n            ...files,\n            \"react-router.config.ts\": js`\n            export default {\n              async prerender() {\n                await new Promise(r => setTimeout(r, 1));\n                return ['/', '/about'];\n              },\n            }\n          `,\n            \"vite.config.ts\": js`\n            import { defineConfig } from \"vite\";\n            import { reactRouter } from \"@react-router/dev/vite\";\n\n            export default defineConfig({\n              build: { manifest: true },\n              plugins: [\n                reactRouter()\n              ],\n            });\n          `,\n          },\n        });\n        appFixture = await createAppFixture(fixture);\n\n        let clientDir = path.join(fixture.projectDir, \"build\", \"client\");\n        expect(listAllFiles(clientDir).sort()).toEqual([\n          \"_root.data\",\n          \"about.data\",\n          \"about/index.html\",\n          \"favicon.ico\",\n          \"index.html\",\n        ]);\n\n        let res = await fixture.requestDocument(\"/\");\n        let html = await res.text();\n        expect(html).toMatch(\"<title>Index Title: Index Loader Data</title>\");\n        expect(html).toMatch(\"<h1>Root</h1>\");\n        expect(html).toMatch('<h2 data-route=\"true\">Index</h2>');\n        expect(html).toMatch(\n          '<p data-loader-data=\"true\">Index Loader Data</p>',\n        );\n\n        res = await fixture.requestDocument(\"/about\");\n        html = await res.text();\n        expect(html).toMatch(\"<title>About Title: About Loader Data</title>\");\n        expect(html).toMatch(\"<h1>Root</h1>\");\n        expect(html).toMatch('<h2 data-route=\"true\">About</h2>');\n        expect(html).toMatch(\n          '<p data-loader-data=\"true\">About Loader Data</p>',\n        );\n      });\n\n      test(\"Prerenders a static array of routes with server bundles\", async () => {\n        fixture = await createFixture({\n          prerender: true,\n          files: {\n            ...files,\n            \"react-router.config.ts\": js`\n            let counter = 1;\n            export default {\n              serverBundles: () => \"server\" + counter++,\n              async prerender() {\n                await new Promise(r => setTimeout(r, 1));\n                return ['/', '/about'];\n              },\n            }\n          `,\n            \"vite.config.ts\": js`\n            import { defineConfig } from \"vite\";\n            import { reactRouter } from \"@react-router/dev/vite\";\n\n            export default defineConfig({\n              build: { manifest: true },\n              plugins: [\n                reactRouter()\n              ],\n            });\n          `,\n          },\n        });\n        appFixture = await createAppFixture(fixture);\n\n        let clientDir = path.join(fixture.projectDir, \"build\", \"client\");\n        expect(listAllFiles(clientDir).sort()).toEqual([\n          \"_root.data\",\n          \"about.data\",\n          \"about/index.html\",\n          \"favicon.ico\",\n          \"index.html\",\n        ]);\n\n        let res = await fixture.requestDocument(\"/\");\n        let html = await res.text();\n        expect(html).toMatch(\"<title>Index Title: Index Loader Data</title>\");\n        expect(html).toMatch(\"<h1>Root</h1>\");\n        expect(html).toMatch('<h2 data-route=\"true\">Index</h2>');\n        expect(html).toMatch(\n          '<p data-loader-data=\"true\">Index Loader Data</p>',\n        );\n\n        res = await fixture.requestDocument(\"/about\");\n        html = await res.text();\n        expect(html).toMatch(\"<title>About Title: About Loader Data</title>\");\n        expect(html).toMatch(\"<h1>Root</h1>\");\n        expect(html).toMatch('<h2 data-route=\"true\">About</h2>');\n        expect(html).toMatch(\n          '<p data-loader-data=\"true\">About Loader Data</p>',\n        );\n      });\n\n      test(\"Prerenders a dynamic array of routes based on the static routes\", async () => {\n        fixture = await createFixture({\n          files: {\n            ...files,\n            \"react-router.config.ts\": js`\n            export default {\n              async prerender({ getStaticPaths }) {\n                return [...getStaticPaths(), \"/a\", \"/b\"];\n              },\n            }\n          `,\n            \"vite.config.ts\": js`\n            import { defineConfig } from \"vite\";\n            import { reactRouter } from \"@react-router/dev/vite\";\n\n            export default defineConfig({\n              build: { manifest: true },\n              plugins: [reactRouter()],\n            });\n          `,\n            \"app/routes/$slug.tsx\": js`\n            export function loader() {\n              return null\n            }\n            export default function component() {\n              return null;\n            }\n          `,\n          },\n        });\n        appFixture = await createAppFixture(fixture);\n\n        let clientDir = path.join(fixture.projectDir, \"build\", \"client\");\n        expect(listAllFiles(clientDir).sort()).toEqual([\n          \"_root.data\",\n          \"a.data\",\n          \"a/index.html\",\n          \"about.data\",\n          \"about/index.html\",\n          \"b.data\",\n          \"b/index.html\",\n          \"favicon.ico\",\n          \"index.html\",\n        ]);\n\n        let res = await fixture.requestDocument(\"/\");\n        let html = await res.text();\n        expect(html).toMatch(\"<title>Index Title: Index Loader Data</title>\");\n        expect(html).toMatch(\"<h1>Root</h1>\");\n        expect(html).toMatch('<h2 data-route=\"true\">Index</h2>');\n        expect(html).toMatch(\n          '<p data-loader-data=\"true\">Index Loader Data</p>',\n        );\n\n        res = await fixture.requestDocument(\"/about\");\n        html = await res.text();\n        expect(html).toMatch(\"<title>About Title: About Loader Data</title>\");\n        expect(html).toMatch(\"<h1>Root</h1>\");\n        expect(html).toMatch('<h2 data-route=\"true\">About</h2>');\n        expect(html).toMatch(\n          '<p data-loader-data=\"true\">About Loader Data</p>',\n        );\n      });\n\n      test(\"Skips action-only resource routes prerender:true\", async () => {\n        let buildStdio = new PassThrough();\n        fixture = await createFixture({\n          buildStdio,\n          files: {\n            \"react-router.config.ts\": reactRouterConfig({\n              prerender: true,\n            }),\n            \"vite.config.ts\": files[\"vite.config.ts\"],\n            \"app/root.tsx\": files[\"app/root.tsx\"],\n            \"app/routes/_index.tsx\": files[\"app/routes/_index.tsx\"],\n            \"app/routes/action.tsx\": js`\n            export function action() {\n              return null\n            }\n          `,\n          },\n        });\n\n        let buildOutput: string;\n        let chunks: Buffer[] = [];\n        buildOutput = await new Promise<string>((resolve, reject) => {\n          buildStdio.on(\"data\", (chunk) => chunks.push(Buffer.from(chunk)));\n          buildStdio.on(\"error\", (err) => reject(err));\n          buildStdio.on(\"end\", () =>\n            resolve(Buffer.concat(chunks).toString(\"utf8\")),\n          );\n        });\n\n        expect(buildOutput).toContain(\n          \"⚠️ Skipping prerendering for resource route without a loader: routes/action\",\n        );\n        // Only logs once\n        expect(buildOutput.match(/routes\\/action/g)?.length).toBe(1);\n      });\n\n      test(\"Pre-renders resource routes with file extensions\", async () => {\n        const base64Png =\n          \"iVBORw0KGgoAAAANSUhEUgAAABwAAAAcCAYAAAByDd+UAAAABGdBTUEAALGPC/xhBQAAACBjSFJNAAB6JgAAgIQAAPoAAACA6AAAdTAAAOpgAAA6mAAAF3CculE8AAAAhGVYSWZNTQAqAAAACAAFARIAAwAAAAEAAQAAARoABQAAAAEAAABKARsABQAAAAEAAABSASgAAwAAAAEAAgAAh2kABAAAAAEAAABaAAAAAAAAAEgAAAABAAAASAAAAAEAA6ABAAMAAAABAAEAAKACAAQAAAABAAAAHKADAAQAAAABAAAAHAAAAACXh5mhAAAACXBIWXMAAAsTAAALEwEAmpwYAAACyGlUWHRYTUw6Y29tLmFkb2JlLnhtcAAAAAAAPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iWE1QIENvcmUgNi4wLjAiPgogICA8cmRmOlJERiB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiPgogICAgICA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIgogICAgICAgICAgICB4bWxuczp0aWZmPSJodHRwOi8vbnMuYWRvYmUuY29tL3RpZmYvMS4wLyIKICAgICAgICAgICAgeG1sbnM6ZXhpZj0iaHR0cDovL25zLmFkb2JlLmNvbS9leGlmLzEuMC8iPgogICAgICAgICA8dGlmZjpZUmVzb2x1dGlvbj43MjwvdGlmZjpZUmVzb2x1dGlvbj4KICAgICAgICAgPHRpZmY6UmVzb2x1dGlvblVuaXQ+MjwvdGlmZjpSZXNvbHV0aW9uVW5pdD4KICAgICAgICAgPHRpZmY6WFJlc29sdXRpb24+NzI8L3RpZmY6WFJlc29sdXRpb24+CiAgICAgICAgIDx0aWZmOk9yaWVudGF0aW9uPjE8L3RpZmY6T3JpZW50YXRpb24+CiAgICAgICAgIDxleGlmOlBpeGVsWERpbWVuc2lvbj41NjwvZXhpZjpQaXhlbFhEaW1lbnNpb24+CiAgICAgICAgIDxleGlmOkNvbG9yU3BhY2U+MTwvZXhpZjpDb2xvclNwYWNlPgogICAgICAgICA8ZXhpZjpQaXhlbFlEaW1lbnNpb24+NTY8L2V4aWY6UGl4ZWxZRGltZW5zaW9uPgogICAgICA8L3JkZjpEZXNjcmlwdGlvbj4KICAgPC9yZGY6UkRGPgo8L3g6eG1wbWV0YT4KJNwP9wAABj1JREFUSA2FVnlQ1FUc/+yyC7ss96UCAgmYmjIcWjOWjVpoRU46OTbkMTY6muNM1jgjVo6jKZI4palDKVnpZP/lgeSoeEEeeRY2HsSlyNrKobL3we72/T549HNd6zuzfH/vfe/zobJYLH48Bfx+P1QqlaAqvwPZmaYEKSPvpCxjjTxIosRSiaQrlUga8/K9ksZ3T6Mzn5oZGJRMfA5UwncM/+UA05T0QB1M0/SpedKAFJR0iYMpCbxjXmmck81FUavVIghNMGYWeNq90hHJ4/P5WAQhISHCkNfrpW81QkNDCWvAdKfLJQwPRCgkAv4olStJ0gDT2YheryOyCo96ehATHQ2vzw+H0wGjyYg2oxH19deg0miwYHbxvylVKpTfMgJ5Zs81JKiln0arhdvjgdVqQ2tbG46dqoU+3IDMtFRcuXoV53+vR3XVccBmEuK1Z89BFxYGldls9rPiYNHIO46IaxCuD0ePxYKbjY3QUmTt5P2VP+pRunoVQkc/j0+LZyAiwoDIyGgkJiXB09uLWdOnYXVpGUo+XAavtxcqnkNWzBAYEd/zT6/Xw+5w4MxvF/Dlzm8xbvRoFBW+gkFJibCYLUhITEBSYhK8xOt0efCQ7pxuD6r2/YxVO3ag8WQNBsXHUS1prqXBQGMcFadOTZFwFBcuX4HP5cS0N4uQnZ39mHNOtxvGeyYY/zahs7MLZk5zazPWrfoYr898B0MGD8L82e+iICcneEq5VhxVj9mMfQerMOa5URhXUCDSyk3CYLVaYbrfgbvGewJb7XY4nU7xc1A2QElrv9uGyq+3Y/2mL/DB+4t4VvoMsgIZIRszGAxoa2/Hj3t/wpJFC5FI6WpqbkZ8XByiqQs5zexMCNVVqw2Fm2rl9vRSGt2w2hyw2R1wUUOt+WQlElOS8VX5RhiokzlrjzUNX0RFRuJWUzO2VVSgbO0a9JKyw0eO4bWpU5CYEM++DYCLDLS130Pz7bvosVpEBxuoU7VaDXZ/t4tq6kV5aSlioyOpYcgYNacYfPaYu5Aju0T12lC+CXt3VaK7uxvp6ek4dvpXxMfHi8jY2oOHD3GroRF/kWMdXV1wUEROmrs7t1vxgGR44KsP7sfi5SuEDKkfgIHl7XK5caK2FtevXcOend+QR16sWFmCrRU7kJGaAjXtJxakMUJnVzcMkVHIp7qKIact4qGUskx3dxcqt28RBtIpnbqwUPgpcwwiME4jR1Z37jzeKipCbm4ujp84gXlz56Jg3AvIHJaB7MxnhACnpJeMOqjlTR0daGxpJdxJafeK+uio0Sy0bY7X1KDks/VYNH8ewnU67h+RTpZX2e12v8Vmw4bPy5E1fCS0tA185KndZkV6chLGUhRcR5PpPh70mHHuwiVoQ7XQk6KGmzfQ0tSECRMnY3LhFDQ13MSShe+hZM06LF+6hBZFf6OQIQkqMw1+2cZNGJY9AmMoOje1to0cIEfQ0tJEG1KFyKgoLJ4/B3kvTsCE8eNhvNOGSa8WIq9gLPbu/h4VWzfjpZcn4kzdaWzbWYnimW9DR3UUXUnGOJUcHWPNqdo6ODw+5OTmoauzA79UV4mURujCEBsTg9S0NEwqnIryzVswp7hYLOfDR45g5ozpWLrsI1T8sAepOfmIpW1zqu4MxubnimKzMbIyUAppVBPGKfR5EUbFPXhgP+ISYnDxbB2OHq3BgUPVyMzKQhPtznnFs6i9o6j1Q8iRWKGooeEWTh7aj6GpqYigPoiNiYabRoWBI5LAxiRo8imNe2jAL1+6iKFp6Xj0qK+tvX4fjUQGHLQbRw7PQsqQIXBRN7LnGRlpuErjk5KcTHMbIVLF90wXjdGfPqVRNsiG1ZG03cvWrYW/10URRCCP9h2vLaKSMTeiDDq8MbWQjvRi0KPKrT+YXoJRI56FIVxP4+ARTSXrJY1ILFPJBoUz/DzxG6dWh5CwW7zQvCn+vH4DNadrsWDuHOgp3V6KQKkkUJGMQPLwORgMvBYiXE5FPxe/En0H/0C3SQVKY3wXeJZ8wbD4F0OE2l9kWWqeRQmBXv/fWcoFw2r2jiEQS6USS7pSibxjLH9KuvxW8qmlwkAsmSWWdHlmzHesLBgtkE/yPxFhMEblnfxWRiQjYJryW56VvP8AfCpfCs3OlKsAAAAASUVORK5CYII=\";\n        fixture = await createFixture({\n          prerender: true,\n          files: {\n            ...files,\n            \"app/routes/text[.txt].tsx\": js`\n            export function loader() {\n              return new Response(\"Hello, world\");\n            }\n          `,\n            \"app/routes/json[.json].tsx\": js`\n            export function loader() {\n              return new Response(JSON.stringify({ hello: 'world' }), {\n                headers: {\n                  'Content-Type': 'application/json',\n                }\n              });\n            }\n          `,\n            \"app/routes/image[.png].tsx\": js`\n            export function loader() {\n              return new Response(\n                Buffer.from(\"${base64Png}\", 'base64'),\n                {\n                  headers: {\n                    'Content-Type': 'image/png',\n                  }\n                },\n              );\n            }\n          `,\n          },\n        });\n        appFixture = await createAppFixture(fixture);\n\n        let clientDir = path.join(fixture.projectDir, \"build\", \"client\");\n        expect(listAllFiles(clientDir).sort()).toEqual([\n          \"_root.data\",\n          \"about.data\",\n          \"about/index.html\",\n          \"favicon.ico\",\n          \"image.png\",\n          \"image.png.data\",\n          \"index.html\",\n          \"json.json\",\n          \"json.json.data\",\n          \"text.txt\",\n          \"text.txt.data\",\n        ]);\n\n        expect(\n          await fs.promises.readFile(path.join(clientDir, \"json.json\"), \"utf8\"),\n        ).toEqual('{\"hello\":\"world\"}');\n        expect(\n          await fs.promises.readFile(path.join(clientDir, \"text.txt\"), \"utf8\"),\n        ).toEqual(\"Hello, world\");\n        expect(\n          await fs.promises.readFile(\n            path.join(clientDir, \"image.png\"),\n            \"base64\",\n          ),\n        ).toEqual(base64Png);\n\n        let res = await fixture.requestResource(\"/json.json\");\n        expect(await res.json()).toEqual({ hello: \"world\" });\n\n        let dataRes = await fixture.requestSingleFetchData(\"/json.json.data\");\n        expect(dataRes.data).toEqual({\n          \"routes/json[.json]\": {\n            data: {\n              hello: \"world\",\n            },\n          },\n        });\n\n        res = await fixture.requestResource(\"/text.txt\");\n        expect(await res.text()).toBe(\"Hello, world\");\n\n        dataRes = await fixture.requestSingleFetchData(\"/text.txt.data\");\n        expect(dataRes.data).toEqual({\n          \"routes/text[.txt]\": {\n            data: \"Hello, world\",\n          },\n        });\n\n        res = await fixture.requestResource(\"/image.png\");\n        expect(Buffer.from(await res.arrayBuffer()).toString(\"base64\")).toBe(\n          base64Png,\n        );\n      });\n\n      test(\"Adds leading slashes if omitted in config\", async () => {\n        fixture = await createFixture({\n          prerender: true,\n          files: {\n            ...files,\n            \"react-router.config.ts\": js`\n            export default {\n              async prerender() {\n                await new Promise(r => setTimeout(r, 1));\n                return ['/', 'about'];\n              },\n            }\n          `,\n            \"vite.config.ts\": js`\n            import { defineConfig } from \"vite\";\n            import { reactRouter } from \"@react-router/dev/vite\";\n\n            export default defineConfig({\n              build: { manifest: true },\n              plugins: [\n                reactRouter()\n              ],\n            });\n          `,\n          },\n        });\n        appFixture = await createAppFixture(fixture);\n\n        let clientDir = path.join(fixture.projectDir, \"build\", \"client\");\n        expect(listAllFiles(clientDir).sort()).toEqual([\n          \"_root.data\",\n          \"about.data\",\n          \"about/index.html\",\n          \"favicon.ico\",\n          \"index.html\",\n        ]);\n\n        let res = await fixture.requestDocument(\"/\");\n        let html = await res.text();\n        expect(html).toMatch(\"<title>Index Title: Index Loader Data</title>\");\n        expect(html).toMatch(\"<h1>Root</h1>\");\n        expect(html).toMatch('<h2 data-route=\"true\">Index</h2>');\n        expect(html).toMatch(\n          '<p data-loader-data=\"true\">Index Loader Data</p>',\n        );\n\n        res = await fixture.requestDocument(\"/about\");\n        html = await res.text();\n        expect(html).toMatch(\"<title>About Title: About Loader Data</title>\");\n        expect(html).toMatch(\"<h1>Root</h1>\");\n        expect(html).toMatch('<h2 data-route=\"true\">About</h2>');\n        expect(html).toMatch(\n          '<p data-loader-data=\"true\">About Loader Data</p>',\n        );\n      });\n\n      test(\"Permits a concurrency option\", async () => {\n        fixture = await createFixture({\n          prerender: true,\n          files: {\n            ...files,\n            \"react-router.config.ts\": reactRouterConfig({\n              prerender: {\n                paths: [\"/\", \"/about\"],\n                unstable_concurrency: 2,\n              },\n            }),\n            \"vite.config.ts\": js`\n            import { defineConfig } from \"vite\";\n            import { reactRouter } from \"@react-router/dev/vite\";\n\n            export default defineConfig({\n              build: { manifest: true },\n              plugins: [\n                reactRouter()\n              ],\n            });\n          `,\n          },\n        });\n        appFixture = await createAppFixture(fixture);\n\n        let clientDir = path.join(fixture.projectDir, \"build\", \"client\");\n        expect(listAllFiles(clientDir).sort()).toEqual([\n          \"_root.data\",\n          \"about.data\",\n          \"about/index.html\",\n          \"favicon.ico\",\n          \"index.html\",\n        ]);\n\n        let res = await fixture.requestDocument(\"/\");\n        let html = await res.text();\n        expect(html).toMatch(\"<title>Index Title: Index Loader Data</title>\");\n        expect(html).toMatch(\"<h1>Root</h1>\");\n        expect(html).toMatch('<h2 data-route=\"true\">Index</h2>');\n        expect(html).toMatch(\n          '<p data-loader-data=\"true\">Index Loader Data</p>',\n        );\n\n        res = await fixture.requestDocument(\"/about\");\n        html = await res.text();\n        expect(html).toMatch(\"<title>About Title: About Loader Data</title>\");\n        expect(html).toMatch(\"<h1>Root</h1>\");\n        expect(html).toMatch('<h2 data-route=\"true\">About</h2>');\n        expect(html).toMatch(\n          '<p data-loader-data=\"true\">About Loader Data</p>',\n        );\n      });\n    });\n\n    test.describe(\"ssr: true\", () => {\n      test(\"Serves the prerendered HTML file alongside runtime routes\", async ({\n        page,\n      }) => {\n        fixture = await createFixture({\n          files: {\n            ...files,\n            \"react-router.config.ts\": reactRouterConfig({\n              // Don't prerender the /not-prerendered route\n              prerender: [\"/\", \"/about\"],\n            }),\n            \"vite.config.ts\": js`\n            import { defineConfig } from \"vite\";\n            import { reactRouter } from \"@react-router/dev/vite\";\n\n            export default defineConfig({\n              build: { manifest: true },\n              plugins: [reactRouter()],\n            });\n          `,\n            \"app/routes/about.tsx\": js`\n            import { useLoaderData } from 'react-router';\n            export function loader({ request }) {\n              return \"ABOUT-\" + Boolean(process.env.IS_RR_BUILD_REQUEST);\n            }\n\n            export default function Comp() {\n              let data = useLoaderData();\n              return <h1>About: <span>{data}</span></h1>\n            }\n          `,\n            \"app/routes/not-prerendered.tsx\": js`\n            import { useLoaderData } from 'react-router';\n            export function loader({ request }) {\n              return \"NOT-PRERENDERED-\" + Boolean(process.env.IS_RR_BUILD_REQUEST);\n            }\n\n            export default function Comp() {\n              let data = useLoaderData();\n              return <h1>Not-Prerendered: <span>{data}</span></h1>\n            }\n          `,\n          },\n        });\n        appFixture = await createAppFixture(fixture);\n\n        let app = new PlaywrightFixture(appFixture, page);\n        await app.goto(\"/about\");\n        await page.waitForSelector(\"[data-mounted]\");\n        expect(await app.getHtml()).toContain(\"<span>ABOUT-true</span>\");\n\n        await app.goto(\"/not-prerendered\");\n        await page.waitForSelector(\"[data-mounted]\");\n        expect(await app.getHtml()).toContain(\n          \"<span>NOT-PRERENDERED-false</span>\",\n        );\n      });\n\n      test(\"Does not encounter header limits on large prerendered data\", async ({\n        page,\n      }) => {\n        fixture = await createFixture({\n          files: {\n            ...files,\n            \"react-router.config.ts\": reactRouterConfig({\n              prerender: [\"/\", \"/about\"],\n            }),\n            \"vite.config.ts\": js`\n            import { defineConfig } from \"vite\";\n            import { reactRouter } from \"@react-router/dev/vite\";\n\n            export default defineConfig({\n              build: { manifest: true },\n              plugins: [reactRouter()],\n            });\n          `,\n            \"app/routes/about.tsx\": js`\n            import { useLoaderData } from 'react-router';\n            export function loader({ request }) {\n              return {\n                prerendered: process.env.IS_RR_BUILD_REQUEST ?? \"no\",\n                // 24999 characters\n                data: new Array(5000).fill('test').join('-'),\n              };\n            }\n\n            export default function Comp() {\n              let data = useLoaderData();\n              return (\n                <>\n                  <h1 data-title>Large loader</h1>\n                  <p data-prerendered>{data.prerendered}</p>\n                  <p data-length>{data.data.length}</p>\n                </>\n              );\n            }\n          `,\n          },\n        });\n        appFixture = await createAppFixture(fixture);\n\n        let app = new PlaywrightFixture(appFixture, page);\n        await app.goto(\"/about\");\n        await page.waitForSelector(\"[data-mounted]\");\n        expect(await app.getHtml(\"[data-title]\")).toContain(\"Large loader\");\n        expect(await app.getHtml(\"[data-prerendered]\")).toContain(\"yes\");\n        expect(await app.getHtml(\"[data-length]\")).toBe(\n          '<p data-length=\"true\">24999</p>',\n        );\n      });\n\n      test(\"Handles UTF-8 characters in prerendered and non-prerendered routes\", async ({\n        page,\n      }) => {\n        fixture = await createFixture({\n          files: {\n            ...files,\n            \"react-router.config.ts\": reactRouterConfig({\n              prerender: [\"/\", \"/utf8-prerendered\"],\n            }),\n            \"vite.config.ts\": js`\n            import { defineConfig } from \"vite\";\n            import { reactRouter } from \"@react-router/dev/vite\";\n\n            export default defineConfig({\n              build: { manifest: true },\n              plugins: [reactRouter()],\n            });\n          `,\n            \"app/routes/utf8-prerendered.tsx\": js`\n            import { useLoaderData } from 'react-router';\n            export function loader({ request }) {\n              return {\n                prerendered: process.env.IS_RR_BUILD_REQUEST ?? \"no\",\n                data: \"한글 데이터 - UTF-8 문자\",\n              };\n            }\n\n            export default function Comp() {\n              let data = useLoaderData();\n              return (\n                <>\n                  <h1 data-title>UTF-8 Prerendered</h1>\n                  <p data-prerendered>{data.prerendered}</p>\n                  <p data-content>{data.data}</p>\n                </>\n              );\n            }\n          `,\n            \"app/routes/utf8-not-prerendered.tsx\": js`\n            import { useLoaderData } from 'react-router';\n            export function loader({ request }) {\n              return {\n                prerendered: process.env.IS_RR_BUILD_REQUEST ?? \"no\",\n                data: \"非プリレンダリングデータ - UTF-8文字\",\n              };\n            }\n\n            export default function Comp() {\n              let data = useLoaderData();\n              return (\n                <>\n                  <h1 data-title>UTF-8 Not Prerendered</h1>\n                  <p data-prerendered>{data.prerendered}</p>\n                  <p data-content>{data.data}</p>\n                </>\n              );\n            }\n          `,\n          },\n        });\n        appFixture = await createAppFixture(fixture);\n\n        let app = new PlaywrightFixture(appFixture, page);\n\n        // Test prerendered route with UTF-8 characters\n        await app.goto(\"/utf8-prerendered\");\n        await page.waitForSelector(\"[data-mounted]\");\n        expect(await app.getHtml(\"[data-title]\")).toContain(\n          \"UTF-8 Prerendered\",\n        );\n        expect(await app.getHtml(\"[data-prerendered]\")).toContain(\"yes\");\n        expect(await app.getHtml(\"[data-content]\")).toContain(\n          \"한글 데이터 - UTF-8 문자\",\n        );\n\n        // Test non-prerendered route with UTF-8 characters\n        await app.goto(\"/utf8-not-prerendered\");\n        await page.waitForSelector(\"[data-mounted]\");\n        expect(await app.getHtml(\"[data-title]\")).toContain(\n          \"UTF-8 Not Prerendered\",\n        );\n        expect(await app.getHtml(\"[data-prerendered]\")).toContain(\"no\");\n        expect(await app.getHtml(\"[data-content]\")).toContain(\n          \"非プリレンダリングデータ - UTF-8文字\",\n        );\n      });\n\n      test(\"Renders down to the proper HydrateFallback\", async ({ page }) => {\n        fixture = await createFixture({\n          files: {\n            ...files,\n            \"react-router.config.ts\": reactRouterConfig({\n              prerender: [\"/\", \"/parent\", \"/parent/child\"],\n            }),\n            \"vite.config.ts\": js`\n            import { defineConfig } from \"vite\";\n            import { reactRouter } from \"@react-router/dev/vite\";\n\n            export default defineConfig({\n              build: { manifest: true },\n              plugins: [reactRouter()],\n            });\n          `,\n            \"app/routes/parent.tsx\": js`\n            import { Outlet, useLoaderData } from 'react-router';\n            export function loader() {\n              return \"PARENT\";\n            }\n            export default function Comp() {\n              let data = useLoaderData();\n              return <><p>Parent: {data}</p><Outlet/></>\n            }\n          `,\n            \"app/routes/parent.child.tsx\": js`\n            import { Outlet, useLoaderData } from 'react-router';\n            export function loader() {\n              return \"CHILD\";\n            }\n            export function HydrateFallback() {\n              return <p>Child loading...</p>\n            }\n            export default function Comp() {\n              let data = useLoaderData();\n              return <><p>Child: {data}</p><Outlet/></>\n            }\n          `,\n            \"app/routes/parent.child._index.tsx\": js`\n            import { Outlet, useLoaderData } from 'react-router';\n            export function clientLoader() {\n              return \"INDEX\";\n            }\n            export default function Comp() {\n              let data = useLoaderData();\n              return <><p>Index: {data}</p><Outlet/></>\n            }\n          `,\n          },\n        });\n        appFixture = await createAppFixture(fixture);\n\n        let res = await fixture.requestDocument(\"/parent/child\");\n        let html = await res.text();\n        expect(html).toContain(\"<p>Child loading...</p>\");\n\n        let app = new PlaywrightFixture(appFixture, page);\n        await app.goto(\"/parent/child\");\n        await page.waitForSelector(\"[data-mounted]\");\n        expect(await app.getHtml()).toMatch(\"Index: INDEX\");\n      });\n\n      test(\"Ignores build-time headers at runtime\", async () => {\n        fixture = await createFixture({ files });\n        let res = await fixture.requestSingleFetchData(\"/_root.data\", {\n          headers: {\n            \"X-React-Router-Prerender-Data\": encodeURI(\n              '[{\"_1\":2},\"routes/_index\",{\"_3\":4},\"data\",\"Hello World!\"]',\n            ),\n          },\n        });\n        expect((res.data as any)[\"routes/_index\"].data).toBe(\n          \"Index Loader Data\",\n        );\n      });\n    });\n\n    test.describe(\"ssr: false\", () => {\n      function captureRequests(page: Page) {\n        let requests: string[] = [];\n        page.on(\"request\", (request) => {\n          let url = new URL(request.url());\n          if (\n            url.pathname.endsWith(\".data\") ||\n            url.pathname.endsWith(\"__manifest\")\n          ) {\n            requests.push(url.pathname + url.search);\n          }\n        });\n        return requests;\n      }\n\n      function clearRequests(requests: string[]) {\n        while (requests.length) {\n          requests.pop();\n        }\n      }\n\n      test(\"Errors on headers/action functions in any route\", async () => {\n        let cwd = await createProject({\n          \"react-router.config.ts\": reactRouterConfig({\n            ssr: false,\n            prerender: [\"/\", \"/a\"],\n          }),\n          \"app/routes/a.tsx\": String.raw`\n          // Invalid exports\n          export function headers() {}\n          export function action() {}\n\n          // Valid exports\n          export function loader() {}\n          export function clientLoader() {}\n          export function clientAction() {}\n          export default function Component() {}\n        `,\n        });\n        let result = build({ cwd });\n        let stderr = result.stderr.toString(\"utf8\");\n        expect(stderr).toMatch(\n          \"Prerender: 2 invalid route export(s) in `routes/a` when pre-rendering \" +\n            \"with `ssr:false`: `headers`, `action`.  \" +\n            \"See https://reactrouter.com/how-to/pre-rendering#invalid-exports for more information.\",\n        );\n      });\n\n      test(\"Errors on loader functions in non-prerendered routes\", async () => {\n        let cwd = await createProject({\n          \"react-router.config.ts\": reactRouterConfig({\n            ssr: false,\n            prerender: [\"/\", \"/a\"],\n          }),\n          \"app/routes/a.tsx\": String.raw`\n          export function loader() {}\n          export function clientLoader() {}\n          export function clientAction() {}\n          export default function Component() {}\n        `,\n          \"app/routes/b.tsx\": String.raw`\n          export function loader() {}\n          export function clientLoader() {}\n          export function clientAction() {}\n          export default function Component() {}\n        `,\n        });\n        let result = build({ cwd });\n        let stderr = result.stderr.toString(\"utf8\");\n        expect(stderr).toMatch(\n          \"Prerender: 1 invalid route export in `routes/b` when pre-rendering \" +\n            \"with `ssr:false`: `loader`. \" +\n            \"See https://reactrouter.com/how-to/pre-rendering#invalid-exports for more information.\",\n        );\n      });\n\n      test(\"Errors on loader functions in parent routes with non-pre-rendered children\", async () => {\n        let cwd = await createProject({\n          \"react-router.config.ts\": reactRouterConfig({\n            ssr: false,\n            prerender: [\"/\", \"/a\"],\n          }),\n          \"app/routes/a.tsx\": String.raw`\n          export function loader() {}\n          export function clientAction() {}\n          export default function Component() {}\n        `,\n          \"app/routes/a.b.tsx\": String.raw`\n          export function clientLoader() {}\n          export function clientAction() {}\n          export default function Component() {}\n        `,\n        });\n        let result = build({ cwd });\n        let stderr = result.stderr.toString(\"utf8\");\n        expect(stderr).toMatch(\n          \"Prerender: 1 invalid route export in `routes/a` when pre-rendering \" +\n            \"with `ssr:false`: `loader`. \" +\n            \"See https://reactrouter.com/how-to/pre-rendering#invalid-exports for more information.\",\n        );\n      });\n\n      test(\"Warns on parameterized routes with prerender:true + ssr:false\", async () => {\n        let buildStdio = new PassThrough();\n        fixture = await createFixture({\n          buildStdio,\n          prerender: true,\n          files: {\n            \"react-router.config.ts\": reactRouterConfig({\n              ssr: false,\n              prerender: true,\n            }),\n            \"vite.config.ts\": files[\"vite.config.ts\"],\n            \"app/root.tsx\": files[\"app/root.tsx\"],\n            \"app/routes/_index.tsx\": files[\"app/routes/_index.tsx\"],\n            \"app/routes/$slug.tsx\": js`\n            import { Outlet } from 'react-router'\n            export default function Component() {\n              return <Outlet/>\n            }\n          `,\n            \"app/routes/$.tsx\": js`\n            import { Outlet } from 'react-router'\n            export default function Component() {\n              return <Outlet/>\n            }\n          `,\n          },\n        });\n\n        let buildOutput: string;\n        let chunks: Buffer[] = [];\n        buildOutput = await new Promise<string>((resolve, reject) => {\n          buildStdio.on(\"data\", (chunk) => chunks.push(Buffer.from(chunk)));\n          buildStdio.on(\"error\", (err) => reject(err));\n          buildStdio.on(\"end\", () =>\n            resolve(Buffer.concat(chunks).toString(\"utf8\")),\n          );\n        });\n\n        expect(buildOutput).toContain(\n          [\n            \"⚠️ Paths with dynamic/splat params cannot be prerendered when using `prerender: true`. \" +\n              \"You may want to use the `prerender()` API to prerender the following paths:\",\n            \"  - :slug\",\n            \"  - *\",\n          ].join(\"\\n\"),\n        );\n        // Only logs once\n        expect(buildOutput.match(/with dynamic\\/splat params/g)?.length).toBe(\n          1,\n        );\n      });\n\n      test(\"Prerenders a spa fallback with prerender:['/'] + ssr:false\", async () => {\n        let buildStdio = new PassThrough();\n        fixture = await createFixture({\n          buildStdio,\n          prerender: true,\n          files: {\n            \"react-router.config.ts\": reactRouterConfig({\n              ssr: false,\n              prerender: [\"/\"],\n            }),\n            \"vite.config.ts\": files[\"vite.config.ts\"],\n            \"app/root.tsx\": files[\"app/root.tsx\"],\n            \"app/routes/_index.tsx\": files[\"app/routes/_index.tsx\"],\n            \"app/routes/page.tsx\": js`\n            export function clientLoader() {\n              return \"PAGE DATA\"\n            }\n            export default function Page({ loaderData }) {\n              return <p>{loaderData}</p>\n            }\n          `,\n          },\n        });\n\n        appFixture = await createAppFixture(fixture);\n\n        let clientDir = path.join(fixture.projectDir, \"build\", \"client\");\n        expect(listAllFiles(clientDir).sort()).toEqual([\n          \"__spa-fallback.html\",\n          \"_root.data\",\n          \"favicon.ico\",\n          \"index.html\",\n        ]);\n\n        let res = await fixture.requestDocument(\"/\");\n        let html = await res.text();\n        expect(html).toMatch(\"<title>Index Title: Index Loader Data</title>\");\n        expect(html).toMatch(\"<h1>Root</h1>\");\n        expect(html).toMatch('<h2 data-route=\"true\">Index</h2>');\n        expect(html).toMatch(\n          '<p data-loader-data=\"true\">Index Loader Data</p>',\n        );\n        expect(html).not.toMatch(\"<p>Loading...</p>\");\n\n        res = await fixture.requestDocument(\"/page\");\n        html = await res.text();\n        expect(html).toMatch(\"<p>Loading...</p>\");\n      });\n\n      test(\"Hydrates into a navigable app\", async ({ page }) => {\n        fixture = await createFixture({\n          prerender: true,\n          files: {\n            ...files,\n            \"react-router.config.ts\": reactRouterConfig({\n              ssr: false, // turn off fog of war since we're serving with a static server\n              prerender: true,\n            }),\n          },\n        });\n        appFixture = await createAppFixture(fixture);\n\n        let requests = captureRequests(page);\n        let app = new PlaywrightFixture(appFixture, page);\n        await app.goto(\"/\");\n        await page.waitForSelector(\"[data-mounted]\");\n        await app.clickLink(\"/about\");\n        await page.waitForSelector(\"[data-route]:has-text('About')\");\n        expect(requests).toEqual([\"/about.data\"]);\n      });\n\n      test(\"Hydrates into a navigable app from the spa fallback\", async ({\n        page,\n      }) => {\n        fixture = await createFixture({\n          prerender: true,\n          files: {\n            \"react-router.config.ts\": reactRouterConfig({\n              ssr: false, // turn off fog of war since we're serving with a static server\n              prerender: [\"/\"],\n            }),\n            \"vite.config.ts\": files[\"vite.config.ts\"],\n            \"app/root.tsx\": files[\"app/root.tsx\"],\n            \"app/routes/_index.tsx\": files[\"app/routes/_index.tsx\"],\n            \"app/routes/page.tsx\": js`\n            import { Link } from 'react-router';\n            export async function clientLoader() {\n              await new Promise(r => setTimeout(r, 1000));\n              return \"PAGE DATA\"\n            }\n            export default function Page({ loaderData }) {\n              return (\n                <>\n                  <p data-page>{loaderData}</p>\n                  <Link to=\"/page2\">Go to page2</Link>\n                </>\n              );\n            }\n          `,\n            \"app/routes/page2.tsx\": js`\n            export function clientLoader() {\n              return \"PAGE2 DATA\"\n            }\n            export default function Page({ loaderData }) {\n              return <p data-page2>{loaderData}</p>\n            }\n          `,\n          },\n        });\n        appFixture = await createAppFixture(fixture);\n\n        let app = new PlaywrightFixture(appFixture, page);\n        // Load a path we didn't prerender, ensure it starts with the root fallback,\n        // hydrates, and then lets you navigate\n        await app.goto(\"/page\");\n        expect(await page.getByText(\"Loading...\")).toBeVisible();\n        await page.waitForSelector(\"[data-page]\");\n        await app.clickLink(\"/page2\");\n        await page.waitForSelector(\"[data-page2]\");\n        expect(await (await page.$(\"[data-page2]\"))?.innerText()).toBe(\n          \"PAGE2 DATA\",\n        );\n      });\n\n      test(\"Navigates across SPA/prerender pages when starting from a SPA page\", async ({\n        page,\n      }) => {\n        fixture = await createFixture({\n          prerender: true,\n          files: {\n            \"react-router.config.ts\": reactRouterConfig({\n              ssr: false, // turn off fog of war since we're serving with a static server\n              prerender: [\"/page\"],\n            }),\n            \"vite.config.ts\": files[\"vite.config.ts\"],\n            \"app/root.tsx\": js`\n            import * as React from \"react\";\n            import { Outlet, Scripts } from \"react-router\";\n\n            export function Layout({ children }) {\n              return (\n                <html lang=\"en\">\n                  <head />\n                  <body>\n                    {children}\n                    <Scripts />\n                  </body>\n                </html>\n              );\n            }\n\n            export default function Root({ loaderData }) {\n              return <Outlet />\n            }\n          `,\n            \"app/routes/_index.tsx\": js`\n            import { Link } from 'react-router';\n            export default function Index() {\n              return <Link to=\"/page\">Go to page</Link>\n            }\n          `,\n            \"app/routes/page.tsx\": js`\n            import { Link, Form } from 'react-router';\n            export async function loader() {\n              return \"PAGE DATA\"\n            }\n            let count = 0;\n            export function clientAction() {\n              return \"PAGE ACTION \" + (++count)\n            }\n            export default function Page({ loaderData, actionData }) {\n              return (\n                <>\n                  <p data-page>{loaderData}</p>\n                  {actionData ? <p data-page-action>{actionData}</p> : null}\n                  <Link to=\"/page2\">Go to page2</Link>\n                  <Form method=\"post\" action=\"/page\">\n                    <button type=\"submit\">Submit</button>\n                  </Form>\n                  <Form method=\"post\" action=\"/page2\">\n                    <button type=\"submit\">Submit /page2</button>\n                  </Form>\n                </>\n              );\n            }\n          `,\n            \"app/routes/page2.tsx\": js`\n            import { Form } from 'react-router';\n            export function clientLoader() {\n              return \"PAGE2 DATA\"\n            }\n            let count = 0;\n            export function clientAction() {\n              return \"PAGE2 ACTION \" + (++count)\n            }\n            export default function Page({ loaderData, actionData }) {\n              return (\n                <>\n                  <p data-page2>{loaderData}</p>\n                  {actionData ? <p data-page2-action>{actionData}</p> : null}\n                  <Form method=\"post\" action=\"/page\">\n                    <button type=\"submit\">Submit</button>\n                  </Form>\n                  <Form method=\"post\" action=\"/page2\">\n                    <button type=\"submit\">Submit /page2</button>\n                  </Form>\n                </>\n              );\n            }\n          `,\n          },\n        });\n        appFixture = await createAppFixture(fixture);\n\n        let requests = captureRequests(page);\n        let app = new PlaywrightFixture(appFixture, page);\n        await app.goto(\"/\", true);\n        await page.waitForSelector('a[href=\"/page\"]');\n\n        await app.clickLink(\"/page\");\n        await page.waitForSelector(\"[data-page]\");\n        expect(await (await page.$(\"[data-page]\"))?.innerText()).toBe(\n          \"PAGE DATA\",\n        );\n        expect(requests).toEqual([\"/page.data\"]);\n        clearRequests(requests);\n\n        await app.clickSubmitButton(\"/page\");\n        await page.waitForSelector(\"[data-page-action]\");\n        expect(await (await page.$(\"[data-page-action]\"))?.innerText()).toBe(\n          \"PAGE ACTION 1\",\n        );\n        // No revalidation after submission to self\n        expect(requests).toEqual([]);\n\n        await app.clickLink(\"/page2\");\n        await page.waitForSelector(\"[data-page2]\");\n        expect(await (await page.$(\"[data-page2]\"))?.innerText()).toBe(\n          \"PAGE2 DATA\",\n        );\n        expect(requests).toEqual([]);\n\n        await app.clickSubmitButton(\"/page2\");\n        await page.waitForSelector(\"[data-page2-action]\");\n        expect(await (await page.$(\"[data-page2-action]\"))?.innerText()).toBe(\n          \"PAGE2 ACTION 1\",\n        );\n        expect(requests).toEqual([]);\n\n        await app.clickSubmitButton(\"/page\");\n        await page.waitForSelector(\"[data-page-action]\");\n        expect(await (await page.$(\"[data-page-action]\"))?.innerText()).toBe(\n          \"PAGE ACTION 2\",\n        );\n        expect(requests).toEqual([\"/page.data\"]);\n        clearRequests(requests);\n\n        await app.clickSubmitButton(\"/page2\");\n        await page.waitForSelector(\"[data-page2-action]\");\n        expect(await (await page.$(\"[data-page2-action]\"))?.innerText()).toBe(\n          \"PAGE2 ACTION 2\",\n        );\n        expect(requests).toEqual([]);\n      });\n\n      test(\"Navigates across SPA/prerender pages when starting from a prerendered page\", async ({\n        page,\n      }) => {\n        fixture = await createFixture({\n          prerender: true,\n          files: {\n            \"react-router.config.ts\": reactRouterConfig({\n              ssr: false, // turn off fog of war since we're serving with a static server\n              prerender: [\"/\", \"/page\"],\n            }),\n            \"vite.config.ts\": files[\"vite.config.ts\"],\n            \"app/root.tsx\": js`\n            import * as React from \"react\";\n            import { Outlet, Scripts } from \"react-router\";\n\n            export function Layout({ children }) {\n              return (\n                <html lang=\"en\">\n                  <head />\n                  <body>\n                    {children}\n                    <Scripts />\n                  </body>\n                </html>\n              );\n            }\n\n            export default function Root({ loaderData }) {\n              return <Outlet />;\n            }\n          `,\n            \"app/routes/_index.tsx\": js`\n            import { Link } from 'react-router';\n            export default function Index() {\n              return <Link to=\"/page\">Go to page</Link>\n            }\n          `,\n            \"app/routes/page.tsx\": js`\n            import { Link, Form } from 'react-router';\n            export async function loader() {\n              return \"PAGE DATA\"\n            }\n            let count = 0;\n            export function clientAction() {\n              return \"PAGE ACTION \" + (++count)\n            }\n            export default function Page({ loaderData, actionData }) {\n              return (\n                <>\n                  <p data-page>{loaderData}</p>\n                  {actionData ? <p data-page-action>{actionData}</p> : null}\n                  <Link to=\"/page2\">Go to page2</Link>\n                  <Form method=\"post\" action=\"/page\">\n                    <button type=\"submit\">Submit</button>\n                  </Form>\n                  <Form method=\"post\" action=\"/page2\">\n                    <button type=\"submit\">Submit /page2</button>\n                  </Form>\n                </>\n              );\n            }\n          `,\n            \"app/routes/page2.tsx\": js`\n            import { Form } from 'react-router';\n            export function clientLoader() {\n              return \"PAGE2 DATA\"\n            }\n            let count = 0;\n            export function clientAction() {\n              return \"PAGE2 ACTION \" + (++count)\n            }\n            export default function Page({ loaderData, actionData }) {\n              return (\n                <>\n                  <p data-page2>{loaderData}</p>\n                  {actionData ? <p data-page2-action>{actionData}</p> : null}\n                  <Form method=\"post\" action=\"/page\">\n                    <button type=\"submit\">Submit</button>\n                  </Form>\n                  <Form method=\"post\" action=\"/page2\">\n                    <button type=\"submit\">Submit /page2</button>\n                  </Form>\n                </>\n              );\n            }\n          `,\n          },\n        });\n        appFixture = await createAppFixture(fixture);\n\n        let requests = captureRequests(page);\n        let app = new PlaywrightFixture(appFixture, page);\n        await app.goto(\"/\", true);\n        await page.waitForSelector('a[href=\"/page\"]');\n\n        await app.clickLink(\"/page\");\n        await page.waitForSelector(\"[data-page]\");\n        expect(await (await page.$(\"[data-page]\"))?.innerText()).toBe(\n          \"PAGE DATA\",\n        );\n        expect(requests).toEqual([\"/page.data\"]);\n        clearRequests(requests);\n\n        await app.clickSubmitButton(\"/page\");\n        await page.waitForSelector(\"[data-page-action]\");\n        expect(await (await page.$(\"[data-page-action]\"))?.innerText()).toBe(\n          \"PAGE ACTION 1\",\n        );\n        // No revalidation after submission to self\n        expect(requests).toEqual([]);\n\n        await app.clickLink(\"/page2\");\n        await page.waitForSelector(\"[data-page2]\");\n        expect(await (await page.$(\"[data-page2]\"))?.innerText()).toBe(\n          \"PAGE2 DATA\",\n        );\n        expect(requests).toEqual([]);\n\n        await app.clickSubmitButton(\"/page2\");\n        await page.waitForSelector(\"[data-page2-action]\");\n        expect(await (await page.$(\"[data-page2-action]\"))?.innerText()).toBe(\n          \"PAGE2 ACTION 1\",\n        );\n        expect(requests).toEqual([]);\n\n        await app.clickSubmitButton(\"/page\");\n        await page.waitForSelector(\"[data-page-action]\");\n        expect(await (await page.$(\"[data-page-action]\"))?.innerText()).toBe(\n          \"PAGE ACTION 2\",\n        );\n        expect(requests).toEqual([\"/page.data\"]);\n        clearRequests(requests);\n\n        await app.clickSubmitButton(\"/page2\");\n        await page.waitForSelector(\"[data-page2-action]\");\n        expect(await (await page.$(\"[data-page2-action]\"))?.innerText()).toBe(\n          \"PAGE2 ACTION 2\",\n        );\n        expect(requests).toEqual([]);\n      });\n\n      test(\"Navigates across SPA/prerender pages when starting from a SPA page and a root loader exists\", async ({\n        page,\n      }) => {\n        fixture = await createFixture({\n          prerender: true,\n          files: {\n            \"react-router.config.ts\": reactRouterConfig({\n              ssr: false, // turn off fog of war since we're serving with a static server\n              prerender: [\"/page\"],\n            }),\n            \"vite.config.ts\": files[\"vite.config.ts\"],\n            \"app/root.tsx\": js`\n            import * as React from \"react\";\n            import { Outlet, Scripts } from \"react-router\";\n\n            export function loader() {\n              return \"ROOT DATA\";\n            }\n\n            export function Layout({ children }) {\n              return (\n                <html lang=\"en\">\n                  <head />\n                  <body>\n                    {children}\n                    <Scripts />\n                  </body>\n                </html>\n              );\n            }\n\n            export default function Root({ loaderData }) {\n              return (\n                <>\n                  <p data-root>{loaderData}</p>\n                  <Outlet />\n                </>\n              );\n            }\n          `,\n            \"app/routes/_index.tsx\": js`\n            import { Link } from 'react-router';\n            export default function Index() {\n              return <Link to=\"/page\">Go to page</Link>\n            }\n          `,\n            \"app/routes/page.tsx\": js`\n            import { Link, Form } from 'react-router';\n            export async function loader() {\n              return \"PAGE DATA\"\n            }\n            let count = 0;\n            export function clientAction() {\n              return \"PAGE ACTION \" + (++count)\n            }\n            export default function Page({ loaderData, actionData }) {\n              return (\n                <>\n                  <p data-page>{loaderData}</p>\n                  {actionData ? <p data-page-action>{actionData}</p> : null}\n                  <Link to=\"/page2\">Go to page2</Link>\n                  <Form method=\"post\" action=\"/page\">\n                    <button type=\"submit\">Submit</button>\n                  </Form>\n                  <Form method=\"post\" action=\"/page2\">\n                    <button type=\"submit\">Submit /page2</button>\n                  </Form>\n                </>\n              );\n            }\n          `,\n            \"app/routes/page2.tsx\": js`\n            import { Form } from 'react-router';\n            export function clientLoader() {\n              return \"PAGE2 DATA\"\n            }\n            let count = 0;\n            export function clientAction() {\n              return \"PAGE2 ACTION \" + (++count)\n            }\n            export default function Page({ loaderData, actionData }) {\n              return (\n                <>\n                  <p data-page2>{loaderData}</p>\n                  {actionData ? <p data-page2-action>{actionData}</p> : null}\n                  <Form method=\"post\" action=\"/page\">\n                    <button type=\"submit\">Submit</button>\n                  </Form>\n                  <Form method=\"post\" action=\"/page2\">\n                    <button type=\"submit\">Submit /page2</button>\n                  </Form>\n                </>\n              );\n            }\n          `,\n          },\n        });\n        appFixture = await createAppFixture(fixture);\n\n        let requests = captureRequests(page);\n        let app = new PlaywrightFixture(appFixture, page);\n        await app.goto(\"/\", true);\n        await page.waitForSelector(\"[data-root]\");\n        expect(await (await page.$(\"[data-root]\"))?.innerText()).toBe(\n          \"ROOT DATA\",\n        );\n\n        await app.clickLink(\"/page\");\n        await page.waitForSelector(\"[data-page]\");\n        expect(await (await page.$(\"[data-page]\"))?.innerText()).toBe(\n          \"PAGE DATA\",\n        );\n        expect(requests).toEqual([\"/page.data\"]);\n        clearRequests(requests);\n\n        await app.clickSubmitButton(\"/page\");\n        await page.waitForSelector(\"[data-page-action]\");\n        expect(await (await page.$(\"[data-page-action]\"))?.innerText()).toBe(\n          \"PAGE ACTION 1\",\n        );\n        // No revalidation after submission to self\n        expect(requests).toEqual([]);\n\n        await app.clickLink(\"/page2\");\n        await page.waitForSelector(\"[data-page2]\");\n        expect(await (await page.$(\"[data-page2]\"))?.innerText()).toBe(\n          \"PAGE2 DATA\",\n        );\n        expect(requests).toEqual([]);\n\n        await app.clickSubmitButton(\"/page2\");\n        await page.waitForSelector(\"[data-page2-action]\");\n        expect(await (await page.$(\"[data-page2-action]\"))?.innerText()).toBe(\n          \"PAGE2 ACTION 1\",\n        );\n        expect(requests).toEqual([]);\n\n        await app.clickSubmitButton(\"/page\");\n        await page.waitForSelector(\"[data-page-action]\");\n        expect(await (await page.$(\"[data-page-action]\"))?.innerText()).toBe(\n          \"PAGE ACTION 2\",\n        );\n        expect(requests).toEqual([\"/page.data\"]);\n        clearRequests(requests);\n\n        await app.clickSubmitButton(\"/page2\");\n        await page.waitForSelector(\"[data-page2-action]\");\n        expect(await (await page.$(\"[data-page2-action]\"))?.innerText()).toBe(\n          \"PAGE2 ACTION 2\",\n        );\n        expect(requests).toEqual([]);\n      });\n\n      test(\"Navigates across SPA/prerender pages when starting from a prerendered page and a root loader exists\", async ({\n        page,\n      }) => {\n        fixture = await createFixture({\n          prerender: true,\n          files: {\n            \"react-router.config.ts\": reactRouterConfig({\n              ssr: false, // turn off fog of war since we're serving with a static server\n              prerender: [\"/\", \"/page\"],\n            }),\n            \"vite.config.ts\": files[\"vite.config.ts\"],\n            \"app/root.tsx\": js`\n            import * as React from \"react\";\n            import { Outlet, Scripts } from \"react-router\";\n\n            export function loader() {\n              return \"ROOT DATA\";\n            }\n\n            export function Layout({ children }) {\n              return (\n                <html lang=\"en\">\n                  <head />\n                  <body>\n                    {children}\n                    <Scripts />\n                  </body>\n                </html>\n              );\n            }\n\n            export default function Root({ loaderData }) {\n              return (\n                <>\n                  <p data-root>{loaderData}</p>\n                  <Outlet />\n                </>\n              );\n            }\n          `,\n            \"app/routes/_index.tsx\": js`\n            import { Link } from 'react-router';\n            export default function Index() {\n              return <Link to=\"/page\">Go to page</Link>\n            }\n          `,\n            \"app/routes/page.tsx\": js`\n            import { Link, Form } from 'react-router';\n            export async function loader() {\n              return \"PAGE DATA\"\n            }\n            let count = 0;\n            export function clientAction() {\n              return \"PAGE ACTION \" + (++count)\n            }\n            export default function Page({ loaderData, actionData }) {\n              return (\n                <>\n                  <p data-page>{loaderData}</p>\n                  {actionData ? <p data-page-action>{actionData}</p> : null}\n                  <Link to=\"/page2\">Go to page2</Link>\n                  <Form method=\"post\" action=\"/page\">\n                    <button type=\"submit\">Submit</button>\n                  </Form>\n                  <Form method=\"post\" action=\"/page2\">\n                    <button type=\"submit\">Submit /page2</button>\n                  </Form>\n                </>\n              );\n            }\n          `,\n            \"app/routes/page2.tsx\": js`\n            import { Form } from 'react-router';\n            export function clientLoader() {\n              return \"PAGE2 DATA\"\n            }\n            let count = 0;\n            export function clientAction() {\n              return \"PAGE2 ACTION \" + (++count)\n            }\n            export default function Page({ loaderData, actionData }) {\n              return (\n                <>\n                  <p data-page2>{loaderData}</p>\n                  {actionData ? <p data-page2-action>{actionData}</p> : null}\n                  <Form method=\"post\" action=\"/page\">\n                    <button type=\"submit\">Submit</button>\n                  </Form>\n                  <Form method=\"post\" action=\"/page2\">\n                    <button type=\"submit\">Submit /page2</button>\n                  </Form>\n                </>\n              );\n            }\n          `,\n          },\n        });\n        appFixture = await createAppFixture(fixture);\n\n        let requests = captureRequests(page);\n        let app = new PlaywrightFixture(appFixture, page);\n        await app.goto(\"/\", true);\n        await page.waitForSelector(\"[data-root]\");\n        expect(await (await page.$(\"[data-root]\"))?.innerText()).toBe(\n          \"ROOT DATA\",\n        );\n\n        await app.clickLink(\"/page\");\n        await page.waitForSelector(\"[data-page]\");\n        expect(await (await page.$(\"[data-page]\"))?.innerText()).toBe(\n          \"PAGE DATA\",\n        );\n        expect(requests).toEqual([\"/page.data\"]);\n        clearRequests(requests);\n\n        await app.clickSubmitButton(\"/page\");\n        await page.waitForSelector(\"[data-page-action]\");\n        expect(await (await page.$(\"[data-page-action]\"))?.innerText()).toBe(\n          \"PAGE ACTION 1\",\n        );\n        // No revalidation after submission to self\n        expect(requests).toEqual([]);\n\n        await app.clickLink(\"/page2\");\n        await page.waitForSelector(\"[data-page2]\");\n        expect(await (await page.$(\"[data-page2]\"))?.innerText()).toBe(\n          \"PAGE2 DATA\",\n        );\n        expect(requests).toEqual([]);\n\n        await app.clickSubmitButton(\"/page2\");\n        await page.waitForSelector(\"[data-page2-action]\");\n        expect(await (await page.$(\"[data-page2-action]\"))?.innerText()).toBe(\n          \"PAGE2 ACTION 1\",\n        );\n        expect(requests).toEqual([]);\n\n        await app.clickSubmitButton(\"/page\");\n        await page.waitForSelector(\"[data-page-action]\");\n        expect(await (await page.$(\"[data-page-action]\"))?.innerText()).toBe(\n          \"PAGE ACTION 2\",\n        );\n        expect(requests).toEqual([\"/page.data\"]);\n        clearRequests(requests);\n\n        await app.clickSubmitButton(\"/page2\");\n        await page.waitForSelector(\"[data-page2-action]\");\n        expect(await (await page.$(\"[data-page2-action]\"))?.innerText()).toBe(\n          \"PAGE2 ACTION 2\",\n        );\n        expect(requests).toEqual([]);\n      });\n\n      test(\"Navigates between prerendered parent and child SPA route\", async ({\n        page,\n      }) => {\n        fixture = await createFixture({\n          prerender: true,\n          files: {\n            \"react-router.config.ts\": reactRouterConfig({\n              ssr: false,\n              prerender: [\"/\", \"/parent\"],\n            }),\n            \"vite.config.ts\": files[\"vite.config.ts\"],\n            \"app/root.tsx\": js`\n            import * as React from \"react\";\n            import { Outlet, Scripts } from \"react-router\";\n\n            export function Layout({ children }) {\n              return (\n                <html lang=\"en\">\n                  <head />\n                  <body>\n                    {children}\n                    <Scripts />\n                  </body>\n                </html>\n              );\n            }\n\n            export default function Root({ loaderData }) {\n              return <Outlet />\n            }\n          `,\n            \"app/routes/parent.tsx\": js`\n            import { Link, Form, Outlet } from 'react-router';\n            export async function loader() {\n              return \"PARENT DATA\"\n            }\n            export async function clientLoader() {\n              return \"PARENT CLIENT DATA\"\n            }\n            export function clientAction() {\n              return \"PARENT ACTION\"\n            }\n            export default function Parent({ loaderData, actionData }) {\n              return (\n                <>\n                  <p data-parent>{loaderData}</p>\n                  {actionData ? <p data-parent-action>{actionData}</p> : null}\n                  <Link to=\"/parent/child\">Go to child</Link>\n                  <Form method=\"post\" action=\"/parent\">\n                    <button type=\"submit\">Submit</button>\n                  </Form>\n                  <Form method=\"post\" action=\"/parent/child\">\n                    <button type=\"submit\">Submit child</button>\n                  </Form>\n                  <Outlet />\n                </>\n              );\n            }\n          `,\n            \"app/routes/parent.child.tsx\": js`\n            import { Link, Form } from 'react-router';\n            export function clientLoader() {\n              return \"CHILD DATA\"\n            }\n            export function clientAction() {\n              return \"CHILD ACTION\"\n            }\n            export default function Child({ loaderData, actionData }) {\n              return (\n                <>\n                  <p data-child>{loaderData}</p>\n                  {actionData ? <p data-child-action>{actionData}</p> : null}\n                  <Link to=\"/parent\">Go to parent</Link>\n                  <Form method=\"post\" action=\"/parent/child\">\n                    <button type=\"submit\">Submit</button>\n                  </Form>\n                  <Form method=\"post\" action=\"/parent\">\n                    <button type=\"submit\">Submit parent</button>\n                  </Form>\n                </>\n              );\n            }\n          `,\n          },\n        });\n        appFixture = await createAppFixture(fixture);\n\n        let requests = captureRequests(page);\n        let app = new PlaywrightFixture(appFixture, page);\n        await app.goto(\"/parent\", true);\n        await expect(page.getByText(\"PARENT DATA\")).toBeVisible();\n\n        await app.clickLink(\"/parent/child\");\n        await expect(page.getByText(\"CHILD DATA\")).toBeVisible();\n\n        // Submit to self\n        await app.clickSubmitButton(\"/parent/child\");\n        await expect(page.getByText(\"PARENT CLIENT DATA\")).toBeVisible();\n        await expect(page.getByText(\"CHILD ACTION\")).toBeVisible();\n        await expect(page.getByText(\"CHILD DATA\")).toBeVisible();\n\n        await app.goBack();\n        await expect(page.getByText(\"PARENT CLIENT DATA\")).toBeVisible();\n        await expect(page.getByText(\"CHILD DATA\")).not.toBeVisible();\n\n        // Submit across routes\n        await app.clickSubmitButton(\"/parent/child\");\n        await expect(page.getByText(\"PARENT CLIENT DATA\")).toBeVisible();\n        await expect(page.getByText(\"CHILD ACTION\")).toBeVisible();\n        await expect(page.getByText(\"CHILD DATA\")).toBeVisible();\n\n        // Submit to self\n        await app.clickSubmitButton(\"/parent/child\");\n        await expect(page.getByText(\"PARENT CLIENT DATA\")).toBeVisible();\n        await expect(page.getByText(\"CHILD ACTION\")).toBeVisible();\n        await expect(page.getByText(\"CHILD DATA\")).toBeVisible();\n\n        // Submit across routes\n        await app.clickSubmitButton(\"/parent\");\n        await expect(page.getByText(\"PARENT ACTION\")).toBeVisible();\n        await expect(page.getByText(\"PARENT CLIENT DATA\")).toBeVisible();\n        await expect(page.getByText(\"CHILD DATA\")).not.toBeVisible();\n\n        // Submit to self\n        await app.clickSubmitButton(\"/parent\");\n        await expect(page.getByText(\"PARENT ACTION\")).toBeVisible();\n        await expect(page.getByText(\"PARENT CLIENT DATA\")).toBeVisible();\n        await expect(page.getByText(\"CHILD DATA\")).not.toBeVisible();\n\n        // We should never make this call because we started on this route and it never unmounts\n        expect(requests).toEqual([]);\n      });\n\n      test(\"Navigates between SPA parent and prerendered child route\", async ({\n        page,\n      }) => {\n        fixture = await createFixture({\n          prerender: true,\n          files: {\n            \"react-router.config.ts\": reactRouterConfig({\n              ssr: false,\n              prerender: [\"/\", \"/parent/child\"],\n            }),\n            \"vite.config.ts\": files[\"vite.config.ts\"],\n            \"app/root.tsx\": js`\n            import * as React from \"react\";\n            import { Outlet, Scripts } from \"react-router\";\n\n            export function Layout({ children }) {\n              return (\n                <html lang=\"en\">\n                  <head />\n                  <body>\n                    {children}\n                    <Scripts />\n                  </body>\n                </html>\n              );\n            }\n\n            export default function Root({ loaderData }) {\n              return <Outlet />\n            }\n          `,\n            \"app/routes/parent.tsx\": js`\n            import { Link, Form, Outlet } from 'react-router';\n            export async function clientLoader() {\n              return \"PARENT DATA\"\n            }\n            export function clientAction() {\n              return \"PARENT ACTION\"\n            }\n            export default function Parent({ loaderData, actionData }) {\n              return (\n                <>\n                  <p data-parent>{loaderData}</p>\n                  {actionData ? <p data-parent-action>{actionData}</p> : null}\n                  <Link to=\"/parent/child\">Go to child</Link>\n                  <Form method=\"post\" action=\"/parent\">\n                    <button type=\"submit\">Submit</button>\n                  </Form>\n                  <Form method=\"post\" action=\"/parent/child\">\n                    <button type=\"submit\">Submit child</button>\n                  </Form>\n                  <Outlet />\n                </>\n              );\n            }\n          `,\n            \"app/routes/parent.child.tsx\": js`\n            import { Link, Form } from 'react-router';\n            export function loader() {\n              return \"CHILD DATA\"\n            }\n            export function clientAction() {\n              return \"CHILD ACTION\"\n            }\n            export default function Child({ loaderData, actionData }) {\n              return (\n                <>\n                  <p data-child>{loaderData}</p>\n                  {actionData ? <p data-child-action>{actionData}</p> : null}\n                  <Link to=\"/parent\">Go to parent</Link>\n                  <Form method=\"post\" action=\"/parent/child\">\n                    <button type=\"submit\">Submit</button>\n                  </Form>\n                  <Form method=\"post\" action=\"/parent\">\n                    <button type=\"submit\">Submit parent</button>\n                  </Form>\n                </>\n              );\n            }\n          `,\n          },\n        });\n        appFixture = await createAppFixture(fixture);\n\n        let requests = captureRequests(page);\n        let app = new PlaywrightFixture(appFixture, page);\n        await app.goto(\"/parent\", true);\n        await expect(page.getByText(\"PARENT DATA\")).toBeVisible();\n\n        await app.clickLink(\"/parent/child\");\n        await expect(page.getByText(\"CHILD DATA\")).toBeVisible();\n\n        await app.goBack();\n        await expect(page.getByText(\"PARENT DATA\")).toBeVisible();\n        await expect(page.getByText(\"CHILD DATA\")).not.toBeVisible();\n\n        await app.clickSubmitButton(\"/parent/child\");\n        await expect(page.getByText(\"PARENT DATA\")).toBeVisible();\n        await expect(page.getByText(\"CHILD ACTION\")).toBeVisible();\n        await expect(page.getByText(\"CHILD DATA\")).toBeVisible();\n\n        await app.clickSubmitButton(\"/parent\");\n        await expect(page.getByText(\"PARENT ACTION\")).toBeVisible();\n        await expect(page.getByText(\"PARENT DATA\")).toBeVisible();\n        await expect(page.getByText(\"CHILD DATA\")).not.toBeVisible();\n\n        // Initial navigation and submission from /parent\n        expect(requests).toEqual([\"/parent/child.data\", \"/parent/child.data\"]);\n        while (requests.length) requests.pop();\n\n        await app.goto(\"/parent/child\", true);\n        await expect(page.getByText(\"PARENT DATA\")).toBeVisible();\n        await expect(page.getByText(\"CHILD DATA\")).toBeVisible();\n\n        await app.clickLink(\"/parent\");\n        await expect(page.getByText(\"PARENT DATA\")).toBeVisible();\n        await expect(page.getByText(\"CHILD DATA\")).not.toBeVisible();\n\n        await app.clickSubmitButton(\"/parent/child\");\n        await expect(page.getByText(\"PARENT DATA\")).toBeVisible();\n        await expect(page.getByText(\"CHILD ACTION\")).toBeVisible();\n        await expect(page.getByText(\"CHILD DATA\")).toBeVisible();\n\n        await app.clickSubmitButton(\"/parent\");\n        await expect(page.getByText(\"PARENT ACTION\")).toBeVisible();\n        await expect(page.getByText(\"PARENT DATA\")).toBeVisible();\n        await expect(page.getByText(\"CHILD DATA\")).not.toBeVisible();\n\n        // Submission from /parent\n        expect(requests).toEqual([\"/parent/child.data\"]);\n      });\n\n      test(\"Navigates between prerendered parent and child SPA route (with a root loader)\", async ({\n        page,\n      }) => {\n        fixture = await createFixture({\n          prerender: true,\n          files: {\n            \"react-router.config.ts\": reactRouterConfig({\n              ssr: false,\n              prerender: [\"/\", \"/parent\"],\n            }),\n            \"vite.config.ts\": files[\"vite.config.ts\"],\n            \"app/root.tsx\": js`\n            import * as React from \"react\";\n            import { Outlet, Scripts } from \"react-router\";\n\n            export function loader() {\n              return \"ROOT DATA\"\n            }\n\n            export function Layout({ children }) {\n              return (\n                <html lang=\"en\">\n                  <head />\n                  <body>\n                    {children}\n                    <Scripts />\n                  </body>\n                </html>\n              );\n            }\n\n            export default function Root({ loaderData }) {\n              return (\n                <>\n                  <p data-root>{loaderData}</p>\n                  <Outlet/>\n                </>\n              );\n            }\n\n            export function HydrateFallback() {\n              return <p>Loading...</p>;\n            }\n          `,\n            \"app/routes/parent.tsx\": js`\n            import { Link, Form, Outlet } from 'react-router';\n            export async function loader() {\n              return \"PARENT DATA\"\n            }\n            export async function clientLoader() {\n              return \"PARENT CLIENT DATA\"\n            }\n            export function clientAction() {\n              return \"PARENT ACTION\"\n            }\n            export default function Parent({ loaderData, actionData }) {\n              return (\n                <>\n                  <p data-parent>{loaderData}</p>\n                  {actionData ? <p data-parent-action>{actionData}</p> : null}\n                  <Link to=\"/parent/child\">Go to child</Link>\n                  <Form method=\"post\" action=\"/parent\">\n                    <button type=\"submit\">Submit</button>\n                  </Form>\n                  <Form method=\"post\" action=\"/parent/child\">\n                    <button type=\"submit\">Submit child</button>\n                  </Form>\n                  <Outlet />\n                </>\n              );\n            }\n          `,\n            \"app/routes/parent.child.tsx\": js`\n            import { Link, Form } from 'react-router';\n            export function clientLoader() {\n              return \"CHILD DATA\"\n            }\n            export function clientAction() {\n              return \"CHILD ACTION\"\n            }\n            export default function Child({ loaderData, actionData }) {\n              return (\n                <>\n                  <p data-child>{loaderData}</p>\n                  {actionData ? <p data-child-action>{actionData}</p> : null}\n                  <Link to=\"/parent\">Go to parent</Link>\n                  <Form method=\"post\" action=\"/parent/child\">\n                    <button type=\"submit\">Submit</button>\n                  </Form>\n                  <Form method=\"post\" action=\"/parent\">\n                    <button type=\"submit\">Submit parent</button>\n                  </Form>\n                </>\n              );\n            }\n          `,\n          },\n        });\n        appFixture = await createAppFixture(fixture);\n\n        let requests = captureRequests(page);\n        let app = new PlaywrightFixture(appFixture, page);\n        await app.goto(\"/parent\", true);\n        await expect(page.getByText(\"ROOT DATA\")).toBeVisible();\n        await expect(page.getByText(\"PARENT DATA\")).toBeVisible();\n\n        await app.clickLink(\"/parent/child\");\n        await new Promise((resolve) => setTimeout(resolve, 1000));\n        await expect(page.getByText(\"CHILD DATA\")).toBeVisible();\n\n        // Submit to self\n        await app.clickSubmitButton(\"/parent/child\");\n        await expect(page.getByText(\"PARENT CLIENT DATA\")).toBeVisible();\n        await expect(page.getByText(\"CHILD ACTION\")).toBeVisible();\n        await expect(page.getByText(\"CHILD DATA\")).toBeVisible();\n\n        await app.goBack();\n        await expect(page.getByText(\"PARENT CLIENT DATA\")).toBeVisible();\n        await expect(page.getByText(\"CHILD DATA\")).not.toBeVisible();\n\n        // Submit across routes\n        await app.clickSubmitButton(\"/parent/child\");\n        await expect(page.getByText(\"PARENT CLIENT DATA\")).toBeVisible();\n        await expect(page.getByText(\"CHILD ACTION\")).toBeVisible();\n        await expect(page.getByText(\"CHILD DATA\")).toBeVisible();\n\n        // Submit to self\n        await app.clickSubmitButton(\"/parent/child\");\n        await expect(page.getByText(\"PARENT CLIENT DATA\")).toBeVisible();\n        await expect(page.getByText(\"CHILD ACTION\")).toBeVisible();\n        await expect(page.getByText(\"CHILD DATA\")).toBeVisible();\n\n        // Submit across routes\n        await app.clickSubmitButton(\"/parent\");\n        await expect(page.getByText(\"PARENT ACTION\")).toBeVisible();\n        await expect(page.getByText(\"PARENT CLIENT DATA\")).toBeVisible();\n        await expect(page.getByText(\"CHILD DATA\")).not.toBeVisible();\n\n        // Submit to self\n        await app.clickSubmitButton(\"/parent\");\n        await expect(page.getByText(\"PARENT ACTION\")).toBeVisible();\n        await expect(page.getByText(\"PARENT CLIENT DATA\")).toBeVisible();\n        await expect(page.getByText(\"CHILD DATA\")).not.toBeVisible();\n\n        // We should never make this call because we started on this route and it never unmounts\n        expect(requests).toEqual([]);\n      });\n\n      test(\"Navigates between SPA parent and prerendered child route (with a root loader)\", async ({\n        page,\n      }) => {\n        fixture = await createFixture({\n          prerender: true,\n          files: {\n            \"react-router.config.ts\": reactRouterConfig({\n              ssr: false,\n              prerender: [\"/\", \"/parent/child\"],\n            }),\n            \"vite.config.ts\": files[\"vite.config.ts\"],\n            \"app/root.tsx\": js`\n            import * as React from \"react\";\n            import { Outlet, Scripts } from \"react-router\";\n\n            export function loader() {\n              return \"ROOT DATA\"\n            }\n\n            export function Layout({ children }) {\n              return (\n                <html lang=\"en\">\n                  <head />\n                  <body>\n                    {children}\n                    <Scripts />\n                  </body>\n                </html>\n              );\n            }\n\n\n            export default function Root({ loaderData }) {\n              return (\n                <>\n                  <p data-root>{loaderData}</p>\n                  <Outlet/>\n                </>\n              );\n            }          `,\n            \"app/routes/parent.tsx\": js`\n            import { Link, Form, Outlet } from 'react-router';\n            export async function clientLoader() {\n              return \"PARENT DATA\"\n            }\n            export function clientAction() {\n              return \"PARENT ACTION\"\n            }\n            export default function Parent({ loaderData, actionData }) {\n              return (\n                <>\n                  <p data-parent>{loaderData}</p>\n                  {actionData ? <p data-parent-action>{actionData}</p> : null}\n                  <Link to=\"/parent/child\">Go to child</Link>\n                  <Form method=\"post\" action=\"/parent\">\n                    <button type=\"submit\">Submit</button>\n                  </Form>\n                  <Form method=\"post\" action=\"/parent/child\">\n                    <button type=\"submit\">Submit child</button>\n                  </Form>\n                  <Outlet />\n                </>\n              );\n            }\n          `,\n            \"app/routes/parent.child.tsx\": js`\n            import { Link, Form } from 'react-router';\n            export function loader() {\n              return \"CHILD DATA\"\n            }\n            export function clientAction() {\n              return \"CHILD ACTION\"\n            }\n            export default function Child({ loaderData, actionData }) {\n              return (\n                <>\n                  <p data-child>{loaderData}</p>\n                  {actionData ? <p data-child-action>{actionData}</p> : null}\n                  <Link to=\"/parent\">Go to parent</Link>\n                  <Form method=\"post\" action=\"/parent/child\">\n                    <button type=\"submit\">Submit</button>\n                  </Form>\n                  <Form method=\"post\" action=\"/parent\">\n                    <button type=\"submit\">Submit parent</button>\n                  </Form>\n                </>\n              );\n            }\n          `,\n          },\n        });\n        appFixture = await createAppFixture(fixture);\n\n        let requests = captureRequests(page);\n        let app = new PlaywrightFixture(appFixture, page);\n        await app.goto(\"/parent\", true);\n        await expect(page.getByText(\"ROOT DATA\")).toBeVisible();\n        await expect(page.getByText(\"PARENT DATA\")).toBeVisible();\n\n        await app.clickLink(\"/parent/child\");\n        await expect(page.getByText(\"CHILD DATA\")).toBeVisible();\n\n        await app.goBack();\n        await expect(page.getByText(\"PARENT DATA\")).toBeVisible();\n        await expect(page.getByText(\"CHILD DATA\")).not.toBeVisible();\n\n        await app.clickSubmitButton(\"/parent/child\");\n        await expect(page.getByText(\"PARENT DATA\")).toBeVisible();\n        await expect(page.getByText(\"CHILD ACTION\")).toBeVisible();\n        await expect(page.getByText(\"CHILD DATA\")).toBeVisible();\n\n        await app.clickSubmitButton(\"/parent\");\n        await expect(page.getByText(\"PARENT ACTION\")).toBeVisible();\n        await expect(page.getByText(\"PARENT DATA\")).toBeVisible();\n        await expect(page.getByText(\"CHILD DATA\")).not.toBeVisible();\n\n        // Initial navigation and submission from /parent\n        expect(requests).toEqual([\"/parent/child.data\", \"/parent/child.data\"]);\n        while (requests.length) requests.pop();\n\n        await app.goto(\"/parent/child\", true);\n        await expect(page.getByText(\"PARENT DATA\")).toBeVisible();\n        await expect(page.getByText(\"CHILD DATA\")).toBeVisible();\n\n        await app.clickLink(\"/parent\");\n        await expect(page.getByText(\"PARENT DATA\")).toBeVisible();\n        await expect(page.getByText(\"CHILD DATA\")).not.toBeVisible();\n\n        await app.clickSubmitButton(\"/parent/child\");\n        await expect(page.getByText(\"PARENT DATA\")).toBeVisible();\n        await expect(page.getByText(\"CHILD ACTION\")).toBeVisible();\n        await expect(page.getByText(\"CHILD DATA\")).toBeVisible();\n\n        await app.clickSubmitButton(\"/parent\");\n        await expect(page.getByText(\"PARENT ACTION\")).toBeVisible();\n        await expect(page.getByText(\"PARENT DATA\")).toBeVisible();\n        await expect(page.getByText(\"CHILD DATA\")).not.toBeVisible();\n\n        // Submission from /parent\n        expect(requests).toEqual([\"/parent/child.data\"]);\n      });\n\n      test(\"Navigates prerender pages when params exist\", async ({ page }) => {\n        fixture = await createFixture({\n          prerender: true,\n          files: {\n            \"react-router.config.ts\": reactRouterConfig({\n              ssr: false, // turn off fog of war since we're serving with a static server\n              prerender: [\"/\", \"/page\", \"/param/1\", \"/param/2\"],\n            }),\n            \"vite.config.ts\": files[\"vite.config.ts\"],\n            \"app/root.tsx\": js`\n            import * as React from \"react\";\n            import { Link, Outlet, Scripts, useNavigation } from \"react-router\";\n\n            export function Layout({ children }) {\n              let navigation = useNavigation();\n              return (\n                <html lang=\"en\">\n                  <head />\n                  <body>\n                    <nav>\n                      <Link to=\"/page\">Page</Link><br/>\n                      <Link to=\"/param/1\">Param 1</Link><br/>\n                      <Link to=\"/param/2\">Param 2</Link><br/>\n                    </nav>\n                    <p id={\"navigation-\" + navigation.state}>{navigation.state}</p>\n                    {children}\n                    <Scripts />\n                  </body>\n                </html>\n              );\n            }\n\n            export default function Root({ loaderData }) {\n              return <Outlet />\n            }\n          `,\n            \"app/routes/_index.tsx\": js`\n            export default function Index() {\n              return <h1 data-index>Index</h1>\n            }\n          `,\n            \"app/routes/page.tsx\": js`\n            export function loader() {\n              return \"PAGE DATA\"\n            }\n            export default function Page({ loaderData }) {\n              return <h1 data-page>{loaderData}</h1>;\n            }\n          `,\n            \"app/routes/param.$id.tsx\": js`\n            export function loader({ params }) {\n              return params.id;\n            }\n            export default function Page({ loaderData }) {\n              return <h1 data-param={loaderData}>Param {loaderData}</h1>;\n            }\n          `,\n          },\n        });\n        appFixture = await createAppFixture(fixture);\n\n        let requests = captureRequests(page);\n        let app = new PlaywrightFixture(appFixture, page);\n        await app.goto(\"/\", true);\n        await page.waitForSelector(\"[data-index]\");\n\n        await app.clickLink(\"/page\");\n        await page.waitForSelector(\"[data-page]\");\n        expect(await (await page.$(\"[data-page]\"))?.innerText()).toBe(\n          \"PAGE DATA\",\n        );\n        expect(requests).toEqual([\"/page.data\"]);\n        clearRequests(requests);\n\n        await app.clickLink(\"/page\");\n        await page.waitForSelector(\"#navigation-idle\");\n        expect(await (await page.$(\"[data-page]\"))?.innerText()).toBe(\n          \"PAGE DATA\",\n        );\n        // No revalidation since page.data is static\n        expect(requests).toEqual([]);\n\n        await app.clickLink(\"/param/1\");\n        await page.waitForSelector('[data-param=\"1\"]');\n        expect(await (await page.$(\"[data-param]\"))?.innerText()).toBe(\n          \"Param 1\",\n        );\n        expect(requests).toEqual([\"/param/1.data\"]);\n        clearRequests(requests);\n\n        await app.clickLink(\"/param/2\");\n        await page.waitForSelector('[data-param=\"2\"]');\n        expect(await (await page.$(\"[data-param]\"))?.innerText()).toBe(\n          \"Param 2\",\n        );\n        expect(requests).toEqual([\"/param/2.data\"]);\n        clearRequests(requests);\n\n        await app.clickLink(\"/page\");\n        await page.waitForSelector(\"[data-page]\");\n        expect(await (await page.$(\"[data-page]\"))?.innerText()).toBe(\n          \"PAGE DATA\",\n        );\n        expect(requests).toEqual([\"/page.data\"]);\n      });\n\n      test(\"Navigates prerendered multibyte path routes\", async ({ page }) => {\n        fixture = await createFixture({\n          prerender: true,\n          files: {\n            \"react-router.config.ts\": reactRouterConfig({\n              ssr: false, // turn off fog of war since we're serving with a static server\n              prerender: [\"/\", \"/page\", \"/ページ\"],\n            }),\n            \"vite.config.ts\": files[\"vite.config.ts\"],\n            \"app/root.tsx\": js`\n            import * as React from \"react\";\n            import { Link, Outlet, Scripts } from \"react-router\";\n\n            export function Layout({ children }) {\n              return (\n                <html lang=\"en\">\n                  <head />\n                  <body>\n                    <nav>\n                      <Link to=\"/page\">Page</Link><br/>\n                      <Link to=\"/ページ\">ページ</Link><br/>\n                    </nav>\n                    {children}\n                    <Scripts />\n                  </body>\n                </html>\n              );\n            }\n\n            export default function Root({ loaderData }) {\n              return <Outlet />\n            }\n          `,\n            \"app/routes/_index.tsx\": js`\n            export default function Index() {\n              return <h1 data-index>Index</h1>\n            }\n          `,\n            \"app/routes/page.tsx\": js`\n            export function loader() {\n              return \"PAGE DATA\"\n            }\n            export default function Page({ loaderData }) {\n              return <h1 data-page>{loaderData}</h1>;\n            }\n          `,\n            \"app/routes/ページ.tsx\": js`\n            export function loader() {\n              return \"ページ データ\";\n            }\n            export default function Page({ loaderData }) {\n              return <h1 data-multibyte-page>{loaderData}</h1>;\n            }\n          `,\n          },\n        });\n        appFixture = await createAppFixture(fixture);\n\n        let encodedMultibytePath = encodeURIComponent(\"ページ\");\n        let requests = captureRequests(page);\n        let app = new PlaywrightFixture(appFixture, page);\n        await app.goto(\"/\", true);\n        await page.waitForSelector(\"[data-index]\");\n\n        await app.clickLink(\"/page\");\n        await page.waitForSelector(\"[data-page]\");\n        expect(await (await page.$(\"[data-page]\"))?.innerText()).toBe(\n          \"PAGE DATA\",\n        );\n        expect(requests).toEqual([\"/page.data\"]);\n        clearRequests(requests);\n\n        await app.clickLink(\"/ページ\");\n        await page.waitForSelector(\"[data-multibyte-page]\");\n        expect(await (await page.$(\"[data-multibyte-page]\"))?.innerText()).toBe(\n          \"ページ データ\",\n        );\n        expect(requests).toEqual([`/${encodedMultibytePath}.data`]);\n      });\n\n      test(\"Returns a 404 if navigating to a non-prerendered param value\", async ({\n        page,\n      }) => {\n        fixture = await createFixture({\n          prerender: true,\n          files: {\n            \"react-router.config.ts\": reactRouterConfig({\n              ssr: false, // turn off fog of war since we're serving with a static server\n              prerender: [\"/param/1\"],\n            }),\n            \"vite.config.ts\": files[\"vite.config.ts\"],\n            \"app/root.tsx\": js`\n            import * as React from \"react\";\n            import { Link, Outlet, Scripts, useNavigation } from \"react-router\";\n\n            export function Layout({ children }) {\n              let navigation = useNavigation();\n              return (\n                <html lang=\"en\">\n                  <head />\n                  <body>\n                    <nav>\n                      <Link to=\"/page\">Page</Link><br/>\n                      <Link to=\"/param/1\">Param 1</Link><br/>\n                      <Link to=\"/param/404\">Param 404</Link><br/>\n                    </nav>\n                    <p id={\"navigation-\" + navigation.state}>{navigation.state}</p>\n                    {children}\n                    <Scripts />\n                  </body>\n                </html>\n              );\n            }\n\n            export default function Root({ loaderData }) {\n              return <Outlet />\n            }\n          `,\n            \"app/routes/_index.tsx\": js`\n            export default function Index() {\n              return <h1 data-index>Index</h1>\n            }\n          `,\n            \"app/routes/param.$id.tsx\": js`\n            export function loader({ params }) {\n              return params.id;\n            }\n            export default function Page({ loaderData }) {\n              return <h1 data-param={loaderData}>Param {loaderData}</h1>;\n            }\n\n            export function ErrorBoundary({ error }) {\n              return <h1 data-error={error.status}>{error.status}</h1>;\n            }\n            `,\n          },\n        });\n        appFixture = await createAppFixture(fixture);\n\n        let requests = captureRequests(page);\n        let app = new PlaywrightFixture(appFixture, page);\n        await app.goto(\"/\", true);\n        await page.waitForSelector(\"[data-index]\");\n\n        await app.clickLink(\"/param/1\");\n        await page.waitForSelector('[data-param=\"1\"]');\n        expect(await (await page.$(\"[data-param]\"))?.innerText()).toBe(\n          \"Param 1\",\n        );\n        expect(requests).toEqual([\"/param/1.data\"]);\n        clearRequests(requests);\n\n        await app.clickLink(\"/param/404\");\n        await page.waitForSelector('[data-error=\"404\"]');\n        expect(requests).toEqual([\"/param/404.data\"]);\n      });\n\n      test(\"Navigates to prerendered parent with clientLoader calling loader\", async ({\n        page,\n      }) => {\n        fixture = await createFixture({\n          prerender: true,\n          files: {\n            \"react-router.config.ts\": reactRouterConfig({\n              ssr: false,\n              prerender: [\"/\", \"/parent\"],\n            }),\n            \"vite.config.ts\": files[\"vite.config.ts\"],\n            \"app/root.tsx\": js`\n            import * as React from \"react\";\n            import { Link, Outlet, Scripts } from \"react-router\";\n\n            export function Layout({ children }) {\n              return (\n                <html lang=\"en\">\n                  <head />\n                  <body>\n                    {children}\n                    <Scripts />\n                  </body>\n                </html>\n              );\n            }\n\n            export default function Root({ loaderData }) {\n              return (\n                <>\n                  <Link to=\"/parent\">Go to parent</Link>\n                  <Outlet/>\n                </>\n              );\n            }\n\n            export function HydrateFallback() {\n              return <p>Loading...</p>;\n            }\n          `,\n            \"app/routes/parent.tsx\": js`\n            import { Link, Form, Outlet } from 'react-router';\n            export async function loader() {\n              return \"PARENT DATA\"\n            }\n            export async function clientLoader({ serverLoader }) {\n              let str = await serverLoader();\n              return str + \" - CLIENT\"\n            }\n            export function clientAction() {\n              return \"PARENT ACTION\"\n            }\n            export default function Parent({ loaderData, actionData }) {\n              return <p data-parent>{loaderData}</p>;\n            }\n          `,\n          },\n        });\n        appFixture = await createAppFixture(fixture);\n\n        let requests = captureRequests(page);\n        let app = new PlaywrightFixture(appFixture, page);\n        await app.goto(\"/\", true);\n        await expect(page.getByText(\"Go to parent\")).toBeVisible();\n\n        await app.clickLink(\"/parent\");\n        await expect(page.getByText(\"PARENT DATA - CLIENT\")).toBeVisible();\n\n        expect(requests).toEqual([\"/parent.data?_routes=routes%2Fparent\"]);\n      });\n\n      test(\"Handles 404s on data requests\", async ({ page }) => {\n        fixture = await createFixture({\n          prerender: true,\n          files: {\n            \"react-router.config.ts\": reactRouterConfig({\n              ssr: false, // turn off fog of war since we're serving with a static server\n              prerender: [\"/\", \"/slug\"],\n            }),\n            // Just bring in the root instead of all `files` since we can't have\n            // loaders in non-prerendered routes\n            \"app/root.tsx\": files[\"app/root.tsx\"],\n            \"app/routes/$slug.tsx\": js`\n            import * as React  from \"react\";\n            import { useLoaderData } from \"react-router\";\n\n            export async function loader() {\n              return null;\n            }\n\n            export default function Component() {\n              return <h2>Slug</h2>\n            }\n          `,\n          },\n        });\n        appFixture = await createAppFixture(fixture);\n\n        let requests = captureRequests(page);\n        let app = new PlaywrightFixture(appFixture, page);\n        await app.goto(\"/\");\n        await page.waitForSelector(\"[data-mounted]\");\n        await app.clickLink(\"/not-found\");\n        await page.waitForSelector(\"[data-error]:has-text('404 Not Found')\");\n        expect(requests).toEqual([\"/not-found.data\"]);\n      });\n\n      test(\"Handles redirects in prerendered pages\", async ({ page }) => {\n        fixture = await createFixture({\n          prerender: true,\n          files: {\n            ...files,\n            \"react-router.config.ts\": reactRouterConfig({\n              ssr: false, // turn off fog of war since we're serving with a static server\n              prerender: true,\n            }),\n            \"app/routes/redirect.tsx\": js`\n            import { redirect } from \"react-router\"\n            export function loader() {\n              return redirect('/target', 301);\n            }\n            export default function Component() {\n              <h1>Nope</h1>\n            }\n          `,\n            \"app/routes/target.tsx\": js`\n            export default function Component() {\n              return <h1 id=\"target\">Target</h1>\n            }\n          `,\n          },\n        });\n\n        appFixture = await createAppFixture(fixture);\n\n        // Document loads\n        let requests = captureRequests(page);\n        let app = new PlaywrightFixture(appFixture, page);\n        await app.goto(\"/redirect\");\n        await page.waitForSelector(\"#target\");\n        expect(requests).toEqual([]);\n\n        // Client side navigations\n        await app.goto(\"/\", true);\n        app.clickLink(\"/redirect\");\n        await page.waitForSelector(\"#target\");\n        expect(requests).toEqual([\"/redirect.data\"]);\n      });\n\n      test(\"Navigates across SPA/prerender pages when starting from a SPA page (w/basename)\", async ({\n        page,\n      }) => {\n        fixture = await createFixture({\n          prerender: true,\n          files: {\n            \"react-router.config.ts\": reactRouterConfig({\n              ssr: false, // turn off fog of war since we're serving with a static server\n              prerender: [\"/page\"],\n              basename: \"/base\",\n            }),\n            \"vite.config.ts\": files[\"vite.config.ts\"],\n            \"app/root.tsx\": js`\n            import * as React from \"react\";\n            import { Outlet, Scripts } from \"react-router\";\n\n            export function Layout({ children }) {\n              return (\n                <html lang=\"en\">\n                  <head />\n                  <body>\n                    {children}\n                    <Scripts />\n                  </body>\n                </html>\n              );\n            }\n\n            export default function Root({ loaderData }) {\n              return <Outlet />\n            }\n          `,\n            \"app/routes/_index.tsx\": js`\n            import { Link } from 'react-router';\n            export default function Index() {\n              return <Link to=\"/page\">Go to page</Link>\n            }\n          `,\n            \"app/routes/page.tsx\": js`\n            import { Link, Form } from 'react-router';\n            export async function loader() {\n              return \"PAGE DATA\"\n            }\n            let count = 0;\n            export function clientAction() {\n              return \"PAGE ACTION \" + (++count)\n            }\n            export default function Page({ loaderData, actionData }) {\n              return (\n                <>\n                  <p data-page>{loaderData}</p>\n                  {actionData ? <p data-page-action>{actionData}</p> : null}\n                  <Link to=\"/page2\">Go to page2</Link>\n                  <Form method=\"post\" action=\"/page\">\n                    <button type=\"submit\">Submit</button>\n                  </Form>\n                  <Form method=\"post\" action=\"/page2\">\n                    <button type=\"submit\">Submit /page2</button>\n                  </Form>\n                </>\n              );\n            }\n          `,\n            \"app/routes/page2.tsx\": js`\n            import { Form } from 'react-router';\n            export function clientLoader() {\n              return \"PAGE2 DATA\"\n            }\n            let count = 0;\n            export function clientAction() {\n              return \"PAGE2 ACTION \" + (++count)\n            }\n            export default function Page({ loaderData, actionData }) {\n              return (\n                <>\n                  <p data-page2>{loaderData}</p>\n                  {actionData ? <p data-page2-action>{actionData}</p> : null}\n                  <Form method=\"post\" action=\"/page\">\n                    <button type=\"submit\">Submit</button>\n                  </Form>\n                  <Form method=\"post\" action=\"/page2\">\n                    <button type=\"submit\">Submit /page2</button>\n                  </Form>\n                </>\n              );\n            }\n          `,\n          },\n        });\n        appFixture = await createAppFixture(fixture);\n\n        let requests = captureRequests(page);\n        let app = new PlaywrightFixture(appFixture, page);\n\n        await app.goto(\"/base\", true);\n        await page.waitForSelector('a[href=\"/base/page\"]');\n\n        await app.clickLink(\"/base/page\");\n        await page.waitForSelector(\"[data-page]\");\n        expect(await (await page.$(\"[data-page]\"))?.innerText()).toBe(\n          \"PAGE DATA\",\n        );\n        expect(requests).toEqual([\"/base/page.data\"]);\n        clearRequests(requests);\n\n        await app.clickSubmitButton(\"/base/page\");\n        await page.waitForSelector(\"[data-page-action]\");\n        expect(await (await page.$(\"[data-page-action]\"))?.innerText()).toBe(\n          \"PAGE ACTION 1\",\n        );\n        // No revalidation after submission to self\n        expect(requests).toEqual([]);\n\n        await app.clickLink(\"/base/page2\");\n        await page.waitForSelector(\"[data-page2]\");\n        expect(await (await page.$(\"[data-page2]\"))?.innerText()).toBe(\n          \"PAGE2 DATA\",\n        );\n        expect(requests).toEqual([]);\n\n        await app.clickSubmitButton(\"/base/page2\");\n        await page.waitForSelector(\"[data-page2-action]\");\n        expect(await (await page.$(\"[data-page2-action]\"))?.innerText()).toBe(\n          \"PAGE2 ACTION 1\",\n        );\n        expect(requests).toEqual([]);\n\n        await app.clickSubmitButton(\"/base/page\");\n        await page.waitForSelector(\"[data-page-action]\");\n        expect(await (await page.$(\"[data-page-action]\"))?.innerText()).toBe(\n          \"PAGE ACTION 2\",\n        );\n        expect(requests).toEqual([\"/base/page.data\"]);\n        clearRequests(requests);\n\n        await app.clickSubmitButton(\"/base/page2\");\n        await page.waitForSelector(\"[data-page2-action]\");\n        expect(await (await page.$(\"[data-page2-action]\"))?.innerText()).toBe(\n          \"PAGE2 ACTION 2\",\n        );\n        expect(requests).toEqual([]);\n      });\n\n      test(\"Navigates across SPA/prerender pages when starting from a prerendered page (w/basename)\", async ({\n        page,\n      }) => {\n        fixture = await createFixture({\n          prerender: true,\n          files: {\n            \"react-router.config.ts\": reactRouterConfig({\n              ssr: false, // turn off fog of war since we're serving with a static server\n              prerender: [\"/\", \"/page\"],\n              basename: \"/base\",\n            }),\n            \"vite.config.ts\": files[\"vite.config.ts\"],\n            \"app/root.tsx\": js`\n            import * as React from \"react\";\n            import { Outlet, Scripts } from \"react-router\";\n\n            export function Layout({ children }) {\n              return (\n                <html lang=\"en\">\n                  <head />\n                  <body>\n                    {children}\n                    <Scripts />\n                  </body>\n                </html>\n              );\n            }\n\n            export default function Root({ loaderData }) {\n              return <Outlet />;\n            }\n          `,\n            \"app/routes/_index.tsx\": js`\n            import { Link } from 'react-router';\n            export default function Index() {\n              return <Link to=\"/page\">Go to page</Link>\n            }\n          `,\n            \"app/routes/page.tsx\": js`\n            import { Link, Form } from 'react-router';\n            export async function loader() {\n              return \"PAGE DATA\"\n            }\n            let count = 0;\n            export function clientAction() {\n              return \"PAGE ACTION \" + (++count)\n            }\n            export default function Page({ loaderData, actionData }) {\n              return (\n                <>\n                  <p data-page>{loaderData}</p>\n                  {actionData ? <p data-page-action>{actionData}</p> : null}\n                  <Link to=\"/page2\">Go to page2</Link>\n                  <Form method=\"post\" action=\"/page\">\n                    <button type=\"submit\">Submit</button>\n                  </Form>\n                  <Form method=\"post\" action=\"/page2\">\n                    <button type=\"submit\">Submit /page2</button>\n                  </Form>\n                </>\n              );\n            }\n          `,\n            \"app/routes/page2.tsx\": js`\n            import { Form } from 'react-router';\n            export function clientLoader() {\n              return \"PAGE2 DATA\"\n            }\n            let count = 0;\n            export function clientAction() {\n              return \"PAGE2 ACTION \" + (++count)\n            }\n            export default function Page({ loaderData, actionData }) {\n              return (\n                <>\n                  <p data-page2>{loaderData}</p>\n                  {actionData ? <p data-page2-action>{actionData}</p> : null}\n                  <Form method=\"post\" action=\"/page\">\n                    <button type=\"submit\">Submit</button>\n                  </Form>\n                  <Form method=\"post\" action=\"/page2\">\n                    <button type=\"submit\">Submit /page2</button>\n                  </Form>\n                </>\n              );\n            }\n          `,\n          },\n        });\n        appFixture = await createAppFixture(fixture);\n\n        let requests = captureRequests(page);\n        let app = new PlaywrightFixture(appFixture, page);\n        await app.goto(\"/base\", true);\n        await page.waitForSelector('a[href=\"/base/page\"]');\n\n        await app.clickLink(\"/base/page\");\n        await page.waitForSelector(\"[data-page]\");\n        expect(await (await page.$(\"[data-page]\"))?.innerText()).toBe(\n          \"PAGE DATA\",\n        );\n        expect(requests).toEqual([\"/base/page.data\"]);\n        clearRequests(requests);\n\n        await app.clickSubmitButton(\"/base/page\");\n        await page.waitForSelector(\"[data-page-action]\");\n        expect(await (await page.$(\"[data-page-action]\"))?.innerText()).toBe(\n          \"PAGE ACTION 1\",\n        );\n        // No revalidation after submission to self\n        expect(requests).toEqual([]);\n\n        await app.clickLink(\"/base/page2\");\n        await page.waitForSelector(\"[data-page2]\");\n        expect(await (await page.$(\"[data-page2]\"))?.innerText()).toBe(\n          \"PAGE2 DATA\",\n        );\n        expect(requests).toEqual([]);\n\n        await app.clickSubmitButton(\"/base/page2\");\n        await page.waitForSelector(\"[data-page2-action]\");\n        expect(await (await page.$(\"[data-page2-action]\"))?.innerText()).toBe(\n          \"PAGE2 ACTION 1\",\n        );\n        expect(requests).toEqual([]);\n\n        await app.clickSubmitButton(\"/base/page\");\n        await page.waitForSelector(\"[data-page-action]\");\n        expect(await (await page.$(\"[data-page-action]\"))?.innerText()).toBe(\n          \"PAGE ACTION 2\",\n        );\n        expect(requests).toEqual([\"/base/page.data\"]);\n        clearRequests(requests);\n\n        await app.clickSubmitButton(\"/base/page2\");\n        await page.waitForSelector(\"[data-page2-action]\");\n        expect(await (await page.$(\"[data-page2-action]\"))?.innerText()).toBe(\n          \"PAGE2 ACTION 2\",\n        );\n        expect(requests).toEqual([]);\n      });\n    });\n  });\n}\n"
  },
  {
    "path": "integration/vite-presets-test.ts",
    "content": "import fs from \"node:fs/promises\";\nimport * as path from \"node:path\";\nimport URL from \"node:url\";\nimport { expect } from \"@playwright/test\";\nimport { normalizePath } from \"vite\";\nimport dedent from \"dedent\";\n\nimport {\n  build,\n  test,\n  createProject,\n  viteMajorTemplates,\n  viteConfig,\n} from \"./helpers/vite.js\";\n\nconst js = String.raw;\n\nconst files = {\n  \"react-router.config.ts\": dedent(js`\n    import fs from \"node:fs/promises\";\n    import serializeJs from \"serialize-javascript\";\n\n    let isDeepFrozen = (obj: any) =>\n      Object.isFrozen(obj) &&\n      Object.keys(obj).every(\n        prop => typeof obj[prop] !== 'object' || obj[prop] === null || isDeepFrozen(obj[prop])\n      );\n\n    export default {\n      // Ensure user config takes precedence over preset config\n      appDirectory: \"app\",\n\n      presets: [\n        // Ensure user config is passed to reactRouterConfig hook\n        {\n          name: \"test-preset\",\n          reactRouterConfig: async ({ reactRouterUserConfig: { presets, ...restUserConfig } }) => {\n            if (!Array.isArray(presets)) {\n              throw new Error(\"React Router user config doesn't have presets array.\");\n            }\n\n            let expected = JSON.stringify({ appDirectory: \"app\"});\n            let actual = JSON.stringify(restUserConfig);\n\n            if (actual !== expected) {\n              throw new Error([\n                \"React Router user config wasn't passed to reactRouterConfig hook.\",\n                \"Expected: \" + expected,\n                \"Actual: \" + actual,\n              ].join(\" \"));\n            }\n\n            return {};\n          },\n        },\n\n        // Ensure preset config takes lower precedence than user config\n        {\n          name: \"test-preset\",\n          reactRouterConfig: async () => ({\n            appDirectory: \"INCORRECT_APP_DIR\", // This is overridden by the user config further down this file\n          }),\n        },\n        {\n          name: \"test-preset\",\n          reactRouterConfigResolved: async ({ reactRouterConfig }) => {\n            if (reactRouterConfig.appDirectory.includes(\"INCORRECT_APP_DIR\")) {\n              throw new Error(\"React Router preset config wasn't overridden with user config\");\n            }\n          }\n        },\n\n        // Ensure config presets are merged in the correct order\n        {\n          name: \"test-preset\",\n          reactRouterConfig: async () => ({\n            buildDirectory: \"INCORRECT_BUILD_DIR\",\n          }),\n        },\n        {\n          name: \"test-preset\",\n          reactRouterConfig: async () => ({\n            buildDirectory: \"build\",\n          }),\n        },\n\n        // Ensure reactRouterConfig is called with a frozen user config\n        {\n          name: \"test-preset\",\n          reactRouterConfig: async ({ reactRouterUserConfig }) => {\n            await fs.writeFile(\"PRESET_REACT_ROUTER_CONFIG_META.json\", JSON.stringify({\n              reactRouterUserConfigFrozen: isDeepFrozen(reactRouterUserConfig),\n            }), \"utf-8\");\n          }\n        },\n\n        // Ensure reactRouterConfigResolved is called with a frozen config\n        {\n          name: \"test-preset\",\n          reactRouterConfigResolved: async ({ reactRouterConfig }) => {\n            await fs.writeFile(\"PRESET_REACT_ROUTER_CONFIG_RESOLVED_META.json\", JSON.stringify({\n              reactRouterUserConfigFrozen: isDeepFrozen(reactRouterConfig),\n            }), \"utf-8\");\n          }\n        },\n\n        // Ensure presets can set serverBundles option (this is critical for Vercel support)\n        {\n          name: \"test-preset\",\n          reactRouterConfig: async () => ({\n            serverBundles() {\n              return \"preset-server-bundle-id\";\n            },\n          }),\n        },\n\n        // Ensure presets can set future flags\n        {\n          name: \"test-preset\",\n          reactRouterConfig: async () => ({\n            future: {\n              v8_middleware: true,\n              unstable_optimizeDeps: true,\n            },\n          }),\n        },\n\n        // Ensure presets can set buildEnd option (this is critical for Vercel support)\n        {\n          name: \"test-preset\",\n          reactRouterConfig: async () => ({\n            async buildEnd(buildEndArgs) {\n              let { viteConfig, buildManifest, reactRouterConfig } = buildEndArgs;\n\n              await fs.writeFile(\n                \"BUILD_END_META.js\",\n                [\n                  \"export const keys = \" + JSON.stringify(Object.keys(buildEndArgs)) + \";\",\n                  \"export const buildManifest = \" + serializeJs(buildManifest, { space: 2, unsafe: true }) + \";\",\n                  \"export const reactRouterConfig = \" + serializeJs(reactRouterConfig, { space: 2, unsafe: true }) + \";\",\n                  \"export const assetsDir = \" + JSON.stringify(viteConfig.build.assetsDir) + \";\",\n                  \"export const futureFlags = \" + JSON.stringify(reactRouterConfig.future) + \";\",\n                ].join(\"\\\\n\"),\n                \"utf-8\"\n              );\n            },\n          }),\n        },\n      ],\n    }\n  `),\n  \"vite.config.ts\": await viteConfig.basic({\n    assetsDir: \"custom-assets-dir\",\n  }),\n};\n\ntest.describe(\"Vite / presets\", async () => {\n  viteMajorTemplates.forEach(({ templateName, templateDisplayName }) => {\n    test(templateDisplayName, async () => {\n      let cwd = await createProject(files, templateName);\n      let { status, stderr } = build({ cwd });\n      expect(stderr.toString()).toBeFalsy();\n      expect(status).toBe(0);\n\n      function pathStartsWithCwd(pathname: string) {\n        return normalizePath(pathname).startsWith(normalizePath(cwd));\n      }\n\n      function relativeToCwd(pathname: string) {\n        return normalizePath(path.relative(cwd, pathname));\n      }\n\n      let buildEndArgsMeta: any = await import(\n        URL.pathToFileURL(path.join(cwd, \"BUILD_END_META.js\")).href\n      );\n\n      let { reactRouterConfig } = buildEndArgsMeta;\n\n      // Smoke test Vite config\n      expect(buildEndArgsMeta.assetsDir).toBe(\"custom-assets-dir\");\n\n      // Before rewriting to relative paths, assert that paths are absolute within cwd\n      expect(pathStartsWithCwd(reactRouterConfig.buildDirectory)).toBe(true);\n\n      // Rewrite path args to be relative and normalized for snapshot test\n      reactRouterConfig.buildDirectory = relativeToCwd(\n        reactRouterConfig.buildDirectory,\n      );\n\n      // Ensure preset configs are merged in correct order, resulting in the correct build directory\n      expect(reactRouterConfig.buildDirectory).toBe(\"build\");\n\n      // Ensure preset config takes lower precedence than user config\n      expect(reactRouterConfig.serverModuleFormat).toBe(\"esm\");\n\n      // Ensure `reactRouterConfig` is called with a frozen user config\n      expect(\n        JSON.parse(\n          await fs.readFile(\n            path.join(cwd, \"PRESET_REACT_ROUTER_CONFIG_META.json\"),\n            \"utf-8\",\n          ),\n        ),\n      ).toEqual({\n        reactRouterUserConfigFrozen: true,\n      });\n\n      // Ensure `reactRouterConfigResolved` is called with a frozen config\n      expect(\n        JSON.parse(\n          await fs.readFile(\n            path.join(cwd, \"PRESET_REACT_ROUTER_CONFIG_RESOLVED_META.json\"),\n            \"utf-8\",\n          ),\n        ),\n      ).toEqual({\n        reactRouterUserConfigFrozen: true,\n      });\n\n      // Snapshot the buildEnd args keys\n      expect(buildEndArgsMeta.keys).toEqual([\n        \"buildManifest\",\n        \"reactRouterConfig\",\n        \"viteConfig\",\n      ]);\n\n      // Smoke test the resolved config\n      expect(Object.keys(reactRouterConfig)).toEqual([\n        \"appDirectory\",\n        \"basename\",\n        \"buildDirectory\",\n        \"buildEnd\",\n        \"future\",\n        \"prerender\",\n        \"routes\",\n        \"routeDiscovery\",\n        \"serverBuildFile\",\n        \"serverBundles\",\n        \"serverModuleFormat\",\n        \"ssr\",\n        \"allowedActionOrigins\",\n        \"unstable_routeConfig\",\n      ]);\n\n      // Ensure future flags from presets are properly merged\n      expect(buildEndArgsMeta.futureFlags).toEqual({\n        unstable_optimizeDeps: true,\n        unstable_subResourceIntegrity: false,\n        unstable_trailingSlashAwareDataRequests: false,\n        unstable_previewServerPrerendering: false,\n        v8_middleware: true,\n        v8_splitRouteModules: false,\n        v8_viteEnvironmentApi: false,\n      });\n\n      // Ensure we get a valid build manifest\n      expect(buildEndArgsMeta.buildManifest).toEqual({\n        routeIdToServerBundleId: {\n          \"routes/_index\": \"preset-server-bundle-id\",\n        },\n        routes: {\n          root: {\n            file: \"app/root.tsx\",\n            id: \"root\",\n            path: \"\",\n          },\n          \"routes/_index\": {\n            file: \"app/routes/_index.tsx\",\n            id: \"routes/_index\",\n            index: true,\n            parentId: \"root\",\n          },\n        },\n        serverBundles: {\n          \"preset-server-bundle-id\": {\n            file: \"build/server/preset-server-bundle-id/index.js\",\n            id: \"preset-server-bundle-id\",\n          },\n        },\n      });\n    });\n  });\n});\n"
  },
  {
    "path": "integration/vite-preview-test.ts",
    "content": "import { expect } from \"@playwright/test\";\nimport dedent from \"dedent\";\n\nimport {\n  reactRouterConfig,\n  viteConfig,\n  test,\n  type Files,\n} from \"./helpers/vite.js\";\n\nconst tsx = dedent;\n\ntest.describe(\"Vite preview\", () => {\n  test(\"serves built app with vite preview\", async ({ vitePreview, page }) => {\n    const files: Files = async ({ port }) => ({\n      \"react-router.config.ts\": reactRouterConfig({\n        future: { v8_viteEnvironmentApi: true },\n      }),\n      \"vite.config.ts\": await viteConfig.basic({\n        port,\n        templateName: \"vite-6-template\",\n      }),\n      \"app/root.tsx\": tsx`\n        import { Links, Meta, Outlet, Scripts } from \"react-router\";\n\n        export default function Root() {\n          return (\n            <html lang=\"en\">\n              <head>\n                <Meta />\n                <Links />\n              </head>\n              <body>\n                <div id=\"content\">\n                  <h1>Root</h1>\n                  <Outlet />\n                </div>\n                <Scripts />\n              </body>\n            </html>\n          );\n        }\n      `,\n      \"app/routes/_index.tsx\": tsx`\n        export default function IndexRoute() {\n          return (\n            <div id=\"index\">\n              <h2 data-title>Index</h2>\n              <p data-env>Environment: production</p>\n            </div>\n          );\n        }\n      `,\n      \"app/routes/about.tsx\": tsx`\n        export default function AboutRoute() {\n          return (\n            <div id=\"about\">\n              <h2 data-title>About</h2>\n              <p>This is the about page</p>\n            </div>\n          );\n        }\n      `,\n      \"app/routes/loader-data.tsx\": tsx`\n        import { useLoaderData } from \"react-router\";\n\n        export function loader() {\n          return { message: \"Hello from loader\" };\n        }\n\n        export default function LoaderDataRoute() {\n          const { message } = useLoaderData<typeof loader>();\n          return (\n            <div id=\"loader-data\">\n              <h2 data-title>Loader Data</h2>\n              <p data-message>{message}</p>\n            </div>\n          );\n        }\n      `,\n    });\n\n    const { port } = await vitePreview(files, \"vite-6-template\");\n    await page.goto(`http://localhost:${port}/`, {\n      waitUntil: \"networkidle\",\n    });\n\n    // Ensure no errors on page load\n    expect(page.errors).toEqual([]);\n\n    await expect(page.locator(\"#index [data-title]\")).toHaveText(\"Index\");\n    await expect(page.locator(\"#index [data-env]\")).toHaveText(\n      \"Environment: production\",\n    );\n  });\n\n  test(\"handles navigation between routes\", async ({ vitePreview, page }) => {\n    const files: Files = async ({ port }) => ({\n      \"react-router.config.ts\": reactRouterConfig({\n        future: { v8_viteEnvironmentApi: true },\n      }),\n      \"vite.config.ts\": await viteConfig.basic({\n        port,\n        templateName: \"vite-6-template\",\n      }),\n      \"app/root.tsx\": tsx`\n        import { Links, Meta, Outlet, Scripts, Link } from \"react-router\";\n\n        export default function Root() {\n          return (\n            <html lang=\"en\">\n              <head>\n                <Meta />\n                <Links />\n              </head>\n              <body>\n                <div id=\"content\">\n                  <nav>\n                    <Link to=\"/\" data-link-home>Home</Link>\n                    <Link to=\"/about\" data-link-about>About</Link>\n                  </nav>\n                  <Outlet />\n                </div>\n                <Scripts />\n              </body>\n            </html>\n          );\n        }\n      `,\n      \"app/routes/_index.tsx\": tsx`\n        export default function IndexRoute() {\n          return (\n            <div id=\"index\">\n              <h2 data-title>Index</h2>\n            </div>\n          );\n        }\n      `,\n      \"app/routes/about.tsx\": tsx`\n        export default function AboutRoute() {\n          return (\n            <div id=\"about\">\n              <h2 data-title>About</h2>\n            </div>\n          );\n        }\n      `,\n    });\n\n    const { port } = await vitePreview(files, \"vite-6-template\");\n    await page.goto(`http://localhost:${port}/`, {\n      waitUntil: \"networkidle\",\n    });\n\n    expect(page.errors).toEqual([]);\n    await expect(page.locator(\"#index [data-title]\")).toHaveText(\"Index\");\n\n    // Navigate to about page\n    await page.click(\"[data-link-about]\");\n    await page.waitForLoadState(\"networkidle\");\n\n    expect(page.errors).toEqual([]);\n    await expect(page.locator(\"#about [data-title]\")).toHaveText(\"About\");\n\n    // Navigate back to home\n    await page.click(\"[data-link-home]\");\n    await page.waitForLoadState(\"networkidle\");\n\n    expect(page.errors).toEqual([]);\n    await expect(page.locator(\"#index [data-title]\")).toHaveText(\"Index\");\n  });\n\n  test(\"handles loader data correctly\", async ({ vitePreview, page }) => {\n    const files: Files = async ({ port }) => ({\n      \"react-router.config.ts\": reactRouterConfig({\n        future: { v8_viteEnvironmentApi: true },\n      }),\n      \"vite.config.ts\": await viteConfig.basic({\n        port,\n        templateName: \"vite-6-template\",\n      }),\n      \"app/root.tsx\": tsx`\n        import { Links, Meta, Outlet, Scripts } from \"react-router\";\n\n        export default function Root() {\n          return (\n            <html lang=\"en\">\n              <head>\n                <Meta />\n                <Links />\n              </head>\n              <body>\n                <div id=\"content\">\n                  <Outlet />\n                </div>\n                <Scripts />\n              </body>\n            </html>\n          );\n        }\n      `,\n      \"app/routes/_index.tsx\": tsx`\n        import { useLoaderData } from \"react-router\";\n\n        export function loader() {\n          return {\n            message: \"Hello from loader\",\n            timestamp: Date.now()\n          };\n        }\n\n        export default function IndexRoute() {\n          const { message, timestamp } = useLoaderData<typeof loader>();\n          return (\n            <div id=\"index\">\n              <h2 data-title>Index</h2>\n              <p data-message>{message}</p>\n              <p data-timestamp>{timestamp}</p>\n            </div>\n          );\n        }\n      `,\n    });\n\n    const { port } = await vitePreview(files, \"vite-6-template\");\n    await page.goto(`http://localhost:${port}/`, {\n      waitUntil: \"networkidle\",\n    });\n\n    expect(page.errors).toEqual([]);\n    await expect(page.locator(\"#index [data-title]\")).toHaveText(\"Index\");\n    await expect(page.locator(\"#index [data-message]\")).toHaveText(\n      \"Hello from loader\",\n    );\n\n    // Check that timestamp exists and is a number\n    const timestampText = await page\n      .locator(\"#index [data-timestamp]\")\n      .textContent();\n    expect(timestampText).toBeTruthy();\n    expect(Number(timestampText)).toBeGreaterThan(0);\n  });\n\n  test(\"handles direct navigation to dynamic routes\", async ({\n    vitePreview,\n    page,\n  }) => {\n    const files: Files = async ({ port }) => ({\n      \"react-router.config.ts\": reactRouterConfig({\n        future: { v8_viteEnvironmentApi: true },\n      }),\n      \"vite.config.ts\": await viteConfig.basic({\n        port,\n        templateName: \"vite-6-template\",\n      }),\n      \"app/root.tsx\": tsx`\n        import { Links, Meta, Outlet, Scripts } from \"react-router\";\n\n        export default function Root() {\n          return (\n            <html lang=\"en\">\n              <head>\n                <Meta />\n                <Links />\n              </head>\n              <body>\n                <div id=\"content\">\n                  <Outlet />\n                </div>\n                <Scripts />\n              </body>\n            </html>\n          );\n        }\n      `,\n      \"app/routes/_index.tsx\": tsx`\n        export default function IndexRoute() {\n          return <div id=\"index\"><h2>Index</h2></div>;\n        }\n      `,\n      \"app/routes/products.$id.tsx\": tsx`\n        import { useLoaderData, useParams } from \"react-router\";\n\n        export function loader({ params }: { params: { id: string } }) {\n          return {\n            productId: params.id,\n          };\n        }\n\n        export default function ProductRoute() {\n          const { productId } = useLoaderData<typeof loader>();\n          return (\n            <div id=\"product\">\n              <h2 data-title>Product Details</h2>\n              <p data-id>{productId}</p>\n              <p data-name>Product {productId}</p>\n            </div>\n          );\n        }\n      `,\n    });\n\n    const { port } = await vitePreview(files, \"vite-6-template\");\n    await page.goto(`http://localhost:${port}/products/123`, {\n      waitUntil: \"networkidle\",\n    });\n\n    expect(page.errors).toEqual([]);\n    await expect(page.locator(\"#product [data-title]\")).toHaveText(\n      \"Product Details\",\n    );\n    await expect(page.locator(\"#product [data-id]\")).toHaveText(\"123\");\n    await expect(page.locator(\"#product [data-name]\")).toHaveText(\n      \"Product 123\",\n    );\n  });\n\n  test(\"serves SPA mode app with vite preview\", async ({\n    vitePreview,\n    page,\n  }) => {\n    const files: Files = async ({ port }) => ({\n      \"react-router.config.ts\": reactRouterConfig({\n        ssr: false,\n        future: { v8_viteEnvironmentApi: true },\n      }),\n      \"vite.config.ts\": await viteConfig.basic({\n        port,\n        templateName: \"vite-6-template\",\n      }),\n      \"app/root.tsx\": tsx`\n        import { Links, Meta, Outlet, Scripts } from \"react-router\";\n\n        export default function Root() {\n          return (\n            <html lang=\"en\">\n              <head>\n                <Meta />\n                <Links />\n              </head>\n              <body>\n                <div id=\"content\">\n                  <h1>SPA Mode</h1>\n                  <Outlet />\n                </div>\n                <Scripts />\n              </body>\n            </html>\n          );\n        }\n      `,\n      \"app/routes/_index.tsx\": tsx`\n        export default function IndexRoute() {\n          return (\n            <div id=\"index\">\n              <h2 data-title>Index</h2>\n              <p data-spa-mode>SPA Mode Enabled</p>\n            </div>\n          );\n        }\n      `,\n      \"app/routes/about.tsx\": tsx`\n        export default function AboutRoute() {\n          return (\n            <div id=\"about\">\n              <h2 data-title>About</h2>\n              <p>About page in SPA mode</p>\n            </div>\n          );\n        }\n      `,\n    });\n\n    const { port } = await vitePreview(files, \"vite-6-template\");\n    await page.goto(`http://localhost:${port}/`, {\n      waitUntil: \"networkidle\",\n    });\n\n    // Ensure no errors on page load (this would fail without the fix)\n    expect(page.errors).toEqual([]);\n\n    await expect(page.locator(\"#index [data-title]\")).toHaveText(\"Index\");\n    await expect(page.locator(\"#index [data-spa-mode]\")).toHaveText(\n      \"SPA Mode Enabled\",\n    );\n  });\n});\n"
  },
  {
    "path": "integration/vite-route-added-test.ts",
    "content": "import fs from \"node:fs/promises\";\nimport path from \"node:path\";\nimport { test, expect } from \"@playwright/test\";\nimport getPort from \"get-port\";\n\nimport { createProject, dev, viteConfig } from \"./helpers/vite.js\";\n\nconst files = {\n  \"app/routes/_index.tsx\": String.raw`\n    import { useState, useEffect } from \"react\";\n    import { Link } from \"react-router\";\n\n    export default function IndexRoute() {\n      const [mounted, setMounted] = useState(false);\n      useEffect(() => {\n        setMounted(true);\n      }, []);\n\n      return (\n        <p data-mounted>Mounted: {mounted ? \"yes\" : \"no\"}</p>\n      );\n    }\n  `,\n};\n\ntest.describe(async () => {\n  let port: number;\n  let cwd: string;\n  let stop: () => void;\n\n  test.beforeAll(async () => {\n    port = await getPort();\n    cwd = await createProject({\n      \"vite.config.js\": await viteConfig.basic({ port }),\n      ...files,\n    });\n    stop = await dev({ cwd, port });\n  });\n  test.afterAll(() => stop());\n\n  test(\"Vite / dev / route added\", async ({ page, browserName }) => {\n    test.skip(\n      browserName === \"webkit\",\n      \"Safari caches too aggressively, browser manifest is cached with old routes\",\n    );\n\n    let pageErrors: Error[] = [];\n    page.on(\"pageerror\", (error) => pageErrors.push(error));\n\n    // wait for hydration to make sure initial virtual modules are loaded\n    await page.goto(`http://localhost:${port}/`, { waitUntil: \"networkidle\" });\n    await expect(page.locator(\"[data-mounted]\")).toHaveText(\"Mounted: yes\");\n\n    // add new route file\n    await fs.writeFile(\n      path.join(cwd, \"app/routes/new.tsx\"),\n      String.raw`\n        export default function Route() {\n          return (\n            <div id=\"new\">new route</div>\n          );\n        }\n      `,\n      \"utf-8\",\n    );\n\n    // client is not notified of new route addition (https://github.com/remix-run/remix/issues/7894)\n    // however server can handle new route\n    await expect\n      .poll(async () => {\n        await page.goto(`http://localhost:${port}/new`);\n        return page.getByText(\"new route\").isVisible();\n      })\n      .toBe(true);\n  });\n});\n"
  },
  {
    "path": "integration/vite-route-exports-modified-offscreen-test.ts",
    "content": "import { test, expect } from \"@playwright/test\";\nimport getPort from \"get-port\";\n\nimport {\n  createProject,\n  createEditor,\n  dev,\n  viteConfig,\n} from \"./helpers/vite.js\";\n\nconst files = {\n  \"app/routes/_index.tsx\": String.raw`\n    import { useState, useEffect } from \"react\";\n    import { Link } from \"react-router\";\n\n    export default function IndexRoute() {\n      const [mounted, setMounted] = useState(false);\n      useEffect(() => {\n        setMounted(true);\n      }, []);\n\n      return (\n        <div>\n          <p data-mounted>Mounted: {mounted ? \"yes\" : \"no\"}</p>\n          <Link to=\"/other\">/other</Link>\n        </div>\n      );\n    }\n  `,\n  \"app/routes/other.tsx\": String.raw`\n    import { useLoaderData } from \"react-router\";\n\n    export const loader = () => \"hello\";\n\n    export default function Route() {\n      const loaderData = useLoaderData();\n      return (\n        <div data-loader-data>loaderData = {JSON.stringify(loaderData)}</div>\n      );\n    }\n  `,\n};\n\ntest.describe(async () => {\n  let port: number;\n  let cwd: string;\n  let stop: () => void;\n\n  test.beforeAll(async () => {\n    port = await getPort();\n    cwd = await createProject({\n      \"vite.config.js\": await viteConfig.basic({ port }),\n      ...files,\n    });\n    stop = await dev({ cwd, port });\n  });\n  test.afterAll(() => stop());\n\n  test(\"Vite / dev / route exports modified offscreen\", async ({\n    page,\n    context,\n    browserName,\n  }) => {\n    let pageErrors: Error[] = [];\n    page.on(\"pageerror\", (error) => pageErrors.push(error));\n    let edit = createEditor(cwd);\n\n    await page.goto(`http://localhost:${port}`, { waitUntil: \"networkidle\" });\n    await expect(page.locator(\"[data-mounted]\")).toHaveText(\"Mounted: yes\");\n    expect(pageErrors).toEqual([]);\n\n    let originalContents: string;\n\n    // Removing loader export in other page should invalidate manifest\n    await edit(\"app/routes/other.tsx\", (contents) => {\n      originalContents = contents;\n      return contents.replace(/export const loader.*/, \"\");\n    });\n    // Give the server time to pick the manifest change\n    await new Promise((resolve) => setTimeout(resolve, 200));\n\n    // After browser reload, client should be aware that there's no loader on the other route\n    if (browserName === \"webkit\") {\n      // Force new page instance for webkit.\n      // Otherwise browser doesn't seem to fetch new manifest probably due to caching.\n      page = await context.newPage();\n    }\n    // In case the earlier wait wasn't enough, let the test try again\n    await expect(async () => {\n      await page.goto(`http://localhost:${port}`, { waitUntil: \"networkidle\" });\n      await expect(page.locator(\"[data-mounted]\")).toHaveText(\"Mounted: yes\");\n      await page.getByRole(\"link\", { name: \"/other\" }).click();\n      await expect(page.locator(\"[data-loader-data]\")).toHaveText(\n        \"loaderData = null\",\n      );\n    }).toPass();\n    expect(pageErrors).toEqual([]);\n\n    // Revert route to original state to check HMR works and to ensure the\n    // original file contents were valid\n    await edit(\"app/routes/other.tsx\", () => originalContents);\n    await expect(page.locator(\"[data-loader-data]\")).toHaveText(\n      'loaderData = \"hello\"',\n    );\n    expect(pageErrors).toEqual([]);\n  });\n});\n"
  },
  {
    "path": "integration/vite-server-bundles-test.ts",
    "content": "import fs from \"node:fs\";\nimport path from \"node:path\";\nimport { type Page, test, expect } from \"@playwright/test\";\nimport getPort from \"get-port\";\nimport dedent from \"dedent\";\n\nimport {\n  createProject,\n  dev,\n  build,\n  reactRouterServe,\n  viteConfig,\n} from \"./helpers/vite.js\";\n\nconst js = String.raw;\n\nconst withBundleServer = async (\n  cwd: string,\n  serverBundle: string,\n  callback: (port: number) => Promise<void>,\n): Promise<void> => {\n  let port = await getPort();\n  let stop = await reactRouterServe({ cwd, port, serverBundle });\n  await callback(port);\n  stop();\n};\n\nconst ROUTE_FILE_COMMENT = \"// THIS IS A ROUTE FILE\";\n\nfunction createRoute(path: string) {\n  return {\n    [`app/routes/${path}`]: js`\n      ${ROUTE_FILE_COMMENT}\n      import { Outlet } from \"react-router\";\n      import { useState, useEffect } from \"react\";\n\n      export default function Route() {\n        const [mounted, setMounted] = useState(false);\n        useEffect(() => {\n          setMounted(true);\n        }, []);\n        return (\n          <>\n            <div data-route-file=\"${path}\">\n              Route: ${path}\n              {mounted ? <span data-mounted> (Mounted)</span> : null}\n            </div>\n            <Outlet />\n          </>\n        );\n      }\n    `,\n  };\n}\n\nconst TEST_ROUTES = [\n  \"_index.tsx\",\n\n  // Bundle A has an index route\n  \"bundle_a.tsx\",\n  \"bundle_a._index.tsx\",\n  \"bundle_a.route_a.tsx\",\n  \"bundle_a.route_b.tsx\",\n\n  // Bundle B doesn't have an index route\n  \"bundle_b.tsx\",\n  \"bundle_b.route_a.tsx\",\n  \"bundle_b.route_b.tsx\",\n\n  // Bundle C is nested in a pathless route\n  \"_pathless.tsx\",\n  \"_pathless.bundle_c.tsx\",\n  \"_pathless.bundle_c.route_a.tsx\",\n  \"_pathless.bundle_c.route_b.tsx\",\n];\n\nconst files = {\n  \"app/root.tsx\": js`\n    ${ROUTE_FILE_COMMENT}\n    import { Links, Meta, Outlet, Scripts } from \"react-router\";\n\n    export default function Root() {\n      return (\n        <html lang=\"en\">\n          <head>\n            <Meta />\n            <Links />\n          </head>\n          <body>\n            <Outlet />\n            <Scripts />\n          </body>\n        </html>\n      );\n    }\n  `,\n  ...Object.assign({}, ...TEST_ROUTES.map(createRoute)),\n};\n\nconst expectRenderedRoutes = async (page: Page, routeFiles: string[]) => {\n  await Promise.all(\n    TEST_ROUTES.map(async (routeFile) => {\n      let locator = page.locator(\n        `[data-route-file=\"${routeFile}\"] [data-mounted]`,\n      );\n      if (routeFiles.includes(routeFile)) {\n        await expect(locator).toBeAttached();\n      } else {\n        // Assert no other routes are rendered\n        await expect(locator).not.toBeAttached();\n      }\n    }),\n  );\n};\n\ntest.describe(\"Server bundles\", () => {\n  let cwd: string;\n  let port: number;\n\n  [false, true].forEach((v8_viteEnvironmentApi) => {\n    test.describe(`v8_viteEnvironmentApi enabled: ${v8_viteEnvironmentApi}`, () => {\n      test.beforeAll(async () => {\n        port = await getPort();\n        cwd = await createProject(\n          {\n            \"react-router.config.ts\": dedent(js`\n              export default {\n                future: {\n                  v8_viteEnvironmentApi: ${v8_viteEnvironmentApi},\n                },\n                buildEnd: async ({ buildManifest }) => {\n                  let fs = await import(\"node:fs\");\n                  await fs.promises.writeFile(\n                    \"build/test-manifest.json\",\n                    JSON.stringify(buildManifest, null, 2)\n                  );\n                },\n                serverBundles: async ({ branch }) => {\n                  // Smoke test to ensure we can read the route files via 'route.file'\n                  await Promise.all(branch.map(async (route) => {\n                    const fs = await import(\"node:fs/promises\");\n                    const routeFileContents = await fs.readFile(route.file, \"utf8\");\n                    if (!routeFileContents.includes(${JSON.stringify(\n                      ROUTE_FILE_COMMENT,\n                    )})) {\n                      throw new Error(\"Couldn't file route file test comment\");\n                    }\n                  }));\n\n                  if (branch.some((route) => route.id === \"routes/_index\")) {\n                    return \"root\";\n                  }\n\n                  if (branch.some((route) => route.id === \"routes/bundle_a\")) {\n                    return \"bundle_a\";\n                  }\n\n                  if (branch.some((route) => route.id === \"routes/bundle_b\")) {\n                    return \"bundle_b\";\n                  }\n\n                  if (branch.some((route) => route.id === \"routes/_pathless.bundle_c\")) {\n                    return \"bundle_c\";\n                  }\n\n                  throw new Error(\"No bundle defined for route \" + branch[branch.length - 1].id);\n                }\n              }\n            `),\n            \"vite.config.ts\": dedent(js`\n              import { reactRouter } from \"@react-router/dev/vite\";\n\n              export default {\n                ${await viteConfig.server({ port })}\n                build: { manifest: true },\n                plugins: [reactRouter()]\n              }\n            `),\n            ...files,\n          },\n          v8_viteEnvironmentApi ? \"vite-6-template\" : \"vite-5-template\",\n        );\n      });\n\n      test.describe(() => {\n        let stop: () => void;\n        test.beforeAll(async () => {\n          stop = await dev({ cwd, port });\n        });\n\n        test.afterAll(() => stop());\n\n        test(\"dev\", async ({ page }) => {\n          // There are no server bundles in dev mode, this is just a smoke test to\n          // ensure dev mode works and that routes from all bundles are available\n          let pageErrors: Error[] = [];\n          page.on(\"pageerror\", (error) => pageErrors.push(error));\n\n          await page.goto(`http://localhost:${port}/`);\n          await expectRenderedRoutes(page, [\"_index.tsx\"]);\n\n          await page.goto(`http://localhost:${port}/bundle_a`);\n          await expectRenderedRoutes(page, [\n            \"bundle_a.tsx\",\n            \"bundle_a._index.tsx\",\n          ]);\n\n          await page.goto(`http://localhost:${port}/bundle_b`);\n          await expectRenderedRoutes(page, [\"bundle_b.tsx\"]);\n\n          await page.goto(`http://localhost:${port}/bundle_c`);\n          await expectRenderedRoutes(page, [\n            \"_pathless.tsx\",\n            \"_pathless.bundle_c.tsx\",\n          ]);\n\n          expect(pageErrors).toEqual([]);\n        });\n      });\n\n      test.describe(\"build\", () => {\n        let stdout: string;\n        test.beforeAll(() => {\n          let buildResult = build({ cwd });\n          stdout = buildResult.stdout.toString();\n        });\n\n        test(\"Vite Environment API message\", () => {\n          let viteEnvironmentApiMessage =\n            \"Using Vite Environment API (experimental)\";\n          if (v8_viteEnvironmentApi) {\n            expect(stdout).toContain(viteEnvironmentApiMessage);\n          } else {\n            expect(stdout).not.toContain(viteEnvironmentApiMessage);\n          }\n        });\n\n        test(\"server\", async ({ page }) => {\n          let pageErrors: Error[] = [];\n          page.on(\"pageerror\", (error) => pageErrors.push(error));\n\n          await withBundleServer(cwd, \"root\", async (port) => {\n            await page.goto(`http://localhost:${port}/`);\n            await expectRenderedRoutes(page, [\"_index.tsx\"]);\n\n            let _404s = [\"/bundle_a\", \"/bundle_b\", \"/bundle_c\"];\n            for (let path of _404s) {\n              let response = await page.goto(`http://localhost:${port}${path}`);\n              expect(response?.status()).toBe(404);\n            }\n          });\n\n          await withBundleServer(cwd, \"bundle_a\", async (port) => {\n            await page.goto(`http://localhost:${port}/bundle_a`);\n            await expectRenderedRoutes(page, [\n              \"bundle_a.tsx\",\n              \"bundle_a._index.tsx\",\n            ]);\n\n            await page.goto(`http://localhost:${port}/bundle_a/route_a`);\n            await expectRenderedRoutes(page, [\n              \"bundle_a.tsx\",\n              \"bundle_a.route_a.tsx\",\n            ]);\n\n            await page.goto(`http://localhost:${port}/bundle_a/route_b`);\n            await expectRenderedRoutes(page, [\n              \"bundle_a.tsx\",\n              \"bundle_a.route_b.tsx\",\n            ]);\n\n            let _404s = [\"/bundle_b\", \"/bundle_c\"];\n            for (let path of _404s) {\n              let response = await page.goto(`http://localhost:${port}${path}`);\n              expect(response?.status()).toBe(404);\n            }\n          });\n\n          await withBundleServer(cwd, \"bundle_b\", async (port) => {\n            await page.goto(`http://localhost:${port}/bundle_b`);\n            await expectRenderedRoutes(page, [\"bundle_b.tsx\"]);\n\n            await page.goto(`http://localhost:${port}/bundle_b/route_a`);\n            await expectRenderedRoutes(page, [\n              \"bundle_b.tsx\",\n              \"bundle_b.route_a.tsx\",\n            ]);\n\n            await page.goto(`http://localhost:${port}/bundle_b/route_b`);\n            await expectRenderedRoutes(page, [\n              \"bundle_b.tsx\",\n              \"bundle_b.route_b.tsx\",\n            ]);\n\n            let _404s = [\"/bundle_a\", \"/bundle_c\"];\n            for (let path of _404s) {\n              let response = await page.goto(`http://localhost:${port}${path}`);\n              expect(response?.status()).toBe(404);\n            }\n          });\n\n          await withBundleServer(cwd, \"bundle_c\", async (port) => {\n            await page.goto(`http://localhost:${port}/bundle_c`);\n            await expectRenderedRoutes(page, [\n              \"_pathless.tsx\",\n              \"_pathless.bundle_c.tsx\",\n            ]);\n\n            await page.goto(`http://localhost:${port}/bundle_c/route_a`);\n            await expectRenderedRoutes(page, [\n              \"_pathless.tsx\",\n              \"_pathless.bundle_c.tsx\",\n              \"_pathless.bundle_c.route_a.tsx\",\n            ]);\n\n            await page.goto(`http://localhost:${port}/bundle_c/route_b`);\n            await expectRenderedRoutes(page, [\n              \"_pathless.tsx\",\n              \"_pathless.bundle_c.tsx\",\n              \"_pathless.bundle_c.route_b.tsx\",\n            ]);\n\n            let _404s = [\"/bundle_a\", \"/bundle_b\"];\n            for (let path of _404s) {\n              let response = await page.goto(`http://localhost:${port}${path}`);\n              expect(response?.status()).toBe(404);\n            }\n          });\n\n          expect(pageErrors).toEqual([]);\n        });\n\n        test(\"React Router browser manifest\", () => {\n          let clientAssetFiles = fs.readdirSync(\n            path.join(cwd, \"build\", \"client\", \"assets\"),\n          );\n          let manifestFiles = clientAssetFiles.filter((filename) =>\n            filename.startsWith(\"manifest-\"),\n          );\n\n          expect(manifestFiles.length).toEqual(1);\n        });\n\n        test(\"Vite manifests\", () => {\n          [\n            [\"client\"],\n            [\"server\", \"bundle_a\"],\n            [\"server\", \"bundle_b\"],\n            [\"server\", \"bundle_c\"],\n            [\"server\", \"root\"],\n          ].forEach((buildPaths) => {\n            let viteManifestFiles = fs.readdirSync(\n              path.join(cwd, \"build\", ...buildPaths, \".vite\"),\n            );\n            expect(viteManifestFiles).toEqual([\"manifest.json\"]);\n          });\n        });\n\n        test(\"React Router build manifest\", () => {\n          let manifestPath = path.join(cwd, \"build\", \"test-manifest.json\");\n          expect(JSON.parse(fs.readFileSync(manifestPath, \"utf8\"))).toEqual({\n            serverBundles: {\n              bundle_c: {\n                id: \"bundle_c\",\n                file: \"build/server/bundle_c/index.js\",\n              },\n              bundle_a: {\n                id: \"bundle_a\",\n                file: \"build/server/bundle_a/index.js\",\n              },\n              bundle_b: {\n                id: \"bundle_b\",\n                file: \"build/server/bundle_b/index.js\",\n              },\n              root: {\n                id: \"root\",\n                file: \"build/server/root/index.js\",\n              },\n            },\n            routeIdToServerBundleId: {\n              \"routes/_pathless.bundle_c.route_a\": \"bundle_c\",\n              \"routes/_pathless.bundle_c.route_b\": \"bundle_c\",\n              \"routes/_pathless.bundle_c\": \"bundle_c\",\n              \"routes/bundle_a.route_a\": \"bundle_a\",\n              \"routes/bundle_a.route_b\": \"bundle_a\",\n              \"routes/bundle_b.route_a\": \"bundle_b\",\n              \"routes/bundle_b.route_b\": \"bundle_b\",\n              \"routes/bundle_a._index\": \"bundle_a\",\n              \"routes/bundle_b\": \"bundle_b\",\n              \"routes/_index\": \"root\",\n            },\n            routes: {\n              root: {\n                path: \"\",\n                id: \"root\",\n                file: \"app/root.tsx\",\n              },\n              \"routes/_pathless.bundle_c.route_a\": {\n                file: \"app/routes/_pathless.bundle_c.route_a.tsx\",\n                id: \"routes/_pathless.bundle_c.route_a\",\n                path: \"route_a\",\n                parentId: \"routes/_pathless.bundle_c\",\n              },\n              \"routes/_pathless.bundle_c.route_b\": {\n                file: \"app/routes/_pathless.bundle_c.route_b.tsx\",\n                id: \"routes/_pathless.bundle_c.route_b\",\n                path: \"route_b\",\n                parentId: \"routes/_pathless.bundle_c\",\n              },\n              \"routes/_pathless.bundle_c\": {\n                file: \"app/routes/_pathless.bundle_c.tsx\",\n                id: \"routes/_pathless.bundle_c\",\n                path: \"bundle_c\",\n                parentId: \"routes/_pathless\",\n              },\n              \"routes/bundle_a.route_a\": {\n                file: \"app/routes/bundle_a.route_a.tsx\",\n                id: \"routes/bundle_a.route_a\",\n                path: \"route_a\",\n                parentId: \"routes/bundle_a\",\n              },\n              \"routes/bundle_a.route_b\": {\n                file: \"app/routes/bundle_a.route_b.tsx\",\n                id: \"routes/bundle_a.route_b\",\n                path: \"route_b\",\n                parentId: \"routes/bundle_a\",\n              },\n              \"routes/bundle_b.route_a\": {\n                file: \"app/routes/bundle_b.route_a.tsx\",\n                id: \"routes/bundle_b.route_a\",\n                path: \"route_a\",\n                parentId: \"routes/bundle_b\",\n              },\n              \"routes/bundle_b.route_b\": {\n                file: \"app/routes/bundle_b.route_b.tsx\",\n                id: \"routes/bundle_b.route_b\",\n                path: \"route_b\",\n                parentId: \"routes/bundle_b\",\n              },\n              \"routes/bundle_a._index\": {\n                file: \"app/routes/bundle_a._index.tsx\",\n                id: \"routes/bundle_a._index\",\n                index: true,\n                parentId: \"routes/bundle_a\",\n              },\n              \"routes/_pathless\": {\n                file: \"app/routes/_pathless.tsx\",\n                id: \"routes/_pathless\",\n                parentId: \"root\",\n              },\n              \"routes/bundle_a\": {\n                file: \"app/routes/bundle_a.tsx\",\n                id: \"routes/bundle_a\",\n                path: \"bundle_a\",\n                parentId: \"root\",\n              },\n              \"routes/bundle_b\": {\n                file: \"app/routes/bundle_b.tsx\",\n                id: \"routes/bundle_b\",\n                path: \"bundle_b\",\n                parentId: \"root\",\n              },\n              \"routes/_index\": {\n                file: \"app/routes/_index.tsx\",\n                id: \"routes/_index\",\n                index: true,\n                parentId: \"root\",\n              },\n            },\n          });\n        });\n      });\n    });\n  });\n});\n"
  },
  {
    "path": "integration/vite-server-fs-allow-test.ts",
    "content": "import { test, expect } from \"@playwright/test\";\nimport getPort from \"get-port\";\n\nimport {\n  createProject,\n  customDev,\n  EXPRESS_SERVER,\n  viteConfig,\n} from \"./helpers/vite.js\";\n\nlet files = {\n  \"app/routes/test-route.tsx\": String.raw`\n    export default function IndexRoute() {\n      return <div id=\"test\">Hello world</div>\n    }\n  `,\n};\n\ntest.describe(async () => {\n  let port: number;\n  let cwd: string;\n  let stop: () => void;\n\n  test.beforeAll(async () => {\n    port = await getPort();\n    cwd = await createProject({\n      \"vite.config.ts\": await viteConfig.basic({ port, fsAllow: [\"app\"] }),\n      \"server.mjs\": EXPRESS_SERVER({ port }),\n      ...files,\n    });\n    stop = await customDev({ cwd, port });\n  });\n  test.afterAll(() => stop());\n\n  test(\"Vite / server.fs.allow / works with basic allow list\", async ({\n    page,\n  }) => {\n    let pageErrors: unknown[] = [];\n    page.on(\"pageerror\", (error) => pageErrors.push(error));\n\n    await page.goto(`http://localhost:${port}/test-route`, {\n      waitUntil: \"networkidle\",\n    });\n    expect(pageErrors).toEqual([]);\n\n    let testContent = page.locator(\"#test\");\n    await expect(testContent).toBeAttached();\n\n    expect(pageErrors).toEqual([]);\n  });\n});\n"
  },
  {
    "path": "integration/vite-spa-mode-test.ts",
    "content": "import fs from \"node:fs\";\nimport path from \"node:path\";\nimport { test, expect } from \"@playwright/test\";\n\nimport {\n  createAppFixture,\n  createFixture,\n  css,\n  js,\n} from \"./helpers/create-fixture.js\";\nimport type { Fixture, AppFixture } from \"./helpers/create-fixture.js\";\nimport { PlaywrightFixture } from \"./helpers/playwright-fixture.js\";\nimport { createProject, build, reactRouterConfig } from \"./helpers/vite.js\";\n\ntest.describe(\"SPA Mode\", () => {\n  let fixture: Fixture;\n  let appFixture: AppFixture;\n\n  [true, false].forEach((v8_splitRouteModules) => {\n    test.describe(`v8_splitRouteModules: ${v8_splitRouteModules}`, () => {\n      test.describe(\"custom builds\", () => {\n        test.describe(\"build errors\", () => {\n          test(\"errors on server-only exports\", async () => {\n            let cwd = await createProject({\n              \"react-router.config.ts\": reactRouterConfig({\n                ssr: false,\n                future: { v8_splitRouteModules },\n              }),\n              \"app/routes/invalid-exports.tsx\": String.raw`\n              // Invalid exports\n              export function headers() {}\n              export function loader() {}\n              export function action() {}\n\n              // Valid exports\n              export function clientLoader() {}\n              export function clientAction() {}\n              export default function Component() {}\n            `,\n            });\n            let result = build({ cwd });\n            let stderr = result.stderr.toString(\"utf8\");\n            expect(stderr).toMatch(\n              \"SPA Mode: 3 invalid route export(s) in `routes/invalid-exports.tsx`: \" +\n                \"`headers`, `loader`, `action`. See https://reactrouter.com/how-to/spa \" +\n                \"for more information.\",\n            );\n          });\n\n          test(\"allows loader in root route\", async () => {\n            let cwd = await createProject({\n              \"react-router.config.ts\": reactRouterConfig({\n                ssr: false,\n                future: { v8_splitRouteModules },\n              }),\n              \"app/root.tsx\": String.raw`\n                // Invalid exports\n                export function headers() {}\n                export function action() {}\n\n                // Valid exports\n                export function loader() {}\n                export function clientLoader() {}\n                export function clientAction() {}\n                export default function Component() {}\n              `,\n              \"app/routes/_index.tsx\": String.raw`\n                // Valid exports\n                export function clientLoader() {}\n                export function clientAction() {}\n                export default function Component() {}\n              `,\n            });\n            let result = build({ cwd });\n            let stderr = result.stderr.toString(\"utf8\");\n            expect(stderr).toMatch(\n              \"SPA Mode: 2 invalid route export(s) in `root.tsx`: `headers`, `action`. \" +\n                \"See https://reactrouter.com/how-to/spa for more information.\",\n            );\n          });\n\n          test(\"errors on HydrateFallback export from non-root route\", async () => {\n            let cwd = await createProject({\n              \"react-router.config.ts\": reactRouterConfig({\n                ssr: false,\n                future: { v8_splitRouteModules },\n              }),\n              \"app/routes/invalid-exports.tsx\": String.raw`\n              // Invalid exports\n              export function HydrateFallback() {}\n\n              // Valid exports\n              export function clientLoader() {}\n              export function clientAction() {}\n              export default function Component() {}\n            `,\n            });\n            let result = build({ cwd });\n            let stderr = result.stderr.toString(\"utf8\");\n            expect(stderr).toMatch(\n              \"SPA Mode: Invalid `HydrateFallback` export found in `routes/invalid-exports.tsx`. \" +\n                \"`HydrateFallback` is only permitted on the root route in SPA Mode. \" +\n                \"See https://reactrouter.com/how-to/spa for more information.\",\n            );\n          });\n\n          test(\"errors on a non-200 status from entry.server.tsx\", async () => {\n            let cwd = await createProject({\n              \"react-router.config.ts\": reactRouterConfig({\n                ssr: false,\n                future: { v8_splitRouteModules },\n              }),\n              \"app/entry.server.tsx\": js`\n              import { ServerRouter } from \"react-router\";\n              import { renderToString } from \"react-dom/server\";\n\n              export default function handleRequest(\n                request,\n                responseStatusCode,\n                responseHeaders,\n                remixContext\n              ) {\n                const html = renderToString(\n                  <ServerRouter context={remixContext} url={request.url} />\n                );\n                return new Response(html, {\n                  headers: { \"Content-Type\": \"text/html\" },\n                  status: 500,\n                });\n              }\n            `,\n              \"app/root.tsx\": js`\n              import { Links, Meta, Outlet, Scripts } from \"react-router\";\n\n              export default function Root() {\n                return (\n                  <html lang=\"en\">\n                    <head>\n                      <Meta />\n                      <Links />\n                    </head>\n                    <body>\n                      <Outlet />\n                      <Scripts />\n                    </body>\n                  </html>\n                );\n              }\n\n              export function HydrateFallback() {\n                return (\n                  <html lang=\"en\">\n                    <head>\n                      <Meta />\n                      <Links />\n                    </head>\n                    <body>\n                      <h1>Loading...</h1>\n                      <Scripts />\n                    </body>\n                  </html>\n                );\n              }\n            `,\n            });\n            let result = build({ cwd });\n            let stderr = result.stderr.toString(\"utf8\");\n            expect(stderr).toMatch(\n              \"SPA Mode: Received a 500 status code from `entry.server.tsx` while \" +\n                \"prerendering your `index.html` file.\",\n            );\n            expect(stderr).toMatch(\"<h1>Loading...</h1>\");\n          });\n\n          test(\"errors if you do not include <Scripts> in your root <HydrateFallback>\", async () => {\n            let cwd = await createProject({\n              \"react-router.config.ts\": reactRouterConfig({\n                ssr: false,\n                future: { v8_splitRouteModules },\n              }),\n              \"app/root.tsx\": String.raw`\n              export function HydrateFallback() {\n                return <h1>Loading</h1>\n              }\n            `,\n            });\n            let result = build({ cwd });\n            let stderr = result.stderr.toString(\"utf8\");\n            expect(stderr).toMatch(\n              \"SPA Mode: Did you forget to include `<Scripts/>` in your root route? \" +\n                \"Your pre-rendered HTML cannot hydrate without `<Scripts />`.\",\n            );\n          });\n        });\n\n        test(\"prepends DOCTYPE to HTML in the default entry.server.tsx\", async () => {\n          let fixture = await createFixture({\n            spaMode: true,\n            files: {\n              \"react-router.config.ts\": reactRouterConfig({\n                ssr: false,\n                future: { v8_splitRouteModules },\n              }),\n              \"app/root.tsx\": js`\n                import { Outlet, Scripts } from \"react-router\";\n\n                export default function Root() {\n                  return (\n                    <html lang=\"en\">\n                      <head></head>\n                      <body>\n                        <h1 data-root>Root</h1>\n                        <Scripts />\n                      </body>\n                    </html>\n                  );\n                }\n\n                export function HydrateFallback() {\n                  return (\n                    <html lang=\"en\">\n                      <head></head>\n                      <body>\n                        <h1 data-loading>Loading SPA...</h1>\n                        <Scripts />\n                      </body>\n                    </html>\n                  );\n                }\n              `,\n            },\n          });\n          let res = await fixture.requestDocument(\"/\");\n          expect(await res.text()).toMatch(/^<!DOCTYPE html><html lang=\"en\">/);\n        });\n\n        test(\"Ignores build-time headers at runtime\", async () => {\n          let fixture = await createFixture({\n            files: {\n              \"react-router.config.ts\": reactRouterConfig({\n                future: { v8_splitRouteModules },\n              }),\n              \"app/root.tsx\": js`\n                import { Outlet, Scripts } from \"react-router\";\n\n                export default function Root() {\n                  return (\n                    <html lang=\"en\">\n                      <head></head>\n                      <body>\n                        <h1 data-root>Root</h1>\n                        <Scripts />\n                      </body>\n                    </html>\n                  );\n                }\n              `,\n            },\n          });\n          let res = await fixture.requestDocument(\"/\", {\n            headers: { \"X-React-Router-SPA-Mode\": \"yes\" },\n          });\n          let html = await res.text();\n          expect(html).toMatch('\"isSpaMode\":false');\n          expect(html).toMatch('<h1 data-root=\"true\">Root</h1>');\n        });\n\n        test(\"works when combined with a basename\", async ({ page }) => {\n          fixture = await createFixture({\n            spaMode: true,\n            files: {\n              \"react-router.config.ts\": reactRouterConfig({\n                basename: \"/base/\",\n                ssr: false,\n                future: { v8_splitRouteModules },\n              }),\n              \"app/root.tsx\": js`\n                import { Outlet, Scripts } from \"react-router\";\n\n                export default function Root() {\n                  return (\n                    <html lang=\"en\">\n                      <head></head>\n                      <body>\n                        <h1 data-root>Root</h1>\n                        <Outlet />\n                        <Scripts />\n                      </body>\n                    </html>\n                  );\n                }\n\n                export function HydrateFallback() {\n                  return (\n                    <html lang=\"en\">\n                      <head></head>\n                      <body>\n                        <h1 data-loading>Loading SPA...</h1>\n                        <Scripts />\n                      </body>\n                    </html>\n                  );\n                }\n              `,\n              \"app/routes/_index.tsx\": js`\n                import * as React  from \"react\";\n                import { useLoaderData } from \"react-router\";\n\n                export async function clientLoader({ request }) {\n                  return \"Index Loader Data\";\n                }\n\n                export default function Component() {\n                  let data = useLoaderData();\n                  const [mounted, setMounted] = React.useState(false);\n                  React.useEffect(() => setMounted(true), []);\n\n                  return (\n                    <>\n                      <h2 data-route>Index</h2>\n                      <p data-loader-data>{data}</p>\n                      {!mounted ? <h3>Unmounted</h3> : <h3 data-mounted>Mounted</h3>}\n                    </>\n                  );\n                }\n              `,\n            },\n          });\n          appFixture = await createAppFixture(fixture);\n\n          let app = new PlaywrightFixture(appFixture, page);\n          await app.goto(\"/base/\");\n          await page.waitForSelector(\"[data-mounted]\");\n          expect(await page.locator(\"[data-route]\").textContent()).toBe(\n            \"Index\",\n          );\n          expect(await page.locator(\"[data-loader-data]\").textContent()).toBe(\n            \"Index Loader Data\",\n          );\n        });\n\n        test(\"can be used to hydrate only a div\", async ({ page }) => {\n          fixture = await createFixture({\n            spaMode: true,\n            files: {\n              \"react-router.config.ts\": reactRouterConfig({\n                ssr: false,\n                future: { v8_splitRouteModules },\n              }),\n              \"app/index.html\": String.raw`\n                <!DOCTYPE html>\n                <html lang=\"en\">\n                  <head>\n                    <title>Not from Remix!</title>\n                  </head>\n                  <body>\n                    <div id=\"app\"><!-- Remix SPA --></div>\n                  </body>\n                </html>\n              `,\n              \"app/entry.client.tsx\": js`\n                import { HydratedRouter } from \"react-router/dom\";\n                import { startTransition, StrictMode } from \"react\";\n                import { hydrateRoot } from \"react-dom/client\";\n\n                startTransition(() => {\n                  hydrateRoot(\n                    document.querySelector(\"#app\"),\n                    <StrictMode>\n                      <HydratedRouter />\n                    </StrictMode>\n                  );\n                });\n              `,\n              \"app/entry.server.tsx\": js`\n                import * as fs from \"node:fs\";\n                import * as path from \"node:path\";\n                import { PassThrough } from \"node:stream\";\n\n                import type { AppLoadContext, EntryContext } from \"react-router\";\n                import { createReadableStreamFromReadable } from \"@react-router/node\";\n                import { ServerRouter } from \"react-router\";\n                import { renderToPipeableStream } from \"react-dom/server\";\n\n                export default function handleRequest(\n                  request: Request,\n                  responseStatusCode: number,\n                  responseHeaders: Headers,\n                  remixContext: EntryContext,\n                  loadContext: AppLoadContext\n                ) {\n                  return handleBotRequest(\n                    request,\n                    responseStatusCode,\n                    responseHeaders,\n                    remixContext\n                  );\n                }\n\n                async function handleBotRequest(\n                  request: Request,\n                  responseStatusCode: number,\n                  responseHeaders: Headers,\n                  remixContext: EntryContext\n                ) {\n                  const html = await new Promise((resolve, reject) => {\n                    let shellRendered = false;\n                    const { pipe, abort } = renderToPipeableStream(\n                      <ServerRouter context={remixContext} url={request.url} />,\n                      {\n                        onAllReady() {\n                          shellRendered = true;\n                          const body = new PassThrough();\n                          const stream = createReadableStreamFromReadable(body);\n\n                          responseHeaders.set(\"Content-Type\", \"text/html\");\n\n                          resolve(\n                            new Response(stream, {\n                              headers: responseHeaders,\n                              status: responseStatusCode,\n                            }).text()\n                          );\n\n                          pipe(body);\n                        },\n                        onShellError(error: unknown) {\n                          reject(error);\n                        },\n                        onError(error: unknown) {\n                          responseStatusCode = 500;\n                          // Log streaming rendering errors from inside the shell.  Don't log\n                          // errors encountered during initial shell rendering since they'll\n                          // reject and get logged in handleDocumentRequest.\n                          if (shellRendered) {\n                            console.error(error);\n                          }\n                        },\n                      }\n                    );\n\n                    setTimeout(abort, 5000);\n                  });\n\n                  const shellHtml = fs\n                    .readFileSync(\n                      path.join(process.cwd(), \"app/index.html\")\n                    )\n                    .toString();\n\n                  const finalHTML = shellHtml.replace(\"<!-- Remix SPA -->\", html);\n                  return new Response(finalHTML, {\n                    headers: responseHeaders,\n                    status: responseStatusCode,\n                  });\n                }\n              `,\n              \"app/root.tsx\": js`\n                import { Outlet, Scripts } from \"react-router\";\n\n                export default function Root() {\n                  return (\n                    <>\n                      <h1 data-root>Root</h1>\n                      <Outlet />\n                      <Scripts />\n                    </>\n                  );\n                }\n\n                export function HydrateFallback() {\n                  return (\n                    <>\n                      <h1 data-loading>Loading SPA...</h1>\n                      <Scripts />\n                    </>\n                  );\n                }\n              `,\n              \"app/routes/_index.tsx\": js`\n                import * as React  from \"react\";\n                import { useLoaderData } from \"react-router\";\n\n                export async function clientLoader({ request }) {\n                  return \"Index Loader Data\";\n                }\n\n                export default function Component() {\n                  let data = useLoaderData();\n                  const [mounted, setMounted] = React.useState(false);\n                  React.useEffect(() => setMounted(true), []);\n\n                  return (\n                    <>\n                      <h2 data-route>Index</h2>\n                      <p data-loader-data>{data}</p>\n                      {!mounted ? <h3>Unmounted</h3> : <h3 data-mounted>Mounted</h3>}\n                    </>\n                  );\n                }\n              `,\n            },\n          });\n          appFixture = await createAppFixture(fixture);\n\n          let app = new PlaywrightFixture(appFixture, page);\n          await app.goto(\"/\");\n          expect(await page.locator(\"title\").textContent()).toBe(\n            \"Not from Remix!\",\n          );\n          await page.waitForSelector(\"[data-mounted]\");\n          expect(await page.locator(\"[data-route]\").textContent()).toBe(\n            \"Index\",\n          );\n          expect(await page.locator(\"[data-loader-data]\").textContent()).toBe(\n            \"Index Loader Data\",\n          );\n        });\n\n        test(\"works for migration apps with only a root route and no loader\", async ({\n          page,\n        }) => {\n          fixture = await createFixture({\n            spaMode: true,\n            files: {\n              \"react-router.config.ts\": reactRouterConfig({\n                // We don't want to pick up the app/routes/_index.tsx file from\n                // the template and instead want to use only the src/root.tsx\n                // file below\n                appDirectory: \"src\",\n                ssr: false,\n                future: { v8_splitRouteModules },\n              }),\n              \"src/routes.ts\": js`\n                import { type RouteConfig } from \"@react-router/dev/routes\";\n                import { flatRoutes } from \"@react-router/fs-routes\";\n\n                export default flatRoutes() satisfies RouteConfig;\n              `,\n              \"src/root.tsx\": js`\n                import {\n                  Meta,\n                  Links,\n                  Outlet,\n                  Routes,\n                  Route,\n                  Scripts,\n                  ScrollRestoration,\n                } from \"react-router\";\n\n                export function Layout({ children }: { children: React.ReactNode }) {\n                  return (\n                    <html>\n                      <head>\n                        <Meta />\n                        <Links />\n                      </head>\n                      <body>\n                        {children}\n                        <ScrollRestoration />\n                        <Scripts />\n                      </body>\n                    </html>\n                  );\n                }\n\n                export default function Root() {\n                  return (\n                    <>\n                      <h1 data-root>Root</h1>\n                      <Routes>\n                        <Route path=\"/\" element={<h2 data-index>Index</h2>} />\n                      </Routes>\n                    </>\n                  );\n                }\n\n                export function HydrateFallback() {\n                  return <h1 data-loading>Loading SPA...</h1>;\n                }\n              `,\n            },\n          });\n          appFixture = await createAppFixture(fixture);\n\n          let res = await fixture.requestDocument(\"/\");\n          let html = await res.text();\n          expect(html).toMatch('<h1 data-loading=\"true\">Loading SPA...</h1>');\n\n          let logs: string[] = [];\n          page.on(\"console\", (msg) => logs.push(msg.text()));\n\n          let app = new PlaywrightFixture(appFixture, page);\n          await app.goto(\"/\");\n          await page.waitForSelector(\"[data-root]\");\n          expect(await page.locator(\"[data-root]\").textContent()).toBe(\"Root\");\n          expect(await page.locator(\"[data-index]\").textContent()).toBe(\n            \"Index\",\n          );\n        });\n\n        test(\"wraps default root HydrateFallback in user-provided Layout\", async ({\n          page,\n        }) => {\n          fixture = await createFixture({\n            spaMode: true,\n            files: {\n              \"react-router.config.ts\": reactRouterConfig({\n                // We don't want to pick up the app/routes/_index.tsx file from\n                // the template and instead want to use only the src/root.tsx\n                // file below\n                appDirectory: \"src\",\n                ssr: false,\n                future: { v8_splitRouteModules },\n              }),\n              \"src/routes.ts\": js`\n                import { type RouteConfig } from \"@react-router/dev/routes\";\n                import { flatRoutes } from \"@react-router/fs-routes\";\n\n                export default flatRoutes() satisfies RouteConfig;\n              `,\n              \"src/root.tsx\": js`\n                import {\n                  Meta,\n                  Links,\n                  Outlet,\n                  Routes,\n                  Route,\n                  Scripts,\n                  ScrollRestoration,\n                } from \"react-router\";\n\n                export function Layout({ children }: { children: React.ReactNode }) {\n                  return (\n                    <html>\n                      <head>\n                        <Meta />\n                        <Links />\n                      </head>\n                      <body>\n                        {children}\n                        <ScrollRestoration />\n                        <Scripts />\n                      </body>\n                    </html>\n                  );\n                }\n\n                export default function Root() {\n                  return (\n                    <>\n                      <h1 data-root>Root</h1>\n                      <Routes>\n                        <Route path=\"/\" element={<h2 data-index>Index</h2>} />\n                      </Routes>\n                    </>\n                  );\n                }\n              `,\n            },\n          });\n          appFixture = await createAppFixture(fixture);\n\n          let res = await fixture.requestDocument(\"/\");\n          let html = await res.text();\n          expect(html.match(/<html/g)?.length).toBe(1);\n          expect(html.match(/<\\/html/g)?.length).toBe(1);\n          expect(html.match(/window.__reactRouterContext =/g)?.length).toBe(1);\n          expect(html.match(/💿 Hey developer 👋/g)?.length).toBe(1);\n        });\n\n        test(\"does not inherit single fetch revalidation behavior\", async ({\n          page,\n        }) => {\n          fixture = await createFixture({\n            spaMode: true,\n            files: {\n              \"react-router.config.ts\": reactRouterConfig({\n                ssr: false,\n                future: { v8_splitRouteModules },\n              }),\n              \"app/routes/_index.tsx\": js`\n                import { Link } from 'react-router';\n                export default function Component() {\n                  return <Link to=\"/parent\">Go to parent</Link>;\n                }\n              `,\n              \"app/routes/parent.tsx\": js`\n                import { Link, Outlet } from 'react-router';\n                let count = 0;\n                export function clientLoader() {\n                  return ++count;\n                }\n                export default function Component({ loaderData }) {\n                  return (\n                    <>\n                      <h1>Parent: {loaderData}</h1>\n                      <Link to=\"./child\">Go to child</Link>\n                      <Outlet />\n                    </>\n                  )\n                }\n              `,\n              \"app/routes/parent.child.tsx\": js`\n                import { Link } from 'react-router';\n                export default function Component({ loaderData }) {\n                  return <h2>Child</h2>;\n                }\n              `,\n            },\n          });\n          appFixture = await createAppFixture(fixture);\n\n          let app = new PlaywrightFixture(appFixture, page);\n          await app.goto(\"/\", true);\n          await page.waitForSelector('a[href=\"/parent\"]');\n          await app.clickLink(\"/parent\");\n          await page.waitForSelector(\"h1\");\n          expect(await page.locator(\"h1\").textContent()).toBe(\"Parent: 1\");\n\n          await app.clickLink(\"/parent/child\");\n          await page.waitForSelector(\"h2\");\n          expect(await page.locator(\"h1\").textContent()).toBe(\"Parent: 1\");\n          expect(await page.locator(\"h2\").textContent()).toBe(\"Child\");\n        });\n\n        test(\"does not hydrate root loaderData if there's no root loader\", async ({\n          page,\n        }) => {\n          fixture = await createFixture({\n            spaMode: true,\n            files: {\n              \"react-router.config.ts\": reactRouterConfig({\n                ssr: false,\n                future: { v8_splitRouteModules },\n              }),\n              \"app/root.tsx\": js`\n                import {\n                  Meta,\n                  Links,\n                  Outlet,\n                  Routes,\n                  Route,\n                  Scripts,\n                  ScrollRestoration,\n                } from \"react-router\";\n\n                export function Layout({ children }: { children: React.ReactNode }) {\n                  return (\n                    <html>\n                      <head>\n                        <Meta />\n                        <Links />\n                      </head>\n                      <body>\n                        {children}\n                        <ScrollRestoration />\n                        <Scripts />\n                      </body>\n                    </html>\n                  );\n                }\n\n                let count = 0;\n                export function clientLoader() {\n                  return ++count;\n                }\n\n                export default function Root({ loaderData }) {\n                  return (\n                    <>\n                      <h1>{loaderData}</h1>\n                      <Outlet />\n                    </>\n                  );\n                }\n              `,\n              \"app/routes/_index.tsx\": js`\n                import { redirect } from 'react-router';\n                export const clientLoader = () => redirect('/target');\n                export default function() { return null; }\n              `,\n              \"app/routes/target.tsx\": js`\n                import { useRouteLoaderData } from 'react-router';\n                export default function Comp() {\n                  return <h2>{useRouteLoaderData('root')}</h2>;\n                }\n              `,\n            },\n          });\n          appFixture = await createAppFixture(fixture);\n\n          let app = new PlaywrightFixture(appFixture, page);\n          await app.goto(\"/\");\n          await page.waitForSelector(\"h2\");\n          expect(await page.locator(\"h1\").textContent()).toBe(\"2\");\n          expect(await page.locator(\"h2\").textContent()).toBe(\"2\");\n        });\n      });\n\n      test.describe(\"normal apps\", () => {\n        test.beforeAll(async () => {\n          fixture = await createFixture({\n            spaMode: true,\n            files: {\n              \"react-router.config.ts\": reactRouterConfig({\n                ssr: false,\n                future: { v8_splitRouteModules },\n              }),\n              \"vite.config.ts\": js`\n                import { defineConfig } from \"vite\";\n                import { reactRouter } from \"@react-router/dev/vite\";\n\n                export default defineConfig({\n                  build: { manifest: true },\n                  plugins: [reactRouter()],\n                });\n              `,\n              \"public/styles-root.css\": css`\n                body {\n                  background-color: rgba(255, 0, 0, 0.25);\n                }\n              `,\n              \"public/styles-index.css\": css`\n                body {\n                  background-color: rgba(0, 255, 0, 0.25);\n                }\n              `,\n              \"app/root.tsx\": js`\n                import * as React from \"react\";\n                import { Form, Link, Links, Meta, Outlet, Scripts, useLoaderData } from \"react-router\";\n\n                export function meta({ data }) {\n                  return [{\n                    title: \"Root Title\"\n                  }];\n                }\n\n                export function links() {\n                  return [{\n                    rel: \"stylesheet\",\n                    href: \"styles-root.css\"\n                  }];\n                }\n\n                export function loader() {\n                  return { message: \"Root Loader Data\" };\n                }\n\n                export default function Root() {\n                  let id = React.useId();\n                  return (\n                    <html lang=\"en\">\n                      <head>\n                        <Meta />\n                        <Links />\n                      </head>\n                      <body>\n                          <h1 data-root>Root</h1>\n                          <pre data-use-id>{id}</pre>\n                          <nav>\n                            <Link to=\"/about\">/about</Link>\n                            <br/>\n\n                            <Form method=\"post\" action=\"/about\">\n                              <button type=\"submit\">\n                                Submit /about\n                              </button>\n                            </Form>\n                            <br/>\n\n                            <Link to=\"/error\">/error</Link>\n                            <br/>\n\n                            <Form method=\"post\" action=\"/error\">\n                              <button type=\"submit\">\n                                Submit /error\n                              </button>\n                            </Form>\n                            <br/>\n                          </nav>\n                          <Outlet />\n                        <Scripts />\n                      </body>\n                    </html>\n                  );\n                }\n\n                export function HydrateFallback() {\n                  const id = React.useId();\n                  const loaderData = useLoaderData();\n                  const [hydrated, setHydrated] = React.useState(false);\n                  React.useEffect(() => setHydrated(true), []);\n\n                  return (\n                    <html lang=\"en\">\n                      <head>\n                        <Meta />\n                        <Links />\n                      </head>\n                      <body>\n                        <h1 data-loading>Loading SPA...</h1>\n                        <p data-loader-data>{loaderData?.message}</p>\n                        <pre data-use-id>{id}</pre>\n                        {hydrated ? <h3 data-hydrated>Hydrated</h3> : null}\n                        <Scripts />\n                      </body>\n                    </html>\n                  );\n                }\n              `,\n              \"app/routes/_index.tsx\": js`\n                import * as React  from \"react\";\n                import { useLoaderData } from \"react-router\";\n\n                export function meta({ data }) {\n                  return [{\n                    title: \"Index Title: \" + data\n                  }];\n                }\n\n                export function links() {\n                  return [{\n                    rel: \"stylesheet\",\n                    href: \"styles-index.css\"\n                  }];\n                }\n\n                export async function clientLoader({ request }) {\n                  if (new URL(request.url).searchParams.has('slow')) {\n                    await new Promise(r => setTimeout(r, 1000));\n                  }\n                  return \"Index Loader Data\";\n                }\n\n                export default function Component() {\n                  let data = useLoaderData();\n                  const [mounted, setMounted] = React.useState(false);\n                  React.useEffect(() => setMounted(true), []);\n\n                  return (\n                    <>\n                      <h2 data-route>Index</h2>\n                      <p data-loader-data>{data}</p>\n                      {!mounted ? <h3>Unmounted</h3> : <h3 data-mounted>Mounted</h3>}\n                    </>\n                  );\n                }\n              `,\n              \"app/routes/about.tsx\": js`\n                import { useActionData, useLoaderData } from \"react-router\";\n\n                export function meta({ data }) {\n                  return [{\n                    title: \"About Title: \" + data\n                  }];\n                }\n\n                export function clientLoader() {\n                  return \"About Loader Data\";\n                }\n\n                export function clientAction() {\n                  return \"About Action Data\";\n                }\n\n                export default function Component() {\n                  let data = useLoaderData();\n                  let actionData = useActionData();\n\n                  return (\n                    <>\n                      <h2 data-route>About</h2>\n                      <p data-loader-data>{data}</p>\n                      <p data-action-data>{actionData}</p>\n                    </>\n                  );\n                }\n              `,\n              \"app/routes/error.tsx\": js`\n                import { useRouteError } from \"react-router\";\n\n                export async function clientLoader({ serverLoader }) {\n                  await serverLoader();\n                  return null;\n                }\n\n                export async function clientAction({ serverAction }) {\n                  await serverAction();\n                  return null;\n                }\n\n                export default function Component() {\n                  return <h2>Error</h2>;\n                }\n\n                export function ErrorBoundary() {\n                  let error = useRouteError();\n                  return <pre data-error>{error.data}</pre>\n                }\n              `,\n            },\n          });\n\n          appFixture = await createAppFixture(fixture);\n        });\n\n        test.afterAll(() => {\n          appFixture.close();\n        });\n\n        test(\"renders the root HydrateFallback initially with access to the root loader data\", async () => {\n          let res = await fixture.requestDocument(\"/\");\n          let html = await res.text();\n          expect(html).toMatch('<h1 data-loading=\"true\">Loading SPA...</h1>');\n          expect(html).toMatch(\n            '<p data-loader-data=\"true\">Root Loader Data</p>',\n          );\n        });\n\n        test(\"does not include Meta/Links from routes below the root\", async ({\n          page,\n        }) => {\n          let res = await fixture.requestDocument(\"/\");\n          let html = await res.text();\n          expect(html).toMatch(\"<title>Root Title</title>\");\n          expect(html).toMatch(\n            '<link rel=\"stylesheet\" href=\"styles-root.css\"/>',\n          );\n          expect(html).not.toMatch(\"Index Title\");\n          expect(html).not.toMatch(\"styles-index.css\");\n\n          let app = new PlaywrightFixture(appFixture, page);\n          await app.goto(\"/\");\n          await page.waitForSelector(\"[data-mounted]\");\n          expect(\n            await page.locator('link[href=\"styles-index.css\"]'),\n          ).toBeDefined();\n          expect(await page.locator(\"[data-route]\").textContent()).toBe(\n            \"Index\",\n          );\n          expect(await page.locator(\"title\").textContent()).toBe(\n            \"Index Title: Index Loader Data\",\n          );\n        });\n\n        test(\"hydrates\", async ({ page }) => {\n          let app = new PlaywrightFixture(appFixture, page);\n          await app.goto(\"/\");\n          expect(await page.locator(\"[data-route]\").textContent()).toBe(\n            \"Index\",\n          );\n          expect(await page.locator(\"[data-loader-data]\").textContent()).toBe(\n            \"Index Loader Data\",\n          );\n          expect(await page.locator(\"[data-mounted]\").textContent()).toBe(\n            \"Mounted\",\n          );\n          expect(await page.locator(\"title\").textContent()).toBe(\n            \"Index Title: Index Loader Data\",\n          );\n        });\n\n        test(\"hydrates a proper useId value\", async ({ page }) => {\n          // Ensure we SSR a proper useId value\n          let res = await fixture.requestDocument(\"/\");\n          let html = await res.text();\n          expect(html).toMatch(/<pre data-use-id=\"true\">([^<>]+)<\\/pre>/);\n          let matches = /<pre data-use-id=\"true\">([^<>]+)<\\/pre>/.exec(html);\n          expect(matches?.length).toBe(2);\n          let useIdValue = matches?.[1];\n\n          // We should hydrate the same useId value in HydrateFallback\n          let app = new PlaywrightFixture(appFixture, page);\n          await app.goto(\"/?slow\");\n          await page.waitForSelector(\"[data-hydrated]\");\n          expect(await page.locator(\"[data-use-id]\").textContent()).toBe(\n            useIdValue,\n          );\n\n          // Once hydrated, we should get a different useId value from the root Component\n          await page.waitForSelector(\"[data-route]\");\n          expect(await page.locator(\"[data-route]\").textContent()).toBe(\n            \"Index\",\n          );\n          expect(await page.locator(\"[data-use-id]\").textContent()).not.toBe(\n            useIdValue,\n          );\n        });\n\n        test(\"navigates and calls loaders\", async ({ page }) => {\n          let app = new PlaywrightFixture(appFixture, page);\n          await app.goto(\"/\");\n          expect(await page.locator(\"[data-route]\").textContent()).toBe(\n            \"Index\",\n          );\n\n          await app.clickLink(\"/about\");\n          await page.waitForSelector('[data-route]:has-text(\"About\")');\n          expect(await page.locator(\"[data-route]\").textContent()).toBe(\n            \"About\",\n          );\n          expect(await page.locator(\"[data-loader-data]\").textContent()).toBe(\n            \"About Loader Data\",\n          );\n          expect(await page.locator(\"title\").textContent()).toBe(\n            \"About Title: About Loader Data\",\n          );\n        });\n\n        test(\"navigates and calls actions/loaders\", async ({ page }) => {\n          let app = new PlaywrightFixture(appFixture, page);\n          await app.goto(\"/\");\n          expect(await page.locator(\"[data-route]\").textContent()).toBe(\n            \"Index\",\n          );\n\n          await app.clickSubmitButton(\"/about\");\n          await page.waitForSelector('[data-route]:has-text(\"About\")');\n          expect(await page.locator(\"[data-route]\").textContent()).toBe(\n            \"About\",\n          );\n          expect(await page.locator(\"[data-action-data]\").textContent()).toBe(\n            \"About Action Data\",\n          );\n          expect(await page.locator(\"[data-loader-data]\").textContent()).toBe(\n            \"About Loader Data\",\n          );\n          expect(await page.locator(\"title\").textContent()).toBe(\n            \"About Title: About Loader Data\",\n          );\n        });\n\n        test(\"errors if you call serverLoader\", async ({ page }) => {\n          let app = new PlaywrightFixture(appFixture, page);\n          await app.goto(\"/\");\n          expect(await page.locator(\"[data-route]\").textContent()).toBe(\n            \"Index\",\n          );\n\n          await app.clickLink(\"/error\");\n          await page.waitForSelector(\"[data-error]\");\n          expect(await page.locator(\"[data-error]\").textContent()).toBe(\n            'Error: You are trying to call serverLoader() on a route that does not have a server loader (routeId: \"routes/error\")',\n          );\n        });\n\n        test(\"errors if you call serverAction\", async ({ page }) => {\n          let app = new PlaywrightFixture(appFixture, page);\n          await app.goto(\"/\");\n          expect(await page.locator(\"[data-route]\").textContent()).toBe(\n            \"Index\",\n          );\n\n          await app.clickSubmitButton(\"/error\");\n          await page.waitForSelector(\"[data-error]\");\n          expect(await page.locator(\"[data-error]\").textContent()).toBe(\n            'Error: You are trying to call serverAction() on a route that does not have a server action (routeId: \"routes/error\")',\n          );\n        });\n\n        test(\"only generates client Vite manifest\", () => {\n          let viteManifestFiles = fs.readdirSync(\n            path.join(fixture.projectDir, \"build\", \"client\", \".vite\"),\n          );\n\n          expect(viteManifestFiles).toEqual([\"manifest.json\"]);\n        });\n      });\n    });\n  });\n\n  test(\"only imports the root route in the server build when SSRing index.html\", async ({\n    page,\n  }) => {\n    let fixture = await createFixture({\n      spaMode: true,\n      files: {\n        \"react-router.config.ts\": reactRouterConfig({\n          ssr: false,\n        }),\n        \"vite.config.ts\": js`\n          import { defineConfig } from \"vite\";\n          import { reactRouter } from \"@react-router/dev/vite\";\n\n          export default defineConfig({\n            build: { manifest: true },\n            plugins: [reactRouter()],\n          });\n        `,\n        \"app/routeImportTracker.ts\": js`\n          // this is kinda silly, but this way we can track imports\n          // that happen during SSR and during CSR\n          export async function logImport(url: string) {\n            try {\n              const fs = await import(\"node:fs\");\n              const path = await import(\"node:path\");\n              fs.appendFileSync(path.join(process.cwd(), \"ssr-route-imports.txt\"), url + \"\\n\");\n            }\n            catch (e) {\n              (window.csrRouteImports ??= []).push(url);\n            }\n          }\n        `,\n        \"app/root.tsx\": js`\n          import { Links, Meta, Outlet, Scripts } from \"react-router\";\n          import { logImport } from \"./routeImportTracker\";\n          logImport(\"app/root.tsx\");\n\n          export default function Root() {\n            return (\n              <html lang=\"en\">\n                <head>\n                  <Meta />\n                  <Links />\n                </head>\n                <body>\n                  hello world\n                  <Outlet />\n                  <Scripts />\n                </body>\n              </html>\n            );\n          }\n        `,\n        \"app/routes/_index.tsx\": js`\n          import { logImport } from \"../routeImportTracker\";\n          logImport(\"app/routes/_index.tsx\");\n\n          // This should not cause an error on SSR because the module is not loaded\n          console.log(window);\n\n          export default function Component() {\n            return \"index\";\n          }\n        `,\n        \"app/routes/about.tsx\": js`\n          import * as React  from \"react\";\n          import { logImport } from \"../routeImportTracker\";\n          logImport(\"app/routes/about.tsx\");\n\n          // This should not cause an error on SSR because the module is not loaded\n          console.log(window);\n\n          export default function Component() {\n            const [mounted, setMounted] = React.useState(false);\n            React.useEffect(() => setMounted(true), []);\n\n            return (\n              <>\n                {!mounted ? <span>Unmounted</span> : <span data-mounted>Mounted</span>}\n              </>\n            );\n          }\n        `,\n      },\n    });\n\n    let importedRoutes = (\n      await fs.promises.readFile(\n        path.join(fixture.projectDir, \"ssr-route-imports.txt\"),\n        \"utf-8\",\n      )\n    )\n      .trim()\n      .split(\"\\n\");\n    expect(importedRoutes).toStrictEqual([\n      \"app/root.tsx\",\n      // we should not have imported app/routes/_index.tsx\n      // we should not have imported app/routes/about.tsx\n    ]);\n\n    appFixture = await createAppFixture(fixture);\n    let app = new PlaywrightFixture(appFixture, page);\n    await app.goto(\"/about\");\n    await page.waitForSelector(\"[data-mounted]\");\n    // @ts-expect-error\n    expect(await page.evaluate(() => window.csrRouteImports)).toStrictEqual([\n      \"app/root.tsx\",\n      \"app/routes/about.tsx\",\n    ]);\n  });\n});\n"
  },
  {
    "path": "integration/vite-unused-route-exports-test.ts",
    "content": "import * as path from \"node:path\";\nimport { test, expect } from \"@playwright/test\";\n\nimport { createProject, grep, build } from \"./helpers/vite.js\";\n\ntest(\"Vite / dead-code elimination for unused route exports\", async () => {\n  let cwd = await createProject({\n    \"app/routes/custom-route-exports.tsx\": String.raw`\n      const unusedMessage = \"ROUTE_EXPORT_THAT_ISNT_USED\";\n      const usedMessage = \"ROUTE_EXPORT_THAT_IS_USED\";\n\n      export const unusedRouteExport = unusedMessage;\n      export const usedRouteExport = usedMessage;\n\n      export default function CustomExportsRoute() {\n        return <h1>Custom route exports</h1>\n      }\n    `,\n    \"app/routes/use-route-export.tsx\": String.raw`\n      import { usedRouteExport } from \"./custom-route-exports\";\n\n      export default function CustomExportsRoute() {\n        return <h1>{usedRouteExport}</h1>\n      }\n    `,\n  });\n  let { status } = build({ cwd });\n  expect(status).toBe(0);\n\n  expect(\n    grep(path.join(cwd, \"build/client\"), /ROUTE_EXPORT_THAT_ISNT_USED/).length,\n  ).toBe(0);\n\n  expect(\n    grep(path.join(cwd, \"build/client\"), /ROUTE_EXPORT_THAT_IS_USED/).length,\n  ).toBeGreaterThanOrEqual(1);\n});\n"
  },
  {
    "path": "jest/jest.config.shared.js",
    "content": "const ignorePatterns = [\n  \"\\\\/build\\\\/\",\n  \"\\\\/coverage\\\\/\",\n  \"\\\\/\\\\.vscode\\\\/\",\n  \"\\\\/\\\\.tmp\\\\/\",\n  \"\\\\/\\\\.cache\\\\/\",\n];\n\n/** @type {import('jest').Config} */\nmodule.exports = {\n  moduleNameMapper: {\n    \"@react-router/dev$\": \"<rootDir>/../react-router-dev/index.ts\",\n    \"@react-router/express$\": \"<rootDir>/../react-router-express/index.ts\",\n    \"@react-router/node$\": \"<rootDir>/../react-router-node/index.ts\",\n    \"@react-router/serve$\": \"<rootDir>/../react-router-serve/index.ts\",\n    \"^react-router$\": \"<rootDir>/../react-router/index.ts\",\n    \"^@web3-storage/multipart-parser$\": require.resolve(\n      \"@web3-storage/multipart-parser\",\n    ),\n  },\n  modulePathIgnorePatterns: ignorePatterns,\n  testMatch: [\"<rootDir>/**/*-test.[jt]s?(x)\"],\n  transform: {\n    \"\\\\.[jt]sx?$\": require.resolve(\"./transform\"),\n  },\n  watchPathIgnorePatterns: [...ignorePatterns, \"\\\\/node_modules\\\\/\"],\n  globals: {\n    __DEV__: true,\n  },\n};\n"
  },
  {
    "path": "jest/transform.js",
    "content": "let { default: babelJest } = require(\"babel-jest\");\n\n/**\n * Replace `import.meta` with `undefined`\n *\n * Needed to support server-side CJS in Jest\n * when `import.meta.hot` is used for HMR.\n */\nlet metaPlugin = ({ types: t }) => ({\n  visitor: {\n    MetaProperty: (path) => {\n      path.replaceWith(t.identifier(\"undefined\"));\n    },\n  },\n});\n\nmodule.exports = babelJest.createTransformer({\n  babelrc: false,\n  presets: [\n    [\"@babel/preset-env\", { loose: true }],\n    \"@babel/preset-react\",\n    \"@babel/preset-typescript\",\n  ],\n  plugins: [\"babel-plugin-dev-expression\", metaPlugin],\n});\n"
  },
  {
    "path": "package.json",
    "content": "{\n  \"name\": \"@remix-run/react-router\",\n  \"private\": true,\n  \"scripts\": {\n    \"build\": \"pnpm run --filter=\\\"./packages/**/*\\\" build\",\n    \"watch\": \"pnpm build && pnpm run --filter=\\\"./packages/**/*\\\" --parallel build --watch\",\n    \"clean\": \"git clean -fdX .\",\n    \"clean:build\": \"git clean -fdx -e node_modules .\",\n    \"clean:integration\": \"node ./integration/helpers/cleanup.mjs\",\n    \"docs\": \"pnpm run docs:typedoc && pnpm run docs:jsdoc\",\n    \"docs:typedoc\": \"typedoc\",\n    \"docs:jsdoc\": \"node --experimental-strip-types scripts/docs.ts\",\n    \"format\": \"prettier --ignore-path .eslintignore --write .\",\n    \"format:check\": \"prettier --ignore-path .eslintignore --check .\",\n    \"lint\": \"eslint --cache .\",\n    \"playground\": \"node ./scripts/playground.js\",\n    \"prerelease\": \"pnpm build\",\n    \"release\": \"changeset publish\",\n    \"test\": \"jest\",\n    \"test:inspect\": \"node --inspect-brk ./node_modules/.bin/jest\",\n    \"typegen\": \"pnpm run --recursive --parallel typegen\",\n    \"typecheck\": \"pnpm run --recursive --parallel typecheck\",\n    \"test:integration:run\": \"pnpm playwright:integration\",\n    \"test:integration\": \"pnpm build && pnpm test:integration:run\",\n    \"posttest:integration:run\": \"pnpm clean:integration\",\n    \"playwright:integration\": \"playwright test --config ./integration/playwright.config.ts\",\n    \"vite-ecosystem-ci:build\": \"pnpm build\",\n    \"vite-ecosystem-ci:before-test\": \"pnpm playwright install\",\n    \"vite-ecosystem-ci:test\": \"pnpm playwright:integration --project=chromium && pnpm clean:integration\",\n    \"changeset\": \"changeset\",\n    \"changeset:version\": \"changeset version && node ./scripts/remove-prerelease-changelogs.mjs\",\n    \"publish\": \"node scripts/publish.js\",\n    \"version\": \"node ./scripts/version\"\n  },\n  \"jest\": {\n    \"projects\": [\n      \"<rootDir>/packages/*\"\n    ],\n    \"reporters\": [\n      \"default\"\n    ]\n  },\n  \"packageManager\": \"pnpm@9.10.0\",\n  \"resolutions\": {\n    \"@types/react\": \"catalog:\",\n    \"@types/react-dom\": \"catalog:\",\n    \"@types/react-test-renderer\": \"^18.3.1\",\n    \"jsdom\": \"22.1.0\"\n  },\n  \"dependencies\": {\n    \"@babel/core\": \"^7.27.7\",\n    \"@babel/preset-env\": \"^7.27.2\",\n    \"@babel/preset-react\": \"^7.27.1\",\n    \"@babel/preset-typescript\": \"^7.27.1\",\n    \"@changesets/cli\": \"^2.26.2\",\n    \"@manypkg/get-packages\": \"^1.1.3\",\n    \"@mdx-js/rollup\": \"^3.1.0\",\n    \"@playwright/test\": \"^1.49.1\",\n    \"@remix-run/changelog-github\": \"^0.0.5\",\n    \"@types/jest\": \"^29.5.4\",\n    \"@types/jsdom\": \"^21.1.1\",\n    \"@types/react\": \"catalog:\",\n    \"@types/react-dom\": \"catalog:\",\n    \"@types/react-test-renderer\": \"^19.0.0\",\n    \"@typescript-eslint/eslint-plugin\": \"^7.5.0\",\n    \"@typescript-eslint/parser\": \"^7.5.0\",\n    \"babel-jest\": \"^29.7.0\",\n    \"babel-plugin-dev-expression\": \"^0.2.3\",\n    \"chalk\": \"^4.1.2\",\n    \"dox\": \"^1.0.0\",\n    \"eslint\": \"^8.57.0\",\n    \"eslint-config-react-app\": \"^7.0.1\",\n    \"eslint-plugin-flowtype\": \"^8.0.3\",\n    \"eslint-plugin-import\": \"^2.29.1\",\n    \"eslint-plugin-jest\": \"^27.9.0\",\n    \"eslint-plugin-jsdoc\": \"^51.3.4\",\n    \"eslint-plugin-jsx-a11y\": \"^6.8.0\",\n    \"eslint-plugin-react\": \"^7.34.1\",\n    \"eslint-plugin-react-hooks\": \"next\",\n    \"fast-glob\": \"3.2.11\",\n    \"isbot\": \"^5.1.11\",\n    \"jest\": \"^29.6.4\",\n    \"jsonfile\": \"^6.1.0\",\n    \"prettier\": \"^3.6.2\",\n    \"prompts\": \"^2.4.2\",\n    \"remark-gfm\": \"^3.0.1\",\n    \"remark-parse\": \"^10.0.1\",\n    \"remark-stringify\": \"^10.0.2\",\n    \"semver\": \"^7.5.4\",\n    \"typedoc\": \"^0.28.7\",\n    \"typescript\": \"catalog:\",\n    \"unified\": \"^10.1.2\",\n    \"unist-util-remove\": \"^3.1.0\",\n    \"vite\": \"^6.3.0\"\n  },\n  \"engines\": {\n    \"node\": \">=20.0.0\"\n  },\n  \"pnpm\": {\n    \"patchedDependencies\": {\n      \"@changesets/get-dependents-graph@1.3.6\": \"patches/@changesets__get-dependents-graph@1.3.6.patch\",\n      \"@changesets/assemble-release-plan\": \"patches/@changesets__assemble-release-plan.patch\",\n      \"@mdx-js/rollup\": \"patches/@mdx-js__rollup.patch\"\n    },\n    \"overrides\": {\n      \"workerd\": \"1.20250705.0\",\n      \"react-is\": \"19.1.0\"\n    }\n  }\n}\n"
  },
  {
    "path": "packages/create-react-router/CHANGELOG.md",
    "content": "# `create-react-router`\n\n## 7.13.1\n\n## 7.13.0\n\n_No changes_\n\n## 7.12.0\n\n_No changes_\n\n## 7.11.0\n\n_No changes_\n\n## 7.10.1\n\n_No changes_\n\n## 7.10.0\n\n_No changes_\n\n## 7.9.6\n\n_No changes_\n\n## 7.9.5\n\n_No changes_\n\n## 7.9.4\n\n_No changes_\n\n## 7.9.3\n\n_No changes_\n\n## 7.9.2\n\n_No changes_\n\n## 7.9.1\n\n_No changes_\n\n## 7.9.0\n\n_No changes_\n\n## 7.8.2\n\n_No changes_\n\n## 7.8.1\n\n_No changes_\n\n## 7.8.0\n\n_No changes_\n\n## 7.7.1\n\n_No changes_\n\n## 7.7.0\n\n### Minor Changes\n\n- Add Deno as a supported and detectable package manager. Note that this detection will only work with Deno versions 2.0.5 and above. If you are using an older version version of Deno then you must specify the --package-manager CLI flag set to `deno`. ([#12327](https://github.com/remix-run/react-router/pull/12327))\n\n## 7.6.3\n\n_No changes_\n\n## 7.6.2\n\n### Patch Changes\n\n- Update `tar-fs` ([#13675](https://github.com/remix-run/react-router/pull/13675))\n\n## 7.6.1\n\n_No changes_\n\n## 7.6.0\n\n_No changes_\n\n## 7.5.3\n\n_No changes_\n\n## 7.5.2\n\n_No changes_\n\n## 7.5.1\n\n_No changes_\n\n## 7.5.0\n\n_No changes_\n\n## 7.4.1\n\n_No changes_\n\n## 7.4.0\n\n_No changes_\n\n## 7.3.0\n\n_No changes_\n\n## 7.2.0\n\n_No changes_\n\n## 7.1.5\n\n_No changes_\n\n## 7.1.4\n\n_No changes_\n\n## 7.1.3\n\n_No changes_\n\n## 7.1.2\n\n_No changes_\n\n## 7.1.1\n\n_No changes_\n\n## 7.1.0\n\n### Patch Changes\n\n- Fix missing `fs-extra` dependency ([#12556](https://github.com/remix-run/react-router/pull/12556))\n\n## 7.0.2\n\n_No changes_\n\n## 7.0.1\n\n_No changes_\n\n## 7.0.0\n\nInitial release.\n"
  },
  {
    "path": "packages/create-react-router/README.md",
    "content": "Create a new React Router app.\n\n```sh\nnpm create react-router\n```\n"
  },
  {
    "path": "packages/create-react-router/__tests__/create-react-router-test.ts",
    "content": "import type { ChildProcessWithoutNullStreams } from \"node:child_process\";\nimport { spawn } from \"node:child_process\";\nimport {\n  existsSync,\n  mkdirSync,\n  readFileSync,\n  realpathSync,\n  rmSync,\n  writeFileSync,\n} from \"node:fs\";\nimport { mkdir, rm } from \"node:fs/promises\";\nimport { tmpdir } from \"node:os\";\nimport path from \"node:path\";\nimport { pathToFileURL } from \"node:url\";\nimport semver from \"semver\";\nimport stripAnsi from \"strip-ansi\";\n\nimport { jestTimeout } from \"./setupAfterEnv\";\nimport { createReactRouter } from \"../index\";\nimport { server } from \"./msw\";\n\nbeforeAll(() => server.listen({ onUnhandledRequest: \"error\" }));\nafterAll(() => server.close());\n\n// this is so we can mock \"npm install\" etc. in a cross-platform way\njest.mock(\"execa\");\n\nconst DOWN = \"\\x1B\\x5B\\x42\";\nconst ENTER = \"\\x0D\";\n\nconst TEMP_DIR = path.join(\n  realpathSync(tmpdir()),\n  `react-router-tests-${Math.random().toString(32).slice(2)}`,\n);\nfunction maskTempDir(string: string) {\n  return string.replace(TEMP_DIR, \"<TEMP_DIR>\");\n}\n\njest.setTimeout(30_000);\nbeforeAll(async () => {\n  await rm(TEMP_DIR, { force: true, recursive: true });\n  await mkdir(TEMP_DIR, { recursive: true });\n});\n\nafterAll(async () => {\n  await rm(TEMP_DIR, { force: true, recursive: true });\n});\n\ndescribe(\"create-react-router CLI\", () => {\n  let tempDirs = new Set<string>();\n\n  beforeEach(() => {\n    jest.clearAllMocks();\n  });\n\n  afterEach(async () => {\n    for (let dir of tempDirs) {\n      await rm(dir, { force: true, recursive: true });\n    }\n    tempDirs = new Set<string>();\n  });\n\n  function getProjectDir(name: string) {\n    let tmpDir = path.join(TEMP_DIR, name);\n    tempDirs.add(tmpDir);\n    return tmpDir;\n  }\n\n  it(\"supports the --help flag\", async () => {\n    let { stdout } = await execCreateReactRouter({\n      args: [\"--help\"],\n    });\n    expect(stdout.trim()).toMatchInlineSnapshot(`\n      \"create-react-router  \n\n      Usage:\n\n      $ create-react-router <projectDir> <...options>\n\n      Values:\n\n      projectDir          The React Router project directory\n\n      Options:\n\n      --help, -h          Print this help message and exit\n      --version, -V       Print the CLI version and exit\n      --no-color          Disable ANSI colors in console output\n      --no-motion         Disable animations in console output\n\n      --template <name>   The project template to use\n      --[no-]install      Whether or not to install dependencies after creation\n      --package-manager   The package manager to use\n      --show-install-output   Whether to show the output of the install process\n      --[no-]git-init     Whether or not to initialize a Git repository\n      --yes, -y           Skip all option prompts and run setup\n      --react-router-version, -v     The version of React Router to use\n\n      Creating a new project:\n\n      React Router projects are created from templates. A template can be:\n\n      - a GitHub repo shorthand, :username/:repo or :username/:repo/:directory\n      - the URL of a GitHub repo (or directory within it)\n      - the URL of a tarball\n      - a file path to a directory of files\n      - a file path to a tarball\n\n      $ create-react-router my-app --template remix-run/react-router/templates/basic\n      $ create-react-router my-app --template remix-run/react-router/examples/basic\n      $ create-react-router my-app --template :username/:repo\n      $ create-react-router my-app --template :username/:repo/:directory\n      $ create-react-router my-app --template https://github.com/:username/:repo\n      $ create-react-router my-app --template https://github.com/:username/:repo/tree/:branch\n      $ create-react-router my-app --template https://github.com/:username/:repo/tree/:branch/:directory\n      $ create-react-router my-app --template https://github.com/:username/:repo/archive/refs/tags/:tag.tar.gz\n      $ create-react-router my-app --template https://example.com/template.tar.gz\n      $ create-react-router my-app --template ./path/to/template\n      $ create-react-router my-app --template ./path/to/template.tar.gz\n\n      To create a new project from a template in a private GitHub repo,\n      pass the \\`token\\` flag with a personal access token with access\n      to that repo.\"\n    `);\n  });\n\n  it(\"supports the --version flag\", async () => {\n    let { stdout } = await execCreateReactRouter({\n      args: [\"--version\"],\n    });\n    expect(!!semver.valid(stdout.trim())).toBe(true);\n  });\n\n  it(\"allows you to go through the prompts\", async () => {\n    let projectDir = getProjectDir(\"prompts\");\n\n    let { status, stderr } = await execCreateReactRouter({\n      args: [],\n      interactions: [\n        {\n          question: /where.*create.*project/i,\n          type: [projectDir, ENTER],\n        },\n        {\n          question: /init.*git/i,\n          type: [\"n\"],\n        },\n        {\n          question: /install dependencies/i,\n          type: [\"n\"],\n        },\n      ],\n    });\n\n    expect(stderr.trim()).toBeFalsy();\n    expect(status).toBe(0);\n    expect(existsSync(path.join(projectDir, \"package.json\"))).toBeTruthy();\n    expect(existsSync(path.join(projectDir, \"app/root.tsx\"))).toBeTruthy();\n  });\n\n  it(\"supports the --yes flag\", async () => {\n    let projectDir = getProjectDir(\"yes\");\n\n    let { status, stderr } = await execCreateReactRouter({\n      args: [projectDir, \"--yes\", \"--no-git-init\", \"--no-install\"],\n    });\n\n    expect(stderr.trim()).toBeFalsy();\n    expect(status).toBe(0);\n    expect(existsSync(path.join(projectDir, \"package.json\"))).toBeTruthy();\n    expect(existsSync(path.join(projectDir, \"app/root.tsx\"))).toBeTruthy();\n  });\n\n  it(\"errors when project directory isn't provided when shell isn't interactive\", async () => {\n    let projectDir = getProjectDir(\"non-interactive-no-project-dir\");\n\n    let { status, stderr } = await execCreateReactRouter({\n      args: [\"--no-install\"],\n      interactive: false,\n    });\n\n    expect(stderr.trim()).toMatchInlineSnapshot(\n      `\"▲  Oh no! No project directory provided\"`,\n    );\n    expect(status).toBe(1);\n    expect(existsSync(path.join(projectDir, \"package.json\"))).toBeFalsy();\n    expect(existsSync(path.join(projectDir, \"app/root.tsx\"))).toBeFalsy();\n  });\n\n  it(\"works for GitHub username/repo combo\", async () => {\n    let projectDir = getProjectDir(\"github-username-repo\");\n\n    let { status, stderr } = await execCreateReactRouter({\n      args: [\n        projectDir,\n        \"--template\",\n        \"react-router-fake-tester-username/react-router-fake-tester-repo\",\n        \"--no-git-init\",\n        \"--no-install\",\n      ],\n    });\n\n    expect(stderr.trim()).toBeFalsy();\n    expect(status).toBe(0);\n    expect(existsSync(path.join(projectDir, \"package.json\"))).toBeTruthy();\n    expect(existsSync(path.join(projectDir, \"app/root.tsx\"))).toBeTruthy();\n  });\n\n  it(\"works for GitHub username/repo/path combo\", async () => {\n    let projectDir = getProjectDir(\"github-username-repo-path\");\n\n    let { status, stderr } = await execCreateReactRouter({\n      args: [\n        projectDir,\n        \"--template\",\n        \"fake-react-router-tester/nested-dir/stack\",\n        \"--no-git-init\",\n        \"--no-install\",\n      ],\n    });\n\n    expect(stderr.trim()).toBeFalsy();\n    expect(status).toBe(0);\n    expect(existsSync(path.join(projectDir, \"package.json\"))).toBeTruthy();\n    expect(existsSync(path.join(projectDir, \"app/root.tsx\"))).toBeTruthy();\n  });\n\n  it(\"works for GitHub username/repo/path combo (when dots exist in folder)\", async () => {\n    let projectDir = getProjectDir(\"github-username-repo-path-dots\");\n\n    let { status, stderr } = await execCreateReactRouter({\n      args: [\n        projectDir,\n        \"--template\",\n        \"fake-react-router-tester/nested-dir/folder.with.dots\",\n        \"--no-git-init\",\n        \"--no-install\",\n      ],\n    });\n\n    expect(stderr.trim()).toBeFalsy();\n    expect(status).toBe(0);\n    expect(existsSync(path.join(projectDir, \"package.json\"))).toBeTruthy();\n    expect(existsSync(path.join(projectDir, \"app/root.tsx\"))).toBeTruthy();\n  });\n\n  it(\"fails for GitHub username/repo/path combo when path doesn't exist\", async () => {\n    let projectDir = getProjectDir(\"github-username-repo-path-missing\");\n\n    let { status, stderr } = await execCreateReactRouter({\n      args: [\n        projectDir,\n        \"--template\",\n        \"fake-react-router-tester/nested-dir/this/path/does/not/exist\",\n        \"--no-git-init\",\n        \"--no-install\",\n      ],\n    });\n\n    expect(stderr.trim()).toMatchInlineSnapshot(\n      `\"▲  Oh no! The path \"this/path/does/not/exist\" was not found in this GitHub repo.\"`,\n    );\n    expect(status).toBe(1);\n    expect(existsSync(path.join(projectDir, \"package.json\"))).toBeFalsy();\n    expect(existsSync(path.join(projectDir, \"app/root.tsx\"))).toBeFalsy();\n  });\n\n  it(\"fails for private GitHub username/repo combo without a token\", async () => {\n    let projectDir = getProjectDir(\"private-repo-no-token\");\n\n    let { status, stderr } = await execCreateReactRouter({\n      args: [\n        projectDir,\n        \"--template\",\n        \"private-org/private-repo\",\n        \"--no-git-init\",\n        \"--no-install\",\n      ],\n    });\n\n    expect(stderr.trim()).toMatchInlineSnapshot(\n      `\"▲  Oh no! There was a problem fetching the file from GitHub. The request responded with a 404 status. Please try again later.\"`,\n    );\n    expect(status).toBe(1);\n  });\n\n  it(\"succeeds for private GitHub username/repo combo with a valid token\", async () => {\n    let projectDir = getProjectDir(\"github-username-repo-with-token\");\n\n    let { status, stderr } = await execCreateReactRouter({\n      args: [\n        projectDir,\n        \"--template\",\n        \"private-org/private-repo\",\n        \"--no-git-init\",\n        \"--no-install\",\n        \"--token\",\n        \"valid-token\",\n      ],\n    });\n\n    expect(stderr.trim()).toBeFalsy();\n    expect(status).toBe(0);\n    expect(existsSync(path.join(projectDir, \"package.json\"))).toBeTruthy();\n    expect(existsSync(path.join(projectDir, \"app/root.tsx\"))).toBeTruthy();\n  });\n\n  it(\"works for remote tarballs\", async () => {\n    let projectDir = getProjectDir(\"remote-tarball\");\n\n    let { status, stderr } = await execCreateReactRouter({\n      args: [\n        projectDir,\n        \"--template\",\n        \"https://example.com/template.tar.gz\",\n        \"--no-git-init\",\n        \"--no-install\",\n      ],\n    });\n\n    expect(stderr.trim()).toBeFalsy();\n    expect(status).toBe(0);\n    expect(existsSync(path.join(projectDir, \"package.json\"))).toBeTruthy();\n    expect(existsSync(path.join(projectDir, \"app/root.tsx\"))).toBeTruthy();\n  });\n\n  it(\"fails for private github release tarballs\", async () => {\n    let projectDir = getProjectDir(\"private-release-tarball-no-token\");\n\n    let { status, stderr } = await execCreateReactRouter({\n      args: [\n        projectDir,\n        \"--template\",\n        \"https://github.com/private-org/private-repo/releases/download/v0.0.1/template.tar.gz\",\n        \"--no-git-init\",\n        \"--no-install\",\n      ],\n    });\n\n    expect(stderr.trim()).toMatchInlineSnapshot(\n      `\"▲  Oh no! There was a problem fetching the file from GitHub. The request responded with a 404 status. Please try again later.\"`,\n    );\n    expect(status).toBe(1);\n  });\n\n  it(\"succeeds for private github release tarballs when including token\", async () => {\n    let projectDir = getProjectDir(\"private-release-tarball-with-token\");\n\n    let { status, stderr } = await execCreateReactRouter({\n      args: [\n        projectDir,\n        \"--template\",\n        \"https://github.com/private-org/private-repo/releases/download/v0.0.1/template.tar.gz\",\n        \"--token\",\n        \"valid-token\",\n        \"--no-git-init\",\n        \"--no-install\",\n      ],\n    });\n\n    expect(stderr.trim()).toBeFalsy();\n    expect(status).toBe(0);\n    expect(existsSync(path.join(projectDir, \"package.json\"))).toBeTruthy();\n    expect(existsSync(path.join(projectDir, \"app/root.tsx\"))).toBeTruthy();\n  });\n\n  it(\"works for different branches and nested paths\", async () => {\n    let projectDir = getProjectDir(\"diff-branch\");\n\n    let { status, stderr } = await execCreateReactRouter({\n      args: [\n        projectDir,\n        \"--template\",\n        \"https://github.com/fake-react-router-tester/nested-dir/tree/dev/stack\",\n        \"--no-git-init\",\n        \"--no-install\",\n      ],\n    });\n\n    expect(stderr.trim()).toBeFalsy();\n    expect(status).toBe(0);\n    expect(existsSync(path.join(projectDir, \"package.json\"))).toBeTruthy();\n    expect(existsSync(path.join(projectDir, \"app/root.tsx\"))).toBeTruthy();\n  });\n\n  it(\"fails for different branches and nested paths when path doesn't exist\", async () => {\n    let projectDir = getProjectDir(\"diff-branch-invalid-path\");\n\n    let { status, stderr } = await execCreateReactRouter({\n      args: [\n        projectDir,\n        \"--template\",\n        \"https://github.com/fake-react-router-tester/nested-dir/tree/dev/this/path/does/not/exist\",\n        \"--no-git-init\",\n        \"--no-install\",\n      ],\n    });\n\n    expect(stderr.trim()).toMatchInlineSnapshot(\n      `\"▲  Oh no! The path \"this/path/does/not/exist\" was not found in this GitHub repo.\"`,\n    );\n    expect(status).toBe(1);\n    expect(existsSync(path.join(projectDir, \"package.json\"))).toBeFalsy();\n    expect(existsSync(path.join(projectDir, \"app/root.tsx\"))).toBeFalsy();\n  });\n\n  it(\"works for a path to a tarball on disk\", async () => {\n    let projectDir = getProjectDir(\"local-tarball\");\n\n    let { status, stderr } = await execCreateReactRouter({\n      args: [\n        projectDir,\n        \"--template\",\n        path.join(__dirname, \"fixtures\", \"template.tar.gz\"),\n        \"--no-git-init\",\n        \"--no-install\",\n      ],\n    });\n\n    expect(stderr.trim()).toBeFalsy();\n    expect(status).toBe(0);\n    expect(existsSync(path.join(projectDir, \"package.json\"))).toBeTruthy();\n    expect(existsSync(path.join(projectDir, \"app/root.tsx\"))).toBeTruthy();\n  });\n\n  it(\"works for a path to a tgz tarball on disk\", async () => {\n    let projectDir = getProjectDir(\"local-tarball\");\n\n    let { status, stderr } = await execCreateReactRouter({\n      args: [\n        projectDir,\n        \"--template\",\n        path.join(__dirname, \"fixtures\", \"template.tgz\"),\n        \"--no-git-init\",\n        \"--no-install\",\n      ],\n    });\n\n    expect(stderr.trim()).toBeFalsy();\n    expect(status).toBe(0);\n    expect(existsSync(path.join(projectDir, \"package.json\"))).toBeTruthy();\n    expect(existsSync(path.join(projectDir, \"app/root.tsx\"))).toBeTruthy();\n  });\n\n  it(\"works for a file URL to a tarball on disk\", async () => {\n    let projectDir = getProjectDir(\"file-url-tarball\");\n\n    let { status, stderr } = await execCreateReactRouter({\n      args: [\n        projectDir,\n        \"--template\",\n        pathToFileURL(\n          path.join(__dirname, \"fixtures\", \"template.tar.gz\"),\n        ).toString(),\n        \"--no-git-init\",\n        \"--no-install\",\n      ],\n    });\n\n    expect(stderr.trim()).toBeFalsy();\n    expect(status).toBe(0);\n    expect(existsSync(path.join(projectDir, \"package.json\"))).toBeTruthy();\n  });\n\n  it(\"works for a file path to a directory on disk\", async () => {\n    let projectDir = getProjectDir(\"local-directory\");\n\n    let { status, stderr } = await execCreateReactRouter({\n      args: [\n        projectDir,\n        \"--template\",\n        path.join(__dirname, \"fixtures\", \"blank\"),\n        \"--no-git-init\",\n        \"--no-install\",\n      ],\n    });\n\n    expect(stderr.trim()).toBeFalsy();\n    expect(status).toBe(0);\n    expect(existsSync(path.join(projectDir, \"package.json\"))).toBeTruthy();\n  });\n\n  it(\"works for a file URL to a directory on disk\", async () => {\n    let projectDir = getProjectDir(\"file-url-directory\");\n\n    let { status, stderr } = await execCreateReactRouter({\n      args: [\n        projectDir,\n        \"--template\",\n        pathToFileURL(path.join(__dirname, \"fixtures\", \"blank\")).toString(),\n        \"--no-git-init\",\n        \"--no-install\",\n      ],\n    });\n\n    expect(stderr.trim()).toBeFalsy();\n    expect(status).toBe(0);\n    expect(existsSync(path.join(projectDir, \"package.json\"))).toBeTruthy();\n  });\n\n  it(\"runs npm install by default\", async () => {\n    let originalUserAgent = process.env.npm_config_user_agent;\n    process.env.npm_config_user_agent = undefined;\n\n    let projectDir = getProjectDir(\"npm-install-default\");\n\n    let execa = require(\"execa\");\n    execa.mockImplementation(async () => {});\n\n    // Suppress terminal output\n    let stdoutMock = jest\n      .spyOn(process.stdout, \"write\")\n      .mockImplementation(() => true);\n\n    await createReactRouter([\n      projectDir,\n      \"--template\",\n      path.join(__dirname, \"fixtures\", \"blank\"),\n      \"--no-git-init\",\n      \"--yes\",\n    ]);\n\n    stdoutMock.mockReset();\n\n    expect(execa).toHaveBeenCalledWith(\n      \"npm\",\n      expect.arrayContaining([\"install\"]),\n      expect.anything(),\n    );\n\n    process.env.npm_config_user_agent = originalUserAgent;\n  });\n\n  it(\"runs npm install if package manager in user agent string is unknown\", async () => {\n    let originalUserAgent = process.env.npm_config_user_agent;\n    process.env.npm_config_user_agent =\n      \"unknown_package_manager/1.0.0 npm/? node/v14.17.0 linux x64\";\n\n    let projectDir = getProjectDir(\"npm-install-on-unknown-package-manager\");\n\n    let execa = require(\"execa\");\n    execa.mockImplementation(async () => {});\n\n    // Suppress terminal output\n    let stdoutMock = jest\n      .spyOn(process.stdout, \"write\")\n      .mockImplementation(() => true);\n\n    await createReactRouter([\n      projectDir,\n      \"--template\",\n      path.join(__dirname, \"fixtures\", \"blank\"),\n      \"--no-git-init\",\n      \"--yes\",\n    ]);\n\n    stdoutMock.mockReset();\n\n    expect(execa).toHaveBeenCalledWith(\n      \"npm\",\n      expect.arrayContaining([\"install\"]),\n      expect.anything(),\n    );\n\n    process.env.npm_config_user_agent = originalUserAgent;\n  });\n\n  it(\"recognizes when npm was used to run the command\", async () => {\n    let originalUserAgent = process.env.npm_config_user_agent;\n    process.env.npm_config_user_agent =\n      \"npm/8.19.4 npm/? node/v14.17.0 linux x64\";\n\n    let projectDir = getProjectDir(\"npm-install-from-user-agent\");\n\n    let execa = require(\"execa\");\n    execa.mockImplementation(async () => {});\n\n    // Suppress terminal output\n    let stdoutMock = jest\n      .spyOn(process.stdout, \"write\")\n      .mockImplementation(() => true);\n\n    await createReactRouter([\n      projectDir,\n      \"--template\",\n      path.join(__dirname, \"fixtures\", \"blank\"),\n      \"--no-git-init\",\n      \"--yes\",\n    ]);\n\n    stdoutMock.mockReset();\n\n    expect(execa).toHaveBeenCalledWith(\n      \"npm\",\n      expect.arrayContaining([\"install\"]),\n      expect.anything(),\n    );\n    process.env.npm_config_user_agent = originalUserAgent;\n  });\n\n  it(\"recognizes when Yarn was used to run the command\", async () => {\n    let originalUserAgent = process.env.npm_config_user_agent;\n    process.env.npm_config_user_agent =\n      \"yarn/1.22.18 npm/? node/v14.17.0 linux x64\";\n\n    let projectDir = getProjectDir(\"yarn-create-from-user-agent\");\n\n    let execa = require(\"execa\");\n    execa.mockImplementation(async () => {});\n\n    // Suppress terminal output\n    let stdoutMock = jest\n      .spyOn(process.stdout, \"write\")\n      .mockImplementation(() => true);\n\n    await createReactRouter([\n      projectDir,\n      \"--template\",\n      path.join(__dirname, \"fixtures\", \"blank\"),\n      \"--no-git-init\",\n      \"--yes\",\n    ]);\n\n    stdoutMock.mockReset();\n\n    expect(execa).toHaveBeenCalledWith(\n      \"yarn\",\n      expect.arrayContaining([\"install\"]),\n      expect.anything(),\n    );\n    process.env.npm_config_user_agent = originalUserAgent;\n  });\n\n  it(\"recognizes when pnpm was used to run the command\", async () => {\n    let originalUserAgent = process.env.npm_config_user_agent;\n    process.env.npm_config_user_agent =\n      \"pnpm/6.32.3 npm/? node/v14.17.0 linux x64\";\n\n    let projectDir = getProjectDir(\"pnpm-create-from-user-agent\");\n\n    let execa = require(\"execa\");\n    execa.mockImplementation(async () => {});\n\n    // Suppress terminal output\n    let stdoutMock = jest\n      .spyOn(process.stdout, \"write\")\n      .mockImplementation(() => true);\n\n    await createReactRouter([\n      projectDir,\n      \"--template\",\n      path.join(__dirname, \"fixtures\", \"blank\"),\n      \"--no-git-init\",\n      \"--yes\",\n    ]);\n\n    stdoutMock.mockReset();\n\n    expect(execa).toHaveBeenCalledWith(\n      \"pnpm\",\n      expect.arrayContaining([\"install\"]),\n      expect.anything(),\n    );\n    process.env.npm_config_user_agent = originalUserAgent;\n  });\n\n  it(\"recognizes when Bun was used to run the command\", async () => {\n    let originalUserAgent = process.env.npm_config_user_agent;\n    process.env.npm_config_user_agent =\n      \"bun/0.7.0 npm/? node/v14.17.0 linux x64\";\n\n    let projectDir = getProjectDir(\"bun-create-from-user-agent\");\n\n    let execa = require(\"execa\");\n    execa.mockImplementation(async () => {});\n\n    // Suppress terminal output\n    let stdoutMock = jest\n      .spyOn(process.stdout, \"write\")\n      .mockImplementation(() => true);\n\n    await createReactRouter([\n      projectDir,\n      \"--template\",\n      path.join(__dirname, \"fixtures\", \"blank\"),\n      \"--no-git-init\",\n      \"--yes\",\n    ]);\n\n    stdoutMock.mockReset();\n\n    expect(execa).toHaveBeenCalledWith(\n      \"bun\",\n      expect.arrayContaining([\"install\"]),\n      expect.anything(),\n    );\n    process.env.npm_config_user_agent = originalUserAgent;\n  });\n\n  it(\"recognizes when Deno was used to run the command\", async () => {\n    let originalUserAgent = process.env.npm_config_user_agent;\n    process.env.npm_config_user_agent =\n      \"deno/2.0.6 npm/? deno/2.0.6 linux x86_64\";\n\n    let projectDir = getProjectDir(\"deno-create-from-user-agent\");\n\n    let execa = require(\"execa\");\n    execa.mockImplementation(async () => {});\n\n    // Suppress terminal output\n    let stdoutMock = jest\n      .spyOn(process.stdout, \"write\")\n      .mockImplementation(() => true);\n\n    await createReactRouter([\n      projectDir,\n      \"--template\",\n      path.join(__dirname, \"fixtures\", \"blank\"),\n      \"--no-git-init\",\n      \"--yes\",\n    ]);\n\n    stdoutMock.mockReset();\n\n    expect(execa).toHaveBeenCalledWith(\n      \"deno\",\n      expect.arrayContaining([\"install\"]),\n      expect.anything(),\n    );\n    process.env.npm_config_user_agent = originalUserAgent;\n  });\n\n  it(\"supports specifying the package manager, regardless of user agent\", async () => {\n    let originalUserAgent = process.env.npm_config_user_agent;\n    process.env.npm_config_user_agent =\n      \"yarn/1.22.18 npm/? node/v14.17.0 linux x64\";\n\n    let projectDir = getProjectDir(\"pnpm-create-override\");\n\n    let execa = require(\"execa\");\n    execa.mockImplementation(async () => {});\n\n    // Suppress terminal output\n    let stdoutMock = jest\n      .spyOn(process.stdout, \"write\")\n      .mockImplementation(() => true);\n\n    await createReactRouter([\n      projectDir,\n      \"--template\",\n      path.join(__dirname, \"fixtures\", \"blank\"),\n      \"--no-git-init\",\n      \"--yes\",\n      \"--package-manager\",\n      \"pnpm\",\n    ]);\n\n    stdoutMock.mockReset();\n\n    expect(execa).toHaveBeenCalledWith(\n      \"pnpm\",\n      expect.arrayContaining([\"install\"]),\n      expect.anything(),\n    );\n    process.env.npm_config_user_agent = originalUserAgent;\n  });\n\n  it(\"works when creating an app in the current dir\", async () => {\n    let emptyDir = getProjectDir(\"current-dir-if-empty\");\n    mkdirSync(emptyDir);\n\n    let { status, stderr } = await execCreateReactRouter({\n      cwd: emptyDir,\n      args: [\n        \".\",\n        \"--template\",\n        path.join(__dirname, \"fixtures\", \"basic\"),\n        \"--no-git-init\",\n        \"--no-install\",\n      ],\n    });\n\n    expect(stderr.trim()).toBeFalsy();\n    expect(status).toBe(0);\n    expect(existsSync(path.join(emptyDir, \"package.json\"))).toBeTruthy();\n    expect(existsSync(path.join(emptyDir, \"app/root.tsx\"))).toBeTruthy();\n  });\n\n  it(\"does not copy .git nor node_modules directories if they exist in the template\", async () => {\n    // Can't really commit this file into a git repo, so just create it as\n    // part of the test and then remove it when we're done\n    let templateWithIgnoredDirs = path.join(\n      __dirname,\n      \"fixtures\",\n      \"with-ignored-dir\",\n    );\n    mkdirSync(path.join(templateWithIgnoredDirs, \".git\"));\n    writeFileSync(\n      path.join(templateWithIgnoredDirs, \".git\", \"some-git-file.txt\"),\n      \"\",\n    );\n    mkdirSync(path.join(templateWithIgnoredDirs, \"node_modules\"));\n    writeFileSync(\n      path.join(\n        templateWithIgnoredDirs,\n        \"node_modules\",\n        \"some-node-module-file.txt\",\n      ),\n      \"\",\n    );\n\n    let projectDir = getProjectDir(\"with-git-dir\");\n\n    try {\n      let { status, stderr } = await execCreateReactRouter({\n        args: [\n          projectDir,\n          \"--template\",\n          templateWithIgnoredDirs,\n          \"--no-git-init\",\n          \"--no-install\",\n        ],\n      });\n\n      expect(stderr.trim()).toBeFalsy();\n      expect(status).toBe(0);\n      expect(existsSync(path.join(projectDir, \".git\"))).toBeFalsy();\n      expect(existsSync(path.join(projectDir, \"node_modules\"))).toBeFalsy();\n      expect(existsSync(path.join(projectDir, \"package.json\"))).toBeTruthy();\n    } finally {\n      rmSync(path.join(templateWithIgnoredDirs, \".git\"), {\n        force: true,\n        recursive: true,\n      });\n      rmSync(path.join(templateWithIgnoredDirs, \"node_modules\"), {\n        force: true,\n        recursive: true,\n      });\n    }\n  });\n\n  it(\"changes star dependencies for only React Router packages\", async () => {\n    let projectDir = getProjectDir(\"local-directory\");\n\n    let { status } = await execCreateReactRouter({\n      args: [\n        projectDir,\n        \"--template\",\n        path.join(__dirname, \"fixtures\", \"basic\"),\n        \"--no-git-init\",\n        \"--no-install\",\n      ],\n    });\n\n    expect(status).toBe(0);\n\n    let packageJsonPath = path.join(projectDir, \"package.json\");\n    let packageJson = JSON.parse(String(readFileSync(packageJsonPath, \"utf8\")));\n    let dependencies = packageJson.dependencies;\n\n    expect(dependencies).toMatchObject({\n      \"react-router\": expect.any(String),\n      \"react-router-dom\": expect.any(String),\n      \"@react-router/node\": expect.any(String),\n      \"not-react-router\": \"*\",\n    });\n  });\n\n  describe(\"when project directory contains files\", () => {\n    describe(\"interactive shell\", () => {\n      let interactive = true;\n\n      it(\"works without prompt when there are no collisions\", async () => {\n        let projectDir = getProjectDir(\"not-empty-dir-interactive\");\n        mkdirSync(projectDir);\n        writeFileSync(path.join(projectDir, \"some-file.txt\"), \"\");\n\n        let { status, stderr } = await execCreateReactRouter({\n          args: [\n            projectDir,\n            \"--template\",\n            path.join(__dirname, \"fixtures\", \"basic\"),\n            \"--no-git-init\",\n            \"--no-install\",\n          ],\n          interactive,\n        });\n\n        expect(stderr.trim()).toBeFalsy();\n        expect(status).toBe(0);\n        expect(existsSync(path.join(projectDir, \"package.json\"))).toBeTruthy();\n        expect(existsSync(path.join(projectDir, \"app/root.tsx\"))).toBeTruthy();\n      });\n\n      it(\"prompts for overwrite when there are collisions\", async () => {\n        let notEmptyDir = getProjectDir(\"not-empty-dir-interactive-collisions\");\n        mkdirSync(notEmptyDir);\n        writeFileSync(path.join(notEmptyDir, \"package.json\"), \"\");\n        writeFileSync(path.join(notEmptyDir, \"tsconfig.json\"), \"\");\n\n        let { status, stdout, stderr } = await execCreateReactRouter({\n          args: [\n            notEmptyDir,\n            \"--template\",\n            path.join(__dirname, \"fixtures\", \"basic\"),\n            \"--no-git-init\",\n            \"--no-install\",\n          ],\n          interactive,\n          interactions: [\n            {\n              question: /contains files that will be overwritten/i,\n              type: [\"y\"],\n            },\n          ],\n        });\n\n        expect(stdout).toContain(\"Files that would be overwritten:\");\n        expect(stdout).toContain(\"package.json\");\n        expect(stdout).toContain(\"tsconfig.json\");\n        expect(status).toBe(0);\n        expect(stderr.trim()).toBeFalsy();\n        expect(existsSync(path.join(notEmptyDir, \"package.json\"))).toBeTruthy();\n        expect(\n          existsSync(path.join(notEmptyDir, \"tsconfig.json\")),\n        ).toBeTruthy();\n        expect(existsSync(path.join(notEmptyDir, \"app/root.tsx\"))).toBeTruthy();\n      });\n\n      it(\"works without prompt when --overwrite is specified\", async () => {\n        let projectDir = getProjectDir(\n          \"not-empty-dir-interactive-collisions-overwrite\",\n        );\n        mkdirSync(projectDir);\n        writeFileSync(path.join(projectDir, \"package.json\"), \"\");\n        writeFileSync(path.join(projectDir, \"tsconfig.json\"), \"\");\n\n        let { status, stdout, stderr } = await execCreateReactRouter({\n          args: [\n            projectDir,\n            \"--template\",\n            path.join(__dirname, \"fixtures\", \"basic\"),\n            \"--overwrite\",\n            \"--no-git-init\",\n            \"--no-install\",\n          ],\n        });\n\n        expect(stdout).toContain(\n          \"Overwrite: overwriting files due to `--overwrite`\",\n        );\n        expect(stdout).toContain(\"package.json\");\n        expect(stdout).toContain(\"tsconfig.json\");\n        expect(status).toBe(0);\n        expect(stderr.trim()).toBeFalsy();\n        expect(existsSync(path.join(projectDir, \"package.json\"))).toBeTruthy();\n        expect(existsSync(path.join(projectDir, \"tsconfig.json\"))).toBeTruthy();\n        expect(existsSync(path.join(projectDir, \"app/root.tsx\"))).toBeTruthy();\n      });\n    });\n\n    describe(\"non-interactive shell\", () => {\n      let interactive = false;\n\n      it(\"works when there are no collisions\", async () => {\n        let projectDir = getProjectDir(\"not-empty-dir-non-interactive\");\n        mkdirSync(projectDir);\n        writeFileSync(path.join(projectDir, \"some-file.txt\"), \"\");\n\n        let { status, stderr } = await execCreateReactRouter({\n          args: [\n            projectDir,\n            \"--template\",\n            path.join(__dirname, \"fixtures\", \"basic\"),\n            \"--no-git-init\",\n            \"--no-install\",\n          ],\n          interactive,\n        });\n\n        expect(stderr.trim()).toBeFalsy();\n        expect(status).toBe(0);\n        expect(existsSync(path.join(projectDir, \"package.json\"))).toBeTruthy();\n        expect(existsSync(path.join(projectDir, \"app/root.tsx\"))).toBeTruthy();\n      });\n\n      it(\"errors when there are collisions\", async () => {\n        let projectDir = getProjectDir(\n          \"not-empty-dir-non-interactive-collisions\",\n        );\n        mkdirSync(projectDir);\n        writeFileSync(path.join(projectDir, \"package.json\"), \"\");\n        writeFileSync(path.join(projectDir, \"tsconfig.json\"), \"\");\n\n        let { status, stderr } = await execCreateReactRouter({\n          args: [\n            projectDir,\n            \"--template\",\n            path.join(__dirname, \"fixtures\", \"basic\"),\n            \"--no-git-init\",\n            \"--no-install\",\n          ],\n          interactive,\n        });\n\n        expect(stderr.trim()).toMatchInlineSnapshot(`\n                  \"▲  Oh no! Destination directory contains files that would be overwritten\n                           and no \\`--overwrite\\` flag was included in a non-interactive\n                           environment. The following files would be overwritten:\n                             package.json\n                             tsconfig.json\"\n              `);\n        expect(status).toBe(1);\n        expect(existsSync(path.join(projectDir, \"app/root.tsx\"))).toBeFalsy();\n      });\n\n      it(\"works when there are collisions and --overwrite is specified\", async () => {\n        let projectDir = getProjectDir(\n          \"not-empty-dir-non-interactive-collisions-overwrite\",\n        );\n        mkdirSync(projectDir);\n        writeFileSync(path.join(projectDir, \"package.json\"), \"\");\n        writeFileSync(path.join(projectDir, \"tsconfig.json\"), \"\");\n\n        let { status, stdout, stderr } = await execCreateReactRouter({\n          args: [\n            projectDir,\n            \"--template\",\n            path.join(__dirname, \"fixtures\", \"basic\"),\n            \"--no-git-init\",\n            \"--no-install\",\n            \"--overwrite\",\n          ],\n          interactive,\n        });\n\n        expect(stdout).toContain(\n          \"Overwrite: overwriting files due to `--overwrite`\",\n        );\n        expect(stdout).toContain(\"package.json\");\n        expect(stdout).toContain(\"tsconfig.json\");\n        expect(status).toBe(0);\n        expect(stderr.trim()).toBeFalsy();\n        expect(existsSync(path.join(projectDir, \"package.json\"))).toBeTruthy();\n        expect(existsSync(path.join(projectDir, \"tsconfig.json\"))).toBeTruthy();\n        expect(existsSync(path.join(projectDir, \"app/root.tsx\"))).toBeTruthy();\n      });\n    });\n  });\n\n  describe(\"errors\", () => {\n    it(\"identifies when a github repo is not accessible (403)\", async () => {\n      let projectDir = getProjectDir(\"repo-403\");\n\n      let { status, stderr } = await execCreateReactRouter({\n        args: [\n          projectDir,\n          \"--template\",\n          \"error-username/403\",\n          \"--no-git-init\",\n          \"--no-install\",\n        ],\n      });\n\n      expect(stderr.trim()).toMatchInlineSnapshot(\n        `\"▲  Oh no! There was a problem fetching the file from GitHub. The request responded with a 403 status. Please try again later.\"`,\n      );\n      expect(status).toBe(1);\n    });\n\n    it(\"identifies when a github repo does not exist (404)\", async () => {\n      let projectDir = getProjectDir(\"repo-404\");\n\n      let { status, stderr } = await execCreateReactRouter({\n        args: [\n          projectDir,\n          \"--template\",\n          \"error-username/404\",\n          \"--no-git-init\",\n          \"--no-install\",\n        ],\n      });\n\n      expect(stderr.trim()).toMatchInlineSnapshot(\n        `\"▲  Oh no! There was a problem fetching the file from GitHub. The request responded with a 404 status. Please try again later.\"`,\n      );\n      expect(status).toBe(1);\n    });\n\n    it(\"identifies when something unknown goes wrong with the repo request (4xx)\", async () => {\n      let projectDir = getProjectDir(\"repo-4xx\");\n\n      let { status, stderr } = await execCreateReactRouter({\n        args: [\n          projectDir,\n          \"--template\",\n          \"error-username/400\",\n          \"--no-git-init\",\n          \"--no-install\",\n        ],\n      });\n\n      expect(stderr.trim()).toMatchInlineSnapshot(\n        `\"▲  Oh no! There was a problem fetching the file from GitHub. The request responded with a 400 status. Please try again later.\"`,\n      );\n      expect(status).toBe(1);\n    });\n\n    it(\"identifies when a remote tarball does not exist (404)\", async () => {\n      let projectDir = getProjectDir(\"remote-tarball-404\");\n\n      let { status, stderr } = await execCreateReactRouter({\n        args: [\n          projectDir,\n          \"--template\",\n          \"https://example.com/error/404/template.tar.gz\",\n          \"--no-git-init\",\n          \"--no-install\",\n        ],\n      });\n\n      expect(stderr.trim()).toMatchInlineSnapshot(\n        `\"▲  Oh no! There was a problem fetching the file. The request responded with a 404 status. Please try again later.\"`,\n      );\n      expect(status).toBe(1);\n    });\n\n    it(\"identifies when a remote tarball does not exist (4xx)\", async () => {\n      let projectDir = getProjectDir(\"remote-tarball-4xx\");\n\n      let { status, stderr } = await execCreateReactRouter({\n        args: [\n          projectDir,\n          \"--template\",\n          \"https://example.com/error/400/template.tar.gz\",\n          \"--no-git-init\",\n          \"--no-install\",\n        ],\n      });\n\n      expect(stderr.trim()).toMatchInlineSnapshot(\n        `\"▲  Oh no! There was a problem fetching the file. The request responded with a 400 status. Please try again later.\"`,\n      );\n      expect(status).toBe(1);\n    });\n  });\n\n  describe(\"supports proxy usage\", () => {\n    beforeAll(() => {\n      server.close();\n    });\n    afterAll(() => {\n      server.listen({ onUnhandledRequest: \"error\" });\n    });\n    it(\"uses the proxy from env var\", async () => {\n      let projectDir = await getProjectDir(\"template\");\n\n      let { stderr } = await execCreateReactRouter({\n        args: [\n          projectDir,\n          \"--template\",\n          \"remix-run/grunge-stack\",\n          \"--no-install\",\n          \"--no-git-init\",\n          \"--debug\",\n        ],\n        mockNetwork: false,\n        env: { HTTPS_PROXY: \"http://127.0.0.1:33128\" },\n      });\n\n      expect(stderr.trim()).toMatch(\"127.0.0.1:33\");\n    });\n  });\n});\n\nasync function execCreateReactRouter({\n  args = [],\n  interactions = [],\n  interactive = true,\n  env = {},\n  mockNetwork = true,\n  cwd,\n}: {\n  args: string[];\n  interactive?: boolean;\n  interactions?: ShellInteractions;\n  env?: Record<string, string>;\n  mockNetwork?: boolean;\n  cwd?: string;\n}) {\n  let proc = spawn(\n    \"node\",\n    [\n      \"--require\",\n      require.resolve(\"esbuild-register\"),\n      ...(mockNetwork\n        ? [\"--require\", path.join(__dirname, \"./msw-register.ts\")]\n        : []),\n      path.resolve(__dirname, \"../cli.ts\"),\n      ...args,\n    ],\n    {\n      cwd,\n      stdio: [null, null, null],\n      env: {\n        ...process.env,\n        ...env,\n        ...(interactive\n          ? { CREATE_REACT_ROUTER_FORCE_INTERACTIVE: \"true\" }\n          : {}),\n      },\n    },\n  );\n\n  return await interactWithShell(proc, interactions);\n}\n\ninterface ShellResult {\n  status: number | \"timeout\" | null;\n  stdout: string;\n  stderr: string;\n}\n\ntype ShellInteractions = Array<\n  | { question: RegExp; type: Array<String>; answer?: never }\n  | { question: RegExp; answer: RegExp; type?: never }\n>;\n\nasync function interactWithShell(\n  proc: ChildProcessWithoutNullStreams,\n  interactions: ShellInteractions,\n): Promise<ShellResult> {\n  proc.stdin.setDefaultEncoding(\"utf-8\");\n\n  let deferred = defer<ShellResult>();\n\n  let stepNumber = 0;\n\n  let stdout = \"\";\n  let stderr = \"\";\n  proc.stdout.on(\"data\", (chunk: unknown) => {\n    if (chunk instanceof Buffer) {\n      chunk = String(chunk);\n    }\n    if (typeof chunk !== \"string\") {\n      console.error({ stdoutChunk: chunk });\n      throw new Error(\"stdout chunk is not a string\");\n    }\n    stdout += stripAnsi(maskTempDir(chunk));\n    let step = interactions[stepNumber];\n    if (!step) return;\n    let { question, answer, type } = step;\n    if (question.test(chunk)) {\n      if (answer) {\n        let currentSelection = chunk\n          .split(\"\\n\")\n          .slice(1)\n          .find(\n            (line) =>\n              line.includes(\"❯\") || line.includes(\">\") || line.includes(\"●\"),\n          );\n\n        if (currentSelection && answer.test(currentSelection)) {\n          proc.stdin.write(ENTER);\n          stepNumber += 1;\n        } else {\n          proc.stdin.write(DOWN);\n        }\n      } else if (type) {\n        for (let command of type) {\n          proc.stdin.write(command);\n        }\n        stepNumber += 1;\n      }\n    }\n\n    if (stepNumber === interactions.length) {\n      proc.stdin.end();\n    }\n  });\n\n  proc.stderr.on(\"data\", (chunk: unknown) => {\n    if (chunk instanceof Buffer) {\n      chunk = String(chunk);\n    }\n    if (typeof chunk !== \"string\") {\n      console.error({ stderrChunk: chunk });\n      throw new Error(\"stderr chunk is not a string\");\n    }\n    stderr += stripAnsi(maskTempDir(chunk));\n  });\n\n  proc.on(\"close\", (status) => {\n    deferred.resolve({ status, stdout, stderr });\n  });\n\n  // this ensures that if we do timeout we at least get as much useful\n  // output as possible.\n  let timeout = setTimeout(() => {\n    if (deferred.state.current === \"pending\") {\n      proc.kill();\n      deferred.resolve({ status: \"timeout\", stdout, stderr });\n    }\n  }, jestTimeout);\n\n  let result = await deferred.promise;\n  clearTimeout(timeout);\n\n  return result;\n}\n\nfunction defer<Value>() {\n  let resolve: (value: Value) => void, reject: (reason?: any) => void;\n  let state: { current: \"pending\" | \"resolved\" | \"rejected\" } = {\n    current: \"pending\",\n  };\n  let promise = new Promise<Value>((res, rej) => {\n    resolve = (value: Value) => {\n      state.current = \"resolved\";\n      return res(value);\n    };\n    reject = (reason?: any) => {\n      state.current = \"rejected\";\n      return rej(reason);\n    };\n  });\n  return { promise, resolve: resolve!, reject: reject!, state };\n}\n"
  },
  {
    "path": "packages/create-react-router/__tests__/fixtures/basic/.gitignore",
    "content": "node_modules\n\n/.cache\n/build\n.env\n.react-router\n"
  },
  {
    "path": "packages/create-react-router/__tests__/fixtures/basic/README.md",
    "content": "# Welcome to React Router!\n\n- 📖 [React Router docs](https://reactrouter.com/dev)\n\n## Development\n\nRun the dev server:\n\n```shellscript\nnpm run dev\n```\n\n## Deployment\n\nFirst, build your app for production:\n\n```sh\nnpm run build\n```\n\nThen run the app in production mode:\n\n```sh\nnpm start\n```\n\nNow you'll need to pick a host to deploy it to.\n\n### DIY\n\nIf you're familiar with deploying Node applications, the built-in app server is production-ready.\n\nMake sure to deploy the output of `npm run build`\n\n- `build/server`\n- `build/client`\n\n## Styling\n\nThis template comes with [Tailwind CSS](https://tailwindcss.com/) already configured for a simple default starting experience. You can use whatever CSS framework you prefer.\n"
  },
  {
    "path": "packages/create-react-router/__tests__/fixtures/basic/app/root.tsx",
    "content": "import { Links, Meta, Outlet, Scripts, ScrollRestoration } from \"react-router\";\n\nexport function Layout({ children }: { children: React.ReactNode }) {\n  return (\n    <html lang=\"en\">\n      <head>\n        <meta charSet=\"utf-8\" />\n        <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />\n        <Meta />\n        <Links />\n      </head>\n      <body>\n        {children}\n        <ScrollRestoration />\n        <Scripts />\n      </body>\n    </html>\n  );\n}\n\nexport default function App() {\n  return <Outlet />;\n}\n"
  },
  {
    "path": "packages/create-react-router/__tests__/fixtures/basic/app/routes/home.tsx",
    "content": "import type { MetaFunction } from \"react-router\";\n\nexport const meta: MetaFunction = () => {\n  return [\n    { title: \"New React Router App\" },\n    { name: \"description\", content: \"Welcome to React Router!\" },\n  ];\n};\n\nexport default function Index() {\n  return <h1>Welcome to React Router</h1>;\n}\n"
  },
  {
    "path": "packages/create-react-router/__tests__/fixtures/basic/app/routes.ts",
    "content": "import type { RouteConfig } from \"@react-router/dev/routes\";\nimport { index } from \"@react-router/dev/routes\";\n\nexport default [index(\"routes/home.tsx\")] satisfies RouteConfig;\n"
  },
  {
    "path": "packages/create-react-router/__tests__/fixtures/basic/package.json",
    "content": "{\n  \"private\": true,\n  \"sideEffects\": false,\n  \"type\": \"module\",\n  \"scripts\": {\n    \"dev\": \"react-router dev\",\n    \"build\": \"react-router build\",\n    \"start\": \"react-router-serve ./build/server/index.js\"\n  },\n  \"dependencies\": {\n    \"react-router\": \"*\",\n    \"react-router-dom\": \"*\",\n    \"@react-router/node\": \"*\",\n    \"not-react-router\": \"*\"\n  },\n  \"devDependencies\": {},\n  \"engines\": {\n    \"node\": \">=20.0.0\"\n  }\n}\n"
  },
  {
    "path": "packages/create-react-router/__tests__/fixtures/basic/tsconfig.json",
    "content": "{\n  \"include\": [\n    \"**/*.ts\",\n    \"**/*.tsx\",\n    \"**/.server/**/*.ts\",\n    \"**/.server/**/*.tsx\",\n    \"**/.client/**/*.ts\",\n    \"**/.client/**/*.tsx\",\n    \".react-router/types/**/*\"\n  ],\n  \"compilerOptions\": {\n    \"lib\": [\"DOM\", \"DOM.Iterable\", \"ES2022\"],\n    \"types\": [\"@react-router/node\", \"vite/client\"],\n    \"verbatimModuleSyntax\": true,\n    \"esModuleInterop\": true,\n    \"jsx\": \"react-jsx\",\n    \"module\": \"ESNext\",\n    \"moduleResolution\": \"Bundler\",\n    \"resolveJsonModule\": true,\n    \"target\": \"ES2022\",\n    \"strict\": true,\n    \"allowJs\": true,\n    \"skipLibCheck\": true,\n    \"baseUrl\": \".\",\n    \"paths\": {\n      \"~/*\": [\"./app/*\"]\n    },\n    \"noEmit\": true,\n    \"rootDirs\": [\".\", \"./.react-router/types\"],\n    \"plugins\": [{ \"name\": \"@react-router/dev\" }]\n  }\n}\n"
  },
  {
    "path": "packages/create-react-router/__tests__/fixtures/basic/vite.config.ts",
    "content": "import { reactRouter } from \"@react-router/dev/vite\";\nimport { defineConfig } from \"vite\";\n\nexport default defineConfig({\n  plugins: [reactRouter()],\n});\n"
  },
  {
    "path": "packages/create-react-router/__tests__/fixtures/blank/package.json",
    "content": "{}\n"
  },
  {
    "path": "packages/create-react-router/__tests__/fixtures/with-ignored-dir/package.json",
    "content": "{}\n"
  },
  {
    "path": "packages/create-react-router/__tests__/github-mocks.ts",
    "content": "import fsp from \"node:fs/promises\";\nimport * as path from \"node:path\";\nimport { http } from \"msw\";\nimport type { setupServer } from \"msw/node\";\nimport invariant from \"tiny-invariant\";\n\ntype RequestHandler = Parameters<typeof setupServer>[0];\n\nasync function isDirectory(d: string) {\n  try {\n    return (await fsp.lstat(d)).isDirectory();\n  } catch {\n    return false;\n  }\n}\nasync function isFile(d: string) {\n  try {\n    return (await fsp.lstat(d)).isFile();\n  } catch {\n    return false;\n  }\n}\n\ntype GHContentsDescription = {\n  name: string;\n  path: string;\n  sha: string;\n  size: number;\n  url: string;\n  html_url: string;\n  git_url: string;\n  download_url: string | null;\n  type: \"dir\" | \"file\";\n  _links: {\n    self: string;\n    git: string;\n    html: string;\n  };\n};\n\ntype GHContent = {\n  sha: string;\n  node_id: string;\n  size: number;\n  url: string;\n  content: string;\n  encoding: \"base64\";\n};\n\nlet sendTarball = async (args: { owner: string; repo: string }) => {\n  let { owner, repo } = args;\n  invariant(typeof owner === \"string\", \"owner must be a string\");\n  invariant(typeof repo === \"string\", \"repo must be a string\");\n\n  let pathToTarball: string;\n  if (owner === \"remix-run\" && repo === \"react-router-templates\") {\n    pathToTarball = path.join(__dirname, \"fixtures\", \"templates-repo.tar.gz\");\n  } else if (owner === \"fake-react-router-tester\" && repo === \"nested-dir\") {\n    pathToTarball = path.join(__dirname, \"fixtures\", \"nested-dir-repo.tar.gz\");\n  } else {\n    pathToTarball = path.join(__dirname, \"fixtures\", \"template.tar.gz\");\n  }\n\n  let fileBuffer = await fsp.readFile(pathToTarball);\n\n  return new Response(fileBuffer, {\n    headers: {\n      \"Content-Type\": \"application/x-gzip\",\n    },\n  });\n};\n\nlet githubHandlers: Array<RequestHandler> = [\n  http.head(\n    `https://github.com/remix-run/react-router/tree/main/:type/:name`,\n    () => {\n      return new Response();\n    },\n  ),\n  http.head(\n    `https://github.com/remix-run/examples/tree/main/:type/:name`,\n    () => {\n      return new Response();\n    },\n  ),\n  http.head<{ status: string }>(\n    `https://github.com/error-username/:status`,\n    ({ params }) => {\n      return new Response(null, { status: Number(params.status) });\n    },\n  ),\n  http.head(`https://github.com/:owner/:repo`, () => {\n    return new Response();\n  }),\n  http.head<{ status: string }>(\n    `https://api.github.com/repos/error-username/:status`,\n    ({ params }) => {\n      return new Response(null, { status: Number(params.status) });\n    },\n  ),\n  http.head(\n    `https://api.github.com/repos/private-org/private-repo`,\n    ({ request }) => {\n      let status =\n        request.headers.get(\"Authorization\") === \"token valid-token\"\n          ? 200\n          : 404;\n      return new Response(null, { status });\n    },\n  ),\n  http.head(`https://api.github.com/repos/:owner/:repo`, () => {\n    return new Response();\n  }),\n  http.head(`https://github.com/:owner/:repo/tree/:branch/:path*`, () => {\n    return new Response();\n  }),\n  http.get<{ owner: string; repo: string }>(\n    `https://api.github.com/repos/:owner/:repo/git/trees/:branch`,\n    async ({ params }) => {\n      let { owner, repo } = params;\n\n      return Response.json({\n        sha: \"7d906ff5bbb79401a4a8ec1e1799845ed502c0a1\",\n        url: `https://api.github.com/repos/${owner}/${repo}/trees/7d906ff5bbb79401a4a8ec1e1799845ed502c0a1`,\n        tree: [\n          {\n            path: \"package.json\",\n            mode: \"040000\",\n            type: \"blob\",\n            sha: \"a405cd8355516db9c96e1467fb14b74c97ac0a65\",\n            size: 138,\n            url: `https://api.github.com/repos/${owner}/${repo}/git/blobs/a405cd8355516db9c96e1467fb14b74c97ac0a65`,\n          },\n          {\n            path: \"template\",\n            mode: \"040000\",\n            type: \"tree\",\n            sha: \"3f350a670e8fefd58535a9e1878539dc19afb4b5\",\n            url: `https://api.github.com/repos/${owner}/${repo}/trees/3f350a670e8fefd58535a9e1878539dc19afb4b5`,\n          },\n        ],\n      });\n    },\n  ),\n  http.get<{ owner: string; repo: string; path: string }>(\n    `https://api.github.com/repos/:owner/:repo/contents/:path`,\n    async ({ request, params }) => {\n      let { owner, repo } = params;\n      if (typeof params.path !== \"string\") {\n        throw new Error(\"params.path must be a string\");\n      }\n      let contentsPath = decodeURIComponent(params.path).trim();\n      let isMockable = owner === \"remix-run\" && repo === \"react-router\";\n\n      if (!isMockable) {\n        let message = `Attempting to get content description for unmockable resource: ${owner}/${repo}/${contentsPath}`;\n        console.error(message);\n        throw new Error(message);\n      }\n\n      let localPath = path.join(__dirname, \"../../..\", contentsPath);\n      let isLocalDir = await isDirectory(localPath);\n      let isLocalFile = await isFile(localPath);\n\n      if (!isLocalDir && !isLocalFile) {\n        return Response.json(\n          {\n            message: \"Not Found\",\n            documentation_url:\n              \"https://docs.github.com/rest/reference/repos#get-repository-content\",\n          },\n          { status: 404 },\n        );\n      }\n\n      if (isLocalFile) {\n        let encoding = \"base64\" as const;\n        let content = await fsp.readFile(localPath, { encoding: \"utf-8\" });\n        return Response.json({\n          content: Buffer.from(content, \"utf-8\").toString(encoding),\n          encoding,\n        });\n      }\n\n      let dirList = await fsp.readdir(localPath);\n      let url = new URL(request.url);\n\n      let contentDescriptions = await Promise.all(\n        dirList.map(async (name): Promise<GHContentsDescription> => {\n          let relativePath = path.join(contentsPath, name);\n          // NOTE: this is a cheat-code so we don't have to determine the sha of the file\n          // and our sha endpoint handler doesn't have to do a reverse-lookup.\n          let sha = relativePath;\n          let fullPath = path.join(localPath, name);\n          let isDir = await isDirectory(fullPath);\n          let size = isDir ? 0 : (await fsp.stat(fullPath)).size;\n          return {\n            name,\n            path: relativePath,\n            sha,\n            size,\n            url: `https://api.github.com/repos/${owner}/${repo}/contents/${contentsPath}?${url.searchParams}`,\n            html_url: `https://github.com/${owner}/${repo}/tree/main/${contentsPath}`,\n            git_url: `https://api.github.com/repos/${owner}/${repo}/git/trees/${sha}`,\n            download_url: null,\n            type: isDir ? \"dir\" : \"file\",\n            _links: {\n              self: `https://api.github.com/repos/${owner}/${repo}/contents/${contentsPath}${url.searchParams}`,\n              git: `https://api.github.com/repos/${owner}/${repo}/git/trees/${sha}`,\n              html: `https://github.com/${owner}/${repo}/tree/main/${contentsPath}`,\n            },\n          };\n        }),\n      );\n\n      return Response.json(contentDescriptions);\n    },\n  ),\n  http.get<{ owner: string; repo: string; sha: string }>(\n    `https://api.github.com/repos/:owner/:repo/git/blobs/:sha`,\n    async ({ params }) => {\n      let { owner, repo } = params;\n      if (typeof params.sha !== \"string\") {\n        throw new Error(\"params.sha must be a string\");\n      }\n      let sha = decodeURIComponent(params.sha).trim();\n      // if the sha includes a \"/\" that means it's not a sha but a relativePath\n      // and therefore the client is getting content it got from the local\n      // mock environment, not the actual github API.\n      if (!sha.includes(\"/\")) {\n        let message = `Attempting to get content for sha, but no sha exists locally: ${sha}`;\n        console.error(message);\n        throw new Error(message);\n      }\n\n      // NOTE: we cheat a bit and in the contents/:path handler, we set the sha to the relativePath\n      let relativePath = sha;\n      let fullPath = path.join(__dirname, \"..\", relativePath);\n      let encoding = \"base64\" as const;\n      let size = (await fsp.stat(fullPath)).size;\n      let content = await fsp.readFile(fullPath, { encoding: \"utf-8\" });\n\n      let resource: GHContent = {\n        sha,\n        node_id: `${sha}_node_id`,\n        size,\n        url: `https://api.github.com/repos/${owner}/${repo}/git/blobs/${sha}`,\n        content: Buffer.from(content, \"utf-8\").toString(encoding),\n        encoding,\n      };\n\n      return Response.json(resource);\n    },\n  ),\n  http.get<{ owner: string; repo: string; path?: string[] }>(\n    `https://api.github.com/repos/:owner/:repo/contents{/*path}`,\n    async ({ params }) => {\n      let { owner, repo } = params;\n\n      let relativePath = params.path;\n      if (typeof relativePath !== \"string\") {\n        throw new Error(\"params.path must be a string\");\n      }\n      let fullPath = path.join(__dirname, \"..\", relativePath);\n      let encoding = \"base64\" as const;\n      let size = (await fsp.stat(fullPath)).size;\n      let content = await fsp.readFile(fullPath, { encoding: \"utf-8\" });\n      let sha = `${relativePath}_sha`;\n\n      let resource: GHContent = {\n        sha,\n        node_id: `${params.path}_node_id`,\n        size,\n        url: `https://api.github.com/repos/${owner}/${repo}/git/blobs/${sha}`,\n        content: Buffer.from(content, \"utf-8\").toString(encoding),\n        encoding,\n      };\n\n      return Response.json(resource);\n    },\n  ),\n  http.get<{ branch: string }>(\n    `https://codeload.github.com/private-org/private-repo/tar.gz/:branch`,\n    ({ request }) => {\n      if (request.headers.get(\"Authorization\") !== \"token valid-token\") {\n        return new Response(null, { status: 404 });\n      }\n      return sendTarball({ owner: \"private-org\", repo: \"private-repo\" });\n    },\n  ),\n  http.get<{ owner: string; repo: string; branch: string }>(\n    `https://codeload.github.com/:owner/:repo/tar.gz/:branch`,\n    ({ params }) => {\n      return sendTarball({ owner: params.owner, repo: params.repo });\n    },\n  ),\n  http.get(\n    `https://api.github.com/repos/private-org/private-repo/tarball`,\n    ({ request }) => {\n      if (request.headers.get(\"Authorization\") !== \"token valid-token\") {\n        return new Response(null, { status: 404 });\n      }\n      return sendTarball({ owner: \"private-org\", repo: \"private-repo\" });\n    },\n  ),\n  http.get<{ tag: string }>(\n    `https://api.github.com/repos/private-org/private-repo/releases/tags/:tag`,\n    ({ request, params }) => {\n      if (request.headers.get(\"Authorization\") !== \"token valid-token\") {\n        return new Response(null, { status: 404 });\n      }\n      let { tag } = params;\n      return Response.json({\n        assets: [\n          {\n            browser_download_url: `https://github.com/private-org/private-repo/releases/download/${tag}/template.tar.gz`,\n            id: \"working-asset-id\",\n          },\n        ],\n      });\n    },\n  ),\n  http.get(\n    `https://api.github.com/repos/private-org/private-repo/releases/assets/working-asset-id`,\n    ({ request }) => {\n      if (request.headers.get(\"Authorization\") !== \"token valid-token\") {\n        return new Response(null, { status: 404 });\n      }\n      return sendTarball({ owner: \"private-org\", repo: \"private-repo\" });\n    },\n  ),\n  http.get<{ status: string }>(\n    `https://api.github.com/repos/error-username/:status/tarball`,\n    ({ params }) => {\n      return new Response(null, { status: Number(params.status) });\n    },\n  ),\n  http.get<{ owner: string; repo: string }>(\n    `https://api.github.com/repos/:owner/:repo/tarball`,\n    ({ params }) => {\n      return sendTarball({ owner: params.owner, repo: params.repo });\n    },\n  ),\n  http.get(`https://api.github.com/repos/:repo*`, () => {\n    return Response.json({ default_branch: \"main\" });\n  }),\n];\n\nexport { githubHandlers };\n"
  },
  {
    "path": "packages/create-react-router/__tests__/msw-register.ts",
    "content": "import process from \"node:process\";\n\nimport { server } from \"./msw\";\n\nserver.listen({ onUnhandledRequest: \"error\" });\n\nprocess.on(\"exit\", () => {\n  server.close();\n});\n"
  },
  {
    "path": "packages/create-react-router/__tests__/msw.ts",
    "content": "import path from \"node:path\";\nimport fsp from \"node:fs/promises\";\nimport { setupServer } from \"msw/node\";\nimport { http, type RequestHandler } from \"msw\";\n\nimport { githubHandlers } from \"./github-mocks\";\n\nlet miscHandlers: Array<RequestHandler> = [\n  http.get(\"https://registry.npmjs.org/react-router/latest\", () => {\n    return Response.json({ version: \"123.0.0\" });\n  }),\n  http.head<{ status: string }>(\n    \"https://example.com/error/:status/template.tar.gz\",\n    ({ params }) => {\n      return new Response(null, { status: Number(params.status) });\n    },\n  ),\n  http.get<{ status: string }>(\n    \"https://example.com/error/:status/template.tar.gz\",\n    ({ params }) => {\n      return new Response(null, { status: Number(params.status) });\n    },\n  ),\n  http.head(\"https://example.com/template.tar.gz\", () => {\n    return new Response();\n  }),\n  http.get(\"https://example.com/template.tar.gz\", async () => {\n    let fileBuffer = await fsp.readFile(\n      path.join(__dirname, \"fixtures\", \"template.tar.gz\"),\n    );\n\n    return new Response(fileBuffer, {\n      headers: {\n        \"Content-Type\": \"application/x-gzip\",\n      },\n    });\n  }),\n];\n\nlet server = setupServer(...githubHandlers, ...miscHandlers);\nexport { server };\n"
  },
  {
    "path": "packages/create-react-router/__tests__/setupAfterEnv.ts",
    "content": "export let jestTimeout = process.platform === \"win32\" ? 20_000 : 10_000;\n\njest.setTimeout(jestTimeout);\n"
  },
  {
    "path": "packages/create-react-router/cli.ts",
    "content": "#!/usr/bin/env node\nimport process from \"node:process\";\n\nimport { createReactRouter } from \"./index\";\n\nprocess.on(\"SIGINT\", () => process.exit(0));\nprocess.on(\"SIGTERM\", () => process.exit(0));\n\nlet argv = process.argv.slice(2).filter((arg) => arg !== \"--\");\n\ncreateReactRouter(argv).then(\n  () => process.exit(0),\n  () => process.exit(1),\n);\n"
  },
  {
    "path": "packages/create-react-router/copy-template.ts",
    "content": "import process from \"node:process\";\nimport url from \"node:url\";\nimport fs from \"node:fs\";\nimport path from \"node:path\";\nimport stream from \"node:stream\";\nimport { promisify } from \"node:util\";\nimport { fetch } from \"@remix-run/web-fetch\";\nimport gunzip from \"gunzip-maybe\";\nimport tar from \"tar-fs\";\nimport { ProxyAgent } from \"proxy-agent\";\n\nimport { color, isUrl } from \"./utils\";\n\nconst defaultAgent = new ProxyAgent();\nconst httpsAgent = new ProxyAgent();\nhttpsAgent.protocol = \"https:\";\nfunction agent(url: string) {\n  return new URL(url).protocol === \"https:\" ? httpsAgent : defaultAgent;\n}\n\nexport async function copyTemplate(\n  template: string,\n  destPath: string,\n  options: CopyTemplateOptions,\n): Promise<{ localTemplateDirectory: string } | undefined> {\n  let { log = () => {} } = options;\n\n  /**\n   * Valid templates are:\n   * - local file or directory on disk\n   * - GitHub owner/repo shorthand\n   * - GitHub owner/repo/directory shorthand\n   * - full GitHub repo URL\n   * - any tarball URL\n   */\n\n  try {\n    if (isLocalFilePath(template)) {\n      log(`Using the template from local file at \"${template}\"`);\n      let filepath = template.startsWith(\"file://\")\n        ? url.fileURLToPath(template)\n        : template;\n      let isLocalDir = await copyTemplateFromLocalFilePath(filepath, destPath);\n      return isLocalDir ? { localTemplateDirectory: filepath } : undefined;\n    }\n\n    if (isGithubRepoShorthand(template)) {\n      log(`Using the template from the \"${template}\" repo`);\n      await copyTemplateFromGithubRepoShorthand(template, destPath, options);\n      return;\n    }\n\n    if (isValidGithubRepoUrl(template)) {\n      log(`Using the template from \"${template}\"`);\n      await copyTemplateFromGithubRepoUrl(template, destPath, options);\n      return;\n    }\n\n    if (isUrl(template)) {\n      log(`Using the template from \"${template}\"`);\n      await copyTemplateFromGenericUrl(template, destPath, options);\n      return;\n    }\n\n    throw new CopyTemplateError(\n      `\"${color.bold(template)}\" is an invalid template. Run ${color.bold(\n        \"create-react-router --help\",\n      )} to see supported template formats.`,\n    );\n  } catch (error) {\n    await options.onError(error);\n  }\n}\n\ninterface CopyTemplateOptions {\n  debug?: boolean;\n  token?: string;\n  onError(error: unknown): any;\n  log?(message: string): any;\n}\n\nfunction isLocalFilePath(input: string): boolean {\n  try {\n    return (\n      input.startsWith(\"file://\") ||\n      fs.existsSync(\n        path.isAbsolute(input) ? input : path.resolve(process.cwd(), input),\n      )\n    );\n  } catch (_) {\n    return false;\n  }\n}\n\nasync function copyTemplateFromRemoteTarball(\n  url: string,\n  destPath: string,\n  options: CopyTemplateOptions,\n) {\n  return await downloadAndExtractTarball(destPath, url, options);\n}\n\nasync function copyTemplateFromGithubRepoShorthand(\n  repoShorthand: string,\n  destPath: string,\n  options: CopyTemplateOptions,\n) {\n  let [owner, name, ...path] = repoShorthand.split(\"/\");\n  let filePath = path.length ? path.join(\"/\") : null;\n\n  await downloadAndExtractRepoTarball(\n    { owner, name, filePath },\n    destPath,\n    options,\n  );\n}\n\nasync function copyTemplateFromGithubRepoUrl(\n  repoUrl: string,\n  destPath: string,\n  options: CopyTemplateOptions,\n) {\n  await downloadAndExtractRepoTarball(getRepoInfo(repoUrl), destPath, options);\n}\n\nasync function copyTemplateFromGenericUrl(\n  url: string,\n  destPath: string,\n  options: CopyTemplateOptions,\n) {\n  await copyTemplateFromRemoteTarball(url, destPath, options);\n}\n\nasync function copyTemplateFromLocalFilePath(\n  filePath: string,\n  destPath: string,\n): Promise<boolean> {\n  if (filePath.endsWith(\".tar.gz\") || filePath.endsWith(\".tgz\")) {\n    await extractLocalTarball(filePath, destPath);\n    return false;\n  }\n  if (fs.statSync(filePath).isDirectory()) {\n    // If our template is just a directory on disk, return true here, and we'll\n    // just copy directly from there instead of \"extracting\" to a temp\n    // directory first\n    return true;\n  }\n  throw new CopyTemplateError(\n    \"The provided template is not a valid local directory or tarball.\",\n  );\n}\n\nconst pipeline = promisify(stream.pipeline);\n\nasync function extractLocalTarball(\n  tarballPath: string,\n  destPath: string,\n): Promise<void> {\n  try {\n    await pipeline(\n      fs.createReadStream(tarballPath),\n      gunzip(),\n      tar.extract(destPath, { strip: 1 }),\n    );\n  } catch (error: unknown) {\n    throw new CopyTemplateError(\n      \"There was a problem extracting the file from the provided template.\" +\n        `  Template filepath: \\`${tarballPath}\\`` +\n        `  Destination directory: \\`${destPath}\\`` +\n        `  ${error}`,\n    );\n  }\n}\n\ninterface TarballDownloadOptions {\n  debug?: boolean;\n  filePath?: string | null;\n  token?: string;\n}\n\nasync function downloadAndExtractRepoTarball(\n  repo: RepoInfo,\n  destPath: string,\n  options: TarballDownloadOptions,\n) {\n  // If we have a direct file path we will also have the branch. We can skip the\n  // redirect and get the tarball URL directly.\n  if (repo.branch && repo.filePath) {\n    let tarballURL = `https://codeload.github.com/${repo.owner}/${repo.name}/tar.gz/${repo.branch}`;\n    return await downloadAndExtractTarball(destPath, tarballURL, {\n      ...options,\n      filePath: repo.filePath,\n    });\n  }\n\n  // If we don't know the branch, the GitHub API will figure out the default and\n  // redirect the request to the tarball.\n  // https://docs.github.com/en/rest/reference/repos#download-a-repository-archive-tar\n  let url = `https://api.github.com/repos/${repo.owner}/${repo.name}/tarball`;\n  if (repo.branch) {\n    url += `/${repo.branch}`;\n  }\n\n  return await downloadAndExtractTarball(destPath, url, {\n    ...options,\n    filePath: repo.filePath ?? null,\n  });\n}\n\ninterface DownloadAndExtractTarballOptions {\n  token?: string;\n  filePath?: string | null;\n}\n\nasync function downloadAndExtractTarball(\n  downloadPath: string,\n  tarballUrl: string,\n  { token, filePath }: DownloadAndExtractTarballOptions,\n): Promise<void> {\n  let resourceUrl = tarballUrl;\n  let headers: HeadersInit = {};\n  let isGithubUrl = new URL(tarballUrl).host.endsWith(\"github.com\");\n  if (token && isGithubUrl) {\n    headers.Authorization = `token ${token}`;\n  }\n  if (isGithubReleaseAssetUrl(tarballUrl)) {\n    // We can download the asset via the GitHub api, but first we need to look\n    // up the asset id\n    let info = getGithubReleaseAssetInfo(tarballUrl);\n    headers.Accept = \"application/vnd.github.v3+json\";\n\n    let releaseUrl =\n      info.tag === \"latest\"\n        ? `https://api.github.com/repos/${info.owner}/${info.name}/releases/latest`\n        : `https://api.github.com/repos/${info.owner}/${info.name}/releases/tags/${info.tag}`;\n\n    let response = await fetch(releaseUrl, {\n      agent: agent(\"https://api.github.com\"),\n      headers,\n    });\n\n    if (response.status !== 200) {\n      throw new CopyTemplateError(\n        \"There was a problem fetching the file from GitHub. The request \" +\n          `responded with a ${response.status} status. Please try again later.`,\n      );\n    }\n\n    let body = (await response.json()) as { assets: GitHubApiReleaseAsset[] };\n    if (\n      !body ||\n      typeof body !== \"object\" ||\n      !body.assets ||\n      !Array.isArray(body.assets)\n    ) {\n      throw new CopyTemplateError(\n        \"There was a problem fetching the file from GitHub. No asset was \" +\n          \"found at that url. Please try again later.\",\n      );\n    }\n\n    let assetId = body.assets.find((asset) => {\n      // If the release is \"latest\", the url won't match the download url\n      return info.tag === \"latest\"\n        ? asset?.browser_download_url?.includes(info.asset)\n        : asset?.browser_download_url === tarballUrl;\n    })?.id;\n    if (assetId == null) {\n      throw new CopyTemplateError(\n        \"There was a problem fetching the file from GitHub. No asset was \" +\n          \"found at that url. Please try again later.\",\n      );\n    }\n    resourceUrl = `https://api.github.com/repos/${info.owner}/${info.name}/releases/assets/${assetId}`;\n    headers.Accept = \"application/octet-stream\";\n  }\n  let response = await fetch(resourceUrl, {\n    agent: agent(resourceUrl),\n    headers,\n  });\n\n  if (!response.body || response.status !== 200) {\n    if (token) {\n      throw new CopyTemplateError(\n        `There was a problem fetching the file${\n          isGithubUrl ? \" from GitHub\" : \"\"\n        }. The request ` +\n          `responded with a ${response.status} status. Perhaps your \\`--token\\`` +\n          \"is expired or invalid.\",\n      );\n    }\n    throw new CopyTemplateError(\n      `There was a problem fetching the file${\n        isGithubUrl ? \" from GitHub\" : \"\"\n      }. The request ` +\n        `responded with a ${response.status} status. Please try again later.`,\n    );\n  }\n\n  // file paths returned from GitHub are always unix style\n  if (filePath) {\n    filePath = filePath.split(path.sep).join(path.posix.sep);\n  }\n\n  let filePathHasFiles = false;\n\n  try {\n    let input = new stream.PassThrough();\n    // Start reading stream into passthrough, don't await to avoid buffering\n    writeReadableStreamToWritable(response.body, input);\n    await pipeline(\n      input,\n      gunzip(),\n      tar.extract(downloadPath, {\n        map(header) {\n          let originalDirName = header.name.split(\"/\")[0];\n          header.name = header.name.replace(`${originalDirName}/`, \"\");\n\n          if (filePath) {\n            // Include trailing slash on startsWith when filePath doesn't include\n            // it so something like `templates/basic` doesn't inadvertently\n            // include `templates/basic-javascript/*` files\n            if (\n              (filePath.endsWith(path.posix.sep) &&\n                header.name.startsWith(filePath)) ||\n              (!filePath.endsWith(path.posix.sep) &&\n                header.name.startsWith(filePath + path.posix.sep))\n            ) {\n              filePathHasFiles = true;\n              header.name = header.name.replace(filePath, \"\");\n            } else {\n              header.name = \"__IGNORE__\";\n            }\n          }\n\n          return header;\n        },\n        ignore(_filename, header) {\n          if (!header) {\n            throw Error(\"Header is undefined\");\n          }\n          return header.name === \"__IGNORE__\";\n        },\n      }),\n    );\n  } catch (_) {\n    throw new CopyTemplateError(\n      \"There was a problem extracting the file from the provided template.\" +\n        `  Template URL: \\`${tarballUrl}\\`` +\n        `  Destination directory: \\`${downloadPath}\\``,\n    );\n  }\n\n  if (filePath && !filePathHasFiles) {\n    throw new CopyTemplateError(\n      `The path \"${filePath}\" was not found in this ${\n        isGithubUrl ? \"GitHub repo.\" : \"tarball.\"\n      }`,\n    );\n  }\n}\n\n// Copied from react-router-node/stream.ts\nasync function writeReadableStreamToWritable(\n  stream: ReadableStream,\n  writable: stream.Writable,\n) {\n  let reader = stream.getReader();\n  let flushable = writable as { flush?: Function };\n\n  try {\n    while (true) {\n      let { done, value } = await reader.read();\n\n      if (done) {\n        writable.end();\n        break;\n      }\n\n      writable.write(value);\n      if (typeof flushable.flush === \"function\") {\n        flushable.flush();\n      }\n    }\n  } catch (error: unknown) {\n    writable.destroy(error as Error);\n    throw error;\n  }\n}\n\nfunction isValidGithubRepoUrl(\n  input: string | URL,\n): input is URL | GithubUrlString {\n  if (!isUrl(input)) {\n    return false;\n  }\n  try {\n    let url = new URL(input);\n    let pathSegments = url.pathname.slice(1).split(\"/\");\n\n    return (\n      url.protocol === \"https:\" &&\n      url.hostname === \"github.com\" &&\n      // The pathname must have at least 2 segments. If it has more than 2, the\n      // third must be \"tree\" and it must have at least 4 segments.\n      // https://github.com/:owner/:repo\n      // https://github.com/:owner/:repo/tree/:ref\n      pathSegments.length >= 2 &&\n      (pathSegments.length > 2\n        ? pathSegments[2] === \"tree\" && pathSegments.length >= 4\n        : true)\n    );\n  } catch (_) {\n    return false;\n  }\n}\n\nfunction isGithubRepoShorthand(value: string) {\n  if (isUrl(value)) {\n    return false;\n  }\n  // This supports :owner/:repo and :owner/:repo/nested/path, e.g.\n  // remix-run/react-router\n  // remix-run/react-router/examples/basic\n  return /^[\\w-]+\\/[\\w-.]+(\\/[\\w-.]+)*$/.test(value);\n}\n\nfunction isGithubReleaseAssetUrl(url: string) {\n  /**\n   * Accounts for the following formats:\n   * https://github.com/owner/repository/releases/download/v0.0.1/template.tar.gz\n   * ~or~\n   * https://github.com/owner/repository/releases/latest/download/template.tar.gz\n   */\n  return (\n    url.startsWith(\"https://github.com\") &&\n    (url.includes(\"/releases/download/\") ||\n      url.includes(\"/releases/latest/download/\"))\n  );\n}\n\nfunction getGithubReleaseAssetInfo(browserUrl: string): ReleaseAssetInfo {\n  /**\n   * https://github.com/owner/repository/releases/download/v0.0.1/template.tar.gz\n   * ~or~\n   * https://github.com/owner/repository/releases/latest/download/template.tar.gz\n   */\n\n  let url = new URL(browserUrl);\n  let [, owner, name, , downloadOrLatest, tag, asset] = url.pathname.split(\"/\");\n\n  if (downloadOrLatest === \"latest\" && tag === \"download\") {\n    // handle the GitHub URL quirk for latest releases\n    tag = \"latest\";\n  }\n\n  return {\n    browserUrl,\n    owner,\n    name,\n    asset,\n    tag,\n  };\n}\n\nfunction getRepoInfo(validatedGithubUrl: string): RepoInfo {\n  let url = new URL(validatedGithubUrl);\n  let [, owner, name, tree, branch, ...file] = url.pathname.split(\"/\") as [\n    _: string,\n    Owner: string,\n    Name: string,\n    Tree: string | undefined,\n    Branch: string | undefined,\n    FileInfo: string | undefined,\n  ];\n  let filePath = file.join(\"/\");\n\n  if (tree === undefined) {\n    return {\n      owner,\n      name,\n      branch: null,\n      filePath: null,\n    };\n  }\n\n  return {\n    owner,\n    name,\n    // If we've validated the GitHub URL and there is a tree, there will also be\n    // a branch\n    branch: branch!,\n    filePath: filePath === \"\" || filePath === \"/\" ? null : filePath,\n  };\n}\n\nexport class CopyTemplateError extends Error {\n  constructor(message: string) {\n    super(message);\n    this.name = \"CopyTemplateError\";\n  }\n}\n\ninterface RepoInfo {\n  owner: string;\n  name: string;\n  branch?: string | null;\n  filePath?: string | null;\n}\n\n// https://docs.github.com/en/rest/releases/assets?apiVersion=2022-11-28#get-a-release-asset\ninterface GitHubApiReleaseAsset {\n  url: string;\n  browser_download_url: string;\n  id: number;\n  node_id: string;\n  name: string;\n  label: string;\n  state: \"uploaded\" | \"open\";\n  content_type: string;\n  size: number;\n  download_count: number;\n  created_at: string;\n  updated_at: string;\n  uploader: null | GitHubApiUploader;\n}\n\ninterface GitHubApiUploader {\n  name: string | null;\n  email: string | null;\n  login: string;\n  id: number;\n  node_id: string;\n  avatar_url: string;\n  gravatar_id: string | null;\n  url: string;\n  html_url: string;\n  followers_url: string;\n  following_url: string;\n  gists_url: string;\n  starred_url: string;\n  subscriptions_url: string;\n  organizations_url: string;\n  repos_url: string;\n  events_url: string;\n  received_events_url: string;\n  type: string;\n  site_admin: boolean;\n  starred_at: string;\n}\n\ninterface ReleaseAssetInfo {\n  browserUrl: string;\n  owner: string;\n  name: string;\n  asset: string;\n  tag: string;\n}\n\ntype GithubUrlString =\n  | `https://github.com/${string}/${string}`\n  | `https://www.github.com/${string}/${string}`;\n"
  },
  {
    "path": "packages/create-react-router/index.ts",
    "content": "import process from \"node:process\";\nimport { existsSync } from \"node:fs\";\nimport { cp, readFile, realpath, writeFile } from \"node:fs/promises\";\nimport os from \"node:os\";\nimport path from \"node:path\";\nimport stripAnsi from \"strip-ansi\";\nimport execa from \"execa\";\nimport arg from \"arg\";\nimport * as semver from \"semver\";\nimport sortPackageJSON from \"sort-package-json\";\n\nimport { version as thisReactRouterVersion } from \"./package.json\";\nimport { prompt } from \"./prompt\";\nimport {\n  IGNORED_TEMPLATE_DIRECTORIES,\n  color,\n  debug,\n  ensureDirectory,\n  error,\n  getDirectoryFilesRecursive,\n  info,\n  isInteractive,\n  isValidJsonObject,\n  log,\n  sleep,\n  strip,\n  stripDirectoryFromPath,\n  toValidProjectName,\n} from \"./utils\";\nimport { renderLoadingIndicator } from \"./loading-indicator\";\nimport { copyTemplate, CopyTemplateError } from \"./copy-template\";\n\nasync function createReactRouter(argv: string[]) {\n  let ctx = await getContext(argv);\n  if (ctx.help) {\n    printHelp(ctx);\n    return;\n  }\n  if (ctx.versionRequested) {\n    log(thisReactRouterVersion);\n    return;\n  }\n\n  let steps = [\n    introStep,\n    projectNameStep,\n    copyTemplateToTempDirStep,\n    copyTempDirToAppDirStep,\n    gitInitQuestionStep,\n    installDependenciesQuestionStep,\n    installDependenciesStep,\n    gitInitStep,\n    doneStep,\n  ];\n\n  try {\n    for (let step of steps) {\n      await step(ctx);\n    }\n  } catch (err) {\n    if (ctx.debug) {\n      console.error(err);\n    }\n    throw err;\n  }\n}\n\nasync function getContext(argv: string[]): Promise<Context> {\n  let flags = arg(\n    {\n      \"--debug\": Boolean,\n      \"--react-router-version\": String,\n      \"-v\": \"--react-router-version\",\n      \"--template\": String,\n      \"--token\": String,\n      \"--yes\": Boolean,\n      \"-y\": \"--yes\",\n      \"--install\": Boolean,\n      \"--no-install\": Boolean,\n      \"--package-manager\": String,\n      \"--show-install-output\": Boolean,\n      \"--git-init\": Boolean,\n      \"--no-git-init\": Boolean,\n      \"--help\": Boolean,\n      \"-h\": \"--help\",\n      \"--version\": Boolean,\n      \"--V\": \"--version\",\n      \"--no-color\": Boolean,\n      \"--no-motion\": Boolean,\n      \"--overwrite\": Boolean,\n    },\n    { argv, permissive: true },\n  );\n\n  let {\n    \"--debug\": debug = false,\n    \"--help\": help = false,\n    \"--react-router-version\": selectedReactRouterVersion,\n    \"--template\": template,\n    \"--token\": token,\n    \"--install\": install,\n    \"--no-install\": noInstall,\n    \"--package-manager\": pkgManager,\n    \"--show-install-output\": showInstallOutput = false,\n    \"--git-init\": git,\n    \"--no-git-init\": noGit,\n    \"--no-motion\": noMotion,\n    \"--yes\": yes,\n    \"--version\": versionRequested,\n    \"--overwrite\": overwrite,\n  } = flags;\n\n  let cwd = flags[\"_\"][0] as string;\n  let interactive = isInteractive();\n  let projectName = cwd;\n\n  if (!interactive) {\n    yes = true;\n  }\n\n  if (selectedReactRouterVersion) {\n    if (semver.valid(selectedReactRouterVersion)) {\n      // do nothing, we're good\n    } else if (semver.coerce(selectedReactRouterVersion)) {\n      selectedReactRouterVersion = semver.coerce(\n        selectedReactRouterVersion,\n      )!.version;\n    } else {\n      log(\n        `\\n${color.warning(\n          `${selectedReactRouterVersion} is an invalid version specifier. Using React Router v${thisReactRouterVersion}.`,\n        )}`,\n      );\n      selectedReactRouterVersion = undefined;\n    }\n  }\n\n  let context: Context = {\n    tempDir: path.join(\n      await realpath(os.tmpdir()),\n      `create-react-router--${Math.random().toString(36).substr(2, 8)}`,\n    ),\n    cwd,\n    overwrite,\n    interactive,\n    debug,\n    git: git ?? (noGit ? false : yes),\n    help,\n    install: install ?? (noInstall ? false : yes),\n    showInstallOutput,\n    noMotion,\n    pkgManager: validatePackageManager(\n      pkgManager ??\n        // npm, pnpm, Yarn, Bun and Deno (v2.0.5+) set the user agent environment variable that can be used\n        // to determine which package manager ran the command.\n        (process.env.npm_config_user_agent ?? \"npm\").split(\"/\")[0],\n    ),\n    projectName,\n    prompt,\n    reactRouterVersion: selectedReactRouterVersion || thisReactRouterVersion,\n    template,\n    token,\n    versionRequested,\n  };\n\n  return context;\n}\n\ninterface Context {\n  tempDir: string;\n  cwd: string;\n  interactive: boolean;\n  debug: boolean;\n  git?: boolean;\n  help: boolean;\n  install?: boolean;\n  showInstallOutput: boolean;\n  noMotion?: boolean;\n  pkgManager: PackageManager;\n  projectName?: string;\n  prompt: typeof prompt;\n  reactRouterVersion: string;\n  stdin?: typeof process.stdin;\n  stdout?: typeof process.stdout;\n  template?: string;\n  token?: string;\n  versionRequested?: boolean;\n  overwrite?: boolean;\n}\n\nasync function introStep(ctx: Context) {\n  log(\n    `\\n${\" \".repeat(9)}${color.green(\n      color.bold(\"create-react-router\"),\n    )} ${color.bold(`v${ctx.reactRouterVersion}`)}`,\n  );\n\n  if (!ctx.interactive) {\n    log(\"\");\n    info(\"Shell is not interactive.\", [\n      `Using default options. This is equivalent to running with the `,\n      color.reset(\"--yes\"),\n      ` flag.`,\n    ]);\n  }\n}\n\nasync function projectNameStep(ctx: Context) {\n  // valid cwd is required if shell isn't interactive\n  if (!ctx.interactive && !ctx.cwd) {\n    error(\"Oh no!\", \"No project directory provided\");\n    throw new Error(\"No project directory provided\");\n  }\n\n  if (ctx.cwd) {\n    await sleep(100);\n    info(\"Directory:\", [\n      \"Using \",\n      color.reset(ctx.cwd),\n      \" as project directory\",\n    ]);\n  }\n\n  if (!ctx.cwd) {\n    let { name } = await ctx.prompt({\n      name: \"name\",\n      type: \"text\",\n      label: title(\"dir\"),\n      message: \"Where should we create your new project?\",\n      initial: \"./my-react-router-app\",\n    });\n    ctx.cwd = name!;\n    ctx.projectName = toValidProjectName(name!);\n    return;\n  }\n\n  let name = ctx.cwd;\n  if (name === \".\" || name === \"./\") {\n    let parts = process.cwd().split(path.sep);\n    name = parts[parts.length - 1];\n  } else if (name.startsWith(\"./\") || name.startsWith(\"../\")) {\n    let parts = name.split(\"/\");\n    name = parts[parts.length - 1];\n  }\n  ctx.projectName = toValidProjectName(name);\n}\n\nasync function copyTemplateToTempDirStep(ctx: Context) {\n  if (ctx.template) {\n    log(\"\");\n    info(\"Template:\", [\"Using \", color.reset(ctx.template), \"...\"]);\n  } else {\n    log(\"\");\n    info(\"Using default template\", [\n      \"See https://github.com/remix-run/react-router-templates for more\",\n    ]);\n  }\n\n  let template =\n    ctx.template ??\n    \"https://github.com/remix-run/react-router-templates/tree/main/default\";\n\n  await loadingIndicator({\n    start: \"Template copying...\",\n    end: \"Template copied\",\n    while: async () => {\n      await ensureDirectory(ctx.tempDir);\n      if (ctx.debug) {\n        debug(`Extracting to: ${ctx.tempDir}`);\n      }\n\n      let result = await copyTemplate(template, ctx.tempDir, {\n        debug: ctx.debug,\n        token: ctx.token,\n        async onError(err) {\n          error(\n            \"Oh no!\",\n            err instanceof CopyTemplateError\n              ? err.message\n              : \"Something went wrong. Run `create-react-router --debug` to see more info.\\n\\n\" +\n                  \"Open an issue to report the problem at \" +\n                  \"https://github.com/remix-run/react-router/issues/new\",\n          );\n          throw err;\n        },\n        async log(message) {\n          if (ctx.debug) {\n            debug(message);\n            await sleep(500);\n          }\n        },\n      });\n\n      if (result?.localTemplateDirectory) {\n        ctx.tempDir = path.resolve(result.localTemplateDirectory);\n      }\n    },\n    ctx,\n  });\n}\n\nasync function copyTempDirToAppDirStep(ctx: Context) {\n  await ensureDirectory(ctx.cwd);\n\n  let files1 = await getDirectoryFilesRecursive(ctx.tempDir);\n  let files2 = await getDirectoryFilesRecursive(ctx.cwd);\n  let collisions = files1\n    .filter((f) => files2.includes(f))\n    .sort((a, b) => a.localeCompare(b));\n\n  if (collisions.length > 0) {\n    let getFileList = (prefix: string) => {\n      let moreFiles = collisions.length - 5;\n      let lines = [\"\", ...collisions.slice(0, 5)];\n      if (moreFiles > 0) {\n        lines.push(`and ${moreFiles} more...`);\n      }\n      return lines.join(`\\n${prefix}`);\n    };\n\n    if (ctx.overwrite) {\n      info(\n        \"Overwrite:\",\n        `overwriting files due to \\`--overwrite\\`:${getFileList(\"           \")}`,\n      );\n    } else if (!ctx.interactive) {\n      error(\n        \"Oh no!\",\n        `Destination directory contains files that would be overwritten\\n` +\n          `         and no \\`--overwrite\\` flag was included in a non-interactive\\n` +\n          `         environment. The following files would be overwritten:` +\n          getFileList(\"           \"),\n      );\n      throw new Error(\n        \"File collisions detected in a non-interactive environment\",\n      );\n    } else {\n      if (ctx.debug) {\n        debug(`Colliding files:${getFileList(\"          \")}`);\n      }\n\n      let { overwrite } = await ctx.prompt({\n        name: \"overwrite\",\n        type: \"confirm\",\n        label: title(\"overwrite\"),\n        message:\n          `Your project directory contains files that will be overwritten by\\n` +\n          `             this template (you can force with \\`--overwrite\\`)\\n\\n` +\n          `             Files that would be overwritten:` +\n          `${getFileList(\"               \")}\\n\\n` +\n          `             Do you wish to continue?\\n` +\n          `             `,\n        initial: false,\n      });\n      if (!overwrite) {\n        throw new Error(\"Exiting to avoid overwriting files\");\n      }\n    }\n  }\n\n  await cp(ctx.tempDir, ctx.cwd, {\n    filter(src) {\n      // We never copy .git/ or node_modules/ directories since it's highly\n      // unlikely we want them copied - and because templates are primarily\n      // being pulled from git tarballs which won't have .git/ and shouldn't\n      // have node_modules/\n      let file = stripDirectoryFromPath(ctx.tempDir, src);\n      let isIgnored = IGNORED_TEMPLATE_DIRECTORIES.includes(file);\n      if (isIgnored) {\n        if (ctx.debug) {\n          debug(`Skipping copy of ${file} directory from template`);\n        }\n        return false;\n      }\n      return true;\n    },\n    recursive: true,\n  });\n\n  await updatePackageJSON(ctx);\n}\n\nasync function installDependenciesQuestionStep(ctx: Context) {\n  if (ctx.install === undefined) {\n    let { deps = true } = await ctx.prompt({\n      name: \"deps\",\n      type: \"confirm\",\n      label: title(\"deps\"),\n      message: `Install dependencies with ${ctx.pkgManager}?`,\n      hint: \"recommended\",\n      initial: true,\n    });\n    ctx.install = deps;\n  }\n}\n\nasync function installDependenciesStep(ctx: Context) {\n  let { install, pkgManager, showInstallOutput, cwd } = ctx;\n\n  if (!install) {\n    await sleep(100);\n    info(\"Skipping install step.\", [\n      \"Remember to install dependencies after setup with \",\n      color.reset(`${pkgManager} install`),\n      \".\",\n    ]);\n    return;\n  }\n\n  function runInstall() {\n    return installDependencies({\n      cwd,\n      pkgManager,\n      showInstallOutput,\n    });\n  }\n\n  if (showInstallOutput) {\n    log(\"\");\n    info(`Install`, `Dependencies installing with ${pkgManager}...`);\n    log(\"\");\n    await runInstall();\n    log(\"\");\n    return;\n  }\n\n  log(\"\");\n  await loadingIndicator({\n    start: `Dependencies installing with ${pkgManager}...`,\n    end: \"Dependencies installed\",\n    while: runInstall,\n    ctx,\n  });\n}\n\nasync function gitInitQuestionStep(ctx: Context) {\n  if (existsSync(path.join(ctx.cwd, \".git\"))) {\n    info(\"Nice!\", `Git has already been initialized`);\n    return;\n  }\n\n  let git = ctx.git;\n  if (ctx.git === undefined) {\n    ({ git } = await ctx.prompt({\n      name: \"git\",\n      type: \"confirm\",\n      label: title(\"git\"),\n      message: `Initialize a new git repository?`,\n      hint: \"recommended\",\n      initial: true,\n    }));\n  }\n\n  ctx.git = git ?? false;\n}\n\nasync function gitInitStep(ctx: Context) {\n  if (!ctx.git) {\n    return;\n  }\n\n  if (existsSync(path.join(ctx.cwd, \".git\"))) {\n    log(\"\");\n    info(\"Nice!\", `Git has already been initialized`);\n    return;\n  }\n\n  log(\"\");\n  await loadingIndicator({\n    start: \"Git initializing...\",\n    end: \"Git initialized\",\n    while: async () => {\n      let options = { cwd: ctx.cwd, stdio: \"ignore\" } as const;\n      let commitMsg = \"Initial commit from create-react-router\";\n      try {\n        await execa(\"git\", [\"init\"], options);\n        await execa(\"git\", [\"add\", \".\"], options);\n        await execa(\"git\", [\"commit\", \"-m\", commitMsg], options);\n      } catch (err) {\n        error(\"Oh no!\", \"Failed to initialize git.\");\n        throw err;\n      }\n    },\n    ctx,\n  });\n}\n\nasync function doneStep(ctx: Context) {\n  let projectDir = path.relative(process.cwd(), ctx.cwd);\n\n  let max = process.stdout.columns;\n  let prefix = max < 80 ? \" \" : \" \".repeat(9);\n  await sleep(200);\n\n  log(`\\n ${color.bgWhite(color.black(\" done \"))}  That's it!`);\n  await sleep(100);\n  if (projectDir !== \"\") {\n    let enter = [\n      `\\n${prefix}Enter your project directory using`,\n      color.cyan(`cd .${path.sep}${projectDir}`),\n    ];\n    let len = enter[0].length + stripAnsi(enter[1]).length;\n    log(enter.join(len > max ? \"\\n\" + prefix : \" \"));\n  }\n  log(\n    `${prefix}Check out ${color.bold(\n      \"README.md\",\n    )} for development and deploy instructions.`,\n  );\n  await sleep(100);\n  log(\n    `\\n${prefix}Join the community at ${color.cyan(`https://rmx.as/discord`)}\\n`,\n  );\n  await sleep(200);\n}\n\nconst validPackageManagers = [\"npm\", \"yarn\", \"pnpm\", \"bun\", \"deno\"] as const;\ntype PackageManager = (typeof validPackageManagers)[number];\n\nfunction validatePackageManager(pkgManager: string): PackageManager {\n  return validPackageManagers.find((name) => pkgManager === name) ?? \"npm\";\n}\n\nasync function installDependencies({\n  pkgManager,\n  cwd,\n  showInstallOutput,\n}: {\n  pkgManager: PackageManager;\n  cwd: string;\n  showInstallOutput: boolean;\n}) {\n  try {\n    await execa(pkgManager, [\"install\"], {\n      cwd,\n      stdio: showInstallOutput ? \"inherit\" : \"ignore\",\n    });\n  } catch (err) {\n    error(\"Oh no!\", \"Failed to install dependencies.\");\n    throw err;\n  }\n}\n\nasync function updatePackageJSON(ctx: Context) {\n  let packageJSONPath = path.join(ctx.cwd, \"package.json\");\n  if (!existsSync(packageJSONPath)) {\n    let relativePath = path.relative(process.cwd(), ctx.cwd);\n    error(\n      \"Oh no!\",\n      \"The provided template must be a React Router project with a `package.json` \" +\n        `file, but that file does not exist in ${color.bold(relativePath)}.`,\n    );\n    throw new Error(`package.json does not exist in ${ctx.cwd}`);\n  }\n\n  let contents = await readFile(packageJSONPath, \"utf-8\");\n  let packageJSON: any;\n  try {\n    packageJSON = JSON.parse(contents);\n    if (!isValidJsonObject(packageJSON)) {\n      throw Error();\n    }\n  } catch (err) {\n    error(\n      \"Oh no!\",\n      \"The provided template must be a React Router project with a `package.json` \" +\n        `file, but that file is invalid.`,\n    );\n    throw err;\n  }\n\n  for (let pkgKey of [\"dependencies\", \"devDependencies\"] as const) {\n    let dependencies = packageJSON[pkgKey];\n    if (!dependencies) continue;\n\n    if (!isValidJsonObject(dependencies)) {\n      error(\n        \"Oh no!\",\n        \"The provided template must be a React Router project with a `package.json` \" +\n          `file, but its ${pkgKey} value is invalid.`,\n      );\n      throw new Error(`package.json ${pkgKey} are invalid`);\n    }\n\n    for (let dependency in dependencies) {\n      let version = dependencies[dependency];\n      if (\n        (dependency.startsWith(\"@react-router/\") ||\n          dependency === \"react-router\" ||\n          dependency === \"react-router-dom\") &&\n        version === \"*\"\n      ) {\n        dependencies[dependency] = semver.prerelease(ctx.reactRouterVersion)\n          ? // Templates created from prereleases should pin to a specific version\n            ctx.reactRouterVersion\n          : \"^\" + ctx.reactRouterVersion;\n      }\n    }\n  }\n\n  packageJSON.name = ctx.projectName;\n\n  writeFile(\n    packageJSONPath,\n    JSON.stringify(sortPackageJSON(packageJSON), null, 2),\n    \"utf-8\",\n  );\n}\n\nasync function loadingIndicator(args: {\n  start: string;\n  end: string;\n  while: (...args: any) => Promise<any>;\n  ctx: Context;\n}) {\n  let { ctx, ...rest } = args;\n  await renderLoadingIndicator({\n    ...rest,\n    noMotion: args.ctx.noMotion,\n  });\n}\n\nfunction title(text: string) {\n  return align(color.bgWhite(` ${color.black(text)} `), \"end\", 7) + \" \";\n}\n\nfunction printHelp(ctx: Context) {\n  // prettier-ignore\n  let output = `\n${title(\"create-react-router\")}\n\n${color.heading(\"Usage\")}:\n\n${color.dim(\"$\")} ${color.greenBright(\"create-react-router\")} ${color.arg(\"<projectDir>\")} ${color.arg(\"<...options>\")}\n\n${color.heading(\"Values\")}:\n\n${color.arg(\"projectDir\")}          ${color.dim(`The React Router project directory`)}\n\n${color.heading(\"Options\")}:\n\n${color.arg(\"--help, -h\")}          ${color.dim(`Print this help message and exit`)}\n${color.arg(\"--version, -V\")}       ${color.dim(`Print the CLI version and exit`)}\n${color.arg(\"--no-color\")}          ${color.dim(`Disable ANSI colors in console output`)}\n${color.arg(\"--no-motion\")}         ${color.dim(`Disable animations in console output`)}\n\n${color.arg(\"--template <name>\")}   ${color.dim(`The project template to use`)}\n${color.arg(\"--[no-]install\")}      ${color.dim(`Whether or not to install dependencies after creation`)}\n${color.arg(\"--package-manager\")}   ${color.dim(`The package manager to use`)}\n${color.arg(\"--show-install-output\")}   ${color.dim(`Whether to show the output of the install process`)}\n${color.arg(\"--[no-]git-init\")}     ${color.dim(`Whether or not to initialize a Git repository`)}\n${color.arg(\"--yes, -y\")}           ${color.dim(`Skip all option prompts and run setup`)}\n${color.arg(\"--react-router-version, -v\")}     ${color.dim(`The version of React Router to use`)}\n\n${color.heading(\"Creating a new project\")}:\n\nReact Router projects are created from templates. A template can be:\n\n- a GitHub repo shorthand, :username/:repo or :username/:repo/:directory\n- the URL of a GitHub repo (or directory within it)\n- the URL of a tarball\n- a file path to a directory of files\n- a file path to a tarball\n${[\n  \"remix-run/react-router/templates/basic\",\n  \"remix-run/react-router/examples/basic\",\n  \":username/:repo\",\n  \":username/:repo/:directory\",\n  \"https://github.com/:username/:repo\",\n  \"https://github.com/:username/:repo/tree/:branch\",\n  \"https://github.com/:username/:repo/tree/:branch/:directory\",\n  \"https://github.com/:username/:repo/archive/refs/tags/:tag.tar.gz\",\n  \"https://example.com/template.tar.gz\",\n  \"./path/to/template\",\n  \"./path/to/template.tar.gz\",\n].reduce((str, example) => {\n  return `${str}\\n${color.dim(\"$\")} ${color.greenBright(\"create-react-router\")} my-app ${color.arg(`--template ${example}`)}`;\n}, \"\")}\n\nTo create a new project from a template in a private GitHub repo,\npass the \\`token\\` flag with a personal access token with access\nto that repo.\n`;\n\n  log(output);\n}\n\nfunction align(text: string, dir: \"start\" | \"end\" | \"center\", len: number) {\n  let pad = Math.max(len - strip(text).length, 0);\n  switch (dir) {\n    case \"start\":\n      return text + \" \".repeat(pad);\n    case \"end\":\n      return \" \".repeat(pad) + text;\n    case \"center\":\n      return (\n        \" \".repeat(Math.floor(pad / 2)) + text + \" \".repeat(Math.floor(pad / 2))\n      );\n    default:\n      return text;\n  }\n}\n\nexport { createReactRouter };\nexport type { Context };\n"
  },
  {
    "path": "packages/create-react-router/jest.config.js",
    "content": "/** @type {import('jest').Config} */\nmodule.exports = {\n  ...require(\"../../jest/jest.config.shared\"),\n  displayName: \"create-react-router\",\n  setupFilesAfterEnv: [\"<rootDir>/__tests__/setupAfterEnv.ts\"],\n  setupFiles: [],\n};\n"
  },
  {
    "path": "packages/create-react-router/loading-indicator.ts",
    "content": "// Adapted from https://github.com/withastro/cli-kit\n// MIT License Copyright (c) 2022 Nate Moore\nimport process from \"node:process\";\nimport readline from \"node:readline\";\nimport { erase, cursor } from \"sisteransi\";\n\nimport { reverse, sleep, color } from \"./utils\";\n\nconst GRADIENT_COLORS: Array<`#${string}`> = [\n  \"#ffffff\",\n  \"#dadada\",\n  \"#dadada\",\n  \"#a8deaa\",\n  \"#a8deaa\",\n  \"#a8deaa\",\n  \"#d0f0bd\",\n  \"#d0f0bd\",\n  \"#ffffed\",\n  \"#ffffed\",\n  \"#ffffed\",\n  \"#ffffed\",\n  \"#ffffed\",\n  \"#ffffed\",\n  \"#ffffed\",\n  \"#ffffed\",\n  \"#ffffed\",\n  \"#f7f8ca\",\n  \"#f7f8ca\",\n  \"#eae6ba\",\n  \"#eae6ba\",\n  \"#eae6ba\",\n  \"#dadada\",\n  \"#dadada\",\n  \"#ffffff\",\n];\n\nconst MAX_FRAMES = 8;\n\nconst LEADING_FRAMES = Array.from(\n  { length: MAX_FRAMES * 2 },\n  () => GRADIENT_COLORS[0],\n);\nconst TRAILING_FRAMES = Array.from(\n  { length: MAX_FRAMES * 2 },\n  () => GRADIENT_COLORS[GRADIENT_COLORS.length - 1],\n);\nconst INDICATOR_FULL_FRAMES = [\n  ...LEADING_FRAMES,\n  ...GRADIENT_COLORS,\n  ...TRAILING_FRAMES,\n  ...reverse(GRADIENT_COLORS),\n];\nconst INDICATOR_GRADIENT = reverse(\n  INDICATOR_FULL_FRAMES.map((_, i) => loadingIndicatorFrame(i)),\n);\n\nexport async function renderLoadingIndicator({\n  start,\n  end,\n  while: update = () => sleep(100),\n  noMotion = false,\n  stdin = process.stdin,\n  stdout = process.stdout,\n}: {\n  start: string;\n  end: string;\n  while: (...args: any) => Promise<any>;\n  noMotion?: boolean;\n  stdin?: NodeJS.ReadStream & { fd: 0 };\n  stdout?: NodeJS.WriteStream & { fd: 1 };\n}) {\n  let act = update();\n  let tooSlow = Object.create(null);\n  let result = await Promise.race([sleep(500).then(() => tooSlow), act]);\n  if (result === tooSlow) {\n    let loading = await gradient(color.green(start), {\n      stdin,\n      stdout,\n      noMotion,\n    });\n    await act;\n    loading.stop();\n  }\n  stdout.write(`${\" \".repeat(5)} ${color.green(\"✔\")}  ${color.green(end)}\\n`);\n}\n\nfunction loadingIndicatorFrame(offset = 0) {\n  let frames = INDICATOR_FULL_FRAMES.slice(offset, offset + (MAX_FRAMES - 2));\n  if (frames.length < MAX_FRAMES - 2) {\n    let filled = new Array(MAX_FRAMES - frames.length - 2).fill(\n      GRADIENT_COLORS[0],\n    );\n    frames.push(...filled);\n  }\n  return frames;\n}\n\nfunction getGradientAnimationFrames() {\n  return INDICATOR_GRADIENT.map(\n    (colors) => \" \" + colors.map((g, i) => color.hex(g)(\"█\")).join(\"\"),\n  );\n}\n\nasync function gradient(\n  text: string,\n  { stdin = process.stdin, stdout = process.stdout, noMotion = false } = {},\n) {\n  let { createLogUpdate } = await import(\"log-update\");\n  let logUpdate = createLogUpdate(stdout);\n  let frameIndex = 0;\n  let frames = getGradientAnimationFrames();\n  let interval: NodeJS.Timeout;\n  let rl = readline.createInterface({ input: stdin, escapeCodeTimeout: 50 });\n  readline.emitKeypressEvents(stdin, rl);\n\n  if (stdin.isTTY) stdin.setRawMode(true);\n  function keypress(char: string) {\n    if (char === \"\\x03\") {\n      loadingIndicator.stop();\n      process.exit(0);\n    }\n    if (stdin.isTTY) stdin.setRawMode(true);\n    stdout.write(cursor.hide + erase.lines(1));\n  }\n\n  let done = false;\n  let loadingIndicator = {\n    start() {\n      stdout.write(cursor.hide);\n      stdin.on(\"keypress\", keypress);\n      logUpdate(`${frames[0]}  ${text}`);\n\n      async function loop() {\n        if (done) return;\n        if (frameIndex < frames.length - 1) {\n          frameIndex++;\n        } else {\n          frameIndex = 0;\n        }\n        let frame = frames[frameIndex];\n        logUpdate(\n          `${(noMotion\n            ? getMotionlessFrame(frameIndex)\n            : color.supportsColor\n              ? frame\n              : getColorlessFrame(frameIndex)\n          ).padEnd(MAX_FRAMES - 1, \" \")}  ${text}`,\n        );\n        if (!done) await sleep(20);\n        loop();\n      }\n\n      loop();\n    },\n    stop() {\n      done = true;\n      stdin.removeListener(\"keypress\", keypress);\n      clearInterval(interval);\n      logUpdate.clear();\n      rl.close();\n    },\n  };\n  loadingIndicator.start();\n  return loadingIndicator;\n}\n\nfunction getColorlessFrame(frameIndex: number) {\n  return (\n    frameIndex % 3 === 0 ? \".. .. \" : frameIndex % 3 === 1 ? \" .. ..\" : \". .. .\"\n  ).padEnd(MAX_FRAMES - 1 + 20, \" \");\n}\n\nfunction getMotionlessFrame(frameIndex: number) {\n  return \" \".repeat(MAX_FRAMES - 1);\n}\n"
  },
  {
    "path": "packages/create-react-router/package.json",
    "content": "{\n  \"name\": \"create-react-router\",\n  \"version\": \"7.13.1\",\n  \"description\": \"Create a new React Router app\",\n  \"homepage\": \"https://reactrouter.com\",\n  \"bugs\": {\n    \"url\": \"https://github.com/remix-run/remix/issues\"\n  },\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"https://github.com/remix-run/react-router\",\n    \"directory\": \"packages/create-react-router\"\n  },\n  \"license\": \"MIT\",\n  \"bin\": {\n    \"create-react-router\": \"dist/cli.js\"\n  },\n  \"exports\": {\n    \"./package.json\": \"./package.json\"\n  },\n  \"scripts\": {\n    \"build\": \"wireit\",\n    \"typecheck\": \"tsc\"\n  },\n  \"wireit\": {\n    \"build\": {\n      \"command\": \"tsup\",\n      \"files\": [\n        \"../../pnpm-workspace.yaml\",\n        \"*.ts\",\n        \"tsconfig.json\",\n        \"package.json\"\n      ],\n      \"output\": [\n        \"dist/**\"\n      ]\n    }\n  },\n  \"dependencies\": {\n    \"@remix-run/web-fetch\": \"^4.4.2\",\n    \"arg\": \"^5.0.1\",\n    \"chalk\": \"^4.1.2\",\n    \"execa\": \"5.1.1\",\n    \"gunzip-maybe\": \"^1.4.2\",\n    \"log-update\": \"^5.0.1\",\n    \"proxy-agent\": \"^6.3.0\",\n    \"semver\": \"^7.3.7\",\n    \"sisteransi\": \"^1.0.5\",\n    \"sort-package-json\": \"^1.55.0\",\n    \"strip-ansi\": \"^6.0.1\",\n    \"tar-fs\": \"^2.1.3\"\n  },\n  \"devDependencies\": {\n    \"@types/gunzip-maybe\": \"^1.4.0\",\n    \"@types/recursive-readdir\": \"^2.2.1\",\n    \"@types/semver\": \"^7.7.0\",\n    \"@types/tar-fs\": \"^2.0.1\",\n    \"esbuild\": \"0.25.0\",\n    \"esbuild-register\": \"^3.6.0\",\n    \"msw\": \"^2.7.5\",\n    \"tiny-invariant\": \"^1.2.0\",\n    \"tsup\": \"catalog:\",\n    \"typescript\": \"catalog:\",\n    \"wireit\": \"catalog:\"\n  },\n  \"engines\": {\n    \"node\": \">=18.0.0\"\n  },\n  \"files\": [\n    \"dist/\",\n    \"CHANGELOG.md\",\n    \"LICENSE.md\",\n    \"README.md\"\n  ]\n}\n"
  },
  {
    "path": "packages/create-react-router/prompt.ts",
    "content": "// Adapted from https://github.com/withastro/cli-kit\n// MIT License Copyright (c) 2022 Nate Moore\n// https://github.com/withastro/cli-kit/tree/main/src/prompt\nimport process from \"node:process\";\n\nimport { ConfirmPrompt, type ConfirmPromptOptions } from \"./prompts-confirm\";\nimport {\n  SelectPrompt,\n  type SelectPromptOptions,\n  type SelectChoice,\n} from \"./prompts-select\";\nimport {\n  MultiSelectPrompt,\n  type MultiSelectPromptOptions,\n} from \"./prompts-multi-select\";\nimport { TextPrompt, type TextPromptOptions } from \"./prompts-text\";\nimport { identity } from \"./utils\";\n\nconst prompts = {\n  text: (args: TextPromptOptions) => toPrompt(TextPrompt, args),\n  confirm: (args: ConfirmPromptOptions) => toPrompt(ConfirmPrompt, args),\n  select: <Choices extends readonly Readonly<SelectChoice>[]>(\n    args: SelectPromptOptions<Choices>,\n  ) => toPrompt(SelectPrompt, args),\n  multiselect: <Choices extends readonly Readonly<SelectChoice>[]>(\n    args: MultiSelectPromptOptions<Choices>,\n  ) => toPrompt(MultiSelectPrompt, args),\n};\n\nexport async function prompt<\n  T extends Readonly<PromptType<any>> | Readonly<PromptType<any>[]>,\n  P extends T extends Readonly<any[]> ? T[number] : T = T extends Readonly<\n    any[]\n  >\n    ? T[number]\n    : T,\n>(questions: T, opts: PromptTypeOptions<P> = {}): Promise<Answers<T>> {\n  let {\n    onSubmit = identity,\n    onCancel = () => process.exit(0),\n    stdin = process.stdin,\n    stdout = process.stdout,\n  } = opts;\n\n  let answers = {} as Answers<T>;\n\n  let questionsArray = (\n    Array.isArray(questions) ? questions : [questions]\n  ) as Readonly<P[]>;\n  let answer: Answer<P>;\n  let quit: any;\n  let name: string;\n  let type: P[\"type\"];\n\n  for (let question of questionsArray) {\n    ({ name, type } = question);\n\n    try {\n      // Get the injected answer if there is one or prompt the user\n      // @ts-expect-error\n      answer = await prompts[type](Object.assign({ stdin, stdout }, question));\n      answers[name] = answer as any;\n      quit = await onSubmit(question, answer, answers);\n    } catch (err) {\n      quit = !(await onCancel(question, answers));\n    }\n    if (quit) {\n      return answers;\n    }\n  }\n  return answers;\n}\n\nfunction toPrompt<\n  T extends\n    | typeof TextPrompt\n    | typeof ConfirmPrompt\n    | typeof SelectPrompt<any>\n    | typeof MultiSelectPrompt<any>,\n>(el: T, args: any, opts: any = {}) {\n  if (\n    el !== TextPrompt &&\n    el !== ConfirmPrompt &&\n    el !== SelectPrompt &&\n    el !== MultiSelectPrompt\n  ) {\n    throw new Error(`Invalid prompt type: ${el.name}`);\n  }\n\n  return new Promise((res, rej) => {\n    let p = new el(\n      args,\n      // @ts-expect-error\n      opts,\n    );\n    let onAbort = args.onAbort || opts.onAbort || identity;\n    let onSubmit = args.onSubmit || opts.onSubmit || identity;\n    let onExit = args.onExit || opts.onExit || identity;\n    p.on(\"state\", args.onState || identity);\n    p.on(\"submit\", (x: any) => res(onSubmit(x)));\n    p.on(\"exit\", (x: any) => res(onExit(x)));\n    p.on(\"abort\", (x: any) => rej(onAbort(x)));\n  });\n}\n\ntype UnionToIntersection<U> = (U extends any ? (k: U) => void : never) extends (\n  k: infer I,\n) => void\n  ? I\n  : never;\n\ninterface BasePromptType {\n  name: string;\n}\n\ninterface TextPromptType extends BasePromptType {\n  type: \"text\";\n}\n\ninterface ConfirmPromptType extends BasePromptType {\n  type: \"confirm\";\n}\n\ninterface SelectPromptType<\n  Choices extends Readonly<Readonly<SelectChoiceType>[]>,\n> extends BasePromptType {\n  type: \"select\";\n  choices: Choices;\n}\n\ninterface MultiSelectPromptType<\n  Choices extends Readonly<Readonly<SelectChoiceType>[]>,\n> extends BasePromptType {\n  type: \"multiselect\";\n  choices: Choices;\n}\n\ninterface SelectChoiceType {\n  value: unknown;\n  label: string;\n  hint?: string;\n}\n\ntype PromptType<\n  Choices extends Readonly<SelectChoiceType[]> = Readonly<SelectChoiceType[]>,\n> =\n  | TextPromptType\n  | ConfirmPromptType\n  | SelectPromptType<Choices>\n  | MultiSelectPromptType<Choices>;\n\ntype PromptChoices<T extends PromptType<any>> =\n  T extends SelectPromptType<infer Choices>\n    ? Choices\n    : T extends MultiSelectPromptType<infer Choices>\n      ? Choices\n      : never;\n\ntype Answer<\n  T extends PromptType<any>,\n  Choices extends Readonly<SelectChoiceType[]> = PromptChoices<T>,\n> = T extends TextPromptType\n  ? string\n  : T extends ConfirmPromptType\n    ? boolean\n    : T extends SelectPromptType<Choices>\n      ? Choices[number][\"value\"]\n      : T extends MultiSelectPromptType<Choices>\n        ? (Choices[number][\"value\"] | undefined)[]\n        : never;\n\ntype Answers<\n  T extends Readonly<PromptType<any>> | Readonly<PromptType<any>[]>,\n> =\n  T extends Readonly<PromptType<any>>\n    ? Partial<{ [key in T[\"name\"]]: Answer<T> }>\n    : T extends Readonly<PromptType<any>[]>\n      ? UnionToIntersection<Answers<T[number]>>\n      : never;\n\ninterface PromptTypeOptions<\n  T extends PromptType<any>,\n  Choices extends Readonly<SelectChoiceType[]> = PromptChoices<T>,\n> {\n  onSubmit?(\n    question: T | Readonly<T>,\n    answer: Answer<T, Choices>,\n    answers: Answers<T>,\n  ): any;\n  onCancel?(question: T | Readonly<T>, answers: Answers<T>): any;\n  stdin?: NodeJS.ReadStream;\n  stdout?: NodeJS.WriteStream;\n}\n"
  },
  {
    "path": "packages/create-react-router/prompts-confirm.ts",
    "content": "/**\n * Adapted from https://github.com/withastro/cli-kit\n * @license MIT License Copyright (c) 2022 Nate Moore\n */\nimport { cursor, erase } from \"sisteransi\";\n\nimport { Prompt, type PromptOptions } from \"./prompts-prompt-base\";\nimport { color, strip, clear, type ActionKey } from \"./utils\";\n\nexport interface ConfirmPromptOptions extends PromptOptions {\n  label: string;\n  message: string;\n  initial?: boolean;\n  hint?: string;\n  validate?: (v: any) => boolean;\n  error?: string;\n}\n\nexport type ConfirmPromptChoices = [\n  { value: true; label: string },\n  { value: false; label: string },\n];\n\nexport class ConfirmPrompt extends Prompt {\n  label: string;\n  msg: string;\n  value: boolean | undefined;\n  initialValue: boolean;\n  hint?: string;\n  choices: ConfirmPromptChoices;\n  cursor: number;\n  done: boolean | undefined;\n  name = \"ConfirmPrompt\" as const;\n\n  // set by render which is called in constructor\n  outputText!: string;\n\n  constructor(opts: ConfirmPromptOptions) {\n    super(opts);\n    this.label = opts.label;\n    this.hint = opts.hint;\n    this.msg = opts.message;\n    this.value = opts.initial;\n    this.initialValue = !!opts.initial;\n    this.choices = [\n      { value: true, label: \"Yes\" },\n      { value: false, label: \"No\" },\n    ];\n    this.cursor = this.choices.findIndex((c) => c.value === this.initialValue);\n    this.render();\n  }\n\n  get type() {\n    return \"confirm\" as const;\n  }\n\n  exit() {\n    this.abort();\n  }\n\n  abort() {\n    this.done = this.aborted = true;\n    this.fire();\n    this.render();\n    this.out.write(\"\\n\");\n    this.close();\n  }\n\n  submit() {\n    this.value = this.value || false;\n    this.cursor = this.choices.findIndex((c) => c.value === this.value);\n    this.done = true;\n    this.aborted = false;\n    this.fire();\n    this.render();\n    this.out.write(\"\\n\");\n    this.close();\n  }\n\n  moveCursor(n: number) {\n    this.cursor = n;\n    this.value = this.choices[n].value;\n    this.fire();\n  }\n\n  reset() {\n    this.moveCursor(0);\n    this.fire();\n    this.render();\n  }\n\n  first() {\n    this.moveCursor(0);\n    this.render();\n  }\n\n  last() {\n    this.moveCursor(this.choices.length - 1);\n    this.render();\n  }\n\n  left() {\n    if (this.cursor === 0) {\n      this.moveCursor(this.choices.length - 1);\n    } else {\n      this.moveCursor(this.cursor - 1);\n    }\n    this.render();\n  }\n\n  right() {\n    if (this.cursor === this.choices.length - 1) {\n      this.moveCursor(0);\n    } else {\n      this.moveCursor(this.cursor + 1);\n    }\n    this.render();\n  }\n\n  _(c: string, key: ActionKey) {\n    if (!Number.isNaN(Number.parseInt(c))) {\n      let n = Number.parseInt(c) - 1;\n      this.moveCursor(n);\n      this.render();\n      return this.submit();\n    }\n    if (c.toLowerCase() === \"y\") {\n      this.value = true;\n      return this.submit();\n    }\n    if (c.toLowerCase() === \"n\") {\n      this.value = false;\n      return this.submit();\n    }\n    return;\n  }\n\n  render() {\n    if (this.closed) {\n      return;\n    }\n    if (this.firstRender) {\n      this.out.write(cursor.hide);\n    } else {\n      this.out.write(clear(this.outputText, this.out.columns));\n    }\n    super.render();\n    let outputText = [\n      \"\\n\",\n      this.label,\n      \" \",\n      this.msg,\n      this.done ? \"\" : this.hint ? color.dim(` (${this.hint})`) : \"\",\n      \"\\n\",\n    ];\n\n    outputText.push(\" \".repeat(strip(this.label).length));\n\n    if (this.done) {\n      outputText.push(\" \", color.dim(`${this.choices[this.cursor].label}`));\n    } else {\n      outputText.push(\n        \" \",\n        this.choices\n          .map((choice, i) =>\n            i === this.cursor\n              ? `${color.green(\"●\")} ${choice.label} `\n              : color.dim(`○ ${choice.label} `),\n          )\n          .join(color.dim(\" \")),\n      );\n    }\n    this.outputText = outputText.join(\"\");\n\n    this.out.write(erase.line + cursor.to(0) + this.outputText);\n  }\n}\n"
  },
  {
    "path": "packages/create-react-router/prompts-multi-select.ts",
    "content": "/**\n * Adapted from https://github.com/withastro/cli-kit\n * @license MIT License Copyright (c) 2022 Nate Moore\n */\nimport { cursor, erase } from \"sisteransi\";\n\nimport { Prompt, type PromptOptions } from \"./prompts-prompt-base\";\nimport { type SelectChoice } from \"./prompts-select\";\nimport { color, strip, clear, type ActionKey } from \"./utils\";\n\nexport interface MultiSelectPromptOptions<\n  Choices extends Readonly<Readonly<SelectChoice>[]>,\n> extends PromptOptions {\n  hint?: string;\n  message: string;\n  label: string;\n  initial?: Choices[number][\"value\"];\n  validate?: (v: any) => boolean;\n  error?: string;\n  choices: Choices;\n}\n\nexport class MultiSelectPrompt<\n  Choices extends Readonly<Readonly<SelectChoice>[]>,\n> extends Prompt {\n  choices: Readonly<Array<Choices[number] & { selected: boolean }>>;\n  label: string;\n  msg: string;\n  hint?: string;\n  value: Array<Choices[number][\"value\"]>;\n  initialValue: Choices[number][\"value\"];\n  done: boolean | undefined;\n  cursor: number;\n  name = \"MultiSelectPrompt\" as const;\n\n  // set by render which is called in constructor\n  outputText!: string;\n\n  constructor(opts: MultiSelectPromptOptions<Choices>) {\n    if (\n      !opts.choices ||\n      !Array.isArray(opts.choices) ||\n      opts.choices.length < 1\n    ) {\n      throw new Error(\"MultiSelectPrompt must contain choices\");\n    }\n    super(opts);\n    this.label = opts.label;\n    this.msg = opts.message;\n    this.hint = opts.hint;\n    this.value = [];\n    this.choices =\n      opts.choices.map((choice) => ({ ...choice, selected: false })) || [];\n    this.initialValue = opts.initial || this.choices[0].value;\n    this.cursor = this.choices.findIndex((c) => c.value === this.initialValue);\n    this.render();\n  }\n\n  get type() {\n    return \"multiselect\" as const;\n  }\n\n  exit() {\n    this.abort();\n  }\n\n  abort() {\n    this.done = this.aborted = true;\n    this.cursor = this.choices.findIndex((c) => c.value === this.initialValue);\n    this.fire();\n    this.render();\n    this.out.write(\"\\n\");\n    this.close();\n  }\n\n  submit() {\n    return this.toggle();\n  }\n\n  finish() {\n    // eslint-disable-next-line no-self-assign\n    this.value = this.value;\n    this.done = true;\n    this.aborted = false;\n    this.fire();\n    this.render();\n    this.out.write(\"\\n\");\n    this.close();\n  }\n\n  moveCursor(n: number) {\n    this.cursor = n;\n    this.fire();\n  }\n\n  toggle() {\n    let choice = this.choices[this.cursor];\n    if (!choice) return;\n    choice.selected = !choice.selected;\n    this.render();\n  }\n\n  _(c: string, key: ActionKey) {\n    if (c === \" \") {\n      return this.toggle();\n    }\n    if (c.toLowerCase() === \"c\") {\n      return this.finish();\n    }\n    return;\n  }\n\n  reset() {\n    this.moveCursor(0);\n    this.fire();\n    this.render();\n  }\n\n  first() {\n    this.moveCursor(0);\n    this.render();\n  }\n\n  last() {\n    this.moveCursor(this.choices.length - 1);\n    this.render();\n  }\n\n  up() {\n    if (this.cursor === 0) {\n      this.moveCursor(this.choices.length - 1);\n    } else {\n      this.moveCursor(this.cursor - 1);\n    }\n    this.render();\n  }\n\n  down() {\n    if (this.cursor === this.choices.length - 1) {\n      this.moveCursor(0);\n    } else {\n      this.moveCursor(this.cursor + 1);\n    }\n    this.render();\n  }\n\n  render() {\n    if (this.closed) return;\n    if (this.firstRender) {\n      this.out.write(cursor.hide);\n    } else {\n      this.out.write(clear(this.outputText, this.out.columns));\n    }\n    super.render();\n\n    let outputText = [\"\\n\", this.label, \" \", this.msg, \"\\n\"];\n\n    let prefix = \" \".repeat(strip(this.label).length);\n\n    if (this.done) {\n      outputText.push(\n        this.choices\n          .map((choice) =>\n            choice.selected\n              ? `${prefix} ${color.dim(`${choice.label}`)}\\n`\n              : \"\",\n          )\n          .join(\"\")\n          .trimEnd(),\n      );\n    } else {\n      outputText.push(\n        this.choices\n          .map((choice, i) =>\n            i === this.cursor\n              ? `${prefix.slice(0, -2)}${color.cyanBright(\"▶\")}  ${\n                  choice.selected ? color.green(\"■\") : color.whiteBright(\"□\")\n                } ${color.underline(choice.label)} ${\n                  choice.hint ? color.dim(choice.hint) : \"\"\n                }`\n              : color[choice.selected ? \"reset\" : \"dim\"](\n                  `${prefix} ${choice.selected ? color.green(\"■\") : \"□\"} ${\n                    choice.label\n                  } `,\n                ),\n          )\n          .join(\"\\n\"),\n      );\n      outputText.push(\n        `\\n\\n${prefix} Press ${color.inverse(\" C \")} to continue`,\n      );\n    }\n    this.outputText = outputText.join(\"\");\n    this.out.write(erase.line + cursor.to(0) + this.outputText);\n  }\n}\n"
  },
  {
    "path": "packages/create-react-router/prompts-prompt-base.ts",
    "content": "/**\n * Adapted from https://github.com/withastro/cli-kit\n * @license MIT License Copyright (c) 2022 Nate Moore\n */\nimport process from \"node:process\";\nimport EventEmitter from \"node:events\";\nimport readline from \"node:readline\";\nimport { beep, cursor } from \"sisteransi\";\n\nimport { color, action, type ActionKey } from \"./utils\";\n\nexport class Prompt extends EventEmitter {\n  firstRender: boolean;\n  in: any;\n  out: any;\n  onRender: any;\n  close: () => void;\n  aborted: any;\n  exited: any;\n  closed: boolean | undefined;\n  name = \"Prompt\";\n\n  constructor(opts: PromptOptions = {}) {\n    super();\n    this.firstRender = true;\n    this.in = opts.stdin || process.stdin;\n    this.out = opts.stdout || process.stdout;\n    this.onRender = (opts.onRender || (() => void 0)).bind(this);\n    let rl = readline.createInterface({\n      input: this.in,\n      escapeCodeTimeout: 50,\n    });\n    readline.emitKeypressEvents(this.in, rl);\n\n    if (this.in.isTTY) this.in.setRawMode(true);\n    let isSelect =\n      [\"SelectPrompt\", \"MultiSelectPrompt\"].indexOf(this.constructor.name) > -1;\n\n    let keypress = (str: string, key: ActionKey) => {\n      if (this.in.isTTY) this.in.setRawMode(true);\n      let a = action(key, isSelect);\n      if (a === false) {\n        try {\n          this._(str, key);\n        } catch (_) {}\n        // @ts-expect-error\n      } else if (typeof this[a] === \"function\") {\n        // @ts-expect-error\n        this[a](key);\n      }\n    };\n\n    this.close = () => {\n      this.out.write(cursor.show);\n      this.in.removeListener(\"keypress\", keypress);\n      if (this.in.isTTY) this.in.setRawMode(false);\n      rl.close();\n      this.emit(\n        this.aborted ? \"abort\" : this.exited ? \"exit\" : \"submit\",\n        // @ts-expect-error\n        this.value,\n      );\n      this.closed = true;\n    };\n\n    this.in.on(\"keypress\", keypress);\n  }\n\n  get type(): string {\n    throw new Error(\"Method type not implemented.\");\n  }\n\n  bell() {\n    this.out.write(beep);\n  }\n\n  fire() {\n    this.emit(\"state\", {\n      // @ts-expect-error\n      value: this.value,\n      aborted: !!this.aborted,\n      exited: !!this.exited,\n    });\n  }\n\n  render() {\n    this.onRender(color);\n    if (this.firstRender) this.firstRender = false;\n  }\n\n  _(c: string, key: ActionKey) {\n    throw new Error(\"Method _ not implemented.\");\n  }\n}\n\nexport interface PromptOptions {\n  stdin?: typeof process.stdin;\n  stdout?: typeof process.stdout;\n  onRender?(render: (...text: unknown[]) => string): void;\n  onSubmit?(\n    v: any,\n  ): void | undefined | boolean | Promise<void | undefined | boolean>;\n  onCancel?(\n    v: any,\n  ): void | undefined | boolean | Promise<void | undefined | boolean>;\n  onAbort?(\n    v: any,\n  ): void | undefined | boolean | Promise<void | undefined | boolean>;\n  onExit?(\n    v: any,\n  ): void | undefined | boolean | Promise<void | undefined | boolean>;\n  onState?(\n    v: any,\n  ): void | undefined | boolean | Promise<void | undefined | boolean>;\n}\n"
  },
  {
    "path": "packages/create-react-router/prompts-select.ts",
    "content": "/**\n * Adapted from https://github.com/withastro/cli-kit\n * @license MIT License Copyright (c) 2022 Nate Moore\n */\nimport { cursor, erase } from \"sisteransi\";\n\nimport { Prompt, type PromptOptions } from \"./prompts-prompt-base\";\nimport { color, strip, clear, shouldUseAscii, type ActionKey } from \"./utils\";\n\nexport interface SelectChoice {\n  value: unknown;\n  label: string;\n  hint?: string;\n}\n\nexport interface SelectPromptOptions<\n  Choices extends Readonly<Readonly<SelectChoice>[]>,\n> extends PromptOptions {\n  hint?: string;\n  message: string;\n  label: string;\n  initial?: Choices[number][\"value\"] | undefined;\n  validate?: (v: any) => boolean;\n  error?: string;\n  choices: Choices;\n}\n\nexport class SelectPrompt<\n  Choices extends Readonly<Readonly<SelectChoice>[]>,\n> extends Prompt {\n  choices: Choices;\n  label: string;\n  msg: string;\n  hint?: string;\n  value: Choices[number][\"value\"] | undefined;\n  initialValue: Choices[number][\"value\"];\n  search: string | null;\n  done: boolean | undefined;\n  cursor: number;\n  name = \"SelectPrompt\" as const;\n  private _timeout: NodeJS.Timeout | undefined;\n\n  // set by render which is called in constructor\n  outputText!: string;\n\n  constructor(opts: SelectPromptOptions<Choices>) {\n    if (\n      !opts.choices ||\n      !Array.isArray(opts.choices) ||\n      opts.choices.length < 1\n    ) {\n      throw new Error(\"SelectPrompt must contain choices\");\n    }\n    super(opts);\n    this.label = opts.label;\n    this.hint = opts.hint;\n    this.msg = opts.message;\n    this.value = opts.initial;\n    this.choices = opts.choices;\n    this.initialValue = opts.initial || this.choices[0].value;\n    this.cursor = this.choices.findIndex((c) => c.value === this.initialValue);\n    this.search = null;\n    this.render();\n  }\n\n  get type() {\n    return \"select\" as const;\n  }\n\n  exit() {\n    this.abort();\n  }\n\n  abort() {\n    this.done = this.aborted = true;\n    this.cursor = this.choices.findIndex((c) => c.value === this.initialValue);\n    this.fire();\n    this.render();\n    this.out.write(\"\\n\");\n    this.close();\n  }\n\n  submit() {\n    this.value = this.value || undefined;\n    this.cursor = this.choices.findIndex((c) => c.value === this.value);\n    this.done = true;\n    this.aborted = false;\n    this.fire();\n    this.render();\n    this.out.write(\"\\n\");\n    this.close();\n  }\n\n  delete() {\n    this.search = null;\n    this.render();\n  }\n\n  _(c: string, key: ActionKey) {\n    if (this._timeout) clearTimeout(this._timeout);\n    if (!Number.isNaN(Number.parseInt(c))) {\n      let n = Number.parseInt(c) - 1;\n      this.moveCursor(n);\n      this.render();\n      return this.submit();\n    }\n    this.search = this.search || \"\";\n    this.search += c.toLowerCase();\n    let choices = !this.search ? this.choices.slice(this.cursor) : this.choices;\n    let n = choices.findIndex((c) =>\n      c.label.toLowerCase().includes(this.search!),\n    );\n    if (n > -1) {\n      this.moveCursor(n);\n      this.render();\n    }\n    this._timeout = setTimeout(() => {\n      this.search = null;\n    }, 500);\n  }\n\n  moveCursor(n: number) {\n    this.cursor = n;\n    this.value = this.choices[n].value;\n    this.fire();\n  }\n\n  reset() {\n    this.moveCursor(0);\n    this.fire();\n    this.render();\n  }\n\n  first() {\n    this.moveCursor(0);\n    this.render();\n  }\n\n  last() {\n    this.moveCursor(this.choices.length - 1);\n    this.render();\n  }\n\n  up() {\n    if (this.cursor === 0) {\n      this.moveCursor(this.choices.length - 1);\n    } else {\n      this.moveCursor(this.cursor - 1);\n    }\n    this.render();\n  }\n\n  down() {\n    if (this.cursor === this.choices.length - 1) {\n      this.moveCursor(0);\n    } else {\n      this.moveCursor(this.cursor + 1);\n    }\n    this.render();\n  }\n\n  highlight(label: string) {\n    if (!this.search) return label;\n    let n = label.toLowerCase().indexOf(this.search.toLowerCase());\n    if (n === -1) return label;\n    return [\n      label.slice(0, n),\n      color.underline(label.slice(n, n + this.search.length)),\n      label.slice(n + this.search.length),\n    ].join(\"\");\n  }\n\n  render() {\n    if (this.closed) return;\n    if (this.firstRender) this.out.write(cursor.hide);\n    else this.out.write(clear(this.outputText, this.out.columns));\n    super.render();\n\n    let outputText = [\n      \"\\n\",\n      this.label,\n      \" \",\n      this.msg,\n      this.done\n        ? \"\"\n        : this.hint\n          ? (this.out.columns < 80 ? \"\\n\" + \" \".repeat(8) : \"\") +\n            color.dim(` (${this.hint})`)\n          : \"\",\n      \"\\n\",\n    ];\n\n    let prefix = \" \".repeat(strip(this.label).length);\n\n    if (this.done) {\n      outputText.push(\n        `${prefix} `,\n        color.dim(`${this.choices[this.cursor]?.label}`),\n      );\n    } else {\n      outputText.push(\n        this.choices\n          .map((choice, i) =>\n            i === this.cursor\n              ? `${prefix} ${color.green(\n                  shouldUseAscii() ? \">\" : \"●\",\n                )} ${this.highlight(choice.label)} ${\n                  choice.hint ? color.dim(choice.hint) : \"\"\n                }`\n              : color.dim(\n                  `${prefix} ${shouldUseAscii() ? \"—\" : \"○\"} ${choice.label} `,\n                ),\n          )\n          .join(\"\\n\"),\n      );\n    }\n    this.outputText = outputText.join(\"\");\n\n    this.out.write(erase.line + cursor.to(0) + this.outputText);\n  }\n}\n"
  },
  {
    "path": "packages/create-react-router/prompts-text.ts",
    "content": "/**\n * Adapted from https://github.com/withastro/cli-kit\n * @license MIT License Copyright (c) 2022 Nate Moore\n */\nimport { cursor, erase } from \"sisteransi\";\n\nimport { Prompt, type PromptOptions } from \"./prompts-prompt-base\";\nimport {\n  color,\n  strip,\n  clear,\n  lines,\n  shouldUseAscii,\n  type ActionKey,\n} from \"./utils\";\n\nexport interface TextPromptOptions extends PromptOptions {\n  label: string;\n  message: string;\n  initial?: string;\n  style?: string;\n  validate?: (v: any) => v is string;\n  error?: string;\n  hint?: string;\n}\n\nexport class TextPrompt extends Prompt {\n  transform: { render: (v: string) => any; scale: number };\n  label: string;\n  scale: number;\n  msg: string;\n  initial: string;\n  hint?: string;\n  validator: (v: any) => boolean | Promise<boolean>;\n  errorMsg: string;\n  cursor: number;\n  cursorOffset: number;\n  clear: any;\n  done: boolean | undefined;\n  error: boolean | undefined;\n  red: boolean | undefined;\n  outputError: string | undefined;\n  name = \"TextPrompt\" as const;\n\n  // set by value setter, value is set in constructor\n  _value!: string;\n  placeholder!: boolean;\n  rendered!: string;\n\n  // set by render which is called in constructor\n  outputText!: string;\n\n  constructor(opts: TextPromptOptions) {\n    super(opts);\n    this.transform = { render: (v) => v, scale: 1 };\n    this.label = opts.label;\n    this.scale = this.transform.scale;\n    this.msg = opts.message;\n    this.hint = opts.hint;\n    this.initial = opts.initial || \"\";\n    this.validator = opts.validate || (() => true);\n    this.value = \"\";\n    this.errorMsg = opts.error || \"Please enter a valid value\";\n    this.cursor = Number(!!this.initial);\n    this.cursorOffset = 0;\n    this.clear = clear(``, this.out.columns);\n    this.render();\n  }\n\n  get type() {\n    return \"text\" as const;\n  }\n\n  set value(v: string) {\n    if (!v && this.initial) {\n      this.placeholder = true;\n      this.rendered = color.dim(this.initial);\n    } else {\n      this.placeholder = false;\n      this.rendered = this.transform.render(v);\n    }\n    this._value = v;\n    this.fire();\n  }\n\n  get value() {\n    return this._value;\n  }\n\n  reset() {\n    this.value = \"\";\n    this.cursor = Number(!!this.initial);\n    this.cursorOffset = 0;\n    this.fire();\n    this.render();\n  }\n\n  exit() {\n    this.abort();\n  }\n\n  abort() {\n    this.value = this.value || this.initial;\n    this.done = this.aborted = true;\n    this.error = false;\n    this.red = false;\n    this.fire();\n    this.render();\n    this.out.write(\"\\n\");\n    this.close();\n  }\n\n  async validate() {\n    let valid = await this.validator(this.value);\n    if (typeof valid === `string`) {\n      this.errorMsg = valid;\n      valid = false;\n    }\n    this.error = !valid;\n  }\n\n  async submit() {\n    this.value = this.value || this.initial;\n    this.cursorOffset = 0;\n    this.cursor = this.rendered.length;\n    await this.validate();\n    if (this.error) {\n      this.red = true;\n      this.fire();\n      this.render();\n      return;\n    }\n    this.done = true;\n    this.aborted = false;\n    this.fire();\n    this.render();\n    this.out.write(\"\\n\");\n    this.close();\n  }\n\n  next() {\n    if (!this.placeholder) return this.bell();\n    this.value = this.initial;\n    this.cursor = this.rendered.length;\n    this.fire();\n    this.render();\n  }\n\n  moveCursor(n: number) {\n    if (this.placeholder) return;\n    this.cursor = this.cursor + n;\n    this.cursorOffset += n;\n  }\n\n  _(c: string, key: ActionKey) {\n    let s1 = this.value.slice(0, this.cursor);\n    let s2 = this.value.slice(this.cursor);\n    this.value = `${s1}${c}${s2}`;\n    this.red = false;\n    this.cursor = this.placeholder ? 0 : s1.length + 1;\n    this.render();\n  }\n\n  delete() {\n    if (this.isCursorAtStart()) return this.bell();\n    let s1 = this.value.slice(0, this.cursor - 1);\n    let s2 = this.value.slice(this.cursor);\n    this.value = `${s1}${s2}`;\n    this.red = false;\n    this.outputError = \"\";\n    this.error = false;\n    if (this.isCursorAtStart()) {\n      this.cursorOffset = 0;\n    } else {\n      this.cursorOffset++;\n      this.moveCursor(-1);\n    }\n    this.render();\n  }\n\n  deleteForward() {\n    if (this.cursor * this.scale >= this.rendered.length || this.placeholder)\n      return this.bell();\n    let s1 = this.value.slice(0, this.cursor);\n    let s2 = this.value.slice(this.cursor + 1);\n    this.value = `${s1}${s2}`;\n    this.red = false;\n    this.outputError = \"\";\n    this.error = false;\n    if (this.isCursorAtEnd()) {\n      this.cursorOffset = 0;\n    } else {\n      this.cursorOffset++;\n    }\n    this.render();\n  }\n\n  first() {\n    this.cursor = 0;\n    this.render();\n  }\n\n  last() {\n    this.cursor = this.value.length;\n    this.render();\n  }\n\n  left() {\n    if (this.cursor <= 0 || this.placeholder) return this.bell();\n    this.moveCursor(-1);\n    this.render();\n  }\n\n  right() {\n    if (this.cursor * this.scale >= this.rendered.length || this.placeholder)\n      return this.bell();\n    this.moveCursor(1);\n    this.render();\n  }\n\n  isCursorAtStart() {\n    return this.cursor === 0 || (this.placeholder && this.cursor === 1);\n  }\n\n  isCursorAtEnd() {\n    return (\n      this.cursor === this.rendered.length ||\n      (this.placeholder && this.cursor === this.rendered.length + 1)\n    );\n  }\n\n  render() {\n    if (this.closed) return;\n    if (!this.firstRender) {\n      if (this.outputError)\n        this.out.write(\n          cursor.down(lines(this.outputError, this.out.columns) - 1) +\n            clear(this.outputError, this.out.columns),\n        );\n      this.out.write(clear(this.outputText, this.out.columns));\n    }\n    super.render();\n    this.outputError = \"\";\n\n    let prefix = \" \".repeat(strip(this.label).length);\n\n    this.outputText = [\n      \"\\n\",\n      this.label,\n      \" \",\n      this.msg,\n      this.done\n        ? \"\"\n        : this.hint\n          ? (this.out.columns < 80 ? \"\\n\" + \" \".repeat(8) : \"\") +\n            color.dim(` (${this.hint})`)\n          : \"\",\n      \"\\n\" + prefix,\n      \" \",\n      this.done ? color.dim(this.rendered) : this.rendered,\n    ].join(\"\");\n\n    if (this.error) {\n      this.outputError += `  ${color.redBright(\n        (shouldUseAscii() ? \"> \" : \"▶ \") + this.errorMsg,\n      )}`;\n    }\n\n    this.out.write(\n      erase.line +\n        cursor.to(0) +\n        this.outputText +\n        cursor.save +\n        this.outputError +\n        cursor.restore +\n        cursor.move(\n          this.placeholder\n            ? (this.rendered.length - 9) * -1\n            : this.cursorOffset,\n          0,\n        ),\n    );\n  }\n}\n"
  },
  {
    "path": "packages/create-react-router/tsconfig.json",
    "content": "{\n  \"include\": [\"**/*.ts\", \"./package.json\"],\n  \"exclude\": [\"dist\", \"**/node_modules/**\", \"__tests__\"],\n  \"compilerOptions\": {\n    \"lib\": [\"DOM\", \"DOM.Iterable\", \"ES2022\"],\n    \"target\": \"ES2022\",\n    \"module\": \"ES2022\",\n    \"skipLibCheck\": true,\n    \"moduleResolution\": \"Bundler\",\n    \"allowSyntheticDefaultImports\": true,\n    \"strict\": true,\n    \"resolveJsonModule\": true,\n    \"declaration\": true,\n    \"noEmit\": true,\n    \"rootDir\": \".\",\n    \"outDir\": \"dist\"\n  }\n}\n"
  },
  {
    "path": "packages/create-react-router/tsup.config.ts",
    "content": "import { defineConfig } from \"tsup\";\n\n// @ts-ignore - out of scope\nimport { createBanner } from \"../../build.utils.js\";\n\nimport pkg from \"./package.json\";\n\nconst entry = [\"cli.ts\"];\n\nexport default defineConfig([\n  {\n    clean: true,\n    entry,\n    format: [\"cjs\"],\n    outDir: \"dist\",\n    dts: true,\n    banner: {\n      js: createBanner(pkg.name, pkg.version),\n    },\n  },\n]);\n"
  },
  {
    "path": "packages/create-react-router/utils.ts",
    "content": "import fs from \"node:fs\";\nimport { readdir } from \"node:fs/promises\";\nimport path from \"node:path\";\nimport process from \"node:process\";\nimport os from \"node:os\";\nimport { type Key as ActionKey } from \"node:readline\";\nimport { erase, cursor } from \"sisteransi\";\nimport chalk from \"chalk\";\n\n// https://no-color.org/\nconst SUPPORTS_COLOR = chalk.supportsColor && !process.env.NO_COLOR;\n\nexport const color = {\n  supportsColor: SUPPORTS_COLOR,\n  heading: safeColor(chalk.bold),\n  arg: safeColor(chalk.yellowBright),\n  error: safeColor(chalk.red),\n  warning: safeColor(chalk.yellow),\n  hint: safeColor(chalk.blue),\n  bold: safeColor(chalk.bold),\n  black: safeColor(chalk.black),\n  white: safeColor(chalk.white),\n  blue: safeColor(chalk.blue),\n  cyan: safeColor(chalk.cyan),\n  red: safeColor(chalk.red),\n  yellow: safeColor(chalk.yellow),\n  green: safeColor(chalk.green),\n  blackBright: safeColor(chalk.blackBright),\n  whiteBright: safeColor(chalk.whiteBright),\n  blueBright: safeColor(chalk.blueBright),\n  cyanBright: safeColor(chalk.cyanBright),\n  redBright: safeColor(chalk.redBright),\n  yellowBright: safeColor(chalk.yellowBright),\n  greenBright: safeColor(chalk.greenBright),\n  bgBlack: safeColor(chalk.bgBlack),\n  bgWhite: safeColor(chalk.bgWhite),\n  bgBlue: safeColor(chalk.bgBlue),\n  bgCyan: safeColor(chalk.bgCyan),\n  bgRed: safeColor(chalk.bgRed),\n  bgYellow: safeColor(chalk.bgYellow),\n  bgGreen: safeColor(chalk.bgGreen),\n  bgBlackBright: safeColor(chalk.bgBlackBright),\n  bgWhiteBright: safeColor(chalk.bgWhiteBright),\n  bgBlueBright: safeColor(chalk.bgBlueBright),\n  bgCyanBright: safeColor(chalk.bgCyanBright),\n  bgRedBright: safeColor(chalk.bgRedBright),\n  bgYellowBright: safeColor(chalk.bgYellowBright),\n  bgGreenBright: safeColor(chalk.bgGreenBright),\n  gray: safeColor(chalk.gray),\n  dim: safeColor(chalk.dim),\n  reset: safeColor(chalk.reset),\n  inverse: safeColor(chalk.inverse),\n  hex: (color: string) => safeColor(chalk.hex(color)),\n  underline: chalk.underline,\n};\n\nfunction safeColor(style: chalk.Chalk) {\n  return SUPPORTS_COLOR ? style : identity;\n}\n\nexport { type ActionKey };\n\nconst unicode = { enabled: os.platform() !== \"win32\" };\nexport const shouldUseAscii = () => !unicode.enabled;\n\nexport function isInteractive() {\n  // Support explicit override for testing purposes\n  if (\"CREATE_REACT_ROUTER_FORCE_INTERACTIVE\" in process.env) {\n    return true;\n  }\n\n  // Adapted from https://github.com/sindresorhus/is-interactive\n  return Boolean(\n    process.stdout.isTTY &&\n      process.env.TERM !== \"dumb\" &&\n      !(\"CI\" in process.env),\n  );\n}\n\nexport function log(message: string) {\n  return process.stdout.write(message + \"\\n\");\n}\n\nexport let stderr = process.stderr;\n/** @internal Used to mock `process.stderr.write` for testing purposes */\nexport function setStderr(writable: typeof process.stderr) {\n  stderr = writable;\n}\n\nexport function logError(message: string) {\n  return stderr.write(message + \"\\n\");\n}\n\nfunction logBullet(\n  logger: typeof log | typeof logError,\n  colorizePrefix: <V>(v: V) => V,\n  colorizeText: <V>(v: V) => V,\n  symbol: string,\n  prefix: string,\n  text?: string | string[],\n) {\n  let textParts = Array.isArray(text) ? text : [text || \"\"].filter(Boolean);\n  let formattedText = textParts\n    .map((textPart) => colorizeText(textPart))\n    .join(\"\");\n\n  if (process.stdout.columns < 80) {\n    logger(\n      `${\" \".repeat(5)} ${colorizePrefix(symbol)}  ${colorizePrefix(prefix)}`,\n    );\n    logger(`${\" \".repeat(9)}${formattedText}`);\n  } else {\n    logger(\n      `${\" \".repeat(5)} ${colorizePrefix(symbol)}  ${colorizePrefix(\n        prefix,\n      )} ${formattedText}`,\n    );\n  }\n}\n\nexport function debug(prefix: string, text?: string | string[]) {\n  logBullet(log, color.yellow, color.dim, \"●\", prefix, text);\n}\n\nexport function info(prefix: string, text?: string | string[]) {\n  logBullet(log, color.cyan, color.dim, \"◼\", prefix, text);\n}\n\nexport function success(text: string) {\n  logBullet(log, color.green, color.dim, \"✔\", text);\n}\n\nexport function error(prefix: string, text?: string | string[]) {\n  log(\"\");\n  logBullet(logError, color.red, color.error, \"▲\", prefix, text);\n}\n\nexport function sleep(ms: number) {\n  return new Promise<void>((resolve) => setTimeout(resolve, ms));\n}\n\nexport function toValidProjectName(projectName: string) {\n  if (isValidProjectName(projectName)) {\n    return projectName;\n  }\n  return projectName\n    .trim()\n    .toLowerCase()\n    .replace(/\\s+/g, \"-\")\n    .replace(/^[._]/, \"\")\n    .replace(/[^a-z\\d\\-~]+/g, \"-\")\n    .replace(/^-+/, \"\")\n    .replace(/-+$/, \"\");\n}\n\nfunction isValidProjectName(projectName: string) {\n  return /^(?:@[a-z\\d\\-*~][a-z\\d\\-*._~]*\\/)?[a-z\\d\\-~][a-z\\d\\-._~]*$/.test(\n    projectName,\n  );\n}\n\nexport function identity<V>(v: V) {\n  return v;\n}\n\nexport function strip(str: string) {\n  let pattern = [\n    \"[\\\\u001B\\\\u009B][[\\\\]()#;?]*(?:(?:(?:(?:;[-a-zA-Z\\\\d\\\\/#&.:=?%@~_]+)*|[a-zA-Z\\\\d]+(?:;[-a-zA-Z\\\\d\\\\/#&.:=?%@~_]*)*)?\\\\u0007)\",\n    \"(?:(?:\\\\d{1,4}(?:;\\\\d{0,4})*)?[\\\\dA-PRZcf-ntqry=><~]))\",\n  ].join(\"|\");\n  let RGX = new RegExp(pattern, \"g\");\n  return typeof str === \"string\" ? str.replace(RGX, \"\") : str;\n}\n\nexport function reverse<T>(arr: T[]): T[] {\n  return [...arr].reverse();\n}\n\nexport function isValidJsonObject(obj: any): obj is Record<string, unknown> {\n  return !!(obj && typeof obj === \"object\" && !Array.isArray(obj));\n}\n\nexport async function directoryExists(p: string) {\n  try {\n    let stat = await fs.promises.stat(p);\n    return stat.isDirectory();\n  } catch {\n    return false;\n  }\n}\n\nexport async function fileExists(p: string) {\n  try {\n    let stat = await fs.promises.stat(p);\n    return stat.isFile();\n  } catch {\n    return false;\n  }\n}\n\nexport async function ensureDirectory(dir: string) {\n  if (!(await directoryExists(dir))) {\n    await fs.promises.mkdir(dir, { recursive: true });\n  }\n}\n\nexport function pathContains(path: string, dir: string) {\n  let relative = path.replace(dir, \"\");\n  return relative.length < path.length && !relative.startsWith(\"..\");\n}\n\nexport function isUrl(value: string | URL) {\n  try {\n    new URL(value);\n    return true;\n  } catch (_) {\n    return false;\n  }\n}\n\nexport function clear(prompt: string, perLine: number) {\n  if (!perLine) return erase.line + cursor.to(0);\n  let rows = 0;\n  let lines = prompt.split(/\\r?\\n/);\n  for (let line of lines) {\n    rows += 1 + Math.floor(Math.max(strip(line).length - 1, 0) / perLine);\n  }\n\n  return erase.lines(rows);\n}\n\nexport function lines(msg: string, perLine: number) {\n  let lines = String(strip(msg) || \"\").split(/\\r?\\n/);\n  if (!perLine) return lines.length;\n  return lines\n    .map((l) => Math.ceil(l.length / perLine))\n    .reduce((a, b) => a + b);\n}\n\nexport function action(key: ActionKey, isSelect: boolean) {\n  if (key.meta && key.name !== \"escape\") return;\n\n  if (key.ctrl) {\n    if (key.name === \"a\") return \"first\";\n    if (key.name === \"c\") return \"abort\";\n    if (key.name === \"d\") return \"abort\";\n    if (key.name === \"e\") return \"last\";\n    if (key.name === \"g\") return \"reset\";\n  }\n\n  if (isSelect) {\n    if (key.name === \"j\") return \"down\";\n    if (key.name === \"k\") return \"up\";\n  }\n\n  if (key.name === \"return\") return \"submit\";\n  if (key.name === \"enter\") return \"submit\"; // ctrl + J\n  if (key.name === \"backspace\") return \"delete\";\n  if (key.name === \"delete\") return \"deleteForward\";\n  if (key.name === \"abort\") return \"abort\";\n  if (key.name === \"escape\") return \"exit\";\n  if (key.name === \"tab\") return \"next\";\n  if (key.name === \"pagedown\") return \"nextPage\";\n  if (key.name === \"pageup\") return \"prevPage\";\n  if (key.name === \"home\") return \"home\";\n  if (key.name === \"end\") return \"end\";\n\n  if (key.name === \"up\") return \"up\";\n  if (key.name === \"down\") return \"down\";\n  if (key.name === \"right\") return \"right\";\n  if (key.name === \"left\") return \"left\";\n\n  return false;\n}\n\nexport function stripDirectoryFromPath(dir: string, filePath: string) {\n  // Can't just do a regexp replace here since the windows paths mess it up :/\n  let stripped = filePath;\n  if (\n    (dir.endsWith(path.sep) && filePath.startsWith(dir)) ||\n    (!dir.endsWith(path.sep) && filePath.startsWith(dir + path.sep))\n  ) {\n    stripped = filePath.slice(dir.length);\n    if (stripped.startsWith(path.sep)) {\n      stripped = stripped.slice(1);\n    }\n  }\n  return stripped;\n}\n\n// We do not copy these folders from templates so we can ignore them for comparisons\nexport const IGNORED_TEMPLATE_DIRECTORIES = [\".git\", \"node_modules\"];\n\nexport async function getDirectoryFilesRecursive(dir: string) {\n  return (await readdir(dir, { recursive: true })).filter((file) => {\n    let parts = file.split(path.sep);\n\n    return (\n      parts.length <= 1 || !IGNORED_TEMPLATE_DIRECTORIES.includes(parts[0])\n    );\n  });\n}\n"
  },
  {
    "path": "packages/react-router/.eslintrc.js",
    "content": "let restrictedGlobalsError = `Node globals are not allowed in this package.`;\n\nmodule.exports = {\n  env: {\n    browser: true,\n    commonjs: true,\n  },\n  rules: {\n    strict: 0,\n    \"no-restricted-globals\": [\n      \"error\",\n      { name: \"__dirname\", message: restrictedGlobalsError },\n      { name: \"__filename\", message: restrictedGlobalsError },\n      { name: \"Buffer\", message: restrictedGlobalsError },\n    ],\n    \"import/no-nodejs-modules\": \"error\",\n  },\n};\n"
  },
  {
    "path": "packages/react-router/CHANGELOG.md",
    "content": "# `react-router`\n\n## 7.13.1\n\n### Patch Changes\n\n- fix null reference exception in bad codepath leading to invalid route tree comparisons ([#14780](https://github.com/remix-run/react-router/pull/14780))\n\n- fix: clear timeout when turbo-stream encoding completes ([#14810](https://github.com/remix-run/react-router/pull/14810))\n\n- Improve error message when Origin header is invalid ([#14743](https://github.com/remix-run/react-router/pull/14743))\n\n- Fix matchPath optional params matching without a \"/\" separator. ([#14689](https://github.com/remix-run/react-router/pull/14689))\n  - matchPath(\"/users/:id?\", \"/usersblah\") now returns null.\n  - matchPath(\"/test_route/:part?\", \"/test_route_more\") now returns null.\n\n- add RSC unstable_getRequest ([#14758](https://github.com/remix-run/react-router/pull/14758))\n\n- Fix `HydrateFallback` rendering during initial lazy route discovery with matching splat route ([#14740](https://github.com/remix-run/react-router/pull/14740))\n\n- \\[UNSTABLE] Add support for `<Link unstable_mask>` in Data Mode which allows users to navigate to a URL in the router but \"mask\" the URL displayed in the browser. This is useful for contextual routing usages such as displaying an image in a model on top of a gallery, but displaying a browser URL directly to the image that can be shared and loaded without the contextual gallery in the background. ([#14716](https://github.com/remix-run/react-router/pull/14716))\n\n  ```tsx\n  // routes/gallery.tsx\n  export function clientLoader({ request }: Route.LoaderArgs) {\n    let sp = new URL(request.url).searchParams;\n    return {\n      images: getImages(),\n      // When the router location has the image param, load the modal data\n      modalImage: sp.has(\"image\") ? getImage(sp.get(\"image\")!) : null,\n    };\n  }\n\n  export default function Gallery({ loaderData }: Route.ComponentProps) {\n    return (\n      <>\n        <GalleryGrid>\n          {loaderData.images.map((image) => (\n            <Link\n              key={image.id}\n              {/* Navigate the router to /galley?image=N */}}\n              to={`/gallery?image=${image.id}`}\n              {/* But display /images/N in the URL bar */}}\n              unstable_mask={`/images/${image.id}`}\n            >\n              <img src={image.url} alt={image.alt} />\n            </Link>\n          ))}\n        </GalleryGrid>\n\n        {/* When the modal data exists, display the modal */}\n        {data.modalImage ? (\n          <dialog open>\n            <img src={data.modalImage.url} alt={data.modalImage.alt} />\n          </dialog>\n        ) : null}\n      </>\n    );\n  }\n  ```\n\n  Notes:\n  - The masked location, if present, will be available on `useLocation().unstable_mask` so you can detect whether you are currently masked or not.\n  - Masked URLs only work for SPA use cases, and will be removed from `history.state` during SSR.\n  - This provides a first-class API to mask URLs in Data Mode to achieve the same behavior you could do in Declarative Mode via [manual `backgroundLocation` management](https://github.com/remix-run/react-router/tree/main/examples/modal).\n\n- RSC: Update failed origin checks to return a 400 status and appropriate UI instead of a generic 500 ([#14755](https://github.com/remix-run/react-router/pull/14755))\n\n- Preserve query parameters and hash on manifest version mismatch reload ([#14813](https://github.com/remix-run/react-router/pull/14813))\n\n## 7.13.0\n\n### Minor Changes\n\n- Add `crossOrigin` prop to `Links` component ([#14687](https://github.com/remix-run/react-router/pull/14687))\n\n### Patch Changes\n\n- Fix double slash normalization for useNavigate colon urls ([#14718](https://github.com/remix-run/react-router/pull/14718))\n- Update failed origin checks to return a 400 status instead of a 500 ([#14737](https://github.com/remix-run/react-router/pull/14737))\n- Bugfix #14666: Inline criticalCss is missing nonce ([#14691](https://github.com/remix-run/react-router/pull/14691))\n- Loosen `allowedActionOrigins` glob check so `**` matches all domains ([#14722](https://github.com/remix-run/react-router/pull/14722))\n\n## 7.12.0\n\n### Minor Changes\n\n- Add additional layer of CSRF protection by rejecting submissions to UI routes from external origins. If you need to permit access to specific external origins, you can specify them in the `react-router.config.ts` config `allowedActionOrigins` field. ([#14708](https://github.com/remix-run/react-router/pull/14708))\n\n### Patch Changes\n\n- Fix `generatePath` when used with suffixed params (i.e., \"/books/:id.json\") ([#14269](https://github.com/remix-run/react-router/pull/14269))\n\n- Export `UNSAFE_createMemoryHistory` and `UNSAFE_createHashHistory` alongside `UNSAFE_createBrowserHistory` for consistency. These are not intended to be used for new apps but intended to help apps usiong `unstable_HistoryRouter` migrate from v6->v7 so they can adopt the newer APIs. ([#14663](https://github.com/remix-run/react-router/pull/14663))\n\n- Escape HTML in scroll restoration keys ([#14705](https://github.com/remix-run/react-router/pull/14705))\n\n- Validate redirect locations ([#14706](https://github.com/remix-run/react-router/pull/14706))\n\n- \\[UNSTABLE] Pass `<Scripts nonce>` value through to the underlying `importmap` `script` tag when using `future.unstable_subResourceIntegrity` ([#14675](https://github.com/remix-run/react-router/pull/14675))\n\n- \\[UNSTABLE] Add a new `future.unstable_trailingSlashAwareDataRequests` flag to provide consistent behavior of `request.pathname` inside `middleware`, `loader`, and `action` functions on document and data requests when a trailing slash is present in the browser URL. ([#14644](https://github.com/remix-run/react-router/pull/14644))\n\n  Currently, your HTTP and `request` pathnames would be as follows for `/a/b/c` and `/a/b/c/`\n\n  | URL `/a/b/c` | **HTTP pathname** | **`request` pathname\\`** |\n  | ------------ | ----------------- | ------------------------ |\n  | **Document** | `/a/b/c`          | `/a/b/c` ✅              |\n  | **Data**     | `/a/b/c.data`     | `/a/b/c` ✅              |\n\n  | URL `/a/b/c/` | **HTTP pathname** | **`request` pathname\\`** |\n  | ------------- | ----------------- | ------------------------ |\n  | **Document**  | `/a/b/c/`         | `/a/b/c/` ✅             |\n  | **Data**      | `/a/b/c.data`     | `/a/b/c` ⚠️              |\n\n  With this flag enabled, these pathnames will be made consistent though a new `_.data` format for client-side `.data` requests:\n\n  | URL `/a/b/c` | **HTTP pathname** | **`request` pathname\\`** |\n  | ------------ | ----------------- | ------------------------ |\n  | **Document** | `/a/b/c`          | `/a/b/c` ✅              |\n  | **Data**     | `/a/b/c.data`     | `/a/b/c` ✅              |\n\n  | URL `/a/b/c/` | **HTTP pathname**  | **`request` pathname\\`** |\n  | ------------- | ------------------ | ------------------------ |\n  | **Document**  | `/a/b/c/`          | `/a/b/c/` ✅             |\n  | **Data**      | `/a/b/c/_.data` ⬅️ | `/a/b/c/` ✅             |\n\n  This a bug fix but we are putting it behind an opt-in flag because it has the potential to be a \"breaking bug fix\" if you are relying on the URL format for any other application or caching logic.\n\n  Enabling this flag also changes the format of client side `.data` requests from `/_root.data` to `/_.data` when navigating to `/` to align with the new format. This does not impact the `request` pathname which is still `/` in all cases.\n\n- Preserve `clientLoader.hydrate=true` when using `<HydratedRouter unstable_instrumentations>` ([#14674](https://github.com/remix-run/react-router/pull/14674))\n\n## 7.11.0\n\n### Minor Changes\n\n- Stabilize `<HydratedRouter onError>`/`<RouterProvider onError>` ([#14546](https://github.com/remix-run/react-router/pull/14546))\n\n### Patch Changes\n\n- add support for throwing redirect Response's at RSC render time ([#14596](https://github.com/remix-run/react-router/pull/14596))\n\n- Support for throwing `data()` and Response from server component render phase. Response body is not serialized as async work is not allowed as error encoding phase. If you wish to transmit data to the boundary, throw `data()` instead. ([#14632](https://github.com/remix-run/react-router/pull/14632))\n\n- Fix `unstable_useTransitions` prop on `<Router>` component to permit omission for backewards compatibility ([#14646](https://github.com/remix-run/react-router/pull/14646))\n\n- `routeRSCServerRequest` replace `fetchServer` with `serverResponse` ([#14597](https://github.com/remix-run/react-router/pull/14597))\n\n- \\[UNSTABLE] Add a new `unstable_defaultShouldRevalidate` flag to various APIs to allow opt-ing out of standard revalidation behaviors. ([#14542](https://github.com/remix-run/react-router/pull/14542))\n\n  If active routes include a `shouldRevalidate` function, then your value will be passed as `defaultShouldRevalidate` in those function so that the route always has the final revalidation determination.\n  - `<Form method=\"post\" unstable_defaultShouldRevalidate={false}>`\n  - `submit(data, { method: \"post\", unstable_defaultShouldRevalidate: false })`\n  - `<fetcher.Form method=\"post\" unstable_defaultShouldRevalidate={false}>`\n  - `fetcher.submit(data, { method: \"post\", unstable_defaultShouldRevalidate: false })`\n\n  This is also available on non-submission APIs that may trigger revalidations due to changing search params:\n  - `<Link to=\"/\" unstable_defaultShouldRevalidate={false}>`\n  - `navigate(\"/?foo=bar\", { unstable_defaultShouldRevalidate: false })`\n  - `setSearchParams(params, { unstable_defaultShouldRevalidate: false })`\n\n- Allow redirects to be returned from client side middleware ([#14598](https://github.com/remix-run/react-router/pull/14598))\n\n- Handle `dataStrategy` implementations that return insufficient result sets by adding errors for routes without any available result ([#14627](https://github.com/remix-run/react-router/pull/14627))\n\n## 7.10.1\n\n### Patch Changes\n\n- Update the `useOptimistic` stub we provide for React 18 users to use a stable setter function to avoid potential `useEffect` loops - specifically when using `<Link viewTransition>` ([#14628](https://github.com/remix-run/react-router/pull/14628))\n\n## 7.10.0\n\n### Minor Changes\n\n- Stabilize `fetcher.reset()` ([#14545](https://github.com/remix-run/react-router/pull/14545))\n  - ⚠️ This is a breaking change if you have begun using `fetcher.unstable_reset()`\n\n- Stabilize the `dataStrategy` `match.shouldRevalidateArgs`/`match.shouldCallHandler()` APIs. ([#14592](https://github.com/remix-run/react-router/pull/14592))\n  - The `match.shouldLoad` API is now marked deprecated in favor of these more powerful alternatives\n\n  - If you're using this API in a custom `dataStrategy` today, you can swap to the new API at your convenience:\n\n    ```tsx\n    // Before\n    const matchesToLoad = matches.filter((m) => m.shouldLoad);\n\n    // After\n    const matchesToLoad = matches.filter((m) => m.shouldCallHandler());\n    ```\n\n  - `match.shouldRevalidateArgs` is the argument that will be passed to the route `shouldRevaliate` function\n\n  - Combined with the parameter accepted by `match.shouldCallHandler`, you can define a custom revalidation behavior for your `dataStrategy`:\n\n  ```tsx\n  const matchesToLoad = matches.filter((m) => {\n    const defaultShouldRevalidate = customRevalidationBehavior(\n      match.shouldRevalidateArgs,\n    );\n    return m.shouldCallHandler(defaultShouldRevalidate);\n    // The argument here will override the internal `defaultShouldRevalidate` value\n  });\n  ```\n\n### Patch Changes\n\n- Fix a Framework Mode bug where the `defaultShouldRevalidate` parameter to `shouldRevalidate` would not be correct after `action` returned a 4xx/5xx response (`true` when it should have been `false`) ([#14592](https://github.com/remix-run/react-router/pull/14592))\n  - If your `shouldRevalidate` function relied on that parameter, you may have seen unintended revalidations\n\n- Fix `fetcher.submit` failing with plain objects containing a `tagName` property ([#14534](https://github.com/remix-run/react-router/pull/14534))\n\n- \\[UNSTABLE] Add `unstable_pattern` to the parameters for client side `unstable_onError`, refactor how it's called by `RouterProvider` to avoid potential strict mode issues ([#14573](https://github.com/remix-run/react-router/pull/14573))\n\n- Add new `unstable_useTransitions` flag to routers to give users control over the usage of [`React.startTransition`](https://react.dev/reference/react/startTransition) and [`React.useOptimistic`](https://react.dev/reference/react/useOptimistic). ([#14524](https://github.com/remix-run/react-router/pull/14524))\n  - Framework Mode + Data Mode:\n    - `<HydratedRouter unstable_transition>`/`<RouterProvider unstable_transition>`\n    - When left unset (current default behavior)\n      - Router state updates are wrapped in `React.startTransition`\n      - ⚠️ This can lead to buggy behaviors if you are wrapping your own navigations/fetchers in `React.startTransition`\n      - You should set the flag to `true` if you run into this scenario to get the enhanced `useOptimistic` behavior (requires React 19)\n    - When set to `true`\n      - Router state updates remain wrapped in `React.startTransition` (as they are without the flag)\n      - `Link`/`Form` navigations will be wrapped in `React.startTransition`\n      - A subset of router state info will be surfaced to the UI _during_ navigations via `React.useOptimistic` (i.e., `useNavigation()`, `useFetchers()`, etc.)\n        - ⚠️ This is a React 19 API so you must also be React 19 to opt into this flag for Framework/Data Mode\n    - When set to `false`\n      - The router will not leverage `React.startTransition` or `React.useOptimistic` on any navigations or state changes\n  - Declarative Mode\n    - `<BrowserRouter unstable_useTransitions>`\n    - When left unset\n      - Router state updates are wrapped in `React.startTransition`\n    - When set to `true`\n      - Router state updates remain wrapped in `React.startTransition` (as they are without the flag)\n      - `Link`/`Form` navigations will be wrapped in `React.startTransition`\n    - When set to `false`\n      - the router will not leverage `React.startTransition` on any navigations or state changes\n\n- Fix the promise returned from `useNavigate` in Framework/Data Mode so that it properly tracks the duration of `popstate` navigations (i.e., `navigate(-1)`) ([#14524](https://github.com/remix-run/react-router/pull/14524))\n\n- Fix internal type error in useRoute types that surfaces when skipLibCheck is disabled ([#14577](https://github.com/remix-run/react-router/pull/14577))\n\n- Preserve `statusText` on the `ErrorResponse` instance when throwing `data()` from a route handler ([#14555](https://github.com/remix-run/react-router/pull/14555))\n\n- Optimize href() to avoid backtracking regex on splat ([#14329](https://github.com/remix-run/react-router/pull/14329))\n\n## 7.9.6\n\n### Patch Changes\n\n- \\[UNSTABLE] Add `location`/`params` as arguments to client-side `unstable_onError` to permit enhanced error reporting. ([#14509](https://github.com/remix-run/react-router/pull/14509))\n\n  ⚠️ This is a breaking change if you've already adopted `unstable_onError`. The second `errorInfo` parameter is now an object with `location` and `params`:\n\n  ```tsx\n  // Before\n  function errorHandler(error: unknown, errorInfo?: React.errorInfo) {\n    /*...*/\n  }\n\n  // After\n  function errorHandler(\n    error: unknown,\n    info: {\n      location: Location;\n      params: Params;\n      errorInfo?: React.ErrorInfo;\n    },\n  ) {\n    /*...*/\n  }\n  ```\n\n- Properly handle ancestor thrown middleware errors before `next()` on fetcher submissions ([#14517](https://github.com/remix-run/react-router/pull/14517))\n\n- Fix issue with splat routes interfering with multiple calls to patchRoutesOnNavigation ([#14487](https://github.com/remix-run/react-router/pull/14487))\n\n- Normalize double-slashes in `resolvePath` ([#14529](https://github.com/remix-run/react-router/pull/14529))\n\n## 7.9.5\n\n### Patch Changes\n\n- Move RSCHydratedRouter and utils to `/dom` export. ([#14457](https://github.com/remix-run/react-router/pull/14457))\n\n- useRoute: return type-safe `handle` ([#14462](https://github.com/remix-run/react-router/pull/14462))\n\n  For example:\n\n  ```ts\n  // app/routes/admin.tsx\n  const handle = { hello: \"world\" };\n  ```\n\n  ```ts\n  // app/routes/some-other-route.tsx\n  export default function Component() {\n    const admin = useRoute(\"routes/admin\");\n    if (!admin) throw new Error(\"Not nested within 'routes/admin'\");\n    console.log(admin.handle);\n    //                ^? { hello: string }\n  }\n  ```\n\n- Ensure action handlers run for routes with middleware even if no loader is present ([#14443](https://github.com/remix-run/react-router/pull/14443))\n\n- Add `unstable_instrumentations` API to allow users to add observablity to their apps by instrumenting route loaders, actions, middlewares, lazy, as well as server-side request handlers and client side navigations/fetches ([#14412](https://github.com/remix-run/react-router/pull/14412))\n  - Framework Mode:\n    - `entry.server.tsx`: `export const unstable_instrumentations = [...]`\n    - `entry.client.tsx`: `<HydratedRouter unstable_instrumentations={[...]} />`\n  - Data Mode\n    - `createBrowserRouter(routes, { unstable_instrumentations: [...] })`\n\n  This also adds a new `unstable_pattern` parameter to loaders/actions/middleware which contains the un-interpolated route pattern (i.e., `/blog/:slug`) which is useful for aggregating performance metrics by route\n\n## 7.9.4\n\n### Patch Changes\n\n- handle external redirects in from server actions ([#14400](https://github.com/remix-run/react-router/pull/14400))\n- New (unstable) `useRoute` hook for accessing data from specific routes ([#14407](https://github.com/remix-run/react-router/pull/14407))\n\n  For example, let's say you have an `admin` route somewhere in your app and you want any child routes of `admin` to all have access to the `loaderData` and `actionData` from `admin.`\n\n  ```tsx\n  // app/routes/admin.tsx\n  import { Outlet } from \"react-router\";\n\n  export const loader = () => ({ message: \"Hello, loader!\" });\n\n  export const action = () => ({ count: 1 });\n\n  export default function Component() {\n    return (\n      <div>\n        {/* ... */}\n        <Outlet />\n        {/* ... */}\n      </div>\n    );\n  }\n  ```\n\n  You might even want to create a reusable widget that all of the routes nested under `admin` could use:\n\n  ```tsx\n  import { unstable_useRoute as useRoute } from \"react-router\";\n\n  export function AdminWidget() {\n    // How to get `message` and `count` from `admin` route?\n  }\n  ```\n\n  In framework mode, `useRoute` knows all your app's routes and gives you TS errors when invalid route IDs are passed in:\n\n  ```tsx\n  export function AdminWidget() {\n    const admin = useRoute(\"routes/dmin\");\n    //                      ^^^^^^^^^^^\n  }\n  ```\n\n  `useRoute` returns `undefined` if the route is not part of the current page:\n\n  ```tsx\n  export function AdminWidget() {\n    const admin = useRoute(\"routes/admin\");\n    if (!admin) {\n      throw new Error(`AdminWidget used outside of \"routes/admin\"`);\n    }\n  }\n  ```\n\n  Note: the `root` route is the exception since it is guaranteed to be part of the current page.\n  As a result, `useRoute` never returns `undefined` for `root`.\n\n  `loaderData` and `actionData` are marked as optional since they could be accessed before the `action` is triggered or after the `loader` threw an error:\n\n  ```tsx\n  export function AdminWidget() {\n    const admin = useRoute(\"routes/admin\");\n    if (!admin) {\n      throw new Error(`AdminWidget used outside of \"routes/admin\"`);\n    }\n    const { loaderData, actionData } = admin;\n    console.log(loaderData);\n    //          ^? { message: string } | undefined\n    console.log(actionData);\n    //          ^? { count: number } | undefined\n  }\n  ```\n\n  If instead of a specific route, you wanted access to the _current_ route's `loaderData` and `actionData`, you can call `useRoute` without arguments:\n\n  ```tsx\n  export function AdminWidget() {\n    const currentRoute = useRoute();\n    currentRoute.loaderData;\n    currentRoute.actionData;\n  }\n  ```\n\n  This usage is equivalent to calling `useLoaderData` and `useActionData`, but consolidates all route data access into one hook: `useRoute`.\n\n  Note: when calling `useRoute()` (without a route ID), TS has no way to know which route is the current route.\n  As a result, `loaderData` and `actionData` are typed as `unknown`.\n  If you want more type-safety, you can either narrow the type yourself with something like `zod` or you can refactor your app to pass down typed props to your `AdminWidget`:\n\n  ```tsx\n  export function AdminWidget({\n    message,\n    count,\n  }: {\n    message: string;\n    count: number;\n  }) {\n    /* ... */\n  }\n  ```\n\n## 7.9.3\n\n### Patch Changes\n\n- Do not try to use `turbo-stream` to decode CDN errors that never reached the server ([#14385](https://github.com/remix-run/react-router/pull/14385))\n  - We used to do this but lost this check with the adoption of single fetch\n\n- Fix Data Mode regression causing a 404 during initial load in when `middleware` exists without any `loader` functions ([#14393](https://github.com/remix-run/react-router/pull/14393))\n\n## 7.9.2\n\n### Patch Changes\n\n- - Update client-side router to run client `middleware` on initial load even if no loaders exist ([#14348](https://github.com/remix-run/react-router/pull/14348))\n  - Update `createRoutesStub` to run route middleware\n    - You will need to set the `<RoutesStub future={{ v8_middleware: true }} />` flag to enable the proper `context` type\n\n- Update Lazy Route Discovery manifest requests to use a singular comma-separated `paths` query param instead of repeated `p` query params ([#14321](https://github.com/remix-run/react-router/pull/14321))\n  - This is because Cloudflare has a hard limit of 100 URL search param key/value pairs when used as a key for caching purposes\n  - If more that 100 paths were included, the cache key would be incomplete and could produce false-positive cache hits\n\n- \\[UNSTABLE] Add `fetcher.unstable_reset()` API ([#14206](https://github.com/remix-run/react-router/pull/14206))\n\n- Made useOutlet element reference have stable identity in-between route chages ([#13382](https://github.com/remix-run/react-router/pull/13382))\n\n- feat: enable full transition support for the rsc router ([#14362](https://github.com/remix-run/react-router/pull/14362))\n\n- In RSC Data Mode, handle SSR'd client errors and re-try in the browser ([#14342](https://github.com/remix-run/react-router/pull/14342))\n\n- Support `middleware` prop on `<Route>` for usage with a data router via `createRoutesFromElements` ([#14357](https://github.com/remix-run/react-router/pull/14357))\n\n- Handle encoded question mark and hash characters in ancestor splat routes ([#14249](https://github.com/remix-run/react-router/pull/14249))\n\n- Fail gracefully on manifest version mismatch logic if `sessionStorage` access is blocked ([#14335](https://github.com/remix-run/react-router/pull/14335))\n\n## 7.9.1\n\n### Patch Changes\n\n- Fix internal `Future` interface naming from `middleware` -> `v8_middleware` ([#14327](https://github.com/remix-run/react-router/pull/14327))\n\n## 7.9.0\n\n### Minor Changes\n\n- Stabilize middleware and context APIs. ([#14215](https://github.com/remix-run/react-router/pull/14215))\n\n  We have removed the `unstable_` prefix from the following APIs and they are now considered stable and ready for production use:\n  - [`RouterContextProvider`](https://reactrouter.com/api/utils/RouterContextProvider)\n  - [`createContext`](https://reactrouter.com/api/utils/createContext)\n  - `createBrowserRouter` [`getContext`](https://reactrouter.com/api/data-routers/createBrowserRouter#optsgetcontext) option\n  - `<HydratedRouter>` [`getContext`](https://reactrouter.com/api/framework-routers/HydratedRouter#getcontext) prop\n\n  Please see the [Middleware Docs](https://reactrouter.com/how-to/middleware), the [Middleware RFC](https://github.com/remix-run/remix/discussions/7642), and the [Client-side Context RFC](https://github.com/remix-run/react-router/discussions/9856) for more information.\n\n### Patch Changes\n\n- Escape HTML in `meta()` JSON-LD content ([#14316](https://github.com/remix-run/react-router/pull/14316))\n- Add react-server Await component implementation ([#14261](https://github.com/remix-run/react-router/pull/14261))\n- In RSC Data Mode when using a custom basename, fix hydration errors for routes that only have client loaders ([#14264](https://github.com/remix-run/react-router/pull/14264))\n- Make `href` function available in a react-server context ([#14262](https://github.com/remix-run/react-router/pull/14262))\n- decode each time `getPayload()` is called to allow for \"in-context\" decoding and hoisting of contextual assets ([#14248](https://github.com/remix-run/react-router/pull/14248))\n- `href()` now correctly processes routes that have an extension after the parameter or are a single optional parameter. ([#13797](https://github.com/remix-run/react-router/pull/13797))\n\n## 7.8.2\n\n### Patch Changes\n\n- \\[UNSTABLE] Remove Data Mode `future.unstable_middleware` flag from `createBrowserRouter` ([#14213](https://github.com/remix-run/react-router/pull/14213))\n  - This is only needed as a Framework Mode flag because of the route modules and the `getLoadContext` type behavior change\n  - In Data Mode, it's an opt-in feature because it's just a new property on a route object, so there's no behavior changes that necessitate a flag\n\n- \\[UNSTABLE] Add `<RouterProvider unstable_onError>`/`<HydratedRouter unstable_onError>` prop for client side error reporting ([#14162](https://github.com/remix-run/react-router/pull/14162))\n\n- server action revalidation opt out via $SKIP_REVALIDATION field ([#14154](https://github.com/remix-run/react-router/pull/14154))\n\n- Properly escape interpolated param values in `generatePath()` ([#13530](https://github.com/remix-run/react-router/pull/13530))\n\n- Maintain `ReadonlyMap` and `ReadonlySet` types in server response data. ([#13092](https://github.com/remix-run/react-router/pull/13092))\n\n- \\[UNSTABLE] Delay serialization of `.data` redirects to 202 responses until after middleware chain ([#14205](https://github.com/remix-run/react-router/pull/14205))\n\n- Fix `TypeError` if you throw from `patchRoutesOnNavigation` when no partial matches exist ([#14198](https://github.com/remix-run/react-router/pull/14198))\n\n- Fix `basename` usage without a leading slash in data routers ([#11671](https://github.com/remix-run/react-router/pull/11671))\n\n- \\[UNSTABLE] Update client middleware so it returns the data strategy results allowing for more advanced post-processing middleware ([#14151](https://github.com/remix-run/react-router/pull/14151))\n\n## 7.8.1\n\n### Patch Changes\n\n- Fix usage of optional path segments in nested routes defined using absolute paths ([#14135](https://github.com/remix-run/react-router/pull/14135))\n- Bubble client pre-next middleware error to the shallowest ancestor that needs to load, not strictly the shallowest ancestor with a loader ([#14150](https://github.com/remix-run/react-router/pull/14150))\n- Fix optional static segment matching in `matchPath` ([#11813](https://github.com/remix-run/react-router/pull/11813))\n- Fix prerendering when a `basename` is set with `ssr:false` ([#13791](https://github.com/remix-run/react-router/pull/13791))\n- Provide `isRouteErrorResponse` utility in `react-server` environments ([#14166](https://github.com/remix-run/react-router/pull/14166))\n- Propagate non-redirect Responses thrown from middleware to the error boundary on document/data requests ([#14182](https://github.com/remix-run/react-router/pull/14182))\n- Handle `meta` and `links` Route Exports in RSC Data Mode ([#14136](https://github.com/remix-run/react-router/pull/14136))\n- Properly convert returned/thrown `data()` values to `Response` instances via `Response.json()` in resource routes and middleware ([#14159](https://github.com/remix-run/react-router/pull/14159), [#14181](https://github.com/remix-run/react-router/pull/14181))\n\n## 7.8.0\n\n### Minor Changes\n\n- Add `nonce` prop to `Links` & `PrefetchPageLinks` ([#14048](https://github.com/remix-run/react-router/pull/14048))\n- Add `loaderData` arguments/properties alongside existing `data` arguments/properties to provide consistency and clarity between `loaderData` and `actionData` across the board ([#14047](https://github.com/remix-run/react-router/pull/14047))\n  - Updated types: `Route.MetaArgs`, `Route.MetaMatch`, `MetaArgs`, `MetaMatch`, `Route.ComponentProps.matches`, `UIMatch`\n  - `@deprecated` warnings have been added to the existing `data` properties to point users to new `loaderData` properties, in preparation for removing the `data` properties in a future major release\n\n### Patch Changes\n\n- Prevent _\"Did not find corresponding fetcher result\"_ console error when navigating during a `fetcher.submit` revalidation ([#14114](https://github.com/remix-run/react-router/pull/14114))\n\n- Bubble client-side middleware errors prior to `next` to the appropriate ancestor error boundary ([#14138](https://github.com/remix-run/react-router/pull/14138))\n\n- Switch Lazy Route Discovery manifest URL generation to usea standalone `URLSearchParams` instance instead of `URL.searchParams` to avoid a major performance bottleneck in Chrome ([#14084](https://github.com/remix-run/react-router/pull/14084))\n\n- Adjust internal RSC usage of `React.use` to avoid Webpack compilation errors when using React 18 ([#14113](https://github.com/remix-run/react-router/pull/14113))\n\n- Remove dependency on `@types/node` in TypeScript declaration files ([#14059](https://github.com/remix-run/react-router/pull/14059))\n\n- Fix types for `UIMatch` to reflect that the `loaderData`/`data` properties may be `undefined` ([#12206](https://github.com/remix-run/react-router/pull/12206))\n  - When an `ErrorBoundary` is being rendered, not all active matches will have loader data available, since it may have been their `loader` that threw to trigger the boundary\n  - The `UIMatch.data` type was not correctly handing this and would always reflect the presence of data, leading to the unexpected runtime errors when an `ErrorBoundary` was rendered\n  - ⚠️ This may cause some type errors to show up in your code for unguarded `match.data` accesses - you should properly guard for `undefined` values in those scenarios.\n\n  ```tsx\n  // app/root.tsx\n  export function loader() {\n    someFunctionThatThrows(); // ❌ Throws an Error\n    return { title: \"My Title\" };\n  }\n\n  export function Layout({ children }: { children: React.ReactNode }) {\n    let matches = useMatches();\n    let rootMatch = matches[0] as UIMatch<Awaited<ReturnType<typeof loader>>>;\n    //  ^ rootMatch.data is incorrectly typed here, so TypeScript does not\n    //    complain if you do the following which throws an error at runtime:\n    let { title } = rootMatch.data; // 💥\n\n    return <html>...</html>;\n  }\n  ```\n\n- \\[UNSTABLE] Ensure resource route errors go through `handleError` w/middleware enabled ([#14078](https://github.com/remix-run/react-router/pull/14078))\n\n- \\[UNSTABLE] Propagate returned Response from server middleware if next wasn't called ([#14093](https://github.com/remix-run/react-router/pull/14093))\n\n- \\[UNSTABLE] Allow server middlewares to return `data()` values which will be converted into a `Response` ([#14093](https://github.com/remix-run/react-router/pull/14093))\n\n- \\[UNSTABLE] Update middleware error handling so that the `next` function never throws and instead handles any middleware errors at the proper `ErrorBoundary` and returns the `Response` up through the ancestor `next` function ([#14118](https://github.com/remix-run/react-router/pull/14118))\n\n- \\[UNSTABLE] When middleware is enabled, make the `context` parameter read-only (via `Readonly<unstable_RouterContextProvider>`) so that TypeScript will not allow you to write arbitrary fields to it in loaders, actions, or middleware. ([#14097](https://github.com/remix-run/react-router/pull/14097))\n\n- \\[UNSTABLE] Rename and alter the signature/functionality of the `unstable_respond` API in `staticHandler.query`/`staticHandler.queryRoute` ([#14103](https://github.com/remix-run/react-router/pull/14103))\n  - The API has been renamed to `unstable_generateMiddlewareResponse` for clarity\n  - The main functional change is that instead of running the loaders/actions before calling `unstable_respond` and handing you the result, we now pass a `query`/`queryRoute` function as a parameter and you execute the loaders/actions inside your callback, giving you full access to pre-processing and error handling\n  - The `query` version of the API now has a signature of `(query: (r: Request) => Promise<StaticHandlerContext | Response>) => Promise<Response>`\n  - The `queryRoute` version of the API now has a signature of `(queryRoute: (r: Request) => Promise<Response>) => Promise<Response>`\n  - This allows for more advanced usages such as running logic before/after calling `query` and direct error handling of errors thrown from query\n  - ⚠️ This is a breaking change if you've adopted the `staticHandler` `unstable_respond` API\n\n  ```tsx\n  let response = await staticHandler.query(request, {\n    requestContext: new unstable_RouterContextProvider(),\n    async unstable_generateMiddlewareResponse(query) {\n      try {\n        // At this point we've run middleware top-down so we need to call the\n        // handlers and generate the Response to bubble back up the middleware\n        let result = await query(request);\n        if (isResponse(result)) {\n          return result; // Redirects, etc.\n        }\n        return await generateHtmlResponse(result);\n      } catch (error: unknown) {\n        return generateErrorResponse(error);\n      }\n    },\n  });\n  ```\n\n- \\[UNSTABLE] Convert internal middleware implementations to use the new `unstable_generateMiddlewareResponse` API ([#14103](https://github.com/remix-run/react-router/pull/14103))\n\n- \\[UNSTABLE] Change `getLoadContext` signature (`type GetLoadContextFunction`) when `future.unstable_middleware` is enabled so that it returns an `unstable_RouterContextProvider` instance instead of a `Map` used to contruct the instance internally ([#14097](https://github.com/remix-run/react-router/pull/14097))\n  - This also removes the `type unstable_InitialContext` export\n  - ⚠️ This is a breaking change if you have adopted middleware and are using a custom server with a `getLoadContext` function\n\n- \\[UNSTABLE] Run client middleware on client navigations even if no loaders exist ([#14106](https://github.com/remix-run/react-router/pull/14106))\n\n- \\[UNSTABLE] Change the `unstable_getContext` signature on `RouterProvider`/`HydratedRouter`/`unstable_RSCHydratedRouter` so that it returns an `unstable_RouterContextProvider` instance instead of a `Map` used to contruct the instance internally ([#14097](https://github.com/remix-run/react-router/pull/14097))\n  - ⚠️ This is a breaking change if you have adopted the `unstable_getContext` prop\n\n- \\[UNSTABLE] proxy server action side-effect redirects from actions for document and callServer requests ([#14131](https://github.com/remix-run/react-router/pull/14131))\n\n- \\[UNSTABLE] Fix RSC Data Mode issue where routes that return `false` from `shouldRevalidate` would be replaced by an `<Outlet />` ([#14071](https://github.com/remix-run/react-router/pull/14071))\n\n## 7.7.1\n\n### Patch Changes\n\n- In RSC Data Mode, fix bug where routes with errors weren't forced to revalidate when `shouldRevalidate` returned false ([#14026](https://github.com/remix-run/react-router/pull/14026))\n- In RSC Data Mode, fix `Matched leaf route at location \"/...\" does not have an element or Component` warnings when error boundaries are rendered. ([#14021](https://github.com/remix-run/react-router/pull/14021))\n\n## 7.7.0\n\n### Minor Changes\n\n- Add unstable RSC support ([#13700](https://github.com/remix-run/react-router/pull/13700))\n\n  For more information, see the [RSC documentation](https://reactrouter.com/start/rsc/installation).\n\n### Patch Changes\n\n- Handle `InvalidCharacterError` when validating cookie signature ([#13847](https://github.com/remix-run/react-router/pull/13847))\n\n- Pass a copy of `searchParams` to the `setSearchParams` callback function to avoid muations of the internal `searchParams` instance. This was an issue when navigations were blocked because the internal instance be out of sync with `useLocation().search`. ([#12784](https://github.com/remix-run/react-router/pull/12784))\n\n- Support invalid `Date` in `turbo-stream` v2 fork ([#13684](https://github.com/remix-run/react-router/pull/13684))\n\n- In Framework Mode, clear critical CSS in development after initial render ([#13872](https://github.com/remix-run/react-router/pull/13872))\n\n- Strip search parameters from `patchRoutesOnNavigation` `path` param for fetcher calls ([#13911](https://github.com/remix-run/react-router/pull/13911))\n\n- Skip scroll restoration on useRevalidator() calls because they're not new locations ([#13671](https://github.com/remix-run/react-router/pull/13671))\n\n- Support unencoded UTF-8 routes in prerender config with `ssr` set to `false` ([#13699](https://github.com/remix-run/react-router/pull/13699))\n\n- Do not throw if the url hash is not a valid URI component ([#13247](https://github.com/remix-run/react-router/pull/13247))\n\n- Fix a regression in `createRoutesStub` introduced with the middleware feature. ([#13946](https://github.com/remix-run/react-router/pull/13946))\n\n  As part of that work we altered the signature to align with the new middleware APIs without making it backwards compatible with the prior `AppLoadContext` API. This permitted `createRoutesStub` to work if you were opting into middleware and the updated `context` typings, but broke `createRoutesStub` for users not yet opting into middleware.\n\n  We've reverted this change and re-implemented it in such a way that both sets of users can leverage it.\n\n  ```tsx\n  // If you have not opted into middleware, the old API should work again\n  let context: AppLoadContext = {\n    /*...*/\n  };\n  let Stub = createRoutesStub(routes, context);\n\n  // If you have opted into middleware, you should now pass an instantiated `unstable_routerContextProvider` instead of a `getContext` factory function.\n  let context = new unstable_RouterContextProvider();\n  context.set(SomeContext, someValue);\n  let Stub = createRoutesStub(routes, context);\n  ```\n\n  ⚠️ This may be a breaking bug for if you have adopted the unstable Middleware feature and are using `createRoutesStub` with the updated API.\n\n- Remove `Content-Length` header from Single Fetch responses ([#13902](https://github.com/remix-run/react-router/pull/13902))\n\n## 7.6.3\n\n### Patch Changes\n\n- Do not serialize types for `useRouteLoaderData<typeof clientLoader>` ([#13752](https://github.com/remix-run/react-router/pull/13752))\n\n  For types to distinguish a `clientLoader` from a `serverLoader`, you MUST annotate `clientLoader` args:\n\n  ```ts\n  //                                   👇 annotation required to skip serializing types\n  export function clientLoader({}: Route.ClientLoaderArgs) {\n    return { fn: () => \"earth\" };\n  }\n\n  function SomeComponent() {\n    const data = useRouteLoaderData<typeof clientLoader>(\"routes/this-route\");\n    const planet = data?.fn() ?? \"world\";\n    return <h1>Hello, {planet}!</h1>;\n  }\n  ```\n\n## 7.6.2\n\n### Patch Changes\n\n- Avoid additional `with-props` chunk in Framework Mode by moving route module component prop logic from the Vite plugin to `react-router` ([#13650](https://github.com/remix-run/react-router/pull/13650))\n- Slight refactor of internal `headers()` function processing for use with RSC ([#13639](https://github.com/remix-run/react-router/pull/13639))\n\n## 7.6.1\n\n### Patch Changes\n\n- Update `Route.MetaArgs` to reflect that `data` can be potentially `undefined` ([#13563](https://github.com/remix-run/react-router/pull/13563))\n\n  This is primarily for cases where a route `loader` threw an error to it's own `ErrorBoundary`. but it also arises in the case of a 404 which renders the root `ErrorBoundary`/`meta` but the root loader did not run because not routes matched.\n\n- Partially revert optimization added in `7.1.4` to reduce calls to `matchRoutes` because it surfaced other issues ([#13562](https://github.com/remix-run/react-router/pull/13562))\n\n- Fix typegen when same route is used at multiple paths ([#13574](https://github.com/remix-run/react-router/pull/13574))\n\n  For example, `routes/route.tsx` is used at 4 different paths here:\n\n  ```ts\n  import { type RouteConfig, route } from \"@react-router/dev/routes\";\n  export default [\n    route(\"base/:base\", \"routes/base.tsx\", [\n      route(\"home/:home\", \"routes/route.tsx\", { id: \"home\" }),\n      route(\"changelog/:changelog\", \"routes/route.tsx\", { id: \"changelog\" }),\n      route(\"splat/*\", \"routes/route.tsx\", { id: \"splat\" }),\n    ]),\n    route(\"other/:other\", \"routes/route.tsx\", { id: \"other\" }),\n  ] satisfies RouteConfig;\n  ```\n\n  Previously, typegen would arbitrarily pick one of these paths to be the \"winner\" and generate types for the route module based on that path.\n  Now, typegen creates unions as necessary for alternate paths for the same route file.\n\n- Better types for `params` ([#13543](https://github.com/remix-run/react-router/pull/13543))\n\n  For example:\n\n  ```ts\n  // routes.ts\n  import { type RouteConfig, route } from \"@react-router/dev/routes\";\n\n  export default [\n    route(\"parent/:p\", \"routes/parent.tsx\", [\n      route(\"layout/:l\", \"routes/layout.tsx\", [\n        route(\"child1/:c1a/:c1b\", \"routes/child1.tsx\"),\n        route(\"child2/:c2a/:c2b\", \"routes/child2.tsx\"),\n      ]),\n    ]),\n  ] satisfies RouteConfig;\n  ```\n\n  Previously, `params` for the `routes/layout.tsx` route were calculated as `{ p: string, l: string }`.\n  This incorrectly ignores params that could come from child routes.\n  If visiting `/parent/1/layout/2/child1/3/4`, the actual params passed to `routes/layout.tsx` will have a type of `{ p: string, l: string, c1a: string, c1b: string }`.\n\n  Now, `params` are aware of child routes and autocompletion will include child params as optionals:\n\n  ```ts\n  params.|\n  //     ^ cursor is here and you ask for autocompletion\n  // p: string\n  // l: string\n  // c1a?: string\n  // c1b?: string\n  // c2a?: string\n  // c2b?: string\n  ```\n\n  You can also narrow the types for `params` as it is implemented as a normalized union of params for each page that includes `routes/layout.tsx`:\n\n  ```ts\n  if (typeof params.c1a === 'string') {\n    params.|\n    //     ^ cursor is here and you ask for autocompletion\n    // p: string\n    // l: string\n    // c1a: string\n    // c1b: string\n  }\n  ```\n\n  ***\n\n  UNSTABLE: renamed internal `react-router/route-module` export to `react-router/internal`\n  UNSTABLE: removed `Info` export from generated `+types/*` files\n\n- Avoid initial fetcher execution 404 error when Lazy Route Discovery is interrupted by a navigation ([#13564](https://github.com/remix-run/react-router/pull/13564))\n\n- href replaces splats `*` ([#13593](https://github.com/remix-run/react-router/pull/13593))\n\n  ```ts\n  const a = href(\"/products/*\", { \"*\": \"/1/edit\" });\n  // -> /products/1/edit\n  ```\n\n## 7.6.0\n\n### Minor Changes\n\n- Added a new `react-router.config.ts` `routeDiscovery` option to configure Lazy Route Discovery behavior. ([#13451](https://github.com/remix-run/react-router/pull/13451))\n  - By default, Lazy Route Discovery is enabled and makes manifest requests to the `/__manifest` path:\n    - `routeDiscovery: { mode: \"lazy\", manifestPath: \"/__manifest\" }`\n  - You can modify the manifest path used:\n    - `routeDiscovery: { mode: \"lazy\", manifestPath: \"/custom-manifest\" }`\n  - Or you can disable this feature entirely and include all routes in the manifest on initial document load:\n    - `routeDiscovery: { mode: \"initial\" }`\n\n- Add support for route component props in `createRoutesStub`. This allows you to unit test your route components using the props instead of the hooks: ([#13528](https://github.com/remix-run/react-router/pull/13528))\n\n  ```tsx\n  let RoutesStub = createRoutesStub([\n    {\n      path: \"/\",\n      Component({ loaderData }) {\n        let data = loaderData as { message: string };\n        return <pre data-testid=\"data\">Message: {data.message}</pre>;\n      },\n      loader() {\n        return { message: \"hello\" };\n      },\n    },\n  ]);\n\n  render(<RoutesStub />);\n\n  await waitFor(() => screen.findByText(\"Message: hello\"));\n  ```\n\n### Patch Changes\n\n- Fix `react-router` module augmentation for `NodeNext` ([#13498](https://github.com/remix-run/react-router/pull/13498))\n\n- Don't bundle `react-router` in `react-router/dom` CJS export ([#13497](https://github.com/remix-run/react-router/pull/13497))\n\n- Fix bug where a submitting `fetcher` would get stuck in a `loading` state if a revalidating `loader` redirected ([#12873](https://github.com/remix-run/react-router/pull/12873))\n\n- Fix hydration error if a server `loader` returned `undefined` ([#13496](https://github.com/remix-run/react-router/pull/13496))\n\n- Fix initial load 404 scenarios in data mode ([#13500](https://github.com/remix-run/react-router/pull/13500))\n\n- Stabilize `useRevalidator`'s `revalidate` function ([#13542](https://github.com/remix-run/react-router/pull/13542))\n\n- Preserve status code if a `clientAction` throws a `data()` result in framework mode ([#13522](https://github.com/remix-run/react-router/pull/13522))\n\n- Be defensive against leading double slashes in paths to avoid `Invalid URL` errors from the URL constructor ([#13510](https://github.com/remix-run/react-router/pull/13510))\n  - Note we do not sanitize/normalize these paths - we only detect them so we can avoid the error that would be thrown by `new URL(\"//\", window.location.origin)`\n\n- Remove `Navigator` declaration for `navigator.connection.saveData` to avoid messing with any other types beyond `saveData` in userland ([#13512](https://github.com/remix-run/react-router/pull/13512))\n\n- Fix `handleError` `params` values on `.data` requests for routes with a dynamic param as the last URL segment ([#13481](https://github.com/remix-run/react-router/pull/13481))\n\n- Don't trigger an `ErrorBoundary` UI before the reload when we detect a manifest verison mismatch in Lazy Route Discovery ([#13480](https://github.com/remix-run/react-router/pull/13480))\n\n- Inline `turbo-stream@2.4.1` dependency and fix decoding ordering of Map/Set instances ([#13518](https://github.com/remix-run/react-router/pull/13518))\n\n- Only render dev warnings in DEV mode ([#13461](https://github.com/remix-run/react-router/pull/13461))\n\n- UNSTABLE: Fix a few bugs with error bubbling in middleware use-cases ([#13538](https://github.com/remix-run/react-router/pull/13538))\n\n- Short circuit post-processing on aborted `dataStrategy` requests ([#13521](https://github.com/remix-run/react-router/pull/13521))\n  - This resolves non-user-facing console errors of the form `Cannot read properties of undefined (reading 'result')`\n\n## 7.5.3\n\n### Patch Changes\n\n- Fix bug where bubbled action errors would result in `loaderData` being cleared at the handling `ErrorBoundary` route ([#13476](https://github.com/remix-run/react-router/pull/13476))\n- Handle redirects from `clientLoader.hydrate` initial load executions ([#13477](https://github.com/remix-run/react-router/pull/13477))\n\n## 7.5.2\n\n### Patch Changes\n\n- Update Single Fetch to also handle the 204 redirects used in `?_data` requests in Remix v2 ([#13364](https://github.com/remix-run/react-router/pull/13364))\n  - This allows applications to return a redirect on `.data` requests from outside the scope of React Router (i.e., an `express`/`hono` middleware)\n  - ⚠️ Please note that doing so relies on implementation details that are subject to change without a SemVer major release\n  - This is primarily done to ease upgrading to Single Fetch for existing Remix v2 applications, but the recommended way to handle this is redirecting from a route middleware\n\n- Adjust approach for Prerendering/SPA Mode via headers ([#13453](https://github.com/remix-run/react-router/pull/13453))\n\n## 7.5.1\n\n### Patch Changes\n\n- Fix single fetch bug where no revalidation request would be made when navigating upwards to a reused parent route ([#13253](https://github.com/remix-run/react-router/pull/13253))\n\n- When using the object-based `route.lazy` API, the `HydrateFallback` and `hydrateFallbackElement` properties are now skipped when lazy loading routes after hydration. ([#13376](https://github.com/remix-run/react-router/pull/13376))\n\n  If you move the code for these properties into a separate file, you can use this optimization to avoid downloading unused hydration code. For example:\n\n  ```ts\n  createBrowserRouter([\n    {\n      path: \"/show/:showId\",\n      lazy: {\n        loader: async () => (await import(\"./show.loader.js\")).loader,\n        Component: async () => (await import(\"./show.component.js\")).Component,\n        HydrateFallback: async () =>\n          (await import(\"./show.hydrate-fallback.js\")).HydrateFallback,\n      },\n    },\n  ]);\n  ```\n\n- Properly revalidate prerendered paths when param values change ([#13380](https://github.com/remix-run/react-router/pull/13380))\n\n- UNSTABLE: Add a new `unstable_runClientMiddleware` argument to `dataStrategy` to enable middleware execution in custom `dataStrategy` implementations ([#13395](https://github.com/remix-run/react-router/pull/13395))\n\n- UNSTABLE: Add better error messaging when `getLoadContext` is not updated to return a `Map`\" ([#13242](https://github.com/remix-run/react-router/pull/13242))\n\n- Do not automatically add `null` to `staticHandler.query()` `context.loaderData` if routes do not have loaders ([#13223](https://github.com/remix-run/react-router/pull/13223))\n  - This was a Remix v2 implementation detail inadvertently left in for React Router v7\n  - Now that we allow returning `undefined` from loaders, our prior check of `loaderData[routeId] !== undefined` was no longer sufficient and was changed to a `routeId in loaderData` check - these `null` values can cause issues for this new check\n  - ⚠️ This could be a \"breaking bug fix\" for you if you are doing manual SSR with `createStaticHandler()`/`<StaticRouterProvider>`, and using `context.loaderData` to control `<RouterProvider>` hydration behavior on the client\n\n- Fix prerendering when a loader returns a redirect ([#13365](https://github.com/remix-run/react-router/pull/13365))\n\n- UNSTABLE: Update context type for `LoaderFunctionArgs`/`ActionFunctionArgs` when middleware is enabled ([#13381](https://github.com/remix-run/react-router/pull/13381))\n\n- Add support for the new `unstable_shouldCallHandler`/`unstable_shouldRevalidateArgs` APIs in `dataStrategy` ([#13253](https://github.com/remix-run/react-router/pull/13253))\n\n## 7.5.0\n\n### Minor Changes\n\n- Add granular object-based API for `route.lazy` to support lazy loading of individual route properties, for example: ([#13294](https://github.com/remix-run/react-router/pull/13294))\n\n  ```ts\n  createBrowserRouter([\n    {\n      path: \"/show/:showId\",\n      lazy: {\n        loader: async () => (await import(\"./show.loader.js\")).loader,\n        action: async () => (await import(\"./show.action.js\")).action,\n        Component: async () => (await import(\"./show.component.js\")).Component,\n      },\n    },\n  ]);\n  ```\n\n  **Breaking change for `route.unstable_lazyMiddleware` consumers**\n\n  The `route.unstable_lazyMiddleware` property is no longer supported. If you want to lazily load middleware, you must use the new object-based `route.lazy` API with `route.lazy.unstable_middleware`, for example:\n\n  ```ts\n  createBrowserRouter([\n    {\n      path: \"/show/:showId\",\n      lazy: {\n        unstable_middleware: async () =>\n          (await import(\"./show.middleware.js\")).middleware,\n        // etc.\n      },\n    },\n  ]);\n  ```\n\n### Patch Changes\n\n- Introduce `unstable_subResourceIntegrity` future flag that enables generation of an importmap with integrity for the scripts that will be loaded by the browser. ([#13163](https://github.com/remix-run/react-router/pull/13163))\n\n## 7.4.1\n\n### Patch Changes\n\n- Fix types on `unstable_MiddlewareFunction` to avoid type errors when a middleware doesn't return a value ([#13311](https://github.com/remix-run/react-router/pull/13311))\n- Dedupe calls to `route.lazy` functions ([#13260](https://github.com/remix-run/react-router/pull/13260))\n- Add support for `route.unstable_lazyMiddleware` function to allow lazy loading of middleware logic. ([#13210](https://github.com/remix-run/react-router/pull/13210))\n\n  **Breaking change for `unstable_middleware` consumers**\n\n  The `route.unstable_middleware` property is no longer supported in the return value from `route.lazy`. If you want to lazily load middleware, you must use `route.unstable_lazyMiddleware`.\n\n## 7.4.0\n\n### Patch Changes\n\n- Fix root loader data on initial load redirects in SPA mode ([#13222](https://github.com/remix-run/react-router/pull/13222))\n- Load ancestor pathless/index routes in lazy route discovery for upwards non-eager-discoery routing ([#13203](https://github.com/remix-run/react-router/pull/13203))\n- Fix `shouldRevalidate` behavior for `clientLoader`-only routes in `ssr:true` apps ([#13221](https://github.com/remix-run/react-router/pull/13221))\n- UNSTABLE: Fix `RequestHandler` `loadContext` parameter type when middleware is enabled ([#13204](https://github.com/remix-run/react-router/pull/13204))\n- UNSTABLE: Update `Route.unstable_MiddlewareFunction` to have a return value of `Response | undefined` instead of `Response | void` becaue you should not return anything if you aren't returning the `Response` ([#13199](https://github.com/remix-run/react-router/pull/13199))\n- UNSTABLE(BREAKING): If a middleware throws an error, ensure we only bubble the error itself via `next()` and are no longer leaking the `MiddlewareError` implementation detail ([#13180](https://github.com/remix-run/react-router/pull/13180))\n\n## 7.3.0\n\n### Minor Changes\n\n- Add `fetcherKey` as a parameter to `patchRoutesOnNavigation` ([#13061](https://github.com/remix-run/react-router/pull/13061))\n  - In framework mode, Lazy Route Discovery will now detect manifest version mismatches after a new deploy\n  - On navigations to undiscovered routes, this mismatch will trigger a document reload of the destination path\n  - On `fetcher` calls to undiscovered routes, this mismatch will trigger a document reload of the current path\n\n### Patch Changes\n\n- Skip resource route flow in dev server in SPA mode ([#13113](https://github.com/remix-run/react-router/pull/13113))\n\n- Support middleware on routes (unstable) ([#12941](https://github.com/remix-run/react-router/pull/12941))\n\n  Middleware is implemented behind a `future.unstable_middleware` flag. To enable, you must enable the flag and the types in your `react-router-config.ts` file:\n\n  ```ts\n  import type { Config } from \"@react-router/dev/config\";\n  import type { Future } from \"react-router\";\n\n  declare module \"react-router\" {\n    interface Future {\n      unstable_middleware: true; // 👈 Enable middleware types\n    }\n  }\n\n  export default {\n    future: {\n      unstable_middleware: true, // 👈 Enable middleware\n    },\n  } satisfies Config;\n  ```\n\n  ⚠️ Middleware is unstable and should not be adopted in production. There is at least one known de-optimization in route module loading for `clientMiddleware` that we will be addressing this before a stable release.\n\n  ⚠️ Enabling middleware contains a breaking change to the `context` parameter passed to your `loader`/`action` functions - see below for more information.\n\n  Once enabled, routes can define an array of middleware functions that will run sequentially before route handlers run. These functions accept the same parameters as `loader`/`action` plus an additional `next` parameter to run the remaining data pipeline. This allows middlewares to perform logic before and after handlers execute.\n\n  ```tsx\n  // Framework mode\n  export const unstable_middleware = [serverLogger, serverAuth]; // server\n  export const unstable_clientMiddleware = [clientLogger]; // client\n\n  // Library mode\n  const routes = [\n    {\n      path: \"/\",\n      // Middlewares are client-side for library mode SPA's\n      unstable_middleware: [clientLogger, clientAuth],\n      loader: rootLoader,\n      Component: Root,\n    },\n  ];\n  ```\n\n  Here's a simple example of a client-side logging middleware that can be placed on the root route:\n\n  ```tsx\n  const clientLogger: Route.unstable_ClientMiddlewareFunction = async (\n    { request },\n    next,\n  ) => {\n    let start = performance.now();\n\n    // Run the remaining middlewares and all route loaders\n    await next();\n\n    let duration = performance.now() - start;\n    console.log(`Navigated to ${request.url} (${duration}ms)`);\n  };\n  ```\n\n  Note that in the above example, the `next`/`middleware` functions don't return anything. This is by design as on the client there is no \"response\" to send over the network like there would be for middlewares running on the server. The data is all handled behind the scenes by the stateful `router`.\n\n  For a server-side middleware, the `next` function will return the HTTP `Response` that React Router will be sending across the wire, thus giving you a chance to make changes as needed. You may throw a new response to short circuit and respond immediately, or you may return a new or altered response to override the default returned by `next()`.\n\n  ```tsx\n  const serverLogger: Route.unstable_MiddlewareFunction = async (\n    { request, params, context },\n    next,\n  ) => {\n    let start = performance.now();\n\n    // 👇 Grab the response here\n    let res = await next();\n\n    let duration = performance.now() - start;\n    console.log(`Navigated to ${request.url} (${duration}ms)`);\n\n    // 👇 And return it here (optional if you don't modify the response)\n    return res;\n  };\n  ```\n\n  You can throw a `redirect` from a middleware to short circuit any remaining processing:\n\n  ```tsx\n  import { sessionContext } from \"../context\";\n  const serverAuth: Route.unstable_MiddlewareFunction = (\n    { request, params, context },\n    next,\n  ) => {\n    let session = context.get(sessionContext);\n    let user = session.get(\"user\");\n    if (!user) {\n      session.set(\"returnTo\", request.url);\n      throw redirect(\"/login\", 302);\n    }\n  };\n  ```\n\n  _Note that in cases like this where you don't need to do any post-processing you don't need to call the `next` function or return a `Response`._\n\n  Here's another example of using a server middleware to detect 404s and check the CMS for a redirect:\n\n  ```tsx\n  const redirects: Route.unstable_MiddlewareFunction = async ({\n    request,\n    next,\n  }) => {\n    // attempt to handle the request\n    let res = await next();\n\n    // if it's a 404, check the CMS for a redirect, do it last\n    // because it's expensive\n    if (res.status === 404) {\n      let cmsRedirect = await checkCMSRedirects(request.url);\n      if (cmsRedirect) {\n        throw redirect(cmsRedirect, 302);\n      }\n    }\n\n    return res;\n  };\n  ```\n\n  **`context` parameter**\n\n  When middleware is enabled, your application will use a different type of `context` parameter in your loaders and actions to provide better type safety. Instead of `AppLoadContext`, `context` will now be an instance of `ContextProvider` that you can use with type-safe contexts (similar to `React.createContext`):\n\n  ```ts\n  import { unstable_createContext } from \"react-router\";\n  import { Route } from \"./+types/root\";\n  import type { Session } from \"./sessions.server\";\n  import { getSession } from \"./sessions.server\";\n\n  let sessionContext = unstable_createContext<Session>();\n\n  const sessionMiddleware: Route.unstable_MiddlewareFunction = ({\n    context,\n    request,\n  }) => {\n    let session = await getSession(request);\n    context.set(sessionContext, session);\n    //                          ^ must be of type Session\n  };\n\n  // ... then in some downstream middleware\n  const loggerMiddleware: Route.unstable_MiddlewareFunction = ({\n    context,\n    request,\n  }) => {\n    let session = context.get(sessionContext);\n    //  ^ typeof Session\n    console.log(session.get(\"userId\"), request.method, request.url);\n  };\n\n  // ... or some downstream loader\n  export function loader({ context }: Route.LoaderArgs) {\n    let session = context.get(sessionContext);\n    let profile = await getProfile(session.get(\"userId\"));\n    return { profile };\n  }\n  ```\n\n  If you are using a custom server with a `getLoadContext` function, the return value for initial context values passed from the server adapter layer is no longer an object and should now return an `unstable_InitialContext` (`Map<RouterContext, unknown>`):\n\n  ```ts\n  let adapterContext = unstable_createContext<MyAdapterContext>();\n\n  function getLoadContext(req, res): unstable_InitialContext {\n    let map = new Map();\n    map.set(adapterContext, getAdapterContext(req));\n    return map;\n  }\n  ```\n\n- Fix types for loaderData and actionData that contained `Record`s ([#13139](https://github.com/remix-run/react-router/pull/13139))\n\n  UNSTABLE(BREAKING):\n\n  `unstable_SerializesTo` added a way to register custom serialization types in Single Fetch for other library and framework authors like Apollo.\n  It was implemented with branded type whose branded property that was made optional so that casting arbitrary values was easy:\n\n  ```ts\n  // without the brand being marked as optional\n  let x1 = 42 as unknown as unstable_SerializesTo<number>;\n  //          ^^^^^^^^^^\n\n  // with the brand being marked as optional\n  let x2 = 42 as unstable_SerializesTo<number>;\n  ```\n\n  However, this broke type inference in `loaderData` and `actionData` for any `Record` types as those would now (incorrectly) match `unstable_SerializesTo`.\n  This affected all users, not just those that depended on `unstable_SerializesTo`.\n  To fix this, the branded property of `unstable_SerializesTo` is marked as required instead of optional.\n\n  For library and framework authors using `unstable_SerializesTo`, you may need to add `as unknown` casts before casting to `unstable_SerializesTo`.\n\n- Fix single fetch `_root.data` requests when a `basename` is used ([#12898](https://github.com/remix-run/react-router/pull/12898))\n\n- Add `context` support to client side data routers (unstable) ([#12941](https://github.com/remix-run/react-router/pull/12941))\n\n  Your application `loader` and `action` functions on the client will now receive a `context` parameter. This is an instance of `unstable_RouterContextProvider` that you use with type-safe contexts (similar to `React.createContext`) and is most useful with the corresponding `middleware`/`clientMiddleware` API's:\n\n  ```ts\n  import { unstable_createContext } from \"react-router\";\n\n  type User = {\n    /*...*/\n  };\n\n  let userContext = unstable_createContext<User>();\n\n  function sessionMiddleware({ context }) {\n    let user = await getUser();\n    context.set(userContext, user);\n  }\n\n  // ... then in some downstream loader\n  function loader({ context }) {\n    let user = context.get(userContext);\n    let profile = await getProfile(user.id);\n    return { profile };\n  }\n  ```\n\n  Similar to server-side requests, a fresh `context` will be created per navigation (or `fetcher` call). If you have initial data you'd like to populate in the context for every request, you can provide an `unstable_getContext` function at the root of your app:\n  - Library mode - `createBrowserRouter(routes, { unstable_getContext })`\n  - Framework mode - `<HydratedRouter unstable_getContext>`\n\n  This function should return an value of type `unstable_InitialContext` which is a `Map<unstable_RouterContext, unknown>` of context's and initial values:\n\n  ```ts\n  const loggerContext = unstable_createContext<(...args: unknown[]) => void>();\n\n  function logger(...args: unknown[]) {\n    console.log(new Date.toISOString(), ...args);\n  }\n\n  function unstable_getContext() {\n    let map = new Map();\n    map.set(loggerContext, logger);\n    return map;\n  }\n  ```\n\n## 7.2.0\n\n### Minor Changes\n\n- New type-safe `href` utility that guarantees links point to actual paths in your app ([#13012](https://github.com/remix-run/react-router/pull/13012))\n\n  ```tsx\n  import { href } from \"react-router\";\n\n  export default function Component() {\n    const link = href(\"/blog/:slug\", { slug: \"my-first-post\" });\n    return (\n      <main>\n        <Link to={href(\"/products/:id\", { id: \"asdf\" })} />\n        <NavLink to={href(\"/:lang?/about\", { lang: \"en\" })} />\n      </main>\n    );\n  }\n  ```\n\n### Patch Changes\n\n- Fix typegen for repeated params ([#13012](https://github.com/remix-run/react-router/pull/13012))\n\n  In React Router, path parameters are keyed by their name.\n  So for a path pattern like `/a/:id/b/:id?/c/:id`, the last `:id` will set the value for `id` in `useParams` and the `params` prop.\n  For example, `/a/1/b/2/c/3` will result in the value `{ id: 3 }` at runtime.\n\n  Previously, generated types for params incorrectly modeled repeated params with an array.\n  So `/a/1/b/2/c/3` generated a type like `{ id: [1,2,3] }`.\n\n  To be consistent with runtime behavior, the generated types now correctly model the \"last one wins\" semantics of path parameters.\n  So `/a/1/b/2/c/3` now generates a type like `{ id: 3 }`.\n\n- Don't apply Single Fetch revalidation de-optimization when in SPA mode since there is no server HTTP request ([#12948](https://github.com/remix-run/react-router/pull/12948))\n\n- Properly handle revalidations to across a prerender/SPA boundary ([#13021](https://github.com/remix-run/react-router/pull/13021))\n  - In \"hybrid\" applications where some routes are pre-rendered and some are served from a SPA fallback, we need to avoid making `.data` requests if the path wasn't pre-rendered because the request will 404\n  - We don't know all the pre-rendered paths client-side, however:\n    - All `loader` data in `ssr:false` mode is static because it's generated at build time\n    - A route must use a `clientLoader` to do anything dynamic\n    - Therefore, if a route only has a `loader` and not a `clientLoader`, we disable revalidation by default because there is no new data to retrieve\n    - We short circuit and skip single fetch `.data` request logic if there are no server loaders with `shouldLoad=true` in our single fetch `dataStrategy`\n    - This ensures that the route doesn't cause a `.data` request that would 404 after a submission\n\n- Error at build time in `ssr:false` + `prerender` apps for the edge case scenario of: ([#13021](https://github.com/remix-run/react-router/pull/13021))\n  - A parent route has only a `loader` (does not have a `clientLoader`)\n  - The parent route is pre-rendered\n  - The parent route has children routes which are not prerendered\n  - This means that when the child paths are loaded via the SPA fallback, the parent won't have any `loaderData` because there is no server on which to run the `loader`\n  - This can be resolved by either adding a parent `clientLoader` or pre-rendering the child paths\n  - If you add a `clientLoader`, calling the `serverLoader()` on non-prerendered paths will throw a 404\n\n- Add unstable support for splitting route modules in framework mode via `future.unstable_splitRouteModules` ([#11871](https://github.com/remix-run/react-router/pull/11871))\n\n- Add `unstable_SerializesTo` brand type for library authors to register types serializable by React Router's streaming format (`turbo-stream`) ([`ab5b05b02`](https://github.com/remix-run/react-router/commit/ab5b05b02f99f062edb3c536c392197c88eb6c77))\n\n- Align dev server behavior with static file server behavior when `ssr:false` is set ([#12948](https://github.com/remix-run/react-router/pull/12948))\n  - When no `prerender` config exists, only SSR down to the root `HydrateFallback` (SPA Mode)\n  - When a `prerender` config exists but the current path is not prerendered, only SSR down to the root `HydrateFallback` (SPA Fallback)\n  - Return a 404 on `.data` requests to non-pre-rendered paths\n\n- Improve prefetch performance of CSS side effects in framework mode ([#12889](https://github.com/remix-run/react-router/pull/12889))\n\n- Disable Lazy Route Discovery for all `ssr:false` apps and not just \"SPA Mode\" because there is no runtime server to serve the search-param-configured `__manifest` requests ([#12894](https://github.com/remix-run/react-router/pull/12894))\n  - We previously only disabled this for \"SPA Mode\" which is `ssr:false` and no `prerender` config but we realized it should apply to all `ssr:false` apps, including those prerendering multiple pages\n  - In those `prerender` scenarios we would prerender the `/__manifest` file assuming the static file server would serve it but that makes some unneccesary assumptions about the static file server behaviors\n\n- Properly handle interrupted manifest requests in lazy route discovery ([#12915](https://github.com/remix-run/react-router/pull/12915))\n\n## 7.1.5\n\n### Patch Changes\n\n- Fix regression introduced in `7.1.4` via [#12800](https://github.com/remix-run/react-router/pull/12800) that caused issues navigating to hash routes inside splat routes for applications using Lazy Route Discovery (`patchRoutesOnNavigation`) ([#12927](https://github.com/remix-run/react-router/pull/12927))\n\n## 7.1.4\n\n### Patch Changes\n\n- Internal reorg to clean up some duplicated route module types ([#12799](https://github.com/remix-run/react-router/pull/12799))\n- Properly handle status codes that cannot have a body in single fetch responses (204, etc.) ([#12760](https://github.com/remix-run/react-router/pull/12760))\n- Stop erroring on resource routes that return raw strings/objects and instead serialize them as `text/plain` or `application/json` responses ([#12848](https://github.com/remix-run/react-router/pull/12848))\n  - This only applies when accessed as a resource route without the `.data` extension\n  - When accessed from a Single Fetch `.data` request, they will still be encoded via `turbo-stream`\n- Optimize Lazy Route Discovery path discovery to favor a single `querySelectorAll` call at the `body` level instead of many calls at the sub-tree level ([#12731](https://github.com/remix-run/react-router/pull/12731))\n- Properly bubble headers as `errorHeaders` when throwing a `data()` result ([#12846](https://github.com/remix-run/react-router/pull/12846))\n  - Avoid duplication of `Set-Cookie` headers could be duplicated if also returned from `headers`\n- Optimize route matching by skipping redundant `matchRoutes` calls when possible ([#12800](https://github.com/remix-run/react-router/pull/12800))\n\n## 7.1.3\n\n_No changes_\n\n## 7.1.2\n\n### Patch Changes\n\n- Fix issue with fetcher data cleanup in the data layer on fetcher unmount ([#12681](https://github.com/remix-run/react-router/pull/12681))\n- Do not rely on `symbol` for filtering out `redirect` responses from loader data ([#12694](https://github.com/remix-run/react-router/pull/12694))\n\n  Previously, some projects were getting type checking errors like:\n\n  ```ts\n  error TS4058: Return type of exported function has or is using name 'redirectSymbol' from external module \"node_modules/...\" but cannot be named.\n  ```\n\n  Now that `symbol`s are not used for the `redirect` response type, these errors should no longer be present.\n\n## 7.1.1\n\n_No changes_\n\n## 7.1.0\n\n### Patch Changes\n\n- Throw unwrapped single fetch redirect to align with pre-single fetch behavior ([#12506](https://github.com/remix-run/react-router/pull/12506))\n- Ignore redirects when inferring loader data types ([#12527](https://github.com/remix-run/react-router/pull/12527))\n- Remove `<Link prefetch>` warning which suffers from false positives in a lazy route discovery world ([#12485](https://github.com/remix-run/react-router/pull/12485))\n\n## 7.0.2\n\n### Patch Changes\n\n- temporarily only use one build in export map so packages can have a peer dependency on react router ([#12437](https://github.com/remix-run/react-router/pull/12437))\n- Generate wide `matches` and `params` types for current route and child routes ([#12397](https://github.com/remix-run/react-router/pull/12397))\n\n  At runtime, `matches` includes child route matches and `params` include child route path parameters.\n  But previously, we only generated types for parent routes in `matches`; for `params`, we only considered the parent routes and the current route.\n  To align our generated types more closely to the runtime behavior, we now generate more permissive, wider types when accessing child route information.\n\n## 7.0.1\n\n_No changes_\n\n## 7.0.0\n\n### Major Changes\n\n- Remove the original `defer` implementation in favor of using raw promises via single fetch and `turbo-stream`. This removes these exports from React Router: ([#11744](https://github.com/remix-run/react-router/pull/11744))\n  - `defer`\n  - `AbortedDeferredError`\n  - `type TypedDeferredData`\n  - `UNSAFE_DeferredData`\n  - `UNSAFE_DEFERRED_SYMBOL`,\n\n- - Collapse `@remix-run/router` into `react-router` ([#11505](https://github.com/remix-run/react-router/pull/11505))\n  - Collapse `react-router-dom` into `react-router`\n  - Collapse `@remix-run/server-runtime` into `react-router`\n  - Collapse `@remix-run/testing` into `react-router`\n\n- Remove single fetch future flag. ([#11522](https://github.com/remix-run/react-router/pull/11522))\n\n- Drop support for Node 16, React Router SSR now requires Node 18 or higher ([#11391](https://github.com/remix-run/react-router/pull/11391))\n\n- Remove `future.v7_startTransition` flag ([#11696](https://github.com/remix-run/react-router/pull/11696))\n\n- - Expose the underlying router promises from the following APIs for compsition in React 19 APIs: ([#11521](https://github.com/remix-run/react-router/pull/11521))\n    - `useNavigate()`\n    - `useSubmit`\n    - `useFetcher().load`\n    - `useFetcher().submit`\n    - `useRevalidator.revalidate`\n\n- Remove `future.v7_normalizeFormMethod` future flag ([#11697](https://github.com/remix-run/react-router/pull/11697))\n\n- For Remix consumers migrating to React Router, the `crypto` global from the [Web Crypto API](https://developer.mozilla.org/en-US/docs/Web/API/Web_Crypto_API) is now required when using cookie and session APIs. This means that the following APIs are provided from `react-router` rather than platform-specific packages: ([#11837](https://github.com/remix-run/react-router/pull/11837))\n  - `createCookie`\n  - `createCookieSessionStorage`\n  - `createMemorySessionStorage`\n  - `createSessionStorage`\n\n  For consumers running older versions of Node, the `installGlobals` function from `@remix-run/node` has been updated to define `globalThis.crypto`, using [Node's `require('node:crypto').webcrypto` implementation.](https://nodejs.org/api/webcrypto.html)\n\n  Since platform-specific packages no longer need to implement this API, the following low-level APIs have been removed:\n  - `createCookieFactory`\n  - `createSessionStorageFactory`\n  - `createCookieSessionStorageFactory`\n  - `createMemorySessionStorageFactory`\n\n- Imports/Exports cleanup ([#11840](https://github.com/remix-run/react-router/pull/11840))\n  - Removed the following exports that were previously public API from `@remix-run/router`\n    - types\n      - `AgnosticDataIndexRouteObject`\n      - `AgnosticDataNonIndexRouteObject`\n      - `AgnosticDataRouteMatch`\n      - `AgnosticDataRouteObject`\n      - `AgnosticIndexRouteObject`\n      - `AgnosticNonIndexRouteObject`\n      - `AgnosticRouteMatch`\n      - `AgnosticRouteObject`\n      - `TrackedPromise`\n      - `unstable_AgnosticPatchRoutesOnMissFunction`\n      - `Action` -> exported as `NavigationType` via `react-router`\n      - `Router` exported as `DataRouter` to differentiate from RR's `<Router>`\n    - API\n      - `getToPathname` (`@private`)\n      - `joinPaths` (`@private`)\n      - `normalizePathname` (`@private`)\n      - `resolveTo` (`@private`)\n      - `stripBasename` (`@private`)\n      - `createBrowserHistory` -> in favor of `createBrowserRouter`\n      - `createHashHistory` -> in favor of `createHashRouter`\n      - `createMemoryHistory` -> in favor of `createMemoryRouter`\n      - `createRouter`\n      - `createStaticHandler` -> in favor of wrapper `createStaticHandler` in RR Dom\n      - `getStaticContextFromError`\n  - Removed the following exports that were previously public API from `react-router`\n    - `Hash`\n    - `Pathname`\n    - `Search`\n\n- update minimum node version to 18 ([#11690](https://github.com/remix-run/react-router/pull/11690))\n\n- Remove `future.v7_prependBasename` from the ionternalized `@remix-run/router` package ([#11726](https://github.com/remix-run/react-router/pull/11726))\n\n- Migrate Remix type generics to React Router ([#12180](https://github.com/remix-run/react-router/pull/12180))\n  - These generics are provided for Remix v2 migration purposes\n  - These generics and the APIs they exist on should be considered informally deprecated in favor of the new `Route.*` types\n  - Anyone migrating from React Router v6 should probably not leverage these new generics and should migrate straight to the `Route.*` types\n  - For React Router v6 users, these generics are new and should not impact your app, with one exception\n    - `useFetcher` previously had an optional generic (used primarily by Remix v2) that expected the data type\n    - This has been updated in v7 to expect the type of the function that generates the data (i.e., `typeof loader`/`typeof action`)\n    - Therefore, you should update your usages:\n      - ❌ `useFetcher<LoaderData>()`\n      - ✅ `useFetcher<typeof loader>()`\n\n- Remove `future.v7_throwAbortReason` from internalized `@remix-run/router` package ([#11728](https://github.com/remix-run/react-router/pull/11728))\n\n- Add `exports` field to all packages ([#11675](https://github.com/remix-run/react-router/pull/11675))\n\n- node package no longer re-exports from react-router ([#11702](https://github.com/remix-run/react-router/pull/11702))\n\n- renamed RemixContext to FrameworkContext ([#11705](https://github.com/remix-run/react-router/pull/11705))\n\n- updates the minimum React version to 18 ([#11689](https://github.com/remix-run/react-router/pull/11689))\n\n- PrefetchPageDescriptor replaced by PageLinkDescriptor ([#11960](https://github.com/remix-run/react-router/pull/11960))\n\n- - Consolidate types previously duplicated across `@remix-run/router`, `@remix-run/server-runtime`, and `@remix-run/react` now that they all live in `react-router` ([#12177](https://github.com/remix-run/react-router/pull/12177))\n    - Examples: `LoaderFunction`, `LoaderFunctionArgs`, `ActionFunction`, `ActionFunctionArgs`, `DataFunctionArgs`, `RouteManifest`, `LinksFunction`, `Route`, `EntryRoute`\n    - The `RouteManifest` type used by the \"remix\" code is now slightly stricter because it is using the former `@remix-run/router` `RouteManifest`\n      - `Record<string, Route> -> Record<string, Route | undefined>`\n    - Removed `AppData` type in favor of inlining `unknown` in the few locations it was used\n    - Removed `ServerRuntimeMeta*` types in favor of the `Meta*` types they were duplicated from\n\n- - Remove the `future.v7_partialHydration` flag ([#11725](https://github.com/remix-run/react-router/pull/11725))\n    - This also removes the `<RouterProvider fallbackElement>` prop\n      - To migrate, move the `fallbackElement` to a `hydrateFallbackElement`/`HydrateFallback` on your root route\n    - Also worth nothing there is a related breaking changer with this future flag:\n      - Without `future.v7_partialHydration` (when using `fallbackElement`), `state.navigation` was populated during the initial load\n      - With `future.v7_partialHydration`, `state.navigation` remains in an `\"idle\"` state during the initial load\n\n- Remove `v7_relativeSplatPath` future flag ([#11695](https://github.com/remix-run/react-router/pull/11695))\n\n- Drop support for Node 18, update minimum Node vestion to 20 ([#12171](https://github.com/remix-run/react-router/pull/12171))\n  - Remove `installGlobals()` as this should no longer be necessary\n\n- Remove remaining future flags ([#11820](https://github.com/remix-run/react-router/pull/11820))\n  - React Router `v7_skipActionErrorRevalidation`\n  - Remix `v3_fetcherPersist`, `v3_relativeSplatPath`, `v3_throwAbortReason`\n\n- rename createRemixStub to createRoutesStub ([#11692](https://github.com/remix-run/react-router/pull/11692))\n\n- Remove `@remix-run/router` deprecated `detectErrorBoundary` option in favor of `mapRouteProperties` ([#11751](https://github.com/remix-run/react-router/pull/11751))\n\n- Add `react-router/dom` subpath export to properly enable `react-dom` as an optional `peerDependency` ([#11851](https://github.com/remix-run/react-router/pull/11851))\n  - This ensures that we don't blindly `import ReactDOM from \"react-dom\"` in `<RouterProvider>` in order to access `ReactDOM.flushSync()`, since that would break `createMemoryRouter` use cases in non-DOM environments\n  - DOM environments should import from `react-router/dom` to get the proper component that makes `ReactDOM.flushSync()` available:\n    - If you are using the Vite plugin, use this in your `entry.client.tsx`:\n      - `import { HydratedRouter } from 'react-router/dom'`\n    - If you are not using the Vite plugin and are manually calling `createBrowserRouter`/`createHashRouter`:\n      - `import { RouterProvider } from \"react-router/dom\"`\n\n- Remove `future.v7_fetcherPersist` flag ([#11731](https://github.com/remix-run/react-router/pull/11731))\n\n- Update `cookie` dependency to `^1.0.1` - please see the [release notes](https://github.com/jshttp/cookie/releases) for any breaking changes ([#12172](https://github.com/remix-run/react-router/pull/12172))\n\n### Minor Changes\n\n- - Add support for `prerender` config in the React Router vite plugin, to support existing SSG use-cases ([#11539](https://github.com/remix-run/react-router/pull/11539))\n    - You can use the `prerender` config to pre-render your `.html` and `.data` files at build time and then serve them statically at runtime (either from a running server or a CDN)\n    - `prerender` can either be an array of string paths, or a function (sync or async) that returns an array of strings so that you can dynamically generate the paths by talking to your CMS, etc.\n\n  ```ts\n  // react-router.config.ts\n  import type { Config } from \"@react-router/dev/config\";\n\n  export default {\n    async prerender() {\n      let slugs = await fakeGetSlugsFromCms();\n      // Prerender these paths into `.html` files at build time, and `.data`\n      // files if they have loaders\n      return [\"/\", \"/about\", ...slugs.map((slug) => `/product/${slug}`)];\n    },\n  } satisfies Config;\n\n  async function fakeGetSlugsFromCms() {\n    await new Promise((r) => setTimeout(r, 1000));\n    return [\"shirt\", \"hat\"];\n  }\n  ```\n\n- Params, loader data, and action data as props for route component exports ([#11961](https://github.com/remix-run/react-router/pull/11961))\n\n  ```tsx\n  export default function Component({ params, loaderData, actionData }) {}\n\n  export function HydrateFallback({ params }) {}\n  export function ErrorBoundary({ params, loaderData, actionData }) {}\n  ```\n\n- Remove duplicate `RouterProvider` impliementations ([#11679](https://github.com/remix-run/react-router/pull/11679))\n\n- ### Typesafety improvements ([#12019](https://github.com/remix-run/react-router/pull/12019))\n\n  React Router now generates types for each of your route modules.\n  You can access those types by importing them from `./+types.<route filename without extension>`.\n  For example:\n\n  ```ts\n  // app/routes/product.tsx\n  import type * as Route from \"./+types.product\";\n\n  export function loader({ params }: Route.LoaderArgs) {}\n\n  export default function Component({ loaderData }: Route.ComponentProps) {}\n  ```\n\n  This initial implementation targets type inference for:\n  - `Params` : Path parameters from your routing config in `routes.ts` including file-based routing\n  - `LoaderData` : Loader data from `loader` and/or `clientLoader` within your route module\n  - `ActionData` : Action data from `action` and/or `clientAction` within your route module\n\n  In the future, we plan to add types for the rest of the route module exports: `meta`, `links`, `headers`, `shouldRevalidate`, etc.\n  We also plan to generate types for typesafe `Link`s:\n\n  ```tsx\n  <Link to=\"/products/:id\" params={{ id: 1 }} />\n  //        ^^^^^^^^^^^^^          ^^^^^^^^^\n  // typesafe `to` and `params` based on the available routes in your app\n  ```\n\n  Check out our docs for more:\n  - [_Explanations > Type Safety_](https://reactrouter.com/dev/guides/explanation/type-safety)\n  - [_How-To > Setting up type safety_](https://reactrouter.com/dev/guides/how-to/setting-up-type-safety)\n\n- Stabilize `unstable_dataStrategy` ([#11969](https://github.com/remix-run/react-router/pull/11969))\n\n- Stabilize `unstable_patchRoutesOnNavigation` ([#11970](https://github.com/remix-run/react-router/pull/11970))\n\n### Patch Changes\n\n- No changes ([`506329c4e`](https://github.com/remix-run/react-router/commit/506329c4e2e7aba9837cbfa44df6103b49423745))\n\n- chore: re-enable development warnings through a `development` exports condition. ([#12269](https://github.com/remix-run/react-router/pull/12269))\n\n- Remove unstable upload handler. ([#12015](https://github.com/remix-run/react-router/pull/12015))\n\n- Remove unneeded dependency on @web3-storage/multipart-parser ([#12274](https://github.com/remix-run/react-router/pull/12274))\n\n- Fix redirects returned from loaders/actions using `data()` ([#12021](https://github.com/remix-run/react-router/pull/12021))\n\n- fix(react-router): (v7) fix static prerender of non-ascii characters ([#12161](https://github.com/remix-run/react-router/pull/12161))\n\n- Replace `substr` with `substring` ([#12080](https://github.com/remix-run/react-router/pull/12080))\n\n- Remove the deprecated `json` utility ([#12146](https://github.com/remix-run/react-router/pull/12146))\n  - You can use [`Response.json`](https://developer.mozilla.org/en-US/docs/Web/API/Response/json_static) if you still need to construct JSON responses in your app\n\n- Remove unneeded dependency on source-map ([#12275](https://github.com/remix-run/react-router/pull/12275))\n\n## 6.28.0\n\n### Minor Changes\n\n- - Log deprecation warnings for v7 flags ([#11750](https://github.com/remix-run/react-router/pull/11750))\n  - Add deprecation warnings to `json`/`defer` in favor of returning raw objects\n    - These methods will be removed in React Router v7\n\n### Patch Changes\n\n- Update JSDoc URLs for new website structure (add /v6/ segment) ([#12141](https://github.com/remix-run/react-router/pull/12141))\n- Updated dependencies:\n  - `@remix-run/router@1.21.0`\n\n## 6.27.0\n\n### Minor Changes\n\n- Stabilize `unstable_patchRoutesOnNavigation` ([#11973](https://github.com/remix-run/react-router/pull/11973))\n  - Add new `PatchRoutesOnNavigationFunctionArgs` type for convenience ([#11967](https://github.com/remix-run/react-router/pull/11967))\n- Stabilize `unstable_dataStrategy` ([#11974](https://github.com/remix-run/react-router/pull/11974))\n- Stabilize the `unstable_flushSync` option for navigations and fetchers ([#11989](https://github.com/remix-run/react-router/pull/11989))\n- Stabilize the `unstable_viewTransition` option for navigations and the corresponding `unstable_useViewTransitionState` hook ([#11989](https://github.com/remix-run/react-router/pull/11989))\n\n### Patch Changes\n\n- Fix bug when submitting to the current contextual route (parent route with an index child) when an `?index` param already exists from a prior submission ([#12003](https://github.com/remix-run/react-router/pull/12003))\n\n- Fix `useFormAction` bug - when removing `?index` param it would not keep other non-Remix `index` params ([#12003](https://github.com/remix-run/react-router/pull/12003))\n\n- Fix types for `RouteObject` within `PatchRoutesOnNavigationFunction`'s `patch` method so it doesn't expect agnostic route objects passed to `patch` ([#11967](https://github.com/remix-run/react-router/pull/11967))\n\n- Updated dependencies:\n  - `@remix-run/router@1.20.0`\n\n## 6.26.2\n\n### Patch Changes\n\n- Updated dependencies:\n  - `@remix-run/router@1.19.2`\n\n## 6.26.1\n\n### Patch Changes\n\n- Rename `unstable_patchRoutesOnMiss` to `unstable_patchRoutesOnNavigation` to match new behavior ([#11888](https://github.com/remix-run/react-router/pull/11888))\n- Updated dependencies:\n  - `@remix-run/router@1.19.1`\n\n## 6.26.0\n\n### Minor Changes\n\n- Add a new `replace(url, init?)` alternative to `redirect(url, init?)` that performs a `history.replaceState` instead of a `history.pushState` on client-side navigation redirects ([#11811](https://github.com/remix-run/react-router/pull/11811))\n\n### Patch Changes\n\n- Fix initial hydration behavior when using `future.v7_partialHydration` along with `unstable_patchRoutesOnMiss` ([#11838](https://github.com/remix-run/react-router/pull/11838))\n  - During initial hydration, `router.state.matches` will now include any partial matches so that we can render ancestor `HydrateFallback` components\n- Updated dependencies:\n  - `@remix-run/router@1.19.0`\n\n## 6.25.1\n\nNo significant changes to this package were made in this release. [See the repo `CHANGELOG.md`](https://github.com/remix-run/react-router/blob/main/CHANGELOG.md) for an overview of all changes in v6.25.1.\n\n## 6.25.0\n\n### Minor Changes\n\n- Stabilize `future.unstable_skipActionErrorRevalidation` as `future.v7_skipActionErrorRevalidation` ([#11769](https://github.com/remix-run/react-router/pull/11769))\n  - When this flag is enabled, actions will not automatically trigger a revalidation if they return/throw a `Response` with a `4xx`/`5xx` status code\n  - You may still opt-into revalidation via `shouldRevalidate`\n  - This also changes `shouldRevalidate`'s `unstable_actionStatus` parameter to `actionStatus`\n\n### Patch Changes\n\n- Fix regression and properly decode paths inside `useMatch` so matches/params reflect decoded params ([#11789](https://github.com/remix-run/react-router/pull/11789))\n- Updated dependencies:\n  - `@remix-run/router@1.18.0`\n\n## 6.24.1\n\n### Patch Changes\n\n- When using `future.v7_relativeSplatPath`, properly resolve relative paths in splat routes that are children of pathless routes ([#11633](https://github.com/remix-run/react-router/pull/11633))\n- Updated dependencies:\n  - `@remix-run/router@1.17.1`\n\n## 6.24.0\n\n### Minor Changes\n\n- Add support for Lazy Route Discovery (a.k.a. Fog of War) ([#11626](https://github.com/remix-run/react-router/pull/11626))\n  - RFC: <https://github.com/remix-run/react-router/discussions/11113>\n  - `unstable_patchRoutesOnMiss` docs: <https://reactrouter.com/v6/routers/create-browser-router>\n\n### Patch Changes\n\n- Updated dependencies:\n  - `@remix-run/router@1.17.0`\n\n## 6.23.1\n\n### Patch Changes\n\n- allow undefined to be resolved with `<Await>` ([#11513](https://github.com/remix-run/react-router/pull/11513))\n- Updated dependencies:\n  - `@remix-run/router@1.16.1`\n\n## 6.23.0\n\n### Minor Changes\n\n- Add a new `unstable_dataStrategy` configuration option ([#11098](https://github.com/remix-run/react-router/pull/11098))\n  - This option allows Data Router applications to take control over the approach for executing route loaders and actions\n  - The default implementation is today's behavior, to fetch all loaders in parallel, but this option allows users to implement more advanced data flows including Remix single-fetch, middleware/context APIs, automatic loader caching, and more\n\n### Patch Changes\n\n- Updated dependencies:\n  - `@remix-run/router@1.16.0`\n\n## 6.22.3\n\n### Patch Changes\n\n- Updated dependencies:\n  - `@remix-run/router@1.15.3`\n\n## 6.22.2\n\n### Patch Changes\n\n- Updated dependencies:\n  - `@remix-run/router@1.15.2`\n\n## 6.22.1\n\n### Patch Changes\n\n- Fix encoding/decoding issues with pre-encoded dynamic parameter values ([#11199](https://github.com/remix-run/react-router/pull/11199))\n- Updated dependencies:\n  - `@remix-run/router@1.15.1`\n\n## 6.22.0\n\n### Patch Changes\n\n- Updated dependencies:\n  - `@remix-run/router@1.15.0`\n\n## 6.21.3\n\n### Patch Changes\n\n- Remove leftover `unstable_` prefix from `Blocker`/`BlockerFunction` types ([#11187](https://github.com/remix-run/react-router/pull/11187))\n\n## 6.21.2\n\n### Patch Changes\n\n- Updated dependencies:\n  - `@remix-run/router@1.14.2`\n\n## 6.21.1\n\n### Patch Changes\n\n- Fix bug with `route.lazy` not working correctly on initial SPA load when `v7_partialHydration` is specified ([#11121](https://github.com/remix-run/react-router/pull/11121))\n- Updated dependencies:\n  - `@remix-run/router@1.14.1`\n\n## 6.21.0\n\n### Minor Changes\n\n- Add a new `future.v7_relativeSplatPath` flag to implement a breaking bug fix to relative routing when inside a splat route. ([#11087](https://github.com/remix-run/react-router/pull/11087))\n\n  This fix was originally added in [#10983](https://github.com/remix-run/react-router/issues/10983) and was later reverted in [#11078](https://github.com/remix-run/react-router/pull/11078) because it was determined that a large number of existing applications were relying on the buggy behavior (see [#11052](https://github.com/remix-run/react-router/issues/11052))\n\n  **The Bug**\n  The buggy behavior is that without this flag, the default behavior when resolving relative paths is to _ignore_ any splat (`*`) portion of the current route path.\n\n  **The Background**\n  This decision was originally made thinking that it would make the concept of nested different sections of your apps in `<Routes>` easier if relative routing would _replace_ the current splat:\n\n  ```jsx\n  <BrowserRouter>\n    <Routes>\n      <Route path=\"/\" element={<Home />} />\n      <Route path=\"dashboard/*\" element={<Dashboard />} />\n    </Routes>\n  </BrowserRouter>\n  ```\n\n  Any paths like `/dashboard`, `/dashboard/team`, `/dashboard/projects` will match the `Dashboard` route. The dashboard component itself can then render nested `<Routes>`:\n\n  ```jsx\n  function Dashboard() {\n    return (\n      <div>\n        <h2>Dashboard</h2>\n        <nav>\n          <Link to=\"/\">Dashboard Home</Link>\n          <Link to=\"team\">Team</Link>\n          <Link to=\"projects\">Projects</Link>\n        </nav>\n\n        <Routes>\n          <Route path=\"/\" element={<DashboardHome />} />\n          <Route path=\"team\" element={<DashboardTeam />} />\n          <Route path=\"projects\" element={<DashboardProjects />} />\n        </Routes>\n      </div>\n    );\n  }\n  ```\n\n  Now, all links and route paths are relative to the router above them. This makes code splitting and compartmentalizing your app really easy. You could render the `Dashboard` as its own independent app, or embed it into your large app without making any changes to it.\n\n  **The Problem**\n\n  The problem is that this concept of ignoring part of a path breaks a lot of other assumptions in React Router - namely that `\".\"` always means the current location pathname for that route. When we ignore the splat portion, we start getting invalid paths when using `\".\"`:\n\n  ```jsx\n  // If we are on URL /dashboard/team, and we want to link to /dashboard/team:\n  function DashboardTeam() {\n    // ❌ This is broken and results in <a href=\"/dashboard\">\n    return <Link to=\".\">A broken link to the Current URL</Link>;\n\n    // ✅ This is fixed but super unintuitive since we're already at /dashboard/team!\n    return <Link to=\"./team\">A broken link to the Current URL</Link>;\n  }\n  ```\n\n  We've also introduced an issue that we can no longer move our `DashboardTeam` component around our route hierarchy easily - since it behaves differently if we're underneath a non-splat route, such as `/dashboard/:widget`. Now, our `\".\"` links will, properly point to ourself _inclusive of the dynamic param value_ so behavior will break from it's corresponding usage in a `/dashboard/*` route.\n\n  Even worse, consider a nested splat route configuration:\n\n  ```jsx\n  <BrowserRouter>\n    <Routes>\n      <Route path=\"dashboard\">\n        <Route path=\"*\" element={<Dashboard />} />\n      </Route>\n    </Routes>\n  </BrowserRouter>\n  ```\n\n  Now, a `<Link to=\".\">` and a `<Link to=\"..\">` inside the `Dashboard` component go to the same place! That is definitely not correct!\n\n  Another common issue arose in Data Routers (and Remix) where any `<Form>` should post to it's own route `action` if you the user doesn't specify a form action:\n\n  ```jsx\n  let router = createBrowserRouter({\n    path: \"/dashboard\",\n    children: [\n      {\n        path: \"*\",\n        action: dashboardAction,\n        Component() {\n          // ❌ This form is broken!  It throws a 405 error when it submits because\n          // it tries to submit to /dashboard (without the splat value) and the parent\n          // `/dashboard` route doesn't have an action\n          return <Form method=\"post\">...</Form>;\n        },\n      },\n    ],\n  });\n  ```\n\n  This is just a compounded issue from the above because the default location for a `Form` to submit to is itself (`\".\"`) - and if we ignore the splat portion, that now resolves to the parent route.\n\n  **The Solution**\n  If you are leveraging this behavior, it's recommended to enable the future flag, move your splat to it's own route, and leverage `../` for any links to \"sibling\" pages:\n\n  ```jsx\n  <BrowserRouter>\n    <Routes>\n      <Route path=\"dashboard\">\n        <Route index path=\"*\" element={<Dashboard />} />\n      </Route>\n    </Routes>\n  </BrowserRouter>\n\n  function Dashboard() {\n    return (\n      <div>\n        <h2>Dashboard</h2>\n        <nav>\n          <Link to=\"..\">Dashboard Home</Link>\n          <Link to=\"../team\">Team</Link>\n          <Link to=\"../projects\">Projects</Link>\n        </nav>\n\n        <Routes>\n          <Route path=\"/\" element={<DashboardHome />} />\n          <Route path=\"team\" element={<DashboardTeam />} />\n          <Route path=\"projects\" element={<DashboardProjects />} />\n        </Router>\n      </div>\n    );\n  }\n  ```\n\n  This way, `.` means \"the full current pathname for my route\" in all cases (including static, dynamic, and splat routes) and `..` always means \"my parents pathname\".\n\n### Patch Changes\n\n- Properly handle falsy error values in ErrorBoundary's ([#11071](https://github.com/remix-run/react-router/pull/11071))\n- Updated dependencies:\n  - `@remix-run/router@1.14.0`\n\n## 6.20.1\n\n### Patch Changes\n\n- Revert the `useResolvedPath` fix for splat routes due to a large number of applications that were relying on the buggy behavior (see <https://github.com/remix-run/react-router/issues/11052#issuecomment-1836589329>). We plan to re-introduce this fix behind a future flag in the next minor version. ([#11078](https://github.com/remix-run/react-router/pull/11078))\n- Updated dependencies:\n  - `@remix-run/router@1.13.1`\n\n## 6.20.0\n\n### Minor Changes\n\n- Export the `PathParam` type from the public API ([#10719](https://github.com/remix-run/react-router/pull/10719))\n\n### Patch Changes\n\n- Fix bug with `resolveTo` in splat routes ([#11045](https://github.com/remix-run/react-router/pull/11045))\n  - This is a follow up to [#10983](https://github.com/remix-run/react-router/pull/10983) to handle the few other code paths using `getPathContributingMatches`\n  - This removes the `UNSAFE_getPathContributingMatches` export from `@remix-run/router` since we no longer need this in the `react-router`/`react-router-dom` layers\n- Updated dependencies:\n  - `@remix-run/router@1.13.0`\n\n## 6.19.0\n\n### Minor Changes\n\n- Add `unstable_flushSync` option to `useNavigate`/`useSumbit`/`fetcher.load`/`fetcher.submit` to opt-out of `React.startTransition` and into `ReactDOM.flushSync` for state updates ([#11005](https://github.com/remix-run/react-router/pull/11005))\n- Remove the `unstable_` prefix from the [`useBlocker`](https://reactrouter.com/v6/hooks/use-blocker) hook as it's been in use for enough time that we are confident in the API. We do not plan to remove the prefix from `unstable_usePrompt` due to differences in how browsers handle `window.confirm` that prevent React Router from guaranteeing consistent/correct behavior. ([#10991](https://github.com/remix-run/react-router/pull/10991))\n\n### Patch Changes\n\n- Fix `useActionData` so it returns proper contextual action data and not _any_ action data in the tree ([#11023](https://github.com/remix-run/react-router/pull/11023))\n\n- Fix bug in `useResolvedPath` that would cause `useResolvedPath(\".\")` in a splat route to lose the splat portion of the URL path. ([#10983](https://github.com/remix-run/react-router/pull/10983))\n  - ⚠️ This fixes a quite long-standing bug specifically for `\".\"` paths inside a splat route which incorrectly dropped the splat portion of the URL. If you are relative routing via `\".\"` inside a splat route in your application you should double check that your logic is not relying on this buggy behavior and update accordingly.\n\n- Updated dependencies:\n  - `@remix-run/router@1.12.0`\n\n## 6.18.0\n\n### Patch Changes\n\n- Fix the `future` prop on `BrowserRouter`, `HashRouter` and `MemoryRouter` so that it accepts a `Partial<FutureConfig>` instead of requiring all flags to be included. ([#10962](https://github.com/remix-run/react-router/pull/10962))\n- Updated dependencies:\n  - `@remix-run/router@1.11.0`\n\n## 6.17.0\n\n### Patch Changes\n\n- Fix `RouterProvider` `future` prop type to be a `Partial<FutureConfig>` so that not all flags must be specified ([#10900](https://github.com/remix-run/react-router/pull/10900))\n- Updated dependencies:\n  - `@remix-run/router@1.10.0`\n\n## 6.16.0\n\n### Minor Changes\n\n- In order to move towards stricter TypeScript support in the future, we're aiming to replace current usages of `any` with `unknown` on exposed typings for user-provided data. To do this in Remix v2 without introducing breaking changes in React Router v6, we have added generics to a number of shared types. These continue to default to `any` in React Router and are overridden with `unknown` in Remix. In React Router v7 we plan to move these to `unknown` as a breaking change. ([#10843](https://github.com/remix-run/react-router/pull/10843))\n  - `Location` now accepts a generic for the `location.state` value\n  - `ActionFunctionArgs`/`ActionFunction`/`LoaderFunctionArgs`/`LoaderFunction` now accept a generic for the `context` parameter (only used in SSR usages via `createStaticHandler`)\n  - The return type of `useMatches` (now exported as `UIMatch`) accepts generics for `match.data` and `match.handle` - both of which were already set to `unknown`\n- Move the `@private` class export `ErrorResponse` to an `UNSAFE_ErrorResponseImpl` export since it is an implementation detail and there should be no construction of `ErrorResponse` instances in userland. This frees us up to export a `type ErrorResponse` which correlates to an instance of the class via `InstanceType`. Userland code should only ever be using `ErrorResponse` as a type and should be type-narrowing via `isRouteErrorResponse`. ([#10811](https://github.com/remix-run/react-router/pull/10811))\n- Export `ShouldRevalidateFunctionArgs` interface ([#10797](https://github.com/remix-run/react-router/pull/10797))\n- Removed private/internal APIs only required for the Remix v1 backwards compatibility layer and no longer needed in Remix v2 (`_isFetchActionRedirect`, `_hasFetcherDoneAnything`) ([#10715](https://github.com/remix-run/react-router/pull/10715))\n\n### Patch Changes\n\n- Updated dependencies:\n  - `@remix-run/router@1.9.0`\n\n## 6.15.0\n\n### Minor Changes\n\n- Add's a new `redirectDocument()` function which allows users to specify that a redirect from a `loader`/`action` should trigger a document reload (via `window.location`) instead of attempting to navigate to the redirected location via React Router ([#10705](https://github.com/remix-run/react-router/pull/10705))\n\n### Patch Changes\n\n- Ensure `useRevalidator` is referentially stable across re-renders if revalidations are not actively occurring ([#10707](https://github.com/remix-run/react-router/pull/10707))\n- Updated dependencies:\n  - `@remix-run/router@1.8.0`\n\n## 6.14.2\n\n### Patch Changes\n\n- Updated dependencies:\n  - `@remix-run/router@1.7.2`\n\n## 6.14.1\n\n### Patch Changes\n\n- Fix loop in `unstable_useBlocker` when used with an unstable blocker function ([#10652](https://github.com/remix-run/react-router/pull/10652))\n- Fix issues with reused blockers on subsequent navigations ([#10656](https://github.com/remix-run/react-router/pull/10656))\n- Updated dependencies:\n  - `@remix-run/router@1.7.1`\n\n## 6.14.0\n\n### Patch Changes\n\n- Strip `basename` from locations provided to `unstable_useBlocker` functions to match `useLocation` ([#10573](https://github.com/remix-run/react-router/pull/10573))\n- Fix `generatePath` when passed a numeric `0` value parameter ([#10612](https://github.com/remix-run/react-router/pull/10612))\n- Fix `unstable_useBlocker` key issues in `StrictMode` ([#10573](https://github.com/remix-run/react-router/pull/10573))\n- Fix `tsc --skipLibCheck:false` issues on React 17 ([#10622](https://github.com/remix-run/react-router/pull/10622))\n- Upgrade `typescript` to 5.1 ([#10581](https://github.com/remix-run/react-router/pull/10581))\n- Updated dependencies:\n  - `@remix-run/router@1.7.0`\n\n## 6.13.0\n\n### Minor Changes\n\n- Move [`React.startTransition`](https://react.dev/reference/react/startTransition) usage behind a [future flag](https://reactrouter.com/v6/guides/api-development-strategy) to avoid issues with existing incompatible `Suspense` usages. We recommend folks adopting this flag to be better compatible with React concurrent mode, but if you run into issues you can continue without the use of `startTransition` until v7. Issues usually boils down to creating net-new promises during the render cycle, so if you run into issues you should either lift your promise creation out of the render cycle or put it behind a `useMemo`. ([#10596](https://github.com/remix-run/react-router/pull/10596))\n\n  Existing behavior will no longer include `React.startTransition`:\n\n  ```jsx\n  <BrowserRouter>\n    <Routes>{/*...*/}</Routes>\n  </BrowserRouter>\n\n  <RouterProvider router={router} />\n  ```\n\n  If you wish to enable `React.startTransition`, pass the future flag to your component:\n\n  ```jsx\n  <BrowserRouter future={{ v7_startTransition: true }}>\n    <Routes>{/*...*/}</Routes>\n  </BrowserRouter>\n\n  <RouterProvider router={router} future={{ v7_startTransition: true }}/>\n  ```\n\n### Patch Changes\n\n- Work around webpack/terser `React.startTransition` minification bug in production mode ([#10588](https://github.com/remix-run/react-router/pull/10588))\n\n## 6.12.1\n\n> \\[!WARNING]\n> Please use version `6.13.0` or later instead of `6.12.1`. This version suffers from a `webpack`/`terser` minification issue resulting in invalid minified code in your resulting production bundles which can cause issues in your application. See [#10579](https://github.com/remix-run/react-router/issues/10579) for more details.\n\n### Patch Changes\n\n- Adjust feature detection of `React.startTransition` to fix webpack + react 17 compilation error ([#10569](https://github.com/remix-run/react-router/pull/10569))\n\n## 6.12.0\n\n### Minor Changes\n\n- Wrap internal router state updates with `React.startTransition` if it exists ([#10438](https://github.com/remix-run/react-router/pull/10438))\n\n### Patch Changes\n\n- Updated dependencies:\n  - `@remix-run/router@1.6.3`\n\n## 6.11.2\n\n### Patch Changes\n\n- Fix `basename` duplication in descendant `<Routes>` inside a `<RouterProvider>` ([#10492](https://github.com/remix-run/react-router/pull/10492))\n- Updated dependencies:\n  - `@remix-run/router@1.6.2`\n\n## 6.11.1\n\n### Patch Changes\n\n- Fix usage of `Component` API within descendant `<Routes>` ([#10434](https://github.com/remix-run/react-router/pull/10434))\n- Fix bug when calling `useNavigate` from `<Routes>` inside a `<RouterProvider>` ([#10432](https://github.com/remix-run/react-router/pull/10432))\n- Fix usage of `<Navigate>` in strict mode when using a data router ([#10435](https://github.com/remix-run/react-router/pull/10435))\n- Updated dependencies:\n  - `@remix-run/router@1.6.1`\n\n## 6.11.0\n\n### Patch Changes\n\n- Log loader/action errors to the console in dev for easier stack trace evaluation ([#10286](https://github.com/remix-run/react-router/pull/10286))\n- Fix bug preventing rendering of descendant `<Routes>` when `RouterProvider` errors existed ([#10374](https://github.com/remix-run/react-router/pull/10374))\n- Fix inadvertent re-renders when using `Component` instead of `element` on a route definition ([#10287](https://github.com/remix-run/react-router/pull/10287))\n- Fix detection of `useNavigate` in the render cycle by setting the `activeRef` in a layout effect, allowing the `navigate` function to be passed to child components and called in a `useEffect` there. ([#10394](https://github.com/remix-run/react-router/pull/10394))\n- Switched from `useSyncExternalStore` to `useState` for internal `@remix-run/router` router state syncing in `<RouterProvider>`. We found some [subtle bugs](https://codesandbox.io/s/use-sync-external-store-loop-9g7b81) where router state updates got propagated _before_ other normal `useState` updates, which could lead to footguns in `useEffect` calls. ([#10377](https://github.com/remix-run/react-router/pull/10377), [#10409](https://github.com/remix-run/react-router/pull/10409))\n- Allow `useRevalidator()` to resolve a loader-driven error boundary scenario ([#10369](https://github.com/remix-run/react-router/pull/10369))\n- Avoid unnecessary unsubscribe/resubscribes on router state changes ([#10409](https://github.com/remix-run/react-router/pull/10409))\n- When using a `RouterProvider`, `useNavigate`/`useSubmit`/`fetcher.submit` are now stable across location changes, since we can handle relative routing via the `@remix-run/router` instance and get rid of our dependence on `useLocation()`. When using `BrowserRouter`, these hooks remain unstable across location changes because they still rely on `useLocation()`. ([#10336](https://github.com/remix-run/react-router/pull/10336))\n- Updated dependencies:\n  - `@remix-run/router@1.6.0`\n\n## 6.10.0\n\n### Minor Changes\n\n- Added support for [**Future Flags**](https://reactrouter.com/v6/guides/api-development-strategy) in React Router. The first flag being introduced is `future.v7_normalizeFormMethod` which will normalize the exposed `useNavigation()/useFetcher()` `formMethod` fields as uppercase HTTP methods to align with the `fetch()` behavior. ([#10207](https://github.com/remix-run/react-router/pull/10207))\n  - When `future.v7_normalizeFormMethod === false` (default v6 behavior),\n    - `useNavigation().formMethod` is lowercase\n    - `useFetcher().formMethod` is lowercase\n  - When `future.v7_normalizeFormMethod === true`:\n    - `useNavigation().formMethod` is uppercase\n    - `useFetcher().formMethod` is uppercase\n\n### Patch Changes\n\n- Fix route ID generation when using Fragments in `createRoutesFromElements` ([#10193](https://github.com/remix-run/react-router/pull/10193))\n- Updated dependencies:\n  - `@remix-run/router@1.5.0`\n\n## 6.9.0\n\n### Minor Changes\n\n- React Router now supports an alternative way to define your route `element` and `errorElement` fields as React Components instead of React Elements. You can instead pass a React Component to the new `Component` and `ErrorBoundary` fields if you choose. There is no functional difference between the two, so use whichever approach you prefer 😀. You shouldn't be defining both, but if you do `Component`/`ErrorBoundary` will \"win\". ([#10045](https://github.com/remix-run/react-router/pull/10045))\n\n  **Example JSON Syntax**\n\n  ```jsx\n  // Both of these work the same:\n  const elementRoutes = [{\n    path: '/',\n    element: <Home />,\n    errorElement: <HomeError />,\n  }]\n\n  const componentRoutes = [{\n    path: '/',\n    Component: Home,\n    ErrorBoundary: HomeError,\n  }]\n\n  function Home() { ... }\n  function HomeError() { ... }\n  ```\n\n  **Example JSX Syntax**\n\n  ```jsx\n  // Both of these work the same:\n  const elementRoutes = createRoutesFromElements(\n    <Route path='/' element={<Home />} errorElement={<HomeError /> } />\n  );\n\n  const componentRoutes = createRoutesFromElements(\n    <Route path='/' Component={Home} ErrorBoundary={HomeError} />\n  );\n\n  function Home() { ... }\n  function HomeError() { ... }\n  ```\n\n- **Introducing Lazy Route Modules!** ([#10045](https://github.com/remix-run/react-router/pull/10045))\n\n  In order to keep your application bundles small and support code-splitting of your routes, we've introduced a new `lazy()` route property. This is an async function that resolves the non-route-matching portions of your route definition (`loader`, `action`, `element`/`Component`, `errorElement`/`ErrorBoundary`, `shouldRevalidate`, `handle`).\n\n  Lazy routes are resolved on initial load and during the `loading` or `submitting` phase of a navigation or fetcher call. You cannot lazily define route-matching properties (`path`, `index`, `children`) since we only execute your lazy route functions after we've matched known routes.\n\n  Your `lazy` functions will typically return the result of a dynamic import.\n\n  ```jsx\n  // In this example, we assume most folks land on the homepage so we include that\n  // in our critical-path bundle, but then we lazily load modules for /a and /b so\n  // they don't load until the user navigates to those routes\n  let routes = createRoutesFromElements(\n    <Route path=\"/\" element={<Layout />}>\n      <Route index element={<Home />} />\n      <Route path=\"a\" lazy={() => import(\"./a\")} />\n      <Route path=\"b\" lazy={() => import(\"./b\")} />\n    </Route>,\n  );\n  ```\n\n  Then in your lazy route modules, export the properties you want defined for the route:\n\n  ```jsx\n  export async function loader({ request }) {\n    let data = await fetchData(request);\n    return json(data);\n  }\n\n  // Export a `Component` directly instead of needing to create a React Element from it\n  export function Component() {\n    let data = useLoaderData();\n\n    return (\n      <>\n        <h1>You made it!</h1>\n        <p>{data}</p>\n      </>\n    );\n  }\n\n  // Export an `ErrorBoundary` directly instead of needing to create a React Element from it\n  export function ErrorBoundary() {\n    let error = useRouteError();\n    return isRouteErrorResponse(error) ? (\n      <h1>\n        {error.status} {error.statusText}\n      </h1>\n    ) : (\n      <h1>{error.message || error}</h1>\n    );\n  }\n  ```\n\n  An example of this in action can be found in the [`examples/lazy-loading-router-provider`](https://github.com/remix-run/react-router/tree/main/examples/lazy-loading-router-provider) directory of the repository.\n\n  🙌 Huge thanks to @rossipedia for the [Initial Proposal](https://github.com/remix-run/react-router/discussions/9826) and [POC Implementation](https://github.com/remix-run/react-router/pull/9830).\n\n- Updated dependencies:\n  - `@remix-run/router@1.4.0`\n\n### Patch Changes\n\n- Fix `generatePath` incorrectly applying parameters in some cases ([#10078](https://github.com/remix-run/react-router/pull/10078))\n- Improve memoization for context providers to avoid unnecessary re-renders ([#9983](https://github.com/remix-run/react-router/pull/9983))\n\n## 6.8.2\n\n### Patch Changes\n\n- Updated dependencies:\n  - `@remix-run/router@1.3.3`\n\n## 6.8.1\n\n### Patch Changes\n\n- Remove inaccurate console warning for POP navigations and update active blocker logic ([#10030](https://github.com/remix-run/react-router/pull/10030))\n- Updated dependencies:\n  - `@remix-run/router@1.3.2`\n\n## 6.8.0\n\n### Patch Changes\n\n- Updated dependencies:\n  - `@remix-run/router@1.3.1`\n\n## 6.7.0\n\n### Minor Changes\n\n- Add `unstable_useBlocker` hook for blocking navigations within the app's location origin ([#9709](https://github.com/remix-run/react-router/pull/9709))\n\n### Patch Changes\n\n- Fix `generatePath` when optional params are present ([#9764](https://github.com/remix-run/react-router/pull/9764))\n- Update `<Await>` to accept `ReactNode` as children function return result ([#9896](https://github.com/remix-run/react-router/pull/9896))\n- Updated dependencies:\n  - `@remix-run/router@1.3.0`\n\n## 6.6.2\n\n### Patch Changes\n\n- Ensure `useId` consistency during SSR ([#9805](https://github.com/remix-run/react-router/pull/9805))\n\n## 6.6.1\n\n### Patch Changes\n\n- Updated dependencies:\n  - `@remix-run/router@1.2.1`\n\n## 6.6.0\n\n### Patch Changes\n\n- Prevent `useLoaderData` usage in `errorElement` ([#9735](https://github.com/remix-run/react-router/pull/9735))\n- Updated dependencies:\n  - `@remix-run/router@1.2.0`\n\n## 6.5.0\n\nThis release introduces support for [Optional Route Segments](https://github.com/remix-run/react-router/issues/9546). Now, adding a `?` to the end of any path segment will make that entire segment optional. This works for both static segments and dynamic parameters.\n\n**Optional Params Examples**\n\n- `<Route path=\":lang?/about>` will match:\n  - `/:lang/about`\n  - `/about`\n- `<Route path=\"/multistep/:widget1?/widget2?/widget3?\">` will match:\n  - `/multistep`\n  - `/multistep/:widget1`\n  - `/multistep/:widget1/:widget2`\n  - `/multistep/:widget1/:widget2/:widget3`\n\n**Optional Static Segment Example**\n\n- `<Route path=\"/home?\">` will match:\n  - `/`\n  - `/home`\n- `<Route path=\"/fr?/about\">` will match:\n  - `/about`\n  - `/fr/about`\n\n### Minor Changes\n\n- Allows optional routes and optional static segments ([#9650](https://github.com/remix-run/react-router/pull/9650))\n\n### Patch Changes\n\n- Stop incorrectly matching on partial named parameters, i.e. `<Route path=\"prefix-:param\">`, to align with how splat parameters work. If you were previously relying on this behavior then it's recommended to extract the static portion of the path at the `useParams` call site: ([#9506](https://github.com/remix-run/react-router/pull/9506))\n\n```jsx\n// Old behavior at URL /prefix-123\n<Route path=\"prefix-:id\" element={<Comp /> }>\n\nfunction Comp() {\n  let params = useParams(); // { id: '123' }\n  let id = params.id; // \"123\"\n  ...\n}\n\n// New behavior at URL /prefix-123\n<Route path=\":id\" element={<Comp /> }>\n\nfunction Comp() {\n  let params = useParams(); // { id: 'prefix-123' }\n  let id = params.id.replace(/^prefix-/, ''); // \"123\"\n  ...\n}\n```\n\n- Updated dependencies:\n  - `@remix-run/router@1.1.0`\n\n## 6.4.5\n\n### Patch Changes\n\n- Updated dependencies:\n  - `@remix-run/router@1.0.5`\n\n## 6.4.4\n\n### Patch Changes\n\n- Updated dependencies:\n  - `@remix-run/router@1.0.4`\n\n## 6.4.3\n\n### Patch Changes\n\n- `useRoutes` should be able to return `null` when passing `locationArg` ([#9485](https://github.com/remix-run/react-router/pull/9485))\n- fix `initialEntries` type in `createMemoryRouter` ([#9498](https://github.com/remix-run/react-router/pull/9498))\n- Updated dependencies:\n  - `@remix-run/router@1.0.3`\n\n## 6.4.2\n\n### Patch Changes\n\n- Fix `IndexRouteObject` and `NonIndexRouteObject` types to make `hasErrorElement` optional ([#9394](https://github.com/remix-run/react-router/pull/9394))\n- Enhance console error messages for invalid usage of data router hooks ([#9311](https://github.com/remix-run/react-router/pull/9311))\n- If an index route has children, it will result in a runtime error. We have strengthened our `RouteObject`/`RouteProps` types to surface the error in TypeScript. ([#9366](https://github.com/remix-run/react-router/pull/9366))\n- Updated dependencies:\n  - `@remix-run/router@1.0.2`\n\n## 6.4.1\n\n### Patch Changes\n\n- Preserve state from `initialEntries` ([#9288](https://github.com/remix-run/react-router/pull/9288))\n- Updated dependencies:\n  - `@remix-run/router@1.0.1`\n\n## 6.4.0\n\nWhoa this is a big one! `6.4.0` brings all the data loading and mutation APIs over from Remix. Here's a quick high level overview, but it's recommended you go check out the [docs](https://reactrouter.com), especially the [feature overview](https://reactrouter.com/en/6.4.0/start/overview) and the [tutorial](https://reactrouter.com/en/6.4.0/start/tutorial).\n\n**New APIs**\n\n- Create your router with `createMemoryRouter`\n- Render your router with `<RouterProvider>`\n- Load data with a Route `loader` and mutate with a Route `action`\n- Handle errors with Route `errorElement`\n- Defer non-critical data with `defer` and `Await`\n\n**Bug Fixes**\n\n- Path resolution is now trailing slash agnostic (#8861)\n- `useLocation` returns the scoped location inside a `<Routes location>` component (#9094)\n\n**Updated Dependencies**\n\n- `@remix-run/router@1.0.0`\n"
  },
  {
    "path": "packages/react-router/README.md",
    "content": "`react-router` is the primary package in the React Router project.\n\n## Installation\n\n```sh\nnpm i react-router\n```\n"
  },
  {
    "path": "packages/react-router/__tests__/.eslintrc",
    "content": "{\n  \"env\": {\n    \"jest\": true\n  },\n  \"rules\": {\n    \"no-console\": 0,\n    \"no-restricted-globals\": \"off\",\n    \"import/no-nodejs-modules\": \"off\"\n  }\n}\n"
  },
  {
    "path": "packages/react-router/__tests__/Route-test.tsx",
    "content": "import * as React from \"react\";\nimport * as TestRenderer from \"react-test-renderer\";\nimport { MemoryRouter, Routes, Route } from \"react-router\";\n\ndescribe(\"A <Route>\", () => {\n  it(\"renders its `element` prop\", () => {\n    let renderer: TestRenderer.ReactTestRenderer;\n    TestRenderer.act(() => {\n      renderer = TestRenderer.create(\n        <MemoryRouter initialEntries={[\"/home\"]}>\n          <Routes>\n            <Route path=\"home\" element={<h1>Home</h1>} />\n          </Routes>\n        </MemoryRouter>,\n      );\n    });\n\n    expect(renderer.toJSON()).toMatchInlineSnapshot(`\n      <h1>\n        Home\n      </h1>\n    `);\n  });\n\n  it(\"renders its `Component` prop\", () => {\n    let renderer: TestRenderer.ReactTestRenderer;\n    TestRenderer.act(() => {\n      renderer = TestRenderer.create(\n        <MemoryRouter initialEntries={[\"/home\"]}>\n          <Routes>\n            <Route path=\"home\" Component={() => <h1>Home</h1>} />\n          </Routes>\n        </MemoryRouter>,\n      );\n    });\n\n    expect(renderer.toJSON()).toMatchInlineSnapshot(`\n      <h1>\n        Home\n      </h1>\n    `);\n  });\n\n  it(\"renders its child routes when no `element` prop is given\", () => {\n    let renderer: TestRenderer.ReactTestRenderer;\n    TestRenderer.act(() => {\n      renderer = TestRenderer.create(\n        <MemoryRouter initialEntries={[\"/app/home\"]}>\n          <Routes>\n            <Route path=\"app\">\n              <Route path=\"home\" element={<h1>Home</h1>} />\n            </Route>\n          </Routes>\n        </MemoryRouter>,\n      );\n    });\n\n    expect(renderer.toJSON()).toMatchInlineSnapshot(`\n      <h1>\n        Home\n      </h1>\n    `);\n  });\n});\n"
  },
  {
    "path": "packages/react-router/__tests__/Router-basename-test.tsx",
    "content": "import * as React from \"react\";\nimport * as TestRenderer from \"react-test-renderer\";\nimport { MemoryRouter, Routes, Route } from \"react-router\";\n\ndescribe(\"<Router basename>\", () => {\n  let consoleWarn: jest.SpyInstance;\n  beforeEach(() => {\n    consoleWarn = jest.spyOn(console, \"warn\").mockImplementation(() => {});\n  });\n\n  afterEach(() => {\n    consoleWarn.mockRestore();\n  });\n\n  it(\"renders null and issues a warning when the URL does not match the basename\", () => {\n    let renderer: TestRenderer.ReactTestRenderer;\n    TestRenderer.act(() => {\n      renderer = TestRenderer.create(\n        <MemoryRouter basename=\"/app\" initialEntries={[\"/home\"]}>\n          <Routes>\n            <Route path=\"/\" element={<h1>App</h1>} />\n          </Routes>\n        </MemoryRouter>,\n      );\n    });\n\n    expect(renderer.toJSON()).toBeNull();\n    expect(consoleWarn).toHaveBeenCalledTimes(1);\n    expect(consoleWarn).toHaveBeenCalledWith(\n      expect.stringContaining(\"<Router> won't render anything\"),\n    );\n  });\n\n  it(\"renders the first route that matches the URL\", () => {\n    let renderer: TestRenderer.ReactTestRenderer;\n    TestRenderer.act(() => {\n      renderer = TestRenderer.create(\n        <MemoryRouter basename=\"/home\" initialEntries={[\"/home\"]}>\n          <Routes>\n            <Route path=\"/\" element={<h1>Home</h1>} />\n          </Routes>\n        </MemoryRouter>,\n      );\n    });\n\n    expect(renderer.toJSON()).toMatchInlineSnapshot(`\n      <h1>\n        Home\n      </h1>\n    `);\n  });\n\n  it(\"does not render a 2nd route that also matches the URL\", () => {\n    let renderer: TestRenderer.ReactTestRenderer;\n    TestRenderer.act(() => {\n      renderer = TestRenderer.create(\n        <MemoryRouter basename=\"/app\" initialEntries={[\"/app/home\"]}>\n          <Routes>\n            <Route path=\"home\" element={<h1>Home</h1>} />\n            <Route path=\"home\" element={<h1>Something else</h1>} />\n          </Routes>\n        </MemoryRouter>,\n      );\n    });\n\n    expect(renderer.toJSON()).toMatchInlineSnapshot(`\n      <h1>\n        Home\n      </h1>\n    `);\n  });\n\n  it(\"matches regardless of basename casing\", () => {\n    let renderer: TestRenderer.ReactTestRenderer;\n    TestRenderer.act(() => {\n      renderer = TestRenderer.create(\n        <MemoryRouter basename=\"/HoME\" initialEntries={[\"/home\"]}>\n          <Routes>\n            <Route path=\"/\" element={<h1>Home</h1>} />\n          </Routes>\n        </MemoryRouter>,\n      );\n    });\n\n    expect(renderer.toJSON()).toMatchInlineSnapshot(`\n      <h1>\n        Home\n      </h1>\n    `);\n  });\n\n  it(\"matches regardless of URL casing\", () => {\n    let renderer: TestRenderer.ReactTestRenderer;\n    TestRenderer.act(() => {\n      renderer = TestRenderer.create(\n        <MemoryRouter basename=\"/home\" initialEntries={[\"/hOmE\"]}>\n          <Routes>\n            <Route path=\"/\" element={<h1>Home</h1>} />\n          </Routes>\n        </MemoryRouter>,\n      );\n    });\n\n    expect(renderer.toJSON()).toMatchInlineSnapshot(`\n      <h1>\n        Home\n      </h1>\n    `);\n  });\n});\n"
  },
  {
    "path": "packages/react-router/__tests__/Router-test.tsx",
    "content": "import * as React from \"react\";\nimport * as TestRenderer from \"react-test-renderer\";\nimport { MemoryRouter, useLocation } from \"react-router\";\n\ndescribe(\"<Router>\", () => {\n  let consoleError: jest.SpyInstance;\n  beforeEach(() => {\n    consoleError = jest.spyOn(console, \"error\").mockImplementation(() => {});\n  });\n\n  afterEach(() => {\n    consoleError.mockRestore();\n  });\n\n  it(\"throws if another <Router> is already in context\", () => {\n    expect(() => {\n      TestRenderer.act(() => {\n        TestRenderer.create(\n          <MemoryRouter>\n            <MemoryRouter />\n          </MemoryRouter>,\n        );\n      });\n    }).toThrow(/cannot render a <Router> inside another <Router>/);\n\n    expect(consoleError).toHaveBeenCalledTimes(1);\n  });\n\n  it(\"memoizes the current location\", () => {\n    let location1;\n    function CaptureLocation1() {\n      location1 = useLocation();\n      return null;\n    }\n\n    let renderer: TestRenderer.ReactTestRenderer;\n    TestRenderer.act(() => {\n      renderer = TestRenderer.create(\n        <MemoryRouter>\n          <CaptureLocation1 />\n        </MemoryRouter>,\n      );\n    });\n\n    expect(location1).toBeDefined();\n\n    let location2;\n    function CaptureLocation2() {\n      location2 = useLocation();\n      return null;\n    }\n\n    TestRenderer.act(() => {\n      renderer.update(\n        <MemoryRouter>\n          <CaptureLocation2 />\n        </MemoryRouter>,\n      );\n    });\n\n    expect(location2).toBeDefined();\n    expect(location1).toBe(location2);\n  });\n});\n"
  },
  {
    "path": "packages/react-router/__tests__/Routes-location-test.tsx",
    "content": "import * as React from \"react\";\nimport * as TestRenderer from \"react-test-renderer\";\nimport { MemoryRouter, Route, Routes, useParams } from \"react-router\";\n\ndescribe(\"<Routes> with a location\", () => {\n  function Home() {\n    return <h1>Home</h1>;\n  }\n\n  function User() {\n    let { userId } = useParams();\n    return (\n      <div>\n        <h1>User: {userId}</h1>\n      </div>\n    );\n  }\n\n  it(\"matches when the location is overridden\", () => {\n    let location = {\n      pathname: \"/home\",\n      search: \"\",\n      hash: \"\",\n      state: null,\n      key: \"r9qntrej\",\n    };\n\n    let renderer: TestRenderer.ReactTestRenderer;\n    TestRenderer.act(() => {\n      renderer = TestRenderer.create(\n        <MemoryRouter initialEntries={[\"/users/michael\"]}>\n          <Routes location={location}>\n            <Route path=\"home\" element={<Home />} />\n            <Route path=\"users/:userId\" element={<User />} />\n          </Routes>\n        </MemoryRouter>,\n      );\n    });\n\n    expect(renderer.toJSON()).toMatchInlineSnapshot(`\n     <h1>\n       Home\n     </h1>\n    `);\n  });\n\n  it(\"matches when the location is not overridden\", () => {\n    let renderer: TestRenderer.ReactTestRenderer;\n    TestRenderer.act(() => {\n      renderer = TestRenderer.create(\n        <MemoryRouter initialEntries={[\"/users/michael\"]}>\n          <Routes>\n            <Route path=\"home\" element={<Home />} />\n            <Route path=\"users/:userId\" element={<User />} />\n          </Routes>\n        </MemoryRouter>,\n      );\n    });\n\n    expect(renderer.toJSON()).toMatchInlineSnapshot(`\n      <div>\n        <h1>\n          User: \n          michael\n        </h1>\n      </div>\n    `);\n  });\n});\n"
  },
  {
    "path": "packages/react-router/__tests__/Routes-test.tsx",
    "content": "import * as React from \"react\";\nimport * as TestRenderer from \"react-test-renderer\";\nimport { MemoryRouter, Routes, Route } from \"react-router\";\n\ndescribe(\"<Routes>\", () => {\n  let consoleWarn: jest.SpyInstance;\n  let consoleError: jest.SpyInstance;\n  beforeEach(() => {\n    consoleWarn = jest.spyOn(console, \"warn\").mockImplementation(() => {});\n    consoleError = jest.spyOn(console, \"error\").mockImplementation(() => {});\n  });\n\n  afterEach(() => {\n    consoleWarn.mockRestore();\n    consoleError.mockRestore();\n  });\n\n  it(\"renders null and issues a warning when no routes match the URL\", () => {\n    let renderer: TestRenderer.ReactTestRenderer;\n    TestRenderer.act(() => {\n      renderer = TestRenderer.create(\n        <MemoryRouter>\n          <Routes />\n        </MemoryRouter>,\n      );\n    });\n\n    expect(renderer.toJSON()).toBeNull();\n    expect(consoleWarn).toHaveBeenCalledTimes(1);\n    expect(consoleWarn).toHaveBeenCalledWith(\n      expect.stringContaining(\"No routes matched location\"),\n    );\n  });\n\n  it(\"renders the first route that matches the URL\", () => {\n    let renderer: TestRenderer.ReactTestRenderer;\n    TestRenderer.act(() => {\n      renderer = TestRenderer.create(\n        <MemoryRouter initialEntries={[\"/\"]}>\n          <Routes>\n            <Route path=\"/\" element={<h1>Home</h1>} />\n          </Routes>\n        </MemoryRouter>,\n      );\n    });\n\n    expect(renderer.toJSON()).toMatchInlineSnapshot(`\n      <h1>\n        Home\n      </h1>\n    `);\n  });\n\n  it(\"does not render a 2nd route that also matches the URL\", () => {\n    let renderer: TestRenderer.ReactTestRenderer;\n    TestRenderer.act(() => {\n      renderer = TestRenderer.create(\n        <MemoryRouter initialEntries={[\"/home\"]}>\n          <Routes>\n            <Route path=\"home\" element={<h1>Home</h1>} />\n            <Route path=\"home\" element={<h1>Dashboard</h1>} />\n          </Routes>\n        </MemoryRouter>,\n      );\n    });\n\n    expect(renderer.toJSON()).toMatchInlineSnapshot(`\n      <h1>\n        Home\n      </h1>\n    `);\n  });\n\n  it(\"renders with non-element children\", () => {\n    let renderer: TestRenderer.ReactTestRenderer;\n    TestRenderer.act(() => {\n      renderer = TestRenderer.create(\n        <MemoryRouter initialEntries={[\"/\"]}>\n          <Routes>\n            <Route path=\"/\" element={<h1>Home</h1>} />\n            {false}\n            {undefined}\n          </Routes>\n        </MemoryRouter>,\n      );\n    });\n\n    expect(renderer.toJSON()).toMatchInlineSnapshot(`\n      <h1>\n        Home\n      </h1>\n    `);\n  });\n\n  it(\"renders with React.Fragment children\", () => {\n    let renderer: TestRenderer.ReactTestRenderer;\n    TestRenderer.act(() => {\n      renderer = TestRenderer.create(\n        <MemoryRouter initialEntries={[\"/admin\"]}>\n          <Routes>\n            <Route path=\"/\" element={<h1>Home</h1>} />\n            <React.Fragment>\n              <Route path=\"admin\" element={<h1>Admin</h1>} />\n            </React.Fragment>\n          </Routes>\n        </MemoryRouter>,\n      );\n    });\n\n    expect(renderer.toJSON()).toMatchInlineSnapshot(`\n      <h1>\n        Admin\n      </h1>\n    `);\n  });\n\n  it(\"throws if some <CustomRoute> is passed as a child of <Routes>\", () => {\n    const CustomRoute = (...args: any) => <Route />;\n\n    expect(() => {\n      TestRenderer.act(() => {\n        TestRenderer.create(\n          <MemoryRouter initialEntries={[\"/admin\"]}>\n            <Routes>\n              <Route path=\"/\" element={<h1>Home</h1>} />\n              <CustomRoute path=\"admin\" element={<h1>Admin</h1>} />\n            </Routes>\n          </MemoryRouter>,\n        );\n      });\n    }).toThrow(/children of <Routes> must be a <Route>/);\n\n    expect(consoleError).toHaveBeenCalledTimes(1);\n  });\n\n  it(\"throws if a regular element (ex: <div>) is passed as a child of <Routes>\", () => {\n    expect(() => {\n      TestRenderer.act(() => {\n        TestRenderer.create(\n          <MemoryRouter initialEntries={[\"/admin\"]}>\n            <Routes>\n              <Route path=\"/\" element={<h1>Home</h1>} />\n              <div {...({ path: \"admin\", element: <h1>Admin</h1> } as any)} />\n            </Routes>\n          </MemoryRouter>,\n        );\n      });\n    }).toThrow(/children of <Routes> must be a <Route>/);\n\n    expect(consoleError).toHaveBeenCalledTimes(1);\n  });\n});\n"
  },
  {
    "path": "packages/react-router/__tests__/__snapshots__/route-matching-test.tsx.snap",
    "content": "// Jest Snapshot v1, https://goo.gl/fbAQLP\n\nexports[`route matching using <Routes> with <Route> elements renders the right elements at / 1`] = `\n<p>\n  Home\n</p>\n`;\n\nexports[`route matching using <Routes> with <Route> elements renders the right elements at /courses 1`] = `\n<div>\n  <h1>\n    Courses\n  </h1>\n  <p>\n    All Courses\n  </p>\n</div>\n`;\n\nexports[`route matching using <Routes> with <Route> elements renders the right elements at /courses/advanced-react 1`] = `\n<p>\n  <h1>\n    Welcome to React Training\n  </h1>\n  <p>\n    Advanced React\n  </p>\n</p>\n`;\n\nexports[`route matching using <Routes> with <Route> elements renders the right elements at /courses/new 1`] = `\n<div>\n  <h1>\n    Courses\n  </h1>\n  <p>\n    New Course\n  </p>\n</div>\n`;\n\nexports[`route matching using <Routes> with <Route> elements renders the right elements at /courses/not/found 1`] = `\n<div>\n  <h1>\n    Courses\n  </h1>\n  <p>\n    Course Not Found\n  </p>\n</div>\n`;\n\nexports[`route matching using <Routes> with <Route> elements renders the right elements at /courses/react-fundamentals 1`] = `\n<p>\n  <h1>\n    Welcome to React Training\n  </h1>\n  <p>\n    React Fundamentals\n  </p>\n</p>\n`;\n\nexports[`route matching using <Routes> with <Route> elements renders the right elements at /courses/routing 1`] = `\n<div>\n  <h1>\n    Courses\n  </h1>\n  <div>\n    <h2>\n      Course \n      routing\n    </h2>\n  </div>\n</div>\n`;\n\nexports[`route matching using <Routes> with <Route> elements renders the right elements at /courses/routing/grades 1`] = `\n<div>\n  <h1>\n    Courses\n  </h1>\n  <div>\n    <h2>\n      Course \n      routing\n    </h2>\n    <p>\n      Course Grades\n    </p>\n  </div>\n</div>\n`;\n\nexports[`route matching using <Routes> with <Route> elements renders the right elements at /not-found 1`] = `\n<p>\n  Not Found\n</p>\n`;\n\nexports[`route matching using a route config object renders the right elements at / 1`] = `\n<p>\n  Home\n</p>\n`;\n\nexports[`route matching using a route config object renders the right elements at /courses 1`] = `\n<div>\n  <h1>\n    Courses\n  </h1>\n  <p>\n    All Courses\n  </p>\n</div>\n`;\n\nexports[`route matching using a route config object renders the right elements at /courses/advanced-react 1`] = `\n<p>\n  <h1>\n    Welcome to React Training\n  </h1>\n  <p>\n    Advanced React\n  </p>\n</p>\n`;\n\nexports[`route matching using a route config object renders the right elements at /courses/new 1`] = `\n<div>\n  <h1>\n    Courses\n  </h1>\n  <p>\n    New Course\n  </p>\n</div>\n`;\n\nexports[`route matching using a route config object renders the right elements at /courses/not/found 1`] = `\n<div>\n  <h1>\n    Courses\n  </h1>\n  <p>\n    Course Not Found\n  </p>\n</div>\n`;\n\nexports[`route matching using a route config object renders the right elements at /courses/react-fundamentals 1`] = `\n<p>\n  <h1>\n    Welcome to React Training\n  </h1>\n  <p>\n    React Fundamentals\n  </p>\n</p>\n`;\n\nexports[`route matching using a route config object renders the right elements at /courses/routing 1`] = `\n<div>\n  <h1>\n    Courses\n  </h1>\n  <div>\n    <h2>\n      Course \n      routing\n    </h2>\n  </div>\n</div>\n`;\n\nexports[`route matching using a route config object renders the right elements at /courses/routing/grades 1`] = `\n<div>\n  <h1>\n    Courses\n  </h1>\n  <div>\n    <h2>\n      Course \n      routing\n    </h2>\n    <p>\n      Course Grades\n    </p>\n  </div>\n</div>\n`;\n\nexports[`route matching using a route config object renders the right elements at /not-found 1`] = `\n<p>\n  Not Found\n</p>\n`;\n"
  },
  {
    "path": "packages/react-router/__tests__/absolute-path-matching-test.tsx",
    "content": "import type { RouteObject } from \"react-router\";\nimport { matchRoutes } from \"react-router\";\n\nfunction pickPaths(routes: RouteObject[], pathname: string): string[] | null {\n  let matches = matchRoutes(routes, pathname);\n  return matches && matches.map((match) => match.route.path || \"\");\n}\n\ndescribe(\"absolute path matching\", () => {\n  it(\"matches a nested route with an absolute path\", () => {\n    let routes = [\n      {\n        path: \"/users\",\n        children: [\n          { index: true },\n          { path: \"add\" },\n          { path: \"remove\" },\n          { path: \"/users/:id\" },\n        ],\n      },\n    ];\n\n    expect(pickPaths(routes, \"/users\")).toEqual([\"/users\", \"\"]);\n    expect(pickPaths(routes, \"/users/add\")).toEqual([\"/users\", \"add\"]);\n    expect(pickPaths(routes, \"/users/remove\")).toEqual([\"/users\", \"remove\"]);\n    expect(pickPaths(routes, \"/users/123\")).toEqual([\"/users\", \"/users/:id\"]);\n  });\n\n  it(\"matches a nested splat route with an absolute path\", () => {\n    let routes = [\n      {\n        path: \"/users\",\n        children: [{ path: \"/users/*\" }],\n      },\n    ];\n\n    expect(pickPaths(routes, \"/users\")).toEqual([\"/users\"]);\n    expect(pickPaths(routes, \"/users/not-found\")).toEqual([\n      \"/users\",\n      \"/users/*\",\n    ]);\n  });\n\n  it(\"throws when the nested path does not begin with its parent path\", () => {\n    expect(() => {\n      matchRoutes(\n        [\n          {\n            path: \"/users\",\n            children: [\n              { path: \":id\" },\n              // This one should throw because it doesn't begin with /users\n              { path: \"/not/users\" },\n            ],\n          },\n        ],\n        \"/users/123\",\n      );\n    }).toThrow(\"absolute child route path must start\");\n  });\n});\n"
  },
  {
    "path": "packages/react-router/__tests__/createRoutesFromChildren-test.tsx",
    "content": "import * as React from \"react\";\nimport { Route, createRoutesFromChildren } from \"react-router\";\n\ndescribe(\"creating routes from JSX\", () => {\n  it(\"creates a route config of nested JavaScript objects\", () => {\n    expect(\n      createRoutesFromChildren(\n        <Route path=\"/\">\n          <Route path=\"home\" element={<h1>home</h1>} />\n          <Route path=\"about\" element={<h1>about</h1>} />\n          <Route path=\"users\">\n            <Route index element={<h1>users index</h1>} />\n            <Route path=\":id\" element={<h1>user profile</h1>} />\n          </Route>\n        </Route>,\n      ),\n    ).toMatchInlineSnapshot(`\n      [\n        {\n          \"Component\": undefined,\n          \"ErrorBoundary\": undefined,\n          \"HydrateFallback\": undefined,\n          \"action\": undefined,\n          \"caseSensitive\": undefined,\n          \"children\": [\n            {\n              \"Component\": undefined,\n              \"ErrorBoundary\": undefined,\n              \"HydrateFallback\": undefined,\n              \"action\": undefined,\n              \"caseSensitive\": undefined,\n              \"element\": <h1>\n                home\n              </h1>,\n              \"errorElement\": undefined,\n              \"handle\": undefined,\n              \"hasErrorBoundary\": false,\n              \"hydrateFallbackElement\": undefined,\n              \"id\": \"0-0\",\n              \"index\": undefined,\n              \"lazy\": undefined,\n              \"loader\": undefined,\n              \"middleware\": undefined,\n              \"path\": \"home\",\n              \"shouldRevalidate\": undefined,\n            },\n            {\n              \"Component\": undefined,\n              \"ErrorBoundary\": undefined,\n              \"HydrateFallback\": undefined,\n              \"action\": undefined,\n              \"caseSensitive\": undefined,\n              \"element\": <h1>\n                about\n              </h1>,\n              \"errorElement\": undefined,\n              \"handle\": undefined,\n              \"hasErrorBoundary\": false,\n              \"hydrateFallbackElement\": undefined,\n              \"id\": \"0-1\",\n              \"index\": undefined,\n              \"lazy\": undefined,\n              \"loader\": undefined,\n              \"middleware\": undefined,\n              \"path\": \"about\",\n              \"shouldRevalidate\": undefined,\n            },\n            {\n              \"Component\": undefined,\n              \"ErrorBoundary\": undefined,\n              \"HydrateFallback\": undefined,\n              \"action\": undefined,\n              \"caseSensitive\": undefined,\n              \"children\": [\n                {\n                  \"Component\": undefined,\n                  \"ErrorBoundary\": undefined,\n                  \"HydrateFallback\": undefined,\n                  \"action\": undefined,\n                  \"caseSensitive\": undefined,\n                  \"element\": <h1>\n                    users index\n                  </h1>,\n                  \"errorElement\": undefined,\n                  \"handle\": undefined,\n                  \"hasErrorBoundary\": false,\n                  \"hydrateFallbackElement\": undefined,\n                  \"id\": \"0-2-0\",\n                  \"index\": true,\n                  \"lazy\": undefined,\n                  \"loader\": undefined,\n                  \"middleware\": undefined,\n                  \"path\": undefined,\n                  \"shouldRevalidate\": undefined,\n                },\n                {\n                  \"Component\": undefined,\n                  \"ErrorBoundary\": undefined,\n                  \"HydrateFallback\": undefined,\n                  \"action\": undefined,\n                  \"caseSensitive\": undefined,\n                  \"element\": <h1>\n                    user profile\n                  </h1>,\n                  \"errorElement\": undefined,\n                  \"handle\": undefined,\n                  \"hasErrorBoundary\": false,\n                  \"hydrateFallbackElement\": undefined,\n                  \"id\": \"0-2-1\",\n                  \"index\": undefined,\n                  \"lazy\": undefined,\n                  \"loader\": undefined,\n                  \"middleware\": undefined,\n                  \"path\": \":id\",\n                  \"shouldRevalidate\": undefined,\n                },\n              ],\n              \"element\": undefined,\n              \"errorElement\": undefined,\n              \"handle\": undefined,\n              \"hasErrorBoundary\": false,\n              \"hydrateFallbackElement\": undefined,\n              \"id\": \"0-2\",\n              \"index\": undefined,\n              \"lazy\": undefined,\n              \"loader\": undefined,\n              \"middleware\": undefined,\n              \"path\": \"users\",\n              \"shouldRevalidate\": undefined,\n            },\n          ],\n          \"element\": undefined,\n          \"errorElement\": undefined,\n          \"handle\": undefined,\n          \"hasErrorBoundary\": false,\n          \"hydrateFallbackElement\": undefined,\n          \"id\": \"0\",\n          \"index\": undefined,\n          \"lazy\": undefined,\n          \"loader\": undefined,\n          \"middleware\": undefined,\n          \"path\": \"/\",\n          \"shouldRevalidate\": undefined,\n        },\n      ]\n    `);\n  });\n\n  it(\"creates a data-aware route config of nested JavaScript objects\", () => {\n    expect(\n      createRoutesFromChildren(\n        <Route errorElement={<h1>💥</h1>} path=\"/\">\n          <Route\n            path=\"home\"\n            loader={async () => {}}\n            shouldRevalidate={() => true}\n            element={<h1>home</h1>}\n          />\n\n          <Route path=\"users\">\n            <Route\n              index\n              action={async () => {}}\n              element={<h1>users index</h1>}\n            />\n          </Route>\n        </Route>,\n      ),\n    ).toMatchInlineSnapshot(`\n      [\n        {\n          \"Component\": undefined,\n          \"ErrorBoundary\": undefined,\n          \"HydrateFallback\": undefined,\n          \"action\": undefined,\n          \"caseSensitive\": undefined,\n          \"children\": [\n            {\n              \"Component\": undefined,\n              \"ErrorBoundary\": undefined,\n              \"HydrateFallback\": undefined,\n              \"action\": undefined,\n              \"caseSensitive\": undefined,\n              \"element\": <h1>\n                home\n              </h1>,\n              \"errorElement\": undefined,\n              \"handle\": undefined,\n              \"hasErrorBoundary\": false,\n              \"hydrateFallbackElement\": undefined,\n              \"id\": \"0-0\",\n              \"index\": undefined,\n              \"lazy\": undefined,\n              \"loader\": [Function],\n              \"middleware\": undefined,\n              \"path\": \"home\",\n              \"shouldRevalidate\": [Function],\n            },\n            {\n              \"Component\": undefined,\n              \"ErrorBoundary\": undefined,\n              \"HydrateFallback\": undefined,\n              \"action\": undefined,\n              \"caseSensitive\": undefined,\n              \"children\": [\n                {\n                  \"Component\": undefined,\n                  \"ErrorBoundary\": undefined,\n                  \"HydrateFallback\": undefined,\n                  \"action\": [Function],\n                  \"caseSensitive\": undefined,\n                  \"element\": <h1>\n                    users index\n                  </h1>,\n                  \"errorElement\": undefined,\n                  \"handle\": undefined,\n                  \"hasErrorBoundary\": false,\n                  \"hydrateFallbackElement\": undefined,\n                  \"id\": \"0-1-0\",\n                  \"index\": true,\n                  \"lazy\": undefined,\n                  \"loader\": undefined,\n                  \"middleware\": undefined,\n                  \"path\": undefined,\n                  \"shouldRevalidate\": undefined,\n                },\n              ],\n              \"element\": undefined,\n              \"errorElement\": undefined,\n              \"handle\": undefined,\n              \"hasErrorBoundary\": false,\n              \"hydrateFallbackElement\": undefined,\n              \"id\": \"0-1\",\n              \"index\": undefined,\n              \"lazy\": undefined,\n              \"loader\": undefined,\n              \"middleware\": undefined,\n              \"path\": \"users\",\n              \"shouldRevalidate\": undefined,\n            },\n          ],\n          \"element\": undefined,\n          \"errorElement\": <h1>\n            💥\n          </h1>,\n          \"handle\": undefined,\n          \"hasErrorBoundary\": true,\n          \"hydrateFallbackElement\": undefined,\n          \"id\": \"0\",\n          \"index\": undefined,\n          \"lazy\": undefined,\n          \"loader\": undefined,\n          \"middleware\": undefined,\n          \"path\": \"/\",\n          \"shouldRevalidate\": undefined,\n        },\n      ]\n    `);\n  });\n\n  it(\"throws when the index route has children\", () => {\n    expect(() => {\n      createRoutesFromChildren(\n        <Route path=\"/\">\n          {/* @ts-expect-error */}\n          <Route index>\n            <Route path=\"users\" />\n          </Route>\n        </Route>,\n      );\n    }).toThrow(\"An index route cannot have child routes.\");\n  });\n\n  it(\"supports react fragments for automatic ID generation\", () => {\n    expect(\n      createRoutesFromChildren(\n        <Route path=\"/\">\n          <Route index />\n          <>\n            <Route path=\"a\">\n              <>\n                <Route path=\"1\" />\n                <Route path=\"2\" />\n              </>\n            </Route>\n            <Route path=\"b\" />\n          </>\n        </Route>,\n      ),\n    ).toEqual([\n      {\n        id: \"0\",\n        path: \"/\",\n        hasErrorBoundary: false,\n        children: [\n          {\n            id: \"0-0\",\n            index: true,\n            hasErrorBoundary: false,\n          },\n          {\n            id: \"0-1-0\",\n            path: \"a\",\n            hasErrorBoundary: false,\n            children: [\n              {\n                id: \"0-1-0-0-0\",\n                path: \"1\",\n                hasErrorBoundary: false,\n              },\n              {\n                id: \"0-1-0-0-1\",\n                path: \"2\",\n                hasErrorBoundary: false,\n              },\n            ],\n          },\n          {\n            id: \"0-1-1\",\n            path: \"b\",\n            hasErrorBoundary: false,\n          },\n        ],\n      },\n    ]);\n  });\n});\n"
  },
  {
    "path": "packages/react-router/__tests__/data-memory-router-test.tsx",
    "content": "import \"@testing-library/jest-dom\";\nimport {\n  fireEvent,\n  queryByText,\n  render,\n  screen,\n  waitFor,\n} from \"@testing-library/react\";\nimport * as React from \"react\";\nimport {\n  Await,\n  MemoryRouter,\n  Outlet,\n  Route,\n  RouterProvider,\n  Routes,\n  createMemoryRouter,\n  createRoutesFromElements,\n  redirect,\n  useActionData,\n  useAsyncError,\n  useAsyncValue,\n  useLoaderData,\n  useLocation,\n  useMatches,\n  useNavigation,\n  useRouteError,\n  useRouteLoaderData,\n} from \"react-router\";\n\nimport {\n  useFetcher,\n  useNavigate,\n  useRevalidator,\n  useSubmit,\n  type ErrorResponse,\n} from \"../index\";\nimport urlDataStrategy from \"./router/utils/urlDataStrategy\";\nimport { createDeferred } from \"./router/utils/utils\";\nimport MemoryNavigate from \"./utils/MemoryNavigate\";\nimport getHtml from \"./utils/getHtml\";\n\ndescribe(\"createMemoryRouter\", () => {\n  let consoleWarn: jest.SpyInstance;\n  let consoleError: jest.SpyInstance;\n\n  beforeEach(() => {\n    consoleWarn = jest.spyOn(console, \"warn\").mockImplementation(() => {});\n    consoleError = jest.spyOn(console, \"error\").mockImplementation(() => {});\n  });\n\n  afterEach(() => {\n    consoleWarn.mockRestore();\n    consoleError.mockRestore();\n  });\n\n  it(\"renders the first route that matches the URL (element)\", () => {\n    let router = createMemoryRouter(\n      createRoutesFromElements(<Route path=\"/\" element={<h1>Home</h1>} />),\n    );\n    let { container } = render(<RouterProvider router={router} />);\n\n    expect(getHtml(container)).toMatchInlineSnapshot(`\n      \"<div>\n        <h1>\n          Home\n        </h1>\n      </div>\"\n    `);\n  });\n\n  it(\"renders the first route that matches the URL (Component)\", () => {\n    let router = createMemoryRouter(\n      createRoutesFromElements(\n        <Route path=\"/\" Component={() => <h1>Home</h1>} />,\n      ),\n    );\n    let { container } = render(<RouterProvider router={router} />);\n\n    expect(getHtml(container)).toMatchInlineSnapshot(`\n      \"<div>\n        <h1>\n          Home\n        </h1>\n      </div>\"\n    `);\n  });\n\n  it(\"supports a `routes` prop instead of <Route /> children\", () => {\n    let router = createMemoryRouter([\n      {\n        path: \"/\",\n        element: <h1>Home</h1>,\n      },\n    ]);\n    let { container } = render(<RouterProvider router={router} />);\n\n    expect(getHtml(container)).toMatchInlineSnapshot(`\n      \"<div>\n        <h1>\n          Home\n        </h1>\n      </div>\"\n    `);\n  });\n\n  it(\"renders the first route that matches the URL when wrapped in a root route\", () => {\n    let router = createMemoryRouter(\n      createRoutesFromElements(\n        <Route path=\"/my/base/path\">\n          <Route path=\"thing\" element={<h1>Heyooo</h1>} />\n        </Route>,\n      ),\n      {\n        initialEntries: [\"/my/base/path/thing\"],\n      },\n    );\n    let { container } = render(<RouterProvider router={router} />);\n\n    expect(getHtml(container)).toMatchInlineSnapshot(`\n      \"<div>\n        <h1>\n          Heyooo\n        </h1>\n      </div>\"\n    `);\n  });\n\n  it(\"supports a basename prop\", () => {\n    let router = createMemoryRouter(\n      createRoutesFromElements(\n        <Route path=\"thing\" element={<h1>Heyooo</h1>} />,\n      ),\n      {\n        basename: \"/my/base/path\",\n        initialEntries: [\"/my/base/path/thing\"],\n      },\n    );\n    let { container } = render(<RouterProvider router={router} />);\n\n    expect(getHtml(container)).toMatchInlineSnapshot(`\n      \"<div>\n        <h1>\n          Heyooo\n        </h1>\n      </div>\"\n    `);\n  });\n\n  it(\"prepends basename to loader/action redirects\", async () => {\n    let router = createMemoryRouter(\n      createRoutesFromElements(\n        <Route path=\"/\" element={<Root />}>\n          <Route path=\"thing\" loader={() => redirect(\"/other\")} />\n          <Route path=\"other\" element={<h1>Other</h1>} />\n        </Route>,\n      ),\n      {\n        basename: \"/my/base/path\",\n        initialEntries: [\"/my/base/path\"],\n      },\n    );\n    let { container } = render(<RouterProvider router={router} />);\n\n    function Root() {\n      return (\n        <>\n          <MemoryNavigate to=\"/thing\">Link to thing</MemoryNavigate>\n          <Outlet />\n        </>\n      );\n    }\n\n    expect(getHtml(container)).toMatchInlineSnapshot(`\n      \"<div>\n        <a\n          href=\"/my/base/path/thing\"\n        >\n          Link to thing\n        </a>\n      </div>\"\n    `);\n\n    fireEvent.click(screen.getByText(\"Link to thing\"));\n    await waitFor(() => screen.getByText(\"Other\"));\n    expect(getHtml(container)).toMatchInlineSnapshot(`\n      \"<div>\n        <a\n          href=\"/my/base/path/thing\"\n        >\n          Link to thing\n        </a>\n        <h1>\n          Other\n        </h1>\n      </div>\"\n    `);\n  });\n\n  it(\"supports relative routing in loader/action redirects\", async () => {\n    let router = createMemoryRouter(\n      createRoutesFromElements(\n        <Route path=\"/\" element={<Root />}>\n          <Route path=\"parent\" element={<Parent />}>\n            <Route path=\"child\" loader={() => redirect(\"../other\")} />\n            <Route path=\"other\" element={<h2>Other</h2>} />\n          </Route>\n        </Route>,\n      ),\n      {\n        basename: \"/my/base/path\",\n        initialEntries: [\"/my/base/path\"],\n      },\n    );\n    let { container } = render(<RouterProvider router={router} />);\n\n    function Root() {\n      return (\n        <>\n          <MemoryNavigate to=\"/parent/child\">Link to child</MemoryNavigate>\n          <Outlet />\n        </>\n      );\n    }\n\n    function Parent() {\n      return (\n        <>\n          <h1>Parent</h1>\n          <Outlet />\n        </>\n      );\n    }\n\n    expect(getHtml(container)).toMatchInlineSnapshot(`\n      \"<div>\n        <a\n          href=\"/my/base/path/parent/child\"\n        >\n          Link to child\n        </a>\n      </div>\"\n    `);\n\n    fireEvent.click(screen.getByText(\"Link to child\"));\n    await waitFor(() => screen.getByText(\"Parent\"));\n    expect(getHtml(container)).toMatchInlineSnapshot(`\n      \"<div>\n        <a\n          href=\"/my/base/path/parent/child\"\n        >\n          Link to child\n        </a>\n        <h1>\n          Parent\n        </h1>\n        <h2>\n          Other\n        </h2>\n      </div>\"\n    `);\n  });\n\n  it(\"renders with hydration data\", async () => {\n    let router = createMemoryRouter(\n      createRoutesFromElements(\n        <Route path=\"/\" element={<Comp />}>\n          <Route path=\"child\" element={<Comp />} />\n        </Route>,\n      ),\n      {\n        initialEntries: [\"/child\"],\n        hydrationData: {\n          loaderData: {\n            \"0\": \"parent data\",\n            \"0-0\": \"child data\",\n          },\n          actionData: {\n            \"0-0\": \"child action\",\n          },\n        },\n      },\n    );\n    let { container } = render(<RouterProvider router={router} />);\n\n    function Comp() {\n      let data = useLoaderData() as { message: string };\n      let actionData = useActionData();\n      let navigation = useNavigation();\n      return (\n        <div>\n          <>{data}</>\n          <>{actionData}</>\n          <>{navigation.state}</>\n          <Outlet />\n        </div>\n      );\n    }\n\n    expect(getHtml(container)).toMatchInlineSnapshot(`\n      \"<div>\n        <div>\n          parent data\n          idle\n          <div>\n            child data\n            child action\n            idle\n          </div>\n        </div>\n      </div>\"\n    `);\n  });\n\n  it(\"renders hydrateFallbackElement while first data fetch happens\", async () => {\n    let fooDefer = createDeferred();\n    let router = createMemoryRouter(\n      createRoutesFromElements(\n        <Route\n          path=\"/\"\n          element={<Outlet />}\n          hydrateFallbackElement={<FallbackElement />}\n        >\n          <Route path=\"foo\" loader={() => fooDefer.promise} element={<Foo />} />\n          <Route path=\"bar\" element={<Bar />} />\n        </Route>,\n      ),\n      {\n        initialEntries: [\"/foo\"],\n      },\n    );\n    let { container } = render(<RouterProvider router={router} />);\n\n    function FallbackElement() {\n      return <p>Loading...</p>;\n    }\n\n    function Foo() {\n      let data = useLoaderData() as { message: string };\n      return <h1>Foo:{data?.message}</h1>;\n    }\n\n    function Bar() {\n      return <h1>Bar Heading</h1>;\n    }\n\n    expect(getHtml(container)).toMatchInlineSnapshot(`\n      \"<div>\n        <p>\n          Loading...\n        </p>\n      </div>\"\n    `);\n\n    fooDefer.resolve({ message: \"From Foo Loader\" });\n    await waitFor(() => screen.getByText(\"Foo:From Foo Loader\"));\n    expect(getHtml(container)).toMatchInlineSnapshot(`\n      \"<div>\n        <h1>\n          Foo:\n          From Foo Loader\n        </h1>\n      </div>\"\n    `);\n  });\n\n  it(\"renders a null fallback if none is provided\", async () => {\n    let fooDefer = createDeferred();\n    let router = createMemoryRouter(\n      createRoutesFromElements(\n        <Route path=\"/\" element={<Outlet />}>\n          <Route path=\"foo\" loader={() => fooDefer.promise} element={<Foo />} />\n          <Route path=\"bar\" element={<Bar />} />\n        </Route>,\n      ),\n      {\n        initialEntries: [\"/foo\"],\n      },\n    );\n    let { container } = render(<RouterProvider router={router} />);\n\n    function Foo() {\n      let data = useLoaderData() as { message: string };\n      return <h1>Foo:{data?.message}</h1>;\n    }\n\n    function Bar() {\n      return <h1>Bar Heading</h1>;\n    }\n\n    expect(getHtml(container)).toMatchInlineSnapshot(`\"<div />\"`);\n\n    fooDefer.resolve({ message: \"From Foo Loader\" });\n    await waitFor(() => screen.getByText(\"Foo:From Foo Loader\"));\n    expect(getHtml(container)).toMatchInlineSnapshot(`\n      \"<div>\n        <h1>\n          Foo:\n          From Foo Loader\n        </h1>\n      </div>\"\n    `);\n  });\n\n  it(\"does not render hydrateFallbackElement if no data fetch is required\", async () => {\n    let fooDefer = createDeferred();\n\n    let router = createMemoryRouter(\n      createRoutesFromElements(\n        <Route\n          path=\"/\"\n          element={<Outlet />}\n          hydrateFallbackElement={<FallbackElement />}\n        >\n          <Route path=\"foo\" loader={() => fooDefer.promise} element={<Foo />} />\n          <Route path=\"bar\" element={<Bar />} />\n        </Route>,\n      ),\n      {\n        initialEntries: [\"/bar\"],\n      },\n    );\n    let { container } = render(<RouterProvider router={router} />);\n\n    function FallbackElement() {\n      return <p>Loading...</p>;\n    }\n\n    function Foo() {\n      let data = useLoaderData() as { message: string };\n      return <h1>Foo:{data?.message}</h1>;\n    }\n\n    function Bar() {\n      return <h1>Bar Heading</h1>;\n    }\n\n    expect(getHtml(container)).toMatchInlineSnapshot(`\n      \"<div>\n        <h1>\n          Bar Heading\n        </h1>\n      </div>\"\n    `);\n  });\n\n  it(\"renders hydrateFallbackElement within router contexts\", async () => {\n    let fooDefer = createDeferred();\n    let router = createMemoryRouter(\n      createRoutesFromElements(\n        <Route\n          path=\"/\"\n          element={<Outlet />}\n          hydrateFallbackElement={<FallbackElement />}\n        >\n          <Route path=\"foo\" loader={() => fooDefer.promise} element={<Foo />} />\n        </Route>,\n      ),\n      { initialEntries: [\"/foo\"] },\n    );\n    let { container } = render(<RouterProvider router={router} />);\n\n    function FallbackElement() {\n      let location = useLocation();\n      return (\n        <>\n          <p>Loading{location.pathname}</p>\n        </>\n      );\n    }\n\n    function Foo() {\n      let data = useLoaderData() as { message: string };\n      return <h1>Foo:{data?.message}</h1>;\n    }\n\n    expect(getHtml(container)).toMatchInlineSnapshot(`\n      \"<div>\n        <p>\n          Loading\n          /foo\n        </p>\n      </div>\"\n    `);\n\n    fooDefer.resolve({ message: \"From Foo Loader\" });\n    await waitFor(() => screen.getByText(\"Foo:From Foo Loader\"));\n    expect(getHtml(container)).toMatchInlineSnapshot(`\n      \"<div>\n        <h1>\n          Foo:\n          From Foo Loader\n        </h1>\n      </div>\"\n    `);\n  });\n\n  it(\"handles link navigations\", async () => {\n    let router = createMemoryRouter(\n      createRoutesFromElements(\n        <Route path=\"/\" element={<Layout />}>\n          <Route path=\"foo\" element={<Foo />} />\n          <Route path=\"bar\" element={<Bar />} />\n        </Route>,\n      ),\n      { initialEntries: [\"/foo\"] },\n    );\n    render(<RouterProvider router={router} />);\n\n    function Layout() {\n      return (\n        <div>\n          <MemoryNavigate to=\"/foo\">Link to Foo</MemoryNavigate>\n          <MemoryNavigate to=\"/bar\">Link to Bar</MemoryNavigate>\n          <Outlet />\n        </div>\n      );\n    }\n\n    function Foo() {\n      return <h1>Foo Heading</h1>;\n    }\n\n    function Bar() {\n      return <h1>Bar Heading</h1>;\n    }\n\n    expect(screen.getByText(\"Foo Heading\")).toBeDefined();\n    fireEvent.click(screen.getByText(\"Link to Bar\"));\n    await waitFor(() => screen.getByText(\"Bar Heading\"));\n\n    fireEvent.click(screen.getByText(\"Link to Foo\"));\n    await waitFor(() => screen.getByText(\"Foo Heading\"));\n  });\n\n  it(\"executes route loaders on navigation\", async () => {\n    let barDefer = createDeferred();\n    let router = createMemoryRouter(\n      createRoutesFromElements(\n        <Route path=\"/\" element={<Layout />}>\n          <Route path=\"foo\" element={<Foo />} />\n          <Route path=\"bar\" loader={() => barDefer.promise} element={<Bar />} />\n        </Route>,\n      ),\n      { initialEntries: [\"/foo\"] },\n    );\n    let { container } = render(<RouterProvider router={router} />);\n\n    function Layout() {\n      let navigation = useNavigation();\n      return (\n        <div>\n          <MemoryNavigate to=\"/bar\">Link to Bar</MemoryNavigate>\n          <p>{navigation.state}</p>\n          <Outlet />\n        </div>\n      );\n    }\n\n    function Foo() {\n      return <h1>Foo</h1>;\n    }\n    function Bar() {\n      let data = useLoaderData() as { message: string };\n      return <h1>{data?.message}</h1>;\n    }\n\n    expect(getHtml(container)).toMatchInlineSnapshot(`\n      \"<div>\n        <div>\n          <a\n            href=\"/bar\"\n          >\n            Link to Bar\n          </a>\n          <p>\n            idle\n          </p>\n          <h1>\n            Foo\n          </h1>\n        </div>\n      </div>\"\n    `);\n\n    fireEvent.click(screen.getByText(\"Link to Bar\"));\n    expect(getHtml(container)).toMatchInlineSnapshot(`\n      \"<div>\n        <div>\n          <a\n            href=\"/bar\"\n          >\n            Link to Bar\n          </a>\n          <p>\n            loading\n          </p>\n          <h1>\n            Foo\n          </h1>\n        </div>\n      </div>\"\n    `);\n\n    barDefer.resolve({ message: \"Bar Loader\" });\n    await waitFor(() => screen.getByText(\"idle\"));\n    expect(getHtml(container)).toMatchInlineSnapshot(`\n      \"<div>\n        <div>\n          <a\n            href=\"/bar\"\n          >\n            Link to Bar\n          </a>\n          <p>\n            idle\n          </p>\n          <h1>\n            Bar Loader\n          </h1>\n        </div>\n      </div>\"\n    `);\n  });\n\n  it(\"executes route actions/loaders on submission navigations\", async () => {\n    let barDefer = createDeferred();\n    let barActionDefer = createDeferred();\n    let formData = new FormData();\n    formData.append(\"test\", \"value\");\n\n    let router = createMemoryRouter(\n      createRoutesFromElements(\n        <Route path=\"/\" element={<Layout />}>\n          <Route path=\"foo\" element={<Foo />} />\n          <Route\n            path=\"bar\"\n            action={() => barActionDefer.promise}\n            loader={() => barDefer.promise}\n            element={<Bar />}\n          />\n        </Route>,\n      ),\n      { initialEntries: [\"/foo\"] },\n    );\n    let { container } = render(<RouterProvider router={router} />);\n\n    function Layout() {\n      let navigation = useNavigation();\n      return (\n        <div>\n          <MemoryNavigate to=\"/bar\" formMethod=\"post\" formData={formData}>\n            Post to Bar\n          </MemoryNavigate>\n          <p>{navigation.state}</p>\n          <Outlet />\n        </div>\n      );\n    }\n\n    function Foo() {\n      return <h1>Foo</h1>;\n    }\n    function Bar() {\n      let data = useLoaderData() as { message: string };\n      let actionData = useActionData();\n      return (\n        <h1>\n          <>{data}</>\n          <>{actionData}</>\n        </h1>\n      );\n    }\n\n    expect(getHtml(container)).toMatchInlineSnapshot(`\n      \"<div>\n        <div>\n          <form>\n            Post to Bar\n          </form>\n          <p>\n            idle\n          </p>\n          <h1>\n            Foo\n          </h1>\n        </div>\n      </div>\"\n    `);\n\n    fireEvent.click(screen.getByText(\"Post to Bar\"));\n    expect(getHtml(container)).toMatchInlineSnapshot(`\n      \"<div>\n        <div>\n          <form>\n            Post to Bar\n          </form>\n          <p>\n            submitting\n          </p>\n          <h1>\n            Foo\n          </h1>\n        </div>\n      </div>\"\n    `);\n\n    barActionDefer.resolve(\"Bar Action\");\n    await waitFor(() => screen.getByText(\"loading\"));\n    expect(getHtml(container)).toMatchInlineSnapshot(`\n      \"<div>\n        <div>\n          <form>\n            Post to Bar\n          </form>\n          <p>\n            loading\n          </p>\n          <h1>\n            Foo\n          </h1>\n        </div>\n      </div>\"\n    `);\n\n    barDefer.resolve(\"Bar Loader\");\n    await waitFor(() => screen.getByText(\"idle\"));\n    expect(getHtml(container)).toMatchInlineSnapshot(`\n      \"<div>\n        <div>\n          <form>\n            Post to Bar\n          </form>\n          <p>\n            idle\n          </p>\n          <h1>\n            Bar Loader\n            Bar Action\n          </h1>\n        </div>\n      </div>\"\n    `);\n  });\n\n  it(\"provides useMatches\", async () => {\n    let spy = jest.fn();\n\n    let router = createMemoryRouter(\n      createRoutesFromElements(\n        <Route path=\"/\" element={<Layout />}>\n          <Route\n            path=\"foo\"\n            loader={async () => \"FOO LOADER\"}\n            element={<Foo />}\n          />\n          <Route\n            path=\"bar\"\n            loader={async () => \"BAR LOADER\"}\n            element={<Bar />}\n            handle={{ key: \"value\" }}\n          />\n        </Route>,\n      ),\n    );\n    render(<RouterProvider router={router} />);\n\n    function Layout() {\n      spy(\"Layout\", useMatches());\n      return (\n        <>\n          <MemoryNavigate to=\"/bar\">Link to Bar</MemoryNavigate>\n          <Outlet />\n        </>\n      );\n    }\n\n    function Foo() {\n      spy(\"Foo\", useMatches());\n      return <h1>Foo</h1>;\n    }\n\n    function Bar() {\n      spy(\"Bar\", useMatches());\n      return <h1>Bar</h1>;\n    }\n\n    expect(spy).toHaveBeenCalledWith(\"Layout\", [\n      {\n        data: undefined,\n        handle: undefined,\n        id: \"0\",\n        params: {},\n        pathname: \"/\",\n      },\n    ]);\n\n    spy.mockClear();\n    fireEvent.click(screen.getByText(\"Link to Bar\"));\n    expect(spy).toHaveBeenCalledWith(\"Layout\", [\n      {\n        data: undefined,\n        handle: undefined,\n        id: \"0\",\n        params: {},\n        pathname: \"/\",\n      },\n    ]);\n\n    spy.mockClear();\n    await waitFor(() => screen.getByText(\"Bar\"));\n    expect(spy).toHaveBeenCalledWith(\"Layout\", [\n      {\n        data: undefined,\n        loaderData: undefined,\n        handle: undefined,\n        id: \"0\",\n        params: {},\n        pathname: \"/\",\n      },\n      {\n        data: \"BAR LOADER\",\n        loaderData: \"BAR LOADER\",\n        handle: {\n          key: \"value\",\n        },\n        id: \"0-1\",\n        params: {},\n        pathname: \"/bar\",\n      },\n    ]);\n  });\n\n  it(\"provides useRouteLoaderData\", async () => {\n    let spy = jest.fn();\n\n    let router = createMemoryRouter(\n      createRoutesFromElements(\n        <Route id=\"layout\" path=\"/\" element={<Layout />}>\n          <Route\n            id=\"foo\"\n            path=\"foo\"\n            loader={async () => \"FOO2\"}\n            element={<h2>Foo</h2>}\n          />\n          <Route id=\"bar\" path=\"bar\" element={<Outlet />}>\n            <Route\n              id=\"child\"\n              path=\"child\"\n              loader={async () => \"CHILD\"}\n              element={<h2>Child</h2>}\n            />\n          </Route>\n        </Route>,\n      ),\n      {\n        initialEntries: [\"/foo\"],\n        hydrationData: {\n          loaderData: {\n            foo: \"FOO\",\n          },\n        },\n      },\n    );\n    let { container } = render(<RouterProvider router={router} />);\n\n    function Layout() {\n      spy({\n        layout: useRouteLoaderData(\"layout\"),\n        foo: useRouteLoaderData(\"foo\"),\n        bar: useRouteLoaderData(\"bar\"),\n        child: useRouteLoaderData(\"child\"),\n      });\n      return (\n        <>\n          <MemoryNavigate to=\"/bar/child\">Link to Child</MemoryNavigate>\n          <Outlet />\n        </>\n      );\n    }\n\n    expect(spy).toHaveBeenCalledWith({\n      layout: undefined,\n      foo: \"FOO\",\n      bar: undefined,\n      child: undefined,\n    });\n    expect(getHtml(container)).toMatchInlineSnapshot(`\n      \"<div>\n        <a\n          href=\"/bar/child\"\n        >\n          Link to Child\n        </a>\n        <h2>\n          Foo\n        </h2>\n      </div>\"\n    `);\n\n    spy.mockClear();\n    fireEvent.click(screen.getByText(\"Link to Child\"));\n    await waitFor(() => screen.getByText(\"Child\"));\n\n    expect(getHtml(container)).toMatchInlineSnapshot(`\n      \"<div>\n        <a\n          href=\"/bar/child\"\n        >\n          Link to Child\n        </a>\n        <h2>\n          Child\n        </h2>\n      </div>\"\n    `);\n    expect(spy).toHaveBeenCalledWith({\n      layout: undefined,\n      foo: undefined,\n      bar: undefined,\n      child: \"CHILD\",\n    });\n  });\n\n  it(\"renders descendent routes inside a data router\", () => {\n    let router = createMemoryRouter(\n      createRoutesFromElements(\n        <Route path=\"/deep\">\n          <Route path=\"path/*\" element={<Child />} />\n        </Route>,\n      ),\n      {\n        initialEntries: [\"/deep/path/to/descendant/routes\"],\n      },\n    );\n    let { container } = render(<RouterProvider router={router} />);\n\n    function GrandChild() {\n      return (\n        <Routes>\n          <Route path=\"descendant\">\n            <Route\n              path=\"routes\"\n              element={<h1>👋 Hello from the other side!</h1>}\n            />\n          </Route>\n        </Routes>\n      );\n    }\n\n    function Child() {\n      return (\n        <Routes>\n          <Route path=\"to/*\" element={<GrandChild />} />\n        </Routes>\n      );\n    }\n\n    expect(getHtml(container)).toMatchInlineSnapshot(`\n      \"<div>\n        <h1>\n          👋 Hello from the other side!\n        </h1>\n      </div>\"\n    `);\n  });\n\n  it(\"renders <Routes> alongside a data router ErrorBoundary\", () => {\n    let router = createMemoryRouter(\n      [\n        {\n          path: \"*\",\n          Component() {\n            return (\n              <>\n                <Outlet />\n                <Routes>\n                  <Route index element={<h1>Descendant</h1>} />\n                </Routes>\n              </>\n            );\n          },\n          children: [\n            {\n              id: \"index\",\n              index: true,\n              Component: () => <h1>Child</h1>,\n              ErrorBoundary() {\n                return <p>{(useRouteError() as Error).message}</p>;\n              },\n            },\n          ],\n        },\n      ],\n      {\n        initialEntries: [\"/\"],\n        hydrationData: {\n          errors: {\n            index: new Error(\"Broken!\"),\n          },\n        },\n      },\n    );\n    let { container } = render(<RouterProvider router={router} />);\n\n    expect(getHtml(container)).toMatchInlineSnapshot(`\n      \"<div>\n        <p>\n          Broken!\n        </p>\n        <h1>\n          Descendant\n        </h1>\n      </div>\"\n    `);\n  });\n\n  it(\"exposes promise from useNavigate\", async () => {\n    let sequence: string[] = [];\n    let router = createMemoryRouter([\n      {\n        path: \"/\",\n        Component() {\n          let navigate = useNavigate();\n          return (\n            <>\n              <h1>Home</h1>\n              <button\n                onClick={async () => {\n                  sequence.push(\"call navigate\");\n                  await navigate(\"/page\");\n                  sequence.push(\"navigate resolved\");\n                }}\n              >\n                Navigate\n              </button>\n            </>\n          );\n        },\n      },\n      {\n        path: \"/page\",\n        async loader() {\n          sequence.push(\"loader start\");\n          await new Promise((r) => setTimeout(r, 100));\n          sequence.push(\"loader end\");\n          return null;\n        },\n        Component: () => {\n          sequence.push(\"render\");\n          return <h1>Page</h1>;\n        },\n      },\n    ]);\n    let { container } = render(<RouterProvider router={router} />);\n\n    expect(getHtml(container)).toContain(\"Home\");\n    fireEvent.click(screen.getByText(\"Navigate\"));\n    await waitFor(() => screen.getByText(\"Page\"));\n\n    expect(sequence).toEqual([\n      \"call navigate\",\n      \"loader start\",\n      \"loader end\",\n      \"navigate resolved\",\n      \"render\",\n    ]);\n  });\n\n  it(\"exposes promise from useNavigate (popstate)\", async () => {\n    let sequence: string[] = [];\n    let router = createMemoryRouter(\n      [\n        {\n          path: \"/\",\n          async loader() {\n            sequence.push(\"loader start\");\n            await new Promise((r) => setTimeout(r, 100));\n            sequence.push(\"loader end\");\n            return null;\n          },\n          Component() {\n            sequence.push(\"render\");\n            return <h1>Home</h1>;\n          },\n        },\n        {\n          path: \"/page\",\n          Component: () => {\n            let navigate = useNavigate();\n            return (\n              <>\n                <h1>Page</h1>\n                <button\n                  onClick={async () => {\n                    sequence.push(\"call navigate\");\n                    await navigate(-1);\n                    sequence.push(\"navigate resolved\");\n                  }}\n                >\n                  Back\n                </button>\n              </>\n            );\n          },\n        },\n      ],\n      { initialEntries: [\"/\", \"/page\"] },\n    );\n\n    let { container } = render(<RouterProvider router={router} />);\n\n    expect(getHtml(container)).toContain(\"Page\");\n    fireEvent.click(screen.getByText(\"Back\"));\n    await waitFor(() => screen.getByText(\"Home\"));\n\n    expect(sequence).toEqual([\n      \"call navigate\",\n      \"loader start\",\n      \"loader end\",\n      \"navigate resolved\",\n      \"render\",\n    ]);\n  });\n\n  it(\"exposes promise from useSubmit\", async () => {\n    let sequence: string[] = [];\n    let router = createMemoryRouter([\n      {\n        path: \"/\",\n        Component() {\n          let submit = useSubmit();\n          return (\n            <>\n              <h1>Home</h1>\n              <button\n                onClick={async () => {\n                  sequence.push(\"call submit\");\n                  await submit({}, { action: \"/page\" });\n                  sequence.push(\"submit resolved\");\n                }}\n              >\n                Submit\n              </button>\n            </>\n          );\n        },\n      },\n      {\n        path: \"/page\",\n        async loader() {\n          sequence.push(\"loader start\");\n          await new Promise((r) => setTimeout(r, 100));\n          sequence.push(\"loader end\");\n          return null;\n        },\n        Component: () => {\n          sequence.push(\"render\");\n          return <h1>Page</h1>;\n        },\n      },\n    ]);\n    let { container } = render(<RouterProvider router={router} />);\n\n    expect(getHtml(container)).toContain(\"Home\");\n    fireEvent.click(screen.getByText(\"Submit\"));\n    await waitFor(() => screen.getByText(\"Page\"));\n\n    expect(sequence).toEqual([\n      \"call submit\",\n      \"loader start\",\n      \"loader end\",\n      \"submit resolved\",\n      \"render\",\n    ]);\n  });\n\n  it(\"exposes promise from useRevalidator\", async () => {\n    let sequence: string[] = [];\n    let count = 0;\n    let router = createMemoryRouter([\n      {\n        path: \"/\",\n        async loader() {\n          sequence.push(\"loader start\");\n          await new Promise((r) => setTimeout(r, 100));\n          sequence.push(\"loader end\");\n          return ++count;\n        },\n        Component() {\n          let loaderCount = useLoaderData() as number;\n          let revalidator = useRevalidator();\n          sequence.push(`render ${loaderCount}`);\n          return (\n            <button\n              onClick={async () => {\n                sequence.push(\"call revalidate\");\n                await revalidator.revalidate();\n                sequence.push(\"revalidate resolved\");\n              }}\n            >\n              Revalidate ({loaderCount})\n            </button>\n          );\n        },\n      },\n    ]);\n    render(<RouterProvider router={router} />);\n\n    await waitFor(() => screen.getByText(\"Revalidate (1)\"));\n    fireEvent.click(screen.getByText(\"Revalidate (1)\"));\n    await waitFor(() => screen.getByText(\"Revalidate (2)\"));\n\n    expect(sequence).toEqual([\n      \"loader start\",\n      \"loader end\",\n      \"render 1\",\n      \"call revalidate\",\n      \"loader start\",\n      \"render 1\", // revalidator.state === 'loading'\n      \"loader end\",\n      \"revalidate resolved\",\n      \"render 2\",\n    ]);\n  });\n\n  it(\"exposes promise from useFetcher.load\", async () => {\n    let sequence: string[] = [];\n    let count = 0;\n    let router = createMemoryRouter([\n      {\n        path: \"/\",\n        async loader() {\n          sequence.push(\"loader start\");\n          await new Promise((r) => setTimeout(r, 100));\n          sequence.push(\"loader end\");\n          return ++count;\n        },\n        Component() {\n          let loaderCount = useLoaderData();\n          let fetcher = useFetcher();\n          sequence.push(`render ${loaderCount} ${fetcher.data || \"empty\"}`);\n          return (\n            <button\n              onClick={async () => {\n                sequence.push(\"call fetcher.load\");\n                await fetcher.load(\"/\");\n                sequence.push(\"fetcher.load resolved\");\n              }}\n            >\n              Fetch ({`${loaderCount}, ${fetcher.data || \"empty\"}`})\n            </button>\n          );\n        },\n      },\n    ]);\n\n    render(<RouterProvider router={router} />);\n\n    await waitFor(() => screen.getByText(\"Fetch (1, empty)\"));\n    fireEvent.click(screen.getByText(\"Fetch (1, empty)\"));\n    await waitFor(() => screen.getByText(\"Fetch (1, 2)\"));\n\n    expect(sequence).toEqual([\n      \"loader start\",\n      \"loader end\",\n      \"render 1 empty\",\n      \"call fetcher.load\",\n      \"loader start\",\n      \"render 1 empty\", // fetcher.state === 'loading'\n      \"loader end\",\n      \"fetcher.load resolved\",\n      \"render 1 2\",\n    ]);\n  });\n\n  it(\"exposes promise from useFetcher.submit\", async () => {\n    let sequence: string[] = [];\n    let count = 0;\n    let router = createMemoryRouter([\n      {\n        path: \"/\",\n        async action() {\n          sequence.push(\"action start\");\n          await new Promise((r) => setTimeout(r, 100));\n          sequence.push(\"action end\");\n          return ++count;\n        },\n        async loader() {\n          sequence.push(\"loader start\");\n          await new Promise((r) => setTimeout(r, 100));\n          sequence.push(\"loader end\");\n          return ++count;\n        },\n        Component() {\n          let loaderCount = useLoaderData();\n          let fetcher = useFetcher();\n          sequence.push(`render ${loaderCount} ${fetcher.data || \"empty\"}`);\n          return (\n            <button\n              onClick={async () => {\n                sequence.push(\"call fetcher.submit\");\n                await fetcher.submit({}, { method: \"post\", action: \"/\" });\n                sequence.push(\"fetcher.submit resolved\");\n              }}\n            >\n              Fetch ({`${loaderCount}, ${fetcher.data || \"empty\"}`})\n            </button>\n          );\n        },\n      },\n    ]);\n\n    render(<RouterProvider router={router} />);\n\n    await waitFor(() => screen.getByText(\"Fetch (1, empty)\"));\n    fireEvent.click(screen.getByText(\"Fetch (1, empty)\"));\n    await waitFor(() => screen.getByText(\"Fetch (3, 2)\"));\n\n    expect(sequence).toEqual([\n      \"loader start\",\n      \"loader end\",\n      \"render 1 empty\",\n      \"call fetcher.submit\",\n      \"action start\",\n      \"render 1 empty\", // fetcher.state === 'submitting'\n      \"action end\",\n      \"loader start\",\n      \"render 1 2\", // fetcher.state === 'loading'\n      \"loader end\",\n      \"fetcher.submit resolved\",\n      \"render 3 2\",\n    ]);\n  });\n\n  describe(\"errors\", () => {\n    it(\"renders hydration errors on leaf elements using errorElement\", async () => {\n      let router = createMemoryRouter(\n        createRoutesFromElements(\n          <Route path=\"/\" element={<Comp />}>\n            <Route\n              path=\"child\"\n              element={<Comp />}\n              errorElement={<ErrorBoundary />}\n            />\n          </Route>,\n        ),\n        {\n          initialEntries: [\"/child\"],\n          hydrationData: {\n            loaderData: {\n              \"0\": \"parent data\",\n            },\n            actionData: {\n              \"0\": \"parent action\",\n            },\n            errors: {\n              \"0-0\": new Error(\"Kaboom 💥\"),\n            },\n          },\n        },\n      );\n      let { container } = render(<RouterProvider router={router} />);\n\n      function Comp() {\n        let data = useLoaderData() as { message: string };\n        let actionData = useActionData();\n        let navigation = useNavigation();\n        return (\n          <div>\n            <>{data}</>\n            <>{actionData}</>\n            <>{navigation.state}</>\n            <Outlet />\n          </div>\n        );\n      }\n\n      function ErrorBoundary() {\n        let error = useRouteError() as Error;\n        return <p>{error.message}</p>;\n      }\n\n      expect(getHtml(container)).toMatchInlineSnapshot(`\n        \"<div>\n          <div>\n            parent data\n            parent action\n            idle\n            <p>\n              Kaboom 💥\n            </p>\n          </div>\n        </div>\"\n      `);\n    });\n\n    it(\"renders hydration errors on leaf elements using ErrorBoundary\", async () => {\n      let router = createMemoryRouter(\n        createRoutesFromElements(\n          <Route path=\"/\" element={<Comp />}>\n            <Route\n              path=\"child\"\n              element={<Comp />}\n              ErrorBoundary={() => <p>{(useRouteError() as Error).message}</p>}\n            />\n          </Route>,\n        ),\n        {\n          initialEntries: [\"/child\"],\n          hydrationData: {\n            loaderData: {\n              \"0\": \"parent data\",\n            },\n            actionData: {\n              \"0\": \"parent action\",\n            },\n            errors: {\n              \"0-0\": new Error(\"Kaboom 💥\"),\n            },\n          },\n        },\n      );\n      let { container } = render(<RouterProvider router={router} />);\n\n      function Comp() {\n        let data = useLoaderData() as { message: string };\n        let actionData = useActionData();\n        let navigation = useNavigation();\n        return (\n          <div>\n            <>{data}</>\n            <>{actionData}</>\n            <>{navigation.state}</>\n            <Outlet />\n          </div>\n        );\n      }\n\n      expect(getHtml(container)).toMatchInlineSnapshot(`\n        \"<div>\n          <div>\n            parent data\n            parent action\n            idle\n            <p>\n              Kaboom 💥\n            </p>\n          </div>\n        </div>\"\n      `);\n    });\n\n    it(\"renders hydration errors on parent elements\", async () => {\n      let router = createMemoryRouter(\n        createRoutesFromElements(\n          <Route path=\"/\" element={<Comp />} errorElement={<ErrorBoundary />}>\n            <Route path=\"child\" element={<Comp />} />\n          </Route>,\n        ),\n        {\n          initialEntries: [\"/child\"],\n          hydrationData: {\n            loaderData: {},\n            actionData: null,\n            errors: {\n              \"0\": new Error(\"Kaboom 💥\"),\n            },\n          },\n        },\n      );\n      let { container } = render(<RouterProvider router={router} />);\n\n      function Comp() {\n        let data = useLoaderData() as { message: string };\n        let actionData = useActionData();\n        let navigation = useNavigation();\n        return (\n          <div>\n            <>{data}</>\n            <>{actionData}</>\n            <>{navigation.state}</>\n            <Outlet />\n          </div>\n        );\n      }\n\n      function ErrorBoundary() {\n        let error = useRouteError() as Error;\n        return <p>{error.message}</p>;\n      }\n\n      expect(getHtml(container)).toMatchInlineSnapshot(`\n        \"<div>\n          <p>\n            Kaboom 💥\n          </p>\n        </div>\"\n      `);\n    });\n\n    it(\"renders navigation errors on leaf elements using errorElement\", async () => {\n      let fooDefer = createDeferred();\n      let barDefer = createDeferred();\n\n      let router = createMemoryRouter(\n        createRoutesFromElements(\n          <Route path=\"/\" element={<Layout />}>\n            <Route\n              path=\"foo\"\n              loader={() => fooDefer.promise}\n              element={<Foo />}\n              errorElement={<FooError />}\n            />\n            <Route\n              path=\"bar\"\n              loader={() => barDefer.promise}\n              element={<Bar />}\n              errorElement={<BarError />}\n            />\n          </Route>,\n        ),\n        {\n          initialEntries: [\"/foo\"],\n          hydrationData: {\n            loaderData: {\n              \"0-0\": {\n                message: \"hydrated from foo\",\n              },\n            },\n          },\n        },\n      );\n      let { container } = render(<RouterProvider router={router} />);\n\n      function Layout() {\n        let navigation = useNavigation();\n        return (\n          <div>\n            <MemoryNavigate to=\"/foo\">Link to Foo</MemoryNavigate>\n            <MemoryNavigate to=\"/bar\">Link to Bar</MemoryNavigate>\n            <p>{navigation.state}</p>\n            <Outlet />\n          </div>\n        );\n      }\n\n      function Foo() {\n        let data = useLoaderData() as { message: string };\n        return <h1>Foo:{data?.message}</h1>;\n      }\n      function FooError() {\n        let error = useRouteError() as Error;\n        return <p>Foo Error:{error.message}</p>;\n      }\n      function Bar() {\n        let data = useLoaderData() as { message: string };\n        return <h1>Bar:{data?.message}</h1>;\n      }\n      function BarError() {\n        let error = useRouteError() as Error;\n        return <p>Bar Error:{error.message}</p>;\n      }\n\n      expect(getHtml(container)).toMatchInlineSnapshot(`\n        \"<div>\n          <div>\n            <a\n              href=\"/foo\"\n            >\n              Link to Foo\n            </a>\n            <a\n              href=\"/bar\"\n            >\n              Link to Bar\n            </a>\n            <p>\n              idle\n            </p>\n            <h1>\n              Foo:\n              hydrated from foo\n            </h1>\n          </div>\n        </div>\"\n      `);\n\n      fireEvent.click(screen.getByText(\"Link to Bar\"));\n      barDefer.reject(new Error(\"Kaboom!\"));\n      await waitFor(() => screen.getByText(\"idle\"));\n      expect(getHtml(container)).toMatchInlineSnapshot(`\n        \"<div>\n          <div>\n            <a\n              href=\"/foo\"\n            >\n              Link to Foo\n            </a>\n            <a\n              href=\"/bar\"\n            >\n              Link to Bar\n            </a>\n            <p>\n              idle\n            </p>\n            <p>\n              Bar Error:\n              Kaboom!\n            </p>\n          </div>\n        </div>\"\n      `);\n\n      fireEvent.click(screen.getByText(\"Link to Foo\"));\n      fooDefer.reject(new Error(\"Kaboom!\"));\n      await waitFor(() => screen.getByText(\"idle\"));\n      expect(getHtml(container)).toMatchInlineSnapshot(`\n        \"<div>\n          <div>\n            <a\n              href=\"/foo\"\n            >\n              Link to Foo\n            </a>\n            <a\n              href=\"/bar\"\n            >\n              Link to Bar\n            </a>\n            <p>\n              idle\n            </p>\n            <p>\n              Foo Error:\n              Kaboom!\n            </p>\n          </div>\n        </div>\"\n      `);\n    });\n\n    it(\"renders navigation errors on leaf elements using ErrorBoundary\", async () => {\n      let fooDefer = createDeferred();\n      let barDefer = createDeferred();\n\n      let router = createMemoryRouter(\n        createRoutesFromElements(\n          <Route path=\"/\" element={<Layout />}>\n            <Route\n              path=\"foo\"\n              loader={() => fooDefer.promise}\n              element={<Foo />}\n              ErrorBoundary={FooError}\n            />\n            <Route\n              path=\"bar\"\n              loader={() => barDefer.promise}\n              element={<Bar />}\n              ErrorBoundary={BarError}\n            />\n          </Route>,\n        ),\n        {\n          initialEntries: [\"/foo\"],\n          hydrationData: {\n            loaderData: {\n              \"0-0\": {\n                message: \"hydrated from foo\",\n              },\n            },\n          },\n        },\n      );\n      let { container } = render(<RouterProvider router={router} />);\n\n      function Layout() {\n        let navigation = useNavigation();\n        return (\n          <div>\n            <MemoryNavigate to=\"/foo\">Link to Foo</MemoryNavigate>\n            <MemoryNavigate to=\"/bar\">Link to Bar</MemoryNavigate>\n            <p>{navigation.state}</p>\n            <Outlet />\n          </div>\n        );\n      }\n\n      function Foo() {\n        let data = useLoaderData() as { message: string };\n        return <h1>Foo:{data?.message}</h1>;\n      }\n      function FooError() {\n        let error = useRouteError() as Error;\n        return <p>Foo Error:{error.message}</p>;\n      }\n      function Bar() {\n        let data = useLoaderData() as { message: string };\n        return <h1>Bar:{data?.message}</h1>;\n      }\n      function BarError() {\n        let error = useRouteError() as Error;\n        return <p>Bar Error:{error.message}</p>;\n      }\n\n      expect(getHtml(container)).toMatchInlineSnapshot(`\n        \"<div>\n          <div>\n            <a\n              href=\"/foo\"\n            >\n              Link to Foo\n            </a>\n            <a\n              href=\"/bar\"\n            >\n              Link to Bar\n            </a>\n            <p>\n              idle\n            </p>\n            <h1>\n              Foo:\n              hydrated from foo\n            </h1>\n          </div>\n        </div>\"\n      `);\n\n      fireEvent.click(screen.getByText(\"Link to Bar\"));\n      barDefer.reject(new Error(\"Kaboom!\"));\n      await waitFor(() => screen.getByText(\"idle\"));\n      expect(getHtml(container)).toMatchInlineSnapshot(`\n        \"<div>\n          <div>\n            <a\n              href=\"/foo\"\n            >\n              Link to Foo\n            </a>\n            <a\n              href=\"/bar\"\n            >\n              Link to Bar\n            </a>\n            <p>\n              idle\n            </p>\n            <p>\n              Bar Error:\n              Kaboom!\n            </p>\n          </div>\n        </div>\"\n      `);\n\n      fireEvent.click(screen.getByText(\"Link to Foo\"));\n      fooDefer.reject(new Error(\"Kaboom!\"));\n      await waitFor(() => screen.getByText(\"idle\"));\n      expect(getHtml(container)).toMatchInlineSnapshot(`\n        \"<div>\n          <div>\n            <a\n              href=\"/foo\"\n            >\n              Link to Foo\n            </a>\n            <a\n              href=\"/bar\"\n            >\n              Link to Bar\n            </a>\n            <p>\n              idle\n            </p>\n            <p>\n              Foo Error:\n              Kaboom!\n            </p>\n          </div>\n        </div>\"\n      `);\n    });\n\n    it(\"renders navigation errors on parent elements\", async () => {\n      let fooDefer = createDeferred();\n      let barDefer = createDeferred();\n\n      let router = createMemoryRouter(\n        createRoutesFromElements(\n          <Route path=\"/\" element={<Layout />} errorElement={<LayoutError />}>\n            <Route\n              path=\"foo\"\n              loader={() => fooDefer.promise}\n              element={<Foo />}\n              errorElement={<FooError />}\n            />\n            <Route\n              path=\"bar\"\n              loader={() => barDefer.promise}\n              element={<Bar />}\n            />\n          </Route>,\n        ),\n        {\n          initialEntries: [\"/foo\"],\n          hydrationData: {\n            loaderData: {\n              \"0-0\": {\n                message: \"hydrated from foo\",\n              },\n            },\n          },\n        },\n      );\n      let { container } = render(<RouterProvider router={router} />);\n\n      function Layout() {\n        let navigation = useNavigation();\n        return (\n          <div>\n            <MemoryNavigate to=\"/foo\">Link to Foo</MemoryNavigate>\n            <MemoryNavigate to=\"/bar\">Link to Bar</MemoryNavigate>\n            <p>{navigation.state}</p>\n            <Outlet />\n          </div>\n        );\n      }\n      function LayoutError() {\n        let error = useRouteError() as Error;\n        return <p>Layout Error:{error.message}</p>;\n      }\n      function Foo() {\n        let data = useLoaderData() as { message: string };\n        return <h1>Foo:{data?.message}</h1>;\n      }\n      function FooError() {\n        let error = useRouteError() as Error;\n        return <p>Foo Error:{error.message}</p>;\n      }\n      function Bar() {\n        let data = useLoaderData() as { message: string };\n        return <h1>Bar:{data?.message}</h1>;\n      }\n\n      expect(getHtml(container)).toMatchInlineSnapshot(`\n        \"<div>\n          <div>\n            <a\n              href=\"/foo\"\n            >\n              Link to Foo\n            </a>\n            <a\n              href=\"/bar\"\n            >\n              Link to Bar\n            </a>\n            <p>\n              idle\n            </p>\n            <h1>\n              Foo:\n              hydrated from foo\n            </h1>\n          </div>\n        </div>\"\n      `);\n\n      fireEvent.click(screen.getByText(\"Link to Bar\"));\n      barDefer.reject(new Error(\"Kaboom!\"));\n      await waitFor(() => screen.getByText(\"Layout Error:Kaboom!\"));\n      expect(getHtml(container)).toMatchInlineSnapshot(`\n        \"<div>\n          <p>\n            Layout Error:\n            Kaboom!\n          </p>\n        </div>\"\n      `);\n    });\n\n    it(\"renders 404 errors using path='/' error boundary\", async () => {\n      let router = createMemoryRouter(\n        createRoutesFromElements(\n          <>\n            <Route\n              path=\"/thing\"\n              element={<h1>Thing 1</h1>}\n              errorElement={<p>Not I!</p>}\n            />\n            <Route\n              path=\"/\"\n              element={\n                <div>\n                  <h1>Hello</h1>\n                  <Outlet />\n                </div>\n              }\n              errorElement={<Boundary />}\n            />\n          </>,\n        ),\n        {\n          initialEntries: [\"/foo\"],\n          hydrationData: {\n            loaderData: {\n              \"0\": {\n                message: \"hydrated from foo\",\n              },\n            },\n          },\n        },\n      );\n      let { container } = render(<RouterProvider router={router} />);\n\n      function Boundary() {\n        let error = useRouteError() as ErrorResponse;\n        return (\n          <p>\n            Error:\n            <>{error.status}</>\n            <>{error.statusText}</>\n          </p>\n        );\n      }\n\n      expect(getHtml(container)).toMatchInlineSnapshot(`\n        \"<div>\n          <p>\n            Error:\n            404\n            Not Found\n          </p>\n        </div>\"\n      `);\n    });\n\n    it(\"renders 404 errors using index error boundary\", async () => {\n      let router = createMemoryRouter(\n        createRoutesFromElements(\n          <>\n            <Route\n              path=\"/thing\"\n              element={<h1>Thing 1</h1>}\n              errorElement={<p>Not I!</p>}\n            />\n            <Route\n              index\n              element={\n                <div>\n                  <h1>Hello</h1>\n                  <Outlet />\n                </div>\n              }\n              errorElement={<Boundary />}\n            />\n          </>,\n        ),\n        {\n          initialEntries: [\"/foo\"],\n          hydrationData: {\n            loaderData: {\n              \"0\": {\n                message: \"hydrated from foo\",\n              },\n            },\n          },\n        },\n      );\n      let { container } = render(<RouterProvider router={router} />);\n\n      function Boundary() {\n        let error = useRouteError() as ErrorResponse;\n        return (\n          <p>\n            Error:\n            <>{error.status}</>\n            <>{error.statusText}</>\n          </p>\n        );\n      }\n\n      expect(getHtml(container)).toMatchInlineSnapshot(`\n        \"<div>\n          <p>\n            Error:\n            404\n            Not Found\n          </p>\n        </div>\"\n      `);\n    });\n\n    it(\"renders 404 errors using fallback boundary if no root layout route exists\", async () => {\n      let router = createMemoryRouter(\n        createRoutesFromElements(\n          <>\n            <Route\n              path=\"/thing1\"\n              element={<h1>Thing 1</h1>}\n              errorElement={<p>Not I!</p>}\n            />\n            <Route\n              path=\"/thing2\"\n              element={<h1>Thing 2</h1>}\n              errorElement={<p>Not I!</p>}\n            />\n          </>,\n        ),\n        {\n          initialEntries: [\"/foo\"],\n          hydrationData: {\n            loaderData: {\n              \"0\": {\n                message: \"hydrated from foo\",\n              },\n            },\n          },\n        },\n      );\n      let { container } = render(<RouterProvider router={router} />);\n\n      let html = getHtml(container);\n      expect(html).toMatch(\"Unexpected Application Error!\");\n      expect(html).toMatch(\"404 Not Found\");\n      expect(html).toMatch(\"💿 Hey developer 👋\");\n      expect(html).not.toMatch(/stack/i);\n    });\n\n    it(\"renders navigation errors with a default if no errorElements are provided\", async () => {\n      let fooDefer = createDeferred();\n      let barDefer = createDeferred();\n\n      let router = createMemoryRouter(\n        createRoutesFromElements(\n          <Route path=\"/\" element={<Layout />}>\n            <Route\n              path=\"foo\"\n              loader={() => fooDefer.promise}\n              element={<Foo />}\n            />\n            <Route\n              path=\"bar\"\n              loader={() => barDefer.promise}\n              element={<Bar />}\n            />\n          </Route>,\n        ),\n        {\n          initialEntries: [\"/foo\"],\n          hydrationData: {\n            loaderData: {\n              \"0-0\": {\n                message: \"hydrated from foo\",\n              },\n            },\n          },\n        },\n      );\n      let { container } = render(<RouterProvider router={router} />);\n\n      function Layout() {\n        let navigation = useNavigation();\n        return (\n          <div>\n            <MemoryNavigate to=\"/foo\">Link to Foo</MemoryNavigate>\n            <MemoryNavigate to=\"/bar\">Link to Bar</MemoryNavigate>\n            <p>{navigation.state}</p>\n            <Outlet />\n          </div>\n        );\n      }\n      function Foo() {\n        let data = useLoaderData() as { message: string };\n        return <h1>Foo:{data?.message}</h1>;\n      }\n      function Bar() {\n        let data = useLoaderData() as { message: string };\n        return <h1>Bar:{data?.message}</h1>;\n      }\n\n      expect(getHtml(container)).toMatchInlineSnapshot(`\n        \"<div>\n          <div>\n            <a\n              href=\"/foo\"\n            >\n              Link to Foo\n            </a>\n            <a\n              href=\"/bar\"\n            >\n              Link to Bar\n            </a>\n            <p>\n              idle\n            </p>\n            <h1>\n              Foo:\n              hydrated from foo\n            </h1>\n          </div>\n        </div>\"\n      `);\n\n      fireEvent.click(screen.getByText(\"Link to Bar\"));\n      let error = new Error(\"Kaboom!\");\n      error.stack = \"FAKE STACK TRACE\";\n      barDefer.reject(error);\n      await waitFor(() => screen.getByText(\"Kaboom!\"));\n      let html = getHtml(container);\n      expect(html).toMatch(\"Unexpected Application Error!\");\n      expect(html).toMatch(\"Kaboom!\");\n      expect(html).toMatch(\"FAKE STACK TRACE\");\n      expect(html).toMatch(\"💿 Hey developer 👋\");\n    });\n\n    // This test ensures that when manual routes are used, we add hasErrorBoundary\n    it(\"renders navigation errors on leaf elements (when using manual route objects)\", async () => {\n      let barDefer = createDeferred();\n\n      let routes = [\n        {\n          path: \"/\",\n          element: <Layout />,\n          children: [\n            {\n              path: \"foo\",\n              element: <h1>Foo</h1>,\n            },\n            {\n              path: \"bar\",\n              loader: () => barDefer.promise,\n              element: <Bar />,\n              errorElement: <BarError />,\n            },\n          ],\n        },\n      ];\n\n      let router = createMemoryRouter(routes, { initialEntries: [\"/foo\"] });\n      let { container } = render(<RouterProvider router={router} />);\n\n      function Layout() {\n        let navigation = useNavigation();\n        return (\n          <div>\n            <MemoryNavigate to=\"/bar\">Link to Bar</MemoryNavigate>\n            <p>{navigation.state}</p>\n            <Outlet />\n          </div>\n        );\n      }\n      function Bar() {\n        let data = useLoaderData() as { message: string };\n        return <h1>Bar:{data?.message}</h1>;\n      }\n      function BarError() {\n        let error = useRouteError() as Error;\n        return <p>Bar Error:{error.message}</p>;\n      }\n\n      expect(getHtml(container)).toMatchInlineSnapshot(`\n        \"<div>\n          <div>\n            <a\n              href=\"/bar\"\n            >\n              Link to Bar\n            </a>\n            <p>\n              idle\n            </p>\n            <h1>\n              Foo\n            </h1>\n          </div>\n        </div>\"\n      `);\n\n      fireEvent.click(screen.getByText(\"Link to Bar\"));\n      barDefer.reject(new Error(\"Kaboom!\"));\n      await waitFor(() => screen.getByText(\"Bar Error:Kaboom!\"));\n      expect(getHtml(container)).toMatchInlineSnapshot(`\n        \"<div>\n          <div>\n            <a\n              href=\"/bar\"\n            >\n              Link to Bar\n            </a>\n            <p>\n              idle\n            </p>\n            <p>\n              Bar Error:\n              Kaboom!\n            </p>\n          </div>\n        </div>\"\n      `);\n    });\n\n    it(\"handles render errors in parent errorElement\", async () => {\n      let router = createMemoryRouter(\n        createRoutesFromElements(\n          <Route\n            path=\"/\"\n            element={\n              <div>\n                <h1>This should not show</h1>\n                <Outlet />\n              </div>\n            }\n            errorElement={<ErrorBoundary />}\n          >\n            <Route path=\"child\" element={<ChildComp />} />\n          </Route>,\n        ),\n        { initialEntries: [\"/child\"] },\n      );\n      let { container } = render(<RouterProvider router={router} />);\n\n      function ChildComp(): React.ReactElement {\n        throw new Error(\"Kaboom!\");\n      }\n\n      function ErrorBoundary() {\n        let error = useRouteError() as Error;\n        return <p>{error.message}</p>;\n      }\n\n      expect(getHtml(container)).toMatchInlineSnapshot(`\n        \"<div>\n          <p>\n            Kaboom!\n          </p>\n        </div>\"\n      `);\n    });\n\n    it(\"handles render errors in child errorElement\", async () => {\n      let router = createMemoryRouter(\n        createRoutesFromElements(\n          <Route\n            path=\"/\"\n            element={\n              <div>\n                <h1>Parent</h1>\n                <Outlet />\n              </div>\n            }\n            errorElement={<p>Don't show this</p>}\n          >\n            <Route\n              path=\"child\"\n              element={<ChildComp />}\n              errorElement={<ErrorBoundary />}\n            />\n          </Route>,\n        ),\n        { initialEntries: [\"/child\"] },\n      );\n      let { container } = render(<RouterProvider router={router} />);\n\n      function ChildComp(): React.ReactElement {\n        throw new Error(\"Kaboom!\");\n      }\n\n      function ErrorBoundary() {\n        let error = useRouteError() as Error;\n        return <p>{error.message}</p>;\n      }\n\n      expect(getHtml(container)).toMatchInlineSnapshot(`\n        \"<div>\n          <div>\n            <h1>\n              Parent\n            </h1>\n            <p>\n              Kaboom!\n            </p>\n          </div>\n        </div>\"\n      `);\n    });\n\n    it(\"handles render errors in default errorElement\", async () => {\n      let router = createMemoryRouter(\n        createRoutesFromElements(\n          <Route\n            path=\"/\"\n            element={\n              <div>\n                <h1>Parent</h1>\n                <Outlet />\n              </div>\n            }\n          >\n            <Route path=\"child\" element={<ChildComp />} />\n          </Route>,\n        ),\n        { initialEntries: [\"/child\"] },\n      );\n      let { container } = render(<RouterProvider router={router} />);\n\n      function ChildComp(): React.ReactElement {\n        let error = new Error(\"Kaboom!\");\n        error.stack = \"FAKE STACK TRACE\";\n        throw error;\n      }\n\n      let html = getHtml(container);\n      expect(html).toMatch(\"Unexpected Application Error!\");\n      expect(html).toMatch(\"Kaboom!\");\n      expect(html).toMatch(\"FAKE STACK TRACE\");\n      expect(html).toMatch(\"💿 Hey developer 👋\");\n    });\n\n    it(\"does not handle render errors for non-data routers\", async () => {\n      expect(() =>\n        render(\n          <MemoryRouter initialEntries={[\"/child\"]}>\n            <Routes>\n              <Route\n                path=\"/\"\n                element={\n                  <div>\n                    <h1>Parent</h1>\n                    <Outlet />\n                  </div>\n                }\n              >\n                <Route path=\"child\" element={<ChildComp />} />\n              </Route>\n            </Routes>\n          </MemoryRouter>,\n        ),\n      ).toThrowErrorMatchingInlineSnapshot(`\"Kaboom!\"`);\n\n      function ChildComp(): React.ReactElement {\n        throw new Error(\"Kaboom!\");\n      }\n    });\n\n    it(\"handles a `null` render-error\", async () => {\n      let router = createMemoryRouter([\n        {\n          path: \"/\",\n          Component() {\n            // eslint-disable-next-line no-throw-literal\n            throw null;\n          },\n          ErrorBoundary() {\n            return <pre>{useRouteError() === null ? \"Yes\" : \"No\"}</pre>;\n          },\n        },\n      ]);\n      let { container } = render(<RouterProvider router={router} />);\n\n      await waitFor(() => screen.getByText(\"Yes\"));\n      expect(getHtml(container)).toMatch(\"Yes\");\n    });\n\n    it(\"handles a `null` render-error from a promise\", async () => {\n      let router = createMemoryRouter([\n        {\n          path: \"/\",\n          loader() {\n            let promise = Promise.reject(null);\n            promise.catch(() => {});\n            return { lazy: promise };\n          },\n          Component() {\n            let data = useLoaderData() as { lazy: Promise<unknown> };\n            return (\n              <React.Suspense>\n                <Await resolve={data.lazy}>No</Await>\n              </React.Suspense>\n            );\n          },\n          ErrorBoundary() {\n            return <pre>{useRouteError() === null ? \"Yes\" : \"No\"}</pre>;\n          },\n        },\n      ]);\n      let { container } = render(<RouterProvider router={router} />);\n\n      await waitFor(() => screen.getByText(\"Yes\"));\n      expect(getHtml(container)).toMatch(\"Yes\");\n    });\n\n    it(\"handles back button routing away from a child error boundary\", async () => {\n      let router = createMemoryRouter(\n        createRoutesFromElements(\n          <Route\n            path=\"/\"\n            element={<Parent />}\n            errorElement={<p>Don't show this</p>}\n          >\n            <Route\n              path=\"child\"\n              element={<Child />}\n              errorElement={<ErrorBoundary />}\n            />\n          </Route>,\n        ),\n      );\n\n      let { container } = render(\n        <div>\n          <RouterProvider router={router} />\n        </div>,\n      );\n\n      function Parent() {\n        return (\n          <>\n            <h1>Parent</h1>\n            <Outlet />\n          </>\n        );\n      }\n      function Child(): React.ReactElement {\n        throw new Error(\"Kaboom!\");\n      }\n\n      function ErrorBoundary() {\n        let error = useRouteError() as Error;\n        return <p>{error.message}</p>;\n      }\n\n      expect(getHtml(container)).toMatchInlineSnapshot(`\n        \"<div>\n          <div>\n            <h1>\n              Parent\n            </h1>\n          </div>\n        </div>\"\n      `);\n\n      router.navigate(\"/child\");\n      await waitFor(() => screen.getByText(\"Kaboom!\"));\n      expect(getHtml(container)).toMatchInlineSnapshot(`\n        \"<div>\n          <div>\n            <h1>\n              Parent\n            </h1>\n            <p>\n              Kaboom!\n            </p>\n          </div>\n        </div>\"\n      `);\n\n      router.navigate(-1);\n      await waitFor(() => {\n        expect(queryByText(container, \"Kaboom!\")).not.toBeInTheDocument();\n      });\n      expect(getHtml(container)).toMatchInlineSnapshot(`\n        \"<div>\n          <div>\n            <h1>\n              Parent\n            </h1>\n          </div>\n        </div>\"\n      `);\n    });\n\n    it(\"handles back button routing away from a default error boundary\", async () => {\n      let router = createMemoryRouter(\n        createRoutesFromElements(\n          <Route path=\"/\" element={<Parent />}>\n            <Route path=\"child\" element={<Child />} />\n          </Route>,\n        ),\n      );\n      let { container } = render(\n        <div>\n          <RouterProvider router={router} />\n        </div>,\n      );\n\n      function Parent() {\n        return (\n          <>\n            <h1>Parent</h1>\n            <Outlet />\n          </>\n        );\n      }\n\n      function Child(): React.ReactElement {\n        let error = new Error(\"Kaboom!\");\n        error.stack = \"FAKE STACK TRACE\";\n        throw error;\n      }\n\n      expect(getHtml(container)).toMatchInlineSnapshot(`\n        \"<div>\n          <div>\n            <h1>\n              Parent\n            </h1>\n          </div>\n        </div>\"\n      `);\n\n      router.navigate(\"/child\");\n      await waitFor(() => screen.getByText(\"Kaboom!\"));\n      let html = getHtml(container);\n      expect(html).toMatch(\"Unexpected Application Error!\");\n      expect(html).toMatch(\"Kaboom!\");\n      expect(html).toMatch(\"FAKE STACK TRACE\");\n      expect(html).toMatch(\"💿 Hey developer 👋\");\n\n      router.navigate(-1);\n      await waitFor(() => {\n        expect(queryByText(container, \"Kaboom!\")).not.toBeInTheDocument();\n      });\n      expect(getHtml(container)).toMatchInlineSnapshot(`\n        \"<div>\n          <div>\n            <h1>\n              Parent\n            </h1>\n          </div>\n        </div>\"\n      `);\n    });\n  });\n\n  describe(\"defer\", () => {\n    function setupDeferredTest({\n      useRenderProp = false,\n      hasRouteErrorElement = false,\n      hasAwaitErrorElement = false,\n      triggerRenderError = false,\n      triggerFallbackError = false,\n    } = {}) {\n      let awaitRenderCount = 0;\n      let barDefer = createDeferred();\n      let bazDefer = createDeferred();\n\n      let router = createMemoryRouter(\n        createRoutesFromElements(\n          <Route path=\"/\" element={<Layout />}>\n            <Route path=\"foo\" element={<h1>Foo</h1>} />\n            <Route\n              path=\"bar\"\n              loader={() => barDefer.promise}\n              element={<Bar />}\n              errorElement={hasRouteErrorElement ? <RouteError /> : null}\n            />\n            <Route\n              path=\"baz\"\n              loader={() => bazDefer.promise}\n              element={<h1>Baz</h1>}\n            />\n          </Route>,\n        ),\n        { initialEntries: [\"/foo\"] },\n      );\n      let { container } = render(<RouterProvider router={router} />);\n\n      function Layout() {\n        let navigation = useNavigation();\n        return (\n          <>\n            <MemoryNavigate to=\"/bar\">Link to Bar</MemoryNavigate>\n            <MemoryNavigate to=\"/baz\">Link to Baz</MemoryNavigate>\n            <div id=\"content\">\n              <p>{navigation.state}</p>\n              <Outlet />\n            </div>\n          </>\n        );\n      }\n\n      function Bar() {\n        let data = useLoaderData() as { critical: string };\n        return (\n          <>\n            <p>{data.critical}</p>\n            <React.Suspense fallback={<LazyFallback />}>\n              <AwaitCounter data={data} />\n            </React.Suspense>\n          </>\n        );\n      }\n\n      function AwaitCounter({ data }) {\n        awaitRenderCount++;\n        return (\n          <>\n            <Await\n              resolve={data.lazy}\n              errorElement={hasAwaitErrorElement ? <LazyError /> : null}\n            >\n              {useRenderProp ? (value) => <p>{value}</p> : <LazyData />}\n            </Await>\n          </>\n        );\n      }\n\n      function RouteError() {\n        let error = useRouteError() as Error;\n        return <p>Route Error:{error.message}</p>;\n      }\n\n      function LazyFallback() {\n        return triggerFallbackError ? (\n          // @ts-expect-error\n          <p>{oops.i.did.it}</p>\n        ) : (\n          <p>Loading...</p>\n        );\n      }\n\n      function LazyData() {\n        let data = useAsyncValue() as string;\n        return triggerRenderError ? (\n          // @ts-expect-error\n          <p>{oops.i.did.it.again}</p>\n        ) : (\n          <p>{data}</p>\n        );\n      }\n\n      function LazyError() {\n        let data = useAsyncError() as Error;\n        return <p>Await Error:{data.message}</p>;\n      }\n\n      return {\n        container: container.querySelector(\"#content\") as HTMLElement,\n        barDefer,\n        bazDefer,\n        getAwaitRenderCount() {\n          return awaitRenderCount;\n        },\n      };\n    }\n\n    it(\"allows loaders to returned deferred data (child component)\", async () => {\n      let { barDefer, container } = setupDeferredTest();\n      fireEvent.click(screen.getByText(\"Link to Bar\"));\n      expect(getHtml(container)).toMatchInlineSnapshot(`\n        \"<div\n          id=\"content\"\n        >\n          <p>\n            loading\n          </p>\n          <h1>\n            Foo\n          </h1>\n        </div>\"\n      `);\n\n      let barValueDfd = createDeferred();\n      barDefer.resolve({\n        critical: \"CRITICAL\",\n        lazy: barValueDfd.promise,\n      });\n      await waitFor(() => screen.getByText(\"idle\"));\n      expect(getHtml(container)).toMatchInlineSnapshot(`\n        \"<div\n          id=\"content\"\n        >\n          <p>\n            idle\n          </p>\n          <p>\n            CRITICAL\n          </p>\n          <p>\n            Loading...\n          </p>\n        </div>\"\n      `);\n\n      barValueDfd.resolve(\"LAZY\");\n      await waitFor(() => screen.getByText(\"LAZY\"));\n      expect(getHtml(container)).toMatchInlineSnapshot(`\n        \"<div\n          id=\"content\"\n        >\n          <p>\n            idle\n          </p>\n          <p>\n            CRITICAL\n          </p>\n          <p>\n            LAZY\n          </p>\n        </div>\"\n      `);\n    });\n\n    it(\"allows loaders to returned deferred data (render prop)\", async () => {\n      let { barDefer, container } = setupDeferredTest({ useRenderProp: true });\n\n      fireEvent.click(screen.getByText(\"Link to Bar\"));\n      expect(getHtml(container)).toMatchInlineSnapshot(`\n        \"<div\n          id=\"content\"\n        >\n          <p>\n            loading\n          </p>\n          <h1>\n            Foo\n          </h1>\n        </div>\"\n      `);\n\n      let barValueDfd = createDeferred();\n      barDefer.resolve({\n        critical: \"CRITICAL\",\n        lazy: barValueDfd.promise,\n      });\n      await waitFor(() => screen.getByText(\"idle\"));\n      expect(getHtml(container)).toMatchInlineSnapshot(`\n        \"<div\n          id=\"content\"\n        >\n          <p>\n            idle\n          </p>\n          <p>\n            CRITICAL\n          </p>\n          <p>\n            Loading...\n          </p>\n        </div>\"\n      `);\n\n      barValueDfd.resolve(\"LAZY\");\n      await waitFor(() => screen.getByText(\"LAZY\"));\n      expect(getHtml(container)).toMatchInlineSnapshot(`\n        \"<div\n          id=\"content\"\n        >\n          <p>\n            idle\n          </p>\n          <p>\n            CRITICAL\n          </p>\n          <p>\n            LAZY\n          </p>\n        </div>\"\n      `);\n    });\n\n    it(\"sends data errors to the provided errorElement\", async () => {\n      let { barDefer, container } = setupDeferredTest({\n        hasRouteErrorElement: true,\n        hasAwaitErrorElement: true,\n      });\n\n      fireEvent.click(screen.getByText(\"Link to Bar\"));\n      expect(getHtml(container)).toMatchInlineSnapshot(`\n        \"<div\n          id=\"content\"\n        >\n          <p>\n            loading\n          </p>\n          <h1>\n            Foo\n          </h1>\n        </div>\"\n      `);\n\n      let barValueDfd = createDeferred();\n      barDefer.resolve({\n        critical: \"CRITICAL\",\n        lazy: barValueDfd.promise,\n      });\n      await waitFor(() => screen.getByText(\"idle\"));\n      expect(getHtml(container)).toMatchInlineSnapshot(`\n        \"<div\n          id=\"content\"\n        >\n          <p>\n            idle\n          </p>\n          <p>\n            CRITICAL\n          </p>\n          <p>\n            Loading...\n          </p>\n        </div>\"\n      `);\n\n      barValueDfd.reject(new Error(\"Kaboom!\"));\n      await waitFor(() => screen.getByText(/Kaboom!/));\n      expect(getHtml(container)).toMatchInlineSnapshot(`\n        \"<div\n          id=\"content\"\n        >\n          <p>\n            idle\n          </p>\n          <p>\n            CRITICAL\n          </p>\n          <p>\n            Await Error:\n            Kaboom!\n          </p>\n        </div>\"\n      `);\n    });\n\n    it(\"sends unhandled data errors to the nearest route error boundary\", async () => {\n      let { barDefer, container } = setupDeferredTest({\n        hasRouteErrorElement: true,\n        hasAwaitErrorElement: false,\n      });\n\n      fireEvent.click(screen.getByText(\"Link to Bar\"));\n      expect(getHtml(container)).toMatchInlineSnapshot(`\n        \"<div\n          id=\"content\"\n        >\n          <p>\n            loading\n          </p>\n          <h1>\n            Foo\n          </h1>\n        </div>\"\n      `);\n\n      let barValueDfd = createDeferred();\n      barDefer.resolve({\n        critical: \"CRITICAL\",\n        lazy: barValueDfd.promise,\n      });\n      await waitFor(() => screen.getByText(\"idle\"));\n      expect(getHtml(container)).toMatchInlineSnapshot(`\n        \"<div\n          id=\"content\"\n        >\n          <p>\n            idle\n          </p>\n          <p>\n            CRITICAL\n          </p>\n          <p>\n            Loading...\n          </p>\n        </div>\"\n      `);\n\n      barValueDfd.reject(new Error(\"Kaboom!\"));\n      await waitFor(() => screen.getByText(/Kaboom!/));\n      expect(getHtml(container)).toMatchInlineSnapshot(`\n        \"<div\n          id=\"content\"\n        >\n          <p>\n            idle\n          </p>\n          <p>\n            Route Error:\n            Kaboom!\n          </p>\n        </div>\"\n      `);\n    });\n\n    it(\"sends render errors to the provided errorElement\", async () => {\n      let { barDefer, container } = setupDeferredTest({\n        hasRouteErrorElement: true,\n        hasAwaitErrorElement: true,\n        triggerRenderError: true,\n      });\n\n      fireEvent.click(screen.getByText(\"Link to Bar\"));\n      expect(getHtml(container)).toMatchInlineSnapshot(`\n        \"<div\n          id=\"content\"\n        >\n          <p>\n            loading\n          </p>\n          <h1>\n            Foo\n          </h1>\n        </div>\"\n      `);\n\n      let barValueDfd = createDeferred();\n      barDefer.resolve({\n        critical: \"CRITICAL\",\n        lazy: barValueDfd.promise,\n      });\n      await waitFor(() => screen.getByText(\"idle\"));\n      expect(getHtml(container)).toMatchInlineSnapshot(`\n        \"<div\n          id=\"content\"\n        >\n          <p>\n            idle\n          </p>\n          <p>\n            CRITICAL\n          </p>\n          <p>\n            Loading...\n          </p>\n        </div>\"\n      `);\n\n      barValueDfd.resolve(\"LAZY\");\n      await waitFor(() => screen.getByText(/oops is not defined/));\n      expect(getHtml(container)).toMatchInlineSnapshot(`\n        \"<div\n          id=\"content\"\n        >\n          <p>\n            idle\n          </p>\n          <p>\n            CRITICAL\n          </p>\n          <p>\n            Await Error:\n            oops is not defined\n          </p>\n        </div>\"\n      `);\n    });\n\n    it(\"sends unhandled render errors to the nearest route error boundary\", async () => {\n      let { barDefer, container } = setupDeferredTest({\n        hasRouteErrorElement: true,\n        hasAwaitErrorElement: false,\n        triggerRenderError: true,\n      });\n\n      fireEvent.click(screen.getByText(\"Link to Bar\"));\n      expect(getHtml(container)).toMatchInlineSnapshot(`\n        \"<div\n          id=\"content\"\n        >\n          <p>\n            loading\n          </p>\n          <h1>\n            Foo\n          </h1>\n        </div>\"\n      `);\n\n      let barValueDfd = createDeferred();\n      barDefer.resolve({\n        critical: \"CRITICAL\",\n        lazy: barValueDfd.promise,\n      });\n      await waitFor(() => screen.getByText(\"idle\"));\n      expect(getHtml(container)).toMatchInlineSnapshot(`\n        \"<div\n          id=\"content\"\n        >\n          <p>\n            idle\n          </p>\n          <p>\n            CRITICAL\n          </p>\n          <p>\n            Loading...\n          </p>\n        </div>\"\n      `);\n\n      barValueDfd.resolve(\"LAZY\");\n      await waitFor(() => screen.getByText(/oops is not defined/));\n      expect(getHtml(container)).toMatchInlineSnapshot(`\n        \"<div\n          id=\"content\"\n        >\n          <p>\n            idle\n          </p>\n          <p>\n            Route Error:\n            oops is not defined\n          </p>\n        </div>\"\n      `);\n    });\n\n    it(\"does not handle fallback render errors in the Deferred errorElement\", async () => {\n      let { barDefer, container } = setupDeferredTest({\n        hasRouteErrorElement: true,\n        hasAwaitErrorElement: true,\n        triggerRenderError: true,\n        triggerFallbackError: true,\n      });\n\n      fireEvent.click(screen.getByText(\"Link to Bar\"));\n      expect(getHtml(container)).toMatchInlineSnapshot(`\n        \"<div\n          id=\"content\"\n        >\n          <p>\n            loading\n          </p>\n          <h1>\n            Foo\n          </h1>\n        </div>\"\n      `);\n\n      let barValueDfd = createDeferred();\n      barDefer.resolve({\n        critical: \"CRITICAL\",\n        lazy: barValueDfd.promise,\n      });\n      await waitFor(() => screen.getByText(\"idle\"));\n      expect(getHtml(container)).toMatchInlineSnapshot(`\n        \"<div\n          id=\"content\"\n        >\n          <p>\n            idle\n          </p>\n          <p>\n            Route Error:\n            oops is not defined\n          </p>\n        </div>\"\n      `);\n\n      // Resolving doesn't do anything\n      barValueDfd.resolve(\"LAZY\");\n      await new Promise((r) => setTimeout(r, 1));\n      expect(getHtml(container)).toMatchInlineSnapshot(`\n        \"<div\n          id=\"content\"\n        >\n          <p>\n            idle\n          </p>\n          <p>\n            Route Error:\n            oops is not defined\n          </p>\n        </div>\"\n      `);\n    });\n\n    it(\"freezes the UI for aborted deferreds\", async () => {\n      let { barDefer, bazDefer, container, getAwaitRenderCount } =\n        setupDeferredTest();\n      fireEvent.click(screen.getByText(\"Link to Bar\"));\n      expect(getHtml(container)).toMatchInlineSnapshot(`\n        \"<div\n          id=\"content\"\n        >\n          <p>\n            loading\n          </p>\n          <h1>\n            Foo\n          </h1>\n        </div>\"\n      `);\n      expect(getAwaitRenderCount()).toBe(0);\n\n      let barValueDfd = createDeferred();\n      barDefer.resolve({\n        critical: \"CRITICAL\",\n        lazy: barValueDfd.promise,\n      });\n      await waitFor(() => screen.getByText(\"idle\"));\n      expect(getHtml(container)).toMatchInlineSnapshot(`\n        \"<div\n          id=\"content\"\n        >\n          <p>\n            idle\n          </p>\n          <p>\n            CRITICAL\n          </p>\n          <p>\n            Loading...\n          </p>\n        </div>\"\n      `);\n      expect(getAwaitRenderCount()).toBe(2);\n\n      // Abort the deferred by navigating to /baz\n      fireEvent.click(screen.getByText(\"Link to Baz\"));\n      await new Promise((r) => setTimeout(r, 50));\n      expect(getHtml(container)).toMatchInlineSnapshot(`\n        \"<div\n          id=\"content\"\n        >\n          <p>\n            loading\n          </p>\n          <p>\n            CRITICAL\n          </p>\n          <p>\n            Loading...\n          </p>\n        </div>\"\n      `);\n      expect(getAwaitRenderCount()).toBe(4);\n\n      // complete /baz navigation\n      bazDefer.resolve(null);\n      await waitFor(() => screen.getByText(\"Baz\"));\n      expect(getHtml(container)).toMatchInlineSnapshot(`\n        \"<div\n          id=\"content\"\n        >\n          <p>\n            idle\n          </p>\n          <h1>\n            Baz\n          </h1>\n        </div>\"\n      `);\n\n      // Does nothing now\n      barValueDfd.resolve(\"LAZY\");\n      expect(getHtml(container)).toMatchInlineSnapshot(`\n        \"<div\n          id=\"content\"\n        >\n          <p>\n            idle\n          </p>\n          <h1>\n            Baz\n          </h1>\n        </div>\"\n      `);\n      expect(getAwaitRenderCount()).toBe(4);\n    });\n\n    it(\"should permit direct access to resolved values\", async () => {\n      let barDefer = createDeferred();\n\n      let router = createMemoryRouter(\n        createRoutesFromElements(\n          <>\n            <Route\n              path=\"foo\"\n              element={\n                <>\n                  <h1>Foo</h1>\n                  <MemoryNavigate to=\"/bar\">Link to bar</MemoryNavigate>\n                </>\n              }\n            />\n            <Route\n              path=\"bar\"\n              loader={() => barDefer.promise}\n              element={<Bar />}\n            />\n          </>,\n        ),\n        { initialEntries: [\"/foo\"] },\n      );\n      let { container } = render(<RouterProvider router={router} />);\n\n      let count = 0;\n      function Bar() {\n        let { bar } = useLoaderData() as { bar: Promise<string> };\n\n        React.useEffect(() => {\n          bar.then((data) => {\n            container.querySelector(\"#content\")!.innerHTML =\n              data + \" \" + ++count;\n          });\n        }, [bar]);\n\n        return <div id=\"content\">Waiting for data...</div>;\n      }\n\n      fireEvent.click(screen.getByText(\"Link to bar\"));\n      let barValueDefer = createDeferred();\n      await barDefer.resolve({ bar: barValueDefer.promise });\n      await waitFor(() => screen.getByText(\"Waiting for data...\"));\n\n      expect(getHtml(container)).toMatchInlineSnapshot(`\n        \"<div>\n          <div\n            id=\"content\"\n          >\n            Waiting for data...\n          </div>\n        </div>\"\n      `);\n\n      await barValueDefer.resolve(\"BAR\");\n\n      expect(getHtml(container)).toMatchInlineSnapshot(`\n        \"<div>\n          <div\n            id=\"content\"\n          >\n            BAR 1\n          </div>\n        </div>\"\n      `);\n    });\n\n    it(\"can render raw resolved to undefined promises with <Await>\", async () => {\n      let dfd = createDeferred();\n\n      let { container } = render(\n        <React.Suspense fallback={<p>Loading...</p>}>\n          <Await resolve={dfd.promise}>{(data) => <p>{String(data)}</p>}</Await>\n        </React.Suspense>,\n      );\n\n      expect(getHtml(container)).toMatchInlineSnapshot(`\n        \"<div>\n          <p>\n            Loading...\n          </p>\n        </div>\"\n      `);\n\n      dfd.resolve(undefined);\n      await waitFor(() => screen.getByText(\"undefined\"));\n      expect(getHtml(container)).toMatchInlineSnapshot(`\n        \"<div>\n          <p>\n            undefined\n          </p>\n        </div>\"\n      `);\n    });\n\n    it(\"can render raw resolved promises with <Await>\", async () => {\n      let dfd = createDeferred();\n\n      let { container } = render(\n        <React.Suspense fallback={<p>Loading...</p>}>\n          <Await resolve={dfd.promise}>{(data) => <p>{data}</p>}</Await>\n        </React.Suspense>,\n      );\n\n      expect(getHtml(container)).toMatchInlineSnapshot(`\n        \"<div>\n          <p>\n            Loading...\n          </p>\n        </div>\"\n      `);\n\n      dfd.resolve(\"RESOLVED\");\n      await waitFor(() => screen.getByText(\"RESOLVED\"));\n      expect(getHtml(container)).toMatchInlineSnapshot(`\n        \"<div>\n          <p>\n            RESOLVED\n          </p>\n        </div>\"\n      `);\n    });\n\n    it(\"can render raw rejected promises with <Await>\", async () => {\n      let dfd = createDeferred();\n\n      let { container } = render(\n        <React.Suspense fallback={<p>Loading...</p>}>\n          <Await resolve={dfd.promise} errorElement={<ErrorElement />}>\n            {(data) => <p>{data}</p>}\n          </Await>\n        </React.Suspense>,\n      );\n\n      function ErrorElement() {\n        let error = useAsyncError() as Error;\n        return <p>Error:{error.message}</p>;\n      }\n\n      expect(getHtml(container)).toMatchInlineSnapshot(`\n        \"<div>\n          <p>\n            Loading...\n          </p>\n        </div>\"\n      `);\n\n      await dfd.reject(new Error(\"REJECTED\"));\n      await waitFor(() => screen.getByText(\"Error:REJECTED\"));\n      expect(getHtml(container)).toMatchInlineSnapshot(`\n        \"<div>\n          <p>\n            Error:\n            REJECTED\n          </p>\n        </div>\"\n      `);\n    });\n\n    it(\"can render raw values with <Await>\", async () => {\n      let { container } = render(\n        <Await resolve={\"VALUE\"}>{(data) => <p>{data}</p>}</Await>,\n      );\n\n      expect(getHtml(container)).toMatchInlineSnapshot(`\n        \"<div>\n          <p>\n            VALUE\n          </p>\n        </div>\"\n      `);\n    });\n  });\n\n  describe(\"router dataStrategy\", () => {\n    it(\"executes route loaders on navigation\", async () => {\n      let barDefer = createDeferred();\n      let router = createMemoryRouter(\n        createRoutesFromElements(\n          <Route path=\"/\" element={<Layout />}>\n            <Route path=\"foo\" element={<Foo />} />\n            <Route\n              path=\"bar\"\n              loader={() => barDefer.promise}\n              element={<Bar />}\n            />\n          </Route>,\n        ),\n        { initialEntries: [\"/foo\"], dataStrategy: urlDataStrategy },\n      );\n      let { container } = render(<RouterProvider router={router} />);\n\n      function Layout() {\n        let navigation = useNavigation();\n        return (\n          <div>\n            <MemoryNavigate to=\"/bar\">Link to Bar</MemoryNavigate>\n            <p>{navigation.state}</p>\n            <Outlet />\n          </div>\n        );\n      }\n\n      function Foo() {\n        return <h1>Foo</h1>;\n      }\n      function Bar() {\n        let data = useLoaderData() as URLSearchParams;\n        return <h1>{data?.get(\"message\")}</h1>;\n      }\n\n      expect(getHtml(container)).toMatchInlineSnapshot(`\n        \"<div>\n          <div>\n            <a\n              href=\"/bar\"\n            >\n              Link to Bar\n            </a>\n            <p>\n              idle\n            </p>\n            <h1>\n              Foo\n            </h1>\n          </div>\n        </div>\"\n      `);\n\n      fireEvent.click(screen.getByText(\"Link to Bar\"));\n      expect(getHtml(container)).toMatchInlineSnapshot(`\n        \"<div>\n          <div>\n            <a\n              href=\"/bar\"\n            >\n              Link to Bar\n            </a>\n            <p>\n              loading\n            </p>\n            <h1>\n              Foo\n            </h1>\n          </div>\n        </div>\"\n      `);\n\n      // barDefer.resolve({ message: \"Bar Loader\" });\n      barDefer.resolve(\n        new Response(\n          new URLSearchParams([[\"message\", \"Bar Loader\"]]).toString(),\n          {\n            headers: {\n              \"Content-Type\": \"application/x-www-form-urlencoded\",\n            },\n          },\n        ),\n      );\n      await waitFor(() => screen.getByText(\"idle\"));\n      expect(getHtml(container)).toMatchInlineSnapshot(`\n        \"<div>\n          <div>\n            <a\n              href=\"/bar\"\n            >\n              Link to Bar\n            </a>\n            <p>\n              idle\n            </p>\n            <h1>\n              Bar Loader\n            </h1>\n          </div>\n        </div>\"\n      `);\n    });\n  });\n});\n"
  },
  {
    "path": "packages/react-router/__tests__/data-router-no-dom-test.tsx",
    "content": "/**\n * @jest-environment node\n */\n\nimport * as React from \"react\";\nimport renderer from \"react-test-renderer\";\nimport { useFetcher } from \"../lib/dom/lib\";\nimport { RouterProvider } from \"../lib/dom-export/dom-router-provider\";\nimport { createMemoryRouter } from \"../lib/components\";\nimport { useLoaderData, useNavigate } from \"../lib/hooks\";\n\ndescribe(\"RouterProvider works when no DOM APIs are available\", () => {\n  it(\"renders and navigates\", async () => {\n    let router = createMemoryRouter([\n      {\n        path: \"/\",\n        Component: () => {\n          let navigate = useNavigate();\n          return <button onClick={() => navigate(\"/foo\")}>Go to /foo</button>;\n        },\n      },\n      {\n        path: \"/foo\",\n        loader: () => \"FOO\",\n        Component: () => {\n          let data = useLoaderData() as string;\n          return <h1>{data}</h1>;\n        },\n      },\n    ]);\n    let component: renderer.ReactTestRenderer | undefined;\n    renderer.act(() => {\n      component = renderer.create(<RouterProvider router={router} />);\n    });\n    component = component!;\n    let tree = component.toJSON();\n    expect(tree).toMatchInlineSnapshot(`\n      <button\n        onClick={[Function]}\n      >\n        Go to /foo\n      </button>\n    `);\n\n    await renderer.act(async () => {\n      // @ts-expect-error\n      tree.props.onClick();\n      await new Promise((resolve) => setTimeout(resolve, 0));\n    });\n\n    tree = component.toJSON();\n    expect(tree).toMatchInlineSnapshot(`\n      <h1>\n        FOO\n      </h1>\n    `);\n  });\n\n  it(\"is defensive against a view transition navigation\", async () => {\n    let warnSpy = jest.spyOn(console, \"warn\").mockImplementation(() => {});\n\n    let router = createMemoryRouter([\n      {\n        path: \"/\",\n        Component: () => {\n          let navigate = useNavigate();\n          return <button onClick={() => navigate(\"/foo\")}>Go to /foo</button>;\n        },\n      },\n      {\n        path: \"/foo\",\n        loader: () => \"FOO\",\n        Component: () => {\n          let data = useLoaderData() as string;\n          return <h1>{data}</h1>;\n        },\n      },\n    ]);\n    let component: renderer.ReactTestRenderer | undefined;\n    renderer.act(() => {\n      component = renderer.create(<RouterProvider router={router} />);\n    });\n    component = component!;\n    let tree = component.toJSON();\n    expect(tree).toMatchInlineSnapshot(`\n      <button\n        onClick={[Function]}\n      >\n        Go to /foo\n      </button>\n    `);\n\n    let spy = jest.fn();\n    let unsubscribe = router.subscribe(spy);\n\n    await renderer.act(async () => {\n      router.navigate(\"/foo\", {\n        viewTransition: true,\n      });\n      await new Promise((resolve) => setTimeout(resolve, 0));\n    });\n\n    tree = component.toJSON();\n    expect(tree).toMatchInlineSnapshot(`\n      <h1>\n        FOO\n      </h1>\n    `);\n\n    expect(spy.mock.calls[0][0].location.pathname).toBe(\"/\");\n    expect(spy.mock.calls[0][0].navigation.state).toBe(\"loading\");\n    expect(spy.mock.calls[0][0].navigation.location.pathname).toBe(\"/foo\");\n    expect(spy.mock.calls[0][1].viewTransitionOpts).toBeUndefined();\n\n    expect(spy.mock.calls[1][0].location.pathname).toBe(\"/foo\");\n    expect(spy.mock.calls[1][0].navigation.state).toBe(\"idle\");\n    expect(spy.mock.calls[1][1].viewTransitionOpts).toEqual({\n      currentLocation: {\n        hash: \"\",\n        key: \"default\",\n        pathname: \"/\",\n        search: \"\",\n        state: null,\n      },\n      nextLocation: {\n        hash: \"\",\n        key: expect.any(String),\n        pathname: \"/foo\",\n        search: \"\",\n        state: null,\n      },\n    });\n\n    expect(warnSpy).toHaveBeenCalledTimes(1);\n    expect(warnSpy).toHaveBeenCalledWith(\n      \"You provided the `viewTransition` option to a router update, but you do \" +\n        \"not appear to be running in a DOM environment as `window.startViewTransition` \" +\n        \"is not available.\",\n    );\n    warnSpy.mockRestore();\n\n    unsubscribe();\n  });\n\n  it(\"is defensive against a flushSync navigation\", async () => {\n    let router = createMemoryRouter([\n      {\n        path: \"/\",\n        Component: () => {\n          let navigate = useNavigate();\n          return <button onClick={() => navigate(\"/foo\")}>Go to /foo</button>;\n        },\n      },\n      {\n        path: \"/foo\",\n        loader: () => \"FOO\",\n        Component: () => {\n          let data = useLoaderData() as string;\n          return <h1>{data}</h1>;\n        },\n      },\n    ]);\n    let component: renderer.ReactTestRenderer | undefined;\n    renderer.act(() => {\n      component = renderer.create(<RouterProvider router={router} />);\n    });\n    component = component!;\n    let tree = component.toJSON();\n    expect(tree).toMatchInlineSnapshot(`\n      <button\n        onClick={[Function]}\n      >\n        Go to /foo\n      </button>\n    `);\n\n    let spy = jest.fn();\n    let unsubscribe = router.subscribe(spy);\n\n    await renderer.act(async () => {\n      router.navigate(\"/foo\", {\n        flushSync: true,\n      });\n      await new Promise((resolve) => setTimeout(resolve, 0));\n    });\n\n    tree = component.toJSON();\n    expect(tree).toMatchInlineSnapshot(`\n      <h1>\n        FOO\n      </h1>\n    `);\n\n    expect(spy.mock.calls[0][0].location.pathname).toBe(\"/\");\n    expect(spy.mock.calls[0][0].navigation.state).toBe(\"loading\");\n    expect(spy.mock.calls[0][0].navigation.location.pathname).toBe(\"/foo\");\n    expect(spy.mock.calls[0][1].flushSync).toBe(true);\n\n    expect(spy.mock.calls[1][0].location.pathname).toBe(\"/foo\");\n    expect(spy.mock.calls[1][0].navigation.state).toBe(\"idle\");\n    expect(spy.mock.calls[1][1].flushSync).toBe(false);\n\n    unsubscribe();\n  });\n\n  it(\"supports fetcher loads\", async () => {\n    let router = createMemoryRouter([\n      {\n        path: \"/\",\n        Component: () => {\n          let fetcher = useFetcher();\n          return (\n            <button onClick={() => fetcher.load(\"/fetch\")}>\n              Load fetcher\n              {fetcher.data || \"\"}\n            </button>\n          );\n        },\n      },\n      {\n        path: \"/fetch\",\n        loader() {\n          return \"LOADER\";\n        },\n      },\n    ]);\n    let component: renderer.ReactTestRenderer | undefined;\n    renderer.act(() => {\n      component = renderer.create(<RouterProvider router={router} />);\n    });\n    component = component!;\n    let tree = component.toJSON();\n    expect(tree).toMatchInlineSnapshot(`\n      <button\n        onClick={[Function]}\n      >\n        Load fetcher\n      </button>\n    `);\n\n    await renderer.act(async () => {\n      // @ts-expect-error\n      tree.props.onClick();\n      await new Promise((resolve) => setTimeout(resolve, 100));\n    });\n\n    tree = component.toJSON();\n    expect(tree).toMatchInlineSnapshot(`\n      <button\n        onClick={[Function]}\n      >\n        Load fetcher\n        LOADER\n      </button>\n    `);\n  });\n\n  it(\"supports fetcher submissions\", async () => {\n    let router = createMemoryRouter([\n      {\n        path: \"/\",\n        Component: () => {\n          let fetcher = useFetcher();\n          return (\n            <button\n              onClick={() =>\n                fetcher.submit(\n                  { message: \"echo\" },\n                  {\n                    method: \"post\",\n                    action: \"/fetch\",\n                    encType: \"application/json\",\n                  },\n                )\n              }\n            >\n              Submit fetcher\n              {fetcher.data?.message || \"\"}\n            </button>\n          );\n        },\n      },\n      {\n        path: \"/fetch\",\n        async action({ request }) {\n          let data = await request.json();\n          return { message: data.message.toUpperCase() };\n        },\n      },\n    ]);\n    let component: renderer.ReactTestRenderer | undefined;\n    renderer.act(() => {\n      component = renderer.create(<RouterProvider router={router} />);\n    });\n    component = component!;\n    let tree = component.toJSON();\n    expect(tree).toMatchInlineSnapshot(`\n      <button\n        onClick={[Function]}\n      >\n        Submit fetcher\n      </button>\n    `);\n\n    await renderer.act(async () => {\n      // @ts-expect-error\n      tree.props.onClick();\n      await new Promise((resolve) => setTimeout(resolve, 100));\n    });\n\n    tree = component.toJSON();\n    expect(tree).toMatchInlineSnapshot(`\n      <button\n        onClick={[Function]}\n      >\n        Submit fetcher\n        ECHO\n      </button>\n    `);\n  });\n});\n"
  },
  {
    "path": "packages/react-router/__tests__/descendant-routes-params-test.tsx",
    "content": "import * as React from \"react\";\nimport * as TestRenderer from \"react-test-renderer\";\nimport { MemoryRouter, Routes, Route, useParams } from \"react-router\";\n\ndescribe(\"Descendant <Routes>\", () => {\n  it(\"receive all params from ancestor <Routes>\", () => {\n    function ShowParams() {\n      return <p>The params are {JSON.stringify(useParams())}</p>;\n    }\n\n    let renderer: TestRenderer.ReactTestRenderer;\n    TestRenderer.act(() => {\n      renderer = TestRenderer.create(\n        <MemoryRouter initialEntries={[\"/users/mj/messages/123\"]}>\n          <Routes>\n            <Route\n              path=\"users/:userId/*\"\n              element={\n                <Routes>\n                  <Route path=\"messages/:messageId\" element={<ShowParams />} />\n                </Routes>\n              }\n            />\n          </Routes>\n        </MemoryRouter>,\n      );\n    });\n\n    expect(renderer.toJSON()).toMatchInlineSnapshot(`\n      <p>\n        The params are \n        {\"userId\":\"mj\",\"*\":\"messages/123\",\"messageId\":\"123\"}\n      </p>\n    `);\n  });\n\n  it(\"overrides params of the same name from ancestor <Routes>\", () => {\n    function ShowParams() {\n      return <p>The params are {JSON.stringify(useParams())}</p>;\n    }\n\n    let renderer: TestRenderer.ReactTestRenderer;\n    TestRenderer.act(() => {\n      renderer = TestRenderer.create(\n        <MemoryRouter initialEntries={[\"/users/mj/messages/123\"]}>\n          <Routes>\n            <Route\n              path=\"users/:id/*\"\n              element={\n                <Routes>\n                  <Route path=\"messages/:id\" element={<ShowParams />} />\n                </Routes>\n              }\n            />\n          </Routes>\n        </MemoryRouter>,\n      );\n    });\n\n    expect(renderer.toJSON()).toMatchInlineSnapshot(`\n      <p>\n        The params are \n        {\"id\":\"123\",\"*\":\"messages/123\"}\n      </p>\n    `);\n  });\n});\n"
  },
  {
    "path": "packages/react-router/__tests__/descendant-routes-splat-matching-test.tsx",
    "content": "import * as React from \"react\";\nimport * as TestRenderer from \"react-test-renderer\";\nimport { MemoryRouter, Outlet, Routes, Route, useParams } from \"react-router\";\nimport type { InitialEntry } from \"react-router\";\n\ndescribe(\"Descendant <Routes> splat matching\", () => {\n  describe(\"when the parent route path ends with /*\", () => {\n    it(\"works\", () => {\n      function ReactCourses() {\n        return (\n          <div>\n            <h1>React</h1>\n            <Routes>\n              <Route\n                path=\"react-fundamentals\"\n                element={<h1>React Fundamentals</h1>}\n              />\n            </Routes>\n          </div>\n        );\n      }\n\n      function Courses() {\n        return (\n          <div>\n            <h1>Courses</h1>\n            <Outlet />\n          </div>\n        );\n      }\n\n      let renderer: TestRenderer.ReactTestRenderer;\n      TestRenderer.act(() => {\n        renderer = TestRenderer.create(\n          <MemoryRouter initialEntries={[\"/courses/react/react-fundamentals\"]}>\n            <Routes>\n              <Route path=\"courses\" element={<Courses />}>\n                <Route path=\"react/*\" element={<ReactCourses />} />\n              </Route>\n            </Routes>\n          </MemoryRouter>,\n        );\n      });\n\n      expect(renderer.toJSON()).toMatchInlineSnapshot(`\n        <div>\n          <h1>\n            Courses\n          </h1>\n          <div>\n            <h1>\n              React\n            </h1>\n            <h1>\n              React Fundamentals\n            </h1>\n          </div>\n        </div>\n      `);\n    });\n    describe(\"works with paths beginning with special characters\", () => {\n      function PrintParams() {\n        return <p>The params are {JSON.stringify(useParams())}</p>;\n      }\n      function ReactCourses() {\n        return (\n          <div>\n            <h1>React</h1>\n            <Routes>\n              <Route\n                path=\":splat\"\n                element={\n                  <div>\n                    <h1>React Fundamentals</h1>\n                    <PrintParams />\n                  </div>\n                }\n              />\n            </Routes>\n          </div>\n        );\n      }\n\n      function Courses() {\n        return (\n          <div>\n            <h1>Courses</h1>\n            <Outlet />\n          </div>\n        );\n      }\n\n      function renderNestedSplatRoute(initialEntries: InitialEntry[]) {\n        let renderer: TestRenderer.ReactTestRenderer;\n        TestRenderer.act(() => {\n          renderer = TestRenderer.create(\n            <MemoryRouter initialEntries={initialEntries}>\n              <Routes>\n                <Route path=\"courses\" element={<Courses />}>\n                  <Route path=\"react/*\" element={<ReactCourses />} />\n                </Route>\n              </Routes>\n            </MemoryRouter>,\n          );\n        });\n        return renderer;\n      }\n\n      it(\"allows `-` to appear at the beginning\", () => {\n        let renderer = renderNestedSplatRoute([\n          \"/courses/react/-react-fundamentals\",\n        ]);\n        expect(renderer.toJSON()).toMatchInlineSnapshot(`\n          <div>\n            <h1>\n              Courses\n            </h1>\n            <div>\n              <h1>\n                React\n              </h1>\n              <div>\n                <h1>\n                  React Fundamentals\n                </h1>\n                <p>\n                  The params are \n                  {\"*\":\"-react-fundamentals\",\"splat\":\"-react-fundamentals\"}\n                </p>\n              </div>\n            </div>\n          </div>\n        `);\n      });\n      it(\"allows `.` to appear at the beginning\", () => {\n        let renderer = renderNestedSplatRoute([\n          \"/courses/react/.react-fundamentals\",\n        ]);\n        expect(renderer.toJSON()).toMatchInlineSnapshot(`\n          <div>\n            <h1>\n              Courses\n            </h1>\n            <div>\n              <h1>\n                React\n              </h1>\n              <div>\n                <h1>\n                  React Fundamentals\n                </h1>\n                <p>\n                  The params are \n                  {\"*\":\".react-fundamentals\",\"splat\":\".react-fundamentals\"}\n                </p>\n              </div>\n            </div>\n          </div>\n        `);\n      });\n      it(\"allows `~` to appear at the beginning\", () => {\n        let renderer = renderNestedSplatRoute([\n          \"/courses/react/~react-fundamentals\",\n        ]);\n        expect(renderer.toJSON()).toMatchInlineSnapshot(`\n          <div>\n            <h1>\n              Courses\n            </h1>\n            <div>\n              <h1>\n                React\n              </h1>\n              <div>\n                <h1>\n                  React Fundamentals\n                </h1>\n                <p>\n                  The params are \n                  {\"*\":\"~react-fundamentals\",\"splat\":\"~react-fundamentals\"}\n                </p>\n              </div>\n            </div>\n          </div>\n        `);\n      });\n      it(\"allows `@` to appear at the beginning\", () => {\n        let renderer = renderNestedSplatRoute([\n          \"/courses/react/@react-fundamentals\",\n        ]);\n        expect(renderer.toJSON()).toMatchInlineSnapshot(`\n          <div>\n            <h1>\n              Courses\n            </h1>\n            <div>\n              <h1>\n                React\n              </h1>\n              <div>\n                <h1>\n                  React Fundamentals\n                </h1>\n                <p>\n                  The params are \n                  {\"*\":\"@react-fundamentals\",\"splat\":\"@react-fundamentals\"}\n                </p>\n              </div>\n            </div>\n          </div>\n        `);\n      });\n      it(\"allows url-encoded entities to appear at the beginning\", () => {\n        let renderer = renderNestedSplatRoute([\n          \"/courses/react/%20react-fundamentals\",\n        ]);\n        expect(renderer.toJSON()).toMatchInlineSnapshot(`\n          <div>\n            <h1>\n              Courses\n            </h1>\n            <div>\n              <h1>\n                React\n              </h1>\n              <div>\n                <h1>\n                  React Fundamentals\n                </h1>\n                <p>\n                  The params are \n                  {\"*\":\" react-fundamentals\",\"splat\":\" react-fundamentals\"}\n                </p>\n              </div>\n            </div>\n          </div>\n        `);\n      });\n    });\n  });\n});\n"
  },
  {
    "path": "packages/react-router/__tests__/descendant-routes-warning-test.tsx",
    "content": "import * as React from \"react\";\nimport * as TestRenderer from \"react-test-renderer\";\nimport { MemoryRouter, Outlet, Routes, Route } from \"react-router\";\n\ndescribe(\"Descendant <Routes>\", () => {\n  let consoleWarn: jest.SpyInstance<void, any>;\n  beforeEach(() => {\n    consoleWarn = jest.spyOn(console, \"warn\").mockImplementation();\n  });\n\n  afterEach(() => {\n    consoleWarn.mockRestore();\n  });\n\n  describe(\"when the parent route path does not have a trailing *\", () => {\n    it(\"warns once when you visit the parent route\", () => {\n      function ReactCourses() {\n        return (\n          <div>\n            <h1>React</h1>\n\n            <Routes>\n              <Route\n                path=\"react-fundamentals\"\n                element={<h1>React Fundamentals</h1>}\n              />\n              <Route path=\"advanced-react\" element={<h1>Advanced React</h1>} />\n            </Routes>\n          </div>\n        );\n      }\n\n      function Courses() {\n        return (\n          <div>\n            <h1>Courses</h1>\n            <Outlet />\n          </div>\n        );\n      }\n\n      TestRenderer.act(() => {\n        TestRenderer.create(\n          <MemoryRouter initialEntries={[\"/courses/react\"]}>\n            <Routes>\n              <Route path=\"courses\" element={<Courses />}>\n                <Route path=\"react\" element={<ReactCourses />} />\n              </Route>\n            </Routes>\n          </MemoryRouter>,\n        );\n      });\n\n      expect(consoleWarn).toHaveBeenCalledTimes(1);\n\n      expect(consoleWarn.mock.calls[0][0])\n        .toMatch(`You rendered descendant <Routes> (or called \\`useRoutes()\\`) at \"/courses/react\" (under <Route path=\"react\">) but the parent route path has no trailing \"*\". This means if you navigate deeper, the parent won't match anymore and therefore the child routes will never render.\n\nPlease change the parent <Route path=\"react\"> to <Route path=\"react/*\">.`);\n    });\n  });\n\n  describe(\"when the parent route path does not have a trailing * and is the root\", () => {\n    it(\"warns once when you visit the parent route and only shows a hint like *\", () => {\n      function ReactCourses() {\n        return (\n          <div>\n            <h1>React</h1>\n\n            <Routes>\n              <Route\n                path=\"/react-fundamentals\"\n                element={<h1>React Fundamentals</h1>}\n              />\n            </Routes>\n          </div>\n        );\n      }\n\n      TestRenderer.act(() => {\n        TestRenderer.create(\n          <MemoryRouter initialEntries={[\"/\"]}>\n            <Routes>\n              <Route path=\"/\" element={<ReactCourses />} />\n            </Routes>\n          </MemoryRouter>,\n        );\n      });\n\n      expect(consoleWarn).toHaveBeenCalledTimes(1);\n\n      expect(consoleWarn.mock.calls[0][0])\n        .toMatch(`You rendered descendant <Routes> (or called \\`useRoutes()\\`) at \"/\" (under <Route path=\"/\">) but the parent route path has no trailing \"*\". This means if you navigate deeper, the parent won't match anymore and therefore the child routes will never render.\n\nPlease change the parent <Route path=\"/\"> to <Route path=\"*\">.`);\n    });\n  });\n\n  describe(\"when the parent route has a trailing *\", () => {\n    it(\"does not warn when you visit the parent route\", () => {\n      function ReactCourses() {\n        return (\n          <div>\n            <h1>React</h1>\n\n            <Routes>\n              <Route\n                path=\"react-fundamentals\"\n                element={<h1>React Fundamentals</h1>}\n              />\n            </Routes>\n          </div>\n        );\n      }\n\n      function Courses() {\n        return (\n          <div>\n            <h1>Courses</h1>\n            <Outlet />\n          </div>\n        );\n      }\n\n      TestRenderer.act(() => {\n        TestRenderer.create(\n          <MemoryRouter initialEntries={[\"/courses/react\"]}>\n            <Routes>\n              <Route path=\"courses\" element={<Courses />}>\n                <Route path=\"react/*\" element={<ReactCourses />} />\n              </Route>\n            </Routes>\n          </MemoryRouter>,\n        );\n      });\n\n      expect(consoleWarn).not.toHaveBeenCalled();\n    });\n  });\n});\n"
  },
  {
    "path": "packages/react-router/__tests__/dom/client-on-error-test.tsx",
    "content": "import {\n  act,\n  fireEvent,\n  render,\n  screen,\n  waitFor,\n} from \"@testing-library/react\";\nimport * as React from \"react\";\n\nimport {\n  Await,\n  RouterProvider,\n  createMemoryRouter,\n  useFetcher,\n  useLoaderData,\n  useRouteError,\n} from \"../../index\";\n\nimport { createFormData, tick } from \"../router/utils/utils\";\nimport getHtml from \"../utils/getHtml\";\n\ndescribe(`handleError`, () => {\n  let consoleError: jest.SpyInstance;\n\n  beforeEach(() => {\n    consoleError = jest.spyOn(console, \"error\").mockImplementation(() => {});\n  });\n\n  afterEach(() => {\n    consoleError.mockRestore();\n  });\n\n  it(\"handles hydration lazy errors\", async () => {\n    let spy = jest.fn();\n    let router = createMemoryRouter([\n      {\n        path: \"/\",\n        async lazy() {\n          await tick();\n          throw new Error(\"lazy error!\");\n        },\n        HydrateFallback: () => <h1>Loading...</h1>,\n      },\n    ]);\n\n    let { container } = render(\n      <RouterProvider router={router} onError={spy} />,\n    );\n    await waitFor(() => screen.getByText(\"lazy error!\"));\n\n    expect(spy).toHaveBeenCalledWith(new Error(\"lazy error!\"), {\n      location: expect.objectContaining({ pathname: \"/\" }),\n      params: {},\n      unstable_pattern: \"/\",\n    });\n    expect(spy).toHaveBeenCalledTimes(1);\n    expect(getHtml(container)).toContain(\"Unexpected Application Error!\");\n  });\n\n  it(\"handles hydration middleware errors\", async () => {\n    let spy = jest.fn();\n    let router = createMemoryRouter([\n      {\n        path: \"/\",\n        middleware: [\n          async () => {\n            await tick();\n            throw new Error(\"middleware error!\");\n          },\n        ],\n        HydrateFallback: () => <h1>Loading...</h1>,\n        Component: () => <h1>Home</h1>,\n        ErrorBoundary: () => (\n          <h1>Error:{(useRouteError() as Error).message}</h1>\n        ),\n      },\n    ]);\n\n    render(<RouterProvider router={router} onError={spy} />);\n\n    await waitFor(() => screen.getByText(\"Error:middleware error!\"));\n\n    expect(spy).toHaveBeenCalledWith(new Error(\"middleware error!\"), {\n      location: expect.objectContaining({ pathname: \"/\" }),\n      params: {},\n      unstable_pattern: \"/\",\n    });\n    expect(spy).toHaveBeenCalledTimes(1);\n  });\n\n  it(\"handles hydration loader errors\", async () => {\n    let spy = jest.fn();\n    let router = createMemoryRouter([\n      {\n        path: \"/\",\n        async loader() {\n          await tick;\n          throw new Error(\"loader error!\");\n        },\n        Component: () => <h1>Home</h1>,\n        ErrorBoundary: () => (\n          <h1>Error:{(useRouteError() as Error).message}</h1>\n        ),\n        HydrateFallback: () => <h1>Loading...</h1>,\n      },\n    ]);\n\n    render(<RouterProvider router={router} onError={spy} />);\n\n    await waitFor(() => screen.getByText(\"Error:loader error!\"));\n\n    expect(spy).toHaveBeenCalledWith(new Error(\"loader error!\"), {\n      location: expect.objectContaining({ pathname: \"/\" }),\n      params: {},\n      unstable_pattern: \"/\",\n    });\n    expect(spy).toHaveBeenCalledTimes(1);\n  });\n\n  it(\"handles navigation lazy errors\", async () => {\n    let spy = jest.fn();\n    let router = createMemoryRouter([\n      {\n        path: \"/\",\n        Component: () => <h1>Home</h1>,\n      },\n      {\n        id: \"page\",\n        path: \"/page\",\n        async lazy() {\n          throw new Error(\"lazy error!\");\n        },\n        HydrateFallback: () => <h1>Loading...</h1>,\n      },\n    ]);\n\n    let { container } = render(\n      <RouterProvider router={router} onError={spy} />,\n    );\n\n    await act(() => router.navigate(\"/page\"));\n\n    expect(spy).toHaveBeenCalledWith(new Error(\"lazy error!\"), {\n      location: expect.objectContaining({ pathname: \"/page\" }),\n      params: {},\n      unstable_pattern: \"/page\",\n    });\n    expect(spy).toHaveBeenCalledTimes(1);\n    let html = getHtml(container);\n    expect(html).toContain(\"Unexpected Application Error!\");\n    expect(html).toContain(\"lazy error!\");\n  });\n\n  it(\"handles navigation middleware errors\", async () => {\n    let spy = jest.fn();\n    let router = createMemoryRouter([\n      {\n        path: \"/\",\n        Component: () => <h1>Home</h1>,\n      },\n      {\n        path: \"/page\",\n        middleware: [\n          () => {\n            throw new Error(\"middleware error!\");\n          },\n        ],\n        Component: () => <h1>Page</h1>,\n        ErrorBoundary: () => <h1>Error</h1>,\n      },\n    ]);\n\n    let { container } = render(\n      <RouterProvider router={router} onError={spy} />,\n    );\n\n    await act(() => router.navigate(\"/page\"));\n\n    expect(spy).toHaveBeenCalledWith(new Error(\"middleware error!\"), {\n      location: expect.objectContaining({ pathname: \"/page\" }),\n      params: {},\n      unstable_pattern: \"/page\",\n    });\n    expect(spy).toHaveBeenCalledTimes(1);\n    expect(getHtml(container)).toContain(\"Error\");\n  });\n\n  it(\"handles navigation loader errors\", async () => {\n    let spy = jest.fn();\n    let router = createMemoryRouter([\n      {\n        path: \"/\",\n        Component: () => <h1>Home</h1>,\n      },\n      {\n        path: \"/page\",\n        loader() {\n          throw new Error(\"loader error!\");\n        },\n        Component: () => <h1>Page</h1>,\n        ErrorBoundary: () => <h1>Error</h1>,\n      },\n    ]);\n\n    let { container } = render(\n      <RouterProvider router={router} onError={spy} />,\n    );\n\n    await act(() => router.navigate(\"/page\"));\n\n    expect(spy).toHaveBeenCalledWith(new Error(\"loader error!\"), {\n      location: expect.objectContaining({ pathname: \"/page\" }),\n      params: {},\n      unstable_pattern: \"/page\",\n    });\n    expect(spy).toHaveBeenCalledTimes(1);\n    expect(getHtml(container)).toContain(\"Error\");\n  });\n\n  it(\"handles navigation action errors\", async () => {\n    let spy = jest.fn();\n    let router = createMemoryRouter([\n      {\n        path: \"/\",\n        Component: () => <h1>Home</h1>,\n      },\n      {\n        path: \"/page\",\n        action() {\n          throw new Error(\"action error!\");\n        },\n        Component: () => <h1>Page</h1>,\n        ErrorBoundary: () => <h1>Error</h1>,\n      },\n    ]);\n\n    let { container } = render(\n      <RouterProvider router={router} onError={spy} />,\n    );\n\n    await act(() =>\n      router.navigate(\"/page\", {\n        formMethod: \"post\",\n        formData: createFormData({}),\n      }),\n    );\n\n    expect(spy).toHaveBeenCalledWith(new Error(\"action error!\"), {\n      location: expect.objectContaining({ pathname: \"/page\" }),\n      params: {},\n      unstable_pattern: \"/page\",\n    });\n    expect(spy).toHaveBeenCalledTimes(1);\n    expect(getHtml(container)).toContain(\"Error\");\n  });\n\n  it(\"handles fetcher loader errors\", async () => {\n    let spy = jest.fn();\n    let router = createMemoryRouter([\n      {\n        path: \"/\",\n        Component: () => <h1>Home</h1>,\n      },\n      {\n        path: \"/fetch\",\n        loader() {\n          throw new Error(\"loader error!\");\n        },\n      },\n    ]);\n\n    let { container } = render(\n      <RouterProvider router={router} onError={spy} />,\n    );\n\n    await act(() => router.fetch(\"key\", \"0\", \"/fetch\"));\n\n    expect(spy).toHaveBeenCalledWith(new Error(\"loader error!\"), {\n      location: expect.objectContaining({ pathname: \"/\" }),\n      params: {},\n      unstable_pattern: \"/\",\n    });\n    expect(spy).toHaveBeenCalledTimes(1);\n    expect(getHtml(container)).toContain(\"Error\");\n  });\n\n  it(\"handles fetcher action errors\", async () => {\n    let spy = jest.fn();\n    let router = createMemoryRouter([\n      {\n        path: \"/\",\n        Component: () => <h1>Home</h1>,\n      },\n      {\n        path: \"/fetch\",\n        action() {\n          throw new Error(\"action error!\");\n        },\n      },\n    ]);\n\n    let { container } = render(\n      <RouterProvider router={router} onError={spy} />,\n    );\n\n    await act(() =>\n      router.fetch(\"key\", \"0\", \"/fetch\", {\n        formMethod: \"post\",\n        formData: createFormData({}),\n      }),\n    );\n\n    expect(spy).toHaveBeenCalledWith(new Error(\"action error!\"), {\n      location: expect.objectContaining({ pathname: \"/\" }),\n      params: {},\n      unstable_pattern: \"/\",\n    });\n    expect(spy).toHaveBeenCalledTimes(1);\n    expect(getHtml(container)).toContain(\"Error\");\n  });\n\n  it(\"handles render errors\", async () => {\n    let spy = jest.fn();\n    let router = createMemoryRouter([\n      {\n        path: \"/\",\n        Component: () => <h1>Home</h1>,\n      },\n      {\n        path: \"/page\",\n        Component: () => {\n          throw new Error(\"render error!\");\n        },\n        ErrorBoundary: () => <h1>Error</h1>,\n      },\n    ]);\n\n    let { container } = render(\n      <RouterProvider router={router} onError={spy} />,\n    );\n\n    await act(() => router.navigate(\"/page\"));\n\n    expect(spy).toHaveBeenCalledWith(new Error(\"render error!\"), {\n      location: expect.objectContaining({ pathname: \"/page\" }),\n      params: {},\n      unstable_pattern: \"/page\",\n      errorInfo: expect.objectContaining({\n        componentStack: expect.any(String),\n      }),\n    });\n    expect(spy).toHaveBeenCalledTimes(1);\n    expect(getHtml(container)).toContain(\"Error\");\n  });\n\n  it(\"handles deferred data rejections from <Await>\", async () => {\n    let spy = jest.fn();\n    let router = createMemoryRouter([\n      {\n        path: \"/\",\n        Component: () => <h1>Home</h1>,\n      },\n      {\n        path: \"/page\",\n        loader() {\n          return {\n            promise: new Promise((_, r) =>\n              setTimeout(() => r(new Error(\"await error!\")), 1),\n            ),\n          };\n        },\n        Component() {\n          let data = useLoaderData();\n          return (\n            <Await resolve={data.promise} errorElement={<h1>Await Error</h1>}>\n              {() => <p>Should not see me</p>}\n            </Await>\n          );\n        },\n      },\n    ]);\n\n    let { container } = render(\n      <RouterProvider router={router} onError={spy} />,\n    );\n\n    await act(() => router.navigate(\"/page\"));\n    await waitFor(() => screen.getByText(\"Await Error\"));\n\n    expect(spy).toHaveBeenCalledWith(new Error(\"await error!\"), {\n      location: expect.objectContaining({ pathname: \"/page\" }),\n      params: {},\n      unstable_pattern: \"/page\",\n    });\n    expect(spy).toHaveBeenCalledTimes(1);\n    expect(getHtml(container)).toContain(\"Await Error\");\n  });\n\n  it(\"handles render errors from Await components\", async () => {\n    let spy = jest.fn();\n    let router = createMemoryRouter([\n      {\n        path: \"/\",\n        Component: () => <h1>Home</h1>,\n      },\n      {\n        path: \"/page\",\n        loader() {\n          return {\n            promise: new Promise((r) => setTimeout(() => r(\"data\"), 10)),\n          };\n        },\n        Component() {\n          let data = useLoaderData();\n          return (\n            <React.Suspense fallback={<p>Loading...</p>}>\n              <Await resolve={data.promise} errorElement={<h1>Await Error</h1>}>\n                <RenderAwait />\n              </Await>\n            </React.Suspense>\n          );\n        },\n      },\n    ]);\n\n    function RenderAwait() {\n      throw new Error(\"await error!\");\n      // eslint-disable-next-line no-unreachable\n      return <p>should not see me</p>;\n    }\n\n    let { container } = render(\n      <RouterProvider router={router} onError={spy} />,\n    );\n\n    await act(() => router.navigate(\"/page\"));\n    await waitFor(() => screen.getByText(\"Await Error\"));\n\n    expect(spy).toHaveBeenCalledWith(new Error(\"await error!\"), {\n      location: expect.objectContaining({ pathname: \"/page\" }),\n      params: {},\n      unstable_pattern: \"/page\",\n      errorInfo: expect.objectContaining({\n        componentStack: expect.any(String),\n      }),\n    });\n    expect(spy).toHaveBeenCalledTimes(1);\n    expect(getHtml(container)).toContain(\"Await Error\");\n  });\n\n  it(\"handles render errors from Await errorElement\", async () => {\n    let spy = jest.fn();\n    let router = createMemoryRouter([\n      {\n        path: \"/\",\n        Component: () => <h1>Home</h1>,\n      },\n      {\n        path: \"/page\",\n        loader() {\n          return {\n            promise: new Promise((_, r) =>\n              setTimeout(() => r(new Error(\"await error!\")), 10),\n            ),\n          };\n        },\n        Component() {\n          let data = useLoaderData();\n          return (\n            <React.Suspense fallback={<p>Loading...</p>}>\n              <Await resolve={data.promise} errorElement={<RenderError />}>\n                {() => <p>Should not see me</p>}\n              </Await>\n            </React.Suspense>\n          );\n        },\n        ErrorBoundary: () => <h1>Route Error</h1>,\n      },\n    ]);\n\n    function RenderError() {\n      throw new Error(\"errorElement error!\");\n      // eslint-disable-next-line no-unreachable\n      return <p>should not see me</p>;\n    }\n\n    let { container } = render(\n      <RouterProvider router={router} onError={spy} />,\n    );\n\n    await act(() => router.navigate(\"/page\"));\n    await waitFor(() => screen.getByText(\"Route Error\"));\n\n    expect(spy).toHaveBeenCalledWith(new Error(\"await error!\"), {\n      location: expect.objectContaining({ pathname: \"/page\" }),\n      params: {},\n      unstable_pattern: \"/page\",\n    });\n    expect(spy).toHaveBeenCalledWith(new Error(\"errorElement error!\"), {\n      location: expect.objectContaining({ pathname: \"/page\" }),\n      params: {},\n      unstable_pattern: \"/page\",\n      errorInfo: expect.objectContaining({\n        componentStack: expect.any(String),\n      }),\n    });\n    expect(spy).toHaveBeenCalledTimes(2);\n    expect(getHtml(container)).toContain(\"Route Error\");\n  });\n\n  it(\"doesn't double report on state updates during an error boundary from a data error\", async () => {\n    let spy = jest.fn();\n    let router = createMemoryRouter([\n      {\n        path: \"/\",\n        Component: () => <h1>Home</h1>,\n      },\n      {\n        path: \"/page\",\n        loader() {\n          throw new Error(\"loader error!\");\n        },\n        Component: () => <h1>Page</h1>,\n        ErrorBoundary() {\n          let fetcher = useFetcher();\n          return (\n            <>\n              <h1>Error</h1>\n              <button onClick={() => fetcher.load(\"/fetch\")}>Fetch</button>\n              <p>{fetcher.data}</p>\n            </>\n          );\n        },\n      },\n      {\n        path: \"/fetch\",\n        loader() {\n          return \"FETCH\";\n        },\n      },\n    ]);\n\n    let { container } = render(\n      <RouterProvider router={router} onError={spy} />,\n    );\n\n    await act(() => router.navigate(\"/page\"));\n\n    expect(spy).toHaveBeenCalledWith(new Error(\"loader error!\"), {\n      location: expect.objectContaining({ pathname: \"/page\" }),\n      params: {},\n      unstable_pattern: \"/page\",\n    });\n    expect(spy).toHaveBeenCalledTimes(1);\n    expect(getHtml(container)).toContain(\"Error\");\n\n    // Doesn't re-call on a fetcher update from a rendered error boundary\n    await fireEvent.click(container.querySelector(\"button\")!);\n    await waitFor(() => screen.getByText(\"FETCH\"));\n    expect(spy.mock.calls.length).toBe(1);\n  });\n\n  it(\"doesn't double report on state updates during an error boundary from a render error\", async () => {\n    let spy = jest.fn();\n    let router = createMemoryRouter([\n      {\n        path: \"/\",\n        Component: () => <h1>Home</h1>,\n      },\n      {\n        path: \"/page\",\n        Component: () => {\n          throw new Error(\"render error!\");\n        },\n        ErrorBoundary() {\n          let fetcher = useFetcher();\n          return (\n            <>\n              <h1>Error</h1>\n              <button onClick={() => fetcher.load(\"/fetch\")}>Fetch</button>\n              <p>{fetcher.data}</p>\n            </>\n          );\n        },\n      },\n      {\n        path: \"/fetch\",\n        loader() {\n          return \"FETCH\";\n        },\n      },\n    ]);\n\n    let { container } = render(\n      <RouterProvider router={router} onError={spy} />,\n    );\n\n    await act(() => router.navigate(\"/page\"));\n\n    expect(spy).toHaveBeenCalledWith(new Error(\"render error!\"), {\n      location: expect.objectContaining({ pathname: \"/page\" }),\n      params: {},\n      unstable_pattern: \"/page\",\n      errorInfo: expect.objectContaining({\n        componentStack: expect.any(String),\n      }),\n    });\n    expect(spy).toHaveBeenCalledTimes(1);\n    expect(getHtml(container)).toContain(\"Error\");\n\n    // Doesn't re-call on a fetcher update from a rendered error boundary\n    await fireEvent.click(container.querySelector(\"button\")!);\n    await waitFor(() => screen.getByText(\"FETCH\"));\n    expect(spy.mock.calls.length).toBe(1);\n  });\n});\n"
  },
  {
    "path": "packages/react-router/__tests__/dom/components/LazyComponent.tsx",
    "content": "import * as React from \"react\";\n\nexport default function LazyComponent() {\n  return <h1>Lazy</h1>;\n}\n"
  },
  {
    "path": "packages/react-router/__tests__/dom/concurrent-mode-navigations-test.tsx",
    "content": "import * as React from \"react\";\nimport {\n  MemoryRouter,\n  Routes,\n  Route,\n  useNavigate,\n  BrowserRouter,\n  HashRouter,\n  createMemoryRouter,\n  createRoutesFromElements,\n  RouterProvider,\n} from \"../../index\";\nimport {\n  act,\n  fireEvent,\n  render,\n  screen,\n  waitFor,\n} from \"@testing-library/react\";\nimport { createDeferred } from \"../router/utils/utils\";\nimport getHtml from \"../utils/getHtml\";\nimport getWindow from \"../utils/getWindow\";\n\ndescribe(\"Handles concurrent mode features during navigations\", () => {\n  function getComponents() {\n    function Home() {\n      let navigate = useNavigate();\n      return (\n        <>\n          <h1>Home</h1>\n          <button onClick={() => navigate(\"/about\")}>/about</button>\n          <button onClick={() => navigate(\"/lazy\")}>/lazy</button>\n        </>\n      );\n    }\n\n    let resolved = false;\n    let dfd = createDeferred();\n    function resolve() {\n      resolved = true;\n      return dfd.resolve();\n    }\n\n    function About() {\n      let navigate = useNavigate();\n      if (!resolved) {\n        throw dfd.promise;\n      }\n      return (\n        <>\n          <h1>About</h1>\n          <button onClick={() => navigate(-1)}>back</button>\n        </>\n      );\n    }\n\n    let lazyDfd = createDeferred();\n\n    const LazyComponent = React.lazy(async () => {\n      await lazyDfd.promise;\n      return import(\"./components/LazyComponent\");\n    });\n\n    return {\n      Home,\n      About,\n      LazyComponent,\n      resolve,\n      resolveLazy: lazyDfd.resolve,\n    };\n  }\n\n  describe(\"when the destination route suspends with a boundary\", () => {\n    async function assertNavigation(\n      container: HTMLElement,\n      resolve: () => void,\n      resolveLazy: () => void,\n    ) {\n      // Start on home\n      expect(getHtml(container)).toMatch(\"Home\");\n\n      // Click to /about and should see Suspense boundary\n      await act(() => {\n        fireEvent.click(screen.getByText(\"/about\"));\n      });\n      await waitFor(() => screen.getByText(\"Loading...\"));\n      expect(getHtml(container)).toMatch(\"Loading...\");\n\n      // Resolve the destination UI to clear the boundary\n      await act(() => resolve());\n      await waitFor(() => screen.getByText(\"About\"));\n      expect(getHtml(container)).toMatch(\"About\");\n\n      // Back to home\n      await act(() => {\n        fireEvent.click(screen.getByText(\"back\"));\n      });\n      await waitFor(() => screen.getByText(\"Home\"));\n      expect(getHtml(container)).toMatch(\"Home\");\n\n      // Click to /lazy and should see Suspense boundary\n      await act(() => {\n        fireEvent.click(screen.getByText(\"/lazy\"));\n      });\n      await waitFor(() => screen.getByText(\"Loading Lazy Component...\"));\n      expect(getHtml(container)).toMatch(\"Loading Lazy Component...\");\n\n      // Resolve the lazy component to clear the boundary\n      await act(() => resolveLazy());\n      await waitFor(() => screen.getByText(\"Lazy\"));\n      expect(getHtml(container)).toMatch(\"Lazy\");\n    }\n\n    // eslint-disable-next-line jest/expect-expect\n    it(\"MemoryRouter\", async () => {\n      let { Home, About, LazyComponent, resolve, resolveLazy } =\n        getComponents();\n\n      let { container } = render(\n        <MemoryRouter>\n          <Routes>\n            <Route path=\"/\" element={<Home />} />\n            <Route\n              path=\"/about\"\n              element={\n                <React.Suspense fallback={<p>Loading...</p>}>\n                  <About />\n                </React.Suspense>\n              }\n            />\n            <Route\n              path=\"/lazy\"\n              element={\n                <React.Suspense fallback={<p>Loading Lazy Component...</p>}>\n                  <LazyComponent />\n                </React.Suspense>\n              }\n            />\n          </Routes>\n        </MemoryRouter>,\n      );\n\n      await assertNavigation(container, resolve, resolveLazy);\n    });\n\n    // eslint-disable-next-line jest/expect-expect\n    it(\"BrowserRouter\", async () => {\n      let { Home, About, LazyComponent, resolve, resolveLazy } =\n        getComponents();\n\n      let { container } = render(\n        <BrowserRouter window={getWindow(\"/\", false)}>\n          <Routes>\n            <Route path=\"/\" element={<Home />} />\n            <Route\n              path=\"/about\"\n              element={\n                <React.Suspense fallback={<p>Loading...</p>}>\n                  <About />\n                </React.Suspense>\n              }\n            />\n            <Route\n              path=\"/lazy\"\n              element={\n                <React.Suspense fallback={<p>Loading Lazy Component...</p>}>\n                  <LazyComponent />\n                </React.Suspense>\n              }\n            />\n          </Routes>\n        </BrowserRouter>,\n      );\n\n      await assertNavigation(container, resolve, resolveLazy);\n    });\n\n    // eslint-disable-next-line jest/expect-expect\n    it(\"HashRouter\", async () => {\n      let { Home, About, LazyComponent, resolve, resolveLazy } =\n        getComponents();\n\n      let { container } = render(\n        <HashRouter window={getWindow(\"/\", true)}>\n          <Routes>\n            <Route path=\"/\" element={<Home />} />\n            <Route\n              path=\"/about\"\n              element={\n                <React.Suspense fallback={<p>Loading...</p>}>\n                  <About />\n                </React.Suspense>\n              }\n            />\n            <Route\n              path=\"/lazy\"\n              element={\n                <React.Suspense fallback={<p>Loading Lazy Component...</p>}>\n                  <LazyComponent />\n                </React.Suspense>\n              }\n            />\n          </Routes>\n        </HashRouter>,\n      );\n\n      await assertNavigation(container, resolve, resolveLazy);\n    });\n\n    // eslint-disable-next-line jest/expect-expect\n    it(\"RouterProvider\", async () => {\n      let { Home, About, LazyComponent, resolve, resolveLazy } =\n        getComponents();\n\n      let router = createMemoryRouter(\n        createRoutesFromElements(\n          <>\n            <Route path=\"/\" element={<Home />} />\n            <Route\n              path=\"/about\"\n              element={\n                <React.Suspense fallback={<p>Loading...</p>}>\n                  <About />\n                </React.Suspense>\n              }\n            />\n            <Route\n              path=\"/lazy\"\n              element={\n                <React.Suspense fallback={<p>Loading Lazy Component...</p>}>\n                  <LazyComponent />\n                </React.Suspense>\n              }\n            />\n          </>,\n        ),\n      );\n      let { container } = render(<RouterProvider router={router} />);\n\n      await assertNavigation(container, resolve, resolveLazy);\n    });\n  });\n\n  describe(\"when the destination route suspends without a boundary\", () => {\n    async function assertNavigation(\n      container: HTMLElement,\n      resolve: () => void,\n      resolveLazy: () => void,\n    ) {\n      // Start on home\n      expect(getHtml(container)).toMatch(\"Home\");\n\n      // Click to /about and should see the frozen current UI\n      await act(() => {\n        fireEvent.click(screen.getByText(\"/about\"));\n      });\n      await waitFor(() => screen.getByText(\"Home\"));\n      expect(getHtml(container)).toMatch(\"Home\");\n\n      // Resolve the destination UI to clear the boundary\n      await act(() => resolve());\n      await waitFor(() => screen.getByText(\"About\"));\n      expect(getHtml(container)).toMatch(\"About\");\n\n      // Back to home\n      await act(() => {\n        fireEvent.click(screen.getByText(\"back\"));\n      });\n      await waitFor(() => screen.getByText(\"Home\"));\n      expect(getHtml(container)).toMatch(\"Home\");\n\n      // Click to /lazy and should see the frozen current UI\n      await act(() => {\n        fireEvent.click(screen.getByText(\"/lazy\"));\n      });\n      await waitFor(() => screen.getByText(\"Home\"));\n      expect(getHtml(container)).toMatch(\"Home\");\n\n      // Resolve the lazy component to clear the boundary\n      await act(() => resolveLazy());\n      await waitFor(() => screen.getByText(\"Lazy\"));\n      expect(getHtml(container)).toMatch(\"Lazy\");\n    }\n\n    // eslint-disable-next-line jest/expect-expect\n    it(\"MemoryRouter\", async () => {\n      let { Home, About, resolve, LazyComponent, resolveLazy } =\n        getComponents();\n\n      let { container } = render(\n        <MemoryRouter>\n          <Routes>\n            <Route path=\"/\" element={<Home />} />\n            <Route path=\"/about\" element={<About />} />\n            <Route path=\"/lazy\" element={<LazyComponent />} />\n          </Routes>\n        </MemoryRouter>,\n      );\n\n      await assertNavigation(container, resolve, resolveLazy);\n    });\n\n    // eslint-disable-next-line jest/expect-expect\n    it(\"BrowserRouter\", async () => {\n      let { Home, About, resolve, LazyComponent, resolveLazy } =\n        getComponents();\n\n      let { container } = render(\n        <BrowserRouter window-={getWindow(\"/\", true)}>\n          <Routes>\n            <Route path=\"/\" element={<Home />} />\n            <Route path=\"/about\" element={<About />} />\n            <Route path=\"/lazy\" element={<LazyComponent />} />\n          </Routes>\n        </BrowserRouter>,\n      );\n\n      await assertNavigation(container, resolve, resolveLazy);\n    });\n\n    // eslint-disable-next-line jest/expect-expect\n    it(\"HashRouter\", async () => {\n      let { Home, About, resolve, LazyComponent, resolveLazy } =\n        getComponents();\n\n      let { container } = render(\n        <HashRouter window-={getWindow(\"/\", true)}>\n          <Routes>\n            <Route path=\"/\" element={<Home />} />\n            <Route path=\"/about\" element={<About />} />\n            <Route path=\"/lazy\" element={<LazyComponent />} />\n          </Routes>\n        </HashRouter>,\n      );\n\n      await assertNavigation(container, resolve, resolveLazy);\n    });\n\n    // eslint-disable-next-line jest/expect-expect\n    it(\"RouterProvider\", async () => {\n      let { Home, About, resolve, LazyComponent, resolveLazy } =\n        getComponents();\n\n      let router = createMemoryRouter(\n        createRoutesFromElements(\n          <>\n            <Route path=\"/\" element={<Home />} />\n            <Route path=\"/about\" element={<About />} />\n            <Route path=\"/lazy\" element={<LazyComponent />} />\n          </>,\n        ),\n      );\n      let { container } = render(<RouterProvider router={router} />);\n\n      await assertNavigation(container, resolve, resolveLazy);\n    });\n  });\n});\n"
  },
  {
    "path": "packages/react-router/__tests__/dom/data-browser-router-legacy-formdata-test.tsx",
    "content": "// Drop support for the submitter parameter, as in a legacy browser. This\n// needs to be done before react-router-dom is required, since it does some\n// FormData detection.\nimport \"./polyfills/drop-FormData-submitter\";\nimport * as React from \"react\";\nimport { render, fireEvent, screen } from \"@testing-library/react\";\nimport \"@testing-library/jest-dom\";\nimport {\n  Form,\n  Route,\n  RouterProvider,\n  createBrowserRouter,\n  createHashRouter,\n  createRoutesFromElements,\n} from \"../../index\";\nimport getWindow from \"../utils/getWindow\";\n\ntestDomRouter(\"<DataBrowserRouter>\", createBrowserRouter, (url) =>\n  getWindow(url, false),\n);\n\ntestDomRouter(\"<DataHashRouter>\", createHashRouter, (url) =>\n  getWindow(url, true),\n);\n\nfunction testDomRouter(\n  name: string,\n  createTestRouter: typeof createBrowserRouter | typeof createHashRouter,\n  getWindow: (initialUrl: string, isHash?: boolean) => Window,\n) {\n  describe(`Router: ${name} with a legacy FormData implementation`, () => {\n    let consoleWarn: jest.SpyInstance;\n    let consoleError: jest.SpyInstance;\n\n    beforeEach(() => {\n      consoleWarn = jest.spyOn(console, \"warn\").mockImplementation(() => {});\n      consoleError = jest.spyOn(console, \"error\").mockImplementation(() => {});\n    });\n\n    afterEach(() => {\n      window.__staticRouterHydrationData = undefined;\n      consoleWarn.mockRestore();\n      consoleError.mockRestore();\n    });\n\n    describe(\"useSubmit/Form FormData\", () => {\n      it(\"appends basic submitter value(s)\", async () => {\n        let actionSpy = jest.fn();\n        actionSpy.mockReturnValue({});\n        async function getPayload() {\n          let formData =\n            await actionSpy.mock.calls[\n              actionSpy.mock.calls.length - 1\n            ][0].request.formData();\n          return new URLSearchParams(formData.entries()).toString();\n        }\n\n        let router = createTestRouter(\n          createRoutesFromElements(\n            <Route path=\"/\" action={actionSpy} element={<FormPage />} />,\n          ),\n          { window: getWindow(\"/\") },\n        );\n        render(<RouterProvider router={router} />);\n\n        function FormPage() {\n          return (\n            <>\n              <button name=\"tasks\" value=\"outside\" form=\"myform\">\n                Outside\n              </button>\n              <Form id=\"myform\" method=\"post\">\n                <input type=\"text\" name=\"tasks\" defaultValue=\"first\" />\n                <input type=\"text\" name=\"tasks\" defaultValue=\"second\" />\n\n                <button name=\"tasks\" value=\"\">\n                  Add Task\n                </button>\n                <button value=\"\">No Name</button>\n                <input type=\"image\" name=\"tasks\" alt=\"Add Task\" />\n                <input type=\"image\" alt=\"No Name\" />\n\n                <input type=\"text\" name=\"tasks\" defaultValue=\"last\" />\n              </Form>\n            </>\n          );\n        }\n\n        fireEvent.click(screen.getByText(\"Add Task\"));\n        expect(await getPayload()).toEqual(\n          \"tasks=first&tasks=second&tasks=last&tasks=\",\n        );\n\n        fireEvent.click(screen.getByText(\"No Name\"));\n        expect(await getPayload()).toEqual(\n          \"tasks=first&tasks=second&tasks=last\",\n        );\n\n        fireEvent.click(screen.getByAltText(\"Add Task\"), {\n          clientX: 1,\n          clientY: 2,\n        });\n        expect(await getPayload()).toEqual(\n          \"tasks=first&tasks=second&tasks=last&tasks.x=0&tasks.y=0\",\n        );\n\n        fireEvent.click(screen.getByAltText(\"No Name\"), {\n          clientX: 1,\n          clientY: 2,\n        });\n        expect(await getPayload()).toEqual(\n          \"tasks=first&tasks=second&tasks=last&x=0&y=0\",\n        );\n\n        fireEvent.click(screen.getByText(\"Outside\"));\n        expect(await getPayload()).toEqual(\n          \"tasks=first&tasks=second&tasks=last&tasks=outside\",\n        );\n      });\n    });\n  });\n}\n"
  },
  {
    "path": "packages/react-router/__tests__/dom/data-browser-router-test.tsx",
    "content": "import {\n  act,\n  fireEvent,\n  render,\n  screen,\n  waitFor,\n} from \"@testing-library/react\";\nimport * as React from \"react\";\nimport type {\n  DataStrategyResult,\n  ErrorResponse,\n  Fetcher,\n  Location,\n  Navigation,\n  RouteObject,\n} from \"../../index\";\nimport {\n  Await,\n  UNSAFE_DataRouterStateContext as DataRouterStateContext,\n  Form,\n  Link,\n  Outlet,\n  RouterProvider,\n  createBrowserRouter,\n  createHashRouter,\n  isRouteErrorResponse,\n  matchRoutes,\n  redirect,\n  useActionData,\n  useAsyncError,\n  useFetcher,\n  useFetchers,\n  useLoaderData,\n  useLocation,\n  useNavigate,\n  useNavigation,\n  useRouteError,\n  useSearchParams,\n  useSubmit,\n} from \"../../index\";\nimport { createDeferred, tick } from \"../router/utils/utils\";\nimport getHtml from \"../utils/getHtml\";\nimport getWindow from \"../utils/getWindow\";\n\ntestDomRouter(\"<DataBrowserRouter>\", createBrowserRouter, (url) =>\n  getWindow(url, false),\n);\n\ntestDomRouter(\"<DataHashRouter>\", createHashRouter, (url) =>\n  getWindow(url, true),\n);\n\nfunction testDomRouter(\n  name: string,\n  createTestRouter: typeof createBrowserRouter | typeof createHashRouter,\n  getWindow: (initialUrl: string, isHash?: boolean) => Window,\n) {\n  // Utility to assert location info based on the type of router\n  function assertLocation(\n    testWindow: Window,\n    pathname: string,\n    search?: string,\n  ) {\n    if (name === \"<DataHashRouter>\") {\n      expect(testWindow.location.hash).toEqual(\"#\" + pathname + (search || \"\"));\n    } else {\n      expect(testWindow.location.pathname).toEqual(pathname);\n      if (search) {\n        expect(testWindow.location.search).toEqual(search);\n      }\n    }\n  }\n\n  describe(`Router: ${name}`, () => {\n    let consoleWarn: jest.SpyInstance;\n    let consoleError: jest.SpyInstance;\n\n    beforeEach(() => {\n      consoleWarn = jest.spyOn(console, \"warn\").mockImplementation(() => {});\n      consoleError = jest.spyOn(console, \"error\").mockImplementation(() => {});\n    });\n\n    afterEach(() => {\n      window.__staticRouterHydrationData = undefined;\n      consoleWarn.mockRestore();\n      consoleError.mockRestore();\n    });\n\n    describe(\"hydration\", () => {\n      it(\"renders the first route that matches the URL\", () => {\n        let router = createTestRouter([\n          { path: \"/\", Component: () => <h1>Home</h1> },\n        ]);\n        let { container } = render(<RouterProvider router={router} />);\n\n        expect(getHtml(container)).toMatchInlineSnapshot(`\n          \"<div>\n            <h1>\n              Home\n            </h1>\n          </div>\"\n        `);\n      });\n\n      it(\"renders the first route that matches the URL when wrapped in a root Route\", () => {\n        let router = createTestRouter(\n          [\n            {\n              path: \"/my/base/path\",\n              children: [\n                {\n                  Component: Outlet,\n                  children: [\n                    { path: \"thing\", Component: () => <h1>Heyooo</h1> },\n                  ],\n                },\n              ],\n            },\n          ],\n          {\n            window: getWindow(\"/my/base/path/thing\"),\n          },\n        );\n        let { container } = render(<RouterProvider router={router} />);\n\n        expect(getHtml(container)).toMatchInlineSnapshot(`\n          \"<div>\n            <h1>\n              Heyooo\n            </h1>\n          </div>\"\n        `);\n      });\n\n      it(\"renders with hydration data\", async () => {\n        let router = createTestRouter(\n          [\n            {\n              path: \"/\",\n              Component: Comp,\n              children: [{ path: \"child\", Component: Comp }],\n            },\n          ],\n          {\n            window: getWindow(\"/child\"),\n            hydrationData: {\n              loaderData: {\n                \"0\": \"parent data\",\n                \"0-0\": \"child data\",\n              },\n              actionData: {\n                \"0-0\": \"child action\",\n              },\n            },\n          },\n        );\n        let { container } = render(<RouterProvider router={router} />);\n\n        function Comp() {\n          let data = useLoaderData();\n          let actionData = useActionData();\n          let navigation = useNavigation();\n          return (\n            <div>\n              <>{data}</>\n              <>{actionData}</>\n              <>{navigation.state}</>\n              <Outlet />\n            </div>\n          );\n        }\n\n        expect(getHtml(container)).toMatchInlineSnapshot(`\n          \"<div>\n            <div>\n              parent data\n              idle\n              <div>\n                child data\n                child action\n                idle\n              </div>\n            </div>\n          </div>\"\n        `);\n      });\n\n      it(\"handles automatic hydration from the window\", async () => {\n        window.__staticRouterHydrationData = {\n          loaderData: {\n            \"0\": \"parent data\",\n            \"0-0\": \"child data\",\n          },\n          actionData: {\n            \"0-0\": \"child action\",\n          },\n        };\n        let router = createTestRouter(\n          [\n            {\n              path: \"/\",\n              Component: Comp,\n              children: [{ path: \"child\", Component: Comp }],\n            },\n          ],\n          {\n            window: getWindow(\"/child\"),\n          },\n        );\n        let { container } = render(<RouterProvider router={router} />);\n\n        function Comp() {\n          let data = useLoaderData();\n          let actionData = useActionData();\n          let navigation = useNavigation();\n          return (\n            <div>\n              <>{data}</>\n              <>{actionData}</>\n              <>{navigation.state}</>\n              <Outlet />\n            </div>\n          );\n        }\n\n        expect(getHtml(container)).toMatchInlineSnapshot(`\n          \"<div>\n            <div>\n              parent data\n              idle\n              <div>\n                child data\n                child action\n                idle\n              </div>\n            </div>\n          </div>\"\n        `);\n      });\n\n      it(\"renders HydrateFallback while first data fetch happens\", async () => {\n        let fooDefer = createDeferred();\n        let router = createTestRouter(\n          [\n            {\n              path: \"/\",\n              Component: Outlet,\n              HydrateFallback: () => <p>Loading...</p>,\n              children: [\n                {\n                  path: \"foo\",\n                  loader: () => fooDefer.promise,\n                  Component: () => {\n                    let data = useLoaderData() as { message: string };\n                    return <h1>Foo:{data.message}</h1>;\n                  },\n                },\n                { path: \"bar\", Component: () => <h1>Bar Heading</h1> },\n              ],\n            },\n          ],\n          {\n            window: getWindow(\"/foo\"),\n          },\n        );\n        let { container } = render(<RouterProvider router={router} />);\n\n        expect(getHtml(container)).toMatchInlineSnapshot(`\n          \"<div>\n            <p>\n              Loading...\n            </p>\n          </div>\"\n        `);\n\n        fooDefer.resolve({ message: \"From Foo Loader\" });\n        await waitFor(() => screen.getByText(\"Foo:From Foo Loader\"));\n        expect(getHtml(container)).toMatchInlineSnapshot(`\n          \"<div>\n            <h1>\n              Foo:\n              From Foo Loader\n            </h1>\n          </div>\"\n        `);\n      });\n\n      it(\"renders hydrateFallbackElement while first data fetch and lazy route load happens\", async () => {\n        let fooDefer = createDeferred();\n        let router = createTestRouter(\n          [\n            {\n              path: \"/\",\n              Component: Outlet,\n              HydrateFallback: () => <p>Loading...</p>,\n              children: [\n                {\n                  path: \"foo\",\n                  lazy: async () => {\n                    return {\n                      loader: () => fooDefer.promise,\n                      Component: () => {\n                        let data = useLoaderData() as { message: string };\n                        return <h1>Foo:{data.message}</h1>;\n                      },\n                    };\n                  },\n                },\n                { path: \"bar\", Component: () => <h1>Bar Heading</h1> },\n              ],\n            },\n          ],\n          {\n            window: getWindow(\"/foo\"),\n          },\n        );\n        let { container } = render(<RouterProvider router={router} />);\n\n        expect(getHtml(container)).toMatchInlineSnapshot(`\n          \"<div>\n            <p>\n              Loading...\n            </p>\n          </div>\"\n        `);\n\n        fooDefer.resolve({ message: \"From Lazy Foo Loader\" });\n        await waitFor(() => screen.getByText(\"Foo:From Lazy Foo Loader\"));\n        expect(getHtml(container)).toMatchInlineSnapshot(`\n          \"<div>\n            <h1>\n              Foo:\n              From Lazy Foo Loader\n            </h1>\n          </div>\"\n        `);\n      });\n\n      it(\"does not render hydrateFallback if no data fetch or lazy loading is required\", async () => {\n        let fooDefer = createDeferred();\n        let router = createTestRouter(\n          [\n            {\n              path: \"/\",\n              Component: Outlet,\n              children: [\n                {\n                  path: \"foo\",\n                  loader: () => fooDefer.promise,\n                  HydrateFallback: () => <p>Loading...</p>,\n                  Component: () => {\n                    let data = useLoaderData() as { message: string };\n                    return <h1>Foo:{data.message}</h1>;\n                  },\n                },\n                { path: \"bar\", Component: () => <h1>Bar Heading</h1> },\n              ],\n            },\n          ],\n          {\n            window: getWindow(\"/bar\"),\n          },\n        );\n        let { container } = render(<RouterProvider router={router} />);\n\n        expect(getHtml(container)).toMatchInlineSnapshot(`\n          \"<div>\n            <h1>\n              Bar Heading\n            </h1>\n          </div>\"\n        `);\n      });\n\n      it(\"renders HydrateFallback within router contexts\", async () => {\n        let fooDefer = createDeferred();\n        let router = createTestRouter(\n          [\n            {\n              path: \"/\",\n              Component: Outlet,\n              HydrateFallback: () => <FallbackElement />,\n              children: [\n                {\n                  path: \"foo\",\n                  loader: () => fooDefer.promise,\n                  Component: () => {\n                    let data = useLoaderData() as { message: string };\n                    return <h1>Foo:{data.message}</h1>;\n                  },\n                },\n              ],\n            },\n          ],\n          { window: getWindow(\"/foo\") },\n        );\n        let { container } = render(<RouterProvider router={router} />);\n\n        function FallbackElement() {\n          let location = useLocation();\n          return <p>Loading{location.pathname}</p>;\n        }\n\n        expect(getHtml(container)).toMatchInlineSnapshot(`\n          \"<div>\n            <p>\n              Loading\n              /foo\n            </p>\n          </div>\"\n        `);\n\n        fooDefer.resolve({ message: \"From Foo Loader\" });\n        await waitFor(() => screen.getByText(\"Foo:From Foo Loader\"));\n        expect(getHtml(container)).toMatchInlineSnapshot(`\n          \"<div>\n            <h1>\n              Foo:\n              From Foo Loader\n            </h1>\n          </div>\"\n        `);\n      });\n\n      it(\"clears the HydrateFallback when dataStrategy returns partial results during hydration\", async () => {\n        let dfd = createDeferred<Record<string, DataStrategyResult>>();\n        let router = createTestRouter(\n          [\n            {\n              id: \"root\",\n              path: \"/\",\n              loader: true,\n              HydrateFallback: () => \"Loading...\",\n              Component: () => (\n                <>\n                  <h1>Root:{useLoaderData()}</h1>\n                  <Outlet />\n                </>\n              ),\n              ErrorBoundary: () => {\n                let error = useRouteError();\n                return (\n                  <pre>\n                    Root:\n                    {error instanceof Error ? error.message : (error as string)}\n                  </pre>\n                );\n              },\n              children: [\n                {\n                  id: \"index\",\n                  index: true,\n                  loader: true,\n                  Component: () => <h2>Index:{useLoaderData()}</h2>,\n                  ErrorBoundary: () => (\n                    <pre>Index:{useRouteError() as string}</pre>\n                  ),\n                },\n              ],\n            },\n          ],\n          {\n            dataStrategy: () => dfd.promise,\n          },\n        );\n        let { container } = render(<RouterProvider router={router} />);\n\n        expect(getHtml(container)).toMatchInlineSnapshot(`\n          \"<div>\n            Loading...\n          </div>\"\n        `);\n\n        // Resolve data strategy with only an error at the index route but nothing\n        // for the root route\n        await dfd.resolve({\n          index: {\n            type: \"error\",\n            result: \"INDEX ERROR\",\n          },\n        });\n        await tick();\n        await tick();\n\n        // The router stubs in an error for the root route to get out of\n        // displaying the HydrateFallback\n        expect(getHtml(container)).toMatchInlineSnapshot(`\n          \"<div>\n            <pre>\n              Root:\n              No result returned from dataStrategy for route root\n            </pre>\n          </div>\"\n        `);\n      });\n    });\n\n    describe(\"navigations\", () => {\n      it(\"handles link navigations\", async () => {\n        let router = createTestRouter(\n          [\n            {\n              path: \"/\",\n              Component: Layout,\n              children: [\n                { path: \"foo\", Component: () => <h1>Foo Heading</h1> },\n                { path: \"bar\", Component: () => <h1>Bar Heading</h1> },\n              ],\n            },\n          ],\n          { window: getWindow(\"/foo\") },\n        );\n        render(<RouterProvider router={router} />);\n\n        function Layout() {\n          return (\n            <div>\n              <Link to=\"/foo\">Link to Foo</Link>\n              <Link to=\"/bar\">Link to Bar</Link>\n              <Outlet />\n            </div>\n          );\n        }\n\n        expect(screen.getByText(\"Foo Heading\")).toBeDefined();\n        fireEvent.click(screen.getByText(\"Link to Bar\"));\n        await waitFor(() => screen.getByText(\"Bar Heading\"));\n\n        fireEvent.click(screen.getByText(\"Link to Foo\"));\n        await waitFor(() => screen.getByText(\"Foo Heading\"));\n      });\n\n      it(\"executes route loaders on navigation\", async () => {\n        let barDefer = createDeferred();\n\n        let router = createTestRouter(\n          [\n            {\n              path: \"/\",\n              Component: Layout,\n              children: [\n                { path: \"foo\", Component: () => <h1>Foo</h1> },\n                {\n                  path: \"bar\",\n                  loader: () => barDefer.promise,\n                  Component: () => {\n                    let data = useLoaderData() as { message: string };\n                    return <h1>{data.message}</h1>;\n                  },\n                },\n              ],\n            },\n          ],\n          { window: getWindow(\"/foo\") },\n        );\n        let { container } = render(<RouterProvider router={router} />);\n\n        function Layout() {\n          let navigation = useNavigation();\n          return (\n            <div>\n              <Link to=\"/bar\">Link to Bar</Link>\n              <div id=\"output\">\n                <p>{navigation.state}</p>\n                <Outlet />\n              </div>\n            </div>\n          );\n        }\n\n        expect(getHtml(container.querySelector(\"#output\")!))\n          .toMatchInlineSnapshot(`\n            \"<div\n              id=\"output\"\n            >\n              <p>\n                idle\n              </p>\n              <h1>\n                Foo\n              </h1>\n            </div>\"\n          `);\n\n        fireEvent.click(screen.getByText(\"Link to Bar\"));\n        expect(getHtml(container.querySelector(\"#output\")!))\n          .toMatchInlineSnapshot(`\n            \"<div\n              id=\"output\"\n            >\n              <p>\n                loading\n              </p>\n              <h1>\n                Foo\n              </h1>\n            </div>\"\n          `);\n\n        barDefer.resolve({ message: \"Bar Loader\" });\n        await waitFor(() => screen.getByText(\"idle\"));\n        expect(getHtml(container.querySelector(\"#output\")!))\n          .toMatchInlineSnapshot(`\n            \"<div\n              id=\"output\"\n            >\n              <p>\n                idle\n              </p>\n              <h1>\n                Bar Loader\n              </h1>\n            </div>\"\n          `);\n      });\n\n      it(\"executes lazy route loaders on navigation\", async () => {\n        let barDefer = createDeferred();\n\n        let router = createTestRouter(\n          [\n            {\n              path: \"/\",\n              Component: Layout,\n              children: [\n                { path: \"foo\", Component: () => <h1>Foo</h1> },\n                {\n                  path: \"bar\",\n                  lazy: async () => ({\n                    loader: () => barDefer.promise,\n                    Component: () => {\n                      let data = useLoaderData() as { message: string };\n                      return <h1>{data.message}</h1>;\n                    },\n                  }),\n                },\n              ],\n            },\n          ],\n          {\n            window: getWindow(\"/foo\"),\n          },\n        );\n        let { container } = render(<RouterProvider router={router} />);\n\n        function Layout() {\n          let navigation = useNavigation();\n          return (\n            <div>\n              <Link to=\"/bar\">Link to Bar</Link>\n              <div id=\"output\">\n                <p>{navigation.state}</p>\n                <Outlet />\n              </div>\n            </div>\n          );\n        }\n\n        expect(getHtml(container.querySelector(\"#output\")!))\n          .toMatchInlineSnapshot(`\n            \"<div\n              id=\"output\"\n            >\n              <p>\n                idle\n              </p>\n              <h1>\n                Foo\n              </h1>\n            </div>\"\n          `);\n\n        fireEvent.click(screen.getByText(\"Link to Bar\"));\n        expect(getHtml(container.querySelector(\"#output\")!))\n          .toMatchInlineSnapshot(`\n            \"<div\n              id=\"output\"\n            >\n              <p>\n                loading\n              </p>\n              <h1>\n                Foo\n              </h1>\n            </div>\"\n          `);\n\n        barDefer.resolve({ message: \"Bar Loader\" });\n        await waitFor(() => screen.getByText(\"idle\"));\n        expect(getHtml(container.querySelector(\"#output\")!))\n          .toMatchInlineSnapshot(`\n            \"<div\n              id=\"output\"\n            >\n              <p>\n                idle\n              </p>\n              <h1>\n                Bar Loader\n              </h1>\n            </div>\"\n          `);\n      });\n\n      it(\"executes route loaders on <Form method=get> navigations\", async () => {\n        let loaderDefer = createDeferred();\n        let actionDefer = createDeferred();\n\n        let router = createTestRouter(\n          [\n            {\n              path: \"/\",\n              action: () => actionDefer.promise,\n              loader: async ({ request }) => {\n                let resolvedValue = await loaderDefer.promise;\n                let urlParam = new URL(\n                  `https://remix.run${request.url}`,\n                ).searchParams.get(\"test\");\n                return `${resolvedValue}:${urlParam}`;\n              },\n              Component: Home,\n            },\n          ],\n          {\n            window: getWindow(\"/\"),\n            hydrationData: { loaderData: { \"0\": null } },\n          },\n        );\n        let { container } = render(<RouterProvider router={router} />);\n\n        function Home() {\n          let data = useLoaderData() as string;\n          let actionData = useActionData() as string | undefined;\n          let navigation = useNavigation();\n          return (\n            <div>\n              <Form method=\"get\">\n                <input name=\"test\" value=\"value\" />\n                <button type=\"submit\">Submit Form</button>\n              </Form>\n              <div id=\"output\">\n                <p>{navigation.state}</p>\n                <p>{data}</p>\n                <p>{actionData}</p>\n              </div>\n              <Outlet />\n            </div>\n          );\n        }\n\n        expect(getHtml(container.querySelector(\"#output\")!))\n          .toMatchInlineSnapshot(`\n            \"<div\n              id=\"output\"\n            >\n              <p>\n                idle\n              </p>\n              <p />\n              <p />\n            </div>\"\n          `);\n\n        fireEvent.click(screen.getByText(\"Submit Form\"));\n        await waitFor(() => screen.getByText(\"loading\"));\n        expect(getHtml(container.querySelector(\"#output\")!))\n          .toMatchInlineSnapshot(`\n            \"<div\n              id=\"output\"\n            >\n              <p>\n                loading\n              </p>\n              <p />\n              <p />\n            </div>\"\n          `);\n\n        loaderDefer.resolve(\"Loader Data\");\n        await waitFor(() => screen.getByText(\"idle\"));\n        expect(getHtml(container.querySelector(\"#output\")!))\n          .toMatchInlineSnapshot(`\n            \"<div\n              id=\"output\"\n            >\n              <p>\n                idle\n              </p>\n              <p>\n                Loader Data:value\n              </p>\n              <p />\n            </div>\"\n          `);\n      });\n\n      it(\"executes lazy route loaders on <Form method=get> navigations\", async () => {\n        let loaderDefer = createDeferred();\n        let actionDefer = createDeferred();\n\n        let router = createTestRouter(\n          [\n            {\n              path: \"/\",\n              Component: Home,\n              children: [\n                { index: true, Component: () => <h1>Home</h1> },\n                {\n                  path: \"path\",\n                  lazy: async () => ({\n                    action: () => actionDefer.promise,\n                    loader: async ({ request }) => {\n                      let resolvedValue = await loaderDefer.promise;\n                      let urlParam = new URL(\n                        `https://remix.run${request.url}`,\n                      ).searchParams.get(\"test\");\n                      return `${resolvedValue}:${urlParam}`;\n                    },\n                    Component() {\n                      let data = useLoaderData() as string;\n                      let actionData = useActionData() as string | undefined;\n                      return (\n                        <>\n                          <h1>Path</h1>\n                          <p>{data}</p>\n                          <p>{actionData}</p>\n                        </>\n                      );\n                    },\n                  }),\n                },\n              ],\n            },\n          ],\n          {\n            window: getWindow(\"/\"),\n          },\n        );\n        let { container } = render(<RouterProvider router={router} />);\n\n        function Home() {\n          let navigation = useNavigation();\n          return (\n            <div>\n              <Form method=\"get\" action=\"path\">\n                <input name=\"test\" value=\"value\" />\n                <button type=\"submit\">Submit Form</button>\n              </Form>\n              <div id=\"output\">\n                <p>{navigation.state}</p>\n                <Outlet />\n              </div>\n            </div>\n          );\n        }\n\n        await waitFor(() => screen.getByText(\"idle\"));\n        expect(getHtml(container.querySelector(\"#output\")!))\n          .toMatchInlineSnapshot(`\n            \"<div\n              id=\"output\"\n            >\n              <p>\n                idle\n              </p>\n              <h1>\n                Home\n              </h1>\n            </div>\"\n          `);\n\n        fireEvent.click(screen.getByText(\"Submit Form\"));\n        await waitFor(() => screen.getByText(\"loading\"));\n        expect(getHtml(container.querySelector(\"#output\")!))\n          .toMatchInlineSnapshot(`\n            \"<div\n              id=\"output\"\n            >\n              <p>\n                loading\n              </p>\n              <h1>\n                Home\n              </h1>\n            </div>\"\n          `);\n\n        loaderDefer.resolve(\"Loader Data\");\n        await waitFor(() => screen.getByText(\"idle\"));\n        expect(getHtml(container.querySelector(\"#output\")!))\n          .toMatchInlineSnapshot(`\n            \"<div\n              id=\"output\"\n            >\n              <p>\n                idle\n              </p>\n              <h1>\n                Path\n              </h1>\n              <p>\n                Loader Data:value\n              </p>\n              <p />\n            </div>\"\n          `);\n      });\n    });\n\n    describe(\"submissions\", () => {\n      it(\"executes route actions/loaders on useSubmit navigations\", async () => {\n        let loaderDefer = createDeferred();\n        let actionDefer = createDeferred();\n\n        let router = createTestRouter(\n          [\n            {\n              path: \"/\",\n              action: () => actionDefer.promise,\n              loader: () => loaderDefer.promise,\n              Component: Home,\n            },\n          ],\n          {\n            window: getWindow(\"/\"),\n            hydrationData: { loaderData: { \"0\": null } },\n          },\n        );\n        let { container } = render(<RouterProvider router={router} />);\n\n        function Home() {\n          let data = useLoaderData() as string;\n          let actionData = useActionData() as string | undefined;\n          let navigation = useNavigation();\n          let submit = useSubmit();\n          let formRef = React.useRef<HTMLFormElement>(null);\n          return (\n            <div>\n              <form method=\"post\" action=\"/\" ref={formRef}>\n                <input name=\"test\" value=\"value\" />\n              </form>\n              <button onClick={() => submit(formRef.current!)}>\n                Submit Form\n              </button>\n              <div id=\"output\">\n                <p>{navigation.state}</p>\n                <p>{data}</p>\n                <p>{actionData}</p>\n              </div>\n              <Outlet />\n            </div>\n          );\n        }\n\n        expect(getHtml(container.querySelector(\"#output\")!))\n          .toMatchInlineSnapshot(`\n            \"<div\n              id=\"output\"\n            >\n              <p>\n                idle\n              </p>\n              <p />\n              <p />\n            </div>\"\n          `);\n\n        fireEvent.click(screen.getByText(\"Submit Form\"));\n        await waitFor(() => screen.getByText(\"submitting\"));\n        expect(getHtml(container.querySelector(\"#output\")!))\n          .toMatchInlineSnapshot(`\n            \"<div\n              id=\"output\"\n            >\n              <p>\n                submitting\n              </p>\n              <p />\n              <p />\n            </div>\"\n          `);\n\n        actionDefer.resolve(\"Action Data\");\n        await waitFor(() => screen.getByText(\"loading\"));\n        expect(getHtml(container.querySelector(\"#output\")!))\n          .toMatchInlineSnapshot(`\n            \"<div\n              id=\"output\"\n            >\n              <p>\n                loading\n              </p>\n              <p />\n              <p>\n                Action Data\n              </p>\n            </div>\"\n          `);\n\n        loaderDefer.resolve(\"Loader Data\");\n        await waitFor(() => screen.getByText(\"idle\"));\n        expect(getHtml(container.querySelector(\"#output\")!))\n          .toMatchInlineSnapshot(`\n            \"<div\n              id=\"output\"\n            >\n              <p>\n                idle\n              </p>\n              <p>\n                Loader Data\n              </p>\n              <p>\n                Action Data\n              </p>\n            </div>\"\n          `);\n      });\n\n      it(\"executes lazy route actions/loaders on useSubmit navigations\", async () => {\n        let loaderDefer = createDeferred();\n        let actionDefer = createDeferred();\n\n        let router = createTestRouter(\n          [\n            {\n              path: \"/\",\n              Component: Home,\n              children: [\n                { index: true, Component: () => <h1>Home</h1> },\n                {\n                  path: \"action\",\n                  lazy: async () => ({\n                    action: () => actionDefer.promise,\n                    loader: () => loaderDefer.promise,\n                    Component() {\n                      let data = useLoaderData() as string;\n                      let actionData = useActionData() as string | undefined;\n                      return (\n                        <>\n                          <h1>Action</h1>\n                          <p>{data}</p>\n                          <p>{actionData}</p>\n                        </>\n                      );\n                    },\n                  }),\n                },\n              ],\n            },\n          ],\n          {\n            window: getWindow(\"/\"),\n          },\n        );\n        let { container } = render(<RouterProvider router={router} />);\n\n        function Home() {\n          let navigation = useNavigation();\n          let submit = useSubmit();\n          let formRef = React.useRef<HTMLFormElement>(null);\n          return (\n            <div>\n              <form method=\"post\" action=\"/action\" ref={formRef}>\n                <input name=\"test\" value=\"value\" />\n              </form>\n              <button onClick={() => submit(formRef.current)}>\n                Submit Form\n              </button>\n              <div id=\"output\">\n                <p>{navigation.state}</p>\n                <Outlet />\n              </div>\n            </div>\n          );\n        }\n\n        await waitFor(() => screen.getByText(\"idle\"));\n        expect(getHtml(container.querySelector(\"#output\")!))\n          .toMatchInlineSnapshot(`\n            \"<div\n              id=\"output\"\n            >\n              <p>\n                idle\n              </p>\n              <h1>\n                Home\n              </h1>\n            </div>\"\n          `);\n\n        fireEvent.click(screen.getByText(\"Submit Form\"));\n        await waitFor(() => screen.getByText(\"submitting\"));\n        expect(getHtml(container.querySelector(\"#output\")!))\n          .toMatchInlineSnapshot(`\n            \"<div\n              id=\"output\"\n            >\n              <p>\n                submitting\n              </p>\n              <h1>\n                Home\n              </h1>\n            </div>\"\n          `);\n\n        actionDefer.resolve(\"Action Data\");\n        await waitFor(() => screen.getByText(\"loading\"));\n        expect(getHtml(container.querySelector(\"#output\")!))\n          .toMatchInlineSnapshot(`\n            \"<div\n              id=\"output\"\n            >\n              <p>\n                loading\n              </p>\n              <h1>\n                Home\n              </h1>\n            </div>\"\n          `);\n\n        loaderDefer.resolve(\"Loader Data\");\n        await waitFor(() => screen.getByText(\"idle\"));\n        expect(getHtml(container.querySelector(\"#output\")!))\n          .toMatchInlineSnapshot(`\n            \"<div\n              id=\"output\"\n            >\n              <p>\n                idle\n              </p>\n              <h1>\n                Action\n              </h1>\n              <p>\n                Loader Data\n              </p>\n              <p>\n                Action Data\n              </p>\n            </div>\"\n          `);\n      });\n\n      it(\"executes route actions/loaders on <Form method=post> navigations\", async () => {\n        let loaderDefer = createDeferred();\n        let actionDefer = createDeferred();\n\n        let router = createTestRouter(\n          [\n            {\n              path: \"/\",\n              action: async ({ request }) => {\n                let resolvedValue = await actionDefer.promise;\n                let formData = await request.formData();\n                return `${resolvedValue}:${formData.get(\"test\")}`;\n              },\n              loader: () => loaderDefer.promise,\n              Component: Home,\n            },\n          ],\n          {\n            window: getWindow(\"/\"),\n            hydrationData: { loaderData: { \"0\": null } },\n          },\n        );\n        let { container } = render(<RouterProvider router={router} />);\n\n        function Home() {\n          let data = useLoaderData() as string;\n          let actionData = useActionData() as string | undefined;\n          let navigation = useNavigation();\n          return (\n            <div>\n              <Form method=\"post\">\n                <input name=\"test\" value=\"value\" />\n                <button type=\"submit\">Submit Form</button>\n              </Form>\n              <div id=\"output\">\n                <p>{navigation.state}</p>\n                <p>{data}</p>\n                <p>{actionData}</p>\n              </div>\n              <Outlet />\n            </div>\n          );\n        }\n\n        expect(getHtml(container.querySelector(\"#output\")!))\n          .toMatchInlineSnapshot(`\n            \"<div\n              id=\"output\"\n            >\n              <p>\n                idle\n              </p>\n              <p />\n              <p />\n            </div>\"\n          `);\n\n        fireEvent.click(screen.getByText(\"Submit Form\"));\n        await waitFor(() => screen.getByText(\"submitting\"));\n        expect(getHtml(container.querySelector(\"#output\")!))\n          .toMatchInlineSnapshot(`\n            \"<div\n              id=\"output\"\n            >\n              <p>\n                submitting\n              </p>\n              <p />\n              <p />\n            </div>\"\n          `);\n\n        actionDefer.resolve(\"Action Data\");\n        await waitFor(() => screen.getByText(\"loading\"));\n        expect(getHtml(container.querySelector(\"#output\")!))\n          .toMatchInlineSnapshot(`\n            \"<div\n              id=\"output\"\n            >\n              <p>\n                loading\n              </p>\n              <p />\n              <p>\n                Action Data:value\n              </p>\n            </div>\"\n          `);\n\n        loaderDefer.resolve(\"Loader Data\");\n        await waitFor(() => screen.getByText(\"idle\"));\n        expect(getHtml(container.querySelector(\"#output\")!))\n          .toMatchInlineSnapshot(`\n            \"<div\n              id=\"output\"\n            >\n              <p>\n                idle\n              </p>\n              <p>\n                Loader Data\n              </p>\n              <p>\n                Action Data:value\n              </p>\n            </div>\"\n          `);\n      });\n\n      it(\"executes lazy route actions/loaders on <Form method=post> navigations\", async () => {\n        let loaderDefer = createDeferred();\n        let actionDefer = createDeferred();\n\n        let router = createTestRouter(\n          [\n            {\n              path: \"/\",\n              Component: Home,\n              children: [\n                { index: true, Component: () => <h1>Home</h1> },\n                {\n                  path: \"action\",\n                  lazy: async () => ({\n                    action: async ({ request }) => {\n                      let resolvedValue = await actionDefer.promise;\n                      let formData = await request.formData();\n                      return `${resolvedValue}:${formData.get(\"test\")}`;\n                    },\n                    loader: () => loaderDefer.promise,\n                    Component() {\n                      let data = useLoaderData() as string;\n                      let actionData = useActionData() as string | undefined;\n                      return (\n                        <>\n                          <h1>Action</h1>\n                          <p>{data}</p>\n                          <p>{actionData}</p>\n                        </>\n                      );\n                    },\n                  }),\n                },\n              ],\n            },\n          ],\n          {\n            window: getWindow(\"/\"),\n            hydrationData: { loaderData: { \"0\": null } },\n          },\n        );\n        let { container } = render(<RouterProvider router={router} />);\n\n        function Home() {\n          let navigation = useNavigation();\n          return (\n            <div>\n              <Form method=\"post\" action=\"action\">\n                <input name=\"test\" value=\"value\" />\n                <button type=\"submit\">Submit Form</button>\n              </Form>\n              <div id=\"output\">\n                <p>{navigation.state}</p>\n                <Outlet />\n              </div>\n            </div>\n          );\n        }\n\n        await waitFor(() => screen.getByText(\"idle\"));\n        expect(getHtml(container.querySelector(\"#output\")!))\n          .toMatchInlineSnapshot(`\n            \"<div\n              id=\"output\"\n            >\n              <p>\n                idle\n              </p>\n              <h1>\n                Home\n              </h1>\n            </div>\"\n          `);\n\n        fireEvent.click(screen.getByText(\"Submit Form\"));\n        await waitFor(() => screen.getByText(\"submitting\"));\n        expect(getHtml(container.querySelector(\"#output\")!))\n          .toMatchInlineSnapshot(`\n            \"<div\n              id=\"output\"\n            >\n              <p>\n                submitting\n              </p>\n              <h1>\n                Home\n              </h1>\n            </div>\"\n          `);\n\n        actionDefer.resolve(\"Action Data\");\n        await waitFor(() => screen.getByText(\"loading\"));\n        expect(getHtml(container.querySelector(\"#output\")!))\n          .toMatchInlineSnapshot(`\n            \"<div\n              id=\"output\"\n            >\n              <p>\n                loading\n              </p>\n              <h1>\n                Home\n              </h1>\n            </div>\"\n          `);\n\n        loaderDefer.resolve(\"Loader Data\");\n        await waitFor(() => screen.getByText(\"idle\"));\n        expect(getHtml(container.querySelector(\"#output\")!))\n          .toMatchInlineSnapshot(`\n            \"<div\n              id=\"output\"\n            >\n              <p>\n                idle\n              </p>\n              <h1>\n                Action\n              </h1>\n              <p>\n                Loader Data\n              </p>\n              <p>\n                Action Data:value\n              </p>\n            </div>\"\n          `);\n      });\n\n      it(\"supports <Form state>\", async () => {\n        let testWindow = getWindow(\"/\");\n        let router = createTestRouter(\n          [\n            {\n              path: \"/\",\n              Component() {\n                return (\n                  <Form method=\"post\" action=\"/action\" state={{ key: \"value\" }}>\n                    <button type=\"submit\">Submit</button>\n                  </Form>\n                );\n              },\n            },\n            {\n              path: \"/action\",\n              action: () => null,\n              Component() {\n                let state = useLocation().state;\n                return <p>{JSON.stringify(state)}</p>;\n              },\n            },\n          ],\n          { window: testWindow },\n        );\n        let { container } = render(<RouterProvider router={router} />);\n        expect(testWindow.history.state.usr).toBeUndefined();\n\n        fireEvent.click(screen.getByText(\"Submit\"));\n        await waitFor(() => screen.getByText('{\"key\":\"value\"}'));\n        expect(getHtml(container)).toMatchInlineSnapshot(`\n          \"<div>\n            <p>\n              {\"key\":\"value\"}\n            </p>\n          </div>\"\n        `);\n        expect(testWindow.history.state.usr).toEqual({ key: \"value\" });\n      });\n\n      it(\"supports <Form reloadDocument={true}>\", async () => {\n        let actionSpy = jest.fn();\n        let router = createTestRouter([\n          { path: \"/\", action: actionSpy, Component: Home },\n        ]);\n        render(<RouterProvider router={router} />);\n\n        let handlerCalled;\n        let defaultPrevented;\n\n        function Home() {\n          return (\n            <Form\n              method=\"post\"\n              reloadDocument={true}\n              onSubmit={(e) => {\n                handlerCalled = true;\n                defaultPrevented = e.defaultPrevented;\n              }}\n            >\n              <input name=\"test\" value=\"value\" />\n              <button type=\"submit\">Submit Form</button>\n            </Form>\n          );\n        }\n\n        fireEvent.click(screen.getByText(\"Submit Form\"));\n        expect(handlerCalled).toBe(true);\n        expect(defaultPrevented).toBe(false);\n        expect(actionSpy).not.toHaveBeenCalled();\n      });\n\n      it(\"allows a button to override the <form method>\", async () => {\n        let loaderDefer = createDeferred();\n\n        let router = createTestRouter(\n          [\n            {\n              id: \"index\",\n              path: \"/\",\n              action: async () => {\n                throw new Error(\"Should not hit this\");\n              },\n              loader: () => loaderDefer.promise,\n              Component: Home,\n            },\n          ],\n          {\n            hydrationData: { loaderData: { index: \"Initial Data\" } },\n            window: getWindow(\"/\"),\n          },\n        );\n        let { container } = render(<RouterProvider router={router} />);\n\n        function Home() {\n          let data = useLoaderData() as string;\n          let navigation = useNavigation();\n          return (\n            <div>\n              <Form method=\"post\">\n                <input name=\"test\" value=\"value\" />\n                <button type=\"submit\" formMethod=\"get\">\n                  Submit Form\n                </button>\n              </Form>\n              <div id=\"output\">\n                <p>{navigation.state}</p>\n                <p>{data}</p>\n              </div>\n              <Outlet />\n            </div>\n          );\n        }\n\n        expect(getHtml(container.querySelector(\"#output\")!))\n          .toMatchInlineSnapshot(`\n            \"<div\n              id=\"output\"\n            >\n              <p>\n                idle\n              </p>\n              <p>\n                Initial Data\n              </p>\n            </div>\"\n          `);\n\n        fireEvent.click(screen.getByText(\"Submit Form\"));\n        await waitFor(() => screen.getByText(\"loading\"));\n        expect(getHtml(container.querySelector(\"#output\")!))\n          .toMatchInlineSnapshot(`\n            \"<div\n              id=\"output\"\n            >\n              <p>\n                loading\n              </p>\n              <p>\n                Initial Data\n              </p>\n            </div>\"\n          `);\n\n        loaderDefer.resolve(\"Loader Data\");\n        await waitFor(() => screen.getByText(\"idle\"));\n        expect(getHtml(container.querySelector(\"#output\")!))\n          .toMatchInlineSnapshot(`\n            \"<div\n              id=\"output\"\n            >\n              <p>\n                idle\n              </p>\n              <p>\n                Loader Data\n              </p>\n            </div>\"\n          `);\n      });\n\n      it(\"allows a button to override the <form action>\", async () => {\n        let router = createTestRouter(\n          [\n            {\n              path: \"/\",\n              children: [\n                {\n                  path: \"foo\",\n                  action: () => {\n                    throw new Error(\"No\");\n                  },\n                  children: [\n                    {\n                      path: \"bar\",\n                      action: () => \"Yes\",\n                      Component: () => {\n                        let actionData = useActionData() as string | undefined;\n                        return (\n                          <Form method=\"post\" action=\"/foo\">\n                            <p>{actionData || \"No\"}</p>\n                            <button type=\"submit\" formAction=\"/foo/bar\">\n                              Submit\n                            </button>\n                          </Form>\n                        );\n                      },\n                    },\n                  ],\n                },\n              ],\n            },\n          ],\n          {\n            window: getWindow(\"/foo/bar\"),\n          },\n        );\n        let { container } = render(<RouterProvider router={router} />);\n\n        expect(container.querySelector(\"form\")?.getAttribute(\"action\")).toBe(\n          \"/foo\",\n        );\n        expect(\n          container.querySelector(\"button\")?.getAttribute(\"formaction\"),\n        ).toBe(\"/foo/bar\");\n\n        fireEvent.click(screen.getByText(\"Submit\"));\n        await waitFor(() => screen.getByText(\"Yes\"));\n        expect(getHtml(container)).toMatchInlineSnapshot(`\n          \"<div>\n            <form\n              action=\"/foo\"\n              data-discover=\"true\"\n              method=\"post\"\n            >\n              <p>\n                Yes\n              </p>\n              <button\n                formaction=\"/foo/bar\"\n                type=\"submit\"\n              >\n                Submit\n              </button>\n            </form>\n          </div>\"\n        `);\n      });\n\n      it(\"supports uppercase form method attributes\", async () => {\n        let loaderDefer = createDeferred();\n        let actionDefer = createDeferred();\n\n        let router = createTestRouter(\n          [\n            {\n              id: \"index\",\n              path: \"/\",\n              action: async ({ request }) => {\n                let resolvedValue = await actionDefer.promise;\n                let formData = await request.formData();\n                return `${resolvedValue}:${formData.get(\"test\")}`;\n              },\n              loader: () => loaderDefer.promise,\n              Component: Home,\n            },\n          ],\n          {\n            hydrationData: { loaderData: { index: \"Initial Data\" } },\n            window: getWindow(\"/\"),\n          },\n        );\n        let { container } = render(<RouterProvider router={router} />);\n\n        function Home() {\n          let data = useLoaderData() as string;\n          let actionData = useActionData() as string | undefined;\n          let navigation = useNavigation();\n          return (\n            <div>\n              <Form method=\"post\">\n                <input name=\"test\" value=\"value\" />\n                <button type=\"submit\">Submit Form</button>\n              </Form>\n              <div id=\"output\">\n                <p>{navigation.state}</p>\n                <p>{data}</p>\n                <p>{actionData}</p>\n              </div>\n              <Outlet />\n            </div>\n          );\n        }\n\n        fireEvent.click(screen.getByText(\"Submit Form\"));\n        await waitFor(() => screen.getByText(\"submitting\"));\n        actionDefer.resolve(\"Action Data\");\n        await waitFor(() => screen.getByText(\"loading\"));\n        loaderDefer.resolve(\"Loader Data\");\n        await waitFor(() => screen.getByText(\"idle\"));\n        expect(getHtml(container.querySelector(\"#output\")!))\n          .toMatchInlineSnapshot(`\n            \"<div\n              id=\"output\"\n            >\n              <p>\n                idle\n              </p>\n              <p>\n                Loader Data\n              </p>\n              <p>\n                Action Data:value\n              </p>\n            </div>\"\n          `);\n      });\n    });\n\n    describe(\"POP navigations\", () => {\n      it(\"exposes promise from useNavigate (popstate)\", async () => {\n        let sequence: string[] = [];\n        let router = createTestRouter(\n          [\n            {\n              id: \"home\",\n              path: \"/\",\n              async loader() {\n                sequence.push(\"loader start\");\n                await new Promise((r) => setTimeout(r, 100));\n                sequence.push(\"loader end\");\n                return null;\n              },\n              Component() {\n                sequence.push(\"render\");\n                return (\n                  <>\n                    <h1>Home</h1>\n                    <Link to=\"/page\">Go to page</Link>\n                  </>\n                );\n              },\n            },\n            {\n              path: \"/page\",\n              Component: () => {\n                let navigate = useNavigate();\n                return (\n                  <>\n                    <h1>Page</h1>\n                    <button\n                      onClick={async () => {\n                        sequence.push(\"call navigate\");\n                        await navigate(-1);\n                        sequence.push(\"navigate resolved\");\n                      }}\n                    >\n                      Back\n                    </button>\n                  </>\n                );\n              },\n            },\n          ],\n          {\n            hydrationData: { loaderData: { home: null } },\n            window: getWindow(\"/\"),\n          },\n        );\n\n        let { container } = render(<RouterProvider router={router} />);\n\n        expect(getHtml(container)).toContain(\"Home\");\n        fireEvent.click(screen.getByText(\"Go to page\"));\n        await waitFor(() => screen.getByText(\"Page\"));\n        sequence.splice(0); // clear sequence\n\n        fireEvent.click(screen.getByText(\"Back\"));\n        await waitFor(() => screen.getByText(\"Home\"));\n\n        expect(sequence).toEqual([\n          \"call navigate\",\n          \"loader start\",\n          \"loader end\",\n          \"navigate resolved\",\n          \"render\",\n        ]);\n      });\n    });\n\n    describe(\"history action\", () => {\n      it('defaults <Form method=\"get\"> to be a PUSH navigation', async () => {\n        let router = createTestRouter(\n          [\n            {\n              Component: Layout,\n              children: [\n                { index: true, Component: () => <h1>index</h1> },\n                { path: \"1\", Component: () => <h1>Page 1</h1> },\n                { path: \"2\", Component: () => <h1>Page 2</h1> },\n              ],\n            },\n          ],\n          {},\n        );\n        let { container } = render(<RouterProvider router={router} />);\n\n        function Layout() {\n          let navigate = useNavigate();\n          return (\n            <>\n              <Form action=\"1\">\n                <input name=\"test\" defaultValue=\"value\" />\n                <button type=\"submit\">Submit Form</button>\n              </Form>\n              <button onClick={() => navigate(-1)}>Go back</button>\n              <div className=\"output\">\n                <Outlet />\n              </div>\n            </>\n          );\n        }\n\n        expect(getHtml(container.querySelector(\".output\")!))\n          .toMatchInlineSnapshot(`\n            \"<div\n              class=\"output\"\n            >\n              <h1>\n                index\n              </h1>\n            </div>\"\n          `);\n\n        fireEvent.click(screen.getByText(\"Submit Form\"));\n        await waitFor(() => screen.getByText(\"Page 1\"));\n        expect(getHtml(container.querySelector(\".output\")!))\n          .toMatchInlineSnapshot(`\n            \"<div\n              class=\"output\"\n            >\n              <h1>\n                Page 1\n              </h1>\n            </div>\"\n          `);\n\n        fireEvent.click(screen.getByText(\"Go back\"));\n        await waitFor(() => screen.getByText(\"index\"));\n        expect(getHtml(container.querySelector(\".output\")!))\n          .toMatchInlineSnapshot(`\n            \"<div\n              class=\"output\"\n            >\n              <h1>\n                index\n              </h1>\n            </div>\"\n          `);\n      });\n\n      it('defaults <Form method=\"post\"> to be a REPLACE navigation', async () => {\n        let router = createTestRouter(\n          [\n            {\n              Component: Layout,\n              children: [\n                { index: true, Component: () => <h1>Index Page</h1> },\n                {\n                  path: \"form\",\n                  action: () => \"action data\",\n                  Component: FormPage,\n                },\n                { path: \"result\", Component: () => <h1>Result Page</h1> },\n              ],\n            },\n          ],\n          {},\n        );\n        let { container } = render(<RouterProvider router={router} />);\n\n        function Layout() {\n          let navigate = useNavigate();\n          return (\n            <>\n              <Link to=\"form\">Go to Form</Link>\n              <button onClick={() => navigate(-1)}>Go back</button>\n              <div className=\"output\">\n                <Outlet />\n              </div>\n            </>\n          );\n        }\n\n        function FormPage() {\n          let data = useActionData() as string | undefined;\n          return (\n            <Form method=\"post\">\n              <p>Form Page</p>\n              <p>{data}</p>\n              <input name=\"test\" defaultValue=\"value\" />\n              <button type=\"submit\">Submit</button>\n            </Form>\n          );\n        }\n\n        let html = () => getHtml(container.querySelector(\".output\")!);\n\n        // Start on index page\n        expect(html()).toMatch(\"Index Page\");\n\n        // Navigate to form page\n        fireEvent.click(screen.getByText(\"Go to Form\"));\n        await waitFor(() => screen.getByText(\"Form Page\"));\n        expect(html()).not.toMatch(\"action result\");\n\n        // Submit without redirect does a replace\n        fireEvent.click(screen.getByText(\"Submit\"));\n        await waitFor(() => screen.getByText(\"action data\"));\n        expect(html()).toMatch(\"Form Page\");\n        expect(html()).toMatch(\"action data\");\n\n        // Back navigate to index page\n        fireEvent.click(screen.getByText(\"Go back\"));\n        await waitFor(() => screen.getByText(\"Index Page\"));\n      });\n\n      it('Uses a PUSH navigation on <Form method=\"post\"> if it redirects', async () => {\n        let router = createTestRouter(\n          [\n            {\n              Component: Layout,\n              children: [\n                { index: true, Component: () => <h1>Index Page</h1> },\n                {\n                  path: \"form\",\n                  action: () =>\n                    new Response(null, {\n                      status: 302,\n                      headers: { Location: \"/result\" },\n                    }),\n                  Component: FormPage,\n                },\n                { path: \"result\", Component: () => <h1>Result Page</h1> },\n              ],\n            },\n          ],\n          { hydrationData: {} },\n        );\n        let { container } = render(<RouterProvider router={router} />);\n\n        function Layout() {\n          let navigate = useNavigate();\n          return (\n            <>\n              <Link to=\"form\">Go to Form</Link>\n              <button onClick={() => navigate(-1)}>Go back</button>\n              <div className=\"output\">\n                <Outlet />\n              </div>\n            </>\n          );\n        }\n\n        function FormPage() {\n          let data = useActionData() as string | undefined;\n          return (\n            <Form method=\"post\">\n              <p>Form Page</p>\n              <p>{data}</p>\n              <input name=\"test\" defaultValue=\"value\" />\n              <button type=\"submit\">Submit</button>\n            </Form>\n          );\n        }\n\n        let html = () => getHtml(container.querySelector(\".output\")!);\n\n        // Start on index page\n        expect(html()).toMatch(\"Index Page\");\n\n        // Navigate to form page\n        fireEvent.click(screen.getByText(\"Go to Form\"));\n        await waitFor(() => screen.getByText(\"Form Page\"));\n\n        // Submit with redirect\n        fireEvent.click(screen.getByText(\"Submit\"));\n        await waitFor(() => screen.getByText(\"Result Page\"));\n\n        // Back navigate to form page\n        fireEvent.click(screen.getByText(\"Go back\"));\n        await waitFor(() => screen.getByText(\"Form Page\"));\n      });\n\n      it('defaults useSubmit({ method: \"get\" }) to be a PUSH navigation', async () => {\n        let router = createTestRouter(\n          [\n            {\n              Component: Layout,\n              children: [\n                { index: true, Component: () => <h1>index</h1> },\n                {\n                  path: \"1\",\n                  loader: () => \"1\",\n                  Component: () => <h1>Page 1</h1>,\n                },\n                {\n                  path: \"2\",\n                  loader: () => \"2\",\n                  Component: () => <h1>Page 2</h1>,\n                },\n              ],\n            },\n          ],\n          {\n            window: getWindow(\"/\"),\n          },\n        );\n        let { container } = render(<RouterProvider router={router} />);\n\n        function Layout() {\n          let navigate = useNavigate();\n          let submit = useSubmit();\n          let formData = new FormData();\n          formData.append(\"test\", \"value\");\n          return (\n            <>\n              <button\n                onClick={() => submit(formData, { action: \"1\", method: \"get\" })}\n              >\n                Submit\n              </button>\n              <button onClick={() => navigate(-1)}>Go back</button>\n              <div className=\"output\">\n                <Outlet />\n              </div>\n            </>\n          );\n        }\n\n        expect(getHtml(container.querySelector(\".output\")!))\n          .toMatchInlineSnapshot(`\n            \"<div\n              class=\"output\"\n            >\n              <h1>\n                index\n              </h1>\n            </div>\"\n          `);\n\n        fireEvent.click(screen.getByText(\"Submit\"));\n        await waitFor(() => screen.getByText(\"Page 1\"));\n        expect(getHtml(container.querySelector(\".output\")!))\n          .toMatchInlineSnapshot(`\n            \"<div\n              class=\"output\"\n            >\n              <h1>\n                Page 1\n              </h1>\n            </div>\"\n          `);\n\n        fireEvent.click(screen.getByText(\"Go back\"));\n        await waitFor(() => screen.getByText(\"index\"));\n        expect(getHtml(container.querySelector(\".output\")!))\n          .toMatchInlineSnapshot(`\n            \"<div\n              class=\"output\"\n            >\n              <h1>\n                index\n              </h1>\n            </div>\"\n          `);\n      });\n\n      it('defaults useSubmit({ method: \"post\" }) to a new location to be a PUSH navigation', async () => {\n        let router = createTestRouter(\n          [\n            {\n              Component: Layout,\n              children: [\n                { index: true, Component: () => <h1>index</h1> },\n                { path: \"1\", Component: () => <h1>Page 1</h1> },\n                {\n                  path: \"2\",\n                  action: () => \"action\",\n                  Component: () => <h1>Page 2</h1>,\n                },\n              ],\n            },\n          ],\n          {\n            window: getWindow(\"/\"),\n          },\n        );\n        let { container } = render(<RouterProvider router={router} />);\n\n        function Layout() {\n          let navigate = useNavigate();\n          let submit = useSubmit();\n          let formData = new FormData();\n          formData.append(\"test\", \"value\");\n          return (\n            <>\n              <Link to=\"1\">Go to 1</Link>\n              <button\n                onClick={() => {\n                  submit(formData, { action: \"2\", method: \"post\" });\n                }}\n              >\n                Submit\n              </button>\n              <button onClick={() => navigate(-1)}>Go back</button>\n              <div className=\"output\">\n                <Outlet />\n              </div>\n            </>\n          );\n        }\n\n        expect(getHtml(container.querySelector(\".output\")!))\n          .toMatchInlineSnapshot(`\n            \"<div\n              class=\"output\"\n            >\n              <h1>\n                index\n              </h1>\n            </div>\"\n          `);\n\n        fireEvent.click(screen.getByText(\"Go to 1\"));\n        await waitFor(() => screen.getByText(\"Page 1\"));\n        expect(getHtml(container.querySelector(\".output\")!))\n          .toMatchInlineSnapshot(`\n            \"<div\n              class=\"output\"\n            >\n              <h1>\n                Page 1\n              </h1>\n            </div>\"\n          `);\n\n        fireEvent.click(screen.getByText(\"Submit\"));\n        await waitFor(() => screen.getByText(\"Page 2\"));\n        expect(getHtml(container.querySelector(\".output\")!))\n          .toMatchInlineSnapshot(`\n            \"<div\n              class=\"output\"\n            >\n              <h1>\n                Page 2\n              </h1>\n            </div>\"\n          `);\n\n        fireEvent.click(screen.getByText(\"Go back\"));\n        await waitFor(() => screen.getByText(\"Page 1\"));\n        expect(getHtml(container.querySelector(\".output\")!))\n          .toMatchInlineSnapshot(`\n            \"<div\n              class=\"output\"\n            >\n              <h1>\n                Page 1\n              </h1>\n            </div>\"\n          `);\n      });\n\n      it('defaults useSubmit({ method: \"post\" }) to the same location to be a REPLACE navigation', async () => {\n        let router = createTestRouter(\n          [\n            {\n              Component: Layout,\n              children: [\n                { index: true, Component: () => <h1>index</h1> },\n                {\n                  path: \"1\",\n                  action: () => \"action\",\n                  loader: () => \"1\",\n                  Component: Page,\n                },\n              ],\n            },\n          ],\n          {\n            window: getWindow(\"/\"),\n          },\n        );\n        let { container } = render(<RouterProvider router={router} />);\n\n        function Layout() {\n          let navigate = useNavigate();\n          let submit = useSubmit();\n          let formData = new FormData();\n          formData.append(\"test\", \"value\");\n          return (\n            <>\n              <Link to=\"1\">Go to 1</Link>\n              <button\n                onClick={() => {\n                  submit(formData, { action: \"1\", method: \"post\" });\n                }}\n              >\n                Submit\n              </button>\n              <button onClick={() => navigate(-1)}>Go back</button>\n              <div className=\"output\">\n                <Outlet />\n              </div>\n            </>\n          );\n        }\n\n        function Page() {\n          let actionData = useActionData() as string | undefined;\n          return (\n            <>\n              <h1>Page 1</h1>\n              <p>{actionData}</p>\n            </>\n          );\n        }\n\n        expect(getHtml(container.querySelector(\".output\")!))\n          .toMatchInlineSnapshot(`\n            \"<div\n              class=\"output\"\n            >\n              <h1>\n                index\n              </h1>\n            </div>\"\n          `);\n\n        fireEvent.click(screen.getByText(\"Go to 1\"));\n        await waitFor(() => screen.getByText(\"Page 1\"));\n        expect(getHtml(container.querySelector(\".output\")!))\n          .toMatchInlineSnapshot(`\n            \"<div\n              class=\"output\"\n            >\n              <h1>\n                Page 1\n              </h1>\n              <p />\n            </div>\"\n          `);\n\n        fireEvent.click(screen.getByText(\"Submit\"));\n        await waitFor(() => screen.getByText(\"action\"));\n        expect(getHtml(container.querySelector(\".output\")!))\n          .toMatchInlineSnapshot(`\n            \"<div\n              class=\"output\"\n            >\n              <h1>\n                Page 1\n              </h1>\n              <p>\n                action\n              </p>\n            </div>\"\n          `);\n\n        fireEvent.click(screen.getByText(\"Go back\"));\n        await waitFor(() => screen.getByText(\"index\"));\n        expect(getHtml(container.querySelector(\".output\")!))\n          .toMatchInlineSnapshot(`\n            \"<div\n              class=\"output\"\n            >\n              <h1>\n                index\n              </h1>\n            </div>\"\n          `);\n      });\n    });\n\n    describe(\"preventScrollReset\", () => {\n      it(\"handles link navigations with preventScrollReset\", async () => {\n        let router = createTestRouter(\n          [\n            {\n              path: \"/\",\n              Component: Layout,\n              children: [\n                { path: \"foo\", Component: () => <h1>Foo Heading</h1> },\n                { path: \"bar\", Component: () => <h1>Bar Heading</h1> },\n              ],\n            },\n          ],\n          { window: getWindow(\"/foo\") },\n        );\n        let { container } = render(<RouterProvider router={router} />);\n\n        function Layout() {\n          let state = React.useContext(DataRouterStateContext);\n          return (\n            <div>\n              <Link to=\"/foo\" preventScrollReset>\n                Link to Foo\n              </Link>\n              <Link to=\"/bar\">Link to Bar</Link>\n              <p id=\"preventScrollReset\">{String(state?.preventScrollReset)}</p>\n              <Outlet />\n            </div>\n          );\n        }\n\n        fireEvent.click(screen.getByText(\"Link to Bar\"));\n        await waitFor(() => screen.getByText(\"Bar Heading\"));\n        expect(getHtml(container.querySelector(\"#preventScrollReset\")!))\n          .toMatchInlineSnapshot(`\n            \"<p\n              id=\"preventScrollReset\"\n            >\n              false\n            </p>\"\n          `);\n\n        fireEvent.click(screen.getByText(\"Link to Foo\"));\n        await waitFor(() => screen.getByText(\"Foo Heading\"));\n        expect(getHtml(container.querySelector(\"#preventScrollReset\")!))\n          .toMatchInlineSnapshot(`\n            \"<p\n              id=\"preventScrollReset\"\n            >\n              true\n            </p>\"\n          `);\n      });\n\n      it(\"handles link navigations with preventScrollReset={true}\", async () => {\n        let router = createTestRouter(\n          [\n            {\n              path: \"/\",\n              Component: Layout,\n              children: [\n                { path: \"foo\", Component: () => <h1>Foo Heading</h1> },\n                { path: \"bar\", Component: () => <h1>Bar Heading</h1> },\n              ],\n            },\n          ],\n          { window: getWindow(\"/foo\") },\n        );\n        let { container } = render(<RouterProvider router={router} />);\n\n        function Layout() {\n          let state = React.useContext(DataRouterStateContext);\n          return (\n            <div>\n              <Link to=\"/foo\" preventScrollReset={true}>\n                Link to Foo\n              </Link>\n              <Link to=\"/bar\">Link to Bar</Link>\n              <p id=\"preventScrollReset\">{String(state?.preventScrollReset)}</p>\n              <Outlet />\n            </div>\n          );\n        }\n\n        fireEvent.click(screen.getByText(\"Link to Bar\"));\n        await waitFor(() => screen.getByText(\"Bar Heading\"));\n        expect(getHtml(container.querySelector(\"#preventScrollReset\")!))\n          .toMatchInlineSnapshot(`\n            \"<p\n              id=\"preventScrollReset\"\n            >\n              false\n            </p>\"\n          `);\n\n        fireEvent.click(screen.getByText(\"Link to Foo\"));\n        await waitFor(() => screen.getByText(\"Foo Heading\"));\n        expect(getHtml(container.querySelector(\"#preventScrollReset\")!))\n          .toMatchInlineSnapshot(`\n            \"<p\n              id=\"preventScrollReset\"\n            >\n              true\n            </p>\"\n          `);\n      });\n    });\n\n    describe(\"basename\", () => {\n      it(\"supports a basename prop\", () => {\n        let router = createTestRouter(\n          [{ path: \"thing\", Component: () => <h1>Heyooo</h1> }],\n          {\n            basename: \"/my/base/path\",\n            window: getWindow(\"/my/base/path/thing\"),\n          },\n        );\n        let { container } = render(<RouterProvider router={router} />);\n\n        expect(getHtml(container)).toMatchInlineSnapshot(`\n          \"<div>\n            <h1>\n              Heyooo\n            </h1>\n          </div>\"\n        `);\n      });\n      it(\"handles link navigations when using a basename\", async () => {\n        let testWindow = getWindow(\"/base/name/foo\");\n        let router = createTestRouter(\n          [\n            {\n              path: \"/\",\n              Component: Layout,\n              children: [\n                { path: \"foo\", Component: () => <h1>Foo Heading</h1> },\n                { path: \"bar\", Component: () => <h1>Bar Heading</h1> },\n              ],\n            },\n          ],\n          {\n            window: testWindow,\n            basename: \"/base/name\",\n          },\n        );\n        render(<RouterProvider router={router} />);\n\n        function Layout() {\n          return (\n            <div>\n              <Link to=\"/foo\">Link to Foo</Link>\n              <Link to=\"/bar\">Link to Bar</Link>\n              <div id=\"output\">\n                <Outlet />\n              </div>\n            </div>\n          );\n        }\n\n        assertLocation(testWindow, \"/base/name/foo\");\n        expect(screen.getByText(\"Foo Heading\")).toBeDefined();\n\n        fireEvent.click(screen.getByText(\"Link to Bar\"));\n        await waitFor(() => screen.getByText(\"Bar Heading\"));\n        assertLocation(testWindow, \"/base/name/bar\");\n\n        fireEvent.click(screen.getByText(\"Link to Foo\"));\n        await waitFor(() => screen.getByText(\"Foo Heading\"));\n        assertLocation(testWindow, \"/base/name/foo\");\n      });\n\n      it('supports a basename on <Form method=\"get\">', async () => {\n        let testWindow = getWindow(\"/base/path\");\n        let router = createTestRouter([{ path: \"path\", Component: Comp }], {\n          basename: \"/base\",\n          window: testWindow,\n        });\n        let { container } = render(<RouterProvider router={router} />);\n\n        function Comp() {\n          let location = useLocation();\n          return (\n            <Form>\n              <p>{location.pathname + location.search}</p>\n              <input name=\"a\" defaultValue=\"1\" />\n              <button type=\"submit\" name=\"b\" value=\"2\">\n                Submit\n              </button>\n            </Form>\n          );\n        }\n\n        assertLocation(testWindow, \"/base/path\");\n        expect(getHtml(container)).toMatchInlineSnapshot(`\n          \"<div>\n            <form\n              action=\"/base/path\"\n              data-discover=\"true\"\n              method=\"get\"\n            >\n              <p>\n                /path\n              </p>\n              <input\n                name=\"a\"\n                value=\"1\"\n              />\n              <button\n                name=\"b\"\n                type=\"submit\"\n                value=\"2\"\n              >\n                Submit\n              </button>\n            </form>\n          </div>\"\n        `);\n\n        fireEvent.click(screen.getByText(\"Submit\"));\n        assertLocation(testWindow, \"/base/path\", \"?a=1&b=2\");\n        expect(getHtml(container)).toMatchInlineSnapshot(`\n          \"<div>\n            <form\n              action=\"/base/path?a=1&b=2\"\n              data-discover=\"true\"\n              method=\"get\"\n            >\n              <p>\n                /path?a=1&b=2\n              </p>\n              <input\n                name=\"a\"\n                value=\"1\"\n              />\n              <button\n                name=\"b\"\n                type=\"submit\"\n                value=\"2\"\n              >\n                Submit\n              </button>\n            </form>\n          </div>\"\n        `);\n      });\n\n      it('supports a basename on <Form method=\"post\">', async () => {\n        let testWindow = getWindow(\"/base/path\");\n        let router = createTestRouter(\n          [\n            {\n              path: \"path\",\n              action: () => \"action data\",\n              Component: Comp,\n            },\n          ],\n          {\n            basename: \"/base\",\n\n            window: testWindow,\n          },\n        );\n        let { container } = render(<RouterProvider router={router} />);\n\n        function Comp() {\n          let location = useLocation();\n          let data = useActionData() as string | undefined;\n          return (\n            <Form method=\"post\">\n              <p>{location.pathname + location.search}</p>\n              {data && <p>{data}</p>}\n              <input name=\"a\" defaultValue=\"1\" />\n              <button type=\"submit\" name=\"b\" value=\"2\">\n                Submit\n              </button>\n            </Form>\n          );\n        }\n\n        assertLocation(testWindow, \"/base/path\");\n        expect(getHtml(container)).toMatchInlineSnapshot(`\n          \"<div>\n            <form\n              action=\"/base/path\"\n              data-discover=\"true\"\n              method=\"post\"\n            >\n              <p>\n                /path\n              </p>\n              <input\n                name=\"a\"\n                value=\"1\"\n              />\n              <button\n                name=\"b\"\n                type=\"submit\"\n                value=\"2\"\n              >\n                Submit\n              </button>\n            </form>\n          </div>\"\n        `);\n\n        fireEvent.click(screen.getByText(\"Submit\"));\n        await waitFor(() => screen.getByText(\"action data\"));\n        assertLocation(testWindow, \"/base/path\");\n        expect(getHtml(container)).toMatchInlineSnapshot(`\n          \"<div>\n            <form\n              action=\"/base/path\"\n              data-discover=\"true\"\n              method=\"post\"\n            >\n              <p>\n                /path\n              </p>\n              <p>\n                action data\n              </p>\n              <input\n                name=\"a\"\n                value=\"1\"\n              />\n              <button\n                name=\"b\"\n                type=\"submit\"\n                value=\"2\"\n              >\n                Submit\n              </button>\n            </form>\n          </div>\"\n        `);\n      });\n    });\n\n    describe(\"call-site revalidation opt-out\", () => {\n      it(\"accepts unstable_defaultShouldRevalidate on <Link> navigations\", async () => {\n        let loaderDefer = createDeferred();\n\n        let router = createTestRouter(\n          [{ path: \"/\", loader: () => loaderDefer.promise, Component: Home }],\n          {\n            window: getWindow(\"/\"),\n            hydrationData: { loaderData: { \"0\": null } },\n          },\n        );\n        let { container } = render(<RouterProvider router={router} />);\n\n        function Home() {\n          let data = useLoaderData() as string;\n          let location = useLocation();\n          let navigation = useNavigation();\n          return (\n            <div>\n              <Link to=\"/?foo=bar\" unstable_defaultShouldRevalidate={false}>\n                Change Search Params\n              </Link>\n              <div id=\"output\">\n                <p>{location.pathname + location.search}</p>\n                <p>{navigation.state}</p>\n                <p>{data}</p>\n              </div>\n            </div>\n          );\n        }\n\n        expect(getHtml(container.querySelector(\"#output\")!))\n          .toMatchInlineSnapshot(`\n        \"<div\n          id=\"output\"\n        >\n          <p>\n            /\n          </p>\n          <p>\n            idle\n          </p>\n          <p />\n        </div>\"\n      `);\n\n        fireEvent.click(screen.getByText(\"Change Search Params\"));\n        await waitFor(() => screen.getByText(\"idle\"));\n        loaderDefer.resolve(\"SHOULD NOT SEE ME\");\n        expect(getHtml(container.querySelector(\"#output\")!))\n          .toMatchInlineSnapshot(`\n        \"<div\n          id=\"output\"\n        >\n          <p>\n            /?foo=bar\n          </p>\n          <p>\n            idle\n          </p>\n          <p />\n        </div>\"\n      `);\n      });\n\n      it(\"accepts unstable_defaultShouldRevalidate on setSearchParams navigations\", async () => {\n        let loaderDefer = createDeferred();\n\n        let router = createTestRouter(\n          [{ path: \"/\", loader: () => loaderDefer.promise, Component: Home }],\n          {\n            window: getWindow(\"/\"),\n            hydrationData: { loaderData: { \"0\": null } },\n          },\n        );\n        let { container } = render(<RouterProvider router={router} />);\n\n        function Home() {\n          let data = useLoaderData() as string;\n          let location = useLocation();\n          let navigation = useNavigation();\n          let [, setSearchParams] = useSearchParams();\n          return (\n            <div>\n              <button\n                onClick={() =>\n                  setSearchParams(new URLSearchParams([[\"foo\", \"bar\"]]), {\n                    unstable_defaultShouldRevalidate: false,\n                  })\n                }\n              >\n                Change Search Params\n              </button>\n              <div id=\"output\">\n                <p>{location.pathname + location.search}</p>\n                <p>{navigation.state}</p>\n                <p>{data}</p>\n              </div>\n            </div>\n          );\n        }\n\n        expect(getHtml(container.querySelector(\"#output\")!))\n          .toMatchInlineSnapshot(`\n        \"<div\n          id=\"output\"\n        >\n          <p>\n            /\n          </p>\n          <p>\n            idle\n          </p>\n          <p />\n        </div>\"\n      `);\n\n        fireEvent.click(screen.getByText(\"Change Search Params\"));\n        await waitFor(() => screen.getByText(\"idle\"));\n        loaderDefer.resolve(\"SHOULD NOT SEE ME\");\n        expect(getHtml(container.querySelector(\"#output\")!))\n          .toMatchInlineSnapshot(`\n        \"<div\n          id=\"output\"\n        >\n          <p>\n            /?foo=bar\n          </p>\n          <p>\n            idle\n          </p>\n          <p />\n        </div>\"\n      `);\n      });\n\n      it(\"accepts unstable_defaultShouldRevalidate on <Form method=post> navigations\", async () => {\n        let loaderDefer = createDeferred();\n        let actionDefer = createDeferred();\n\n        let router = createTestRouter(\n          [\n            {\n              path: \"/\",\n              loader: () => loaderDefer.promise,\n              action: () => actionDefer.promise,\n              Component: Home,\n            },\n          ],\n          {\n            window: getWindow(\"/\"),\n            hydrationData: { loaderData: { \"0\": null } },\n          },\n        );\n        let { container } = render(<RouterProvider router={router} />);\n\n        function Home() {\n          let data = useLoaderData() as string;\n          let actionData = useActionData() as string | undefined;\n          let navigation = useNavigation();\n          return (\n            <div>\n              <Form method=\"post\" unstable_defaultShouldRevalidate={false}>\n                <input name=\"test\" value=\"value\" />\n                <button type=\"submit\">Submit Form</button>\n              </Form>\n              <div id=\"output\">\n                <p>{navigation.state}</p>\n                <p>{data}</p>\n                <p>{actionData}</p>\n              </div>\n            </div>\n          );\n        }\n\n        expect(getHtml(container.querySelector(\"#output\")!))\n          .toMatchInlineSnapshot(`\n        \"<div\n          id=\"output\"\n        >\n          <p>\n            idle\n          </p>\n          <p />\n          <p />\n        </div>\"\n      `);\n\n        fireEvent.click(screen.getByText(\"Submit Form\"));\n        await waitFor(() => screen.getByText(\"submitting\"));\n        actionDefer.resolve(\"Action Data\");\n        await waitFor(() => screen.getByText(\"idle\"));\n        loaderDefer.resolve(\"SHOULD NOT SEE ME\");\n        expect(getHtml(container.querySelector(\"#output\")!))\n          .toMatchInlineSnapshot(`\n        \"<div\n          id=\"output\"\n        >\n          <p>\n            idle\n          </p>\n          <p />\n          <p>\n            Action Data\n          </p>\n        </div>\"\n      `);\n      });\n\n      it(\"accepts unstable_defaultShouldRevalidate on fetcher.submit\", async () => {\n        let loaderDefer = createDeferred();\n        let actionDefer = createDeferred();\n\n        let router = createTestRouter(\n          [\n            {\n              path: \"/\",\n              loader: () => loaderDefer.promise,\n              action: () => actionDefer.promise,\n              Component: Home,\n            },\n          ],\n          {\n            window: getWindow(\"/\"),\n            hydrationData: { loaderData: { \"0\": null } },\n          },\n        );\n        let { container } = render(<RouterProvider router={router} />);\n\n        function Home() {\n          let data = useLoaderData() as string;\n          let fetcher = useFetcher<string>();\n          return (\n            <div>\n              <button\n                onClick={() =>\n                  fetcher.submit(\n                    {},\n                    {\n                      method: \"post\",\n                      action: \"/\",\n                      unstable_defaultShouldRevalidate: false,\n                    },\n                  )\n                }\n              >\n                Submit Fetcher\n              </button>\n              <div id=\"output\">\n                <p>{`${fetcher.state}:${fetcher.data}`}</p>\n                <p>{data}</p>\n              </div>\n            </div>\n          );\n        }\n\n        expect(getHtml(container.querySelector(\"#output\")!))\n          .toMatchInlineSnapshot(`\n        \"<div\n          id=\"output\"\n        >\n          <p>\n            idle:undefined\n          </p>\n          <p />\n        </div>\"\n      `);\n\n        fireEvent.click(screen.getByText(\"Submit Fetcher\"));\n        await waitFor(() => screen.getByText(\"submitting:undefined\"));\n        actionDefer.resolve(\"Action Data\");\n        await waitFor(() => screen.getByText(\"idle:Action Data\"));\n        loaderDefer.resolve(\"SHOULD NOT SEE ME\");\n        expect(getHtml(container.querySelector(\"#output\")!))\n          .toMatchInlineSnapshot(`\n        \"<div\n          id=\"output\"\n        >\n          <p>\n            idle:Action Data\n          </p>\n          <p />\n        </div>\"\n      `);\n      });\n    });\n\n    describe(\"<Form action>\", () => {\n      function NoActionComponent() {\n        return (\n          <Form method=\"post\">\n            <input name=\"b\" value=\"2\" />\n            <button type=\"submit\">Submit Form</button>\n          </Form>\n        );\n      }\n\n      function ActionDotComponent() {\n        return (\n          <Form method=\"post\" action=\".\">\n            <input name=\"b\" value=\"2\" />\n            <button type=\"submit\">Submit Form</button>\n          </Form>\n        );\n      }\n\n      function ActionEmptyComponent() {\n        return (\n          <Form method=\"post\" action=\"\">\n            <input name=\"b\" value=\"2\" />\n            <button type=\"submit\">Submit Form</button>\n          </Form>\n        );\n      }\n\n      describe(\"static routes\", () => {\n        it(\"includes search params when no action is specified\", async () => {\n          let router = createTestRouter(\n            [\n              {\n                path: \"/\",\n                children: [\n                  {\n                    path: \"foo\",\n                    children: [{ path: \"bar\", Component: NoActionComponent }],\n                  },\n                ],\n              },\n            ],\n            {\n              window: getWindow(\"/foo/bar?a=1#hash\"),\n            },\n          );\n          let { container } = render(<RouterProvider router={router} />);\n\n          expect(container.querySelector(\"form\")?.getAttribute(\"action\")).toBe(\n            \"/foo/bar?a=1\",\n          );\n        });\n\n        it(\"does not include search params when action='.'\", async () => {\n          let router = createTestRouter(\n            [\n              {\n                path: \"/\",\n                children: [\n                  {\n                    path: \"foo\",\n                    children: [{ path: \"bar\", Component: ActionDotComponent }],\n                  },\n                ],\n              },\n            ],\n            { window: getWindow(\"/foo/bar?a=1#hash\") },\n          );\n          let { container } = render(<RouterProvider router={router} />);\n\n          expect(container.querySelector(\"form\")?.getAttribute(\"action\")).toBe(\n            \"/foo/bar\",\n          );\n        });\n\n        it(\"does not include search params when action=''\", async () => {\n          let router = createTestRouter(\n            [\n              {\n                path: \"/\",\n                children: [\n                  {\n                    path: \"foo\",\n                    children: [\n                      {\n                        path: \"bar\",\n                        Component: ActionEmptyComponent,\n                      },\n                    ],\n                  },\n                ],\n              },\n            ],\n            { window: getWindow(\"/foo/bar?a=1#hash\") },\n          );\n          let { container } = render(<RouterProvider router={router} />);\n\n          expect(container.querySelector(\"form\")?.getAttribute(\"action\")).toBe(\n            \"/foo/bar\",\n          );\n        });\n      });\n\n      describe(\"layout routes\", () => {\n        it(\"includes search params when no action is specified\", async () => {\n          let router = createTestRouter(\n            [\n              {\n                path: \"/\",\n                children: [\n                  {\n                    path: \"foo\",\n                    children: [\n                      {\n                        path: \"bar\",\n                        Component: NoActionComponent,\n                        children: [\n                          { index: true, Component: () => <h1>Index</h1> },\n                        ],\n                      },\n                    ],\n                  },\n                ],\n              },\n            ],\n            {\n              window: getWindow(\"/foo/bar?a=1#hash\"),\n            },\n          );\n          let { container } = render(<RouterProvider router={router} />);\n\n          expect(container.querySelector(\"form\")?.getAttribute(\"action\")).toBe(\n            \"/foo/bar?a=1\",\n          );\n        });\n\n        it(\"does not include search params when action='.'\", async () => {\n          let router = createTestRouter(\n            [\n              {\n                path: \"/\",\n                children: [\n                  {\n                    path: \"foo\",\n                    children: [\n                      {\n                        path: \"bar\",\n                        Component: ActionDotComponent,\n                        children: [\n                          { index: true, Component: () => <h1>Index</h1> },\n                        ],\n                      },\n                    ],\n                  },\n                ],\n              },\n            ],\n            {\n              window: getWindow(\"/foo/bar?a=1#hash\"),\n            },\n          );\n          let { container } = render(<RouterProvider router={router} />);\n\n          expect(container.querySelector(\"form\")?.getAttribute(\"action\")).toBe(\n            \"/foo/bar\",\n          );\n        });\n\n        it(\"does not include search params when action=''\", async () => {\n          let router = createTestRouter(\n            [\n              {\n                path: \"/\",\n                children: [\n                  {\n                    path: \"foo\",\n                    children: [\n                      {\n                        path: \"bar\",\n                        Component: ActionEmptyComponent,\n                        children: [\n                          { index: true, Component: () => <h1>Index</h1> },\n                        ],\n                      },\n                    ],\n                  },\n                ],\n              },\n            ],\n            {\n              window: getWindow(\"/foo/bar?a=1#hash\"),\n            },\n          );\n          let { container } = render(<RouterProvider router={router} />);\n\n          expect(container.querySelector(\"form\")?.getAttribute(\"action\")).toBe(\n            \"/foo/bar\",\n          );\n        });\n\n        it(\"does not include dynamic parameters from a parent layout route\", async () => {\n          let router = createTestRouter(\n            [\n              {\n                path: \"/\",\n                children: [\n                  {\n                    path: \"foo\",\n                    Component: ActionEmptyComponent,\n                    children: [\n                      { path: \":param\", Component: () => <h1>Param</h1> },\n                    ],\n                  },\n                ],\n              },\n            ],\n            {\n              window: getWindow(\"/foo/bar\"),\n            },\n          );\n          let { container } = render(<RouterProvider router={router} />);\n\n          expect(container.querySelector(\"form\")?.getAttribute(\"action\")).toBe(\n            \"/foo\",\n          );\n        });\n\n        it(\"does not include splat parameters from a parent layout route\", async () => {\n          let router = createTestRouter(\n            [\n              {\n                path: \"/\",\n                children: [\n                  {\n                    path: \"foo\",\n                    Component: ActionEmptyComponent,\n                    children: [{ path: \"*\", Component: () => <h1>Splat</h1> }],\n                  },\n                ],\n              },\n            ],\n            {\n              window: getWindow(\"/foo/bar/baz/qux\"),\n            },\n          );\n          let { container } = render(<RouterProvider router={router} />);\n\n          expect(container.querySelector(\"form\")?.getAttribute(\"action\")).toBe(\n            \"/foo\",\n          );\n        });\n\n        it(\"does not include the index parameter if we've submitted to a child index route\", async () => {\n          let router = createTestRouter(\n            [\n              {\n                path: \"/\",\n                children: [\n                  {\n                    path: \"foo\",\n                    children: [\n                      {\n                        path: \"bar\",\n                        Component: NoActionComponent,\n                        children: [\n                          { index: true, Component: () => <h1>Index</h1> },\n                        ],\n                      },\n                    ],\n                  },\n                ],\n              },\n            ],\n            {\n              window: getWindow(\"/foo/bar?index&a=1\"),\n            },\n          );\n          let { container } = render(<RouterProvider router={router} />);\n\n          expect(container.querySelector(\"form\")?.getAttribute(\"action\")).toBe(\n            \"/foo/bar?a=1\",\n          );\n        });\n      });\n\n      describe(\"index routes\", () => {\n        it(\"includes search params when no action is specified\", async () => {\n          let router = createTestRouter(\n            [\n              {\n                path: \"/\",\n                children: [\n                  {\n                    path: \"foo\",\n                    children: [\n                      {\n                        path: \"bar\",\n                        children: [\n                          {\n                            index: true,\n                            Component: NoActionComponent,\n                          },\n                        ],\n                      },\n                    ],\n                  },\n                ],\n              },\n            ],\n            {\n              window: getWindow(\"/foo/bar?a=1#hash\"),\n            },\n          );\n          let { container } = render(<RouterProvider router={router} />);\n\n          expect(container.querySelector(\"form\")?.getAttribute(\"action\")).toBe(\n            \"/foo/bar?index&a=1\",\n          );\n        });\n\n        it(\"does not include search params action='.'\", async () => {\n          let router = createTestRouter(\n            [\n              {\n                path: \"/\",\n                children: [\n                  {\n                    path: \"foo\",\n                    children: [\n                      {\n                        path: \"bar\",\n                        children: [\n                          {\n                            index: true,\n                            Component: ActionDotComponent,\n                          },\n                        ],\n                      },\n                    ],\n                  },\n                ],\n              },\n            ],\n            {\n              window: getWindow(\"/foo/bar?a=1#hash\"),\n            },\n          );\n          let { container } = render(<RouterProvider router={router} />);\n\n          expect(container.querySelector(\"form\")?.getAttribute(\"action\")).toBe(\n            \"/foo/bar?index\",\n          );\n        });\n\n        it(\"does not include search params action=''\", async () => {\n          let router = createTestRouter(\n            [\n              {\n                path: \"/\",\n                children: [\n                  {\n                    path: \"foo\",\n                    children: [\n                      {\n                        path: \"bar\",\n                        children: [\n                          {\n                            index: true,\n                            Component: ActionEmptyComponent,\n                          },\n                        ],\n                      },\n                    ],\n                  },\n                ],\n              },\n            ],\n            {\n              window: getWindow(\"/foo/bar?a=1#hash\"),\n            },\n          );\n          let { container } = render(<RouterProvider router={router} />);\n\n          expect(container.querySelector(\"form\")?.getAttribute(\"action\")).toBe(\n            \"/foo/bar?index\",\n          );\n        });\n\n        // eslint-disable-next-line jest/expect-expect\n        it(\"does not repeatedly add ?index params on submissions\", async () => {\n          let testWindow = getWindow(\"/form\");\n          let router = createTestRouter(\n            [\n              {\n                path: \"/\",\n                children: [\n                  {\n                    path: \"form\",\n                    children: [\n                      {\n                        index: true,\n                        action: () => ({}),\n                        Component() {\n                          return (\n                            <Form method=\"post\">\n                              <button type=\"submit\" name=\"name\" value=\"value\">\n                                Submit\n                              </button>\n                            </Form>\n                          );\n                        },\n                      },\n                    ],\n                  },\n                ],\n              },\n            ],\n            {\n              window: testWindow,\n            },\n          );\n          render(<RouterProvider router={router} />);\n\n          assertLocation(testWindow, \"/form\", \"\");\n\n          fireEvent.click(screen.getByText(\"Submit\"));\n          await new Promise((r) => setTimeout(r, 0));\n          assertLocation(testWindow, \"/form\", \"?index\");\n\n          fireEvent.click(screen.getByText(\"Submit\"));\n          await new Promise((r) => setTimeout(r, 0));\n          assertLocation(testWindow, \"/form\", \"?index\");\n        });\n\n        it(\"handles index routes with a path\", async () => {\n          let router = createTestRouter(\n            [\n              {\n                path: \"/\",\n                children: [\n                  {\n                    path: \"foo\",\n                    children: [\n                      {\n                        index: true,\n                        path: \"bar\",\n                        Component: NoActionComponent,\n                      },\n                    ],\n                  },\n                ],\n              },\n            ],\n            { window: getWindow(\"/foo/bar?a=1#hash\") },\n          );\n          let { container } = render(<RouterProvider router={router} />);\n\n          expect(container.querySelector(\"form\")?.getAttribute(\"action\")).toBe(\n            \"/foo/bar?index&a=1\",\n          );\n        });\n\n        // eslint-disable-next-line jest/expect-expect\n        it('does not put ?index param in final URL for <Form method=\"get\"', async () => {\n          let testWindow = getWindow(\"/form\");\n          let router = createTestRouter(\n            [\n              {\n                path: \"/\",\n                children: [\n                  {\n                    path: \"form\",\n                    children: [\n                      {\n                        index: true,\n                        Component() {\n                          return (\n                            <Form>\n                              <button type=\"submit\" name=\"name\" value=\"value\">\n                                Submit\n                              </button>\n                            </Form>\n                          );\n                        },\n                      },\n                    ],\n                  },\n                ],\n              },\n            ],\n            {\n              window: testWindow,\n            },\n          );\n          render(<RouterProvider router={router} />);\n\n          assertLocation(testWindow, \"/form\", \"\");\n\n          fireEvent.click(screen.getByText(\"Submit\"));\n          await new Promise((r) => setTimeout(r, 0));\n          assertLocation(testWindow, \"/form\", \"?name=value\");\n        });\n      });\n\n      describe(\"dynamic routes\", () => {\n        it(\"includes search params when no action is specified\", async () => {\n          let router = createTestRouter(\n            [\n              {\n                path: \"/\",\n                children: [\n                  {\n                    path: \"foo\",\n                    children: [\n                      {\n                        path: \":param\",\n                        Component: NoActionComponent,\n                      },\n                    ],\n                  },\n                ],\n              },\n            ],\n            {\n              window: getWindow(\"/foo/bar?a=1#hash\"),\n            },\n          );\n          let { container } = render(<RouterProvider router={router} />);\n\n          expect(container.querySelector(\"form\")?.getAttribute(\"action\")).toBe(\n            \"/foo/bar?a=1\",\n          );\n        });\n\n        it(\"does not include search params action='.'\", async () => {\n          let router = createTestRouter(\n            [\n              {\n                path: \"/\",\n                children: [\n                  {\n                    path: \"foo\",\n                    children: [\n                      {\n                        path: \":param\",\n                        Component: ActionDotComponent,\n                      },\n                    ],\n                  },\n                ],\n              },\n            ],\n            {\n              window: getWindow(\"/foo/bar?a=1#hash\"),\n            },\n          );\n          let { container } = render(<RouterProvider router={router} />);\n\n          expect(container.querySelector(\"form\")?.getAttribute(\"action\")).toBe(\n            \"/foo/bar\",\n          );\n        });\n\n        it(\"does not include search params when action=''\", async () => {\n          let router = createTestRouter(\n            [\n              {\n                path: \"/\",\n                children: [\n                  {\n                    path: \"foo\",\n                    children: [\n                      {\n                        path: \":param\",\n                        Component: ActionEmptyComponent,\n                      },\n                    ],\n                  },\n                ],\n              },\n            ],\n            {\n              window: getWindow(\"/foo/bar?a=1#hash\"),\n            },\n          );\n          let { container } = render(<RouterProvider router={router} />);\n\n          expect(container.querySelector(\"form\")?.getAttribute(\"action\")).toBe(\n            \"/foo/bar\",\n          );\n        });\n      });\n\n      describe(\"splat routes\", () => {\n        it(\"includes search params when no action is specified\", async () => {\n          let router = createTestRouter(\n            [\n              {\n                path: \"/\",\n                children: [\n                  {\n                    path: \"foo\",\n                    children: [{ path: \"*\", Component: NoActionComponent }],\n                  },\n                ],\n              },\n            ],\n            {\n              window: getWindow(\"/foo/bar?a=1#hash\"),\n            },\n          );\n          let { container } = render(<RouterProvider router={router} />);\n\n          expect(container.querySelector(\"form\")?.getAttribute(\"action\")).toBe(\n            \"/foo/bar?a=1\",\n          );\n        });\n\n        it(\"does not include search params when action='.'\", async () => {\n          let router = createTestRouter(\n            [\n              {\n                path: \"/\",\n                children: [\n                  {\n                    path: \"foo\",\n                    children: [{ path: \"*\", Component: ActionDotComponent }],\n                  },\n                ],\n              },\n            ],\n            {\n              window: getWindow(\"/foo/bar?a=1#hash\"),\n            },\n          );\n          let { container } = render(<RouterProvider router={router} />);\n\n          expect(container.querySelector(\"form\")?.getAttribute(\"action\")).toBe(\n            \"/foo/bar\",\n          );\n        });\n\n        it(\"does not include search params when action=''\", async () => {\n          let router = createTestRouter(\n            [\n              {\n                path: \"/\",\n                children: [\n                  {\n                    path: \"foo\",\n                    children: [{ path: \"*\", Component: ActionEmptyComponent }],\n                  },\n                ],\n              },\n            ],\n            {\n              window: getWindow(\"/foo/bar?a=1#hash\"),\n            },\n          );\n          let { container } = render(<RouterProvider router={router} />);\n\n          expect(container.querySelector(\"form\")?.getAttribute(\"action\")).toBe(\n            \"/foo/bar\",\n          );\n        });\n      });\n\n      describe(\"submitting to self from parent/index when ?index param exists\", () => {\n        it(\"useSubmit\", async () => {\n          let router = createTestRouter(\n            [\n              {\n                id: \"parent\",\n                path: \"/parent\",\n                Component: Parent,\n                action: ({ request }) => \"PARENT ACTION: \" + request.url,\n                children: [\n                  {\n                    id: \"index\",\n                    index: true,\n                    Component: Index,\n                    action: ({ request }) => \"INDEX ACTION: \" + request.url,\n                  },\n                ],\n              },\n            ],\n            {\n              window: getWindow(\"/parent?index&index=keep\"),\n            },\n          );\n          let { container } = render(<RouterProvider router={router} />);\n\n          function Parent() {\n            let actionData = useActionData();\n            let submit = useSubmit();\n            return (\n              <>\n                <p id=\"parent\">{actionData}</p>\n                <button onClick={() => submit({}, { method: \"post\" })}>\n                  Submit from parent\n                </button>\n                <Outlet />\n              </>\n            );\n          }\n\n          function Index() {\n            let actionData = useActionData();\n            let submit = useSubmit();\n            return (\n              <>\n                <p id=\"index\">{actionData}</p>\n                <button onClick={() => submit({}, { method: \"post\" })}>\n                  Submit from index\n                </button>\n              </>\n            );\n          }\n\n          fireEvent.click(screen.getByText(\"Submit from parent\"));\n          await tick();\n          await waitFor(() => screen.getByText(new RegExp(\"PARENT ACTION\")));\n          expect(getHtml(container.querySelector(\"#parent\")!)).toContain(\n            \"PARENT ACTION: http://localhost/parent?index=keep\",\n          );\n\n          fireEvent.click(screen.getByText(\"Submit from index\"));\n          await tick();\n          await waitFor(() => screen.getByText(new RegExp(\"INDEX ACTION\")));\n          expect(getHtml(container.querySelector(\"#index\")!)).toContain(\n            \"INDEX ACTION: http://localhost/parent?index&index=keep\",\n          );\n        });\n\n        it(\"Form\", async () => {\n          let router = createTestRouter(\n            [\n              {\n                id: \"parent\",\n                path: \"/parent\",\n                Component: Parent,\n                action: ({ request }) => \"PARENT ACTION: \" + request.url,\n                children: [\n                  {\n                    id: \"index\",\n                    index: true,\n                    Component: Index,\n                    action: ({ request }) => \"INDEX ACTION: \" + request.url,\n                  },\n                ],\n              },\n            ],\n            {\n              window: getWindow(\"/parent?index&index=keep\"),\n            },\n          );\n          let { container } = render(<RouterProvider router={router} />);\n\n          function Parent() {\n            let actionData = useActionData();\n            return (\n              <>\n                <p id=\"parent\">{actionData}</p>\n                <Form method=\"post\" id=\"parent-form\">\n                  <button type=\"submit\">Submit from parent</button>\n                </Form>\n                <Outlet />\n              </>\n            );\n          }\n\n          function Index() {\n            let actionData = useActionData();\n            return (\n              <>\n                <p id=\"index\">{actionData}</p>\n                <Form method=\"post\" id=\"index-form\">\n                  <button type=\"submit\">Submit from index</button>\n                </Form>\n              </>\n            );\n          }\n\n          expect(\n            container.querySelector(\"#parent-form\")?.getAttribute(\"action\"),\n          ).toBe(\"/parent?index=keep\");\n          expect(\n            container.querySelector(\"#index-form\")?.getAttribute(\"action\"),\n          ).toBe(\"/parent?index&index=keep\");\n\n          fireEvent.click(screen.getByText(\"Submit from parent\"));\n          await tick();\n          await waitFor(() => screen.getByText(new RegExp(\"PARENT ACTION\")));\n          expect(getHtml(container.querySelector(\"#parent\")!)).toContain(\n            \"PARENT ACTION: http://localhost/parent?index=keep\",\n          );\n\n          fireEvent.click(screen.getByText(\"Submit from index\"));\n          await tick();\n          await waitFor(() => screen.getByText(new RegExp(\"INDEX ACTION\")));\n          expect(getHtml(container.querySelector(\"#index\")!)).toContain(\n            \"INDEX ACTION: http://localhost/parent?index&index=keep\",\n          );\n        });\n\n        it(\"fetcher.submit\", async () => {\n          let router = createTestRouter(\n            [\n              {\n                id: \"parent\",\n                path: \"/parent\",\n                Component: Parent,\n                action: ({ request }) => \"PARENT ACTION: \" + request.url,\n                children: [\n                  {\n                    id: \"index\",\n                    index: true,\n                    Component: Index,\n                    action: ({ request }) => \"INDEX ACTION: \" + request.url,\n                  },\n                ],\n              },\n            ],\n            {\n              window: getWindow(\"/parent?index&index=keep\"),\n            },\n          );\n          let { container } = render(<RouterProvider router={router} />);\n\n          function Parent() {\n            let fetcher = useFetcher();\n\n            return (\n              <>\n                <p id=\"parent\">{fetcher.data}</p>\n                <button onClick={() => fetcher.submit({}, { method: \"post\" })}>\n                  Submit from parent\n                </button>\n                <Outlet />\n              </>\n            );\n          }\n\n          function Index() {\n            let fetcher = useFetcher();\n\n            return (\n              <>\n                <p id=\"index\">{fetcher.data}</p>\n                <button onClick={() => fetcher.submit({}, { method: \"post\" })}>\n                  Submit from index\n                </button>\n              </>\n            );\n          }\n\n          fireEvent.click(screen.getByText(\"Submit from parent\"));\n          await tick();\n          await waitFor(() => screen.getByText(new RegExp(\"PARENT ACTION\")));\n          expect(getHtml(container.querySelector(\"#parent\")!)).toContain(\n            \"PARENT ACTION: http://localhost/parent?index=keep\",\n          );\n\n          fireEvent.click(screen.getByText(\"Submit from index\"));\n          await tick();\n          await waitFor(() => screen.getByText(new RegExp(\"INDEX ACTION\")));\n          expect(getHtml(container.querySelector(\"#index\")!)).toContain(\n            \"INDEX ACTION: http://localhost/parent?index&index=keep\",\n          );\n        });\n\n        it(\"fetcher.Form\", async () => {\n          let router = createTestRouter(\n            [\n              {\n                id: \"parent\",\n                path: \"/parent\",\n                Component: Parent,\n                action: ({ request }) => \"PARENT ACTION: \" + request.url,\n                children: [\n                  {\n                    id: \"index\",\n                    index: true,\n                    Component: Index,\n                    action: ({ request }) => \"INDEX ACTION: \" + request.url,\n                  },\n                ],\n              },\n            ],\n            {\n              window: getWindow(\"/parent?index&index=keep\"),\n            },\n          );\n          let { container } = render(<RouterProvider router={router} />);\n\n          function Parent() {\n            let fetcher = useFetcher();\n\n            return (\n              <>\n                <p id=\"parent\">{fetcher.data}</p>\n                <fetcher.Form method=\"post\" id=\"parent-form\">\n                  <button type=\"submit\">Submit from parent</button>\n                </fetcher.Form>\n                <Outlet />\n              </>\n            );\n          }\n\n          function Index() {\n            let fetcher = useFetcher();\n\n            return (\n              <>\n                <p id=\"index\">{fetcher.data}</p>\n                <fetcher.Form method=\"post\" id=\"index-form\">\n                  <button type=\"submit\">Submit from index</button>\n                </fetcher.Form>\n              </>\n            );\n          }\n\n          expect(\n            container.querySelector(\"#parent-form\")?.getAttribute(\"action\"),\n          ).toBe(\"/parent?index=keep\");\n          expect(\n            container.querySelector(\"#index-form\")?.getAttribute(\"action\"),\n          ).toBe(\"/parent?index&index=keep\");\n\n          fireEvent.click(screen.getByText(\"Submit from parent\"));\n          await tick();\n          await waitFor(() => screen.getByText(new RegExp(\"PARENT ACTION\")));\n          expect(getHtml(container.querySelector(\"#parent\")!)).toContain(\n            \"PARENT ACTION: http://localhost/parent?index=keep\",\n          );\n\n          fireEvent.click(screen.getByText(\"Submit from index\"));\n          await tick();\n          await waitFor(() => screen.getByText(new RegExp(\"INDEX ACTION\")));\n          expect(getHtml(container.querySelector(\"#index\")!)).toContain(\n            \"INDEX ACTION: http://localhost/parent?index&index=keep\",\n          );\n        });\n      });\n\n      it(\"allows user to specify search params and hash\", async () => {\n        let router = createTestRouter(\n          [\n            {\n              path: \"/\",\n              children: [\n                {\n                  path: \"foo\",\n                  children: [\n                    {\n                      path: \"bar\",\n                      Component: () => <Form action=\".?a=1#newhash\" />,\n                    },\n                  ],\n                },\n              ],\n            },\n          ],\n          { window: getWindow(\"/foo/bar?a=1#hash\") },\n        );\n        let { container } = render(<RouterProvider router={router} />);\n\n        expect(container.querySelector(\"form\")?.getAttribute(\"action\")).toBe(\n          \"/foo/bar?a=1#newhash\",\n        );\n      });\n    });\n\n    describe('<Form action relative=\"path\">', () => {\n      it(\"navigates relative to the URL for static routes\", async () => {\n        let router = createTestRouter(\n          [\n            {\n              path: \"inbox\",\n              children: [\n                { path: \"messages\" },\n                {\n                  path: \"messages/edit\",\n                  Component: () => <Form action=\"..\" relative=\"path\" />,\n                },\n              ],\n            },\n          ],\n          {\n            window: getWindow(\"/inbox/messages/edit\"),\n          },\n        );\n        let { container } = render(<RouterProvider router={router} />);\n\n        expect(container.querySelector(\"form\")?.getAttribute(\"action\")).toBe(\n          \"/inbox/messages\",\n        );\n      });\n\n      it(\"navigates relative to the URL for dynamic routes\", async () => {\n        let router = createTestRouter(\n          [\n            {\n              path: \"inbox\",\n              children: [\n                { path: \"messages\" },\n                {\n                  path: \"messages/:id\",\n                  Component: () => <Form action=\"..\" relative=\"path\" />,\n                },\n              ],\n            },\n          ],\n          {\n            window: getWindow(\"/inbox/messages/1\"),\n          },\n        );\n        let { container } = render(<RouterProvider router={router} />);\n\n        expect(container.querySelector(\"form\")?.getAttribute(\"action\")).toBe(\n          \"/inbox/messages\",\n        );\n      });\n\n      it(\"navigates relative to the URL for layout routes\", async () => {\n        let router = createTestRouter(\n          [\n            {\n              path: \"inbox\",\n              children: [\n                { path: \"messages\" },\n                {\n                  path: \"messages/:id\",\n                  Component() {\n                    return (\n                      <>\n                        <Form action=\"..\" relative=\"path\" />\n                        <Outlet />\n                      </>\n                    );\n                  },\n                  children: [{ index: true, Component: () => <h1>Form</h1> }],\n                },\n              ],\n            },\n          ],\n          {\n            window: getWindow(\"/inbox/messages/1\"),\n          },\n        );\n        let { container } = render(<RouterProvider router={router} />);\n\n        expect(container.querySelector(\"form\")?.getAttribute(\"action\")).toBe(\n          \"/inbox/messages\",\n        );\n      });\n\n      it(\"navigates relative to the URL for index routes\", async () => {\n        let router = createTestRouter(\n          [\n            {\n              path: \"inbox\",\n              children: [\n                { path: \"messages\" },\n                {\n                  path: \"messages/:id\",\n                  children: [\n                    {\n                      index: true,\n                      Component: () => <Form action=\"..\" relative=\"path\" />,\n                    },\n                  ],\n                },\n              ],\n            },\n          ],\n          {\n            window: getWindow(\"/inbox/messages/1\"),\n          },\n        );\n        let { container } = render(<RouterProvider router={router} />);\n\n        expect(container.querySelector(\"form\")?.getAttribute(\"action\")).toBe(\n          \"/inbox/messages\",\n        );\n      });\n\n      it(\"navigates relative to the URL for splat routes\", async () => {\n        let router = createTestRouter(\n          [\n            {\n              path: \"inbox/messages/*\",\n              Component: () => <Form action=\"..\" relative=\"path\" />,\n            },\n          ],\n          {\n            window: getWindow(\"/inbox/messages/1/2/3\"),\n          },\n        );\n        let { container } = render(<RouterProvider router={router} />);\n\n        expect(container.querySelector(\"form\")?.getAttribute(\"action\")).toBe(\n          \"/inbox/messages/1/2\",\n        );\n      });\n    });\n\n    describe(\"useSubmit/Form FormData\", () => {\n      it(\"gathers form data on <Form> submissions\", async () => {\n        let actionSpy = jest.fn();\n        let router = createTestRouter(\n          [{ path: \"/\", action: actionSpy, Component: FormPage }],\n          { window: getWindow(\"/\") },\n        );\n        render(<RouterProvider router={router} />);\n\n        function FormPage() {\n          return (\n            <Form method=\"post\">\n              <input name=\"a\" defaultValue=\"1\" />\n              <input name=\"b\" defaultValue=\"2\" />\n              <button type=\"submit\">Submit</button>\n            </Form>\n          );\n        }\n\n        fireEvent.click(screen.getByText(\"Submit\"));\n        let formData = await actionSpy.mock.calls[0][0].request.formData();\n        expect(formData.get(\"a\")).toBe(\"1\");\n        expect(formData.get(\"b\")).toBe(\"2\");\n      });\n\n      it(\"gathers form data on submit(form) submissions\", async () => {\n        let actionSpy = jest.fn();\n        let router = createTestRouter(\n          [{ path: \"/\", action: actionSpy, Component: FormPage }],\n          { window: getWindow(\"/\") },\n        );\n        render(<RouterProvider router={router} />);\n\n        function FormPage() {\n          let submit = useSubmit();\n          let formRef = React.useRef(null);\n          return (\n            <>\n              <Form method=\"post\" ref={formRef}>\n                <input name=\"a\" defaultValue=\"1\" />\n                <input name=\"b\" defaultValue=\"2\" />\n              </Form>\n              <button onClick={() => submit(formRef.current)}>Submit</button>\n            </>\n          );\n        }\n\n        fireEvent.click(screen.getByText(\"Submit\"));\n        let formData = await actionSpy.mock.calls[0][0].request.formData();\n        expect(formData.get(\"a\")).toBe(\"1\");\n        expect(formData.get(\"b\")).toBe(\"2\");\n      });\n\n      it(\"gathers form data on submit(button) submissions\", async () => {\n        let actionSpy = jest.fn();\n        let router = createTestRouter(\n          [{ path: \"/\", action: actionSpy, Component: FormPage }],\n          { window: getWindow(\"/\") },\n        );\n        render(<RouterProvider router={router} />);\n\n        function FormPage() {\n          let submit = useSubmit();\n          return (\n            <>\n              <Form method=\"post\">\n                <input name=\"a\" defaultValue=\"1\" />\n                <input name=\"b\" defaultValue=\"2\" />\n                <button\n                  onClick={(e) => {\n                    e.preventDefault();\n                    submit(e.currentTarget);\n                  }}\n                >\n                  Submit\n                </button>\n              </Form>\n            </>\n          );\n        }\n\n        fireEvent.click(screen.getByText(\"Submit\"));\n        let formData = await actionSpy.mock.calls[0][0].request.formData();\n        expect(formData.get(\"a\")).toBe(\"1\");\n        expect(formData.get(\"b\")).toBe(\"2\");\n      });\n\n      it(\"gathers form data on submit(input[type=submit]) submissions\", async () => {\n        let actionSpy = jest.fn();\n        let router = createTestRouter(\n          [{ path: \"/\", action: actionSpy, Component: FormPage }],\n          { window: getWindow(\"/\") },\n        );\n        render(<RouterProvider router={router} />);\n\n        function FormPage() {\n          let submit = useSubmit();\n          return (\n            <>\n              <Form method=\"post\">\n                <input name=\"a\" defaultValue=\"1\" />\n                <input name=\"b\" defaultValue=\"2\" />\n                <input\n                  type=\"submit\"\n                  value=\"Submit\"\n                  onClick={(e) => {\n                    e.preventDefault();\n                    submit(e.currentTarget);\n                  }}\n                />\n              </Form>\n            </>\n          );\n        }\n\n        fireEvent.click(screen.getByText(\"Submit\"));\n        let formData = await actionSpy.mock.calls[0][0].request.formData();\n        expect(formData.get(\"a\")).toBe(\"1\");\n        expect(formData.get(\"b\")).toBe(\"2\");\n      });\n\n      it(\"gathers form data on submit(FormData) submissions\", async () => {\n        let actionSpy = jest.fn();\n        let router = createTestRouter(\n          [{ path: \"/\", action: actionSpy, Component: FormPage }],\n          { window: getWindow(\"/\") },\n        );\n        render(<RouterProvider router={router} />);\n\n        function FormPage() {\n          let submit = useSubmit();\n          let formData = new FormData();\n          formData.set(\"a\", \"1\");\n          formData.set(\"b\", \"2\");\n          return (\n            <button onClick={() => submit(formData, { method: \"post\" })}>\n              Submit\n            </button>\n          );\n        }\n\n        fireEvent.click(screen.getByText(\"Submit\"));\n        let formData = await actionSpy.mock.calls[0][0].request.formData();\n        expect(formData.get(\"a\")).toBe(\"1\");\n        expect(formData.get(\"b\")).toBe(\"2\");\n      });\n\n      it(\"serializes formData on submit(object) submissions\", async () => {\n        let actionSpy = jest.fn();\n        let body = { a: \"1\", b: \"2\" };\n        let navigation: Navigation | undefined;\n        let router = createTestRouter(\n          [\n            {\n              path: \"/\",\n              action: actionSpy,\n              Component() {\n                let submit = useSubmit();\n                let n = useNavigation();\n                if (n.state === \"submitting\") {\n                  navigation = n;\n                }\n                return (\n                  <button onClick={() => submit(body, { method: \"post\" })}>\n                    Submit\n                  </button>\n                );\n              },\n            },\n          ],\n          { window: getWindow(\"/\") },\n        );\n        render(<RouterProvider router={router} />);\n\n        fireEvent.click(screen.getByText(\"Submit\"));\n        expect(navigation?.formData?.get(\"a\")).toBe(\"1\");\n        expect(navigation?.formData?.get(\"b\")).toBe(\"2\");\n        expect(navigation?.text).toBeUndefined();\n        expect(navigation?.json).toBeUndefined();\n        let { request } = actionSpy.mock.calls[0][0];\n        expect(request.headers.get(\"Content-Type\")).toMatchInlineSnapshot(\n          `\"application/x-www-form-urlencoded;charset=UTF-8\"`,\n        );\n        let actionFormData = await request.formData();\n        expect(actionFormData.get(\"a\")).toBe(\"1\");\n        expect(actionFormData.get(\"b\")).toBe(\"2\");\n      });\n\n      it(\"serializes formData on submit(object)/encType:application/x-www-form-urlencoded submissions\", async () => {\n        let actionSpy = jest.fn();\n        let body = { a: \"1\", b: \"2\" };\n        let navigation: Navigation | undefined;\n        let router = createTestRouter(\n          [\n            {\n              path: \"/\",\n              action: actionSpy,\n              Component() {\n                let submit = useSubmit();\n                let n = useNavigation();\n                if (n.state === \"submitting\") {\n                  navigation = n;\n                }\n                return (\n                  <button\n                    onClick={() =>\n                      submit(body, {\n                        method: \"post\",\n                        encType: \"application/x-www-form-urlencoded\",\n                      })\n                    }\n                  >\n                    Submit\n                  </button>\n                );\n              },\n            },\n          ],\n          { window: getWindow(\"/\") },\n        );\n        render(<RouterProvider router={router} />);\n\n        fireEvent.click(screen.getByText(\"Submit\"));\n        expect(navigation?.formData?.get(\"a\")).toBe(\"1\");\n        expect(navigation?.formData?.get(\"b\")).toBe(\"2\");\n        expect(navigation?.text).toBeUndefined();\n        expect(navigation?.json).toBeUndefined();\n        let { request } = actionSpy.mock.calls[0][0];\n        expect(request.headers.get(\"Content-Type\")).toMatchInlineSnapshot(\n          `\"application/x-www-form-urlencoded;charset=UTF-8\"`,\n        );\n        let actionFormData = await request.formData();\n        expect(actionFormData.get(\"a\")).toBe(\"1\");\n        expect(actionFormData.get(\"b\")).toBe(\"2\");\n      });\n\n      it(\"serializes JSON on submit(object)/encType:application/json submissions\", async () => {\n        let actionSpy = jest.fn();\n        let body = { a: \"1\", b: \"2\" };\n        let navigation: Navigation | undefined;\n        let router = createTestRouter(\n          [\n            {\n              path: \"/\",\n              action: actionSpy,\n              Component() {\n                let submit = useSubmit();\n                let n = useNavigation();\n                if (n.state === \"submitting\") {\n                  navigation = n;\n                }\n                return (\n                  <button\n                    onClick={() =>\n                      submit(body, {\n                        method: \"post\",\n                        encType: \"application/json\",\n                      })\n                    }\n                  >\n                    Submit\n                  </button>\n                );\n              },\n            },\n          ],\n          { window: getWindow(\"/\") },\n        );\n        render(<RouterProvider router={router} />);\n\n        fireEvent.click(screen.getByText(\"Submit\"));\n        expect(navigation?.json).toBe(body);\n        expect(navigation?.text).toBeUndefined();\n        expect(navigation?.formData).toBeUndefined();\n        let { request } = actionSpy.mock.calls[0][0];\n        expect(request.headers.get(\"Content-Type\")).toBe(\"application/json\");\n        expect(await request.json()).toEqual({ a: \"1\", b: \"2\" });\n      });\n\n      it(\"serializes text on submit(object)/encType:text/plain submissions\", async () => {\n        let actionSpy = jest.fn();\n        let body = \"look ma, no formData!\";\n        let navigation: Navigation | undefined;\n        let router = createTestRouter(\n          [\n            {\n              path: \"/\",\n              action: actionSpy,\n              Component() {\n                let submit = useSubmit();\n                let n = useNavigation();\n                if (n.state === \"submitting\") {\n                  navigation = n;\n                }\n                return (\n                  <button\n                    onClick={() =>\n                      submit(body, {\n                        method: \"post\",\n                        encType: \"text/plain\",\n                      })\n                    }\n                  >\n                    Submit\n                  </button>\n                );\n              },\n            },\n          ],\n          { window: getWindow(\"/\") },\n        );\n        render(<RouterProvider router={router} />);\n\n        fireEvent.click(screen.getByText(\"Submit\"));\n        expect(navigation?.text).toBe(body);\n        expect(navigation?.formData).toBeUndefined();\n        expect(navigation?.json).toBeUndefined();\n        let { request } = actionSpy.mock.calls[0][0];\n        expect(request.headers.get(\"Content-Type\")).toBe(\n          \"text/plain;charset=UTF-8\",\n        );\n        expect(await request.text()).toEqual(body);\n      });\n\n      it('serializes into text on <Form encType=\"text/plain\" submissions', async () => {\n        let actionSpy = jest.fn();\n        let router = createTestRouter(\n          [{ path: \"/\", action: actionSpy, Component: FormPage }],\n          { window: getWindow(\"/\") },\n        );\n        render(<RouterProvider router={router} />);\n\n        function FormPage() {\n          return (\n            <Form method=\"post\" encType=\"text/plain\">\n              <input name=\"a\" defaultValue=\"1\" />\n              <input name=\"b\" defaultValue=\"2\" />\n              <button type=\"submit\">Submit</button>\n            </Form>\n          );\n        }\n\n        fireEvent.click(screen.getByText(\"Submit\"));\n        expect(await actionSpy.mock.calls[0][0].request.text())\n          .toMatchInlineSnapshot(`\n            \"a=1\n            b=2\n            \"\n          `);\n      });\n\n      it(\"includes submit button name/value on form submission\", async () => {\n        let actionSpy = jest.fn();\n        let router = createTestRouter(\n          [{ path: \"/\", action: actionSpy, Component: FormPage }],\n          { window: getWindow(\"/\") },\n        );\n        render(<RouterProvider router={router} />);\n\n        function FormPage() {\n          return (\n            <Form method=\"post\">\n              <input name=\"a\" defaultValue=\"1\" />\n              <input name=\"b\" defaultValue=\"2\" />\n              <button name=\"c\" value=\"3\" type=\"submit\">\n                Submit\n              </button>\n            </Form>\n          );\n        }\n\n        fireEvent.click(screen.getByText(\"Submit\"));\n        let formData = await actionSpy.mock.calls[0][0].request.formData();\n        expect(formData.get(\"a\")).toBe(\"1\");\n        expect(formData.get(\"b\")).toBe(\"2\");\n        expect(formData.get(\"c\")).toBe(\"3\");\n      });\n\n      it(\"includes submit button name/value on button submission\", async () => {\n        let actionSpy = jest.fn();\n        let router = createTestRouter(\n          [{ path: \"/\", action: actionSpy, Component: FormPage }],\n          { window: getWindow(\"/\") },\n        );\n        render(<RouterProvider router={router} />);\n\n        function FormPage() {\n          let submit = useSubmit();\n          return (\n            <Form method=\"post\">\n              <input name=\"a\" defaultValue=\"1\" />\n              <input name=\"b\" defaultValue=\"2\" />\n              <button\n                name=\"c\"\n                value=\"3\"\n                onClick={(e) => {\n                  e.preventDefault();\n                  submit(e.currentTarget);\n                }}\n              >\n                Submit\n              </button>\n            </Form>\n          );\n        }\n\n        fireEvent.click(screen.getByText(\"Submit\"));\n        let formData = await actionSpy.mock.calls[0][0].request.formData();\n        expect(formData.get(\"a\")).toBe(\"1\");\n        expect(formData.get(\"b\")).toBe(\"2\");\n        expect(formData.get(\"c\")).toBe(\"3\");\n      });\n\n      it(\"appends button name/value and doesn't overwrite inputs with same name (form)\", async () => {\n        let actionSpy = jest.fn();\n        let router = createTestRouter(\n          [{ path: \"/\", action: actionSpy, Component: FormPage }],\n          { window: getWindow(\"/\") },\n        );\n        render(<RouterProvider router={router} />);\n\n        function FormPage() {\n          return (\n            <Form method=\"post\">\n              <input name=\"a\" defaultValue=\"1\" />\n              <input name=\"b\" defaultValue=\"2\" />\n              <button name=\"b\" value=\"3\" type=\"submit\">\n                Submit\n              </button>\n            </Form>\n          );\n        }\n\n        fireEvent.click(screen.getByText(\"Submit\"));\n        let formData = await actionSpy.mock.calls[0][0].request.formData();\n        expect(formData.get(\"a\")).toBe(\"1\");\n        expect(formData.getAll(\"b\")).toEqual([\"2\", \"3\"]);\n      });\n\n      it(\"appends button name/value and doesn't overwrite inputs with same name (button)\", async () => {\n        let actionSpy = jest.fn();\n        let router = createTestRouter(\n          [{ path: \"/\", action: actionSpy, Component: FormPage }],\n          { window: getWindow(\"/\") },\n        );\n        render(<RouterProvider router={router} />);\n\n        function FormPage() {\n          let submit = useSubmit();\n          return (\n            <Form method=\"post\">\n              <input name=\"a\" defaultValue=\"1\" />\n              <input name=\"b\" defaultValue=\"2\" />\n              <button\n                name=\"b\"\n                value=\"3\"\n                onClick={(e) => {\n                  e.preventDefault();\n                  submit(e.currentTarget);\n                }}\n              >\n                Submit\n              </button>\n            </Form>\n          );\n        }\n\n        fireEvent.click(screen.getByText(\"Submit\"));\n        let formData = await actionSpy.mock.calls[0][0].request.formData();\n        expect(formData.get(\"a\")).toBe(\"1\");\n        expect(formData.getAll(\"b\")).toEqual([\"2\", \"3\"]);\n      });\n\n      it(\"includes the correct submitter value(s) in tree order\", async () => {\n        let actionSpy = jest.fn();\n        actionSpy.mockReturnValue({});\n        async function getPayload() {\n          let formData =\n            await actionSpy.mock.calls[\n              actionSpy.mock.calls.length - 1\n            ][0].request.formData();\n          return new URLSearchParams(formData.entries()).toString();\n        }\n\n        let router = createTestRouter(\n          [{ path: \"/\", action: actionSpy, Component: FormPage }],\n          { window: getWindow(\"/\") },\n        );\n        render(<RouterProvider router={router} />);\n\n        function FormPage() {\n          return (\n            <>\n              <button name=\"tasks\" value=\"outside\" form=\"myform\">\n                Outside\n              </button>\n              <Form id=\"myform\" method=\"post\">\n                <input type=\"text\" name=\"tasks\" defaultValue=\"first\" />\n                <input type=\"text\" name=\"tasks\" defaultValue=\"second\" />\n\n                <button name=\"tasks\" value=\"\">\n                  Add Task\n                </button>\n                <button value=\"\">No Name</button>\n                <input type=\"image\" name=\"tasks\" alt=\"Add Task\" />\n                <input type=\"image\" alt=\"No Name\" />\n\n                <input type=\"text\" name=\"tasks\" defaultValue=\"last\" />\n              </Form>\n            </>\n          );\n        }\n\n        fireEvent.click(screen.getByText(\"Add Task\"));\n        expect(await getPayload()).toEqual(\n          \"tasks=first&tasks=second&tasks=&tasks=last\",\n        );\n\n        fireEvent.click(screen.getByText(\"No Name\"));\n        expect(await getPayload()).toEqual(\n          \"tasks=first&tasks=second&tasks=last\",\n        );\n\n        fireEvent.click(screen.getByAltText(\"Add Task\"), {\n          clientX: 1,\n          clientY: 2,\n        });\n        expect(await getPayload()).toMatch(\n          \"tasks=first&tasks=second&tasks.x=1&tasks.y=2&tasks=last\",\n        );\n\n        fireEvent.click(screen.getByAltText(\"No Name\"), {\n          clientX: 1,\n          clientY: 2,\n        });\n        expect(await getPayload()).toMatch(\n          \"tasks=first&tasks=second&x=1&y=2&tasks=last\",\n        );\n\n        fireEvent.click(screen.getByText(\"Outside\"));\n        expect(await getPayload()).toEqual(\n          \"tasks=outside&tasks=first&tasks=second&tasks=last\",\n        );\n      });\n    });\n\n    describe(\"useFetcher(s)\", () => {\n      it(\"handles fetcher.load and fetcher.submit\", async () => {\n        let count = 0;\n        let router = createTestRouter(\n          [\n            {\n              path: \"/\",\n              Component: Comp,\n              action: async ({ request }) => {\n                let formData = await request.formData();\n                count = count + parseInt(String(formData.get(\"increment\")), 10);\n                return { count };\n              },\n              loader: async ({ request }) => {\n                // Need to add a domain on here in node unit testing so it's a\n                // valid URL. When running in the browser the domain is\n                // automatically added in new Request()\n                let increment =\n                  new URL(`https://remix.test${request.url}`).searchParams.get(\n                    \"increment\",\n                  ) || \"1\";\n                count = count + parseInt(increment, 10);\n                return { count };\n              },\n            },\n          ],\n          {\n            window: getWindow(\"/\"),\n            hydrationData: { loaderData: { \"0\": null } },\n          },\n        );\n        let { container } = render(<RouterProvider router={router} />);\n\n        function Comp() {\n          let fetcher = useFetcher();\n          let fd = new FormData();\n          fd.append(\"increment\", \"10\");\n          return (\n            <>\n              <p id=\"output\">\n                {fetcher.state}\n                {fetcher.data ? JSON.stringify(fetcher.data) : null}\n              </p>\n              <button onClick={() => fetcher.load(\"/\")}>load 1</button>\n              <button onClick={() => fetcher.load(\"/?increment=5\")}>\n                load 5\n              </button>\n              <button onClick={() => fetcher.submit(fd, { method: \"post\" })}>\n                submit 10\n              </button>\n            </>\n          );\n        }\n\n        expect(getHtml(container.querySelector(\"#output\")!))\n          .toMatchInlineSnapshot(`\n            \"<p\n              id=\"output\"\n            >\n              idle\n            </p>\"\n          `);\n\n        fireEvent.click(screen.getByText(\"load 1\"));\n        expect(getHtml(container.querySelector(\"#output\")!))\n          .toMatchInlineSnapshot(`\n            \"<p\n              id=\"output\"\n            >\n              loading\n            </p>\"\n          `);\n\n        await waitFor(() => screen.getByText(/idle/));\n        expect(getHtml(container.querySelector(\"#output\")!))\n          .toMatchInlineSnapshot(`\n            \"<p\n              id=\"output\"\n            >\n              idle\n              {\"count\":1}\n            </p>\"\n          `);\n\n        fireEvent.click(screen.getByText(\"load 5\"));\n        expect(getHtml(container.querySelector(\"#output\")!))\n          .toMatchInlineSnapshot(`\n            \"<p\n              id=\"output\"\n            >\n              loading\n              {\"count\":1}\n            </p>\"\n          `);\n\n        await waitFor(() => screen.getByText(/idle/));\n        expect(getHtml(container.querySelector(\"#output\")!))\n          .toMatchInlineSnapshot(`\n            \"<p\n              id=\"output\"\n            >\n              idle\n              {\"count\":6}\n            </p>\"\n          `);\n\n        fireEvent.click(screen.getByText(\"submit 10\"));\n        expect(getHtml(container.querySelector(\"#output\")!))\n          .toMatchInlineSnapshot(`\n            \"<p\n              id=\"output\"\n            >\n              submitting\n              {\"count\":6}\n            </p>\"\n          `);\n\n        await waitFor(() => screen.getByText(/idle/));\n        expect(getHtml(container.querySelector(\"#output\")!))\n          .toMatchInlineSnapshot(`\n            \"<p\n              id=\"output\"\n            >\n              idle\n              {\"count\":16}\n            </p>\"\n          `);\n      });\n\n      it(\"handles fetcher ?index params\", async () => {\n        let router = createTestRouter(\n          [\n            {\n              id: \"parent\",\n              path: \"/parent\",\n              Component: Outlet,\n              action: () => \"PARENT ACTION\",\n              loader: () => \"PARENT LOADER\",\n              children: [\n                {\n                  id: \"index\",\n                  index: true,\n                  Component: Index,\n                  action: () => \"INDEX ACTION\",\n                  loader: () => \"INDEX LOADER\",\n                },\n              ],\n            },\n          ],\n          {\n            window: getWindow(\"/parent\"),\n            hydrationData: { loaderData: { parent: null, index: null } },\n          },\n        );\n        let { container } = render(<RouterProvider router={router} />);\n\n        function Index() {\n          let fetcher = useFetcher();\n\n          return (\n            <>\n              <p id=\"output\">{fetcher.data}</p>\n              <button onClick={() => fetcher.load(\"/parent\")}>\n                Load parent\n              </button>\n              <button onClick={() => fetcher.load(\"/parent?index\")}>\n                Load index\n              </button>\n              <button onClick={() => fetcher.submit({})}>Submit empty</button>\n              <button\n                onClick={() =>\n                  fetcher.submit({}, { method: \"get\", action: \"/parent\" })\n                }\n              >\n                Submit parent get\n              </button>\n              <button\n                onClick={() =>\n                  fetcher.submit({}, { method: \"get\", action: \"/parent?index\" })\n                }\n              >\n                Submit index get\n              </button>\n              <button\n                onClick={() =>\n                  fetcher.submit({}, { method: \"post\", action: \"/parent\" })\n                }\n              >\n                Submit parent post\n              </button>\n              <button\n                onClick={() =>\n                  fetcher.submit(\n                    {},\n                    { method: \"post\", action: \"/parent?index\" },\n                  )\n                }\n              >\n                Submit index post\n              </button>\n            </>\n          );\n        }\n\n        async function clickAndAssert(btnText: string, expectedOutput: string) {\n          fireEvent.click(screen.getByText(btnText));\n          await new Promise((r) => setTimeout(r, 1));\n          await waitFor(() => screen.getByText(new RegExp(expectedOutput)));\n          expect(getHtml(container.querySelector(\"#output\")!)).toContain(\n            expectedOutput,\n          );\n        }\n\n        await clickAndAssert(\"Load parent\", \"PARENT LOADER\");\n        await clickAndAssert(\"Load index\", \"INDEX LOADER\");\n        await clickAndAssert(\"Submit empty\", \"INDEX LOADER\");\n        await clickAndAssert(\"Submit parent get\", \"PARENT LOADER\");\n        await clickAndAssert(\"Submit index get\", \"INDEX LOADER\");\n        await clickAndAssert(\"Submit parent post\", \"PARENT ACTION\");\n        await clickAndAssert(\"Submit index post\", \"INDEX ACTION\");\n      });\n\n      it(\"handles fetcher.load errors\", async () => {\n        let router = createTestRouter(\n          [\n            {\n              path: \"/\",\n              Component: Comp,\n              ErrorBoundary: () => <ErrorElement />,\n              loader: async () => {\n                throw new Error(\"Kaboom!\");\n              },\n            },\n          ],\n          {\n            window: getWindow(\"/\"),\n            hydrationData: { loaderData: { \"0\": null } },\n          },\n        );\n        let { container } = render(<RouterProvider router={router} />);\n\n        function Comp() {\n          let fetcher = useFetcher();\n          return (\n            <>\n              <p>\n                {fetcher.state}\n                {fetcher.data ? JSON.stringify(fetcher.data) : null}\n              </p>\n              <button onClick={() => fetcher.load(\"/\")}>load</button>\n            </>\n          );\n        }\n\n        function ErrorElement() {\n          let error = useRouteError() as Error;\n          return <p>{error.message}</p>;\n        }\n\n        expect(getHtml(container)).toMatchInlineSnapshot(`\n          \"<div>\n            <p>\n              idle\n            </p>\n            <button>\n              load\n            </button>\n          </div>\"\n        `);\n\n        fireEvent.click(screen.getByText(\"load\"));\n        expect(getHtml(container)).toMatchInlineSnapshot(`\n          \"<div>\n            <p>\n              loading\n            </p>\n            <button>\n              load\n            </button>\n          </div>\"\n        `);\n\n        await waitFor(() => screen.getByText(\"Kaboom!\"));\n        expect(getHtml(container)).toMatchInlineSnapshot(`\n          \"<div>\n            <p>\n              Kaboom!\n            </p>\n          </div>\"\n        `);\n      });\n\n      it(\"handles fetcher.load errors (defer)\", async () => {\n        let dfd = createDeferred();\n        let router = createTestRouter(\n          [\n            {\n              path: \"/\",\n              Component: Comp,\n              ErrorBoundary: () => <ErrorElement />,\n              loader: () => ({ value: dfd.promise }),\n            },\n          ],\n          {\n            window: getWindow(\"/\"),\n            hydrationData: { loaderData: { \"0\": null } },\n          },\n        );\n        let { container } = render(<RouterProvider router={router} />);\n\n        function Comp() {\n          let fetcher = useFetcher();\n          return (\n            <>\n              <p>{fetcher.state}</p>\n              {fetcher.data ? (\n                <React.Suspense fallback={<p>Loading 2...</p>}>\n                  <Await\n                    resolve={fetcher.data.value}\n                    errorElement={<ErrorElement />}\n                  >\n                    {(val) => <p>{val}</p>}\n                  </Await>\n                </React.Suspense>\n              ) : (\n                <p>Loading 1...</p>\n              )}\n              <button onClick={() => fetcher.load(\"/\")}>load</button>\n            </>\n          );\n        }\n\n        function ErrorElement() {\n          let error = useAsyncError() as Error;\n          return <p>{error.message}</p>;\n        }\n\n        expect(getHtml(container)).toMatchInlineSnapshot(`\n          \"<div>\n            <p>\n              idle\n            </p>\n            <p>\n              Loading 1...\n            </p>\n            <button>\n              load\n            </button>\n          </div>\"\n        `);\n\n        fireEvent.click(screen.getByText(\"load\"));\n        expect(getHtml(container)).toMatchInlineSnapshot(`\n          \"<div>\n            <p>\n              loading\n            </p>\n            <p>\n              Loading 1...\n            </p>\n            <button>\n              load\n            </button>\n          </div>\"\n        `);\n\n        await waitFor(() => screen.getByText(\"Loading 2...\"));\n        expect(getHtml(container)).toMatchInlineSnapshot(`\n          \"<div>\n            <p>\n              idle\n            </p>\n            <p>\n              Loading 2...\n            </p>\n            <button>\n              load\n            </button>\n          </div>\"\n        `);\n\n        dfd.reject(new Error(\"Kaboom!\"));\n        await waitFor(() => screen.getByText(\"Kaboom!\"));\n        expect(getHtml(container)).toMatchInlineSnapshot(`\n          \"<div>\n            <p>\n              idle\n            </p>\n            <p>\n              Kaboom!\n            </p>\n            <button>\n              load\n            </button>\n          </div>\"\n        `);\n      });\n\n      it(\"handles fetcher.submit errors\", async () => {\n        let router = createTestRouter(\n          [\n            {\n              path: \"/\",\n              Component: Comp,\n              ErrorBoundary: () => <ErrorElement />,\n              action: async () => {\n                throw new Error(\"Kaboom!\");\n              },\n            },\n          ],\n          {\n            window: getWindow(\"/\"),\n            hydrationData: { loaderData: { \"0\": null } },\n          },\n        );\n        let { container } = render(<RouterProvider router={router} />);\n\n        function Comp() {\n          let fetcher = useFetcher();\n          return (\n            <>\n              <p>\n                {fetcher.state}\n                {fetcher.data ? JSON.stringify(fetcher.data) : null}\n              </p>\n              <button\n                onClick={() =>\n                  fetcher.submit(new FormData(), { method: \"post\" })\n                }\n              >\n                submit\n              </button>\n            </>\n          );\n        }\n\n        function ErrorElement() {\n          let error = useRouteError() as Error;\n          return <p>{error.message}</p>;\n        }\n\n        expect(getHtml(container)).toMatchInlineSnapshot(`\n          \"<div>\n            <p>\n              idle\n            </p>\n            <button>\n              submit\n            </button>\n          </div>\"\n        `);\n\n        fireEvent.click(screen.getByText(\"submit\"));\n        expect(getHtml(container)).toMatchInlineSnapshot(`\n          \"<div>\n            <p>\n              submitting\n            </p>\n            <button>\n              submit\n            </button>\n          </div>\"\n        `);\n\n        await waitFor(() => screen.getByText(\"Kaboom!\"));\n        expect(getHtml(container)).toMatchInlineSnapshot(`\n          \"<div>\n            <p>\n              Kaboom!\n            </p>\n          </div>\"\n        `);\n      });\n\n      it(\"handles fetcher.Form\", async () => {\n        let count = 0;\n        let router = createTestRouter(\n          [\n            {\n              path: \"/\",\n              Component: Comp,\n              action: async ({ request }) => {\n                let formData = await request.formData();\n                count = count + parseInt(String(formData.get(\"increment\")), 10);\n                return { count };\n              },\n              loader: async ({ request }) => {\n                // Need to add a domain on here in node unit testing so it's a\n                // valid URL. When running in the browser the domain is\n                // automatically added in new Request()\n                let increment =\n                  new URL(`https://remix.test${request.url}`).searchParams.get(\n                    \"increment\",\n                  ) || \"1\";\n                count = count + parseInt(increment, 10);\n                return { count };\n              },\n            },\n          ],\n          {\n            window: getWindow(\"/\"),\n            hydrationData: { loaderData: { \"0\": null } },\n          },\n        );\n        let { container } = render(<RouterProvider router={router} />);\n\n        function Comp() {\n          let fetcher = useFetcher();\n          return (\n            <>\n              <p id=\"output\">\n                {fetcher.state}\n                {fetcher.data ? JSON.stringify(fetcher.data) : null}\n              </p>\n              <fetcher.Form>\n                <input name=\"increment\" value=\"1\" />\n                <button type=\"submit\">submit get 1</button>\n              </fetcher.Form>\n              <fetcher.Form method=\"post\">\n                <input name=\"increment\" value=\"10\" />\n                <button type=\"submit\">submit post 10</button>\n              </fetcher.Form>\n            </>\n          );\n        }\n\n        expect(getHtml(container.querySelector(\"#output\")!))\n          .toMatchInlineSnapshot(`\n            \"<p\n              id=\"output\"\n            >\n              idle\n            </p>\"\n          `);\n\n        fireEvent.click(screen.getByText(\"submit get 1\"));\n        expect(getHtml(container.querySelector(\"#output\")!))\n          .toMatchInlineSnapshot(`\n            \"<p\n              id=\"output\"\n            >\n              loading\n            </p>\"\n          `);\n\n        await waitFor(() => screen.getByText(/idle/));\n        expect(getHtml(container.querySelector(\"#output\")!))\n          .toMatchInlineSnapshot(`\n            \"<p\n              id=\"output\"\n            >\n              idle\n              {\"count\":1}\n            </p>\"\n          `);\n\n        fireEvent.click(screen.getByText(\"submit post 10\"));\n        expect(getHtml(container.querySelector(\"#output\")!))\n          .toMatchInlineSnapshot(`\n            \"<p\n              id=\"output\"\n            >\n              submitting\n              {\"count\":1}\n            </p>\"\n          `);\n\n        await waitFor(() => screen.getByText(/idle/));\n        expect(getHtml(container.querySelector(\"#output\")!))\n          .toMatchInlineSnapshot(`\n            \"<p\n              id=\"output\"\n            >\n              idle\n              {\"count\":11}\n            </p>\"\n          `);\n      });\n\n      it(\"handles fetcher.Form get errors\", async () => {\n        let router = createTestRouter(\n          [\n            {\n              path: \"/\",\n              Component: Comp,\n              ErrorBoundary: () => <ErrorElement />,\n              loader: async () => {\n                throw new Error(\"Kaboom!\");\n              },\n            },\n          ],\n          {\n            window: getWindow(\"/\"),\n            hydrationData: { loaderData: { \"0\": null } },\n          },\n        );\n        let { container } = render(<RouterProvider router={router} />);\n\n        function Comp() {\n          let fetcher = useFetcher();\n          return (\n            <>\n              <p id=\"output\">\n                {fetcher.state}\n                {fetcher.data ? JSON.stringify(fetcher.data) : null}\n              </p>\n              <fetcher.Form>\n                <button type=\"submit\">submit</button>\n              </fetcher.Form>\n            </>\n          );\n        }\n\n        function ErrorElement() {\n          let error = useRouteError() as Error;\n          return <p>{error.message}</p>;\n        }\n\n        expect(getHtml(container.querySelector(\"#output\")!))\n          .toMatchInlineSnapshot(`\n            \"<p\n              id=\"output\"\n            >\n              idle\n            </p>\"\n          `);\n\n        fireEvent.click(screen.getByText(\"submit\"));\n        expect(getHtml(container.querySelector(\"#output\")!))\n          .toMatchInlineSnapshot(`\n            \"<p\n              id=\"output\"\n            >\n              loading\n            </p>\"\n          `);\n\n        await waitFor(() => screen.getByText(\"Kaboom!\"));\n        expect(getHtml(container)).toMatchInlineSnapshot(`\n          \"<div>\n            <p>\n              Kaboom!\n            </p>\n          </div>\"\n        `);\n      });\n\n      it(\"handles fetcher.Form post errors\", async () => {\n        let router = createTestRouter(\n          [\n            {\n              path: \"/\",\n              Component: Comp,\n              ErrorBoundary: () => <ErrorElement />,\n              action: async () => {\n                throw new Error(\"Kaboom!\");\n              },\n            },\n          ],\n          {\n            window: getWindow(\"/\"),\n            hydrationData: { loaderData: { \"0\": null } },\n          },\n        );\n        let { container } = render(<RouterProvider router={router} />);\n\n        function Comp() {\n          let fetcher = useFetcher();\n          return (\n            <>\n              <p id=\"output\">\n                {fetcher.state}\n                {fetcher.data ? JSON.stringify(fetcher.data) : null}\n              </p>\n              <fetcher.Form method=\"post\">\n                <button type=\"submit\">submit</button>\n              </fetcher.Form>\n            </>\n          );\n        }\n\n        function ErrorElement() {\n          let error = useRouteError() as Error;\n          return <p>{error.message}</p>;\n        }\n\n        expect(getHtml(container.querySelector(\"#output\")!))\n          .toMatchInlineSnapshot(`\n            \"<p\n              id=\"output\"\n            >\n              idle\n            </p>\"\n          `);\n\n        fireEvent.click(screen.getByText(\"submit\"));\n        expect(getHtml(container.querySelector(\"#output\")!))\n          .toMatchInlineSnapshot(`\n            \"<p\n              id=\"output\"\n            >\n              submitting\n            </p>\"\n          `);\n\n        await waitFor(() => screen.getByText(\"Kaboom!\"));\n        expect(getHtml(container)).toMatchInlineSnapshot(`\n          \"<div>\n            <p>\n              Kaboom!\n            </p>\n          </div>\"\n        `);\n      });\n\n      it(\"serializes fetcher.submit(object) as FormData\", async () => {\n        let actionSpy = jest.fn();\n        let body = { key: \"value\" };\n        let fetcher: Fetcher;\n        let router = createTestRouter(\n          [\n            {\n              path: \"/\",\n              action: actionSpy,\n              Component() {\n                let f = useFetcher();\n                fetcher = f;\n                return (\n                  <button onClick={() => f.submit(body, { method: \"post\" })}>\n                    Submit\n                  </button>\n                );\n              },\n            },\n          ],\n          {\n            window: getWindow(\"/\"),\n          },\n        );\n\n        render(<RouterProvider router={router} />);\n        fireEvent.click(screen.getByText(\"Submit\"));\n        // @ts-expect-error\n        expect(fetcher.formData?.get(\"key\")).toBe(\"value\");\n        // @ts-expect-error\n        expect(fetcher.text).toBeUndefined();\n        // @ts-expect-error\n        expect(fetcher.json).toBeUndefined();\n        let formData = await actionSpy.mock.calls[0][0].request.formData();\n        expect(formData.get(\"key\")).toBe(\"value\");\n      });\n\n      it(\"serializes fetcher.submit(object, { encType:application/x-www-form-urlencoded }) as FormData\", async () => {\n        let actionSpy = jest.fn();\n        let body = { key: \"value\" };\n        let fetcher: Fetcher;\n        let router = createTestRouter(\n          [\n            {\n              path: \"/\",\n              action: actionSpy,\n              Component() {\n                let f = useFetcher();\n                fetcher = f;\n                return (\n                  <button\n                    onClick={() =>\n                      f.submit(body, {\n                        method: \"post\",\n                        encType: \"application/x-www-form-urlencoded\",\n                      })\n                    }\n                  >\n                    Submit\n                  </button>\n                );\n              },\n            },\n          ],\n          {\n            window: getWindow(\"/\"),\n          },\n        );\n\n        render(<RouterProvider router={router} />);\n        fireEvent.click(screen.getByText(\"Submit\"));\n        // @ts-expect-error\n        expect(fetcher.formData?.get(\"key\")).toBe(\"value\");\n        // @ts-expect-error\n        expect(fetcher.text).toBeUndefined();\n        // @ts-expect-error\n        expect(fetcher.json).toBeUndefined();\n        let formData = await actionSpy.mock.calls[0][0].request.formData();\n        expect(formData.get(\"key\")).toBe(\"value\");\n      });\n\n      it(\"serializes fetcher.submit(object, { encType:application/json }) as FormData\", async () => {\n        let actionSpy = jest.fn();\n        let body = { key: \"value\" };\n        let fetcher: Fetcher;\n        let router = createTestRouter(\n          [\n            {\n              path: \"/\",\n              action: actionSpy,\n              Component() {\n                let f = useFetcher();\n                fetcher = f;\n                return (\n                  <button\n                    onClick={() =>\n                      f.submit(body, {\n                        method: \"post\",\n                        encType: \"application/json\",\n                      })\n                    }\n                  >\n                    Submit\n                  </button>\n                );\n              },\n            },\n          ],\n          {\n            window: getWindow(\"/\"),\n          },\n        );\n\n        render(<RouterProvider router={router} />);\n        fireEvent.click(screen.getByText(\"Submit\"));\n        // @ts-expect-error\n        expect(fetcher.json).toBe(body);\n        // @ts-expect-error\n        expect(fetcher.text).toBeUndefined();\n        // @ts-expect-error\n        expect(fetcher.formData).toBeUndefined();\n        let json = await actionSpy.mock.calls[0][0].request.json();\n        expect(json).toEqual(body);\n      });\n\n      it(\"serializes fetcher.submit(object, { encType:text/plain }) as text\", async () => {\n        let actionSpy = jest.fn();\n        let body = \"Look ma, no FormData!\";\n        let fetcher: Fetcher;\n        let router = createTestRouter(\n          [\n            {\n              path: \"/\",\n              action: actionSpy,\n              Component() {\n                let f = useFetcher();\n                fetcher = f;\n                return (\n                  <button\n                    onClick={() =>\n                      f.submit(body, {\n                        method: \"post\",\n                        encType: \"text/plain\",\n                      })\n                    }\n                  >\n                    Submit\n                  </button>\n                );\n              },\n            },\n          ],\n          {\n            window: getWindow(\"/\"),\n          },\n        );\n\n        render(<RouterProvider router={router} />);\n        fireEvent.click(screen.getByText(\"Submit\"));\n        // @ts-expect-error\n        expect(fetcher.text).toBe(body);\n        // @ts-expect-error\n        expect(fetcher.formData).toBeUndefined();\n        // @ts-expect-error\n        expect(fetcher.json).toBeUndefined();\n        let text = await actionSpy.mock.calls[0][0].request.text();\n        expect(text).toEqual(body);\n      });\n\n      it(\"show all active fetchers via useFetchers and cleans up fetchers on unmount\", async () => {\n        let navDfd = createDeferred();\n        let fetchDfd1 = createDeferred();\n        let fetchDfd2 = createDeferred();\n        let router = createTestRouter(\n          [\n            {\n              path: \"/\",\n              Component: Parent,\n              children: [\n                { path: \"/1\", Component: Comp1 },\n                {\n                  path: \"/2\",\n                  loader: () => navDfd.promise,\n                  Component: Comp2,\n                },\n                { path: \"/fetch-1\", loader: () => fetchDfd1.promise },\n                { path: \"/fetch-2\", loader: () => fetchDfd2.promise },\n              ],\n            },\n          ],\n          {\n            window: getWindow(\"/1\"),\n            hydrationData: { loaderData: { \"0\": null, \"0-0\": null } },\n          },\n        );\n        let { container } = render(<RouterProvider router={router} />);\n\n        function Parent() {\n          let fetchers = useFetchers();\n          return (\n            <>\n              <Link to=\"/1\">Link to 1</Link>\n              <Link to=\"/2\">Link to 2</Link>\n              <div id=\"output\">\n                <p>{JSON.stringify(fetchers.map((f) => f.state))}</p>\n                <Outlet />\n              </div>\n            </>\n          );\n        }\n\n        function Comp1() {\n          let fetcher = useFetcher();\n          return (\n            <>\n              <p>\n                1{fetcher.state}\n                {fetcher.data || \"null\"}\n              </p>\n              <button onClick={() => fetcher.load(\"/fetch-1\")}>load</button>\n            </>\n          );\n        }\n\n        function Comp2() {\n          let fetcher = useFetcher();\n          return (\n            <>\n              <p>\n                2{fetcher.state}\n                {fetcher.data || \"null\"}\n              </p>\n              <button onClick={() => fetcher.load(\"/fetch-2\")}>load</button>\n            </>\n          );\n        }\n\n        // Initial state - no useFetchers reflected yet\n        expect(getHtml(container.querySelector(\"#output\")!))\n          .toMatchInlineSnapshot(`\n            \"<div\n              id=\"output\"\n            >\n              <p>\n                []\n              </p>\n              <p>\n                1\n                idle\n                null\n              </p>\n              <button>\n                load\n              </button>\n            </div>\"\n          `);\n\n        // Activate Comp1 fetcher\n        fireEvent.click(screen.getByText(\"load\"));\n        expect(getHtml(container.querySelector(\"#output\")!))\n          .toMatchInlineSnapshot(`\n            \"<div\n              id=\"output\"\n            >\n              <p>\n                [\"loading\"]\n              </p>\n              <p>\n                1\n                loading\n                null\n              </p>\n              <button>\n                load\n              </button>\n            </div>\"\n          `);\n\n        // Resolve Comp1 fetcher - UI updates\n        fetchDfd1.resolve(\"data 1\");\n        await waitFor(() => screen.getByText(/data 1/));\n        expect(getHtml(container.querySelector(\"#output\")!))\n          .toMatchInlineSnapshot(`\n            \"<div\n              id=\"output\"\n            >\n              <p>\n                []\n              </p>\n              <p>\n                1\n                idle\n                data 1\n              </p>\n              <button>\n                load\n              </button>\n            </div>\"\n          `);\n\n        // Link to Comp2 - loaders run\n        fireEvent.click(screen.getByText(\"Link to 2\"));\n        expect(getHtml(container.querySelector(\"#output\")!))\n          .toMatchInlineSnapshot(`\n            \"<div\n              id=\"output\"\n            >\n              <p>\n                []\n              </p>\n              <p>\n                1\n                idle\n                data 1\n              </p>\n              <button>\n                load\n              </button>\n            </div>\"\n          `);\n\n        // Resolve Comp2 loader and complete navigation\n        navDfd.resolve(\"nav data\");\n        await waitFor(() => screen.getByText(/2.*idle/));\n        expect(getHtml(container.querySelector(\"#output\")!))\n          .toMatchInlineSnapshot(`\n            \"<div\n              id=\"output\"\n            >\n              <p>\n                []\n              </p>\n              <p>\n                2\n                idle\n                null\n              </p>\n              <button>\n                load\n              </button>\n            </div>\"\n          `);\n\n        // Activate Comp2 fetcher\n        fireEvent.click(screen.getByText(\"load\"));\n        expect(getHtml(container.querySelector(\"#output\")!))\n          .toMatchInlineSnapshot(`\n            \"<div\n              id=\"output\"\n            >\n              <p>\n                [\"loading\"]\n              </p>\n              <p>\n                2\n                loading\n                null\n              </p>\n              <button>\n                load\n              </button>\n            </div>\"\n          `);\n\n        // Comp2 loader resolves with the same data, useFetchers reflects idle-done\n        fetchDfd2.resolve(\"data 2\");\n        await waitFor(() => screen.getByText(/data 2/));\n        expect(getHtml(container.querySelector(\"#output\")!))\n          .toMatchInlineSnapshot(`\n            \"<div\n              id=\"output\"\n            >\n              <p>\n                []\n              </p>\n              <p>\n                2\n                idle\n                data 2\n              </p>\n              <button>\n                load\n              </button>\n            </div>\"\n          `);\n      });\n\n      it(\"handles revalidating fetchers\", async () => {\n        let count = 0;\n        let fetchCount = 0;\n        let router = createTestRouter(\n          [\n            {\n              id: \"index\",\n              path: \"/\",\n              Component: Comp,\n              action: async ({ request }) => {\n                let formData = await request.formData();\n                count = count + parseInt(String(formData.get(\"increment\")), 10);\n                return { count };\n              },\n              loader: async () => ({ count: ++count }),\n            },\n            {\n              path: \"/fetch\",\n              loader: async () => ({ fetchCount: ++fetchCount }),\n            },\n          ],\n          {\n            window: getWindow(\"/\"),\n            hydrationData: { loaderData: { index: null } },\n          },\n        );\n        let { container } = render(<RouterProvider router={router} />);\n\n        function Comp() {\n          let fetcher = useFetcher();\n          return (\n            <>\n              <p id=\"output\">\n                {fetcher.state}\n                {fetcher.data ? JSON.stringify(fetcher.data) : null}\n              </p>\n              <button onClick={() => fetcher.load(\"/fetch\")}>\n                load fetcher\n              </button>\n              <Form method=\"post\">\n                <button type=\"submit\" name=\"increment\" value=\"10\">\n                  submit\n                </button>\n              </Form>\n            </>\n          );\n        }\n\n        expect(getHtml(container.querySelector(\"#output\")!))\n          .toMatchInlineSnapshot(`\n            \"<p\n              id=\"output\"\n            >\n              idle\n            </p>\"\n          `);\n\n        await act(async () => {\n          fireEvent.click(screen.getByText(\"load fetcher\"));\n          await waitFor(() => screen.getByText(/idle/));\n        });\n        expect(getHtml(container.querySelector(\"#output\")!))\n          .toMatchInlineSnapshot(`\n            \"<p\n              id=\"output\"\n            >\n              idle\n              {\"fetchCount\":1}\n            </p>\"\n          `);\n\n        await act(async () => {\n          fireEvent.click(screen.getByText(\"submit\"));\n          await waitFor(() => screen.getByText(/idle/));\n        });\n        expect(getHtml(container.querySelector(\"#output\")!))\n          .toMatchInlineSnapshot(`\n            \"<p\n              id=\"output\"\n            >\n              idle\n              {\"fetchCount\":2}\n            </p>\"\n          `);\n      });\n\n      it(\"handles fetcher 404 errors at the correct spot in the route hierarchy\", async () => {\n        let router = createTestRouter(\n          [\n            {\n              path: \"/\",\n              Component: Outlet,\n              ErrorBoundary: () => <p>Not I!</p>,\n              children: [\n                {\n                  path: \"child\",\n                  Component: Comp,\n                  ErrorBoundary: () => <ErrorElement />,\n                },\n              ],\n            },\n          ],\n          {\n            window: getWindow(\"/child\"),\n            hydrationData: { loaderData: { \"0\": null } },\n          },\n        );\n        let { container } = render(<RouterProvider router={router} />);\n\n        function Comp() {\n          let fetcher = useFetcher();\n          return (\n            <button onClick={() => fetcher.load(\"/not-found\")}>load</button>\n          );\n        }\n\n        function ErrorElement() {\n          let { status, statusText } = useRouteError() as ErrorResponse;\n          return <p>contextual error:{`${status} ${statusText}`}</p>;\n        }\n\n        expect(getHtml(container)).toMatchInlineSnapshot(`\n          \"<div>\n            <button>\n              load\n            </button>\n          </div>\"\n        `);\n\n        fireEvent.click(screen.getByText(\"load\"));\n        await waitFor(() => screen.getByText(/Not Found/));\n        expect(getHtml(container)).toMatchInlineSnapshot(`\n          \"<div>\n            <p>\n              contextual error:\n              404 Not Found\n            </p>\n          </div>\"\n        `);\n      });\n\n      it(\"handles fetcher.load errors at the correct spot in the route hierarchy\", async () => {\n        let router = createTestRouter(\n          [\n            {\n              path: \"/\",\n              Component: Outlet,\n              ErrorBoundary: () => <p>Not I!</p>,\n              children: [\n                {\n                  path: \"child\",\n                  Component: Comp,\n                  ErrorBoundary: () => <ErrorElement />,\n                },\n                {\n                  path: \"fetch\",\n                  loader: () => {\n                    throw new Error(\"Kaboom!\");\n                  },\n                  ErrorBoundary: () => <p>Not I!</p>,\n                },\n              ],\n            },\n          ],\n          {\n            window: getWindow(\"/child\"),\n            hydrationData: { loaderData: { \"0\": null } },\n          },\n        );\n        let { container } = render(<RouterProvider router={router} />);\n\n        function Comp() {\n          let fetcher = useFetcher();\n          return <button onClick={() => fetcher.load(\"/fetch\")}>load</button>;\n        }\n\n        function ErrorElement() {\n          let error = useRouteError() as Error;\n          return <p>contextual error:{error.message}</p>;\n        }\n\n        expect(getHtml(container)).toMatchInlineSnapshot(`\n          \"<div>\n            <button>\n              load\n            </button>\n          </div>\"\n        `);\n\n        fireEvent.click(screen.getByText(\"load\"));\n        await waitFor(() => screen.getByText(/Kaboom!/));\n        expect(getHtml(container)).toMatchInlineSnapshot(`\n          \"<div>\n            <p>\n              contextual error:\n              Kaboom!\n            </p>\n          </div>\"\n        `);\n      });\n\n      it(\"handles fetcher.submit errors at the correct spot in the route hierarchy\", async () => {\n        let router = createTestRouter(\n          [\n            {\n              path: \"/\",\n              Component: Outlet,\n              ErrorBoundary: () => <p>Not I!</p>,\n              children: [\n                {\n                  path: \"child\",\n                  Component: Comp,\n                  ErrorBoundary: () => <ErrorElement />,\n                },\n                {\n                  path: \"fetch\",\n                  action: () => {\n                    throw new Error(\"Kaboom!\");\n                  },\n                  ErrorBoundary: () => <p>Not I!</p>,\n                },\n              ],\n            },\n          ],\n          {\n            window: getWindow(\"/child\"),\n            hydrationData: { loaderData: { \"0\": null } },\n          },\n        );\n        let { container } = render(<RouterProvider router={router} />);\n\n        function Comp() {\n          let fetcher = useFetcher();\n          return (\n            <button\n              onClick={() =>\n                fetcher.submit(\n                  { key: \"value\" },\n                  { method: \"post\", action: \"/fetch\" },\n                )\n              }\n            >\n              submit\n            </button>\n          );\n        }\n\n        function ErrorElement() {\n          let error = useRouteError() as Error;\n          return <p>contextual error:{error.message}</p>;\n        }\n\n        expect(getHtml(container)).toMatchInlineSnapshot(`\n          \"<div>\n            <button>\n              submit\n            </button>\n          </div>\"\n        `);\n\n        fireEvent.click(screen.getByText(\"submit\"));\n        await waitFor(() => screen.getByText(/Kaboom!/));\n        expect(getHtml(container)).toMatchInlineSnapshot(`\n          \"<div>\n            <p>\n              contextual error:\n              Kaboom!\n            </p>\n          </div>\"\n        `);\n      });\n\n      it(\"handles fetcher.Form errors at the correct spot in the route hierarchy\", async () => {\n        let router = createTestRouter(\n          [\n            {\n              path: \"/\",\n              Component: Outlet,\n              ErrorBoundary: () => <p>Not I!</p>,\n              children: [\n                {\n                  path: \"child\",\n                  Component: Comp,\n                  ErrorBoundary: () => <ErrorElement />,\n                },\n                {\n                  path: \"fetch\",\n                  action: () => {\n                    throw new Error(\"Kaboom!\");\n                  },\n                  ErrorBoundary: () => <p>Not I!</p>,\n                },\n              ],\n            },\n          ],\n          {\n            window: getWindow(\"/child\"),\n            hydrationData: { loaderData: { \"0\": null } },\n          },\n        );\n        let { container } = render(<RouterProvider router={router} />);\n\n        function Comp() {\n          let fetcher = useFetcher();\n          return (\n            <fetcher.Form method=\"post\" action=\"/fetch\">\n              <button type=\"submit\" name=\"key\" value=\"value\">\n                submit\n              </button>\n            </fetcher.Form>\n          );\n        }\n\n        function ErrorElement() {\n          let error = useRouteError() as Error;\n          return <p>contextual error:{error.message}</p>;\n        }\n\n        expect(getHtml(container)).toMatchInlineSnapshot(`\n          \"<div>\n            <form\n              action=\"/fetch\"\n              data-discover=\"true\"\n              method=\"post\"\n            >\n              <button\n                name=\"key\"\n                type=\"submit\"\n                value=\"value\"\n              >\n                submit\n              </button>\n            </form>\n          </div>\"\n        `);\n\n        fireEvent.click(screen.getByText(\"submit\"));\n        await waitFor(() => screen.getByText(/Kaboom!/));\n        expect(getHtml(container)).toMatchInlineSnapshot(`\n          \"<div>\n            <p>\n              contextual error:\n              Kaboom!\n            </p>\n          </div>\"\n        `);\n      });\n\n      it(\"useFetcher is stable across across location changes\", async () => {\n        let router = createBrowserRouter(\n          [\n            {\n              path: \"/\",\n              Component() {\n                const [, setSearchParams] = useSearchParams();\n                let [count, setCount] = React.useState(0);\n                let fetcherCount = React.useRef(0);\n                let fetcher = useFetcher();\n                React.useEffect(() => {\n                  fetcherCount.current++;\n                }, [fetcher.submit]);\n                return (\n                  <>\n                    <button\n                      onClick={() => {\n                        setCount(count + 1);\n                        setSearchParams({\n                          random: Math.random().toString(),\n                        });\n                      }}\n                    >\n                      Click\n                    </button>\n                    <p>\n                      {`render count:${count}`}\n                      {`fetcher count:${fetcherCount.current}`}\n                    </p>\n                  </>\n                );\n              },\n            },\n          ],\n          {\n            window: getWindow(\"/\"),\n          },\n        );\n\n        let { container } = render(<RouterProvider router={router} />);\n\n        let html = getHtml(container);\n        expect(html).toContain(\"render count:0\");\n        expect(html).toContain(\"fetcher count:0\");\n\n        fireEvent.click(screen.getByText(\"Click\"));\n        fireEvent.click(screen.getByText(\"Click\"));\n        fireEvent.click(screen.getByText(\"Click\"));\n        await waitFor(() => screen.getByText(/render count:3/));\n\n        html = getHtml(container);\n        expect(html).toContain(\"render count:3\");\n        expect(html).toContain(\"fetcher count:1\");\n      });\n\n      it(\"resets a fetcher\", async () => {\n        let router = createTestRouter(\n          [\n            {\n              path: \"/\",\n              Component() {\n                let fetcher = useFetcher();\n                return (\n                  <>\n                    <p id=\"output\">{`${fetcher.state}-${fetcher.data}`}</p>\n                    <button onClick={() => fetcher.load(\"/\")}>load</button>\n                    <button onClick={() => fetcher.reset()}>reset</button>\n                  </>\n                );\n              },\n              async loader() {\n                return \"FETCH\";\n              },\n            },\n          ],\n          {\n            window: getWindow(\"/\"),\n            hydrationData: { loaderData: { \"0\": null } },\n          },\n        );\n        let { container } = render(<RouterProvider router={router} />);\n\n        expect(getHtml(container.querySelector(\"#output\")!))\n          .toMatchInlineSnapshot(`\n            \"<p\n              id=\"output\"\n            >\n              idle-undefined\n            </p>\"\n          `);\n\n        fireEvent.click(screen.getByText(\"load\"));\n        expect(getHtml(container.querySelector(\"#output\")!))\n          .toMatchInlineSnapshot(`\n            \"<p\n              id=\"output\"\n            >\n              loading-undefined\n            </p>\"\n          `);\n\n        await waitFor(() => screen.getByText(/idle/));\n        expect(getHtml(container.querySelector(\"#output\")!))\n          .toMatchInlineSnapshot(`\n            \"<p\n              id=\"output\"\n            >\n              idle-FETCH\n            </p>\"\n          `);\n\n        fireEvent.click(screen.getByText(\"reset\"));\n        await waitFor(() => screen.getByText(/idle/));\n        expect(getHtml(container.querySelector(\"#output\")!))\n          .toMatchInlineSnapshot(`\n            \"<p\n              id=\"output\"\n            >\n              idle-null\n            </p>\"\n          `);\n      });\n\n      describe(\"useFetcher({ key })\", () => {\n        it(\"generates unique keys for fetchers by default\", async () => {\n          let dfd1 = createDeferred();\n          let dfd2 = createDeferred();\n          let router = createTestRouter(\n            [\n              {\n                path: \"/\",\n                Component() {\n                  let fetcher1 = useFetcher();\n                  let fetcher2 = useFetcher();\n                  let fetchers = useFetchers();\n                  return (\n                    <>\n                      <button onClick={() => fetcher1.load(\"/fetch1\")}>\n                        Load 1\n                      </button>\n                      <button onClick={() => fetcher2.load(\"/fetch2\")}>\n                        Load 2\n                      </button>\n                      <pre>{`${fetchers.length}, ${fetcher1.state}/${fetcher1.data}, ${fetcher2.state}/${fetcher2.data}`}</pre>\n                    </>\n                  );\n                },\n              },\n              {\n                path: \"/fetch1\",\n                loader: () => dfd1.promise,\n              },\n              {\n                path: \"/fetch2\",\n                loader: () => dfd2.promise,\n              },\n            ],\n            { window: getWindow(\"/\") },\n          );\n          let { container } = render(<RouterProvider router={router} />);\n\n          expect(container.querySelector(\"pre\")!.innerHTML).toBe(\n            \"0, idle/undefined, idle/undefined\",\n          );\n\n          fireEvent.click(screen.getByText(\"Load 1\"));\n          await waitFor(() =>\n            screen.getByText(\"1, loading/undefined, idle/undefined\"),\n          );\n\n          dfd1.resolve(\"FETCH 1\");\n          await waitFor(() =>\n            screen.getByText(\"0, idle/FETCH 1, idle/undefined\"),\n          );\n\n          fireEvent.click(screen.getByText(\"Load 2\"));\n          await waitFor(() =>\n            screen.getByText(\"1, idle/FETCH 1, loading/undefined\"),\n          );\n\n          dfd2.resolve(\"FETCH 2\");\n          await waitFor(() =>\n            screen.getByText(\"0, idle/FETCH 1, idle/FETCH 2\"),\n          );\n        });\n\n        it(\"allows users to specify their own key to share fetchers\", async () => {\n          let dfd1 = createDeferred();\n          let dfd2 = createDeferred();\n          let router = createTestRouter(\n            [\n              {\n                path: \"/\",\n                Component() {\n                  let fetcher1 = useFetcher({ key: \"shared\" });\n                  let fetcher2 = useFetcher({ key: \"shared\" });\n                  let fetchers = useFetchers();\n                  return (\n                    <>\n                      <button onClick={() => fetcher1.load(\"/fetch1\")}>\n                        Load 1\n                      </button>\n                      <button onClick={() => fetcher2.load(\"/fetch2\")}>\n                        Load 2\n                      </button>\n                      <pre>{`${fetchers.length}, ${fetcher1.state}/${fetcher1.data}, ${fetcher2.state}/${fetcher2.data}`}</pre>\n                    </>\n                  );\n                },\n              },\n              {\n                path: \"/fetch1\",\n                loader: () => dfd1.promise,\n              },\n              {\n                path: \"/fetch2\",\n                loader: () => dfd2.promise,\n              },\n            ],\n            { window: getWindow(\"/\") },\n          );\n          let { container } = render(<RouterProvider router={router} />);\n\n          expect(container.querySelector(\"pre\")!.innerHTML).toBe(\n            \"0, idle/undefined, idle/undefined\",\n          );\n\n          fireEvent.click(screen.getByText(\"Load 1\"));\n          await waitFor(() =>\n            screen.getByText(\"1, loading/undefined, loading/undefined\"),\n          );\n\n          dfd1.resolve(\"FETCH 1\");\n          await waitFor(() =>\n            screen.getByText(\"0, idle/FETCH 1, idle/FETCH 1\"),\n          );\n\n          fireEvent.click(screen.getByText(\"Load 2\"));\n          await waitFor(() =>\n            screen.getByText(\"1, loading/FETCH 1, loading/FETCH 1\"),\n          );\n\n          dfd2.resolve(\"FETCH 2\");\n          await waitFor(() =>\n            screen.getByText(\"0, idle/FETCH 2, idle/FETCH 2\"),\n          );\n        });\n\n        it(\"updates the key if it changes while the fetcher remains mounted\", async () => {\n          let router = createTestRouter(\n            [\n              {\n                path: \"/\",\n                Component() {\n                  let fetchers = useFetchers();\n                  let [fetcherKey, setFetcherKey] = React.useState(\"a\");\n                  return (\n                    <>\n                      <ReusedFetcher fetcherKey={fetcherKey} />\n                      <button onClick={() => setFetcherKey(\"b\")}>\n                        Change Key\n                      </button>\n                      <p>Fetchers:</p>\n                      <pre>{JSON.stringify(fetchers)}</pre>\n                    </>\n                  );\n                },\n              },\n              {\n                path: \"/echo\",\n                loader: ({ request }) => request.url,\n              },\n            ],\n            { window: getWindow(\"/\") },\n          );\n\n          function ReusedFetcher({ fetcherKey }: { fetcherKey: string }) {\n            let fetcher = useFetcher({ key: fetcherKey });\n\n            return (\n              <>\n                <button\n                  onClick={() => fetcher.load(`/echo?fetcherKey=${fetcherKey}`)}\n                >\n                  Load Fetcher\n                </button>\n                <p>{`fetcherKey:${fetcherKey}`}</p>\n                <p>Fetcher:{JSON.stringify(fetcher)}</p>\n              </>\n            );\n          }\n\n          let { container } = render(<RouterProvider router={router} />);\n\n          // Start with idle fetcher 'a'\n          expect(getHtml(container)).toMatchInlineSnapshot(`\n            \"<div>\n              <button>\n                Load Fetcher\n              </button>\n              <p>\n                fetcherKey:a\n              </p>\n              <p>\n                Fetcher:\n                {\"Form\":{},\"state\":\"idle\"}\n              </p>\n              <button>\n                Change Key\n              </button>\n              <p>\n                Fetchers:\n              </p>\n              <pre>\n                []\n              </pre>\n            </div>\"\n          `);\n\n          fireEvent.click(screen.getByText(\"Load Fetcher\"));\n          await waitFor(\n            () => screen.getAllByText(/\\/echo\\?fetcherKey=a/).length > 0,\n          );\n\n          // Fetcher 'a' now has data\n          expect(getHtml(container)).toMatchInlineSnapshot(`\n            \"<div>\n              <button>\n                Load Fetcher\n              </button>\n              <p>\n                fetcherKey:a\n              </p>\n              <p>\n                Fetcher:\n                {\"Form\":{},\"state\":\"idle\",\"data\":\"http://localhost/echo?fetcherKey=a\"}\n              </p>\n              <button>\n                Change Key\n              </button>\n              <p>\n                Fetchers:\n              </p>\n              <pre>\n                []\n              </pre>\n            </div>\"\n          `);\n\n          fireEvent.click(screen.getByText(\"Change Key\"));\n          await waitFor(() => screen.getByText(\"fetcherKey:b\"));\n\n          // We should have a new uninitialized/idle fetcher 'b'\n          expect(getHtml(container)).toMatchInlineSnapshot(`\n            \"<div>\n              <button>\n                Load Fetcher\n              </button>\n              <p>\n                fetcherKey:b\n              </p>\n              <p>\n                Fetcher:\n                {\"Form\":{},\"state\":\"idle\"}\n              </p>\n              <button>\n                Change Key\n              </button>\n              <p>\n                Fetchers:\n              </p>\n              <pre>\n                []\n              </pre>\n            </div>\"\n          `);\n        });\n\n        it(\"exposes fetcher keys via useFetchers\", async () => {\n          let dfd = createDeferred();\n          let router = createTestRouter(\n            [\n              {\n                path: \"/\",\n                Component() {\n                  let fetcher1 = useFetcher();\n                  let fetcher2 = useFetcher({ key: \"my-key\" });\n                  let fetchers = useFetchers();\n                  return (\n                    <>\n                      <pre>{fetchers.map((f) => f.key).join(\",\")}</pre>\n                      <button\n                        onClick={() => {\n                          fetcher1.load(\"/fetch\");\n                          fetcher2.load(\"/fetch\");\n                        }}\n                      >\n                        Load fetchers\n                      </button>\n                    </>\n                  );\n                },\n              },\n              {\n                path: \"/fetch\",\n                loader: () => dfd.promise,\n              },\n            ],\n            { window: getWindow(\"/\") },\n          );\n          let { container } = render(<RouterProvider router={router} />);\n\n          expect(container.querySelector(\"pre\")?.innerHTML).toBe(\"\");\n          fireEvent.click(screen.getByText(\"Load fetchers\"));\n          await waitFor(() =>\n            // React `useId()` results in something such as `_r_2k_` or `_r_u_`\n            // depending on `DataBrowserRouter`/`DataHashRouter`\n            expect(container.querySelector(\"pre\")?.innerHTML).toMatch(\n              /^_r_[0-9]?[a-z]_,my-key$/,\n            ),\n          );\n        });\n\n        it(\"cleans up keyed fetcher data on unmount\", async () => {\n          let count = 0;\n          let router = createTestRouter(\n            [\n              {\n                path: \"/\",\n                loader() {\n                  return ++count;\n                },\n                Component() {\n                  let [shown, setShown] = React.useState(false);\n                  return (\n                    <div>\n                      <button onClick={() => setShown(!shown)}>\n                        {shown ? \"Unmount\" : \"Mount\"}\n                      </button>\n                      {shown ? <FetcherComponent /> : null}\n                    </div>\n                  );\n                },\n                ErrorBoundary() {\n                  let error = useRouteError();\n                  return <pre>{JSON.stringify(error)}</pre>;\n                },\n              },\n            ],\n            {\n              window: getWindow(\"/\"),\n            },\n          );\n\n          render(<RouterProvider router={router} />);\n\n          function FetcherComponent() {\n            let fetcher = useFetcher({ key: \"shared\" });\n            return (\n              <div>\n                <p>{`Fetcher state:${fetcher.state}`}</p>\n                {fetcher.data != null ? (\n                  <p data-testid=\"value\">{fetcher.data}</p>\n                ) : null}\n                <button onClick={() => fetcher.load(\".\")}>Fetch</button>\n              </div>\n            );\n          }\n\n          await waitFor(() => screen.getByText(\"Mount\"));\n\n          fireEvent.click(screen.getByText(\"Mount\"));\n          await waitFor(() => screen.getByText(\"Fetcher state:idle\"));\n\n          fireEvent.click(screen.getByText(\"Fetch\"));\n          await waitFor(() => screen.getByTestId(\"value\"));\n          let value = screen.getByTestId(\"value\").innerHTML;\n\n          fireEvent.click(screen.getByText(\"Unmount\"));\n          await waitFor(() => screen.getByText(\"Mount\"));\n\n          fireEvent.click(screen.getByText(\"Mount\"));\n          await waitFor(() => screen.getByText(\"Fetcher state:idle\"));\n          expect(screen.queryByTestId(\"value\")).toBe(null);\n\n          fireEvent.click(screen.getByText(\"Fetch\"));\n          await waitFor(() => screen.getByTestId(\"value\"));\n          let value2 = screen.getByTestId(\"value\").innerHTML;\n          expect(value2).not.toBe(value);\n        });\n      });\n\n      describe(\"fetcher persistence\", () => {\n        it(\"loading fetchers persist until completion\", async () => {\n          let dfd = createDeferred();\n          let router = createTestRouter(\n            [\n              {\n                path: \"/\",\n                Component() {\n                  let fetchers = useFetchers();\n                  return (\n                    <>\n                      <pre>{`Num fetchers: ${fetchers.length}`}</pre>\n                      <Link to=\"/page\">Go to /page</Link>\n                      <Outlet />\n                    </>\n                  );\n                },\n                children: [\n                  {\n                    index: true,\n                    Component() {\n                      let fetcher = useFetcher();\n                      return (\n                        <button onClick={() => fetcher.load(\"/fetch\")}>\n                          {`Load (${fetcher.state})`}\n                        </button>\n                      );\n                    },\n                  },\n                  {\n                    path: \"page\",\n                    Component() {\n                      return <h1>Page</h1>;\n                    },\n                  },\n                ],\n              },\n              {\n                path: \"/fetch\",\n                loader: () => dfd.promise,\n              },\n            ],\n            { window: getWindow(\"/\") },\n          );\n          let { container } = render(<RouterProvider router={router} />);\n\n          expect(getHtml(container)).toMatch(\"Num fetchers: 0\");\n\n          fireEvent.click(screen.getByText(\"Load (idle)\"));\n          expect(getHtml(container)).toMatch(\"Num fetchers: 1\");\n          expect(getHtml(container)).toMatch(\"Load (loading)\");\n\n          fireEvent.click(screen.getByText(\"Go to /page\"));\n          await waitFor(() => screen.getByText(\"Page\"));\n          expect(getHtml(container)).toMatch(\"Num fetchers: 1\");\n          expect(getHtml(container)).toMatch(\"Page\");\n\n          // Resolve after the navigation - no-op\n          dfd.resolve(\"FETCH\");\n          await waitFor(() => screen.getByText(\"Num fetchers: 0\"));\n          expect(getHtml(container)).toMatch(\"Page\");\n        });\n\n        it(\"submitting fetchers persist until completion\", async () => {\n          let dfd = createDeferred();\n          let router = createTestRouter(\n            [\n              {\n                path: \"/\",\n                Component() {\n                  let fetchers = useFetchers();\n                  return (\n                    <>\n                      <pre>{`Num fetchers: ${fetchers.length}`}</pre>\n                      <Link to=\"/page\">Go to /page</Link>\n                      <Outlet />\n                    </>\n                  );\n                },\n                children: [\n                  {\n                    index: true,\n                    Component() {\n                      let fetcher = useFetcher();\n                      return (\n                        <button\n                          onClick={() =>\n                            fetcher.submit(\n                              {},\n                              { method: \"post\", action: \"/fetch\" },\n                            )\n                          }\n                        >\n                          {`Submit (${fetcher.state})`}\n                        </button>\n                      );\n                    },\n                  },\n                  {\n                    path: \"page\",\n                    Component() {\n                      return <h1>Page</h1>;\n                    },\n                  },\n                ],\n              },\n              {\n                path: \"/fetch\",\n                action: () => dfd.promise,\n              },\n            ],\n            { window: getWindow(\"/\") },\n          );\n          let { container } = render(<RouterProvider router={router} />);\n\n          expect(getHtml(container)).toMatch(\"Num fetchers: 0\");\n\n          fireEvent.click(screen.getByText(\"Submit (idle)\"));\n          expect(getHtml(container)).toMatch(\"Num fetchers: 1\");\n          expect(getHtml(container)).toMatch(\"Submit (submitting)\");\n\n          fireEvent.click(screen.getByText(\"Go to /page\"));\n          await waitFor(() => screen.getByText(\"Page\"));\n          expect(getHtml(container)).toMatch(\"Num fetchers: 1\");\n\n          // Resolve after the navigation - trigger cleanup\n          dfd.resolve(\"FETCH\");\n          await waitFor(() => screen.getByText(\"Num fetchers: 0\"));\n        });\n\n        it(\"submitting fetchers w/revalidations are cleaned up on completion\", async () => {\n          let count = 0;\n          let dfd = createDeferred();\n          let router = createTestRouter(\n            [\n              {\n                path: \"/\",\n                Component() {\n                  let fetchers = useFetchers();\n                  return (\n                    <>\n                      <pre>{`Num fetchers: ${fetchers.length}`}</pre>\n                      <Link to=\"/page\">Go to /page</Link>\n                      <Outlet />\n                    </>\n                  );\n                },\n                children: [\n                  {\n                    index: true,\n                    Component() {\n                      let fetcher = useFetcher();\n                      return (\n                        <button\n                          onClick={() =>\n                            fetcher.submit(\n                              {},\n                              { method: \"post\", action: \"/fetch\" },\n                            )\n                          }\n                        >\n                          {`Submit (${fetcher.state})`}\n                        </button>\n                      );\n                    },\n                  },\n                  {\n                    path: \"page\",\n                    Component() {\n                      let data = useLoaderData() as { count: number };\n                      return <h1>{`Page (${data.count})`}</h1>;\n                    },\n                    async loader() {\n                      await new Promise((r) => setTimeout(r, 10));\n                      return { count: ++count };\n                    },\n                  },\n                ],\n              },\n              {\n                path: \"/fetch\",\n                action: () => dfd.promise,\n              },\n            ],\n            { window: getWindow(\"/\") },\n          );\n          let { container } = render(<RouterProvider router={router} />);\n\n          expect(getHtml(container)).toMatch(\"Num fetchers: 0\");\n\n          fireEvent.click(screen.getByText(\"Submit (idle)\"));\n          expect(getHtml(container)).toMatch(\"Num fetchers: 1\");\n          expect(getHtml(container)).toMatch(\"Submit (submitting)\");\n\n          fireEvent.click(screen.getByText(\"Go to /page\"));\n          await waitFor(() => screen.getByText(\"Page (1)\"));\n          expect(getHtml(container)).toMatch(\"Num fetchers: 1\");\n\n          // Resolve action after the navigation and trigger revalidation\n          dfd.resolve(\"FETCH\");\n          await waitFor(() => screen.getByText(\"Num fetchers: 0\"));\n          expect(getHtml(container)).toMatch(\"Page (2)\");\n        });\n\n        it(\"submitting fetchers w/revalidations are cleaned up on completion (remounted)\", async () => {\n          let count = 0;\n          let dfd = createDeferred();\n          let router = createTestRouter(\n            [\n              {\n                path: \"/\",\n                Component() {\n                  let fetchers = useFetchers();\n                  return (\n                    <>\n                      <pre>{`Num fetchers: ${fetchers.length}`}</pre>\n                      <Link to=\"/page\">Go to /page</Link>\n                      <Outlet />\n                    </>\n                  );\n                },\n                children: [\n                  {\n                    index: true,\n                    Component() {\n                      let fetcher = useFetcher({ key: \"me\" });\n                      return (\n                        <button\n                          onClick={() =>\n                            fetcher.submit(\n                              {},\n                              { method: \"post\", action: \"/fetch\" },\n                            )\n                          }\n                        >\n                          {`Submit (${fetcher.state})`}\n                        </button>\n                      );\n                    },\n                  },\n                  {\n                    path: \"page\",\n                    Component() {\n                      let fetcher = useFetcher({ key: \"me\" });\n                      let data = useLoaderData() as { count: number };\n                      return (\n                        <>\n                          <h1>{`Page (${data.count})`}</h1>\n                          <p>{fetcher.data}</p>\n                        </>\n                      );\n                    },\n                    async loader() {\n                      await new Promise((r) => setTimeout(r, 10));\n                      return { count: ++count };\n                    },\n                  },\n                ],\n              },\n              {\n                path: \"/fetch\",\n                action: () => dfd.promise,\n              },\n            ],\n            { window: getWindow(\"/\") },\n          );\n          let { container } = render(<RouterProvider router={router} />);\n\n          expect(getHtml(container)).toMatch(\"Num fetchers: 0\");\n\n          fireEvent.click(screen.getByText(\"Submit (idle)\"));\n          expect(getHtml(container)).toMatch(\"Num fetchers: 1\");\n          expect(getHtml(container)).toMatch(\"Submit (submitting)\");\n\n          fireEvent.click(screen.getByText(\"Go to /page\"));\n          await waitFor(() => screen.getByText(\"Page (1)\"));\n          expect(getHtml(container)).toMatch(\"Num fetchers: 1\");\n\n          // Resolve after the navigation and revalidation\n          dfd.resolve(\"FETCH\");\n          await waitFor(() => screen.getByText(\"Num fetchers: 0\"));\n          expect(getHtml(container)).toMatch(\"Page (2)\");\n          expect(getHtml(container)).toMatch(\"FETCH\");\n        });\n\n        it(\"submitting fetchers w/redirects are cleaned up on completion\", async () => {\n          let dfd = createDeferred();\n          let router = createTestRouter(\n            [\n              {\n                path: \"/\",\n                Component() {\n                  let fetchers = useFetchers();\n                  return (\n                    <>\n                      <pre>{`Num fetchers: ${fetchers.length}`}</pre>\n                      <Link to=\"/page\">Go to /page</Link>\n                      <Outlet />\n                    </>\n                  );\n                },\n                children: [\n                  {\n                    index: true,\n                    Component() {\n                      let fetcher = useFetcher();\n                      return (\n                        <button\n                          onClick={() =>\n                            fetcher.submit(\n                              {},\n                              { method: \"post\", action: \"/fetch\" },\n                            )\n                          }\n                        >\n                          {`Submit (${fetcher.state})`}\n                        </button>\n                      );\n                    },\n                  },\n                  {\n                    path: \"page\",\n                    Component() {\n                      return <h1>Page</h1>;\n                    },\n                  },\n                  {\n                    path: \"redirect\",\n                    Component() {\n                      return <h1>Redirect</h1>;\n                    },\n                  },\n                ],\n              },\n              {\n                path: \"/fetch\",\n                action: () => dfd.promise,\n              },\n            ],\n            { window: getWindow(\"/\") },\n          );\n          let { container } = render(<RouterProvider router={router} />);\n\n          expect(getHtml(container)).toMatch(\"Num fetchers: 0\");\n\n          fireEvent.click(screen.getByText(\"Submit (idle)\"));\n          expect(getHtml(container)).toMatch(\"Num fetchers: 1\");\n          expect(getHtml(container)).toMatch(\"Submit (submitting)\");\n\n          fireEvent.click(screen.getByText(\"Go to /page\"));\n          await waitFor(() => screen.getByText(\"Page\"));\n          expect(getHtml(container)).toMatch(\"Num fetchers: 1\");\n\n          // Resolve after the navigation - trigger cleanup\n          // We don't process the redirect here since it was superseded by a\n          // navigation, but we assert that it gets cleaned up afterwards\n          dfd.resolve(redirect(\"/redirect\"));\n          await waitFor(() => screen.getByText(\"Num fetchers: 0\"));\n          expect(getHtml(container)).toMatch(\"Page\");\n        });\n\n        it(\"submitting fetcher.Form persist until completion\", async () => {\n          let dfd = createDeferred();\n          let router = createTestRouter(\n            [\n              {\n                path: \"/\",\n                Component() {\n                  let fetchers = useFetchers();\n                  return (\n                    <>\n                      <pre>{`Num fetchers: ${fetchers.length}`}</pre>\n                      <Link to=\"/page\">Go to /page</Link>\n                      <Outlet />\n                    </>\n                  );\n                },\n                children: [\n                  {\n                    index: true,\n                    Component() {\n                      let fetcher = useFetcher();\n                      return (\n                        <fetcher.Form method=\"post\" action=\"/fetch\">\n                          <button type=\"submit\">\n                            {`Submit (${fetcher.state})`}\n                          </button>\n                        </fetcher.Form>\n                      );\n                    },\n                  },\n                  {\n                    path: \"page\",\n                    Component() {\n                      return <h1>Page</h1>;\n                    },\n                  },\n                ],\n              },\n              {\n                path: \"/fetch\",\n                action: () => dfd.promise,\n              },\n            ],\n            { window: getWindow(\"/\") },\n          );\n          let { container } = render(<RouterProvider router={router} />);\n\n          expect(getHtml(container)).toMatch(\"Num fetchers: 0\");\n\n          fireEvent.click(screen.getByText(\"Submit (idle)\"));\n          expect(getHtml(container)).toMatch(\"Num fetchers: 1\");\n          expect(getHtml(container)).toMatch(\"Submit (submitting)\");\n\n          fireEvent.click(screen.getByText(\"Go to /page\"));\n          await waitFor(() => screen.getByText(\"Page\"));\n          expect(getHtml(container)).toMatch(\"Num fetchers: 1\");\n\n          // Resolve after the navigation and revalidation\n          dfd.resolve(\"FETCH\");\n          await waitFor(() => screen.getByText(\"Num fetchers: 0\"));\n        });\n\n        it(\"unmounted fetcher.load errors should not bubble up to the UI\", async () => {\n          let dfd = createDeferred();\n          let router = createTestRouter(\n            [\n              {\n                path: \"/\",\n                Component() {\n                  let fetchers = useFetchers();\n                  return (\n                    <>\n                      <pre>{`Num fetchers: ${fetchers.length}`}</pre>\n                      <Link to=\"/page\">Go to /page</Link>\n                      <Outlet />\n                    </>\n                  );\n                },\n                children: [\n                  {\n                    index: true,\n                    Component() {\n                      let fetcher = useFetcher();\n                      return (\n                        <button onClick={() => fetcher.load(\"/fetch\")}>\n                          {`Load (${fetcher.state})`}\n                        </button>\n                      );\n                    },\n                  },\n                  {\n                    path: \"page\",\n                    Component() {\n                      return <h1>Page</h1>;\n                    },\n                  },\n                ],\n              },\n              {\n                path: \"/fetch\",\n                loader: () => dfd.promise,\n              },\n            ],\n            { window: getWindow(\"/\") },\n          );\n          let { container } = render(<RouterProvider router={router} />);\n\n          expect(getHtml(container)).toMatch(\"Num fetchers: 0\");\n\n          fireEvent.click(screen.getByText(\"Load (idle)\"));\n          expect(getHtml(container)).toMatch(\"Num fetchers: 1\");\n          expect(getHtml(container)).toMatch(\"Load (loading)\");\n\n          fireEvent.click(screen.getByText(\"Go to /page\"));\n          await waitFor(() => screen.getByText(\"Page\"));\n          expect(getHtml(container)).toMatch(\"Num fetchers: 1\");\n          expect(getHtml(container)).toMatch(\"Page\");\n\n          // Reject after the navigation - no-op because the fetcher is no longer mounted\n          dfd.reject(new Error(\"FETCH ERROR\"));\n          await waitFor(() => screen.getByText(\"Num fetchers: 0\"));\n          expect(getHtml(container)).toMatch(\"Page\");\n          expect(getHtml(container)).not.toMatch(\n            \"Unexpected Application Error!\",\n          );\n          expect(getHtml(container)).not.toMatch(\"FETCH ERROR\");\n        });\n\n        it(\"unmounted/remounted fetcher.load errors should bubble up to the UI\", async () => {\n          let dfd = createDeferred();\n          let router = createTestRouter(\n            [\n              {\n                path: \"/\",\n                Component() {\n                  let fetchers = useFetchers();\n                  return (\n                    <>\n                      <pre>{`Num fetchers: ${fetchers.length}`}</pre>\n                      <Link to=\"/page\">Go to /page</Link>\n                      <Outlet />\n                    </>\n                  );\n                },\n                children: [\n                  {\n                    index: true,\n                    Component() {\n                      let fetcher = useFetcher({ key: \"me\" });\n                      return (\n                        <>\n                          <h1>Index</h1>\n                          <button onClick={() => fetcher.load(\"/fetch\")}>\n                            {`Load (${fetcher.state})`}\n                          </button>\n                        </>\n                      );\n                    },\n                  },\n                  {\n                    path: \"page\",\n                    Component() {\n                      let fetcher = useFetcher({ key: \"me\" });\n                      return (\n                        <>\n                          <h1>Page</h1>\n                          <pre>{fetcher.data}</pre>\n                        </>\n                      );\n                    },\n                  },\n                ],\n              },\n              {\n                path: \"/fetch\",\n                loader: () => dfd.promise,\n              },\n            ],\n            { window: getWindow(\"/\") },\n          );\n          let { container } = render(<RouterProvider router={router} />);\n\n          await waitFor(() => screen.getByText(\"Index\"));\n          expect(getHtml(container)).toMatch(\"Num fetchers: 0\");\n\n          fireEvent.click(screen.getByText(\"Load (idle)\"));\n          expect(getHtml(container)).toMatch(\"Num fetchers: 1\");\n          expect(getHtml(container)).toMatch(\"Load (loading)\");\n\n          fireEvent.click(screen.getByText(\"Go to /page\"));\n          await waitFor(() => screen.getByText(\"Page\"));\n          expect(getHtml(container)).toMatch(\"Num fetchers: 1\");\n          expect(getHtml(container)).toMatch(\"Page\");\n\n          // Reject after the navigation - should trigger the error boundary\n          // because the fetcher is still mounted in the new location\n          dfd.reject(new Error(\"FETCH ERROR\"));\n          await waitFor(() => screen.getByText(\"FETCH ERROR\"));\n          expect(getHtml(container)).toMatch(\"Unexpected Application Error!\");\n        });\n\n        it(\"unmounted fetcher.submit errors should not bubble up to the UI\", async () => {\n          let dfd = createDeferred();\n          let router = createTestRouter(\n            [\n              {\n                path: \"/\",\n                Component() {\n                  let fetchers = useFetchers();\n                  return (\n                    <>\n                      <pre>{`Num fetchers: ${fetchers.length}`}</pre>\n                      <Link to=\"/page\">Go to /page</Link>\n                      <Outlet />\n                    </>\n                  );\n                },\n                children: [\n                  {\n                    index: true,\n                    Component() {\n                      let fetcher = useFetcher();\n                      return (\n                        <button\n                          onClick={() =>\n                            fetcher.submit(\n                              {},\n                              { method: \"post\", action: \"/fetch\" },\n                            )\n                          }\n                        >\n                          {`Submit (${fetcher.state})`}\n                        </button>\n                      );\n                    },\n                  },\n                  {\n                    path: \"page\",\n                    Component() {\n                      return <h1>Page</h1>;\n                    },\n                  },\n                ],\n              },\n              {\n                path: \"/fetch\",\n                action: () => dfd.promise,\n              },\n            ],\n            { window: getWindow(\"/\") },\n          );\n          let { container } = render(<RouterProvider router={router} />);\n\n          expect(getHtml(container)).toMatch(\"Num fetchers: 0\");\n\n          fireEvent.click(screen.getByText(\"Submit (idle)\"));\n          expect(getHtml(container)).toMatch(\"Num fetchers: 1\");\n          expect(getHtml(container)).toMatch(\"Submit (submitting)\");\n\n          fireEvent.click(screen.getByText(\"Go to /page\"));\n          await waitFor(() => screen.getByText(\"Page\"));\n          expect(getHtml(container)).toMatch(\"Num fetchers: 1\");\n          expect(getHtml(container)).toMatch(\"Page\");\n\n          // Reject after the navigation - no-op because the fetcher is no longer mounted\n          dfd.reject(new Error(\"FETCH ERROR\"));\n          await waitFor(() => screen.getByText(\"Num fetchers: 0\"));\n          expect(getHtml(container)).toMatch(\"Page\");\n          expect(getHtml(container)).not.toMatch(\n            \"Unexpected Application Error!\",\n          );\n          expect(getHtml(container)).not.toMatch(\"FETCH ERROR\");\n        });\n\n        it(\"unmounted/remounted fetcher.submit errors should bubble up to the UI\", async () => {\n          let dfd = createDeferred();\n          let router = createTestRouter(\n            [\n              {\n                path: \"/\",\n                Component() {\n                  let fetchers = useFetchers();\n                  return (\n                    <>\n                      <pre>{`Num fetchers: ${fetchers.length}`}</pre>\n                      <Link to=\"/page\">Go to /page</Link>\n                      <Outlet />\n                    </>\n                  );\n                },\n                children: [\n                  {\n                    index: true,\n                    Component() {\n                      let fetcher = useFetcher({ key: \"me\" });\n                      return (\n                        <>\n                          <h1>Index</h1>\n                          <button\n                            onClick={() =>\n                              fetcher.submit(\n                                {},\n                                { method: \"post\", action: \"/fetch\" },\n                              )\n                            }\n                          >\n                            {`Submit (${fetcher.state})`}\n                          </button>\n                        </>\n                      );\n                    },\n                  },\n                  {\n                    path: \"page\",\n                    Component() {\n                      let fetcher = useFetcher({ key: \"me\" });\n                      return (\n                        <>\n                          <h1>Page</h1>\n                          <pre>{fetcher.data}</pre>\n                        </>\n                      );\n                    },\n                  },\n                ],\n              },\n              {\n                path: \"/fetch\",\n                action: () => dfd.promise,\n              },\n            ],\n            { window: getWindow(\"/\") },\n          );\n          let { container } = render(<RouterProvider router={router} />);\n\n          await waitFor(() => screen.getByText(\"Index\"));\n          expect(getHtml(container)).toMatch(\"Num fetchers: 0\");\n\n          fireEvent.click(screen.getByText(\"Submit (idle)\"));\n          expect(getHtml(container)).toMatch(\"Num fetchers: 1\");\n          expect(getHtml(container)).toMatch(\"Submit (submitting)\");\n\n          fireEvent.click(screen.getByText(\"Go to /page\"));\n          await waitFor(() => screen.getByText(\"Page\"));\n          expect(getHtml(container)).toMatch(\"Num fetchers: 1\");\n          expect(getHtml(container)).toMatch(\"Page\");\n\n          // Reject after the navigation - should trigger the error boundary\n          // because the fetcher is still mounted in the new location\n          dfd.reject(new Error(\"FETCH ERROR\"));\n          await waitFor(() => screen.getByText(\"FETCH ERROR\"));\n          expect(getHtml(container)).toMatch(\"Unexpected Application Error!\");\n        });\n\n        it(\"unmounted fetchers should not revalidate\", async () => {\n          let count = 0;\n          let loaderDfd = createDeferred();\n          let actionDfd = createDeferred();\n          let router = createTestRouter(\n            [\n              {\n                path: \"/\",\n                action: () => actionDfd.promise,\n                Component() {\n                  let [showFetcher, setShowFetcher] = React.useState(true);\n                  let [fetcherData, setFetcherData] = React.useState(null);\n                  let fetchers = useFetchers();\n                  let actionData = useActionData();\n                  let navigation = useNavigation();\n\n                  return (\n                    <>\n                      <Form method=\"post\">\n                        <button type=\"submit\">Submit Form</button>\n                        <p>{`Navigation State: ${navigation.state}`}</p>\n                        <p>{`Action Data: ${actionData}`}</p>\n                        <p>{`Active Fetchers: ${fetchers.length}`}</p>\n                      </Form>\n                      {showFetcher ? (\n                        <FetcherComponent\n                          onClose={(data) => {\n                            setFetcherData(data);\n                            setShowFetcher(false);\n                          }}\n                        />\n                      ) : (\n                        <p>{fetcherData}</p>\n                      )}\n                    </>\n                  );\n                },\n              },\n              {\n                path: \"/fetch\",\n                async loader() {\n                  count++;\n                  if (count === 1) return await loaderDfd.promise;\n                  throw new Error(\"Fetcher load called too many times\");\n                },\n              },\n            ],\n            { window: getWindow(\"/\") },\n          );\n\n          function FetcherComponent({\n            onClose,\n          }: {\n            onClose: (data: any) => void;\n          }) {\n            let fetcher = useFetcher();\n\n            React.useEffect(() => {\n              if (fetcher.state === \"idle\" && fetcher.data) {\n                onClose(fetcher.data);\n              }\n            }, [fetcher, onClose]);\n\n            return (\n              <>\n                <button onClick={() => fetcher.load(\"/fetch\")}>\n                  Load Fetcher\n                </button>\n                <pre>{`Fetcher State: ${fetcher.state}`}</pre>\n              </>\n            );\n          }\n\n          render(<RouterProvider router={router} />);\n\n          fireEvent.click(screen.getByText(\"Load Fetcher\"));\n          await waitFor(\n            () =>\n              screen.getByText(\"Active Fetchers: 1\") &&\n              screen.getByText(\"Fetcher State: loading\"),\n          );\n\n          loaderDfd.resolve(\"FETCHER DATA\");\n          await waitFor(\n            () =>\n              screen.getByText(\"FETCHER DATA\") &&\n              screen.getByText(\"Active Fetchers: 0\"),\n          );\n\n          fireEvent.click(screen.getByText(\"Submit Form\"));\n          await waitFor(() => screen.getByText(\"Navigation State: submitting\"));\n\n          actionDfd.resolve(\"ACTION\");\n          await waitFor(\n            () =>\n              screen.getByText(\"Navigation State: idle\") &&\n              screen.getByText(\"Active Fetchers: 0\") &&\n              screen.getByText(\"Action Data: ACTION\"),\n          );\n\n          expect(count).toBe(1);\n        });\n      });\n\n      describe(\"<Form navigate={false}>\", () => {\n        function setupTest(\n          method: \"get\" | \"post\",\n          navigate: boolean,\n          renderFetcher = false,\n        ) {\n          let loaderDefer = createDeferred();\n          let actionDefer = createDeferred();\n\n          let router = createTestRouter(\n            [\n              {\n                path: \"/\",\n                async action({ request }) {\n                  let resolvedValue = await actionDefer.promise;\n                  let formData = await request.formData();\n                  return `${resolvedValue}:${formData.get(\"test\")}`;\n                },\n                loader: () => loaderDefer.promise,\n                Component() {\n                  let data = useLoaderData() as string;\n                  let actionData = useActionData() as string | undefined;\n                  let location = useLocation();\n                  let navigation = useNavigation();\n                  let fetchers = useFetchers();\n                  return (\n                    <div>\n                      <Form\n                        method={method}\n                        navigate={navigate}\n                        fetcherKey={renderFetcher ? \"my-key\" : undefined}\n                      >\n                        <input name=\"test\" value=\"value\" />\n                        <button type=\"submit\">Submit Form</button>\n                      </Form>\n                      <pre>\n                        {[\n                          location.key,\n                          navigation.state,\n                          data,\n                          actionData,\n                          fetchers.map((f) => f.state),\n                        ].join(\",\")}\n                      </pre>\n                      <Outlet />\n                    </div>\n                  );\n                },\n                ...(renderFetcher\n                  ? {\n                      children: [\n                        {\n                          index: true,\n                          Component() {\n                            let fetcher = useFetcher({ key: \"my-key\" });\n                            return (\n                              <pre>{`fetcher:${fetcher.state}:${fetcher.data}`}</pre>\n                            );\n                          },\n                        },\n                      ],\n                    }\n                  : {}),\n              },\n            ],\n            {\n              window: getWindow(\"/\"),\n              hydrationData: { loaderData: { \"0\": \"INIT\" } },\n            },\n          );\n\n          let { container } = render(<RouterProvider router={router} />);\n\n          return { container, loaderDefer, actionDefer };\n        }\n\n        it('defaults to a navigation on <Form method=\"get\">', async () => {\n          let { container, loaderDefer } = setupTest(\"get\", true);\n\n          // location key, nav state, loader data, action data, fetcher states\n          expect(getHtml(container)).toMatch(\"default,idle,INIT,,\");\n\n          fireEvent.click(screen.getByText(\"Submit Form\"));\n          await waitFor(() => screen.getByText(\"default,loading,INIT,,\"));\n\n          loaderDefer.resolve(\"LOADER\");\n          await waitFor(() => screen.getByText(/idle,LOADER,/));\n          // Navigation changes the location key\n          expect(getHtml(container)).not.toMatch(\"default\");\n        });\n\n        it('defaults to a navigation on <Form method=\"post\">', async () => {\n          let { container, loaderDefer, actionDefer } = setupTest(\"post\", true);\n\n          // location key, nav state, loader data, action data, fetcher states\n          expect(getHtml(container)).toMatch(\"default,idle,INIT,,\");\n\n          fireEvent.click(screen.getByText(\"Submit Form\"));\n          await waitFor(() => screen.getByText(\"default,submitting,INIT,,\"));\n\n          actionDefer.resolve(\"ACTION\");\n          await waitFor(() =>\n            screen.getByText(\"default,loading,INIT,ACTION:value,\"),\n          );\n\n          loaderDefer.resolve(\"LOADER\");\n          await waitFor(() => screen.getByText(/idle,LOADER,ACTION:value/));\n          // Navigation changes the location key\n          expect(getHtml(container)).not.toMatch(\"default\");\n        });\n\n        it('uses a fetcher for <Form method=\"get\" navigate={false}>', async () => {\n          let { container, loaderDefer } = setupTest(\"get\", false);\n\n          // location.key,navigation.state\n          expect(getHtml(container)).toMatch(\"default,idle,INIT,,\");\n\n          fireEvent.click(screen.getByText(\"Submit Form\"));\n          // Fetcher does not trigger useNavigation\n          await waitFor(() => screen.getByText(\"default,idle,INIT,,loading\"));\n\n          loaderDefer.resolve(\"LOADER\");\n          // Fetcher does not change the location key.  Because no useFetcher()\n          // accessed this key, the fetcher/data doesn't stick around\n          await waitFor(() => screen.getByText(\"default,idle,INIT,,\"));\n        });\n\n        it('uses a fetcher for <Form method=\"post\" navigate={false}>', async () => {\n          let { container, loaderDefer, actionDefer } = setupTest(\n            \"post\",\n            false,\n          );\n\n          expect(getHtml(container)).toMatch(\"default,idle,INIT,\");\n\n          fireEvent.click(screen.getByText(\"Submit Form\"));\n          // Fetcher does not trigger useNavigation\n          await waitFor(() =>\n            screen.getByText(\"default,idle,INIT,,submitting\"),\n          );\n\n          actionDefer.resolve(\"ACTION\");\n          await waitFor(() => screen.getByText(\"default,idle,INIT,,loading\"));\n\n          loaderDefer.resolve(\"LOADER\");\n          // Fetcher does not change the location key.  Because no useFetcher()\n          // accessed this key, the fetcher/data doesn't stick around\n          await waitFor(() => screen.getByText(\"default,idle,LOADER,,\"));\n        });\n\n        it('uses a fetcher for <Form method=\"get\" navigate={false} fetcherKey>', async () => {\n          let { container, loaderDefer } = setupTest(\"get\", false, true);\n\n          expect(getHtml(container)).toMatch(\"default,idle,INIT,,\");\n\n          fireEvent.click(screen.getByText(\"Submit Form\"));\n          // Fetcher does not trigger useNavigation\n          await waitFor(() => screen.getByText(\"default,idle,INIT,,loading\"));\n          expect(getHtml(container)).toMatch(\"fetcher:loading:undefined\");\n\n          loaderDefer.resolve(\"LOADER\");\n          // Fetcher does not change the location key.  Because no useFetcher()\n          // accessed this key, the fetcher/data doesn't stick around\n          await waitFor(() => screen.getByText(\"default,idle,INIT,,\"));\n          expect(getHtml(container)).toMatch(\"fetcher:idle:LOADER\");\n        });\n\n        it('uses a fetcher for <Form method=\"post\" navigate={false} fetcherKey>', async () => {\n          let { container, loaderDefer, actionDefer } = setupTest(\n            \"post\",\n            false,\n            true,\n          );\n\n          expect(getHtml(container)).toMatch(\"default,idle,INIT,\");\n\n          fireEvent.click(screen.getByText(\"Submit Form\"));\n          // Fetcher does not trigger useNavigation\n          await waitFor(() =>\n            screen.getByText(\"default,idle,INIT,,submitting\"),\n          );\n\n          actionDefer.resolve(\"ACTION\");\n          await waitFor(() => screen.getByText(\"default,idle,INIT,,loading\"));\n          expect(getHtml(container)).toMatch(\"fetcher:loading:ACTION:value\");\n\n          loaderDefer.resolve(\"LOADER\");\n          // Fetcher does not change the location key.  Because no useFetcher()\n          // accessed this key, the fetcher/data doesn't stick around\n          await waitFor(() => screen.getByText(\"default,idle,LOADER,,\"));\n          expect(getHtml(container)).toMatch(\"fetcher:idle:ACTION:value\");\n        });\n      });\n\n      describe(\"with a basename\", () => {\n        it(\"prepends the basename to fetcher.load paths\", async () => {\n          let router = createTestRouter(\n            [\n              {\n                path: \"/\",\n                Component: Comp,\n                children: [{ path: \"fetch\", loader: () => \"FETCH\" }],\n              },\n            ],\n            {\n              basename: \"/base\",\n              window: getWindow(\"/base\"),\n            },\n          );\n          let { container } = render(<RouterProvider router={router} />);\n\n          function Comp() {\n            let fetcher = useFetcher();\n            return (\n              <>\n                <p>{`data:${fetcher.data}`}</p>\n                <button onClick={() => fetcher.load(\"/fetch\")}>load</button>\n              </>\n            );\n          }\n\n          expect(getHtml(container)).toMatchInlineSnapshot(`\n            \"<div>\n              <p>\n                data:undefined\n              </p>\n              <button>\n                load\n              </button>\n            </div>\"\n          `);\n\n          fireEvent.click(screen.getByText(\"load\"));\n          await waitFor(() => screen.getByText(/FETCH/));\n          expect(getHtml(container)).toMatchInlineSnapshot(`\n            \"<div>\n              <p>\n                data:FETCH\n              </p>\n              <button>\n                load\n              </button>\n            </div>\"\n          `);\n        });\n\n        it('prepends the basename to fetcher.submit({ method: \"get\" }) paths', async () => {\n          let router = createTestRouter(\n            [\n              {\n                path: \"/\",\n                Component: Comp,\n                children: [{ path: \"fetch\", loader: () => \"FETCH\" }],\n              },\n            ],\n            {\n              basename: \"/base\",\n              window: getWindow(\"/base\"),\n            },\n          );\n          let { container } = render(<RouterProvider router={router} />);\n\n          function Comp() {\n            let fetcher = useFetcher();\n            return (\n              <>\n                <p>{`data:${fetcher.data}`}</p>\n                <button\n                  onClick={() =>\n                    fetcher.submit({}, { method: \"get\", action: \"/fetch\" })\n                  }\n                >\n                  load\n                </button>\n              </>\n            );\n          }\n\n          expect(getHtml(container)).toMatchInlineSnapshot(`\n            \"<div>\n              <p>\n                data:undefined\n              </p>\n              <button>\n                load\n              </button>\n            </div>\"\n          `);\n\n          fireEvent.click(screen.getByText(\"load\"));\n          await waitFor(() => screen.getByText(/FETCH/));\n          expect(getHtml(container)).toMatchInlineSnapshot(`\n            \"<div>\n              <p>\n                data:FETCH\n              </p>\n              <button>\n                load\n              </button>\n            </div>\"\n          `);\n        });\n\n        it('prepends the basename to fetcher.submit({ method: \"post\" }) paths', async () => {\n          let router = createTestRouter(\n            [\n              {\n                path: \"/\",\n                Component: Comp,\n                children: [{ path: \"fetch\", action: () => \"FETCH\" }],\n              },\n            ],\n            {\n              basename: \"/base\",\n              window: getWindow(\"/base\"),\n            },\n          );\n          let { container } = render(<RouterProvider router={router} />);\n\n          function Comp() {\n            let fetcher = useFetcher();\n            return (\n              <>\n                <p>{`data:${fetcher.data}`}</p>\n                <button\n                  onClick={() =>\n                    fetcher.submit({}, { method: \"post\", action: \"/fetch\" })\n                  }\n                >\n                  submit\n                </button>\n              </>\n            );\n          }\n\n          expect(getHtml(container)).toMatchInlineSnapshot(`\n            \"<div>\n              <p>\n                data:undefined\n              </p>\n              <button>\n                submit\n              </button>\n            </div>\"\n          `);\n\n          fireEvent.click(screen.getByText(\"submit\"));\n          await waitFor(() => screen.getByText(/FETCH/));\n          expect(getHtml(container)).toMatchInlineSnapshot(`\n            \"<div>\n              <p>\n                data:FETCH\n              </p>\n              <button>\n                submit\n              </button>\n            </div>\"\n          `);\n        });\n        it(\"prepends the basename to fetcher.Form paths\", async () => {\n          let router = createTestRouter(\n            [\n              {\n                path: \"/\",\n                Component: Comp,\n                children: [{ path: \"fetch\", action: () => \"FETCH\" }],\n              },\n            ],\n            {\n              basename: \"/base\",\n              window: getWindow(\"/base\"),\n            },\n          );\n          let { container } = render(<RouterProvider router={router} />);\n\n          function Comp() {\n            let fetcher = useFetcher();\n            return (\n              <>\n                <p>{`data:${fetcher.data}`}</p>\n                <fetcher.Form method=\"post\" action=\"/fetch\">\n                  <button type=\"submit\">submit</button>\n                </fetcher.Form>\n              </>\n            );\n          }\n\n          expect(getHtml(container)).toMatchInlineSnapshot(`\n            \"<div>\n              <p>\n                data:undefined\n              </p>\n              <form\n                action=\"/base/fetch\"\n                data-discover=\"true\"\n                method=\"post\"\n              >\n                <button\n                  type=\"submit\"\n                >\n                  submit\n                </button>\n              </form>\n            </div>\"\n          `);\n\n          fireEvent.click(screen.getByText(\"submit\"));\n          await waitFor(() => screen.getByText(/FETCH/));\n          expect(getHtml(container)).toMatchInlineSnapshot(`\n            \"<div>\n              <p>\n                data:FETCH\n              </p>\n              <form\n                action=\"/base/fetch\"\n                data-discover=\"true\"\n                method=\"post\"\n              >\n                <button\n                  type=\"submit\"\n                >\n                  submit\n                </button>\n              </form>\n            </div>\"\n          `);\n        });\n      });\n    });\n\n    describe(\"errors\", () => {\n      it(\"deserializes ErrorResponse instances from the window\", async () => {\n        window.__staticRouterHydrationData = {\n          loaderData: {},\n          actionData: null,\n          errors: {\n            \"0\": {\n              status: 404,\n              statusText: \"Not Found\",\n              internal: false,\n              data: { not: \"found\" },\n              __type: \"RouteErrorResponse\",\n            },\n          },\n        };\n        let router = createTestRouter([\n          {\n            path: \"/\",\n            Component: () => <h1>Nope</h1>,\n            ErrorBoundary: () => <Boundary />,\n          },\n        ]);\n        let { container } = render(<RouterProvider router={router} />);\n\n        function Boundary() {\n          let error = useRouteError() as unknown;\n          return isRouteErrorResponse(error) ? (\n            <pre>{JSON.stringify(error)}</pre>\n          ) : (\n            <p>No :(</p>\n          );\n        }\n\n        expect(getHtml(container)).toMatchInlineSnapshot(`\n          \"<div>\n            <pre>\n              {\"status\":404,\"statusText\":\"Not Found\",\"internal\":false,\"data\":{\"not\":\"found\"}}\n            </pre>\n          </div>\"\n        `);\n      });\n\n      it(\"deserializes Error instances from the window\", async () => {\n        window.__staticRouterHydrationData = {\n          loaderData: {},\n          actionData: null,\n          errors: {\n            \"0\": {\n              message: \"error message\",\n              __type: \"Error\",\n            },\n          },\n        };\n        let router = createTestRouter([\n          {\n            path: \"/\",\n            Component: () => <h1>Nope</h1>,\n            ErrorBoundary: () => <Boundary />,\n          },\n        ]);\n        let { container } = render(<RouterProvider router={router} />);\n\n        function Boundary() {\n          let error = useRouteError() as Error;\n          return error instanceof Error ? (\n            <>\n              <pre>{error.toString()}</pre>\n              <pre>stack:{error.stack}</pre>\n            </>\n          ) : (\n            <p>No :(</p>\n          );\n        }\n\n        expect(getHtml(container)).toMatchInlineSnapshot(`\n          \"<div>\n            <pre>\n              Error: error message\n            </pre>\n            <pre>\n              stack:\n            </pre>\n          </div>\"\n        `);\n      });\n\n      it(\"deserializes Error subclass instances from the window\", async () => {\n        window.__staticRouterHydrationData = {\n          loaderData: {},\n          actionData: null,\n          errors: {\n            \"0\": {\n              message: \"error message\",\n              __type: \"Error\",\n              __subType: \"ReferenceError\",\n            },\n          },\n        };\n        let router = createTestRouter([\n          {\n            path: \"/\",\n            Component: () => <h1>Nope</h1>,\n            ErrorBoundary: () => <Boundary />,\n          },\n        ]);\n        let { container } = render(<RouterProvider router={router} />);\n\n        function Boundary() {\n          let error = useRouteError() as Error;\n          return error instanceof Error ? (\n            <>\n              <pre>{error.toString()}</pre>\n              <pre>stack:{error.stack}</pre>\n            </>\n          ) : (\n            <p>No :(</p>\n          );\n        }\n\n        expect(getHtml(container)).toMatchInlineSnapshot(`\n          \"<div>\n            <pre>\n              ReferenceError: error message\n            </pre>\n            <pre>\n              stack:\n            </pre>\n          </div>\"\n        `);\n      });\n\n      it(\"renders hydration errors on leaf elements\", async () => {\n        let router = createTestRouter(\n          [\n            {\n              path: \"/\",\n              Component: Comp,\n              children: [\n                {\n                  path: \"child\",\n                  Component: Comp,\n                  ErrorBoundary: () => <ErrorBoundary />,\n                },\n              ],\n            },\n          ],\n          {\n            window: getWindow(\"/child\"),\n            hydrationData: {\n              loaderData: {\n                \"0\": \"parent data\",\n              },\n              actionData: {\n                \"0\": \"parent action\",\n              },\n              errors: {\n                \"0-0\": new Error(\"Kaboom 💥\"),\n              },\n            },\n          },\n        );\n        let { container } = render(<RouterProvider router={router} />);\n\n        function Comp() {\n          let data = useLoaderData();\n          let actionData = useActionData();\n          let navigation = useNavigation();\n          return (\n            <div>\n              <>{data}</>\n              <>{actionData}</>\n              <>{navigation.state}</>\n              <Outlet />\n            </div>\n          );\n        }\n\n        function ErrorBoundary() {\n          let error = useRouteError() as Error;\n          return <p>{error.message}</p>;\n        }\n\n        expect(getHtml(container)).toMatchInlineSnapshot(`\n          \"<div>\n            <div>\n              parent data\n              parent action\n              idle\n              <p>\n                Kaboom 💥\n              </p>\n            </div>\n          </div>\"\n        `);\n      });\n\n      it(\"renders hydration errors on lazy leaf elements with preloading\", async () => {\n        let routes: RouteObject[] = [\n          {\n            path: \"/\",\n            Component: Comp,\n            children: [\n              {\n                path: \"child\",\n                lazy: async () => ({\n                  Component: Comp,\n                  ErrorBoundary: () => <ErrorBoundary />,\n                }),\n              },\n            ],\n          },\n        ];\n\n        let lazyMatches = matchRoutes(routes, { pathname: \"/child\" })?.filter(\n          (m) => m.route.lazy,\n        );\n\n        if (lazyMatches && lazyMatches?.length > 0) {\n          await Promise.all(\n            lazyMatches.map(async (m) => {\n              let routeModule = await (m.route.lazy as Function)();\n              Object.assign(m.route, { ...routeModule, lazy: undefined });\n            }),\n          );\n        }\n\n        let router = createTestRouter(routes, {\n          window: getWindow(\"/child\"),\n          hydrationData: {\n            loaderData: {\n              \"0\": \"parent data\",\n            },\n            actionData: {\n              \"0\": \"parent action\",\n            },\n            errors: {\n              \"0-0\": new Error(\"Kaboom 💥\"),\n            },\n          },\n        });\n\n        let { container } = render(<RouterProvider router={router} />);\n\n        function Comp() {\n          let data = useLoaderData();\n          let actionData = useActionData();\n          let navigation = useNavigation();\n          return (\n            <div>\n              <>{data}</>\n              <>{actionData}</>\n              <>{navigation.state}</>\n              <Outlet />\n            </div>\n          );\n        }\n\n        function ErrorBoundary() {\n          let error = useRouteError() as Error;\n          return <p>{error.message}</p>;\n        }\n\n        expect(getHtml(container)).toMatchInlineSnapshot(`\n          \"<div>\n            <div>\n              parent data\n              parent action\n              idle\n              <p>\n                Kaboom 💥\n              </p>\n            </div>\n          </div>\"\n        `);\n      });\n\n      it(\"renders hydration errors on parent elements\", async () => {\n        let router = createTestRouter(\n          [\n            {\n              path: \"/\",\n              Component: Comp,\n              ErrorBoundary: () => <ErrorBoundary />,\n              children: [{ path: \"child\", Component: Comp }],\n            },\n          ],\n          {\n            window: getWindow(\"/child\"),\n            hydrationData: {\n              loaderData: {},\n              actionData: null,\n              errors: {\n                \"0\": new Error(\"Kaboom 💥\"),\n              },\n            },\n          },\n        );\n        let { container } = render(<RouterProvider router={router} />);\n\n        function Comp() {\n          let data = useLoaderData();\n          let actionData = useActionData();\n          let navigation = useNavigation();\n          return (\n            <div>\n              <>{data}</>\n              <>{actionData}</>\n              <>{navigation.state}</>\n              <Outlet />\n            </div>\n          );\n        }\n\n        function ErrorBoundary() {\n          let error = useRouteError() as Error;\n          return <p>{error.message}</p>;\n        }\n\n        expect(getHtml(container)).toMatchInlineSnapshot(`\n          \"<div>\n            <p>\n              Kaboom 💥\n            </p>\n          </div>\"\n        `);\n      });\n\n      it(\"renders hydration errors on lazy parent elements with preloading\", async () => {\n        let routes: RouteObject[] = [\n          {\n            path: \"/\",\n            lazy: async () => ({\n              Component: Comp,\n              ErrorBoundary: () => <ErrorBoundary />,\n            }),\n            children: [{ path: \"child\", Component: Comp }],\n          },\n        ];\n\n        let lazyMatches = matchRoutes(routes, { pathname: \"/child\" })?.filter(\n          (m) => m.route.lazy,\n        );\n\n        if (lazyMatches && lazyMatches?.length > 0) {\n          await Promise.all(\n            lazyMatches.map(async (m) => {\n              let routeModule = await (m.route.lazy as Function)();\n              Object.assign(m.route, { ...routeModule, lazy: undefined });\n            }),\n          );\n        }\n\n        let router = createTestRouter(routes, {\n          window: getWindow(\"/child\"),\n          hydrationData: {\n            loaderData: {},\n            actionData: null,\n            errors: {\n              \"0\": new Error(\"Kaboom 💥\"),\n            },\n          },\n        });\n\n        let { container } = render(<RouterProvider router={router} />);\n\n        function Comp() {\n          let data = useLoaderData();\n          let actionData = useActionData();\n          let navigation = useNavigation();\n          return (\n            <div>\n              <>{data}</>\n              <>{actionData}</>\n              <>{navigation.state}</>\n              <Outlet />\n            </div>\n          );\n        }\n\n        function ErrorBoundary() {\n          let error = useRouteError() as Error;\n          return <p>{error.message}</p>;\n        }\n\n        expect(getHtml(container)).toMatchInlineSnapshot(`\n          \"<div>\n            <p>\n              Kaboom 💥\n            </p>\n          </div>\"\n        `);\n      });\n\n      it(\"renders navigation errors on leaf elements\", async () => {\n        let fooDefer = createDeferred();\n        let barDefer = createDeferred();\n\n        let router = createTestRouter(\n          [\n            {\n              path: \"/\",\n              Component: Layout,\n              children: [\n                {\n                  path: \"foo\",\n                  loader: () => fooDefer.promise,\n                  Component: () => {\n                    let data = useLoaderData() as { message: string };\n                    return <h1>Foo:{data.message}</h1>;\n                  },\n                  ErrorBoundary: () => <FooError />,\n                },\n                {\n                  path: \"bar\",\n                  loader: () => barDefer.promise,\n                  Component: () => {\n                    let data = useLoaderData() as { message: string };\n                    return <h1>Bar:{data.message}</h1>;\n                  },\n                  ErrorBoundary: () => <BarError />,\n                },\n              ],\n            },\n          ],\n          {\n            window: getWindow(\"/foo\"),\n            hydrationData: {\n              loaderData: {\n                \"0-0\": {\n                  message: \"hydrated from foo\",\n                },\n              },\n            },\n          },\n        );\n        let { container } = render(<RouterProvider router={router} />);\n\n        function Layout() {\n          let navigation = useNavigation();\n          return (\n            <div>\n              <Link to=\"/foo\">Link to Foo</Link>\n              <Link to=\"/bar\">Link to Bar</Link>\n              <div id=\"output\">\n                <p>{navigation.state}</p>\n                <Outlet />\n              </div>\n            </div>\n          );\n        }\n        function FooError() {\n          let error = useRouteError() as Error;\n          return <p>Foo Error:{error.message}</p>;\n        }\n        function BarError() {\n          let error = useRouteError() as Error;\n          return <p>Bar Error:{error.message}</p>;\n        }\n\n        expect(getHtml(container.querySelector(\"#output\")!))\n          .toMatchInlineSnapshot(`\n            \"<div\n              id=\"output\"\n            >\n              <p>\n                idle\n              </p>\n              <h1>\n                Foo:\n                hydrated from foo\n              </h1>\n            </div>\"\n          `);\n\n        fireEvent.click(screen.getByText(\"Link to Bar\"));\n        barDefer.reject(new Error(\"Kaboom!\"));\n        await waitFor(() => screen.getByText(\"idle\"));\n        expect(getHtml(container.querySelector(\"#output\")!))\n          .toMatchInlineSnapshot(`\n            \"<div\n              id=\"output\"\n            >\n              <p>\n                idle\n              </p>\n              <p>\n                Bar Error:\n                Kaboom!\n              </p>\n            </div>\"\n          `);\n\n        fireEvent.click(screen.getByText(\"Link to Foo\"));\n        fooDefer.reject(new Error(\"Kaboom!\"));\n        await waitFor(() => screen.getByText(\"idle\"));\n        expect(getHtml(container.querySelector(\"#output\")!))\n          .toMatchInlineSnapshot(`\n            \"<div\n              id=\"output\"\n            >\n              <p>\n                idle\n              </p>\n              <p>\n                Foo Error:\n                Kaboom!\n              </p>\n            </div>\"\n          `);\n      });\n\n      it(\"renders navigation errors on parent elements\", async () => {\n        let fooDefer = createDeferred();\n        let barDefer = createDeferred();\n\n        let router = createTestRouter(\n          [\n            {\n              path: \"/\",\n              Component: Layout,\n              ErrorBoundary: () => <LayoutError />,\n              children: [\n                {\n                  path: \"foo\",\n                  loader: () => fooDefer.promise,\n                  Component: () => {\n                    let data = useLoaderData() as { message: string };\n                    return <h1>Foo:{data.message}</h1>;\n                  },\n                  ErrorBoundary: () => <FooError />,\n                },\n                {\n                  path: \"bar\",\n                  loader: () => barDefer.promise,\n                  Component: () => {\n                    let data = useLoaderData() as { message: string };\n                    return <h1>Bar:{data.message}</h1>;\n                  },\n                },\n              ],\n            },\n          ],\n          {\n            window: getWindow(\"/foo\"),\n            hydrationData: {\n              loaderData: {\n                \"0-0\": {\n                  message: \"hydrated from foo\",\n                },\n              },\n            },\n          },\n        );\n        let { container } = render(<RouterProvider router={router} />);\n\n        function Layout() {\n          let navigation = useNavigation();\n          return (\n            <div>\n              <Link to=\"/foo\">Link to Foo</Link>\n              <Link to=\"/bar\">Link to Bar</Link>\n              <div id=\"output\">\n                <p>{navigation.state}</p>\n                <Outlet />\n              </div>\n            </div>\n          );\n        }\n        function LayoutError() {\n          let error = useRouteError() as Error;\n          return <p>Layout Error:{error.message}</p>;\n        }\n        function FooError() {\n          let error = useRouteError() as Error;\n          return <p>Foo Error:{error.message}</p>;\n        }\n\n        expect(getHtml(container.querySelector(\"#output\")!))\n          .toMatchInlineSnapshot(`\n            \"<div\n              id=\"output\"\n            >\n              <p>\n                idle\n              </p>\n              <h1>\n                Foo:\n                hydrated from foo\n              </h1>\n            </div>\"\n          `);\n\n        fireEvent.click(screen.getByText(\"Link to Bar\"));\n        barDefer.reject(new Error(\"Kaboom!\"));\n        await waitFor(() => screen.getByText(\"Layout Error:Kaboom!\"));\n        expect(getHtml(container)).toMatchInlineSnapshot(`\n          \"<div>\n            <p>\n              Layout Error:\n              Kaboom!\n            </p>\n          </div>\"\n        `);\n      });\n\n      // This test ensures that when manual routes are used, we add hasErrorBoundary\n      it(\"renders navigation errors on leaf elements (when using manual route objects)\", async () => {\n        let barDefer = createDeferred();\n\n        let routes = [\n          {\n            path: \"/\",\n            Component: Layout,\n            children: [\n              {\n                path: \"foo\",\n                Component: () => <h1>Foo</h1>,\n              },\n              {\n                path: \"bar\",\n                loader: () => barDefer.promise,\n                Component: () => {\n                  let data = useLoaderData() as { message: string };\n                  return <h1>Bar:{data.message}</h1>;\n                },\n                ErrorBoundary: () => <BarError />,\n              },\n            ],\n          },\n        ];\n\n        let router = createTestRouter(routes, { window: getWindow(\"/foo\") });\n        let { container } = render(<RouterProvider router={router} />);\n\n        function Layout() {\n          let navigation = useNavigation();\n          return (\n            <div>\n              <Link to=\"/bar\">Link to Bar</Link>\n              <div id=\"output\">\n                <p>{navigation.state}</p>\n                <Outlet />\n              </div>\n            </div>\n          );\n        }\n        function BarError() {\n          let error = useRouteError() as Error;\n          return <p>Bar Error:{error.message}</p>;\n        }\n\n        expect(getHtml(container.querySelector(\"#output\")!))\n          .toMatchInlineSnapshot(`\n            \"<div\n              id=\"output\"\n            >\n              <p>\n                idle\n              </p>\n              <h1>\n                Foo\n              </h1>\n            </div>\"\n          `);\n\n        fireEvent.click(screen.getByText(\"Link to Bar\"));\n        barDefer.reject(new Error(\"Kaboom!\"));\n        await waitFor(() => screen.getByText(\"idle\"));\n        expect(getHtml(container.querySelector(\"#output\")!))\n          .toMatchInlineSnapshot(`\n            \"<div\n              id=\"output\"\n            >\n              <p>\n                idle\n              </p>\n              <p>\n                Bar Error:\n                Kaboom!\n              </p>\n            </div>\"\n          `);\n      });\n\n      // This test ensures that when manual routes are used, we add hasErrorBoundary\n      it(\"renders navigation errors on lazy leaf elements (when using manual route objects)\", async () => {\n        let lazyRouteModule = {\n          loader: () => barDefer.promise,\n          Component: Bar,\n          ErrorBoundary: BarError,\n        };\n        let lazyDefer = createDeferred<typeof lazyRouteModule>();\n        let barDefer = createDeferred();\n\n        let routes: RouteObject[] = [\n          {\n            path: \"/\",\n            Component: Layout,\n            children: [\n              {\n                path: \"foo\",\n                Component: () => <h1>Foo</h1>,\n              },\n              {\n                path: \"bar\",\n                lazy: () => lazyDefer.promise,\n              },\n            ],\n          },\n        ];\n\n        let router = createTestRouter(routes, { window: getWindow(\"/foo\") });\n        let { container } = render(<RouterProvider router={router} />);\n\n        function Layout() {\n          let navigation = useNavigation();\n          return (\n            <div>\n              <Link to=\"/bar\">Link to Bar</Link>\n              <div id=\"output\">\n                <p>{navigation.state}</p>\n                <Outlet />\n              </div>\n            </div>\n          );\n        }\n\n        function Bar() {\n          let data = useLoaderData() as { message: string };\n          return <h1>Bar:{data.message}</h1>;\n        }\n        function BarError() {\n          let error = useRouteError() as Error;\n          return <p>Bar Error:{error.message}</p>;\n        }\n\n        expect(getHtml(container.querySelector(\"#output\")!))\n          .toMatchInlineSnapshot(`\n            \"<div\n              id=\"output\"\n            >\n              <p>\n                idle\n              </p>\n              <h1>\n                Foo\n              </h1>\n            </div>\"\n          `);\n\n        fireEvent.click(screen.getByText(\"Link to Bar\"));\n        await lazyDefer.resolve(lazyRouteModule);\n        barDefer.reject(new Error(\"Kaboom!\"));\n        await waitFor(() => screen.getByText(\"idle\"));\n        expect(getHtml(container.querySelector(\"#output\")!))\n          .toMatchInlineSnapshot(`\n            \"<div\n              id=\"output\"\n            >\n              <p>\n                idle\n              </p>\n              <p>\n                Bar Error:\n                Kaboom!\n              </p>\n            </div>\"\n          `);\n      });\n    });\n\n    describe(\"view transitions\", () => {\n      it(\"applies view transitions to navigations when opted in\", async () => {\n        let testWindow = getWindow(\"/\");\n        let spy = jest.fn((cb) => {\n          cb();\n          return {\n            ready: Promise.resolve(),\n            finished: Promise.resolve(),\n            updateCallbackDone: Promise.resolve(),\n            skipTransition: () => {},\n          };\n        });\n        testWindow.document.startViewTransition = spy;\n\n        let router = createTestRouter(\n          [\n            {\n              path: \"/\",\n              Component() {\n                return (\n                  <div>\n                    <Link to=\"/a\">/a</Link>\n                    <Link to=\"/b\" viewTransition>\n                      /b\n                    </Link>\n                    <Form action=\"/c\">\n                      <button type=\"submit\">/c</button>\n                    </Form>\n                    <Form action=\"/d\" viewTransition>\n                      <button type=\"submit\">/d</button>\n                    </Form>\n                    <Outlet />\n                  </div>\n                );\n              },\n              children: [\n                {\n                  index: true,\n                  Component: () => <h1>Home</h1>,\n                },\n                {\n                  path: \"a\",\n                  Component: () => <h1>A</h1>,\n                },\n                {\n                  path: \"b\",\n                  Component: () => <h1>B</h1>,\n                },\n                {\n                  path: \"c\",\n                  action: () => null,\n                  Component: () => <h1>C</h1>,\n                },\n                {\n                  path: \"d\",\n                  action: () => null,\n                  Component: () => <h1>D</h1>,\n                },\n              ],\n            },\n          ],\n          { window: testWindow },\n        );\n        render(<RouterProvider router={router} />);\n\n        expect(screen.getByText(\"Home\")).toBeDefined();\n        fireEvent.click(screen.getByText(\"/a\"));\n        await waitFor(() => screen.getByText(\"A\"));\n        expect(spy).not.toHaveBeenCalled();\n\n        fireEvent.click(screen.getByText(\"/b\"));\n        await waitFor(() => screen.getByText(\"B\"));\n        expect(spy).toHaveBeenCalledTimes(1);\n\n        fireEvent.click(screen.getByText(\"/c\"));\n        await waitFor(() => screen.getByText(\"C\"));\n        expect(spy).toHaveBeenCalledTimes(1);\n\n        fireEvent.click(screen.getByText(\"/d\"));\n        await waitFor(() => screen.getByText(\"D\"));\n        expect(spy).toHaveBeenCalledTimes(2);\n      });\n\n      it(\"Does not cause extra re-renders due to ViewTransitionContext updates\", async () => {\n        let testWindow = getWindow(\"/\");\n        testWindow.document.startViewTransition = (cb) => {\n          cb();\n          return {\n            ready: Promise.resolve(),\n            finished: Promise.resolve(),\n            updateCallbackDone: Promise.resolve(),\n            skipTransition: () => {},\n          };\n        };\n\n        let renders: [Location, Navigation][] = [];\n        let router = createTestRouter(\n          [\n            {\n              path: \"/\",\n              Component() {\n                return (\n                  <>\n                    <Link to=\"/page\" viewTransition>\n                      /page\n                    </Link>\n                    <Outlet />\n                  </>\n                );\n              },\n              children: [\n                {\n                  index: true,\n                  async loader() {\n                    await tick();\n                    return \"INDEX\";\n                  },\n                  Component() {\n                    renders.push([useLocation(), useNavigation()]);\n                    return <h1>{useLoaderData()}</h1>;\n                  },\n                },\n                {\n                  path: \"page\",\n                  async loader() {\n                    await tick();\n                    return \"PAGE\";\n                  },\n                  Component() {\n                    renders.push([useLocation(), useNavigation()]);\n                    return <h1>{useLoaderData()}</h1>;\n                  },\n                },\n              ],\n            },\n          ],\n          { window: testWindow },\n        );\n        render(<RouterProvider router={router} />);\n        await waitFor(() => screen.getByText(\"INDEX\"));\n\n        renders = [];\n        fireEvent.click(screen.getByText(\"/page\"));\n        await waitFor(() => screen.getByText(\"PAGE\"));\n\n        expect(renders).toMatchObject([\n          // Re-render of current location with navigation.state = \"loading\"\n          [\n            { pathname: \"/\" },\n            {\n              state: \"loading\",\n              location: { pathname: \"/page\" },\n            },\n          ],\n          // Render of new location with navigation.state = \"idle\"\n          [{ pathname: \"/page\" }, { state: \"idle\" }],\n        ]);\n      });\n    });\n\n    if (name === \"<DataBrowserRouter>\") {\n      describe(\"DataBrowserRouter-only tests\", () => {\n        it(\"is defensive against double slash URLs in window.location\", async () => {\n          let testWindow = getWindow(\"http://localhost//\");\n          let router = createTestRouter(\n            [\n              {\n                path: \"*\",\n                Component() {\n                  return <Link to=\"/page\">Go to Page</Link>;\n                },\n              },\n              {\n                path: \"/page\",\n                Component() {\n                  return <h1>Worked!</h1>;\n                },\n              },\n            ],\n            {\n              window: testWindow,\n            },\n          );\n          render(<RouterProvider router={router} />);\n          expect(testWindow.location.pathname).toBe(\"//\");\n          expect(router.state.location.pathname).toBe(\"//\");\n\n          fireEvent.click(screen.getByText(\"Go to Page\"));\n          await waitFor(() => screen.getByText(\"Worked!\"));\n          expect(testWindow.location.pathname).toBe(\"/page\");\n          expect(router.state.location.pathname).toBe(\"/page\");\n        });\n      });\n\n      it(\"handles different-origin absolute redirect URLs\", async () => {\n        let testWindow = getWindow(\"http://localhost/\");\n\n        // jsdom is making more and more properties non-configurable, so we inject\n        // our own jest-friendly window\n        testWindow = {\n          ...testWindow,\n          addEventListener: testWindow.addEventListener.bind(testWindow),\n          location: {\n            ...testWindow.location,\n            assign: jest.fn(),\n            replace: jest.fn(),\n          },\n        };\n\n        let router = createTestRouter(\n          [\n            {\n              path: \"/\",\n              Component() {\n                return <Link to=\"/page\">Go to Page</Link>;\n              },\n            },\n            {\n              path: \"/page\",\n              loader() {\n                return redirect(\"http://otherhost/parent\");\n              },\n              Component() {\n                return null;\n              },\n            },\n          ],\n          {\n            window: testWindow,\n          },\n        );\n\n        await router.navigate(\"/page\");\n        expect(testWindow.location.assign).toHaveBeenCalledWith(\n          \"http://otherhost/parent\",\n        );\n      });\n\n      it(\"handles different-origin protocol-less absolute redirect URLs\", async () => {\n        let testWindow = getWindow(\"http://localhost/\");\n\n        // jsdom is making more and more properties non-configurable, so we inject\n        // our own jest-friendly window\n        testWindow = {\n          ...testWindow,\n          addEventListener: testWindow.addEventListener.bind(testWindow),\n          location: {\n            ...testWindow.location,\n            assign: jest.fn(),\n            replace: jest.fn(),\n          },\n        };\n\n        let router = createTestRouter(\n          [\n            {\n              path: \"/\",\n              Component() {\n                return <Link to=\"/page\">Go to Page</Link>;\n              },\n            },\n            {\n              path: \"/page\",\n              loader() {\n                return redirect(\"//otherhost/parent\");\n              },\n              Component() {\n                return null;\n              },\n            },\n          ],\n          {\n            window: testWindow,\n          },\n        );\n\n        await router.navigate(\"/page\");\n        expect(testWindow.location.assign).toHaveBeenCalledWith(\n          \"//otherhost/parent\",\n        );\n      });\n    }\n  });\n}\n"
  },
  {
    "path": "packages/react-router/__tests__/dom/data-static-router-test.tsx",
    "content": "/**\n * @jest-environment node\n */\n\nimport * as React from \"react\";\nimport * as ReactDOMServer from \"react-dom/server\";\nimport type { StaticHandlerContext } from \"../../index\";\nimport {\n  Form,\n  Link,\n  Outlet,\n  useLoaderData,\n  useLocation,\n  useMatches,\n  createStaticHandler,\n  createStaticRouter,\n  StaticRouterProvider,\n} from \"../../index\";\n\nbeforeEach(() => {\n  jest.spyOn(console, \"warn\").mockImplementation(() => {});\n  jest.spyOn(console, \"error\").mockImplementation(() => {});\n});\n\ndescribe(\"A <StaticRouterProvider>\", () => {\n  it(\"renders an initialized router\", async () => {\n    let hooksData1: {\n      location: ReturnType<typeof useLocation>;\n      loaderData: ReturnType<typeof useLoaderData>;\n      matches: ReturnType<typeof useMatches>;\n    };\n    let hooksData2: {\n      location: ReturnType<typeof useLocation>;\n      loaderData: ReturnType<typeof useLoaderData>;\n      matches: ReturnType<typeof useMatches>;\n    };\n\n    function HooksChecker1() {\n      hooksData1 = {\n        location: useLocation(),\n        loaderData: useLoaderData(),\n        matches: useMatches(),\n      };\n      return <Outlet />;\n    }\n\n    function HooksChecker2() {\n      hooksData2 = {\n        location: useLocation(),\n        loaderData: useLoaderData(),\n        matches: useMatches(),\n      };\n      return (\n        <>\n          <h1>👋</h1>\n          <Link to=\"/the/other/path\">Other</Link>\n        </>\n      );\n    }\n\n    let routes = [\n      {\n        path: \"the\",\n        loader: () => ({\n          key1: \"value1\",\n        }),\n        element: <HooksChecker1 />,\n        handle: \"1\",\n        children: [\n          {\n            path: \"path\",\n            loader: () => ({\n              key2: \"value2\",\n            }),\n            element: <HooksChecker2 />,\n            handle: \"2\",\n          },\n        ],\n      },\n    ];\n    let { query } = createStaticHandler(routes);\n\n    let context = (await query(\n      new Request(\"http://localhost/the/path?the=query#the-hash\", {\n        signal: new AbortController().signal,\n      }),\n    )) as StaticHandlerContext;\n\n    let html = ReactDOMServer.renderToStaticMarkup(\n      <React.StrictMode>\n        <StaticRouterProvider\n          router={createStaticRouter(routes, context)}\n          context={context}\n        />\n      </React.StrictMode>,\n    );\n    expect(html).toMatch(\"<h1>👋</h1>\");\n    expect(html).toMatch('<a href=\"/the/other/path\" data-discover=\"true\">');\n\n    // @ts-expect-error\n    expect(hooksData1.location).toEqual({\n      pathname: \"/the/path\",\n      search: \"?the=query\",\n      hash: \"#the-hash\",\n      state: null,\n      key: expect.any(String),\n    });\n    // @ts-expect-error\n    expect(hooksData1.loaderData).toEqual({\n      key1: \"value1\",\n    });\n    // @ts-expect-error\n    expect(hooksData1.matches).toEqual([\n      {\n        data: {\n          key1: \"value1\",\n        },\n        loaderData: {\n          key1: \"value1\",\n        },\n        handle: \"1\",\n        id: \"0\",\n        params: {},\n        pathname: \"/the\",\n      },\n      {\n        data: {\n          key2: \"value2\",\n        },\n        loaderData: {\n          key2: \"value2\",\n        },\n        handle: \"2\",\n        id: \"0-0\",\n        params: {},\n        pathname: \"/the/path\",\n      },\n    ]);\n\n    // @ts-expect-error\n    expect(hooksData2.location).toEqual({\n      pathname: \"/the/path\",\n      search: \"?the=query\",\n      hash: \"#the-hash\",\n      state: null,\n      key: expect.any(String),\n    });\n    // @ts-expect-error\n    expect(hooksData2.loaderData).toEqual({\n      key2: \"value2\",\n    });\n    // @ts-expect-error\n    expect(hooksData2.matches).toEqual([\n      {\n        data: {\n          key1: \"value1\",\n        },\n        loaderData: {\n          key1: \"value1\",\n        },\n        handle: \"1\",\n        id: \"0\",\n        params: {},\n        pathname: \"/the\",\n      },\n      {\n        data: {\n          key2: \"value2\",\n        },\n        loaderData: {\n          key2: \"value2\",\n        },\n        handle: \"2\",\n        id: \"0-0\",\n        params: {},\n        pathname: \"/the/path\",\n      },\n    ]);\n  });\n\n  it(\"renders an initialized router with lazy routes\", async () => {\n    let hooksData1: {\n      location: ReturnType<typeof useLocation>;\n      loaderData: ReturnType<typeof useLoaderData>;\n      matches: ReturnType<typeof useMatches>;\n    };\n    let hooksData2: {\n      location: ReturnType<typeof useLocation>;\n      loaderData: ReturnType<typeof useLoaderData>;\n      matches: ReturnType<typeof useMatches>;\n    };\n\n    function HooksChecker1() {\n      hooksData1 = {\n        location: useLocation(),\n        loaderData: useLoaderData(),\n        matches: useMatches(),\n      };\n      return <Outlet />;\n    }\n\n    function HooksChecker2() {\n      hooksData2 = {\n        location: useLocation(),\n        loaderData: useLoaderData(),\n        matches: useMatches(),\n      };\n      return (\n        <>\n          <h1>👋</h1>\n          <Link to=\"/the/other/path\">Other</Link>\n        </>\n      );\n    }\n\n    let routes = [\n      {\n        path: \"the\",\n        lazy: async () => ({\n          loader: () => ({\n            key1: \"value1\",\n          }),\n          element: <HooksChecker1 />,\n          handle: \"1\",\n        }),\n        children: [\n          {\n            path: \"path\",\n            lazy: async () => ({\n              loader: () => ({\n                key2: \"value2\",\n              }),\n              element: <HooksChecker2 />,\n              handle: \"2\",\n            }),\n          },\n        ],\n      },\n    ];\n    let { query, dataRoutes } = createStaticHandler(routes);\n\n    let context = (await query(\n      new Request(\"http://localhost/the/path?the=query#the-hash\", {\n        signal: new AbortController().signal,\n      }),\n    )) as StaticHandlerContext;\n\n    let html = ReactDOMServer.renderToStaticMarkup(\n      <React.StrictMode>\n        <StaticRouterProvider\n          router={createStaticRouter(dataRoutes, context)}\n          context={context}\n        />\n      </React.StrictMode>,\n    );\n    expect(html).toMatch(\"<h1>👋</h1>\");\n    expect(html).toMatch('<a href=\"/the/other/path\" data-discover=\"true\">');\n\n    // @ts-expect-error\n    expect(hooksData1.location).toEqual({\n      pathname: \"/the/path\",\n      search: \"?the=query\",\n      hash: \"#the-hash\",\n      state: null,\n      key: expect.any(String),\n    });\n    // @ts-expect-error\n    expect(hooksData1.loaderData).toEqual({\n      key1: \"value1\",\n    });\n    // @ts-expect-error\n    expect(hooksData1.matches).toEqual([\n      {\n        data: {\n          key1: \"value1\",\n        },\n        loaderData: {\n          key1: \"value1\",\n        },\n        handle: \"1\",\n        id: \"0\",\n        params: {},\n        pathname: \"/the\",\n      },\n      {\n        data: {\n          key2: \"value2\",\n        },\n        loaderData: {\n          key2: \"value2\",\n        },\n        handle: \"2\",\n        id: \"0-0\",\n        params: {},\n        pathname: \"/the/path\",\n      },\n    ]);\n\n    // @ts-expect-error\n    expect(hooksData2.location).toEqual({\n      pathname: \"/the/path\",\n      search: \"?the=query\",\n      hash: \"#the-hash\",\n      state: null,\n      key: expect.any(String),\n    });\n    // @ts-expect-error\n    expect(hooksData2.loaderData).toEqual({\n      key2: \"value2\",\n    });\n    // @ts-expect-error\n    expect(hooksData2.matches).toEqual([\n      {\n        data: {\n          key1: \"value1\",\n        },\n        loaderData: {\n          key1: \"value1\",\n        },\n        handle: \"1\",\n        id: \"0\",\n        params: {},\n        pathname: \"/the\",\n      },\n      {\n        data: {\n          key2: \"value2\",\n        },\n        loaderData: {\n          key2: \"value2\",\n        },\n        handle: \"2\",\n        id: \"0-0\",\n        params: {},\n        pathname: \"/the/path\",\n      },\n    ]);\n  });\n\n  it(\"renders an initialized router with a basename\", async () => {\n    let location: ReturnType<typeof useLocation>;\n\n    function GetLocation() {\n      location = useLocation();\n      return (\n        <>\n          <h1>👋</h1>\n          <Link to=\"/the/other/path\">Other</Link>\n        </>\n      );\n    }\n\n    let routes = [\n      {\n        path: \"the\",\n        children: [\n          {\n            path: \"path\",\n            element: <GetLocation />,\n          },\n        ],\n      },\n    ];\n    let { query } = createStaticHandler(routes, { basename: \"/base\" });\n\n    let context = (await query(\n      new Request(\"http://localhost/base/the/path?the=query#the-hash\", {\n        signal: new AbortController().signal,\n      }),\n    )) as StaticHandlerContext;\n\n    let html = ReactDOMServer.renderToStaticMarkup(\n      <React.StrictMode>\n        <StaticRouterProvider\n          router={createStaticRouter(routes, context)}\n          context={context}\n        />\n      </React.StrictMode>,\n    );\n    expect(html).toMatch(\"<h1>👋</h1>\");\n    expect(html).toMatch(\n      '<a href=\"/base/the/other/path\" data-discover=\"true\">',\n    );\n\n    // @ts-expect-error\n    expect(location).toEqual({\n      pathname: \"/the/path\",\n      search: \"?the=query\",\n      hash: \"#the-hash\",\n      state: null,\n      key: expect.any(String),\n    });\n  });\n\n  it(\"renders hydration data by default\", async () => {\n    let routes = [\n      {\n        // provide unique id here but not below, to ensure we add where needed\n        id: \"the\",\n        path: \"the\",\n        loader: () => ({\n          key1: \"value1\",\n        }),\n        element: <Outlet />,\n        children: [\n          {\n            path: \"path\",\n            loader: () => ({\n              key2: \"value2\",\n            }),\n            element: <h1>👋</h1>,\n          },\n        ],\n      },\n    ];\n    let { query } = createStaticHandler(routes);\n\n    let context = (await query(\n      new Request(\"http://localhost/the/path\", {\n        signal: new AbortController().signal,\n      }),\n    )) as StaticHandlerContext;\n\n    let html = ReactDOMServer.renderToStaticMarkup(\n      <React.StrictMode>\n        <StaticRouterProvider\n          router={createStaticRouter(routes, context)}\n          context={context}\n        />\n      </React.StrictMode>,\n    );\n    expect(html).toMatch(\"<h1>👋</h1>\");\n\n    let expectedJsonString = JSON.stringify(\n      JSON.stringify({\n        loaderData: {\n          the: { key1: \"value1\" },\n          \"0-0\": { key2: \"value2\" },\n        },\n        actionData: null,\n        errors: null,\n      }),\n    );\n    expect(html).toMatch(\n      `<script>window.__staticRouterHydrationData = JSON.parse(${expectedJsonString});</script>`,\n    );\n  });\n\n  it(\"renders hydration data from lazy routes by default\", async () => {\n    let routes = [\n      {\n        // provide unique id here but not below, to ensure we add where needed\n        id: \"the\",\n        path: \"the\",\n        lazy: async () => ({\n          loader: () => ({\n            key1: \"value1\",\n          }),\n          element: <Outlet />,\n        }),\n        children: [\n          {\n            path: \"path\",\n            lazy: async () => ({\n              loader: () => ({\n                key2: \"value2\",\n              }),\n              element: <h1>👋</h1>,\n            }),\n          },\n        ],\n      },\n    ];\n    let { query, dataRoutes } = createStaticHandler(routes);\n\n    let context = (await query(\n      new Request(\"http://localhost/the/path\", {\n        signal: new AbortController().signal,\n      }),\n    )) as StaticHandlerContext;\n\n    let html = ReactDOMServer.renderToStaticMarkup(\n      <React.StrictMode>\n        <StaticRouterProvider\n          router={createStaticRouter(dataRoutes, context)}\n          context={context}\n        />\n      </React.StrictMode>,\n    );\n    expect(html).toMatch(\"<h1>👋</h1>\");\n\n    let expectedJsonString = JSON.stringify(\n      JSON.stringify({\n        loaderData: {\n          the: { key1: \"value1\" },\n          \"0-0\": { key2: \"value2\" },\n        },\n        actionData: null,\n        errors: null,\n      }),\n    );\n    expect(html).toMatch(\n      `<script>window.__staticRouterHydrationData = JSON.parse(${expectedJsonString});</script>`,\n    );\n  });\n\n  it(\"escapes HTML tags in serialized hydration data\", async () => {\n    let routes = [\n      {\n        path: \"/\",\n        loader: () => ({\n          key: \"uh </script> oh\",\n        }),\n        element: <h1>👋</h1>,\n      },\n    ];\n    let { query } = createStaticHandler(routes);\n\n    let context = (await query(\n      new Request(\"http://localhost/\", {\n        signal: new AbortController().signal,\n      }),\n    )) as StaticHandlerContext;\n\n    let html = ReactDOMServer.renderToStaticMarkup(\n      <React.StrictMode>\n        <StaticRouterProvider\n          router={createStaticRouter(routes, context)}\n          context={context}\n        />\n      </React.StrictMode>,\n    );\n    expect(html).toMatchInlineSnapshot(\n      `\"<h1>👋</h1><script>window.__staticRouterHydrationData = JSON.parse(\"{\\\\\"loaderData\\\\\":{\\\\\"0\\\\\":{\\\\\"key\\\\\":\\\\\"uh \\\\u003c/script\\\\u003e oh\\\\\"}},\\\\\"actionData\\\\\":null,\\\\\"errors\\\\\":null}\");</script>\"`,\n    );\n  });\n\n  it(\"encodes auto-generated <a href> values to avoid hydration errors\", async () => {\n    let routes = [{ path: \"/path/:param\", element: <Link to=\".\">👋</Link> }];\n    let { query } = createStaticHandler(routes);\n\n    let context = (await query(\n      new Request(\"http://localhost/path/with space\", {\n        signal: new AbortController().signal,\n      }),\n    )) as StaticHandlerContext;\n\n    let html = ReactDOMServer.renderToStaticMarkup(\n      <React.StrictMode>\n        <StaticRouterProvider\n          router={createStaticRouter(routes, context)}\n          context={context}\n        />\n      </React.StrictMode>,\n    );\n    expect(html).toContain(\n      '<a href=\"/path/with%20space\" data-discover=\"true\">👋</a>',\n    );\n  });\n\n  it(\"does not encode user-specified <a href> values\", async () => {\n    let routes = [\n      { path: \"/\", element: <Link to=\"/path/with space\">👋</Link> },\n    ];\n    let { query } = createStaticHandler(routes);\n\n    let context = (await query(\n      new Request(\"http://localhost/\", {\n        signal: new AbortController().signal,\n      }),\n    )) as StaticHandlerContext;\n\n    let html = ReactDOMServer.renderToStaticMarkup(\n      <React.StrictMode>\n        <StaticRouterProvider\n          router={createStaticRouter(routes, context)}\n          context={context}\n        />\n      </React.StrictMode>,\n    );\n    expect(html).toContain(\n      '<a href=\"/path/with space\" data-discover=\"true\">👋</a>',\n    );\n  });\n\n  it(\"encodes auto-generated <form action> values to avoid hydration errors (action=undefined)\", async () => {\n    let routes = [{ path: \"/path/:param\", element: <Form>👋</Form> }];\n    let { query } = createStaticHandler(routes);\n\n    let context = (await query(\n      new Request(\"http://localhost/path/with space\", {\n        signal: new AbortController().signal,\n      }),\n    )) as StaticHandlerContext;\n\n    let html = ReactDOMServer.renderToStaticMarkup(\n      <React.StrictMode>\n        <StaticRouterProvider\n          router={createStaticRouter(routes, context)}\n          context={context}\n        />\n      </React.StrictMode>,\n    );\n    expect(html).toContain(\n      '<form data-discover=\"true\" action=\"/path/with%20space\" method=\"get\">👋</form>',\n    );\n  });\n\n  it('encodes auto-generated <form action> values to avoid hydration errors (action=\".\")', async () => {\n    let routes = [\n      { path: \"/path/:param\", element: <Form action=\".\">👋</Form> },\n    ];\n    let { query } = createStaticHandler(routes);\n\n    let context = (await query(\n      new Request(\"http://localhost/path/with space\", {\n        signal: new AbortController().signal,\n      }),\n    )) as StaticHandlerContext;\n\n    let html = ReactDOMServer.renderToStaticMarkup(\n      <React.StrictMode>\n        <StaticRouterProvider\n          router={createStaticRouter(routes, context)}\n          context={context}\n        />\n      </React.StrictMode>,\n    );\n    expect(html).toContain(\n      '<form data-discover=\"true\" action=\"/path/with%20space\" method=\"get\">👋</form>',\n    );\n  });\n\n  it(\"does not encode user-specified <form action> values\", async () => {\n    let routes = [\n      { path: \"/\", element: <Form action=\"/path/with space\">👋</Form> },\n    ];\n    let { query } = createStaticHandler(routes);\n\n    let context = (await query(\n      new Request(\"http://localhost/\", {\n        signal: new AbortController().signal,\n      }),\n    )) as StaticHandlerContext;\n\n    let html = ReactDOMServer.renderToStaticMarkup(\n      <React.StrictMode>\n        <StaticRouterProvider\n          router={createStaticRouter(routes, context)}\n          context={context}\n        />\n      </React.StrictMode>,\n    );\n    expect(html).toContain(\n      '<form data-discover=\"true\" action=\"/path/with space\" method=\"get\">👋</form>',\n    );\n  });\n\n  it(\"serializes ErrorResponse instances\", async () => {\n    let routes = [\n      {\n        path: \"/\",\n        loader: () => {\n          throw Response.json(\n            { not: \"found\" },\n            { status: 404, statusText: \"Not Found\" },\n          );\n        },\n      },\n    ];\n    let { query } = createStaticHandler(routes);\n\n    let context = (await query(\n      new Request(\"http://localhost/\", {\n        signal: new AbortController().signal,\n      }),\n    )) as StaticHandlerContext;\n\n    let html = ReactDOMServer.renderToStaticMarkup(\n      <React.StrictMode>\n        <StaticRouterProvider\n          router={createStaticRouter(routes, context)}\n          context={context}\n        />\n      </React.StrictMode>,\n    );\n\n    let expectedJsonString = JSON.stringify(\n      JSON.stringify({\n        loaderData: {},\n        actionData: null,\n        errors: {\n          \"0\": {\n            status: 404,\n            statusText: \"Not Found\",\n            internal: false,\n            data: { not: \"found\" },\n            __type: \"RouteErrorResponse\",\n          },\n        },\n      }),\n    );\n    expect(html).toMatch(\n      `<script>window.__staticRouterHydrationData = JSON.parse(${expectedJsonString});</script>`,\n    );\n  });\n\n  it(\"serializes ErrorResponse instances from lazy routes\", async () => {\n    let routes = [\n      {\n        path: \"/\",\n        lazy: async () => ({\n          loader: () => {\n            throw Response.json(\n              { not: \"found\" },\n              { status: 404, statusText: \"Not Found\" },\n            );\n          },\n        }),\n      },\n    ];\n    let { query, dataRoutes } = createStaticHandler(routes);\n\n    let context = (await query(\n      new Request(\"http://localhost/\", {\n        signal: new AbortController().signal,\n      }),\n    )) as StaticHandlerContext;\n\n    let html = ReactDOMServer.renderToStaticMarkup(\n      <React.StrictMode>\n        <StaticRouterProvider\n          router={createStaticRouter(dataRoutes, context)}\n          context={context}\n        />\n      </React.StrictMode>,\n    );\n\n    let expectedJsonString = JSON.stringify(\n      JSON.stringify({\n        loaderData: {},\n        actionData: null,\n        errors: {\n          \"0\": {\n            status: 404,\n            statusText: \"Not Found\",\n            internal: false,\n            data: { not: \"found\" },\n            __type: \"RouteErrorResponse\",\n          },\n        },\n      }),\n    );\n    expect(html).toMatch(\n      `<script>window.__staticRouterHydrationData = JSON.parse(${expectedJsonString});</script>`,\n    );\n  });\n\n  it(\"serializes Error instances\", async () => {\n    let routes = [\n      {\n        path: \"/\",\n        loader: () => {\n          throw new Error(\"oh no\");\n        },\n      },\n    ];\n    let { query } = createStaticHandler(routes);\n\n    let context = (await query(\n      new Request(\"http://localhost/\", {\n        signal: new AbortController().signal,\n      }),\n    )) as StaticHandlerContext;\n\n    let html = ReactDOMServer.renderToStaticMarkup(\n      <React.StrictMode>\n        <StaticRouterProvider\n          router={createStaticRouter(routes, context)}\n          context={context}\n        />\n      </React.StrictMode>,\n    );\n\n    // stack is stripped by default from SSR errors\n    let expectedJsonString = JSON.stringify(\n      JSON.stringify({\n        loaderData: {},\n        actionData: null,\n        errors: {\n          \"0\": {\n            message: \"oh no\",\n            __type: \"Error\",\n          },\n        },\n      }),\n    );\n    expect(html).toMatch(\n      `<script>window.__staticRouterHydrationData = JSON.parse(${expectedJsonString});</script>`,\n    );\n  });\n\n  it(\"serializes Error instances from lazy routes\", async () => {\n    let routes = [\n      {\n        path: \"/\",\n        lazy: async () => ({\n          loader: () => {\n            throw new Error(\"oh no\");\n          },\n        }),\n      },\n    ];\n    let { query, dataRoutes } = createStaticHandler(routes);\n\n    let context = (await query(\n      new Request(\"http://localhost/\", {\n        signal: new AbortController().signal,\n      }),\n    )) as StaticHandlerContext;\n\n    let html = ReactDOMServer.renderToStaticMarkup(\n      <React.StrictMode>\n        <StaticRouterProvider\n          router={createStaticRouter(dataRoutes, context)}\n          context={context}\n        />\n      </React.StrictMode>,\n    );\n\n    // stack is stripped by default from SSR errors\n    let expectedJsonString = JSON.stringify(\n      JSON.stringify({\n        loaderData: {},\n        actionData: null,\n        errors: {\n          \"0\": {\n            message: \"oh no\",\n            __type: \"Error\",\n          },\n        },\n      }),\n    );\n    expect(html).toMatch(\n      `<script>window.__staticRouterHydrationData = JSON.parse(${expectedJsonString});</script>`,\n    );\n  });\n\n  it(\"serializes Error subclass instances\", async () => {\n    let routes = [\n      {\n        path: \"/\",\n        loader: () => {\n          throw new ReferenceError(\"oh no\");\n        },\n      },\n    ];\n    let { query } = createStaticHandler(routes);\n\n    let context = (await query(\n      new Request(\"http://localhost/\", {\n        signal: new AbortController().signal,\n      }),\n    )) as StaticHandlerContext;\n\n    let html = ReactDOMServer.renderToStaticMarkup(\n      <React.StrictMode>\n        <StaticRouterProvider\n          router={createStaticRouter(routes, context)}\n          context={context}\n        />\n      </React.StrictMode>,\n    );\n\n    // stack is stripped by default from SSR errors\n    let expectedJsonString = JSON.stringify(\n      JSON.stringify({\n        loaderData: {},\n        actionData: null,\n        errors: {\n          \"0\": {\n            message: \"oh no\",\n            __type: \"Error\",\n            __subType: \"ReferenceError\",\n          },\n        },\n      }),\n    );\n    expect(html).toMatch(\n      `<script>window.__staticRouterHydrationData = JSON.parse(${expectedJsonString});</script>`,\n    );\n  });\n\n  it(\"supports a nonce prop\", async () => {\n    let routes = [\n      {\n        path: \"the\",\n        element: <Outlet />,\n        children: [\n          {\n            path: \"path\",\n            element: <h1>👋</h1>,\n          },\n        ],\n      },\n    ];\n    let { query } = createStaticHandler(routes);\n\n    let context = (await query(\n      new Request(\"http://localhost/the/path\", {\n        signal: new AbortController().signal,\n      }),\n    )) as StaticHandlerContext;\n\n    let html = ReactDOMServer.renderToStaticMarkup(\n      <React.StrictMode>\n        <StaticRouterProvider\n          router={createStaticRouter(routes, context)}\n          context={context}\n          nonce=\"nonce-string\"\n        />\n      </React.StrictMode>,\n    );\n    expect(html).toMatch(\"<h1>👋</h1>\");\n\n    let expectedJsonString = JSON.stringify(\n      JSON.stringify({\n        loaderData: {},\n        actionData: null,\n        errors: null,\n      }),\n    );\n    expect(html).toMatch(\n      `<script nonce=\"nonce-string\">window.__staticRouterHydrationData = JSON.parse(${expectedJsonString});</script>`,\n    );\n  });\n\n  it(\"allows disabling of automatic hydration\", async () => {\n    let routes = [\n      {\n        path: \"the\",\n        loader: () => ({\n          key1: \"value1\",\n        }),\n        element: <Outlet />,\n        children: [\n          {\n            path: \"path\",\n            loader: () => ({\n              key2: \"value2\",\n            }),\n            element: <h1>👋</h1>,\n          },\n        ],\n      },\n    ];\n    let { query } = createStaticHandler(routes);\n\n    let context = (await query(\n      new Request(\"http://localhost/the/path\", {\n        signal: new AbortController().signal,\n      }),\n    )) as StaticHandlerContext;\n\n    let html = ReactDOMServer.renderToStaticMarkup(\n      <React.StrictMode>\n        <StaticRouterProvider\n          router={createStaticRouter(routes, context)}\n          context={context}\n          hydrate={false}\n        />\n      </React.StrictMode>,\n    );\n    expect(html).toMatch(\"<h1>👋</h1>\");\n    expect(html).not.toMatch(\"<script>\");\n    expect(html).not.toMatch(\"window\");\n    expect(html).not.toMatch(\"__staticRouterHydrationData\");\n  });\n\n  it(\"errors if required props are not passed\", async () => {\n    let routes = [\n      {\n        path: \"\",\n        element: <h1>👋</h1>,\n      },\n    ];\n    let { query } = createStaticHandler(routes);\n\n    let context = (await query(\n      new Request(\"http://localhost/\", {\n        signal: new AbortController().signal,\n      }),\n    )) as StaticHandlerContext;\n\n    expect(() =>\n      ReactDOMServer.renderToStaticMarkup(\n        <React.StrictMode>\n          {/* @ts-expect-error */}\n          <StaticRouterProvider context={context} />\n        </React.StrictMode>,\n      ),\n    ).toThrowErrorMatchingInlineSnapshot(\n      `\"You must provide \\`router\\` and \\`context\\` to <StaticRouterProvider>\"`,\n    );\n\n    expect(() =>\n      ReactDOMServer.renderToStaticMarkup(\n        <React.StrictMode>\n          {/* @ts-expect-error */}\n          <StaticRouterProvider router={createStaticRouter(routes, context)} />\n        </React.StrictMode>,\n      ),\n    ).toThrowErrorMatchingInlineSnapshot(\n      `\"You must provide \\`router\\` and \\`context\\` to <StaticRouterProvider>\"`,\n    );\n  });\n\n  it(\"handles framework agnostic static handler routes\", async () => {\n    let frameworkAgnosticRoutes = [\n      {\n        path: \"the\",\n        hasErrorBoundary: true,\n        children: [\n          {\n            path: \"path\",\n            hasErrorBoundary: true,\n          },\n        ],\n      },\n    ];\n    let { query } = createStaticHandler(frameworkAgnosticRoutes);\n\n    let context = (await query(\n      new Request(\"http://localhost/the/path\", {\n        signal: new AbortController().signal,\n      }),\n    )) as StaticHandlerContext;\n\n    let frameworkAwareRoutes = [\n      {\n        path: \"the\",\n        element: <h1>Hi!</h1>,\n        errorElement: <h1>Error!</h1>,\n        children: [\n          {\n            path: \"path\",\n            element: <h2>Hi again!</h2>,\n            errorElement: <h2>Error again!</h2>,\n          },\n        ],\n      },\n    ];\n\n    // This should add route ids + hasErrorBoundary, and also update the\n    // context.matches to include the full framework-aware routes\n    let router = createStaticRouter(frameworkAwareRoutes, context);\n\n    expect(router.routes).toMatchInlineSnapshot(`\n      [\n        {\n          \"children\": [\n            {\n              \"children\": undefined,\n              \"element\": <h2>\n                Hi again!\n              </h2>,\n              \"errorElement\": <h2>\n                Error again!\n              </h2>,\n              \"hasErrorBoundary\": true,\n              \"id\": \"0-0\",\n              \"path\": \"path\",\n            },\n          ],\n          \"element\": <h1>\n            Hi!\n          </h1>,\n          \"errorElement\": <h1>\n            Error!\n          </h1>,\n          \"hasErrorBoundary\": true,\n          \"id\": \"0\",\n          \"path\": \"the\",\n        },\n      ]\n    `);\n    expect(router.state.matches).toMatchInlineSnapshot(`\n      [\n        {\n          \"params\": {},\n          \"pathname\": \"/the\",\n          \"pathnameBase\": \"/the\",\n          \"route\": {\n            \"children\": [\n              {\n                \"children\": undefined,\n                \"element\": <h2>\n                  Hi again!\n                </h2>,\n                \"errorElement\": <h2>\n                  Error again!\n                </h2>,\n                \"hasErrorBoundary\": true,\n                \"id\": \"0-0\",\n                \"path\": \"path\",\n              },\n            ],\n            \"element\": <h1>\n              Hi!\n            </h1>,\n            \"errorElement\": <h1>\n              Error!\n            </h1>,\n            \"hasErrorBoundary\": true,\n            \"id\": \"0\",\n            \"path\": \"the\",\n          },\n        },\n        {\n          \"params\": {},\n          \"pathname\": \"/the/path\",\n          \"pathnameBase\": \"/the/path\",\n          \"route\": {\n            \"children\": undefined,\n            \"element\": <h2>\n              Hi again!\n            </h2>,\n            \"errorElement\": <h2>\n              Error again!\n            </h2>,\n            \"hasErrorBoundary\": true,\n            \"id\": \"0-0\",\n            \"path\": \"path\",\n          },\n        },\n      ]\n    `);\n  });\n\n  it(\"handles framework agnostic static handler routes (using ErrorBoundary)\", async () => {\n    let frameworkAgnosticRoutes = [\n      {\n        path: \"the\",\n        hasErrorBoundary: true,\n        children: [\n          {\n            path: \"path\",\n            hasErrorBoundary: true,\n          },\n        ],\n      },\n    ];\n    let { query } = createStaticHandler(frameworkAgnosticRoutes);\n\n    let context = (await query(\n      new Request(\"http://localhost/the/path\", {\n        signal: new AbortController().signal,\n      }),\n    )) as StaticHandlerContext;\n\n    let frameworkAwareRoutes = [\n      {\n        path: \"the\",\n        element: <h1>Hi!</h1>,\n        ErrorBoundary: () => <h1>Error!</h1>,\n        children: [\n          {\n            path: \"path\",\n            element: <h2>Hi again!</h2>,\n            ErrorBoundary: () => <h2>Error again!</h2>,\n          },\n        ],\n      },\n    ];\n\n    // This should add route ids + hasErrorBoundary, and also update the\n    // context.matches to include the full framework-aware routes\n    let router = createStaticRouter(frameworkAwareRoutes, context);\n\n    expect(router.routes).toMatchInlineSnapshot(`\n      [\n        {\n          \"ErrorBoundary\": undefined,\n          \"children\": [\n            {\n              \"ErrorBoundary\": undefined,\n              \"children\": undefined,\n              \"element\": <h2>\n                Hi again!\n              </h2>,\n              \"errorElement\": <ErrorBoundary />,\n              \"hasErrorBoundary\": true,\n              \"id\": \"0-0\",\n              \"path\": \"path\",\n            },\n          ],\n          \"element\": <h1>\n            Hi!\n          </h1>,\n          \"errorElement\": <ErrorBoundary />,\n          \"hasErrorBoundary\": true,\n          \"id\": \"0\",\n          \"path\": \"the\",\n        },\n      ]\n    `);\n    expect(router.state.matches).toMatchInlineSnapshot(`\n      [\n        {\n          \"params\": {},\n          \"pathname\": \"/the\",\n          \"pathnameBase\": \"/the\",\n          \"route\": {\n            \"ErrorBoundary\": undefined,\n            \"children\": [\n              {\n                \"ErrorBoundary\": undefined,\n                \"children\": undefined,\n                \"element\": <h2>\n                  Hi again!\n                </h2>,\n                \"errorElement\": <ErrorBoundary />,\n                \"hasErrorBoundary\": true,\n                \"id\": \"0-0\",\n                \"path\": \"path\",\n              },\n            ],\n            \"element\": <h1>\n              Hi!\n            </h1>,\n            \"errorElement\": <ErrorBoundary />,\n            \"hasErrorBoundary\": true,\n            \"id\": \"0\",\n            \"path\": \"the\",\n          },\n        },\n        {\n          \"params\": {},\n          \"pathname\": \"/the/path\",\n          \"pathnameBase\": \"/the/path\",\n          \"route\": {\n            \"ErrorBoundary\": undefined,\n            \"children\": undefined,\n            \"element\": <h2>\n              Hi again!\n            </h2>,\n            \"errorElement\": <ErrorBoundary />,\n            \"hasErrorBoundary\": true,\n            \"id\": \"0-0\",\n            \"path\": \"path\",\n          },\n        },\n      ]\n    `);\n  });\n\n  it(\"renders absolute links correctly\", async () => {\n    let routes = [\n      {\n        path: \"/\",\n        element: (\n          <>\n            <Link to=\"/the/path\">relative path</Link>\n            <Link to=\"http://localhost/the/path\">absolute same-origin url</Link>\n            <Link to=\"https://remix.run\">absolute different-origin url</Link>\n            <Link to=\"mailto:foo@baz.com\">absolute mailto: url</Link>\n          </>\n        ),\n      },\n    ];\n    let { query } = createStaticHandler(routes);\n\n    let context = (await query(\n      new Request(\"http://localhost/\", {\n        signal: new AbortController().signal,\n      }),\n    )) as StaticHandlerContext;\n\n    let html = ReactDOMServer.renderToStaticMarkup(\n      <React.StrictMode>\n        <StaticRouterProvider\n          router={createStaticRouter(routes, context)}\n          context={context}\n        />\n      </React.StrictMode>,\n    );\n    expect(html).toMatch(\n      '<a href=\"/the/path\" data-discover=\"true\">relative path</a>' +\n        '<a href=\"http://localhost/the/path\">absolute same-origin url</a>' +\n        '<a href=\"https://remix.run\">absolute different-origin url</a>' +\n        '<a href=\"mailto:foo@baz.com\">absolute mailto: url</a>',\n    );\n  });\n\n  describe(\"boundary tracking\", () => {\n    it(\"tracks the deepest boundary during render\", async () => {\n      let routes = [\n        {\n          path: \"/\",\n          element: <Outlet />,\n          ErrorBoundary: () => <p>Error</p>,\n          children: [\n            {\n              index: true,\n              element: <h1>👋</h1>,\n              errorElement: <p>Error</p>,\n            },\n          ],\n        },\n      ];\n\n      let context = (await createStaticHandler(routes).query(\n        new Request(\"http://localhost/\", {\n          signal: new AbortController().signal,\n        }),\n      )) as StaticHandlerContext;\n\n      let html = ReactDOMServer.renderToStaticMarkup(\n        <React.StrictMode>\n          <StaticRouterProvider\n            router={createStaticRouter(routes, context)}\n            context={context}\n            hydrate={false}\n          />\n        </React.StrictMode>,\n      );\n      expect(html).toMatchInlineSnapshot(`\"<h1>👋</h1>\"`);\n      expect(context._deepestRenderedBoundaryId).toBe(\"0-0\");\n    });\n\n    it(\"tracks the deepest boundary during render with lazy routes\", async () => {\n      let routes = [\n        {\n          path: \"/\",\n          lazy: async () => ({\n            element: <Outlet />,\n            errorElement: <p>Error</p>,\n          }),\n          children: [\n            {\n              index: true,\n              lazy: async () => ({\n                element: <h1>👋</h1>,\n                ErrorBoundary: () => <p>Error</p>,\n              }),\n            },\n          ],\n        },\n      ];\n\n      let { query, dataRoutes } = createStaticHandler(routes);\n      let context = (await query(\n        new Request(\"http://localhost/\", {\n          signal: new AbortController().signal,\n        }),\n      )) as StaticHandlerContext;\n\n      let html = ReactDOMServer.renderToStaticMarkup(\n        <React.StrictMode>\n          <StaticRouterProvider\n            router={createStaticRouter(dataRoutes, context)}\n            context={context}\n            hydrate={false}\n          />\n        </React.StrictMode>,\n      );\n      expect(html).toMatchInlineSnapshot(`\"<h1>👋</h1>\"`);\n      expect(context._deepestRenderedBoundaryId).toBe(\"0-0\");\n    });\n\n    it(\"tracks only boundaries that expose an errorElement\", async () => {\n      let routes = [\n        {\n          path: \"/\",\n          element: <Outlet />,\n          errorElement: <p>Error</p>,\n          children: [\n            {\n              index: true,\n              element: <h1>👋</h1>,\n            },\n          ],\n        },\n      ];\n\n      let context = (await createStaticHandler(routes).query(\n        new Request(\"http://localhost/\", {\n          signal: new AbortController().signal,\n        }),\n      )) as StaticHandlerContext;\n\n      let html = ReactDOMServer.renderToStaticMarkup(\n        <React.StrictMode>\n          <StaticRouterProvider\n            router={createStaticRouter(routes, context)}\n            context={context}\n            hydrate={false}\n          />\n        </React.StrictMode>,\n      );\n      expect(html).toMatchInlineSnapshot(`\"<h1>👋</h1>\"`);\n      expect(context._deepestRenderedBoundaryId).toBe(\"0\");\n    });\n\n    it(\"tracks only boundaries that expose an errorElement with lazy routes\", async () => {\n      let routes = [\n        {\n          path: \"/\",\n          lazy: async () => ({\n            element: <Outlet />,\n            errorElement: <p>Error</p>,\n          }),\n          children: [\n            {\n              index: true,\n              element: <h1>👋</h1>,\n            },\n          ],\n        },\n      ];\n\n      let { query, dataRoutes } = createStaticHandler(routes);\n      let context = (await query(\n        new Request(\"http://localhost/\", {\n          signal: new AbortController().signal,\n        }),\n      )) as StaticHandlerContext;\n\n      let html = ReactDOMServer.renderToStaticMarkup(\n        <React.StrictMode>\n          <StaticRouterProvider\n            router={createStaticRouter(dataRoutes, context)}\n            context={context}\n            hydrate={false}\n          />\n        </React.StrictMode>,\n      );\n      expect(html).toMatchInlineSnapshot(`\"<h1>👋</h1>\"`);\n      expect(context._deepestRenderedBoundaryId).toBe(\"0\");\n    });\n  });\n});\n"
  },
  {
    "path": "packages/react-router/__tests__/dom/dom-export-test.tsx",
    "content": "import * as React from \"react\";\n\nimport { render, screen } from \"@testing-library/react\";\nimport { createMemoryRouter, useParams } from \"react-router\";\nimport { RouterProvider } from \"react-router/dom\";\n\ndescribe(\"react-router/dom\", () => {\n  function ShowParams() {\n    return <pre data-testid=\"params\">{JSON.stringify(useParams())}</pre>;\n  }\n\n  describe(\"Does not bundle react-router causing duplicate context issues\", () => {\n    it(\"with route provider shows the url params\", async () => {\n      const router = createMemoryRouter(\n        [\n          {\n            path: \"/blog/:slug\",\n            element: <ShowParams />,\n          },\n        ],\n        {\n          initialEntries: [\"/blog/react-router\"],\n        },\n      );\n\n      // When react-router was bundled in CJS scenarios, this `react-router/dom`\n      // version of `RouterProvider` caused duplicate contexts and we would not\n      // find the param values\n      render(<RouterProvider router={router} />);\n\n      expect(await screen.findByTestId(\"params\")).toMatchInlineSnapshot(`\n      <pre\n        data-testid=\"params\"\n      >\n        {\"slug\":\"react-router\"}\n      </pre>\n    `);\n    });\n  });\n});\n"
  },
  {
    "path": "packages/react-router/__tests__/dom/fetcher-submit-tagname-test.tsx",
    "content": "import * as React from \"react\";\nimport { render, fireEvent, screen, waitFor } from \"@testing-library/react\";\nimport \"@testing-library/jest-dom\";\nimport { RouterProvider, createBrowserRouter, useFetcher } from \"../../index\";\nimport getWindow from \"../utils/getWindow\";\n\ndescribe(\"fetcher.submit with tagName property\", () => {\n  it(\"should handle plain object with tagName property\", async () => {\n    let actionSpy = jest.fn();\n    actionSpy.mockReturnValue({ ok: true });\n\n    let router = createBrowserRouter(\n      [\n        {\n          path: \"/\",\n          action: actionSpy,\n          Component() {\n            let fetcher = useFetcher();\n            return (\n              <button\n                onClick={() =>\n                  fetcher.submit(\n                    { tagName: \"div\", data: \"test\" },\n                    { method: \"post\" },\n                  )\n                }\n              >\n                Submit\n              </button>\n            );\n          },\n        },\n      ],\n      {\n        window: getWindow(\"/\"),\n      },\n    );\n\n    render(<RouterProvider router={router} />);\n    fireEvent.click(screen.getByText(\"Submit\"));\n    await waitFor(() => expect(actionSpy).toHaveBeenCalled());\n\n    let formData = await actionSpy.mock.calls[0][0].request.formData();\n    expect(formData.get(\"tagName\")).toBe(\"div\");\n    expect(formData.get(\"data\")).toBe(\"test\");\n  });\n\n  it(\"should handle plain object with various HTML element-like properties\", async () => {\n    let actionSpy = jest.fn();\n    actionSpy.mockReturnValue({ ok: true });\n\n    let router = createBrowserRouter(\n      [\n        {\n          path: \"/\",\n          action: actionSpy,\n          Component() {\n            let fetcher = useFetcher();\n            return (\n              <button\n                onClick={() =>\n                  fetcher.submit(\n                    {\n                      tagName: \"button\",\n                      className: \"test-class\",\n                      id: \"test-id\",\n                      value: \"test-value\",\n                    },\n                    { method: \"post\" },\n                  )\n                }\n              >\n                Submit\n              </button>\n            );\n          },\n        },\n      ],\n      {\n        window: getWindow(\"/\"),\n      },\n    );\n\n    render(<RouterProvider router={router} />);\n    fireEvent.click(screen.getByText(\"Submit\"));\n    await waitFor(() => expect(actionSpy).toHaveBeenCalled());\n\n    let formData = await actionSpy.mock.calls[0][0].request.formData();\n    expect(formData.get(\"tagName\")).toBe(\"button\");\n    expect(formData.get(\"className\")).toBe(\"test-class\");\n    expect(formData.get(\"id\")).toBe(\"test-id\");\n    expect(formData.get(\"value\")).toBe(\"test-value\");\n  });\n});\n"
  },
  {
    "path": "packages/react-router/__tests__/dom/flush-sync-navigations-test.tsx",
    "content": "import * as React from \"react\";\nimport { fireEvent, render, screen, waitFor } from \"@testing-library/react\";\n\nimport {\n  createBrowserRouter,\n  useNavigate,\n  useSubmit,\n  useFetcher,\n} from \"../../index\";\nimport { RouterProvider } from \"../../lib/dom-export/dom-router-provider\";\nimport getWindow from \"../utils/getWindow\";\n\ndescribe(\"flushSync\", () => {\n  it(\"wraps useNavigate updates in flushSync when specified\", async () => {\n    let router = createBrowserRouter(\n      [\n        {\n          path: \"/\",\n          Component() {\n            let navigate = useNavigate();\n            return (\n              <>\n                <h1>Home</h1>\n                <button onClick={() => navigate(\"/about\")}>Go to /about</button>\n              </>\n            );\n          },\n        },\n        {\n          path: \"/about\",\n          Component() {\n            let navigate = useNavigate();\n            return (\n              <>\n                <h1>About</h1>\n                <button onClick={() => navigate(\"/\", { flushSync: true })}>\n                  Go to /\n                </button>\n              </>\n            );\n          },\n        },\n      ],\n      {\n        window: getWindow(\"/\"),\n      },\n    );\n    render(<RouterProvider router={router} />);\n\n    // This isn't the best way to test this but it seems that startTransition is\n    // performing sync updates in the test/JSDOM/whatever environment which is\n    // not how it behaves in the live DOM :/\n    let spy = jest.fn();\n    router.subscribe(spy);\n\n    fireEvent.click(screen.getByText(\"Go to /about\"));\n    await waitFor(() => screen.getByText(\"About\"));\n    expect(spy).toHaveBeenCalledTimes(1);\n    expect(spy).toHaveBeenLastCalledWith(\n      expect.anything(),\n      expect.objectContaining({ flushSync: false }),\n    );\n\n    fireEvent.click(screen.getByText(\"Go to /\"));\n    await waitFor(() => screen.getByText(\"Home\"));\n    expect(spy).toHaveBeenLastCalledWith(\n      expect.anything(),\n      expect.objectContaining({ flushSync: true }),\n    );\n\n    expect(spy).toHaveBeenCalledTimes(2);\n\n    router.dispose();\n  });\n\n  it(\"wraps useSubmit updates in flushSync when specified\", async () => {\n    let router = createBrowserRouter(\n      [\n        {\n          path: \"/\",\n          action: () => null,\n          Component() {\n            let submit = useSubmit();\n            return (\n              <>\n                <h1>Home</h1>\n                <button\n                  onClick={() =>\n                    submit({}, { method: \"post\", action: \"/about\" })\n                  }\n                >\n                  Go to /about\n                </button>\n              </>\n            );\n          },\n        },\n        {\n          path: \"/about\",\n          action: () => null,\n          Component() {\n            let submit = useSubmit();\n            return (\n              <>\n                <h1>About</h1>\n                <button\n                  onClick={() =>\n                    submit({}, { method: \"post\", action: \"/\", flushSync: true })\n                  }\n                >\n                  Go to /\n                </button>\n              </>\n            );\n          },\n        },\n      ],\n      {\n        window: getWindow(\"/\"),\n      },\n    );\n    render(<RouterProvider router={router} />);\n\n    // This isn't the best way to test this but it seems that startTransition is\n    // performing sync updates in the test/JSDOM/whatever environment which is\n    // not how it behaves in the live DOM :/\n    let spy = jest.fn();\n    router.subscribe(spy);\n\n    fireEvent.click(screen.getByText(\"Go to /about\"));\n    await waitFor(() => screen.getByText(\"About\"));\n    expect(spy).toHaveBeenCalledTimes(2);\n    expect(spy.mock.calls[0][1].flushSync).toBe(false);\n    expect(spy.mock.calls[1][1].flushSync).toBe(false);\n\n    fireEvent.click(screen.getByText(\"Go to /\"));\n    await waitFor(() => screen.getByText(\"Home\"));\n    expect(spy).toHaveBeenCalledTimes(4);\n    expect(spy.mock.calls[2][1].flushSync).toBe(true);\n    expect(spy.mock.calls[3][1].flushSync).toBe(false);\n\n    router.dispose();\n  });\n\n  it(\"wraps fetcher.load updates in flushSync when specified\", async () => {\n    let router = createBrowserRouter(\n      [\n        {\n          path: \"/\",\n          Component() {\n            let fetcher1 = useFetcher();\n            let fetcher2 = useFetcher();\n            return (\n              <>\n                <h1>Home</h1>\n                <button onClick={() => fetcher1.load(\"/fetch\")}>\n                  Load async\n                </button>\n                <pre>{`async:${fetcher1.data}:${fetcher1.state}`}</pre>\n                <button\n                  onClick={() => fetcher2.load(\"/fetch\", { flushSync: true })}\n                >\n                  Load sync\n                </button>\n                <pre>{`sync:${fetcher2.data}:${fetcher2.state}`}</pre>\n              </>\n            );\n          },\n        },\n        {\n          path: \"/fetch\",\n          loader: async () => {\n            await new Promise((r) => setTimeout(r, 10));\n            return \"LOADER\";\n          },\n        },\n      ],\n      {\n        window: getWindow(\"/\"),\n      },\n    );\n    render(<RouterProvider router={router} />);\n\n    // This isn't the best way to test this but it seems that startTransition is\n    // performing sync updates in the test/JSDOM/whatever environment which is\n    // not how it behaves in the live DOM :/\n    let spy = jest.fn();\n    router.subscribe(spy);\n\n    fireEvent.click(screen.getByText(\"Load async\"));\n    await waitFor(() => screen.getByText(\"async:LOADER:idle\"));\n    expect(spy).toHaveBeenCalledTimes(2);\n    expect(spy.mock.calls[0][1].flushSync).toBe(false);\n    expect(spy.mock.calls[1][1].flushSync).toBe(false);\n\n    fireEvent.click(screen.getByText(\"Load sync\"));\n    await waitFor(() => screen.getByText(\"sync:LOADER:idle\"));\n    expect(spy).toHaveBeenCalledTimes(4);\n    expect(spy.mock.calls[2][1].flushSync).toBe(true);\n    expect(spy.mock.calls[3][1].flushSync).toBe(false);\n\n    router.dispose();\n  });\n\n  it(\"wraps fetcher.submit updates in flushSync when specified\", async () => {\n    let router = createBrowserRouter(\n      [\n        {\n          path: \"/\",\n          action: () => \"ACTION\",\n          Component() {\n            let fetcher1 = useFetcher();\n            let fetcher2 = useFetcher();\n            return (\n              <>\n                <h1>Home</h1>\n                <button\n                  onClick={() =>\n                    fetcher1.submit({}, { method: \"post\", action: \"/\" })\n                  }\n                >\n                  Submit async\n                </button>\n                <pre>{`async:${fetcher1.data}:${fetcher1.state}`}</pre>\n                <button\n                  onClick={() =>\n                    fetcher2.submit(\n                      {},\n                      { method: \"post\", action: \"/\", flushSync: true },\n                    )\n                  }\n                >\n                  Submit sync\n                </button>\n                <pre>{`sync:${fetcher2.data}:${fetcher2.state}`}</pre>\n              </>\n            );\n          },\n        },\n      ],\n      {\n        window: getWindow(\"/\"),\n      },\n    );\n    render(<RouterProvider router={router} />);\n\n    // This isn't the best way to test this but it seems that startTransition is\n    // performing sync updates in the test/JSDOM/whatever environment which is\n    // not how it behaves in the live DOM :/\n    let spy = jest.fn();\n    router.subscribe(spy);\n\n    fireEvent.click(screen.getByText(\"Submit async\"));\n    await waitFor(() => screen.getByText(\"async:ACTION:idle\"));\n    expect(spy).toHaveBeenCalledTimes(3);\n    expect(spy.mock.calls[0][1].flushSync).toBe(false);\n    expect(spy.mock.calls[1][1].flushSync).toBe(false);\n    expect(spy.mock.calls[2][1].flushSync).toBe(false);\n\n    fireEvent.click(screen.getByText(\"Submit sync\"));\n    await waitFor(() => screen.getByText(\"sync:ACTION:idle\"));\n    expect(spy).toHaveBeenCalledTimes(6);\n    expect(spy.mock.calls[3][1].flushSync).toBe(true);\n    expect(spy.mock.calls[4][1].flushSync).toBe(false);\n    expect(spy.mock.calls[5][1].flushSync).toBe(false);\n\n    router.dispose();\n  });\n});\n"
  },
  {
    "path": "packages/react-router/__tests__/dom/link-click-test.tsx",
    "content": "import * as React from \"react\";\nimport * as ReactDOM from \"react-dom/client\";\nimport { act } from \"@testing-library/react\";\nimport { MemoryRouter, Routes, Route, Link } from \"../../index\";\n\nfunction click(anchor: HTMLAnchorElement, eventInit?: MouseEventInit) {\n  let event = new MouseEvent(\"click\", {\n    view: window,\n    bubbles: true,\n    cancelable: true,\n    ...eventInit,\n  });\n  anchor.dispatchEvent(event);\n  return event;\n}\n\ndescribe(\"A <Link> click\", () => {\n  let node: HTMLDivElement;\n  beforeEach(() => {\n    node = document.createElement(\"div\");\n    document.body.appendChild(node);\n  });\n\n  afterEach(() => {\n    document.body.removeChild(node);\n    node = null!;\n  });\n\n  it(\"navigates to the new page\", () => {\n    function Home() {\n      return (\n        <div>\n          <h1>Home</h1>\n          <Link to=\"../about\">About</Link>\n        </div>\n      );\n    }\n\n    act(() => {\n      ReactDOM.createRoot(node).render(\n        <MemoryRouter initialEntries={[\"/home\"]}>\n          <Routes>\n            <Route path=\"home\" element={<Home />} />\n            <Route path=\"about\" element={<h1>About</h1>} />\n          </Routes>\n        </MemoryRouter>,\n      );\n    });\n\n    let anchor = node.querySelector(\"a\");\n    expect(anchor).not.toBeNull();\n\n    let event: MouseEvent;\n    act(() => {\n      event = click(anchor);\n    });\n\n    expect(event.defaultPrevented).toBe(true);\n    let h1 = node.querySelector(\"h1\");\n    expect(h1).not.toBeNull();\n    expect(h1?.textContent).toEqual(\"About\");\n  });\n\n  it(\"navigates to the new page when using an absolute URL on the same origin\", () => {\n    function Home() {\n      return (\n        <div>\n          <h1>Home</h1>\n          <Link to=\"http://localhost/about\">About</Link>\n        </div>\n      );\n    }\n\n    act(() => {\n      ReactDOM.createRoot(node).render(\n        <MemoryRouter initialEntries={[\"/home\"]}>\n          <Routes>\n            <Route path=\"home\" element={<Home />} />\n            <Route path=\"about\" element={<h1>About</h1>} />\n          </Routes>\n        </MemoryRouter>,\n      );\n    });\n\n    let anchor = node.querySelector(\"a\");\n    expect(anchor).not.toBeNull();\n\n    let event: MouseEvent;\n    act(() => {\n      event = click(anchor);\n    });\n\n    expect(event.defaultPrevented).toBe(true);\n    let h1 = node.querySelector(\"h1\");\n    expect(h1).not.toBeNull();\n    expect(h1?.textContent).toEqual(\"About\");\n  });\n\n  describe(\"when an external absolute URL is specified\", () => {\n    it(\"does not prevent default\", () => {\n      function Home() {\n        return (\n          <div>\n            <h1>Home</h1>\n            <Link to=\"https://remix.run\">About</Link>\n          </div>\n        );\n      }\n\n      act(() => {\n        ReactDOM.createRoot(node).render(\n          <MemoryRouter initialEntries={[\"/home\"]}>\n            <Routes>\n              <Route path=\"home\" element={<Home />} />\n              <Route path=\"about\" element={<h1>About</h1>} />\n            </Routes>\n          </MemoryRouter>,\n        );\n      });\n\n      let anchor = node.querySelector(\"a\");\n      expect(anchor).not.toBeNull();\n\n      let event: MouseEvent;\n      act(() => {\n        event = click(anchor);\n      });\n\n      expect(event.defaultPrevented).toBe(false);\n    });\n\n    it(\"calls provided listener\", () => {\n      let handlerCalled;\n      let defaultPrevented;\n\n      function Home() {\n        return (\n          <div>\n            <h1>Home</h1>\n            <Link\n              to=\"https://remix.run\"\n              onClick={(e) => {\n                handlerCalled = true;\n                defaultPrevented = e.defaultPrevented;\n              }}\n            >\n              About\n            </Link>\n          </div>\n        );\n      }\n\n      act(() => {\n        ReactDOM.createRoot(node).render(\n          <MemoryRouter initialEntries={[\"/home\"]}>\n            <Routes>\n              <Route path=\"home\" element={<Home />} />\n              <Route path=\"about\" element={<h1>About</h1>} />\n            </Routes>\n          </MemoryRouter>,\n        );\n      });\n\n      act(() => {\n        click(node.querySelector(\"a\"));\n      });\n\n      expect(handlerCalled).toBe(true);\n      expect(defaultPrevented).toBe(false);\n    });\n  });\n\n  describe(\"when a same-origin/different-basename absolute URL is specified\", () => {\n    it(\"does not prevent default\", () => {\n      function Home() {\n        return (\n          <div>\n            <h1>Home</h1>\n            <Link to=\"http://localhost/not/base\">About</Link>\n          </div>\n        );\n      }\n\n      act(() => {\n        ReactDOM.createRoot(node).render(\n          <MemoryRouter initialEntries={[\"/base/home\"]} basename=\"/base\">\n            <Routes>\n              <Route path=\"home\" element={<Home />} />\n            </Routes>\n          </MemoryRouter>,\n        );\n      });\n\n      let anchor = node.querySelector(\"a\");\n      expect(anchor).not.toBeNull();\n\n      let event: MouseEvent;\n      act(() => {\n        event = click(anchor);\n      });\n\n      expect(event.defaultPrevented).toBe(false);\n    });\n\n    it(\"calls provided listener\", () => {\n      let handlerCalled;\n      let defaultPrevented;\n\n      function Home() {\n        return (\n          <div>\n            <h1>Home</h1>\n            <Link\n              to=\"http://localhost/not/base\"\n              onClick={(e) => {\n                handlerCalled = true;\n                defaultPrevented = e.defaultPrevented;\n              }}\n            >\n              About\n            </Link>\n          </div>\n        );\n      }\n\n      act(() => {\n        ReactDOM.createRoot(node).render(\n          <MemoryRouter initialEntries={[\"/base/home\"]} basename=\"/base\">\n            <Routes>\n              <Route path=\"home\" element={<Home />} />\n            </Routes>\n          </MemoryRouter>,\n        );\n      });\n\n      act(() => {\n        click(node.querySelector(\"a\"));\n      });\n\n      expect(handlerCalled).toBe(true);\n      expect(defaultPrevented).toBe(false);\n    });\n  });\n\n  describe(\"when reloadDocument is specified\", () => {\n    it(\"does not prevent default\", () => {\n      function Home() {\n        return (\n          <div>\n            <h1>Home</h1>\n            <Link reloadDocument to=\"../about\">\n              About\n            </Link>\n          </div>\n        );\n      }\n\n      act(() => {\n        ReactDOM.createRoot(node).render(\n          <MemoryRouter initialEntries={[\"/home\"]}>\n            <Routes>\n              <Route path=\"home\" element={<Home />} />\n              <Route path=\"about\" element={<h1>About</h1>} />\n            </Routes>\n          </MemoryRouter>,\n        );\n      });\n\n      let anchor = node.querySelector(\"a\");\n      expect(anchor).not.toBeNull();\n\n      let event: MouseEvent;\n      act(() => {\n        event = click(anchor);\n      });\n\n      expect(event.defaultPrevented).toBe(false);\n    });\n\n    it(\"calls provided listener\", () => {\n      let handlerCalled;\n      let defaultPrevented;\n\n      function Home() {\n        return (\n          <div>\n            <h1>Home</h1>\n            <Link\n              reloadDocument\n              to=\"../about\"\n              onClick={(e) => {\n                handlerCalled = true;\n                defaultPrevented = e.defaultPrevented;\n              }}\n            >\n              About\n            </Link>\n          </div>\n        );\n      }\n\n      act(() => {\n        ReactDOM.createRoot(node).render(\n          <MemoryRouter initialEntries={[\"/home\"]}>\n            <Routes>\n              <Route path=\"home\" element={<Home />} />\n              <Route path=\"about\" element={<h1>About</h1>} />\n            </Routes>\n          </MemoryRouter>,\n        );\n      });\n\n      act(() => {\n        click(node.querySelector(\"a\"));\n      });\n\n      expect(handlerCalled).toBe(true);\n      expect(defaultPrevented).toBe(false);\n    });\n  });\n\n  describe(\"when preventDefault is used on the click handler\", () => {\n    it(\"stays on the same page\", () => {\n      function Home() {\n        function handleClick(event: React.MouseEvent<HTMLAnchorElement>) {\n          event.preventDefault();\n        }\n\n        return (\n          <div>\n            <h1>Home</h1>\n            <Link to=\"../about\" onClick={handleClick}>\n              About\n            </Link>\n          </div>\n        );\n      }\n\n      act(() => {\n        ReactDOM.createRoot(node).render(\n          <MemoryRouter initialEntries={[\"/home\"]}>\n            <Routes>\n              <Route path=\"home\" element={<Home />} />\n              <Route path=\"about\" element={<h1>About</h1>} />\n            </Routes>\n          </MemoryRouter>,\n        );\n      });\n\n      let anchor = node.querySelector(\"a\");\n      expect(anchor).not.toBeNull();\n\n      act(() => {\n        click(anchor);\n      });\n\n      let h1 = node.querySelector(\"h1\");\n      expect(h1).not.toBeNull();\n      expect(h1?.textContent).toEqual(\"Home\");\n    });\n  });\n\n  describe(\"with a right click\", () => {\n    it(\"stays on the same page\", () => {\n      function Home() {\n        return (\n          <div>\n            <h1>Home</h1>\n            <Link to=\"../about\">About</Link>\n          </div>\n        );\n      }\n\n      act(() => {\n        ReactDOM.createRoot(node).render(\n          <MemoryRouter initialEntries={[\"/home\"]}>\n            <Routes>\n              <Route path=\"home\" element={<Home />} />\n              <Route path=\"about\" element={<h1>About</h1>} />\n            </Routes>\n          </MemoryRouter>,\n        );\n      });\n\n      let anchor = node.querySelector(\"a\");\n      expect(anchor).not.toBeNull();\n\n      act(() => {\n        // https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/button\n        let RightMouseButton = 2;\n        click(anchor, { button: RightMouseButton });\n      });\n\n      let h1 = node.querySelector(\"h1\");\n      expect(h1).not.toBeNull();\n      expect(h1?.textContent).toEqual(\"Home\");\n    });\n  });\n\n  describe(\"when the link is supposed to open in a new window\", () => {\n    it(\"stays on the same page\", () => {\n      function Home() {\n        return (\n          <div>\n            <h1>Home</h1>\n            <Link to=\"../about\" target=\"_blank\">\n              About\n            </Link>\n          </div>\n        );\n      }\n\n      act(() => {\n        ReactDOM.createRoot(node).render(\n          <MemoryRouter initialEntries={[\"/home\"]}>\n            <Routes>\n              <Route path=\"home\" element={<Home />} />\n              <Route path=\"about\" element={<h1>About</h1>} />\n            </Routes>\n          </MemoryRouter>,\n        );\n      });\n\n      let anchor = node.querySelector(\"a\");\n      expect(anchor).not.toBeNull();\n\n      act(() => {\n        click(anchor);\n      });\n\n      let h1 = node.querySelector(\"h1\");\n      expect(h1).not.toBeNull();\n      expect(h1?.textContent).toEqual(\"Home\");\n    });\n  });\n\n  describe(\"when the modifier keys are used\", () => {\n    it(\"stays on the same page\", () => {\n      function Home() {\n        return (\n          <div>\n            <h1>Home</h1>\n            <Link to=\"../about\">About</Link>\n          </div>\n        );\n      }\n\n      act(() => {\n        ReactDOM.createRoot(node).render(\n          <MemoryRouter initialEntries={[\"/home\"]}>\n            <Routes>\n              <Route path=\"home\" element={<Home />} />\n              <Route path=\"about\" element={<h1>About</h1>} />\n            </Routes>\n          </MemoryRouter>,\n        );\n      });\n\n      let anchor = node.querySelector(\"a\");\n      expect(anchor).not.toBeNull();\n\n      act(() => {\n        click(anchor, { ctrlKey: true });\n      });\n\n      let h1 = node.querySelector(\"h1\");\n      expect(h1).not.toBeNull();\n      expect(h1?.textContent).toEqual(\"Home\");\n    });\n  });\n});\n"
  },
  {
    "path": "packages/react-router/__tests__/dom/link-href-test.tsx",
    "content": "import * as React from \"react\";\nimport {\n  BrowserRouter,\n  HashRouter,\n  Link,\n  MemoryRouter,\n  Outlet,\n  Route,\n  RouterProvider,\n  Routes,\n  createBrowserRouter,\n  createHashRouter,\n} from \"../../index\";\nimport * as TestRenderer from \"react-test-renderer\";\n\ndescribe(\"<Link> href\", () => {\n  describe(\"in a static route\", () => {\n    test(\"absolute <Link to> resolves relative to the root URL\", () => {\n      let renderer: TestRenderer.ReactTestRenderer;\n      TestRenderer.act(() => {\n        renderer = TestRenderer.create(\n          <MemoryRouter initialEntries={[\"/inbox\"]}>\n            <Routes>\n              <Route path=\"inbox\" element={<Link to=\"/about\" />} />\n            </Routes>\n          </MemoryRouter>,\n        );\n      });\n\n      expect(renderer.root.findByType(\"a\").props.href).toEqual(\"/about\");\n    });\n\n    test('<Link to=\".\"> resolves relative to the current route', () => {\n      let renderer: TestRenderer.ReactTestRenderer;\n      TestRenderer.act(() => {\n        renderer = TestRenderer.create(\n          <MemoryRouter initialEntries={[\"/inbox\"]}>\n            <Routes>\n              <Route path=\"inbox\" element={<Link to=\".\" />} />\n            </Routes>\n          </MemoryRouter>,\n        );\n      });\n\n      expect(renderer.root.findByType(\"a\").props.href).toEqual(\"/inbox\");\n    });\n\n    test('<Link to=\"..\"> resolves relative to the parent route', () => {\n      let renderer: TestRenderer.ReactTestRenderer;\n      TestRenderer.act(() => {\n        renderer = TestRenderer.create(\n          <MemoryRouter initialEntries={[\"/inbox/messages\"]}>\n            <Routes>\n              <Route path=\"inbox\">\n                <Route path=\"messages\" element={<Link to=\"..\" />} />\n              </Route>\n            </Routes>\n          </MemoryRouter>,\n        );\n      });\n\n      expect(renderer.root.findByType(\"a\").props.href).toEqual(\"/inbox\");\n    });\n\n    test('<Link to=\"..\"> with more .. segments than parent routes resolves to the root URL', () => {\n      let renderer: TestRenderer.ReactTestRenderer;\n      TestRenderer.act(() => {\n        renderer = TestRenderer.create(\n          <MemoryRouter initialEntries={[\"/inbox/messages\"]}>\n            <Routes>\n              <Route path=\"inbox\">\n                <Route\n                  path=\"messages\"\n                  element={\n                    <>\n                      <Link to=\"../../about\" />\n                      {/* traverse past the root */}\n                      <Link to=\"../../../about\" />\n                    </>\n                  }\n                />\n              </Route>\n            </Routes>\n          </MemoryRouter>,\n        );\n      });\n\n      expect(renderer.root.findAllByType(\"a\").map((a) => a.props.href)).toEqual(\n        [\"/about\", \"/about\"],\n      );\n    });\n\n    test('<Link to=\"https://remix.run\"> is treated as external link', () => {\n      let renderer: TestRenderer.ReactTestRenderer;\n      TestRenderer.act(() => {\n        renderer = TestRenderer.create(\n          <MemoryRouter initialEntries={[\"/inbox/messages\"]}>\n            <Routes>\n              <Route path=\"inbox\">\n                <Route\n                  path=\"messages\"\n                  element={<Link to=\"https://remix.run\" />}\n                />\n              </Route>\n            </Routes>\n          </MemoryRouter>,\n        );\n      });\n\n      expect(renderer.root.findByType(\"a\").props.href).toEqual(\n        \"https://remix.run\",\n      );\n    });\n\n    test('<Link to=\"//remix.run\"> is treated as external link', () => {\n      let renderer: TestRenderer.ReactTestRenderer;\n      TestRenderer.act(() => {\n        renderer = TestRenderer.create(\n          <MemoryRouter initialEntries={[\"/inbox/messages\"]}>\n            <Routes>\n              <Route path=\"inbox\">\n                <Route path=\"messages\" element={<Link to=\"//remix.run\" />} />\n              </Route>\n            </Routes>\n          </MemoryRouter>,\n        );\n      });\n\n      expect(renderer.root.findByType(\"a\").props.href).toEqual(\"//remix.run\");\n    });\n\n    test('<Link to=\"mailto:remix@example.com\"> is treated as external link', () => {\n      let renderer: TestRenderer.ReactTestRenderer;\n      TestRenderer.act(() => {\n        renderer = TestRenderer.create(\n          <MemoryRouter initialEntries={[\"/inbox/messages\"]}>\n            <Routes>\n              <Route path=\"inbox\">\n                <Route\n                  path=\"messages\"\n                  element={<Link to=\"mailto:remix@example.com\" />}\n                />\n              </Route>\n            </Routes>\n          </MemoryRouter>,\n        );\n      });\n\n      expect(renderer.root.findByType(\"a\").props.href).toEqual(\n        \"mailto:remix@example.com\",\n      );\n    });\n\n    test('<Link to=\"web+remix://somepath\"> is treated as external link', () => {\n      let renderer: TestRenderer.ReactTestRenderer;\n      TestRenderer.act(() => {\n        renderer = TestRenderer.create(\n          <MemoryRouter initialEntries={[\"/inbox/messages\"]}>\n            <Routes>\n              <Route path=\"inbox\">\n                <Route\n                  path=\"messages\"\n                  element={<Link to=\"web+remix://somepath\" />}\n                />\n              </Route>\n            </Routes>\n          </MemoryRouter>,\n        );\n      });\n\n      expect(renderer.root.findByType(\"a\").props.href).toEqual(\n        \"web+remix://somepath\",\n      );\n    });\n\n    test('<Link to=\"http://localhost/inbox\"> is treated as an absolute link', () => {\n      let renderer: TestRenderer.ReactTestRenderer;\n      TestRenderer.act(() => {\n        renderer = TestRenderer.create(\n          <MemoryRouter initialEntries={[\"/inbox/messages\"]}>\n            <Routes>\n              <Route path=\"inbox\">\n                <Route\n                  path=\"messages\"\n                  element={<Link to=\"http://localhost/inbox\" />}\n                />\n              </Route>\n            </Routes>\n          </MemoryRouter>,\n        );\n      });\n\n      expect(renderer.root.findByType(\"a\").props.href).toEqual(\n        \"http://localhost/inbox\",\n      );\n    });\n\n    test(\"<Link to=\\\"{ search: 'key=value'\\\"> is handled with the current pathname\", () => {\n      let renderer: TestRenderer.ReactTestRenderer;\n      TestRenderer.act(() => {\n        renderer = TestRenderer.create(\n          <MemoryRouter initialEntries={[\"/inbox/messages\"]}>\n            <Routes>\n              <Route path=\"inbox\">\n                <Route\n                  path=\"messages\"\n                  element={<Link to={{ search: \"key=value\" }} />}\n                />\n              </Route>\n            </Routes>\n          </MemoryRouter>,\n        );\n      });\n\n      expect(renderer.root.findByType(\"a\").props.href).toEqual(\n        \"/inbox/messages?key=value\",\n      );\n    });\n\n    test(\"<Link to=\\\"{ hash: 'hash'\\\"> is handled with the current pathname\", () => {\n      let renderer: TestRenderer.ReactTestRenderer;\n      TestRenderer.act(() => {\n        renderer = TestRenderer.create(\n          <MemoryRouter initialEntries={[\"/inbox/messages\"]}>\n            <Routes>\n              <Route path=\"inbox\">\n                <Route\n                  path=\"messages\"\n                  element={<Link to={{ hash: \"hash\" }} />}\n                />\n              </Route>\n            </Routes>\n          </MemoryRouter>,\n        );\n      });\n\n      expect(renderer.root.findByType(\"a\").props.href).toEqual(\n        \"/inbox/messages#hash\",\n      );\n    });\n  });\n\n  describe(\"in a dynamic route\", () => {\n    test(\"absolute <Link to> resolves relative to the root URL\", () => {\n      let renderer: TestRenderer.ReactTestRenderer;\n      TestRenderer.act(() => {\n        renderer = TestRenderer.create(\n          <MemoryRouter initialEntries={[\"/inbox/messages/abc\"]}>\n            <Routes>\n              <Route path=\"inbox\">\n                <Route path=\"messages/:id\" element={<Link to=\"/about\" />} />\n              </Route>\n            </Routes>\n          </MemoryRouter>,\n        );\n      });\n\n      expect(renderer.root.findByType(\"a\").props.href).toEqual(\"/about\");\n    });\n\n    test('<Link to=\".\"> resolves relative to the current route', () => {\n      let renderer: TestRenderer.ReactTestRenderer;\n      TestRenderer.act(() => {\n        renderer = TestRenderer.create(\n          <MemoryRouter initialEntries={[\"/inbox/messages/abc\"]}>\n            <Routes>\n              <Route path=\"inbox\">\n                <Route path=\"messages/:id\" element={<Link to=\".\" />} />\n              </Route>\n            </Routes>\n          </MemoryRouter>,\n        );\n      });\n\n      expect(renderer.root.findByType(\"a\").props.href).toEqual(\n        \"/inbox/messages/abc\",\n      );\n    });\n\n    test('<Link to=\"..\"> resolves relative to the parent route', () => {\n      let renderer: TestRenderer.ReactTestRenderer;\n      TestRenderer.act(() => {\n        renderer = TestRenderer.create(\n          <MemoryRouter initialEntries={[\"/inbox/messages/abc\"]}>\n            <Routes>\n              <Route path=\"inbox\">\n                <Route path=\"messages/:id\" element={<Link to=\"..\" />} />\n              </Route>\n            </Routes>\n          </MemoryRouter>,\n        );\n      });\n\n      expect(renderer.root.findByType(\"a\").props.href).toEqual(\"/inbox\");\n    });\n\n    test('<Link to=\"..\"> with more .. segments than parent routes resolves to the root URL', () => {\n      let renderer: TestRenderer.ReactTestRenderer;\n      TestRenderer.act(() => {\n        renderer = TestRenderer.create(\n          <MemoryRouter initialEntries={[\"/inbox/messages/abc\"]}>\n            <Routes>\n              <Route path=\"inbox\">\n                <Route\n                  path=\"messages/:id\"\n                  element={<Link to=\"../../about\" />}\n                />\n              </Route>\n            </Routes>\n          </MemoryRouter>,\n        );\n      });\n\n      expect(renderer.root.findByType(\"a\").props.href).toEqual(\"/about\");\n    });\n  });\n\n  describe(\"in an index route\", () => {\n    test(\"absolute <Link to> resolves relative to the root URL\", () => {\n      let renderer: TestRenderer.ReactTestRenderer;\n      TestRenderer.act(() => {\n        renderer = TestRenderer.create(\n          <MemoryRouter initialEntries={[\"/inbox\"]}>\n            <Routes>\n              <Route path=\"inbox\">\n                <Route index element={<Link to=\"/home\" />} />\n              </Route>\n            </Routes>\n          </MemoryRouter>,\n        );\n      });\n\n      expect(renderer.root.findByType(\"a\").props.href).toEqual(\"/home\");\n    });\n\n    test('<Link to=\".\"> resolves relative to the current route', () => {\n      let renderer: TestRenderer.ReactTestRenderer;\n      TestRenderer.act(() => {\n        renderer = TestRenderer.create(\n          <MemoryRouter initialEntries={[\"/inbox\"]}>\n            <Routes>\n              <Route path=\"inbox\">\n                <Route index element={<Link to=\".\" />} />\n              </Route>\n            </Routes>\n          </MemoryRouter>,\n        );\n      });\n\n      expect(renderer.root.findByType(\"a\").props.href).toEqual(\"/inbox\");\n    });\n\n    test('<Link to=\"..\"> resolves relative to the parent route (ignoring the index route)', () => {\n      let renderer: TestRenderer.ReactTestRenderer;\n      TestRenderer.act(() => {\n        renderer = TestRenderer.create(\n          <MemoryRouter initialEntries={[\"/inbox\"]}>\n            <Routes>\n              <Route path=\"inbox\">\n                <Route index element={<Link to=\"..\" />} />\n              </Route>\n            </Routes>\n          </MemoryRouter>,\n        );\n      });\n\n      expect(renderer.root.findByType(\"a\").props.href).toEqual(\"/\");\n    });\n\n    test('<Link to=\"..\"> with more .. segments than parent routes resolves to the root URL', () => {\n      let renderer: TestRenderer.ReactTestRenderer;\n      TestRenderer.act(() => {\n        renderer = TestRenderer.create(\n          <MemoryRouter initialEntries={[\"/inbox\"]}>\n            <Routes>\n              <Route path=\"inbox\">\n                <Route\n                  index\n                  element={\n                    <>\n                      <Link to=\"../../about\" />\n                      {/* traverse past the root */}\n                      <Link to=\"../../../about\" />\n                    </>\n                  }\n                />\n              </Route>\n            </Routes>\n          </MemoryRouter>,\n        );\n      });\n\n      expect(renderer.root.findAllByType(\"a\").map((a) => a.props.href)).toEqual(\n        [\"/about\", \"/about\"],\n      );\n    });\n  });\n\n  describe(\"in a layout route\", () => {\n    function MessagesLayout({ link }: { link: React.ReactElement }) {\n      return (\n        <div>\n          {link}\n          <Outlet />\n        </div>\n      );\n    }\n\n    test(\"absolute <Link to> resolves relative to the root URL\", () => {\n      let renderer: TestRenderer.ReactTestRenderer;\n      TestRenderer.act(() => {\n        renderer = TestRenderer.create(\n          <MemoryRouter initialEntries={[\"/inbox/messages\"]}>\n            <Routes>\n              <Route path=\"inbox\">\n                <Route element={<MessagesLayout link={<Link to=\"/home\" />} />}>\n                  <Route path=\"messages\" element={<h1>Messages</h1>} />\n                </Route>\n              </Route>\n            </Routes>\n          </MemoryRouter>,\n        );\n      });\n\n      expect(renderer.root.findByType(\"a\").props.href).toEqual(\"/home\");\n    });\n\n    test('<Link to=\".\"> resolves relative to the current route', () => {\n      let renderer: TestRenderer.ReactTestRenderer;\n      TestRenderer.act(() => {\n        renderer = TestRenderer.create(\n          <MemoryRouter initialEntries={[\"/inbox/messages\"]}>\n            <Routes>\n              <Route path=\"inbox\">\n                <Route element={<MessagesLayout link={<Link to=\".\" />} />}>\n                  <Route path=\"messages\" element={<h1>Messages</h1>} />\n                </Route>\n              </Route>\n            </Routes>\n          </MemoryRouter>,\n        );\n      });\n\n      expect(renderer.root.findByType(\"a\").props.href).toEqual(\"/inbox\");\n    });\n\n    test('<Link to=\"..\"> resolves relative to the parent route (ignoring the pathless route)', () => {\n      let renderer: TestRenderer.ReactTestRenderer;\n      TestRenderer.act(() => {\n        renderer = TestRenderer.create(\n          <MemoryRouter initialEntries={[\"/inbox/messages\"]}>\n            <Routes>\n              <Route path=\"inbox\">\n                <Route element={<MessagesLayout link={<Link to=\"..\" />} />}>\n                  <Route path=\"messages\" element={<h1>Messages</h1>} />\n                </Route>\n              </Route>\n            </Routes>\n          </MemoryRouter>,\n        );\n      });\n\n      expect(renderer.root.findByType(\"a\").props.href).toEqual(\"/\");\n    });\n\n    test('<Link to=\"..\"> with more .. segments than parent routes resolves to the root URL', () => {\n      let renderer: TestRenderer.ReactTestRenderer;\n      TestRenderer.act(() => {\n        renderer = TestRenderer.create(\n          <MemoryRouter initialEntries={[\"/inbox/messages\"]}>\n            <Routes>\n              <Route path=\"inbox\">\n                <Route\n                  element={\n                    <MessagesLayout\n                      link={\n                        <>\n                          <Link to=\"../../about\" />\n                          {/* traverse past the root */}\n                          <Link to=\"../../../about\" />\n                        </>\n                      }\n                    />\n                  }\n                >\n                  <Route path=\"messages\" element={<h1>Messages</h1>} />\n                </Route>\n              </Route>\n            </Routes>\n          </MemoryRouter>,\n        );\n      });\n\n      expect(renderer.root.findAllByType(\"a\").map((a) => a.props.href)).toEqual(\n        [\"/about\", \"/about\"],\n      );\n    });\n  });\n\n  describe(\"in a splat route\", () => {\n    test(\"absolute <Link to> resolves relative to the root URL\", () => {\n      let renderer: TestRenderer.ReactTestRenderer;\n      TestRenderer.act(() => {\n        renderer = TestRenderer.create(\n          <MemoryRouter initialEntries={[\"/inbox/messages/123\"]}>\n            <Routes>\n              <Route path=\"inbox\">\n                <Route path=\"messages/*\" element={<Link to=\"/about\" />} />\n              </Route>\n            </Routes>\n          </MemoryRouter>,\n        );\n      });\n\n      expect(renderer.root.findByType(\"a\").props.href).toEqual(\"/about\");\n    });\n\n    test('<Link to=\".\"> resolves relative to the current route', () => {\n      let renderer: TestRenderer.ReactTestRenderer;\n      TestRenderer.act(() => {\n        renderer = TestRenderer.create(\n          <MemoryRouter initialEntries={[\"/inbox/messages/abc\"]}>\n            <Routes>\n              <Route path=\"inbox\">\n                <Route path=\"messages/*\" element={<Link to=\".\" />} />\n              </Route>\n            </Routes>\n          </MemoryRouter>,\n        );\n      });\n\n      expect(renderer.root.findByType(\"a\").props.href).toEqual(\n        \"/inbox/messages/abc\",\n      );\n    });\n\n    test('<Link to=\"..\"> resolves relative to the parent route', () => {\n      let renderer: TestRenderer.ReactTestRenderer;\n      TestRenderer.act(() => {\n        renderer = TestRenderer.create(\n          <MemoryRouter initialEntries={[\"/inbox/messages/abc\"]}>\n            <Routes>\n              <Route path=\"inbox\">\n                <Route path=\"messages/*\" element={<Link to=\"..\" />} />\n              </Route>\n            </Routes>\n          </MemoryRouter>,\n        );\n      });\n\n      expect(renderer.root.findByType(\"a\").props.href).toEqual(\"/inbox\");\n    });\n\n    test(\"<Link to> pointing to a sibling route resolves relative to its parent route\", () => {\n      let renderer: TestRenderer.ReactTestRenderer;\n      TestRenderer.act(() => {\n        renderer = TestRenderer.create(\n          <MemoryRouter initialEntries={[\"/inbox/messages/abc\"]}>\n            <Routes>\n              <Route path=\"inbox\">\n                <Route\n                  path=\"messages/*\"\n                  element={<Link to=\"../messages/def\" />}\n                />\n              </Route>\n            </Routes>\n          </MemoryRouter>,\n        );\n      });\n\n      expect(renderer.root.findByType(\"a\").props.href).toEqual(\n        \"/inbox/messages/def\",\n      );\n    });\n\n    test('<Link to=\"..\"> with more .. segments than parent routes resolves to the root URL', () => {\n      let renderer: TestRenderer.ReactTestRenderer;\n      TestRenderer.act(() => {\n        renderer = TestRenderer.create(\n          <MemoryRouter initialEntries={[\"/inbox/messages\"]}>\n            <Routes>\n              <Route path=\"inbox\">\n                <Route\n                  path=\"messages/*\"\n                  element={\n                    <>\n                      <Link to=\"../../about\" />\n                      {/* traverse past the root */}\n                      <Link to=\"../../../about\" />\n                    </>\n                  }\n                />\n              </Route>\n            </Routes>\n          </MemoryRouter>,\n        );\n      });\n\n      expect(renderer.root.findAllByType(\"a\").map((a) => a.props.href)).toEqual(\n        [\"/about\", \"/about\"],\n      );\n    });\n  });\n\n  describe(\"under a <Router basename>\", () => {\n    test(\"absolute <Link to> resolves relative to the basename\", () => {\n      let renderer: TestRenderer.ReactTestRenderer;\n      TestRenderer.act(() => {\n        renderer = TestRenderer.create(\n          <MemoryRouter basename=\"/app\" initialEntries={[\"/app/inbox\"]}>\n            <Routes>\n              <Route path=\"inbox\" element={<Link to=\"/about\" />} />\n            </Routes>\n          </MemoryRouter>,\n        );\n      });\n\n      expect(renderer.root.findByType(\"a\").props.href).toEqual(\"/app/about\");\n    });\n\n    test('<Link to=\".\"> resolves relative to the current route', () => {\n      let renderer: TestRenderer.ReactTestRenderer;\n      TestRenderer.act(() => {\n        renderer = TestRenderer.create(\n          <MemoryRouter basename=\"/app\" initialEntries={[\"/app/inbox\"]}>\n            <Routes>\n              <Route path=\"inbox\" element={<Link to=\".\" />} />\n            </Routes>\n          </MemoryRouter>,\n        );\n      });\n\n      expect(renderer.root.findByType(\"a\").props.href).toEqual(\"/app/inbox\");\n    });\n\n    test('<Link to=\"..\"> with no parent route resolves relative to the basename', () => {\n      let renderer: TestRenderer.ReactTestRenderer;\n      TestRenderer.act(() => {\n        renderer = TestRenderer.create(\n          <MemoryRouter basename=\"/app\" initialEntries={[\"/app/inbox\"]}>\n            <Routes>\n              <Route path=\"inbox\" element={<Link to=\"../about\" />} />\n            </Routes>\n          </MemoryRouter>,\n        );\n      });\n\n      expect(renderer.root.findByType(\"a\").props.href).toEqual(\"/app/about\");\n    });\n  });\n\n  describe(\"in a descendant <Routes>\", () => {\n    test(\"absolute <Link to> resolves relative to the root URL\", () => {\n      let renderer: TestRenderer.ReactTestRenderer;\n      TestRenderer.act(() => {\n        renderer = TestRenderer.create(\n          <MemoryRouter initialEntries={[\"/auth/login\"]}>\n            <Routes>\n              <Route\n                path=\"auth/*\"\n                element={\n                  <Routes>\n                    <Route\n                      path=\"login\"\n                      element={<Link to=\"/auth/forgot-password\" />}\n                    />\n                  </Routes>\n                }\n              />\n            </Routes>\n          </MemoryRouter>,\n        );\n      });\n\n      expect(renderer.root.findByType(\"a\").props.href).toEqual(\n        \"/auth/forgot-password\",\n      );\n    });\n\n    test('<Link to=\".\"> resolves relative to the current route', () => {\n      let renderer: TestRenderer.ReactTestRenderer;\n      TestRenderer.act(() => {\n        renderer = TestRenderer.create(\n          <MemoryRouter initialEntries={[\"/auth/login\"]}>\n            <Routes>\n              <Route\n                path=\"auth/*\"\n                element={\n                  <Routes>\n                    <Route path=\"login\" element={<Link to=\".\" />} />\n                  </Routes>\n                }\n              />\n            </Routes>\n          </MemoryRouter>,\n        );\n      });\n\n      expect(renderer.root.findByType(\"a\").props.href).toEqual(\"/auth/login\");\n    });\n\n    test('<Link to=\"..\"> resolves relative to the ancestor route', () => {\n      let renderer: TestRenderer.ReactTestRenderer;\n      TestRenderer.act(() => {\n        renderer = TestRenderer.create(\n          <MemoryRouter initialEntries={[\"/auth/login\"]}>\n            <Routes>\n              <Route\n                path=\"auth/*\"\n                element={\n                  <Routes>\n                    <Route path=\"login\" element={<Link to=\"..\" />} />\n                  </Routes>\n                }\n              />\n            </Routes>\n          </MemoryRouter>,\n        );\n      });\n\n      expect(renderer.root.findByType(\"a\").props.href).toEqual(\"/auth\");\n    });\n\n    test('<Link to=\"..\"> with more .. segments than ancestor routes resolves relative to the root URL', () => {\n      let renderer: TestRenderer.ReactTestRenderer;\n      TestRenderer.act(() => {\n        renderer = TestRenderer.create(\n          <MemoryRouter initialEntries={[\"/auth/login\"]}>\n            <Routes>\n              <Route\n                path=\"auth/*\"\n                element={\n                  <Routes>\n                    <Route\n                      path=\"login\"\n                      element={\n                        <>\n                          <Link to=\"../../about\" />\n                          {/* traverse past the root */}\n                          <Link to=\"../../../about\" />\n                        </>\n                      }\n                    />\n                  </Routes>\n                }\n              />\n            </Routes>\n          </MemoryRouter>,\n        );\n      });\n\n      expect(renderer.root.findAllByType(\"a\").map((a) => a.props.href)).toEqual(\n        [\"/about\", \"/about\"],\n      );\n    });\n  });\n\n  describe(\"when using relative=path\", () => {\n    test(\"absolute <Link to> resolves relative to the root URL\", () => {\n      let renderer: TestRenderer.ReactTestRenderer;\n      TestRenderer.act(() => {\n        renderer = TestRenderer.create(\n          <MemoryRouter initialEntries={[\"/inbox\"]}>\n            <Routes>\n              <Route\n                path=\"inbox\"\n                element={<Link to=\"/about\" relative=\"path\" />}\n              />\n            </Routes>\n          </MemoryRouter>,\n        );\n      });\n\n      expect(renderer.root.findByType(\"a\").props.href).toEqual(\"/about\");\n    });\n\n    test('<Link to=\".\"> resolves relative to the current route', () => {\n      let renderer: TestRenderer.ReactTestRenderer;\n      TestRenderer.act(() => {\n        renderer = TestRenderer.create(\n          <MemoryRouter initialEntries={[\"/inbox\"]}>\n            <Routes>\n              <Route path=\"inbox\" element={<Link to=\".\" relative=\"path\" />} />\n            </Routes>\n          </MemoryRouter>,\n        );\n      });\n\n      expect(renderer.root.findByType(\"a\").props.href).toEqual(\"/inbox\");\n    });\n\n    test('<Link to=\"..\"> resolves relative to the parent URL segment', () => {\n      let renderer: TestRenderer.ReactTestRenderer;\n      TestRenderer.act(() => {\n        renderer = TestRenderer.create(\n          <MemoryRouter initialEntries={[\"/inbox/messages/1\"]}>\n            <Routes>\n              <Route path=\"inbox\" />\n              <Route path=\"inbox/messages\" />\n              <Route\n                path=\"inbox/messages/:id\"\n                element={<Link to=\"..\" relative=\"path\" />}\n              />\n            </Routes>\n          </MemoryRouter>,\n        );\n      });\n\n      expect(renderer.root.findByType(\"a\").props.href).toEqual(\n        \"/inbox/messages\",\n      );\n    });\n\n    test('<Link to=\"..\"> with more .. segments than parent routes resolves to the root URL', () => {\n      let renderer: TestRenderer.ReactTestRenderer;\n      TestRenderer.act(() => {\n        renderer = TestRenderer.create(\n          <MemoryRouter initialEntries={[\"/inbox/messages\"]}>\n            <Routes>\n              <Route path=\"inbox\">\n                <Route\n                  path=\"messages\"\n                  element={\n                    <>\n                      <Link to=\"../../about\" relative=\"path\" />\n                      {/* traverse past the root */}\n                      <Link to=\"../../../about\" relative=\"path\" />\n                    </>\n                  }\n                />\n              </Route>\n            </Routes>\n          </MemoryRouter>,\n        );\n      });\n\n      expect(renderer.root.findAllByType(\"a\").map((a) => a.props.href)).toEqual(\n        [\"/about\", \"/about\"],\n      );\n    });\n  });\n\n  describe(\"when using a browser router\", () => {\n    it(\"renders proper <a href> for BrowserRouter\", () => {\n      let renderer: TestRenderer.ReactTestRenderer;\n      TestRenderer.act(() => {\n        renderer = TestRenderer.create(\n          <BrowserRouter>\n            <Routes>\n              <Route path=\"/\" element={<Link to=\"/path?search=value#hash\" />} />\n            </Routes>\n          </BrowserRouter>,\n        );\n      });\n      expect(renderer.root.findByType(\"a\").props.href).toEqual(\n        \"/path?search=value#hash\",\n      );\n    });\n\n    it(\"renders proper <a href> for createBrowserRouter\", () => {\n      let renderer: TestRenderer.ReactTestRenderer;\n      TestRenderer.act(() => {\n        let router = createBrowserRouter([\n          {\n            path: \"/\",\n            element: <Link to=\"/path?search=value#hash\">Link</Link>,\n          },\n        ]);\n        renderer = TestRenderer.create(<RouterProvider router={router} />);\n      });\n      expect(renderer.root.findByType(\"a\").props.href).toEqual(\n        \"/path?search=value#hash\",\n      );\n    });\n  });\n\n  describe(\"when using a hash router\", () => {\n    it(\"renders proper <a href> for HashRouter\", () => {\n      let renderer: TestRenderer.ReactTestRenderer;\n      TestRenderer.act(() => {\n        renderer = TestRenderer.create(\n          <HashRouter>\n            <Routes>\n              <Route path=\"/\" element={<Link to=\"/path?search=value#hash\" />} />\n            </Routes>\n          </HashRouter>,\n        );\n      });\n      expect(renderer.root.findByType(\"a\").props.href).toEqual(\n        \"#/path?search=value#hash\",\n      );\n    });\n\n    it(\"renders proper <a href> for createHashRouter\", () => {\n      let renderer: TestRenderer.ReactTestRenderer;\n      TestRenderer.act(() => {\n        let router = createHashRouter([\n          {\n            path: \"/\",\n            element: <Link to=\"/path?search=value#hash\">Link</Link>,\n          },\n        ]);\n        renderer = TestRenderer.create(<RouterProvider router={router} />);\n      });\n      expect(renderer.root.findByType(\"a\").props.href).toEqual(\n        \"#/path?search=value#hash\",\n      );\n    });\n  });\n\n  test(\"fails gracefully on invalid `to` values\", () => {\n    let warnSpy = jest.spyOn(console, \"warn\").mockImplementation(() => {});\n    let renderer: TestRenderer.ReactTestRenderer;\n    TestRenderer.act(() => {\n      renderer = TestRenderer.create(\n        <MemoryRouter>\n          <Routes>\n            <Route path=\"/\" element={<Link to=\"//\" />} />\n          </Routes>\n        </MemoryRouter>,\n      );\n    });\n\n    expect(renderer.root.findByType(\"a\").props.href).toEqual(\"//\");\n    expect(warnSpy).toHaveBeenCalledWith(\n      '<Link to=\"//\"> contains an invalid URL which will probably break when clicked - please update to a valid URL path.',\n    );\n    warnSpy.mockRestore();\n  });\n\n  test(\"renders fine when used outside a route context\", () => {\n    let renderer: TestRenderer.ReactTestRenderer;\n    TestRenderer.act(() => {\n      renderer = TestRenderer.create(\n        <MemoryRouter>\n          <Link to=\"route\">Route</Link>\n          <Link to=\"path\" relative=\"path\">\n            Path\n          </Link>\n        </MemoryRouter>,\n      );\n    });\n\n    let anchors = renderer.root.findAllByType(\"a\");\n    expect(anchors.map((a) => ({ href: a.props.href, text: a.children })))\n      .toMatchInlineSnapshot(`\n      [\n        {\n          \"href\": \"/route\",\n          \"text\": [\n            \"Route\",\n          ],\n        },\n        {\n          \"href\": \"/path\",\n          \"text\": [\n            \"Path\",\n          ],\n        },\n      ]\n    `);\n  });\n});\n"
  },
  {
    "path": "packages/react-router/__tests__/dom/link-push-test.tsx",
    "content": "import * as React from \"react\";\nimport * as TestRenderer from \"react-test-renderer\";\nimport {\n  MemoryRouter,\n  Routes,\n  Route,\n  Link,\n  useNavigationType,\n} from \"../../index\";\n\nfunction ShowNavigationType() {\n  return <p>{useNavigationType()}</p>;\n}\n\ndescribe(\"Link push and replace\", () => {\n  describe(\"to a different pathname, when it is clicked\", () => {\n    it(\"performs a push\", () => {\n      function Home() {\n        return (\n          <div>\n            <h1>Home</h1>\n            <Link to=\"../about\">About</Link>\n          </div>\n        );\n      }\n\n      let renderer: TestRenderer.ReactTestRenderer;\n      TestRenderer.act(() => {\n        renderer = TestRenderer.create(\n          <MemoryRouter initialEntries={[\"/home\"]}>\n            <Routes>\n              <Route path=\"home\" element={<Home />} />\n              <Route path=\"about\" element={<ShowNavigationType />} />\n            </Routes>\n          </MemoryRouter>,\n        );\n      });\n\n      let anchor = renderer.root.findByType(\"a\");\n\n      TestRenderer.act(() => {\n        anchor.props.onClick(\n          new MouseEvent(\"click\", {\n            view: window,\n            bubbles: true,\n            cancelable: true,\n          }),\n        );\n      });\n\n      expect(renderer.toJSON()).toMatchInlineSnapshot(`\n        <p>\n          PUSH\n        </p>\n      `);\n    });\n  });\n\n  describe(\"to a different search string, when it is clicked\", () => {\n    it(\"performs a push with the existing pathname\", () => {\n      function Home() {\n        return (\n          <div>\n            <h1>Home</h1>\n            <Link to=\"?name=michael\">Michael</Link>\n            <ShowNavigationType />\n          </div>\n        );\n      }\n\n      let renderer: TestRenderer.ReactTestRenderer;\n      TestRenderer.act(() => {\n        renderer = TestRenderer.create(\n          <MemoryRouter initialEntries={[\"/home\"]}>\n            <Routes>\n              <Route path=\"home\" element={<Home />} />\n            </Routes>\n          </MemoryRouter>,\n        );\n      });\n\n      let anchor = renderer.root.findByType(\"a\");\n\n      TestRenderer.act(() => {\n        anchor.props.onClick(\n          new MouseEvent(\"click\", {\n            view: window,\n            bubbles: true,\n            cancelable: true,\n          }),\n        );\n      });\n\n      expect(renderer.toJSON()).toMatchInlineSnapshot(`\n        <div>\n          <h1>\n            Home\n          </h1>\n          <a\n            data-discover=\"true\"\n            href=\"/home?name=michael\"\n            onClick={[Function]}\n            ref={[Function]}\n          >\n            Michael\n          </a>\n          <p>\n            PUSH\n          </p>\n        </div>\n      `);\n    });\n  });\n\n  describe(\"to a different hash, when it is clicked\", () => {\n    it(\"performs a push with the existing pathname\", () => {\n      function Home() {\n        return (\n          <div>\n            <h1>Home</h1>\n            <Link to=\"#bio\">Bio</Link>\n            <ShowNavigationType />\n          </div>\n        );\n      }\n\n      let renderer: TestRenderer.ReactTestRenderer;\n      TestRenderer.act(() => {\n        renderer = TestRenderer.create(\n          <MemoryRouter initialEntries={[\"/home\"]}>\n            <Routes>\n              <Route path=\"home\" element={<Home />} />\n            </Routes>\n          </MemoryRouter>,\n        );\n      });\n\n      let anchor = renderer.root.findByType(\"a\");\n\n      TestRenderer.act(() => {\n        anchor.props.onClick(\n          new MouseEvent(\"click\", {\n            view: window,\n            bubbles: true,\n            cancelable: true,\n          }),\n        );\n      });\n\n      expect(renderer.toJSON()).toMatchInlineSnapshot(`\n        <div>\n          <h1>\n            Home\n          </h1>\n          <a\n            data-discover=\"true\"\n            href=\"/home#bio\"\n            onClick={[Function]}\n            ref={[Function]}\n          >\n            Bio\n          </a>\n          <p>\n            PUSH\n          </p>\n        </div>\n      `);\n    });\n  });\n\n  describe(\"to the same page, when it is clicked\", () => {\n    it(\"performs a replace\", () => {\n      function Home() {\n        return (\n          <div>\n            <h1>Home</h1>\n            <Link to=\".\">Home</Link>\n            <ShowNavigationType />\n          </div>\n        );\n      }\n\n      function About() {\n        return <h1>About</h1>;\n      }\n\n      let renderer: TestRenderer.ReactTestRenderer;\n      TestRenderer.act(() => {\n        renderer = TestRenderer.create(\n          <MemoryRouter initialEntries={[\"/home\"]}>\n            <Routes>\n              <Route path=\"home\" element={<Home />} />\n              <Route path=\"about\" element={<About />} />\n            </Routes>\n          </MemoryRouter>,\n        );\n      });\n\n      let anchor = renderer.root.findByType(\"a\");\n\n      TestRenderer.act(() => {\n        anchor.props.onClick(\n          new MouseEvent(\"click\", {\n            view: window,\n            bubbles: true,\n            cancelable: true,\n          }),\n        );\n      });\n\n      expect(renderer.toJSON()).toMatchInlineSnapshot(`\n        <div>\n          <h1>\n            Home\n          </h1>\n          <a\n            data-discover=\"true\"\n            href=\"/home\"\n            onClick={[Function]}\n            ref={[Function]}\n          >\n            Home\n          </a>\n          <p>\n            REPLACE\n          </p>\n        </div>\n      `);\n    });\n  });\n\n  describe(\"to the same page with replace={false}, when it is clicked\", () => {\n    it(\"performs a push\", () => {\n      function Home() {\n        return (\n          <div>\n            <h1>Home</h1>\n            <Link to=\".\" replace={false}>\n              Home\n            </Link>\n            <ShowNavigationType />\n          </div>\n        );\n      }\n\n      function About() {\n        return <h1>About</h1>;\n      }\n\n      let renderer: TestRenderer.ReactTestRenderer;\n      TestRenderer.act(() => {\n        renderer = TestRenderer.create(\n          <MemoryRouter initialEntries={[\"/home\"]}>\n            <Routes>\n              <Route path=\"home\" element={<Home />} />\n              <Route path=\"about\" element={<About />} />\n            </Routes>\n          </MemoryRouter>,\n        );\n      });\n\n      let anchor = renderer.root.findByType(\"a\");\n\n      TestRenderer.act(() => {\n        anchor.props.onClick(\n          new MouseEvent(\"click\", {\n            view: window,\n            bubbles: true,\n            cancelable: true,\n          }),\n        );\n      });\n\n      expect(renderer.toJSON()).toMatchInlineSnapshot(`\n        <div>\n          <h1>\n            Home\n          </h1>\n          <a\n            data-discover=\"true\"\n            href=\"/home\"\n            onClick={[Function]}\n            ref={[Function]}\n          >\n            Home\n          </a>\n          <p>\n            PUSH\n          </p>\n        </div>\n      `);\n    });\n  });\n\n  describe(\"to an absolute same-origin/same-basename URL, when it is clicked\", () => {\n    it(\"performs a push\", () => {\n      function Home() {\n        return (\n          <div>\n            <h1>Home</h1>\n            <Link to=\"http://localhost/base/about\">About</Link>\n          </div>\n        );\n      }\n\n      let renderer: TestRenderer.ReactTestRenderer;\n      TestRenderer.act(() => {\n        renderer = TestRenderer.create(\n          <MemoryRouter initialEntries={[\"/base/home\"]} basename=\"/base\">\n            <Routes>\n              <Route path=\"home\" element={<Home />} />\n              <Route path=\"about\" element={<ShowNavigationType />} />\n            </Routes>\n          </MemoryRouter>,\n        );\n      });\n\n      let anchor = renderer.root.findByType(\"a\");\n\n      TestRenderer.act(() => {\n        anchor.props.onClick(\n          new MouseEvent(\"click\", {\n            view: window,\n            bubbles: true,\n            cancelable: true,\n          }),\n        );\n      });\n\n      expect(renderer.toJSON()).toMatchInlineSnapshot(`\n        <p>\n          PUSH\n        </p>\n      `);\n    });\n  });\n});\n"
  },
  {
    "path": "packages/react-router/__tests__/dom/nav-link-active-test.tsx",
    "content": "import { render, fireEvent, waitFor, screen } from \"@testing-library/react\";\nimport \"@testing-library/jest-dom\";\nimport * as React from \"react\";\nimport * as TestRenderer from \"react-test-renderer\";\nimport {\n  BrowserRouter,\n  MemoryRouter,\n  Routes,\n  Route,\n  RouterProvider,\n  NavLink,\n  Outlet,\n  createBrowserRouter,\n  createRoutesFromElements,\n} from \"../../index\";\nimport getWindow from \"../utils/getWindow\";\n\ndescribe(\"NavLink\", () => {\n  describe(\"when it does not match\", () => {\n    it(\"does not apply an 'active' className to the underlying <a>\", () => {\n      let renderer: TestRenderer.ReactTestRenderer;\n      TestRenderer.act(() => {\n        renderer = TestRenderer.create(\n          <MemoryRouter initialEntries={[\"/home\"]}>\n            <Routes>\n              <Route\n                path=\"/home\"\n                element={<NavLink to=\"somewhere-else\">Somewhere else</NavLink>}\n              />\n            </Routes>\n          </MemoryRouter>,\n        );\n      });\n\n      let anchor = renderer.root.findByType(\"a\");\n\n      expect(anchor.props.className).not.toMatch(\"active\");\n    });\n\n    it(\"does not change the content inside the <a>\", () => {\n      let renderer: TestRenderer.ReactTestRenderer;\n      TestRenderer.act(() => {\n        renderer = TestRenderer.create(\n          <MemoryRouter initialEntries={[\"/home\"]}>\n            <Routes>\n              <Route\n                path=\"/home\"\n                element={\n                  <NavLink to=\"somewhere-else\">\n                    {({ isActive }) =>\n                      isActive ? \"Current\" : \"Somewhere else\"\n                    }\n                  </NavLink>\n                }\n              />\n            </Routes>\n          </MemoryRouter>,\n        );\n      });\n      renderer = renderer!;\n\n      let anchor = renderer.root.findByType(\"a\");\n\n      expect(anchor.children[0]).toMatch(\"Somewhere else\");\n    });\n\n    it(\"applies an 'undefined' className to the underlying <a>\", () => {\n      let renderer: TestRenderer.ReactTestRenderer;\n      TestRenderer.act(() => {\n        renderer = TestRenderer.create(\n          <MemoryRouter initialEntries={[\"/home\"]}>\n            <Routes>\n              <Route\n                path=\"/home\"\n                element={\n                  <NavLink\n                    to=\"somewhere-else\"\n                    className={({ isActive }) =>\n                      isActive ? \"some-active-classname\" : undefined\n                    }\n                  >\n                    Home\n                  </NavLink>\n                }\n              />\n            </Routes>\n          </MemoryRouter>,\n        );\n      });\n\n      let anchor = renderer.root.findByType(\"a\");\n\n      expect(anchor.props.className).toBeUndefined();\n    });\n  });\n\n  describe(\"when it matches to the end\", () => {\n    it(\"applies the default 'active' className to the underlying <a>\", () => {\n      let renderer: TestRenderer.ReactTestRenderer;\n      TestRenderer.act(() => {\n        renderer = TestRenderer.create(\n          <MemoryRouter initialEntries={[\"/home\"]}>\n            <Routes>\n              <Route path=\"/home\" element={<NavLink to=\".\">Home</NavLink>} />\n            </Routes>\n          </MemoryRouter>,\n        );\n      });\n\n      let anchor = renderer.root.findByType(\"a\");\n\n      expect(anchor.props.className).toMatch(\"active\");\n    });\n\n    it(\"when the current URL has a trailing slash\", () => {\n      let renderer: TestRenderer.ReactTestRenderer;\n      TestRenderer.act(() => {\n        renderer = TestRenderer.create(\n          <MemoryRouter initialEntries={[\"/home/\"]}>\n            <Routes>\n              <Route\n                path=\"/home\"\n                element={<NavLink to=\"/home/\">Home</NavLink>}\n              />\n            </Routes>\n          </MemoryRouter>,\n        );\n      });\n\n      let anchor = renderer.root.findByType(\"a\");\n\n      expect(anchor.props.className).toMatch(\"active\");\n    });\n\n    it(\"applies its className correctly when provided as a function\", () => {\n      let renderer: TestRenderer.ReactTestRenderer;\n      TestRenderer.act(() => {\n        renderer = TestRenderer.create(\n          <MemoryRouter initialEntries={[\"/home\"]}>\n            <Routes>\n              <Route\n                path=\"/home\"\n                element={\n                  <NavLink\n                    to=\".\"\n                    className={({ isActive }) =>\n                      \"nav-link\" + (isActive ? \" highlighted\" : \" plain\")\n                    }\n                  >\n                    Home\n                  </NavLink>\n                }\n              />\n            </Routes>\n          </MemoryRouter>,\n        );\n      });\n\n      let anchor = renderer.root.findByType(\"a\");\n\n      expect(anchor.props.className.includes(\"nav-link\")).toBe(true);\n      expect(anchor.props.className.includes(\"highlighted\")).toBe(true);\n      expect(anchor.props.className.includes(\"plain\")).toBe(false);\n    });\n\n    it(\"applies its style correctly when provided as a function\", () => {\n      let renderer: TestRenderer.ReactTestRenderer;\n      TestRenderer.act(() => {\n        renderer = TestRenderer.create(\n          <MemoryRouter initialEntries={[\"/home\"]}>\n            <Routes>\n              <Route\n                path=\"/home\"\n                element={\n                  <NavLink\n                    to=\".\"\n                    style={({ isActive }) =>\n                      isActive ? { textTransform: \"uppercase\" } : {}\n                    }\n                  >\n                    Home\n                  </NavLink>\n                }\n              />\n            </Routes>\n          </MemoryRouter>,\n        );\n      });\n\n      let anchor = renderer.root.findByType(\"a\");\n\n      expect(anchor.props.style).toMatchObject({ textTransform: \"uppercase\" });\n    });\n\n    it(\"applies its children correctly when provided as a function\", () => {\n      let renderer: TestRenderer.ReactTestRenderer;\n      TestRenderer.act(() => {\n        renderer = TestRenderer.create(\n          <MemoryRouter initialEntries={[\"/home\"]}>\n            <Routes>\n              <Route\n                path=\"/home\"\n                element={\n                  <NavLink to=\".\">\n                    {({ isActive }) => (isActive ? \"Home (current)\" : \"Home\")}\n                  </NavLink>\n                }\n              />\n            </Routes>\n          </MemoryRouter>,\n        );\n      });\n      renderer = renderer!;\n\n      let anchor = renderer.root.findByType(\"a\");\n\n      expect(anchor.children[0]).toMatch(\"Home (current)\");\n    });\n\n    it(\"matches when portions of the url are encoded\", () => {\n      let renderer: TestRenderer.ReactTestRenderer;\n\n      TestRenderer.act(() => {\n        renderer = TestRenderer.create(\n          <BrowserRouter window={getWindow(\"/users/matt brophy\")}>\n            <Routes>\n              <Route\n                path=\"/users/:name\"\n                element={\n                  <>\n                    <NavLink to=\".\">Matt</NavLink>\n                    <NavLink to=\"/users/matt brophy\">Matt</NavLink>\n                    <NavLink to=\"/users/michael jackson\">Michael</NavLink>\n                  </>\n                }\n              />\n            </Routes>\n          </BrowserRouter>,\n        );\n      });\n\n      let anchors = renderer.root.findAllByType(\"a\");\n\n      expect(anchors.map((a) => a.props.className)).toEqual([\n        \"active\",\n        \"active\",\n        \"\",\n      ]);\n    });\n  });\n\n  describe(\"when it matches a partial URL segment\", () => {\n    it(\"does not apply the 'active' className to the underlying <a>\", () => {\n      let renderer: TestRenderer.ReactTestRenderer;\n      TestRenderer.act(() => {\n        renderer = TestRenderer.create(\n          <MemoryRouter initialEntries={[\"/home/children\"]}>\n            <Routes>\n              <Route\n                path=\"home\"\n                element={\n                  <div>\n                    <NavLink to=\"child\">Home</NavLink>\n                    <Outlet />\n                  </div>\n                }\n              >\n                <Route path=\"children\" element={<div>Child</div>} />\n              </Route>\n            </Routes>\n          </MemoryRouter>,\n        );\n      });\n\n      let anchor = renderer.root.findByType(\"a\");\n\n      expect(anchor.props.className).not.toMatch(\"active\");\n    });\n\n    it(\"does not match when <Link to> path is a subset of the active url\", () => {\n      let renderer: TestRenderer.ReactTestRenderer;\n      TestRenderer.act(() => {\n        renderer = TestRenderer.create(\n          <MemoryRouter initialEntries={[\"/user-preferences\"]}>\n            <Routes>\n              <Route\n                path=\"/\"\n                element={\n                  <div>\n                    <NavLink to=\"user\">Go to /user</NavLink>\n                    <NavLink to=\"user-preferences\">\n                      Go to /user-preferences\n                    </NavLink>\n                    <Outlet />\n                  </div>\n                }\n              >\n                <Route index element={<p>Index</p>} />\n                <Route path=\"user\" element={<p>User</p>} />\n                <Route\n                  path=\"user-preferences\"\n                  element={<p>User Preferences</p>}\n                />\n              </Route>\n            </Routes>\n          </MemoryRouter>,\n        );\n      });\n\n      let anchors = renderer.root.findAllByType(\"a\");\n\n      expect(anchors.map((a) => a.props.className)).toEqual([\"\", \"active\"]);\n    });\n\n    it(\"does not match when active url is a subset of a <Route path> segment\", () => {\n      let renderer: TestRenderer.ReactTestRenderer;\n      TestRenderer.act(() => {\n        renderer = TestRenderer.create(\n          <MemoryRouter initialEntries={[\"/user\"]}>\n            <Routes>\n              <Route\n                path=\"/\"\n                element={\n                  <div>\n                    <NavLink to=\"user\">Go to /user</NavLink>\n                    <NavLink to=\"user-preferences\">\n                      Go to /user-preferences\n                    </NavLink>\n                    <Outlet />\n                  </div>\n                }\n              >\n                <Route index element={<p>Index</p>} />\n                <Route path=\"user\" element={<p>User</p>} />\n                <Route\n                  path=\"user-preferences\"\n                  element={<p>User Preferences</p>}\n                />\n              </Route>\n            </Routes>\n          </MemoryRouter>,\n        );\n      });\n\n      let anchors = renderer.root.findAllByType(\"a\");\n\n      expect(anchors.map((a) => a.props.className)).toEqual([\"active\", \"\"]);\n    });\n\n    it(\"matches the root route with or without the end prop\", () => {\n      let renderer: TestRenderer.ReactTestRenderer;\n      TestRenderer.act(() => {\n        renderer = TestRenderer.create(\n          <MemoryRouter>\n            <Routes>\n              <Route index element={<NavLink to=\"/\">Root</NavLink>} />\n            </Routes>\n          </MemoryRouter>,\n        );\n      });\n\n      let anchor = renderer.root.findByType(\"a\");\n      expect(anchor.props.className).toMatch(\"active\");\n\n      TestRenderer.act(() => {\n        renderer = TestRenderer.create(\n          <MemoryRouter>\n            <Routes>\n              <Route\n                index\n                element={\n                  <NavLink to=\"/\" end>\n                    Root\n                  </NavLink>\n                }\n              />\n            </Routes>\n          </MemoryRouter>,\n        );\n      });\n\n      anchor = renderer.root.findByType(\"a\");\n      expect(anchor.props.className).toMatch(\"active\");\n    });\n\n    it(\"does not automatically apply to root non-layout segments\", () => {\n      let renderer: TestRenderer.ReactTestRenderer;\n      TestRenderer.act(() => {\n        renderer = TestRenderer.create(\n          <MemoryRouter initialEntries={[\"/home\"]}>\n            <Routes>\n              <Route index element={<h1>Root</h1>} />\n              <Route\n                path=\"home\"\n                element={<NavLink to=\"/\">Root</NavLink>}\n              ></Route>\n            </Routes>\n          </MemoryRouter>,\n        );\n      });\n\n      let anchor = renderer.root.findByType(\"a\");\n\n      expect(anchor.props.className).not.toMatch(\"active\");\n    });\n\n    it(\"does not automatically apply to root layout segments\", () => {\n      let renderer: TestRenderer.ReactTestRenderer;\n      TestRenderer.act(() => {\n        renderer = TestRenderer.create(\n          <MemoryRouter initialEntries={[\"/home\"]}>\n            <Routes>\n              <Route\n                path=\"/\"\n                element={\n                  <>\n                    <h1>Root</h1>\n                    <Outlet />\n                  </>\n                }\n              >\n                <Route\n                  path=\"home\"\n                  element={<NavLink to=\"/\">Root</NavLink>}\n                ></Route>\n              </Route>\n            </Routes>\n          </MemoryRouter>,\n        );\n      });\n\n      let anchor = renderer.root.findByType(\"a\");\n\n      expect(anchor.props.className).not.toMatch(\"active\");\n    });\n  });\n\n  describe(\"when it matches just the beginning but not to the end\", () => {\n    it(\"applies the default 'active' className to the underlying <a>\", () => {\n      let renderer: TestRenderer.ReactTestRenderer;\n      TestRenderer.act(() => {\n        renderer = TestRenderer.create(\n          <MemoryRouter initialEntries={[\"/home/child\"]}>\n            <Routes>\n              <Route\n                path=\"home\"\n                element={\n                  <div>\n                    <NavLink to=\".\">Home</NavLink>\n                    <Outlet />\n                  </div>\n                }\n              >\n                <Route path=\"child\" element={<div>Child</div>} />\n              </Route>\n            </Routes>\n          </MemoryRouter>,\n        );\n      });\n\n      let anchor = renderer.root.findByType(\"a\");\n\n      expect(anchor.props.className).toMatch(\"active\");\n    });\n\n    it(\"In case of trailing slash at the end of link\", () => {\n      let renderer: TestRenderer.ReactTestRenderer;\n      TestRenderer.act(() => {\n        renderer = TestRenderer.create(\n          <MemoryRouter initialEntries={[\"/home/child\"]}>\n            <Routes>\n              <Route\n                path=\"home\"\n                element={\n                  <div>\n                    <NavLink to=\"/home/\">Home</NavLink>\n                    <Outlet />\n                  </div>\n                }\n              >\n                <Route path=\"child\" element={<div>Child</div>} />\n              </Route>\n            </Routes>\n          </MemoryRouter>,\n        );\n      });\n\n      let anchor = renderer.root.findByType(\"a\");\n      expect(anchor.props.className).toMatch(\"active\");\n    });\n\n    describe(\"when end=true\", () => {\n      it(\"does not apply the default 'active' className to the underlying <a>\", () => {\n        let renderer: TestRenderer.ReactTestRenderer;\n        TestRenderer.act(() => {\n          renderer = TestRenderer.create(\n            <MemoryRouter initialEntries={[\"/home/child\"]}>\n              <Routes>\n                <Route\n                  path=\"home\"\n                  element={\n                    <div>\n                      <NavLink to=\".\" end={true}>\n                        Home\n                      </NavLink>\n                      <Outlet />\n                    </div>\n                  }\n                >\n                  <Route path=\"child\" element={<div>Child</div>} />\n                </Route>\n              </Routes>\n            </MemoryRouter>,\n          );\n        });\n\n        let anchor = renderer.root.findByType(\"a\");\n\n        expect(anchor.props.className).not.toMatch(\"active\");\n      });\n\n      it(\"Handles trailing slashes accordingly when the URL does not have a trailing slash\", () => {\n        let renderer: TestRenderer.ReactTestRenderer;\n        TestRenderer.act(() => {\n          renderer = TestRenderer.create(\n            <MemoryRouter initialEntries={[\"/home\"]}>\n              <Routes>\n                <Route\n                  path=\"home\"\n                  element={\n                    <div>\n                      <NavLink to=\"/home\" end>\n                        Home\n                      </NavLink>\n                      <NavLink to=\"/home/\" end>\n                        Home\n                      </NavLink>\n                      <Outlet />\n                    </div>\n                  }\n                >\n                  <Route path=\"child\" element={<div>Child</div>} />\n                </Route>\n              </Routes>\n            </MemoryRouter>,\n          );\n        });\n\n        let anchors = renderer.root.findAllByType(\"a\");\n        expect(anchors.map((a) => a.props.className)).toEqual([\"active\", \"\"]);\n      });\n\n      it(\"Handles trailing slashes accordingly when the URL has a trailing slash\", () => {\n        let renderer: TestRenderer.ReactTestRenderer;\n        TestRenderer.act(() => {\n          renderer = TestRenderer.create(\n            <MemoryRouter initialEntries={[\"/home/\"]}>\n              <Routes>\n                <Route\n                  path=\"home\"\n                  element={\n                    <div>\n                      <NavLink to=\"/home\" end>\n                        Home\n                      </NavLink>\n                      <NavLink to=\"/home/\" end>\n                        Home\n                      </NavLink>\n                      <Outlet />\n                    </div>\n                  }\n                >\n                  <Route path=\"child\" element={<div>Child</div>} />\n                </Route>\n              </Routes>\n            </MemoryRouter>,\n          );\n        });\n\n        let anchors = renderer.root.findAllByType(\"a\");\n        expect(anchors.map((a) => a.props.className)).toEqual([\"\", \"active\"]);\n      });\n    });\n  });\n\n  describe(\"when it matches without matching case\", () => {\n    it(\"applies the default 'active' className to the underlying <a>\", () => {\n      let renderer: TestRenderer.ReactTestRenderer;\n      TestRenderer.act(() => {\n        renderer = TestRenderer.create(\n          <MemoryRouter initialEntries={[\"/Home\"]}>\n            <Routes>\n              <Route path=\"home\" element={<NavLink to=\".\">Home</NavLink>} />\n            </Routes>\n          </MemoryRouter>,\n        );\n      });\n\n      let anchor = renderer.root.findByType(\"a\");\n\n      expect(anchor.props.className).toMatch(\"active\");\n    });\n\n    describe(\"when caseSensitive=true\", () => {\n      it(\"does not apply the default 'active' className to the underlying <a>\", () => {\n        let renderer: TestRenderer.ReactTestRenderer;\n        TestRenderer.act(() => {\n          renderer = TestRenderer.create(\n            <MemoryRouter initialEntries={[\"/Home\"]}>\n              <Routes>\n                <Route\n                  path=\"home\"\n                  element={\n                    <NavLink to=\"/home\" caseSensitive={true}>\n                      Home\n                    </NavLink>\n                  }\n                />\n              </Routes>\n            </MemoryRouter>,\n          );\n        });\n\n        let anchor = renderer.root.findByType(\"a\");\n\n        expect(anchor.props.className).not.toMatch(\"active\");\n      });\n    });\n  });\n\n  describe(\"when it matches with relative=path links\", () => {\n    it(\"applies the default 'active' className to the underlying <a>\", () => {\n      let renderer: TestRenderer.ReactTestRenderer;\n      TestRenderer.act(() => {\n        renderer = TestRenderer.create(\n          <MemoryRouter initialEntries={[\"/contacts/1\"]}>\n            <Routes>\n              <Route\n                path=\"contacts/:id\"\n                element={\n                  <NavLink to=\"../1\" relative=\"path\">\n                    Link\n                  </NavLink>\n                }\n              />\n            </Routes>\n          </MemoryRouter>,\n        );\n      });\n\n      let anchor = renderer.root.findByType(\"a\");\n\n      expect(anchor.props.href).toEqual(\"/contacts/1\");\n      expect(anchor.props.className).toEqual(\"active\");\n    });\n  });\n});\n\ndescribe(\"NavLink using a data router\", () => {\n  it(\"applies the default 'active'/'pending' classNames to the underlying <a>\", async () => {\n    let dfd = createDeferred();\n    let router = createBrowserRouter(\n      createRoutesFromElements(\n        <Route path=\"/\" element={<Layout />}>\n          <Route path=\"foo\" element={<p>Foo page</p>} />\n          <Route\n            path=\"bar\"\n            loader={() => dfd.promise}\n            element={<p>Bar page</p>}\n          />\n        </Route>,\n      ),\n      {\n        window: getWindow(\"/foo\"),\n      },\n    );\n    render(<RouterProvider router={router} />);\n\n    function Layout() {\n      return (\n        <>\n          <NavLink to=\"/foo\">Link to Foo</NavLink>\n          <NavLink to=\"/bar\">Link to Bar</NavLink>\n          <Outlet />\n        </>\n      );\n    }\n\n    expect(screen.getByText(\"Link to Bar\").className).toBe(\"\");\n\n    fireEvent.click(screen.getByText(\"Link to Bar\"));\n    expect(screen.getByText(\"Link to Bar\").className).toBe(\"pending\");\n\n    dfd.resolve(null);\n    await waitFor(() => screen.getByText(\"Bar page\"));\n    expect(screen.getByText(\"Link to Bar\").className).toBe(\"active\");\n  });\n\n  it(\"applies its className correctly when provided as a function\", async () => {\n    let dfd = createDeferred();\n    let router = createBrowserRouter(\n      createRoutesFromElements(\n        <Route path=\"/\" element={<Layout />}>\n          <Route path=\"foo\" element={<p>Foo page</p>} />\n          <Route\n            path=\"bar\"\n            loader={() => dfd.promise}\n            element={<p>Bar page</p>}\n          />\n        </Route>,\n      ),\n      {\n        window: getWindow(\"/foo\"),\n      },\n    );\n    render(<RouterProvider router={router} />);\n\n    function Layout() {\n      return (\n        <>\n          <NavLink to=\"/foo\">Link to Foo</NavLink>\n          <NavLink\n            to=\"/bar\"\n            className={({ isActive, isPending }) =>\n              isPending\n                ? \"some-pending-classname\"\n                : isActive\n                  ? \"some-active-classname\"\n                  : undefined\n            }\n          >\n            Link to Bar\n          </NavLink>\n\n          <Outlet />\n        </>\n      );\n    }\n\n    expect(screen.getByText(\"Link to Bar\").className).toBe(\"\");\n\n    fireEvent.click(screen.getByText(\"Link to Bar\"));\n    expect(screen.getByText(\"Link to Bar\").className).toBe(\n      \"some-pending-classname\",\n    );\n\n    dfd.resolve(null);\n    await waitFor(() => screen.getByText(\"Bar page\"));\n    expect(screen.getByText(\"Link to Bar\").className).toBe(\n      \"some-active-classname\",\n    );\n  });\n\n  it(\"applies its style correctly when provided as a function\", async () => {\n    let dfd = createDeferred();\n    let router = createBrowserRouter(\n      createRoutesFromElements(\n        <Route path=\"/\" element={<Layout />}>\n          <Route path=\"foo\" element={<p>Foo page</p>} />\n          <Route\n            path=\"bar\"\n            loader={() => dfd.promise}\n            element={<p>Bar page</p>}\n          />\n        </Route>,\n      ),\n      {\n        window: getWindow(\"/foo\"),\n      },\n    );\n    render(<RouterProvider router={router} />);\n\n    function Layout() {\n      return (\n        <>\n          <NavLink to=\"/foo\">Link to Foo</NavLink>\n          <NavLink\n            to=\"/bar\"\n            style={({ isActive, isPending }) =>\n              isPending\n                ? { textTransform: \"lowercase\" }\n                : isActive\n                  ? { textTransform: \"uppercase\" }\n                  : undefined\n            }\n          >\n            Link to Bar\n          </NavLink>\n\n          <Outlet />\n        </>\n      );\n    }\n\n    expect(screen.getByText(\"Link to Bar\").style.textTransform).toBe(\"\");\n\n    fireEvent.click(screen.getByText(\"Link to Bar\"));\n    expect(screen.getByText(\"Link to Bar\").style.textTransform).toBe(\n      \"lowercase\",\n    );\n\n    dfd.resolve(null);\n    await waitFor(() => screen.getByText(\"Bar page\"));\n    expect(screen.getByText(\"Link to Bar\").style.textTransform).toBe(\n      \"uppercase\",\n    );\n  });\n\n  it(\"applies its children correctly when provided as a function\", async () => {\n    let dfd = createDeferred();\n    let router = createBrowserRouter(\n      createRoutesFromElements(\n        <Route path=\"/\" element={<Layout />}>\n          <Route path=\"foo\" element={<p>Foo page</p>} />\n          <Route\n            path=\"bar\"\n            loader={() => dfd.promise}\n            element={<p>Bar page</p>}\n          />\n        </Route>,\n      ),\n      {\n        window: getWindow(\"/foo\"),\n      },\n    );\n    render(<RouterProvider router={router} />);\n\n    function Layout() {\n      return (\n        <>\n          <NavLink to=\"/foo\">Link to Foo</NavLink>\n          <NavLink to=\"/bar\">\n            {({ isActive, isPending }) =>\n              isPending\n                ? \"Link to Bar (loading...)\"\n                : isActive\n                  ? \"Link to Bar (current)\"\n                  : \"Link to Bar (idle)\"\n            }\n          </NavLink>\n\n          <Outlet />\n        </>\n      );\n    }\n\n    expect(screen.getByText(\"Link to Bar (idle)\")).toBeDefined();\n\n    fireEvent.click(screen.getByText(\"Link to Bar (idle)\"));\n    expect(screen.getByText(\"Link to Bar (loading...)\")).toBeDefined();\n\n    dfd.resolve(null);\n    await waitFor(() => screen.getByText(\"Bar page\"));\n    expect(screen.getByText(\"Link to Bar (current)\")).toBeDefined();\n  });\n\n  it(\"does not apply during transitions to non-matching locations\", async () => {\n    let dfd = createDeferred();\n    let router = createBrowserRouter(\n      createRoutesFromElements(\n        <Route path=\"/\" element={<Layout />}>\n          <Route path=\"foo\" element={<p>Foo page</p>} />\n          <Route path=\"bar\" element={<p>Bar page</p>} />\n          <Route\n            path=\"baz\"\n            loader={() => dfd.promise}\n            element={<p>Baz page</p>}\n          />\n        </Route>,\n      ),\n      {\n        window: getWindow(\"/foo\"),\n      },\n    );\n    render(<RouterProvider router={router} />);\n\n    function Layout() {\n      return (\n        <>\n          <NavLink to=\"/foo\">Link to Foo</NavLink>\n          <NavLink to=\"/bar\">Link to Bar</NavLink>\n          <NavLink to=\"/baz\">Link to Baz</NavLink>\n          <Outlet />\n        </>\n      );\n    }\n\n    expect(screen.getByText(\"Link to Bar\").className).toBe(\"\");\n\n    fireEvent.click(screen.getByText(\"Link to Baz\"));\n    expect(screen.getByText(\"Link to Bar\").className).toBe(\"\");\n\n    dfd.resolve(null);\n    await waitFor(() => screen.getByText(\"Baz page\"));\n    expect(screen.getByText(\"Link to Bar\").className).toBe(\"\");\n  });\n\n  it(\"applies the default 'active'/'pending' classNames when the url has encoded characters\", async () => {\n    let barDfd = createDeferred();\n    let bazDfd = createDeferred();\n    let router = createBrowserRouter(\n      createRoutesFromElements(\n        <Route path=\"/\" element={<Layout />}>\n          <Route path=\"foo\" element={<p>Foo page</p>} />\n          <Route\n            path=\"bar/:param\"\n            loader={() => barDfd.promise}\n            element={<p>Bar page</p>}\n          />\n          <Route\n            path=\"baz-✅\"\n            loader={() => bazDfd.promise}\n            element={<p>Baz page</p>}\n          />\n        </Route>,\n      ),\n      {\n        window: getWindow(\"/foo\"),\n      },\n    );\n    render(<RouterProvider router={router} />);\n\n    function Layout() {\n      return (\n        <>\n          <NavLink to=\"/foo\">Link to Foo</NavLink>\n          <NavLink to=\"/bar/matt brophy\">Link to Bar</NavLink>\n          <NavLink to=\"/baz-✅\">Link to Baz</NavLink>\n          <Outlet />\n        </>\n      );\n    }\n\n    expect(screen.getByText(\"Link to Bar\").className).toBe(\"\");\n    expect(screen.getByText(\"Link to Baz\").className).toBe(\"\");\n\n    fireEvent.click(screen.getByText(\"Link to Bar\"));\n    expect(screen.getByText(\"Link to Bar\").className).toBe(\"pending\");\n    expect(screen.getByText(\"Link to Baz\").className).toBe(\"\");\n\n    barDfd.resolve(null);\n    await waitFor(() => screen.getByText(\"Bar page\"));\n    expect(screen.getByText(\"Link to Bar\").className).toBe(\"active\");\n    expect(screen.getByText(\"Link to Baz\").className).toBe(\"\");\n\n    fireEvent.click(screen.getByText(\"Link to Baz\"));\n    expect(screen.getByText(\"Link to Bar\").className).toBe(\"active\");\n    expect(screen.getByText(\"Link to Baz\").className).toBe(\"pending\");\n\n    bazDfd.resolve(null);\n    await waitFor(() => screen.getByText(\"Baz page\"));\n    expect(screen.getByText(\"Link to Bar\").className).toBe(\"\");\n    expect(screen.getByText(\"Link to Baz\").className).toBe(\"active\");\n  });\n\n  it(\"applies the default 'active'/'pending' classNames when a basename is used\", async () => {\n    let dfd = createDeferred();\n    let router = createBrowserRouter(\n      createRoutesFromElements(\n        <Route path=\"/\" element={<Layout />}>\n          <Route path=\"foo\" element={<p>Foo page</p>} />\n          <Route\n            path=\"bar\"\n            loader={() => dfd.promise}\n            element={<p>Bar page</p>}\n          />\n        </Route>,\n      ),\n      {\n        window: getWindow(\"/base/foo\"),\n        basename: \"/base\",\n      },\n    );\n    render(<RouterProvider router={router} />);\n\n    function Layout() {\n      return (\n        <>\n          <NavLink to=\"/foo\">Link to Foo</NavLink>\n          <NavLink to=\"/bar\">Link to Bar</NavLink>\n          <Outlet />\n        </>\n      );\n    }\n\n    expect(screen.getByText(\"Link to Bar\").className).toBe(\"\");\n\n    fireEvent.click(screen.getByText(\"Link to Bar\"));\n    expect(screen.getByText(\"Link to Bar\").className).toBe(\"pending\");\n\n    dfd.resolve(null);\n    await waitFor(() => screen.getByText(\"Bar page\"));\n    expect(screen.getByText(\"Link to Bar\").className).toBe(\"active\");\n  });\n});\n\ndescribe(\"NavLink under a Routes with a basename\", () => {\n  describe(\"when it does not match\", () => {\n    it(\"does not apply the default 'active' className to the underlying <a>\", () => {\n      let renderer: TestRenderer.ReactTestRenderer;\n      TestRenderer.act(() => {\n        renderer = TestRenderer.create(\n          <MemoryRouter basename=\"/app\" initialEntries={[\"/app/home\"]}>\n            <Routes>\n              <Route\n                path=\"home\"\n                element={<NavLink to=\"somewhere-else\">Somewhere else</NavLink>}\n              />\n            </Routes>\n          </MemoryRouter>,\n        );\n      });\n\n      let anchor = renderer.root.findByType(\"a\");\n\n      expect(anchor.props.className).not.toMatch(\"active\");\n    });\n  });\n\n  describe(\"when it matches\", () => {\n    it(\"applies the default 'active' className to the underlying <a>\", () => {\n      let renderer: TestRenderer.ReactTestRenderer;\n      TestRenderer.act(() => {\n        renderer = TestRenderer.create(\n          <MemoryRouter basename=\"/app\" initialEntries={[\"/app/home\"]}>\n            <Routes>\n              <Route path=\"home\" element={<NavLink to=\".\">Home</NavLink>} />\n            </Routes>\n          </MemoryRouter>,\n        );\n      });\n\n      let anchor = renderer.root.findByType(\"a\");\n\n      expect(anchor.props.className).toMatch(\"active\");\n    });\n  });\n});\n\nfunction createDeferred() {\n  let resolve: (val?: any) => Promise<void>;\n  let reject: (error?: Error) => Promise<void>;\n  let promise = new Promise((res, rej) => {\n    resolve = async (val: any) => {\n      res(val);\n      try {\n        await promise;\n      } catch (e) {}\n    };\n    reject = async (error?: Error) => {\n      rej(error);\n      try {\n        await promise;\n      } catch (e) {}\n    };\n  });\n  return {\n    promise,\n    //@ts-ignore\n    resolve,\n    //@ts-ignore\n    reject,\n  };\n}\n"
  },
  {
    "path": "packages/react-router/__tests__/dom/navigate-encode-params-test.tsx",
    "content": "import * as React from \"react\";\nimport * as ReactDOM from \"react-dom/client\";\nimport { act } from \"@testing-library/react\";\nimport {\n  BrowserRouter,\n  Routes,\n  Route,\n  useNavigate,\n  useParams,\n} from \"../../index\";\n\ndescribe(\"navigate with params\", () => {\n  let node: HTMLDivElement;\n  beforeEach(() => {\n    global.history.pushState({}, \"\", \"/\");\n    node = document.createElement(\"div\");\n    document.body.appendChild(node);\n  });\n\n  afterEach(() => {\n    document.body.removeChild(node);\n    node = null!;\n  });\n\n  describe(\"when navigate params are not already encoded\", () => {\n    it(\"correctly encodes the param in the URL and decodes the param when it is used\", () => {\n      function Start() {\n        let navigate = useNavigate();\n\n        React.useEffect(() => {\n          navigate(\"/blog/react router\");\n        });\n\n        return null;\n      }\n\n      function Blog() {\n        let params = useParams();\n        return <h1>Blog: {params.slug}</h1>;\n      }\n\n      act(() => {\n        ReactDOM.createRoot(node).render(\n          <BrowserRouter>\n            <Routes>\n              <Route path=\"/\" element={<Start />} />\n              <Route path=\"blog/:slug\" element={<Blog />} />\n            </Routes>\n          </BrowserRouter>,\n        );\n      });\n\n      expect(window.location.pathname).toEqual(\"/blog/react%20router\");\n      expect(node.innerHTML).toMatch(/react router/);\n    });\n  });\n\n  describe(\"when navigate params are encoded using +\", () => {\n    it(\"does not alter the param encoding in the URL and decodes the param when it is used\", () => {\n      function Start() {\n        let navigate = useNavigate();\n\n        React.useEffect(() => {\n          navigate(\"/blog/react+router\");\n        });\n\n        return null;\n      }\n\n      function Blog() {\n        let params = useParams();\n        return <h1>Blog: {params.slug}</h1>;\n      }\n\n      act(() => {\n        ReactDOM.createRoot(node).render(\n          <BrowserRouter>\n            <Routes>\n              <Route path=\"/\" element={<Start />} />\n              <Route path=\"blog/:slug\" element={<Blog />} />\n            </Routes>\n          </BrowserRouter>,\n        );\n      });\n\n      // Need to add the + back for JSDom, but normal browsers leave\n      // the + in the URL pathname. Should probably report this as a\n      // bug in JSDom...\n      let pathname = window.location.pathname.replace(/%20/g, \"+\");\n      expect(pathname).toEqual(\"/blog/react+router\");\n\n      // Note decodeURIComponent doesn't decode +\n      expect(node.innerHTML).toMatch(/react\\+router/);\n    });\n  });\n});\n"
  },
  {
    "path": "packages/react-router/__tests__/dom/partial-hydration-test.tsx",
    "content": "import \"@testing-library/jest-dom\";\nimport { act, render, screen, waitFor } from \"@testing-library/react\";\nimport * as React from \"react\";\nimport type { LoaderFunction } from \"../../index\";\nimport {\n  Outlet,\n  RouterProvider as ReactRouter_RouterProvider,\n  createBrowserRouter,\n  createHashRouter,\n  createMemoryRouter,\n  useLoaderData,\n  useRouteError,\n} from \"../../index\";\nimport { RouterProvider as ReactRouterDom_RouterProvider } from \"../../dom-export\";\n\nimport getHtml from \"../utils/getHtml\";\nimport { createDeferred, tick } from \"../router/utils/utils\";\n\nlet didAssertMissingHydrateFallback = false;\n\ndescribe(\"Partial Hydration Behavior\", () => {\n  describe(\"createBrowserRouter\", () => {\n    testPartialHydration(createBrowserRouter, ReactRouterDom_RouterProvider);\n  });\n\n  describe(\"createHashRouter\", () => {\n    testPartialHydration(createHashRouter, ReactRouterDom_RouterProvider);\n  });\n\n  describe(\"createMemoryRouter\", () => {\n    testPartialHydration(createMemoryRouter, ReactRouter_RouterProvider);\n\n    // these tests only run for memory since we just need to set initialEntries\n    it(\"supports partial hydration w/patchRoutesOnNavigation (leaf fallback)\", async () => {\n      let parentDfd = createDeferred();\n      let childDfd = createDeferred();\n      let router = createMemoryRouter(\n        [\n          {\n            path: \"/\",\n            Component() {\n              return (\n                <>\n                  <h1>Root</h1>\n                  <Outlet />\n                </>\n              );\n            },\n            children: [\n              {\n                id: \"parent\",\n                path: \"parent\",\n                HydrateFallback: () => <p>Parent Loading...</p>,\n                loader: () => parentDfd.promise,\n                Component() {\n                  let data = useLoaderData() as string;\n                  return (\n                    <>\n                      <h2>{`Parent - ${data}`}</h2>\n                      <Outlet />\n                    </>\n                  );\n                },\n              },\n            ],\n          },\n        ],\n        {\n          future: {\n            v7_partialHydration: true,\n          },\n          patchRoutesOnNavigation({ path, patch }) {\n            if (path === \"/parent/child\") {\n              patch(\"parent\", [\n                {\n                  path: \"child\",\n                  loader: () => childDfd.promise,\n                  Component() {\n                    let data = useLoaderData() as string;\n                    return <h3>{`Child - ${data}`}</h3>;\n                  },\n                },\n              ]);\n            }\n          },\n          initialEntries: [\"/parent/child\"],\n        },\n      );\n      let { container } = render(\n        // eslint-disable-next-line react/jsx-pascal-case\n        <ReactRouter_RouterProvider router={router} />,\n      );\n\n      parentDfd.resolve(\"PARENT DATA\");\n      expect(getHtml(container)).toMatchInlineSnapshot(`\n        \"<div>\n          <h1>\n            Root\n          </h1>\n          <p>\n            Parent Loading...\n          </p>\n        </div>\"\n      `);\n\n      childDfd.resolve(\"CHILD DATA\");\n      await waitFor(() => screen.getByText(/CHILD DATA/));\n      expect(getHtml(container)).toMatchInlineSnapshot(`\n        \"<div>\n          <h1>\n            Root\n          </h1>\n          <h2>\n            Parent - PARENT DATA\n          </h2>\n          <h3>\n            Child - CHILD DATA\n          </h3>\n        </div>\"\n      `);\n    });\n\n    it(\"supports partial hydration w/patchRoutesOnNavigation (root fallback)\", async () => {\n      let parentDfd = createDeferred();\n      let childDfd = createDeferred();\n      let router = createMemoryRouter(\n        [\n          {\n            path: \"/\",\n            HydrateFallback: () => <p>Root Loading...</p>,\n            Component() {\n              return (\n                <>\n                  <h1>Root</h1>\n                  <Outlet />\n                </>\n              );\n            },\n            children: [\n              {\n                id: \"parent\",\n                path: \"parent\",\n                loader: () => parentDfd.promise,\n                Component() {\n                  let data = useLoaderData() as string;\n                  return (\n                    <>\n                      <h2>{`Parent - ${data}`}</h2>\n                      <Outlet />\n                    </>\n                  );\n                },\n              },\n            ],\n          },\n        ],\n        {\n          future: {\n            v7_partialHydration: true,\n          },\n          patchRoutesOnNavigation({ path, patch }) {\n            if (path === \"/parent/child\") {\n              patch(\"parent\", [\n                {\n                  path: \"child\",\n                  loader: () => childDfd.promise,\n                  Component() {\n                    let data = useLoaderData() as string;\n                    return <h3>{`Child - ${data}`}</h3>;\n                  },\n                },\n              ]);\n            }\n          },\n          initialEntries: [\"/parent/child\"],\n        },\n      );\n      let { container } = render(\n        // eslint-disable-next-line react/jsx-pascal-case\n        <ReactRouter_RouterProvider router={router} />,\n      );\n\n      parentDfd.resolve(\"PARENT DATA\");\n      expect(getHtml(container)).toMatchInlineSnapshot(`\n        \"<div>\n          <p>\n            Root Loading...\n          </p>\n        </div>\"\n      `);\n\n      childDfd.resolve(\"CHILD DATA\");\n      await waitFor(() => screen.getByText(/CHILD DATA/));\n      expect(getHtml(container)).toMatchInlineSnapshot(`\n        \"<div>\n          <h1>\n            Root\n          </h1>\n          <h2>\n            Parent - PARENT DATA\n          </h2>\n          <h3>\n            Child - CHILD DATA\n          </h3>\n        </div>\"\n      `);\n    });\n\n    it(\"supports partial hydration w/patchRoutesOnNavigation and matching splat\", async () => {\n      let patchDfd = createDeferred();\n      let parentDfd = createDeferred();\n      let childDfd = createDeferred();\n      let router = createMemoryRouter(\n        [\n          {\n            path: \"/\",\n            HydrateFallback: () => <p>Root Loading...</p>,\n            Component() {\n              return (\n                <>\n                  <h1>Root</h1>\n                  <Outlet />\n                </>\n              );\n            },\n            children: [\n              {\n                id: \"parent\",\n                path: \"parent\",\n                loader: () => parentDfd.promise,\n                Component() {\n                  let data = useLoaderData() as string;\n                  return (\n                    <>\n                      <h2>{`Parent - ${data}`}</h2>\n                      <Outlet />\n                    </>\n                  );\n                },\n              },\n              {\n                path: \"*\",\n                Component() {\n                  return <h2>Splat</h2>;\n                },\n              },\n            ],\n          },\n        ],\n        {\n          future: {\n            v7_partialHydration: true,\n          },\n          async patchRoutesOnNavigation({ path, patch }) {\n            await patchDfd.promise;\n            if (path === \"/parent/child\") {\n              patch(\"parent\", [\n                {\n                  path: \"child\",\n                  loader: () => childDfd.promise,\n                  Component() {\n                    let data = useLoaderData() as string;\n                    return <h3>{`Child - ${data}`}</h3>;\n                  },\n                },\n              ]);\n            }\n          },\n          initialEntries: [\"/parent/child\"],\n        },\n      );\n      let { container } = render(\n        // eslint-disable-next-line react/jsx-pascal-case\n        <ReactRouter_RouterProvider router={router} />,\n      );\n\n      expect(getHtml(container)).toMatchInlineSnapshot(`\n        \"<div>\n          <p>\n            Root Loading...\n          </p>\n        </div>\"\n      `);\n\n      patchDfd.resolve();\n      parentDfd.resolve(\"PARENT DATA\");\n      await tick();\n      expect(getHtml(container)).toMatchInlineSnapshot(`\n        \"<div>\n          <p>\n            Root Loading...\n          </p>\n        </div>\"\n      `);\n\n      childDfd.resolve(\"CHILD DATA\");\n      await waitFor(() => screen.getByText(/CHILD DATA/));\n      expect(getHtml(container)).toMatchInlineSnapshot(`\n        \"<div>\n          <h1>\n            Root\n          </h1>\n          <h2>\n            Parent - PARENT DATA\n          </h2>\n          <h3>\n            Child - CHILD DATA\n          </h3>\n        </div>\"\n      `);\n    });\n  });\n});\n\nfunction testPartialHydration(\n  createTestRouter:\n    | typeof createBrowserRouter\n    | typeof createHashRouter\n    | typeof createMemoryRouter,\n  RouterProvider:\n    | typeof ReactRouterDom_RouterProvider\n    | typeof ReactRouter_RouterProvider,\n) {\n  let consoleWarn: jest.SpyInstance;\n\n  beforeEach(() => {\n    consoleWarn = jest.spyOn(console, \"warn\").mockImplementation(() => {});\n  });\n\n  afterEach(() => {\n    consoleWarn.mockRestore();\n  });\n\n  it(\"supports partial hydration w/leaf fallback\", async () => {\n    let dfd = createDeferred();\n    let router = createTestRouter(\n      [\n        {\n          id: \"root\",\n          path: \"/\",\n          loader: () => \"ROOT\",\n          Component() {\n            let data = useLoaderData() as string;\n            return (\n              <>\n                <h1>{`Home - ${data}`}</h1>\n                <Outlet />\n              </>\n            );\n          },\n          children: [\n            {\n              id: \"index\",\n              index: true,\n              loader: () => dfd.promise,\n              HydrateFallback: () => <p>Index Loading...</p>,\n              Component() {\n                let data = useLoaderData() as string;\n                return <h2>{`Index - ${data}`}</h2>;\n              },\n            },\n          ],\n        },\n      ],\n      {\n        hydrationData: {\n          loaderData: {\n            root: \"HYDRATED ROOT\",\n          },\n        },\n      },\n    );\n    let { container } = render(<RouterProvider router={router} />);\n\n    expect(getHtml(container)).toMatchInlineSnapshot(`\n      \"<div>\n        <h1>\n          Home - HYDRATED ROOT\n        </h1>\n        <p>\n          Index Loading...\n        </p>\n      </div>\"\n    `);\n\n    dfd.resolve(\"INDEX DATA\");\n    await waitFor(() => screen.getByText(/INDEX DATA/));\n    expect(getHtml(container)).toMatchInlineSnapshot(`\n      \"<div>\n        <h1>\n          Home - HYDRATED ROOT\n        </h1>\n        <h2>\n          Index - INDEX DATA\n        </h2>\n      </div>\"\n    `);\n  });\n\n  it(\"supports partial hydration w/root fallback\", async () => {\n    let dfd = createDeferred();\n    let router = createTestRouter(\n      [\n        {\n          id: \"root\",\n          path: \"/\",\n          loader: () => \"ROOT\",\n          HydrateFallback: () => <p>Root Loading...</p>,\n          Component() {\n            let data = useLoaderData() as string;\n            return (\n              <>\n                <h1>{`Home - ${data}`}</h1>\n                <Outlet />\n              </>\n            );\n          },\n          children: [\n            {\n              id: \"index\",\n              index: true,\n              loader: () => dfd.promise,\n              Component() {\n                let data = useLoaderData() as string;\n                return <h2>{`Index - ${data}`}</h2>;\n              },\n            },\n          ],\n        },\n      ],\n      {\n        hydrationData: {\n          loaderData: {\n            root: \"HYDRATED ROOT\",\n          },\n        },\n      },\n    );\n    let { container } = render(<RouterProvider router={router} />);\n\n    expect(getHtml(container)).toMatchInlineSnapshot(`\n      \"<div>\n        <p>\n          Root Loading...\n        </p>\n      </div>\"\n    `);\n\n    dfd.resolve(\"INDEX DATA\");\n    await waitFor(() => screen.getByText(/INDEX DATA/));\n    expect(getHtml(container)).toMatchInlineSnapshot(`\n      \"<div>\n        <h1>\n          Home - HYDRATED ROOT\n        </h1>\n        <h2>\n          Index - INDEX DATA\n        </h2>\n      </div>\"\n    `);\n  });\n\n  it(\"supports partial hydration w/no fallback\", async () => {\n    let dfd = createDeferred();\n    let router = createTestRouter(\n      [\n        {\n          id: \"root\",\n          path: \"/\",\n          loader: () => \"ROOT\",\n          Component() {\n            let data = useLoaderData() as string;\n            return (\n              <>\n                <h1>{`Home - ${data}`}</h1>\n                <Outlet />\n              </>\n            );\n          },\n          children: [\n            {\n              id: \"index\",\n              index: true,\n              loader: () => dfd.promise,\n              Component() {\n                let data = useLoaderData() as string;\n                return <h2>{`Index - ${data}`}</h2>;\n              },\n            },\n          ],\n        },\n      ],\n      {\n        hydrationData: {\n          loaderData: {\n            root: \"HYDRATED ROOT\",\n          },\n        },\n      },\n    );\n    let { container } = render(<RouterProvider router={router} />);\n\n    expect(getHtml(container)).toMatchInlineSnapshot(`\"<div />\"`);\n\n    // We can't assert this in all 3 test executions because we use `warningOnce`\n    // internally to avoid logging on every render\n    if (!didAssertMissingHydrateFallback) {\n      didAssertMissingHydrateFallback = true;\n      // eslint-disable-next-line jest/no-conditional-expect\n      expect(consoleWarn).toHaveBeenCalledWith(\n        \"No `HydrateFallback` element provided to render during initial hydration\",\n      );\n    }\n\n    dfd.resolve(\"INDEX DATA\");\n    await waitFor(() => screen.getByText(/INDEX DATA/));\n    expect(getHtml(container)).toMatchInlineSnapshot(`\n      \"<div>\n        <h1>\n          Home - HYDRATED ROOT\n        </h1>\n        <h2>\n          Index - INDEX DATA\n        </h2>\n      </div>\"\n    `);\n  });\n\n  it(\"does not re-run loaders that don't have loader data due to errors\", async () => {\n    let spy = jest.fn();\n    let router = createTestRouter(\n      [\n        {\n          id: \"root\",\n          path: \"/\",\n          loader: () => \"ROOT\",\n          Component() {\n            let data = useLoaderData() as string;\n            return (\n              <>\n                <h1>{`Home - ${data}`}</h1>\n                <Outlet />\n              </>\n            );\n          },\n          children: [\n            {\n              id: \"index\",\n              index: true,\n              loader: spy,\n              HydrateFallback: () => <p>Index Loading...</p>,\n              Component() {\n                let data = useLoaderData() as string;\n                return <h2>{`Index - ${data}`}</h2>;\n              },\n              ErrorBoundary() {\n                let error = useRouteError() as string;\n                return <p>{error}</p>;\n              },\n            },\n          ],\n        },\n      ],\n      {\n        hydrationData: {\n          loaderData: {\n            root: \"HYDRATED ROOT\",\n          },\n          errors: {\n            index: \"INDEX ERROR\",\n          },\n        },\n      },\n    );\n    let { container } = render(<RouterProvider router={router} />);\n\n    expect(getHtml(container)).toMatchInlineSnapshot(`\n      \"<div>\n        <h1>\n          Home - HYDRATED ROOT\n        </h1>\n        <p>\n          INDEX ERROR\n        </p>\n      </div>\"\n    `);\n\n    expect(spy).not.toHaveBeenCalled();\n  });\n\n  it(\"lets users force hydration loader execution with loader.hydrate=true\", async () => {\n    let dfd = createDeferred();\n    let indexLoader: LoaderFunction = () => dfd.promise;\n    indexLoader.hydrate = true;\n    let router = createTestRouter(\n      [\n        {\n          id: \"root\",\n          path: \"/\",\n          loader: () => \"ROOT\",\n          Component() {\n            let data = useLoaderData() as string;\n            return (\n              <>\n                <h1>{`Home - ${data}`}</h1>\n                <Outlet />\n              </>\n            );\n          },\n          children: [\n            {\n              id: \"index\",\n              index: true,\n              loader: indexLoader,\n              HydrateFallback: () => <p>Index Loading...</p>,\n              Component() {\n                let data = useLoaderData() as string;\n                return <h2>{`Index - ${data}`}</h2>;\n              },\n            },\n          ],\n        },\n      ],\n      {\n        hydrationData: {\n          loaderData: {\n            root: \"HYDRATED ROOT\",\n            index: \"INDEX INITIAL\",\n          },\n        },\n      },\n    );\n    let { container } = render(<RouterProvider router={router} />);\n\n    expect(getHtml(container)).toMatchInlineSnapshot(`\n      \"<div>\n        <h1>\n          Home - HYDRATED ROOT\n        </h1>\n        <h2>\n          Index - INDEX INITIAL\n        </h2>\n      </div>\"\n    `);\n\n    dfd.resolve(\"INDEX UPDATED\");\n    await waitFor(() => screen.getByText(/INDEX UPDATED/));\n    expect(getHtml(container)).toMatchInlineSnapshot(`\n      \"<div>\n        <h1>\n          Home - HYDRATED ROOT\n        </h1>\n        <h2>\n          Index - INDEX UPDATED\n        </h2>\n      </div>\"\n    `);\n  });\n\n  it(\"supports partial hydration w/lazy initial routes (leaf fallback)\", async () => {\n    let dfd = createDeferred();\n    let router = createTestRouter([\n      {\n        path: \"/\",\n        Component() {\n          return (\n            <>\n              <h1>Root</h1>\n              <Outlet />\n            </>\n          );\n        },\n        children: [\n          {\n            id: \"index\",\n            index: true,\n            HydrateFallback: () => <p>Index Loading...</p>,\n            async lazy() {\n              await tick();\n              return {\n                loader: () => dfd.promise,\n                Component() {\n                  let data = useLoaderData() as string;\n                  return <h2>{`Index - ${data}`}</h2>;\n                },\n              };\n            },\n          },\n        ],\n      },\n    ]);\n    let { container } = render(<RouterProvider router={router} />);\n\n    expect(getHtml(container)).toMatchInlineSnapshot(`\n      \"<div>\n        <h1>\n          Root\n        </h1>\n        <p>\n          Index Loading...\n        </p>\n      </div>\"\n    `);\n\n    dfd.resolve(\"INDEX DATA\");\n    await waitFor(() => screen.getByText(/INDEX DATA/));\n    expect(getHtml(container)).toMatchInlineSnapshot(`\n      \"<div>\n        <h1>\n          Root\n        </h1>\n        <h2>\n          Index - INDEX DATA\n        </h2>\n      </div>\"\n    `);\n  });\n\n  it(\"supports partial hydration w/lazy initial routes (root fallback)\", async () => {\n    let dfd = createDeferred();\n    let router = createTestRouter([\n      {\n        path: \"/\",\n        Component() {\n          return (\n            <>\n              <h1>Root</h1>\n              <Outlet />\n            </>\n          );\n        },\n        HydrateFallback: () => <p>Loading...</p>,\n        children: [\n          {\n            id: \"index\",\n            index: true,\n            async lazy() {\n              await tick();\n              return {\n                loader: () => dfd.promise,\n                Component() {\n                  let data = useLoaderData() as string;\n                  return <h2>{`Index - ${data}`}</h2>;\n                },\n              };\n            },\n          },\n        ],\n      },\n    ]);\n    let { container } = render(<RouterProvider router={router} />);\n\n    expect(getHtml(container)).toMatchInlineSnapshot(`\n      \"<div>\n        <p>\n          Loading...\n        </p>\n      </div>\"\n    `);\n\n    dfd.resolve(\"INDEX DATA\");\n    await waitFor(() => screen.getByText(/INDEX DATA/));\n    expect(getHtml(container)).toMatchInlineSnapshot(`\n      \"<div>\n        <h1>\n          Root\n        </h1>\n        <h2>\n          Index - INDEX DATA\n        </h2>\n      </div>\"\n    `);\n  });\n\n  it(\"preserves hydrated errors for non-hydrating loaders\", async () => {\n    let dfd = createDeferred();\n    let rootSpy: LoaderFunction = jest.fn(() => dfd.promise);\n    rootSpy.hydrate = true;\n\n    let indexSpy = jest.fn();\n\n    let router = createTestRouter(\n      [\n        {\n          id: \"root\",\n          path: \"/\",\n          loader: rootSpy,\n          Component() {\n            let data = useLoaderData() as string;\n            return (\n              <>\n                <h1>{`Home - ${data}`}</h1>\n                <Outlet />\n              </>\n            );\n          },\n          children: [\n            {\n              id: \"index\",\n              index: true,\n              loader: indexSpy,\n              Component() {\n                let data = useLoaderData() as string;\n                return <h2>{`Index - ${data}`}</h2>;\n              },\n              ErrorBoundary() {\n                let error = useRouteError() as string;\n                return <p>{error}</p>;\n              },\n            },\n          ],\n        },\n      ],\n      {\n        hydrationData: {\n          loaderData: {\n            root: \"HYDRATED ROOT\",\n          },\n          errors: {\n            index: \"INDEX ERROR\",\n          },\n        },\n      },\n    );\n    let { container } = render(<RouterProvider router={router} />);\n\n    expect(getHtml(container)).toMatchInlineSnapshot(`\n      \"<div>\n        <h1>\n          Home - HYDRATED ROOT\n        </h1>\n        <p>\n          INDEX ERROR\n        </p>\n      </div>\"\n    `);\n\n    expect(router.state.initialized).toBe(false);\n\n    await act(() => dfd.resolve(\"UPDATED ROOT\"));\n\n    expect(getHtml(container)).toMatchInlineSnapshot(`\n      \"<div>\n        <h1>\n          Home - UPDATED ROOT\n        </h1>\n        <p>\n          INDEX ERROR\n        </p>\n      </div>\"\n    `);\n\n    expect(rootSpy).toHaveBeenCalledTimes(1);\n    expect(indexSpy).not.toHaveBeenCalled();\n  });\n}\n"
  },
  {
    "path": "packages/react-router/__tests__/dom/polyfills/drop-FormData-submitter.ts",
    "content": "// Drop support for the submitter parameter, as in a legacy browser. This needs\n// to be a standalone module due to how jest requires things (i.e. we can't\n// just do this inline in data-browser-router-legacy-formdata-test.tsx)\nwindow.FormData = class FormData extends window[\"FormData\"] {\n  constructor(form?: HTMLFormElement) {\n    super(form, undefined);\n  }\n};\n"
  },
  {
    "path": "packages/react-router/__tests__/dom/scroll-restoration-test.tsx",
    "content": "import * as React from \"react\";\nimport { render, fireEvent, screen } from \"@testing-library/react\";\nimport \"@testing-library/jest-dom\";\n\nimport getHtml from \"../utils/getHtml\";\nimport getWindow from \"../utils/getWindow\";\nimport {\n  Link,\n  Outlet,\n  RouterProvider,\n  ScrollRestoration,\n  createBrowserRouter,\n} from \"../../index\";\nimport { createMemoryRouter, redirect } from \"react-router\";\nimport { FrameworkContext, Scripts } from \"../../lib/dom/ssr/components\";\nimport \"@testing-library/jest-dom\";\nimport { mockFrameworkContext } from \"../utils/framework\";\n\ndescribe(`ScrollRestoration`, () => {\n  it(\"restores the scroll position for a page when re-visited\", () => {\n    const consoleWarnMock = jest\n      .spyOn(console, \"warn\")\n      .mockImplementation(() => {});\n\n    let testWindow = getWindow(\"/base\");\n    const mockScroll = jest.fn();\n    window.scrollTo = mockScroll;\n\n    let router = createBrowserRouter(\n      [\n        {\n          path: \"/\",\n          Component() {\n            return (\n              <>\n                <Outlet />\n                <ScrollRestoration\n                  getKey={(location) => \"test1-\" + location.pathname}\n                />\n              </>\n            );\n          },\n          children: testPages,\n        },\n      ],\n      { basename: \"/base\", window: testWindow },\n    );\n    let { container } = render(<RouterProvider router={router} />);\n\n    expect(getHtml(container)).toMatch(\"On page 1\");\n\n    // simulate scrolling\n    Object.defineProperty(window, \"scrollY\", { writable: true, value: 100 });\n\n    // leave page\n    window.dispatchEvent(new Event(\"pagehide\"));\n    fireEvent.click(screen.getByText(\"Go to page 2\"));\n    expect(getHtml(container)).toMatch(\"On page 2\");\n\n    // return to page\n    window.dispatchEvent(new Event(\"pagehide\"));\n    fireEvent.click(screen.getByText(\"Go to page 1\"));\n\n    expect(getHtml(container)).toMatch(\"On page 1\");\n\n    // check scroll activity\n    expect(mockScroll.mock.calls).toEqual([\n      [0, 0],\n      [0, 0],\n      [0, 100], // restored\n    ]);\n\n    expect(consoleWarnMock).not.toHaveBeenCalled();\n    consoleWarnMock.mockRestore();\n  });\n\n  it(\"removes the basename from the location provided to getKey\", () => {\n    let getKey = jest.fn(() => \"mykey\");\n    let testWindow = getWindow(\"/base\");\n    window.scrollTo = () => {};\n\n    let router = createBrowserRouter(\n      [\n        {\n          path: \"/\",\n          Component() {\n            return (\n              <>\n                <Outlet />\n                <ScrollRestoration getKey={getKey} />\n              </>\n            );\n          },\n          children: [\n            {\n              index: true,\n              Component() {\n                return <Link to=\"/page\">/page</Link>;\n              },\n            },\n            {\n              path: \"page\",\n              Component() {\n                return <h1>Page</h1>;\n              },\n            },\n          ],\n        },\n      ],\n      { basename: \"/base\", window: testWindow },\n    );\n    let { container } = render(<RouterProvider router={router} />);\n\n    expect(getKey.mock.calls.length).toBe(1);\n    // @ts-expect-error\n    expect(getKey.mock.calls[0][0].pathname).toBe(\"/\"); // restore\n\n    expect(getHtml(container)).toMatch(\"/page\");\n    fireEvent.click(screen.getByText(\"/page\"));\n    expect(getHtml(container)).toMatch(\"Page\");\n\n    expect(getKey.mock.calls.length).toBe(3);\n    // @ts-expect-error\n    expect(getKey.mock.calls[1][0].pathname).toBe(\"/\"); // save\n    // @ts-expect-error\n    expect(getKey.mock.calls[2][0].pathname).toBe(\"/page\"); // restore\n  });\n\n  it(\"fails gracefully if sessionStorage is not available\", () => {\n    const consoleWarnMock = jest\n      .spyOn(console, \"warn\")\n      .mockImplementation(() => {});\n\n    let testWindow = getWindow(\"/base\");\n    const mockScroll = jest.fn();\n    window.scrollTo = mockScroll;\n\n    jest.spyOn(window, \"sessionStorage\", \"get\").mockImplementation(() => {\n      throw new Error(\"denied\");\n    });\n\n    let router = createBrowserRouter(\n      [\n        {\n          path: \"/\",\n          Component() {\n            return (\n              <>\n                <Outlet />\n                <ScrollRestoration\n                  getKey={(location) => \"test2-\" + location.pathname}\n                />\n              </>\n            );\n          },\n          children: testPages,\n        },\n      ],\n      { basename: \"/base\", window: testWindow },\n    );\n    let { container } = render(<RouterProvider router={router} />);\n\n    expect(getHtml(container)).toMatch(\"On page 1\");\n\n    // simulate scrolling\n    Object.defineProperty(window, \"scrollY\", { writable: true, value: 100 });\n\n    // leave page\n    window.dispatchEvent(new Event(\"pagehide\"));\n    fireEvent.click(screen.getByText(\"Go to page 2\"));\n    expect(getHtml(container)).toMatch(\"On page 2\");\n\n    // return to page\n    window.dispatchEvent(new Event(\"pagehide\"));\n    fireEvent.click(screen.getByText(\"Go to page 1\"));\n\n    expect(getHtml(container)).toMatch(\"On page 1\");\n\n    // check scroll activity\n    expect(mockScroll.mock.calls).toEqual([\n      [0, 0],\n      [0, 0],\n      [0, 100], // restored (still possible because the user hasn't left the page)\n    ]);\n\n    expect(consoleWarnMock).toHaveBeenCalledWith(\n      expect.stringContaining(\n        \"Failed to save scroll positions in sessionStorage\",\n      ),\n    );\n\n    consoleWarnMock.mockRestore();\n  });\n\n  describe(\"SSR\", () => {\n    let scrollTo = window.scrollTo;\n    beforeAll(() => {\n      window.scrollTo = (options) => {\n        window.scrollY = options.left;\n      };\n    });\n\n    afterEach(() => {\n      jest.resetAllMocks();\n    });\n    afterAll(() => {\n      window.scrollTo = scrollTo;\n    });\n\n    let context = mockFrameworkContext();\n\n    it(\"should render a <script> tag\", () => {\n      let router = createMemoryRouter([\n        {\n          id: \"root\",\n          path: \"/\",\n          element: (\n            <>\n              <Outlet />\n              <ScrollRestoration data-testid=\"scroll-script\" />\n              <Scripts />\n            </>\n          ),\n        },\n      ]);\n\n      render(\n        <FrameworkContext.Provider value={context}>\n          <RouterProvider router={router} />\n        </FrameworkContext.Provider>,\n      );\n      let script = screen.getByTestId(\"scroll-script\");\n      expect(script instanceof HTMLScriptElement).toBe(true);\n    });\n\n    it(\"should pass props to <script>\", () => {\n      let router = createMemoryRouter([\n        {\n          id: \"root\",\n          path: \"/\",\n          element: (\n            <>\n              <Outlet />\n              <ScrollRestoration\n                data-testid=\"scroll-script\"\n                nonce=\"hello\"\n                crossOrigin=\"anonymous\"\n              />\n              <Scripts />\n            </>\n          ),\n        },\n      ]);\n      render(\n        <FrameworkContext.Provider value={context}>\n          <RouterProvider router={router} />\n        </FrameworkContext.Provider>,\n      );\n      let script = screen.getByTestId(\"scroll-script\");\n      expect(script).toHaveAttribute(\"nonce\", \"hello\");\n      expect(script).toHaveAttribute(\"crossorigin\", \"anonymous\");\n    });\n\n    it(\"should restore scroll position\", () => {\n      let scrollToMock = jest.spyOn(window, \"scrollTo\");\n      let router = createMemoryRouter([\n        {\n          id: \"root\",\n          path: \"/\",\n          element: (\n            <>\n              <Outlet />\n              <ScrollRestoration />\n              <Scripts />\n            </>\n          ),\n        },\n      ]);\n      router.state.restoreScrollPosition = 20;\n      render(\n        <FrameworkContext.Provider value={context}>\n          <RouterProvider router={router} />\n        </FrameworkContext.Provider>,\n      );\n\n      expect(scrollToMock).toHaveBeenCalledWith(0, 20);\n    });\n\n    it(\"should restore scroll position on navigation\", () => {\n      let scrollToMock = jest.spyOn(window, \"scrollTo\");\n      let router = createMemoryRouter([\n        {\n          id: \"root\",\n          path: \"/\",\n          element: (\n            <>\n              <Outlet />\n              <ScrollRestoration />\n              <Scripts />\n            </>\n          ),\n        },\n      ]);\n      render(\n        <FrameworkContext.Provider value={context}>\n          <RouterProvider router={router} />\n        </FrameworkContext.Provider>,\n      );\n      // Always called when using <ScrollRestoration />\n      expect(scrollToMock).toHaveBeenCalledWith(0, 0);\n      // Mock user scroll\n      window.scrollTo(0, 20);\n      // Mock navigation\n      redirect(\"/otherplace\");\n      // Mock return to original page where navigation had happened\n      expect(scrollToMock).toHaveBeenCalledWith(0, 0);\n      // Mock return to original page where navigation had happened\n      redirect(\"/\");\n      // Ensure that scroll position is restored\n      expect(scrollToMock).toHaveBeenCalledWith(0, 20);\n    });\n  });\n});\n\nconst testPages = [\n  {\n    index: true,\n    Component() {\n      return (\n        <p>\n          On page 1<br />\n          <Link to=\"/page\">Go to page 2</Link>\n        </p>\n      );\n    },\n  },\n  {\n    path: \"page\",\n    Component() {\n      return (\n        <p>\n          On page 2<br />\n          <Link to=\"/\">Go to page 1</Link>\n        </p>\n      );\n    },\n  },\n];\n"
  },
  {
    "path": "packages/react-router/__tests__/dom/search-params-test.tsx",
    "content": "import * as React from \"react\";\nimport * as ReactDOM from \"react-dom/client\";\nimport { act } from \"@testing-library/react\";\nimport {\n  MemoryRouter,\n  Routes,\n  Route,\n  useSearchParams,\n  createBrowserRouter,\n  useBlocker,\n  RouterProvider,\n  useLocation,\n} from \"../../index\";\n\ndescribe(\"useSearchParams\", () => {\n  let node: HTMLDivElement;\n  beforeEach(() => {\n    node = document.createElement(\"div\");\n    document.body.appendChild(node);\n  });\n\n  afterEach(() => {\n    document.body.removeChild(node);\n    node = null!;\n  });\n\n  it(\"reads and writes the search string\", () => {\n    function SearchPage() {\n      let queryRef = React.useRef<HTMLInputElement>(null);\n      let [searchParams, setSearchParams] = useSearchParams({ q: \"\" });\n      let query = searchParams.get(\"q\")!;\n\n      function handleSubmit(event: React.FormEvent<HTMLFormElement>) {\n        event.preventDefault();\n        if (queryRef.current) {\n          setSearchParams({ q: queryRef.current.value });\n        }\n      }\n\n      return (\n        <div>\n          <p>The current query is \"{query}\".</p>\n          <form onSubmit={handleSubmit}>\n            <input name=\"q\" defaultValue={query} ref={queryRef} />\n          </form>\n        </div>\n      );\n    }\n\n    act(() => {\n      ReactDOM.createRoot(node).render(\n        <MemoryRouter initialEntries={[\"/search?q=Michael+Jackson\"]}>\n          <Routes>\n            <Route path=\"search\" element={<SearchPage />} />\n          </Routes>\n        </MemoryRouter>,\n      );\n    });\n\n    let form = node.querySelector(\"form\")!;\n    expect(form).toBeDefined();\n\n    let queryInput = node.querySelector<HTMLInputElement>(\"input[name=q]\")!;\n    expect(queryInput).toBeDefined();\n\n    expect(node.innerHTML).toMatch(/The current query is \"Michael Jackson\"/);\n\n    act(() => {\n      queryInput.value = \"Ryan Florence\";\n      form.dispatchEvent(\n        new Event(\"submit\", { bubbles: true, cancelable: true }),\n      );\n    });\n\n    expect(node.innerHTML).toMatch(/The current query is \"Ryan Florence\"/);\n  });\n\n  it(\"updates searchParams when a function is provided to setSearchParams (functional updates)\", () => {\n    function SearchPage() {\n      let queryRef = React.useRef<HTMLInputElement>(null);\n      let [searchParams, setSearchParams] = useSearchParams({ q: \"\" });\n      let query = searchParams.get(\"q\")!;\n      let queryNew = searchParams.get(\"new\")!;\n\n      function handleSubmit(event: React.FormEvent<HTMLFormElement>) {\n        event.preventDefault();\n        if (queryRef.current) {\n          setSearchParams((cur) => {\n            cur.set(\"q\", `${cur.get(\"q\")} - appended`);\n            cur.set(\"new\", \"Ryan Florence\");\n            return cur;\n          });\n        }\n      }\n\n      return (\n        <div>\n          <p>The current query is \"{query}\".</p>\n          <p>The new query is \"{queryNew}\"</p>\n          <form onSubmit={handleSubmit}>\n            <input name=\"q\" defaultValue={query} ref={queryRef} />\n          </form>\n        </div>\n      );\n    }\n\n    act(() => {\n      ReactDOM.createRoot(node).render(\n        <MemoryRouter initialEntries={[\"/search?q=Michael+Jackson\"]}>\n          <Routes>\n            <Route path=\"search\" element={<SearchPage />} />\n          </Routes>\n        </MemoryRouter>,\n      );\n    });\n\n    let form = node.querySelector(\"form\")!;\n    expect(form).toBeDefined();\n\n    let queryInput = node.querySelector<HTMLInputElement>(\"input[name=q]\")!;\n    expect(queryInput).toBeDefined();\n\n    expect(node.innerHTML).toMatch(/The current query is \"Michael Jackson\"/);\n    expect(node.innerHTML).toMatch(/The new query is \"\"/);\n\n    act(() => {\n      form.dispatchEvent(\n        new Event(\"submit\", { bubbles: true, cancelable: true }),\n      );\n    });\n\n    expect(node.innerHTML).toMatch(\n      /The current query is \"Michael Jackson - appended\"/,\n    );\n    expect(node.innerHTML).toMatch(/The new query is \"Ryan Florence\"/);\n  });\n\n  it(\"allows removal of search params when a default is provided\", () => {\n    function SearchPage() {\n      let [searchParams, setSearchParams] = useSearchParams({\n        value: \"initial\",\n      });\n\n      return (\n        <div>\n          <p>The current value is \"{searchParams.get(\"value\")}\".</p>\n          <button onClick={() => setSearchParams({})}>Click</button>\n        </div>\n      );\n    }\n\n    act(() => {\n      ReactDOM.createRoot(node).render(\n        <MemoryRouter initialEntries={[\"/search?value=initial\"]}>\n          <Routes>\n            <Route path=\"search\" element={<SearchPage />} />\n          </Routes>\n        </MemoryRouter>,\n      );\n    });\n\n    let button = node.querySelector<HTMLInputElement>(\"button\")!;\n    expect(button).toBeDefined();\n\n    expect(node.innerHTML).toMatch(/The current value is \"initial\"/);\n\n    act(() => {\n      button.dispatchEvent(new Event(\"click\", { bubbles: true }));\n    });\n\n    expect(node.innerHTML).toMatch(/The current value is \"\"/);\n  });\n\n  it(\"returns initial default values in search params\", () => {\n    function SearchPage() {\n      let [searchParams] = useSearchParams({ a: \"1\", b: \"2\" });\n      return <p>{searchParams.toString()}</p>;\n    }\n\n    act(() => {\n      ReactDOM.createRoot(node).render(\n        <MemoryRouter initialEntries={[\"/search?value=initial\"]}>\n          <Routes>\n            <Route path=\"search\" element={<SearchPage />} />\n          </Routes>\n        </MemoryRouter>,\n      );\n    });\n\n    expect(node.innerHTML).toMatchInlineSnapshot(\n      `\"<p>value=initial&amp;a=1&amp;b=2</p>\"`,\n    );\n  });\n\n  it(\"does not reflect functional update mutation when navigation is blocked\", () => {\n    let router = createBrowserRouter([\n      {\n        path: \"/\",\n        Component() {\n          let location = useLocation();\n          let [searchParams, setSearchParams] = useSearchParams();\n          let [shouldBlock, setShouldBlock] = React.useState(false);\n          let b = useBlocker(shouldBlock);\n          return (\n            <>\n              <pre id=\"output\">\n                {`location.search=${location.search}`}\n                {`searchParams=${searchParams.toString()}`}\n                {`blocked=${b.state}`}\n              </pre>\n              <button\n                id=\"toggle-blocking\"\n                onClick={() => setShouldBlock(!shouldBlock)}\n              >\n                Toggle Blocking\n              </button>\n              <button\n                id=\"navigate1\"\n                onClick={() => {\n                  setSearchParams((prev) => {\n                    prev.set(\"foo\", \"bar\");\n                    return prev;\n                  });\n                }}\n              >\n                Navigate 1\n              </button>\n              <button\n                id=\"navigate2\"\n                onClick={() => {\n                  setSearchParams((prev) => {\n                    prev.set(\"foo\", \"baz\");\n                    return prev;\n                  });\n                }}\n              >\n                Navigate 2\n              </button>\n            </>\n          );\n        },\n      },\n    ]);\n\n    act(() => {\n      ReactDOM.createRoot(node).render(<RouterProvider router={router} />);\n    });\n\n    expect(node.querySelector(\"#output\")).toMatchInlineSnapshot(`\n      <pre\n        id=\"output\"\n      >\n        location.search=\n        searchParams=\n        blocked=unblocked\n      </pre>\n    `);\n\n    act(() => {\n      node\n        .querySelector(\"#navigate1\")!\n        .dispatchEvent(new Event(\"click\", { bubbles: true }));\n    });\n\n    expect(node.querySelector(\"#output\")).toMatchInlineSnapshot(`\n      <pre\n        id=\"output\"\n      >\n        location.search=?foo=bar\n        searchParams=foo=bar\n        blocked=unblocked\n      </pre>\n    `);\n\n    act(() => {\n      node\n        .querySelector(\"#toggle-blocking\")!\n        .dispatchEvent(new Event(\"click\", { bubbles: true }));\n    });\n\n    act(() => {\n      node\n        .querySelector(\"#navigate2\")!\n        .dispatchEvent(new Event(\"click\", { bubbles: true }));\n    });\n\n    expect(node.querySelector(\"#output\")).toMatchInlineSnapshot(`\n      <pre\n        id=\"output\"\n      >\n        location.search=?foo=bar\n        searchParams=foo=bar\n        blocked=blocked\n      </pre>\n    `);\n  });\n});\n"
  },
  {
    "path": "packages/react-router/__tests__/dom/special-characters-test.tsx",
    "content": "/* eslint-disable jest/expect-expect */\n\nimport * as React from \"react\";\nimport {\n  cleanup,\n  render,\n  fireEvent,\n  waitFor,\n  screen,\n} from \"@testing-library/react\";\nimport type { Location, Params } from \"../../index\";\nimport {\n  BrowserRouter,\n  HashRouter,\n  MemoryRouter,\n  Link,\n  Outlet,\n  Routes,\n  Route,\n  RouterProvider,\n  createBrowserRouter,\n  createHashRouter,\n  createMemoryRouter,\n  createRoutesFromElements,\n  useLocation,\n  useMatch,\n  useNavigate,\n  useParams,\n} from \"../../index\";\nimport getWindow from \"../utils/getWindow\";\nimport getHtml from \"../utils/getHtml\";\n\n/**\n * Here's all the special characters we want to test against.  This list was\n * generated using the following utility in the Chrome DevTools console for\n * maximum accuracy.  This is instead of programmatically generating during\n * these tests where JSDOM or a bad URL polyfill might not be trustworthy.\n *\n *\n * | Field       | Description                                                          |\n * |-------------|----------------------------------------------------------------------|\n * | char        | The (usually decoded) verbatim \"character\" you put in your <Link to> |\n * | pathChar    | The value we expect to receive from location.pathname                |\n * | searchChar  | The value we expect to receive from location.search                  |\n * | hashChar    | The value we expect to receive from location.hash                    |\n * | decodedChar | The decoded value we expect to receive from params                   |\n *\n * function generateCharDef(char) {\n *   return {\n *       char,\n *       pathChar: new URL('/' + char, window.location.origin).pathname.replace(/^\\//, ''),\n *       searchChar: new URL('/?=' + char, window.location.origin).search.replace(/^\\?=/, ''),\n *       hashChar: new URL('/#' + char, window.location.origin).hash.replace(/^#/, ''),\n *   };\n * }\n */\n\n// prettier-ignore\nlet specialChars = [\n  // This set of characters never gets encoded by window.location\n  { char: \"x\", pathChar: \"x\", searchChar: \"x\", hashChar: \"x\", decodedChar: \"x\" },\n  { char: \"X\", pathChar: \"X\", searchChar: \"X\", hashChar: \"X\", decodedChar: \"X\" },\n  { char: \"~\", pathChar: \"~\", searchChar: \"~\", hashChar: \"~\", decodedChar: \"~\" },\n  { char: \"!\", pathChar: \"!\", searchChar: \"!\", hashChar: \"!\", decodedChar: \"!\" },\n  { char: \"@\", pathChar: \"@\", searchChar: \"@\", hashChar: \"@\", decodedChar: \"@\" },\n  { char: \"$\", pathChar: \"$\", searchChar: \"$\", hashChar: \"$\", decodedChar: \"$\" },\n  { char: \"*\", pathChar: \"*\", searchChar: \"*\", hashChar: \"*\", decodedChar: \"*\" },\n  { char: \"(\", pathChar: \"(\", searchChar: \"(\", hashChar: \"(\", decodedChar: \"(\" },\n  { char: \")\", pathChar: \")\", searchChar: \")\", hashChar: \")\", decodedChar: \")\" },\n  { char: \"_\", pathChar: \"_\", searchChar: \"_\", hashChar: \"_\", decodedChar: \"_\" },\n  { char: \"-\", pathChar: \"-\", searchChar: \"-\", hashChar: \"-\", decodedChar: \"-\" },\n  { char: \"+\", pathChar: \"+\", searchChar: \"+\", hashChar: \"+\", decodedChar: \"+\" },\n  { char: \"=\", pathChar: \"=\", searchChar: \"=\", hashChar: \"=\", decodedChar: \"=\" },\n  { char: \"[\", pathChar: \"[\", searchChar: \"[\", hashChar: \"[\", decodedChar: \"[\" },\n  { char: \"]\", pathChar: \"]\", searchChar: \"]\", hashChar: \"]\", decodedChar: \"]\" },\n  { char: \":\", pathChar: \":\", searchChar: \":\", hashChar: \":\", decodedChar: \":\" },\n  { char: \";\", pathChar: \";\", searchChar: \";\", hashChar: \";\", decodedChar: \";\" },\n  { char: \",\", pathChar: \",\", searchChar: \",\", hashChar: \",\", decodedChar: \",\" },\n\n  // These chars should only get encoded when in the pathname, but JSDOM\n  // seems to have a bug as it does not encode them, so don't test this\n  // case for now\n  // { char: \"^\", pathChar: \"%5E\", searchChar: \"^\", hashChar: \"^\" },\n  // { char: \"|\", pathChar: \"%7C\", searchChar: \"|\", hashChar: \"|\" },\n\n  // These chars get conditionally encoded based on what portion of the\n  // URL they occur in\n  { char: \"{\", pathChar: \"%7B\", searchChar: \"{\", hashChar: \"{\", decodedChar: \"{\" },\n  { char: \"}\", pathChar: \"%7D\", searchChar: \"}\", hashChar: \"}\", decodedChar: \"}\" },\n  { char: \"`\", pathChar: \"%60\", searchChar: \"`\", hashChar: \"%60\", decodedChar: \"`\" },\n  { char: \"'\", pathChar: \"'\", searchChar: \"%27\", hashChar: \"'\", decodedChar: \"'\" },\n  { char: '\"', pathChar: \"%22\", searchChar: \"%22\", hashChar: \"%22\", decodedChar: '\"' },\n  { char: \"<\", pathChar: \"%3C\", searchChar: \"%3C\", hashChar: \"%3C\", decodedChar: \"<\" },\n  { char: \">\", pathChar: \"%3E\", searchChar: \"%3E\", hashChar: \"%3E\", decodedChar: \">\" },\n\n  // These chars get encoded in all portions of the URL\n  { char: \"🤯\", pathChar: \"%F0%9F%A4%AF\", searchChar: \"%F0%9F%A4%AF\", hashChar: \"%F0%9F%A4%AF\", decodedChar: \"🤯\" },\n  { char: \"✅\", pathChar: \"%E2%9C%85\", searchChar: \"%E2%9C%85\", hashChar: \"%E2%9C%85\", decodedChar: \"✅\" },\n  { char: \"🔥\", pathChar: \"%F0%9F%94%A5\", searchChar: \"%F0%9F%94%A5\", hashChar: \"%F0%9F%94%A5\", decodedChar: \"🔥\" },\n  { char: \"ä\", pathChar: \"%C3%A4\", searchChar: \"%C3%A4\", hashChar: \"%C3%A4\", decodedChar: \"ä\" },\n  { char: \"Ä\", pathChar: \"%C3%84\", searchChar: \"%C3%84\", hashChar: \"%C3%84\", decodedChar: \"Ä\" },\n  { char: \"ø\", pathChar: \"%C3%B8\", searchChar: \"%C3%B8\", hashChar: \"%C3%B8\", decodedChar: \"ø\" },\n  { char: \"山\", pathChar: \"%E5%B1%B1\", searchChar: \"%E5%B1%B1\", hashChar: \"%E5%B1%B1\", decodedChar: \"山\" },\n  { char: \"人\", pathChar: \"%E4%BA%BA\", searchChar: \"%E4%BA%BA\", hashChar: \"%E4%BA%BA\", decodedChar: \"人\" },\n  { char: \"口\", pathChar: \"%E5%8F%A3\", searchChar: \"%E5%8F%A3\", hashChar: \"%E5%8F%A3\", decodedChar: \"口\" },\n  { char: \"刀\", pathChar: \"%E5%88%80\", searchChar: \"%E5%88%80\", hashChar: \"%E5%88%80\", decodedChar: \"刀\" },\n  { char: \"木\", pathChar: \"%E6%9C%A8\", searchChar: \"%E6%9C%A8\", hashChar: \"%E6%9C%A8\", decodedChar: \"木\" },\n\n  // Add a few multi-char space use cases for good measure\n  { char: \"a b\", pathChar: \"a%20b\", searchChar: \"a%20b\", hashChar: \"a%20b\", decodedChar: \"a b\" },\n  { char: \"a+b\", pathChar: \"a+b\", searchChar: \"a+b\", hashChar: \"a+b\", decodedChar: \"a+b\" },\n\n  // Edge case scenarios where the incoming `char` (or string) is pre-encoded\n  // because it contains special characters such as `&`, `%`, or `#`.  For these\n  // we provide a `decodedChar` so we can assert the param value gets decoded\n  // properly and so we can ensure we can match these decoded values in static\n  // paths\n  { char: \"a%25b\", pathChar: \"a%25b\", searchChar: \"a%25b\", hashChar: \"a%25b\", decodedChar: \"a%b\" },\n  { char: \"a%23b%25c\", pathChar: \"a%23b%25c\", searchChar: \"a%23b%25c\", hashChar: \"a%23b%25c\", decodedChar: \"a#b%c\" },\n  { char: \"a%26b%25c\", pathChar: \"a%26b%25c\", searchChar: \"a%26b%25c\", hashChar: \"a%26b%25c\", decodedChar: \"a&b%c\" },\n];\n\ndescribe(\"special character tests\", () => {\n  // Mutable vars we'll use to capture the useLocation/useParams values during\n  // the render pass.  this avoids stringifying them into the DOM and parsing\n  // them back out which can mess with the encoding\n  let renderedUseLocation: Omit<Location, \"state\" | \"key\"> | null = null;\n  let renderedParams: Params<string> | null = null;\n\n  function CaptureLocation() {\n    let location = {\n      ...useLocation(),\n      state: undefined,\n      key: undefined,\n    };\n    let params = useParams();\n    renderedUseLocation = {\n      pathname: location.pathname,\n      search: location.search,\n      hash: location.hash,\n    };\n    renderedParams = params;\n    return (\n      <>\n        <p>{location.pathname}</p>\n        <Link to=\"/reset\">Link to reset</Link>\n      </>\n    );\n  }\n\n  beforeEach(() => {\n    renderedUseLocation = null;\n    renderedParams = null;\n  });\n\n  describe(\"when matching as param values\", () => {\n    async function testParamValues(\n      navigatePath: string,\n      expectedHeading: string,\n      expectedLocation: Omit<Location, \"state\" | \"key\">,\n      expectedParams = {},\n    ) {\n      let testWindow = getWindow(navigatePath);\n\n      function Comp({ heading }) {\n        return (\n          <>\n            <h1>{heading}</h1>\n            <CaptureLocation></CaptureLocation>\n          </>\n        );\n      }\n\n      let routeElements = (\n        <>\n          <Route path=\"/path\" element={<Comp heading=\"Static Route\" />} />\n          <Route\n            path=\"/inline-param/:slug\"\n            element={<Comp heading=\"Inline Nested Param Route\" />}\n          />\n          <Route path=\"/param\">\n            <Route\n              path=\":slug\"\n              element={<Comp heading=\"Parent Nested Param Route\" />}\n            />\n          </Route>\n          <Route\n            path=\"/inline-splat/*\"\n            element={<Comp heading=\"Inline Nested Splat Route\" />}\n          />\n          <Route path=\"/splat\">\n            <Route\n              path=\"*\"\n              element={<Comp heading=\"Parent Nested Splat Route\" />}\n            />\n          </Route>\n          <Route\n            path=\"/reset\"\n            element={<Link to={navigatePath}>Link to path</Link>}\n          />\n          <Route\n            path=\"/descendant/:param/*\"\n            element={\n              <Routes>\n                <Route\n                  path=\"match\"\n                  element={<Comp heading=\"Descendant Route\" />}\n                />\n              </Routes>\n            }\n          />\n          <Route path=\"/*\" element={<Comp heading=\"Root Splat Route\" />} />\n        </>\n      );\n\n      // Render BrowserRouter at the initialized location and confirm we get\n      // the right route match, window.location, useLocation(), and useParams()\n      // values\n      let ctx = render(\n        <BrowserRouter window={testWindow}>\n          <Routes>{routeElements}</Routes>\n        </BrowserRouter>,\n      );\n\n      expect(ctx.container.querySelector(\"h1\")?.innerHTML).toBe(\n        expectedHeading,\n      );\n\n      let windowLocation = {\n        pathname: testWindow.location.pathname,\n        search: testWindow.location.search,\n        hash: testWindow.location.hash,\n      };\n      expect(windowLocation).toEqual(expectedLocation);\n      expect(renderedUseLocation).toEqual(expectedLocation);\n      expect(renderedParams).toEqual(expectedParams);\n\n      // Now client side away to a /reset route and back to the original path to confirm that\n      // client-side history.push calls also match the same expectations\n      await fireEvent.click(screen.getByText(\"Link to reset\"));\n      await waitFor(() => screen.getByText(/Link to path/));\n      await fireEvent.click(screen.getByText(\"Link to path\"));\n      await waitFor(() => screen.getByText(/Link to reset/));\n\n      expect(ctx.container.querySelector(\"h1\")?.innerHTML).toBe(\n        expectedHeading,\n      );\n\n      windowLocation = {\n        pathname: testWindow.location.pathname,\n        search: testWindow.location.search,\n        hash: testWindow.location.hash,\n      };\n      expect(windowLocation).toEqual(expectedLocation);\n      expect(renderedUseLocation).toEqual(expectedLocation);\n      expect(renderedParams).toEqual(expectedParams);\n\n      // Reset state\n      ctx.unmount();\n      cleanup();\n      renderedUseLocation = null;\n      renderedParams = null;\n\n      // Now run the same initialized-location render through a data router\n      // and confirm all the same assertions\n      let routes = createRoutesFromElements(routeElements);\n      let router = createBrowserRouter(routes, { window: testWindow });\n      ctx = render(<RouterProvider router={router} />);\n\n      expect(ctx.container.querySelector(\"h1\")?.innerHTML).toBe(\n        expectedHeading,\n      );\n\n      windowLocation = {\n        pathname: testWindow.location.pathname,\n        search: testWindow.location.search,\n        hash: testWindow.location.hash,\n      };\n      // Assert both window.location and useLocation() match what we expect\n      expect(windowLocation).toEqual(expectedLocation);\n      expect(renderedUseLocation).toEqual(expectedLocation);\n      expect(renderedParams).toEqual(expectedParams);\n\n      // Now client side away to a /reset route and back to the original path to confirm that\n      // client-side router.navigate calls also match the same expectations\n      await fireEvent.click(screen.getByText(\"Link to reset\"));\n      await waitFor(() => screen.getByText(/Link to path/));\n      await fireEvent.click(screen.getByText(\"Link to path\"));\n      await waitFor(() => screen.getByText(/Link to reset/));\n\n      expect(ctx.container.querySelector(\"h1\")!.innerHTML).toBe(\n        expectedHeading,\n      );\n\n      windowLocation = {\n        pathname: testWindow.location.pathname,\n        search: testWindow.location.search,\n        hash: testWindow.location.hash,\n      };\n      // Assert both window.location and useLocation() match what we expect\n      expect(windowLocation).toEqual(expectedLocation);\n      expect(renderedUseLocation).toEqual(expectedLocation);\n      expect(renderedParams).toEqual(expectedParams);\n\n      ctx.unmount();\n      cleanup();\n      renderedUseLocation = null;\n      renderedParams = null;\n    }\n\n    it(\"handles special chars in inline nested param route paths\", async () => {\n      for (let charDef of specialChars) {\n        let { char, pathChar, decodedChar } = charDef;\n        await testParamValues(\n          `/inline-param/${char}`,\n          \"Inline Nested Param Route\",\n          {\n            pathname: `/inline-param/${pathChar}`,\n            search: \"\",\n            hash: \"\",\n          },\n          { slug: decodedChar },\n        );\n\n        await testParamValues(\n          `/inline-param/foo${char}bar`,\n          \"Inline Nested Param Route\",\n          {\n            pathname: `/inline-param/foo${pathChar}bar`,\n            search: \"\",\n            hash: \"\",\n          },\n          { slug: `foo${decodedChar}bar` },\n        );\n      }\n    });\n\n    it(\"handles special chars in parent nested param route paths\", async () => {\n      for (let charDef of specialChars) {\n        let { char, pathChar, decodedChar } = charDef;\n        await testParamValues(\n          `/param/${char}`,\n          \"Parent Nested Param Route\",\n          {\n            pathname: `/param/${pathChar}`,\n            search: \"\",\n            hash: \"\",\n          },\n          { slug: decodedChar },\n        );\n\n        await testParamValues(\n          `/param/foo${char}bar`,\n          \"Parent Nested Param Route\",\n          {\n            pathname: `/param/foo${pathChar}bar`,\n            search: \"\",\n            hash: \"\",\n          },\n          { slug: `foo${decodedChar}bar` },\n        );\n      }\n    });\n\n    it(\"handles special chars in inline nested splat routes\", async () => {\n      for (let charDef of specialChars) {\n        let { char, pathChar, decodedChar } = charDef;\n        await testParamValues(\n          `/inline-splat/${char}`,\n          \"Inline Nested Splat Route\",\n          {\n            pathname: `/inline-splat/${pathChar}`,\n            search: \"\",\n            hash: \"\",\n          },\n          { \"*\": decodedChar },\n        );\n\n        await testParamValues(\n          `/inline-splat/foo${char}bar`,\n          \"Inline Nested Splat Route\",\n          {\n            pathname: `/inline-splat/foo${pathChar}bar`,\n            search: \"\",\n            hash: \"\",\n          },\n          { \"*\": `foo${decodedChar}bar` },\n        );\n      }\n    });\n\n    it(\"handles special chars in nested splat routes\", async () => {\n      for (let charDef of specialChars) {\n        let { char, pathChar, decodedChar } = charDef;\n        await testParamValues(\n          `/splat/${char}`,\n          \"Parent Nested Splat Route\",\n          {\n            pathname: `/splat/${pathChar}`,\n            search: \"\",\n            hash: \"\",\n          },\n          { \"*\": decodedChar },\n        );\n\n        await testParamValues(\n          `/splat/foo${char}bar`,\n          \"Parent Nested Splat Route\",\n          {\n            pathname: `/splat/foo${pathChar}bar`,\n            search: \"\",\n            hash: \"\",\n          },\n          { \"*\": `foo${decodedChar}bar` },\n        );\n      }\n    });\n\n    it(\"handles special chars in nested splat routes with separators\", async () => {\n      for (let charDef of specialChars) {\n        let { char, pathChar, decodedChar } = charDef;\n        await testParamValues(\n          `/splat/foo/bar${char}`,\n          \"Parent Nested Splat Route\",\n          {\n            pathname: `/splat/foo/bar${pathChar}`,\n            search: \"\",\n            hash: \"\",\n          },\n          { \"*\": `foo/bar${decodedChar}` },\n        );\n      }\n    });\n\n    it(\"handles special chars in root splat routes\", async () => {\n      for (let charDef of specialChars) {\n        let { char, pathChar, decodedChar } = charDef;\n        await testParamValues(\n          `/${char}`,\n          \"Root Splat Route\",\n          {\n            pathname: `/${pathChar}`,\n            search: \"\",\n            hash: \"\",\n          },\n          { \"*\": decodedChar },\n        );\n\n        await testParamValues(\n          `/foo${char}bar`,\n          \"Root Splat Route\",\n          {\n            pathname: `/foo${pathChar}bar`,\n            search: \"\",\n            hash: \"\",\n          },\n          { \"*\": `foo${decodedChar}bar` },\n        );\n      }\n    });\n\n    it(\"handles special chars in root splat routes with separators\", async () => {\n      for (let charDef of specialChars) {\n        let { char, pathChar, decodedChar } = charDef;\n        await testParamValues(\n          `/foo/bar${char}`,\n          \"Root Splat Route\",\n          {\n            pathname: `/foo/bar${pathChar}`,\n            search: \"\",\n            hash: \"\",\n          },\n          { \"*\": `foo/bar${decodedChar}` },\n        );\n      }\n    });\n\n    it(\"handles special chars in descendant routes paths\", async () => {\n      for (let charDef of specialChars) {\n        let { char, pathChar, decodedChar } = charDef;\n\n        await testParamValues(\n          `/descendant/${char}/match`,\n          \"Descendant Route\",\n          {\n            pathname: `/descendant/${pathChar}/match`,\n            search: \"\",\n            hash: \"\",\n          },\n          { param: decodedChar, \"*\": \"match\" },\n        );\n\n        await testParamValues(\n          `/descendant/foo${char}bar/match`,\n          \"Descendant Route\",\n          {\n            pathname: `/descendant/foo${pathChar}bar/match`,\n            search: \"\",\n            hash: \"\",\n          },\n          { param: `foo${decodedChar}bar`, \"*\": \"match\" },\n        );\n      }\n    });\n\n    it(\"handles special chars in search params\", async () => {\n      for (let charDef of specialChars) {\n        let { char, searchChar } = charDef;\n        await testParamValues(`/path?key=${char}`, \"Static Route\", {\n          pathname: `/path`,\n          search: `?key=${searchChar}`,\n          hash: \"\",\n        });\n      }\n    });\n\n    it(\"handles special chars in hash values\", async () => {\n      for (let charDef of specialChars) {\n        let { char, hashChar } = charDef;\n        await testParamValues(`/path#hash-${char}`, \"Static Route\", {\n          pathname: `/path`,\n          search: \"\",\n          hash: `#hash-${hashChar}`,\n        });\n      }\n    });\n\n    it(\"does not trim trailing spaces on ancestor splat route segments\", async () => {\n      let ctx = render(\n        <BrowserRouter window={getWindow(\"/parent/child/%20%20param%20%20\")}>\n          <App />\n        </BrowserRouter>,\n      );\n\n      expect(getHtml(ctx.container)).toMatchInlineSnapshot(`\n        \"<div>\n          <a\n            data-discover=\"true\"\n            href=\"/parent/child/%20%20param%20%20/grandchild\"\n          >\n            Link to grandchild\n          </a>\n        </div>\"\n      `);\n\n      await fireEvent.click(screen.getByText(\"Link to grandchild\"));\n      await waitFor(() => screen.getByText(\"Grandchild\"));\n\n      expect(getHtml(ctx.container)).toMatchInlineSnapshot(`\n        \"<div>\n          <a\n            data-discover=\"true\"\n            href=\"/parent/child/%20%20param%20%20/grandchild\"\n          >\n            Link to grandchild\n          </a>\n          <h1>\n            Grandchild\n          </h1>\n          <pre>\n            {\"*\":\"grandchild\",\"param\":\"  param  \"}\n          </pre>\n        </div>\"\n      `);\n\n      function App() {\n        return (\n          <Routes>\n            <Route path=\"/parent/*\" element={<Parent />} />\n          </Routes>\n        );\n      }\n\n      function Parent() {\n        return (\n          <Routes>\n            <Route path=\"child/:param/*\" element={<Child />} />\n          </Routes>\n        );\n      }\n\n      function Child() {\n        let location = useLocation();\n        let to = location.pathname.endsWith(\"grandchild\")\n          ? \".\"\n          : \"./grandchild\";\n        return (\n          <>\n            <Link to={to}>Link to grandchild</Link>\n            <Routes>\n              <Route path=\"grandchild\" element={<Grandchild />} />\n            </Routes>\n          </>\n        );\n      }\n\n      function Grandchild() {\n        return (\n          <>\n            <h1>Grandchild</h1>\n            <pre>{JSON.stringify(useParams())}</pre>\n          </>\n        );\n      }\n    });\n\n    it(\"handles encoded question marks in ancestor splat route segments\", async () => {\n      let ctx = render(\n        <BrowserRouter window={getWindow(\"/parent/child/question-%3F-mark\")}>\n          <App />\n        </BrowserRouter>,\n      );\n\n      expect(getHtml(ctx.container)).toMatchInlineSnapshot(`\n        \"<div>\n          <a\n            data-discover=\"true\"\n            href=\"/parent/child/question-%3F-mark/grandchild\"\n          >\n            Link to grandchild\n          </a>\n        </div>\"\n      `);\n\n      await fireEvent.click(screen.getByText(\"Link to grandchild\"));\n      await waitFor(() => screen.getByText(\"Grandchild\"));\n\n      expect(getHtml(ctx.container)).toMatchInlineSnapshot(`\n        \"<div>\n          <a\n            data-discover=\"true\"\n            href=\"/parent/child/question-%3F-mark/grandchild\"\n          >\n            Link to grandchild\n          </a>\n          <h1>\n            Grandchild\n          </h1>\n          <pre>\n            {\"*\":\"grandchild\",\"param\":\"question-?-mark\"}\n          </pre>\n        </div>\"\n      `);\n\n      function App() {\n        return (\n          <Routes>\n            <Route path=\"/parent/*\" element={<Parent />} />\n          </Routes>\n        );\n      }\n\n      function Parent() {\n        return (\n          <Routes>\n            <Route path=\"child/:param/*\" element={<Child />} />\n          </Routes>\n        );\n      }\n\n      function Child() {\n        let location = useLocation();\n        let to = location.pathname.endsWith(\"grandchild\")\n          ? \".\"\n          : \"./grandchild\";\n        return (\n          <>\n            <Link to={to}>Link to grandchild</Link>\n            <Routes>\n              <Route path=\"grandchild\" element={<Grandchild />} />\n            </Routes>\n          </>\n        );\n      }\n\n      function Grandchild() {\n        return (\n          <>\n            <h1>Grandchild</h1>\n            <pre>{JSON.stringify(useParams())}</pre>\n          </>\n        );\n      }\n    });\n\n    it(\"handles encoded hashes in ancestor splat route segments\", async () => {\n      let ctx = render(\n        <BrowserRouter window={getWindow(\"/parent/child/hash-%23-char\")}>\n          <App />\n        </BrowserRouter>,\n      );\n\n      expect(getHtml(ctx.container)).toMatchInlineSnapshot(`\n        \"<div>\n          <a\n            data-discover=\"true\"\n            href=\"/parent/child/hash-%23-char/grandchild\"\n          >\n            Link to grandchild\n          </a>\n        </div>\"\n      `);\n\n      await fireEvent.click(screen.getByText(\"Link to grandchild\"));\n      await waitFor(() => screen.getByText(\"Grandchild\"));\n\n      expect(getHtml(ctx.container)).toMatchInlineSnapshot(`\n        \"<div>\n          <a\n            data-discover=\"true\"\n            href=\"/parent/child/hash-%23-char/grandchild\"\n          >\n            Link to grandchild\n          </a>\n          <h1>\n            Grandchild\n          </h1>\n          <pre>\n            {\"*\":\"grandchild\",\"param\":\"hash-#-char\"}\n          </pre>\n        </div>\"\n      `);\n\n      function App() {\n        return (\n          <Routes>\n            <Route path=\"/parent/*\" element={<Parent />} />\n          </Routes>\n        );\n      }\n\n      function Parent() {\n        return (\n          <Routes>\n            <Route path=\"child/:param/*\" element={<Child />} />\n          </Routes>\n        );\n      }\n\n      function Child() {\n        let location = useLocation();\n        let to = location.pathname.endsWith(\"grandchild\")\n          ? \".\"\n          : \"./grandchild\";\n        return (\n          <>\n            <Link to={to}>Link to grandchild</Link>\n            <Routes>\n              <Route path=\"grandchild\" element={<Grandchild />} />\n            </Routes>\n          </>\n        );\n      }\n\n      function Grandchild() {\n        return (\n          <>\n            <h1>Grandchild</h1>\n            <pre>{JSON.stringify(useParams())}</pre>\n          </>\n        );\n      }\n    });\n  });\n\n  describe(\"when matching as part of the defined route path\", () => {\n    async function assertRouteMatch(\n      char,\n      path: string,\n      expectedHeading: string,\n      expectedLocation: Omit<Location, \"state\" | \"key\">,\n      expectedParams = {},\n    ) {\n      let testWindow = getWindow(path);\n      let renderedUseLocation: Omit<Location, \"state\" | \"key\"> | null = null;\n      let renderedParams: Params<string> | null = null;\n\n      function CaptureLocation() {\n        let location = {\n          ...useLocation(),\n          state: undefined,\n          key: undefined,\n        };\n        let params = useParams();\n        renderedUseLocation = location;\n        renderedParams = params;\n        return (\n          <>\n            <p>{location.pathname}</p>\n            <Link to=\"/reset\">Link to reset</Link>\n          </>\n        );\n      }\n\n      function Root() {\n        return (\n          <>\n            <h1>Matched Root</h1>\n            <CaptureLocation />\n          </>\n        );\n      }\n\n      function StaticNested() {\n        return (\n          <>\n            <h1>Matched Static Nested</h1>\n            <CaptureLocation />\n          </>\n        );\n      }\n\n      function ParamNested() {\n        return (\n          <>\n            <h1>Matched Param Nested</h1>\n            <CaptureLocation />\n          </>\n        );\n      }\n\n      let routeElements = (\n        <>\n          <Route path={`/${char}`} element={<Root />} />\n          <Route path=\"/nested\">\n            <Route path={char} element={<StaticNested />} />\n          </Route>\n          <Route path=\"/:param\">\n            <Route path={char} element={<ParamNested />} />\n          </Route>\n          <Route path=\"/reset\" element={<Link to={path}>Link to path</Link>} />\n          <Route path=\"*\" element={<h1>Not Found</h1>} />\n        </>\n      );\n\n      // Render BrowserRouter at the initialized location and confirm we get\n      // the right route match, window.location, useLocation(), and useParams()\n      // values\n      let ctx = render(\n        <BrowserRouter window={testWindow}>\n          <Routes>{routeElements}</Routes>\n        </BrowserRouter>,\n      );\n\n      expect(ctx.container.querySelector(\"h1\")!.innerHTML).toBe(\n        expectedHeading,\n      );\n      let windowLocation = {\n        pathname: testWindow.location.pathname,\n        search: testWindow.location.search,\n        hash: testWindow.location.hash,\n      };\n      expect(windowLocation).toEqual(expectedLocation);\n      expect(renderedUseLocation).toEqual(expectedLocation);\n      expect(renderedParams).toEqual(expectedParams);\n\n      // Reset state\n      ctx.unmount();\n      cleanup();\n      renderedUseLocation = null;\n      renderedParams = null;\n\n      // Now run the same initialized-location render through a data router\n      // and confirm all the same assertions\n      let routes = createRoutesFromElements(routeElements);\n      let router = createBrowserRouter(routes, { window: testWindow });\n      ctx = render(<RouterProvider router={router} />);\n\n      expect(ctx.container.querySelector(\"h1\")!.innerHTML).toBe(\n        expectedHeading,\n      );\n      windowLocation = {\n        pathname: testWindow.location.pathname,\n        search: testWindow.location.search,\n        hash: testWindow.location.hash,\n      };\n      expect(windowLocation).toEqual(expectedLocation);\n      expect(renderedUseLocation).toEqual(expectedLocation);\n      expect(renderedParams).toEqual(expectedParams);\n\n      // Now client side away to a /reset route and back to the original path to confirm that\n      // client-side router.navigate calls also match the same expectations\n      await fireEvent.click(screen.getByText(\"Link to reset\"));\n      await waitFor(() => screen.getByText(/Link to path/));\n      await fireEvent.click(screen.getByText(\"Link to path\"));\n      await waitFor(() => screen.getByText(/Link to reset/));\n\n      expect(ctx.container.querySelector(\"h1\")!.innerHTML).toBe(\n        expectedHeading,\n      );\n      windowLocation = {\n        pathname: testWindow.location.pathname,\n        search: testWindow.location.search,\n        hash: testWindow.location.hash,\n      };\n      expect(windowLocation).toEqual(expectedLocation);\n      expect(renderedUseLocation).toEqual(expectedLocation);\n      expect(renderedParams).toEqual(expectedParams);\n\n      ctx.unmount();\n      cleanup();\n      renderedUseLocation = null;\n      renderedParams = null;\n    }\n\n    it(\"handles special chars in root route paths\", async () => {\n      for (let charDef of specialChars) {\n        let { char, pathChar, decodedChar } = charDef;\n        // Skip * which is just a splat route\n        if (char === \"*\") {\n          continue;\n        }\n        await assertRouteMatch(decodedChar, `/${char}`, \"Matched Root\", {\n          pathname: `/${pathChar}`,\n          search: \"\",\n          hash: \"\",\n        });\n      }\n    });\n\n    it(\"handles special chars in static nested route paths\", async () => {\n      for (let charDef of specialChars) {\n        let { char, pathChar, decodedChar } = charDef;\n        // Skip * which is just a splat route\n        if (char === \"*\") {\n          continue;\n        }\n        await assertRouteMatch(\n          decodedChar,\n          `/nested/${char}`,\n          \"Matched Static Nested\",\n          {\n            pathname: `/nested/${pathChar}`,\n            search: \"\",\n            hash: \"\",\n          },\n        );\n      }\n    });\n\n    it(\"handles special chars in nested param route paths\", async () => {\n      for (let charDef of specialChars) {\n        let { char, pathChar, decodedChar } = charDef;\n        // Skip * which is just a splat route\n        if (char === \"*\") {\n          continue;\n        }\n        await assertRouteMatch(\n          decodedChar,\n          `/foo/${char}`,\n          \"Matched Param Nested\",\n          {\n            pathname: `/foo/${pathChar}`,\n            search: \"\",\n            hash: \"\",\n          },\n          {\n            param: \"foo\",\n          },\n        );\n      }\n    });\n  });\n\n  describe(\"encodes characters based on history implementation\", () => {\n    function ShowPath() {\n      let { pathname, search, hash } = useLocation();\n      return <pre>{JSON.stringify({ pathname, search, hash })}</pre>;\n    }\n\n    describe(\"memory routers\", () => {\n      it(\"does not encode characters in MemoryRouter\", () => {\n        let ctx = render(\n          <MemoryRouter initialEntries={[\"/with space\"]}>\n            <Routes>\n              <Route path=\"/with space\" element={<ShowPath />} />\n            </Routes>\n          </MemoryRouter>,\n        );\n\n        expect(ctx.container.innerHTML).toMatchInlineSnapshot(\n          `\"<pre>{\"pathname\":\"/with space\",\"search\":\"\",\"hash\":\"\"}</pre>\"`,\n        );\n      });\n\n      it(\"does not encode characters in MemoryRouter (navigate)\", () => {\n        function Start() {\n          let navigate = useNavigate();\n          React.useEffect(() => {\n            navigate(\"/with space\");\n          }, [navigate]);\n          return null;\n        }\n        let ctx = render(\n          <MemoryRouter>\n            <Routes>\n              <Route path=\"/\" element={<Start />} />\n              <Route path=\"/with space\" element={<ShowPath />} />\n            </Routes>\n          </MemoryRouter>,\n        );\n\n        expect(ctx.container.innerHTML).toMatchInlineSnapshot(\n          `\"<pre>{\"pathname\":\"/with space\",\"search\":\"\",\"hash\":\"\"}</pre>\"`,\n        );\n      });\n\n      it(\"does not encode characters in createMemoryRouter\", () => {\n        let router = createMemoryRouter(\n          [{ path: \"/with space\", element: <ShowPath /> }],\n          { initialEntries: [\"/with space\"] },\n        );\n        let ctx = render(<RouterProvider router={router} />);\n\n        expect(ctx.container.innerHTML).toMatchInlineSnapshot(\n          `\"<pre>{\"pathname\":\"/with space\",\"search\":\"\",\"hash\":\"\"}</pre>\"`,\n        );\n      });\n\n      it(\"does not encode characters in createMemoryRouter (navigate)\", () => {\n        function Start() {\n          let navigate = useNavigate();\n          React.useEffect(() => {\n            navigate(\"/with space\");\n          }, [navigate]);\n          return null;\n        }\n        let router = createMemoryRouter([\n          { path: \"/\", element: <Start /> },\n          { path: \"/with space\", element: <ShowPath /> },\n        ]);\n        let ctx = render(<RouterProvider router={router} />);\n\n        expect(ctx.container.innerHTML).toMatchInlineSnapshot(\n          `\"<pre>{\"pathname\":\"/with space\",\"search\":\"\",\"hash\":\"\"}</pre>\"`,\n        );\n      });\n    });\n\n    describe(\"browser routers\", () => {\n      it(\"encodes characters in BrowserRouter\", () => {\n        let testWindow = getWindow(\"/with space\");\n\n        let ctx = render(\n          <BrowserRouter window={testWindow}>\n            <Routes>\n              <Route path=\"/with space\" element={<ShowPath />} />\n            </Routes>\n          </BrowserRouter>,\n        );\n\n        expect(testWindow.location.pathname).toBe(\"/with%20space\");\n        expect(ctx.container.innerHTML).toMatchInlineSnapshot(\n          `\"<pre>{\"pathname\":\"/with%20space\",\"search\":\"\",\"hash\":\"\"}</pre>\"`,\n        );\n      });\n\n      it(\"encodes characters in BrowserRouter (navigate)\", () => {\n        let testWindow = getWindow(\"/\");\n\n        function Start() {\n          let navigate = useNavigate();\n          React.useEffect(() => {\n            navigate(\"/with space\");\n          }, [navigate]);\n          return null;\n        }\n\n        let ctx = render(\n          <BrowserRouter window={testWindow}>\n            <Routes>\n              <Route path=\"/\" element={<Start />} />\n              <Route path=\"/with space\" element={<ShowPath />} />\n            </Routes>\n          </BrowserRouter>,\n        );\n\n        expect(testWindow.location.pathname).toBe(\"/with%20space\");\n        expect(ctx.container.innerHTML).toMatchInlineSnapshot(\n          `\"<pre>{\"pathname\":\"/with%20space\",\"search\":\"\",\"hash\":\"\"}</pre>\"`,\n        );\n      });\n\n      it(\"encodes characters in createBrowserRouter\", () => {\n        let testWindow = getWindow(\"/with space\");\n\n        let router = createBrowserRouter(\n          [{ path: \"/with space\", element: <ShowPath /> }],\n          { window: testWindow },\n        );\n        let ctx = render(<RouterProvider router={router} />);\n\n        expect(testWindow.location.pathname).toBe(\"/with%20space\");\n        expect(ctx.container.innerHTML).toMatchInlineSnapshot(\n          `\"<pre>{\"pathname\":\"/with%20space\",\"search\":\"\",\"hash\":\"\"}</pre>\"`,\n        );\n      });\n\n      it(\"encodes characters in createBrowserRouter (navigate)\", () => {\n        let testWindow = getWindow(\"/\");\n\n        function Start() {\n          let navigate = useNavigate();\n          React.useEffect(() => {\n            navigate(\"/with space\");\n          }, [navigate]);\n          return null;\n        }\n\n        let router = createBrowserRouter(\n          [\n            { path: \"/\", element: <Start /> },\n            { path: \"/with space\", element: <ShowPath /> },\n          ],\n          { window: testWindow },\n        );\n        let ctx = render(<RouterProvider router={router} />);\n\n        expect(testWindow.location.pathname).toBe(\"/with%20space\");\n        expect(ctx.container.innerHTML).toMatchInlineSnapshot(\n          `\"<pre>{\"pathname\":\"/with%20space\",\"search\":\"\",\"hash\":\"\"}</pre>\"`,\n        );\n      });\n\n      it(\"properly decodes params in useMatch\", () => {\n        let testWindow = getWindow(\"/user/bücherwurm\");\n\n        let router = createBrowserRouter(\n          [\n            {\n              path: \"/\",\n              Component() {\n                let match = useMatch(\"/user/:username\");\n                return (\n                  <>\n                    <pre>{JSON.stringify(match, null, 2)}</pre>\n                    <Outlet />\n                  </>\n                );\n              },\n              children: [\n                {\n                  path: \"user/:username\",\n                  element: null,\n                },\n              ],\n            },\n          ],\n          { window: testWindow },\n        );\n        let ctx = render(<RouterProvider router={router} />);\n\n        expect(testWindow.location.pathname).toBe(\"/user/b%C3%BCcherwurm\");\n        expect(ctx.container.innerHTML).toMatchInlineSnapshot(`\n          \"<pre>{\n            \"params\": {\n              \"username\": \"bücherwurm\"\n            },\n            \"pathname\": \"/user/bücherwurm\",\n            \"pathnameBase\": \"/user/bücherwurm\",\n            \"pattern\": {\n              \"path\": \"/user/:username\",\n              \"caseSensitive\": false,\n              \"end\": true\n            }\n          }</pre>\"\n        `);\n      });\n    });\n\n    describe(\"hash routers\", () => {\n      it(\"encodes characters in HashRouter\", () => {\n        let testWindow = getWindow(\"/#/with space\");\n\n        let ctx = render(\n          <HashRouter window={testWindow}>\n            <Routes>\n              <Route path=\"/with space\" element={<ShowPath />} />\n            </Routes>\n          </HashRouter>,\n        );\n\n        expect(testWindow.location.pathname).toBe(\"/\");\n        expect(testWindow.location.hash).toBe(\"#/with%20space\");\n        expect(ctx.container.innerHTML).toMatchInlineSnapshot(\n          `\"<pre>{\"pathname\":\"/with%20space\",\"search\":\"\",\"hash\":\"\"}</pre>\"`,\n        );\n      });\n\n      it(\"encodes characters in HashRouter (navigate)\", () => {\n        let testWindow = getWindow(\"/\");\n\n        function Start() {\n          let navigate = useNavigate();\n          React.useEffect(() => {\n            navigate(\"/with space\");\n          }, [navigate]);\n          return null;\n        }\n\n        let ctx = render(\n          <HashRouter window={testWindow}>\n            <Routes>\n              <Route path=\"/\" element={<Start />} />\n              <Route path=\"/with space\" element={<ShowPath />} />\n            </Routes>\n          </HashRouter>,\n        );\n\n        expect(testWindow.location.pathname).toBe(\"/\");\n        expect(testWindow.location.hash).toBe(\"#/with%20space\");\n        expect(ctx.container.innerHTML).toMatchInlineSnapshot(\n          `\"<pre>{\"pathname\":\"/with%20space\",\"search\":\"\",\"hash\":\"\"}</pre>\"`,\n        );\n      });\n\n      it(\"encodes characters in createHashRouter\", () => {\n        let testWindow = getWindow(\"/#/with space\");\n\n        let router = createHashRouter(\n          [{ path: \"/with space\", element: <ShowPath /> }],\n          { window: testWindow },\n        );\n        let ctx = render(<RouterProvider router={router} />);\n\n        expect(testWindow.location.pathname).toBe(\"/\");\n        expect(testWindow.location.hash).toBe(\"#/with%20space\");\n        expect(ctx.container.innerHTML).toMatchInlineSnapshot(\n          `\"<pre>{\"pathname\":\"/with%20space\",\"search\":\"\",\"hash\":\"\"}</pre>\"`,\n        );\n      });\n\n      it(\"encodes characters in createHashRouter (navigate)\", () => {\n        let testWindow = getWindow(\"/\");\n\n        function Start() {\n          let navigate = useNavigate();\n          React.useEffect(() => {\n            navigate(\"/with space\");\n          }, [navigate]);\n          return null;\n        }\n\n        let router = createHashRouter(\n          [\n            { path: \"/\", element: <Start /> },\n            { path: \"/with space\", element: <ShowPath /> },\n          ],\n          { window: testWindow },\n        );\n        let ctx = render(<RouterProvider router={router} />);\n\n        expect(testWindow.location.pathname).toBe(\"/\");\n        expect(testWindow.location.hash).toBe(\"#/with%20space\");\n        expect(ctx.container.innerHTML).toMatchInlineSnapshot(\n          `\"<pre>{\"pathname\":\"/with%20space\",\"search\":\"\",\"hash\":\"\"}</pre>\"`,\n        );\n      });\n    });\n  });\n});\n"
  },
  {
    "path": "packages/react-router/__tests__/dom/ssr/components-test.tsx",
    "content": "import { createStaticHandler } from \"react-router\";\nimport { act, fireEvent, render } from \"@testing-library/react\";\nimport * as React from \"react\";\n\nimport {\n  createMemoryRouter,\n  Link,\n  Links,\n  NavLink,\n  Outlet,\n  RouterProvider,\n  Scripts,\n} from \"../../../index\";\nimport { HydratedRouter } from \"../../../lib/dom-export/hydrated-router\";\nimport {\n  FrameworkContext,\n  usePrefetchBehavior,\n} from \"../../../lib/dom/ssr/components\";\nimport {\n  DataRouterContext,\n  DataRouterStateContext,\n} from \"../../../lib/context\";\nimport invariant from \"../../../lib/dom/ssr/invariant\";\nimport { ServerRouter } from \"../../../lib/dom/ssr/server\";\nimport \"@testing-library/jest-dom\";\nimport { mockEntryContext, mockFrameworkContext } from \"../../utils/framework\";\n\nconst setIntentEvents = [\"focus\", \"mouseEnter\", \"touchStart\"] as const;\ntype PrefetchEventHandlerProps = {\n  [Property in `on${Capitalize<(typeof setIntentEvents)[number]>}`]?: Function;\n};\n\nfunction itPrefetchesPageLinks<\n  Props extends { to: any; prefetch?: any } & PrefetchEventHandlerProps,\n>(Component: React.ComponentType<Props>) {\n  describe('prefetch=\"intent\"', () => {\n    let context = mockFrameworkContext({\n      routeModules: { idk: { default: () => null } },\n      manifest: {\n        routes: {\n          idk: {\n            hasLoader: true,\n            hasAction: false,\n            hasErrorBoundary: false,\n            id: \"idk\",\n            module: \"idk.js\",\n          },\n        },\n        entry: { imports: [], module: \"\" },\n        url: \"\",\n        version: \"\",\n      },\n    });\n\n    beforeEach(() => {\n      jest.useFakeTimers();\n    });\n\n    setIntentEvents.forEach((event) => {\n      it(`prefetches page links on ${event}`, () => {\n        let router;\n\n        act(() => {\n          router = createMemoryRouter([\n            {\n              id: \"root\",\n              path: \"/\",\n              element: (\n                <Component {...({ to: \"idk\", prefetch: \"intent\" } as Props)} />\n              ),\n            },\n            {\n              id: \"idk\",\n              path: \"idk\",\n              loader: () => null,\n              element: <h1>idk</h1>,\n            },\n          ]);\n        });\n\n        let { container, unmount } = render(\n          <FrameworkContext.Provider value={context}>\n            <RouterProvider router={router} />\n          </FrameworkContext.Provider>,\n        );\n\n        fireEvent[event](container.firstChild);\n        act(() => {\n          jest.runAllTimers();\n        });\n\n        let dataHref = container.ownerDocument\n          .querySelector('link[rel=\"prefetch\"][as=\"fetch\"]')\n          ?.getAttribute(\"href\");\n        expect(dataHref).toBe(\"/idk.data\");\n        let moduleHref = container.ownerDocument\n          .querySelector('link[rel=\"modulepreload\"]')\n          ?.getAttribute(\"href\");\n        expect(moduleHref).toBe(\"idk.js\");\n        unmount();\n      });\n\n      it(`prefetches page links and calls explicit handler on ${event}`, () => {\n        let router;\n        let ranHandler = false;\n        let eventHandler = `on${event[0].toUpperCase()}${event.slice(1)}`;\n        act(() => {\n          router = createMemoryRouter([\n            {\n              id: \"root\",\n              path: \"/\",\n              element: (\n                <Component\n                  {...({\n                    to: \"idk\",\n                    prefetch: \"intent\",\n                    [eventHandler]: () => {\n                      ranHandler = true;\n                    },\n                  } as any)}\n                />\n              ),\n            },\n            {\n              id: \"idk\",\n              path: \"idk\",\n              loader: () => true,\n              element: <h1>idk</h1>,\n            },\n          ]);\n        });\n\n        let { container, unmount } = render(\n          <FrameworkContext.Provider value={context}>\n            <RouterProvider router={router} />\n          </FrameworkContext.Provider>,\n        );\n\n        fireEvent[event](container.firstChild);\n        act(() => {\n          jest.runAllTimers();\n        });\n\n        expect(\n          container.ownerDocument.querySelector(\"link[rel=prefetch]\"),\n        ).toBeTruthy();\n        expect(ranHandler).toBe(true);\n        unmount();\n      });\n    });\n  });\n}\n\ndescribe(\"<Link />\", () => {\n  itPrefetchesPageLinks(Link);\n});\n\ndescribe(\"<NavLink />\", () => {\n  itPrefetchesPageLinks(NavLink);\n});\n\ndescribe(\"<ServerRouter>\", () => {\n  it(\"handles empty default export objects from the compiler\", async () => {\n    let staticHandlerContext = await createStaticHandler([{ path: \"/\" }]).query(\n      new Request(\"http://localhost/\"),\n    );\n\n    invariant(\n      !(staticHandlerContext instanceof Response),\n      \"Expected a context\",\n    );\n\n    let context = mockEntryContext({\n      manifest: {\n        routes: {\n          root: {\n            hasLoader: false,\n            hasAction: false,\n            hasErrorBoundary: false,\n            id: \"root\",\n            module: \"root.js\",\n            path: \"/\",\n          },\n          empty: {\n            hasLoader: false,\n            hasAction: false,\n            hasErrorBoundary: false,\n            id: \"empty\",\n            module: \"empty.js\",\n            index: true,\n            parentId: \"root\",\n          },\n        },\n        entry: { imports: [], module: \"\" },\n        url: \"\",\n        version: \"\",\n      },\n      routeModules: {\n        root: {\n          default: () => {\n            return (\n              <>\n                <h1>Root</h1>\n                <Outlet />\n              </>\n            );\n          },\n        },\n        empty: { default: {} },\n      },\n    });\n\n    jest.spyOn(console, \"warn\").mockImplementation(() => {});\n    jest.spyOn(console, \"error\");\n\n    let { container } = render(\n      <ServerRouter context={context} url=\"http://localhost/\" />,\n    );\n\n    expect(console.warn).toHaveBeenCalledWith(\n      'Matched leaf route at location \"/\" does not have an element or Component. This means it will render an <Outlet /> with a null value by default resulting in an \"empty\" page.',\n    );\n    expect(console.error).not.toHaveBeenCalled();\n    expect(container.innerHTML).toMatch(\"<h1>Root</h1>\");\n  });\n});\n\ndescribe(\"<HydratedRouter>\", () => {\n  it(\"handles empty default export objects from the compiler\", async () => {\n    let context = mockFrameworkContext({\n      manifest: {\n        routes: {\n          root: {\n            hasLoader: false,\n            hasAction: false,\n            hasErrorBoundary: false,\n            id: \"root\",\n            module: \"root.js\",\n            path: \"/\",\n          },\n          empty: {\n            hasLoader: false,\n            hasAction: false,\n            hasErrorBoundary: false,\n            id: \"empty\",\n            module: \"empty.js\",\n            index: true,\n            parentId: \"root\",\n          },\n        },\n        entry: { imports: [], module: \"\" },\n        url: \"\",\n        version: \"\",\n      },\n      routeModules: {\n        root: {\n          default: () => {\n            return (\n              <>\n                <h1>Root</h1>\n                <Outlet />\n              </>\n            );\n          },\n        },\n        empty: { default: {} },\n      },\n    });\n    window.__reactRouterContext = context;\n    window.__reactRouterRouteModules = context.routeModules;\n    window.__reactRouterManifest = context.manifest;\n    window.__reactRouterContext!.stream = new ReadableStream({\n      start(controller) {\n        window.__reactRouterContext!.streamController = controller;\n      },\n    }).pipeThrough(new TextEncoderStream());\n    window.__reactRouterContext!.streamController.enqueue(\n      // @ts-expect-error\n      '[{\"1\":2,\"6\":4,\"7\":4},\"loaderData\",{\"3\":4,\"5\":4},\"root\",null,\"empty\",\"actionData\",\"errors\"]\\n',\n    );\n    window.__reactRouterContext!.streamController.close();\n\n    jest.spyOn(console, \"error\");\n    jest.spyOn(console, \"warn\").mockImplementation(() => {});\n\n    let container;\n    await act(() => {\n      container = render(<HydratedRouter />).container;\n    });\n\n    expect(console.error).not.toHaveBeenCalled();\n    expect(container.innerHTML).toMatch(\"<h1>Root</h1>\");\n  });\n});\n\ndescribe(\"<Links />\", () => {\n  it(\"renders critical css with nonce\", () => {\n    let context = mockFrameworkContext({\n      criticalCss: \".critical { color: red; }\",\n    });\n\n    let { container } = render(\n      <DataRouterStateContext.Provider\n        value={{ matches: [], errors: null } as any}\n      >\n        <FrameworkContext.Provider value={context}>\n          <Links nonce=\"test-nonce\" />\n        </FrameworkContext.Provider>\n      </DataRouterStateContext.Provider>,\n    );\n\n    let style = container.querySelector(\"style\");\n    expect(style).toHaveAttribute(\"data-react-router-critical-css\");\n    expect(style).toHaveAttribute(\"nonce\", \"test-nonce\");\n    expect(style).toHaveTextContent(\".critical { color: red; }\");\n  });\n\n  it(\"renders critical css object with nonce\", () => {\n    let context = mockFrameworkContext({\n      criticalCss: { rel: \"stylesheet\", href: \"/critical.css\" },\n    });\n\n    let { container } = render(\n      <DataRouterStateContext.Provider\n        value={{ matches: [], errors: null } as any}\n      >\n        <FrameworkContext.Provider value={context}>\n          <Links nonce=\"test-nonce\" />\n        </FrameworkContext.Provider>\n      </DataRouterStateContext.Provider>,\n    );\n\n    let link = container.querySelector(\"link[rel='stylesheet']\");\n    expect(link).toHaveAttribute(\"data-react-router-critical-css\");\n    expect(link).toHaveAttribute(\"href\", \"/critical.css\");\n    expect(link).toHaveAttribute(\"nonce\", \"test-nonce\");\n  });\n\n  it(\"propagates nonce to route links\", () => {\n    let context = mockFrameworkContext({\n      routeModules: {\n        root: {\n          default: () => null,\n          links: () => [{ rel: \"stylesheet\", href: \"/style.css\" }],\n        },\n      },\n      manifest: {\n        routes: {\n          root: {\n            id: \"root\",\n            module: \"root.js\",\n            hasLoader: false,\n            hasAction: false,\n            hasErrorBoundary: false,\n            hasClientAction: false,\n            hasClientLoader: false,\n            hasClientMiddleware: false,\n            clientActionModule: undefined,\n            clientLoaderModule: undefined,\n            clientMiddlewareModule: undefined,\n            hydrateFallbackModule: undefined,\n          },\n        },\n        entry: { imports: [], module: \"\" },\n        url: \"\",\n        version: \"\",\n      },\n    });\n\n    let { container } = render(\n      <DataRouterStateContext.Provider\n        value={\n          {\n            matches: [\n              {\n                route: { id: \"root\" },\n              },\n            ],\n          } as any\n        }\n      >\n        <FrameworkContext.Provider value={context}>\n          <Links nonce=\"test-nonce\" />\n        </FrameworkContext.Provider>\n      </DataRouterStateContext.Provider>,\n    );\n\n    let link = container.querySelector(\"link[href='/style.css']\");\n    expect(link).toHaveAttribute(\"nonce\", \"test-nonce\");\n  });\n});\n\ndescribe(\"usePrefetchBehavior\", () => {\n  function TestComponent({\n    prefetch,\n  }: {\n    prefetch: \"intent\" | \"render\" | \"none\" | \"viewport\";\n  }) {\n    let [shouldPrefetch, ref] = usePrefetchBehavior(prefetch, {});\n    return (\n      <a ref={ref} data-prefetch={shouldPrefetch}>\n        Link\n      </a>\n    );\n  }\n\n  it(\"handles prefetch='render'\", () => {\n    let context = mockFrameworkContext({});\n\n    // Wrap in FrameworkContext because usePrefetchBehavior checks for it\n    let { container } = render(\n      <FrameworkContext.Provider value={context}>\n        <TestComponent prefetch=\"render\" />\n      </FrameworkContext.Provider>,\n    );\n\n    expect(container.firstChild).toHaveAttribute(\"data-prefetch\", \"true\");\n  });\n\n  it(\"handles prefetch='viewport'\", () => {\n    let context = mockFrameworkContext({});\n    let observeCallback: IntersectionObserverCallback;\n    let observeMock = jest.fn();\n    let disconnectMock = jest.fn();\n\n    window.IntersectionObserver = class {\n      constructor(cb: IntersectionObserverCallback) {\n        observeCallback = cb;\n      }\n      observe = observeMock;\n      unobserve = jest.fn();\n      disconnect = disconnectMock;\n      takeRecords = () => [];\n      root = null;\n      rootMargin = \"\";\n      thresholds = [];\n    };\n\n    let { container } = render(\n      <FrameworkContext.Provider value={context}>\n        <TestComponent prefetch=\"viewport\" />\n      </FrameworkContext.Provider>,\n    );\n\n    // Initial state\n    expect(container.firstChild).toHaveAttribute(\"data-prefetch\", \"false\");\n    expect(observeMock).toHaveBeenCalled();\n\n    // Trigger intersection\n    act(() => {\n      observeCallback(\n        [{ isIntersecting: true } as IntersectionObserverEntry],\n        new IntersectionObserver(() => {}),\n      );\n    });\n\n    expect(container.firstChild).toHaveAttribute(\"data-prefetch\", \"true\");\n  });\n});\n"
  },
  {
    "path": "packages/react-router/__tests__/dom/ssr/links-test.tsx",
    "content": "import { render } from \"@testing-library/react\";\nimport * as React from \"react\";\n\nimport { Links, Outlet, createRoutesStub } from \"../../../index\";\n\ndescribe(\"<Links>\", () => {\n  describe(\"crossOrigin\", () => {\n    it(\"renders stylesheet links with crossOrigin attribute when provided\", () => {\n      let RoutesStub = createRoutesStub([\n        {\n          id: \"root\",\n          path: \"/\",\n          links: () => [{ rel: \"stylesheet\", href: \"/assets/styles.css\" }],\n          Component() {\n            return (\n              <>\n                <Links crossOrigin=\"anonymous\" />\n                <Outlet />\n              </>\n            );\n          },\n          children: [\n            { id: \"index\", index: true, Component: () => <div>Index</div> },\n          ],\n        },\n      ]);\n\n      let { container } = render(<RoutesStub />);\n\n      let stylesheetLink = container.ownerDocument.querySelector(\n        'link[rel=\"stylesheet\"][href=\"/assets/styles.css\"]',\n      );\n      expect(stylesheetLink).toBeTruthy();\n      expect(stylesheetLink?.getAttribute(\"crossorigin\")).toBe(\"anonymous\");\n    });\n\n    it(\"renders stylesheet links without crossOrigin when not provided\", () => {\n      let RoutesStub = createRoutesStub([\n        {\n          id: \"root\",\n          path: \"/\",\n          links: () => [{ rel: \"stylesheet\", href: \"/assets/styles.css\" }],\n          Component() {\n            return (\n              <>\n                <Links />\n                <Outlet />\n              </>\n            );\n          },\n          children: [\n            { id: \"index\", index: true, Component: () => <div>Index</div> },\n          ],\n        },\n      ]);\n\n      let { container } = render(<RoutesStub />);\n\n      let stylesheetLink = container.ownerDocument.querySelector(\n        'link[rel=\"stylesheet\"][href=\"/assets/styles.css\"]',\n      );\n      expect(stylesheetLink).toBeTruthy();\n      expect(stylesheetLink?.hasAttribute(\"crossorigin\")).toBe(false);\n    });\n\n    it(\"link descriptor crossOrigin overrides the component prop\", () => {\n      let RoutesStub = createRoutesStub([\n        {\n          id: \"root\",\n          path: \"/\",\n          links: () => [\n            {\n              rel: \"stylesheet\",\n              href: \"/assets/styles.css\",\n              crossOrigin: \"use-credentials\",\n            },\n          ],\n          Component() {\n            return (\n              <>\n                <Links crossOrigin=\"anonymous\" />\n                <Outlet />\n              </>\n            );\n          },\n          children: [\n            { id: \"index\", index: true, Component: () => <div>Index</div> },\n          ],\n        },\n      ]);\n\n      let { container } = render(<RoutesStub />);\n\n      let stylesheetLink = container.ownerDocument.querySelector(\n        'link[rel=\"stylesheet\"][href=\"/assets/styles.css\"]',\n      );\n      expect(stylesheetLink).toBeTruthy();\n      expect(stylesheetLink?.getAttribute(\"crossorigin\")).toBe(\n        \"use-credentials\",\n      );\n    });\n\n    it(\"link descriptor crossOrigin works without the component prop\", () => {\n      let RoutesStub = createRoutesStub([\n        {\n          id: \"root\",\n          path: \"/\",\n          links: () => [\n            {\n              rel: \"stylesheet\",\n              href: \"/assets/styles.css\",\n              crossOrigin: \"anonymous\",\n            },\n          ],\n          Component() {\n            return (\n              <>\n                <Links />\n                <Outlet />\n              </>\n            );\n          },\n          children: [\n            { id: \"index\", index: true, Component: () => <div>Index</div> },\n          ],\n        },\n      ]);\n\n      let { container } = render(<RoutesStub />);\n\n      let stylesheetLink = container.ownerDocument.querySelector(\n        'link[rel=\"stylesheet\"][href=\"/assets/styles.css\"]',\n      );\n      expect(stylesheetLink).toBeTruthy();\n      expect(stylesheetLink?.getAttribute(\"crossorigin\")).toBe(\"anonymous\");\n    });\n\n    it(\"link descriptor crossOrigin undefined does not override the component prop\", () => {\n      let RoutesStub = createRoutesStub([\n        {\n          id: \"root\",\n          path: \"/\",\n          links: () => [\n            {\n              rel: \"stylesheet\",\n              href: \"/assets/styles.css\",\n              crossOrigin: undefined,\n            },\n          ],\n          Component() {\n            return (\n              <>\n                <Links crossOrigin=\"anonymous\" />\n                <Outlet />\n              </>\n            );\n          },\n          children: [\n            { id: \"index\", index: true, Component: () => <div>Index</div> },\n          ],\n        },\n      ]);\n\n      let { container } = render(<RoutesStub />);\n\n      let stylesheetLink = container.ownerDocument.querySelector(\n        'link[rel=\"stylesheet\"][href=\"/assets/styles.css\"]',\n      );\n      expect(stylesheetLink).toBeTruthy();\n      expect(stylesheetLink?.getAttribute(\"crossorigin\")).toBe(\"anonymous\");\n    });\n  });\n});\n"
  },
  {
    "path": "packages/react-router/__tests__/dom/ssr/meta-test.tsx",
    "content": "import { prettyDOM, render, screen } from \"@testing-library/react\";\nimport user from \"@testing-library/user-event\";\nimport * as React from \"react\";\n\nimport { Meta, Outlet, createRoutesStub } from \"../../../index\";\n\nconst getDocumentHtmlForElement = (c: HTMLElement) =>\n  prettyDOM(c.ownerDocument, undefined, { highlight: false });\n\ndescribe(\"meta\", () => {\n  it(\"no meta export renders meta from nearest route meta in the tree\", () => {\n    let RoutesStub = createRoutesStub([\n      {\n        id: \"root\",\n        path: \"/\",\n        meta: ({ data }) => [\n          { name: \"description\", content: data.description },\n          { title: data.title },\n        ],\n        Component() {\n          return (\n            <>\n              <Meta />\n              <Outlet />\n            </>\n          );\n        },\n        children: [\n          {\n            index: true,\n            Component() {\n              return <div>Parent meta here!</div>;\n            },\n          },\n        ],\n      },\n    ]);\n\n    let { container } = render(\n      <RoutesStub\n        hydrationData={{\n          loaderData: {\n            root: {\n              description: \"This is a meta page\",\n              title: \"Meta Page\",\n            },\n          },\n        }}\n      />,\n    );\n\n    expect(getDocumentHtmlForElement(container)).toMatchInlineSnapshot(`\n      \"<html>\n        <head>\n          <meta\n            content=\"This is a meta page\"\n            name=\"description\"\n          />\n          <title>\n            Meta Page\n          </title>\n        </head>\n        <body>\n          <div>\n            <div>\n              Parent meta here!\n            </div>\n          </div>\n        </body>\n      </html>\"\n    `);\n  });\n\n  it(\"empty meta array does not render a tag\", () => {\n    let RoutesStub = createRoutesStub([\n      {\n        path: \"/\",\n        meta: () => [],\n        Component() {\n          return (\n            <>\n              <Meta />\n              <p>No meta here!</p>\n            </>\n          );\n        },\n      },\n    ]);\n\n    let { container } = render(<RoutesStub />);\n\n    expect(getDocumentHtmlForElement(container)).toMatchInlineSnapshot(`\n      \"<html>\n        <head />\n        <body>\n          <div>\n            <p>\n              No meta here!\n            </p>\n          </div>\n        </body>\n      </html>\"\n    `);\n  });\n\n  it(\"meta from `matches` renders meta tags\", () => {\n    let RoutesStub = createRoutesStub([\n      {\n        id: \"root\",\n        path: \"/\",\n        meta: () => [{ charSet: \"utf-8\" }],\n        Component() {\n          return (\n            <>\n              <Meta />\n              <Outlet />\n            </>\n          );\n        },\n        children: [\n          {\n            index: true,\n            meta({ matches }) {\n              let rootModule = matches.find((match) => match.id === \"root\");\n              // @ts-expect-error\n              let rootCharSet = rootModule?.meta.find((meta) => meta.charSet);\n              return [rootCharSet, { title: \"Child title\" }];\n            },\n            Component() {\n              return <p>Matches Meta</p>;\n            },\n          },\n        ],\n      },\n    ]);\n\n    let { container } = render(<RoutesStub />);\n\n    expect(getDocumentHtmlForElement(container)).toMatchInlineSnapshot(`\n      \"<html>\n        <head>\n          <meta\n            charset=\"utf-8\"\n          />\n          <title>\n            Child title\n          </title>\n        </head>\n        <body>\n          <div>\n            <p>\n              Matches Meta\n            </p>\n          </div>\n        </body>\n      </html>\"\n    `);\n  });\n\n  it(\"{ charSet } adds a <meta charset='utf-8' />\", () => {\n    let RoutesStub = createRoutesStub([\n      {\n        path: \"/\",\n        meta: () => [{ charSet: \"utf-8\" }],\n        Component: Meta,\n      },\n    ]);\n\n    let { container } = render(<RoutesStub />);\n\n    expect(getDocumentHtmlForElement(container)).toMatchInlineSnapshot(`\n      \"<html>\n        <head>\n          <meta\n            charset=\"utf-8\"\n          />\n        </head>\n        <body>\n          <div />\n        </body>\n      </html>\"\n    `);\n  });\n\n  it(\"{ title } adds a <title />\", () => {\n    let RoutesStub = createRoutesStub([\n      {\n        path: \"/\",\n        meta: () => [{ title: \"Document Title\" }],\n        Component: Meta,\n      },\n    ]);\n\n    let { container } = render(<RoutesStub />);\n\n    expect(getDocumentHtmlForElement(container)).toMatchInlineSnapshot(`\n      \"<html>\n        <head>\n          <title>\n            Document Title\n          </title>\n        </head>\n        <body>\n          <div />\n        </body>\n      </html>\"\n    `);\n  });\n\n  it(\"{ property: 'og:*', content: '*' } adds a <meta property='og:*' />\", () => {\n    let RoutesStub = createRoutesStub([\n      {\n        path: \"/\",\n        meta: () => [\n          { property: \"og:image\", content: \"https://picsum.photos/200/200\" },\n          { property: \"og:type\", content: undefined },\n        ],\n        Component: Meta,\n      },\n    ]);\n\n    let { container } = render(<RoutesStub />);\n    expect(getDocumentHtmlForElement(container)).toMatchInlineSnapshot(`\n      \"<html>\n        <head>\n          <meta\n            content=\"https://picsum.photos/200/200\"\n            property=\"og:image\"\n          />\n          <meta\n            property=\"og:type\"\n          />\n        </head>\n        <body>\n          <div />\n        </body>\n      </html>\"\n    `);\n  });\n\n  it(\"{ 'script:ld+json': {} } adds a <script type='application/ld+json' />\", () => {\n    let jsonLd = {\n      \"@context\": \"http://schema.org\",\n      \"@type\": \"Person\",\n      name: \"Sonny Day\",\n      address: {\n        \"@type\": \"PostalAddress\",\n        streetAddress: \"123 Sunset Cliffs Blvd\",\n        addressLocality: \"San Diego\",\n        addressRegion: \"CA\",\n        postalCode: \"92107\",\n      },\n      email: [\"sonnyday@fancymail.com\", \"surfergal@veryprofessional.org\"],\n      bio: \"A <b>surfer</b> & <em>coder</em>.\",\n    };\n\n    let RoutesStub = createRoutesStub([\n      {\n        path: \"/\",\n        meta: () => [\n          {\n            \"script:ld+json\": jsonLd,\n          },\n        ],\n        Component: Meta,\n      },\n    ]);\n\n    let { container } = render(<RoutesStub />);\n\n    // For some reason, prettyDOM strips the script tag (maybe because of\n    // dangerouslySetInnerHTML), so we just parse the HTML out into JSON and assert that way\n    let scriptTagContents =\n      container.querySelector('script[type=\"application/ld+json\"]')\n        ?.innerHTML || \"{}\";\n    expect(JSON.parse(scriptTagContents)).toEqual(jsonLd);\n    expect(scriptTagContents).toContain(\n      \"A \\\\u003cb\\\\u003esurfer\\\\u003c/b\\\\u003e \\\\u0026 \\\\u003cem\\\\u003ecoder\\\\u003c/em\\\\u003e.\",\n    );\n  });\n\n  it(\"{ tagName: 'link' } adds a <link />\", () => {\n    let RoutesStub = createRoutesStub([\n      {\n        path: \"/\",\n        meta: () => [\n          {\n            tagName: \"link\",\n            rel: \"canonical\",\n            href: \"https://website.com/authors/1\",\n          },\n        ],\n        Component: Meta,\n      },\n    ]);\n\n    let { container } = render(<RoutesStub />);\n    expect(getDocumentHtmlForElement(container)).toMatchInlineSnapshot(`\n      \"<html>\n        <head>\n          <link\n            href=\"https://website.com/authors/1\"\n            rel=\"canonical\"\n          />\n        </head>\n        <body>\n          <div />\n        </body>\n      </html>\"\n    `);\n  });\n\n  it(\"does not mutate meta when using tagName\", async () => {\n    let RoutesStub = createRoutesStub([\n      {\n        path: \"/\",\n        meta: ({ data }) => data?.meta,\n        loader: () => ({\n          meta: [\n            {\n              tagName: \"link\",\n              rel: \"canonical\",\n              href: \"https://website.com/authors/1\",\n            },\n          ],\n        }),\n        HydrateFallback: () => null,\n        Component() {\n          let [count, setCount] = React.useState(0);\n          return (\n            <>\n              <button onClick={() => setCount(count + 1)}>\n                {`Increment ${count}`}\n              </button>\n              <Meta key={count} />\n            </>\n          );\n        },\n      },\n    ]);\n\n    let { container } = render(<RoutesStub />);\n\n    await screen.findByText(\"Increment 0\");\n    expect(getDocumentHtmlForElement(container)).toMatchInlineSnapshot(`\n      \"<html>\n        <head>\n          <link\n            href=\"https://website.com/authors/1\"\n            rel=\"canonical\"\n          />\n        </head>\n        <body>\n          <div>\n            <button>\n              Increment 0\n            </button>\n          </div>\n        </body>\n      </html>\"\n    `);\n\n    user.click(screen.getByRole(\"button\"));\n    await screen.findByText(\"Increment 1\");\n\n    expect(getDocumentHtmlForElement(container)).toMatchInlineSnapshot(`\n      \"<html>\n        <head>\n          <link\n            href=\"https://website.com/authors/1\"\n            rel=\"canonical\"\n          />\n        </head>\n        <body>\n          <div>\n            <button>\n              Increment 1\n            </button>\n          </div>\n        </body>\n      </html>\"\n    `);\n  });\n\n  it(\"loader errors are passed to meta\", () => {\n    let RoutesStub = createRoutesStub([\n      {\n        path: \"/\",\n        Component() {\n          return (\n            <>\n              <Meta />\n              <Outlet />\n            </>\n          );\n        },\n        children: [\n          {\n            id: \"index\",\n            index: true,\n            meta: ({ error }) => [\n              {\n                title: (error as Error)?.message || \"Home\",\n              },\n            ],\n            Component() {\n              return <h1>Page</h1>;\n            },\n            ErrorBoundary() {\n              return <h1>Boundary</h1>;\n            },\n          },\n        ],\n      },\n    ]);\n\n    let { container } = render(\n      <RoutesStub hydrationData={{ errors: { index: new Error(\"Oh no!\") } }} />,\n    );\n    expect(getDocumentHtmlForElement(container)).toMatchInlineSnapshot(`\n      \"<html>\n        <head>\n          <title>\n            Oh no!\n          </title>\n        </head>\n        <body>\n          <div>\n            <h1>\n              Boundary\n            </h1>\n          </div>\n        </body>\n      </html>\"\n    `);\n  });\n});\n"
  },
  {
    "path": "packages/react-router/__tests__/dom/static-link-test.tsx",
    "content": "import * as React from \"react\";\nimport * as TestRenderer from \"react-test-renderer\";\nimport { Link, StaticRouter } from \"../../index\";\n\ndescribe(\"A <Link> in a <StaticRouter>\", () => {\n  describe(\"with a string\", () => {\n    it(\"uses the right href\", () => {\n      let renderer: TestRenderer.ReactTestRenderer;\n      TestRenderer.act(() => {\n        renderer = TestRenderer.create(\n          <StaticRouter location=\"/\">\n            <Link to=\"mjackson\" />\n          </StaticRouter>,\n        );\n      });\n\n      expect(renderer.root.findByType(\"a\").props.href).toEqual(\"/mjackson\");\n    });\n\n    it(\"uses the right href with a basename\", () => {\n      let renderer: TestRenderer.ReactTestRenderer;\n      TestRenderer.act(() => {\n        renderer = TestRenderer.create(\n          <StaticRouter location=\"/base\" basename=\"/base\">\n            <Link to=\"mjackson\" />\n          </StaticRouter>,\n        );\n      });\n\n      expect(renderer.root.findByType(\"a\").props.href).toEqual(\n        \"/base/mjackson\",\n      );\n    });\n  });\n\n  describe(\"with an object\", () => {\n    it(\"uses the right href\", () => {\n      let renderer: TestRenderer.ReactTestRenderer;\n      TestRenderer.act(() => {\n        renderer = TestRenderer.create(\n          <StaticRouter location=\"/\">\n            <Link to={{ pathname: \"/mjackson\" }} />\n          </StaticRouter>,\n        );\n      });\n\n      expect(renderer.root.findByType(\"a\").props.href).toEqual(\"/mjackson\");\n    });\n  });\n});\n"
  },
  {
    "path": "packages/react-router/__tests__/dom/static-location-test.tsx",
    "content": "import * as React from \"react\";\nimport * as ReactDOMServer from \"react-dom/server.edge\";\nimport { Routes, Route, StaticRouter, useLocation } from \"../../index\";\n\ndescribe(\"A <StaticRouter>\", () => {\n  describe(\"with a string location prop\", () => {\n    it(\"parses the location into an object\", () => {\n      let location!: ReturnType<typeof useLocation>;\n      function LocationChecker() {\n        location = useLocation();\n        return null;\n      }\n\n      ReactDOMServer.renderToStaticMarkup(\n        <StaticRouter location=\"/the/path?the=query#the-hash\">\n          <Routes>\n            <Route path=\"/the/path\" element={<LocationChecker />} />\n          </Routes>\n        </StaticRouter>,\n      );\n\n      expect(location).toEqual({\n        pathname: \"/the/path\",\n        search: \"?the=query\",\n        hash: \"#the-hash\",\n        state: null,\n        key: expect.any(String),\n      });\n    });\n  });\n\n  describe(\"with an object location prop\", () => {\n    it(\"adds missing properties\", () => {\n      let location!: ReturnType<typeof useLocation>;\n      function LocationChecker() {\n        location = useLocation();\n        return null;\n      }\n\n      ReactDOMServer.renderToStaticMarkup(\n        <StaticRouter\n          location={{ pathname: \"/the/path\", search: \"?the=query\" }}\n        >\n          <Routes>\n            <Route path=\"/the/path\" element={<LocationChecker />} />\n          </Routes>\n        </StaticRouter>,\n      );\n\n      expect(location).toEqual({\n        pathname: \"/the/path\",\n        search: \"?the=query\",\n        hash: \"\",\n        state: null,\n        key: expect.any(String),\n      });\n    });\n\n    it(\"retains a non-null state when passed explicitly\", () => {\n      let location!: ReturnType<typeof useLocation>;\n      function LocationChecker() {\n        location = useLocation();\n        return null;\n      }\n\n      ReactDOMServer.renderToStaticMarkup(\n        <StaticRouter\n          location={{ pathname: \"/the/path\", search: \"?the=query\", state: 0 }}\n        >\n          <Routes>\n            <Route path=\"/the/path\" element={<LocationChecker />} />\n          </Routes>\n        </StaticRouter>,\n      );\n\n      expect(location).toEqual({\n        pathname: \"/the/path\",\n        search: \"?the=query\",\n        hash: \"\",\n        state: 0,\n        key: expect.any(String),\n      });\n    });\n  });\n});\n"
  },
  {
    "path": "packages/react-router/__tests__/dom/static-navigate-test.tsx",
    "content": "import * as React from \"react\";\nimport * as ReactDOMServer from \"react-dom/server.edge\";\nimport { Navigate, Routes, Route, StaticRouter } from \"../../index\";\n\ndescribe(\"A <Navigate> in a <StaticRouter>\", () => {\n  let consoleWarn: jest.SpyInstance;\n  beforeEach(() => {\n    consoleWarn = jest.spyOn(console, \"warn\").mockImplementation(() => {});\n  });\n\n  afterEach(() => {\n    consoleWarn.mockRestore();\n  });\n\n  it(\"warns about using on the initial render\", () => {\n    ReactDOMServer.renderToStaticMarkup(\n      <StaticRouter location=\"/home\">\n        <Routes>\n          <Route\n            path=\"/home\"\n            element={<Navigate to=\"/somewhere-else?the=query\" />}\n          />\n        </Routes>\n      </StaticRouter>,\n    );\n\n    expect(consoleWarn).toHaveBeenCalledWith(\n      expect.stringMatching(\n        \"<Navigate> must not be used on the initial render\",\n      ),\n    );\n  });\n});\n"
  },
  {
    "path": "packages/react-router/__tests__/dom/stub-test.tsx",
    "content": "import * as React from \"react\";\nimport { act, render, screen, waitFor } from \"@testing-library/react\";\nimport user from \"@testing-library/user-event\";\nimport {\n  Form,\n  Outlet,\n  useActionData,\n  useFetcher,\n  useLoaderData,\n  useMatches,\n  createRoutesStub,\n  type LoaderFunctionArgs,\n  useRouteError,\n} from \"../../index\";\nimport {\n  RouterContextProvider,\n  createContext,\n  redirect,\n} from \"../../lib/router/utils\";\n\ntest(\"renders a route\", () => {\n  let RoutesStub = createRoutesStub([\n    {\n      path: \"/\",\n      Component: () => <div>HOME</div>,\n    },\n  ]);\n\n  render(<RoutesStub />);\n\n  expect(screen.getByText(\"HOME\")).toBeInTheDocument();\n});\n\ntest(\"renders a nested route\", () => {\n  let RoutesStub = createRoutesStub([\n    {\n      Component() {\n        return (\n          <div>\n            <h1>ROOT</h1>\n            <Outlet />\n          </div>\n        );\n      },\n      children: [\n        {\n          path: \"/\",\n          Component: () => <div>INDEX</div>,\n        },\n      ],\n    },\n  ]);\n\n  render(<RoutesStub />);\n\n  expect(screen.getByText(\"ROOT\")).toBeInTheDocument();\n  expect(screen.getByText(\"INDEX\")).toBeInTheDocument();\n});\n\ntest(\"middleware works without loader\", async () => {\n  let RoutesStub = createRoutesStub([\n    {\n      path: \"/\",\n      middleware: [\n        () => {\n          throw redirect(\"/target\");\n        },\n      ],\n      HydrateFallback: () => null,\n      Component() {\n        return <pre>Home</pre>;\n      },\n    },\n    {\n      path: \"/target\",\n      Component() {\n        return <pre>Target</pre>;\n      },\n    },\n  ]);\n\n  act(() => {\n    render(<RoutesStub future={{ v8_middleware: true }} />);\n  });\n\n  await waitFor(() => screen.findByText(\"Target\"));\n});\n\ntest(\"middleware works with loader\", async () => {\n  let stringContext = createContext<string>();\n  let RoutesStub = createRoutesStub([\n    {\n      path: \"/\",\n      HydrateFallback: () => null,\n      Component() {\n        let data = useLoaderData();\n        return <pre data-testid=\"data\">Message: {data.message}</pre>;\n      },\n      middleware: [({ context }) => context.set(stringContext, \"hello\")],\n      loader({ context }) {\n        return { message: context.get(stringContext) };\n      },\n    },\n  ]);\n\n  render(<RoutesStub future={{ v8_middleware: true }} />);\n\n  await waitFor(() => screen.findByText(\"Message: hello\"));\n});\n\n// eslint-disable-next-line jest/expect-expect\ntest(\"loaders work\", async () => {\n  let RoutesStub = createRoutesStub([\n    {\n      path: \"/\",\n      HydrateFallback: () => null,\n      Component() {\n        let data = useLoaderData();\n        return <pre data-testid=\"data\">Message: {data.message}</pre>;\n      },\n      loader() {\n        return Response.json({ message: \"hello\" });\n      },\n    },\n  ]);\n\n  render(<RoutesStub />);\n\n  await waitFor(() => screen.findByText(\"Message: hello\"));\n});\n\n// eslint-disable-next-line jest/expect-expect\ntest(\"loaders work with props\", async () => {\n  let RoutesStub = createRoutesStub([\n    {\n      path: \"/\",\n      HydrateFallback: () => null,\n      Component({ loaderData }) {\n        let data = loaderData as { message: string };\n        return <pre data-testid=\"data\">Message: {data.message}</pre>;\n      },\n      loader() {\n        return { message: \"hello\" };\n      },\n    },\n  ]);\n\n  render(<RoutesStub />);\n\n  await waitFor(() => screen.findByText(\"Message: hello\"));\n});\n\n// eslint-disable-next-line jest/expect-expect\ntest(\"actions work\", async () => {\n  let RoutesStub = createRoutesStub([\n    {\n      path: \"/\",\n      Component() {\n        let data = useActionData() as { message: string } | undefined;\n        return (\n          <Form method=\"post\">\n            <button type=\"submit\">Submit</button>\n            {data ? <pre>Message: {data.message}</pre> : null}\n          </Form>\n        );\n      },\n      action() {\n        return Response.json({ message: \"hello\" });\n      },\n    },\n  ]);\n\n  render(<RoutesStub />);\n\n  user.click(screen.getByText(\"Submit\"));\n  await waitFor(() => screen.findByText(\"Message: hello\"));\n});\n\n// eslint-disable-next-line jest/expect-expect\ntest(\"actions work with props\", async () => {\n  let RoutesStub = createRoutesStub([\n    {\n      path: \"/\",\n      Component({ actionData }) {\n        let data = actionData as { message: string } | undefined;\n        return (\n          <Form method=\"post\">\n            <button type=\"submit\">Submit</button>\n            {data ? <pre>Message: {data.message}</pre> : null}\n          </Form>\n        );\n      },\n      action() {\n        return Response.json({ message: \"hello\" });\n      },\n    },\n  ]);\n\n  render(<RoutesStub />);\n\n  user.click(screen.getByText(\"Submit\"));\n  await waitFor(() => screen.findByText(\"Message: hello\"));\n});\n\n// eslint-disable-next-line jest/expect-expect\ntest(\"errors work\", async () => {\n  let spy = jest.spyOn(console, \"error\").mockImplementation(() => {});\n  let RoutesStub = createRoutesStub([\n    {\n      path: \"/\",\n      Component() {\n        throw new Error(\"Broken!\");\n      },\n      ErrorBoundary() {\n        let error = useRouteError() as Error;\n        return <p>Error: {error.message}</p>;\n      },\n    },\n  ]);\n\n  render(<RoutesStub />);\n\n  await waitFor(() => screen.findByText(\"Error: Broken!\"));\n  spy.mockRestore();\n});\n\n// eslint-disable-next-line jest/expect-expect\ntest(\"errors work with prop\", async () => {\n  let spy = jest.spyOn(console, \"error\").mockImplementation(() => {});\n  let RoutesStub = createRoutesStub([\n    {\n      path: \"/\",\n      Component() {\n        throw new Error(\"Broken!\");\n      },\n      ErrorBoundary({ error }) {\n        return <p>Error: {(error as Error).message}</p>;\n      },\n    },\n  ]);\n\n  render(<RoutesStub />);\n\n  await waitFor(() => screen.findByText(\"Error: Broken!\"));\n  spy.mockRestore();\n});\n\n// eslint-disable-next-line jest/expect-expect\ntest(\"fetchers work\", async () => {\n  let count = 0;\n  let RoutesStub = createRoutesStub([\n    {\n      path: \"/\",\n      Component() {\n        let fetcher = useFetcher<{ count: number }>();\n        return (\n          <button onClick={() => fetcher.load(\"/api\")}>\n            {fetcher.state + \" \" + (fetcher.data?.count || 0)}\n          </button>\n        );\n      },\n    },\n    {\n      path: \"/api\",\n      loader() {\n        return Response.json({ count: ++count });\n      },\n    },\n  ]);\n\n  render(<RoutesStub />);\n\n  user.click(screen.getByText(\"idle 0\"));\n  await waitFor(() => screen.findByText(\"idle 1\"));\n\n  user.click(screen.getByText(\"idle 1\"));\n  await waitFor(() => screen.findByText(\"idle 2\"));\n});\n\n// eslint-disable-next-line jest/expect-expect\ntest(\"can pass a predefined loader\", () => {\n  async function loader(_args: LoaderFunctionArgs) {\n    return Response.json({ hi: \"there\" });\n  }\n\n  createRoutesStub([\n    {\n      path: \"/example\",\n      loader,\n    },\n  ]);\n});\n\ntest(\"can pass context values (w/o middleware)\", async () => {\n  let RoutesStub = createRoutesStub(\n    [\n      {\n        path: \"/\",\n        HydrateFallback: () => null,\n        Component() {\n          let data = useLoaderData() as string;\n          return (\n            <div>\n              <pre data-testid=\"root\">Context: {data}</pre>\n              <Outlet />\n            </div>\n          );\n        },\n        loader({ context }) {\n          return context.message;\n        },\n        children: [\n          {\n            path: \"hello\",\n            Component() {\n              let data = useLoaderData() as string;\n              return <pre data-testid=\"hello\">Context: {data}</pre>;\n            },\n            loader({ context }) {\n              return context.message;\n            },\n          },\n        ],\n      },\n    ],\n    { message: \"hello\" },\n  );\n\n  render(<RoutesStub initialEntries={[\"/hello\"]} />);\n\n  expect(await screen.findByTestId(\"root\")).toHaveTextContent(/Context: hello/);\n  expect(await screen.findByTestId(\"hello\")).toHaveTextContent(\n    /Context: hello/,\n  );\n});\n\ntest(\"can pass context values (w/middleware)\", async () => {\n  let helloContext = createContext();\n  let RoutesStub = createRoutesStub(\n    [\n      {\n        path: \"/\",\n        HydrateFallback: () => null,\n        Component() {\n          let data = useLoaderData() as string;\n          return (\n            <div>\n              <pre data-testid=\"root\">Context: {data}</pre>\n              <Outlet />\n            </div>\n          );\n        },\n        loader({ context }) {\n          return context.get(helloContext);\n        },\n        children: [\n          {\n            path: \"hello\",\n            Component() {\n              let data = useLoaderData() as string;\n              return <pre data-testid=\"hello\">Context: {data}</pre>;\n            },\n            loader({ context }) {\n              return context.get(helloContext);\n            },\n          },\n        ],\n      },\n    ],\n    new RouterContextProvider(new Map([[helloContext, \"hello\"]])),\n  );\n\n  render(\n    <RoutesStub future={{ v8_middleware: true }} initialEntries={[\"/hello\"]} />,\n  );\n\n  expect(await screen.findByTestId(\"root\")).toHaveTextContent(/Context: hello/);\n  expect(await screen.findByTestId(\"hello\")).toHaveTextContent(\n    /Context: hello/,\n  );\n});\n\ntest(\"all routes have ids\", () => {\n  let RoutesStub = createRoutesStub([\n    {\n      Component() {\n        return (\n          <div>\n            <h1>ROOT</h1>\n            <Outlet />\n          </div>\n        );\n      },\n      children: [\n        {\n          path: \"/\",\n          Component() {\n            let matches = useMatches();\n\n            return (\n              <div>\n                <h1>HOME</h1>\n                <pre data-testid=\"matches\">\n                  {JSON.stringify(matches, null, 2)}\n                </pre>\n              </div>\n            );\n          },\n        },\n      ],\n    },\n  ]);\n\n  render(<RoutesStub />);\n\n  let matchesTextContent = screen.getByTestId(\"matches\").textContent;\n\n  expect(matchesTextContent).toBeDefined();\n  let matches = JSON.parse(matchesTextContent!);\n  let matchesWithoutIds = matches.filter((match: any) => match.id == null);\n\n  expect(matchesWithoutIds).toHaveLength(0);\n});\n"
  },
  {
    "path": "packages/react-router/__tests__/dom/trailing-slashes-test.tsx",
    "content": "import * as React from \"react\";\nimport * as ReactDOM from \"react-dom/client\";\nimport { act } from \"@testing-library/react\";\n\nimport type { To } from \"../../index\";\nimport {\n  BrowserRouter,\n  Routes,\n  Route,\n  Link,\n  Outlet,\n  useSearchParams,\n  useNavigate,\n} from \"../../index\";\nimport getWindow from \"../utils/getWindow\";\n\ndescribe(\"trailing slashes\", () => {\n  let node: HTMLDivElement;\n  beforeEach(() => {\n    node = document.createElement(\"div\");\n    document.body.appendChild(node);\n  });\n\n  afterEach(() => {\n    document.body.removeChild(node);\n    node = null!;\n  });\n\n  describe(\"in a <Link> element\", () => {\n    describe(\"with a basename that does not contain a trailing slash\", () => {\n      test(\"never includes trailing slashes on root links (index route)\", () => {\n        let window = getWindow(\"/app\");\n        act(() => {\n          ReactDOM.createRoot(node).render(\n            <BrowserRouter basename=\"/app\" window={window}>\n              <Routes>\n                <Route\n                  index\n                  element={\n                    <>\n                      <Link to=\"\" />\n                      <Link to=\"/\" />\n                    </>\n                  }\n                />\n              </Routes>\n            </BrowserRouter>,\n          );\n        });\n\n        expect(node.querySelectorAll(\"a\")).toMatchInlineSnapshot(`\n          NodeList [\n            <a\n              data-discover=\"true\"\n              href=\"/app\"\n            />,\n            <a\n              data-discover=\"true\"\n              href=\"/app\"\n            />,\n          ]\n        `);\n      });\n\n      test('never includes trailing slashes on root links (path=\"\")', () => {\n        let window = getWindow(\"/app\");\n        act(() => {\n          ReactDOM.createRoot(node).render(\n            <BrowserRouter basename=\"/app\" window={window}>\n              <Routes>\n                <Route\n                  path=\"\"\n                  element={\n                    <>\n                      <Link to=\"\" />\n                      <Link to=\"/\" />\n                    </>\n                  }\n                />\n              </Routes>\n            </BrowserRouter>,\n          );\n        });\n\n        expect(node.querySelectorAll(\"a\")).toMatchInlineSnapshot(`\n          NodeList [\n            <a\n              data-discover=\"true\"\n              href=\"/app\"\n            />,\n            <a\n              data-discover=\"true\"\n              href=\"/app\"\n            />,\n          ]\n        `);\n      });\n\n      test(\"allows non-root links to control trailing slashes\", () => {\n        let window = getWindow(\"/app/parent/child\");\n        act(() => {\n          ReactDOM.createRoot(node).render(\n            <BrowserRouter basename=\"/app\" window={window}>\n              <Routes>\n                <Route\n                  path=\"parent\"\n                  element={\n                    <>\n                      <Link to=\"..\" />\n                      <Link to=\"../\" />\n                      <Outlet />\n                    </>\n                  }\n                >\n                  <Route\n                    path=\"child\"\n                    element={\n                      <>\n                        <Link to=\"../..\" />\n                        <Link to=\"../../\" />\n                        <Link to=\"../../parent\" />\n                        <Link to=\"../../parent/\" />\n                        <Outlet />\n                      </>\n                    }\n                  >\n                    <Route\n                      index\n                      element={\n                        <>\n                          <Link to=\"../..\" />\n                          <Link to=\"../../\" />\n                          <Link to=\"../../parent/child\" />\n                          <Link to=\"../../parent/child/\" />\n                        </>\n                      }\n                    />\n                  </Route>\n                </Route>\n              </Routes>\n            </BrowserRouter>,\n          );\n        });\n\n        expect(node.querySelectorAll(\"a\")).toMatchInlineSnapshot(`\n          NodeList [\n            <a\n              data-discover=\"true\"\n              href=\"/app\"\n            />,\n            <a\n              data-discover=\"true\"\n              href=\"/app\"\n            />,\n            <a\n              data-discover=\"true\"\n              href=\"/app\"\n            />,\n            <a\n              data-discover=\"true\"\n              href=\"/app\"\n            />,\n            <a\n              data-discover=\"true\"\n              href=\"/app/parent\"\n            />,\n            <a\n              data-discover=\"true\"\n              href=\"/app/parent/\"\n            />,\n            <a\n              data-discover=\"true\"\n              href=\"/app\"\n            />,\n            <a\n              data-discover=\"true\"\n              href=\"/app\"\n            />,\n            <a\n              data-discover=\"true\"\n              href=\"/app/parent/child\"\n            />,\n            <a\n              data-discover=\"true\"\n              href=\"/app/parent/child/\"\n            />,\n          ]\n        `);\n      });\n    });\n\n    describe(\"with a basename that contains a trailing slash\", () => {\n      test(\"always contains trailing slashes on root links (index route)\", () => {\n        let window = getWindow(\"/app/\");\n        act(() => {\n          ReactDOM.createRoot(node).render(\n            <BrowserRouter basename=\"/app/\" window={window}>\n              <Routes>\n                <Route\n                  index\n                  element={\n                    <>\n                      <Link to=\"\" />\n                      <Link to=\"/\" />\n                    </>\n                  }\n                />\n              </Routes>\n            </BrowserRouter>,\n          );\n        });\n\n        expect(node.querySelectorAll(\"a\")).toMatchInlineSnapshot(`\n          NodeList [\n            <a\n              data-discover=\"true\"\n              href=\"/app/\"\n            />,\n            <a\n              data-discover=\"true\"\n              href=\"/app/\"\n            />,\n          ]\n        `);\n      });\n\n      test('always contains trailing slashes on root links (path=\"\" route)', () => {\n        let window = getWindow(\"/app/\");\n        act(() => {\n          ReactDOM.createRoot(node).render(\n            <BrowserRouter basename=\"/app/\" window={window}>\n              <Routes>\n                <Route\n                  path=\"\"\n                  element={\n                    <>\n                      <Link to=\"\" />\n                      <Link to=\"/\" />\n                    </>\n                  }\n                />\n              </Routes>\n            </BrowserRouter>,\n          );\n        });\n\n        expect(node.querySelectorAll(\"a\")).toMatchInlineSnapshot(`\n          NodeList [\n            <a\n              data-discover=\"true\"\n              href=\"/app/\"\n            />,\n            <a\n              data-discover=\"true\"\n              href=\"/app/\"\n            />,\n          ]\n        `);\n      });\n\n      test(\"allows non-root links to control trailing slashes\", () => {\n        let window = getWindow(\"/app/parent/child\");\n        act(() => {\n          ReactDOM.createRoot(node).render(\n            <BrowserRouter basename=\"/app/\" window={window}>\n              <Routes>\n                <Route\n                  path=\"parent\"\n                  element={\n                    <>\n                      <Link to=\"..\" />\n                      <Link to=\"../\" />\n                      <Outlet />\n                    </>\n                  }\n                >\n                  <Route\n                    path=\"child\"\n                    element={\n                      <>\n                        <Link to=\"../..\" />\n                        <Link to=\"../../\" />\n                        <Link to=\"../../parent\" />\n                        <Link to=\"../../parent/\" />\n                        <Outlet />\n                      </>\n                    }\n                  >\n                    <Route\n                      index\n                      element={\n                        <>\n                          <Link to=\"../..\" />\n                          <Link to=\"../../\" />\n                          <Link to=\"../../parent/child\" />\n                          <Link to=\"../../parent/child/\" />\n                        </>\n                      }\n                    />\n                  </Route>\n                </Route>\n              </Routes>\n            </BrowserRouter>,\n          );\n        });\n\n        expect(node.querySelectorAll(\"a\")).toMatchInlineSnapshot(`\n          NodeList [\n            <a\n              data-discover=\"true\"\n              href=\"/app/\"\n            />,\n            <a\n              data-discover=\"true\"\n              href=\"/app/\"\n            />,\n            <a\n              data-discover=\"true\"\n              href=\"/app/\"\n            />,\n            <a\n              data-discover=\"true\"\n              href=\"/app/\"\n            />,\n            <a\n              data-discover=\"true\"\n              href=\"/app/parent\"\n            />,\n            <a\n              data-discover=\"true\"\n              href=\"/app/parent/\"\n            />,\n            <a\n              data-discover=\"true\"\n              href=\"/app/\"\n            />,\n            <a\n              data-discover=\"true\"\n              href=\"/app/\"\n            />,\n            <a\n              data-discover=\"true\"\n              href=\"/app/parent/child\"\n            />,\n            <a\n              data-discover=\"true\"\n              href=\"/app/parent/child/\"\n            />,\n          ]\n        `);\n      });\n    });\n  });\n\n  describe(\"in a <Navigate> element\", () => {\n    describe(\"with a basename that does not contain a trailing slash\", () => {\n      test(\"never includes trailing slashes on root links (/)\", () => {\n        let window = getWindow(\"/foo/bar\");\n        jest.spyOn(window.history, \"pushState\");\n        expect(window.location.href).toBe(\"http://localhost/foo/bar\");\n\n        act(() => {\n          ReactDOM.createRoot(node).render(\n            <BrowserRouter basename=\"/foo\" window={window}>\n              <Routes>\n                <Route index element={<h1>👋</h1>} />\n                <Route path=\"bar\" element={<SingleNavigate to=\"/\" />} />\n              </Routes>\n            </BrowserRouter>,\n          );\n        });\n        expect(window.location.href).toBe(\"http://localhost/foo\");\n      });\n\n      test(\"never includes trailing slashes on root links (../)\", () => {\n        let window = getWindow(\"/foo/bar\");\n        jest.spyOn(window.history, \"pushState\");\n        expect(window.location.href).toBe(\"http://localhost/foo/bar\");\n\n        act(() => {\n          ReactDOM.createRoot(node).render(\n            <BrowserRouter basename=\"/foo\" window={window}>\n              <Routes>\n                <Route index element={<h1>👋</h1>} />\n                <Route path=\"bar\" element={<SingleNavigate to=\"../\" />} />\n              </Routes>\n            </BrowserRouter>,\n          );\n        });\n        expect(window.location.href).toBe(\"http://localhost/foo\");\n      });\n\n      test(\"allows non-root links to leave off trailing slashes\", () => {\n        let window = getWindow(\"/foo\");\n        jest.spyOn(window.history, \"pushState\");\n\n        expect(window.location.href).toBe(\"http://localhost/foo\");\n\n        act(() => {\n          ReactDOM.createRoot(node).render(\n            <BrowserRouter basename=\"/foo\" window={window}>\n              <Routes>\n                <Route index element={<SingleNavigate to=\"bar\" />} />\n                <Route path=\"bar\" element={<h1>👋</h1>} />\n              </Routes>\n            </BrowserRouter>,\n          );\n        });\n\n        expect(window.location.href).toBe(\"http://localhost/foo/bar\");\n      });\n\n      test(\"allows non-root links to include trailing slashes\", () => {\n        let window = getWindow(\"/foo\");\n        jest.spyOn(window.history, \"pushState\");\n\n        expect(window.location.href).toBe(\"http://localhost/foo\");\n\n        act(() => {\n          ReactDOM.createRoot(node).render(\n            <BrowserRouter basename=\"/foo\" window={window}>\n              <Routes>\n                <Route index element={<SingleNavigate to=\"bar/\" />} />\n                <Route path=\"bar\" element={<h1>👋</h1>} />\n              </Routes>\n            </BrowserRouter>,\n          );\n        });\n\n        expect(window.location.href).toBe(\"http://localhost/foo/bar/\");\n      });\n    });\n\n    describe(\"with a basename that contains a trailing slash\", () => {\n      test(\"always includes trailing slashes on root links (/)\", () => {\n        let window = getWindow(\"/foo/bar\");\n        jest.spyOn(window.history, \"pushState\");\n        expect(window.location.href).toBe(\"http://localhost/foo/bar\");\n\n        act(() => {\n          ReactDOM.createRoot(node).render(\n            <BrowserRouter basename=\"/foo/\" window={window}>\n              <Routes>\n                <Route index element={<h1>👋</h1>} />\n                <Route path=\"bar\" element={<SingleNavigate to=\"/\" />} />\n              </Routes>\n            </BrowserRouter>,\n          );\n        });\n        expect(window.location.href).toBe(\"http://localhost/foo/\");\n      });\n\n      test(\"always includes trailing slashes on root links (../)\", () => {\n        let window = getWindow(\"/foo/bar\");\n        jest.spyOn(window.history, \"pushState\");\n        expect(window.location.href).toBe(\"http://localhost/foo/bar\");\n\n        act(() => {\n          ReactDOM.createRoot(node).render(\n            <BrowserRouter basename=\"/foo/\" window={window}>\n              <Routes>\n                <Route index element={<h1>👋</h1>} />\n                <Route path=\"bar\" element={<SingleNavigate to=\"..\" />} />\n              </Routes>\n            </BrowserRouter>,\n          );\n        });\n        expect(window.location.href).toBe(\"http://localhost/foo/\");\n      });\n\n      test(\"allows non-root links to leave off trailing slashes\", () => {\n        let window = getWindow(\"/foo/\");\n        jest.spyOn(window.history, \"pushState\");\n\n        expect(window.location.href).toBe(\"http://localhost/foo/\");\n\n        act(() => {\n          ReactDOM.createRoot(node).render(\n            <BrowserRouter basename=\"/foo/\" window={window}>\n              <Routes>\n                <Route index element={<SingleNavigate to=\"bar\" />} />\n                <Route path=\"bar\" element={<h1>👋</h1>} />\n              </Routes>\n            </BrowserRouter>,\n          );\n        });\n\n        expect(window.location.href).toBe(\"http://localhost/foo/bar\");\n      });\n\n      test(\"allows non-root links to include trailing slashes\", () => {\n        let window = getWindow(\"/foo/\");\n        jest.spyOn(window.history, \"pushState\");\n\n        expect(window.location.href).toBe(\"http://localhost/foo/\");\n\n        act(() => {\n          ReactDOM.createRoot(node).render(\n            <BrowserRouter basename=\"/foo/\" window={window}>\n              <Routes>\n                <Route index element={<SingleNavigate to=\"bar/\" />} />\n                <Route path=\"bar\" element={<h1>👋</h1>} />\n              </Routes>\n            </BrowserRouter>,\n          );\n        });\n\n        expect(window.location.href).toBe(\"http://localhost/foo/bar/\");\n      });\n    });\n\n    describe(\"empty string paths\", () => {\n      it(\"should not add trailing slashes\", () => {\n        let window = getWindow(\"/foo/bar\");\n        jest.spyOn(window.history, \"pushState\");\n\n        expect(window.location.href).toBe(\"http://localhost/foo/bar\");\n\n        act(() => {\n          ReactDOM.createRoot(node).render(\n            <BrowserRouter basename=\"/foo\" window={window}>\n              <Routes>\n                <Route index element={<h1>👋</h1>} />\n                <Route path=\"bar\" element={<SingleNavigate to=\"\" />} />\n              </Routes>\n            </BrowserRouter>,\n          );\n        });\n\n        expect(window.location.href).toBe(\"http://localhost/foo/bar\");\n      });\n\n      it(\"should preserve trailing slash\", () => {\n        let window = getWindow(\"/foo/bar/\");\n        jest.spyOn(window.history, \"pushState\");\n\n        expect(window.location.href).toBe(\"http://localhost/foo/bar/\");\n\n        act(() => {\n          ReactDOM.createRoot(node).render(\n            <BrowserRouter basename=\"/foo\" window={window}>\n              <Routes>\n                <Route index element={<h1>👋</h1>} />\n                <Route path=\"bar\" element={<SingleNavigate to=\"\" />} />\n              </Routes>\n            </BrowserRouter>,\n          );\n        });\n\n        expect(window.location.href).toBe(\"http://localhost/foo/bar/\");\n      });\n    });\n\n    describe(\"current location '.' paths\", () => {\n      it(\"should not add trailing slash\", () => {\n        let window = getWindow(\"/foo/bar\");\n        jest.spyOn(window.history, \"pushState\");\n\n        expect(window.location.href).toBe(\"http://localhost/foo/bar\");\n\n        act(() => {\n          ReactDOM.createRoot(node).render(\n            <BrowserRouter basename=\"/foo/\" window={window}>\n              <Routes>\n                <Route index element={<h1>👋</h1>} />\n                <Route path=\"bar\" element={<SingleNavigate to=\".\" />} />\n              </Routes>\n            </BrowserRouter>,\n          );\n        });\n\n        expect(window.location.href).toBe(\"http://localhost/foo/bar\");\n      });\n\n      it(\"should preserve trailing slash\", () => {\n        let window = getWindow(\"/foo/bar/\");\n        jest.spyOn(window.history, \"pushState\");\n\n        expect(window.location.href).toBe(\"http://localhost/foo/bar/\");\n\n        act(() => {\n          ReactDOM.createRoot(node).render(\n            <BrowserRouter basename=\"/foo/\" window={window}>\n              <Routes>\n                <Route index element={<h1>👋</h1>} />\n                <Route path=\"bar\" element={<SingleNavigate to=\".\" />} />\n              </Routes>\n            </BrowserRouter>,\n          );\n        });\n\n        expect(window.location.href).toBe(\"http://localhost/foo/bar/\");\n      });\n    });\n  });\n\n  describe(\"when using setSearchParams\", () => {\n    it(\"should not include trailing slash via useSearchParams\", () => {\n      let window = getWindow(\"/foo\");\n      jest.spyOn(window.history, \"pushState\");\n\n      expect(window.location.href).toBe(\"http://localhost/foo\");\n\n      function SetSearchParams() {\n        let [, setSearchParams] = useSearchParams();\n        React.useEffect(\n          () => setSearchParams({ key: \"value\" }),\n          [setSearchParams],\n        );\n        return <h1>👋</h1>;\n      }\n\n      act(() => {\n        ReactDOM.createRoot(node).render(\n          <BrowserRouter basename=\"/foo\" window={window}>\n            <Routes>\n              <Route index element={<SetSearchParams />} />\n            </Routes>\n          </BrowserRouter>,\n        );\n      });\n\n      expect(window.location.href).toBe(\"http://localhost/foo?key=value\");\n    });\n\n    it(\"should include trailing slash via useSearchParams when basename has one\", () => {\n      let window = getWindow(\"/foo/\");\n      jest.spyOn(window.history, \"pushState\");\n\n      expect(window.location.href).toBe(\"http://localhost/foo/\");\n\n      function SetSearchParams() {\n        let [, setSearchParams] = useSearchParams();\n        React.useEffect(\n          () => setSearchParams({ key: \"value\" }),\n          [setSearchParams],\n        );\n        return <h1>👋</h1>;\n      }\n\n      act(() => {\n        ReactDOM.createRoot(node).render(\n          <BrowserRouter basename=\"/foo/\" window={window}>\n            <Routes>\n              <Route index element={<SetSearchParams />} />\n            </Routes>\n          </BrowserRouter>,\n        );\n      });\n\n      expect(window.location.href).toBe(\"http://localhost/foo/?key=value\");\n    });\n  });\n});\n\nfunction SingleNavigate({ to }: { to: To }) {\n  let navigate = useNavigate();\n  React.useEffect(() => {\n    navigate(to);\n  }, [navigate, to]);\n  return null;\n}\n"
  },
  {
    "path": "packages/react-router/__tests__/dom/use-blocker-test.tsx",
    "content": "import * as React from \"react\";\nimport * as ReactDOM from \"react-dom/client\";\nimport { act } from \"@testing-library/react\";\nimport type { Blocker, RouteObject } from \"../../index\";\nimport {\n  createMemoryRouter,\n  Link,\n  NavLink,\n  Outlet,\n  RouterProvider,\n  useBlocker,\n  useNavigate,\n} from \"../../index\";\n\ntype Router = ReturnType<typeof createMemoryRouter>;\n\nconst LOADER_LATENCY_MS = 200;\n\nasync function slowLoader() {\n  await sleep(LOADER_LATENCY_MS / 2);\n  return Response.json(null);\n}\n\ndescribe(\"navigation blocking with useBlocker\", () => {\n  let node: HTMLDivElement;\n  let router: Router;\n  let blocker: Blocker | null = null;\n  let root: ReactDOM.Root;\n\n  beforeEach(() => {\n    node = document.createElement(\"div\");\n    document.body.appendChild(node);\n  });\n\n  afterEach(() => {\n    document.body.removeChild(node);\n    node = null!;\n  });\n\n  it(\"initializes an 'unblocked' blocker\", async () => {\n    let initialEntries = [\"/\"];\n    let routes: RouteObject[] = [\n      {\n        path: \"/\",\n        element: React.createElement(() => {\n          let b = useBlocker(false);\n          blocker = b;\n          return null;\n        }),\n      },\n    ];\n    router = createMemoryRouter(routes, { initialEntries });\n    act(() => {\n      root = ReactDOM.createRoot(node);\n      root.render(<RouterProvider router={router} />);\n    });\n    expect(blocker).toEqual({\n      state: \"unblocked\",\n      proceed: undefined,\n      reset: undefined,\n    });\n    act(() => {\n      root.unmount();\n    });\n  });\n\n  it(\"strips basename from location provided to blocker function\", async () => {\n    let shouldBlock = jest.fn();\n    router = createMemoryRouter(\n      [\n        {\n          Component() {\n            useBlocker(shouldBlock);\n            return (\n              <div>\n                <Link to=\"/about\">About</Link>\n                <Outlet />\n              </div>\n            );\n          },\n          children: [\n            {\n              path: \"/\",\n              element: <h1>Home</h1>,\n            },\n            {\n              path: \"/about\",\n              element: <h1>About</h1>,\n            },\n          ],\n        },\n      ],\n      {\n        basename: \"/base\",\n        initialEntries: [\"/base\"],\n      },\n    );\n\n    act(() => {\n      root = ReactDOM.createRoot(node);\n      root.render(<RouterProvider router={router} />);\n    });\n\n    act(() => click(node.querySelector(\"a[href='/base/about']\")));\n\n    expect(router.state.location.pathname).toBe(\"/base/about\");\n    expect(shouldBlock).toHaveBeenCalledWith({\n      currentLocation: expect.objectContaining({ pathname: \"/\" }),\n      nextLocation: expect.objectContaining({ pathname: \"/about\" }),\n      historyAction: \"PUSH\",\n    });\n\n    act(() => root.unmount());\n  });\n\n  it(\"handles unstable blocker function identities\", async () => {\n    let count = 0;\n    router = createMemoryRouter([\n      {\n        element: React.createElement(() => {\n          // New function identity on each render\n          let b = useBlocker(() => false);\n          blocker = b;\n          if (++count > 50) {\n            throw new Error(\"useBlocker caused a re-render loop!\");\n          }\n          return (\n            <div>\n              <Link to=\"/about\">/about</Link>\n              <Outlet />\n            </div>\n          );\n        }),\n        children: [\n          {\n            path: \"/\",\n            element: <h1>Home</h1>,\n          },\n          {\n            path: \"/about\",\n            element: <h1>About</h1>,\n          },\n        ],\n      },\n    ]);\n\n    act(() => {\n      root = ReactDOM.createRoot(node);\n      root.render(<RouterProvider router={router} />);\n    });\n\n    expect(node.querySelector(\"h1\")?.textContent).toBe(\"Home\");\n\n    act(() => click(node.querySelector(\"a[href='/about']\")));\n    expect(node.querySelector(\"h1\")?.textContent).toBe(\"About\");\n\n    act(() => root.unmount());\n  });\n\n  it(\"handles reused blocker in a layout route\", async () => {\n    router = createMemoryRouter([\n      {\n        Component() {\n          let blocker = useBlocker(true);\n          return (\n            <div>\n              <Link to=\"/one\">/one</Link>\n              <Link to=\"/two\">/two</Link>\n              <Outlet />\n              <p>{blocker.state}</p>\n              {blocker.state === \"blocked\" ? (\n                <button onClick={() => blocker.proceed?.()}>Proceed</button>\n              ) : null}\n            </div>\n          );\n        },\n        children: [\n          {\n            path: \"/\",\n            element: <h1>Home</h1>,\n          },\n          {\n            path: \"/one\",\n            element: <h1>One</h1>,\n          },\n          {\n            path: \"/two\",\n            element: <h1>Two</h1>,\n          },\n        ],\n      },\n    ]);\n\n    act(() => {\n      root = ReactDOM.createRoot(node);\n      root.render(<RouterProvider router={router} />);\n    });\n\n    // Start on /\n    expect(node.querySelector(\"h1\")?.textContent).toBe(\"Home\");\n    expect(node.querySelector(\"p\")?.textContent).toBe(\"unblocked\");\n    expect(node.querySelector(\"button\")).toBeNull();\n\n    // Blocked navigation to /one\n    act(() => click(node.querySelector(\"a[href='/one']\")));\n    expect(node.querySelector(\"h1\")?.textContent).toBe(\"Home\");\n    expect(node.querySelector(\"p\")?.textContent).toBe(\"blocked\");\n    expect(node.querySelector(\"button\")?.textContent).toBe(\"Proceed\");\n\n    // Proceed to /one\n    act(() => click(node.querySelector(\"button\")));\n    expect(node.querySelector(\"h1\")?.textContent).toBe(\"One\");\n    expect(node.querySelector(\"p\")?.textContent).toBe(\"unblocked\");\n    expect(node.querySelector(\"button\")).toBeNull();\n\n    // Blocked navigation to /two\n    act(() => click(node.querySelector(\"a[href='/two']\")));\n    expect(node.querySelector(\"h1\")?.textContent).toBe(\"One\");\n    expect(node.querySelector(\"p\")?.textContent).toBe(\"blocked\");\n    expect(node.querySelector(\"button\")?.textContent).toBe(\"Proceed\");\n\n    // Proceed to /two\n    act(() => click(node.querySelector(\"button\")));\n    expect(node.querySelector(\"h1\")?.textContent).toBe(\"Two\");\n    expect(node.querySelector(\"p\")?.textContent).toBe(\"unblocked\");\n    expect(node.querySelector(\"button\")).toBeNull();\n\n    act(() => root.unmount());\n  });\n\n  describe(\"on <Link> navigation\", () => {\n    describe(\"blocker returns false\", () => {\n      beforeEach(() => {\n        let initialEntries = [\"/\"];\n        let initialIndex = 0;\n        router = createMemoryRouter(\n          [\n            {\n              element: React.createElement(() => {\n                let b = useBlocker(false);\n                blocker = b;\n                return (\n                  <div>\n                    <NavLink to=\"/\">Home</NavLink>\n                    <NavLink to=\"/about\">About</NavLink>\n                    <Outlet />\n                  </div>\n                );\n              }),\n              children: [\n                {\n                  path: \"/\",\n                  element: <h1>Home</h1>,\n                },\n                {\n                  path: \"/about\",\n                  element: <h1>About</h1>,\n                  loader: slowLoader,\n                },\n              ],\n            },\n          ],\n          {\n            initialEntries,\n            initialIndex,\n          },\n        );\n        act(() => {\n          root = ReactDOM.createRoot(node);\n          root.render(<RouterProvider router={router} />);\n        });\n      });\n\n      afterEach(() => {\n        act(() => root.unmount());\n      });\n\n      it(\"navigates\", async () => {\n        await act(async () => {\n          click(node.querySelector(\"a[href='/about']\"));\n          await sleep(LOADER_LATENCY_MS);\n        });\n        let h1 = node.querySelector(\"h1\");\n        expect(h1?.textContent).toBe(\"About\");\n      });\n\n      it(\"gets an 'unblocked' blocker after navigation starts\", async () => {\n        act(() => {\n          click(node.querySelector(\"a[href='/about']\"));\n        });\n        expect(blocker).toEqual({\n          state: \"unblocked\",\n          proceed: undefined,\n          reset: undefined,\n          location: undefined,\n        });\n      });\n\n      it(\"gets an 'unblocked' blocker after navigation completes\", async () => {\n        await act(async () => {\n          click(node.querySelector(\"a[href='/about']\"));\n          await sleep(LOADER_LATENCY_MS);\n        });\n        expect(blocker).toEqual({\n          state: \"unblocked\",\n          proceed: undefined,\n          reset: undefined,\n          location: undefined,\n        });\n      });\n    });\n\n    describe(\"blocker returns true\", () => {\n      beforeEach(() => {\n        let initialEntries = [\"/\"];\n        let initialIndex = 0;\n        router = createMemoryRouter(\n          [\n            {\n              element: React.createElement(() => {\n                let b = useBlocker(true);\n                blocker = b;\n                return (\n                  <div>\n                    <NavLink to=\"/\">Home</NavLink>\n                    <NavLink to=\"/about\">About</NavLink>\n                    <Outlet />\n                  </div>\n                );\n              }),\n              children: [\n                {\n                  path: \"/\",\n                  element: <h1>Home</h1>,\n                },\n                {\n                  path: \"/about\",\n                  element: <h1>About</h1>,\n                  loader: slowLoader,\n                },\n              ],\n            },\n          ],\n          {\n            initialEntries,\n            initialIndex,\n          },\n        );\n        act(() => {\n          root = ReactDOM.createRoot(node);\n          root.render(<RouterProvider router={router} />);\n        });\n      });\n\n      afterEach(() => {\n        act(() => root.unmount());\n      });\n\n      it(\"does not navigate\", async () => {\n        await act(async () => {\n          click(node.querySelector(\"a[href='/about']\"));\n          await sleep(LOADER_LATENCY_MS);\n        });\n        let h1 = node.querySelector(\"h1\");\n        expect(h1?.textContent).not.toBe(\"About\");\n      });\n\n      it(\"gets a 'blocked' blocker after navigation starts\", async () => {\n        act(() => {\n          click(node.querySelector(\"a[href='/about']\"));\n        });\n        expect(blocker).toEqual({\n          state: \"blocked\",\n          proceed: expect.any(Function),\n          reset: expect.any(Function),\n          location: expect.any(Object),\n        });\n      });\n\n      it(\"gets a 'blocked' blocker after navigation promise resolves\", async () => {\n        await act(async () => {\n          click(node.querySelector(\"a[href='/about']\"));\n          await sleep(LOADER_LATENCY_MS);\n        });\n        expect(blocker).toEqual({\n          state: \"blocked\",\n          proceed: expect.any(Function),\n          reset: expect.any(Function),\n          location: expect.any(Object),\n        });\n      });\n    });\n\n    describe(\"exiting from blocked state\", () => {\n      beforeEach(() => {\n        let initialEntries = [\"/\"];\n        let initialIndex = 0;\n        router = createMemoryRouter(\n          [\n            {\n              element: React.createElement(() => {\n                let b = useBlocker(true);\n                blocker = b;\n                return (\n                  <div>\n                    <NavLink to=\"/\">Home</NavLink>\n                    <NavLink to=\"/about\">About</NavLink>\n                    {b.state === \"blocked\" && (\n                      <div>\n                        <button data-action=\"proceed\" onClick={b.proceed}>\n                          Proceed\n                        </button>\n                        <button data-action=\"reset\" onClick={b.reset}>\n                          Reset\n                        </button>\n                      </div>\n                    )}\n                    <Outlet />\n                  </div>\n                );\n              }),\n              children: [\n                {\n                  path: \"/\",\n                  element: <h1>Home</h1>,\n                },\n                {\n                  path: \"/about\",\n                  element: <h1>About</h1>,\n                  loader: slowLoader,\n                },\n              ],\n            },\n          ],\n          {\n            initialEntries,\n            initialIndex,\n          },\n        );\n        act(() => {\n          root = ReactDOM.createRoot(node);\n          root.render(<RouterProvider router={router} />);\n        });\n      });\n\n      afterEach(() => {\n        act(() => root.unmount());\n      });\n\n      it(\"gets a 'proceeding' blocker after proceeding navigation starts\", async () => {\n        act(() => {\n          click(node.querySelector(\"a[href='/about']\"));\n        });\n        act(() => {\n          click(node.querySelector(\"[data-action='proceed']\"));\n        });\n        expect(blocker).toEqual({\n          state: \"proceeding\",\n          proceed: undefined,\n          reset: undefined,\n          location: expect.any(Object),\n        });\n      });\n\n      it(\"gets an 'unblocked' blocker after proceeding navigation completes\", async () => {\n        act(() => {\n          click(node.querySelector(\"a[href='/about']\"));\n        });\n        await act(async () => {\n          click(node.querySelector(\"[data-action='proceed']\"));\n          await sleep(LOADER_LATENCY_MS);\n        });\n        expect(blocker).toEqual({\n          state: \"unblocked\",\n          proceed: undefined,\n          reset: undefined,\n          location: undefined,\n        });\n      });\n\n      it(\"navigates after proceeding navigation completes\", async () => {\n        act(() => {\n          click(node.querySelector(\"a[href='/about']\"));\n        });\n        await act(async () => {\n          click(node.querySelector(\"[data-action='proceed']\"));\n          await sleep(LOADER_LATENCY_MS);\n        });\n        let h1 = node.querySelector(\"h1\");\n        expect(h1?.textContent).toBe(\"About\");\n      });\n\n      it(\"gets an 'unblocked' blocker after resetting navigation\", async () => {\n        act(() => {\n          click(node.querySelector(\"a[href='/about']\"));\n        });\n        act(() => {\n          click(node.querySelector(\"[data-action='reset']\"));\n        });\n        expect(blocker).toEqual({\n          state: \"unblocked\",\n          proceed: undefined,\n          reset: undefined,\n          location: undefined,\n        });\n      });\n\n      it(\"stays at current location after resetting\", async () => {\n        act(() => {\n          click(node.querySelector(\"a[href='/about']\"));\n        });\n        await act(async () => {\n          click(node.querySelector(\"[data-action='reset']\"));\n          // wait for '/about' loader so we catch failure if navigation proceeds\n          await sleep(LOADER_LATENCY_MS);\n        });\n        let h1 = node.querySelector(\"h1\");\n        expect(h1?.textContent).toBe(\"Home\");\n      });\n    });\n  });\n\n  describe(\"on <Link replace> navigation\", () => {\n    describe(\"blocker returns false\", () => {\n      beforeEach(() => {\n        let initialEntries = [\"/\"];\n        let initialIndex = 0;\n        router = createMemoryRouter(\n          [\n            {\n              element: React.createElement(() => {\n                let b = useBlocker(false);\n                blocker = b;\n                return (\n                  <div>\n                    <NavLink to=\"/\" replace>\n                      Home\n                    </NavLink>\n                    <NavLink to=\"/about\" replace>\n                      About\n                    </NavLink>\n                    <Outlet />\n                  </div>\n                );\n              }),\n              children: [\n                {\n                  path: \"/\",\n                  element: <h1>Home</h1>,\n                },\n                {\n                  path: \"/about\",\n                  element: <h1>About</h1>,\n                  loader: slowLoader,\n                },\n              ],\n            },\n          ],\n          {\n            initialEntries,\n            initialIndex,\n          },\n        );\n        act(() => {\n          root = ReactDOM.createRoot(node);\n          root.render(<RouterProvider router={router} />);\n        });\n      });\n\n      afterEach(() => {\n        act(() => root.unmount());\n      });\n\n      it(\"navigates\", async () => {\n        await act(async () => {\n          click(node.querySelector(\"a[href='/about']\"));\n          await sleep(LOADER_LATENCY_MS);\n        });\n        let h1 = node.querySelector(\"h1\");\n        expect(h1?.textContent).toBe(\"About\");\n      });\n\n      it(\"gets an 'unblocked' blocker after navigation starts\", async () => {\n        act(() => {\n          click(node.querySelector(\"a[href='/about']\"));\n        });\n        expect(blocker).toEqual({\n          state: \"unblocked\",\n          proceed: undefined,\n          reset: undefined,\n          location: undefined,\n        });\n      });\n\n      it(\"gets an 'unblocked' blocker after navigation completes\", async () => {\n        await act(async () => {\n          click(node.querySelector(\"a[href='/about']\"));\n          await sleep(LOADER_LATENCY_MS);\n        });\n        expect(blocker).toEqual({\n          state: \"unblocked\",\n          proceed: undefined,\n          reset: undefined,\n          location: undefined,\n        });\n      });\n    });\n\n    describe(\"blocker returns true\", () => {\n      beforeEach(() => {\n        let initialEntries = [\"/\"];\n        let initialIndex = 0;\n        router = createMemoryRouter(\n          [\n            {\n              element: React.createElement(() => {\n                let b = useBlocker(true);\n                blocker = b;\n                return (\n                  <div>\n                    <NavLink to=\"/\" replace>\n                      Home\n                    </NavLink>\n                    <NavLink to=\"/about\" replace>\n                      About\n                    </NavLink>\n                    <Outlet />\n                  </div>\n                );\n              }),\n              children: [\n                {\n                  path: \"/\",\n                  element: <h1>Home</h1>,\n                },\n                {\n                  path: \"/about\",\n                  element: <h1>About</h1>,\n                  loader: slowLoader,\n                },\n              ],\n            },\n          ],\n          {\n            initialEntries,\n            initialIndex,\n          },\n        );\n        act(() => {\n          root = ReactDOM.createRoot(node);\n          root.render(<RouterProvider router={router} />);\n        });\n      });\n\n      afterEach(() => {\n        act(() => root.unmount());\n      });\n\n      it(\"does not navigate\", async () => {\n        await act(async () => {\n          click(node.querySelector(\"a[href='/about']\"));\n          await sleep(LOADER_LATENCY_MS);\n        });\n        let h1 = node.querySelector(\"h1\");\n        expect(h1?.textContent).not.toBe(\"About\");\n      });\n\n      it(\"gets a 'blocked' blocker after navigation starts\", async () => {\n        act(() => {\n          click(node.querySelector(\"a[href='/about']\"));\n        });\n        expect(blocker).toEqual({\n          state: \"blocked\",\n          proceed: expect.any(Function),\n          reset: expect.any(Function),\n          location: expect.any(Object),\n        });\n      });\n\n      it(\"gets a 'blocked' blocker after navigation promise resolves\", async () => {\n        await act(async () => {\n          click(node.querySelector(\"a[href='/about']\"));\n          await sleep(LOADER_LATENCY_MS);\n        });\n        expect(blocker).toEqual({\n          state: \"blocked\",\n          proceed: expect.any(Function),\n          reset: expect.any(Function),\n          location: expect.any(Object),\n        });\n      });\n    });\n\n    describe(\"exiting from blocked state\", () => {\n      beforeEach(() => {\n        let initialEntries = [\"/\"];\n        let initialIndex = 0;\n        router = createMemoryRouter(\n          [\n            {\n              element: React.createElement(() => {\n                let b = useBlocker(true);\n                blocker = b;\n                return (\n                  <div>\n                    <NavLink to=\"/\" replace>\n                      Home\n                    </NavLink>\n                    <NavLink to=\"/about\" replace>\n                      About\n                    </NavLink>\n                    {b.state === \"blocked\" && (\n                      <div>\n                        <button data-action=\"proceed\" onClick={b.proceed}>\n                          Proceed\n                        </button>\n                        <button data-action=\"reset\" onClick={b.reset}>\n                          Reset\n                        </button>\n                      </div>\n                    )}\n                    <Outlet />\n                  </div>\n                );\n              }),\n              children: [\n                {\n                  path: \"/\",\n                  element: <h1>Home</h1>,\n                },\n                {\n                  path: \"/about\",\n                  element: <h1>About</h1>,\n                  loader: slowLoader,\n                },\n              ],\n            },\n          ],\n          {\n            initialEntries,\n            initialIndex,\n          },\n        );\n        act(() => {\n          root = ReactDOM.createRoot(node);\n          root.render(<RouterProvider router={router} />);\n        });\n      });\n\n      afterEach(() => {\n        act(() => root.unmount());\n      });\n\n      it(\"gets a 'proceeding' blocker after proceeding navigation starts\", async () => {\n        act(() => {\n          click(node.querySelector(\"a[href='/about']\"));\n        });\n        act(() => {\n          click(node.querySelector(\"[data-action='proceed']\"));\n        });\n        expect(blocker).toEqual({\n          state: \"proceeding\",\n          proceed: undefined,\n          reset: undefined,\n          location: expect.any(Object),\n        });\n      });\n\n      it(\"gets an 'unblocked' blocker after proceeding navigation completes\", async () => {\n        act(() => {\n          click(node.querySelector(\"a[href='/about']\"));\n        });\n        await act(async () => {\n          click(node.querySelector(\"[data-action='proceed']\"));\n          await sleep(LOADER_LATENCY_MS);\n        });\n        expect(blocker).toEqual({\n          state: \"unblocked\",\n          proceed: undefined,\n          reset: undefined,\n          location: undefined,\n        });\n      });\n\n      it(\"navigates after proceeding navigation completes\", async () => {\n        act(() => {\n          click(node.querySelector(\"a[href='/about']\"));\n        });\n        await act(async () => {\n          click(node.querySelector(\"[data-action='proceed']\"));\n          await sleep(LOADER_LATENCY_MS);\n        });\n        let h1 = node.querySelector(\"h1\");\n        expect(h1?.textContent).toBe(\"About\");\n      });\n\n      it(\"gets an 'unblocked' blocker after resetting navigation\", async () => {\n        act(() => {\n          click(node.querySelector(\"a[href='/about']\"));\n        });\n        act(() => {\n          click(node.querySelector(\"[data-action='reset']\"));\n        });\n        expect(blocker).toEqual({\n          state: \"unblocked\",\n          proceed: undefined,\n          reset: undefined,\n          location: undefined,\n        });\n      });\n\n      it(\"stays at current location after resetting\", async () => {\n        act(() => {\n          click(node.querySelector(\"a[href='/about']\"));\n        });\n        await act(async () => {\n          click(node.querySelector(\"[data-action='reset']\"));\n          // wait for '/about' loader so we catch failure if navigation proceeds\n          await sleep(LOADER_LATENCY_MS);\n        });\n        let h1 = node.querySelector(\"h1\");\n        expect(h1?.textContent).toBe(\"Home\");\n      });\n    });\n  });\n\n  describe(\"on POP navigation\", () => {\n    describe(\"blocker returns false\", () => {\n      beforeEach(() => {\n        let initialEntries = [\"/\", \"/about\", \"/contact\"];\n        let initialIndex = 2;\n        router = createMemoryRouter(\n          [\n            {\n              element: React.createElement(() => {\n                let b = useBlocker(false);\n                let navigate = useNavigate();\n                blocker = b;\n                return (\n                  <div>\n                    <NavLink to=\"/\" replace>\n                      Home\n                    </NavLink>\n                    <NavLink to=\"/about\" replace>\n                      About\n                    </NavLink>\n                    <button data-action=\"back\" onClick={() => navigate(-1)}>\n                      Go Back\n                    </button>\n                    <Outlet />\n                  </div>\n                );\n              }),\n              children: [\n                {\n                  path: \"/\",\n                  element: <h1>Home</h1>,\n                },\n                {\n                  path: \"/about\",\n                  element: <h1>About</h1>,\n                  loader: slowLoader,\n                },\n                {\n                  path: \"/contact\",\n                  element: <h1>Contact</h1>,\n                },\n              ],\n            },\n          ],\n          {\n            initialEntries,\n            initialIndex,\n          },\n        );\n        act(() => {\n          root = ReactDOM.createRoot(node);\n          root.render(<RouterProvider router={router} />);\n        });\n      });\n\n      afterEach(() => {\n        act(() => root.unmount());\n      });\n\n      it(\"navigates\", async () => {\n        await act(async () => {\n          click(node.querySelector(\"[data-action='back']\"));\n          await sleep(LOADER_LATENCY_MS);\n        });\n        let h1 = node.querySelector(\"h1\");\n        expect(h1?.textContent).toBe(\"About\");\n      });\n\n      it(\"gets an 'unblocked' blocker after navigation starts\", async () => {\n        act(() => {\n          click(node.querySelector(\"[data-action='back']\"));\n        });\n        expect(blocker).toEqual({\n          state: \"unblocked\",\n          proceed: undefined,\n          reset: undefined,\n          location: undefined,\n        });\n      });\n\n      it(\"gets an 'unblocked' blocker after navigation completes\", async () => {\n        await act(async () => {\n          click(node.querySelector(\"[data-action='back']\"));\n          await sleep(LOADER_LATENCY_MS);\n        });\n        expect(blocker).toEqual({\n          state: \"unblocked\",\n          proceed: undefined,\n          reset: undefined,\n          location: undefined,\n        });\n      });\n    });\n\n    describe(\"blocker returns true\", () => {\n      beforeEach(() => {\n        let initialEntries = [\"/\", \"/about\", \"/contact\"];\n        let initialIndex = 2;\n        router = createMemoryRouter(\n          [\n            {\n              element: React.createElement(() => {\n                let b = useBlocker(true);\n                let navigate = useNavigate();\n                blocker = b;\n                return (\n                  <div>\n                    <NavLink to=\"/\" replace>\n                      Home\n                    </NavLink>\n                    <NavLink to=\"/about\" replace>\n                      About\n                    </NavLink>\n                    <button data-action=\"back\" onClick={() => navigate(-1)}>\n                      Go Back\n                    </button>\n                    <Outlet />\n                  </div>\n                );\n              }),\n              children: [\n                {\n                  path: \"/\",\n                  element: <h1>Home</h1>,\n                },\n                {\n                  path: \"/about\",\n                  element: <h1>About</h1>,\n                  loader: slowLoader,\n                },\n                {\n                  path: \"/contact\",\n                  element: <h1>Contact</h1>,\n                },\n              ],\n            },\n          ],\n          {\n            initialEntries,\n            initialIndex,\n          },\n        );\n        act(() => {\n          root = ReactDOM.createRoot(node);\n          root.render(<RouterProvider router={router} />);\n        });\n      });\n\n      afterEach(() => {\n        act(() => root.unmount());\n      });\n\n      it(\"does not navigate\", async () => {\n        await act(async () => {\n          click(node.querySelector(\"[data-action='back']\"));\n          await sleep(LOADER_LATENCY_MS);\n        });\n        let h1 = node.querySelector(\"h1\");\n        expect(h1?.textContent).not.toBe(\"About\");\n      });\n\n      it(\"gets a 'blocked' blocker after navigation starts\", async () => {\n        act(() => {\n          click(node.querySelector(\"[data-action='back']\"));\n        });\n        expect(blocker).toEqual({\n          state: \"blocked\",\n          proceed: expect.any(Function),\n          reset: expect.any(Function),\n          location: expect.any(Object),\n        });\n      });\n\n      it(\"gets a 'blocked' blocker after navigation promise resolves\", async () => {\n        await act(async () => {\n          click(node.querySelector(\"[data-action='back']\"));\n          await sleep(LOADER_LATENCY_MS);\n        });\n        expect(blocker).toEqual({\n          state: \"blocked\",\n          proceed: expect.any(Function),\n          reset: expect.any(Function),\n          location: expect.any(Object),\n        });\n      });\n    });\n\n    describe(\"exiting from blocked state\", () => {\n      beforeEach(() => {\n        let initialEntries = [\"/\", \"/about\", \"/contact\"];\n        let initialIndex = 2;\n        router = createMemoryRouter(\n          [\n            {\n              element: React.createElement(() => {\n                let b = useBlocker(true);\n                let navigate = useNavigate();\n                blocker = b;\n                return (\n                  <div>\n                    <NavLink to=\"/\" replace>\n                      Home\n                    </NavLink>\n                    <NavLink to=\"/about\" replace>\n                      About\n                    </NavLink>\n                    <button data-action=\"back\" onClick={() => navigate(-1)}>\n                      Go Back\n                    </button>\n                    {b.state === \"blocked\" && (\n                      <div>\n                        <button data-action=\"proceed\" onClick={b.proceed}>\n                          Proceed\n                        </button>\n                        <button data-action=\"reset\" onClick={b.reset}>\n                          Reset\n                        </button>\n                      </div>\n                    )}\n                    <Outlet />\n                  </div>\n                );\n              }),\n              children: [\n                {\n                  path: \"/\",\n                  element: <h1>Home</h1>,\n                },\n                {\n                  path: \"/about\",\n                  element: <h1>About</h1>,\n                  loader: slowLoader,\n                },\n                {\n                  path: \"/contact\",\n                  element: <h1>Contact</h1>,\n                },\n              ],\n            },\n          ],\n          {\n            initialEntries,\n            initialIndex,\n          },\n        );\n        act(() => {\n          root = ReactDOM.createRoot(node);\n          root.render(<RouterProvider router={router} />);\n        });\n      });\n\n      afterEach(() => {\n        act(() => root.unmount());\n      });\n\n      it(\"gets a 'proceeding' blocker after proceeding navigation starts\", async () => {\n        act(() => {\n          click(node.querySelector(\"[data-action='back']\"));\n        });\n        expect(node.innerHTML).toContain(\"<h1>Contact</h1>\");\n        await act(async () => {\n          click(node.querySelector(\"[data-action='proceed']\"));\n          expect([...router.state.blockers.values()][0]).toEqual({\n            state: \"proceeding\",\n            proceed: undefined,\n            reset: undefined,\n            location: expect.any(Object),\n          });\n          await sleep(LOADER_LATENCY_MS);\n        });\n        expect(node.innerHTML).toContain(\"<h1>About</h1>\");\n        expect(blocker).toEqual({\n          state: \"unblocked\",\n          proceed: undefined,\n          reset: undefined,\n          location: undefined,\n        });\n      });\n\n      it(\"gets an 'unblocked' blocker after proceeding navigation completes\", async () => {\n        act(() => {\n          click(node.querySelector(\"[data-action='back']\"));\n        });\n        await act(async () => {\n          click(node.querySelector(\"[data-action='proceed']\"));\n          await sleep(LOADER_LATENCY_MS);\n        });\n        expect(blocker).toEqual({\n          state: \"unblocked\",\n          proceed: undefined,\n          reset: undefined,\n          location: undefined,\n        });\n      });\n\n      it(\"navigates after proceeding navigation completes\", async () => {\n        act(() => {\n          click(node.querySelector(\"[data-action='back']\"));\n        });\n        await act(async () => {\n          click(node.querySelector(\"[data-action='proceed']\"));\n          await sleep(LOADER_LATENCY_MS);\n        });\n        let h1 = node.querySelector(\"h1\");\n        expect(h1?.textContent).toBe(\"About\");\n      });\n\n      it(\"gets an 'unblocked' blocker after resetting navigation\", async () => {\n        act(() => {\n          click(node.querySelector(\"[data-action='back']\"));\n        });\n        act(() => {\n          click(node.querySelector(\"[data-action='reset']\"));\n        });\n        expect(blocker).toEqual({\n          state: \"unblocked\",\n          proceed: undefined,\n          reset: undefined,\n          location: undefined,\n        });\n      });\n\n      it(\"stays at current location after resetting\", async () => {\n        act(() => {\n          click(node.querySelector(\"[data-action='back']\"));\n        });\n        await act(async () => {\n          click(node.querySelector(\"[data-action='reset']\"));\n          // wait for '/about' loader so we catch failure if navigation proceeds\n          await sleep(LOADER_LATENCY_MS);\n        });\n        let h1 = node.querySelector(\"h1\");\n        expect(h1?.textContent).toBe(\"Contact\");\n      });\n    });\n  });\n});\n\nfunction sleep(n: number = 500) {\n  return new Promise<void>((r) => setTimeout(r, n));\n}\n\nfunction click(target: EventTarget | null | undefined) {\n  target?.dispatchEvent(\n    new MouseEvent(\"click\", {\n      view: window,\n      bubbles: true,\n      cancelable: true,\n    }),\n  );\n}\n"
  },
  {
    "path": "packages/react-router/__tests__/dom/use-prompt-test.tsx",
    "content": "import * as React from \"react\";\nimport \"@testing-library/jest-dom\";\nimport { fireEvent, render, screen, waitFor } from \"@testing-library/react\";\nimport {\n  Link,\n  RouterProvider,\n  createBrowserRouter,\n  unstable_usePrompt as usePrompt,\n} from \"../../index\";\nimport getWindow from \"../utils/getWindow\";\n\ndescribe(\"usePrompt\", () => {\n  afterEach(() => {\n    jest.clearAllMocks();\n  });\n\n  describe(\"when navigation is blocked\", () => {\n    it(\"shows window.confirm and blocks navigation when it returns false\", async () => {\n      let testWindow = getWindow(\"/\");\n      const windowConfirmMock = jest\n        .spyOn(window, \"confirm\")\n        .mockImplementationOnce(() => false);\n\n      let router = createBrowserRouter(\n        [\n          {\n            path: \"/\",\n            Component() {\n              usePrompt({ when: true, message: \"Are you sure??\" });\n              return <Link to=\"/arbitrary\">Navigate</Link>;\n            },\n          },\n          {\n            path: \"/arbitrary\",\n            Component: () => <h1>Arbitrary</h1>,\n          },\n        ],\n        { window: testWindow },\n      );\n\n      render(<RouterProvider router={router} />);\n      expect(screen.getByText(\"Navigate\")).toBeInTheDocument();\n\n      fireEvent.click(screen.getByText(\"Navigate\"));\n      await new Promise((r) => setTimeout(r, 0));\n\n      expect(windowConfirmMock).toHaveBeenNthCalledWith(1, \"Are you sure??\");\n      expect(screen.getByText(\"Navigate\")).toBeInTheDocument();\n    });\n\n    it(\"shows window.confirm and navigates when it returns true\", async () => {\n      let testWindow = getWindow(\"/\");\n      const windowConfirmMock = jest\n        .spyOn(window, \"confirm\")\n        .mockImplementationOnce(() => true);\n\n      let router = createBrowserRouter(\n        [\n          {\n            path: \"/\",\n            Component() {\n              usePrompt({ when: true, message: \"Are you sure??\" });\n              return <Link to=\"/arbitrary\">Navigate</Link>;\n            },\n          },\n          {\n            path: \"/arbitrary\",\n            Component: () => <h1>Arbitrary</h1>,\n          },\n        ],\n        { window: testWindow },\n      );\n\n      render(<RouterProvider router={router} />);\n      expect(screen.getByText(\"Navigate\")).toBeInTheDocument();\n\n      fireEvent.click(screen.getByText(\"Navigate\"));\n      await waitFor(() => screen.getByText(\"Arbitrary\"));\n\n      expect(windowConfirmMock).toHaveBeenNthCalledWith(1, \"Are you sure??\");\n      expect(screen.getByText(\"Arbitrary\")).toBeInTheDocument();\n    });\n  });\n\n  describe(\"when navigation is not blocked\", () => {\n    it(\"navigates without showing window.confirm\", async () => {\n      let testWindow = getWindow(\"/\");\n      const windowConfirmMock = jest\n        .spyOn(window, \"confirm\")\n        .mockImplementation(() => true);\n\n      let router = createBrowserRouter(\n        [\n          {\n            path: \"/\",\n            Component() {\n              usePrompt({ when: false, message: \"Are you sure??\" });\n              return <Link to=\"/arbitrary\">Navigate</Link>;\n            },\n          },\n          {\n            path: \"/arbitrary\",\n            Component: () => <h1>Arbitrary</h1>,\n          },\n        ],\n        { window: testWindow },\n      );\n\n      render(<RouterProvider router={router} />);\n      expect(screen.getByText(\"Navigate\")).toBeInTheDocument();\n\n      fireEvent.click(screen.getByText(\"Navigate\"));\n      await waitFor(() => screen.getByText(\"Arbitrary\"));\n\n      expect(windowConfirmMock).not.toHaveBeenCalled();\n      expect(screen.getByText(\"Arbitrary\")).toBeInTheDocument();\n    });\n  });\n});\n"
  },
  {
    "path": "packages/react-router/__tests__/dom/useLinkClickHandler-test.tsx",
    "content": "import * as React from \"react\";\nimport * as ReactDOM from \"react-dom/client\";\nimport { act } from \"@testing-library/react\";\nimport type { LinkProps } from \"../../index\";\nimport {\n  MemoryRouter,\n  Routes,\n  Route,\n  useHref,\n  useLinkClickHandler,\n} from \"../../index\";\n\nfunction CustomLink({ to, replace, state, target, ...rest }: LinkProps) {\n  let href = useHref(to);\n  let handleClick = useLinkClickHandler(to, { target, replace, state });\n  return (\n    // eslint-disable-next-line jsx-a11y/anchor-has-content\n    <a {...rest} href={href} onClick={handleClick} target={target} />\n  );\n}\n\ndescribe(\"Custom link with useLinkClickHandler\", () => {\n  let node: HTMLDivElement;\n  beforeEach(() => {\n    node = document.createElement(\"div\");\n    document.body.appendChild(node);\n  });\n\n  afterEach(() => {\n    document.body.removeChild(node);\n    node = null!;\n  });\n\n  it(\"navigates to the new page\", () => {\n    act(() => {\n      ReactDOM.createRoot(node).render(\n        <MemoryRouter initialEntries={[\"/home\"]}>\n          <Routes>\n            <Route\n              path=\"home\"\n              element={\n                <div>\n                  <h1>Home</h1>\n                  <CustomLink to=\"../about\">About</CustomLink>\n                </div>\n              }\n            />\n            <Route path=\"about\" element={<h1>About</h1>} />\n          </Routes>\n        </MemoryRouter>,\n      );\n    });\n\n    let anchor = node.querySelector(\"a\");\n    expect(anchor).not.toBeNull();\n\n    act(() => {\n      anchor?.dispatchEvent(\n        new MouseEvent(\"click\", {\n          view: window,\n          bubbles: true,\n          cancelable: true,\n        }),\n      );\n    });\n\n    let h1 = node.querySelector(\"h1\");\n    expect(h1).not.toBeNull();\n    expect(h1?.textContent).toEqual(\"About\");\n  });\n\n  describe(\"with a right click\", () => {\n    it(\"stays on the same page\", () => {\n      act(() => {\n        ReactDOM.createRoot(node).render(\n          <MemoryRouter initialEntries={[\"/home\"]}>\n            <Routes>\n              <Route\n                path=\"home\"\n                element={\n                  <div>\n                    <h1>Home</h1>\n                    <CustomLink to=\"../about\">About</CustomLink>\n                  </div>\n                }\n              />\n              <Route path=\"about\" element={<h1>About</h1>} />\n            </Routes>\n          </MemoryRouter>,\n        );\n      });\n\n      let anchor = node.querySelector(\"a\");\n      expect(anchor).not.toBeNull();\n\n      act(() => {\n        // https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/button\n        let RightMouseButton = 2;\n        anchor?.dispatchEvent(\n          new MouseEvent(\"click\", {\n            view: window,\n            bubbles: true,\n            cancelable: true,\n            button: RightMouseButton,\n          }),\n        );\n      });\n\n      let h1 = node.querySelector(\"h1\");\n      expect(h1).not.toBeNull();\n      expect(h1?.textContent).toEqual(\"Home\");\n    });\n  });\n\n  describe(\"when the link is supposed to open in a new window\", () => {\n    it(\"stays on the same page\", () => {\n      act(() => {\n        ReactDOM.createRoot(node).render(\n          <MemoryRouter initialEntries={[\"/home\"]}>\n            <Routes>\n              <Route\n                path=\"home\"\n                element={\n                  <div>\n                    <h1>Home</h1>\n                    <CustomLink to=\"../about\" target=\"_blank\">\n                      About\n                    </CustomLink>\n                  </div>\n                }\n              />\n              <Route path=\"about\" element={<h1>About</h1>} />\n            </Routes>\n          </MemoryRouter>,\n        );\n      });\n\n      let anchor = node.querySelector(\"a\");\n      expect(anchor).not.toBeNull();\n\n      act(() => {\n        anchor?.dispatchEvent(\n          new MouseEvent(\"click\", {\n            view: window,\n            bubbles: true,\n            cancelable: true,\n          }),\n        );\n      });\n\n      let h1 = node.querySelector(\"h1\");\n      expect(h1).not.toBeNull();\n      expect(h1?.textContent).toEqual(\"Home\");\n    });\n  });\n\n  describe(\"when the modifier keys are used\", () => {\n    it(\"stays on the same page\", () => {\n      act(() => {\n        ReactDOM.createRoot(node).render(\n          <MemoryRouter initialEntries={[\"/home\"]}>\n            <Routes>\n              <Route\n                path=\"home\"\n                element={\n                  <div>\n                    <h1>Home</h1>\n                    <CustomLink to=\"../about\">About</CustomLink>\n                  </div>\n                }\n              />\n              <Route path=\"about\" element={<h1>About</h1>} />\n            </Routes>\n          </MemoryRouter>,\n        );\n      });\n\n      let anchor = node.querySelector(\"a\");\n      expect(anchor).not.toBeNull();\n\n      act(() => {\n        anchor?.dispatchEvent(\n          new MouseEvent(\"click\", {\n            view: window,\n            bubbles: true,\n            cancelable: true,\n            // The Ctrl key is pressed\n            ctrlKey: true,\n          }),\n        );\n      });\n\n      let h1 = node.querySelector(\"h1\");\n      expect(h1).not.toBeNull();\n      expect(h1?.textContent).toEqual(\"Home\");\n    });\n  });\n});\n"
  },
  {
    "path": "packages/react-router/__tests__/generatePath-test.tsx",
    "content": "import { generatePath } from \"react-router\";\n\ndescribe(\"generatePath\", () => {\n  describe(\"with no params\", () => {\n    it(\"returns the unmodified path\", () => {\n      expect(generatePath(\"/\")).toBe(\"/\");\n      expect(generatePath(\"/courses\")).toBe(\"/courses\");\n    });\n  });\n\n  describe(\"with params\", () => {\n    it(\"returns the path without those params interpolated\", () => {\n      expect(generatePath(\"/courses/:id\", { id: \"routing\" })).toBe(\n        \"/courses/routing\",\n      );\n      expect(\n        generatePath(\"/courses/:id/student/:studentId\", {\n          id: \"routing\",\n          studentId: \"matt\",\n        }),\n      ).toBe(\"/courses/routing/student/matt\");\n      expect(generatePath(\"/courses/*\", { \"*\": \"routing/grades\" })).toBe(\n        \"/courses/routing/grades\",\n      );\n      expect(generatePath(\"*\", { \"*\": \"routing/grades\" })).toBe(\n        \"routing/grades\",\n      );\n      expect(generatePath(\"/*\", {})).toBe(\"/\");\n    });\n    it(\"handles * in parameter values\", () => {\n      expect(generatePath(\"/courses/:name\", { name: \"foo*\" })).toBe(\n        \"/courses/foo*\",\n      );\n      expect(generatePath(\"/courses/:name\", { name: \"*foo\" })).toBe(\n        \"/courses/*foo\",\n      );\n      expect(generatePath(\"/courses/:name\", { name: \"*f*oo*\" })).toBe(\n        \"/courses/*f*oo*\",\n      );\n      expect(\n        generatePath(\"/courses/:name\", {\n          name: \"foo*\",\n          \"*\": \"splat_should_not_be_added\",\n        }),\n      ).toBe(\"/courses/foo*\");\n    });\n    it(\"handles a 0 parameter\", () => {\n      // incorrect usage but worked in 6.3.0 so keep it to avoid the regression\n      expect(generatePath(\"/courses/:id\", { id: 0 })).toBe(\"/courses/0\");\n      // incorrect usage but worked in 6.3.0 so keep it to avoid the regression\n      expect(generatePath(\"/courses/*\", { \"*\": 0 })).toBe(\"/courses/0\");\n    });\n\n    it(\"handles dashes in dynamic params\", () => {\n      expect(generatePath(\"/courses/:foo-bar\", { \"foo-bar\": \"baz\" })).toBe(\n        \"/courses/baz\",\n      );\n    });\n  });\n\n  describe(\"with extraneous params\", () => {\n    it(\"ignores them\", () => {\n      expect(generatePath(\"/\", { course: \"routing\" })).toBe(\"/\");\n      expect(generatePath(\"/courses\", { course: \"routing\" })).toBe(\"/courses\");\n    });\n  });\n\n  describe(\"with missing params\", () => {\n    it(\"throws an error\", () => {\n      expect(() => {\n        generatePath(\"/:lang/login\", {});\n      }).toThrow(/Missing \":lang\" param/);\n    });\n  });\n\n  describe(\"with a missing splat\", () => {\n    it(\"omits the splat and trims the trailing slash\", () => {\n      expect(generatePath(\"/courses/*\", {})).toBe(\"/courses\");\n    });\n  });\n\n  describe(\"with optional params\", () => {\n    it(\"adds optional dynamic params where appropriate\", () => {\n      let path = \"/:one?/:two?/:three?\";\n      expect(generatePath(path, { one: \"uno\" })).toBe(\"/uno\");\n      expect(generatePath(path, { one: \"uno\", two: \"dos\" })).toBe(\"/uno/dos\");\n      expect(\n        generatePath(path, {\n          one: \"uno\",\n          two: \"dos\",\n          three: \"tres\",\n        }),\n      ).toBe(\"/uno/dos/tres\");\n      expect(generatePath(path, { one: \"uno\", three: \"tres\" })).toBe(\n        \"/uno/tres\",\n      );\n      expect(generatePath(path, { two: \"dos\" })).toBe(\"/dos\");\n      expect(generatePath(path, { two: \"dos\", three: \"tres\" })).toBe(\n        \"/dos/tres\",\n      );\n    });\n\n    it(\"strips optional aspects of static segments\", () => {\n      expect(generatePath(\"/one?/two?/:three?\", {})).toBe(\"/one/two\");\n      expect(generatePath(\"/one?/two?/:three?\", { three: \"tres\" })).toBe(\n        \"/one/two/tres\",\n      );\n    });\n\n    it(\"handles intermixed segments\", () => {\n      let path = \"/one?/:two?/three/:four/*\";\n      expect(generatePath(path, { four: \"cuatro\" })).toBe(\"/one/three/cuatro\");\n      expect(\n        generatePath(path, {\n          two: \"dos\",\n          four: \"cuatro\",\n        }),\n      ).toBe(\"/one/dos/three/cuatro\");\n      expect(\n        generatePath(path, {\n          two: \"dos\",\n          four: \"cuatro\",\n          \"*\": \"splat\",\n        }),\n      ).toBe(\"/one/dos/three/cuatro/splat\");\n      expect(\n        generatePath(path, {\n          two: \"dos\",\n          four: \"cuatro\",\n          \"*\": \"splat/and/then/some\",\n        }),\n      ).toBe(\"/one/dos/three/cuatro/splat/and/then/some\");\n    });\n  });\n\n  describe(\"with a param that contains a /\", () => {\n    it(\"properly encodes the slash\", () => {\n      expect(generatePath(\"/courses/:id/grades\", { id: \"a/b\" })).toBe(\n        \"/courses/a%2Fb/grades\",\n      );\n    });\n  });\n\n  it(\"throws only on on missing named parameters, but not missing splat params\", () => {\n    expect(() => generatePath(\":foo\")).toThrow();\n    expect(() => generatePath(\"/:foo\")).toThrow();\n    expect(() => generatePath(\"*\")).not.toThrow();\n    expect(() => generatePath(\"/*\")).not.toThrow();\n  });\n\n  it(\"only interpolates and does not add slashes\", () => {\n    let consoleWarn = jest.spyOn(console, \"warn\").mockImplementation(() => {});\n\n    expect(generatePath(\"*\")).toBe(\"\");\n    expect(generatePath(\"/*\")).toBe(\"/\");\n\n    expect(generatePath(\"foo*\")).toBe(\"foo\");\n    expect(generatePath(\"/foo*\")).toBe(\"/foo\");\n\n    expect(generatePath(\":foo\", { foo: \"bar\" })).toBe(\"bar\");\n    expect(generatePath(\"/:foo\", { foo: \"bar\" })).toBe(\"/bar\");\n\n    expect(generatePath(\"*\", { \"*\": \"bar\" })).toBe(\"bar\");\n    expect(generatePath(\"/*\", { \"*\": \"bar\" })).toBe(\"/bar\");\n\n    // No support for partial dynamic params\n    expect(generatePath(\"foo:bar\", { bar: \"baz\" })).toBe(\"foo:bar\");\n    expect(generatePath(\"/foo:bar\", { bar: \"baz\" })).toBe(\"/foo:bar\");\n\n    // Partial splats are treated as independent path segments\n    expect(generatePath(\"foo*\", { \"*\": \"bar\" })).toBe(\"foo/bar\");\n    expect(generatePath(\"/foo*\", { \"*\": \"bar\" })).toBe(\"/foo/bar\");\n\n    // Ensure we warn on partial splat usages\n    expect(consoleWarn.mock.calls).toMatchInlineSnapshot(`\n      [\n        [\n          \"Route path \"foo*\" will be treated as if it were \"foo/*\" because the \\`*\\` character must always follow a \\`/\\` in the pattern. To get rid of this warning, please change the route path to \"foo/*\".\",\n        ],\n        [\n          \"Route path \"/foo*\" will be treated as if it were \"/foo/*\" because the \\`*\\` character must always follow a \\`/\\` in the pattern. To get rid of this warning, please change the route path to \"/foo/*\".\",\n        ],\n        [\n          \"Route path \"foo*\" will be treated as if it were \"foo/*\" because the \\`*\\` character must always follow a \\`/\\` in the pattern. To get rid of this warning, please change the route path to \"foo/*\".\",\n        ],\n        [\n          \"Route path \"/foo*\" will be treated as if it were \"/foo/*\" because the \\`*\\` character must always follow a \\`/\\` in the pattern. To get rid of this warning, please change the route path to \"/foo/*\".\",\n        ],\n      ]\n    `);\n\n    consoleWarn.mockRestore();\n  });\n\n  describe(\"with params followed by static text\", () => {\n    it(\"interpolates params with file extensions\", () => {\n      expect(generatePath(\"/books/:id.json\", { id: \"42\" })).toBe(\n        \"/books/42.json\",\n      );\n      expect(generatePath(\"/api/:resource.xml\", { resource: \"users\" })).toBe(\n        \"/api/users.xml\",\n      );\n      expect(generatePath(\"/:lang.html\", { lang: \"en\" })).toBe(\"/en.html\");\n    });\n\n    it(\"handles multiple extensions\", () => {\n      expect(generatePath(\"/files/:name.tar.gz\", { name: \"archive\" })).toBe(\n        \"/files/archive.tar.gz\",\n      );\n      expect(generatePath(\"/:file.min.js\", { file: \"app\" })).toBe(\n        \"/app.min.js\",\n      );\n    });\n  });\n});\n"
  },
  {
    "path": "packages/react-router/__tests__/gh-issue-8127-test.tsx",
    "content": "import * as React from \"react\";\nimport * as TestRenderer from \"react-test-renderer\";\nimport { MemoryRouter, Routes, Route } from \"../index\";\n\ndescribe(\"GH Issue #8127\", () => {\n  it(\"works\", () => {\n    let renderer: TestRenderer.ReactTestRenderer;\n    TestRenderer.act(() => {\n      renderer = TestRenderer.create(\n        <MemoryRouter initialEntries={[\"/availability\"]}>\n          <Routes>\n            <Route\n              path=\"*\"\n              element={\n                <Routes>\n                  <Route path=\"*\" element={<h1>sub splat</h1>} />\n                  <Route path=\"availability\" element={<h1>availability</h1>} />\n                </Routes>\n              }\n            />\n          </Routes>\n        </MemoryRouter>,\n      );\n    });\n\n    expect(renderer.toJSON()).toMatchInlineSnapshot(`\n      <h1>\n        availability\n      </h1>\n    `);\n  });\n});\n"
  },
  {
    "path": "packages/react-router/__tests__/gh-issue-8165-test.tsx",
    "content": "import * as React from \"react\";\nimport * as TestRenderer from \"react-test-renderer\";\nimport {\n  MemoryRouter,\n  Routes,\n  Route,\n  useParams,\n  useNavigate,\n  Navigate,\n} from \"../index\";\n\ndescribe(\"GH Issue #8165\", () => {\n  it(\"works\", () => {\n    const Content = () => {\n      return (\n        <div>\n          <h3>Level 2</h3>\n          <Routes>\n            <Route path=\"tab\" element={<span>TAB TEST</span>} />\n            <Route index element={<Navigate to=\"tab\" />} />\n          </Routes>\n          <div>Other:</div>\n          <Routes>\n            <Route path=\"*\" element={<span>TAB TEST</span>} />\n          </Routes>\n        </div>\n      );\n    };\n\n    const Root = () => {\n      const { lang } = useParams();\n      const nav = useNavigate();\n      React.useEffect(() => {\n        if (lang !== \"en\") {\n          nav(\"en\");\n        }\n      }, [lang, nav]);\n\n      return (\n        <>\n          <h2>Level 1</h2>\n          <Routes>\n            <Route path=\"*\" element={<Content />} />\n          </Routes>\n        </>\n      );\n    };\n\n    function App() {\n      return (\n        <div className=\"App\">\n          <h1>Level 0</h1>\n          <Routes>\n            <Route path=\"*\" element={<Root />} />\n            <Route path=\":lang/*\" element={<Root />} />\n          </Routes>\n        </div>\n      );\n    }\n\n    let renderer: TestRenderer.ReactTestRenderer;\n    TestRenderer.act(() => {\n      renderer = TestRenderer.create(\n        <MemoryRouter initialEntries={[\"/en/tab\"]}>\n          <App />\n        </MemoryRouter>,\n      );\n    });\n\n    expect(renderer.toJSON()).toMatchInlineSnapshot(`\n      <div\n        className=\"App\"\n      >\n        <h1>\n          Level 0\n        </h1>\n        <h2>\n          Level 1\n        </h2>\n        <div>\n          <h3>\n            Level 2\n          </h3>\n          <span>\n            TAB TEST\n          </span>\n          <div>\n            Other:\n          </div>\n          <span>\n            TAB TEST\n          </span>\n        </div>\n      </div>\n    `);\n  });\n});\n"
  },
  {
    "path": "packages/react-router/__tests__/greedy-matching-test.tsx",
    "content": "import * as React from \"react\";\nimport * as TestRenderer from \"react-test-renderer\";\nimport { MemoryRouter, Routes, Route, Outlet } from \"../index\";\n\ndescribe(\"greedy matching\", () => {\n  let routes = (\n    <Routes>\n      <Route path=\"/\" element={<p>Root</p>} />\n      <Route\n        path=\"home\"\n        element={\n          <div>\n            Home Layout <Outlet />\n          </div>\n        }\n      >\n        <Route index element={<p>Home</p>} />\n        <Route path=\"*\" element={<p>Home Not Found</p>} />\n      </Route>\n      <Route path=\"*\" element={<p>Not Found</p>} />\n    </Routes>\n  );\n\n  it(\"matches the root route\", () => {\n    let renderer: TestRenderer.ReactTestRenderer;\n    TestRenderer.act(() => {\n      renderer = TestRenderer.create(\n        <MemoryRouter initialEntries={[\"/\"]} children={routes} />,\n      );\n    });\n\n    expect(renderer.toJSON()).toMatchInlineSnapshot(`\n      <p>\n        Root\n      </p>\n    `);\n  });\n\n  it(\"matches the index route\", () => {\n    let renderer: TestRenderer.ReactTestRenderer;\n    TestRenderer.act(() => {\n      renderer = TestRenderer.create(\n        <MemoryRouter initialEntries={[\"/home\"]} children={routes} />,\n      );\n    });\n\n    expect(renderer.toJSON()).toMatchInlineSnapshot(`\n      <div>\n        Home Layout \n        <p>\n          Home\n        </p>\n      </div>\n    `);\n  });\n\n  it('matches the nested \"not found\" route', () => {\n    let renderer: TestRenderer.ReactTestRenderer;\n    TestRenderer.act(() => {\n      renderer = TestRenderer.create(\n        <MemoryRouter initialEntries={[\"/home/typo\"]} children={routes} />,\n      );\n    });\n\n    expect(renderer.toJSON()).toMatchInlineSnapshot(`\n      <div>\n        Home Layout \n        <p>\n          Home Not Found\n        </p>\n      </div>\n    `);\n  });\n\n  it('matches the \"not found\" route', () => {\n    let renderer: TestRenderer.ReactTestRenderer;\n    TestRenderer.act(() => {\n      renderer = TestRenderer.create(\n        <MemoryRouter initialEntries={[\"/hometypo\"]} children={routes} />,\n      );\n    });\n\n    expect(renderer.toJSON()).toMatchInlineSnapshot(`\n      <p>\n        Not Found\n      </p>\n    `);\n  });\n});\n"
  },
  {
    "path": "packages/react-router/__tests__/href-test.ts",
    "content": "import { href } from \"../lib/href\";\n\ndescribe(\"href\", () => {\n  it(\"works with param-less paths\", () => {\n    expect(href(\"/a/b/c\")).toBe(\"/a/b/c\");\n  });\n\n  it(\"works with params\", () => {\n    expect(href(\"/a/:b\", { b: \"hello\", z: \"ignored\" })).toBe(\"/a/hello\");\n    expect(href(\"/a/:b?\", { b: \"hello\", z: \"ignored\" })).toBe(\"/a/hello\");\n    expect(href(\"/a/:b?\")).toBe(\"/a\");\n    expect(href(\"/:b?\")).toBe(\"/\");\n    expect(href(\"/a/:e-z\", { \"e-z\": \"hello\" })).toBe(\"/a/hello\");\n  });\n\n  it(\"works with repeated params\", () => {\n    expect(href(\"/a/:b?/:b/:b?/:b\", { b: \"hello\" })).toBe(\n      \"/a/hello/hello/hello/hello\",\n    );\n    expect(href(\"/a/:c?/:b/:c?/:b\", { b: \"hello\" })).toBe(\"/a/hello/hello\");\n  });\n\n  it(\"works with splats\", () => {\n    expect(href(\"/a/*\", { \"*\": \"b/c\" })).toBe(\"/a/b/c\");\n    expect(href(\"/a/*\", {})).toBe(\"/a\");\n  });\n\n  it(\"works with malformed splats\", () => {\n    // this is how packages\\react-router\\lib\\router\\utils.ts: compilePath() will handle these routes.\n    expect(href(\"/a/z*\", { \"*\": \"b/c\" })).toBe(\"/a/z/b/c\");\n    expect(href(\"/a/z*\", {})).toBe(\"/a/z\");\n  });\n\n  it(\"throws when required params are missing\", () => {\n    expect(() => href(\"/a/:b\")).toThrow(\n      `Path '/a/:b' requires param 'b' but it was not provided`,\n    );\n  });\n\n  it(\"works with periods\", () => {\n    expect(href(\"/a/:b.zip\", { b: \"hello\" })).toBe(\"/a/hello.zip\");\n  });\n});\n"
  },
  {
    "path": "packages/react-router/__tests__/index-routes-test.tsx",
    "content": "import { matchRoutes } from \"react-router\";\n\ndescribe(\"index route matching\", () => {\n  it(\"throws when the index route has children\", () => {\n    expect(() => {\n      matchRoutes(\n        [\n          {\n            path: \"/users\",\n            children: [\n              // This config is not valid because index routes cannot have children\n              // @ts-expect-error\n              {\n                index: true,\n                children: [{ path: \"not-valid\" }],\n              },\n              { path: \":id\" },\n            ],\n          },\n        ],\n        \"/users/mj\",\n      );\n    }).toThrowErrorMatchingInlineSnapshot(\n      `\"Index routes must not have child routes. Please remove all child routes from route path \"/users/\".\"`,\n    );\n  });\n});\n"
  },
  {
    "path": "packages/react-router/__tests__/layout-routes-test.tsx",
    "content": "import * as React from \"react\";\nimport * as TestRenderer from \"react-test-renderer\";\nimport { MemoryRouter, Routes, Route, Outlet } from \"react-router\";\n\ndescribe(\"A layout route\", () => {\n  it(\"does not match when none of its children do\", () => {\n    let renderer: TestRenderer.ReactTestRenderer;\n    TestRenderer.act(() => {\n      renderer = TestRenderer.create(\n        <MemoryRouter initialEntries={[\"/\"]}>\n          <Routes>\n            <Route\n              element={\n                <div>\n                  <h1>Layout</h1>\n                  <Outlet />\n                </div>\n              }\n            >\n              <Route path=\"/home\" element={<h1>Home</h1>} />\n            </Route>\n            <Route index element={<h1>Index</h1>} />\n          </Routes>\n        </MemoryRouter>,\n      );\n    });\n\n    expect(renderer.toJSON()).toMatchInlineSnapshot(`\n      <h1>\n        Index\n      </h1>\n    `);\n  });\n\n  it(\"allows routes starting with `@`\", () => {\n    let renderer: TestRenderer.ReactTestRenderer;\n    TestRenderer.act(() => {\n      renderer = TestRenderer.create(\n        <MemoryRouter initialEntries={[\"/@splat\"]}>\n          <Routes>\n            <Route\n              element={\n                <div>\n                  <h1>Layout</h1>\n                  <Outlet />\n                </div>\n              }\n            >\n              <Route\n                path=\"*\"\n                element={\n                  <div>\n                    <h1>Splat</h1>\n                  </div>\n                }\n              />\n            </Route>\n          </Routes>\n        </MemoryRouter>,\n      );\n    });\n\n    expect(renderer.toJSON()).toMatchInlineSnapshot(`\n      <div>\n        <h1>\n          Layout\n        </h1>\n        <div>\n          <h1>\n            Splat\n          </h1>\n        </div>\n      </div>\n    `);\n  });\n  describe(\"matches when a nested splat route begins with a special character\", () => {\n    it(\"allows routes starting with `-`\", () => {\n      let renderer: TestRenderer.ReactTestRenderer;\n      TestRenderer.act(() => {\n        renderer = TestRenderer.create(\n          <MemoryRouter initialEntries={[\"/-splat\"]}>\n            <Routes>\n              <Route\n                element={\n                  <div>\n                    <h1>Layout</h1>\n                    <Outlet />\n                  </div>\n                }\n              >\n                <Route\n                  path=\"*\"\n                  element={\n                    <div>\n                      <h1>Splat</h1>\n                    </div>\n                  }\n                />\n              </Route>\n            </Routes>\n          </MemoryRouter>,\n        );\n      });\n\n      expect(renderer.toJSON()).toMatchInlineSnapshot(`\n        <div>\n          <h1>\n            Layout\n          </h1>\n          <div>\n            <h1>\n              Splat\n            </h1>\n          </div>\n        </div>\n      `);\n    });\n    it(\"allows routes starting with `~`\", () => {\n      let renderer: TestRenderer.ReactTestRenderer;\n      TestRenderer.act(() => {\n        renderer = TestRenderer.create(\n          <MemoryRouter initialEntries={[\"/~splat\"]}>\n            <Routes>\n              <Route\n                element={\n                  <div>\n                    <h1>Layout</h1>\n                    <Outlet />\n                  </div>\n                }\n              >\n                <Route\n                  path=\"*\"\n                  element={\n                    <div>\n                      <h1>Splat</h1>\n                    </div>\n                  }\n                />\n              </Route>\n            </Routes>\n          </MemoryRouter>,\n        );\n      });\n\n      expect(renderer.toJSON()).toMatchInlineSnapshot(`\n        <div>\n          <h1>\n            Layout\n          </h1>\n          <div>\n            <h1>\n              Splat\n            </h1>\n          </div>\n        </div>\n      `);\n    });\n    it(\"allows routes starting with `_`\", () => {\n      let renderer: TestRenderer.ReactTestRenderer;\n      TestRenderer.act(() => {\n        renderer = TestRenderer.create(\n          <MemoryRouter initialEntries={[\"/_splat\"]}>\n            <Routes>\n              <Route\n                element={\n                  <div>\n                    <h1>Layout</h1>\n                    <Outlet />\n                  </div>\n                }\n              >\n                <Route\n                  path=\"*\"\n                  element={\n                    <div>\n                      <h1>Splat</h1>\n                    </div>\n                  }\n                />\n              </Route>\n            </Routes>\n          </MemoryRouter>,\n        );\n      });\n\n      expect(renderer.toJSON()).toMatchInlineSnapshot(`\n        <div>\n          <h1>\n            Layout\n          </h1>\n          <div>\n            <h1>\n              Splat\n            </h1>\n          </div>\n        </div>\n      `);\n    });\n    it(\"allows routes starting with `.`\", () => {\n      let renderer: TestRenderer.ReactTestRenderer;\n      TestRenderer.act(() => {\n        renderer = TestRenderer.create(\n          <MemoryRouter initialEntries={[\"/.splat\"]}>\n            <Routes>\n              <Route\n                element={\n                  <div>\n                    <h1>Layout</h1>\n                    <Outlet />\n                  </div>\n                }\n              >\n                <Route\n                  path=\"*\"\n                  element={\n                    <div>\n                      <h1>Splat</h1>\n                    </div>\n                  }\n                />\n              </Route>\n            </Routes>\n          </MemoryRouter>,\n        );\n      });\n\n      expect(renderer.toJSON()).toMatchInlineSnapshot(`\n        <div>\n          <h1>\n            Layout\n          </h1>\n          <div>\n            <h1>\n              Splat\n            </h1>\n          </div>\n        </div>\n      `);\n    });\n    it(\"allows routes starting with url-encoded entities\", () => {\n      let renderer: TestRenderer.ReactTestRenderer;\n      TestRenderer.act(() => {\n        renderer = TestRenderer.create(\n          <MemoryRouter initialEntries={[\"/%20splat\"]}>\n            <Routes>\n              <Route\n                element={\n                  <div>\n                    <h1>Layout</h1>\n                    <Outlet />\n                  </div>\n                }\n              >\n                <Route\n                  path=\"*\"\n                  element={\n                    <div>\n                      <h1>Splat</h1>\n                    </div>\n                  }\n                />\n              </Route>\n            </Routes>\n          </MemoryRouter>,\n        );\n      });\n\n      expect(renderer.toJSON()).toMatchInlineSnapshot(`\n        <div>\n          <h1>\n            Layout\n          </h1>\n          <div>\n            <h1>\n              Splat\n            </h1>\n          </div>\n        </div>\n      `);\n    });\n  });\n});\n"
  },
  {
    "path": "packages/react-router/__tests__/matchPath-test.tsx",
    "content": "import { matchPath } from \"react-router\";\n\ndescribe(\"matchPath\", () => {\n  it(\"matches the root / URL\", () => {\n    expect(matchPath(\"/\", \"/\")).toMatchObject({\n      pathname: \"/\",\n      pathnameBase: \"/\",\n    });\n  });\n\n  describe(\"when the pattern has no leading slash\", () => {\n    it(\"fails to match a pathname that does not match\", () => {\n      expect(matchPath(\"users\", \"/usersblah\")).toBeNull();\n    });\n\n    it(\"matches a pathname\", () => {\n      expect(matchPath(\"users\", \"/users\")).toMatchObject({\n        pathname: \"/users\",\n        pathnameBase: \"/users\",\n      });\n    });\n\n    it(\"matches a pathname with multiple segments\", () => {\n      expect(matchPath(\"users/mj\", \"/users/mj\")).toMatchObject({\n        pathname: \"/users/mj\",\n        pathnameBase: \"/users/mj\",\n      });\n    });\n\n    it(\"matches a pathname with a trailing slash\", () => {\n      expect(matchPath(\"users\", \"/users/\")).toMatchObject({\n        pathname: \"/users/\",\n        pathnameBase: \"/users\",\n      });\n    });\n\n    it(\"matches a pathname with multiple segments and a trailing slash\", () => {\n      expect(matchPath(\"users/mj\", \"/users/mj/\")).toMatchObject({\n        pathname: \"/users/mj/\",\n        pathnameBase: \"/users/mj\",\n      });\n    });\n  });\n\n  describe(\"when the pattern has a leading slash\", () => {\n    it(\"fails to match a pathname that does not match\", () => {\n      expect(matchPath(\"/users\", \"/usersblah\")).toBeNull();\n    });\n\n    it(\"matches a pathname\", () => {\n      expect(matchPath(\"/users\", \"/users\")).toMatchObject({\n        pathname: \"/users\",\n        pathnameBase: \"/users\",\n      });\n    });\n\n    it(\"matches a pathname with multiple segments\", () => {\n      expect(matchPath(\"/users/mj\", \"/users/mj\")).toMatchObject({\n        pathname: \"/users/mj\",\n        pathnameBase: \"/users/mj\",\n      });\n    });\n\n    it(\"matches a pathname with a trailing slash\", () => {\n      expect(matchPath(\"/users\", \"/users/\")).toMatchObject({\n        pathname: \"/users/\",\n        pathnameBase: \"/users\",\n      });\n    });\n\n    it(\"matches a pathname with multiple segments and a trailing slash\", () => {\n      expect(matchPath(\"/users/mj\", \"/users/mj/\")).toMatchObject({\n        pathname: \"/users/mj/\",\n        pathnameBase: \"/users/mj\",\n      });\n    });\n  });\n\n  describe(\"when the pattern has a trailing slash\", () => {\n    it(\"fails to match a pathname that does not match\", () => {\n      expect(matchPath(\"users/\", \"/usersblah\")).toBeNull();\n    });\n\n    it(\"matches a pathname\", () => {\n      expect(matchPath(\"users/\", \"/users\")).toMatchObject({\n        pathname: \"/users\",\n        pathnameBase: \"/users\",\n      });\n    });\n\n    it(\"matches a pathname with multiple segments\", () => {\n      expect(matchPath(\"users/mj/\", \"/users/mj\")).toMatchObject({\n        pathname: \"/users/mj\",\n        pathnameBase: \"/users/mj\",\n      });\n    });\n\n    it(\"matches a pathname with a trailing slash\", () => {\n      expect(matchPath(\"users/\", \"/users/\")).toMatchObject({\n        pathname: \"/users/\",\n        pathnameBase: \"/users\",\n      });\n    });\n\n    it(\"matches a pathname with multiple segments and a trailing slash\", () => {\n      expect(matchPath(\"users/mj/\", \"/users/mj/\")).toMatchObject({\n        pathname: \"/users/mj/\",\n        pathnameBase: \"/users/mj\",\n      });\n    });\n  });\n\n  describe(\"with { end: false }\", () => {\n    it(\"matches the beginning of a pathname\", () => {\n      expect(matchPath({ path: \"/users\", end: false }, \"/users\")).toMatchObject(\n        { pathname: \"/users\", pathnameBase: \"/users\" },\n      );\n      expect(\n        matchPath({ path: \"/users/\", end: false }, \"/users\"),\n      ).toMatchObject({\n        pathname: \"/users\",\n        pathnameBase: \"/users\",\n      });\n    });\n\n    it(\"matches the beginning of a pathname with a trailing slash\", () => {\n      expect(\n        matchPath({ path: \"/users\", end: false }, \"/users/\"),\n      ).toMatchObject({ pathname: \"/users\", pathnameBase: \"/users\" });\n      expect(\n        matchPath({ path: \"/users/\", end: false }, \"/users/\"),\n      ).toMatchObject({\n        pathname: \"/users\",\n        pathnameBase: \"/users\",\n      });\n    });\n\n    it(\"matches the beginning of a pathname with multiple segments\", () => {\n      expect(\n        matchPath({ path: \"/users\", end: false }, \"/users/mj\"),\n      ).toMatchObject({ pathname: \"/users\", pathnameBase: \"/users\" });\n      expect(\n        matchPath({ path: \"/users/\", end: false }, \"/users/mj\"),\n      ).toMatchObject({ pathname: \"/users\", pathnameBase: \"/users\" });\n    });\n\n    it(\"matches the beginning of a pathname with multiple segments and a trailing slash\", () => {\n      expect(\n        matchPath({ path: \"/users\", end: false }, \"/users/mj/\"),\n      ).toMatchObject({ pathname: \"/users\", pathnameBase: \"/users\" });\n      expect(\n        matchPath({ path: \"/users/\", end: false }, \"/users/mj/\"),\n      ).toMatchObject({ pathname: \"/users\", pathnameBase: \"/users\" });\n    });\n\n    it(\"fails to match a pathname where the segments do not match\", () => {\n      expect(matchPath({ path: \"/users\", end: false }, \"/\")).toBeNull();\n      expect(matchPath({ path: \"/users\", end: false }, \"/users2\")).toBeNull();\n      expect(matchPath({ path: \"/users\", end: false }, \"/users-2\")).toBeNull();\n      expect(matchPath({ path: \"/users\", end: false }, \"/users~2\")).toBeNull();\n      expect(matchPath({ path: \"/users\", end: false }, \"/users@2\")).toBeNull();\n      expect(matchPath({ path: \"/users\", end: false }, \"/users.2\")).toBeNull();\n      expect(\n        matchPath({ path: \"/users/mj\", end: false }, \"/users/mj2\"),\n      ).toBeNull();\n    });\n  });\n\n  describe(\"with { end: false } and a / pattern\", () => {\n    it(\"matches a pathname\", () => {\n      expect(matchPath({ path: \"/\", end: false }, \"/users\")).toMatchObject({\n        pathname: \"/\",\n        pathnameBase: \"/\",\n      });\n    });\n\n    it(\"matches a pathname with multiple segments\", () => {\n      expect(matchPath({ path: \"/\", end: false }, \"/users/mj\")).toMatchObject({\n        pathname: \"/\",\n        pathnameBase: \"/\",\n      });\n    });\n\n    it(\"matches a pathname with a trailing slash\", () => {\n      expect(matchPath({ path: \"/\", end: false }, \"/users/\")).toMatchObject({\n        pathname: \"/\",\n        pathnameBase: \"/\",\n      });\n    });\n\n    it(\"matches a pathname with multiple segments and a trailing slash\", () => {\n      expect(matchPath({ path: \"/\", end: false }, \"/users/mj/\")).toMatchObject({\n        pathname: \"/\",\n        pathnameBase: \"/\",\n      });\n    });\n  });\n\n  it(\"is not case-sensitive by default\", () => {\n    expect(matchPath(\"/SystemDashboard\", \"/systemdashboard\")).toMatchObject({\n      pathname: \"/systemdashboard\",\n      pathnameBase: \"/systemdashboard\",\n    });\n  });\n\n  it(\"matches a case-sensitive pathname\", () => {\n    expect(\n      matchPath(\n        { path: \"/SystemDashboard\", caseSensitive: true },\n        \"/SystemDashboard\",\n      ),\n    ).toMatchObject({\n      pathname: \"/SystemDashboard\",\n      pathnameBase: \"/SystemDashboard\",\n    });\n  });\n\n  it(\"does not match a case-sensitive pathname with the wrong case\", () => {\n    expect(\n      matchPath(\n        { path: \"/SystemDashboard\", caseSensitive: true },\n        \"/systemDashboard\",\n      ),\n    ).toBeNull();\n  });\n\n  describe(\"when the pattern has a trailing /*\", () => {\n    it(\"matches the remaining portion of the pathname\", () => {\n      expect(matchPath(\"/files/*\", \"/files/mj.jpg\")).toMatchObject({\n        params: { \"*\": \"mj.jpg\" },\n        pathname: \"/files/mj.jpg\",\n        pathnameBase: \"/files\",\n      });\n      expect(matchPath(\"/files/*\", \"/files/\")).toMatchObject({\n        params: { \"*\": \"\" },\n        pathname: \"/files/\",\n        pathnameBase: \"/files\",\n      });\n      expect(matchPath(\"/files/*\", \"/files\")).toMatchObject({\n        params: { \"*\": \"\" },\n        pathname: \"/files\",\n        pathnameBase: \"/files\",\n      });\n    });\n  });\n});\n\ndescribe(\"matchPath optional dynamic segments\", () => {\n  it(\"should match when optional segment is provided\", () => {\n    const match = matchPath(\"/:lang?/user/:id\", \"/en/user/123\");\n    expect(match).toMatchObject({ params: { lang: \"en\", id: \"123\" } });\n  });\n\n  it(\"should match when optional segment is *not* provided\", () => {\n    const match = matchPath(\"/:lang?/user/:id\", \"/user/123\");\n    expect(match).toMatchObject({ params: { lang: undefined, id: \"123\" } });\n  });\n\n  it(\"should match when middle optional segment is provided\", () => {\n    const match = matchPath(\"/user/:lang?/:id\", \"/user/en/123\");\n    expect(match).toMatchObject({ params: { lang: \"en\", id: \"123\" } });\n  });\n\n  it(\"should match when middle optional segment is *not* provided\", () => {\n    const match = matchPath(\"/user/:lang?/:id\", \"/user/123\");\n    expect(match).toMatchObject({ params: { lang: undefined, id: \"123\" } });\n  });\n\n  it(\"should match when end optional segment is provided\", () => {\n    const match = matchPath(\"/user/:id/:lang?\", \"/user/123/en\");\n    expect(match).toMatchObject({ params: { lang: \"en\", id: \"123\" } });\n  });\n\n  it(\"should match when end optional segment is *not* provided\", () => {\n    const match = matchPath(\"/user/:id/:lang?\", \"/user/123\");\n    expect(match).toMatchObject({ params: { lang: undefined, id: \"123\" } });\n  });\n\n  it(\"should match multiple optional segments and none are provided\", () => {\n    const match = matchPath(\"/:lang?/user/:id?\", \"/user\");\n    expect(match).toMatchObject({ params: { lang: undefined, id: undefined } });\n  });\n\n  it(\"should match multiple optional segments and one is provided\", () => {\n    const match = matchPath(\"/:lang?/user/:id?\", \"/en/user\");\n    expect(match).toMatchObject({ params: { lang: \"en\", id: undefined } });\n  });\n\n  it(\"should match multiple optional segments and all are provided\", () => {\n    const match = matchPath(\"/:lang?/user/:id?\", \"/en/user/123\");\n    expect(match).toMatchObject({ params: { lang: \"en\", id: \"123\" } });\n  });\n\n  it(\"should NOT match when pathname extends base path without separator\", () => {\n    expect(matchPath(\"/test_route/:part?\", \"/test_route_more\")).toBeNull();\n  });\n\n  it(\"should NOT match when pathname extends base path without separator (middle optional param)\", () => {\n    expect(\n      matchPath(\"/test_route/:part?/edit\", \"/test_route_more/edit\"),\n    ).toBeNull();\n  });\n\n  it(\"should NOT match optional param when pathname has extra characters after base\", () => {\n    expect(matchPath(\"/users/:id?\", \"/usersblah\")).toBeNull();\n    expect(matchPath(\"/api/:version?\", \"/api123\")).toBeNull();\n    expect(matchPath(\"/home/:section?\", \"/homepage\")).toBeNull();\n  });\n\n  it(\"should still match optional param with proper path separator\", () => {\n    expect(matchPath(\"/test_route/:part?\", \"/test_route/more\")).toMatchObject({\n      params: { part: \"more\" },\n    });\n    expect(matchPath(\"/test_route/:part?\", \"/test_route\")).toMatchObject({\n      params: { part: undefined },\n    });\n    expect(matchPath(\"/test_route/:part?\", \"/test_route/\")).toMatchObject({\n      params: { part: undefined },\n    });\n  });\n\n  it(\"should match a middle optional param with proper path separators\", () => {\n    expect(\n      matchPath(\"/test_route/:part?/edit\", \"/test_route/more/edit\"),\n    ).toMatchObject({\n      params: { part: \"more\" },\n    });\n    expect(\n      matchPath(\"/test_route/:part?/edit\", \"/test_route/edit\"),\n    ).toMatchObject({\n      params: { part: undefined },\n    });\n  });\n});\n\ndescribe(\"matchPath optional static segments\", () => {\n  it(\"should match when optional segment is provided\", () => {\n    const match = matchPath(\"/school?/user/:id\", \"/school/user/123\");\n    expect(match).toMatchObject({\n      pathname: \"/school/user/123\",\n      pathnameBase: \"/school/user/123\",\n    });\n  });\n\n  it(\"should match when optional segment is *not* provided\", () => {\n    const match = matchPath(\"/school?/user/:id\", \"/user/123\");\n    expect(match).toMatchObject({\n      pathname: \"/user/123\",\n      pathnameBase: \"/user/123\",\n    });\n  });\n\n  it(\"should match when middle optional segment is provided\", () => {\n    const match = matchPath(\"/school/user?/:id\", \"/school/user/123\");\n    expect(match).toMatchObject({\n      pathname: \"/school/user/123\",\n      pathnameBase: \"/school/user/123\",\n    });\n  });\n\n  it(\"should match when middle optional segment is *not* provided\", () => {\n    const match = matchPath(\"/school/user?/:id\", \"/school/123\");\n    expect(match).toMatchObject({\n      pathname: \"/school/123\",\n      pathnameBase: \"/school/123\",\n    });\n  });\n\n  it(\"should match when end optional segment is provided\", () => {\n    const match = matchPath(\"/school/user/admin?\", \"/school/user/admin\");\n    expect(match).toMatchObject({\n      pathname: \"/school/user/admin\",\n      pathnameBase: \"/school/user/admin\",\n    });\n  });\n\n  it(\"should match when end optional segment is *not* provided\", () => {\n    const match = matchPath(\"/school/user/admin?\", \"/school/user\");\n    expect(match).toMatchObject({\n      pathname: \"/school/user\",\n      pathnameBase: \"/school/user\",\n    });\n  });\n\n  it(\"should match multiple optional segments and none are provided\", () => {\n    const match = matchPath(\"/school?/user/admin?\", \"/user\");\n    expect(match).toMatchObject({\n      pathname: \"/user\",\n      pathnameBase: \"/user\",\n    });\n  });\n\n  it(\"should match multiple optional segments and one is provided\", () => {\n    const match = matchPath(\"/school?/user/admin?\", \"/user/admin\");\n    expect(match).toMatchObject({\n      pathname: \"/user/admin\",\n      pathnameBase: \"/user/admin\",\n    });\n  });\n\n  it(\"should match multiple optional segments and all are provided\", () => {\n    const match = matchPath(\"/school?/user/admin?\", \"/school/user/admin\");\n    expect(match).toMatchObject({\n      pathname: \"/school/user/admin\",\n      pathnameBase: \"/school/user/admin\",\n    });\n  });\n\n  it(\"does not trigger from question marks in the middle of the optional static segment\", () => {\n    let match = matchPath(\"/school?abc/user/:id\", \"/abc/user/123\");\n    expect(match).toBe(null);\n    match = matchPath(\"/school?abc\", \"/abc\");\n    expect(match).toBe(null);\n  });\n});\n\ndescribe(\"matchPath *\", () => {\n  it(\"matches the root URL\", () => {\n    expect(matchPath(\"*\", \"/\")).toMatchObject({\n      pathname: \"/\",\n      pathnameBase: \"/\",\n    });\n    expect(matchPath(\"/*\", \"/\")).toMatchObject({\n      pathname: \"/\",\n      pathnameBase: \"/\",\n    });\n  });\n\n  it(\"matches a URL with a segment\", () => {\n    expect(matchPath(\"*\", \"/users\")).toMatchObject({\n      pathname: \"/users\",\n      pathnameBase: \"/\",\n    });\n    expect(matchPath(\"/*\", \"/users\")).toMatchObject({\n      pathname: \"/users\",\n      pathnameBase: \"/\",\n    });\n  });\n\n  it(\"matches a URL with a segment and a trailing slash\", () => {\n    expect(matchPath(\"*\", \"/users/\")).toMatchObject({\n      pathname: \"/users/\",\n      pathnameBase: \"/\",\n    });\n    expect(matchPath(\"/*\", \"/users/\")).toMatchObject({\n      pathname: \"/users/\",\n      pathnameBase: \"/\",\n    });\n  });\n\n  it(\"matches a URL with multiple segments\", () => {\n    expect(matchPath(\"*\", \"/users/mj\")).toMatchObject({\n      pathname: \"/users/mj\",\n      pathnameBase: \"/\",\n    });\n    expect(matchPath(\"/*\", \"/users/mj\")).toMatchObject({\n      pathname: \"/users/mj\",\n      pathnameBase: \"/\",\n    });\n  });\n\n  it(\"matches a URL with multiple segments and a trailing slash\", () => {\n    expect(matchPath(\"*\", \"/users/mj/\")).toMatchObject({\n      pathname: \"/users/mj/\",\n      pathnameBase: \"/\",\n    });\n    expect(matchPath(\"/*\", \"/users/mj/\")).toMatchObject({\n      pathname: \"/users/mj/\",\n      pathnameBase: \"/\",\n    });\n  });\n\n  it(\"resolves params containing '*' correctly\", () => {\n    expect(matchPath(\"/users/:name/*\", \"/users/foo*/splat\")).toMatchObject({\n      params: { name: \"foo*\", \"*\": \"splat\" },\n      pathname: \"/users/foo*/splat\",\n      pathnameBase: \"/users/foo*\",\n    });\n  });\n});\n\ndescribe(\"matchPath warnings\", () => {\n  let consoleWarn: jest.SpyInstance<void, any>;\n  beforeEach(() => {\n    consoleWarn = jest.spyOn(console, \"warn\").mockImplementation();\n  });\n\n  afterEach(() => {\n    consoleWarn.mockRestore();\n  });\n\n  describe(\"when the pattern has a trailing *\", () => {\n    it(\"issues a warning and matches the remaining portion of the pathname\", () => {\n      expect(matchPath(\"/files*\", \"/files/mj.jpg\")).toMatchObject({\n        params: { \"*\": \"mj.jpg\" },\n        pathname: \"/files/mj.jpg\",\n        pathnameBase: \"/files\",\n      });\n      expect(consoleWarn).toHaveBeenCalledTimes(1);\n\n      expect(matchPath(\"/files*\", \"/files/\")).toMatchObject({\n        params: { \"*\": \"\" },\n        pathname: \"/files/\",\n        pathnameBase: \"/files\",\n      });\n      expect(consoleWarn).toHaveBeenCalledTimes(2);\n\n      expect(matchPath(\"/files*\", \"/files\")).toMatchObject({\n        params: { \"*\": \"\" },\n        pathname: \"/files\",\n        pathnameBase: \"/files\",\n      });\n      expect(consoleWarn).toHaveBeenCalledTimes(3);\n    });\n  });\n});\n"
  },
  {
    "path": "packages/react-router/__tests__/matchRoutes-test.tsx",
    "content": "import * as React from \"react\";\nimport type { RouteObject } from \"react-router\";\nimport { matchRoutes } from \"react-router\";\n\nfunction pickPaths(\n  routes: RouteObject[],\n  pathname: string,\n  basename?: string,\n): string[] | null {\n  let matches = matchRoutes(routes, pathname, basename);\n  return matches && matches.map((match) => match.route.path || \"\");\n}\n\ndescribe(\"matchRoutes\", () => {\n  let userEditRoute: RouteObject = {\n    path: \"edit\",\n    element: <h1>User Edit</h1>,\n  };\n  let userProfileRoute: RouteObject = {\n    path: \":id\",\n    element: <h1>User Profile</h1>,\n    children: [userEditRoute],\n  };\n  let usersRoute: RouteObject = {\n    path: \"/users\",\n    element: <h1>Users</h1>,\n    children: [{ index: true, element: <h1>Index</h1> }, userProfileRoute],\n  };\n  let indexWithPathRoute: RouteObject = {\n    path: \"/withpath\",\n    index: true,\n  };\n  let layoutRouteIndex: RouteObject = {\n    path: \"/layout\",\n    index: true,\n    element: <h1>Layout</h1>,\n  };\n  let layoutRoute: RouteObject = {\n    path: \"/layout\",\n    children: [\n      { path: \"item\", element: <h1>Item</h1> },\n      { path: \":id\", element: <h1>ID</h1> },\n      { path: \"*\", element: <h1>Not Found</h1> },\n    ],\n  };\n  let routes: RouteObject[] = [\n    { path: \"/\", element: <h1>Root Layout</h1> },\n    {\n      path: \"/home\",\n      element: <h1>Home</h1>,\n      children: [\n        { index: true, element: <h1>Index</h1> },\n        { path: \"*\", element: <h1>Not Found</h1> },\n      ],\n    },\n    indexWithPathRoute,\n    layoutRoute,\n    layoutRouteIndex,\n    usersRoute,\n    { path: \"*\", element: <h1>Not Found</h1> },\n  ];\n\n  it(\"matches root * routes correctly\", () => {\n    expect(pickPaths(routes, \"/not-found\")).toEqual([\"*\"]);\n    expect(pickPaths(routes, \"/hometypo\")).toEqual([\"*\"]);\n  });\n\n  it(\"matches index routes with path correctly\", () => {\n    expect(pickPaths(routes, \"/withpath\")).toEqual([\"/withpath\"]);\n  });\n\n  it(\"matches index routes with path over layout\", () => {\n    expect(matchRoutes(routes, \"/layout\")?.[0].route.index).toBe(true);\n    expect(pickPaths(routes, \"/layout\")).toEqual([\"/layout\"]);\n  });\n\n  it(\"matches static path over index\", () => {\n    expect(pickPaths(routes, \"/layout/item\")).toEqual([\"/layout\", \"item\"]);\n  });\n\n  it(\"matches dynamic layout path with param over index\", () => {\n    expect(pickPaths(routes, \"/layout/id\")).toEqual([\"/layout\", \":id\"]);\n  });\n\n  it(\"matches dynamic layout path with splat over index\", () => {\n    expect(pickPaths(routes, \"/layout/id/more\")).toEqual([\"/layout\", \"*\"]);\n  });\n\n  it(\"matches nested index routes correctly\", () => {\n    expect(pickPaths(routes, \"/users\")).toEqual([\"/users\", \"\"]);\n  });\n\n  it(\"matches nested dynamic routes correctly\", () => {\n    expect(pickPaths(routes, \"/users/mj\")).toEqual([\"/users\", \":id\"]);\n    expect(pickPaths(routes, \"/users/mj/edit\")).toEqual([\n      \"/users\",\n      \":id\",\n      \"edit\",\n    ]);\n  });\n\n  it(\"matches nested dynamic routes with params ending in = (e.x. base64 encoded Id)\", () => {\n    expect(pickPaths(routes, \"/users/VXNlcnM6MQ==\")).toEqual([\"/users\", \":id\"]);\n    expect(pickPaths(routes, \"/users/VXNlcnM6MQ==/edit\")).toEqual([\n      \"/users\",\n      \":id\",\n      \"edit\",\n    ]);\n  });\n\n  it(\"matches nested * routes correctly\", () => {\n    expect(pickPaths(routes, \"/home/typo\")).toEqual([\"/home\", \"*\"]);\n  });\n\n  it(\"returns the same route object on match.route as the one that was passed in\", () => {\n    let matches = matchRoutes(routes, \"/users/mj\")!;\n    expect(matches[0].route).toBe(usersRoute);\n    expect(matches[1].route).toBe(userProfileRoute);\n  });\n\n  describe(\"with a basename\", () => {\n    it(\"matches a pathname that starts with the basename\", () => {\n      expect(pickPaths(routes, \"/app/users/mj\", \"/app\")).toEqual([\n        \"/users\",\n        \":id\",\n      ]);\n\n      // basename should not be case-sensitive\n      expect(pickPaths(routes, \"/APP/users/mj\", \"/app\")).toEqual([\n        \"/users\",\n        \":id\",\n      ]);\n    });\n\n    it(\"does not match a pathname that does not start with the basename\", () => {\n      expect(pickPaths(routes, \"/home\", \"/app\")).toBeNull();\n    });\n\n    it(\"does not match a pathname that does not start with basename/\", () => {\n      expect(pickPaths(routes, \"/appextra/home\", \"/app\")).toBeNull();\n    });\n  });\n});\n"
  },
  {
    "path": "packages/react-router/__tests__/navigate-test.tsx",
    "content": "import * as React from \"react\";\nimport * as TestRenderer from \"react-test-renderer\";\nimport {\n  MemoryRouter,\n  Navigate,\n  Outlet,\n  Routes,\n  Route,\n  RouterProvider,\n  createMemoryRouter,\n  useLocation,\n} from \"react-router\";\nimport { render, screen, waitFor } from \"@testing-library/react\";\n\nimport getHtml from \"../../react-router/__tests__/utils/getHtml\";\n\ndescribe(\"<Navigate>\", () => {\n  describe(\"with an absolute href\", () => {\n    it(\"navigates to the correct URL\", () => {\n      let renderer: TestRenderer.ReactTestRenderer;\n      TestRenderer.act(() => {\n        renderer = TestRenderer.create(\n          <MemoryRouter initialEntries={[\"/home\"]}>\n            <Routes>\n              <Route path=\"home\" element={<Navigate to=\"/about\" />} />\n              <Route path=\"about\" element={<h1>About</h1>} />\n            </Routes>\n          </MemoryRouter>,\n        );\n      });\n\n      // @ts-expect-error\n      expect(renderer.toJSON()).toMatchInlineSnapshot(`\n        <h1>\n          About\n        </h1>\n      `);\n    });\n  });\n\n  describe(\"with a relative href (relative=route)\", () => {\n    it(\"navigates to the correct URL\", () => {\n      let renderer: TestRenderer.ReactTestRenderer;\n      TestRenderer.act(() => {\n        renderer = TestRenderer.create(\n          <MemoryRouter initialEntries={[\"/home\"]}>\n            <Routes>\n              <Route path=\"home\" element={<Navigate to=\"../about\" />} />\n              <Route path=\"about\" element={<h1>About</h1>} />\n            </Routes>\n          </MemoryRouter>,\n        );\n      });\n\n      // @ts-expect-error\n      expect(renderer.toJSON()).toMatchInlineSnapshot(`\n        <h1>\n          About\n        </h1>\n      `);\n    });\n\n    it(\"handles upward navigation from an index routes\", () => {\n      let renderer: TestRenderer.ReactTestRenderer;\n      TestRenderer.act(() => {\n        renderer = TestRenderer.create(\n          <MemoryRouter initialEntries={[\"/home\"]}>\n            <Routes>\n              <Route path=\"home\">\n                <Route index element={<Navigate to=\"../about\" />} />\n              </Route>\n              <Route path=\"about\" element={<h1>About</h1>} />\n            </Routes>\n          </MemoryRouter>,\n        );\n      });\n\n      // @ts-expect-error\n      expect(renderer.toJSON()).toMatchInlineSnapshot(`\n        <h1>\n          About\n        </h1>\n      `);\n    });\n\n    it(\"handles upward navigation from inside a pathless layout route\", () => {\n      let renderer: TestRenderer.ReactTestRenderer;\n      TestRenderer.act(() => {\n        renderer = TestRenderer.create(\n          <MemoryRouter initialEntries={[\"/home\"]}>\n            <Routes>\n              <Route element={<Outlet />}>\n                <Route path=\"home\" element={<Navigate to=\"../about\" />} />\n              </Route>\n              <Route path=\"about\" element={<h1>About</h1>} />\n            </Routes>\n          </MemoryRouter>,\n        );\n      });\n\n      // @ts-expect-error\n      expect(renderer.toJSON()).toMatchInlineSnapshot(`\n        <h1>\n          About\n        </h1>\n      `);\n    });\n\n    it(\"handles upward navigation from inside multiple pathless layout routes + index route\", () => {\n      let renderer: TestRenderer.ReactTestRenderer;\n      TestRenderer.act(() => {\n        renderer = TestRenderer.create(\n          <MemoryRouter initialEntries={[\"/home\"]}>\n            <Routes>\n              <Route path=\"home\">\n                <Route element={<Outlet />}>\n                  <Route element={<Outlet />}>\n                    <Route element={<Outlet />}>\n                      <Route index element={<Navigate to=\"../about\" />} />\n                    </Route>\n                  </Route>\n                </Route>\n              </Route>\n              <Route path=\"about\" element={<h1>About</h1>} />\n            </Routes>\n          </MemoryRouter>,\n        );\n      });\n\n      // @ts-expect-error\n      expect(renderer.toJSON()).toMatchInlineSnapshot(`\n        <h1>\n          About\n        </h1>\n      `);\n    });\n\n    it(\"handles upward navigation from inside multiple pathless layout routes + path route\", () => {\n      let renderer: TestRenderer.ReactTestRenderer;\n      TestRenderer.act(() => {\n        renderer = TestRenderer.create(\n          <MemoryRouter initialEntries={[\"/home/page\"]}>\n            <Routes>\n              <Route path=\"home\" element={<Outlet />}>\n                <Route element={<Outlet />}>\n                  <Route element={<Outlet />}>\n                    <Route element={<Outlet />}>\n                      <Route\n                        path=\"page\"\n                        element={<Navigate to=\"../../about\" />}\n                      />\n                    </Route>\n                  </Route>\n                </Route>\n              </Route>\n              <Route path=\"about\" element={<h1>About</h1>} />\n            </Routes>\n          </MemoryRouter>,\n        );\n      });\n\n      // @ts-expect-error\n      expect(renderer.toJSON()).toMatchInlineSnapshot(`\n        <h1>\n          About\n        </h1>\n      `);\n    });\n\n    it(\"handles parent navigation from inside multiple pathless layout routes\", () => {\n      let renderer: TestRenderer.ReactTestRenderer;\n      TestRenderer.act(() => {\n        renderer = TestRenderer.create(\n          <MemoryRouter initialEntries={[\"/home/page\"]}>\n            <Routes>\n              <Route\n                path=\"home\"\n                element={\n                  <>\n                    <h1>Home</h1>\n                    <Outlet />\n                  </>\n                }\n              >\n                <Route element={<Outlet />}>\n                  <Route element={<Outlet />}>\n                    <Route element={<Outlet />}>\n                      <Route\n                        path=\"page\"\n                        element={\n                          <>\n                            <h2>Page</h2>\n                            <Navigate to=\"..\" />\n                          </>\n                        }\n                      />\n                    </Route>\n                  </Route>\n                </Route>\n              </Route>\n              <Route path=\"about\" element={<h1>About</h1>} />\n            </Routes>\n          </MemoryRouter>,\n        );\n      });\n\n      // @ts-expect-error\n      expect(renderer.toJSON()).toMatchInlineSnapshot(`\n        <h1>\n          Home\n        </h1>\n      `);\n    });\n\n    it(\"handles relative navigation from nested index route\", () => {\n      let renderer: TestRenderer.ReactTestRenderer;\n      TestRenderer.act(() => {\n        renderer = TestRenderer.create(\n          <MemoryRouter initialEntries={[\"/layout/thing\"]}>\n            <Routes>\n              <Route path=\"layout\">\n                <Route path=\":param\">\n                  {/* redirect /layout/:param/ index routes to /layout/:param/dest */}\n                  <Route index element={<Navigate to=\"dest\" />} />\n                  <Route path=\"dest\" element={<h1>Destination</h1>} />\n                </Route>\n              </Route>\n            </Routes>\n          </MemoryRouter>,\n        );\n      });\n\n      // @ts-expect-error\n      expect(renderer.toJSON()).toMatchInlineSnapshot(`\n        <h1>\n          Destination\n        </h1>\n      `);\n    });\n  });\n\n  describe(\"with a relative href (relative=path)\", () => {\n    it(\"navigates to the correct URL\", () => {\n      let renderer: TestRenderer.ReactTestRenderer;\n      TestRenderer.act(() => {\n        renderer = TestRenderer.create(\n          <MemoryRouter initialEntries={[\"/contacts/1\"]}>\n            <Routes>\n              <Route path=\"contacts\" element={<h1>Contacts</h1>} />\n              <Route\n                path=\"contacts/:id\"\n                element={<Navigate to=\"..\" relative=\"path\" />}\n              />\n            </Routes>\n          </MemoryRouter>,\n        );\n      });\n\n      // @ts-expect-error\n      expect(renderer.toJSON()).toMatchInlineSnapshot(`\n        <h1>\n          Contacts\n        </h1>\n      `);\n    });\n\n    it(\"handles upward navigation from an index routes\", () => {\n      let renderer: TestRenderer.ReactTestRenderer;\n      TestRenderer.act(() => {\n        renderer = TestRenderer.create(\n          <MemoryRouter initialEntries={[\"/contacts/1\"]}>\n            <Routes>\n              <Route path=\"contacts\" element={<h1>Contacts</h1>} />\n              <Route path=\"contacts/:id\">\n                <Route index element={<Navigate to=\"..\" relative=\"path\" />} />\n              </Route>\n            </Routes>\n          </MemoryRouter>,\n        );\n      });\n\n      // @ts-expect-error\n      expect(renderer.toJSON()).toMatchInlineSnapshot(`\n        <h1>\n          Contacts\n        </h1>\n      `);\n    });\n\n    it(\"handles upward navigation from inside a pathless layout route\", () => {\n      let renderer: TestRenderer.ReactTestRenderer;\n      TestRenderer.act(() => {\n        renderer = TestRenderer.create(\n          <MemoryRouter initialEntries={[\"/contacts/1\"]}>\n            <Routes>\n              <Route path=\"contacts\" element={<h1>Contacts</h1>} />\n              <Route element={<Outlet />}>\n                <Route\n                  path=\"contacts/:id\"\n                  element={<Navigate to=\"..\" relative=\"path\" />}\n                />\n              </Route>\n            </Routes>\n          </MemoryRouter>,\n        );\n      });\n\n      // @ts-expect-error\n      expect(renderer.toJSON()).toMatchInlineSnapshot(`\n        <h1>\n          Contacts\n        </h1>\n      `);\n    });\n\n    it(\"handles upward navigation from inside multiple pathless layout routes + index route\", () => {\n      let renderer: TestRenderer.ReactTestRenderer;\n      TestRenderer.act(() => {\n        renderer = TestRenderer.create(\n          <MemoryRouter initialEntries={[\"/contacts/1\"]}>\n            <Routes>\n              <Route path=\"contacts\" element={<h1>Contacts</h1>} />\n              <Route path=\"contacts/:id\">\n                <Route element={<Outlet />}>\n                  <Route element={<Outlet />}>\n                    <Route element={<Outlet />}>\n                      <Route\n                        index\n                        element={<Navigate to=\"..\" relative=\"path\" />}\n                      />\n                    </Route>\n                  </Route>\n                </Route>\n              </Route>\n            </Routes>\n          </MemoryRouter>,\n        );\n      });\n\n      // @ts-expect-error\n      expect(renderer.toJSON()).toMatchInlineSnapshot(`\n        <h1>\n          Contacts\n        </h1>\n      `);\n    });\n\n    it(\"handles upward navigation from inside multiple pathless layout routes + path route\", () => {\n      let renderer: TestRenderer.ReactTestRenderer;\n      TestRenderer.act(() => {\n        renderer = TestRenderer.create(\n          <MemoryRouter initialEntries={[\"/contacts/1\"]}>\n            <Routes>\n              <Route path=\"contacts\" element={<Outlet />}>\n                <Route index element={<h1>Contacts</h1>} />\n                <Route element={<Outlet />}>\n                  <Route element={<Outlet />}>\n                    <Route element={<Outlet />}>\n                      <Route\n                        path=\":id\"\n                        element={<Navigate to=\"..\" relative=\"path\" />}\n                      />\n                    </Route>\n                  </Route>\n                </Route>\n              </Route>\n            </Routes>\n          </MemoryRouter>,\n        );\n      });\n\n      // @ts-expect-error\n      expect(renderer.toJSON()).toMatchInlineSnapshot(`\n        <h1>\n          Contacts\n        </h1>\n      `);\n    });\n\n    it(\"handles relative navigation from nested index route\", () => {\n      let renderer: TestRenderer.ReactTestRenderer;\n      TestRenderer.act(() => {\n        renderer = TestRenderer.create(\n          <MemoryRouter initialEntries={[\"/layout/thing\"]}>\n            <Routes>\n              <Route path=\"layout\">\n                <Route path=\":param\">\n                  {/* redirect /layout/:param/ index routes to /layout/:param/dest */}\n                  <Route\n                    index\n                    element={<Navigate to=\"dest\" relative=\"path\" />}\n                  />\n                  <Route path=\"dest\" element={<h1>Destination</h1>} />\n                </Route>\n              </Route>\n            </Routes>\n          </MemoryRouter>,\n        );\n      });\n\n      // @ts-expect-error\n      expect(renderer.toJSON()).toMatchInlineSnapshot(`\n        <h1>\n          Destination\n        </h1>\n      `);\n    });\n\n    it(\"preserves search params and hash\", () => {\n      let renderer: TestRenderer.ReactTestRenderer;\n      TestRenderer.act(() => {\n        renderer = TestRenderer.create(\n          <MemoryRouter initialEntries={[\"/contacts/1\"]}>\n            <Routes>\n              <Route path=\"contacts\" element={<Contacts />} />\n              <Route\n                path=\"contacts/:id\"\n                element={<Navigate to=\"..?foo=bar#hash\" relative=\"path\" />}\n              />\n            </Routes>\n          </MemoryRouter>,\n        );\n      });\n\n      function Contacts() {\n        let { search, hash } = useLocation();\n        return (\n          <>\n            <h1>Contacts</h1>\n            <p>\n              {search}\n              {hash}\n            </p>\n          </>\n        );\n      }\n\n      // @ts-expect-error\n      expect(renderer.toJSON()).toMatchInlineSnapshot(`\n        [\n          <h1>\n            Contacts\n          </h1>,\n          <p>\n            ?foo=bar\n            #hash\n          </p>,\n        ]\n      `);\n    });\n  });\n\n  it(\"does not cause navigation loops in data routers\", async () => {\n    // Note this is not the idiomatic way to do these redirects, they should\n    // be done with loaders in data routers, but this is a likely scenario to\n    // encounter while migrating to a data router\n    let router = createMemoryRouter(\n      [\n        {\n          path: \"home\",\n          element: <Navigate to=\"/about\" />,\n        },\n        {\n          path: \"about\",\n          element: <h1>About</h1>,\n          loader: () => new Promise((r) => setTimeout(() => r(\"ok\"), 10)),\n        },\n      ],\n      {\n        initialEntries: [\"/home\"],\n      },\n    );\n\n    let { container } = render(\n      <React.StrictMode>\n        <RouterProvider router={router} />\n      </React.StrictMode>,\n    );\n\n    await waitFor(() => screen.getByText(\"About\"));\n\n    expect(getHtml(container)).toMatchInlineSnapshot(`\n      \"<div>\n        <h1>\n          About\n        </h1>\n      </div>\"\n    `);\n  });\n\n  it(\"handles sync relative navigations in StrictMode using a data router\", async () => {\n    const router = createMemoryRouter([\n      {\n        path: \"/\",\n        children: [\n          {\n            index: true,\n            // This is a relative navigation from the current location of /a.\n            // Ensure we don't route from / -> /b -> /b/b\n            Component: () => <Navigate to={\"b\"} replace />,\n          },\n          {\n            path: \"b\",\n            element: <h1>Page B</h1>,\n          },\n        ],\n      },\n    ]);\n\n    let { container } = render(\n      <React.StrictMode>\n        <RouterProvider router={router} />\n      </React.StrictMode>,\n    );\n\n    await waitFor(() => screen.getByText(\"Page B\"));\n\n    expect(getHtml(container)).toMatchInlineSnapshot(`\n      \"<div>\n        <h1>\n          Page B\n        </h1>\n      </div>\"\n    `);\n  });\n\n  it(\"handles async relative navigations in StrictMode using a data router\", async () => {\n    const router = createMemoryRouter(\n      [\n        {\n          path: \"/a\",\n          children: [\n            {\n              index: true,\n              // This is a relative navigation from the current location of /a.\n              // Ensure we don't route from /a -> /a/b -> /a/b/b\n              Component: () => <Navigate to={\"b\"} replace />,\n            },\n            {\n              path: \"b\",\n              async loader() {\n                await new Promise((r) => setTimeout(r, 10));\n                return null;\n              },\n              element: <h1>Page B</h1>,\n            },\n          ],\n        },\n      ],\n      { initialEntries: [\"/a\"] },\n    );\n\n    let { container } = render(\n      <React.StrictMode>\n        <RouterProvider router={router} />\n      </React.StrictMode>,\n    );\n\n    await waitFor(() => screen.getByText(\"Page B\"));\n\n    expect(getHtml(container)).toMatchInlineSnapshot(`\n      \"<div>\n        <h1>\n          Page B\n        </h1>\n      </div>\"\n    `);\n  });\n});\n\ndescribe(\"concurrent mode (using React.startTransition for updates)\", () => {\n  it(\"handles setState in render in StrictMode using a data router (sync loader)\", async () => {\n    let renders: number[] = [];\n    const router = createMemoryRouter([\n      {\n        path: \"/\",\n        children: [\n          {\n            index: true,\n            Component() {\n              let [count, setCount] = React.useState(0);\n              if (count === 0) {\n                setCount(1);\n              }\n              return <Navigate to={\"b\"} replace state={{ count }} />;\n            },\n          },\n          {\n            path: \"b\",\n            Component() {\n              let { state } = useLocation() as { state: { count: number } };\n              renders.push(state.count);\n              return (\n                <>\n                  <h1>Page B</h1>\n                  <p>{state.count}</p>\n                </>\n              );\n            },\n          },\n        ],\n      },\n    ]);\n\n    let navigateSpy = jest.spyOn(router, \"navigate\");\n\n    let { container } = render(\n      <React.StrictMode>\n        <RouterProvider router={router} />\n      </React.StrictMode>,\n    );\n\n    await waitFor(() => screen.getByText(\"Page B\"));\n\n    expect(getHtml(container)).toMatchInlineSnapshot(`\n      \"<div>\n        <h1>\n          Page B\n        </h1>\n        <p>\n          1\n        </p>\n      </div>\"\n    `);\n    expect(navigateSpy).toHaveBeenCalledTimes(2);\n    expect(navigateSpy.mock.calls[0]).toMatchObject([\n      { pathname: \"/b\" },\n      { state: { count: 1 } },\n    ]);\n    expect(navigateSpy.mock.calls[1]).toMatchObject([\n      { pathname: \"/b\" },\n      { state: { count: 1 } },\n    ]);\n    expect(renders).toEqual([1, 1]);\n  });\n\n  it(\"handles setState in effect in StrictMode using a data router (sync loader)\", async () => {\n    let renders: number[] = [];\n    const router = createMemoryRouter([\n      {\n        path: \"/\",\n        children: [\n          {\n            index: true,\n            Component() {\n              let [count, setCount] = React.useState(0);\n              React.useEffect(() => {\n                if (count === 0) {\n                  setCount(1);\n                }\n              }, [count]);\n              return <Navigate to={\"b\"} replace state={{ count }} />;\n            },\n          },\n          {\n            path: \"b\",\n            Component() {\n              let { state } = useLocation() as { state: { count: number } };\n              renders.push(state.count);\n              return (\n                <>\n                  <h1>Page B</h1>\n                  <p>{state.count}</p>\n                </>\n              );\n            },\n          },\n        ],\n      },\n    ]);\n\n    let navigateSpy = jest.spyOn(router, \"navigate\");\n\n    let { container } = render(\n      <React.StrictMode>\n        <RouterProvider router={router} />\n      </React.StrictMode>,\n    );\n\n    await waitFor(() => screen.getByText(\"Page B\"));\n\n    expect(getHtml(container)).toMatchInlineSnapshot(`\n      \"<div>\n        <h1>\n          Page B\n        </h1>\n        <p>\n          1\n        </p>\n      </div>\"\n    `);\n    expect(navigateSpy).toHaveBeenCalledTimes(3);\n    expect(navigateSpy.mock.calls[0]).toMatchObject([\n      { pathname: \"/b\" },\n      { state: { count: 0 } },\n    ]);\n    expect(navigateSpy.mock.calls[1]).toMatchObject([\n      { pathname: \"/b\" },\n      { state: { count: 0 } },\n    ]);\n    expect(navigateSpy.mock.calls[2]).toMatchObject([\n      { pathname: \"/b\" },\n      { state: { count: 1 } },\n    ]);\n    expect(renders).toEqual([1, 1]);\n  });\n\n  it(\"handles setState in render in StrictMode using a data router (async loader)\", async () => {\n    let renders: number[] = [];\n    const router = createMemoryRouter([\n      {\n        path: \"/\",\n        children: [\n          {\n            index: true,\n            Component() {\n              let [count, setCount] = React.useState(0);\n              if (count === 0) {\n                setCount(1);\n              }\n              return <Navigate to={\"b\"} replace state={{ count }} />;\n            },\n          },\n          {\n            path: \"b\",\n            async loader() {\n              await new Promise((r) => setTimeout(r, 10));\n              return null;\n            },\n            Component() {\n              let { state } = useLocation() as { state: { count: number } };\n              renders.push(state.count);\n              return (\n                <>\n                  <h1>Page B</h1>\n                  <p>{state.count}</p>\n                </>\n              );\n            },\n          },\n        ],\n      },\n    ]);\n\n    let navigateSpy = jest.spyOn(router, \"navigate\");\n\n    let { container } = render(\n      <React.StrictMode>\n        <RouterProvider router={router} />\n      </React.StrictMode>,\n    );\n\n    await waitFor(() => screen.getByText(\"Page B\"));\n\n    expect(getHtml(container)).toMatchInlineSnapshot(`\n      \"<div>\n        <h1>\n          Page B\n        </h1>\n        <p>\n          1\n        </p>\n      </div>\"\n    `);\n    expect(navigateSpy).toHaveBeenCalledTimes(2);\n    expect(navigateSpy.mock.calls[0]).toMatchObject([\n      { pathname: \"/b\" },\n      { state: { count: 1 } },\n    ]);\n    expect(navigateSpy.mock.calls[1]).toMatchObject([\n      { pathname: \"/b\" },\n      { state: { count: 1 } },\n    ]);\n    // /a/b rendered with the same state value both times\n    expect(renders).toEqual([1, 1]);\n  });\n\n  it(\"handles setState in effect in StrictMode using a data router (async loader)\", async () => {\n    let renders: number[] = [];\n    const router = createMemoryRouter([\n      {\n        path: \"/\",\n        children: [\n          {\n            index: true,\n            Component() {\n              // When state managed by react and changes during render, we'll\n              // only \"see\" the value from the first pass through here in our\n              // effects\n              let [count, setCount] = React.useState(0);\n              React.useEffect(() => {\n                if (count === 0) {\n                  setCount(1);\n                }\n              }, [count]);\n              return <Navigate to={\"b\"} replace state={{ count }} />;\n            },\n          },\n          {\n            path: \"b\",\n            async loader() {\n              await new Promise((r) => setTimeout(r, 10));\n              return null;\n            },\n            Component() {\n              let { state } = useLocation() as { state: { count: number } };\n              renders.push(state.count);\n              return (\n                <>\n                  <h1>Page B</h1>\n                  <p>{state.count}</p>\n                </>\n              );\n            },\n          },\n        ],\n      },\n    ]);\n\n    let navigateSpy = jest.spyOn(router, \"navigate\");\n\n    let { container } = render(\n      <React.StrictMode>\n        <RouterProvider router={router} />\n      </React.StrictMode>,\n    );\n\n    await waitFor(() => screen.getByText(\"Page B\"));\n\n    expect(getHtml(container)).toMatchInlineSnapshot(`\n      \"<div>\n        <h1>\n          Page B\n        </h1>\n        <p>\n          1\n        </p>\n      </div>\"\n    `);\n    expect(navigateSpy).toHaveBeenCalledTimes(3);\n    expect(navigateSpy.mock.calls[0]).toMatchObject([\n      { pathname: \"/b\" },\n      { state: { count: 0 } },\n    ]);\n    expect(navigateSpy.mock.calls[1]).toMatchObject([\n      { pathname: \"/b\" },\n      { state: { count: 0 } },\n    ]);\n    // StrictMode only applies the double-effect execution on component mount,\n    // not component update\n    expect(navigateSpy.mock.calls[2]).toMatchObject([\n      { pathname: \"/b\" },\n      { state: { count: 1 } },\n    ]);\n    // /a/b rendered with the latest state value both times\n    expect(renders).toEqual([1, 1]);\n  });\n});\n"
  },
  {
    "path": "packages/react-router/__tests__/params-decode-test.tsx",
    "content": "import * as React from \"react\";\nimport * as TestRenderer from \"react-test-renderer\";\nimport { MemoryRouter, Routes, Route, useParams } from \"react-router\";\n\ndescribe(\"Decoding params\", () => {\n  it(\"works\", () => {\n    function Content() {\n      return <p>The params are {JSON.stringify(useParams())}</p>;\n    }\n\n    let renderer: TestRenderer.ReactTestRenderer;\n    TestRenderer.act(() => {\n      renderer = TestRenderer.create(\n        <MemoryRouter initialEntries={[\"/content/%2F\"]}>\n          <Routes>\n            <Route\n              path=\"content/*\"\n              element={\n                <Routes>\n                  <Route path=\":id\" element={<Content />} />\n                </Routes>\n              }\n            />\n          </Routes>\n        </MemoryRouter>,\n      );\n    });\n\n    expect(renderer.toJSON()).toMatchInlineSnapshot(`\n      <p>\n        The params are \n        {\"*\":\"/\",\"id\":\"/\"}\n      </p>\n    `);\n  });\n});\n"
  },
  {
    "path": "packages/react-router/__tests__/path-matching-test.tsx",
    "content": "import type { RouteObject } from \"react-router\";\nimport { matchPath, matchRoutes } from \"react-router\";\n\nfunction pickPaths(routes: RouteObject[], pathname: string): string[] | null {\n  let matches = matchRoutes(routes, pathname);\n  return matches && matches.map((match) => match.route.path || \"\");\n}\n\nfunction pickPathsAndParams(routes: RouteObject[], pathname: string) {\n  let matches = matchRoutes(routes, pathname);\n  return (\n    matches &&\n    matches.map((match) => ({\n      ...(match.route.index ? { index: match.route.index } : {}),\n      ...(match.route.path ? { path: match.route.path } : {}),\n      params: match.params,\n    }))\n  );\n}\n\ndescribe(\"path matching\", () => {\n  test(\"root vs. dynamic\", () => {\n    let routes = [{ path: \"/\" }, { path: \":id\" }];\n    expect(pickPaths(routes, \"/\")).toEqual([\"/\"]);\n    expect(pickPaths(routes, \"/123\")).toEqual([\":id\"]);\n  });\n\n  test(\"precedence of a bunch of routes in a flat route config\", () => {\n    let routes = [\n      { path: \"/groups/main/users/me\" },\n      { path: \"/groups/:groupId/users/me\" },\n      { path: \"/groups/:groupId/users/:userId\" },\n      { path: \"/groups/:groupId/users/*\" },\n      { path: \"/groups/main/users\" },\n      { path: \"/groups/:groupId/users\" },\n      { path: \"/groups/main\" },\n      { path: \"/groups/:groupId\" },\n      { path: \"/groups\" },\n      { path: \"/files/*\" },\n      { path: \"/files\" },\n      { path: \"/:one/:two/:three/:four/:five\" },\n      { path: \"/\" },\n      { path: \"*\" },\n    ];\n\n    expect(pickPaths(routes, \"/groups/main/users/me\")).toEqual([\n      \"/groups/main/users/me\",\n    ]);\n    expect(pickPaths(routes, \"/groups/other/users/me\")).toEqual([\n      \"/groups/:groupId/users/me\",\n    ]);\n    expect(pickPaths(routes, \"/groups/123/users/456\")).toEqual([\n      \"/groups/:groupId/users/:userId\",\n    ]);\n    expect(pickPaths(routes, \"/groups/main/users/a/b\")).toEqual([\n      \"/groups/:groupId/users/*\",\n    ]);\n    expect(pickPaths(routes, \"/groups/main/users\")).toEqual([\n      \"/groups/main/users\",\n    ]);\n    expect(pickPaths(routes, \"/groups/123/users\")).toEqual([\n      \"/groups/:groupId/users\",\n    ]);\n    expect(pickPaths(routes, \"/groups/main\")).toEqual([\"/groups/main\"]);\n    expect(pickPaths(routes, \"/groups/123\")).toEqual([\"/groups/:groupId\"]);\n    expect(pickPaths(routes, \"/groups\")).toEqual([\"/groups\"]);\n    expect(pickPaths(routes, \"/files/some/long/path\")).toEqual([\"/files/*\"]);\n    expect(pickPaths(routes, \"/files\")).toEqual([\"/files\"]);\n    expect(pickPaths(routes, \"/one/two/three/four/five\")).toEqual([\n      \"/:one/:two/:three/:four/:five\",\n    ]);\n    expect(pickPaths(routes, \"/\")).toEqual([\"/\"]);\n    expect(pickPaths(routes, \"/no/where\")).toEqual([\"*\"]);\n  });\n\n  test(\"precedence of a bunch of routes in a nested route config\", () => {\n    let routes = [\n      {\n        path: \"courses\",\n        children: [\n          {\n            path: \":id\",\n            children: [{ path: \"subjects\" }],\n          },\n          { path: \"new\" },\n          { index: true },\n          { path: \"*\" },\n        ],\n      },\n      {\n        path: \"courses\",\n        children: [{ path: \"react-fundamentals\" }, { path: \"advanced-react\" }],\n      },\n      { path: \"/\" },\n      { path: \"*\" },\n    ];\n\n    expect(pickPaths(routes, \"/courses\")).toEqual([\"courses\", \"\"]);\n    expect(pickPaths(routes, \"/courses/routing\")).toEqual([\"courses\", \":id\"]);\n    expect(pickPaths(routes, \"/courses/routing/subjects\")).toEqual([\n      \"courses\",\n      \":id\",\n      \"subjects\",\n    ]);\n    expect(pickPaths(routes, \"/courses/new\")).toEqual([\"courses\", \"new\"]);\n    expect(pickPaths(routes, \"/courses/whatever/path\")).toEqual([\n      \"courses\",\n      \"*\",\n    ]);\n    expect(pickPaths(routes, \"/courses/react-fundamentals\")).toEqual([\n      \"courses\",\n      \"react-fundamentals\",\n    ]);\n    expect(pickPaths(routes, \"/courses/advanced-react\")).toEqual([\n      \"courses\",\n      \"advanced-react\",\n    ]);\n    expect(pickPaths(routes, \"/\")).toEqual([\"/\"]);\n    expect(pickPaths(routes, \"/whatever\")).toEqual([\"*\"]);\n  });\n\n  test(\"nested index route vs sibling static route\", () => {\n    let routes = [\n      {\n        path: \":page\",\n        children: [{ index: true }],\n      },\n      { path: \"page\" },\n    ];\n\n    expect(pickPaths(routes, \"/page\")).toEqual([\"page\"]);\n  });\n\n  test(\"dynamic segments can contain dashes\", () => {\n    let routes = [\n      {\n        path: \":foo-bar\",\n      },\n      {\n        path: \"foo-bar\",\n      },\n    ];\n\n    expect(matchRoutes(routes, \"/foo-bar\")).toMatchInlineSnapshot(`\n      [\n        {\n          \"params\": {},\n          \"pathname\": \"/foo-bar\",\n          \"pathnameBase\": \"/foo-bar\",\n          \"route\": {\n            \"path\": \"foo-bar\",\n          },\n        },\n      ]\n    `);\n    expect(matchRoutes(routes, \"/whatever\")).toMatchInlineSnapshot(`\n      [\n        {\n          \"params\": {\n            \"foo-bar\": \"whatever\",\n          },\n          \"pathname\": \"/whatever\",\n          \"pathnameBase\": \"/whatever\",\n          \"route\": {\n            \"path\": \":foo-bar\",\n          },\n        },\n      ]\n    `);\n  });\n\n  test(\"dynamic segment params match word with dashes `(\\\\w-)+`\", () => {\n    expect(\n      matchPath(\"/sitemap/:lang.xml\", \"/sitemap/blah.xml\")?.params,\n    ).toStrictEqual({ lang: \"blah\" });\n    expect(\n      matchPath(\"/sitemap/:lang.xml\", \"/sitemap/blah\")?.params,\n    ).toStrictEqual(undefined);\n    expect(\n      matchPath(\"/sitemap/:lang.:xml\", \"/sitemap/blah.:xml\")?.params,\n    ).toStrictEqual({ lang: \"blah\" });\n    expect(\n      matchPath(\"/sitemap/:lang.:xml\", \"/sitemap/blah.pdf\")?.params,\n    ).toStrictEqual(undefined);\n    expect(\n      matchPath(\"/sitemap/:lang?.xml\", \"/sitemap/.xml\")?.params,\n    ).toStrictEqual({ lang: undefined });\n    expect(\n      matchPath(\"/sitemap/:lang?.xml\", \"/sitemap/en.xml\")?.params,\n    ).toStrictEqual({ lang: \"en\" });\n    expect(matchPath(\"/sitemap/:lang?.xml\", \"/sitemap.xml\")).toBeNull();\n  });\n});\n\ndescribe(\"path matching with a basename\", () => {\n  let routes = [\n    {\n      path: \"/users/:userId\",\n      children: [\n        {\n          path: \"subjects\",\n          children: [\n            {\n              path: \":courseId\",\n            },\n          ],\n        },\n      ],\n    },\n  ];\n\n  test(\"top-level route\", () => {\n    let location = { pathname: \"/users/michael\" };\n    let matches = matchRoutes(routes, location);\n\n    expect(matches).not.toBeNull();\n    expect(matches).toHaveLength(1);\n    expect(matches).toMatchObject([\n      {\n        params: { userId: \"michael\" },\n        pathname: \"/users/michael\",\n        pathnameBase: \"/users/michael\",\n      },\n    ]);\n  });\n\n  test(\"deeply nested route\", () => {\n    let location = { pathname: \"/users/michael/subjects/react\" };\n    let matches = matchRoutes(routes, location);\n\n    expect(matches).not.toBeNull();\n    expect(matches).toHaveLength(3);\n    expect(matches).toMatchObject([\n      {\n        params: { userId: \"michael\", courseId: \"react\" },\n        pathname: \"/users/michael\",\n        pathnameBase: \"/users/michael\",\n      },\n      {\n        params: { userId: \"michael\", courseId: \"react\" },\n        pathname: \"/users/michael/subjects\",\n        pathnameBase: \"/users/michael/subjects\",\n      },\n      {\n        params: { userId: \"michael\", courseId: \"react\" },\n        pathname: \"/users/michael/subjects/react\",\n        pathnameBase: \"/users/michael/subjects/react\",\n      },\n    ]);\n  });\n});\n\ndescribe(\"path matching with splats\", () => {\n  describe(\"splat after /\", () => {\n    let routes = [{ path: \"users/:id/files/*\" }];\n\n    it(\"finds the correct match\", () => {\n      let match = matchRoutes(routes, \"/users/mj/files/secrets.txt\")!;\n\n      expect(match).not.toBeNull();\n      expect(match[0]).toMatchObject({\n        params: { id: \"mj\", \"*\": \"secrets.txt\" },\n        pathname: \"/users/mj/files/secrets.txt\",\n        pathnameBase: \"/users/mj/files\",\n      });\n    });\n\n    describe(\"when other characters come before the /\", () => {\n      it(\"does not find a match\", () => {\n        let match = matchRoutes(routes, \"/users/mj/filesssss/secrets.txt\");\n        expect(match).toBeNull();\n      });\n    });\n  });\n\n  test(\"parent route with splat\", () => {\n    let routes = [\n      { path: \"users/:id/files/*\", children: [{ path: \"secrets.txt\" }] },\n    ];\n    let match = matchRoutes(routes, \"/users/mj/files/secrets.txt\")!;\n\n    expect(match).not.toBeNull();\n    expect(match[0]).toMatchObject({\n      params: { id: \"mj\", \"*\": \"secrets.txt\" },\n      pathname: \"/users/mj/files/secrets.txt\",\n      pathnameBase: \"/users/mj/files\",\n    });\n    expect(match[1]).toMatchObject({\n      params: { id: \"mj\", \"*\": \"secrets.txt\" },\n      pathname: \"/users/mj/files/secrets.txt\",\n    });\n  });\n\n  test(\"multiple nested routes\", () => {\n    let routes = [\n      { path: \"*\", children: [{ path: \"*\", children: [{ path: \"*\" }] }] },\n    ];\n    let match = matchRoutes(routes, \"/one/two/three\")!;\n\n    expect(match).not.toBeNull();\n    expect(match[0]).toMatchObject({\n      params: { \"*\": \"one/two/three\" },\n      pathname: \"/one/two/three\",\n      pathnameBase: \"/\",\n    });\n    expect(match[1]).toMatchObject({\n      params: { \"*\": \"one/two/three\" },\n      pathname: \"/one/two/three\",\n      pathnameBase: \"/\",\n    });\n    expect(match[2]).toMatchObject({\n      params: { \"*\": \"one/two/three\" },\n      pathname: \"/one/two/three\",\n      pathnameBase: \"/\",\n    });\n  });\n\n  test(\"nested routes with partial matching\", () => {\n    let routes = [\n      { path: \"/\", children: [{ path: \"courses\", children: [{ path: \"*\" }] }] },\n    ];\n    let match = matchRoutes(routes, \"/courses/abc\");\n\n    expect(match).not.toBeNull();\n    expect(match).toHaveLength(3);\n    expect(match![0]).toMatchObject({\n      params: { \"*\": \"abc\" },\n      pathname: \"/\",\n      pathnameBase: \"/\",\n    });\n    expect(match![1]).toMatchObject({\n      params: { \"*\": \"abc\" },\n      pathname: \"/courses\",\n      pathnameBase: \"/courses\",\n    });\n    expect(match![2]).toMatchObject({\n      params: { \"*\": \"abc\" },\n      pathname: \"/courses/abc\",\n      pathnameBase: \"/courses\",\n    });\n  });\n\n  test(\"does not support partial path matching with named parameters\", () => {\n    let routes = [{ path: \"/prefix:id\" }];\n    expect(matchRoutes(routes, \"/prefix:id\")).toMatchInlineSnapshot(`\n      [\n        {\n          \"params\": {},\n          \"pathname\": \"/prefix:id\",\n          \"pathnameBase\": \"/prefix:id\",\n          \"route\": {\n            \"path\": \"/prefix:id\",\n          },\n        },\n      ]\n    `);\n    expect(matchRoutes(routes, \"/prefixabc\")).toEqual(null);\n    expect(matchRoutes(routes, \"/prefix/abc\")).toEqual(null);\n  });\n\n  test(\"does not support partial path matching with splat parameters\", () => {\n    let consoleWarn = jest.spyOn(console, \"warn\").mockImplementation(() => {});\n    let routes = [{ path: \"/prefix*\" }];\n    expect(matchRoutes(routes, \"/prefix/abc\")).toMatchInlineSnapshot(`\n      [\n        {\n          \"params\": {\n            \"*\": \"abc\",\n          },\n          \"pathname\": \"/prefix/abc\",\n          \"pathnameBase\": \"/prefix\",\n          \"route\": {\n            \"path\": \"/prefix*\",\n          },\n        },\n      ]\n    `);\n    expect(matchRoutes(routes, \"/prefixabc\")).toMatchInlineSnapshot(`null`);\n\n    // Should warn on each invocation of matchRoutes\n    expect(consoleWarn.mock.calls).toMatchInlineSnapshot(`\n      [\n        [\n          \"Route path \"/prefix*\" will be treated as if it were \"/prefix/*\" because the \\`*\\` character must always follow a \\`/\\` in the pattern. To get rid of this warning, please change the route path to \"/prefix/*\".\",\n        ],\n        [\n          \"Route path \"/prefix*\" will be treated as if it were \"/prefix/*\" because the \\`*\\` character must always follow a \\`/\\` in the pattern. To get rid of this warning, please change the route path to \"/prefix/*\".\",\n        ],\n      ]\n    `);\n\n    consoleWarn.mockRestore();\n  });\n});\n\ndescribe(\"path matching with optional segments\", () => {\n  test(\"optional static segment at the start of the path\", () => {\n    let routes = [\n      {\n        path: \"/en?/abc\",\n      },\n    ];\n\n    expect(pickPathsAndParams(routes, \"/\")).toEqual(null);\n    expect(pickPathsAndParams(routes, \"/abc\")).toEqual([\n      {\n        path: \"/en?/abc\",\n        params: {},\n      },\n    ]);\n    expect(pickPathsAndParams(routes, \"/en/abc\")).toEqual([\n      {\n        path: \"/en?/abc\",\n        params: {},\n      },\n    ]);\n    expect(pickPathsAndParams(routes, \"/en/abc/bar\")).toEqual(null);\n  });\n\n  test(\"optional static segment at the end of the path\", () => {\n    let routes = [\n      {\n        path: \"/nested/one?/two?\",\n      },\n    ];\n\n    expect(pickPathsAndParams(routes, \"/nested\")).toEqual([\n      {\n        path: \"/nested/one?/two?\",\n        params: {},\n      },\n    ]);\n    expect(pickPathsAndParams(routes, \"/nested/one\")).toEqual([\n      {\n        path: \"/nested/one?/two?\",\n        params: {},\n      },\n    ]);\n    expect(pickPathsAndParams(routes, \"/nested/one/two\")).toEqual([\n      {\n        path: \"/nested/one?/two?\",\n        params: {},\n      },\n    ]);\n    expect(pickPathsAndParams(routes, \"/nested/one/two/baz\")).toEqual(null);\n  });\n\n  test(\"intercalated optional static segments\", () => {\n    let routes = [\n      {\n        path: \"/nested/one?/two/three?\",\n      },\n    ];\n\n    expect(pickPathsAndParams(routes, \"/nested\")).toEqual(null);\n    expect(pickPathsAndParams(routes, \"/nested/one\")).toEqual(null);\n    expect(pickPathsAndParams(routes, \"/nested/two\")).toEqual([\n      {\n        path: \"/nested/one?/two/three?\",\n        params: {},\n      },\n    ]);\n    expect(pickPathsAndParams(routes, \"/nested/one/two\")).toEqual([\n      {\n        path: \"/nested/one?/two/three?\",\n        params: {},\n      },\n    ]);\n    expect(pickPathsAndParams(routes, \"/nested/one/two/three\")).toEqual([\n      {\n        path: \"/nested/one?/two/three?\",\n        params: {},\n      },\n    ]);\n  });\n\n  test(\"optional static segment in nested routes\", () => {\n    let nested = [\n      {\n        path: \"/en?\",\n        children: [\n          {\n            path: \"abc\",\n          },\n        ],\n      },\n    ];\n\n    expect(pickPathsAndParams(nested, \"/en/abc\")).toEqual([\n      { path: \"/en?\", params: {} },\n      { path: \"abc\", params: {} },\n    ]);\n  });\n\n  test(\"optional static segments in nested absolute routes (leading)\", () => {\n    let nested = [\n      {\n        path: \"/en?\",\n        children: [\n          {\n            path: \"/en?/abc\",\n            children: [\n              {\n                path: \"/en?/abc/def\",\n              },\n            ],\n          },\n        ],\n      },\n    ];\n\n    expect(pickPathsAndParams(nested, \"/en/abc\")).toEqual([\n      { path: \"/en?\", params: {} },\n      { path: \"/en?/abc\", params: {} },\n    ]);\n    expect(pickPathsAndParams(nested, \"/abc\")).toEqual([\n      { path: \"/en?\", params: {} },\n      { path: \"/en?/abc\", params: {} },\n    ]);\n    expect(pickPathsAndParams(nested, \"/en/abc/def\")).toEqual([\n      { path: \"/en?\", params: {} },\n      { path: \"/en?/abc\", params: {} },\n      { path: \"/en?/abc/def\", params: {} },\n    ]);\n    expect(pickPathsAndParams(nested, \"/abc/def\")).toEqual([\n      { path: \"/en?\", params: {} },\n      { path: \"/en?/abc\", params: {} },\n      { path: \"/en?/abc/def\", params: {} },\n    ]);\n  });\n\n  test(\"optional static segment in nested absolute routes (middle)\", () => {\n    let nested = [\n      {\n        path: \"/en\",\n        children: [\n          {\n            path: \"/en/abc?\",\n            children: [\n              {\n                path: \"/en/abc?/def\",\n              },\n            ],\n          },\n        ],\n      },\n    ];\n\n    expect(pickPathsAndParams(nested, \"/en/abc\")).toEqual([\n      { path: \"/en\", params: {} },\n      { path: \"/en/abc?\", params: {} },\n    ]);\n    expect(pickPathsAndParams(nested, \"/en/abc/def\")).toEqual([\n      { path: \"/en\", params: {} },\n      { path: \"/en/abc?\", params: {} },\n      { path: \"/en/abc?/def\", params: {} },\n    ]);\n    expect(pickPathsAndParams(nested, \"/en/def\")).toEqual([\n      { path: \"/en\", params: {} },\n      { path: \"/en/abc?\", params: {} },\n      { path: \"/en/abc?/def\", params: {} },\n    ]);\n  });\n});\n\ndescribe(\"path matching with optional dynamic segments\", () => {\n  test(\"optional params at the start of the path\", () => {\n    let routes = [\n      {\n        path: \"/:lang?/abc\",\n      },\n    ];\n\n    expect(pickPathsAndParams(routes, \"/\")).toEqual(null);\n    expect(pickPathsAndParams(routes, \"/abc\")).toEqual([\n      {\n        path: \"/:lang?/abc\",\n        params: {},\n      },\n    ]);\n    expect(pickPathsAndParams(routes, \"/en/abc\")).toEqual([\n      {\n        path: \"/:lang?/abc\",\n        params: { lang: \"en\" },\n      },\n    ]);\n    expect(pickPathsAndParams(routes, \"/en/abc/bar\")).toEqual(null);\n  });\n\n  test(\"optional params at the end of the path\", () => {\n    let manualRoutes = [\n      {\n        path: \"/nested\",\n      },\n      {\n        path: \"/nested/:one\",\n      },\n      {\n        path: \"/nested/:one/:two\",\n      },\n      {\n        path: \"/nested/:one/:two/:three\",\n      },\n      {\n        path: \"/nested/:one/:two/:three/:four\",\n      },\n    ];\n    let routes = [\n      {\n        path: \"/nested/:one?/:two?/:three?/:four?\",\n      },\n    ];\n\n    expect(pickPathsAndParams(manualRoutes, \"/nested\")).toEqual([\n      {\n        path: \"/nested\",\n        params: {},\n      },\n    ]);\n    expect(pickPathsAndParams(routes, \"/nested\")).toEqual([\n      {\n        path: \"/nested/:one?/:two?/:three?/:four?\",\n        params: {},\n      },\n    ]);\n    expect(pickPathsAndParams(manualRoutes, \"/nested/foo\")).toEqual([\n      {\n        path: \"/nested/:one\",\n        params: { one: \"foo\" },\n      },\n    ]);\n    expect(pickPathsAndParams(routes, \"/nested/foo\")).toEqual([\n      {\n        path: \"/nested/:one?/:two?/:three?/:four?\",\n        params: { one: \"foo\" },\n      },\n    ]);\n    expect(pickPathsAndParams(manualRoutes, \"/nested/foo/bar\")).toEqual([\n      {\n        path: \"/nested/:one/:two\",\n        params: { one: \"foo\", two: \"bar\" },\n      },\n    ]);\n    expect(pickPathsAndParams(routes, \"/nested/foo/bar\")).toEqual([\n      {\n        path: \"/nested/:one?/:two?/:three?/:four?\",\n        params: { one: \"foo\", two: \"bar\" },\n      },\n    ]);\n    expect(pickPathsAndParams(manualRoutes, \"/nested/foo/bar/baz\")).toEqual([\n      {\n        path: \"/nested/:one/:two/:three\",\n        params: { one: \"foo\", two: \"bar\", three: \"baz\" },\n      },\n    ]);\n    expect(pickPathsAndParams(routes, \"/nested/foo/bar/baz\")).toEqual([\n      {\n        path: \"/nested/:one?/:two?/:three?/:four?\",\n        params: { one: \"foo\", two: \"bar\", three: \"baz\" },\n      },\n    ]);\n    expect(pickPathsAndParams(manualRoutes, \"/nested/foo/bar/baz/qux\")).toEqual(\n      [\n        {\n          path: \"/nested/:one/:two/:three/:four\",\n          params: { one: \"foo\", two: \"bar\", three: \"baz\", four: \"qux\" },\n        },\n      ],\n    );\n    expect(pickPathsAndParams(routes, \"/nested/foo/bar/baz/qux\")).toEqual([\n      {\n        path: \"/nested/:one?/:two?/:three?/:four?\",\n        params: { one: \"foo\", two: \"bar\", three: \"baz\", four: \"qux\" },\n      },\n    ]);\n    expect(\n      pickPathsAndParams(manualRoutes, \"/nested/foo/bar/baz/qux/zod\"),\n    ).toEqual(null);\n    expect(pickPathsAndParams(routes, \"/nested/foo/bar/baz/qux/zod\")).toEqual(\n      null,\n    );\n  });\n\n  test(\"intercalated optional params\", () => {\n    let routes = [\n      {\n        path: \"/nested/:one?/two/:three?\",\n      },\n    ];\n\n    expect(pickPathsAndParams(routes, \"/nested\")).toEqual(null);\n    expect(pickPathsAndParams(routes, \"/nested/foo\")).toEqual(null);\n    expect(pickPathsAndParams(routes, \"/nested/two\")).toEqual([\n      {\n        path: \"/nested/:one?/two/:three?\",\n        params: {},\n      },\n    ]);\n    expect(pickPathsAndParams(routes, \"/nested/foo/two\")).toEqual([\n      {\n        path: \"/nested/:one?/two/:three?\",\n        params: { one: \"foo\" },\n      },\n    ]);\n    expect(pickPathsAndParams(routes, \"/nested/foo/two/bar\")).toEqual([\n      {\n        path: \"/nested/:one?/two/:three?\",\n        params: { one: \"foo\", three: \"bar\" },\n      },\n    ]);\n  });\n\n  test(\"consecutive optional dynamic segments in nested routes\", () => {\n    let manuallyExploded = [\n      {\n        path: \":one\",\n        children: [\n          {\n            path: \":two\",\n            children: [\n              {\n                path: \":three\",\n              },\n              {\n                path: \"\",\n              },\n            ],\n          },\n          {\n            path: \"\",\n            children: [\n              {\n                path: \":three\",\n              },\n              {\n                path: \"\",\n              },\n            ],\n          },\n        ],\n      },\n      {\n        path: \"\",\n        children: [\n          {\n            path: \":two\",\n            children: [\n              {\n                path: \":three\",\n              },\n              {\n                path: \"\",\n              },\n            ],\n          },\n          {\n            path: \"\",\n            children: [\n              {\n                path: \":three\",\n              },\n              {\n                path: \"\",\n              },\n            ],\n          },\n        ],\n      },\n    ];\n\n    let optional = [\n      {\n        path: \":one?\",\n        children: [\n          {\n            path: \":two?\",\n            children: [\n              {\n                path: \":three?\",\n              },\n            ],\n          },\n        ],\n      },\n    ];\n\n    expect(pickPathsAndParams(manuallyExploded, \"/uno\")).toEqual([\n      {\n        path: \":one\",\n        params: { one: \"uno\" },\n      },\n      {\n        params: { one: \"uno\" },\n      },\n      {\n        params: { one: \"uno\" },\n      },\n    ]);\n    expect(pickPathsAndParams(optional, \"/uno\")).toEqual([\n      {\n        path: \":one?\",\n        params: { one: \"uno\" },\n      },\n      {\n        params: { one: \"uno\" },\n        path: \":two?\",\n      },\n      {\n        params: { one: \"uno\" },\n        path: \":three?\",\n      },\n    ]);\n\n    expect(pickPathsAndParams(manuallyExploded, \"/uno/dos\")).toEqual([\n      {\n        path: \":one\",\n        params: { one: \"uno\", two: \"dos\" },\n      },\n      {\n        params: { one: \"uno\", two: \"dos\" },\n        path: \":two\",\n      },\n      {\n        params: { one: \"uno\", two: \"dos\" },\n      },\n    ]);\n    expect(pickPathsAndParams(optional, \"/uno/dos\")).toEqual([\n      {\n        path: \":one?\",\n        params: { one: \"uno\", two: \"dos\" },\n      },\n      {\n        params: { one: \"uno\", two: \"dos\" },\n        path: \":two?\",\n      },\n      {\n        params: { one: \"uno\", two: \"dos\" },\n        path: \":three?\",\n      },\n    ]);\n\n    expect(pickPathsAndParams(manuallyExploded, \"/uno/dos/tres\")).toEqual([\n      {\n        path: \":one\",\n        params: { one: \"uno\", two: \"dos\", three: \"tres\" },\n      },\n      {\n        params: { one: \"uno\", two: \"dos\", three: \"tres\" },\n        path: \":two\",\n      },\n      {\n        params: { one: \"uno\", two: \"dos\", three: \"tres\" },\n        path: \":three\",\n      },\n    ]);\n    expect(pickPathsAndParams(optional, \"/uno/dos/tres\")).toEqual([\n      {\n        path: \":one?\",\n        params: { one: \"uno\", two: \"dos\", three: \"tres\" },\n      },\n      {\n        params: { one: \"uno\", two: \"dos\", three: \"tres\" },\n        path: \":two?\",\n      },\n      {\n        params: { one: \"uno\", two: \"dos\", three: \"tres\" },\n        path: \":three?\",\n      },\n    ]);\n\n    expect(pickPathsAndParams(manuallyExploded, \"/uno/dos/tres/nope\")).toEqual(\n      null,\n    );\n    expect(pickPathsAndParams(optional, \"/uno/dos/tres/nope\")).toEqual(null);\n  });\n\n  test(\"consecutive optional static + dynamic segments in nested routes\", () => {\n    let nested = [\n      {\n        path: \"/one/:two?\",\n        children: [\n          {\n            path: \"three/:four?\",\n            children: [\n              {\n                path: \":five?\",\n              },\n            ],\n          },\n        ],\n      },\n    ];\n    expect(pickPathsAndParams(nested, \"/one/dos/three/cuatro/cinco\")).toEqual([\n      {\n        path: \"/one/:two?\",\n        params: { two: \"dos\", four: \"cuatro\", five: \"cinco\" },\n      },\n      {\n        path: \"three/:four?\",\n        params: { two: \"dos\", four: \"cuatro\", five: \"cinco\" },\n      },\n      { path: \":five?\", params: { two: \"dos\", four: \"cuatro\", five: \"cinco\" } },\n    ]);\n    expect(pickPathsAndParams(nested, \"/one/dos/three/cuatro\")).toEqual([\n      {\n        path: \"/one/:two?\",\n        params: { two: \"dos\", four: \"cuatro\" },\n      },\n      {\n        path: \"three/:four?\",\n        params: { two: \"dos\", four: \"cuatro\" },\n      },\n      {\n        path: \":five?\",\n        params: { two: \"dos\", four: \"cuatro\" },\n      },\n    ]);\n    expect(pickPathsAndParams(nested, \"/one/dos/three\")).toEqual([\n      {\n        path: \"/one/:two?\",\n        params: { two: \"dos\" },\n      },\n      {\n        path: \"three/:four?\",\n        params: { two: \"dos\" },\n      },\n      // Matches into 5 because it's just like if we did path=\"\"\n      {\n        path: \":five?\",\n        params: { two: \"dos\" },\n      },\n    ]);\n    expect(pickPathsAndParams(nested, \"/one/dos\")).toEqual([\n      {\n        path: \"/one/:two?\",\n        params: { two: \"dos\" },\n      },\n    ]);\n    expect(pickPathsAndParams(nested, \"/one\")).toEqual([\n      {\n        path: \"/one/:two?\",\n        params: {},\n      },\n    ]);\n    expect(pickPathsAndParams(nested, \"/one/three/cuatro/cinco\")).toEqual([\n      {\n        path: \"/one/:two?\",\n        params: { four: \"cuatro\", five: \"cinco\" },\n      },\n      {\n        path: \"three/:four?\",\n        params: { four: \"cuatro\", five: \"cinco\" },\n      },\n      { path: \":five?\", params: { four: \"cuatro\", five: \"cinco\" } },\n    ]);\n    expect(pickPathsAndParams(nested, \"/one/three/cuatro\")).toEqual([\n      {\n        path: \"/one/:two?\",\n        params: { four: \"cuatro\" },\n      },\n      {\n        path: \"three/:four?\",\n        params: { four: \"cuatro\" },\n      },\n      {\n        path: \":five?\",\n        params: { four: \"cuatro\" },\n      },\n    ]);\n    expect(pickPathsAndParams(nested, \"/one/three\")).toEqual([\n      {\n        path: \"/one/:two?\",\n        params: {},\n      },\n      {\n        path: \"three/:four?\",\n        params: {},\n      },\n      // Matches into 5 because it's just like if we did path=\"\"\n      {\n        path: \":five?\",\n        params: {},\n      },\n    ]);\n    expect(pickPathsAndParams(nested, \"/one\")).toEqual([\n      {\n        path: \"/one/:two?\",\n        params: {},\n      },\n    ]);\n  });\n\n  test(\"prefers optional static over optional dynamic segments\", () => {\n    let nested = [\n      {\n        path: \"/one\",\n        children: [\n          {\n            path: \":param?\",\n            children: [\n              {\n                path: \"three\",\n              },\n            ],\n          },\n          {\n            path: \"two?\",\n            children: [\n              {\n                path: \"three\",\n              },\n            ],\n          },\n        ],\n      },\n    ];\n\n    // static `two` segment should win\n    expect(pickPathsAndParams(nested, \"/one/two/three\")).toEqual([\n      {\n        params: {},\n        path: \"/one\",\n      },\n      {\n        params: {},\n        path: \"two?\",\n      },\n      {\n        params: {},\n        path: \"three\",\n      },\n    ]);\n\n    // fall back to param when no static match\n    expect(pickPathsAndParams(nested, \"/one/not-two/three\")).toEqual([\n      {\n        params: {\n          param: \"not-two\",\n        },\n        path: \"/one\",\n      },\n      {\n        params: {\n          param: \"not-two\",\n        },\n        path: \":param?\",\n      },\n      {\n        params: {\n          param: \"not-two\",\n        },\n        path: \"three\",\n      },\n    ]);\n\n    // No optional segment provided - earlier \"dup\" route should win\n    expect(pickPathsAndParams(nested, \"/one/three\")).toEqual([\n      {\n        params: {},\n        path: \"/one\",\n      },\n      {\n        params: {},\n        path: \":param?\",\n      },\n      {\n        params: {},\n        path: \"three\",\n      },\n    ]);\n  });\n\n  test(\"prefers index routes over optional static segments\", () => {\n    let nested = [\n      {\n        path: \"/one\",\n        children: [\n          {\n            path: \":param?\",\n            children: [\n              {\n                path: \"three?\",\n              },\n              {\n                index: true,\n              },\n            ],\n          },\n        ],\n      },\n    ];\n\n    expect(pickPathsAndParams(nested, \"/one/two\")).toEqual([\n      {\n        params: {\n          param: \"two\",\n        },\n        path: \"/one\",\n      },\n      {\n        params: {\n          param: \"two\",\n        },\n        path: \":param?\",\n      },\n      {\n        index: true,\n        params: {\n          param: \"two\",\n        },\n      },\n    ]);\n    expect(pickPathsAndParams(nested, \"/one\")).toEqual([\n      {\n        params: {},\n        path: \"/one\",\n      },\n      {\n        params: {},\n        path: \":param?\",\n      },\n      {\n        index: true,\n        params: {},\n      },\n    ]);\n  });\n\n  test(\"prefers index routes over optional dynamic segments\", () => {\n    let nested = [\n      {\n        path: \"/one\",\n        children: [\n          {\n            path: \":param?\",\n            children: [\n              {\n                path: \":three?\",\n              },\n              {\n                index: true,\n              },\n            ],\n          },\n        ],\n      },\n    ];\n\n    expect(pickPathsAndParams(nested, \"/one/two\")).toEqual([\n      {\n        params: {\n          param: \"two\",\n        },\n        path: \"/one\",\n      },\n      {\n        params: {\n          param: \"two\",\n        },\n        path: \":param?\",\n      },\n      {\n        index: true,\n        params: {\n          param: \"two\",\n        },\n      },\n    ]);\n    expect(pickPathsAndParams(nested, \"/one\")).toEqual([\n      {\n        params: {},\n        path: \"/one\",\n      },\n      {\n        params: {},\n        path: \":param?\",\n      },\n      {\n        index: true,\n        params: {},\n      },\n    ]);\n  });\n});\n"
  },
  {
    "path": "packages/react-router/__tests__/react-transitions-test.tsx",
    "content": "import \"@testing-library/jest-dom\";\nimport {\n  act,\n  fireEvent,\n  render,\n  screen,\n  waitFor,\n} from \"@testing-library/react\";\nimport * as React from \"react\";\nimport {\n  Outlet,\n  RouterProvider,\n  createMemoryRouter,\n  useLoaderData,\n  useNavigation,\n  useRevalidator,\n} from \"react-router\";\n\nimport {\n  Form,\n  Link,\n  createBrowserRouter,\n  useActionData,\n  useFetcher,\n  useNavigate,\n  useSubmit,\n} from \"../index\";\nimport { createDeferred, tick } from \"./router/utils/utils\";\nimport getWindow from \"./utils/getWindow\";\n\ndescribe(\"react transitions\", () => {\n  describe(\"<RouterProvider unstable_useTransitions={undefined} />\", () => {\n    it(\"normal navigations surface all updates\", async () => {\n      let loaderDfd = createDeferred();\n      let router = createMemoryRouter([\n        {\n          path: \"/\",\n          Component() {\n            let navigation = useNavigation();\n            let [count, setCount] = React.useState(0);\n            return (\n              <>\n                <p>{`Navigation:${navigation.state}`}</p>\n                <button id=\"increment\" onClick={() => setCount((c) => c + 1)}>\n                  {`Increment:${count}`}\n                </button>\n                <Outlet />\n              </>\n            );\n          },\n          children: [\n            {\n              index: true,\n              Component() {\n                let navigate = useNavigate();\n                return (\n                  <button id=\"link\" onClick={() => navigate(\"/page\")}>\n                    Go to page\n                  </button>\n                );\n              },\n            },\n            {\n              path: \"page\",\n              loader: () => loaderDfd.promise,\n              Component() {\n                return <h1>{useLoaderData()}</h1>;\n              },\n            },\n          ],\n        },\n      ]);\n\n      let { container } = render(<RouterProvider router={router} />);\n\n      await waitFor(() => screen.getByText(\"Go to page\"));\n      expect(screen.getByText(\"Navigation:idle\")).toBeDefined();\n      expect(screen.getByText(\"Increment:0\")).toBeDefined();\n\n      await fireEvent.click(container.querySelector(\"#link\")!);\n      // Without useOptimistic under the hood, our mid-navigation state updates don't surface\n      await waitFor(() => screen.getByText(\"Navigation:loading\"));\n      expect(screen.getByText(\"Increment:0\")).toBeDefined();\n\n      await fireEvent.click(container.querySelector(\"#increment\")!);\n      await waitFor(() => screen.getByText(\"Increment:1\"));\n      await fireEvent.click(container.querySelector(\"#increment\")!);\n      await waitFor(() => screen.getByText(\"Increment:2\"));\n      expect(screen.getByText(\"Navigation:loading\")).toBeDefined();\n\n      loaderDfd.resolve(\"Page\");\n      await waitFor(() => screen.getByText(\"Page\"));\n      await waitFor(() => screen.getByText(\"Increment:2\"));\n      expect(screen.getByText(\"Navigation:idle\")).toBeDefined();\n    });\n\n    it(\"normal submissions surface all updates\", async () => {\n      let actionDfd = createDeferred();\n      let loaderDfd = createDeferred();\n      let router = createMemoryRouter([\n        {\n          path: \"/\",\n          Component() {\n            let navigation = useNavigation();\n            let [count, setCount] = React.useState(0);\n            return (\n              <>\n                <p>{`Navigation:${navigation.state}`}</p>\n                <button id=\"increment\" onClick={() => setCount((c) => c + 1)}>\n                  {`Increment:${count}`}\n                </button>\n                <Outlet />\n              </>\n            );\n          },\n          children: [\n            {\n              index: true,\n              Component() {\n                let submit = useSubmit();\n                return (\n                  <button\n                    id=\"submit\"\n                    onClick={() =>\n                      submit({}, { method: \"post\", action: \"/page\" })\n                    }\n                  >\n                    Submit\n                  </button>\n                );\n              },\n            },\n            {\n              path: \"page\",\n              action: () => actionDfd.promise,\n              loader: () => loaderDfd.promise,\n              Component() {\n                return <h1>{useLoaderData()}</h1>;\n              },\n            },\n          ],\n        },\n      ]);\n\n      let { container } = render(<RouterProvider router={router} />);\n\n      await waitFor(() => screen.getByText(\"Submit\"));\n      expect(screen.getByText(\"Navigation:idle\")).toBeDefined();\n      expect(screen.getByText(\"Increment:0\")).toBeDefined();\n\n      await fireEvent.click(container.querySelector(\"#submit\")!);\n      await waitFor(() => screen.getByText(\"Navigation:submitting\"));\n      expect(screen.getByText(\"Increment:0\")).toBeDefined();\n\n      await fireEvent.click(container.querySelector(\"#increment\")!);\n      await waitFor(() => screen.getByText(\"Increment:1\"));\n      await fireEvent.click(container.querySelector(\"#increment\")!);\n      await waitFor(() => screen.getByText(\"Increment:2\"));\n      expect(screen.getByText(\"Navigation:submitting\")).toBeDefined();\n\n      actionDfd.resolve(\"Action\");\n      await waitFor(() => screen.getByText(\"Navigation:loading\"));\n      await fireEvent.click(container.querySelector(\"#increment\")!);\n      await waitFor(() => screen.getByText(\"Increment:3\"));\n\n      loaderDfd.resolve(\"Page\");\n      await waitFor(() => screen.getByText(\"Page\"));\n      await waitFor(() => screen.getByText(\"Increment:3\"));\n      expect(screen.getByText(\"Navigation:idle\")).toBeDefined();\n    });\n\n    it(\"navigations can be manually wrapped in startTransition (buggy optimistic behavior)\", async () => {\n      let loaderDfd = createDeferred();\n      let router = createMemoryRouter([\n        {\n          path: \"/\",\n          Component() {\n            let navigation = useNavigation();\n            let [count, setCount] = React.useState(0);\n            return (\n              <>\n                <p>{`Navigation:${navigation.state}`}</p>\n                <button\n                  id=\"increment\"\n                  onClick={() =>\n                    React.startTransition(() => setCount((c) => c + 1))\n                  }\n                >\n                  {`Increment:${count}`}\n                </button>\n                <Outlet />\n              </>\n            );\n          },\n          children: [\n            {\n              index: true,\n              Component() {\n                let navigate = useNavigate();\n                return (\n                  <button\n                    id=\"link\"\n                    onClick={() => {\n                      // @ts-expect-error - Needs react 19 types\n                      React.startTransition(() => navigate(\"/page\"));\n                    }}\n                  >\n                    Go to page\n                  </button>\n                );\n              },\n            },\n            {\n              path: \"page\",\n              loader: () => loaderDfd.promise,\n              Component() {\n                return <h1>{useLoaderData()}</h1>;\n              },\n            },\n          ],\n        },\n      ]);\n\n      let { container } = render(<RouterProvider router={router} />);\n\n      await waitFor(() => screen.getByText(\"Go to page\"));\n      expect(screen.getByText(\"Navigation:idle\")).toBeDefined();\n      expect(screen.getByText(\"Increment:0\")).toBeDefined();\n\n      // Without useOptimistic under the hood, our mid-navigation state updates\n      // don't surface\n      await fireEvent.click(container.querySelector(\"#link\")!);\n      await waitFor(() => screen.getByText(\"Navigation:idle\"));\n      expect(screen.getByText(\"Increment:0\")).toBeDefined();\n\n      await fireEvent.click(container.querySelector(\"#increment\")!);\n      await waitFor(() => screen.getByText(\"Increment:0\"));\n      await fireEvent.click(container.querySelector(\"#increment\")!);\n      await waitFor(() => screen.getByText(\"Increment:0\"));\n      expect(screen.getByText(\"Navigation:idle\")).toBeDefined();\n\n      loaderDfd.resolve(\"Page\");\n      await waitFor(() => screen.getByText(\"Page\"));\n      await waitFor(() => screen.getByText(\"Increment:2\"));\n      expect(screen.getByText(\"Navigation:idle\")).toBeDefined();\n    });\n\n    it(\"submissions can be manually wrapped in startTransition (buggy optimistic behavior)\", async () => {\n      let actionDfd = createDeferred();\n      let loaderDfd = createDeferred();\n      let router = createMemoryRouter([\n        {\n          path: \"/\",\n          Component() {\n            let navigation = useNavigation();\n            let [count, setCount] = React.useState(0);\n            return (\n              <>\n                <p>{`Navigation:${navigation.state}`}</p>\n                <button\n                  id=\"increment\"\n                  onClick={() =>\n                    React.startTransition(() => setCount((c) => c + 1))\n                  }\n                >\n                  {`Increment:${count}`}\n                </button>\n                <Outlet />\n              </>\n            );\n          },\n          children: [\n            {\n              index: true,\n              Component() {\n                let submit = useSubmit();\n                return (\n                  <button\n                    id=\"submit\"\n                    onClick={() => {\n                      React.startTransition(() =>\n                        // @ts-expect-error - Needs react 19 types\n                        submit({}, { method: \"post\", action: \"/page\" }),\n                      );\n                    }}\n                  >\n                    Submit\n                  </button>\n                );\n              },\n            },\n            {\n              path: \"page\",\n              action: () => actionDfd.promise,\n              loader: () => loaderDfd.promise,\n              Component() {\n                return <h1>{useLoaderData()}</h1>;\n              },\n            },\n          ],\n        },\n      ]);\n\n      let { container } = render(<RouterProvider router={router} />);\n\n      await waitFor(() => screen.getByText(\"Submit\"));\n      expect(screen.getByText(\"Navigation:idle\")).toBeDefined();\n      expect(screen.getByText(\"Increment:0\")).toBeDefined();\n\n      // Without useOptimistic under the hood, our mid-navigation state updates\n      // don't surface\n      await fireEvent.click(container.querySelector(\"#submit\")!);\n      await waitFor(() => screen.getByText(\"Navigation:idle\"));\n      expect(screen.getByText(\"Increment:0\")).toBeDefined();\n\n      await fireEvent.click(container.querySelector(\"#increment\")!);\n      await waitFor(() => screen.getByText(\"Increment:0\"));\n      await fireEvent.click(container.querySelector(\"#increment\")!);\n      await waitFor(() => screen.getByText(\"Increment:0\"));\n      expect(screen.getByText(\"Navigation:idle\")).toBeDefined();\n\n      await act(() => {\n        actionDfd.resolve(\"Action\");\n      });\n      await tick();\n      await fireEvent.click(container.querySelector(\"#increment\")!);\n      await waitFor(() => screen.getByText(\"Increment:0\"));\n\n      loaderDfd.resolve(\"Page\");\n      await waitFor(() => screen.getByText(\"Page\"));\n      await waitFor(() => screen.getByText(\"Increment:3\"));\n      expect(screen.getByText(\"Navigation:idle\")).toBeDefined();\n    });\n  });\n\n  describe(\"<RouterProvider unstable_useTransitions={false} />\", () => {\n    it(\"navigations are not transition-enabled\", async () => {\n      let loaderDfd = createDeferred();\n      let router = createMemoryRouter([\n        {\n          path: \"/\",\n          Component() {\n            let navigation = useNavigation();\n            let [count, setCount] = React.useState(0);\n            return (\n              <>\n                <p>{`Navigation:${navigation.state}`}</p>\n                <button\n                  id=\"increment\"\n                  onClick={() =>\n                    React.startTransition(() => setCount((c) => c + 1))\n                  }\n                >\n                  {`Increment:${count}`}\n                </button>\n                <Outlet />\n              </>\n            );\n          },\n          children: [\n            {\n              index: true,\n              Component() {\n                let navigate = useNavigate();\n                return (\n                  <button id=\"link\" onClick={() => navigate(\"/page\")}>\n                    Go to page\n                  </button>\n                );\n              },\n            },\n            {\n              path: \"page\",\n              loader: () => loaderDfd.promise,\n              Component() {\n                return <h1>{useLoaderData()}</h1>;\n              },\n            },\n          ],\n        },\n      ]);\n\n      let { container } = render(\n        <RouterProvider router={router} unstable_useTransitions={false} />,\n      );\n\n      await waitFor(() => screen.getByText(\"Go to page\"));\n      expect(screen.getByText(\"Navigation:idle\")).toBeDefined();\n      expect(screen.getByText(\"Increment:0\")).toBeDefined();\n\n      // Without transitions enabled, all updates surface during navigation\n      await fireEvent.click(container.querySelector(\"#link\")!);\n      await waitFor(() => screen.getByText(\"Navigation:loading\"));\n      expect(screen.getByText(\"Increment:0\")).toBeDefined();\n\n      await fireEvent.click(container.querySelector(\"#increment\")!);\n      await waitFor(() => screen.getByText(\"Increment:1\"));\n      await fireEvent.click(container.querySelector(\"#increment\")!);\n      await waitFor(() => screen.getByText(\"Increment:2\"));\n      expect(screen.getByText(\"Navigation:loading\")).toBeDefined();\n\n      loaderDfd.resolve(\"Page\");\n      await waitFor(() => screen.getByText(\"Page\"));\n      await waitFor(() => screen.getByText(\"Increment:2\"));\n      expect(screen.getByText(\"Navigation:idle\")).toBeDefined();\n    });\n\n    it(\"submissions are not transition-enabled\", async () => {\n      let actionDfd = createDeferred();\n      let loaderDfd = createDeferred();\n      let router = createMemoryRouter([\n        {\n          path: \"/\",\n          Component() {\n            let navigation = useNavigation();\n            let [count, setCount] = React.useState(0);\n            return (\n              <>\n                <p>{`Navigation:${navigation.state}`}</p>\n                <button\n                  id=\"increment\"\n                  onClick={() =>\n                    React.startTransition(() => setCount((c) => c + 1))\n                  }\n                >\n                  {`Increment:${count}`}\n                </button>\n                <Outlet />\n              </>\n            );\n          },\n          children: [\n            {\n              index: true,\n              Component() {\n                let submit = useSubmit();\n                return (\n                  <button\n                    id=\"submit\"\n                    onClick={() =>\n                      submit({}, { method: \"post\", action: \"/page\" })\n                    }\n                  >\n                    Submit\n                  </button>\n                );\n              },\n            },\n            {\n              path: \"page\",\n              action: () => actionDfd.promise,\n              loader: () => loaderDfd.promise,\n              Component() {\n                return <h1>{useLoaderData()}</h1>;\n              },\n            },\n          ],\n        },\n      ]);\n\n      let { container } = render(<RouterProvider router={router} />);\n\n      await waitFor(() => screen.getByText(\"Submit\"));\n      expect(screen.getByText(\"Navigation:idle\")).toBeDefined();\n      expect(screen.getByText(\"Increment:0\")).toBeDefined();\n\n      // Without transitions enabled, all updates surface during navigation\n      await fireEvent.click(container.querySelector(\"#submit\")!);\n      await waitFor(() => screen.getByText(\"Navigation:submitting\"));\n      expect(screen.getByText(\"Increment:0\")).toBeDefined();\n\n      await fireEvent.click(container.querySelector(\"#increment\")!);\n      await waitFor(() => screen.getByText(\"Increment:1\"));\n      await fireEvent.click(container.querySelector(\"#increment\")!);\n      await waitFor(() => screen.getByText(\"Increment:2\"));\n      expect(screen.getByText(\"Navigation:submitting\")).toBeDefined();\n\n      actionDfd.resolve(\"Action\");\n      await waitFor(() => screen.getByText(\"Navigation:loading\"));\n      await fireEvent.click(container.querySelector(\"#increment\")!);\n      await waitFor(() => screen.getByText(\"Increment:3\"));\n\n      loaderDfd.resolve(\"Page\");\n      await waitFor(() => screen.getByText(\"Page\"));\n      await waitFor(() => screen.getByText(\"Increment:3\"));\n      expect(screen.getByText(\"Navigation:idle\")).toBeDefined();\n    });\n  });\n\n  describe(\"<RouterProvider unstable_useTransitions={true} />\", () => {\n    it(\"Link navigations are transition-enabled\", async () => {\n      let loaderDfd = createDeferred();\n      let router = createMemoryRouter([\n        {\n          path: \"/\",\n          Component() {\n            let navigation = useNavigation();\n            let [count, setCount] = React.useState(0);\n            return (\n              <>\n                <p>{`Navigation:${navigation.state}`}</p>\n                <button\n                  id=\"increment\"\n                  onClick={() =>\n                    React.startTransition(() => setCount((c) => c + 1))\n                  }\n                >\n                  {`Increment:${count}`}\n                </button>\n                <Outlet />\n              </>\n            );\n          },\n          children: [\n            {\n              index: true,\n              Component() {\n                let navigate = useNavigate();\n                return (\n                  <Link id=\"link\" to=\"/page\">\n                    Go to page\n                  </Link>\n                );\n              },\n            },\n            {\n              path: \"page\",\n              loader: () => loaderDfd.promise,\n              Component() {\n                return <h1>{useLoaderData()}</h1>;\n              },\n            },\n          ],\n        },\n      ]);\n\n      let { container } = render(\n        <RouterProvider router={router} unstable_useTransitions={true} />,\n      );\n\n      await waitFor(() => screen.getByText(\"Go to page\"));\n      expect(screen.getByText(\"Navigation:idle\")).toBeDefined();\n      expect(screen.getByText(\"Increment:0\")).toBeDefined();\n\n      // With transitions enabled, our updates surface via useOptimistic, but\n      // other transition-enabled updates do not\n      await fireEvent.click(container.querySelector(\"#link\")!);\n      await waitFor(() => screen.getByText(\"Navigation:loading\"));\n      expect(screen.getByText(\"Increment:0\")).toBeDefined();\n\n      await fireEvent.click(container.querySelector(\"#increment\")!);\n      await waitFor(() => screen.getByText(\"Increment:0\"));\n      await fireEvent.click(container.querySelector(\"#increment\")!);\n      await waitFor(() => screen.getByText(\"Increment:0\"));\n      expect(screen.getByText(\"Navigation:loading\")).toBeDefined();\n\n      loaderDfd.resolve(\"Page\");\n      await waitFor(() => screen.getByText(\"Page\"));\n      await waitFor(() => screen.getByText(\"Increment:2\"));\n      expect(screen.getByText(\"Navigation:idle\")).toBeDefined();\n    });\n\n    it(\"useNavigate navigations are not transition-enabled\", async () => {\n      let loaderDfd = createDeferred();\n      let router = createMemoryRouter([\n        {\n          path: \"/\",\n          Component() {\n            let navigation = useNavigation();\n            let [count, setCount] = React.useState(0);\n            return (\n              <>\n                <p>{`Navigation:${navigation.state}`}</p>\n                <button\n                  id=\"increment\"\n                  onClick={() =>\n                    React.startTransition(() => setCount((c) => c + 1))\n                  }\n                >\n                  {`Increment:${count}`}\n                </button>\n                <Outlet />\n              </>\n            );\n          },\n          children: [\n            {\n              index: true,\n              Component() {\n                let navigate = useNavigate();\n                return (\n                  <button id=\"link\" onClick={() => navigate(\"/page\")}>\n                    Go to page\n                  </button>\n                );\n              },\n            },\n            {\n              path: \"page\",\n              loader: () => loaderDfd.promise,\n              Component() {\n                return <h1>{useLoaderData()}</h1>;\n              },\n            },\n          ],\n        },\n      ]);\n\n      let { container } = render(\n        <RouterProvider router={router} unstable_useTransitions={true} />,\n      );\n\n      await waitFor(() => screen.getByText(\"Go to page\"));\n      expect(screen.getByText(\"Navigation:idle\")).toBeDefined();\n      expect(screen.getByText(\"Increment:0\")).toBeDefined();\n\n      // With transitions enabled, our updates surface via useOptimistic, but\n      // other transition-enabled updates do not\n      await fireEvent.click(container.querySelector(\"#link\")!);\n      await waitFor(() => screen.getByText(\"Navigation:loading\"));\n      expect(screen.getByText(\"Increment:0\")).toBeDefined();\n\n      await fireEvent.click(container.querySelector(\"#increment\")!);\n      await waitFor(() => screen.getByText(\"Increment:1\"));\n      await fireEvent.click(container.querySelector(\"#increment\")!);\n      await waitFor(() => screen.getByText(\"Increment:2\"));\n      expect(screen.getByText(\"Navigation:loading\")).toBeDefined();\n\n      loaderDfd.resolve(\"Page\");\n      await waitFor(() => screen.getByText(\"Page\"));\n      await waitFor(() => screen.getByText(\"Increment:2\"));\n      expect(screen.getByText(\"Navigation:idle\")).toBeDefined();\n    });\n\n    it(\"useNavigate navigations can be transition-enabled\", async () => {\n      let loaderDfd = createDeferred();\n      let router = createMemoryRouter([\n        {\n          path: \"/\",\n          Component() {\n            let navigation = useNavigation();\n            let [count, setCount] = React.useState(0);\n            return (\n              <>\n                <p>{`Navigation:${navigation.state}`}</p>\n                <button\n                  id=\"increment\"\n                  onClick={() =>\n                    React.startTransition(() => setCount((c) => c + 1))\n                  }\n                >\n                  {`Increment:${count}`}\n                </button>\n                <Outlet />\n              </>\n            );\n          },\n          children: [\n            {\n              index: true,\n              Component() {\n                let navigate = useNavigate();\n                let [pending, startTransition] = React.useTransition();\n                return (\n                  <button\n                    id=\"link\"\n                    // @ts-expect-error Needs react 19 types\n                    onClick={() => startTransition(() => navigate(\"/page\"))}\n                  >\n                    Go to page{pending ? \" (pending)\" : null}\n                  </button>\n                );\n              },\n            },\n            {\n              path: \"page\",\n              loader: () => loaderDfd.promise,\n              Component() {\n                return <h1>{useLoaderData()}</h1>;\n              },\n            },\n          ],\n        },\n      ]);\n\n      let { container } = render(\n        <RouterProvider router={router} unstable_useTransitions={true} />,\n      );\n\n      await waitFor(() => screen.getByText(\"Go to page\"));\n      expect(screen.getByText(\"Navigation:idle\")).toBeDefined();\n      expect(screen.getByText(\"Increment:0\")).toBeDefined();\n\n      // With transitions enabled, our updates surface via useOptimistic, but\n      // other transition-enabled updates do not\n      await fireEvent.click(container.querySelector(\"#link\")!);\n      await waitFor(() => screen.getByText(\"Navigation:loading\"));\n      expect(screen.getByText(\"Increment:0\")).toBeDefined();\n      expect(screen.getByText(\"Go to page (pending)\")).toBeDefined();\n\n      await fireEvent.click(container.querySelector(\"#increment\")!);\n      await waitFor(() => screen.getByText(\"Increment:0\"));\n      await fireEvent.click(container.querySelector(\"#increment\")!);\n      await waitFor(() => screen.getByText(\"Increment:0\"));\n      expect(screen.getByText(\"Navigation:loading\")).toBeDefined();\n\n      loaderDfd.resolve(\"Page\");\n      await waitFor(() => screen.getByText(\"Page\"));\n      await waitFor(() => screen.getByText(\"Increment:2\"));\n      expect(screen.getByText(\"Navigation:idle\")).toBeDefined();\n    });\n\n    it(\"Form submissions are transition-enabled\", async () => {\n      let actionDfd = createDeferred();\n      let loaderDfd = createDeferred();\n      let router = createBrowserRouter([\n        {\n          path: \"/\",\n          Component() {\n            let navigation = useNavigation();\n            let [count, setCount] = React.useState(0);\n            return (\n              <>\n                <p>{`Navigation:${navigation.state}`}</p>\n                <button\n                  id=\"increment\"\n                  onClick={() =>\n                    React.startTransition(() => setCount((c) => c + 1))\n                  }\n                >\n                  {`Increment:${count}`}\n                </button>\n                <Outlet />\n              </>\n            );\n          },\n          children: [\n            {\n              index: true,\n              Component() {\n                return (\n                  <Form method=\"post\" action=\"/page\">\n                    <button id=\"submit\" type=\"submit\" name=\"name\" value=\"value\">\n                      Submit\n                    </button>\n                  </Form>\n                );\n              },\n            },\n            {\n              path: \"page\",\n              action: () => actionDfd.promise,\n              loader: () => loaderDfd.promise,\n              Component() {\n                return <h1>{useLoaderData()}</h1>;\n              },\n            },\n          ],\n        },\n      ]);\n\n      let { container } = render(\n        <RouterProvider router={router} unstable_useTransitions={true} />,\n      );\n\n      await waitFor(() => screen.getByText(\"Submit\"));\n      expect(screen.getByText(\"Navigation:idle\")).toBeDefined();\n      expect(screen.getByText(\"Increment:0\")).toBeDefined();\n\n      // Without transitions enabled, all updates surface during navigation\n      await fireEvent.click(container.querySelector(\"#submit\")!);\n      await waitFor(() => screen.getByText(\"Navigation:submitting\"));\n      expect(screen.getByText(\"Increment:0\")).toBeDefined();\n\n      await fireEvent.click(container.querySelector(\"#increment\")!);\n      await waitFor(() => screen.getByText(\"Increment:0\"));\n      await fireEvent.click(container.querySelector(\"#increment\")!);\n      await waitFor(() => screen.getByText(\"Increment:0\"));\n      expect(screen.getByText(\"Navigation:submitting\")).toBeDefined();\n\n      actionDfd.resolve(\"Action\");\n      await waitFor(() => screen.getByText(\"Navigation:loading\"));\n      await fireEvent.click(container.querySelector(\"#increment\")!);\n      await waitFor(() => screen.getByText(\"Increment:0\"));\n\n      loaderDfd.resolve(\"Page\");\n      await waitFor(() => screen.getByText(\"Page\"));\n      await waitFor(() => screen.getByText(\"Increment:3\"));\n      expect(screen.getByText(\"Navigation:idle\")).toBeDefined();\n    });\n\n    it(\"useSubmit submissions are not transition-enabled\", async () => {\n      let actionDfd = createDeferred();\n      let loaderDfd = createDeferred();\n      let router = createMemoryRouter([\n        {\n          path: \"/\",\n          Component() {\n            let navigation = useNavigation();\n            let [count, setCount] = React.useState(0);\n            return (\n              <>\n                <p>{`Navigation:${navigation.state}`}</p>\n                <button\n                  id=\"increment\"\n                  onClick={() =>\n                    React.startTransition(() => setCount((c) => c + 1))\n                  }\n                >\n                  {`Increment:${count}`}\n                </button>\n                <Outlet />\n              </>\n            );\n          },\n          children: [\n            {\n              index: true,\n              Component() {\n                let submit = useSubmit();\n                return (\n                  <button\n                    id=\"submit\"\n                    onClick={() =>\n                      submit({}, { method: \"post\", action: \"/page\" })\n                    }\n                  >\n                    Submit\n                  </button>\n                );\n              },\n            },\n            {\n              path: \"page\",\n              action: () => actionDfd.promise,\n              loader: () => loaderDfd.promise,\n              Component() {\n                return <h1>{useLoaderData()}</h1>;\n              },\n            },\n          ],\n        },\n      ]);\n\n      let { container } = render(\n        <RouterProvider router={router} unstable_useTransitions={true} />,\n      );\n\n      await waitFor(() => screen.getByText(\"Submit\"));\n      expect(screen.getByText(\"Navigation:idle\")).toBeDefined();\n      expect(screen.getByText(\"Increment:0\")).toBeDefined();\n\n      // Without transitions enabled, all updates surface during navigation\n      await fireEvent.click(container.querySelector(\"#submit\")!);\n      await waitFor(() => screen.getByText(\"Navigation:submitting\"));\n      expect(screen.getByText(\"Increment:0\")).toBeDefined();\n\n      await fireEvent.click(container.querySelector(\"#increment\")!);\n      await waitFor(() => screen.getByText(\"Increment:1\"));\n      await fireEvent.click(container.querySelector(\"#increment\")!);\n      await waitFor(() => screen.getByText(\"Increment:2\"));\n      expect(screen.getByText(\"Navigation:submitting\")).toBeDefined();\n\n      actionDfd.resolve(\"Action\");\n      await waitFor(() => screen.getByText(\"Navigation:loading\"));\n      await fireEvent.click(container.querySelector(\"#increment\")!);\n      await waitFor(() => screen.getByText(\"Increment:3\"));\n\n      loaderDfd.resolve(\"Page\");\n      await waitFor(() => screen.getByText(\"Page\"));\n      await waitFor(() => screen.getByText(\"Increment:3\"));\n      expect(screen.getByText(\"Navigation:idle\")).toBeDefined();\n    });\n\n    it(\"useSubmit submissions can be transition-enabled\", async () => {\n      let actionDfd = createDeferred();\n      let loaderDfd = createDeferred();\n      let router = createMemoryRouter([\n        {\n          path: \"/\",\n          Component() {\n            let navigation = useNavigation();\n            let [count, setCount] = React.useState(0);\n            return (\n              <>\n                <p>{`Navigation:${navigation.state}`}</p>\n                <button\n                  id=\"increment\"\n                  onClick={() =>\n                    React.startTransition(() => setCount((c) => c + 1))\n                  }\n                >\n                  {`Increment:${count}`}\n                </button>\n                <Outlet />\n              </>\n            );\n          },\n          children: [\n            {\n              index: true,\n              Component() {\n                let submit = useSubmit();\n                let [pending, startTransition] = React.useTransition();\n                return (\n                  <button\n                    id=\"submit\"\n                    onClick={() =>\n                      startTransition(() =>\n                        // @ts-expect-error Needs react 19 types\n                        submit({}, { method: \"post\", action: \"/page\" }),\n                      )\n                    }\n                  >\n                    Submit{pending ? \" (pending)\" : null}\n                  </button>\n                );\n              },\n            },\n            {\n              path: \"page\",\n              action: () => actionDfd.promise,\n              loader: () => loaderDfd.promise,\n              Component() {\n                return <h1>{useLoaderData()}</h1>;\n              },\n            },\n          ],\n        },\n      ]);\n\n      let { container } = render(\n        <RouterProvider router={router} unstable_useTransitions={true} />,\n      );\n\n      await waitFor(() => screen.getByText(\"Submit\"));\n      expect(screen.getByText(\"Navigation:idle\")).toBeDefined();\n      expect(screen.getByText(\"Increment:0\")).toBeDefined();\n\n      // Without transitions enabled, all updates surface during navigation\n      await fireEvent.click(container.querySelector(\"#submit\")!);\n      await waitFor(() => screen.getByText(\"Navigation:submitting\"));\n      expect(screen.getByText(\"Increment:0\")).toBeDefined();\n      expect(screen.getByText(\"Submit (pending)\")).toBeDefined();\n\n      await fireEvent.click(container.querySelector(\"#increment\")!);\n      await waitFor(() => screen.getByText(\"Increment:0\"));\n      await fireEvent.click(container.querySelector(\"#increment\")!);\n      await waitFor(() => screen.getByText(\"Increment:0\"));\n      expect(screen.getByText(\"Navigation:submitting\")).toBeDefined();\n\n      actionDfd.resolve(\"Action\");\n      await waitFor(() => screen.getByText(\"Navigation:loading\"));\n      await fireEvent.click(container.querySelector(\"#increment\")!);\n      await waitFor(() => screen.getByText(\"Increment:0\"));\n\n      loaderDfd.resolve(\"Page\");\n      await waitFor(() => screen.getByText(\"Page\"));\n      await waitFor(() => screen.getByText(\"Increment:3\"));\n      expect(screen.getByText(\"Navigation:idle\")).toBeDefined();\n    });\n\n    it(\"actionData surfaces during the transition\", async () => {\n      let actionDfd = createDeferred();\n      let loaderDfd = createDeferred();\n      let router = createBrowserRouter(\n        [\n          {\n            id: \"index\",\n            path: \"/\",\n            action: () => actionDfd.promise,\n            loader: () => loaderDfd.promise,\n            Component() {\n              let loaderData = useLoaderData();\n              let actionData = useActionData();\n              let navigation = useNavigation();\n              return (\n                <>\n                  <p>{`Loader:${loaderData}`}</p>\n                  <p>{`Action:${actionData}`}</p>\n                  <p>{`Navigation:${navigation.state}`}</p>\n                  <Form method=\"post\">\n                    <button id=\"submit\" type=\"submit\" name=\"name\" value=\"value\">\n                      Submit\n                    </button>\n                  </Form>\n                </>\n              );\n            },\n          },\n        ],\n        {\n          hydrationData: {\n            loaderData: {\n              index: \"initial\",\n            },\n          },\n          window: getWindow(\"/\"),\n        },\n      );\n\n      let { container } = render(\n        <RouterProvider router={router} unstable_useTransitions={true} />,\n      );\n\n      await waitFor(() => screen.getByText(\"Submit\"));\n      expect(screen.getByText(\"Navigation:idle\")).toBeDefined();\n      expect(screen.getByText(\"Loader:initial\")).toBeDefined();\n      expect(screen.getByText(\"Action:undefined\")).toBeDefined();\n\n      await fireEvent.click(container.querySelector(\"#submit\")!);\n      await waitFor(() => screen.getByText(\"Navigation:submitting\"));\n\n      actionDfd.resolve(\"action-data\");\n      await waitFor(() => screen.getByText(\"Navigation:loading\"));\n      expect(screen.getByText(\"Loader:initial\")).toBeDefined();\n      expect(screen.getByText(\"Action:action-data\")).toBeDefined();\n\n      loaderDfd.resolve(\"revalidated\");\n      await waitFor(() => screen.getByText(\"Navigation:idle\"));\n      expect(screen.getByText(\"Loader:revalidated\")).toBeDefined();\n      expect(screen.getByText(\"Action:action-data\")).toBeDefined();\n    });\n\n    it(\"useFetcher is not transition-enabled\", async () => {\n      let loaderDfd = createDeferred();\n      let router = createMemoryRouter([\n        {\n          path: \"/\",\n          Component() {\n            let fetcher = useFetcher();\n            let [count, setCount] = React.useState(0);\n            return (\n              <>\n                <p>{`Fetcher:${fetcher.state}:${fetcher.data}`}</p>\n                <button\n                  id=\"increment\"\n                  onClick={() =>\n                    React.startTransition(() => setCount((c) => c + 1))\n                  }\n                >\n                  {`Increment:${count}`}\n                </button>\n                <button id=\"fetch\" onClick={() => fetcher.load(\"/fetch\")}>\n                  Fetch\n                </button>\n                <Outlet />\n              </>\n            );\n          },\n        },\n        {\n          path: \"fetch\",\n          loader: () => loaderDfd.promise,\n        },\n      ]);\n\n      let { container } = render(\n        <RouterProvider router={router} unstable_useTransitions={true} />,\n      );\n\n      await waitFor(() => screen.getByText(\"Fetch\"));\n      expect(screen.getByText(\"Fetcher:idle:undefined\")).toBeDefined();\n      expect(screen.getByText(\"Increment:0\")).toBeDefined();\n\n      // With transitions enabled, our updates surface via useOptimistic, but\n      // other transition-enabled updates do not\n      await fireEvent.click(container.querySelector(\"#fetch\")!);\n      await waitFor(() => screen.getByText(\"Fetcher:loading:undefined\"));\n      expect(screen.getByText(\"Increment:0\")).toBeDefined();\n      expect(screen.getByText(\"Fetch\")).toBeDefined();\n\n      await fireEvent.click(container.querySelector(\"#increment\")!);\n      await waitFor(() => screen.getByText(\"Increment:1\"));\n      await fireEvent.click(container.querySelector(\"#increment\")!);\n      await waitFor(() => screen.getByText(\"Increment:2\"));\n      expect(screen.getByText(\"Fetcher:loading:undefined\")).toBeDefined();\n\n      loaderDfd.resolve(\"data\");\n      await waitFor(() => screen.getByText(\"Increment:2\"));\n      expect(screen.getByText(\"Fetcher:idle:data\")).toBeDefined();\n    });\n\n    it(\"useFetcher can be transition-enabled\", async () => {\n      let loaderDfd = createDeferred();\n      let router = createMemoryRouter([\n        {\n          path: \"/\",\n          Component() {\n            let fetcher = useFetcher();\n            let [count, setCount] = React.useState(0);\n            let [pending, startTransition] = React.useTransition();\n            return (\n              <>\n                <p>{`Fetcher:${fetcher.state}:${fetcher.data}`}</p>\n                <button\n                  id=\"increment\"\n                  onClick={() =>\n                    React.startTransition(() => setCount((c) => c + 1))\n                  }\n                >\n                  {`Increment:${count}`}\n                </button>\n                <button\n                  id=\"fetch\"\n                  // @ts-expect-error Needs react 19 types\n                  onClick={() => startTransition(() => fetcher.load(\"/fetch\"))}\n                >\n                  Fetch{pending ? \" (pending)\" : null}\n                </button>\n                <Outlet />\n              </>\n            );\n          },\n        },\n        {\n          path: \"fetch\",\n          loader: () => loaderDfd.promise,\n        },\n      ]);\n\n      let { container } = render(\n        <RouterProvider router={router} unstable_useTransitions={true} />,\n      );\n\n      await waitFor(() => screen.getByText(\"Fetch\"));\n      expect(screen.getByText(\"Fetcher:idle:undefined\")).toBeDefined();\n      expect(screen.getByText(\"Increment:0\")).toBeDefined();\n\n      // With transitions enabled, our updates surface via useOptimistic, but\n      // other transition-enabled updates do not\n      await fireEvent.click(container.querySelector(\"#fetch\")!);\n      await waitFor(() => screen.getByText(\"Fetcher:loading:undefined\"));\n      expect(screen.getByText(\"Increment:0\")).toBeDefined();\n      expect(screen.getByText(\"Fetch (pending)\")).toBeDefined();\n\n      await fireEvent.click(container.querySelector(\"#increment\")!);\n      await waitFor(() => screen.getByText(\"Increment:0\"));\n      await fireEvent.click(container.querySelector(\"#increment\")!);\n      await waitFor(() => screen.getByText(\"Increment:0\"));\n      expect(screen.getByText(\"Fetcher:loading:undefined\")).toBeDefined();\n\n      loaderDfd.resolve(\"data\");\n      await waitFor(() => screen.getByText(\"Increment:2\"));\n      expect(screen.getByText(\"Fetcher:idle:data\")).toBeDefined();\n    });\n\n    it(\"fetcher updates surface mid-navigation\", async () => {\n      let loaderDfd = createDeferred();\n      let fetcherDfd = createDeferred();\n      let router = createMemoryRouter([\n        {\n          path: \"/\",\n          Component() {\n            let navigation = useNavigation();\n            let fetcher = useFetcher();\n            return (\n              <>\n                <p>{`Navigation:${navigation.state}`}</p>\n                <button id=\"fetch\" onClick={() => fetcher.load(\"/fetch\")}>\n                  {`Fetcher ${fetcher.state}:${fetcher.data}`}\n                </button>\n                <Outlet />\n              </>\n            );\n          },\n          children: [\n            {\n              index: true,\n              Component() {\n                return (\n                  <Link id=\"link\" to=\"/page\">\n                    Go to page\n                  </Link>\n                );\n              },\n            },\n            {\n              path: \"page\",\n              loader: () => loaderDfd.promise,\n              Component() {\n                return <h1>{useLoaderData()}</h1>;\n              },\n            },\n            {\n              path: \"fetch\",\n              loader: () => fetcherDfd.promise,\n            },\n          ],\n        },\n      ]);\n\n      let { container } = render(\n        <RouterProvider router={router} unstable_useTransitions={true} />,\n      );\n\n      await waitFor(() => screen.getByText(\"Go to page\"));\n      expect(screen.getByText(\"Navigation:idle\")).toBeDefined();\n      expect(screen.getByText(\"Fetcher idle:undefined\")).toBeDefined();\n\n      // With transitions enabled, our updates surface via useOptimistic, but\n      // other transition-enabled updates do not\n      await fireEvent.click(container.querySelector(\"#link\")!);\n      await waitFor(() => screen.getByText(\"Navigation:loading\"));\n      expect(screen.getByText(\"Fetcher idle:undefined\")).toBeDefined();\n\n      await fireEvent.click(container.querySelector(\"#fetch\")!);\n      await waitFor(() => screen.getByText(\"Fetcher loading:undefined\"));\n      expect(screen.getByText(\"Navigation:loading\")).toBeDefined();\n\n      fetcherDfd.resolve(\"data\");\n      await waitFor(() => screen.getByText(\"Fetcher idle:data\"));\n      expect(screen.getByText(\"Navigation:loading\")).toBeDefined();\n\n      loaderDfd.resolve(\"Page\");\n      await waitFor(() => screen.getByText(\"Page\"));\n      expect(screen.getByText(\"Fetcher idle:data\")).toBeDefined();\n      expect(screen.getByText(\"Navigation:idle\")).toBeDefined();\n    });\n\n    it(\"useRevalidator is not transition-enabled\", async () => {\n      let loaderDfd = createDeferred();\n      let router = createMemoryRouter(\n        [\n          {\n            id: \"index\",\n            path: \"/\",\n            loader: () => loaderDfd.promise,\n            Component() {\n              let data = useLoaderData();\n              let revalidator = useRevalidator();\n              let [count, setCount] = React.useState(0);\n              return (\n                <>\n                  <p>{`Loader:${data}`}</p>\n                  <p>{`Revalidator:${revalidator.state}`}</p>\n                  <button\n                    id=\"increment\"\n                    onClick={() =>\n                      React.startTransition(() => setCount((c) => c + 1))\n                    }\n                  >\n                    {`Increment:${count}`}\n                  </button>\n                  <button\n                    id=\"revalidate\"\n                    onClick={() => revalidator.revalidate()}\n                  >\n                    Revalidate\n                  </button>\n                </>\n              );\n            },\n          },\n        ],\n        {\n          hydrationData: {\n            loaderData: {\n              index: \"initial\",\n            },\n          },\n        },\n      );\n\n      let { container } = render(\n        <RouterProvider router={router} unstable_useTransitions={true} />,\n      );\n\n      await waitFor(() => screen.getByText(\"Revalidate\"));\n      expect(screen.getByText(\"Revalidator:idle\")).toBeDefined();\n      expect(screen.getByText(\"Increment:0\")).toBeDefined();\n      expect(screen.getByText(\"Loader:initial\")).toBeDefined();\n\n      // With transitions enabled, our updates surface via useOptimistic, but\n      // other transition-enabled updates do not\n      await fireEvent.click(container.querySelector(\"#revalidate\")!);\n      await waitFor(() => screen.getByText(\"Revalidator:loading\"));\n      expect(screen.getByText(\"Increment:0\")).toBeDefined();\n\n      await fireEvent.click(container.querySelector(\"#increment\")!);\n      await waitFor(() => screen.getByText(\"Increment:1\"));\n      await fireEvent.click(container.querySelector(\"#increment\")!);\n      await waitFor(() => screen.getByText(\"Increment:2\"));\n      expect(screen.getByText(\"Revalidator:loading\")).toBeDefined();\n\n      loaderDfd.resolve(\"revalidated\");\n      await waitFor(() => screen.getByText(\"Revalidator:idle\"));\n      await waitFor(() => screen.getByText(\"Increment:2\"));\n      expect(screen.getByText(\"Loader:revalidated\")).toBeDefined();\n    });\n\n    it(\"useRevalidator can be transition-enabled\", async () => {\n      let loaderDfd = createDeferred();\n      let router = createMemoryRouter(\n        [\n          {\n            id: \"index\",\n            path: \"/\",\n            loader: () => loaderDfd.promise,\n            Component() {\n              let data = useLoaderData();\n              let revalidator = useRevalidator();\n              let [count, setCount] = React.useState(0);\n              let [pending, startTransition] = React.useTransition();\n              return (\n                <>\n                  <p>{`Loader:${data}`}</p>\n                  <p>{`Revalidator:${revalidator.state}`}</p>\n                  <button\n                    id=\"increment\"\n                    onClick={() =>\n                      React.startTransition(() => setCount((c) => c + 1))\n                    }\n                  >\n                    {`Increment:${count}`}\n                  </button>\n                  <button\n                    id=\"revalidate\"\n                    onClick={() =>\n                      // @ts-expect-error Needs react 19 types\n                      startTransition(() => revalidator.revalidate())\n                    }\n                  >\n                    {`Revalidate${pending ? \" (pending)\" : \"\"}`}\n                  </button>\n                </>\n              );\n            },\n          },\n        ],\n        {\n          hydrationData: {\n            loaderData: {\n              index: \"initial\",\n            },\n          },\n        },\n      );\n\n      let { container } = render(\n        <RouterProvider router={router} unstable_useTransitions={true} />,\n      );\n\n      await waitFor(() => screen.getByText(\"Revalidate\"));\n      expect(screen.getByText(\"Revalidator:idle\")).toBeDefined();\n      expect(screen.getByText(\"Increment:0\")).toBeDefined();\n      expect(screen.getByText(\"Loader:initial\")).toBeDefined();\n\n      // With transitions enabled, our updates surface via useOptimistic, but\n      // other transition-enabled updates do not\n      await fireEvent.click(container.querySelector(\"#revalidate\")!);\n      await waitFor(() => screen.getByText(\"Revalidator:loading\"));\n      expect(screen.getByText(\"Increment:0\")).toBeDefined();\n      expect(screen.getByText(\"Revalidate (pending)\")).toBeDefined();\n\n      await fireEvent.click(container.querySelector(\"#increment\")!);\n      await waitFor(() => screen.getByText(\"Increment:0\"));\n      await fireEvent.click(container.querySelector(\"#increment\")!);\n      await waitFor(() => screen.getByText(\"Increment:0\"));\n      expect(screen.getByText(\"Revalidator:loading\")).toBeDefined();\n\n      loaderDfd.resolve(\"revalidated\");\n      await waitFor(() => screen.getByText(\"Revalidator:idle\"));\n      await waitFor(() => screen.getByText(\"Increment:2\"));\n      expect(screen.getByText(\"Loader:revalidated\")).toBeDefined();\n    });\n  });\n});\n"
  },
  {
    "path": "packages/react-router/__tests__/resolvePath-test.tsx",
    "content": "import { resolvePath } from \"react-router\";\n\ndescribe(\"resolvePath\", () => {\n  it('resolves absolute paths irrespective of the \"from\" pathname', () => {\n    expect(resolvePath(\"/search\", \"/inbox\")).toMatchObject({\n      pathname: \"/search\",\n    });\n\n    expect(resolvePath(\"/search/../123\", \"/inbox\")).toMatchObject({\n      pathname: \"/123\",\n    });\n\n    expect(resolvePath(\"/search/../../123\", \"/inbox\")).toMatchObject({\n      pathname: \"/123\",\n    });\n\n    expect(resolvePath(\"/search/user/../../123\", \"/inbox\")).toMatchObject({\n      pathname: \"/123\",\n    });\n  });\n\n  it(\"resolves relative paths\", () => {\n    expect(resolvePath(\"../search\", \"/inbox\")).toMatchObject({\n      pathname: \"/search\",\n    });\n\n    expect(resolvePath(\"./search\", \"/inbox\")).toMatchObject({\n      pathname: \"/inbox/search\",\n    });\n\n    expect(resolvePath(\"./search/../123\", \"/inbox\")).toMatchObject({\n      pathname: \"/inbox/123\",\n    });\n\n    expect(resolvePath(\"search\", \"/inbox\")).toMatchObject({\n      pathname: \"/inbox/search\",\n    });\n\n    expect(resolvePath(\"search/../123\", \"/inbox\")).toMatchObject({\n      pathname: \"/inbox/123\",\n    });\n\n    expect(resolvePath(\"search/../../123\", \"/inbox\")).toMatchObject({\n      pathname: \"/123\",\n    });\n\n    expect(resolvePath(\"search/../../../123\", \"/inbox\")).toMatchObject({\n      pathname: \"/123\",\n    });\n  });\n\n  it(\"normalizes any mid-path double-slashes\", () => {\n    let spy = jest.spyOn(console, \"warn\").mockImplementation(() => {});\n\n    expect(resolvePath(\"/search/../..//foo\")).toMatchObject({\n      pathname: \"/foo\",\n    });\n\n    expect(resolvePath(\"search/../..//foo\", \"/inbox\")).toMatchObject({\n      pathname: \"/foo\",\n    });\n\n    spy.mockRestore();\n  });\n\n  it(\"handles relative paths with an embedded colon\", () => {\n    expect(resolvePath(\"foo:bar\", \"/\")).toMatchObject({\n      pathname: \"/foo:bar\",\n    });\n\n    expect(resolvePath(\"./foo:bar\", \"/\")).toMatchObject({\n      pathname: \"/foo:bar\",\n    });\n\n    expect(resolvePath(\"../foo:bar\", \"/\")).toMatchObject({\n      pathname: \"/foo:bar\",\n    });\n\n    expect(resolvePath(\"foo:bar\", \"/path\")).toMatchObject({\n      pathname: \"/path/foo:bar\",\n    });\n\n    expect(resolvePath(\"./foo:bar\", \"/path\")).toMatchObject({\n      pathname: \"/path/foo:bar\",\n    });\n\n    expect(resolvePath(\"../foo:bar\", \"/path\")).toMatchObject({\n      pathname: \"/foo:bar\",\n    });\n  });\n\n  it('ignores trailing slashes on the \"from\" pathname when resolving relative paths', () => {\n    expect(resolvePath(\"../search\", \"/inbox/\")).toMatchObject({\n      pathname: \"/search\",\n    });\n  });\n\n  it('uses the \"from\" pathname when the \"to\" value has no pathname', () => {\n    expect(resolvePath(\"?q=react\", \"/search\")).toMatchObject({\n      pathname: \"/search\",\n      search: \"?q=react\",\n    });\n  });\n\n  it(\"normalizes search and hash values\", () => {\n    expect(\n      resolvePath({ pathname: \"/search\", search: \"q=react\", hash: \"results\" }),\n    ).toEqual({\n      pathname: \"/search\",\n      search: \"?q=react\",\n      hash: \"#results\",\n    });\n  });\n});\n"
  },
  {
    "path": "packages/react-router/__tests__/route-depth-order-matching-test.tsx",
    "content": "import * as React from \"react\";\nimport * as TestRenderer from \"react-test-renderer\";\nimport { MemoryRouter, Outlet, Routes, Route } from \"react-router\";\n\ndescribe(\"nested routes with no path\", () => {\n  it(\"matches them depth-first\", () => {\n    let renderer: TestRenderer.ReactTestRenderer;\n    TestRenderer.act(() => {\n      renderer = TestRenderer.create(\n        <MemoryRouter initialEntries={[\"/\"]}>\n          <Routes>\n            <Route element={<First />}>\n              <Route element={<Second />}>\n                <Route path=\"/\" element={<Third />} />\n              </Route>\n            </Route>\n          </Routes>\n        </MemoryRouter>,\n      );\n    });\n\n    expect(renderer.toJSON()).toMatchInlineSnapshot(`\n      <div>\n        First \n        <div>\n          Second \n          <div>\n            Third\n          </div>\n        </div>\n      </div>\n    `);\n  });\n\n  function First() {\n    return (\n      <div>\n        First <Outlet />\n      </div>\n    );\n  }\n\n  function Second() {\n    return (\n      <div>\n        Second <Outlet />\n      </div>\n    );\n  }\n\n  function Third() {\n    return <div>Third</div>;\n  }\n});\n\ndescribe(\"nested /\", () => {\n  it(\"matches them depth-first\", () => {\n    let renderer: TestRenderer.ReactTestRenderer;\n    TestRenderer.act(() => {\n      renderer = TestRenderer.create(\n        <MemoryRouter initialEntries={[\"/\"]}>\n          <Routes>\n            <Route path=\"/\" element={<First />}>\n              <Route path=\"/\" element={<Second />}>\n                <Route path=\"/\" element={<Third />} />\n              </Route>\n            </Route>\n          </Routes>\n        </MemoryRouter>,\n      );\n    });\n\n    expect(renderer.toJSON()).toMatchInlineSnapshot(`\n      <div>\n        First \n        <div>\n          Second \n          <div>\n            Third\n          </div>\n        </div>\n      </div>\n    `);\n  });\n\n  function First() {\n    return (\n      <div>\n        First <Outlet />\n      </div>\n    );\n  }\n\n  function Second() {\n    return (\n      <div>\n        Second <Outlet />\n      </div>\n    );\n  }\n\n  function Third() {\n    return <div>Third</div>;\n  }\n});\n\ndescribe(\"routes with identical paths\", () => {\n  it(\"matches them in order\", () => {\n    let renderer: TestRenderer.ReactTestRenderer;\n    TestRenderer.act(() => {\n      renderer = TestRenderer.create(\n        <MemoryRouter initialEntries={[\"/home\"]}>\n          <Routes>\n            <Route path=\"/home\" element={<First />} />\n            <Route path=\"/home\" element={<Second />} />\n          </Routes>\n        </MemoryRouter>,\n      );\n    });\n\n    expect(renderer.toJSON()).toMatchInlineSnapshot(`\n      <div>\n        First\n      </div>\n    `);\n  });\n\n  function First() {\n    return <div>First</div>;\n  }\n\n  function Second() {\n    return <div>Second</div>;\n  }\n});\n"
  },
  {
    "path": "packages/react-router/__tests__/route-matching-test.tsx",
    "content": "import * as React from \"react\";\nimport * as TestRenderer from \"react-test-renderer\";\nimport type { RouteObject } from \"react-router\";\nimport {\n  MemoryRouter,\n  Outlet,\n  Routes,\n  Route,\n  useParams,\n  useRoutes,\n} from \"react-router\";\n\ndescribe(\"route matching\", () => {\n  function describeRouteMatching(routes: React.ReactNode) {\n    let testPaths = [\n      \"/courses\",\n      \"/courses/routing\",\n      \"/courses/routing/grades\",\n      \"/courses/new\",\n      \"/courses/not/found\",\n      \"/courses/react-fundamentals\",\n      \"/courses/advanced-react\",\n      \"/\",\n      \"/not-found\",\n    ];\n\n    testPaths.forEach((path) => {\n      it(`renders the right elements at ${path}`, () => {\n        let renderer: TestRenderer.ReactTestRenderer;\n        TestRenderer.act(() => {\n          renderer = TestRenderer.create(\n            <MemoryRouter initialEntries={[path]} children={routes} />,\n          );\n        });\n\n        expect(renderer.toJSON()).toMatchSnapshot();\n      });\n    });\n  }\n\n  describe(\"using a route config object\", () => {\n    let routes = [\n      {\n        path: \"courses\",\n        element: <Courses />,\n        children: [\n          { index: true, element: <CoursesIndex /> },\n          {\n            path: \":id\",\n            element: <Course />,\n            children: [{ path: \"grades\", element: <CourseGrades /> }],\n          },\n          { path: \"new\", element: <NewCourse /> },\n          { path: \"*\", element: <CoursesNotFound /> },\n        ],\n      },\n      {\n        path: \"courses\",\n        element: <Landing />,\n        children: [\n          { path: \"react-fundamentals\", element: <ReactFundamentals /> },\n          { path: \"advanced-react\", element: <AdvancedReact /> },\n          { path: \"*\", element: <NeverRender /> },\n        ],\n      },\n      { index: true, element: <Home /> },\n      { path: \"*\", element: <NotFound /> },\n    ];\n\n    function RoutesRenderer({ routes }: { routes: RouteObject[] }) {\n      return useRoutes(routes);\n    }\n\n    describeRouteMatching(<RoutesRenderer routes={routes} />);\n  });\n\n  describe(\"using <Routes> with <Route> elements\", () => {\n    let routes = (\n      <Routes>\n        <Route path=\"courses\" element={<Courses />}>\n          <Route index element={<CoursesIndex />} />\n          <Route path=\":id\" element={<Course />}>\n            <Route path=\"grades\" element={<CourseGrades />} />\n          </Route>\n          <Route path=\"new\" element={<NewCourse />} />\n          <Route path=\"*\" element={<CoursesNotFound />} />\n        </Route>\n        <Route path=\"courses\" element={<Landing />}>\n          <Route path=\"react-fundamentals\" element={<ReactFundamentals />} />\n          <Route path=\"advanced-react\" element={<AdvancedReact />} />\n          <Route path=\"*\" element={<NeverRender />} />\n        </Route>\n        <Route index element={<Home />} />\n        <Route path=\"*\" element={<NotFound />} />\n      </Routes>\n    );\n\n    describeRouteMatching(routes);\n  });\n\n  function Courses() {\n    return (\n      <div>\n        <h1>Courses</h1>\n        <Outlet />\n      </div>\n    );\n  }\n\n  function Course() {\n    let { id } = useParams();\n    return (\n      <div>\n        <h2>Course {id}</h2>\n        <Outlet />\n      </div>\n    );\n  }\n\n  function CourseGrades() {\n    return <p>Course Grades</p>;\n  }\n\n  function NewCourse() {\n    return <p>New Course</p>;\n  }\n\n  function CoursesIndex() {\n    return <p>All Courses</p>;\n  }\n\n  function CoursesNotFound() {\n    return <p>Course Not Found</p>;\n  }\n\n  function Landing() {\n    return (\n      <p>\n        <h1>Welcome to React Training</h1>\n        <Outlet />\n      </p>\n    );\n  }\n\n  function ReactFundamentals() {\n    return <p>React Fundamentals</p>;\n  }\n\n  function AdvancedReact() {\n    return <p>Advanced React</p>;\n  }\n\n  function Home() {\n    return <p>Home</p>;\n  }\n\n  function NotFound() {\n    return <p>Not Found</p>;\n  }\n\n  function NeverRender(): React.ReactElement {\n    throw new Error(\"NeverRender should ... uh ... never render\");\n  }\n});\n"
  },
  {
    "path": "packages/react-router/__tests__/router/TestSequences/EncodedReservedCharacters.ts",
    "content": "import type { History } from \"../../../lib/router/history\";\n\nexport default function EncodeReservedCharacters(history: History) {\n  let pathname;\n\n  // encoded string\n  pathname = \"/view/%23abc\";\n  history.replace(pathname);\n  expect(history.location).toMatchObject({\n    pathname: \"/view/%23abc\",\n  });\n\n  // encoded object\n  pathname = \"/view/%23abc\";\n  history.replace({ pathname });\n  expect(history.location).toMatchObject({\n    pathname: \"/view/%23abc\",\n  });\n\n  // unencoded string\n  pathname = \"/view/#abc\";\n  history.replace(pathname);\n  expect(history.location).toMatchObject({\n    pathname: \"/view/\",\n    hash: \"#abc\",\n  });\n}\n"
  },
  {
    "path": "packages/react-router/__tests__/router/TestSequences/GoBack.ts",
    "content": "import type { History } from \"../../../lib/router/history\";\n\nexport default async function GoBack(history: History) {\n  let spy: jest.SpyInstance = jest.fn();\n\n  expect(history.location).toMatchObject({\n    pathname: \"/\",\n  });\n\n  history.push(\"/home\");\n  expect(history.action).toEqual(\"PUSH\");\n  expect(history.location).toMatchObject({\n    pathname: \"/home\",\n  });\n  expect(spy).not.toHaveBeenCalled();\n\n  // JSDom doesn't fire the listener synchronously :(\n  let promise = new Promise((resolve) => {\n    let unlisten = history.listen((...args) => {\n      //@ts-expect-error\n      spy(...args);\n      unlisten();\n      resolve(null);\n    });\n  });\n  history.go(-1);\n  await promise;\n  expect(history.action).toEqual(\"POP\");\n  expect(history.location).toMatchObject({\n    pathname: \"/\",\n  });\n  expect(spy).toHaveBeenCalledWith({\n    action: \"POP\",\n    delta: expect.any(Number),\n    location: {\n      hash: \"\",\n      key: expect.any(String),\n      pathname: \"/\",\n      search: \"\",\n      state: null,\n    },\n  });\n  expect(spy.mock.calls.length).toBe(1);\n}\n"
  },
  {
    "path": "packages/react-router/__tests__/router/TestSequences/GoForward.ts",
    "content": "import type { History } from \"../../../lib/router/history\";\n\nexport default async function GoForward(history: History) {\n  let spy: jest.SpyInstance = jest.fn();\n\n  expect(history.location).toMatchObject({\n    pathname: \"/\",\n  });\n\n  history.push(\"/home\");\n  expect(history.action).toEqual(\"PUSH\");\n  expect(history.location).toMatchObject({\n    pathname: \"/home\",\n  });\n  expect(spy).not.toHaveBeenCalled();\n\n  // JSDom doesn't fire the listener synchronously :(\n  let promise1 = new Promise((resolve) => {\n    let unlisten = history.listen((...args) => {\n      //@ts-expect-error\n      spy(...args);\n      unlisten();\n      resolve(null);\n    });\n  });\n  history.go(-1);\n  await promise1;\n  expect(history.action).toEqual(\"POP\");\n  expect(history.location).toMatchObject({\n    pathname: \"/\",\n  });\n  expect(spy).toHaveBeenCalledWith({\n    action: \"POP\",\n    delta: expect.any(Number),\n    location: {\n      hash: \"\",\n      key: expect.any(String),\n      pathname: \"/\",\n      search: \"\",\n      state: null,\n    },\n  });\n  expect(spy.mock.calls.length).toBe(1);\n\n  // JSDom doesn't fire the listener synchronously :(\n  let promise2 = new Promise((resolve) => {\n    let unlisten = history.listen((...args) => {\n      //@ts-expect-error\n      spy(...args);\n      unlisten();\n      resolve(null);\n    });\n  });\n  history.go(1);\n  await promise2;\n  expect(history.action).toEqual(\"POP\");\n  expect(history.location).toMatchObject({\n    pathname: \"/home\",\n  });\n  expect(spy).toHaveBeenCalledWith({\n    action: \"POP\",\n    delta: expect.any(Number),\n    location: {\n      hash: \"\",\n      key: expect.any(String),\n      pathname: \"/home\",\n      search: \"\",\n      state: null,\n    },\n  });\n  expect(spy.mock.calls.length).toBe(2);\n}\n"
  },
  {
    "path": "packages/react-router/__tests__/router/TestSequences/InitialLocationDefaultKey.ts",
    "content": "import type { History } from \"../../../lib/router/history\";\n\nexport default function InitialLocationDefaultKey(history: History) {\n  expect(history.location.key).toBe(\"default\");\n}\n"
  },
  {
    "path": "packages/react-router/__tests__/router/TestSequences/InitialLocationHasKey.ts",
    "content": "import type { History } from \"../../../lib/router/history\";\n\nexport default function InitialLocationHasKey(history: History) {\n  expect(history.location.key).toBeTruthy();\n}\n"
  },
  {
    "path": "packages/react-router/__tests__/router/TestSequences/Listen.ts",
    "content": "import type { History } from \"../../../lib/router/history\";\n\nexport default function Listen(history: History) {\n  let spy = jest.fn();\n  let unlisten = history.listen(spy);\n\n  expect(spy).not.toHaveBeenCalled();\n\n  unlisten();\n}\n"
  },
  {
    "path": "packages/react-router/__tests__/router/TestSequences/ListenPopOnly.ts",
    "content": "import type { History } from \"../../../lib/router/history\";\n\nexport default function ListenPopOnly(history: History) {\n  let spy = jest.fn();\n  let unlisten = history.listen(spy);\n\n  history.push(\"/2\");\n  expect(history.location.pathname).toBe(\"/2\");\n  history.replace(\"/3\");\n  expect(history.location.pathname).toBe(\"/3\");\n\n  expect(spy).not.toHaveBeenCalled();\n  unlisten();\n}\n"
  },
  {
    "path": "packages/react-router/__tests__/router/TestSequences/PushMissingPathname.ts",
    "content": "import type { History } from \"../../../lib/router/history\";\n\nexport default function PushMissingPathname(history: History) {\n  expect(history.location).toMatchObject({\n    pathname: \"/\",\n  });\n\n  history.push(\"/home?the=query#the-hash\");\n  expect(history.action).toBe(\"PUSH\");\n  expect(history.location).toMatchObject({\n    pathname: \"/home\",\n    search: \"?the=query\",\n    hash: \"#the-hash\",\n  });\n\n  history.push(\"?another=query#another-hash\");\n  expect(history.action).toBe(\"PUSH\");\n  expect(history.location).toMatchObject({\n    pathname: \"/home\",\n    search: \"?another=query\",\n    hash: \"#another-hash\",\n  });\n}\n"
  },
  {
    "path": "packages/react-router/__tests__/router/TestSequences/PushNewLocation.ts",
    "content": "import type { History } from \"../../../lib/router/history\";\n\nexport default function PushNewLocation(history: History) {\n  expect(history.location).toMatchObject({\n    pathname: \"/\",\n  });\n\n  history.push(\"/home?the=query#the-hash\");\n  expect(history.action).toBe(\"PUSH\");\n  expect(history.location).toMatchObject({\n    pathname: \"/home\",\n    search: \"?the=query\",\n    hash: \"#the-hash\",\n    state: null,\n    key: expect.any(String),\n  });\n}\n"
  },
  {
    "path": "packages/react-router/__tests__/router/TestSequences/PushRelativePathname.ts",
    "content": "import type { History } from \"../../../lib/router/history\";\n\nexport default function PushRelativePathname(history: History) {\n  expect(history.location).toMatchObject({\n    pathname: \"/\",\n  });\n\n  history.push(\"/the/path?the=query#the-hash\");\n  expect(history.action).toBe(\"PUSH\");\n  expect(history.location).toMatchObject({\n    pathname: \"/the/path\",\n    search: \"?the=query\",\n    hash: \"#the-hash\",\n  });\n\n  history.push(\"../other/path?another=query#another-hash\");\n  expect(history.action).toBe(\"PUSH\");\n  expect(history.location).toMatchObject({\n    pathname: \"/other/path\",\n    search: \"?another=query\",\n    hash: \"#another-hash\",\n  });\n}\n"
  },
  {
    "path": "packages/react-router/__tests__/router/TestSequences/PushRelativePathnameWarning.ts",
    "content": "import type { History } from \"../../../lib/router/history\";\n\nexport default function PushRelativePathnameWarning(history: History) {\n  expect(history.location).toMatchObject({\n    pathname: \"/\",\n  });\n\n  history.push(\"/the/path?the=query#the-hash\");\n  expect(history.action).toBe(\"PUSH\");\n  expect(history.location).toMatchObject({\n    pathname: \"/the/path\",\n    search: \"?the=query\",\n    hash: \"#the-hash\",\n  });\n\n  let spy = jest.spyOn(console, \"warn\").mockImplementation(() => {});\n  history.push(\"../other/path?another=query#another-hash\");\n  expect(spy).toHaveBeenCalledWith(\n    expect.stringContaining(\"relative pathnames are not supported\"),\n  );\n  spy.mockReset();\n\n  expect(history.location).toMatchObject({\n    pathname: \"../other/path\",\n    search: \"?another=query\",\n    hash: \"#another-hash\",\n  });\n}\n"
  },
  {
    "path": "packages/react-router/__tests__/router/TestSequences/PushSamePath.ts",
    "content": "import type { History } from \"../../../lib/router/history\";\n\nexport default async function PushSamePath(history: History) {\n  expect(history.location).toMatchObject({\n    pathname: \"/\",\n  });\n\n  history.push(\"/home\");\n  expect(history.action).toBe(\"PUSH\");\n  expect(history.location).toMatchObject({\n    pathname: \"/home\",\n  });\n\n  history.push(\"/home\");\n  expect(history.action).toBe(\"PUSH\");\n  expect(history.location).toMatchObject({\n    pathname: \"/home\",\n  });\n\n  // JSDom doesn't fire the listener synchronously :(\n  let promise = new Promise((resolve) => {\n    let unlisten = history.listen(() => {\n      unlisten();\n      resolve(null);\n    });\n  });\n  history.go(-1);\n  await promise;\n  expect(history.action).toBe(\"POP\");\n  expect(history.location).toMatchObject({\n    pathname: \"/home\",\n  });\n}\n"
  },
  {
    "path": "packages/react-router/__tests__/router/TestSequences/PushState.ts",
    "content": "import type { History } from \"../../../lib/router/history\";\n\nexport default function PushState(history: History) {\n  expect(history.location).toMatchObject({\n    pathname: \"/\",\n  });\n\n  history.push(\"/home?the=query#the-hash\", { the: \"state\" });\n  expect(history.action).toBe(\"PUSH\");\n  expect(history.location).toMatchObject({\n    pathname: \"/home\",\n    search: \"?the=query\",\n    hash: \"#the-hash\",\n    state: { the: \"state\" },\n  });\n}\n"
  },
  {
    "path": "packages/react-router/__tests__/router/TestSequences/PushStateInvalid.ts",
    "content": "import type { History } from \"../../../lib/router/history\";\n\nexport default function PushState(history: History, window: Window) {\n  let err = new DOMException(\"ERROR\", \"DataCloneError\");\n  jest.spyOn(window.history, \"pushState\").mockImplementation(() => {\n    throw err;\n  });\n\n  expect(history.location).toMatchObject({\n    pathname: \"/\",\n  });\n\n  expect(() =>\n    history.push(\"/home?the=query#the-hash\", { invalid: () => {} }),\n  ).toThrow(err);\n\n  expect(history.location.pathname).toBe(\"/\");\n}\n"
  },
  {
    "path": "packages/react-router/__tests__/router/TestSequences/ReplaceNewLocation.ts",
    "content": "import type { History } from \"../../../lib/router/history\";\n\nexport default function ReplaceNewLocation(history: History) {\n  expect(history.location).toMatchObject({\n    pathname: \"/\",\n  });\n\n  history.replace(\"/home?the=query#the-hash\");\n  expect(history.action).toBe(\"REPLACE\");\n  expect(history.location).toMatchObject({\n    pathname: \"/home\",\n    search: \"?the=query\",\n    hash: \"#the-hash\",\n    state: null,\n    key: expect.any(String),\n  });\n\n  history.replace(\"/\");\n  expect(history.action).toBe(\"REPLACE\");\n  expect(history.location).toMatchObject({\n    pathname: \"/\",\n    search: \"\",\n    state: null,\n    key: expect.any(String),\n  });\n}\n"
  },
  {
    "path": "packages/react-router/__tests__/router/TestSequences/ReplaceSamePath.ts",
    "content": "import type { History } from \"../../../lib/router/history\";\n\nexport default function ReplaceSamePath(history: History) {\n  expect(history.location).toMatchObject({\n    pathname: \"/\",\n  });\n\n  history.replace(\"/home\");\n  expect(history.action).toBe(\"REPLACE\");\n  expect(history.location).toMatchObject({\n    pathname: \"/home\",\n  });\n\n  let prevLocation = history.location;\n\n  history.replace(\"/home\");\n  expect(history.action).toBe(\"REPLACE\");\n  expect(history.location).toMatchObject({\n    pathname: \"/home\",\n  });\n\n  expect(history.location).not.toBe(prevLocation);\n}\n"
  },
  {
    "path": "packages/react-router/__tests__/router/TestSequences/ReplaceState.ts",
    "content": "import type { History } from \"../../../lib/router/history\";\n\nexport default function ReplaceState(history: History) {\n  expect(history.location).toMatchObject({\n    pathname: \"/\",\n  });\n\n  history.replace(\"/home?the=query#the-hash\", { the: \"state\" });\n  expect(history.action).toBe(\"REPLACE\");\n  expect(history.location).toMatchObject({\n    pathname: \"/home\",\n    search: \"?the=query\",\n    hash: \"#the-hash\",\n    state: { the: \"state\" },\n  });\n}\n"
  },
  {
    "path": "packages/react-router/__tests__/router/browser-test.ts",
    "content": "/* eslint-disable jest/expect-expect */\n\nimport {\n  type BrowserHistory,\n  createBrowserHistory,\n} from \"../../lib/router/history\";\n\nimport InitialLocationDefaultKey from \"./TestSequences/InitialLocationDefaultKey\";\nimport Listen from \"./TestSequences/Listen\";\nimport PushNewLocation from \"./TestSequences/PushNewLocation\";\nimport PushSamePath from \"./TestSequences/PushSamePath\";\nimport PushState from \"./TestSequences/PushState\";\nimport PushStateInvalid from \"./TestSequences/PushStateInvalid\";\nimport PushMissingPathname from \"./TestSequences/PushMissingPathname\";\nimport PushRelativePathname from \"./TestSequences/PushRelativePathname\";\nimport ReplaceNewLocation from \"./TestSequences/ReplaceNewLocation\";\nimport ReplaceSamePath from \"./TestSequences/ReplaceSamePath\";\nimport ReplaceState from \"./TestSequences/ReplaceState\";\nimport EncodedReservedCharacters from \"./TestSequences/EncodedReservedCharacters\";\nimport GoBack from \"./TestSequences/GoBack\";\nimport GoForward from \"./TestSequences/GoForward\";\nimport ListenPopOnly from \"./TestSequences/ListenPopOnly\";\nimport getWindow from \"../utils/getWindow\";\n\ndescribe(\"a browser history\", () => {\n  let history: BrowserHistory;\n  let testWindow: Window;\n\n  beforeEach(() => {\n    // Need to use our own custom DOM in order to get a working history\n    testWindow = getWindow(\"/\");\n    history = createBrowserHistory({ window: testWindow });\n  });\n\n  it(\"knows how to create hrefs from location objects\", () => {\n    const href = history.createHref({\n      pathname: \"/the/path\",\n      search: \"?the=query\",\n      hash: \"#the-hash\",\n    });\n\n    expect(href).toEqual(\"/the/path?the=query#the-hash\");\n  });\n\n  it(\"knows how to create hrefs from strings\", () => {\n    const href = history.createHref(\"/the/path?the=query#the-hash\");\n    expect(href).toEqual(\"/the/path?the=query#the-hash\");\n  });\n\n  it(\"does not encode the generated path\", () => {\n    const encodedHref = history.createHref({\n      pathname: \"/%23abc\",\n    });\n    expect(encodedHref).toEqual(\"/%23abc\");\n\n    const unencodedHref = history.createHref({\n      pathname: \"/#abc\",\n    });\n    expect(unencodedHref).toEqual(\"/#abc\");\n  });\n\n  describe(\"listen\", () => {\n    it(\"does not immediately call listeners\", () => {\n      Listen(history);\n    });\n\n    it(\"calls listeners only for POP actions\", () => {\n      ListenPopOnly(history);\n    });\n  });\n\n  describe(\"the initial location\", () => {\n    it('has the \"default\" key', () => {\n      InitialLocationDefaultKey(history);\n    });\n  });\n\n  describe(\"push a new path\", () => {\n    it(\"calls change listeners with the new location\", () => {\n      PushNewLocation(history);\n    });\n  });\n\n  describe(\"push the same path\", () => {\n    it(\"calls change listeners with the new location\", async () => {\n      await PushSamePath(history);\n    });\n  });\n\n  describe(\"push state\", () => {\n    it(\"calls change listeners with the new location\", () => {\n      PushState(history);\n    });\n\n    it(\"re-throws when using non-serializable state\", () => {\n      PushStateInvalid(history, testWindow);\n    });\n  });\n\n  describe(\"push with no pathname\", () => {\n    it(\"reuses the current location pathname\", () => {\n      PushMissingPathname(history);\n    });\n  });\n\n  describe(\"push with a relative pathname\", () => {\n    it(\"normalizes the pathname relative to the current location\", () => {\n      PushRelativePathname(history);\n    });\n  });\n\n  describe(\"replace a new path\", () => {\n    it(\"calls change listeners with the new location\", () => {\n      ReplaceNewLocation(history);\n    });\n  });\n\n  describe(\"replace the same path\", () => {\n    it(\"calls change listeners with the new location\", () => {\n      ReplaceSamePath(history);\n    });\n  });\n\n  describe(\"replace state\", () => {\n    it(\"calls change listeners with the new location\", () => {\n      ReplaceState(history);\n    });\n  });\n\n  describe(\"location created with encoded/unencoded reserved characters\", () => {\n    it(\"produces different location objects\", () => {\n      EncodedReservedCharacters(history);\n    });\n  });\n\n  describe(\"back\", () => {\n    it(\"calls change listeners with the previous location\", async () => {\n      await GoBack(history);\n    });\n  });\n\n  describe(\"forward\", () => {\n    it(\"calls change listeners with the next location\", async () => {\n      await GoForward(history);\n    });\n  });\n});\n"
  },
  {
    "path": "packages/react-router/__tests__/router/context-middleware-test.tsx",
    "content": "import * as React from \"react\";\n\nimport { Route, createRoutesFromElements } from \"../../lib/components\";\nimport { createMemoryHistory } from \"../../lib/router/history\";\nimport type { Router, StaticHandlerContext } from \"../../lib/router/router\";\nimport {\n  createRouter,\n  createStaticHandler,\n  isDataWithResponseInit,\n  isResponse,\n} from \"../../lib/router/router\";\nimport type {\n  DataStrategyResult,\n  MiddlewareFunction,\n  RouterContext,\n} from \"../../lib/router/utils\";\nimport {\n  ErrorResponseImpl,\n  RouterContextProvider,\n  createContext,\n  data,\n  redirect,\n} from \"../../lib/router/utils\";\nimport { cleanup } from \"./utils/data-router-setup\";\nimport { createFormData, invariant, tick } from \"./utils/utils\";\n\nlet router: Router;\n\nafterEach(() => cleanup(router));\n\ndeclare module \"../../lib/router/utils\" {\n  interface RouterContext {\n    count?: { value: number };\n    order?: string[];\n  }\n}\n\nfunction respondWithJson(staticContext: StaticHandlerContext | Response) {\n  invariant(!isResponse(staticContext), \"Expected a StaticHandlerContext\");\n  return new Response(\n    JSON.stringify(staticContext, (key, value) =>\n      value instanceof Error ? `ERROR: ${value.message}` : value,\n    ),\n    {\n      status: staticContext.statusCode ?? 200,\n      headers: {\n        \"Content-Type\": \"application/json\",\n      },\n    },\n  );\n}\n\ndescribe(\"context/middleware\", () => {\n  // Simple context for just asserting that middlewares execute\n  let countContext = createContext(0);\n\n  // String contexts to ensure children middlewares have access to parent context values\n  let parentContext = createContext(\"empty\");\n  let childContext = createContext(\"empty\");\n\n  // Context for tracking the order in which middlewares/handlers run\n  let orderContext = createContext<string[]>([]);\n\n  let pushOrderContext = (\n    context: Readonly<RouterContextProvider>,\n    value: string,\n  ) => context.set(orderContext, [...(context.get(orderContext) || []), value]);\n\n  describe(\"context\", () => {\n    it(\"provides context to loaders and actions\", async () => {\n      router = createRouter({\n        history: createMemoryHistory(),\n        routes: [\n          {\n            path: \"/\",\n          },\n          {\n            id: \"a\",\n            path: \"/a\",\n            loader({ context }) {\n              context.set(countContext, context.get(countContext) + 1);\n              return context.get(countContext);\n            },\n          },\n          {\n            id: \"b\",\n            path: \"/b\",\n            action({ context }) {\n              context.set(countContext, context.get(countContext) + 1);\n              return context.get(countContext);\n            },\n            loader({ context }) {\n              context.set(countContext, context.get(countContext) + 1);\n              return context.get(countContext);\n            },\n          },\n        ],\n      });\n\n      await router.navigate(\"/a\");\n      expect(router.state.loaderData.a).toBe(1);\n\n      await router.navigate(\"/b\", {\n        formMethod: \"post\",\n        formData: createFormData({}),\n      });\n      expect(router.state.actionData?.b).toBe(1);\n      expect(router.state.loaderData.b).toBe(2);\n    });\n\n    it(\"works with dataStrategy for a sequential implementation\", async () => {\n      router = createRouter({\n        history: createMemoryHistory(),\n        routes: [\n          {\n            path: \"/\",\n          },\n          {\n            id: \"parent\",\n            path: \"/parent\",\n            async loader({ context }) {\n              context.set(parentContext, \"PARENT MIDDLEWARE\");\n              return context.get(parentContext);\n            },\n            children: [\n              {\n                id: \"child\",\n                path: \"child\",\n                loader({ context }) {\n                  context.set(childContext, \"CHILD MIDDLEWARE\");\n                  return context.get(childContext);\n                },\n              },\n            ],\n          },\n        ],\n        async dataStrategy({ matches }) {\n          let keyedResults: Record<string, DataStrategyResult> = {};\n          for (let m of matches) {\n            keyedResults[m.route.id] = await m.resolve();\n          }\n          return keyedResults;\n        },\n      });\n\n      await router.navigate(\"/parent/child\");\n\n      expect(router.state.loaderData).toEqual({\n        parent: \"PARENT MIDDLEWARE\",\n        child: \"CHILD MIDDLEWARE\",\n      });\n    });\n\n    it(\"works with dataStrategy for an easy middleware implementation\", async () => {\n      router = createRouter({\n        history: createMemoryHistory(),\n        routes: [\n          {\n            path: \"/\",\n          },\n          {\n            id: \"parent\",\n            path: \"/parent\",\n            loader: ({ context }) => ({\n              parent: context.get(parentContext),\n              child: context.get(childContext),\n            }),\n            handle: {\n              middleware(context: RouterContextProvider) {\n                context.set(parentContext, \"PARENT MIDDLEWARE\");\n              },\n            },\n            children: [\n              {\n                id: \"child\",\n                path: \"child\",\n                loader: ({ context }) => ({\n                  parent: context.get(parentContext),\n                  child: context.get(childContext),\n                }),\n                handle: {\n                  middleware(context: RouterContextProvider) {\n                    context.set(\n                      parentContext,\n                      context.get(parentContext) + \" (amended from child)\",\n                    );\n                    context.set(childContext, \"CHILD MIDDLEWARE\");\n                  },\n                },\n              },\n            ],\n          },\n        ],\n        async dataStrategy({ context, matches }) {\n          // Run middleware sequentially\n          for (let m of matches) {\n            await m.route.handle.middleware(context);\n          }\n\n          // Run loaders in parallel\n          let keyedResults: Record<string, DataStrategyResult> = {};\n          await Promise.all(\n            matches.map(async (m) => {\n              keyedResults[m.route.id] = await m.resolve();\n            }),\n          );\n          return keyedResults;\n        },\n      });\n\n      await router.navigate(\"/parent/child\");\n\n      expect(router.state.loaderData).toEqual({\n        child: expect.objectContaining({\n          child: \"CHILD MIDDLEWARE\",\n          parent: \"PARENT MIDDLEWARE (amended from child)\",\n        }),\n        parent: expect.objectContaining({\n          child: \"CHILD MIDDLEWARE\",\n          parent: \"PARENT MIDDLEWARE (amended from child)\",\n        }),\n      });\n    });\n  });\n\n  describe(\"middleware - client side\", () => {\n    function getOrderMiddleware(\n      orderContext: RouterContext<string[]>,\n      name: string,\n    ): MiddlewareFunction {\n      return async ({ context }, next) => {\n        context.set(orderContext, [\n          ...context.get(orderContext),\n          `${name} middleware - before next()`,\n        ]);\n        await tick(); // Force async to ensure ordering is correct\n        await next();\n        await tick(); // Force async to ensure ordering is correct\n        context.set(orderContext, [\n          ...context.get(orderContext),\n          `${name} middleware - after next()`,\n        ]);\n      };\n    }\n\n    describe(\"ordering\", () => {\n      it(\"runs middleware sequentially before and after loaders\", async () => {\n        let snapshot;\n        router = createRouter({\n          history: createMemoryHistory(),\n          routes: [\n            {\n              path: \"/\",\n            },\n            {\n              id: \"parent\",\n              path: \"/parent\",\n              middleware: [\n                async ({ context }, next) => {\n                  await next();\n                  // Grab a snapshot at the end of the upwards middleware chain\n                  snapshot = context.get(orderContext);\n                },\n                getOrderMiddleware(orderContext, \"a\"),\n                getOrderMiddleware(orderContext, \"b\"),\n              ],\n              loader({ context }) {\n                context.get(orderContext).push(\"parent loader\");\n              },\n              children: [\n                {\n                  id: \"child\",\n                  path: \"child\",\n                  middleware: [\n                    getOrderMiddleware(orderContext, \"c\"),\n                    getOrderMiddleware(orderContext, \"d\"),\n                  ],\n                  loader({ context }) {\n                    context.get(orderContext).push(\"child loader\");\n                  },\n                },\n              ],\n            },\n          ],\n        });\n\n        await router.navigate(\"/parent/child\");\n\n        expect(snapshot).toEqual([\n          \"a middleware - before next()\",\n          \"b middleware - before next()\",\n          \"c middleware - before next()\",\n          \"d middleware - before next()\",\n          \"parent loader\",\n          \"child loader\",\n          \"d middleware - after next()\",\n          \"c middleware - after next()\",\n          \"b middleware - after next()\",\n          \"a middleware - after next()\",\n        ]);\n      });\n\n      it(\"runs middleware on initialization even if no loaders exist\", async () => {\n        let snapshot;\n        router = createRouter({\n          history: createMemoryHistory(),\n          routes: [\n            {\n              path: \"/\",\n              middleware: [\n                async ({ context }, next) => {\n                  await next();\n                  // Grab a snapshot at the end of the upwards middleware chain\n                  snapshot = context.get(orderContext);\n                },\n                getOrderMiddleware(orderContext, \"a\"),\n                getOrderMiddleware(orderContext, \"b\"),\n              ],\n              children: [\n                {\n                  index: true,\n                  middleware: [\n                    getOrderMiddleware(orderContext, \"c\"),\n                    getOrderMiddleware(orderContext, \"d\"),\n                  ],\n                },\n              ],\n            },\n          ],\n        });\n        let initPromise = new Promise((r) => {\n          let unsub = router.subscribe((state) => {\n            if (state.initialized) {\n              unsub();\n              r(undefined);\n            }\n          });\n        });\n        await router.initialize();\n        await initPromise;\n        expect(router.state).toMatchObject({\n          initialized: true,\n          location: { pathname: \"/\" },\n          navigation: { state: \"idle\" },\n          errors: null,\n        });\n        expect(snapshot).toEqual([\n          \"a middleware - before next()\",\n          \"b middleware - before next()\",\n          \"c middleware - before next()\",\n          \"d middleware - before next()\",\n          \"d middleware - after next()\",\n          \"c middleware - after next()\",\n          \"b middleware - after next()\",\n          \"a middleware - after next()\",\n        ]);\n      });\n\n      it(\"runs middleware even if no loaders exist\", async () => {\n        let snapshot;\n        router = createRouter({\n          history: createMemoryHistory(),\n          routes: [\n            {\n              path: \"/\",\n            },\n            {\n              id: \"parent\",\n              path: \"/parent\",\n              middleware: [\n                async ({ context }, next) => {\n                  await next();\n                  // Grab a snapshot at the end of the upwards middleware chain\n                  snapshot = context.get(orderContext);\n                },\n                getOrderMiddleware(orderContext, \"a\"),\n                getOrderMiddleware(orderContext, \"b\"),\n              ],\n              children: [\n                {\n                  id: \"child\",\n                  path: \"child\",\n                  middleware: [\n                    getOrderMiddleware(orderContext, \"c\"),\n                    getOrderMiddleware(orderContext, \"d\"),\n                  ],\n                },\n              ],\n            },\n          ],\n        });\n\n        await router.navigate(\"/parent/child\");\n\n        expect(snapshot).toEqual([\n          \"a middleware - before next()\",\n          \"b middleware - before next()\",\n          \"c middleware - before next()\",\n          \"d middleware - before next()\",\n          \"d middleware - after next()\",\n          \"c middleware - after next()\",\n          \"b middleware - after next()\",\n          \"a middleware - after next()\",\n        ]);\n      });\n\n      it(\"runs middleware sequentially before and after actions\", async () => {\n        let snapshot;\n        router = createRouter({\n          history: createMemoryHistory(),\n          routes: [\n            {\n              path: \"/\",\n            },\n            {\n              id: \"parent\",\n              path: \"/parent\",\n              middleware: [\n                async ({ context }, next) => {\n                  await next();\n                  // Grab a snapshot at the end of the upwards middleware chain\n                  snapshot = context.get(orderContext);\n                },\n                getOrderMiddleware(orderContext, \"a\"),\n                getOrderMiddleware(orderContext, \"b\"),\n              ],\n              loader({ context }) {\n                context.get(orderContext).push(\"parent loader\");\n              },\n              children: [\n                {\n                  id: \"child\",\n                  path: \"child\",\n                  middleware: [\n                    getOrderMiddleware(orderContext, \"c\"),\n                    getOrderMiddleware(orderContext, \"d\"),\n                  ],\n                  action({ context }) {\n                    context.get(orderContext).push(\"child action\");\n                  },\n                  loader({ context }) {\n                    context.get(orderContext).push(\"child loader\");\n                  },\n                },\n              ],\n            },\n          ],\n        });\n\n        await router.navigate(\"/parent/child\", {\n          formMethod: \"post\",\n          formData: createFormData({}),\n        });\n\n        expect(snapshot).toEqual([\n          // Action\n          \"a middleware - before next()\",\n          \"b middleware - before next()\",\n          \"c middleware - before next()\",\n          \"d middleware - before next()\",\n          \"child action\",\n          \"d middleware - after next()\",\n          \"c middleware - after next()\",\n          \"b middleware - after next()\",\n          \"a middleware - after next()\",\n          // Revalidation\n          \"a middleware - before next()\",\n          \"b middleware - before next()\",\n          \"c middleware - before next()\",\n          \"d middleware - before next()\",\n          \"parent loader\",\n          \"child loader\",\n          \"d middleware - after next()\",\n          \"c middleware - after next()\",\n          \"b middleware - after next()\",\n          \"a middleware - after next()\",\n        ]);\n      });\n\n      it(\"runs middleware even if no loader exists but an action is present\", async () => {\n        let snapshot;\n        router = createRouter({\n          history: createMemoryHistory(),\n          routes: [\n            {\n              path: \"/\",\n            },\n            {\n              id: \"parent\",\n              path: \"/parent\",\n              middleware: [\n                async ({ context }, next) => {\n                  await next();\n                  // Grab a snapshot at the end of the upwards middleware chain\n                  snapshot = context.get(orderContext);\n                },\n                getOrderMiddleware(orderContext, \"a\"),\n                getOrderMiddleware(orderContext, \"b\"),\n              ],\n              children: [\n                {\n                  id: \"child\",\n                  path: \"child\",\n                  middleware: [\n                    getOrderMiddleware(orderContext, \"c\"),\n                    getOrderMiddleware(orderContext, \"d\"),\n                  ],\n                  action({ context }) {\n                    context.get(orderContext).push(\"child action\");\n                  },\n                },\n              ],\n            },\n          ],\n        });\n\n        await router.navigate(\"/parent/child\", {\n          formMethod: \"post\",\n          formData: createFormData({}),\n        });\n\n        expect(snapshot).toEqual([\n          // Action\n          \"a middleware - before next()\",\n          \"b middleware - before next()\",\n          \"c middleware - before next()\",\n          \"d middleware - before next()\",\n          \"child action\",\n          \"d middleware - after next()\",\n          \"c middleware - after next()\",\n          \"b middleware - after next()\",\n          \"a middleware - after next()\",\n          // Revalidation\n          \"a middleware - before next()\",\n          \"b middleware - before next()\",\n          \"c middleware - before next()\",\n          \"d middleware - before next()\",\n          \"d middleware - after next()\",\n          \"c middleware - after next()\",\n          \"b middleware - after next()\",\n          \"a middleware - after next()\",\n        ]);\n      });\n\n      it(\"returns result of middleware in client side routers\", async () => {\n        let values: unknown[] = [];\n        let consoleSpy = jest\n          .spyOn(console, \"warn\")\n          .mockImplementation(() => {});\n        router = createRouter({\n          history: createMemoryHistory(),\n          routes: [\n            {\n              path: \"/\",\n            },\n            {\n              id: \"parent\",\n              path: \"/parent\",\n              middleware: [\n                async (_, next) => {\n                  let results = (await next()) as Record<\n                    string,\n                    DataStrategyResult\n                  >;\n                  values.push({ ...results });\n                  return results;\n                },\n              ],\n              loader() {\n                return \"PARENT\";\n              },\n              children: [\n                {\n                  id: \"child\",\n                  path: \"child\",\n                  middleware: [\n                    async (_, next) => {\n                      let results = (await next()) as Record<\n                        string,\n                        DataStrategyResult\n                      >;\n                      values.push({ ...results });\n                      return results;\n                    },\n                  ],\n                  loader() {\n                    return \"CHILD\";\n                  },\n                },\n              ],\n            },\n          ],\n        });\n\n        await router.navigate(\"/parent/child\");\n\n        expect(router.state.loaderData).toMatchObject({\n          parent: \"PARENT\",\n          child: \"CHILD\",\n        });\n        expect(values).toEqual([\n          {\n            parent: { type: \"data\", result: \"PARENT\" },\n            child: { type: \"data\", result: \"CHILD\" },\n          },\n          {\n            parent: { type: \"data\", result: \"PARENT\" },\n            child: { type: \"data\", result: \"CHILD\" },\n          },\n        ]);\n\n        consoleSpy.mockRestore();\n      });\n\n      it(\"does not require that you call next()\", async () => {\n        router = createRouter({\n          history: createMemoryHistory(),\n          routes: [\n            {\n              path: \"/\",\n            },\n            {\n              id: \"parent\",\n              path: \"/parent\",\n              middleware: [\n                ({ context }) => {\n                  context.set(parentContext, \"PARENT MIDDLEWARE\");\n                },\n              ],\n              loader({ context }) {\n                return context.get(parentContext);\n              },\n              children: [\n                {\n                  id: \"child\",\n                  path: \"child\",\n                  middleware: [\n                    ({ context }) => {\n                      context.set(childContext, \"CHILD MIDDLEWARE\");\n                    },\n                  ],\n                  loader({ context }) {\n                    return context.get(childContext);\n                  },\n                },\n              ],\n            },\n          ],\n        });\n\n        await router.navigate(\"/parent/child\");\n\n        expect(router.state.loaderData).toEqual({\n          parent: \"PARENT MIDDLEWARE\",\n          child: \"CHILD MIDDLEWARE\",\n        });\n        expect(router.state.errors).toBeNull();\n      });\n\n      it(\"errors if you try to call next more than once in a middleware\", async () => {\n        router = createRouter({\n          history: createMemoryHistory(),\n          routes: [\n            {\n              path: \"/\",\n            },\n            {\n              id: \"parent\",\n              path: \"/parent\",\n              middleware: [\n                async (_, next) => {\n                  await next();\n                  await next();\n                },\n              ],\n              loader() {\n                return \"PARENT\";\n              },\n            },\n          ],\n        });\n\n        await router.navigate(\"/parent\");\n\n        expect(router.state.loaderData).toEqual({});\n        expect(router.state.errors).toEqual({\n          parent: new Error(\"You may only call `next()` once per middleware\"),\n        });\n      });\n\n      it(\"creates a new context per navigation/fetcher call\", async () => {\n        router = createRouter({\n          history: createMemoryHistory(),\n          routes: [\n            {\n              id: \"index\",\n              path: \"/\",\n            },\n            {\n              id: \"page\",\n              path: \"/page\",\n              middleware: [\n                ({ context }) => {\n                  context.set(countContext, context.get(countContext) + 1);\n                },\n              ],\n              action({ context }) {\n                return context.get(countContext);\n              },\n              loader({ context }) {\n                return context.get(countContext);\n              },\n            },\n          ],\n        });\n\n        await router.navigate(\"/page\");\n        expect(router.state.loaderData.page).toBe(1);\n\n        await router.navigate(\"/\");\n        await router.navigate(\"/page\");\n        expect(router.state.loaderData.page).toBe(1);\n\n        await router.navigate(\"/\");\n        await router.navigate(\"/page\", {\n          formMethod: \"post\",\n          formData: createFormData({}),\n        });\n        expect(router.state.actionData?.page).toBe(1);\n        // context persists from action -> loader\n        expect(router.state.loaderData.page).toBe(2);\n\n        let fetcherData;\n        let unsub = router.subscribe((state) => {\n          if (state.fetchers.get(\"a\")?.data) {\n            fetcherData = state.fetchers.get(\"a\")?.data;\n          }\n        });\n        await router.fetch(\"a\", \"page\", \"/page\");\n        expect(fetcherData).toEqual(1);\n\n        await router.fetch(\"a\", \"page\", \"/page\", {\n          formMethod: \"post\",\n          formData: createFormData({}),\n        });\n        expect(fetcherData).toEqual(1);\n        // context persists from action -> loader\n        expect(router.state.loaderData.page).toEqual(2);\n\n        unsub();\n      });\n\n      it(\"works with createRoutesFromElements\", async () => {\n        let context = new RouterContextProvider();\n        router = createRouter({\n          history: createMemoryHistory(),\n          getContext: () => context,\n          routes: createRoutesFromElements(\n            <>\n              <Route path=\"/\" />\n              <Route\n                id=\"parent\"\n                path=\"/parent\"\n                middleware={[\n                  getOrderMiddleware(orderContext, \"a\"),\n                  getOrderMiddleware(orderContext, \"b\"),\n                ]}\n                loader={({ context }) => {\n                  context.get(orderContext).push(\"parent loader\");\n                }}\n              >\n                <Route\n                  id=\"child\"\n                  path=\"child\"\n                  middleware={[\n                    getOrderMiddleware(orderContext, \"c\"),\n                    getOrderMiddleware(orderContext, \"d\"),\n                  ]}\n                  loader={({ context }) => {\n                    context.get(orderContext).push(\"child loader\");\n                  }}\n                />\n              </Route>\n            </>,\n          ),\n        });\n\n        await router.navigate(\"/parent/child\");\n\n        expect(context.get(orderContext)).toEqual([\n          \"a middleware - before next()\",\n          \"b middleware - before next()\",\n          \"c middleware - before next()\",\n          \"d middleware - before next()\",\n          \"parent loader\",\n          \"child loader\",\n          \"d middleware - after next()\",\n          \"c middleware - after next()\",\n          \"b middleware - after next()\",\n          \"a middleware - after next()\",\n        ]);\n      });\n    });\n\n    describe(\"lazy\", () => {\n      it(\"runs lazy loaded middleware\", async () => {\n        let snapshot;\n        router = createRouter({\n          history: createMemoryHistory(),\n          routes: [\n            {\n              path: \"/\",\n            },\n            {\n              id: \"parent\",\n              path: \"/parent\",\n              lazy: {\n                middleware: async () => [\n                  async ({ context }, next) => {\n                    await next();\n                    // Grab a snapshot at the end of the upwards middleware chain\n                    snapshot = context.get(orderContext);\n                  },\n                  getOrderMiddleware(orderContext, \"a\"),\n                  getOrderMiddleware(orderContext, \"b\"),\n                ],\n              },\n              loader({ context }) {\n                context.get(orderContext).push(\"parent loader\");\n              },\n              children: [\n                {\n                  id: \"child\",\n                  path: \"child\",\n                  lazy: {\n                    middleware: async () => [\n                      getOrderMiddleware(orderContext, \"c\"),\n                      getOrderMiddleware(orderContext, \"d\"),\n                    ],\n                  },\n                  loader({ context }) {\n                    context.get(orderContext).push(\"child loader\");\n                  },\n                },\n              ],\n            },\n          ],\n        });\n\n        await router.navigate(\"/parent/child\");\n\n        expect(snapshot).toEqual([\n          \"a middleware - before next()\",\n          \"b middleware - before next()\",\n          \"c middleware - before next()\",\n          \"d middleware - before next()\",\n          \"parent loader\",\n          \"child loader\",\n          \"d middleware - after next()\",\n          \"c middleware - after next()\",\n          \"b middleware - after next()\",\n          \"a middleware - after next()\",\n        ]);\n      });\n\n      it(\"runs lazy loaded middleware when static middleware is defined\", async () => {\n        let snapshot;\n        router = createRouter({\n          history: createMemoryHistory(),\n          routes: [\n            {\n              path: \"/\",\n            },\n            {\n              id: \"parent\",\n              path: \"/parent\",\n              middleware: [\n                async ({ context }, next) => {\n                  await next();\n                  // Grab a snapshot at the end of the upwards middleware chain\n                  snapshot = context.get(orderContext);\n                },\n                getOrderMiddleware(orderContext, \"a\"),\n                getOrderMiddleware(orderContext, \"b\"),\n              ],\n              loader({ context }) {\n                context.get(orderContext).push(\"parent loader\");\n              },\n              children: [\n                {\n                  id: \"child\",\n                  path: \"child\",\n                  lazy: {\n                    middleware: async () => [\n                      getOrderMiddleware(orderContext, \"c\"),\n                      getOrderMiddleware(orderContext, \"d\"),\n                    ],\n                  },\n                  loader({ context }) {\n                    context.get(orderContext).push(\"child loader\");\n                  },\n                },\n              ],\n            },\n          ],\n        });\n\n        await router.navigate(\"/parent/child\");\n\n        expect(snapshot).toEqual([\n          \"a middleware - before next()\",\n          \"b middleware - before next()\",\n          \"c middleware - before next()\",\n          \"d middleware - before next()\",\n          \"parent loader\",\n          \"child loader\",\n          \"d middleware - after next()\",\n          \"c middleware - after next()\",\n          \"b middleware - after next()\",\n          \"a middleware - after next()\",\n        ]);\n      });\n\n      it(\"ignores middleware returned from route.lazy function\", async () => {\n        let snapshot;\n\n        let consoleWarn = jest\n          .spyOn(console, \"warn\")\n          .mockImplementation(() => {});\n\n        router = createRouter({\n          history: createMemoryHistory(),\n          routes: [\n            {\n              path: \"/\",\n            },\n            {\n              id: \"parent\",\n              path: \"/parent\",\n              lazy: {\n                middleware: async () => [\n                  async ({ context }, next) => {\n                    await next();\n                    // Grab a snapshot at the end of the upwards middleware chain\n                    snapshot = context.get(orderContext);\n                  },\n                  getOrderMiddleware(orderContext, \"a\"),\n                  getOrderMiddleware(orderContext, \"b\"),\n                ],\n              },\n              loader({ context }) {\n                context.get(orderContext).push(\"parent loader\");\n              },\n              children: [\n                {\n                  id: \"child\",\n                  path: \"child\",\n                  // @ts-expect-error\n                  lazy: async () => ({\n                    middleware: [\n                      getOrderMiddleware(orderContext, \"c\"),\n                      getOrderMiddleware(orderContext, \"d\"),\n                    ],\n                  }),\n                  loader({ context }) {\n                    context.get(orderContext).push(\"child loader\");\n                  },\n                },\n              ],\n            },\n          ],\n        });\n\n        await router.navigate(\"/parent/child\");\n\n        expect(snapshot).toEqual([\n          \"a middleware - before next()\",\n          \"b middleware - before next()\",\n          \"parent loader\",\n          \"child loader\",\n          \"b middleware - after next()\",\n          \"a middleware - after next()\",\n        ]);\n\n        expect(consoleWarn).toHaveBeenCalledWith(\n          \"Route property middleware is not a supported property to be returned from a lazy route function. This property will be ignored.\",\n        );\n      });\n    });\n\n    describe(\"throwing\", () => {\n      it(\"throwing from a middleware bubbles up (going down - loader)\", async () => {\n        let context = new RouterContextProvider();\n        router = createRouter({\n          history: createMemoryHistory(),\n          getContext: () => context,\n          routes: [\n            {\n              path: \"/\",\n            },\n            {\n              id: \"parent\",\n              path: \"/parent\",\n              middleware: [\n                async ({ context }, next) => {\n                  pushOrderContext(context, \"PARENT DOWN\");\n                  await next();\n                  pushOrderContext(context, \"PARENT UP\");\n                },\n                () => {\n                  throw new Error(\"PARENT ERROR\");\n                },\n              ],\n              loader() {\n                return \"PARENT\";\n              },\n              children: [\n                {\n                  id: \"child\",\n                  path: \"child\",\n                  middleware: [\n                    async ({ context }, next) => {\n                      context.set(orderContext, [\n                        ...(context.get(orderContext) || []),\n                        \"CHILD DOWN\",\n                      ]);\n                      await next();\n                      context.set(orderContext, [\n                        ...(context.get(orderContext) || []),\n                        \"CHILD UP\",\n                      ]);\n                    },\n                  ],\n                  loader() {\n                    return \"CHILD\";\n                  },\n                },\n              ],\n            },\n          ],\n        });\n\n        await router.navigate(\"/parent/child\");\n\n        expect(router.state.loaderData).toEqual({});\n        expect(router.state.errors).toEqual({\n          parent: new Error(\"PARENT ERROR\"),\n        });\n        expect(context.get(orderContext)).toEqual([\"PARENT DOWN\", \"PARENT UP\"]);\n      });\n\n      it(\"throwing from a middleware bubbles up (going up - loader)\", async () => {\n        let context = new RouterContextProvider();\n        router = createRouter({\n          history: createMemoryHistory(),\n          getContext: () => context,\n          routes: [\n            {\n              path: \"/\",\n            },\n            {\n              id: \"parent\",\n              path: \"/parent\",\n              middleware: [\n                async (_, next) => {\n                  pushOrderContext(context, \"PARENT DOWN\");\n                  await next();\n                  pushOrderContext(context, \"PARENT UP\");\n                },\n              ],\n              loader() {\n                return \"PARENT\";\n              },\n              children: [\n                {\n                  id: \"child\",\n                  path: \"child\",\n                  middleware: [\n                    async (_, next) => {\n                      pushOrderContext(context, \"CHILD DOWN\");\n                      await next();\n                      throw new Error(\"CHILD UP\");\n                    },\n                  ],\n                  loader() {\n                    return \"CHILD\";\n                  },\n                },\n              ],\n            },\n          ],\n        });\n\n        await router.navigate(\"/parent/child\");\n\n        expect(router.state.loaderData).toEqual({\n          parent: \"PARENT\",\n        });\n        expect(router.state.errors).toEqual({\n          parent: new Error(\"CHILD UP\"),\n        });\n        expect(context.get(orderContext)).toEqual([\n          \"PARENT DOWN\",\n          \"CHILD DOWN\",\n          \"PARENT UP\",\n        ]);\n      });\n\n      it(\"throwing from a middleware short circuits immediately (going down - action w/boundary)\", async () => {\n        let context = new RouterContextProvider();\n        router = createRouter({\n          history: createMemoryHistory(),\n          getContext: () => context,\n          routes: [\n            {\n              path: \"/\",\n            },\n            {\n              id: \"parent\",\n              path: \"/parent\",\n              middleware: [\n                async ({ request, context }, next) => {\n                  if (request.method !== \"GET\") {\n                    pushOrderContext(context, \"parent action start\");\n                    await next();\n                    pushOrderContext(context, \"parent action end\");\n                  } else {\n                    pushOrderContext(context, \"parent loader start\");\n                    await next();\n                    pushOrderContext(context, \"parent loader end\");\n                  }\n                },\n              ],\n              loader() {\n                return \"PARENT\";\n              },\n              children: [\n                {\n                  id: \"child\",\n                  path: \"child\",\n                  hasErrorBoundary: true,\n                  middleware: [\n                    async ({ request, context }, next) => {\n                      if (request.method !== \"GET\") {\n                        pushOrderContext(context, \"child 1 start - throwing\");\n                        throw new Error(\"child 1 action error\");\n                      } else {\n                        pushOrderContext(context, \"child 1 loader start\");\n                        await next();\n                        pushOrderContext(context, \"child 1 loader end\");\n                      }\n                    },\n                    async ({ request, context }, next) => {\n                      if (request.method !== \"GET\") {\n                        pushOrderContext(context, \"child 2 start\");\n                        await next();\n                        pushOrderContext(context, \"child 2 end\");\n                      } else {\n                        pushOrderContext(context, \"child 2 loader start\");\n                        await next();\n                        pushOrderContext(context, \"child 2 loader end\");\n                      }\n                    },\n                  ],\n                  action() {\n                    return \"ACTION\";\n                  },\n                  loader() {\n                    return \"CHILD\";\n                  },\n                },\n              ],\n            },\n          ],\n        });\n\n        await router.navigate(\"/parent/child\", {\n          formMethod: \"post\",\n          formData: createFormData({}),\n        });\n\n        expect(context.get(orderContext)).toEqual([\n          \"parent action start\",\n          \"child 1 start - throwing\",\n          \"parent action end\",\n          \"parent loader start\",\n          \"child 1 loader start\",\n          \"child 2 loader start\",\n          \"child 2 loader end\",\n          \"child 1 loader end\",\n          \"parent loader end\",\n        ]);\n        expect(router.state.loaderData).toMatchInlineSnapshot(`\n          {\n            \"child\": undefined,\n            \"parent\": \"PARENT\",\n          }\n        `);\n        expect(router.state.errors).toMatchInlineSnapshot(`\n          {\n            \"child\": [Error: child 1 action error],\n          }\n        `);\n      });\n\n      it(\"throwing from a middleware short circuits immediately (going up - action w/boundary)\", async () => {\n        let context = new RouterContextProvider();\n        router = createRouter({\n          history: createMemoryHistory(),\n          getContext: () => context,\n          routes: [\n            {\n              path: \"/\",\n            },\n            {\n              id: \"parent\",\n              path: \"/parent\",\n              middleware: [\n                async () => {\n                  // next was unused\n                },\n                async ({ request, context }, next) => {\n                  if (request.method !== \"GET\") {\n                    pushOrderContext(context, \"parent action start\");\n                    await next();\n                    pushOrderContext(context, \"parent action end\");\n                  } else {\n                    pushOrderContext(context, \"parent loader start\");\n                    await next();\n                    pushOrderContext(context, \"parent loader end\");\n                  }\n                },\n              ],\n              loader() {\n                return \"PARENT\";\n              },\n              children: [\n                {\n                  id: \"child\",\n                  path: \"child\",\n                  hasErrorBoundary: true,\n                  middleware: [\n                    async ({ request, context }, next) => {\n                      if (request.method !== \"GET\") {\n                        pushOrderContext(context, \"child 1 start\");\n                        await next();\n                        pushOrderContext(context, \"child 1 end\");\n                      } else {\n                        pushOrderContext(context, \"child 1 loader start\");\n                        await next();\n                        pushOrderContext(context, \"child 1 loader end\");\n                      }\n                    },\n                    async ({ request, context }, next) => {\n                      if (request.method !== \"GET\") {\n                        pushOrderContext(context, \"child 2 start\");\n                        await next();\n                        pushOrderContext(context, \"child 2 end - throwing\");\n                        throw new Error(\"child 2 action error\");\n                      } else {\n                        pushOrderContext(context, \"child 2 loader start\");\n                        await next();\n                        pushOrderContext(context, \"child 2 loader end\");\n                      }\n                    },\n                  ],\n                  action() {\n                    return \"ACTION\";\n                  },\n                  loader() {\n                    return \"CHILD\";\n                  },\n                },\n              ],\n            },\n          ],\n        });\n\n        await router.navigate(\"/parent/child\", {\n          formMethod: \"post\",\n          formData: createFormData({}),\n        });\n\n        expect(context.get(orderContext)).toEqual([\n          \"parent action start\",\n          \"child 1 start\",\n          \"child 2 start\",\n          \"child 2 end - throwing\",\n          \"child 1 end\",\n          \"parent action end\",\n          \"parent loader start\",\n          \"child 1 loader start\",\n          \"child 2 loader start\",\n          \"child 2 loader end\",\n          \"child 1 loader end\",\n          \"parent loader end\",\n        ]);\n        expect(router.state.loaderData).toEqual({\n          child: undefined,\n          parent: \"PARENT\",\n        });\n        expect(router.state.errors).toEqual({\n          child: new Error(\"child 2 action error\"),\n        });\n      });\n\n      it(\"throwing from a middleware short circuits immediately (going down - action w/o boundary)\", async () => {\n        let context = new RouterContextProvider();\n        router = createRouter({\n          history: createMemoryHistory(),\n          getContext: () => context,\n          routes: [\n            {\n              path: \"/\",\n            },\n            {\n              id: \"parent\",\n              path: \"/parent\",\n              hasErrorBoundary: true,\n              middleware: [\n                async ({ request, context }, next) => {\n                  if (request.method !== \"GET\") {\n                    pushOrderContext(context, \"parent action start\");\n                    await next();\n                    pushOrderContext(context, \"parent action end\");\n                  } else {\n                    pushOrderContext(context, \"parent loader start\");\n                    await next();\n                    pushOrderContext(context, \"parent loader end\");\n                  }\n                },\n              ],\n              loader() {\n                return \"PARENT\";\n              },\n              children: [\n                {\n                  id: \"child\",\n                  path: \"child\",\n                  middleware: [\n                    async ({ request, context }, next) => {\n                      if (request.method !== \"GET\") {\n                        pushOrderContext(context, \"child 1 start - throwing\");\n                        throw new Error(\"child 1 action error\");\n                      } else {\n                        pushOrderContext(context, \"child 1 loader start\");\n                        await next();\n                        pushOrderContext(context, \"child 1 loader end\");\n                      }\n                    },\n                    async ({ request, context }, next) => {\n                      if (request.method !== \"GET\") {\n                        pushOrderContext(context, \"child 2 start\");\n                        await next();\n                        pushOrderContext(context, \"child 2 end\");\n                      } else {\n                        pushOrderContext(context, \"child 2 loader start\");\n                        await next();\n                        pushOrderContext(context, \"child 2 loader end\");\n                      }\n                    },\n                  ],\n                  action() {\n                    return \"ACTION\";\n                  },\n                  loader() {\n                    return \"CHILD\";\n                  },\n                },\n              ],\n            },\n          ],\n        });\n\n        await router.navigate(\"/parent/child\", {\n          formMethod: \"post\",\n          formData: createFormData({}),\n        });\n\n        expect(context.get(orderContext)).toEqual([\n          \"parent action start\",\n          \"child 1 start - throwing\",\n          \"parent action end\",\n          \"parent loader start\",\n          \"child 1 loader start\",\n          \"child 2 loader start\",\n          \"child 2 loader end\",\n          \"child 1 loader end\",\n          \"parent loader end\",\n        ]);\n        expect(router.state.loaderData).toEqual({});\n        expect(router.state.errors).toEqual({\n          parent: new Error(\"child 1 action error\"),\n        });\n      });\n\n      it(\"throwing from a middleware short circuits immediately (going up - action w/o boundary)\", async () => {\n        let context = new RouterContextProvider();\n        router = createRouter({\n          history: createMemoryHistory(),\n          getContext: () => context,\n          routes: [\n            {\n              path: \"/\",\n            },\n            {\n              id: \"parent\",\n              path: \"/parent\",\n              hasErrorBoundary: true,\n              middleware: [\n                async ({ request, context }, next) => {\n                  if (request.method !== \"GET\") {\n                    pushOrderContext(context, \"parent action start\");\n                    await next();\n                    pushOrderContext(context, \"parent action end\");\n                  } else {\n                    pushOrderContext(context, \"parent loader start\");\n                    await next();\n                    pushOrderContext(context, \"parent loader end\");\n                  }\n                },\n              ],\n              loader() {\n                return \"PARENT\";\n              },\n              children: [\n                {\n                  id: \"child\",\n                  path: \"child\",\n                  middleware: [\n                    async ({ request, context }, next) => {\n                      if (request.method !== \"GET\") {\n                        pushOrderContext(context, \"child 1 start\");\n                        await next();\n                        pushOrderContext(context, \"child 1 end\");\n                      } else {\n                        pushOrderContext(context, \"child 1 loader start\");\n                        await next();\n                        pushOrderContext(context, \"child 1 loader end\");\n                      }\n                    },\n                    async ({ request, context }, next) => {\n                      if (request.method !== \"GET\") {\n                        pushOrderContext(context, \"child 2 start\");\n                        await next();\n                        pushOrderContext(context, \"child 2 end - throwing\");\n                        throw new Error(\"child 2 action error\");\n                      } else {\n                        pushOrderContext(context, \"child 2 loader start\");\n                        await next();\n                        pushOrderContext(context, \"child 2 loader end\");\n                      }\n                    },\n                  ],\n                  action() {\n                    return \"ACTION\";\n                  },\n                  loader() {\n                    return \"CHILD\";\n                  },\n                },\n              ],\n            },\n          ],\n        });\n\n        await router.navigate(\"/parent/child\", {\n          formMethod: \"post\",\n          formData: createFormData({}),\n        });\n\n        expect(context.get(orderContext)).toEqual([\n          \"parent action start\",\n          \"child 1 start\",\n          \"child 2 start\",\n          \"child 2 end - throwing\",\n          \"child 1 end\",\n          \"parent action end\",\n          \"parent loader start\",\n          \"child 1 loader start\",\n          \"child 2 loader start\",\n          \"child 2 loader end\",\n          \"child 1 loader end\",\n          \"parent loader end\",\n        ]);\n        expect(router.state.loaderData).toEqual({});\n        expect(router.state.errors).toEqual({\n          parent: new Error(\"child 2 action error\"),\n        });\n      });\n\n      it(\"allows thrown redirects before next()\", async () => {\n        router = createRouter({\n          history: createMemoryHistory(),\n          routes: [\n            {\n              path: \"/\",\n            },\n            {\n              path: \"/parent\",\n              middleware: [\n                async () => {\n                  throw redirect(\"/target\");\n                },\n              ],\n              loader() {\n                return \"PARENT\";\n              },\n            },\n            {\n              path: \"/target\",\n            },\n          ],\n        });\n\n        await router.navigate(\"/parent\");\n\n        expect(router.state).toMatchObject({\n          location: {\n            pathname: \"/target\",\n          },\n          loaderData: {},\n          errors: null,\n        });\n      });\n\n      it(\"allows thrown redirects after next()\", async () => {\n        router = createRouter({\n          history: createMemoryHistory(),\n          routes: [\n            {\n              path: \"/\",\n            },\n            {\n              path: \"/parent\",\n              middleware: [\n                async (_, next) => {\n                  await next();\n                  throw redirect(\"/target\");\n                },\n              ],\n              loader() {\n                return \"PARENT\";\n              },\n            },\n            {\n              path: \"/target\",\n            },\n          ],\n        });\n\n        await router.navigate(\"/parent\");\n\n        expect(router.state).toMatchObject({\n          location: {\n            pathname: \"/target\",\n          },\n          loaderData: {},\n          errors: null,\n        });\n      });\n\n      it(\"throwing from a middleware before next bubbles up to the highest route with a loader\", async () => {\n        router = createRouter({\n          history: createMemoryHistory(),\n          routes: [\n            {\n              path: \"/\",\n            },\n            {\n              id: \"a\",\n              path: \"/a\",\n              hasErrorBoundary: true,\n              children: [\n                {\n                  id: \"b\",\n                  path: \"b\",\n                  hasErrorBoundary: true,\n                  loader: () => \"B\",\n                  children: [\n                    {\n                      id: \"c\",\n                      path: \"c\",\n                      hasErrorBoundary: true,\n                      children: [\n                        {\n                          id: \"d\",\n                          path: \"d\",\n                          hasErrorBoundary: true,\n                          middleware: [\n                            () => {\n                              throw new Error(\"D ERROR\");\n                            },\n                          ],\n                          loader: () => \"D\",\n                        },\n                        {\n                          id: \"e\",\n                          path: \"e\",\n                          hasErrorBoundary: true,\n                          middleware: [\n                            () => {\n                              throw new Error(\"E ERROR\");\n                            },\n                          ],\n                          loader: () => \"E\",\n                        },\n                      ],\n                    },\n                  ],\n                },\n              ],\n            },\n          ],\n        });\n\n        // Bubbles to B because it's the initial load and it's loader hasn't run\n        await router.navigate(\"/a/b/c/d\");\n        expect(router.state.loaderData).toEqual({});\n        expect(router.state.errors).toEqual({\n          b: new Error(\"D ERROR\"),\n        });\n\n        // Load data into B\n        await router.navigate(\"/a/b\");\n        expect(router.state.loaderData).toEqual({ b: \"B\" });\n        expect(router.state.errors).toEqual(null);\n\n        // B doesn't have to revalidate so we can surface this error at E\n        await router.navigate(\"/a/b/c/e\");\n        expect(router.state.loaderData).toEqual({ b: \"B\" });\n        expect(router.state.errors).toEqual({\n          e: new Error(\"E ERROR\"),\n        });\n      });\n\n      it(\"throwing from a fetcher action middleware before next bubbles up to the boundary\", async () => {\n        router = createRouter({\n          history: createMemoryHistory(),\n          routes: [\n            {\n              path: \"/\",\n            },\n            {\n              id: \"a\",\n              path: \"/a\",\n              hasErrorBoundary: true,\n              children: [\n                {\n                  id: \"b\",\n                  path: \"b\",\n                  hasErrorBoundary: false,\n                  middleware: [\n                    ({ request }) => {\n                      if (request.method === \"POST\") {\n                        throw new Error(\"B ERROR\");\n                      }\n                    },\n                  ],\n                  action: () => \"B\",\n                },\n              ],\n            },\n          ],\n        });\n\n        // Bubbles to B because it's the initial load and it's loader hasn't run\n        await router.navigate(\"/a/b\");\n        expect(router.state.loaderData).toEqual({});\n        expect(router.state.errors).toBeNull();\n\n        let data;\n        router.subscribe((state) => {\n          data ??= state.fetchers.get(\"key\")?.data;\n        });\n\n        await router.fetch(\"key\", \"b\", \"/a/b\", {\n          formMethod: \"post\",\n          formData: createFormData({}),\n        });\n        expect(data).toBeUndefined();\n        expect(router.state.errors).toEqual({\n          a: new Error(\"B ERROR\"),\n        });\n      });\n    });\n\n    describe(\"redirects\", () => {\n      it(\"allows you to return redirects before next from client middleware\", async () => {\n        router = createRouter({\n          history: createMemoryHistory(),\n          routes: [\n            {\n              path: \"/\",\n            },\n            {\n              path: \"/redirect\",\n              middleware: [\n                async () => {\n                  return redirect(\"/target\");\n                },\n              ],\n            },\n            {\n              path: \"/target\",\n            },\n          ],\n        });\n\n        await router.navigate(\"/redirect\");\n        expect(router.state.location.pathname).toBe(\"/target\");\n      });\n\n      it(\"allows you to return redirects after next from client middleware\", async () => {\n        router = createRouter({\n          history: createMemoryHistory(),\n          routes: [\n            {\n              path: \"/\",\n            },\n            {\n              path: \"/redirect\",\n              middleware: [\n                async (_, next) => {\n                  await next();\n                  return redirect(\"/target\");\n                },\n              ],\n            },\n            {\n              path: \"/target\",\n            },\n          ],\n        });\n\n        await router.navigate(\"/redirect\");\n        expect(router.state.location.pathname).toBe(\"/target\");\n      });\n\n      it(\"allows you to throw  redirects before next from client middleware\", async () => {\n        router = createRouter({\n          history: createMemoryHistory(),\n          routes: [\n            {\n              path: \"/\",\n            },\n            {\n              path: \"/redirect\",\n              middleware: [\n                async () => {\n                  throw redirect(\"/target\");\n                },\n              ],\n            },\n            {\n              path: \"/target\",\n            },\n          ],\n        });\n\n        await router.navigate(\"/redirect\");\n        expect(router.state.location.pathname).toBe(\"/target\");\n      });\n\n      it(\"allows you to throw redirects after next from client middleware\", async () => {\n        router = createRouter({\n          history: createMemoryHistory(),\n          routes: [\n            {\n              path: \"/\",\n            },\n            {\n              path: \"/redirect\",\n              middleware: [\n                async (_, next) => {\n                  await next();\n                  throw redirect(\"/target\");\n                },\n              ],\n            },\n            {\n              path: \"/target\",\n            },\n          ],\n        });\n\n        await router.navigate(\"/redirect\");\n        expect(router.state.location.pathname).toBe(\"/target\");\n      });\n    });\n  });\n\n  describe(\"middleware - handler.query\", () => {\n    function getOrderMiddleware(name: string): MiddlewareFunction {\n      return async ({ context }, next) => {\n        context.set(orderContext, [\n          ...context.get(orderContext),\n          `${name} middleware - before next()`,\n        ]);\n        await tick(); // Force async to ensure ordering is correct\n        let res = await next();\n        await tick(); // Force async to ensure ordering is correct\n        context.set(orderContext, [\n          ...context.get(orderContext),\n          `${name} middleware - after next()`,\n        ]);\n        return res;\n      };\n    }\n\n    it(\"propagates a Response through middleware when a `respond` API is passed\", async () => {\n      let handler = createStaticHandler([\n        {\n          path: \"/\",\n        },\n        {\n          id: \"parent\",\n          path: \"/parent\",\n          middleware: [\n            async (_, next) => {\n              let res = (await next()) as Response;\n              res.headers.set(\"parent1\", \"yes\");\n              return res;\n            },\n            async (_, next) => {\n              let res = (await next()) as Response;\n              res.headers.set(\"parent2\", \"yes\");\n              return res;\n            },\n          ],\n          loader() {\n            return \"PARENT\";\n          },\n          children: [\n            {\n              id: \"child\",\n              path: \"child\",\n              middleware: [\n                async (_, next) => {\n                  let res = (await next()) as Response;\n                  res.headers.set(\"child1\", \"yes\");\n                  return res;\n                },\n                async (_, next) => {\n                  let res = (await next()) as Response;\n                  res.headers.set(\"child2\", \"yes\");\n                  return res;\n                },\n              ],\n              loader() {\n                return \"CHILD\";\n              },\n            },\n          ],\n        },\n      ]);\n\n      let request = new Request(\"http://localhost/parent/child\");\n      let res = (await handler.query(request, {\n        generateMiddlewareResponse: async (q) =>\n          respondWithJson(await q(request)),\n      })) as Response;\n      let staticContext = (await res.json()) as StaticHandlerContext;\n\n      expect(staticContext).toMatchObject({\n        location: {\n          pathname: \"/parent/child\",\n        },\n        statusCode: 200,\n        loaderData: {\n          child: \"CHILD\",\n          parent: \"PARENT\",\n        },\n        actionData: null,\n        errors: null,\n      });\n      expect(res.headers.get(\"parent1\")).toEqual(\"yes\");\n      expect(res.headers.get(\"parent2\")).toEqual(\"yes\");\n      expect(res.headers.get(\"child1\")).toEqual(\"yes\");\n      expect(res.headers.get(\"child2\")).toEqual(\"yes\");\n    });\n\n    it(\"propagates a Response through lazy middleware when a `respond` API is passed\", async () => {\n      let handler = createStaticHandler([\n        {\n          path: \"/\",\n        },\n        {\n          id: \"parent\",\n          path: \"/parent\",\n          lazy: {\n            middleware: async () => [\n              async (_, next) => {\n                let res = (await next()) as Response;\n                res.headers.set(\"parent1\", \"yes\");\n                return res;\n              },\n              async (_, next) => {\n                let res = (await next()) as Response;\n                res.headers.set(\"parent2\", \"yes\");\n                return res;\n              },\n            ],\n          },\n          loader() {\n            return \"PARENT\";\n          },\n          children: [\n            {\n              id: \"child\",\n              path: \"child\",\n              lazy: {\n                middleware: async () => [\n                  async (_, next) => {\n                    let res = (await next()) as Response;\n                    res.headers.set(\"child1\", \"yes\");\n                    return res;\n                  },\n                  async (_, next) => {\n                    let res = (await next()) as Response;\n                    res.headers.set(\"child2\", \"yes\");\n                    return res;\n                  },\n                ],\n              },\n              loader() {\n                return \"CHILD\";\n              },\n            },\n          ],\n        },\n      ]);\n\n      let request = new Request(\"http://localhost/parent/child\");\n      let res = (await handler.query(request, {\n        generateMiddlewareResponse: async (q) =>\n          respondWithJson(await q(request)),\n      })) as Response;\n      let staticContext = (await res.json()) as StaticHandlerContext;\n\n      expect(staticContext).toMatchObject({\n        location: {\n          pathname: \"/parent/child\",\n        },\n        statusCode: 200,\n        loaderData: {\n          child: \"CHILD\",\n          parent: \"PARENT\",\n        },\n        actionData: null,\n        errors: null,\n      });\n      expect(res.headers.get(\"parent1\")).toEqual(\"yes\");\n      expect(res.headers.get(\"parent2\")).toEqual(\"yes\");\n      expect(res.headers.get(\"child1\")).toEqual(\"yes\");\n      expect(res.headers.get(\"child2\")).toEqual(\"yes\");\n    });\n\n    it(\"propagates the response even if you call next and forget to return it\", async () => {\n      let handler = createStaticHandler([\n        {\n          path: \"/\",\n        },\n        {\n          id: \"parent\",\n          path: \"/parent\",\n          middleware: [\n            async (_, next) => {\n              let res = (await next()) as Response;\n              res.headers.set(\"parent\", \"yes\");\n            },\n          ],\n          loader() {\n            return \"PARENT\";\n          },\n        },\n      ]);\n\n      let request = new Request(\"http://localhost/parent\");\n      let res = (await handler.query(request, {\n        generateMiddlewareResponse: async (q) =>\n          respondWithJson(await q(request)),\n      })) as Response;\n      let staticContext = (await res.json()) as StaticHandlerContext;\n\n      expect(staticContext).toMatchObject({\n        location: {\n          pathname: \"/parent\",\n        },\n        statusCode: 200,\n        loaderData: {\n          parent: \"PARENT\",\n        },\n        actionData: null,\n        errors: null,\n      });\n      expect(res.headers.get(\"parent\")).toEqual(\"yes\");\n    });\n\n    it(\"propagates a returned response if next isn't called\", async () => {\n      let handler = createStaticHandler([\n        {\n          path: \"/\",\n        },\n        {\n          id: \"parent\",\n          path: \"/parent\",\n          middleware: [\n            async () => {\n              return new Response(\"test\");\n            },\n          ],\n          loader() {\n            return \"PARENT\";\n          },\n        },\n      ]);\n\n      let request = new Request(\"http://localhost/parent\");\n      let res = (await handler.query(request, {\n        generateMiddlewareResponse: async (q) =>\n          respondWithJson(await q(request)),\n      })) as Response;\n      await expect(res.text()).resolves.toEqual(\"test\");\n    });\n\n    it(\"propagates a returned data() response if next isn't called\", async () => {\n      let handler = createStaticHandler([\n        {\n          path: \"/\",\n        },\n        {\n          id: \"parent\",\n          path: \"/parent\",\n          middleware: [\n            async (_, next) => {\n              let result = await next();\n              expect(isDataWithResponseInit(result)).toBe(false);\n              expect(isResponse(result)).toBe(true);\n              return result;\n            },\n            async () => {\n              return data(\"not found\", { status: 404 });\n            },\n          ],\n          loader() {\n            return \"PARENT\";\n          },\n        },\n      ]);\n\n      let request = new Request(\"http://localhost/parent\");\n      let res = (await handler.query(request, {\n        generateMiddlewareResponse: async (q) =>\n          respondWithJson(await q(request)),\n      })) as Response;\n      expect(res.status).toBe(404);\n      await expect(res.json()).resolves.toEqual(\"not found\");\n    });\n\n    it(\"propagates a thrown data() response if next isn't called\", async () => {\n      let handler = createStaticHandler([\n        {\n          path: \"/\",\n        },\n        {\n          id: \"parent\",\n          path: \"/parent\",\n          middleware: [\n            async (_, next) => {\n              let result = await next();\n              expect(isDataWithResponseInit(result)).toBe(false);\n              expect(isResponse(result)).toBe(true);\n              return result;\n            },\n            async () => {\n              throw data(\"not found\", { status: 404, statusText: \"Not Found\" });\n            },\n          ],\n          loader() {\n            return \"PARENT\";\n          },\n        },\n      ]);\n\n      let request = new Request(\"http://localhost/parent\");\n      let res = (await handler.query(request, {\n        generateMiddlewareResponse: async (q) =>\n          respondWithJson(await q(request)),\n      })) as Response;\n      expect(res.status).toBe(404);\n      let staticContext = (await res.json()) as StaticHandlerContext;\n      expect(staticContext).toMatchObject({\n        location: {\n          pathname: \"/parent\",\n        },\n        statusCode: 404,\n        loaderData: {},\n        actionData: null,\n        errors: {\n          parent: {\n            status: 404,\n            statusText: \"Not Found\",\n            data: \"not found\",\n          },\n        },\n      });\n    });\n\n    it(\"propagates a returned data() response if next is called\", async () => {\n      let handler = createStaticHandler([\n        {\n          path: \"/\",\n        },\n        {\n          id: \"parent\",\n          path: \"/parent\",\n          middleware: [\n            async (_, next) => {\n              let result = await next();\n              expect(isDataWithResponseInit(result)).toBe(false);\n              expect(isResponse(result)).toBe(true);\n              return result;\n            },\n            async (_, next) => {\n              await next();\n              return data(\"not found\", { status: 404 });\n            },\n          ],\n          loader() {\n            return \"PARENT\";\n          },\n        },\n      ]);\n\n      let request = new Request(\"http://localhost/parent\");\n      let res = (await handler.query(request, {\n        generateMiddlewareResponse: async (q) =>\n          respondWithJson(await q(request)),\n      })) as Response;\n      expect(res.status).toBe(404);\n      await expect(res.json()).resolves.toEqual(\"not found\");\n    });\n\n    it(\"propagates a thrown data() response if next is called\", async () => {\n      let handler = createStaticHandler([\n        {\n          path: \"/\",\n        },\n        {\n          id: \"parent\",\n          path: \"/parent\",\n          middleware: [\n            async (_, next) => {\n              let result = await next();\n              expect(isDataWithResponseInit(result)).toBe(false);\n              expect(isResponse(result)).toBe(true);\n              return result;\n            },\n            async (_, next) => {\n              await next();\n              throw data(\"not found\", { status: 404, statusText: \"Not Found\" });\n            },\n          ],\n          loader() {\n            return \"PARENT\";\n          },\n        },\n      ]);\n\n      let request = new Request(\"http://localhost/parent\");\n      let res = (await handler.query(request, {\n        generateMiddlewareResponse: async (q) =>\n          respondWithJson(await q(request)),\n      })) as Response;\n      expect(res.status).toBe(404);\n      let staticContext = (await res.json()) as StaticHandlerContext;\n      expect(staticContext).toMatchObject({\n        location: {\n          pathname: \"/parent\",\n        },\n        statusCode: 404,\n        loaderData: {},\n        actionData: null,\n        errors: {\n          parent: {\n            status: 404,\n            statusText: \"Not Found\",\n            data: \"not found\",\n          },\n        },\n      });\n    });\n\n    describe(\"ordering\", () => {\n      it(\"runs middleware sequentially before and after loaders\", async () => {\n        let handler = createStaticHandler([\n          {\n            path: \"/\",\n          },\n          {\n            id: \"parent\",\n            path: \"/parent\",\n            middleware: [getOrderMiddleware(\"a\"), getOrderMiddleware(\"b\")],\n            loader({ context }) {\n              context.set(orderContext, [\n                ...context.get(orderContext),\n                \"parent loader\",\n              ]);\n            },\n            children: [\n              {\n                id: \"child\",\n                path: \"child\",\n                middleware: [getOrderMiddleware(\"c\"), getOrderMiddleware(\"d\")],\n                loader({ context }) {\n                  context.set(orderContext, [\n                    ...context.get(orderContext),\n                    \"child loader\",\n                  ]);\n                },\n              },\n            ],\n          },\n        ]);\n\n        let requestContext = new RouterContextProvider();\n        let request = new Request(\"http://localhost/parent/child\");\n        await handler.query(request, {\n          requestContext,\n          generateMiddlewareResponse: async (q) =>\n            respondWithJson(await q(request)),\n        });\n\n        expect(requestContext.get(orderContext)).toEqual([\n          \"a middleware - before next()\",\n          \"b middleware - before next()\",\n          \"c middleware - before next()\",\n          \"d middleware - before next()\",\n          \"parent loader\",\n          \"child loader\",\n          \"d middleware - after next()\",\n          \"c middleware - after next()\",\n          \"b middleware - after next()\",\n          \"a middleware - after next()\",\n        ]);\n      });\n\n      it(\"runs middleware sequentially before and after actions\", async () => {\n        let handler = createStaticHandler([\n          {\n            path: \"/\",\n          },\n          {\n            id: \"parent\",\n            path: \"/parent\",\n            middleware: [getOrderMiddleware(\"a\"), getOrderMiddleware(\"b\")],\n            loader({ context }) {\n              context.set(orderContext, [\n                ...context.get(orderContext),\n                \"parent loader\",\n              ]);\n            },\n            children: [\n              {\n                id: \"child\",\n                path: \"child\",\n                middleware: [getOrderMiddleware(\"c\"), getOrderMiddleware(\"d\")],\n                action({ context }) {\n                  context.set(orderContext, [\n                    ...context.get(orderContext),\n                    \"child action\",\n                  ]);\n                },\n                loader({ context }) {\n                  context.set(orderContext, [\n                    ...context.get(orderContext),\n                    \"child loader\",\n                  ]);\n                },\n              },\n            ],\n          },\n        ]);\n\n        let requestContext = new RouterContextProvider();\n        let request = new Request(\"http://localhost/parent/child\", {\n          method: \"post\",\n          body: createFormData({}),\n        });\n        await handler.query(request, {\n          requestContext,\n          generateMiddlewareResponse: async (q) =>\n            respondWithJson(await q(request)),\n        });\n\n        expect(requestContext.get(orderContext)).toEqual([\n          // Action\n          \"a middleware - before next()\",\n          \"b middleware - before next()\",\n          \"c middleware - before next()\",\n          \"d middleware - before next()\",\n          \"child action\",\n          \"parent loader\",\n          \"child loader\",\n          \"d middleware - after next()\",\n          \"c middleware - after next()\",\n          \"b middleware - after next()\",\n          \"a middleware - after next()\",\n        ]);\n      });\n\n      it(\"does not require that you call next()\", async () => {\n        let handler = createStaticHandler([\n          {\n            path: \"/\",\n          },\n          {\n            id: \"parent\",\n            path: \"/parent\",\n            middleware: [\n              ({ context }) => {\n                context.set(parentContext, \"PARENT MIDDLEWARE\");\n              },\n            ],\n            loader() {\n              return \"PARENT\";\n            },\n            children: [\n              {\n                id: \"child\",\n                path: \"child\",\n                middleware: [\n                  ({ context }) => {\n                    context.set(childContext, \"CHILD MIDDLEWARE\");\n                  },\n                ],\n                loader() {\n                  return \"CHILD\";\n                },\n              },\n            ],\n          },\n        ]);\n\n        let requestContext = new RouterContextProvider();\n        let request = new Request(\"http://localhost/parent/child\");\n        let res = (await handler.query(request, {\n          requestContext,\n          generateMiddlewareResponse: async (q) =>\n            respondWithJson(await q(request)),\n        })) as Response;\n        let staticContext = (await res.json()) as StaticHandlerContext;\n\n        expect(requestContext.get(parentContext)).toEqual(\"PARENT MIDDLEWARE\");\n        expect(requestContext.get(childContext)).toEqual(\"CHILD MIDDLEWARE\");\n        expect(staticContext).toMatchObject({\n          loaderData: {\n            child: \"CHILD\",\n            parent: \"PARENT\",\n          },\n          errors: null,\n        });\n      });\n\n      it(\"errors if you try to call next more than once in a middleware\", async () => {\n        let handler = createStaticHandler([\n          {\n            path: \"/\",\n          },\n          {\n            id: \"parent\",\n            path: \"/parent\",\n            middleware: [\n              async (_, next) => {\n                await next();\n                await next();\n              },\n            ],\n            loader() {\n              return \"PARENT\";\n            },\n          },\n        ]);\n\n        let request = new Request(\"http://localhost/parent\");\n        let res = (await handler.query(request, {\n          generateMiddlewareResponse: async (q) =>\n            respondWithJson(await q(request)),\n        })) as Response;\n        let staticContext = (await res.json()) as StaticHandlerContext;\n        expect(staticContext).toMatchObject({\n          errors: {\n            parent: \"ERROR: You may only call `next()` once per middleware\",\n          },\n          loaderData: {},\n          statusCode: 500,\n        });\n      });\n    });\n\n    describe(\"throwing\", () => {\n      it(\"throwing from a middleware short circuits immediately (going down - loader)\", async () => {\n        let handler = createStaticHandler([\n          {\n            path: \"/\",\n          },\n          {\n            id: \"parent\",\n            path: \"/parent\",\n            middleware: [\n              async ({ context }, next) => {\n                pushOrderContext(context, \"PARENT 1 DOWN\");\n                await next();\n                pushOrderContext(context, \"PARENT 1 UP\");\n              },\n              async () => {\n                throw new Error(\"PARENT 2\");\n              },\n            ],\n            loader() {\n              return \"PARENT\";\n            },\n            children: [\n              {\n                id: \"child\",\n                path: \"child\",\n                middleware: [\n                  async ({ context }, next) => {\n                    pushOrderContext(context, \"CHILD DOWN\");\n                    await next();\n                    pushOrderContext(context, \"CHILD UP\");\n                  },\n                ],\n                loader() {\n                  return \"CHILD\";\n                },\n              },\n            ],\n          },\n        ]);\n\n        let requestContext = new RouterContextProvider();\n        let request = new Request(\"http://localhost/parent/child\");\n        let res = (await handler.query(request, {\n          requestContext,\n          generateMiddlewareResponse: async (q) =>\n            respondWithJson(await q(request)),\n        })) as Response;\n        let staticContext = (await res.json()) as StaticHandlerContext;\n\n        expect(requestContext.get(orderContext)).toEqual([\n          \"PARENT 1 DOWN\",\n          \"PARENT 1 UP\",\n        ]);\n        expect(staticContext.loaderData).toEqual({});\n        expect(staticContext.errors).toEqual({\n          parent: \"ERROR: PARENT 2\",\n        });\n      });\n\n      it(\"throwing from a middleware short circuits immediately (going up - loader)\", async () => {\n        let handler = createStaticHandler([\n          {\n            path: \"/\",\n          },\n          {\n            id: \"parent\",\n            path: \"/parent\",\n            middleware: [\n              async ({ context }, next) => {\n                pushOrderContext(context, \"PARENT DOWN\");\n                await next();\n                pushOrderContext(context, \"PARENT UP\");\n              },\n            ],\n            loader() {\n              return \"PARENT\";\n            },\n            children: [\n              {\n                id: \"child\",\n                path: \"child\",\n                middleware: [\n                  async ({ context }, next) => {\n                    pushOrderContext(context, \"CHILD DOWN\");\n                    await next();\n                    throw new Error(\"CHILD UP\");\n                  },\n                ],\n                loader() {\n                  return \"CHILD\";\n                },\n              },\n            ],\n          },\n        ]);\n\n        let requestContext = new RouterContextProvider();\n        let request = new Request(\"http://localhost/parent/child\");\n        let res = (await handler.query(request, {\n          requestContext,\n          generateMiddlewareResponse: async (q) =>\n            respondWithJson(await q(request)),\n        })) as Response;\n        let staticContext = (await res.json()) as StaticHandlerContext;\n\n        expect(requestContext.get(orderContext)).toEqual([\n          \"PARENT DOWN\",\n          \"CHILD DOWN\",\n          \"PARENT UP\",\n        ]);\n        expect(staticContext.loaderData).toEqual({\n          parent: \"PARENT\",\n        });\n        expect(staticContext.errors).toEqual({\n          parent: \"ERROR: CHILD UP\",\n        });\n      });\n\n      it(\"throwing from a middleware short circuits immediately (going down - action w/boundary)\", async () => {\n        let handler = createStaticHandler([\n          {\n            path: \"/\",\n          },\n          {\n            id: \"parent\",\n            path: \"/parent\",\n            middleware: [\n              async ({ context }, next) => {\n                pushOrderContext(context, \"parent start\");\n                let res = await next();\n                pushOrderContext(context, \"parent end\");\n                return res;\n              },\n            ],\n            loader() {\n              return \"PARENT\";\n            },\n            children: [\n              {\n                id: \"child\",\n                path: \"child\",\n                hasErrorBoundary: true,\n                middleware: [\n                  async ({ context }) => {\n                    pushOrderContext(context, \"child 1 start - throwing\");\n                    throw new Error(\"child 1 error\");\n                  },\n                  async ({ context }, next) => {\n                    pushOrderContext(context, \"child 2 start\");\n                    let res = await next();\n                    pushOrderContext(context, \"child 2 end\");\n                    return res;\n                  },\n                ],\n                action() {\n                  return \"ACTION\";\n                },\n                loader() {\n                  return \"CHILD\";\n                },\n              },\n            ],\n          },\n        ]);\n\n        let requestContext = new RouterContextProvider();\n        let request = new Request(\"http://localhost/parent/child\", {\n          method: \"post\",\n          body: createFormData({}),\n        });\n        let res = (await handler.query(request, {\n          requestContext,\n          generateMiddlewareResponse: async (q) =>\n            respondWithJson(await q(request)),\n        })) as Response;\n        let staticContext = (await res.json()) as StaticHandlerContext;\n\n        expect(requestContext.get(orderContext)).toEqual([\n          \"parent start\",\n          \"child 1 start - throwing\",\n          \"parent end\",\n        ]);\n        expect(staticContext.loaderData).toEqual({});\n        expect(staticContext.errors).toEqual({\n          // bubbles to parent boundary because we never got to run loaders\n          parent: \"ERROR: child 1 error\",\n        });\n      });\n\n      it(\"throwing from a middleware short circuits immediately (going up - action w/boundary)\", async () => {\n        let handler = createStaticHandler([\n          {\n            path: \"/\",\n          },\n          {\n            id: \"parent\",\n            path: \"/parent\",\n            middleware: [\n              async ({ context }, next) => {\n                pushOrderContext(context, \"parent start\");\n                let res = await next();\n                pushOrderContext(context, \"parent end\");\n                return res;\n              },\n            ],\n            loader() {\n              return \"PARENT\";\n            },\n            children: [\n              {\n                id: \"child\",\n                path: \"child\",\n                hasErrorBoundary: true,\n                middleware: [\n                  async ({ context }, next) => {\n                    pushOrderContext(context, \"child 1 start\");\n                    await next();\n                    pushOrderContext(context, \"child 1 end\");\n                  },\n                  async ({ context }, next) => {\n                    pushOrderContext(context, \"child 2 start\");\n                    await next();\n                    pushOrderContext(context, \"child 2 end - throwing\");\n                    throw new Error(\"child 2 error\");\n                  },\n                ],\n                action() {\n                  return \"ACTION\";\n                },\n                loader() {\n                  return \"CHILD\";\n                },\n              },\n            ],\n          },\n        ]);\n\n        let requestContext = new RouterContextProvider();\n        let request = new Request(\"http://localhost/parent/child\", {\n          method: \"post\",\n          body: createFormData({}),\n        });\n        let res = (await handler.query(request, {\n          requestContext,\n          generateMiddlewareResponse: async (q) =>\n            respondWithJson(await q(request)),\n        })) as Response;\n        let staticContext = (await res.json()) as StaticHandlerContext;\n\n        expect(requestContext.get(orderContext)).toEqual([\n          \"parent start\",\n          \"child 1 start\",\n          \"child 2 start\",\n          \"child 2 end - throwing\",\n          \"child 1 end\",\n          \"parent end\",\n        ]);\n        expect(staticContext.loaderData).toEqual({\n          parent: \"PARENT\",\n        });\n        expect(staticContext.errors).toEqual({\n          child: \"ERROR: child 2 error\",\n        });\n      });\n\n      it(\"throwing from a middleware short circuits immediately (going down - action w/o boundary)\", async () => {\n        let handler = createStaticHandler([\n          {\n            path: \"/\",\n          },\n          {\n            id: \"parent\",\n            path: \"/parent\",\n            hasErrorBoundary: true,\n            middleware: [\n              async ({ context }, next) => {\n                pushOrderContext(context, \"parent start\");\n                let res = await next();\n                pushOrderContext(context, \"parent end\");\n                return res;\n              },\n            ],\n            loader() {\n              return \"PARENT\";\n            },\n            children: [\n              {\n                id: \"child\",\n                path: \"child\",\n                middleware: [\n                  async ({ context }) => {\n                    pushOrderContext(context, \"child 1 start - throwing\");\n                    throw new Error(\"child 1 error\");\n                  },\n                  async ({ context }, next) => {\n                    pushOrderContext(context, \"child 2 start\");\n                    let res = await next();\n                    pushOrderContext(context, \"child 2 end\");\n                    return res;\n                  },\n                ],\n                action() {\n                  return \"ACTION\";\n                },\n                loader() {\n                  return \"CHILD\";\n                },\n              },\n            ],\n          },\n        ]);\n\n        let requestContext = new RouterContextProvider();\n        let request = new Request(\"http://localhost/parent/child\", {\n          method: \"post\",\n          body: createFormData({}),\n        });\n        let res = (await handler.query(request, {\n          requestContext,\n          generateMiddlewareResponse: async (q) =>\n            respondWithJson(await q(request)),\n        })) as Response;\n        let staticContext = (await res.json()) as StaticHandlerContext;\n\n        expect(requestContext.get(orderContext)).toEqual([\n          \"parent start\",\n          \"child 1 start - throwing\",\n          \"parent end\",\n        ]);\n        expect(staticContext.loaderData).toEqual({});\n        expect(staticContext.errors).toEqual({\n          parent: \"ERROR: child 1 error\",\n        });\n      });\n\n      it(\"throwing from a middleware short circuits immediately (going up - action w/o boundary)\", async () => {\n        let handler = createStaticHandler([\n          {\n            path: \"/\",\n          },\n          {\n            id: \"parent\",\n            path: \"/parent\",\n            hasErrorBoundary: true,\n            middleware: [\n              async ({ context }, next) => {\n                pushOrderContext(context, \"parent start\");\n                let res = await next();\n                pushOrderContext(context, \"parent end\");\n                return res;\n              },\n            ],\n            loader() {\n              return \"PARENT\";\n            },\n            children: [\n              {\n                id: \"child\",\n                path: \"child\",\n                middleware: [\n                  async ({ context }, next) => {\n                    pushOrderContext(context, \"child 1 start\");\n                    let res = await next();\n                    pushOrderContext(context, \"child 1 end\");\n                    return res;\n                  },\n                  async ({ context }, next) => {\n                    pushOrderContext(context, \"child 2 start\");\n                    await next();\n                    pushOrderContext(context, \"child 2 end - throwing\");\n                    throw new Error(\"child 2 error\");\n                  },\n                ],\n                action() {\n                  return \"ACTION\";\n                },\n                loader() {\n                  return \"CHILD\";\n                },\n              },\n            ],\n          },\n        ]);\n\n        let requestContext = new RouterContextProvider();\n        let request = new Request(\"http://localhost/parent/child\", {\n          method: \"post\",\n          body: createFormData({}),\n        });\n        let res = (await handler.query(request, {\n          requestContext,\n          generateMiddlewareResponse: async (q) =>\n            respondWithJson(await q(request)),\n        })) as Response;\n        let staticContext = (await res.json()) as StaticHandlerContext;\n\n        expect(requestContext.get(orderContext)).toEqual([\n          \"parent start\",\n          \"child 1 start\",\n          \"child 2 start\",\n          \"child 2 end - throwing\",\n          \"child 1 end\",\n          \"parent end\",\n        ]);\n        expect(staticContext.loaderData).toEqual({\n          parent: \"PARENT\",\n        });\n        expect(staticContext.errors).toEqual({\n          parent: \"ERROR: child 2 error\",\n        });\n      });\n\n      it(\"handles thrown Responses at the ErrorBoundary\", async () => {\n        let handler = createStaticHandler([\n          {\n            path: \"/\",\n            middleware: [\n              async () => {\n                throw new Response(\"Error\", { status: 401 });\n              },\n            ],\n            loader() {\n              return \"INDEX\";\n            },\n          },\n        ]);\n\n        let request = new Request(\"http://localhost/\");\n        let res = (await handler.query(request, {\n          generateMiddlewareResponse: async (q) =>\n            respondWithJson(await q(request)),\n        })) as Response;\n\n        let staticContext = (await res.json()) as StaticHandlerContext;\n        expect(staticContext.errors).toEqual({\n          \"0\": new ErrorResponseImpl(401, undefined, \"Error\"),\n        });\n        expect(staticContext.statusCode).toBe(401);\n      });\n\n      it(\"allows thrown redirects before next()\", async () => {\n        let handler = createStaticHandler([\n          {\n            path: \"/\",\n          },\n          {\n            path: \"/parent\",\n            middleware: [\n              async () => {\n                throw redirect(\"/target\");\n              },\n            ],\n            loader() {\n              return \"PARENT\";\n            },\n          },\n          {\n            path: \"/target\",\n          },\n        ]);\n\n        let request = new Request(\"http://localhost/parent\");\n        let response = (await handler.query(request, {\n          generateMiddlewareResponse: async (q) =>\n            respondWithJson(await q(request)),\n        })) as Response;\n\n        expect(response.status).toBe(302);\n        expect(response.headers.get(\"Location\")).toBe(\"/target\");\n      });\n\n      it(\"allows thrown redirects after next()\", async () => {\n        let handler = createStaticHandler([\n          {\n            path: \"/\",\n          },\n          {\n            path: \"/parent\",\n            middleware: [\n              async (_, next) => {\n                await next();\n                throw redirect(\"/target\");\n              },\n            ],\n            loader() {\n              return \"PARENT\";\n            },\n          },\n          {\n            path: \"/target\",\n          },\n        ]);\n\n        let request = new Request(\"http://localhost/parent\");\n        let response = (await handler.query(request, {\n          generateMiddlewareResponse: async (q) =>\n            respondWithJson(await q(request)),\n        })) as Response;\n\n        expect(response.status).toBe(302);\n        expect(response.headers.get(\"Location\")).toBe(\"/target\");\n      });\n    });\n  });\n\n  describe(\"middleware - handler.queryRoute\", () => {\n    function getOrderMiddleware(name: string): MiddlewareFunction {\n      return async ({ context }, next) => {\n        context.set(orderContext, [\n          ...context.get(orderContext),\n          `${name} middleware - before next()`,\n        ]);\n        await tick(); // Force async to ensure ordering is correct\n        let res = await next();\n        await tick(); // Force async to ensure ordering is correct\n        context.set(orderContext, [\n          ...context.get(orderContext),\n          `${name} middleware - after next()`,\n        ]);\n        return res;\n      };\n    }\n\n    it(\"propagates a Response through middleware when a `respond` API is passed\", async () => {\n      let handler = createStaticHandler([\n        {\n          path: \"/\",\n        },\n        {\n          id: \"parent\",\n          path: \"/parent\",\n          middleware: [\n            async (_, next) => {\n              let res = (await next()) as Response;\n              res.headers.set(\"parent1\", \"yes\");\n              return res;\n            },\n            async (_, next) => {\n              let res = (await next()) as Response;\n              res.headers.set(\"parent2\", \"yes\");\n              return res;\n            },\n          ],\n          loader() {\n            return new Response(\"PARENT\");\n          },\n          children: [\n            {\n              id: \"child\",\n              path: \"child\",\n              middleware: [\n                async (_, next) => {\n                  let res = (await next()) as Response;\n                  res.headers.set(\"child1\", \"yes\");\n                  return res;\n                },\n                async (_, next) => {\n                  let res = (await next()) as Response;\n                  res.headers.set(\"child2\", \"yes\");\n                  return res;\n                },\n              ],\n              loader() {\n                return new Response(\"CHILD\");\n              },\n            },\n          ],\n        },\n      ]);\n\n      let request = new Request(\"http://localhost/parent/child\");\n      let res = (await handler.queryRoute(request, {\n        generateMiddlewareResponse: (q) => q(request),\n      })) as Response;\n\n      expect(await res.text()).toBe(\"CHILD\");\n      expect(res.headers.get(\"parent1\")).toEqual(\"yes\");\n      expect(res.headers.get(\"parent2\")).toEqual(\"yes\");\n      expect(res.headers.get(\"child1\")).toEqual(\"yes\");\n      expect(res.headers.get(\"child2\")).toEqual(\"yes\");\n    });\n\n    it(\"propagates a Response through lazy middleware when a `respond` API is passed\", async () => {\n      let handler = createStaticHandler([\n        {\n          path: \"/\",\n        },\n        {\n          id: \"parent\",\n          path: \"/parent\",\n          lazy: {\n            middleware: async () => [\n              async (_, next) => {\n                let res = (await next()) as Response;\n                res.headers.set(\"parent1\", \"yes\");\n                return res;\n              },\n              async (_, next) => {\n                let res = (await next()) as Response;\n                res.headers.set(\"parent2\", \"yes\");\n                return res;\n              },\n            ],\n          },\n          loader() {\n            return new Response(\"PARENT\");\n          },\n          children: [\n            {\n              id: \"child\",\n              path: \"child\",\n              lazy: {\n                middleware: async () => [\n                  async (_, next) => {\n                    let res = (await next()) as Response;\n                    res.headers.set(\"child1\", \"yes\");\n                    return res;\n                  },\n                  async (_, next) => {\n                    let res = (await next()) as Response;\n                    res.headers.set(\"child2\", \"yes\");\n                    return res;\n                  },\n                ],\n              },\n              loader() {\n                return new Response(\"CHILD\");\n              },\n            },\n          ],\n        },\n      ]);\n\n      let request = new Request(\"http://localhost/parent/child\");\n      let res = (await handler.queryRoute(request, {\n        generateMiddlewareResponse: (q) => q(request),\n      })) as Response;\n\n      expect(await res.text()).toBe(\"CHILD\");\n      expect(res.headers.get(\"parent1\")).toEqual(\"yes\");\n      expect(res.headers.get(\"parent2\")).toEqual(\"yes\");\n      expect(res.headers.get(\"child1\")).toEqual(\"yes\");\n      expect(res.headers.get(\"child2\")).toEqual(\"yes\");\n    });\n\n    it(\"propagates the response even if you call next and forget to return it\", async () => {\n      let handler = createStaticHandler([\n        {\n          path: \"/\",\n        },\n        {\n          id: \"parent\",\n          path: \"/parent\",\n          middleware: [\n            async (_, next) => {\n              let res = (await next()) as Response;\n              res.headers.set(\"parent\", \"yes\");\n            },\n          ],\n          loader() {\n            return new Response(\"PARENT\");\n          },\n        },\n      ]);\n\n      let request = new Request(\"http://localhost/parent\");\n      let res = (await handler.queryRoute(request, {\n        generateMiddlewareResponse: (q) => q(request),\n      })) as Response;\n\n      expect(await res.text()).toBe(\"PARENT\");\n      expect(res.headers.get(\"parent\")).toEqual(\"yes\");\n    });\n\n    it(\"propagates a returned response if next isn't called\", async () => {\n      let handler = createStaticHandler([\n        {\n          path: \"/\",\n        },\n        {\n          id: \"parent\",\n          path: \"/parent\",\n          middleware: [\n            async () => {\n              return new Response(\"test\");\n            },\n          ],\n          loader() {\n            return \"PARENT\";\n          },\n        },\n      ]);\n\n      let request = new Request(\"http://localhost/parent\");\n      let res = (await handler.queryRoute(request, {\n        generateMiddlewareResponse: (q) => q(request),\n      })) as Response;\n      await expect(res.text()).resolves.toEqual(\"test\");\n    });\n\n    it(\"propagates a returned data() response if next isn't called\", async () => {\n      let handler = createStaticHandler([\n        {\n          path: \"/\",\n        },\n        {\n          id: \"parent\",\n          path: \"/parent\",\n          middleware: [\n            async (_, next) => {\n              let result = await next();\n              expect(isDataWithResponseInit(result)).toBe(false);\n              expect(isResponse(result)).toBe(true);\n              return result;\n            },\n            async () => {\n              return data(\"not found\", { status: 404 });\n            },\n          ],\n          loader() {\n            return \"PARENT\";\n          },\n        },\n      ]);\n\n      let request = new Request(\"http://localhost/parent\");\n      let res = (await handler.queryRoute(request, {\n        generateMiddlewareResponse: (q) => q(request),\n      })) as Response;\n      expect(res.status).toBe(404);\n      await expect(res.json()).resolves.toEqual(\"not found\");\n    });\n\n    it(\"propagates a thrown data() response if next isn't called\", async () => {\n      let handler = createStaticHandler([\n        {\n          path: \"/\",\n        },\n        {\n          id: \"parent\",\n          path: \"/parent\",\n          middleware: [\n            async (_, next) => {\n              let result = await next();\n              expect(isDataWithResponseInit(result)).toBe(false);\n              expect(isResponse(result)).toBe(true);\n              return result;\n            },\n            async () => {\n              throw data(\"not found\", { status: 404 });\n            },\n          ],\n          loader() {\n            return \"PARENT\";\n          },\n        },\n      ]);\n\n      let request = new Request(\"http://localhost/parent\");\n      let res = (await handler.queryRoute(request, {\n        generateMiddlewareResponse: async (q) => q(request),\n      })) as Response;\n      expect(res.status).toBe(404);\n      await expect(res.json()).resolves.toEqual(\"not found\");\n    });\n\n    it(\"propagates a returned data() response if next is called\", async () => {\n      let handler = createStaticHandler([\n        {\n          path: \"/\",\n        },\n        {\n          id: \"parent\",\n          path: \"/parent\",\n          middleware: [\n            async (_, next) => {\n              let result = await next();\n              expect(isDataWithResponseInit(result)).toBe(false);\n              expect(isResponse(result)).toBe(true);\n              return result;\n            },\n            async (_, next) => {\n              await next();\n              return data(\"not found\", { status: 404 });\n            },\n          ],\n          loader() {\n            return \"PARENT\";\n          },\n        },\n      ]);\n\n      let request = new Request(\"http://localhost/parent\");\n      let res = (await handler.queryRoute(request, {\n        generateMiddlewareResponse: (q) => q(request),\n      })) as Response;\n      expect(res.status).toBe(404);\n      await expect(res.json()).resolves.toEqual(\"not found\");\n    });\n\n    it(\"propagates a thrown data() response if next is called\", async () => {\n      let handler = createStaticHandler([\n        {\n          path: \"/\",\n        },\n        {\n          id: \"parent\",\n          path: \"/parent\",\n          middleware: [\n            async (_, next) => {\n              let result = await next();\n              expect(isDataWithResponseInit(result)).toBe(false);\n              expect(isResponse(result)).toBe(true);\n              return result;\n            },\n            async (_, next) => {\n              await next();\n              throw data(\"not found\", { status: 404 });\n            },\n          ],\n          loader() {\n            return \"PARENT\";\n          },\n        },\n      ]);\n\n      let request = new Request(\"http://localhost/parent\");\n      let res = (await handler.queryRoute(request, {\n        generateMiddlewareResponse: async (q) => q(request),\n      })) as Response;\n      expect(res.status).toBe(404);\n      await expect(res.json()).resolves.toEqual(\"not found\");\n    });\n\n    describe(\"ordering\", () => {\n      it(\"runs middleware sequentially before and after loaders\", async () => {\n        let handler = createStaticHandler([\n          {\n            path: \"/\",\n          },\n          {\n            id: \"parent\",\n            path: \"/parent\",\n            middleware: [getOrderMiddleware(\"a\"), getOrderMiddleware(\"b\")],\n            loader({ context }) {\n              context.set(orderContext, [\n                ...context.get(orderContext),\n                \"parent loader\",\n              ]);\n            },\n            children: [\n              {\n                id: \"child\",\n                path: \"child\",\n                middleware: [getOrderMiddleware(\"c\"), getOrderMiddleware(\"d\")],\n                loader({ context }) {\n                  context.set(orderContext, [\n                    ...context.get(orderContext),\n                    \"child loader\",\n                  ]);\n                  return new Response(\"CHILD\");\n                },\n              },\n            ],\n          },\n        ]);\n\n        let requestContext = new RouterContextProvider();\n        let request = new Request(\"http://localhost/parent/child\");\n        await handler.queryRoute(request, {\n          requestContext,\n          generateMiddlewareResponse: (q) => q(request),\n        });\n\n        expect(requestContext.get(orderContext)).toEqual([\n          \"a middleware - before next()\",\n          \"b middleware - before next()\",\n          \"c middleware - before next()\",\n          \"d middleware - before next()\",\n          \"child loader\",\n          \"d middleware - after next()\",\n          \"c middleware - after next()\",\n          \"b middleware - after next()\",\n          \"a middleware - after next()\",\n        ]);\n      });\n\n      it(\"runs middleware sequentially before and after actions\", async () => {\n        let handler = createStaticHandler([\n          {\n            path: \"/\",\n          },\n          {\n            id: \"parent\",\n            path: \"/parent\",\n            middleware: [getOrderMiddleware(\"a\"), getOrderMiddleware(\"b\")],\n            loader({ context }) {\n              context.set(orderContext, [\n                ...context.get(orderContext),\n                \"parent loader\",\n              ]);\n            },\n            children: [\n              {\n                id: \"child\",\n                path: \"child\",\n                middleware: [getOrderMiddleware(\"c\"), getOrderMiddleware(\"d\")],\n                action({ context }) {\n                  context.set(orderContext, [\n                    ...context.get(orderContext),\n                    \"child action\",\n                  ]);\n                  return new Response(\"CHILD\");\n                },\n                loader({ context }) {\n                  context.set(orderContext, [\n                    ...context.get(orderContext),\n                    \"child loader\",\n                  ]);\n                  return new Response(\"CHILD\");\n                },\n              },\n            ],\n          },\n        ]);\n\n        let requestContext = new RouterContextProvider();\n        let request = new Request(\"http://localhost/parent/child\", {\n          method: \"post\",\n          body: createFormData({}),\n        });\n        await handler.queryRoute(request, {\n          requestContext,\n          generateMiddlewareResponse: (q) => q(request),\n        });\n\n        expect(requestContext.get(orderContext)).toEqual([\n          \"a middleware - before next()\",\n          \"b middleware - before next()\",\n          \"c middleware - before next()\",\n          \"d middleware - before next()\",\n          \"child action\",\n          \"d middleware - after next()\",\n          \"c middleware - after next()\",\n          \"b middleware - after next()\",\n          \"a middleware - after next()\",\n        ]);\n      });\n\n      it(\"does not require that you call next()\", async () => {\n        let handler = createStaticHandler([\n          {\n            path: \"/\",\n          },\n          {\n            id: \"parent\",\n            path: \"/parent\",\n            middleware: [\n              ({ context }) => {\n                context.set(parentContext, \"PARENT MIDDLEWARE\");\n              },\n            ],\n            loader() {\n              return \"PARENT\";\n            },\n            children: [\n              {\n                id: \"child\",\n                path: \"child\",\n                middleware: [\n                  ({ context }) => {\n                    context.set(childContext, \"CHILD MIDDLEWARE\");\n                  },\n                ],\n                loader() {\n                  return new Response(\"CHILD\");\n                },\n              },\n            ],\n          },\n        ]);\n\n        let requestContext = new RouterContextProvider();\n        let request = new Request(\"http://localhost/parent/child\");\n        let response = (await handler.queryRoute(request, {\n          requestContext,\n          generateMiddlewareResponse: (q) => q(request),\n        })) as Response;\n\n        expect(requestContext.get(parentContext)).toEqual(\"PARENT MIDDLEWARE\");\n        expect(requestContext.get(childContext)).toBe(\"CHILD MIDDLEWARE\");\n        expect(await response.text()).toEqual(\"CHILD\");\n      });\n\n      it(\"errors if you try to call next more than once in a middleware\", async () => {\n        let handler = createStaticHandler([\n          {\n            path: \"/\",\n          },\n          {\n            id: \"parent\",\n            path: \"/parent\",\n            middleware: [\n              async (_, next) => {\n                await next();\n                await next();\n              },\n            ],\n            loader() {\n              return \"PARENT\";\n            },\n          },\n        ]);\n\n        let request = new Request(\"http://localhost/parent/\");\n        await expect(\n          handler.queryRoute(request, {\n            generateMiddlewareResponse: (q) => q(request),\n          }),\n        ).rejects.toThrow(\"You may only call `next()` once per middleware\");\n      });\n    });\n\n    describe(\"throwing\", () => {\n      it(\"throwing from a middleware short circuits immediately (going down - loader)\", async () => {\n        let handler = createStaticHandler([\n          {\n            path: \"/\",\n          },\n          {\n            id: \"parent\",\n            path: \"/parent\",\n            middleware: [\n              async ({ context }) => {\n                context.set(parentContext, \"PARENT 1\");\n              },\n              async () => {\n                throw new Error(\"PARENT 2\");\n              },\n            ],\n            loader() {\n              return \"PARENT\";\n            },\n            children: [\n              {\n                id: \"child\",\n                path: \"child\",\n                middleware: [\n                  async ({ context }, next) => {\n                    context.set(childContext, \"CHILD 1\");\n                    return next();\n                  },\n                ],\n                loader() {\n                  return \"CHILD\";\n                },\n              },\n            ],\n          },\n        ]);\n\n        let requestContext = new RouterContextProvider();\n        let request = new Request(\"http://localhost/parent/child\");\n        await expect(\n          handler.queryRoute(request, {\n            requestContext,\n            generateMiddlewareResponse: (q) => q(request),\n          }),\n        ).rejects.toThrow(\"PARENT 2\");\n\n        expect(requestContext.get(parentContext)).toEqual(\"PARENT 1\");\n        expect(requestContext.get(childContext)).toEqual(\"empty\");\n      });\n\n      it(\"throwing from a middleware short circuits immediately (going up - loader)\", async () => {\n        let handler = createStaticHandler([\n          {\n            path: \"/\",\n          },\n          {\n            id: \"parent\",\n            path: \"/parent\",\n            middleware: [\n              async ({ context }, next) => {\n                context.set(parentContext, \"PARENT DOWN\");\n                let res = await next();\n                context.set(parentContext, \"PARENT UP\");\n                return res;\n              },\n            ],\n            loader() {\n              return \"PARENT\";\n            },\n            children: [\n              {\n                id: \"child\",\n                path: \"child\",\n                middleware: [\n                  async ({ context }, next) => {\n                    context.set(childContext, \"CHILD DOWN\");\n                    await next();\n                    throw new Error(\"CHILD UP\");\n                  },\n                ],\n                loader() {\n                  return \"CHILD\";\n                },\n              },\n            ],\n          },\n        ]);\n\n        let requestContext = new RouterContextProvider();\n        let request = new Request(\"http://localhost/parent/child\");\n        await expect(\n          handler.queryRoute(request, {\n            requestContext,\n            generateMiddlewareResponse: (q) => q(request),\n          }),\n        ).rejects.toThrow(\"CHILD UP\");\n\n        expect(requestContext.get(parentContext)).toEqual(\"PARENT DOWN\");\n        expect(requestContext.get(childContext)).toEqual(\"CHILD DOWN\");\n      });\n\n      it(\"throwing from a middleware short circuits immediately (going down - action w/boundary)\", async () => {\n        let handler = createStaticHandler([\n          {\n            path: \"/\",\n          },\n          {\n            id: \"parent\",\n            path: \"/parent\",\n            middleware: [\n              async ({ context }, next) => {\n                context.set(orderContext, [\n                  ...context.get(orderContext),\n                  \"parent start\",\n                ]);\n                let res = await next();\n                context.set(orderContext, [\n                  ...context.get(orderContext),\n                  \"parent end\",\n                ]);\n                return res;\n              },\n            ],\n            loader() {\n              return \"PARENT\";\n            },\n            children: [\n              {\n                id: \"child\",\n                path: \"child\",\n                hasErrorBoundary: true,\n                middleware: [\n                  async ({ context }) => {\n                    context.set(orderContext, [\n                      ...context.get(orderContext),\n                      \"child 1 start - throwing\",\n                    ]);\n                    throw new Error(\"child 1 error\");\n                  },\n                  async ({ context }, next) => {\n                    context.set(orderContext, [\n                      ...context.get(orderContext),\n                      \"child 2 start\",\n                    ]);\n                    await next();\n                    context.set(orderContext, [\n                      ...context.get(orderContext),\n                      \"child 2 end\",\n                    ]);\n                  },\n                ],\n                action() {\n                  return \"ACTION\";\n                },\n                loader() {\n                  return \"CHILD\";\n                },\n              },\n            ],\n          },\n        ]);\n\n        let requestContext = new RouterContextProvider();\n        let request = new Request(\"http://localhost/parent/child\", {\n          method: \"post\",\n          body: createFormData({}),\n        });\n        await expect(\n          handler.queryRoute(request, {\n            requestContext,\n            generateMiddlewareResponse: (q) => q(request),\n          }),\n        ).rejects.toThrow(\"child 1 error\");\n\n        expect(requestContext.get(orderContext)).toEqual([\n          \"parent start\",\n          \"child 1 start - throwing\",\n        ]);\n      });\n\n      it(\"throwing from a middleware short circuits immediately (going up - action w/boundary)\", async () => {\n        let handler = createStaticHandler([\n          {\n            path: \"/\",\n          },\n          {\n            id: \"parent\",\n            path: \"/parent\",\n            middleware: [\n              async ({ context }, next) => {\n                context.set(orderContext, [\n                  ...context.get(orderContext),\n                  \"parent start\",\n                ]);\n                let res = await next();\n                context.set(orderContext, [\n                  ...context.get(orderContext),\n                  \"parent end\",\n                ]);\n                return res;\n              },\n            ],\n            loader() {\n              return \"PARENT\";\n            },\n            children: [\n              {\n                id: \"child\",\n                path: \"child\",\n                hasErrorBoundary: true,\n                middleware: [\n                  async ({ context }, next) => {\n                    context.set(orderContext, [\n                      ...context.get(orderContext),\n                      \"child 1 start\",\n                    ]);\n                    let res = await next();\n                    context.set(orderContext, [\n                      ...context.get(orderContext),\n                      \"child 1 end\",\n                    ]);\n                    return res;\n                  },\n                  async ({ context }, next) => {\n                    context.set(orderContext, [\n                      ...context.get(orderContext),\n                      \"child 2 start\",\n                    ]);\n                    await next();\n                    context.set(orderContext, [\n                      ...context.get(orderContext),\n                      \"child 2 end - throwing\",\n                    ]);\n                    throw new Error(\"child 2 error\");\n                  },\n                ],\n                action() {\n                  return \"ACTION\";\n                },\n                loader() {\n                  return \"CHILD\";\n                },\n              },\n            ],\n          },\n        ]);\n\n        let requestContext = new RouterContextProvider();\n        let request = new Request(\"http://localhost/parent/child\", {\n          method: \"post\",\n          body: createFormData({}),\n        });\n        await expect(\n          handler.queryRoute(request, {\n            requestContext,\n            generateMiddlewareResponse: (q) => q(request),\n          }),\n        ).rejects.toThrow(\"child 2 error\");\n\n        expect(requestContext.get(orderContext)).toEqual([\n          \"parent start\",\n          \"child 1 start\",\n          \"child 2 start\",\n          \"child 2 end - throwing\",\n        ]);\n      });\n\n      it(\"throwing from a middleware short circuits immediately (going down - action w/o boundary)\", async () => {\n        let handler = createStaticHandler([\n          {\n            path: \"/\",\n          },\n          {\n            id: \"parent\",\n            path: \"/parent\",\n            hasErrorBoundary: true,\n            middleware: [\n              async ({ context }, next) => {\n                context.set(orderContext, [\n                  ...context.get(orderContext),\n                  \"parent action start\",\n                ]);\n                let res = await next();\n                context.set(orderContext, [\n                  ...context.get(orderContext),\n                  \"parent action end\",\n                ]);\n                return res;\n              },\n            ],\n            loader() {\n              return \"PARENT\";\n            },\n            children: [\n              {\n                id: \"child\",\n                path: \"child\",\n                middleware: [\n                  async ({ context }) => {\n                    context.set(orderContext, [\n                      ...context.get(orderContext),\n                      \"child 1 start - throwing\",\n                    ]);\n                    throw new Error(\"child 1 action error\");\n                  },\n                  async ({ context }, next) => {\n                    context.set(orderContext, [\n                      ...context.get(orderContext),\n                      \"child 2 start\",\n                    ]);\n                    let res = await next();\n                    context.set(orderContext, [\n                      ...context.get(orderContext),\n                      \"child 2 end\",\n                    ]);\n                    return res;\n                  },\n                ],\n                action() {\n                  return \"ACTION\";\n                },\n                loader() {\n                  return \"CHILD\";\n                },\n              },\n            ],\n          },\n        ]);\n\n        let requestContext = new RouterContextProvider();\n        let request = new Request(\"http://localhost/parent/child\", {\n          method: \"post\",\n          body: createFormData({}),\n        });\n        await expect(\n          handler.queryRoute(request, {\n            requestContext,\n            generateMiddlewareResponse: (q) => q(request),\n          }),\n        ).rejects.toThrow(\"child 1 action error\");\n\n        expect(requestContext.get(orderContext)).toEqual([\n          \"parent action start\",\n          \"child 1 start - throwing\",\n        ]);\n      });\n\n      it(\"throwing from a middleware short circuits immediately (going up - action w/o boundary)\", async () => {\n        let handler = createStaticHandler([\n          {\n            path: \"/\",\n          },\n          {\n            id: \"parent\",\n            path: \"/parent\",\n            hasErrorBoundary: true,\n            middleware: [\n              async ({ context }, next) => {\n                context.set(orderContext, [\n                  ...context.get(orderContext),\n                  \"parent start\",\n                ]);\n                let res = await next();\n                context.set(orderContext, [\n                  ...context.get(orderContext),\n                  \"parent end\",\n                ]);\n                return res;\n              },\n            ],\n            loader() {\n              return \"PARENT\";\n            },\n            children: [\n              {\n                id: \"child\",\n                path: \"child\",\n                middleware: [\n                  async ({ context }, next) => {\n                    context.set(orderContext, [\n                      ...context.get(orderContext),\n                      \"child 1 start\",\n                    ]);\n                    let res = await next();\n                    context.set(orderContext, [\n                      ...context.get(orderContext),\n                      \"child 1 end\",\n                    ]);\n                    return res;\n                  },\n                  async ({ context }, next) => {\n                    context.set(orderContext, [\n                      ...context.get(orderContext),\n                      \"child 2 start\",\n                    ]);\n                    await next();\n                    context.set(orderContext, [\n                      ...context.get(orderContext),\n                      \"child 2 end - throwing\",\n                    ]);\n                    throw new Error(\"child 2 error\");\n                  },\n                ],\n                action() {\n                  return \"ACTION\";\n                },\n                loader() {\n                  return \"CHILD\";\n                },\n              },\n            ],\n          },\n        ]);\n\n        let requestContext = new RouterContextProvider();\n        let request = new Request(\"http://localhost/parent/child\", {\n          method: \"post\",\n          body: createFormData({}),\n        });\n        await expect(\n          handler.queryRoute(request, {\n            requestContext,\n            generateMiddlewareResponse: (q) => q(request),\n          }),\n        ).rejects.toThrow(\"child 2 error\");\n\n        expect(requestContext.get(orderContext)).toEqual([\n          \"parent start\",\n          \"child 1 start\",\n          \"child 2 start\",\n          \"child 2 end - throwing\",\n        ]);\n      });\n\n      it(\"allows thrown redirects before next()\", async () => {\n        let handler = createStaticHandler([\n          {\n            path: \"/\",\n          },\n          {\n            path: \"/parent\",\n            middleware: [\n              async () => {\n                throw redirect(\"/target\");\n              },\n            ],\n            loader() {\n              return \"PARENT\";\n            },\n          },\n          {\n            path: \"/target\",\n          },\n        ]);\n\n        let request = new Request(\"http://localhost/parent\");\n        let response = (await handler.queryRoute(request, {\n          generateMiddlewareResponse: (q) => q(request),\n        })) as Response;\n\n        expect(response.status).toBe(302);\n        expect(response.headers.get(\"Location\")).toBe(\"/target\");\n      });\n\n      it(\"allows thrown redirects after next()\", async () => {\n        let handler = createStaticHandler([\n          {\n            path: \"/\",\n          },\n          {\n            path: \"/parent\",\n            middleware: [\n              async (_, next) => {\n                await next();\n                throw redirect(\"/target\");\n              },\n            ],\n            loader() {\n              return \"PARENT\";\n            },\n          },\n          {\n            path: \"/target\",\n          },\n        ]);\n\n        let request = new Request(\"http://localhost/parent\");\n        let response = (await handler.queryRoute(request, {\n          generateMiddlewareResponse: (q) => q(request),\n        })) as Response;\n\n        expect(response.status).toBe(302);\n        expect(response.headers.get(\"Location\")).toBe(\"/target\");\n      });\n    });\n  });\n});\n"
  },
  {
    "path": "packages/react-router/__tests__/router/create-path-test.ts",
    "content": "import { createPath } from \"../../lib/router/history\";\n\ndescribe(\"createPath\", () => {\n  describe(\"given only a pathname\", () => {\n    it(\"returns the pathname unchanged\", () => {\n      let path = createPath({ pathname: \"https://google.com\" });\n      expect(path).toBe(\"https://google.com\");\n    });\n  });\n\n  describe(\"given a pathname and a search param\", () => {\n    it(\"returns the constructed pathname\", () => {\n      let path = createPath({\n        pathname: \"https://google.com\",\n        search: \"?something=cool\",\n      });\n      expect(path).toBe(\"https://google.com?something=cool\");\n    });\n  });\n\n  describe(\"given a pathname and a search param without ?\", () => {\n    it(\"returns the constructed pathname\", () => {\n      let path = createPath({\n        pathname: \"https://google.com\",\n        search: \"something=cool\",\n      });\n      expect(path).toBe(\"https://google.com?something=cool\");\n    });\n  });\n\n  describe(\"given a pathname and a hash param\", () => {\n    it(\"returns the constructed pathname\", () => {\n      let path = createPath({\n        pathname: \"https://google.com\",\n        hash: \"#section-1\",\n      });\n      expect(path).toBe(\"https://google.com#section-1\");\n    });\n  });\n\n  describe(\"given a pathname and a hash param without #\", () => {\n    it(\"returns the constructed pathname\", () => {\n      let path = createPath({\n        pathname: \"https://google.com\",\n        hash: \"section-1\",\n      });\n      expect(path).toBe(\"https://google.com#section-1\");\n    });\n  });\n\n  describe(\"given a full location object\", () => {\n    it(\"returns the constructed pathname\", () => {\n      let path = createPath({\n        pathname: \"https://google.com\",\n        search: \"something=cool\",\n        hash: \"#section-1\",\n      });\n      expect(path).toBe(\"https://google.com?something=cool#section-1\");\n    });\n  });\n});\n"
  },
  {
    "path": "packages/react-router/__tests__/router/data-strategy-test.ts",
    "content": "import type {\n  DataStrategyFunction,\n  DataStrategyMatch,\n  DataStrategyResult,\n} from \"../../lib/router/utils\";\nimport {\n  createDeferred,\n  createAsyncStub,\n  setup,\n} from \"./utils/data-router-setup\";\nimport { createFormData, tick } from \"./utils/utils\";\n\ndescribe(\"router dataStrategy\", () => {\n  function mockDataStrategy(fn: DataStrategyFunction) {\n    return jest.fn<\n      ReturnType<DataStrategyFunction>,\n      Parameters<DataStrategyFunction>\n    >(fn);\n  }\n\n  function keyedResults(\n    matches: DataStrategyMatch[],\n    results: DataStrategyResult[],\n  ) {\n    return results.reduce(\n      (acc, r, i) =>\n        Object.assign(\n          acc,\n          matches[i].shouldLoad ? { [matches[i].route.id]: r } : {},\n        ),\n      {},\n    );\n  }\n\n  function keyedResultsUsingShouldCallHandler(\n    matchesToLoad: DataStrategyMatch[],\n    results: DataStrategyResult[],\n  ) {\n    if (matchesToLoad.length !== results.length) {\n      throw new Error(\n        `Mismatched results length: expected ${matchesToLoad.length} but got ${results.length}`,\n      );\n    }\n    return results.reduce(\n      (acc, r, i) =>\n        Object.assign(\n          acc,\n          matchesToLoad[i].shouldCallHandler()\n            ? { [matchesToLoad[i].route.id]: r }\n            : {},\n        ),\n      {},\n    );\n  }\n\n  describe(\"loaders\", () => {\n    it(\"should allow a custom implementation to passthrough to default behavior\", async () => {\n      let dataStrategy = mockDataStrategy(({ matches }) =>\n        Promise.all(matches.map((m) => m.resolve())).then((results) =>\n          keyedResults(matches, results),\n        ),\n      );\n      let t = setup({\n        routes: [\n          {\n            path: \"/\",\n          },\n          {\n            id: \"json\",\n            path: \"/test\",\n            loader: true,\n            children: [\n              {\n                id: \"text\",\n                index: true,\n                loader: true,\n              },\n            ],\n          },\n        ],\n        dataStrategy,\n      });\n\n      let A = await t.navigate(\"/test\");\n\n      // Should be called in parallel\n      expect(A.loaders.json.stub).toHaveBeenCalledTimes(1);\n      expect(A.loaders.text.stub).toHaveBeenCalledTimes(1);\n\n      await A.loaders.json.resolve({ message: \"hello json\" });\n      await A.loaders.text.resolve(new Response(\"hello text\"));\n\n      expect(t.router.state.loaderData).toEqual({\n        json: { message: \"hello json\" },\n        text: \"hello text\",\n      });\n      expect(dataStrategy).toHaveBeenCalledWith(\n        expect.objectContaining({\n          request: expect.any(Request),\n          params: {},\n          matches: [\n            expect.objectContaining({\n              route: expect.objectContaining({\n                id: \"json\",\n              }),\n            }),\n            expect.objectContaining({\n              route: expect.objectContaining({\n                id: \"text\",\n              }),\n            }),\n          ],\n        }),\n      );\n    });\n\n    it(\"should allow a custom implementation to passthrough to default behavior and lazy\", async () => {\n      let dataStrategy = mockDataStrategy(({ matches }) =>\n        Promise.all(matches.map((m) => m.resolve())).then((results) =>\n          keyedResults(matches, results),\n        ),\n      );\n      let [lazyJson, lazyJsonDeferred] = createAsyncStub();\n      let [lazyText, lazyTextDeferred] = createAsyncStub();\n      let t = setup({\n        routes: [\n          {\n            path: \"/\",\n          },\n          {\n            id: \"json\",\n            path: \"/test\",\n            lazy: lazyJson,\n            children: [\n              {\n                id: \"text\",\n                index: true,\n                lazy: lazyText,\n              },\n            ],\n          },\n        ],\n        dataStrategy,\n      });\n\n      await t.navigate(\"/test\");\n      await lazyJsonDeferred.resolve({\n        loader: () => ({ message: \"hello json\" }),\n      });\n      await lazyTextDeferred.resolve({\n        loader: () => \"hello text\",\n      });\n      expect(t.router.state.loaderData).toEqual({\n        json: { message: \"hello json\" },\n        text: \"hello text\",\n      });\n      expect(dataStrategy).toHaveBeenCalledWith(\n        expect.objectContaining({\n          request: expect.any(Request),\n          matches: [\n            expect.objectContaining({\n              route: expect.objectContaining({\n                id: \"json\",\n              }),\n            }),\n            expect.objectContaining({\n              route: expect.objectContaining({\n                id: \"text\",\n              }),\n            }),\n          ],\n        }),\n      );\n    });\n\n    it(\"should allow custom implementations to override default behavior\", async () => {\n      let t = setup({\n        routes: [\n          {\n            path: \"/\",\n          },\n          {\n            id: \"test\",\n            path: \"/test\",\n            loader: true,\n          },\n        ],\n        async dataStrategy({ matches }) {\n          return Promise.all(\n            matches.map((m) =>\n              m.resolve(async (handler) => {\n                let result = await handler();\n                return `Route ID \"${m.route.id}\" returned \"${result}\"`;\n              }),\n            ),\n          ).then((results) => keyedResults(matches, results));\n        },\n      });\n\n      let A = await t.navigate(\"/test\");\n      await A.loaders.test.resolve(\"TEST\");\n\n      expect(t.router.state.loaderData).toMatchObject({\n        test: 'Route ID \"test\" returned \"TEST\"',\n      });\n    });\n\n    it(\"should allow custom implementations to override default behavior when erroring\", async () => {\n      let t = setup({\n        routes: [\n          {\n            path: \"/\",\n          },\n          {\n            id: \"test\",\n            path: \"/test\",\n            loader: true,\n            hasErrorBoundary: true,\n          },\n        ],\n        async dataStrategy({ matches }) {\n          return Promise.all(\n            matches.map((m) =>\n              m.resolve(async () => {\n                throw new Error(`Route ID \"${m.route.id}\" errored!`);\n              }),\n            ),\n          ).then((results) => keyedResults(matches, results));\n        },\n      });\n\n      let A = await t.navigate(\"/test\");\n      await A.loaders.test.resolve(\"TEST\");\n\n      expect(t.router.state.errors).toMatchObject({\n        test: new Error('Route ID \"test\" errored!'),\n      });\n    });\n\n    it(\"should allow custom implementations to override default behavior with lazy\", async () => {\n      let [lazy, lazyDeferred] = createAsyncStub();\n      let t = setup({\n        routes: [\n          {\n            path: \"/\",\n          },\n          {\n            id: \"test\",\n            path: \"/test\",\n            lazy,\n          },\n        ],\n        async dataStrategy({ matches }) {\n          return Promise.all(\n            matches.map((m) =>\n              m.resolve(async (handler) => {\n                let result = await handler();\n                return `Route ID \"${m.route.id}\" returned \"${result}\"`;\n              }),\n            ),\n          ).then((results) => keyedResults(matches, results));\n        },\n      });\n\n      await t.navigate(\"/test\");\n      await lazyDeferred.resolve({ loader: () => \"TEST\" });\n\n      expect(t.router.state.loaderData).toMatchObject({\n        test: 'Route ID \"test\" returned \"TEST\"',\n      });\n    });\n\n    it(\"handles errors at the proper boundary\", async () => {\n      let t = setup({\n        routes: [\n          {\n            path: \"/\",\n          },\n          {\n            path: \"/parent\",\n            children: [\n              {\n                id: \"child\",\n                path: \"child\",\n                hasErrorBoundary: true,\n                children: [\n                  {\n                    id: \"test\",\n                    index: true,\n                    loader: true,\n                  },\n                ],\n              },\n            ],\n          },\n        ],\n        dataStrategy({ matches }) {\n          return Promise.all(\n            matches.map(async (match) => match.resolve()),\n          ).then((results) => keyedResults(matches, results));\n        },\n      });\n\n      let A = await t.navigate(\"/parent/child\");\n      await A.loaders.test.reject(new Error(\"ERROR\"));\n\n      expect(t.router.state.loaderData.test).toBeUndefined();\n      expect(t.router.state.errors?.child.message).toBe(\"ERROR\");\n    });\n\n    it(\"handles errors at the proper boundary with a custom implementation\", async () => {\n      let t = setup({\n        routes: [\n          {\n            path: \"/\",\n          },\n          {\n            path: \"/parent\",\n            children: [\n              {\n                id: \"child\",\n                path: \"child\",\n                hasErrorBoundary: true,\n                children: [\n                  {\n                    id: \"test\",\n                    index: true,\n                    loader: true,\n                  },\n                ],\n              },\n            ],\n          },\n        ],\n        dataStrategy({ matches }) {\n          return Promise.all(\n            matches.map((match) => {\n              return match.resolve(async (handler) => {\n                return {\n                  type: \"data\",\n                  result: await handler(),\n                };\n              });\n            }),\n          ).then((results) => keyedResults(matches, results));\n        },\n      });\n\n      let A = await t.navigate(\"/parent/child\");\n      await A.loaders.test.reject(new Error(\"ERROR\"));\n\n      expect(t.router.state.loaderData.test).toBeUndefined();\n      expect(t.router.state.errors?.child.message).toBe(\"ERROR\");\n    });\n\n    it(\"bubbles to the root if dataStrategy throws\", async () => {\n      let t = setup({\n        routes: [\n          {\n            path: \"/\",\n          },\n          {\n            id: \"parent\",\n            path: \"/parent\",\n            loader: true,\n            children: [\n              {\n                id: \"child\",\n                path: \"child\",\n                loader: true,\n              },\n            ],\n          },\n        ],\n        dataStrategy({ matches }) {\n          throw new Error(\"Uh oh\");\n        },\n      });\n\n      let A = await t.navigate(\"/parent/child\");\n      await A.loaders.parent.resolve(\"PARENT\");\n\n      expect(t.router.state).toMatchObject({\n        actionData: null,\n        errors: {\n          parent: new Error(\"Uh oh\"),\n        },\n        loaderData: {},\n        navigation: {\n          state: \"idle\",\n        },\n      });\n    });\n\n    it(\"does not require resolve to be called if a match is not being loaded\", async () => {\n      let [lazy, lazyDeferred] = createAsyncStub();\n      let t = setup({\n        routes: [\n          {\n            path: \"/\",\n          },\n          {\n            id: \"parent\",\n            path: \"/parent\",\n            loader: true,\n            hasErrorBoundary: true,\n            children: [\n              {\n                id: \"child\",\n                path: \"child\",\n                lazy,\n              },\n            ],\n          },\n        ],\n        dataStrategy({ matches, request }) {\n          return Promise.all(\n            matches.map(async (match) => {\n              if (\n                request.url.endsWith(\"/parent/child\") &&\n                match.route.id === \"parent\"\n              ) {\n                return undefined;\n              }\n              return match.resolve();\n            }),\n          ).then((results) =>\n            // @ts-expect-error\n            keyedResults(matches, results),\n          );\n        },\n      });\n\n      let A = await t.navigate(\"/parent\");\n      await A.loaders.parent.resolve(\"PARENT\");\n      expect(t.router.state).toMatchObject({\n        errors: null,\n        loaderData: {\n          parent: \"PARENT\",\n        },\n        navigation: {\n          state: \"idle\",\n        },\n      });\n\n      let B = await t.navigate(\"/parent/child\");\n      await lazyDeferred.resolve({ loader: () => \"CHILD\" });\n\n      // no-op\n      await B.loaders.parent.resolve(\"XXX\");\n\n      expect(t.router.state).toMatchObject({\n        errors: null,\n        loaderData: {\n          child: \"CHILD\",\n          parent: \"PARENT\",\n        },\n        navigation: {\n          state: \"idle\",\n        },\n      });\n    });\n\n    it(\"indicates which routes need to load via match.shouldLoad\", async () => {\n      let dataStrategy = jest.fn<\n        ReturnType<DataStrategyFunction>,\n        Parameters<DataStrategyFunction>\n      >(({ matches }) => {\n        return Promise.all(matches.map((m) => m.resolve())).then((results) =>\n          keyedResults(matches, results),\n        );\n      });\n      let [lazy, lazyDeferred] = createAsyncStub();\n      let t = setup({\n        routes: [\n          {\n            id: \"root\",\n            path: \"/\",\n            loader: true,\n            children: [\n              {\n                id: \"parent\",\n                path: \"parent\",\n                loader: true,\n                action: true,\n                children: [\n                  {\n                    id: \"child\",\n                    path: \"child\",\n                    lazy,\n                  },\n                ],\n              },\n            ],\n          },\n        ],\n        dataStrategy,\n        hydrationData: {\n          // don't call dataStrategy on hydration\n          loaderData: { root: null },\n        },\n      });\n\n      let A = await t.navigate(\"/\");\n      expect(dataStrategy.mock.calls[0][0].matches).toEqual([\n        expect.objectContaining({\n          shouldLoad: true,\n          route: expect.objectContaining({ id: \"root\" }),\n        }),\n      ]);\n      await A.loaders.root.resolve(\"ROOT\");\n      expect(t.router.state.loaderData).toMatchObject({\n        root: \"ROOT\",\n      });\n\n      let B = await t.navigate(\"/parent\");\n      expect(dataStrategy.mock.calls[1][0].matches).toEqual([\n        expect.objectContaining({\n          shouldLoad: false,\n          route: expect.objectContaining({ id: \"root\" }),\n        }),\n        expect.objectContaining({\n          shouldLoad: true,\n          route: expect.objectContaining({ id: \"parent\" }),\n        }),\n      ]);\n      await B.loaders.parent.resolve(\"PARENT\");\n      expect(t.router.state.loaderData).toMatchObject({\n        root: \"ROOT\",\n        parent: \"PARENT\",\n      });\n\n      await t.navigate(\"/parent/child\");\n      expect(dataStrategy.mock.calls[2][0].matches).toEqual([\n        expect.objectContaining({\n          shouldLoad: false,\n          route: expect.objectContaining({ id: \"root\" }),\n        }),\n        expect.objectContaining({\n          shouldLoad: false,\n          route: expect.objectContaining({ id: \"parent\" }),\n        }),\n        expect.objectContaining({\n          shouldLoad: true,\n          route: expect.objectContaining({ id: \"child\" }),\n        }),\n      ]);\n      await lazyDeferred.resolve({\n        action: () => \"CHILD ACTION\",\n        loader: () => \"CHILD\",\n        shouldRevalidate: () => false,\n      });\n      expect(t.router.state.loaderData).toMatchObject({\n        root: \"ROOT\",\n        parent: \"PARENT\",\n        child: \"CHILD\",\n      });\n\n      await t.navigate(\"/parent/child\", {\n        formMethod: \"post\",\n        formData: createFormData({}),\n      });\n      await tick();\n      expect(dataStrategy.mock.calls[3][0].matches).toEqual([\n        expect.objectContaining({\n          shouldLoad: false,\n          route: expect.objectContaining({ id: \"root\" }),\n        }),\n        expect.objectContaining({\n          shouldLoad: false,\n          route: expect.objectContaining({ id: \"parent\" }),\n        }),\n        expect.objectContaining({\n          shouldLoad: true, // action\n          route: expect.objectContaining({ id: \"child\" }),\n        }),\n      ]);\n      expect(dataStrategy.mock.calls[4][0].matches).toEqual([\n        expect.objectContaining({\n          shouldLoad: true,\n          route: expect.objectContaining({ id: \"root\" }),\n        }),\n        expect.objectContaining({\n          shouldLoad: true,\n          route: expect.objectContaining({ id: \"parent\" }),\n        }),\n        expect.objectContaining({\n          shouldLoad: false, // shouldRevalidate=false\n          route: expect.objectContaining({ id: \"child\" }),\n        }),\n      ]);\n      expect(t.router.state.actionData).toMatchObject({\n        child: \"CHILD ACTION\",\n      });\n      expect(t.router.state.loaderData).toMatchObject({\n        root: \"ROOT\",\n        parent: \"PARENT\",\n        child: \"CHILD\",\n      });\n    });\n\n    it(\"indicates which routes need to load via match.shouldCallHandler()\", async () => {\n      let shouldCallHandlerValues: Record<string, boolean>[] = [];\n      let dataStrategy = jest.fn<\n        ReturnType<DataStrategyFunction>,\n        Parameters<DataStrategyFunction>\n      >(async ({ request, matches }) => {\n        let values = matches.reduce(\n          (acc, m) =>\n            Object.assign(acc, {\n              [m.route.id]: m.shouldCallHandler(),\n            }),\n          {},\n        );\n        shouldCallHandlerValues.push(values);\n        let matchesToLoad = matches.filter((m) => m.shouldCallHandler());\n        let results = await Promise.all(matchesToLoad.map((m) => m.resolve()));\n        return keyedResultsUsingShouldCallHandler(matchesToLoad, results);\n      });\n      let [lazy, lazyDeferred] = createAsyncStub();\n      let t = setup({\n        routes: [\n          {\n            id: \"root\",\n            path: \"/\",\n            loader: true,\n            children: [\n              {\n                id: \"parent\",\n                path: \"parent\",\n                loader: true,\n                action: true,\n                children: [\n                  {\n                    id: \"child\",\n                    path: \"child\",\n                    lazy,\n                  },\n                ],\n              },\n            ],\n          },\n        ],\n        dataStrategy,\n        hydrationData: {\n          // don't call dataStrategy on hydration\n          loaderData: { root: null },\n        },\n      });\n\n      let A = await t.navigate(\"/\");\n      expect(shouldCallHandlerValues[0]).toEqual({ root: true });\n      await A.loaders.root.resolve(\"ROOT\");\n      expect(t.router.state.loaderData).toMatchObject({\n        root: \"ROOT\",\n      });\n\n      let B = await t.navigate(\"/parent\");\n      expect(shouldCallHandlerValues[1]).toEqual({ root: false, parent: true });\n      await B.loaders.parent.resolve(\"PARENT\");\n      expect(t.router.state.loaderData).toMatchObject({\n        root: \"ROOT\",\n        parent: \"PARENT\",\n      });\n\n      await t.navigate(\"/parent/child\");\n      expect(shouldCallHandlerValues[2]).toEqual({\n        root: false,\n        parent: false,\n        child: true,\n      });\n      await lazyDeferred.resolve({\n        action: () => \"CHILD ACTION\",\n        loader: () => \"CHILD\",\n        shouldRevalidate: () => false,\n      });\n      expect(t.router.state.loaderData).toMatchObject({\n        root: \"ROOT\",\n        parent: \"PARENT\",\n        child: \"CHILD\",\n      });\n\n      await t.navigate(\"/parent/child\", {\n        formMethod: \"post\",\n        formData: createFormData({}),\n      });\n      await tick();\n      expect(shouldCallHandlerValues[3]).toEqual({\n        root: false,\n        parent: false,\n        child: true,\n      });\n      expect(shouldCallHandlerValues[4]).toEqual({\n        root: true,\n        parent: true,\n        child: false,\n      });\n      expect(t.router.state.actionData).toMatchObject({\n        child: \"CHILD ACTION\",\n      });\n      expect(t.router.state.loaderData).toMatchObject({\n        root: \"ROOT\",\n        parent: \"PARENT\",\n        child: \"CHILD\",\n      });\n    });\n\n    it(\"does not short circuit when there are no matchesToLoad\", async () => {\n      let dataStrategy = mockDataStrategy(async ({ matches }) => {\n        let results = await Promise.all(\n          matches.map((m) => m.resolve((handler) => handler())),\n        );\n        // Don't use keyedResults since it checks for shouldLoad and this test\n        // is always loading\n        return results.reduce(\n          (acc, r, i) => Object.assign(acc, { [matches[i].route.id]: r }),\n          {},\n        );\n      });\n      let t = setup({\n        routes: [\n          {\n            path: \"/\",\n          },\n          {\n            id: \"parent\",\n            path: \"/parent\",\n            loader: true,\n            children: [\n              {\n                id: \"child\",\n                path: \"child\",\n                loader: true,\n              },\n            ],\n          },\n        ],\n        dataStrategy,\n      });\n\n      let A = await t.navigate(\"/parent\");\n      await A.loaders.parent.resolve(\"PARENT1\");\n      expect(A.loaders.parent.stub).toHaveBeenCalled();\n      expect(t.router.state.loaderData).toEqual({\n        parent: \"PARENT1\",\n      });\n      expect(dataStrategy.mock.calls[0][0].matches).toEqual([\n        expect.objectContaining({\n          route: expect.objectContaining({\n            id: \"parent\",\n          }),\n        }),\n      ]);\n\n      let B = await t.navigate(\"/parent/child\");\n      await B.loaders.parent.resolve(\"PARENT2\");\n      await B.loaders.child.resolve(\"CHILD\");\n      expect(B.loaders.parent.stub).toHaveBeenCalled();\n      expect(B.loaders.child.stub).toHaveBeenCalled();\n      expect(t.router.state.loaderData).toEqual({\n        parent: \"PARENT2\",\n        child: \"CHILD\",\n      });\n      expect(dataStrategy.mock.calls[1][0].matches).toEqual([\n        expect.objectContaining({\n          route: expect.objectContaining({\n            id: \"parent\",\n          }),\n        }),\n        expect.objectContaining({\n          route: expect.objectContaining({\n            id: \"child\",\n          }),\n        }),\n      ]);\n\n      let C = await t.navigate(\"/parent\");\n      await C.loaders.parent.resolve(\"PARENT3\");\n      expect(C.loaders.parent.stub).toHaveBeenCalled();\n      expect(t.router.state.loaderData).toEqual({\n        parent: \"PARENT3\",\n      });\n      expect(dataStrategy.mock.calls[2][0].matches).toEqual([\n        expect.objectContaining({\n          route: expect.objectContaining({\n            id: \"parent\",\n          }),\n        }),\n      ]);\n\n      expect(dataStrategy).toHaveBeenCalledTimes(3);\n    });\n  });\n\n  describe(\"actions\", () => {\n    it(\"should allow a custom implementation to passthrough to default behavior\", async () => {\n      let dataStrategy = mockDataStrategy(({ matches }) =>\n        Promise.all(matches.map((m) => m.resolve())).then((results) =>\n          keyedResults(matches, results),\n        ),\n      );\n      let t = setup({\n        routes: [\n          {\n            path: \"/\",\n          },\n          {\n            id: \"json\",\n            path: \"/test\",\n            action: true,\n          },\n        ],\n        dataStrategy,\n      });\n\n      let A = await t.navigate(\"/test\", {\n        formMethod: \"post\",\n        formData: createFormData({}),\n      });\n\n      await A.actions.json.resolve({ message: \"hello json\" });\n\n      expect(t.router.state.actionData).toEqual({\n        json: { message: \"hello json\" },\n      });\n      expect(dataStrategy).toHaveBeenCalledWith(\n        expect.objectContaining({\n          request: expect.any(Request),\n          matches: [\n            expect.objectContaining({\n              route: expect.objectContaining({\n                id: \"json\",\n              }),\n            }),\n          ],\n        }),\n      );\n    });\n\n    it(\"should allow a custom implementation to passthrough to default behavior and lazy\", async () => {\n      let dataStrategy = mockDataStrategy(({ matches }) =>\n        Promise.all(matches.map((m) => m.resolve())).then((results) =>\n          keyedResults(matches, results),\n        ),\n      );\n      let [lazy, lazyDeferred] = createAsyncStub();\n      let t = setup({\n        routes: [\n          {\n            path: \"/\",\n          },\n          {\n            id: \"json\",\n            path: \"/test\",\n            lazy,\n          },\n        ],\n        dataStrategy,\n      });\n\n      await t.navigate(\"/test\", {\n        formMethod: \"post\",\n        formData: createFormData({}),\n      });\n      await lazyDeferred.resolve({\n        action: () => ({ message: \"hello json\" }),\n      });\n      expect(t.router.state.actionData).toEqual({\n        json: { message: \"hello json\" },\n      });\n      expect(dataStrategy).toHaveBeenCalledWith(\n        expect.objectContaining({\n          request: expect.any(Request),\n          matches: [\n            expect.objectContaining({\n              route: expect.objectContaining({\n                id: \"json\",\n              }),\n            }),\n          ],\n        }),\n      );\n    });\n  });\n\n  describe(\"fetchers\", () => {\n    describe(\"loaders\", () => {\n      it(\"should allow a custom implementation to passthrough to default behavior\", async () => {\n        let dataStrategy = mockDataStrategy(({ matches }) =>\n          Promise.all(matches.map((m) => m.resolve())).then((results) =>\n            keyedResults(matches, results),\n          ),\n        );\n        let t = setup({\n          routes: [\n            {\n              path: \"/\",\n            },\n            {\n              id: \"json\",\n              path: \"/test\",\n              loader: true,\n            },\n          ],\n          dataStrategy,\n        });\n\n        let key = \"key\";\n        let A = await t.fetch(\"/test\", key);\n\n        await A.loaders.json.resolve({ message: \"hello json\" });\n\n        expect(t.fetchers[key].data.message).toBe(\"hello json\");\n\n        expect(dataStrategy).toHaveBeenCalledWith(\n          expect.objectContaining({\n            request: expect.any(Request),\n            matches: [\n              expect.objectContaining({\n                route: expect.objectContaining({\n                  id: \"json\",\n                }),\n              }),\n            ],\n          }),\n        );\n      });\n\n      it(\"should allow a custom implementation to passthrough to default behavior and lazy\", async () => {\n        let dataStrategy = mockDataStrategy(({ matches }) =>\n          Promise.all(matches.map((m) => m.resolve())).then((results) =>\n            keyedResults(matches, results),\n          ),\n        );\n        let [lazy, lazyDeferred] = createAsyncStub();\n        let t = setup({\n          routes: [\n            {\n              path: \"/\",\n            },\n            {\n              id: \"json\",\n              path: \"/test\",\n              lazy,\n            },\n          ],\n          dataStrategy,\n        });\n\n        let key = \"key\";\n        await t.fetch(\"/test\", key);\n        await lazyDeferred.resolve({\n          loader: () => ({ message: \"hello json\" }),\n        });\n        expect(t.fetchers[key].data.message).toBe(\"hello json\");\n        expect(dataStrategy).toHaveBeenCalledWith(\n          expect.objectContaining({\n            request: expect.any(Request),\n            matches: [\n              expect.objectContaining({\n                route: expect.objectContaining({\n                  id: \"json\",\n                }),\n              }),\n            ],\n          }),\n        );\n      });\n    });\n\n    describe(\"actions\", () => {\n      it(\"should allow a custom implementation to passthrough to default behavior\", async () => {\n        let dataStrategy = mockDataStrategy(({ matches }) =>\n          Promise.all(matches.map((m) => m.resolve())).then((results) =>\n            keyedResults(matches, results),\n          ),\n        );\n        let t = setup({\n          routes: [\n            {\n              path: \"/\",\n            },\n            {\n              id: \"json\",\n              path: \"/test\",\n              action: true,\n            },\n          ],\n          dataStrategy,\n        });\n\n        let key = \"key\";\n        let A = await t.fetch(\"/test\", key, {\n          formMethod: \"post\",\n          formData: createFormData({}),\n        });\n\n        await A.actions.json.resolve({ message: \"hello json\" });\n\n        expect(t.fetchers[key].data.message).toBe(\"hello json\");\n\n        expect(dataStrategy).toHaveBeenCalledWith(\n          expect.objectContaining({\n            request: expect.any(Request),\n            matches: expect.arrayContaining([\n              expect.objectContaining({\n                route: expect.objectContaining({\n                  id: \"json\",\n                }),\n              }),\n            ]),\n          }),\n        );\n      });\n\n      it(\"should allow a custom implementation to passthrough to default behavior and lazy\", async () => {\n        let dataStrategy = mockDataStrategy(({ matches }) =>\n          Promise.all(matches.map((m) => m.resolve())).then((results) =>\n            keyedResults(matches, results),\n          ),\n        );\n        let [lazy, lazyDeferred] = createAsyncStub();\n        let t = setup({\n          routes: [\n            {\n              path: \"/\",\n            },\n            {\n              id: \"json\",\n              path: \"/test\",\n              lazy,\n            },\n          ],\n          dataStrategy,\n        });\n\n        let key = \"key\";\n        await t.fetch(\"/test\", key, {\n          formMethod: \"post\",\n          formData: createFormData({}),\n        });\n        await lazyDeferred.resolve({\n          action: () => ({ message: \"hello json\" }),\n        });\n\n        expect(t.fetchers[key].data.message).toBe(\"hello json\");\n\n        expect(dataStrategy).toHaveBeenCalledWith(\n          expect.objectContaining({\n            request: expect.any(Request),\n            matches: expect.arrayContaining([\n              expect.objectContaining({\n                route: expect.objectContaining({\n                  id: \"json\",\n                }),\n              }),\n            ]),\n          }),\n        );\n      });\n    });\n  });\n\n  describe(\"use-cases\", () => {\n    it(\"permits users to take control over response decoding\", async () => {\n      let t = setup({\n        routes: [\n          {\n            path: \"/\",\n          },\n          {\n            id: \"json\",\n            path: \"/test\",\n            loader: true,\n            children: [\n              {\n                id: \"reverse\",\n                index: true,\n                loader: true,\n              },\n            ],\n          },\n        ],\n        async dataStrategy({ matches }) {\n          return Promise.all(\n            matches.map(async (m) => {\n              return await m.resolve(async (handler) => {\n                let result = await handler();\n                if (\n                  result instanceof Response &&\n                  result.headers.get(\"Content-Type\") === \"application/reverse\"\n                ) {\n                  let str = await result.text();\n                  return {\n                    original: str,\n                    reversed: str.split(\"\").reverse().join(\"\"),\n                  };\n                }\n                // This will be a JSON response we expect to be decoded the normal way\n                return result;\n              });\n            }),\n          ).then((results) => keyedResults(matches, results));\n        },\n      });\n\n      let A = await t.navigate(\"/test\");\n      await A.loaders.json.resolve({ message: \"hello json\" });\n      await A.loaders.reverse.resolve(\n        new Response(\"hello text\", {\n          headers: { \"Content-Type\": \"application/reverse\" },\n        }),\n      );\n\n      expect(t.router.state.loaderData).toEqual({\n        json: { message: \"hello json\" },\n        reverse: {\n          original: \"hello text\",\n          reversed: \"txet olleh\",\n        },\n      });\n    });\n\n    it(\"allows a single-fetch type approach\", async () => {\n      let t = setup({\n        routes: [\n          {\n            path: \"/\",\n          },\n          {\n            id: \"parent\",\n            path: \"/parent\",\n            loader: true,\n            children: [\n              {\n                id: \"child\",\n                path: \"child\",\n                loader: true,\n              },\n            ],\n          },\n        ],\n        async dataStrategy({ matches }) {\n          // Hold a deferred for each route we need to load\n          let routeDeferreds: Map<\n            string,\n            ReturnType<typeof createDeferred>\n          > = new Map();\n\n          // Use resolve's to create and await a deferred for each\n          // route that needs to load\n          let matchPromises = matches.map((m) =>\n            m.resolve(() => {\n              // Don't call handler, just create a deferred we can resolve from\n              // the single fetch response and return it's promise\n              let dfd = createDeferred();\n              routeDeferreds.set(m.route.id, dfd);\n              return dfd.promise as Promise<DataStrategyResult>;\n            }),\n          );\n\n          // Mocked single fetch call response for the routes that need loading\n          let result = {\n            loaderData: {\n              parent: \"PARENT\",\n              child: \"CHILD\",\n            },\n          };\n\n          // Resolve the deferred's above and return the mapped match promises\n          routeDeferreds.forEach((dfd, routeId) =>\n            dfd.resolve(result.loaderData[routeId]),\n          );\n          return Promise.all(matchPromises).then((results) =>\n            keyedResults(matches, results),\n          );\n        },\n      });\n\n      await t.navigate(\"/parent/child\");\n\n      // We don't even have to resolve the loader here because it'll never\n      // be called in this test\n      await tick();\n\n      expect(t.router.state.loaderData).toMatchObject({\n        parent: \"PARENT\",\n        child: \"CHILD\",\n      });\n    });\n\n    it(\"allows middleware/context implementations\", async () => {\n      let t = setup({\n        routes: [\n          {\n            path: \"/\",\n          },\n          {\n            id: \"parent\",\n            path: \"/parent\",\n            loader: true,\n            handle: {\n              context: {\n                parent: () => ({ id: \"parent\" }),\n              },\n              middleware(context) {\n                context.parent.whatever = \"PARENT MIDDLEWARE\";\n              },\n            },\n            children: [\n              {\n                id: \"child\",\n                path: \"child\",\n                loader: true,\n                handle: {\n                  context: {\n                    child: () => ({ id: \"child\" }),\n                  },\n                  middleware(context) {\n                    context.child.whatever = \"CHILD MIDDLEWARE\";\n                  },\n                },\n              },\n            ],\n          },\n        ],\n        async dataStrategy({ matches }) {\n          // Run context/middleware sequentially\n          let context = matches.reduce((acc, m) => {\n            if (m.route.handle?.context) {\n              let matchContext = Object.entries(m.route.handle.context).reduce(\n                (acc, [key, value]) =>\n                  Object.assign(acc, {\n                    // @ts-expect-error\n                    [key]: value(),\n                  }),\n                {},\n              );\n              Object.assign(acc, matchContext);\n            }\n            if (m.route.handle?.middleware) {\n              m.route.handle.middleware(acc);\n            }\n            return acc;\n          }, {});\n\n          // Run loaders in parallel only exposing contexts from above\n          return Promise.all(\n            matches.map((m, i) =>\n              m.resolve(async (handler) => {\n                // Only provide context values up to this level in the matches\n                let handlerCtx = matches.slice(0, i + 1).reduce((acc, m) => {\n                  Object.keys(m.route.handle?.context).forEach((k) => {\n                    acc[k] = context[k];\n                  });\n                  return acc;\n                }, {});\n                let result = await handler(handlerCtx);\n                return result;\n              }),\n            ),\n          ).then((results) => keyedResults(matches, results));\n        },\n      });\n\n      let A = await t.navigate(\"/parent/child\");\n\n      // Loaders are called with context from their level and above, and\n      // context reflects any values set by middleware\n      expect(A.loaders.parent.stub).toHaveBeenCalledWith(\n        expect.objectContaining({\n          request: expect.any(Request),\n          params: expect.any(Object),\n        }),\n        {\n          parent: {\n            id: \"parent\",\n            whatever: \"PARENT MIDDLEWARE\",\n          },\n        },\n      );\n\n      expect(A.loaders.child.stub).toHaveBeenCalledWith(\n        expect.objectContaining({\n          request: expect.any(Request),\n          params: expect.any(Object),\n        }),\n        {\n          parent: {\n            id: \"parent\",\n            whatever: \"PARENT MIDDLEWARE\",\n          },\n          child: {\n            id: \"child\",\n            whatever: \"CHILD MIDDLEWARE\",\n          },\n        },\n      );\n\n      await A.loaders.parent.resolve(\"PARENT LOADER\");\n      expect(t.router.state.navigation.state).toBe(\"loading\");\n\n      await A.loaders.child.resolve(\"CHILD LOADER\");\n      expect(t.router.state.navigation.state).toBe(\"idle\");\n\n      expect(t.router.state.loaderData).toMatchObject({\n        parent: \"PARENT LOADER\",\n        child: \"CHILD LOADER\",\n      });\n    });\n\n    it(\"allows middleware/context implementations when some routes don't need to revalidate\", async () => {\n      let t = setup({\n        routes: [\n          {\n            path: \"/\",\n          },\n          {\n            id: \"parent\",\n            path: \"/parent\",\n            loader: true,\n            handle: {\n              context: {\n                parent: () => ({ id: \"parent\" }),\n              },\n              middleware(context) {\n                context.parent.whatever = \"PARENT MIDDLEWARE\";\n              },\n            },\n            children: [\n              {\n                id: \"child\",\n                path: \"child\",\n                loader: true,\n                handle: {\n                  context: {\n                    child: () => ({ id: \"child\" }),\n                  },\n                  middleware(context) {\n                    context.child.whatever = \"CHILD MIDDLEWARE\";\n                  },\n                },\n              },\n            ],\n          },\n        ],\n        async dataStrategy({ matches }) {\n          // Run context/middleware sequentially\n          let context = matches.reduce((acc, m) => {\n            if (m.route.handle?.context) {\n              let matchContext = Object.entries(m.route.handle.context).reduce(\n                (acc, [key, value]) =>\n                  Object.assign(acc, {\n                    // @ts-expect-error\n                    [key]: value(),\n                  }),\n                {},\n              );\n              Object.assign(acc, matchContext);\n            }\n            if (m.route.handle?.middleware) {\n              m.route.handle.middleware(acc);\n            }\n            return acc;\n          }, {});\n\n          // Run loaders in parallel only exposing contexts from above\n          return Promise.all(\n            matches.map((m, i) =>\n              m.resolve(async (callHandler) => {\n                // Only provide context values up to this level in the matches\n                let handlerCtx = matches.slice(0, i + 1).reduce((acc, m) => {\n                  Object.keys(m.route.handle?.context).forEach((k) => {\n                    acc[k] = context[k];\n                  });\n                  return acc;\n                }, {});\n                let result = m.shouldLoad\n                  ? await callHandler(handlerCtx)\n                  : t.router.state.loaderData[m.route.id];\n                return result;\n              }),\n            ),\n          ).then((results) => keyedResults(matches, results));\n        },\n      });\n\n      let A = await t.navigate(\"/parent\");\n      await A.loaders.parent.resolve(\"PARENT\");\n      expect(t.router.state.navigation.state).toBe(\"idle\");\n      expect(t.router.state.loaderData).toMatchObject({\n        parent: \"PARENT\",\n      });\n\n      let B = await t.navigate(\"/parent/child\");\n\n      // Loaders are called with context from their level and above, and\n      // context reflects any values set by middleware\n      expect(B.loaders.child.stub).toHaveBeenCalledWith(\n        expect.objectContaining({\n          request: expect.any(Request),\n          params: expect.any(Object),\n        }),\n        {\n          parent: {\n            id: \"parent\",\n            whatever: \"PARENT MIDDLEWARE\",\n          },\n          child: {\n            id: \"child\",\n            whatever: \"CHILD MIDDLEWARE\",\n          },\n        },\n      );\n\n      await B.loaders.child.resolve(\"CHILD\");\n      expect(t.router.state.navigation.state).toBe(\"idle\");\n\n      expect(t.router.state.loaderData).toMatchObject({\n        parent: \"PARENT\",\n        child: \"CHILD\",\n      });\n    });\n\n    it(\"allows middleware/context implementations when some routes don't need to revalidate (using shouldCallHandler)\", async () => {\n      let t = setup({\n        routes: [\n          {\n            path: \"/\",\n          },\n          {\n            id: \"parent\",\n            path: \"/parent\",\n            loader: true,\n            handle: {\n              context: {\n                parent: () => ({ id: \"parent\" }),\n              },\n              middleware(context) {\n                context.parent.whatever = \"PARENT MIDDLEWARE\";\n              },\n            },\n            children: [\n              {\n                id: \"child\",\n                path: \"child\",\n                loader: true,\n                handle: {\n                  context: {\n                    child: () => ({ id: \"child\" }),\n                  },\n                  middleware(context) {\n                    context.child.whatever = \"CHILD MIDDLEWARE\";\n                  },\n                },\n              },\n            ],\n          },\n        ],\n        async dataStrategy({ matches }) {\n          // Run context/middleware sequentially\n          let context = matches.reduce((acc, m) => {\n            if (m.route.handle?.context) {\n              let matchContext = Object.entries(m.route.handle.context).reduce(\n                (acc, [key, value]) =>\n                  Object.assign(acc, {\n                    // @ts-expect-error\n                    [key]: value(),\n                  }),\n                {},\n              );\n              Object.assign(acc, matchContext);\n            }\n            if (m.route.handle?.middleware) {\n              m.route.handle.middleware(acc);\n            }\n            return acc;\n          }, {});\n\n          // Run loaders in parallel only exposing contexts from above\n          let matchesToLoad = matches.filter((m) => m.shouldCallHandler());\n          let results = await Promise.all(\n            matchesToLoad.map((m) =>\n              m.resolve((callHandler) => callHandler(context)),\n            ),\n          );\n          return keyedResultsUsingShouldCallHandler(matchesToLoad, results);\n        },\n      });\n\n      let A = await t.navigate(\"/parent\");\n      await A.loaders.parent.resolve(\"PARENT\");\n      expect(t.router.state.navigation.state).toBe(\"idle\");\n      expect(t.router.state.loaderData).toMatchObject({\n        parent: \"PARENT\",\n      });\n\n      let B = await t.navigate(\"/parent/child\");\n\n      // Loaders are called with context from their level and above, and\n      // context reflects any values set by middleware\n      expect(B.loaders.child.stub).toHaveBeenCalledWith(\n        expect.objectContaining({\n          request: expect.any(Request),\n          params: expect.any(Object),\n        }),\n        {\n          parent: {\n            id: \"parent\",\n            whatever: \"PARENT MIDDLEWARE\",\n          },\n          child: {\n            id: \"child\",\n            whatever: \"CHILD MIDDLEWARE\",\n          },\n        },\n      );\n\n      await B.loaders.child.resolve(\"CHILD\");\n      expect(t.router.state.navigation.state).toBe(\"idle\");\n\n      expect(t.router.state.loaderData).toMatchObject({\n        parent: \"PARENT\",\n        child: \"CHILD\",\n      });\n    });\n\n    it(\"allows automatic caching of loader results\", async () => {\n      let cache: Record<string, unknown> = {};\n      let t = setup({\n        routes: [\n          {\n            path: \"/\",\n          },\n          {\n            id: \"parent\",\n            path: \"/parent\",\n            loader: true,\n            handle: {\n              cacheKey: (url: string) => new URL(url).pathname,\n            },\n            children: [\n              {\n                id: \"child\",\n                path: \"child\",\n                loader: true,\n                action: true,\n              },\n            ],\n          },\n        ],\n        async dataStrategy({ request, matches }) {\n          const getCacheKey = (m: DataStrategyMatch) =>\n            m.route.handle?.cacheKey\n              ? [m.route.id, m.route.handle.cacheKey(request.url)].join(\"-\")\n              : null;\n\n          if (request.method !== \"GET\") {\n            // invalidate on actions\n            cache = {};\n          }\n\n          let matchesToLoad = matches.filter((m) => m.shouldLoad);\n          return Promise.all(\n            matchesToLoad.map(async (m) => {\n              return m.resolve(async (handler) => {\n                let key = getCacheKey(m);\n                if (key && cache[key]) {\n                  return cache[key];\n                }\n\n                let dsResult = await handler();\n                if (key && request.method === \"GET\") {\n                  cache[key] = dsResult;\n                }\n\n                return dsResult;\n              });\n            }),\n          ).then((results) => keyedResults(matchesToLoad, results));\n        },\n      });\n\n      let A = await t.navigate(\"/parent/child\");\n      await A.loaders.parent.resolve(\"PARENT\");\n      await A.loaders.child.resolve(\"CHILD\");\n\n      expect(t.router.state).toMatchObject({\n        navigation: { state: \"idle\" },\n        loaderData: {\n          parent: \"PARENT\",\n          child: \"CHILD\",\n        },\n      });\n\n      // Changing search params should force revalidation, but pathname-based\n      // cache will serve the old data\n      let B = await t.navigate(\"/parent/child?a=b\");\n      await B.loaders.child.resolve(\"CHILD*\");\n\n      expect(t.router.state).toMatchObject({\n        navigation: { state: \"idle\" },\n        loaderData: {\n          parent: \"PARENT\",\n          child: \"CHILD*\",\n        },\n      });\n\n      // Useless resolution - handler was never called for parent\n      await B.loaders.parent.resolve(\"PARENT*\");\n\n      expect(t.router.state).toMatchObject({\n        navigation: { state: \"idle\" },\n        loaderData: {\n          parent: \"PARENT\",\n          child: \"CHILD*\",\n        },\n      });\n\n      // Action to invalidate the cache\n      let C = await t.navigate(\"/parent/child?a=b\", {\n        formMethod: \"post\",\n        formData: createFormData({}),\n      });\n      await C.actions.child.resolve(\"ACTION\");\n      await C.loaders.parent.resolve(\"PARENT**\");\n      await C.loaders.child.resolve(\"CHILD**\");\n\n      expect(t.router.state).toMatchObject({\n        navigation: { state: \"idle\" },\n        actionData: {\n          child: \"ACTION\",\n        },\n        loaderData: {\n          parent: \"PARENT**\",\n          child: \"CHILD**\",\n        },\n      });\n    });\n\n    it(\"allows automatic caching of loader results (using shouldCallHandler)\", async () => {\n      let cache: Record<string, unknown> = {};\n      let t = setup({\n        routes: [\n          {\n            path: \"/\",\n          },\n          {\n            id: \"parent\",\n            path: \"/parent\",\n            loader: true,\n            handle: {\n              cacheKey: (url: string) => new URL(url).pathname,\n            },\n            children: [\n              {\n                id: \"child\",\n                path: \"child\",\n                loader: true,\n                action: true,\n              },\n            ],\n          },\n        ],\n        async dataStrategy({ request, matches }) {\n          const getCacheKey = (m: DataStrategyMatch) =>\n            m.route.handle?.cacheKey\n              ? [m.route.id, m.route.handle.cacheKey(request.url)].join(\"-\")\n              : null;\n\n          if (request.method !== \"GET\") {\n            // invalidate on actions\n            cache = {};\n          }\n\n          let matchesToLoad = matches.filter((m) => m.shouldCallHandler());\n          let results = await Promise.all(\n            matchesToLoad.map(async (m) => {\n              return m.resolve(async (handler) => {\n                let key = getCacheKey(m);\n                if (key && cache[key]) {\n                  return cache[key];\n                }\n\n                let dsResult = await handler();\n                if (key && request.method === \"GET\") {\n                  cache[key] = dsResult;\n                }\n\n                return dsResult;\n              });\n            }),\n          );\n          return keyedResultsUsingShouldCallHandler(matchesToLoad, results);\n        },\n      });\n\n      let A = await t.navigate(\"/parent/child\");\n      await A.loaders.parent.resolve(\"PARENT\");\n      await A.loaders.child.resolve(\"CHILD\");\n\n      expect(t.router.state).toMatchObject({\n        navigation: { state: \"idle\" },\n        loaderData: {\n          parent: \"PARENT\",\n          child: \"CHILD\",\n        },\n      });\n\n      // Changing search params should force revalidation, but pathname-based\n      // cache will serve the old data\n      let B = await t.navigate(\"/parent/child?a=b\");\n      await B.loaders.child.resolve(\"CHILD*\");\n\n      expect(t.router.state).toMatchObject({\n        navigation: { state: \"idle\" },\n        loaderData: {\n          parent: \"PARENT\",\n          child: \"CHILD*\",\n        },\n      });\n\n      // Useless resolution - handler was never called for parent\n      await B.loaders.parent.resolve(\"PARENT*\");\n\n      expect(t.router.state).toMatchObject({\n        navigation: { state: \"idle\" },\n        loaderData: {\n          parent: \"PARENT\",\n          child: \"CHILD*\",\n        },\n      });\n\n      // Action to invalidate the cache\n      let C = await t.navigate(\"/parent/child?a=b\", {\n        formMethod: \"post\",\n        formData: createFormData({}),\n      });\n      await C.actions.child.resolve(\"ACTION\");\n      await C.loaders.parent.resolve(\"PARENT**\");\n      await C.loaders.child.resolve(\"CHILD**\");\n\n      expect(t.router.state).toMatchObject({\n        navigation: { state: \"idle\" },\n        actionData: {\n          child: \"ACTION\",\n        },\n        loaderData: {\n          parent: \"PARENT**\",\n          child: \"CHILD**\",\n        },\n      });\n    });\n\n    it(\"allows defining a custom revalidation behavior\", async () => {\n      let t = setup({\n        routes: [\n          {\n            id: \"index\",\n            path: \"/\",\n            action: true,\n            loader: true,\n          },\n        ],\n        async dataStrategy({ request, matches, context }) {\n          if (request.method !== \"GET\") {\n            context.actionMethod = request.method;\n          }\n          // Don't revalidate on PUT requests\n          let defaultShouldRevalidate =\n            context.actionMethod === \"PUT\" ? false : undefined;\n          let matchesToLoad = matches.filter((m) =>\n            m.shouldCallHandler(defaultShouldRevalidate),\n          );\n          let results = await Promise.all(\n            matchesToLoad.map((m) => m.resolve()),\n          );\n          return keyedResultsUsingShouldCallHandler(matchesToLoad, results);\n        },\n        hydrationData: {\n          loaderData: {\n            index: \"INDEX\",\n          },\n        },\n      });\n\n      let A = await t.navigate(\"/\", {\n        formMethod: \"post\",\n        formData: createFormData({}),\n      });\n      await A.actions.index.resolve(\"ACTION1\");\n      await A.loaders.index.resolve(\"INDEX1\");\n      expect(t.router.state.navigation.state).toBe(\"idle\");\n      expect(t.router.state).toMatchObject({\n        navigation: { state: \"idle\" },\n        actionData: {\n          index: \"ACTION1\",\n        },\n        loaderData: {\n          index: \"INDEX1\",\n        },\n      });\n\n      let B = await t.navigate(\"/\", {\n        formMethod: \"put\",\n        formData: createFormData({}),\n      });\n      await B.actions.index.resolve(\"ACTION2\");\n      //await B.loaders.index.resolve(\"INDEX2\"); // no-op\n      expect(t.router.state.navigation.state).toBe(\"idle\");\n      expect(t.router.state).toMatchObject({\n        navigation: { state: \"idle\" },\n        actionData: {\n          index: \"ACTION2\",\n        },\n        loaderData: {\n          index: \"INDEX1\",\n        },\n      });\n    });\n  });\n});\n"
  },
  {
    "path": "packages/react-router/__tests__/router/fetchers-test.ts",
    "content": "/* eslint-disable jest/valid-title */\nimport type { HydrationState } from \"../../lib/router/router\";\nimport { createMemoryHistory } from \"../../lib/router/history\";\nimport {\n  createRouter,\n  IDLE_FETCHER,\n  IDLE_NAVIGATION,\n} from \"../../lib/router/router\";\nimport { ErrorResponseImpl } from \"../../lib/router/utils\";\n\nimport {\n  cleanup,\n  createDeferred,\n  getFetcherData,\n  setup,\n  TASK_ROUTES,\n} from \"./utils/data-router-setup\";\nimport { createFormData, sleep, tick } from \"./utils/utils\";\n\nfunction initializeTest(init?: {\n  url?: string;\n  hydrationData?: HydrationState;\n}) {\n  return setup({\n    routes: [\n      {\n        path: \"\",\n        id: \"root\",\n        hasErrorBoundary: true,\n        loader: true,\n        children: [\n          {\n            path: \"/\",\n            id: \"index\",\n            loader: true,\n            action: true,\n          },\n          {\n            path: \"/foo\",\n            id: \"foo\",\n            loader: true,\n            action: true,\n          },\n          {\n            path: \"/foo/bar\",\n            id: \"foobar\",\n            loader: true,\n            action: true,\n          },\n          {\n            path: \"/bar\",\n            id: \"bar\",\n            loader: true,\n            action: true,\n          },\n          {\n            path: \"/baz\",\n            id: \"baz\",\n            loader: true,\n            action: true,\n          },\n        ],\n      },\n    ],\n    hydrationData: init?.hydrationData || {\n      loaderData: { root: \"ROOT\", index: \"INDEX\" },\n    },\n    ...(init?.url ? { initialEntries: [init.url] } : {}),\n  });\n}\n\ndescribe(\"fetchers\", () => {\n  beforeEach(() => {\n    jest.spyOn(console, \"warn\").mockImplementation(() => {});\n  });\n\n  // Detect any failures inside the router navigate code\n  afterEach(() => {\n    cleanup();\n\n    // @ts-ignore\n    console.warn.mockReset();\n  });\n\n  describe(\"fetcher states\", () => {\n    it(\"unabstracted loader fetch\", async () => {\n      let dfd = createDeferred();\n      let router = createRouter({\n        history: createMemoryHistory({ initialEntries: [\"/\"] }),\n        routes: [\n          {\n            id: \"root\",\n            path: \"/\",\n            loader: () => dfd.promise,\n          },\n        ],\n        hydrationData: {\n          loaderData: { root: \"ROOT DATA\" },\n        },\n      });\n      let fetcherData = getFetcherData(router);\n\n      let key = \"key\";\n      router.fetch(key, \"root\", \"/\");\n      expect(router.getFetcher(key)).toEqual({\n        state: \"loading\",\n        formMethod: undefined,\n        formEncType: undefined,\n        formData: undefined,\n      });\n\n      await dfd.resolve(\"DATA\");\n      expect(router.getFetcher(key)).toBe(IDLE_FETCHER);\n      expect(fetcherData.get(key)).toBe(\"DATA\");\n\n      expect(router._internalFetchControllers.size).toBe(0);\n    });\n\n    it(\"unabstracted loader fetch with fog of war\", async () => {\n      let dfd = createDeferred();\n      let router = createRouter({\n        history: createMemoryHistory({ initialEntries: [\"/\"] }),\n        routes: [\n          {\n            id: \"root\",\n            // Note: No path is provided on the root route in this test to\n            // ensure nothing matches before routes are patched\n          },\n        ],\n        hydrationData: {\n          loaderData: { root: \"ROOT DATA\" },\n        },\n        async patchRoutesOnNavigation({ path, patch }) {\n          if (path === \"/lazy\") {\n            patch(\"root\", [\n              {\n                id: \"lazy\",\n                path: \"/lazy\",\n                loader: () => dfd.promise,\n              },\n            ]);\n          }\n        },\n      });\n      let fetcherData = getFetcherData(router);\n\n      let key = \"key\";\n      router.fetch(key, \"lazy\", \"/lazy\");\n      expect(router.getFetcher(key)).toEqual({\n        state: \"loading\",\n        formMethod: undefined,\n        formEncType: undefined,\n        formData: undefined,\n      });\n\n      await dfd.resolve(\"DATA\");\n      expect(router.getFetcher(key)).toBe(IDLE_FETCHER);\n      expect(fetcherData.get(key)).toBe(\"DATA\");\n\n      expect(router._internalFetchControllers.size).toBe(0);\n    });\n\n    it(\"loader fetch\", async () => {\n      let t = initializeTest({\n        url: \"/foo\",\n        hydrationData: { loaderData: { root: \"ROOT\", foo: \"FOO\" } },\n      });\n\n      let A = await t.fetch(\"/foo\");\n      expect(A.fetcher.state).toBe(\"loading\");\n\n      await A.loaders.foo.resolve(\"A DATA\");\n      expect(A.fetcher.state).toBe(\"idle\");\n      expect(A.fetcher.data).toBe(\"A DATA\");\n    });\n\n    it(\"loader re-fetch\", async () => {\n      let t = initializeTest({\n        url: \"/foo\",\n        hydrationData: { loaderData: { root: \"ROOT\", foo: \"FOO\" } },\n      });\n      let key = \"key\";\n\n      let A = await t.fetch(\"/foo\", key);\n      await A.loaders.foo.resolve(\"A DATA\");\n      expect(A.fetcher.state).toBe(\"idle\");\n      expect(A.fetcher.data).toBe(\"A DATA\");\n\n      let B = await t.fetch(\"/foo\", key);\n      expect(B.fetcher.state).toBe(\"loading\");\n      expect(B.fetcher.data).toBe(\"A DATA\");\n\n      await B.loaders.foo.resolve(\"B DATA\");\n      expect(B.fetcher.state).toBe(\"idle\");\n      expect(B.fetcher.data).toBe(\"B DATA\");\n\n      expect(A.fetcher).toEqual(B.fetcher);\n    });\n\n    it(\"loader submission fetch\", async () => {\n      let t = initializeTest({\n        url: \"/foo\",\n        hydrationData: { loaderData: { root: \"ROOT\", foo: \"FOO\" } },\n      });\n\n      let A = await t.fetch(\"/foo\", {\n        formMethod: \"get\",\n        formData: createFormData({ key: \"value\" }),\n      });\n      expect(A.fetcher.state).toBe(\"loading\");\n      expect(A.fetcher.formMethod).toBe(\"GET\");\n      expect(A.fetcher.formAction).toBe(\"/foo\");\n      expect(A.fetcher.formData).toEqual(createFormData({ key: \"value\" }));\n      expect(A.fetcher.formEncType).toBe(\"application/x-www-form-urlencoded\");\n      expect(\n        new URL(\n          A.loaders.foo.stub.mock.calls[0][0].request.url,\n        ).searchParams.toString(),\n      ).toBe(\"key=value\");\n\n      await A.loaders.foo.resolve(\"A DATA\");\n      expect(A.fetcher.state).toBe(\"idle\");\n      expect(A.fetcher.data).toBe(\"A DATA\");\n    });\n\n    it(\"loader submission re-fetch\", async () => {\n      let t = initializeTest({\n        url: \"/foo\",\n        hydrationData: { loaderData: { root: \"ROOT\", foo: \"FOO\" } },\n      });\n      let key = \"key\";\n\n      let A = await t.fetch(\"/foo\", key, {\n        formMethod: \"get\",\n        formData: createFormData({ key: \"value\" }),\n      });\n      expect(A.fetcher.state).toBe(\"loading\");\n      await A.loaders.foo.resolve(\"A DATA\");\n      expect(A.fetcher.state).toBe(\"idle\");\n      expect(A.fetcher.data).toBe(\"A DATA\");\n\n      let B = await t.fetch(\"/foo\", key, {\n        formMethod: \"get\",\n        formData: createFormData({ key: \"value\" }),\n      });\n      expect(B.fetcher.state).toBe(\"loading\");\n      expect(B.fetcher.data).toBe(\"A DATA\");\n\n      await B.loaders.foo.resolve(\"B DATA\");\n      expect(A.fetcher.state).toBe(\"idle\");\n      expect(A.fetcher.data).toBe(\"B DATA\");\n    });\n\n    it(\"action fetch\", async () => {\n      let t = initializeTest({\n        url: \"/foo\",\n        hydrationData: {\n          loaderData: { root: \"ROOT\", foo: \"FOO\" },\n        },\n      });\n\n      let A = await t.fetch(\"/foo\", {\n        formMethod: \"post\",\n        formData: createFormData({ key: \"value\" }),\n      });\n      expect(A.fetcher.state).toBe(\"submitting\");\n\n      await A.actions.foo.resolve(\"A ACTION\");\n      expect(A.fetcher.state).toBe(\"loading\");\n      expect(A.fetcher.data).toBe(\"A ACTION\");\n\n      await A.loaders.root.resolve(\"ROOT DATA\");\n      expect(A.fetcher.state).toBe(\"loading\");\n      expect(A.fetcher.data).toBe(\"A ACTION\");\n\n      await A.loaders.foo.resolve(\"A DATA\");\n      expect(A.fetcher.state).toBe(\"idle\");\n      expect(A.fetcher.data).toBe(\"A ACTION\");\n      expect(t.router.state.loaderData).toEqual({\n        root: \"ROOT DATA\",\n        foo: \"A DATA\",\n      });\n    });\n\n    it(\"action re-fetch\", async () => {\n      let t = initializeTest({\n        url: \"/foo\",\n        hydrationData: { loaderData: { root: \"ROOT\", foo: \"FOO\" } },\n      });\n      let key = \"key\";\n\n      let A = await t.fetch(\"/foo\", key, {\n        formMethod: \"post\",\n        formData: createFormData({ key: \"value\" }),\n      });\n      expect(A.fetcher.state).toBe(\"submitting\");\n\n      await A.actions.foo.resolve(\"A ACTION\");\n      expect(A.fetcher.state).toBe(\"loading\");\n      expect(A.fetcher.data).toBe(\"A ACTION\");\n\n      await A.loaders.root.resolve(\"ROOT DATA\");\n      await A.loaders.foo.resolve(\"A DATA\");\n      expect(A.fetcher.state).toBe(\"idle\");\n      expect(A.fetcher.data).toBe(\"A ACTION\");\n\n      let B = await t.fetch(\"/foo\", key, {\n        formMethod: \"post\",\n        formData: createFormData({ key: \"value\" }),\n      });\n      expect(B.fetcher.state).toBe(\"submitting\");\n      expect(B.fetcher.data).toBe(\"A ACTION\");\n\n      await B.actions.foo.resolve(\"B ACTION\");\n      await B.loaders.root.resolve(\"ROOT DATA*\");\n      await B.loaders.foo.resolve(\"A DATA*\");\n      expect(B.fetcher.state).toBe(\"idle\");\n      expect(B.fetcher.data).toBe(\"B ACTION\");\n    });\n  });\n\n  describe(\"fetcher removal\", () => {\n    it(\"gives an idle fetcher before submission\", async () => {\n      let t = initializeTest();\n      let fetcher = t.router.getFetcher(\"randomKey\");\n      expect(fetcher).toEqual(IDLE_FETCHER);\n    });\n\n    it(\"removes fetchers\", async () => {\n      let t = initializeTest();\n      let A = await t.fetch(\"/foo\");\n      await A.loaders.foo.resolve(\"A\");\n      expect(t.fetchers[A.key].data).toBe(\"A\");\n\n      t.router.deleteFetcher(A.key);\n      expect(t.router.getFetcher(A.key)).toEqual(IDLE_FETCHER);\n    });\n\n    it(\"cleans up abort controllers\", async () => {\n      let t = initializeTest();\n      let A = await t.fetch(\"/foo\");\n      expect(t.router._internalFetchControllers.size).toBe(1);\n      let B = await t.fetch(\"/bar\");\n      expect(t.router._internalFetchControllers.size).toBe(2);\n      await A.loaders.foo.resolve(null);\n      expect(t.router._internalFetchControllers.size).toBe(1);\n      await B.loaders.bar.resolve(null);\n      expect(t.router._internalFetchControllers.size).toBe(0);\n    });\n\n    it(\"uses current page matches and URL when reloading routes after submissions\", async () => {\n      let pagePathname = \"/foo\";\n      let t = initializeTest({\n        url: pagePathname,\n        hydrationData: {\n          loaderData: { root: \"ROOT\", foo: \"FOO\" },\n        },\n      });\n\n      let A = await t.fetch(\"/bar\", {\n        formMethod: \"post\",\n        formData: createFormData({ key: \"value\" }),\n      });\n      await A.actions.bar.resolve(\"ACTION\");\n      await A.loaders.root.resolve(\"ROOT DATA\");\n      await A.loaders.foo.resolve(\"FOO DATA\");\n      expect(t.router.state.loaderData).toEqual({\n        root: \"ROOT DATA\",\n        foo: \"FOO DATA\",\n      });\n      expect(A.loaders.root.stub).toHaveBeenCalledWith({\n        params: {},\n        request: new Request(\"http://localhost/foo\", {\n          signal: A.loaders.root.stub.mock.calls[0][0].request.signal,\n        }),\n        unstable_pattern: expect.any(String),\n        context: {},\n      });\n    });\n  });\n\n  describe(\"fetcher removal \", () => {\n    it(\"loading fetchers persist until completion\", async () => {\n      let t = initializeTest();\n\n      let key = \"key\";\n      t.router.getFetcher(key); // mount\n      expect(t.router.state.fetchers.size).toBe(0);\n\n      let A = await t.fetch(\"/foo\", key);\n      expect(t.router.state.fetchers.size).toBe(1);\n      expect(t.router.getFetcher(key)?.state).toBe(\"loading\");\n\n      t.router.deleteFetcher(key); // unmount\n      expect(t.router.state.fetchers.size).toBe(1);\n      expect(t.router.getFetcher(key)?.state).toBe(\"loading\");\n\n      // Cleaned up on completion\n      await A.loaders.foo.resolve(\"FOO\");\n      expect(t.router.state.fetchers.size).toBe(0);\n    });\n\n    it(\"fetchers removed from data layer upon unmount\", async () => {\n      let t = initializeTest();\n\n      let subscriber = jest.fn();\n      t.router.subscribe(subscriber);\n\n      let key = \"key\";\n      t.router.getFetcher(key); // mount\n      expect(t.router.state.fetchers.size).toBe(0);\n\n      let A = await t.fetch(\"/foo\", key);\n      expect(t.router.state.fetchers.size).toBe(1);\n      expect(t.router.state.fetchers.get(key)?.state).toBe(\"loading\");\n      expect(subscriber.mock.calls.length).toBe(1);\n      expect(subscriber.mock.calls[0][0].fetchers.get(\"key\").state).toBe(\n        \"loading\",\n      );\n      subscriber.mockReset();\n\n      await A.loaders.foo.resolve(\"FOO\");\n      expect(t.router.state.fetchers.size).toBe(0);\n      expect(subscriber.mock.calls.length).toBe(1);\n      // Fetcher removed from router state upon return to idle\n      expect(subscriber.mock.calls[0][0].fetchers.size).toBe(0);\n      // But still mounted so not deleted from data layer yet\n      expect(subscriber.mock.calls[0][1].deletedFetchers.length).toBe(0);\n      subscriber.mockReset();\n\n      t.router.deleteFetcher(key); // unmount\n      expect(t.router.state.fetchers.size).toBe(0);\n      expect(subscriber.mock.calls.length).toBe(1);\n      expect(subscriber.mock.calls[0][0].fetchers.size).toBe(0);\n      // Unmounted so can be deleted from data layer\n      expect(subscriber.mock.calls[0][1].deletedFetchers).toEqual([\"key\"]);\n      subscriber.mockReset();\n    });\n\n    it(\"submitting fetchers persist until completion when removed during submitting phase\", async () => {\n      let t = initializeTest();\n\n      let key = \"key\";\n      expect(t.router.state.fetchers.size).toBe(0);\n\n      t.router.getFetcher(key); // mount\n      let A = await t.fetch(\"/foo\", key, {\n        formMethod: \"post\",\n        formData: createFormData({}),\n      });\n      expect(t.router.state.fetchers.size).toBe(1);\n      expect(t.router.getFetcher(key)?.state).toBe(\"submitting\");\n\n      t.router.deleteFetcher(key); // unmount\n      expect(t.router.state.fetchers.size).toBe(1);\n      expect(t.router.getFetcher(key)?.state).toBe(\"submitting\");\n\n      await A.actions.foo.resolve(\"FOO\");\n      expect(t.router.state.fetchers.size).toBe(1);\n      expect(t.router.getFetcher(key)?.state).toBe(\"loading\");\n\n      // Cleaned up on completion\n      await A.loaders.root.resolve(\"ROOT*\");\n      expect(t.router.state.fetchers.size).toBe(1);\n      expect(t.router.getFetcher(key)?.state).toBe(\"loading\");\n\n      await A.loaders.index.resolve(\"INDEX*\");\n      expect(t.router.state.fetchers.size).toBe(0);\n    });\n\n    it(\"submitting fetchers persist until completion when removed during loading phase\", async () => {\n      let t = initializeTest();\n\n      let key = \"key\";\n      t.router.getFetcher(key); // mount\n      expect(t.router.state.fetchers.size).toBe(0);\n\n      let A = await t.fetch(\"/foo\", key, {\n        formMethod: \"post\",\n        formData: createFormData({}),\n      });\n      expect(t.router.state.fetchers.size).toBe(1);\n      expect(t.router.getFetcher(key)?.state).toBe(\"submitting\");\n\n      await A.actions.foo.resolve(\"FOO\");\n      expect(t.router.state.fetchers.size).toBe(1);\n      expect(t.router.getFetcher(key)?.state).toBe(\"loading\");\n\n      t.router.deleteFetcher(key); // unmount\n      expect(t.router.state.fetchers.size).toBe(1);\n      expect(t.router.getFetcher(key)?.state).toBe(\"loading\");\n\n      // Cleaned up on completion\n      await A.loaders.root.resolve(\"ROOT*\");\n      expect(t.router.state.fetchers.size).toBe(1);\n      expect(t.router.getFetcher(key)?.state).toBe(\"loading\");\n\n      await A.loaders.index.resolve(\"INDEX*\");\n      expect(t.router.state.fetchers.size).toBe(0);\n    });\n\n    it(\"unmounted fetcher.load errors/redirects should not be processed\", async () => {\n      let t = initializeTest();\n\n      t.router.getFetcher(\"a\"); // mount\n      let A = await t.fetch(\"/foo\", \"a\");\n      t.router.deleteFetcher(\"a\"); //unmount\n      await A.loaders.foo.reject(\"ERROR\");\n      expect(t.router.state.fetchers.size).toBe(0);\n      expect(t.router.state.errors).toBe(null);\n\n      t.router.getFetcher(\"b\"); // mount\n      let B = await t.fetch(\"/bar\", \"b\");\n      t.router.deleteFetcher(\"b\"); //unmount\n      await B.loaders.bar.redirect(\"/baz\");\n      expect(t.router.state.fetchers.size).toBe(0);\n      expect(t.router.state.navigation).toBe(IDLE_NAVIGATION);\n      expect(t.router.state.location.pathname).toBe(\"/\");\n    });\n\n    it(\"unmounted fetcher.submit errors/redirects should not be processed\", async () => {\n      let t = initializeTest();\n\n      t.router.getFetcher(\"a\"); // mount\n      let A = await t.fetch(\"/foo\", \"a\", {\n        formMethod: \"post\",\n        formData: createFormData({}),\n      });\n      t.router.deleteFetcher(\"a\"); //unmount\n      await A.actions.foo.reject(\"ERROR\");\n      expect(t.router.state.fetchers.size).toBe(0);\n      expect(t.router.state.errors).toBe(null);\n\n      t.router.getFetcher(\"b\"); // mount\n      let B = await t.fetch(\"/bar\", \"b\", {\n        formMethod: \"post\",\n        formData: createFormData({}),\n      });\n      t.router.deleteFetcher(\"b\"); //unmount\n      await B.actions.bar.redirect(\"/baz\");\n      expect(t.router.state.fetchers.size).toBe(0);\n      expect(t.router.state.navigation).toBe(IDLE_NAVIGATION);\n      expect(t.router.state.location.pathname).toBe(\"/\");\n    });\n  });\n\n  describe(\"fetcher error states (4xx Response)\", () => {\n    it(\"loader fetch\", async () => {\n      let t = initializeTest();\n      let A = await t.fetch(\"/foo\");\n      await A.loaders.foo.reject(new Response(null, { status: 400 }));\n      expect(A.fetcher).toEqual(IDLE_FETCHER);\n      expect(t.router.state.errors).toEqual({\n        root: new ErrorResponseImpl(400, undefined, \"\"),\n      });\n    });\n\n    it(\"loader submission fetch\", async () => {\n      let t = initializeTest();\n      let A = await t.fetch(\"/foo?key=value\", {\n        formMethod: \"get\",\n        formData: createFormData({ key: \"value\" }),\n      });\n      await A.loaders.foo.reject(new Response(null, { status: 400 }));\n      expect(A.fetcher).toEqual(IDLE_FETCHER);\n      expect(t.router.state.errors).toEqual({\n        root: new ErrorResponseImpl(400, undefined, \"\"),\n      });\n    });\n\n    it(\"action fetch\", async () => {\n      let t = initializeTest();\n      let A = await t.fetch(\"/foo\", {\n        formMethod: \"post\",\n        formData: createFormData({ key: \"value\" }),\n      });\n      await A.actions.foo.reject(new Response(null, { status: 400 }));\n      expect(A.fetcher).toEqual(IDLE_FETCHER);\n      expect(t.router.state.errors).toEqual({\n        root: new ErrorResponseImpl(400, undefined, \"\"),\n      });\n    });\n\n    it(\"action fetch without action handler\", async () => {\n      let t = setup({\n        routes: [\n          {\n            id: \"root\",\n            path: \"/\",\n            hasErrorBoundary: true,\n            children: [\n              {\n                id: \"index\",\n                index: true,\n              },\n            ],\n          },\n        ],\n      });\n      let A = await t.fetch(\"/\", {\n        formMethod: \"post\",\n        formData: createFormData({ key: \"value\" }),\n      });\n      expect(A.fetcher).toEqual(IDLE_FETCHER);\n      expect(t.router.state.errors).toEqual({\n        root: new ErrorResponseImpl(\n          405,\n          \"Method Not Allowed\",\n          new Error(\n            'You made a POST request to \"/\" but did not provide an `action` ' +\n              'for route \"root\", so there is no way to handle the request.',\n          ),\n          true,\n        ),\n      });\n    });\n\n    it(\"action fetch with invalid body (json)\", async () => {\n      let t = setup({\n        routes: [\n          {\n            id: \"root\",\n            path: \"/\",\n            hasErrorBoundary: true,\n          },\n        ],\n      });\n      let A = await t.fetch(\"/\", {\n        formMethod: \"post\",\n        body: \"not json\",\n        formEncType: \"application/json\",\n      });\n      expect(A.fetcher).toEqual(IDLE_FETCHER);\n      expect(t.router.state.errors).toEqual({\n        root: new ErrorResponseImpl(\n          400,\n          \"Bad Request\",\n          new Error(\"Unable to encode submission body\"),\n          true,\n        ),\n      });\n    });\n\n    it(\"handles fetcher errors at contextual route boundaries\", async () => {\n      let t = setup({\n        routes: [\n          {\n            id: \"root\",\n            path: \"/\",\n            hasErrorBoundary: true,\n            children: [\n              {\n                id: \"wit\",\n                path: \"wit\",\n                loader: true,\n                hasErrorBoundary: true,\n              },\n              {\n                id: \"witout\",\n                path: \"witout\",\n                loader: true,\n              },\n              {\n                id: \"error\",\n                path: \"error\",\n                loader: true,\n              },\n            ],\n          },\n        ],\n      });\n\n      // If the routeId is not an active match, errors bubble to the root\n      let A = await t.fetch(\"/error\", \"key1\", \"wit\");\n      await A.loaders.error.reject(new Error(\"Kaboom!\"));\n      expect(t.router.getFetcher(\"key1\")).toEqual(IDLE_FETCHER);\n      expect(t.router.state.errors).toEqual({\n        root: new Error(\"Kaboom!\"),\n      });\n\n      await t.fetch(\"/not-found\", \"key2\", \"wit\");\n      expect(t.router.getFetcher(\"key2\")).toEqual(IDLE_FETCHER);\n      expect(t.router.state.errors).toEqual({\n        root: new ErrorResponseImpl(\n          404,\n          \"Not Found\",\n          new Error('No route matches URL \"/not-found\"'),\n          true,\n        ),\n      });\n\n      // Navigate to /wit and trigger errors, handled at the wit boundary\n      let B = await t.navigate(\"/wit\");\n      await B.loaders.wit.resolve(\"WIT\");\n\n      let C = await t.fetch(\"/error\", \"key3\", \"wit\");\n      await C.loaders.error.reject(new Error(\"Kaboom!\"));\n      expect(t.router.getFetcher(\"key3\")).toEqual(IDLE_FETCHER);\n      expect(t.router.state.errors).toEqual({\n        wit: new Error(\"Kaboom!\"),\n      });\n\n      await t.fetch(\"/not-found\", \"key4\", \"wit\", {\n        formMethod: \"post\",\n        formData: createFormData({ key: \"value\" }),\n      });\n      expect(t.router.getFetcher(\"key4\")).toEqual(IDLE_FETCHER);\n      expect(t.router.state.errors).toEqual({\n        wit: new ErrorResponseImpl(\n          404,\n          \"Not Found\",\n          new Error('No route matches URL \"/not-found\"'),\n          true,\n        ),\n      });\n\n      await t.fetch(\"/not-found\", \"key5\", \"wit\");\n      expect(t.router.getFetcher(\"key5\")).toEqual(IDLE_FETCHER);\n      expect(t.router.state.errors).toEqual({\n        wit: new ErrorResponseImpl(\n          404,\n          \"Not Found\",\n          new Error('No route matches URL \"/not-found\"'),\n          true,\n        ),\n      });\n\n      // Navigate to /witout and fetch a 404, handled at the root boundary\n      let D = await t.navigate(\"/witout\");\n      await D.loaders.witout.resolve(\"WITOUT\");\n\n      let E = await t.fetch(\"/error\", \"key6\", \"witout\");\n      await E.loaders.error.reject(new Error(\"Kaboom!\"));\n      expect(t.router.getFetcher(\"key6\")).toEqual(IDLE_FETCHER);\n      expect(t.router.state.errors).toEqual({\n        root: new Error(\"Kaboom!\"),\n      });\n\n      await t.fetch(\"/not-found\", \"key7\", \"witout\");\n      expect(t.router.getFetcher(\"key7\")).toEqual(IDLE_FETCHER);\n      expect(t.router.state.errors).toEqual({\n        root: new ErrorResponseImpl(\n          404,\n          \"Not Found\",\n          new Error('No route matches URL \"/not-found\"'),\n          true,\n        ),\n      });\n    });\n  });\n\n  describe(\"fetcher error states (Error)\", () => {\n    it(\"loader fetch\", async () => {\n      let t = initializeTest();\n      let A = await t.fetch(\"/foo\");\n      await A.loaders.foo.reject(new Error(\"Kaboom!\"));\n      expect(A.fetcher).toEqual(IDLE_FETCHER);\n      expect(t.router.state.errors).toEqual({\n        root: new Error(\"Kaboom!\"),\n      });\n    });\n\n    it(\"loader submission fetch\", async () => {\n      let t = initializeTest();\n      let A = await t.fetch(\"/foo?key=value\", {\n        formMethod: \"get\",\n        formData: createFormData({ key: \"value\" }),\n      });\n      await A.loaders.foo.reject(new Error(\"Kaboom!\"));\n      expect(A.fetcher).toEqual(IDLE_FETCHER);\n      expect(t.router.state.errors).toEqual({\n        root: new Error(\"Kaboom!\"),\n      });\n    });\n\n    it(\"action fetch\", async () => {\n      let t = initializeTest();\n      let A = await t.fetch(\"/foo\", {\n        formMethod: \"post\",\n        formData: createFormData({ key: \"value\" }),\n      });\n      await A.actions.foo.reject(new Error(\"Kaboom!\"));\n      expect(A.fetcher).toEqual(IDLE_FETCHER);\n      expect(t.router.state.errors).toEqual({\n        root: new Error(\"Kaboom!\"),\n      });\n    });\n  });\n\n  describe(\"fetcher redirects\", () => {\n    it(\"loader fetch\", async () => {\n      let t = initializeTest();\n      let key = t.router.state.location.key;\n\n      let A = await t.fetch(\"/foo\");\n\n      let B = await A.loaders.foo.redirect(\"/bar\");\n      expect(t.router.getFetcher(A.key)).toEqual(A.fetcher);\n      expect(t.router.state.navigation.state).toBe(\"loading\");\n      expect(t.router.state.navigation.location?.pathname).toBe(\"/bar\");\n\n      await B.loaders.bar.resolve(\"BAR\");\n      expect(t.router.state.navigation.state).toBe(\"idle\");\n      expect(t.router.state.historyAction).toBe(\"PUSH\");\n      expect(t.router.state.location?.pathname).toBe(\"/bar\");\n\n      // Back button should take us back to location that triggered the fetch\n      // redirect\n      let C = await t.navigate(-1);\n      await C.loaders.index.resolve(\"INDEX\");\n      expect(t.router.state.location.pathname).toBe(\"/\");\n      expect(t.router.state.location.key).toBe(key);\n    });\n\n    it(\"loader submission fetch\", async () => {\n      let t = initializeTest();\n      let key = t.router.state.location.key;\n      let A = await t.fetch(\"/foo?key=value\", {\n        formMethod: \"get\",\n        formData: createFormData({ key: \"value\" }),\n      });\n\n      let B = await A.loaders.foo.redirect(\"/bar\");\n      expect(t.router.getFetcher(A.key)).toEqual(A.fetcher);\n      expect(t.router.state.navigation.state).toBe(\"loading\");\n      expect(t.router.state.navigation.location?.pathname).toBe(\"/bar\");\n\n      await B.loaders.bar.resolve(\"BAR\");\n      expect(t.router.state.navigation.state).toBe(\"idle\");\n      expect(t.router.state.historyAction).toBe(\"PUSH\");\n      expect(t.router.state.location?.pathname).toBe(\"/bar\");\n\n      // Back button should take us back to location that triggered the fetch\n      // redirect\n      let C = await t.navigate(-1);\n      await C.loaders.index.resolve(\"INDEX\");\n      expect(t.router.state.location.pathname).toBe(\"/\");\n      expect(t.router.state.location.key).toBe(key);\n    });\n\n    it(\"action fetch\", async () => {\n      let t = initializeTest();\n      let key = t.router.state.location.key;\n\n      let A = await t.fetch(\"/foo\", {\n        formMethod: \"post\",\n        formData: createFormData({ key: \"value\" }),\n      });\n      expect(A.fetcher.state).toBe(\"submitting\");\n      let AR = await A.actions.foo.redirect(\"/bar\");\n      expect(A.fetcher.state).toBe(\"loading\");\n      expect(t.router.state.navigation).toMatchObject({\n        state: \"loading\",\n        location: {\n          pathname: \"/bar\",\n        },\n        // Fetcher action redirect should not proxy the fetcher submission\n        // onto the loading navigation\n        formAction: undefined,\n        formData: undefined,\n        formEncType: undefined,\n        formMethod: undefined,\n      });\n      await AR.loaders.root.resolve(\"ROOT*\");\n      await AR.loaders.bar.resolve(\"stuff\");\n      expect(A.fetcher).toEqual({\n        data: undefined,\n        state: \"idle\",\n        formMethod: undefined,\n        formAction: undefined,\n        formEncType: undefined,\n        formData: undefined,\n      });\n      expect(t.router.state.historyAction).toBe(\"PUSH\");\n      expect(t.router.state.location.pathname).toBe(\"/bar\");\n      // Root loader should be re-called after fetchActionRedirect\n      expect(t.router.state.loaderData).toEqual({\n        root: \"ROOT*\",\n        bar: \"stuff\",\n      });\n\n      // Back button should take us back to location that triggered the fetch\n      // redirect\n      let C = await t.navigate(-1);\n      await C.loaders.index.resolve(\"INDEX\");\n      expect(t.router.state.location.pathname).toBe(\"/\");\n      expect(t.router.state.location.key).toBe(key);\n    });\n\n    test(\"handles loader redirects after a fetcher submission\", async () => {\n      let t = initializeTest();\n\n      let A = await t.navigate(\"/foo\");\n      await A.loaders.foo.resolve(\"FOO\");\n      expect(t.router.state).toMatchObject({\n        location: { pathname: \"/foo\" },\n        navigation: { state: \"idle\" },\n        loaderData: { root: \"ROOT\", foo: \"FOO\" },\n      });\n\n      let key = \"key\";\n      let B = await t.fetch(\"/bar\", key, {\n        formMethod: \"post\",\n        formData: createFormData({}),\n      });\n      await B.actions.bar.resolve(\"ACTION\");\n      expect(t.fetchers[key]).toMatchObject({\n        state: \"loading\",\n        data: \"ACTION\",\n      });\n      await B.loaders.root.resolve(\"ROOT*\");\n\n      let C = await B.loaders.foo.redirect(\"/\");\n      await C.loaders.root.resolve(\"ROOT**\");\n      await C.loaders.index.resolve(\"INDEX*\");\n      expect(t.router.state).toMatchObject({\n        location: { pathname: \"/\" },\n        navigation: { state: \"idle\" },\n        loaderData: { root: \"ROOT**\", index: \"INDEX*\" },\n      });\n      expect(t.fetchers[key]).toMatchObject({\n        state: \"idle\",\n        data: \"ACTION\",\n      });\n    });\n  });\n\n  describe(\"fetcher resubmissions/re-gets\", () => {\n    it(\"aborts re-gets\", async () => {\n      let t = initializeTest();\n      let key = \"KEY\";\n      let A = await t.fetch(\"/foo\", key);\n      let B = await t.fetch(\"/foo\", key);\n      await A.loaders.foo.resolve(null);\n      let C = await t.fetch(\"/foo\", key);\n      await B.loaders.foo.resolve(null);\n      await C.loaders.foo.resolve(null);\n      expect(A.loaders.foo.signal.aborted).toBe(true);\n      expect(B.loaders.foo.signal.aborted).toBe(true);\n      expect(C.loaders.foo.signal.aborted).toBe(false);\n    });\n\n    it(\"aborts re-get-submissions\", async () => {\n      let t = initializeTest();\n      let key = \"KEY\";\n      let A = await t.fetch(\"/foo\", key, {\n        formMethod: \"get\",\n        formData: createFormData({ key: \"value\" }),\n      });\n      let B = await t.fetch(\"/foo\", key, {\n        formMethod: \"get\",\n        formData: createFormData({ key: \"value\" }),\n      });\n      let C = await t.fetch(\"/foo\", key);\n      expect(A.loaders.foo.signal.aborted).toBe(true);\n      expect(B.loaders.foo.signal.aborted).toBe(true);\n      await C.loaders.foo.resolve(null);\n    });\n\n    it(\"aborts resubmissions action call\", async () => {\n      let t = initializeTest();\n      let key = \"KEY\";\n      let A = await t.fetch(\"/foo\", key, {\n        formMethod: \"post\",\n        formData: createFormData({ key: \"value\" }),\n      });\n      let B = await t.fetch(\"/foo\", key, {\n        formMethod: \"post\",\n        formData: createFormData({ key: \"value\" }),\n      });\n      let C = await t.fetch(\"/foo\", key, {\n        formMethod: \"post\",\n        formData: createFormData({ key: \"value\" }),\n      });\n      expect(A.actions.foo.signal.aborted).toBe(true);\n      expect(B.actions.foo.signal.aborted).toBe(true);\n      await C.actions.foo.resolve(null);\n      await C.loaders.root.resolve(null);\n      await C.loaders.index.resolve(null);\n    });\n\n    it(\"aborts resubmissions loader call\", async () => {\n      let t = initializeTest({\n        url: \"/foo\",\n        hydrationData: { loaderData: { root: \"ROOT\", foo: \"FOO\" } },\n      });\n      let key = \"KEY\";\n      let A = await t.fetch(\"/foo\", key, {\n        formMethod: \"post\",\n        formData: createFormData({ key: \"value\" }),\n      });\n      await A.actions.foo.resolve(\"A ACTION\");\n      let C = await t.fetch(\"/foo\", key, {\n        formMethod: \"post\",\n        formData: createFormData({ key: \"value\" }),\n      });\n      expect(A.loaders.foo.signal.aborted).toBe(true);\n      await C.actions.foo.resolve(null);\n      await C.loaders.root.resolve(null);\n      await C.loaders.foo.resolve(null);\n    });\n\n    describe(`\n      A) POST |--|--XXX\n      B) POST       |----XXX|XXX\n      C) POST            |----|---O\n    `, () => {\n      it(\"aborts A load, ignores A resolve, aborts B action\", async () => {\n        let t = initializeTest({\n          url: \"/foo\",\n          hydrationData: { loaderData: { root: \"ROOT\", foo: \"FOO\" } },\n        });\n        let key = \"KEY\";\n\n        let A = await t.fetch(\"/foo\", key, {\n          formMethod: \"post\",\n          formData: createFormData({ key: \"value\" }),\n        });\n        await A.actions.foo.resolve(\"A ACTION\");\n        expect(t.fetchers[key].data).toBe(\"A ACTION\");\n\n        let B = await t.fetch(\"/foo\", key, {\n          formMethod: \"post\",\n          formData: createFormData({ key: \"value\" }),\n        });\n        expect(A.loaders.foo.signal.aborted).toBe(true);\n        expect(t.fetchers[key].data).toBe(\"A ACTION\");\n\n        await A.loaders.root.resolve(\"A ROOT LOADER\");\n        await A.loaders.foo.resolve(\"A LOADER\");\n        expect(t.router.state.loaderData.foo).toBe(\"FOO\");\n\n        let C = await t.fetch(\"/foo\", key, {\n          formMethod: \"post\",\n          formData: createFormData({ key: \"value\" }),\n        });\n        expect(B.actions.foo.signal.aborted).toBe(true);\n\n        await B.actions.foo.resolve(\"B ACTION\");\n        expect(t.fetchers[key].data).toBe(\"A ACTION\");\n\n        await C.actions.foo.resolve(\"C ACTION\");\n        expect(t.fetchers[key].data).toBe(\"C ACTION\");\n\n        await B.loaders.root.resolve(\"B ROOT LOADER\");\n        await B.loaders.foo.resolve(\"B LOADER\");\n        expect(t.router.state.loaderData.foo).toBe(\"FOO\");\n\n        await C.loaders.root.resolve(\"C ROOT LOADER\");\n        await C.loaders.foo.resolve(\"C LOADER\");\n        expect(t.fetchers[key].data).toBe(\"C ACTION\");\n        expect(t.router.state.loaderData.foo).toBe(\"C LOADER\");\n      });\n    });\n\n    describe(`\n      A) k1 |----|----X\n      B) k2   |----|-----O\n      C) k1           |-----|---O\n    `, () => {\n      it(\"aborts A load, commits B and C loads\", async () => {\n        let t = initializeTest({\n          url: \"/foo\",\n          hydrationData: { loaderData: { root: \"ROOT\", foo: \"FOO\" } },\n        });\n        let k1 = \"1\";\n        let k2 = \"2\";\n\n        let Ak1 = await t.fetch(\"/foo\", k1, {\n          formMethod: \"post\",\n          formData: createFormData({ key: \"value\" }),\n        });\n        let Bk2 = await t.fetch(\"/foo\", k2, {\n          formMethod: \"post\",\n          formData: createFormData({ key: \"value\" }),\n        });\n\n        await Ak1.actions.foo.resolve(\"A ACTION\");\n        await Bk2.actions.foo.resolve(\"B ACTION\");\n        expect(t.fetchers[k2].data).toBe(\"B ACTION\");\n\n        let Ck1 = await t.fetch(\"/foo\", k1, {\n          formMethod: \"post\",\n          formData: createFormData({ key: \"value\" }),\n        });\n        expect(Ak1.loaders.foo.signal.aborted).toBe(true);\n\n        await Ak1.loaders.root.resolve(\"A ROOT LOADER\");\n        await Ak1.loaders.foo.resolve(\"A LOADER\");\n        expect(t.router.state.loaderData.foo).toBe(\"FOO\");\n\n        await Bk2.loaders.root.resolve(\"B ROOT LOADER\");\n        await Bk2.loaders.foo.resolve(\"B LOADER\");\n        expect(Ck1.actions.foo.signal.aborted).toBe(false);\n        expect(t.router.state.loaderData.foo).toBe(\"B LOADER\");\n\n        await Ck1.actions.foo.resolve(\"C ACTION\");\n        await Ck1.loaders.root.resolve(\"C ROOT LOADER\");\n        await Ck1.loaders.foo.resolve(\"C LOADER\");\n\n        expect(t.fetchers[k1].data).toBe(\"C ACTION\");\n        expect(t.router.state.loaderData.foo).toBe(\"C LOADER\");\n      });\n    });\n  });\n\n  describe(\"multiple fetcher action reloads\", () => {\n    describe(`\n      A) POST /foo |---[A]------O\n      B) POST /foo   |-----[A,B]---O\n    `, () => {\n      it(\"commits A, commits B\", async () => {\n        let t = initializeTest({\n          url: \"/foo\",\n          hydrationData: { loaderData: { root: \"ROOT\", foo: \"FOO\" } },\n        });\n        let A = await t.fetch(\"/foo\", {\n          formMethod: \"post\",\n          formData: createFormData({ key: \"value\" }),\n        });\n        let B = await t.fetch(\"/foo\", {\n          formMethod: \"post\",\n          formData: createFormData({ key: \"value\" }),\n        });\n        await A.actions.foo.resolve(\"A action\");\n        await B.actions.foo.resolve(\"B action\");\n\n        await A.loaders.root.resolve(\"A root\");\n        await A.loaders.foo.resolve(\"A loader\");\n        expect(t.router.state.loaderData).toEqual({\n          root: \"A root\",\n          foo: \"A loader\",\n        });\n\n        await B.loaders.root.resolve(\"A,B root\");\n        await B.loaders.foo.resolve(\"A,B loader\");\n        expect(t.router.state.loaderData).toEqual({\n          root: \"A,B root\",\n          foo: \"A,B loader\",\n        });\n      });\n    });\n\n    describe(`\n      A) POST /foo |----🧤\n      B) POST /foo   |--X\n    `, () => {\n      it(\"catches A, persists boundary for B\", async () => {\n        let t = initializeTest({\n          url: \"/foo\",\n          hydrationData: { loaderData: { root: \"ROOT\", foo: \"FOO\" } },\n        });\n        let A = await t.fetch(\"/foo\", {\n          formMethod: \"post\",\n          formData: createFormData({ key: \"value\" }),\n        });\n        let B = await t.fetch(\"/foo\", {\n          formMethod: \"post\",\n          formData: createFormData({ key: \"value\" }),\n        });\n\n        await A.actions.foo.reject(new Response(null, { status: 400 }));\n        expect(t.router.state.errors).toEqual({\n          root: new ErrorResponseImpl(400, undefined, \"\"),\n        });\n\n        await B.actions.foo.resolve(\"B\");\n        expect(t.router.state.errors).toEqual({\n          root: new ErrorResponseImpl(400, undefined, \"\"),\n        });\n\n        await B.loaders.root.resolve(null);\n        await B.loaders.foo.resolve(null);\n      });\n    });\n\n    describe(`\n      A) POST /foo |----[A]-|\n      B) POST /foo   |------🧤\n    `, () => {\n      it(\"commits A, catches B\", async () => {\n        let t = initializeTest({\n          url: \"/foo\",\n          hydrationData: { loaderData: { root: \"ROOT\", foo: \"FOO\" } },\n        });\n        let A = await t.fetch(\"/foo\", {\n          formMethod: \"post\",\n          formData: createFormData({ key: \"value\" }),\n        });\n        let B = await t.fetch(\"/foo\", {\n          formMethod: \"post\",\n          formData: createFormData({ key: \"value\" }),\n        });\n\n        await A.actions.foo.resolve(\"A action\");\n        await A.loaders.root.resolve(\"A root\");\n        await A.loaders.foo.resolve(\"A loader\");\n        expect(t.router.state.loaderData).toEqual({\n          root: \"A root\",\n          foo: \"A loader\",\n        });\n\n        await B.actions.foo.reject(new Response(null, { status: 400 }));\n        expect(t.router.state.errors).toEqual({\n          root: new ErrorResponseImpl(400, undefined, \"\"),\n        });\n      });\n    });\n\n    describe(`\n      A) POST /foo |---[A]-------X\n      B) POST /foo   |----[A,B]--O\n    `, () => {\n      it(\"aborts A, commits B, sets A done\", async () => {\n        let t = initializeTest({\n          url: \"/foo\",\n          hydrationData: { loaderData: { root: \"ROOT\", foo: \"FOO\" } },\n        });\n        let A = await t.fetch(\"/foo\", {\n          formMethod: \"post\",\n          formData: createFormData({ key: \"value\" }),\n        });\n        let B = await t.fetch(\"/foo\", {\n          formMethod: \"post\",\n          formData: createFormData({ key: \"value\" }),\n        });\n        await A.actions.foo.resolve(\"A\");\n        await B.actions.foo.resolve(\"B\");\n\n        await B.loaders.root.resolve(\"A,B root\");\n        await B.loaders.foo.resolve(\"A,B\");\n        expect(t.router.state.loaderData).toEqual({\n          root: \"A,B root\",\n          foo: \"A,B\",\n        });\n        expect(A.loaders.foo.signal.aborted).toBe(true);\n        expect(A.fetcher.state).toBe(\"idle\");\n        expect(A.fetcher.data).toBe(\"A\");\n      });\n    });\n\n    describe(`\n      A) POST /foo |--------[B,A]---O\n      B) POST /foo   |--[B]-------O\n    `, () => {\n      it(\"commits B, commits A\", async () => {\n        let t = initializeTest({\n          url: \"/foo\",\n          hydrationData: { loaderData: { root: \"ROOT\", foo: \"FOO\" } },\n        });\n        let A = await t.fetch(\"/foo\", {\n          formMethod: \"post\",\n          formData: createFormData({ key: \"value\" }),\n        });\n        let B = await t.fetch(\"/foo\", {\n          formMethod: \"post\",\n          formData: createFormData({ key: \"value\" }),\n        });\n\n        await B.actions.foo.resolve(\"B action\");\n        await A.actions.foo.resolve(\"A action\");\n\n        await B.loaders.root.resolve(\"B root\");\n        await B.loaders.foo.resolve(\"B\");\n        expect(t.router.state.loaderData).toEqual({\n          root: \"B root\",\n          foo: \"B\",\n        });\n\n        await A.loaders.root.resolve(\"B,A root\");\n        await A.loaders.foo.resolve(\"B,A\");\n        expect(t.router.state.loaderData).toEqual({\n          root: \"B,A root\",\n          foo: \"B,A\",\n        });\n      });\n    });\n\n    describe(`\n      A) POST /foo |------|---O\n      B) POST /foo   |--|-----X\n    `, () => {\n      it(\"aborts B, commits A, sets B done\", async () => {\n        let t = initializeTest({\n          url: \"/foo\",\n          hydrationData: { loaderData: { root: \"ROOT\", foo: \"FOO\" } },\n        });\n\n        let A = await t.fetch(\"/foo\", {\n          formMethod: \"post\",\n          formData: createFormData({ key: \"value\" }),\n        });\n        let B = await t.fetch(\"/foo\", {\n          formMethod: \"post\",\n          formData: createFormData({ key: \"value\" }),\n        });\n\n        await B.actions.foo.resolve(\"B\");\n        await A.actions.foo.resolve(\"A\");\n\n        await A.loaders.root.resolve(\"B,A root\");\n        await A.loaders.foo.resolve(\"B,A\");\n        expect(t.router.state.loaderData).toEqual({\n          root: \"B,A root\",\n          foo: \"B,A\",\n        });\n        expect(B.loaders.foo.signal.aborted).toBe(true);\n        expect(B.fetcher.state).toBe(\"idle\");\n        expect(B.fetcher.data).toBe(\"B\");\n      });\n    });\n  });\n\n  describe(\"navigating with inflight fetchers\", () => {\n    describe(`\n      A) fetch POST |-------|--O\n      B) nav GET      |---O\n    `, () => {\n      it(\"does not abort A action or data reload\", async () => {\n        let t = initializeTest({\n          url: \"/foo\",\n          hydrationData: { loaderData: { root: \"ROOT\", foo: \"FOO\" } },\n        });\n\n        let A = await t.fetch(\"/foo\", {\n          formMethod: \"post\",\n          formData: createFormData({ key: \"value\" }),\n        });\n        let B = await t.navigate(\"/foo\");\n        expect(A.actions.foo.signal.aborted).toBe(false);\n        expect(t.router.state.navigation.state).toBe(\"loading\");\n        expect(t.router.state.navigation.location?.pathname).toBe(\"/foo\");\n\n        await B.loaders.root.resolve(\"B root\");\n        await B.loaders.foo.resolve(\"B\");\n        expect(t.router.state.navigation.state).toBe(\"idle\");\n        expect(t.router.state.location.pathname).toBe(\"/foo\");\n        expect(t.router.state.loaderData.foo).toBe(\"B\");\n        expect(A.loaders.foo.signal).toBe(undefined); // A loaders not called yet\n\n        await A.actions.foo.resolve(\"A root\");\n        await A.loaders.root.resolve(\"A root\");\n        await A.loaders.foo.resolve(\"A\");\n        expect(A.loaders.foo.signal.aborted).toBe(false);\n        expect(t.router.state.loaderData).toEqual({\n          root: \"A root\",\n          foo: \"A\",\n        });\n      });\n    });\n\n    describe(`\n      A) fetch POST |----|-----O\n      B) nav GET      |-----O\n    `, () => {\n      it(\"Commits A and uses next matches\", async () => {\n        let t = initializeTest({ url: \"/\" });\n\n        let A = await t.fetch(\"/foo\", {\n          formMethod: \"post\",\n          formData: createFormData({ key: \"value\" }),\n        });\n        // This fetcher's helpers take the current locations loaders (root/index).\n        // Since we know we're about to interrupt with /foo let's shim in a\n        // loader helper for foo ahead of time\n        t.shimHelper(A.loaders, \"fetch\", \"loader\", \"foo\");\n\n        let B = await t.navigate(\"/foo\");\n        await A.actions.foo.resolve(\"A action\");\n        await B.loaders.root.resolve(\"B root\");\n        await B.loaders.foo.resolve(\"B\");\n        expect(A.actions.foo.signal.aborted).toBe(false);\n        expect(A.loaders.foo.signal.aborted).toBe(false);\n        expect(t.router.state.navigation.state).toBe(\"idle\");\n        expect(t.router.state.location.pathname).toBe(\"/foo\");\n        expect(t.router.state.loaderData).toEqual({\n          root: \"B root\",\n          foo: \"B\",\n        });\n\n        await A.loaders.root.resolve(\"A root\");\n        await A.loaders.foo.resolve(\"A\");\n        expect(t.router.state.loaderData).toEqual({\n          root: \"A root\",\n          foo: \"A\",\n        });\n      });\n    });\n\n    describe(`\n      A) fetch POST |--|----X\n      B) nav GET         |--O\n    `, () => {\n      it(\"aborts A, sets fetcher done\", async () => {\n        let t = initializeTest({\n          url: \"/foo\",\n          hydrationData: { loaderData: { root: \"ROOT\", foo: \"FOO\" } },\n        });\n\n        let A = await t.fetch(\"/foo\", {\n          formMethod: \"post\",\n          formData: createFormData({ key: \"value\" }),\n        });\n        await A.actions.foo.resolve(\"A\");\n        let B = await t.navigate(\"/foo\");\n        await B.loaders.root.resolve(\"ROOT*\");\n        await B.loaders.foo.resolve(\"B\");\n        expect(t.router.state.navigation.state).toBe(\"idle\");\n        expect(t.router.state.location.pathname).toBe(\"/foo\");\n        expect(t.router.state.loaderData).toEqual({\n          root: \"ROOT*\",\n          foo: \"B\",\n        });\n        expect(A.loaders.foo.signal.aborted).toBe(true);\n        expect(A.fetcher.state).toBe(\"idle\");\n        expect(A.fetcher.data).toBe(\"A\");\n      });\n    });\n\n    describe(`\n      A) fetch POST |--|---O\n      B) nav GET         |---O\n    `, () => {\n      it(\"commits both\", async () => {\n        let t = initializeTest({\n          url: \"/foo\",\n          hydrationData: { loaderData: { root: \"ROOT\", foo: \"FOO\" } },\n        });\n\n        let A = await t.fetch(\"/foo\", {\n          formMethod: \"post\",\n          formData: createFormData({ key: \"value\" }),\n        });\n        await A.actions.foo.resolve(\"A action\");\n        let B = await t.navigate(\"/foo\");\n        await A.loaders.root.resolve(\"A ROOT\");\n        await A.loaders.foo.resolve(\"A\");\n        expect(t.router.state.loaderData).toEqual({\n          root: \"A ROOT\",\n          foo: \"A\",\n        });\n\n        await B.loaders.root.resolve(\"B ROOT\");\n        await B.loaders.foo.resolve(\"B\");\n        expect(t.router.state.loaderData).toEqual({\n          root: \"B ROOT\",\n          foo: \"B\",\n        });\n      });\n    });\n\n    describe(`\n      A) fetch POST |---[A]---O\n      B) nav POST           |---[A,B]--O\n    `, () => {\n      it(\"keeps both\", async () => {\n        let t = initializeTest({\n          url: \"/foo\",\n          hydrationData: { loaderData: { root: \"ROOT\", foo: \"FOO\" } },\n        });\n        let A = await t.fetch(\"/foo\", {\n          formMethod: \"post\",\n          formData: createFormData({ key: \"value\" }),\n        });\n        await A.actions.foo.resolve(\"A action\");\n        let B = await t.navigate(\"/foo\", {\n          formMethod: \"post\",\n          formData: createFormData({ key: \"value\" }),\n        });\n        await A.loaders.root.resolve(\"A ROOT\");\n        await A.loaders.foo.resolve(\"A\");\n        expect(t.router.state.loaderData).toEqual({\n          root: \"A ROOT\",\n          foo: \"A\",\n        });\n\n        await B.actions.foo.resolve(\"A,B\");\n        await B.loaders.root.resolve(\"A,B ROOT\");\n        await B.loaders.foo.resolve(\"A,B\");\n        expect(t.router.state.loaderData).toEqual({\n          root: \"A,B ROOT\",\n          foo: \"A,B\",\n        });\n      });\n    });\n\n    describe(`\n      A) fetch POST |---[A]--------X\n      B) nav POST     |-----[A,B]--O\n    `, () => {\n      it(\"aborts A, commits B, marks fetcher done\", async () => {\n        let t = initializeTest({\n          url: \"/foo\",\n          hydrationData: { loaderData: { root: \"ROOT\", foo: \"FOO\" } },\n        });\n        let A = await t.fetch(\"/foo\", {\n          formMethod: \"post\",\n          formData: createFormData({ key: \"value\" }),\n        });\n        let B = await t.navigate(\"/foo\", {\n          formMethod: \"post\",\n          formData: createFormData({ key: \"value\" }),\n        });\n        await A.actions.foo.resolve(\"A\");\n        await B.actions.foo.resolve(\"A,B\");\n        await B.loaders.root.resolve(\"A,B ROOT\");\n        await B.loaders.foo.resolve(\"A,B\");\n        expect(t.router.state.loaderData).toEqual({\n          root: \"A,B ROOT\",\n          foo: \"A,B\",\n        });\n        expect(A.loaders.foo.signal.aborted).toBe(true);\n        expect(A.fetcher.state).toBe(\"idle\");\n        expect(A.fetcher.data).toBe(\"A\");\n      });\n    });\n\n    describe(`\n      A) fetch POST |-----------[B,A]--O\n      B) nav POST     |--[B]--O\n    `, () => {\n      it(\"commits both, uses the nav's href\", async () => {\n        let t = initializeTest({\n          url: \"/foo\",\n          hydrationData: { loaderData: { root: \"ROOT\", foo: \"FOO\" } },\n        });\n        let A = await t.fetch(\"/foo\", {\n          formMethod: \"post\",\n          formData: createFormData({ key: \"value\" }),\n        });\n        t.shimHelper(A.loaders, \"fetch\", \"loader\", \"bar\");\n        let B = await t.navigate(\"/bar\", {\n          formMethod: \"post\",\n          formData: createFormData({ key: \"value\" }),\n        });\n        await B.actions.bar.resolve(\"B\");\n        await B.loaders.root.resolve(\"B\");\n        await B.loaders.bar.resolve(\"B\");\n        await A.actions.foo.resolve(\"B,A\");\n        await A.loaders.root.resolve(\"B,A ROOT\");\n        await A.loaders.bar.resolve(\"B,A\");\n        expect(t.router.state.loaderData).toEqual({\n          root: \"B,A ROOT\",\n          bar: \"B,A\",\n        });\n      });\n    });\n\n    describe(`\n      A) fetch POST |-------[B,A]--O\n      B) nav POST     |--[B]-------X\n    `, () => {\n      it(\"aborts B, commits A, uses the nav's href\", async () => {\n        let t = initializeTest({\n          url: \"/foo\",\n          hydrationData: { loaderData: { root: \"ROOT\", foo: \"FOO\" } },\n        });\n        let A = await t.fetch(\"/foo\", {\n          formMethod: \"post\",\n          formData: createFormData({ key: \"value\" }),\n        });\n        t.shimHelper(A.loaders, \"fetch\", \"loader\", \"bar\");\n        let B = await t.navigate(\"/bar\", {\n          formMethod: \"post\",\n          formData: createFormData({ key: \"value\" }),\n        });\n        await B.actions.bar.resolve(\"B\");\n        await A.actions.foo.resolve(\"B,A\");\n        await A.loaders.root.resolve(\"B,A ROOT\");\n        await A.loaders.bar.resolve(\"B,A\");\n        expect(B.loaders.bar.signal.aborted).toBe(true);\n        expect(t.router.state.loaderData).toEqual({\n          root: \"B,A ROOT\",\n          bar: \"B,A\",\n        });\n        expect(t.router.state.navigation).toBe(IDLE_NAVIGATION);\n      });\n    });\n\n    describe(`\n      A) fetch POST /foo |--X\n      B) nav   GET  /bar    |-----O\n    `, () => {\n      it(\"forces all loaders to revalidate on interrupted fetcher submission\", async () => {\n        let t = initializeTest();\n        let A = await t.fetch(\"/foo\", {\n          formMethod: \"post\",\n          formData: createFormData({ key: \"value\" }),\n        });\n        t.shimHelper(A.loaders, \"fetch\", \"loader\", \"bar\");\n\n        // Interrupting the submission should cause the next load to call all loaders\n        let B = await t.navigate(\"/bar\");\n        await A.actions.foo.resolve(\"A ACTION\");\n        await B.loaders.root.resolve(\"ROOT*\");\n        await B.loaders.bar.resolve(\"BAR\");\n        expect(t.router.state).toMatchObject({\n          navigation: IDLE_NAVIGATION,\n          location: { pathname: \"/bar\" },\n          actionData: null,\n          loaderData: {\n            root: \"ROOT*\",\n            bar: \"BAR\",\n          },\n        });\n\n        await A.loaders.root.resolve(\"ROOT**\");\n        await A.loaders.bar.resolve(\"BAR*\");\n        expect(t.router.state).toMatchObject({\n          navigation: IDLE_NAVIGATION,\n          location: { pathname: \"/bar\" },\n          actionData: null,\n          loaderData: {\n            root: \"ROOT**\",\n            bar: \"BAR*\",\n          },\n        });\n      });\n    });\n\n    describe(`\n      A) fetch POST /foo |--|--X\n      B) nav   GET  /bar       |-----O\n    `, () => {\n      it(\"forces all loaders to revalidate on interrupted fetcher actionReload\", async () => {\n        let key = \"key\";\n        let t = initializeTest();\n        let A = await t.fetch(\"/foo\", key, {\n          formMethod: \"post\",\n          formData: createFormData({ key: \"value\" }),\n        });\n        await A.actions.foo.resolve(\"A ACTION\");\n        expect(t.router.getFetcher(key)?.state).toBe(\"loading\");\n        expect(t.fetchers[key]?.data).toBe(\"A ACTION\");\n        // Interrupting the actionReload should cause the next load to call all loaders\n        let B = await t.navigate(\"/bar\");\n        await B.loaders.root.resolve(\"ROOT*\");\n        await B.loaders.bar.resolve(\"BAR\");\n        expect(t.router.state).toMatchObject({\n          navigation: IDLE_NAVIGATION,\n          location: { pathname: \"/bar\" },\n          actionData: null,\n          loaderData: {\n            root: \"ROOT*\",\n            bar: \"BAR\",\n          },\n        });\n        expect(t.router.getFetcher(key)?.state).toBe(\"idle\");\n        expect(t.fetchers[key]?.data).toBe(\"A ACTION\");\n      });\n\n      it(\"forces all loaders to revalidate on interrupted fetcher submissionRedirect\", async () => {\n        let key = \"key\";\n        let t = initializeTest();\n        let A = await t.fetch(\"/foo\", key, {\n          formMethod: \"post\",\n          formData: createFormData({ key: \"value\" }),\n        });\n        await A.actions.foo.redirect(\"/baz\");\n        expect(t.router.getFetcher(key)?.state).toBe(\"loading\");\n        // Interrupting the actionReload should cause the next load to call all loaders\n        let B = await t.navigate(\"/bar\");\n        await B.loaders.root.resolve(\"ROOT*\");\n        await B.loaders.bar.resolve(\"BAR\");\n        expect(t.router.state).toMatchObject({\n          navigation: IDLE_NAVIGATION,\n          location: { pathname: \"/bar\" },\n          loaderData: {\n            root: \"ROOT*\",\n            bar: \"BAR\",\n          },\n        });\n        expect(t.router.getFetcher(key)?.state).toBe(\"idle\");\n        expect(t.fetchers[key]?.data).toBeUndefined();\n      });\n    });\n\n    describe(`\n      A) fetch GET /foo |--R\n      B) nav   GET /bar   |---O\n    `, () => {\n      it(\"ignores loader redirect navigation if preceded by a normal GET navigation\", async () => {\n        let key = \"key\";\n        let t = initializeTest();\n\n        // Start a fetch load and interrupt with a navigation\n        let A = await t.fetch(\"/foo\", key);\n        let B = await t.navigate(\"/bar\");\n\n        // The fetcher loader redirect should be ignored\n        await A.loaders.foo.redirect(\"/baz\");\n        expect(t.router.getFetcher(key)?.state).toBe(\"idle\");\n\n        await B.loaders.bar.resolve(\"BAR\");\n        expect(t.router.state).toMatchObject({\n          navigation: IDLE_NAVIGATION,\n          location: { pathname: \"/bar\" },\n          loaderData: {\n            root: \"ROOT\",\n            bar: \"BAR\",\n          },\n        });\n        expect(t.router.getFetcher(key)?.state).toBe(\"idle\");\n        expect(t.fetchers[key]?.data).toBeUndefined();\n      });\n    });\n\n    describe(`\n      A) fetch POST /foo |--R\n      B) nav   GET  /bar   |---O\n    `, () => {\n      it(\"ignores submission redirect navigation if preceded by a normal GET navigation\", async () => {\n        let key = \"key\";\n        let t = initializeTest();\n        let A = await t.fetch(\"/foo\", key, {\n          formMethod: \"post\",\n          formData: createFormData({ key: \"value\" }),\n        });\n        let B = await t.navigate(\"/bar\");\n\n        // This redirect should be ignored\n        await A.actions.foo.redirect(\"/baz\");\n        expect(t.router.getFetcher(key)?.state).toBe(\"idle\");\n\n        await B.loaders.root.resolve(\"ROOT*\");\n        await B.loaders.bar.resolve(\"BAR\");\n        expect(t.router.state).toMatchObject({\n          navigation: IDLE_NAVIGATION,\n          location: { pathname: \"/bar\" },\n          loaderData: {\n            root: \"ROOT*\",\n            bar: \"BAR\",\n          },\n        });\n        expect(t.router.getFetcher(key)?.state).toBe(\"idle\");\n        expect(t.fetchers[key]?.data).toBeUndefined();\n      });\n\n      it(\"ignores submission redirect navigation if preceded by a normal GET navigation (w/o loaders)\", async () => {\n        let key = \"key\";\n        let t = setup({\n          routes: [\n            {\n              path: \"\",\n              id: \"root\",\n              children: [\n                {\n                  path: \"/\",\n                  id: \"index\",\n                },\n                {\n                  path: \"/foo\",\n                  id: \"foo\",\n                  action: true,\n                },\n                {\n                  path: \"/bar\",\n                  id: \"bar\",\n                },\n                {\n                  path: \"/baz\",\n                  id: \"baz\",\n                },\n              ],\n            },\n          ],\n        });\n        let A = await t.fetch(\"/foo\", key, {\n          formMethod: \"post\",\n          formData: createFormData({ key: \"value\" }),\n        });\n        await t.navigate(\"/bar\");\n\n        // This redirect should be ignored\n        await A.actions.foo.redirect(\"/baz\");\n        expect(t.router.getFetcher(key)?.state).toBe(\"idle\");\n\n        expect(t.router.state).toMatchObject({\n          navigation: IDLE_NAVIGATION,\n          location: { pathname: \"/bar\" },\n          loaderData: {},\n        });\n        expect(t.router.getFetcher(key)?.state).toBe(\"idle\");\n        expect(t.fetchers[key]?.data).toBeUndefined();\n      });\n    });\n\n    describe(`\n      A) fetch GET /foo |--R     |---O\n      B) nav   POST /bar   |--|--|---O\n    `, () => {\n      it(\"ignores loader redirect navigation if preceded by a normal POST navigation\", async () => {\n        let key = \"key\";\n        let t = initializeTest();\n\n        // Start a fetch load and interrupt with a POST navigation\n        let A = await t.fetch(\"/foo\", key);\n        let B = await t.navigate(\n          \"/bar\",\n          { formMethod: \"post\", formData: createFormData({}) },\n          [\"foo\"],\n        );\n\n        // The fetcher loader redirect should be ignored\n        await A.loaders.foo.redirect(\"/baz\");\n        expect(t.router.getFetcher(key)?.state).toBe(\"loading\");\n\n        // The navigation should trigger the fetcher to revalidate since it's\n        // not yet \"completed\".  If it returns data this time that should be\n        // reflected\n        await B.actions.bar.resolve(\"ACTION\");\n        await B.loaders.root.resolve(\"ROOT*\");\n        await B.loaders.bar.resolve(\"BAR\");\n        await B.loaders.foo.resolve(\"FOO\");\n\n        expect(t.router.state).toMatchObject({\n          navigation: IDLE_NAVIGATION,\n          location: { pathname: \"/bar\" },\n          loaderData: {\n            root: \"ROOT*\",\n            bar: \"BAR\",\n          },\n        });\n        expect(t.router.getFetcher(key)?.state).toBe(\"idle\");\n        expect(t.fetchers[key]?.data).toBe(\"FOO\");\n      });\n\n      it(\"processes second fetcher load redirect after interruption by normal POST navigation\", async () => {\n        let key = \"key\";\n        let t = initializeTest();\n\n        // Start a fetch load and interrupt with a POST navigation\n        let A = await t.fetch(\"/foo\", key, \"root\");\n        let B = await t.navigate(\n          \"/bar\",\n          { formMethod: \"post\", formData: createFormData({}) },\n          [\"foo\"],\n        );\n        expect(A.loaders.foo.signal.aborted).toBe(true);\n\n        // The fetcher loader redirect should be ignored\n        await A.loaders.foo.redirect(\"/baz\");\n        expect(t.router.state).toMatchObject({\n          navigation: { location: { pathname: \"/bar\" } },\n          location: { pathname: \"/\" },\n        });\n        expect(t.router.getFetcher(key).state).toBe(\"loading\");\n\n        // The navigation should trigger the fetcher to revalidate since it's\n        // not yet \"completed\".  If it redirects again we should follow that\n        await B.actions.bar.resolve(\"ACTION\");\n        await B.loaders.root.resolve(\"ROOT*\");\n        await B.loaders.bar.resolve(\"BAR\");\n        let C = await B.loaders.foo.redirect(\"/foo/bar\", undefined, undefined, [\n          \"foo\",\n        ]);\n        expect(t.router.state).toMatchObject({\n          navigation: { location: { pathname: \"/foo/bar\" } },\n          location: { pathname: \"/\" },\n          loaderData: {\n            root: \"ROOT\",\n          },\n        });\n        expect(t.router.getFetcher(key).state).toBe(\"loading\");\n\n        // The fetcher should not revalidate here since it triggered the redirect\n        await C.loaders.root.resolve(\"ROOT**\");\n        await C.loaders.foobar.resolve(\"FOOBAR\");\n        expect(t.router.state).toMatchObject({\n          navigation: IDLE_NAVIGATION,\n          location: { pathname: \"/foo/bar\" },\n          loaderData: {\n            root: \"ROOT**\",\n            foobar: \"FOOBAR\",\n          },\n        });\n        expect(t.router.getFetcher(key).state).toBe(\"idle\");\n        expect(t.fetchers[key]?.data).toBe(undefined);\n      });\n    });\n\n    describe(`\n      A) fetch GET /foo |-----X\n      B) fetch GET /bar  |---R\n    `, () => {\n      it(\"handles racing fetcher loader redirects\", async () => {\n        let keyA = \"a\";\n        let keyB = \"b\";\n        let t = initializeTest();\n\n        // Start 2 fetch loads\n        let A = await t.fetch(\"/foo\", keyA, \"root\");\n        let B = await t.fetch(\"/bar\", keyB, \"root\");\n\n        // Return a redirect from the second fetcher.load\n        let C = await B.loaders.bar.redirect(\"/baz\");\n        expect(t.router.state).toMatchObject({\n          navigation: { location: { pathname: \"/baz\" } },\n          location: { pathname: \"/\" },\n        });\n        expect(t.router.getFetcher(keyA)?.state).toBe(\"loading\");\n        expect(t.router.getFetcher(keyB)?.state).toBe(\"loading\");\n\n        // The original fetch load redirect should be ignored\n        await A.loaders.foo.redirect(\"/foo/bar\");\n        expect(t.router.state).toMatchObject({\n          navigation: { location: { pathname: \"/baz\" } },\n          location: { pathname: \"/\" },\n        });\n        expect(t.router.getFetcher(keyA)?.state).toBe(\"idle\");\n        expect(t.router.getFetcher(keyB)?.state).toBe(\"loading\");\n\n        // Resolve the navigation loader\n        await C.loaders.baz.resolve(\"BAZ\");\n        expect(t.router.state).toMatchObject({\n          navigation: IDLE_NAVIGATION,\n          location: { pathname: \"/baz\" },\n          loaderData: {\n            root: \"ROOT\",\n            baz: \"BAZ\",\n          },\n        });\n        expect(t.router.getFetcher(keyA)?.state).toBe(\"idle\");\n        expect(t.router.getFetcher(keyB)?.state).toBe(\"idle\");\n      });\n    });\n\n    it(\"properly ignores aborted action revalidation fetchers when a navigation triggers revalidations\", async () => {\n      let keyA = \"a\";\n      let keyB = \"b\";\n      let t = initializeTest();\n\n      // Complete a fetch load\n      let A = await t.fetch(\"/foo\", keyA, \"root\");\n      await A.loaders.foo.resolve(\"FOO\");\n      expect(t.fetchers[keyA]).toMatchObject({\n        state: \"idle\",\n        data: \"FOO\",\n      });\n      expect(t.router.state.fetchers.get(keyA)).toBe(undefined);\n\n      // Submit to trigger fetch revalidation\n      let B = await t.fetch(\"/bar\", keyB, \"root\", {\n        formMethod: \"post\",\n        formData: createFormData({}),\n      });\n      t.shimHelper(B.loaders, \"fetch\", \"loader\", \"foo\");\n      await B.actions.bar.resolve(\"BAR\");\n      expect(t.fetchers[keyB]).toMatchObject({\n        state: \"loading\",\n        data: \"BAR\",\n      });\n      expect(t.fetchers[keyA]).toMatchObject({\n        state: \"loading\",\n        data: \"FOO\",\n      });\n\n      // Interrupt revalidation with GEt navigation\n      // TODO: This shouldn't actually abort the revalidation but it does currently\n      // which then causes the invalid invariant error.  This test is to ensure\n      // the invariant doesn't throw, but we'll fix the unnecessary revalidation\n      // in https://github.com/remix-run/react-router/issues/14115\n      let C = await t.navigate(\"/baz\", undefined, [\"foo\"]);\n      expect(B.loaders.foo.signal.aborted).toBe(true);\n      expect(t.fetchers[keyA]).toMatchObject({\n        state: \"loading\",\n        data: \"FOO\",\n      });\n\n      // Complete the aborted fetcher revalidation calls\n      await B.loaders.root.resolve(\"NOPE\");\n      await B.loaders.index.resolve(\"NOPE\");\n      await B.loaders.foo.resolve(\"NOPE\");\n\n      // Complete the navigation\n      await C.loaders.root.resolve(\"ROOT*\");\n      await C.loaders.baz.resolve(\"BAZ\");\n      await C.loaders.foo.resolve(\"FOO*\");\n      expect(t.router.state).toMatchObject({\n        navigation: IDLE_NAVIGATION,\n        location: { pathname: \"/baz\" },\n        loaderData: {\n          root: \"ROOT*\",\n          baz: \"BAZ\",\n        },\n      });\n      expect(t.fetchers[keyA]).toMatchObject({\n        state: \"idle\",\n        data: \"FOO*\",\n      });\n    });\n  });\n\n  describe(\"fetcher revalidation\", () => {\n    it(\"revalidates fetchers on action submissions\", async () => {\n      let t = setup({\n        routes: TASK_ROUTES,\n        initialEntries: [\"/\"],\n        hydrationData: { loaderData: { root: \"ROOT\", index: \"INDEX\" } },\n      });\n      expect(t.router.state.navigation).toBe(IDLE_NAVIGATION);\n\n      let key1 = \"key1\";\n      let A = await t.fetch(\"/tasks/1\", key1);\n      await A.loaders.tasksId.resolve(\"TASKS 1\");\n      expect(A.fetcher.state).toBe(\"idle\");\n      expect(A.fetcher.data).toBe(\"TASKS 1\");\n\n      let C = await t.navigate(\"/tasks\", {\n        formMethod: \"post\",\n        formData: createFormData({}),\n      });\n      // Add a helper for the fetcher that will be revalidating\n      t.shimHelper(C.loaders, \"navigation\", \"loader\", \"tasksId\");\n\n      // Resolve the action\n      await C.actions.tasks.resolve(\"TASKS ACTION\");\n\n      // Fetcher should go back into a loading state\n      expect(t.router.getFetcher(key1)?.state).toBe(\"loading\");\n\n      // Resolve navigation loaders + fetcher loader\n      await C.loaders.root.resolve(\"ROOT*\");\n      await C.loaders.tasks.resolve(\"TASKS LOADER\");\n      await C.loaders.tasksId.resolve(\"TASKS ID*\");\n      expect(t.fetchers[key1]).toMatchObject({\n        state: \"idle\",\n        data: \"TASKS ID*\",\n      });\n\n      // If a fetcher does a submission, it unsets the revalidation aspect\n      let D = await t.fetch(\"/tasks/3\", key1, {\n        formMethod: \"post\",\n        formData: createFormData({}),\n      });\n      await D.actions.tasksId.resolve(\"TASKS 3\");\n      await D.loaders.root.resolve(\"ROOT**\");\n      await D.loaders.tasks.resolve(\"TASKS**\");\n      expect(t.fetchers[key1]).toMatchObject({\n        state: \"idle\",\n        data: \"TASKS 3\",\n      });\n\n      let E = await t.navigate(\"/tasks\", {\n        formMethod: \"post\",\n        formData: createFormData({}),\n      });\n      await E.actions.tasks.resolve(\"TASKS ACTION\");\n      await E.loaders.root.resolve(\"ROOT***\");\n      await E.actions.tasks.resolve(\"TASKS***\");\n\n      // Remains the same state as it was after the submission\n      expect(t.fetchers[key1]).toMatchObject({\n        state: \"idle\",\n        data: \"TASKS 3\",\n      });\n    });\n\n    it(\"revalidates fetchers on action redirects\", async () => {\n      let t = setup({\n        routes: TASK_ROUTES,\n        initialEntries: [\"/\"],\n        hydrationData: { loaderData: { root: \"ROOT\", index: \"INDEX\" } },\n      });\n      expect(t.router.state.navigation).toBe(IDLE_NAVIGATION);\n\n      let key = \"key\";\n      let A = await t.fetch(\"/tasks/1\", key);\n      await A.loaders.tasksId.resolve(\"TASKS ID\");\n      expect(A.fetcher.state).toBe(\"idle\");\n      expect(A.fetcher.data).toBe(\"TASKS ID\");\n\n      let C = await t.navigate(\"/tasks\", {\n        formMethod: \"post\",\n        formData: createFormData({}),\n      });\n\n      // Redirect the action\n      let D = await C.actions.tasks.redirect(\"/\", undefined, undefined, [\n        \"tasksId\",\n      ]);\n      expect(t.router.getFetcher(key)?.state).toBe(\"loading\");\n\n      // Resolve navigation loaders + fetcher loader\n      await D.loaders.root.resolve(\"ROOT*\");\n      await D.loaders.index.resolve(\"INDEX*\");\n      await D.loaders.tasksId.resolve(\"TASKS ID*\");\n      expect(t.fetchers[key]).toMatchObject({\n        state: \"idle\",\n        data: \"TASKS ID*\",\n      });\n    });\n\n    it(\"revalidates fetchers on action errors\", async () => {\n      let t = setup({\n        routes: TASK_ROUTES,\n        initialEntries: [\"/\"],\n        hydrationData: { loaderData: { root: \"ROOT\", index: \"INDEX\" } },\n      });\n      expect(t.router.state.navigation).toBe(IDLE_NAVIGATION);\n\n      let key = \"key\";\n      let A = await t.fetch(\"/tasks/1\", key);\n      await A.loaders.tasksId.resolve(\"TASKS ID\");\n      expect(A.fetcher.state).toBe(\"idle\");\n      expect(A.fetcher.data).toBe(\"TASKS ID\");\n\n      let C = await t.navigate(\"/tasks\", {\n        formMethod: \"post\",\n        formData: createFormData({}),\n      });\n      t.shimHelper(C.loaders, \"navigation\", \"loader\", \"tasksId\");\n\n      // Reject the action\n      await C.actions.tasks.reject(new Error(\"Kaboom!\"));\n      expect(t.router.getFetcher(key)?.state).toBe(\"loading\");\n\n      // Resolve navigation loaders + fetcher loader\n      await C.loaders.root.resolve(\"ROOT*\");\n      await C.loaders.tasksId.resolve(\"TASKS ID*\");\n      expect(t.fetchers[key]).toMatchObject({\n        state: \"idle\",\n        data: \"TASKS ID*\",\n      });\n    });\n\n    it(\"does not revalidate fetchers on searchParams changes\", async () => {\n      let key = \"key\";\n      let t = setup({\n        routes: TASK_ROUTES,\n        initialEntries: [\"/tasks/1\"],\n        hydrationData: {\n          loaderData: {\n            root: \"ROOT\",\n            taskId: \"TASK 1\",\n          },\n        },\n      });\n\n      let A = await t.fetch(\"/?index\", key);\n      await A.loaders.index.resolve(\"FETCH 1\");\n      expect(t.fetchers[key]).toMatchObject({\n        state: \"idle\",\n        data: \"FETCH 1\",\n      });\n\n      let B = await t.navigate(\"/tasks/1?key=value\", undefined, [\"index\"]);\n      await B.loaders.root.resolve(\"ROOT 2\");\n      await B.loaders.tasksId.resolve(\"TASK 2\");\n      expect(t.router.state.loaderData).toMatchObject({\n        root: \"ROOT 2\",\n        tasksId: \"TASK 2\",\n      });\n      expect(t.fetchers[key]).toMatchObject({\n        state: \"idle\",\n        data: \"FETCH 1\",\n      });\n      expect(B.loaders.index.stub).not.toHaveBeenCalled();\n    });\n\n    it(\"revalidates fetchers on links to the current location\", async () => {\n      let key = \"key\";\n      let t = setup({\n        routes: TASK_ROUTES,\n        initialEntries: [\"/tasks/1\"],\n        hydrationData: {\n          loaderData: {\n            root: \"ROOT\",\n            taskId: \"TASK 1\",\n          },\n        },\n      });\n\n      let A = await t.fetch(\"/?index\", key);\n      await A.loaders.index.resolve(\"FETCH 1\");\n      expect(t.fetchers[key]).toMatchObject({\n        state: \"idle\",\n        data: \"FETCH 1\",\n      });\n\n      let B = await t.navigate(\"/tasks/1\", undefined, [\"index\"]);\n      await B.loaders.root.resolve(\"ROOT 2\");\n      await B.loaders.tasksId.resolve(\"TASK 2\");\n      expect(t.router.state.loaderData).toMatchObject({\n        root: \"ROOT 2\",\n        tasksId: \"TASK 2\",\n      });\n      expect(t.fetchers[key]).toMatchObject({\n        state: \"idle\",\n        data: \"FETCH 1\",\n      });\n      expect(B.loaders.index.stub).not.toHaveBeenCalled();\n    });\n\n    it(\"does not revalidate idle fetchers when a loader navigation is performed\", async () => {\n      let key = \"key\";\n      let t = setup({\n        routes: TASK_ROUTES,\n        initialEntries: [\"/\"],\n        hydrationData: { loaderData: { root: \"ROOT\", index: \"INDEX\" } },\n      });\n\n      let A = await t.fetch(\"/\", key);\n      await A.loaders.root.resolve(\"ROOT FETCH\");\n      expect(t.fetchers[key]).toMatchObject({\n        state: \"idle\",\n        data: \"ROOT FETCH\",\n      });\n\n      let B = await t.navigate(\"/tasks\");\n      await B.loaders.tasks.resolve(\"TASKS\");\n      expect(t.router.state.loaderData).toMatchObject({\n        root: \"ROOT\",\n        tasks: \"TASKS\",\n      });\n      expect(t.fetchers[key]).toMatchObject({\n        state: \"idle\",\n        data: \"ROOT FETCH\",\n      });\n    });\n\n    it(\"respects shouldRevalidate for the fetcher route\", async () => {\n      let key = \"key\";\n      let count = 0;\n      let shouldRevalidate = jest.fn((args) => false);\n      let router = createRouter({\n        history: createMemoryHistory({ initialEntries: [\"/one\"] }),\n        routes: [\n          {\n            id: \"root\",\n            path: \"/\",\n            loader: () => Promise.resolve(++count),\n            children: [\n              {\n                path: \":a\",\n                children: [\n                  {\n                    path: \":b\",\n                    action: () => Promise.resolve(null),\n                  },\n                ],\n              },\n            ],\n          },\n          {\n            id: \"fetch\",\n            path: \"/fetch\",\n            loader: () => Promise.resolve(++count),\n            shouldRevalidate,\n          },\n        ],\n        hydrationData: {\n          loaderData: { root: count },\n        },\n      });\n      let fetcherData = getFetcherData(router);\n      router.initialize();\n\n      expect(router.state.loaderData).toMatchObject({\n        root: 0,\n      });\n      expect(router.getFetcher(key)).toEqual(IDLE_FETCHER);\n\n      // Fetch from a different route\n      router.fetch(key, \"root\", \"/fetch\");\n      await tick();\n      expect(router.getFetcher(key)).toEqual(IDLE_FETCHER);\n      expect(fetcherData.get(key)).toBe(1);\n\n      // Post to the current route\n      router.navigate(\"/two/three\", {\n        formMethod: \"post\",\n        formData: createFormData({}),\n      });\n      await tick();\n      expect(router.state.loaderData).toMatchObject({\n        root: 2,\n      });\n      expect(router.getFetcher(key)).toEqual(IDLE_FETCHER);\n      expect(fetcherData.get(key)).toBe(1);\n\n      expect(shouldRevalidate.mock.calls[0][0]).toMatchInlineSnapshot(`\n        {\n          \"actionResult\": null,\n          \"actionStatus\": undefined,\n          \"currentParams\": {\n            \"a\": \"one\",\n          },\n          \"currentUrl\": \"http://localhost/one\",\n          \"defaultShouldRevalidate\": true,\n          \"formAction\": \"/two/three\",\n          \"formData\": FormData {},\n          \"formEncType\": \"application/x-www-form-urlencoded\",\n          \"formMethod\": \"POST\",\n          \"json\": undefined,\n          \"nextParams\": {\n            \"a\": \"two\",\n            \"b\": \"three\",\n          },\n          \"nextUrl\": \"http://localhost/two/three\",\n          \"text\": undefined,\n        }\n      `);\n\n      expect(router._internalFetchControllers.size).toBe(0);\n      router.dispose();\n    });\n\n    it(\"handles fetcher revalidation errors\", async () => {\n      let key = \"key\";\n      let t = setup({\n        routes: TASK_ROUTES,\n        initialEntries: [\"/\"],\n        hydrationData: { loaderData: { root: \"ROOT\", index: \"INDEX\" } },\n      });\n\n      expect(t.router.state).toMatchObject({\n        loaderData: {\n          root: \"ROOT\",\n          index: \"INDEX\",\n        },\n        errors: null,\n      });\n\n      let A = await t.fetch(\"/tasks/1\", key);\n      await A.loaders.tasksId.resolve(\"ROOT FETCH\");\n      expect(t.fetchers[key]).toMatchObject({\n        state: \"idle\",\n        data: \"ROOT FETCH\",\n      });\n\n      let B = await t.navigate(\"/tasks\", {\n        formMethod: \"post\",\n        formData: createFormData({}),\n      });\n      t.shimHelper(B.loaders, \"navigation\", \"loader\", \"tasksId\");\n      await B.actions.tasks.resolve(\"TASKS ACTION\");\n      await B.loaders.root.resolve(\"ROOT*\");\n      await B.loaders.tasks.resolve(\"TASKS*\");\n      await B.loaders.tasksId.reject(new Error(\"Fetcher error\"));\n      expect(t.router.state).toMatchObject({\n        loaderData: {\n          root: \"ROOT*\",\n          tasks: \"TASKS*\",\n        },\n        errors: {\n          // Even though tasksId has an error boundary, this bubbles up to\n          // the root since it's the closest \"active\" rendered route with an\n          // error boundary\n          root: new Error(\"Fetcher error\"),\n        },\n      });\n      expect(t.router.getFetcher(key)).toBe(IDLE_FETCHER);\n    });\n\n    it(\"revalidates fetchers on fetcher action submissions\", async () => {\n      let key = \"key\";\n      let actionKey = \"actionKey\";\n      let t = setup({\n        routes: TASK_ROUTES,\n        initialEntries: [\"/\"],\n        hydrationData: { loaderData: { root: \"ROOT\", index: \"INDEX\" } },\n      });\n\n      // Load a fetcher\n      let A = await t.fetch(\"/tasks/1\", key);\n      await A.loaders.tasksId.resolve(\"TASKS ID\");\n      expect(t.fetchers[key]).toMatchObject({\n        state: \"idle\",\n        data: \"TASKS ID\",\n      });\n\n      // Submit a fetcher, leaves loaded fetcher untouched\n      let C = await t.fetch(\"/tasks\", actionKey, {\n        formMethod: \"post\",\n        formData: createFormData({}),\n      });\n      t.shimHelper(C.loaders, \"fetch\", \"loader\", \"tasksId\");\n      expect(t.fetchers[key]).toMatchObject({\n        state: \"idle\",\n        data: \"TASKS ID\",\n      });\n      expect(t.fetchers[actionKey]).toMatchObject({\n        state: \"submitting\",\n      });\n\n      // After action resolves, both fetchers go into a loading state, with\n      // the load fetcher still reflecting it's stale data\n      await C.actions.tasks.resolve(\"TASKS ACTION\");\n      expect(t.fetchers[key]).toMatchObject({\n        state: \"loading\",\n        data: \"TASKS ID\",\n      });\n      expect(t.fetchers[actionKey]).toMatchObject({\n        state: \"loading\",\n        data: \"TASKS ACTION\",\n      });\n\n      // All go back to idle on resolutions\n      await C.loaders.root.resolve(\"ROOT*\");\n      await C.loaders.index.resolve(\"INDEX*\");\n      await C.loaders.tasksId.resolve(\"TASKS ID*\");\n\n      expect(t.router.state.loaderData).toMatchObject({\n        root: \"ROOT*\",\n        index: \"INDEX*\",\n      });\n      expect(t.fetchers[key]).toMatchObject({\n        state: \"idle\",\n        data: \"TASKS ID*\",\n      });\n      expect(t.fetchers[actionKey]).toMatchObject({\n        state: \"idle\",\n        data: \"TASKS ACTION\",\n      });\n    });\n\n    it(\"does not revalidate fetchers initiated from removed routes\", async () => {\n      let t = setup({\n        routes: TASK_ROUTES,\n        initialEntries: [\"/\"],\n        hydrationData: { loaderData: { root: \"ROOT\", index: \"INDEX\" } },\n      });\n\n      let key = \"key\";\n\n      // Trigger a fetch from the index route\n      let A = await t.fetch(\"/tasks/1\", key, \"index\");\n      await A.loaders.tasksId.resolve(\"TASKS\");\n      expect(t.fetchers[key]).toMatchObject({\n        state: \"idle\",\n        data: \"TASKS\",\n      });\n\n      // Navigate such that the index route will be removed\n      let B = await t.navigate(\"/tasks\", {\n        formMethod: \"post\",\n        formData: createFormData({}),\n      });\n\n      // Resolve the action\n      await B.actions.tasks.resolve(\"TASKS ACTION\");\n\n      // Fetcher should remain in an idle state since it's calling route is\n      // being removed\n      expect(t.fetchers[key]).toMatchObject({\n        state: \"idle\",\n        data: \"TASKS\",\n      });\n\n      // Resolve navigation loaders\n      await B.loaders.root.resolve(\"ROOT*\");\n      await B.loaders.tasks.resolve(\"TASKS LOADER\");\n      expect(t.router.state.navigation.state).toBe(\"idle\");\n      expect(t.router.state.location.pathname).toBe(\"/tasks\");\n\n      // Fetcher never got called\n      expect(t.fetchers[key]).toMatchObject({\n        state: \"idle\",\n        data: \"TASKS\",\n      });\n    });\n\n    it(\"cancels in-flight fetcher.loads on action submission and forces reload\", async () => {\n      let t = setup({\n        routes: [\n          {\n            path: \"/\",\n            children: [\n              {\n                id: \"index\",\n                index: true,\n              },\n              {\n                id: \"action\",\n                path: \"action\",\n                action: true,\n              },\n              // fetch A will resolve before the action and will be able to opt-out\n              {\n                id: \"fetchA\",\n                path: \"fetch-a\",\n                loader: true,\n                shouldRevalidate: () => false,\n              },\n              // fetch B will resolve before the action but then issue a second\n              // load that gets cancelled.  It will not be able to opt out because\n              // of the cancellation\n              {\n                id: \"fetchB\",\n                path: \"fetch-b\",\n                loader: true,\n                shouldRevalidate: () => false,\n              },\n              // fetch C will not resolve before the action, and will not be able to opt\n              // out because it has no data\n              {\n                id: \"fetchC\",\n                path: \"fetch-c\",\n                loader: true,\n                shouldRevalidate: () => false,\n              },\n            ],\n          },\n        ],\n        initialEntries: [\"/\"],\n        hydrationData: { loaderData: { index: \"INDEX\" } },\n      });\n      expect(t.router.state.navigation).toBe(IDLE_NAVIGATION);\n\n      let keyA = \"a\";\n      let A = await t.fetch(\"/fetch-a\", keyA);\n      await A.loaders.fetchA.resolve(\"A\");\n      expect(t.fetchers[keyA]).toMatchObject({\n        state: \"idle\",\n        data: \"A\",\n      });\n\n      let keyB = \"b\";\n      let B = await t.fetch(\"/fetch-b\", keyB);\n      await B.loaders.fetchB.resolve(\"B\");\n      expect(t.fetchers[keyB]).toMatchObject({\n        state: \"idle\",\n        data: \"B\",\n      });\n\n      // Fetch again for B\n      let B2 = await t.fetch(\"/fetch-b\", keyB);\n      expect(t.fetchers[keyB]).toMatchObject({\n        state: \"loading\",\n        data: \"B\",\n      });\n\n      // Start another fetcher which will not resolve prior to the action\n      let keyC = \"c\";\n      let C = await t.fetch(\"/fetch-c\", keyC);\n      expect(t.fetchers[keyC]).toMatchObject({\n        state: \"loading\",\n        data: undefined,\n      });\n\n      // Navigation should cancel fetcher and since it has no data\n      // shouldRevalidate should be ignored on subsequent fetch\n      let D = await t.navigate(\"/action\", {\n        formMethod: \"post\",\n        formData: createFormData({}),\n      });\n      // Add a helper for the fetcher that will be revalidating\n      t.shimHelper(D.loaders, \"navigation\", \"loader\", \"fetchA\");\n      t.shimHelper(D.loaders, \"navigation\", \"loader\", \"fetchB\");\n      t.shimHelper(D.loaders, \"navigation\", \"loader\", \"fetchC\");\n\n      // Fetcher load aborted and still in a loading state\n      expect(t.router.state.navigation.state).toBe(\"submitting\");\n      expect(A.loaders.fetchA.signal.aborted).toBe(false);\n      expect(B.loaders.fetchB.signal.aborted).toBe(false);\n      expect(B2.loaders.fetchB.signal.aborted).toBe(true);\n      expect(C.loaders.fetchC.signal.aborted).toBe(true);\n      expect(t.fetchers[keyA].state).toBe(\"idle\");\n      expect(t.fetchers[keyB].state).toBe(\"loading\");\n      expect(t.fetchers[keyC].state).toBe(\"loading\");\n      await B.loaders.fetchB.resolve(\"B\"); // ignored due to abort\n      await C.loaders.fetchC.resolve(\"C\"); // ignored due to abort\n\n      // Resolve the action\n      await D.actions.action.resolve(\"ACTION\");\n      expect(t.router.state.navigation.state).toBe(\"loading\");\n      expect(t.fetchers[keyA].state).toBe(\"idle\");\n      expect(t.fetchers[keyB].state).toBe(\"loading\");\n      expect(t.fetchers[keyC].state).toBe(\"loading\");\n\n      // Resolve fetcher loader\n      await D.loaders.fetchB.resolve(\"B2\");\n      await D.loaders.fetchC.resolve(\"C\");\n      expect(t.router.state.navigation.state).toBe(\"idle\");\n      expect(t.fetchers[keyA]).toMatchObject({\n        state: \"idle\",\n        data: \"A\",\n      });\n      expect(t.fetchers[keyB]).toMatchObject({\n        state: \"idle\",\n        data: \"B2\",\n      });\n      expect(t.fetchers[keyC]).toMatchObject({\n        state: \"idle\",\n        data: \"C\",\n      });\n    });\n\n    it(\"cancels in-flight fetcher.loads on fetcher.action submission and forces reload (one time)\", async () => {\n      let t = setup({\n        routes: [\n          {\n            id: \"root\",\n            path: \"/\",\n            children: [\n              {\n                id: \"index\",\n                index: true,\n              },\n              {\n                id: \"page\",\n                path: \"page\",\n                action: true,\n              },\n              {\n                id: \"action\",\n                path: \"action\",\n                action: true,\n              },\n              {\n                id: \"fetch\",\n                path: \"fetch\",\n                loader: true,\n                shouldRevalidate: () => false,\n              },\n            ],\n          },\n        ],\n      });\n      expect(t.router.state.navigation).toBe(IDLE_NAVIGATION);\n\n      // Kick off a fetch from the root route\n      let keyA = \"a\";\n      let A = await t.fetch(\"/fetch\", keyA, \"root\");\n      expect(t.fetchers[keyA]).toMatchObject({\n        state: \"loading\",\n        data: undefined,\n      });\n\n      // Interrupt with a fetcher submission which will trigger a forced\n      // revalidation, ignoring shouldRevalidate=false since the prior load\n      // was interrupted\n      let keyB = \"b\";\n      let B = await t.fetch(\"/action\", keyB, {\n        formMethod: \"post\",\n        formData: createFormData({}),\n      });\n      t.shimHelper(B.loaders, \"fetch\", \"loader\", \"fetch\");\n      expect(t.fetchers[keyB]).toMatchObject({\n        state: \"submitting\",\n        data: undefined,\n      });\n\n      // Resolving the first load is a no-op since it would have been aborted\n      await A.loaders.fetch.resolve(\"A LOADER\");\n      expect(t.fetchers[keyA]).toMatchObject({\n        state: \"loading\",\n        data: undefined,\n      });\n\n      // Resolve the action, kicking off revalidations which will force the\n      // fetcher \"a\" load to run again even through shouldRevalidate=false\n      await B.actions.action.resolve(\"B ACTION\");\n      expect(t.fetchers[keyB]).toMatchObject({\n        state: \"loading\",\n        data: \"B ACTION\",\n      });\n      expect(t.fetchers[keyA]).toMatchObject({\n        state: \"loading\",\n        data: undefined,\n      });\n\n      // Resolve the second loader kicked off after the action\n      await B.loaders.fetch.resolve(\"B LOADER\");\n      expect(t.fetchers[keyA]).toMatchObject({\n        state: \"idle\",\n        data: \"B LOADER\",\n      });\n\n      // submitting to another route\n      let C = await t.navigate(\"/page\", {\n        formMethod: \"post\",\n        formData: createFormData({}),\n      });\n      t.shimHelper(C.loaders, \"navigation\", \"loader\", \"fetch\");\n      expect(t.router.state.navigation.location?.pathname).toBe(\"/page\");\n\n      // should not trigger a fetcher reload due to shouldRevalidate=false\n      // This fixes a prior bug where we didn't remove the fetcher from\n      // cancelledFetcherLoaders upon the forced revalidation\n      await C.actions.page.resolve(\"PAGE ACTION\");\n      expect(t.router.state.location.pathname).toBe(\"/page\");\n      expect(t.router.state.navigation).toBe(IDLE_NAVIGATION);\n      expect(t.fetchers[keyA]).toMatchObject({\n        state: \"idle\",\n        data: \"B LOADER\",\n      });\n      expect(C.loaders.fetch.stub).not.toHaveBeenCalled();\n    });\n\n    // This is another example of the above bug where cancelled fetchers were not\n    // cleaned up correctly (https://github.com/remix-run/remix/issues/8298).\n    // It was also fixed by https://github.com/remix-run/react-router/pull/11839\n    it(\"Remix Github Issue 8298\", async () => {\n      let loaderCount = 0;\n      let router = createRouter({\n        history: createMemoryHistory(),\n        routes: [\n          {\n            id: \"index\",\n            path: \"/\",\n          },\n          {\n            id: \"loader\",\n            path: \"/loader\",\n            async loader() {\n              let count = ++loaderCount;\n              await sleep(100);\n              return count;\n            },\n          },\n          {\n            id: \"action\",\n            path: \"/action\",\n            async action() {\n              await sleep(100);\n              return \"ACTION\";\n            },\n          },\n        ],\n      });\n      router.initialize();\n\n      let fetcherData = new Map<string, unknown>();\n      router.subscribe((state) => {\n        state.fetchers.forEach((fetcher, key) => {\n          fetcherData.set(key, fetcher.data);\n        });\n      });\n\n      router.fetch(\"a\", \"index\", \"/loader\");\n      expect(router.getFetcher(\"a\")).toMatchObject({ state: \"loading\" });\n\n      await sleep(250);\n      router.revalidate();\n\n      await sleep(250);\n      router.fetch(\"b\", \"index\", \"/action\", {\n        formMethod: \"post\",\n        body: createFormData({}),\n      });\n      expect(router.getFetcher(\"b\")).toMatchObject({ state: \"submitting\" });\n\n      await sleep(250);\n\n      expect(router.getFetcher(\"b\")).toMatchObject({ state: \"idle\" });\n      expect(fetcherData.get(\"b\")).toBe(\"ACTION\");\n\n      // fetcher load, router revalidation, action revalidation\n      expect(router.getFetcher(\"a\")).toMatchObject({ state: \"idle\" });\n      expect(fetcherData.get(\"a\")).toBe(3);\n    });\n\n    it(\"does not cancel pending action navigation on deletion of revalidating fetcher\", async () => {\n      let t = setup({\n        routes: TASK_ROUTES,\n        initialEntries: [\"/\"],\n        hydrationData: { loaderData: { root: \"ROOT\", index: \"INDEX\" } },\n      });\n      expect(t.router.state.navigation).toBe(IDLE_NAVIGATION);\n\n      let key1 = \"key1\";\n      let A = await t.fetch(\"/tasks/1\", key1);\n      await A.loaders.tasksId.resolve(\"TASKS 1\");\n\n      let C = await t.navigate(\"/tasks\", {\n        formMethod: \"post\",\n        formData: createFormData({}),\n      });\n      // Add a helper for the fetcher that will be revalidating\n      t.shimHelper(C.loaders, \"navigation\", \"loader\", \"tasksId\");\n\n      // Resolve the action\n      await C.actions.tasks.resolve(\"TASKS ACTION\");\n\n      // Fetcher should go back into a loading state\n      expect(t.fetchers[key1]).toMatchObject({\n        state: \"loading\",\n        data: \"TASKS 1\",\n      });\n\n      // Delete fetcher in the middle of the revalidation\n      t.router.deleteFetcher(key1);\n      expect(t.fetchers[key1]).toMatchObject({\n        state: \"loading\",\n        data: \"TASKS 1\",\n      });\n\n      // Resolve navigation loaders\n      await C.loaders.root.resolve(\"ROOT*\");\n      await C.loaders.tasks.resolve(\"TASKS LOADER\");\n      await C.loaders.tasksId.resolve(\"TASKS 2\");\n\n      expect(t.router.state).toMatchObject({\n        actionData: {\n          tasks: \"TASKS ACTION\",\n        },\n        errors: null,\n        loaderData: {\n          tasks: \"TASKS LOADER\",\n          root: \"ROOT*\",\n        },\n      });\n      expect(t.fetchers[key1]).toMatchObject({\n        state: \"idle\",\n        data: \"TASKS 2\",\n      });\n      expect(t.router.state.fetchers.size).toBe(0);\n    });\n\n    it(\"does not cancel pending loader navigation on deletion of revalidating fetcher\", async () => {\n      let t = setup({\n        routes: TASK_ROUTES,\n        initialEntries: [\"/\"],\n        hydrationData: { loaderData: { root: \"ROOT\", index: \"INDEX\" } },\n      });\n      expect(t.router.state.navigation).toBe(IDLE_NAVIGATION);\n\n      let key1 = \"key1\";\n      let A = await t.fetch(\"/tasks/1\", key1);\n      await A.loaders.tasksId.resolve(\"TASKS 1\");\n\n      // Submission navigation to trigger revalidations\n      let C = await t.navigate(\n        \"/tasks\",\n        {\n          formMethod: \"post\",\n          formData: createFormData({}),\n        },\n        [\"tasksId\"],\n      );\n      await C.actions.tasks.resolve(\"TASKS ACTION\");\n\n      // Fetcher should go back into a loading state\n      expect(t.fetchers[key1]).toMatchObject({\n        state: \"loading\",\n        data: \"TASKS 1\",\n      });\n\n      // Delete fetcher in the middle of the revalidation\n      t.router.deleteFetcher(key1);\n      expect(t.fetchers[key1]).toMatchObject({\n        state: \"loading\",\n        data: \"TASKS 1\",\n      });\n\n      // Resolve navigation action/loaders\n      await C.loaders.root.resolve(\"ROOT*\");\n      await C.loaders.tasks.resolve(\"TASKS LOADER\");\n      await C.loaders.tasksId.resolve(\"TASKS 2\");\n\n      expect(t.router.state).toMatchObject({\n        errors: null,\n        navigation: IDLE_NAVIGATION,\n        actionData: {\n          tasks: \"TASKS ACTION\",\n        },\n        loaderData: {\n          tasks: \"TASKS LOADER\",\n          root: \"ROOT*\",\n        },\n      });\n      expect(t.fetchers[key1]).toMatchObject({\n        state: \"idle\",\n        data: \"TASKS 2\",\n      });\n      expect(t.router.state.fetchers.size).toBe(0);\n    });\n\n    it(\"does not cancel pending router.revalidate() on deletion of revalidating fetcher\", async () => {\n      let t = setup({\n        routes: TASK_ROUTES,\n        initialEntries: [\"/\"],\n        hydrationData: { loaderData: { root: \"ROOT\", index: \"INDEX\" } },\n      });\n      expect(t.router.state.navigation).toBe(IDLE_NAVIGATION);\n\n      let key1 = \"key1\";\n      let A = await t.fetch(\"/tasks/1\", key1);\n      await A.loaders.tasksId.resolve(\"TASKS 1\");\n\n      // Trigger revalidations\n      let C = await t.revalidate(\"navigation\", \"tasksId\");\n\n      // Fetcher should not go back into a loading state since it's a revalidation\n      expect(t.fetchers[key1]).toMatchObject({\n        state: \"idle\",\n        data: \"TASKS 1\",\n      });\n\n      // Delete fetcher in the middle of the revalidation\n      t.router.deleteFetcher(key1);\n      expect(t.fetchers[key1]).toMatchObject({\n        state: \"idle\",\n        data: \"TASKS 1\",\n      });\n\n      // Resolve navigation loaders\n      await C.loaders.root.resolve(\"ROOT*\");\n      await C.loaders.index.resolve(\"INDEX*\");\n      await C.loaders.tasksId.resolve(\"TASKS 2\");\n\n      expect(t.router.state).toMatchObject({\n        errors: null,\n        loaderData: {\n          root: \"ROOT*\",\n          index: \"INDEX*\",\n        },\n      });\n      expect(t.fetchers[key1]).toMatchObject({\n        state: \"idle\",\n        data: \"TASKS 2\",\n      });\n      expect(t.router.state.fetchers.size).toBe(0);\n    });\n\n    it(\"does not cancel pending fetcher submission on deletion of revalidating fetcher\", async () => {\n      let key = \"key\";\n      let actionKey = \"actionKey\";\n      let t = setup({\n        routes: TASK_ROUTES,\n        initialEntries: [\"/\"],\n        hydrationData: { loaderData: { root: \"ROOT\", index: \"INDEX\" } },\n      });\n\n      // Load a fetcher\n      let A = await t.fetch(\"/tasks/1\", key);\n      await A.loaders.tasksId.resolve(\"TASKS ID\");\n\n      // Submit a fetcher, leaves loaded fetcher untouched\n      let C = await t.fetch(\"/tasks\", actionKey, {\n        formMethod: \"post\",\n        formData: createFormData({}),\n      });\n\n      // After action resolves, both fetchers go into a loading state, with\n      // the load fetcher still reflecting it's stale data\n      await C.actions.tasks.resolve(\"TASKS ACTION\");\n      expect(t.fetchers[key]).toMatchObject({\n        state: \"loading\",\n        data: \"TASKS ID\",\n      });\n      expect(t.router.getFetcher(actionKey)).toMatchObject({\n        state: \"loading\",\n        data: \"TASKS ACTION\",\n      });\n\n      // Delete fetcher in the middle of the revalidation\n      t.router.deleteFetcher(key);\n      expect(t.fetchers[key]).toMatchObject({\n        state: \"loading\",\n        data: \"TASKS ID\",\n      });\n\n      // Resolve only active route loaders since fetcher was deleted\n      await C.loaders.root.resolve(\"ROOT*\");\n      await C.loaders.index.resolve(\"INDEX*\");\n\n      expect(t.router.state.loaderData).toMatchObject({\n        root: \"ROOT*\",\n        index: \"INDEX*\",\n      });\n      expect(t.router.getFetcher(key)).toBe(IDLE_FETCHER);\n      expect(t.fetchers[actionKey]).toMatchObject({\n        state: \"idle\",\n        data: \"TASKS ACTION\",\n      });\n    });\n\n    it(\"handles revalidating fetcher when the triggering fetcher is deleted\", async () => {\n      let key = \"key\";\n      let actionKey = \"actionKey\";\n      let t = setup({\n        routes: [\n          {\n            id: \"root\",\n            path: \"/\",\n            children: [\n              {\n                id: \"home\",\n                index: true,\n                loader: true,\n              },\n              {\n                id: \"action\",\n                path: \"action\",\n                action: true,\n              },\n              {\n                id: \"fetch\",\n                path: \"fetch\",\n                loader: true,\n              },\n            ],\n          },\n        ],\n        hydrationData: { loaderData: { home: \"HOME\" } },\n      });\n\n      // Load a fetcher\n      let A = await t.fetch(\"/fetch\", key);\n      await A.loaders.fetch.resolve(\"FETCH\");\n\n      // Submit a different fetcher, which will trigger revalidation\n      let B = await t.fetch(\"/action\", actionKey, {\n        formMethod: \"post\",\n        formData: createFormData({}),\n      });\n      t.shimHelper(B.loaders, \"fetch\", \"loader\", \"fetch\");\n\n      // After action resolves, both fetchers go into a loading state\n      await B.actions.action.resolve(\"ACTION\");\n      expect(t.router.getFetcher(key)?.state).toBe(\"loading\");\n      expect(t.router.getFetcher(actionKey)?.state).toBe(\"loading\");\n\n      // Remove the submitting fetcher (assume it's component unmounts)\n      t.router.deleteFetcher(actionKey);\n\n      await B.loaders.home.resolve(\"HOME*\");\n      await B.loaders.fetch.resolve(\"FETCH*\");\n\n      expect(t.router.state.loaderData).toEqual({ home: \"HOME*\" });\n      expect(t.fetchers[key]).toMatchObject({\n        state: \"idle\",\n        data: \"FETCH*\",\n      });\n      expect(t.router.getFetcher(actionKey)).toBe(IDLE_FETCHER);\n    });\n\n    it(\"does not call shouldRevalidate on POST navigation if fetcher has not yet loaded\", async () => {\n      // This is specifically for a Remix use case where the initial fetcher.load\n      // call hasn't completed (and hasn't even loaded the route module yet), so\n      // there isn't even a shouldRevalidate implementation to access yet.  If\n      // there's no data it should just interrupt the existing load and load again,\n      // it's not a \"revalidation\"\n      let spy = jest.fn(() => true);\n      let t = setup({\n        routes: [\n          {\n            id: \"root\",\n            path: \"/\",\n            children: [\n              {\n                index: true,\n              },\n              {\n                id: \"page\",\n                path: \"page\",\n                action: true,\n              },\n            ],\n          },\n          {\n            id: \"fetch\",\n            path: \"/fetch\",\n            loader: true,\n            shouldRevalidate: spy,\n          },\n        ],\n      });\n\n      let key = \"key\";\n      let A = await t.fetch(\"/fetch\", key, \"root\");\n      expect(t.router.getFetcher(key)?.state).toBe(\"loading\");\n\n      // This should trigger an automatic revalidation of the fetcher since it\n      // hasn't loaded yet\n      let B = await t.navigate(\n        \"/page\",\n        { formMethod: \"post\", body: createFormData({}) },\n        [\"fetch\"],\n      );\n      await B.actions.page.resolve(\"ACTION\");\n      expect(t.router.getFetcher(key)?.state).toBe(\"loading\");\n      expect(A.loaders.fetch.signal.aborted).toBe(true);\n      expect(B.loaders.fetch.signal.aborted).toBe(false);\n\n      // No-op since the original call was aborted\n      await A.loaders.fetch.resolve(\"A\");\n      expect(t.router.getFetcher(key)?.state).toBe(\"loading\");\n\n      // Complete the navigation\n      await B.loaders.fetch.resolve(\"B\");\n      expect(t.router.state.navigation.state).toBe(\"idle\");\n      expect(t.fetchers[key]).toMatchObject({\n        state: \"idle\",\n        data: \"B\",\n      });\n      expect(spy).not.toHaveBeenCalled();\n    });\n\n    it(\"does not trigger revalidation on GET navigation if fetcher has not yet loaded\", async () => {\n      let spy = jest.fn(() => true);\n      let t = setup({\n        routes: [\n          {\n            id: \"root\",\n            path: \"/\",\n            children: [\n              {\n                index: true,\n              },\n              {\n                id: \"page\",\n                path: \"page\",\n                loader: true,\n              },\n            ],\n          },\n          {\n            id: \"fetch\",\n            path: \"/fetch\",\n            loader: true,\n            shouldRevalidate: spy,\n          },\n        ],\n      });\n\n      let key = \"key\";\n      let A = await t.fetch(\"/fetch\", key, \"root\");\n      expect(t.router.getFetcher(key)?.state).toBe(\"loading\");\n\n      let B = await t.navigate(\"/page\");\n      expect(t.router.getFetcher(key)?.state).toBe(\"loading\");\n      expect(A.loaders.fetch.signal.aborted).toBe(false);\n\n      await A.loaders.fetch.resolve(\"A\");\n      expect(t.router.getFetcher(key)?.state).toBe(\"idle\");\n\n      // Complete the navigation\n      await B.loaders.page.resolve(\"PAGE\");\n      expect(t.router.state.navigation.state).toBe(\"idle\");\n      expect(t.fetchers[key]).toMatchObject({\n        state: \"idle\",\n        data: \"A\",\n      });\n      expect(spy).not.toHaveBeenCalled();\n    });\n  });\n\n  describe(\"fetcher ?index params\", () => {\n    it(\"hits the proper Routes when ?index params are present\", async () => {\n      let t = setup({\n        routes: [\n          {\n            id: \"parent\",\n            path: \"parent\",\n            action: true,\n            loader: true,\n            // Turn off revalidation after fetcher action submission for this test\n            shouldRevalidate: () => false,\n            children: [\n              {\n                id: \"index\",\n                index: true,\n                action: true,\n                loader: true,\n                // Turn off revalidation after fetcher action submission for this test\n                shouldRevalidate: () => false,\n              },\n            ],\n          },\n        ],\n        initialEntries: [\"/parent\"],\n        hydrationData: { loaderData: { parent: \"PARENT\", index: \"INDEX\" } },\n      });\n\n      let key = \"KEY\";\n\n      // fetcher.load()\n      let A = await t.fetch(\"/parent\", key);\n      await A.loaders.parent.resolve(\"PARENT LOADER\");\n      expect(t.fetchers[key].data).toBe(\"PARENT LOADER\");\n\n      let B = await t.fetch(\"/parent?index\", key);\n      await B.loaders.index.resolve(\"INDEX LOADER\");\n      expect(t.fetchers[key].data).toBe(\"INDEX LOADER\");\n\n      // fetcher.submit({}, { method: 'get' })\n      let C = await t.fetch(\"/parent\", key, {\n        formMethod: \"get\",\n        formData: createFormData({}),\n      });\n      await C.loaders.parent.resolve(\"PARENT LOADER\");\n      expect(t.fetchers[key].data).toBe(\"PARENT LOADER\");\n\n      let D = await t.fetch(\"/parent?index\", key, {\n        formMethod: \"get\",\n        formData: createFormData({}),\n      });\n      await D.loaders.index.resolve(\"INDEX LOADER\");\n      expect(t.fetchers[key].data).toBe(\"INDEX LOADER\");\n\n      // fetcher.submit({}, { method: 'post' })\n      let E = await t.fetch(\"/parent\", key, {\n        formMethod: \"post\",\n        formData: createFormData({}),\n      });\n      await E.actions.parent.resolve(\"PARENT ACTION\");\n      expect(t.fetchers[key].data).toBe(\"PARENT ACTION\");\n\n      let F = await t.fetch(\"/parent?index\", key, {\n        formMethod: \"post\",\n        formData: createFormData({}),\n      });\n      await F.actions.index.resolve(\"INDEX ACTION\");\n      expect(t.fetchers[key].data).toBe(\"INDEX ACTION\");\n    });\n\n    it(\"throws a 404 ErrorResponse without ?index and parent route has no loader\", async () => {\n      let t = setup({\n        routes: [\n          {\n            id: \"parent\",\n            path: \"parent\",\n            children: [\n              {\n                id: \"index\",\n                index: true,\n                loader: true,\n              },\n            ],\n          },\n        ],\n        initialEntries: [\"/parent\"],\n        hydrationData: { loaderData: { index: \"INDEX\" } },\n      });\n\n      let key = \"KEY\";\n      await t.fetch(\"/parent\");\n      await tick();\n      expect(t.router.state.errors).toMatchInlineSnapshot(`\n        {\n          \"parent\": ErrorResponseImpl {\n            \"data\": \"Error: No route matches URL \"/parent\"\",\n            \"error\": [Error: No route matches URL \"/parent\"],\n            \"internal\": true,\n            \"status\": 404,\n            \"statusText\": \"Not Found\",\n          },\n        }\n      `);\n      expect(t.router.getFetcher(key).data).toBe(undefined);\n    });\n\n    it(\"throws a 404 ErrorResponse with ?index and index route has no loader\", async () => {\n      let t = setup({\n        routes: [\n          {\n            id: \"parent\",\n            path: \"parent\",\n            loader: true,\n            children: [\n              {\n                id: \"index\",\n                index: true,\n              },\n            ],\n          },\n        ],\n        initialEntries: [\"/parent\"],\n        hydrationData: { loaderData: { parent: \"PARENT\" } },\n      });\n\n      let key = \"KEY\";\n      await t.fetch(\"/parent?index\");\n      await tick();\n      expect(t.router.state.errors).toMatchInlineSnapshot(`\n        {\n          \"parent\": ErrorResponseImpl {\n            \"data\": \"Error: No route matches URL \"/parent?index\"\",\n            \"error\": [Error: No route matches URL \"/parent?index\"],\n            \"internal\": true,\n            \"status\": 404,\n            \"statusText\": \"Not Found\",\n          },\n        }\n      `);\n      expect(t.router.getFetcher(key).data).toBe(undefined);\n    });\n\n    it(\"throws a 405 ErrorResponse without ?index and parent route has no action\", async () => {\n      let t = setup({\n        routes: [\n          {\n            id: \"parent\",\n            path: \"parent\",\n            children: [\n              {\n                id: \"index\",\n                index: true,\n                action: true,\n              },\n            ],\n          },\n        ],\n        initialEntries: [\"/parent\"],\n      });\n\n      let key = \"KEY\";\n      await t.fetch(\"/parent\", {\n        formMethod: \"post\",\n        formData: createFormData({}),\n      });\n      expect(t.router.state.errors).toMatchInlineSnapshot(`\n        {\n          \"parent\": ErrorResponseImpl {\n            \"data\": \"Error: You made a POST request to \"/parent\" but did not provide an \\`action\\` for route \"parent\", so there is no way to handle the request.\",\n            \"error\": [Error: You made a POST request to \"/parent\" but did not provide an \\`action\\` for route \"parent\", so there is no way to handle the request.],\n            \"internal\": true,\n            \"status\": 405,\n            \"statusText\": \"Method Not Allowed\",\n          },\n        }\n      `);\n      expect(t.router.getFetcher(key).data).toBe(undefined);\n    });\n\n    it(\"throws a 405 ErrorResponse with ?index and index route has no action\", async () => {\n      let t = setup({\n        routes: [\n          {\n            id: \"parent\",\n            path: \"parent\",\n            action: true,\n            children: [\n              {\n                id: \"index\",\n                index: true,\n              },\n            ],\n          },\n        ],\n        initialEntries: [\"/parent\"],\n      });\n\n      let key = \"KEY\";\n      await t.fetch(\"/parent?index\", {\n        formMethod: \"post\",\n        formData: createFormData({}),\n      });\n      expect(t.router.state.errors).toMatchInlineSnapshot(`\n        {\n          \"parent\": ErrorResponseImpl {\n            \"data\": \"Error: You made a POST request to \"/parent?index\" but did not provide an \\`action\\` for route \"parent\", so there is no way to handle the request.\",\n            \"error\": [Error: You made a POST request to \"/parent?index\" but did not provide an \\`action\\` for route \"parent\", so there is no way to handle the request.],\n            \"internal\": true,\n            \"status\": 405,\n            \"statusText\": \"Method Not Allowed\",\n          },\n        }\n      `);\n      expect(t.router.getFetcher(key).data).toBe(undefined);\n    });\n  });\n\n  describe(\"fetcher submissions\", () => {\n    it(\"serializes body as application/x-www-form-urlencoded\", async () => {\n      let t = setup({\n        routes: [{ id: \"root\", path: \"/\", action: true }],\n      });\n\n      let body = { a: \"1\" };\n      let F = await t.fetch(\"/\", \"key\", {\n        formMethod: \"post\",\n        formEncType: \"application/x-www-form-urlencoded\",\n        body,\n      });\n      expect(t.router.getFetcher(\"key\")?.formData?.get(\"a\")).toBe(\"1\");\n\n      await F.actions.root.resolve(\"ACTION\");\n\n      expect(F.actions.root.stub).toHaveBeenCalledWith({\n        params: {},\n        request: expect.any(Request),\n        unstable_pattern: expect.any(String),\n        context: {},\n      });\n\n      let request = F.actions.root.stub.mock.calls[0][0].request;\n      expect(request.method).toBe(\"POST\");\n      expect(request.url).toBe(\"http://localhost/\");\n      expect(request.headers.get(\"Content-Type\")).toBe(\n        \"application/x-www-form-urlencoded;charset=UTF-8\",\n      );\n      expect((await request.formData()).get(\"a\")).toBe(\"1\");\n    });\n\n    it(\"serializes body as application/json if specified (object)\", async () => {\n      let t = setup({\n        routes: [{ id: \"root\", path: \"/\", action: true }],\n      });\n\n      let body = { a: \"1\" };\n      let F = await t.fetch(\"/\", \"key\", {\n        formMethod: \"post\",\n        formEncType: \"application/json\",\n        body,\n      });\n      expect(t.router.getFetcher(\"key\")?.json).toBe(body);\n      await F.actions.root.resolve(\"ACTION\");\n\n      expect(F.actions.root.stub).toHaveBeenCalledWith({\n        params: {},\n        request: expect.any(Request),\n        unstable_pattern: expect.any(String),\n        context: {},\n      });\n\n      let request = F.actions.root.stub.mock.calls[0][0].request;\n      expect(request.method).toBe(\"POST\");\n      expect(request.url).toBe(\"http://localhost/\");\n      expect(request.headers.get(\"Content-Type\")).toBe(\"application/json\");\n      expect(await request.json()).toEqual(body);\n    });\n\n    it(\"serializes body as application/json if specified (array)\", async () => {\n      let t = setup({\n        routes: [{ id: \"root\", path: \"/\", action: true }],\n      });\n\n      let body = [1, 2, 3];\n      let F = await t.fetch(\"/\", \"key\", {\n        formMethod: \"post\",\n        formEncType: \"application/json\",\n        body,\n      });\n      expect(t.router.getFetcher(\"key\")?.json).toBe(body);\n      await F.actions.root.resolve(\"ACTION\");\n\n      expect(F.actions.root.stub).toHaveBeenCalledWith({\n        params: {},\n        request: expect.any(Request),\n        unstable_pattern: expect.any(String),\n        context: {},\n      });\n\n      let request = F.actions.root.stub.mock.calls[0][0].request;\n      expect(request.method).toBe(\"POST\");\n      expect(request.url).toBe(\"http://localhost/\");\n      expect(request.headers.get(\"Content-Type\")).toBe(\"application/json\");\n      expect(await request.json()).toEqual(body);\n    });\n\n    it(\"serializes body as application/json if specified (null)\", async () => {\n      let t = setup({\n        routes: [{ id: \"root\", path: \"/\", action: true }],\n      });\n\n      let body = null;\n      let F = await t.fetch(\"/\", \"key\", {\n        formMethod: \"post\",\n        formEncType: \"application/json\",\n        body,\n      });\n      expect(t.router.getFetcher(\"key\")?.json).toBe(body);\n      await F.actions.root.resolve(\"ACTION\");\n\n      expect(F.actions.root.stub).toHaveBeenCalledWith({\n        params: {},\n        request: expect.any(Request),\n        unstable_pattern: expect.any(String),\n        context: {},\n      });\n\n      let request = F.actions.root.stub.mock.calls[0][0].request;\n      expect(request.method).toBe(\"POST\");\n      expect(request.url).toBe(\"http://localhost/\");\n      expect(request.headers.get(\"Content-Type\")).toBe(\"application/json\");\n      expect(await request.json()).toEqual(body);\n    });\n\n    it(\"serializes body as text/plain if specified\", async () => {\n      let t = setup({\n        routes: [{ id: \"root\", path: \"/\", action: true }],\n      });\n\n      let body = \"plain text\";\n      let F = await t.fetch(\"/\", \"key\", {\n        formMethod: \"post\",\n        formEncType: \"text/plain\",\n        body,\n      });\n      expect(t.router.getFetcher(\"key\")?.text).toBe(body);\n\n      await F.actions.root.resolve(\"ACTION\");\n\n      expect(F.actions.root.stub).toHaveBeenCalledWith({\n        params: {},\n        request: expect.any(Request),\n        unstable_pattern: expect.any(String),\n        context: {},\n      });\n\n      let request = F.actions.root.stub.mock.calls[0][0].request;\n      expect(request.method).toBe(\"POST\");\n      expect(request.url).toBe(\"http://localhost/\");\n      expect(request.headers.get(\"Content-Type\")).toBe(\n        \"text/plain;charset=UTF-8\",\n      );\n      expect(await request.text()).toEqual(body);\n    });\n\n    it(\"serializes body as text/plain if specified (empty string)\", async () => {\n      let t = setup({\n        routes: [{ id: \"root\", path: \"/\", action: true }],\n      });\n\n      let body = \"\";\n      let F = await t.fetch(\"/\", \"key\", {\n        formMethod: \"post\",\n        formEncType: \"text/plain\",\n        body,\n      });\n      expect(t.router.getFetcher(\"key\")?.text).toBe(body);\n\n      await F.actions.root.resolve(\"ACTION\");\n\n      expect(F.actions.root.stub).toHaveBeenCalledWith({\n        params: {},\n        request: expect.any(Request),\n        unstable_pattern: expect.any(String),\n        context: {},\n      });\n\n      let request = F.actions.root.stub.mock.calls[0][0].request;\n      expect(request.method).toBe(\"POST\");\n      expect(request.url).toBe(\"http://localhost/\");\n      expect(request.headers.get(\"Content-Type\")).toBe(\n        \"text/plain;charset=UTF-8\",\n      );\n      expect(await request.text()).toEqual(body);\n    });\n\n    it(\"serializes body when encType=undefined\", async () => {\n      let t = setup({\n        routes: [{ id: \"root\", path: \"/\", action: true }],\n      });\n\n      let body = { a: \"1\" };\n      let F = await t.fetch(\"/\", \"key\", {\n        formMethod: \"post\",\n        body,\n      });\n      expect(t.router.getFetcher(\"key\")?.formData?.get(\"a\")).toBe(\"1\");\n\n      await F.actions.root.resolve(\"ACTION\");\n\n      expect(F.actions.root.stub).toHaveBeenCalledWith({\n        params: {},\n        request: expect.any(Request),\n        unstable_pattern: expect.any(String),\n        context: {},\n      });\n\n      let request = F.actions.root.stub.mock.calls[0][0].request;\n      expect(request.method).toBe(\"POST\");\n      expect(request.url).toBe(\"http://localhost/\");\n      expect(request.headers.get(\"Content-Type\")).toBe(\n        \"application/x-www-form-urlencoded;charset=UTF-8\",\n      );\n      expect((await request.formData()).get(\"a\")).toBe(\"1\");\n    });\n  });\n\n  describe(\"resetFetcher\", () => {\n    it(\"resets fetcher data\", async () => {\n      let t = setup({\n        routes: [\n          { id: \"root\", path: \"/\" },\n          { id: \"fetch\", path: \"/fetch\", loader: true },\n        ],\n      });\n\n      let A = await t.fetch(\"/fetch\", \"a\", \"root\");\n      expect(t.fetchers[\"a\"]).toMatchObject({\n        state: \"loading\",\n        data: undefined,\n      });\n\n      await A.loaders.fetch.resolve(\"FETCH\");\n      expect(t.fetchers[\"a\"]).toMatchObject({\n        state: \"idle\",\n        data: \"FETCH\",\n      });\n\n      t.router.resetFetcher(\"a\");\n      expect(t.fetchers[\"a\"]).toMatchObject({\n        state: \"idle\",\n        data: null,\n      });\n    });\n\n    it(\"aborts in-flight fetchers (first call)\", async () => {\n      let t = setup({\n        routes: [\n          { id: \"root\", path: \"/\" },\n          { id: \"fetch\", path: \"/fetch\", loader: true },\n        ],\n      });\n\n      let A = await t.fetch(\"/fetch\", \"a\", \"root\");\n      expect(t.fetchers[\"a\"]).toMatchObject({\n        state: \"loading\",\n        data: undefined,\n      });\n\n      t.router.resetFetcher(\"a\");\n      expect(t.fetchers[\"a\"]).toMatchObject({\n        state: \"idle\",\n        data: null,\n      });\n\n      // no-op\n      await A.loaders.fetch.resolve(\"FETCH\");\n      expect(t.fetchers[\"a\"]).toMatchObject({\n        state: \"idle\",\n        data: null,\n      });\n      expect(A.loaders.fetch.signal.aborted).toBe(true);\n    });\n\n    it(\"aborts in-flight fetchers (subsequent call)\", async () => {\n      let t = setup({\n        routes: [\n          { id: \"root\", path: \"/\" },\n          { id: \"fetch\", path: \"/fetch\", loader: true },\n        ],\n      });\n\n      let A = await t.fetch(\"/fetch\", \"a\", \"root\");\n      expect(t.fetchers[\"a\"]).toMatchObject({\n        state: \"loading\",\n        data: undefined,\n      });\n\n      await A.loaders.fetch.resolve(\"FETCH\");\n      expect(t.fetchers[\"a\"]).toMatchObject({\n        state: \"idle\",\n        data: \"FETCH\",\n      });\n\n      let B = await t.fetch(\"/fetch\", \"a\", \"root\");\n      expect(t.fetchers[\"a\"]).toMatchObject({\n        state: \"loading\",\n        data: \"FETCH\",\n      });\n\n      t.router.resetFetcher(\"a\");\n      expect(t.fetchers[\"a\"]).toMatchObject({\n        state: \"idle\",\n        data: null,\n      });\n\n      // no-op\n      await B.loaders.fetch.resolve(\"FETCH*\");\n      expect(t.fetchers[\"a\"]).toMatchObject({\n        state: \"idle\",\n        data: null,\n      });\n      expect(B.loaders.fetch.signal.aborted).toBe(true);\n    });\n\n    it(\"passes along the `reason` to the abort controller\", async () => {\n      let t = setup({\n        routes: [\n          { id: \"root\", path: \"/\" },\n          { id: \"fetch\", path: \"/fetch\", loader: true },\n        ],\n      });\n\n      let A = await t.fetch(\"/fetch\", \"a\", \"root\");\n      t.router.resetFetcher(\"a\", { reason: \"BECAUSE I SAID SO\" });\n      expect(A.loaders.fetch.signal.reason).toBe(\"BECAUSE I SAID SO\");\n    });\n  });\n});\n"
  },
  {
    "path": "packages/react-router/__tests__/router/flush-sync-test.ts",
    "content": "import { cleanup, setup } from \"./utils/data-router-setup\";\nimport { createFormData } from \"./utils/utils\";\n\ndescribe(\"flushSync\", () => {\n  // Detect any failures inside the router navigate code\n  afterEach(() => cleanup());\n\n  it(\"supports GET navigations with flushSync\", async () => {\n    let t = setup({\n      routes: [\n        { path: \"/\" },\n        { id: \"a\", path: \"/a\", loader: true },\n        { id: \"b\", path: \"/b\", loader: true },\n      ],\n    });\n    let spy = jest.fn();\n    let unsubscribe = t.router.subscribe(spy);\n\n    let A = await t.navigate(\"/a\");\n    expect(spy).toHaveBeenLastCalledWith(\n      expect.anything(),\n      expect.objectContaining({ flushSync: false }),\n    );\n    await A.loaders.a.resolve(\"A\");\n    expect(spy).toHaveBeenLastCalledWith(\n      expect.anything(),\n      expect.objectContaining({ flushSync: false }),\n    );\n\n    let B = await t.navigate(\"/b\", { flushSync: true });\n    expect(spy).toHaveBeenLastCalledWith(\n      expect.anything(),\n      expect.objectContaining({ flushSync: true }),\n    );\n    await B.loaders.b.resolve(\"B\");\n    expect(spy).toHaveBeenLastCalledWith(\n      expect.anything(),\n      expect.objectContaining({ flushSync: false }),\n    );\n\n    unsubscribe();\n    t.router.dispose();\n  });\n\n  it(\"supports POST navigations with flushSync\", async () => {\n    let t = setup({\n      routes: [\n        { path: \"/\" },\n        { id: \"a\", path: \"/a\", action: true },\n        { id: \"b\", path: \"/b\", action: true },\n      ],\n    });\n    let spy = jest.fn();\n    let unsubscribe = t.router.subscribe(spy);\n\n    let A = await t.navigate(\"/a\", {\n      formMethod: \"post\",\n      formData: createFormData({}),\n    });\n    expect(spy).toHaveBeenLastCalledWith(\n      expect.anything(),\n      expect.objectContaining({ flushSync: false }),\n    );\n    await A.actions.a.resolve(\"A\");\n    expect(spy).toHaveBeenLastCalledWith(\n      expect.anything(),\n      expect.objectContaining({ flushSync: false }),\n    );\n\n    let B = await t.navigate(\"/b\", {\n      formMethod: \"post\",\n      formData: createFormData({}),\n      flushSync: true,\n    });\n    expect(spy).toHaveBeenLastCalledWith(\n      expect.anything(),\n      expect.objectContaining({ flushSync: true }),\n    );\n    await B.actions.b.resolve(\"B\");\n    expect(spy).toHaveBeenLastCalledWith(\n      expect.anything(),\n      expect.objectContaining({ flushSync: false }),\n    );\n\n    unsubscribe();\n    t.router.dispose();\n  });\n\n  it(\"supports fetch loads with flushSync\", async () => {\n    let t = setup({\n      routes: [{ id: \"root\", path: \"/\", loader: true }],\n    });\n    let spy = jest.fn();\n    let unsubscribe = t.router.subscribe(spy);\n\n    let key = \"key\";\n    let A = await t.fetch(\"/\", key);\n    expect(spy).toHaveBeenLastCalledWith(\n      expect.anything(),\n      expect.objectContaining({ flushSync: false }),\n    );\n    expect(t.fetchers[key].state).toBe(\"loading\");\n\n    await A.loaders.root.resolve(\"ROOT\");\n    expect(t.fetchers[key].data).toBe(\"ROOT\");\n\n    let B = await t.fetch(\"/\", key, { flushSync: true });\n    expect(spy).toHaveBeenLastCalledWith(\n      expect.anything(),\n      expect.objectContaining({ flushSync: true }),\n    );\n    expect(t.fetchers[key].state).toBe(\"loading\");\n\n    await B.loaders.root.resolve(\"ROOT2\");\n    expect(t.fetchers[key].data).toBe(\"ROOT2\");\n    expect(spy).toHaveBeenLastCalledWith(\n      expect.anything(),\n      expect.objectContaining({ flushSync: false }),\n    );\n\n    unsubscribe();\n    t.router.dispose();\n  });\n\n  it(\"supports fetch submissions with flushSync\", async () => {\n    let t = setup({\n      routes: [{ id: \"root\", path: \"/\", action: true }],\n    });\n    let spy = jest.fn();\n    let unsubscribe = t.router.subscribe(spy);\n\n    let key = \"key\";\n    let A = await t.fetch(\"/\", key, {\n      formMethod: \"post\",\n      formData: createFormData({}),\n    });\n    expect(spy).toHaveBeenLastCalledWith(\n      expect.anything(),\n      expect.objectContaining({ flushSync: false }),\n    );\n    expect(t.fetchers[key].state).toBe(\"submitting\");\n\n    await A.actions.root.resolve(\"ROOT\");\n    expect(spy).toHaveBeenLastCalledWith(\n      expect.anything(),\n      expect.objectContaining({ flushSync: false }),\n    );\n    expect(t.fetchers[key].data).toBe(\"ROOT\");\n\n    let B = await t.fetch(\"/\", key, {\n      formMethod: \"post\",\n      formData: createFormData({}),\n      flushSync: true,\n    });\n    expect(spy).toHaveBeenLastCalledWith(\n      expect.anything(),\n      expect.objectContaining({ flushSync: true }),\n    );\n    expect(t.fetchers[key].state).toBe(\"submitting\");\n\n    await B.actions.root.resolve(\"ROOT2\");\n    expect(t.fetchers[key].data).toBe(\"ROOT2\");\n    expect(spy).toHaveBeenLastCalledWith(\n      expect.anything(),\n      expect.objectContaining({ flushSync: false }),\n    );\n\n    unsubscribe();\n    t.router.dispose();\n  });\n});\n"
  },
  {
    "path": "packages/react-router/__tests__/router/hash-base-test.ts",
    "content": "import type { HashHistory } from \"../../lib/router/history\";\nimport { createHashHistory } from \"../../lib/router/history\";\nimport getWindow from \"../utils/getWindow\";\n\ndescribe(\"a hash history on a page with a <base> tag\", () => {\n  let history: HashHistory;\n  let base: HTMLBaseElement;\n  let testWindow: Window;\n\n  beforeEach(() => {\n    // Need to use our own custom DOM in order to get a working history\n    testWindow = getWindow(\"/\", true);\n    base = testWindow.document.createElement(\"base\");\n    base.setAttribute(\"href\", \"/prefix\");\n    testWindow.document.head.appendChild(base);\n\n    history = createHashHistory({ window: testWindow });\n  });\n\n  afterEach(() => {\n    testWindow.document.head.removeChild(base);\n  });\n\n  it(\"knows how to create hrefs\", () => {\n    const hashIndex = testWindow.location.href.indexOf(\"#\");\n    const upToHash =\n      hashIndex === -1\n        ? testWindow.location.href\n        : testWindow.location.href.slice(0, hashIndex);\n\n    const href = history.createHref({\n      pathname: \"/the/path\",\n      search: \"?the=query\",\n      hash: \"#the-hash\",\n    });\n\n    expect(href).toEqual(upToHash + \"#/the/path?the=query#the-hash\");\n  });\n});\n"
  },
  {
    "path": "packages/react-router/__tests__/router/hash-test.ts",
    "content": "/* eslint-disable jest/expect-expect */\n\nimport type { HashHistory } from \"../../lib/router/history\";\nimport { createHashHistory } from \"../../lib/router/history\";\n\nimport Listen from \"./TestSequences/Listen\";\nimport InitialLocationDefaultKey from \"./TestSequences/InitialLocationDefaultKey\";\nimport PushNewLocation from \"./TestSequences/PushNewLocation\";\nimport PushSamePath from \"./TestSequences/PushSamePath\";\nimport PushState from \"./TestSequences/PushState\";\nimport PushStateInvalid from \"./TestSequences/PushStateInvalid\";\nimport PushMissingPathname from \"./TestSequences/PushMissingPathname\";\nimport PushRelativePathnameWarning from \"./TestSequences/PushRelativePathnameWarning\";\nimport ReplaceNewLocation from \"./TestSequences/ReplaceNewLocation\";\nimport ReplaceSamePath from \"./TestSequences/ReplaceSamePath\";\nimport ReplaceState from \"./TestSequences/ReplaceState\";\nimport EncodedReservedCharacters from \"./TestSequences/EncodedReservedCharacters\";\nimport GoBack from \"./TestSequences/GoBack\";\nimport GoForward from \"./TestSequences/GoForward\";\nimport ListenPopOnly from \"./TestSequences/ListenPopOnly\";\nimport getWindow from \"../utils/getWindow\";\n\n// TODO: Do we still need this?\n// const canGoWithoutReload = window.navigator.userAgent.indexOf('Firefox') === -1;\n// const describeGo = canGoWithoutReload ? describe : describe.skip;\n\ndescribe(\"a hash history\", () => {\n  let history: HashHistory;\n  let testWindow: Window;\n\n  beforeEach(() => {\n    // Need to use our own custom DOM in order to get a working history\n    testWindow = getWindow(\"/\");\n    history = createHashHistory({ window: testWindow });\n  });\n\n  it(\"knows how to create hrefs from location objects\", () => {\n    const href = history.createHref({\n      pathname: \"/the/path\",\n      search: \"?the=query\",\n      hash: \"#the-hash\",\n    });\n\n    expect(href).toEqual(\"#/the/path?the=query#the-hash\");\n  });\n\n  it(\"knows how to create hrefs from strings\", () => {\n    const href = history.createHref(\"/the/path?the=query#the-hash\");\n    expect(href).toEqual(\"#/the/path?the=query#the-hash\");\n  });\n\n  it(\"does not encode the generated path\", () => {\n    const encodedHref = history.createHref({\n      pathname: \"/%23abc\",\n    });\n    expect(encodedHref).toEqual(\"#/%23abc\");\n\n    const unencodedHref = history.createHref({\n      pathname: \"/#abc\",\n    });\n    expect(unencodedHref).toEqual(\"#/#abc\");\n  });\n\n  it(\"prefixes raw hash values with /\", () => {\n    let spy = jest.spyOn(console, \"warn\").mockImplementation(() => {});\n\n    testWindow.history.replaceState(null, \"\", \"#hello\");\n    history = createHashHistory({ window: testWindow });\n    expect(history.location.pathname).toBe(\"/hello\");\n\n    history.push(\"world\");\n    expect(history.location.pathname).toBe(\"/world\");\n\n    // Not supported but ensure we don't prefix here\n    history.push(\"./relative\");\n    expect(history.location.pathname).toBe(\"./relative\");\n\n    history.push(\"../relative\");\n    expect(history.location.pathname).toBe(\"../relative\");\n\n    spy.mockReset();\n  });\n\n  describe(\"listen\", () => {\n    it(\"does not immediately call listeners\", () => {\n      Listen(history);\n    });\n\n    it(\"calls listeners only for POP actions\", () => {\n      ListenPopOnly(history);\n    });\n  });\n\n  describe(\"the initial location\", () => {\n    it('has the \"default\" key', () => {\n      InitialLocationDefaultKey(history);\n    });\n  });\n\n  describe(\"push a new path\", () => {\n    it(\"calls change listeners with the new location\", () => {\n      PushNewLocation(history);\n    });\n  });\n\n  describe(\"push the same path\", () => {\n    it(\"calls change listeners with the new location\", async () => {\n      await PushSamePath(history);\n    });\n  });\n\n  describe(\"push state\", () => {\n    it(\"calls change listeners with the new location\", () => {\n      PushState(history);\n    });\n\n    it(\"re-throws when using non-serializable state\", () => {\n      PushStateInvalid(history, testWindow);\n    });\n  });\n\n  describe(\"push with no pathname\", () => {\n    it(\"reuses the current location pathname\", () => {\n      PushMissingPathname(history);\n    });\n  });\n\n  describe(\"push with a relative pathname\", () => {\n    it(\"issues a warning\", () => {\n      PushRelativePathnameWarning(history);\n    });\n  });\n\n  describe(\"replace a new path\", () => {\n    it(\"calls change listeners with the new location\", () => {\n      ReplaceNewLocation(history);\n    });\n  });\n\n  describe(\"replace the same path\", () => {\n    it(\"calls change listeners with the new location\", () => {\n      ReplaceSamePath(history);\n    });\n  });\n\n  describe(\"replace state\", () => {\n    it(\"calls change listeners with the new location\", () => {\n      ReplaceState(history);\n    });\n  });\n\n  describe(\"location created with encoded/unencoded reserved characters\", () => {\n    it(\"produces different location objects\", () => {\n      EncodedReservedCharacters(history);\n    });\n  });\n\n  describe(\"back\", () => {\n    it(\"calls change listeners with the previous location\", async () => {\n      await GoBack(history);\n    });\n  });\n\n  describe(\"forward\", () => {\n    it(\"calls change listeners with the next location\", async () => {\n      await GoForward(history);\n    });\n  });\n});\n"
  },
  {
    "path": "packages/react-router/__tests__/router/instrumentation-test.ts",
    "content": "import { createMemoryRouter } from \"../../lib/components\";\nimport { createMemoryHistory } from \"../../lib/router/history\";\nimport type { StaticHandlerContext } from \"../../lib/router/router\";\nimport { createRouter, createStaticHandler } from \"../../lib/router/router\";\nimport {\n  ErrorResponseImpl,\n  data,\n  redirect,\n  type ActionFunction,\n  type LoaderFunction,\n  type MiddlewareFunction,\n  type MiddlewareNextFunction,\n} from \"../../lib/router/utils\";\nimport { createRequestHandler } from \"../../lib/server-runtime/server\";\nimport { mockServerBuild } from \"../server-runtime/utils\";\nimport { cleanup, setup } from \"./utils/data-router-setup\";\nimport { createDeferred, createFormData, tick } from \"./utils/utils\";\n\n// Detect any failures inside the router navigate code\nafterEach(() => {\n  cleanup();\n});\n\ndescribe(\"instrumentation\", () => {\n  describe(\"client-side router\", () => {\n    it(\"allows instrumentation of middleware\", async () => {\n      let spy = jest.fn();\n      let t = setup({\n        routes: [\n          {\n            index: true,\n          },\n          {\n            id: \"page\",\n            path: \"/page\",\n            middleware: [\n              async (_: unknown, next: MiddlewareNextFunction) => {\n                spy(\"start middleware\");\n                await next();\n                spy(\"end middleware\");\n              },\n            ],\n            loader: true,\n          },\n        ],\n        unstable_instrumentations: [\n          {\n            route(route) {\n              route.instrument({\n                async middleware(middleware) {\n                  spy(\"start\");\n                  await middleware();\n                  spy(\"end\");\n                },\n              });\n            },\n          },\n        ],\n      });\n\n      let A = await t.navigate(\"/page\");\n      expect(spy.mock.calls).toEqual([[\"start\"], [\"start middleware\"]]);\n      await A.loaders.page.resolve(\"PAGE\");\n      expect(spy.mock.calls).toEqual([\n        [\"start\"],\n        [\"start middleware\"],\n        [\"end middleware\"],\n        [\"end\"],\n      ]);\n      expect(t.router.state).toMatchObject({\n        navigation: { state: \"idle\" },\n        location: { pathname: \"/page\" },\n        loaderData: { page: \"PAGE\" },\n      });\n    });\n\n    it(\"allows instrumentation of loaders\", async () => {\n      let spy = jest.fn();\n      let t = setup({\n        routes: [\n          {\n            index: true,\n          },\n          {\n            id: \"page\",\n            path: \"/page\",\n            loader: true,\n          },\n        ],\n        unstable_instrumentations: [\n          {\n            route(route) {\n              route.instrument({\n                async loader(loader) {\n                  spy(\"start\");\n                  await loader();\n                  spy(\"end\");\n                },\n              });\n            },\n          },\n        ],\n      });\n\n      let A = await t.navigate(\"/page\");\n      expect(spy).toHaveBeenNthCalledWith(1, \"start\");\n      await A.loaders.page.resolve(\"PAGE\");\n      expect(spy).toHaveBeenNthCalledWith(2, \"end\");\n      expect(t.router.state).toMatchObject({\n        navigation: { state: \"idle\" },\n        location: { pathname: \"/page\" },\n        loaderData: { page: \"PAGE\" },\n      });\n    });\n\n    it(\"preserves hydrate=true on client side loaders\", async () => {\n      let dfd = createDeferred();\n      let spy = jest.fn();\n      function loader() {\n        dfd.resolve();\n        return \"INDEX*\";\n      }\n      loader.hydrate = true;\n      let router = createRouter({\n        history: createMemoryHistory(),\n        routes: [\n          {\n            id: \"index\",\n            index: true,\n            loader,\n          },\n        ],\n        hydrationData: {\n          loaderData: {\n            index: \"INDEX\",\n          },\n        },\n        unstable_instrumentations: [\n          {\n            route(route) {\n              route.instrument({\n                async loader(loader) {\n                  spy(\"start\");\n                  await loader();\n                  spy(\"end\");\n                },\n              });\n            },\n          },\n        ],\n      });\n\n      expect(router.state.initialized).toBe(false);\n      expect(router.state.loaderData).toEqual({ index: \"INDEX\" });\n\n      router.initialize();\n      await dfd.promise;\n      await tick();\n\n      expect(router.state.initialized).toBe(true);\n      expect(router.state.loaderData).toEqual({ index: \"INDEX*\" });\n      expect(spy).toHaveBeenCalledWith(\"start\");\n      expect(spy).toHaveBeenCalledWith(\"end\");\n    });\n\n    it(\"allows instrumentation of actions\", async () => {\n      let spy = jest.fn();\n      let t = setup({\n        routes: [\n          {\n            index: true,\n          },\n          {\n            id: \"page\",\n            path: \"/page\",\n            action: true,\n          },\n        ],\n        unstable_instrumentations: [\n          {\n            route(route) {\n              route.instrument({\n                async action(action) {\n                  spy(\"start\");\n                  await action();\n                  spy(\"end\");\n                },\n              });\n            },\n          },\n        ],\n      });\n\n      let A = await t.navigate(\"/page\", {\n        formMethod: \"POST\",\n        formData: createFormData({}),\n      });\n      expect(spy).toHaveBeenNthCalledWith(1, \"start\");\n      await A.actions.page.resolve(\"PAGE\");\n      expect(spy).toHaveBeenNthCalledWith(2, \"end\");\n      expect(t.router.state).toMatchObject({\n        navigation: { state: \"idle\" },\n        location: { pathname: \"/page\" },\n        actionData: { page: \"PAGE\" },\n      });\n    });\n\n    it(\"allows instrumentation of lazy function\", async () => {\n      let spy = jest.fn();\n      let lazyDfd = createDeferred<{ loader: LoaderFunction }>();\n      let t = setup({\n        routes: [\n          {\n            index: true,\n          },\n          {\n            id: \"page\",\n            path: \"/page\",\n            lazy: () => lazyDfd.promise,\n          },\n        ],\n        unstable_instrumentations: [\n          {\n            route(route) {\n              route.instrument({\n                async lazy(lazy) {\n                  spy(\"start\");\n                  await lazy();\n                  spy(\"end\");\n                },\n              });\n            },\n          },\n        ],\n      });\n\n      await t.navigate(\"/page\");\n      await tick();\n      expect(spy.mock.calls).toEqual([[\"start\"]]);\n\n      await lazyDfd.resolve({ loader: () => \"PAGE\" });\n      await tick();\n      expect(spy.mock.calls).toEqual([[\"start\"], [\"end\"]]);\n      expect(t.router.state).toMatchObject({\n        navigation: { state: \"idle\" },\n        location: { pathname: \"/page\" },\n        loaderData: { page: \"PAGE\" },\n      });\n    });\n\n    it(\"allows instrumentation of lazy function loaders\", async () => {\n      let spy = jest.fn();\n      let lazyDfd = createDeferred<{ loader: LoaderFunction }>();\n      let loaderDfd = createDeferred();\n      let t = setup({\n        routes: [\n          {\n            index: true,\n          },\n          {\n            id: \"page\",\n            path: \"/page\",\n            lazy: () => lazyDfd.promise,\n          },\n        ],\n        unstable_instrumentations: [\n          {\n            route(route) {\n              route.instrument({\n                async loader(loader) {\n                  spy(\"start\");\n                  await loader();\n                  spy(\"end\");\n                },\n              });\n            },\n          },\n        ],\n      });\n\n      await t.navigate(\"/page\");\n      expect(spy).not.toHaveBeenCalled();\n\n      await lazyDfd.resolve({ loader: () => loaderDfd.promise });\n      expect(spy.mock.calls).toEqual([[\"start\"]]);\n\n      await loaderDfd.resolve(\"PAGE\");\n      await tick();\n      expect(spy.mock.calls).toEqual([[\"start\"], [\"end\"]]);\n\n      await tick();\n      expect(t.router.state).toMatchObject({\n        navigation: { state: \"idle\" },\n        location: { pathname: \"/page\" },\n        loaderData: { page: \"PAGE\" },\n      });\n    });\n\n    it(\"allows instrumentation of lazy function actions\", async () => {\n      let spy = jest.fn();\n      let lazyDfd = createDeferred<{ loader: LoaderFunction }>();\n      let actionDfd = createDeferred();\n      let t = setup({\n        routes: [\n          {\n            index: true,\n          },\n          {\n            id: \"page\",\n            path: \"/page\",\n            lazy: () => lazyDfd.promise,\n          },\n        ],\n        unstable_instrumentations: [\n          {\n            route(route) {\n              route.instrument({\n                async action(action) {\n                  spy(\"start\");\n                  await action();\n                  spy(\"end\");\n                },\n              });\n            },\n          },\n        ],\n      });\n\n      await t.navigate(\"/page\", {\n        formMethod: \"POST\",\n        formData: createFormData({}),\n      });\n      expect(spy).not.toHaveBeenCalled();\n\n      await lazyDfd.resolve({ action: () => actionDfd.promise });\n      expect(spy.mock.calls).toEqual([[\"start\"]]);\n\n      await actionDfd.resolve(\"PAGE\");\n      await tick();\n      expect(spy.mock.calls).toEqual([[\"start\"], [\"end\"]]);\n\n      await tick();\n      expect(t.router.state).toMatchObject({\n        navigation: { state: \"idle\" },\n        location: { pathname: \"/page\" },\n        actionData: { page: \"PAGE\" },\n      });\n    });\n\n    it(\"does not double-instrument when a static `loader` is used alongside `lazy()`\", async () => {\n      let spy = jest.fn();\n      let lazyDfd = createDeferred<{ loader: LoaderFunction }>();\n      let warnSpy = jest.spyOn(console, \"warn\").mockImplementation(() => {});\n      let t = setup({\n        routes: [\n          {\n            index: true,\n          },\n          {\n            id: \"page\",\n            path: \"/page\",\n            loader: true,\n            lazy: () => lazyDfd.promise,\n          },\n        ],\n        unstable_instrumentations: [\n          {\n            route(route) {\n              route.instrument({\n                async loader(loader) {\n                  spy(\"start\");\n                  await loader();\n                  spy(\"end\");\n                },\n              });\n            },\n          },\n        ],\n      });\n\n      let A = await t.navigate(\"/page\");\n      expect(spy.mock.calls).toEqual([[\"start\"]]);\n      await lazyDfd.resolve({ action: () => \"ACTION\", loader: () => \"WRONG\" });\n      await A.loaders.page.resolve(\"PAGE\");\n      expect(spy.mock.calls).toEqual([[\"start\"], [\"end\"]]);\n      expect(t.router.state).toMatchObject({\n        navigation: { state: \"idle\" },\n        location: { pathname: \"/page\" },\n        loaderData: { page: \"PAGE\" },\n      });\n      spy.mockClear();\n\n      await t.navigate(\"/\");\n\n      let C = await t.navigate(\"/page\");\n      expect(spy.mock.calls).toEqual([[\"start\"]]);\n      await C.loaders.page.resolve(\"PAGE\");\n      expect(spy.mock.calls).toEqual([[\"start\"], [\"end\"]]);\n      expect(t.router.state).toMatchObject({\n        navigation: { state: \"idle\" },\n        location: { pathname: \"/page\" },\n        loaderData: { page: \"PAGE\" },\n      });\n      spy.mockClear();\n\n      warnSpy.mockRestore();\n    });\n\n    it(\"does not double-instrument when a static `action` is used alongside `lazy()`\", async () => {\n      let spy = jest.fn();\n      let lazyDfd = createDeferred<{ loader: LoaderFunction }>();\n      let warnSpy = jest.spyOn(console, \"warn\").mockImplementation(() => {});\n      let t = setup({\n        routes: [\n          {\n            index: true,\n          },\n          {\n            id: \"page\",\n            path: \"/page\",\n            action: true,\n            lazy: () => lazyDfd.promise,\n          },\n        ],\n        unstable_instrumentations: [\n          {\n            route(route) {\n              route.instrument({\n                async action(action) {\n                  spy(\"start\");\n                  await action();\n                  spy(\"end\");\n                },\n              });\n            },\n          },\n        ],\n      });\n\n      let A = await t.navigate(\"/page\", {\n        formMethod: \"POST\",\n        formData: createFormData({}),\n      });\n      expect(spy.mock.calls).toEqual([[\"start\"]]);\n      await lazyDfd.resolve({ action: () => \"WRONG\" });\n      await A.actions.page.resolve(\"PAGE\");\n      expect(spy.mock.calls).toEqual([[\"start\"], [\"end\"]]);\n      expect(t.router.state).toMatchObject({\n        navigation: { state: \"idle\" },\n        location: { pathname: \"/page\" },\n        actionData: { page: \"PAGE\" },\n      });\n      spy.mockClear();\n\n      let B = await t.navigate(\"/page\", {\n        formMethod: \"POST\",\n        formData: createFormData({}),\n      });\n      expect(spy.mock.calls).toEqual([[\"start\"]]);\n      await B.actions.page.resolve(\"PAGE\");\n      expect(spy.mock.calls).toEqual([[\"start\"], [\"end\"]]);\n      expect(t.router.state).toMatchObject({\n        navigation: { state: \"idle\" },\n        location: { pathname: \"/page\" },\n        actionData: { page: \"PAGE\" },\n      });\n      spy.mockClear();\n\n      warnSpy.mockRestore();\n    });\n\n    it(\"allows instrumentation of lazy object middleware\", async () => {\n      let spy = jest.fn();\n      let middlewareDfd = createDeferred<MiddlewareFunction[]>();\n      let loaderDfd = createDeferred<LoaderFunction>();\n      let t = setup({\n        routes: [\n          {\n            index: true,\n          },\n          {\n            id: \"page\",\n            path: \"/page\",\n            lazy: {\n              middleware: () => middlewareDfd.promise,\n              loader: () => Promise.resolve(() => loaderDfd.promise),\n            },\n          },\n        ],\n        unstable_instrumentations: [\n          {\n            route(route) {\n              route.instrument({\n                \"lazy.middleware\": async (middleware) => {\n                  spy(\"start\");\n                  await middleware();\n                  spy(\"end\");\n                },\n              });\n            },\n          },\n        ],\n      });\n\n      await t.navigate(\"/page\");\n      expect(spy.mock.calls).toEqual([[\"start\"]]);\n\n      await middlewareDfd.resolve([\n        async (_: unknown, next: MiddlewareNextFunction) => {\n          spy(\"middleware start\");\n          await next();\n          spy(\"middleware end\");\n        },\n      ]);\n      await tick();\n      expect(spy.mock.calls).toEqual([\n        [\"start\"],\n        [\"end\"],\n        [\"middleware start\"],\n      ]);\n\n      await loaderDfd.resolve(\"PAGE\");\n      await tick();\n      expect(spy.mock.calls).toEqual([\n        [\"start\"],\n        [\"end\"],\n        [\"middleware start\"],\n        [\"middleware end\"],\n      ]);\n      expect(t.router.state).toMatchObject({\n        navigation: { state: \"idle\" },\n        location: { pathname: \"/page\" },\n        loaderData: { page: \"PAGE\" },\n      });\n    });\n\n    it(\"allows instrumentation of lazy object loaders\", async () => {\n      let spy = jest.fn();\n      let loaderDfd = createDeferred<LoaderFunction>();\n      let loaderValueDfd = createDeferred();\n      let t = setup({\n        routes: [\n          {\n            index: true,\n          },\n          {\n            id: \"page\",\n            path: \"/page\",\n            lazy: {\n              loader: () => loaderDfd.promise,\n            },\n          },\n        ],\n        unstable_instrumentations: [\n          {\n            route(route) {\n              route.instrument({\n                \"lazy.loader\": async (load) => {\n                  spy(\"start\");\n                  await load();\n                  spy(\"end\");\n                },\n                loader: async (loader) => {\n                  spy(\"loader start\");\n                  await loader();\n                  spy(\"loader end\");\n                },\n              });\n            },\n          },\n        ],\n      });\n\n      await t.navigate(\"/page\");\n      await tick();\n      expect(spy.mock.calls).toEqual([[\"start\"]]);\n\n      await loaderDfd.resolve(() => loaderValueDfd.promise);\n      await tick();\n      expect(spy.mock.calls).toEqual([[\"start\"], [\"end\"], [\"loader start\"]]);\n\n      await loaderValueDfd.resolve(\"PAGE\");\n      await tick();\n      expect(spy.mock.calls).toEqual([\n        [\"start\"],\n        [\"end\"],\n        [\"loader start\"],\n        [\"loader end\"],\n      ]);\n\n      await tick();\n      expect(t.router.state).toMatchObject({\n        navigation: { state: \"idle\" },\n        location: { pathname: \"/page\" },\n        loaderData: { page: \"PAGE\" },\n      });\n    });\n\n    it(\"allows instrumentation of lazy object actions\", async () => {\n      let spy = jest.fn();\n      let actionDfd = createDeferred<LoaderFunction>();\n      let actionValueDfd = createDeferred();\n      let t = setup({\n        routes: [\n          {\n            index: true,\n          },\n          {\n            id: \"page\",\n            path: \"/page\",\n            lazy: {\n              action: () => actionDfd.promise,\n            },\n          },\n        ],\n        unstable_instrumentations: [\n          {\n            route(route) {\n              route.instrument({\n                \"lazy.action\": async (load) => {\n                  spy(\"start\");\n                  await load();\n                  spy(\"end\");\n                },\n                action: async (action) => {\n                  spy(\"action start\");\n                  await action();\n                  spy(\"action end\");\n                },\n              });\n            },\n          },\n        ],\n      });\n\n      await t.navigate(\"/page\", {\n        formMethod: \"POST\",\n        formData: createFormData({}),\n      });\n      await tick();\n      expect(spy.mock.calls).toEqual([[\"start\"]]);\n\n      await actionDfd.resolve(() => actionValueDfd.promise);\n      await tick();\n      expect(spy.mock.calls).toEqual([[\"start\"], [\"end\"], [\"action start\"]]);\n\n      await actionValueDfd.resolve(\"PAGE\");\n      await tick();\n      expect(spy.mock.calls).toEqual([\n        [\"start\"],\n        [\"end\"],\n        [\"action start\"],\n        [\"action end\"],\n      ]);\n\n      await tick();\n      expect(t.router.state).toMatchObject({\n        navigation: { state: \"idle\" },\n        location: { pathname: \"/page\" },\n        actionData: { page: \"PAGE\" },\n      });\n    });\n\n    it(\"allows instrumentation of everything for a statically defined route\", async () => {\n      let spy = jest.fn();\n      let middlewareDfd = createDeferred<MiddlewareFunction[]>();\n      let t = setup({\n        routes: [\n          {\n            index: true,\n          },\n          {\n            id: \"page\",\n            path: \"/page\",\n            middleware: [\n              async (_: unknown, next: MiddlewareNextFunction) => {\n                await middlewareDfd.promise;\n                return next();\n              },\n            ],\n            loader: true,\n            action: true,\n          },\n        ],\n        unstable_instrumentations: [\n          {\n            route(route) {\n              route.instrument({\n                middleware: async (impl) => {\n                  spy(\"start middleware\");\n                  await impl();\n                  spy(\"end middleware\");\n                },\n                action: async (impl) => {\n                  spy(\"start action\");\n                  await impl();\n                  spy(\"end action\");\n                },\n                loader: async (impl) => {\n                  spy(\"start loader\");\n                  await impl();\n                  spy(\"end loader\");\n                },\n              });\n            },\n          },\n        ],\n      });\n\n      let A = await t.navigate(\"/page\", {\n        formMethod: \"POST\",\n        formData: createFormData({}),\n      });\n      expect(spy.mock.calls).toEqual([[\"start middleware\"]]);\n\n      await middlewareDfd.resolve(undefined);\n      await tick();\n      expect(spy.mock.calls).toEqual([[\"start middleware\"], [\"start action\"]]);\n\n      await A.actions.page.resolve(\"ACTION\");\n      expect(spy.mock.calls).toEqual([\n        [\"start middleware\"],\n        [\"start action\"],\n        [\"end action\"],\n        [\"end middleware\"],\n        [\"start middleware\"],\n        [\"start loader\"],\n      ]);\n\n      await A.loaders.page.resolve(\"PAGE\");\n      expect(spy.mock.calls).toEqual([\n        [\"start middleware\"],\n        [\"start action\"],\n        [\"end action\"],\n        [\"end middleware\"],\n        [\"start middleware\"],\n        [\"start loader\"],\n        [\"end loader\"],\n        [\"end middleware\"],\n      ]);\n\n      expect(t.router.state).toMatchObject({\n        navigation: { state: \"idle\" },\n        location: { pathname: \"/page\" },\n        actionData: { page: \"ACTION\" },\n        loaderData: { page: \"PAGE\" },\n      });\n    });\n\n    it(\"allows instrumentation of everything for a lazy function route\", async () => {\n      let spy = jest.fn();\n      let lazyDfd = createDeferred<any>();\n      let t = setup({\n        routes: [\n          {\n            index: true,\n          },\n          {\n            id: \"page\",\n            path: \"/page\",\n            // Middleware can't be returned from lazy()\n            middleware: [(_: unknown, next: MiddlewareNextFunction) => next()],\n            lazy: () => lazyDfd.promise,\n          },\n        ],\n        unstable_instrumentations: [\n          {\n            route(route) {\n              route.instrument({\n                middleware: async (impl) => {\n                  spy(\"start middleware\");\n                  await impl();\n                  spy(\"end middleware\");\n                },\n                action: async (impl) => {\n                  spy(\"start action\");\n                  await impl();\n                  spy(\"end action\");\n                },\n                loader: async (impl) => {\n                  spy(\"start loader\");\n                  await impl();\n                  spy(\"end loader\");\n                },\n              });\n            },\n          },\n        ],\n      });\n\n      await t.navigate(\"/page\", {\n        formMethod: \"POST\",\n        formData: createFormData({}),\n      });\n      expect(spy.mock.calls).toEqual([[\"start middleware\"]]);\n\n      await lazyDfd.resolve({\n        loader: () => \"PAGE\",\n        action: () => \"ACTION\",\n      });\n      await tick();\n      expect(spy.mock.calls).toEqual([\n        [\"start middleware\"],\n        [\"start action\"],\n        [\"end action\"],\n        [\"end middleware\"],\n        [\"start middleware\"],\n        [\"start loader\"],\n        [\"end loader\"],\n        [\"end middleware\"],\n      ]);\n\n      expect(t.router.state).toMatchObject({\n        navigation: { state: \"idle\" },\n        location: { pathname: \"/page\" },\n        actionData: { page: \"ACTION\" },\n        loaderData: { page: \"PAGE\" },\n      });\n    });\n\n    it(\"allows instrumentation of everything for a lazy object route\", async () => {\n      let spy = jest.fn();\n      let middlewareDfd = createDeferred<MiddlewareFunction[]>();\n      let actionDfd = createDeferred<ActionFunction>();\n      let loaderDfd = createDeferred<LoaderFunction>();\n      let t = setup({\n        routes: [\n          {\n            index: true,\n          },\n          {\n            id: \"page\",\n            path: \"/page\",\n            lazy: {\n              middleware: () => middlewareDfd.promise,\n              action: () => actionDfd.promise,\n              loader: () => loaderDfd.promise,\n            },\n          },\n        ],\n        unstable_instrumentations: [\n          {\n            route(route) {\n              route.instrument({\n                middleware: async (impl) => {\n                  spy(\"start middleware\");\n                  await impl();\n                  spy(\"end middleware\");\n                },\n                action: async (impl) => {\n                  spy(\"start action\");\n                  await impl();\n                  spy(\"end action\");\n                },\n                loader: async (impl) => {\n                  spy(\"start loader\");\n                  await impl();\n                  spy(\"end loader\");\n                },\n              });\n            },\n          },\n        ],\n      });\n\n      await t.navigate(\"/page\", {\n        formMethod: \"POST\",\n        formData: createFormData({}),\n      });\n      expect(spy.mock.calls).toEqual([]);\n\n      await middlewareDfd.resolve([\n        (_: unknown, next: MiddlewareNextFunction) => next(),\n      ]);\n      await tick();\n      expect(spy.mock.calls).toEqual([[\"start middleware\"]]);\n\n      await actionDfd.resolve(() => \"ACTION\");\n      await tick();\n      expect(spy.mock.calls).toEqual([\n        [\"start middleware\"],\n        [\"start action\"],\n        [\"end action\"],\n      ]);\n\n      await loaderDfd.resolve(() => \"PAGE\");\n      await tick();\n      expect(spy.mock.calls).toEqual([\n        [\"start middleware\"],\n        [\"start action\"],\n        [\"end action\"],\n        [\"end middleware\"],\n        [\"start middleware\"],\n        [\"start loader\"],\n        [\"end loader\"],\n        [\"end middleware\"],\n      ]);\n\n      expect(t.router.state).toMatchObject({\n        navigation: { state: \"idle\" },\n        location: { pathname: \"/page\" },\n        actionData: { page: \"ACTION\" },\n        loaderData: { page: \"PAGE\" },\n      });\n    });\n\n    it(\"allows instrumentation of everything for a statically defined route via patchRoutesOnNavigation\", async () => {\n      let spy = jest.fn();\n      let middlewareDfd = createDeferred<MiddlewareFunction[]>();\n      let t = setup({\n        routes: [\n          {\n            index: true,\n          },\n        ],\n        patchRoutesOnNavigation: ({ path, patch }) => {\n          if (path === \"/page\") {\n            patch(null, [\n              {\n                id: \"page\",\n                path: \"/page\",\n                middleware: [\n                  async (_: unknown, next: MiddlewareNextFunction) => {\n                    await middlewareDfd.promise;\n                    return next();\n                  },\n                ],\n                loader: () => \"PAGE\",\n                action: () => \"ACTION\",\n              },\n            ]);\n          }\n        },\n        unstable_instrumentations: [\n          {\n            route(route) {\n              route.instrument({\n                middleware: async (impl) => {\n                  spy(\"start middleware\");\n                  await impl();\n                  spy(\"end middleware\");\n                },\n                action: async (impl) => {\n                  spy(\"start action\");\n                  await impl();\n                  spy(\"end action\");\n                },\n                loader: async (impl) => {\n                  spy(\"start loader\");\n                  await impl();\n                  spy(\"end loader\");\n                },\n              });\n            },\n          },\n        ],\n      });\n\n      await t.navigate(\"/page\", {\n        formMethod: \"POST\",\n        formData: createFormData({}),\n      });\n      await tick();\n      await tick();\n      expect(spy.mock.calls).toEqual([[\"start middleware\"]]);\n\n      await middlewareDfd.resolve(undefined);\n      await tick();\n      expect(spy.mock.calls).toEqual([\n        [\"start middleware\"],\n        [\"start action\"],\n        [\"end action\"],\n        [\"end middleware\"],\n        [\"start middleware\"],\n        [\"start loader\"],\n        [\"end loader\"],\n        [\"end middleware\"],\n      ]);\n\n      expect(t.router.state).toMatchObject({\n        navigation: { state: \"idle\" },\n        location: { pathname: \"/page\" },\n        actionData: { page: \"ACTION\" },\n        loaderData: { page: \"PAGE\" },\n      });\n    });\n\n    it(\"allows instrumentation of everything for a lazy function route via patchRoutesOnNavigation\", async () => {\n      let spy = jest.fn();\n      let lazyDfd = createDeferred<any>();\n      let t = setup({\n        routes: [\n          {\n            index: true,\n          },\n        ],\n        patchRoutesOnNavigation: ({ path, patch }) => {\n          if (path === \"/page\") {\n            patch(null, [\n              {\n                id: \"page\",\n                path: \"/page\",\n                // Middleware can't be returned from lazy()\n                middleware: [\n                  (_: unknown, next: MiddlewareNextFunction) => next(),\n                ],\n                lazy: () => lazyDfd.promise,\n              },\n            ]);\n          }\n        },\n        unstable_instrumentations: [\n          {\n            route(route) {\n              route.instrument({\n                middleware: async (impl) => {\n                  spy(\"start middleware\");\n                  await impl();\n                  spy(\"end middleware\");\n                },\n                action: async (impl) => {\n                  spy(\"start action\");\n                  await impl();\n                  spy(\"end action\");\n                },\n                loader: async (impl) => {\n                  spy(\"start loader\");\n                  await impl();\n                  spy(\"end loader\");\n                },\n              });\n            },\n          },\n        ],\n      });\n\n      await t.navigate(\"/page\", {\n        formMethod: \"POST\",\n        formData: createFormData({}),\n      });\n      expect(spy.mock.calls).toEqual([]);\n\n      await lazyDfd.resolve({\n        loader: () => \"PAGE\",\n        action: () => \"ACTION\",\n      });\n      await tick();\n      expect(spy.mock.calls).toEqual([\n        [\"start middleware\"],\n        [\"start action\"],\n        [\"end action\"],\n        [\"end middleware\"],\n        [\"start middleware\"],\n        [\"start loader\"],\n        [\"end loader\"],\n        [\"end middleware\"],\n      ]);\n\n      expect(t.router.state).toMatchObject({\n        navigation: { state: \"idle\" },\n        location: { pathname: \"/page\" },\n        actionData: { page: \"ACTION\" },\n        loaderData: { page: \"PAGE\" },\n      });\n    });\n\n    it(\"allows instrumentation of everything for a lazy object route via patchRoutesOnNavigation\", async () => {\n      let spy = jest.fn();\n      let middlewareDfd = createDeferred<MiddlewareFunction[]>();\n      let actionDfd = createDeferred<ActionFunction>();\n      let loaderDfd = createDeferred<LoaderFunction>();\n      let t = setup({\n        routes: [\n          {\n            index: true,\n          },\n        ],\n        patchRoutesOnNavigation: ({ path, patch }) => {\n          if (path === \"/page\") {\n            patch(null, [\n              {\n                id: \"page\",\n                path: \"/page\",\n                lazy: {\n                  middleware: () => middlewareDfd.promise,\n                  action: () => actionDfd.promise,\n                  loader: () => loaderDfd.promise,\n                },\n              },\n            ]);\n          }\n        },\n        unstable_instrumentations: [\n          {\n            route(route) {\n              route.instrument({\n                middleware: async (impl) => {\n                  spy(\"start middleware\");\n                  await impl();\n                  spy(\"end middleware\");\n                },\n                action: async (impl) => {\n                  spy(\"start action\");\n                  await impl();\n                  spy(\"end action\");\n                },\n                loader: async (impl) => {\n                  spy(\"start loader\");\n                  await impl();\n                  spy(\"end loader\");\n                },\n              });\n            },\n          },\n        ],\n      });\n\n      await t.navigate(\"/page\", {\n        formMethod: \"POST\",\n        formData: createFormData({}),\n      });\n      expect(spy.mock.calls).toEqual([]);\n\n      await middlewareDfd.resolve([\n        (_: unknown, next: MiddlewareNextFunction) => next(),\n      ]);\n      await tick();\n      expect(spy.mock.calls).toEqual([[\"start middleware\"]]);\n\n      await actionDfd.resolve(() => \"ACTION\");\n      await tick();\n      expect(spy.mock.calls).toEqual([\n        [\"start middleware\"],\n        [\"start action\"],\n        [\"end action\"],\n      ]);\n\n      await loaderDfd.resolve(() => \"PAGE\");\n      await tick();\n      expect(spy.mock.calls).toEqual([\n        [\"start middleware\"],\n        [\"start action\"],\n        [\"end action\"],\n        [\"end middleware\"],\n        [\"start middleware\"],\n        [\"start loader\"],\n        [\"end loader\"],\n        [\"end middleware\"],\n      ]);\n\n      expect(t.router.state).toMatchObject({\n        navigation: { state: \"idle\" },\n        location: { pathname: \"/page\" },\n        actionData: { page: \"ACTION\" },\n        loaderData: { page: \"PAGE\" },\n      });\n    });\n\n    it(\"returns handler-thrown errors out to instrumentation implementations\", async () => {\n      let spy = jest.fn();\n      let t = setup({\n        routes: [\n          {\n            index: true,\n          },\n          {\n            id: \"page\",\n            path: \"/page\",\n            loader: true,\n          },\n        ],\n        unstable_instrumentations: [\n          {\n            route(route) {\n              route.instrument({\n                async loader(loader) {\n                  spy(\"inner-start\");\n                  let { status, error } = await loader();\n                  spy(`inner-end:${status}:${(error as Error).message}`);\n                },\n              });\n            },\n          },\n          {\n            route(route) {\n              route.instrument({\n                async loader(loader) {\n                  spy(\"outer-start\");\n                  let { status, error } = await loader();\n                  spy(`outer-end:${status}:${(error as Error).message}`);\n                },\n              });\n            },\n          },\n        ],\n      });\n\n      let A = await t.navigate(\"/page\");\n      expect(spy).toHaveBeenNthCalledWith(1, \"outer-start\");\n      expect(spy).toHaveBeenNthCalledWith(2, \"inner-start\");\n      await A.loaders.page.reject(new Error(\"broken!\"));\n      expect(spy).toHaveBeenNthCalledWith(3, \"inner-end:error:broken!\");\n      expect(spy).toHaveBeenNthCalledWith(4, \"outer-end:error:broken!\");\n      expect(t.router.state).toMatchObject({\n        navigation: { state: \"idle\" },\n        location: { pathname: \"/page\" },\n        loaderData: {},\n        errors: {\n          page: new Error(\"broken!\"),\n        },\n      });\n    });\n\n    it(\"does not return handler-thrown Responses out to instrumentation implementations\", async () => {\n      let spy = jest.fn();\n      let t = setup({\n        routes: [\n          {\n            index: true,\n          },\n          {\n            id: \"page\",\n            path: \"/page\",\n            loader: true,\n          },\n          {\n            id: \"target\",\n            path: \"/target\",\n          },\n        ],\n        unstable_instrumentations: [\n          {\n            route(route) {\n              route.instrument({\n                async loader(loader) {\n                  spy(\"inner-start\");\n                  let { error } = await loader();\n                  // Go back to discriminated union\n                  // thrown responses should not be exposed out here\n                  if (error) {\n                    spy(\"BROKEN\");\n                  } else {\n                    spy(\"inner-end\");\n                  }\n                },\n              });\n            },\n          },\n          {\n            route(route) {\n              route.instrument({\n                async loader(loader) {\n                  spy(\"outer-start\");\n                  let { error } = await loader();\n                  if (error) {\n                    spy(\"BROKEN\");\n                  } else {\n                    spy(\"outer-end\");\n                  }\n                },\n              });\n            },\n          },\n        ],\n      });\n\n      let A = await t.navigate(\"/page\");\n      expect(spy).toHaveBeenNthCalledWith(1, \"outer-start\");\n      expect(spy).toHaveBeenNthCalledWith(2, \"inner-start\");\n      await A.loaders.page.reject(redirect(\"/target\"));\n      expect(spy).toHaveBeenNthCalledWith(3, \"inner-end\");\n      expect(spy).toHaveBeenNthCalledWith(4, \"outer-end\");\n      expect(t.router.state).toMatchObject({\n        navigation: { state: \"idle\" },\n        location: { pathname: \"/target\" },\n        loaderData: {},\n        errors: null,\n      });\n    });\n\n    it(\"does not return handler-thrown data() out to instrumentation implementations\", async () => {\n      let spy = jest.fn();\n      let t = setup({\n        routes: [\n          {\n            index: true,\n          },\n          {\n            id: \"page\",\n            path: \"/page\",\n            loader: true,\n          },\n          {\n            id: \"target\",\n            path: \"/target\",\n          },\n        ],\n        unstable_instrumentations: [\n          {\n            route(route) {\n              route.instrument({\n                async loader(loader) {\n                  spy(\"inner-start\");\n                  let { error } = await loader();\n                  // Go back to discriminated union\n                  // thrown responses should not be exposed out here\n                  if (error) {\n                    spy(\"BROKEN\");\n                  } else {\n                    spy(\"inner-end\");\n                  }\n                },\n              });\n            },\n          },\n          {\n            route(route) {\n              route.instrument({\n                async loader(loader) {\n                  spy(\"outer-start\");\n                  let { error } = await loader();\n                  if (error) {\n                    spy(\"BROKEN\");\n                  } else {\n                    spy(\"outer-end\");\n                  }\n                },\n              });\n            },\n          },\n        ],\n      });\n\n      let A = await t.navigate(\"/page\");\n      expect(spy).toHaveBeenNthCalledWith(1, \"outer-start\");\n      expect(spy).toHaveBeenNthCalledWith(2, \"inner-start\");\n      await A.loaders.page.reject(\n        data({ message: \"hello\" }, { status: 418, statusText: \"I'm a teapot\" }),\n      );\n      expect(spy).toHaveBeenNthCalledWith(3, \"inner-end\");\n      expect(spy).toHaveBeenNthCalledWith(4, \"outer-end\");\n      expect(t.router.state).toMatchObject({\n        navigation: { state: \"idle\" },\n        location: { pathname: \"/page\" },\n        loaderData: {},\n        errors: {\n          page: new ErrorResponseImpl(418, \"I'm a teapot\", {\n            message: \"hello\",\n          }),\n        },\n      });\n    });\n\n    it(\"swallows and console.errors if an instrumentation function throws before calling the handler\", async () => {\n      let spy = jest.fn();\n      let errorSpy = jest.spyOn(console, \"error\").mockImplementation(() => {});\n      let t = setup({\n        routes: [\n          {\n            index: true,\n          },\n          {\n            id: \"page\",\n            path: \"/page\",\n            loader: true,\n          },\n        ],\n        unstable_instrumentations: [\n          {\n            route(route) {\n              route.instrument({\n                async loader() {\n                  throw new Error(\"broken!\");\n                },\n              });\n            },\n          },\n        ],\n      });\n\n      let A = await t.navigate(\"/page\");\n      expect(spy).not.toHaveBeenCalled();\n      await A.loaders.page.resolve(\"PAGE\");\n      expect(t.router.state).toMatchObject({\n        navigation: { state: \"idle\" },\n        location: { pathname: \"/page\" },\n        loaderData: { page: \"PAGE\" },\n        errors: null,\n      });\n      expect(errorSpy).toHaveBeenCalledWith(\n        \"An instrumentation function threw an error:\",\n        new Error(\"broken!\"),\n      );\n      errorSpy.mockRestore();\n    });\n\n    it(\"swallows and warns if an instrumentation function throws after calling the handler\", async () => {\n      let spy = jest.fn();\n      let errorSpy = jest.spyOn(console, \"error\").mockImplementation(() => {});\n      let t = setup({\n        routes: [\n          {\n            index: true,\n          },\n          {\n            id: \"page\",\n            path: \"/page\",\n            loader: true,\n          },\n        ],\n        unstable_instrumentations: [\n          {\n            route(route) {\n              route.instrument({\n                async loader(loader) {\n                  spy(\"start\");\n                  await loader();\n                  throw new Error(\"broken!\");\n                },\n              });\n            },\n          },\n        ],\n      });\n\n      let A = await t.navigate(\"/page\");\n      expect(spy).toHaveBeenNthCalledWith(1, \"start\");\n      await A.loaders.page.resolve(\"PAGE\");\n      expect(t.router.state).toMatchObject({\n        navigation: { state: \"idle\" },\n        location: { pathname: \"/page\" },\n        loaderData: { page: \"PAGE\" },\n        errors: null,\n      });\n      expect(errorSpy).toHaveBeenCalledWith(\n        \"An instrumentation function threw an error:\",\n        new Error(\"broken!\"),\n      );\n      errorSpy.mockRestore();\n    });\n\n    it(\"waits for handler to finish if you forget to await the handler\", async () => {\n      let t = setup({\n        routes: [\n          {\n            index: true,\n          },\n          {\n            id: \"page\",\n            path: \"/page\",\n            loader: true,\n          },\n        ],\n        unstable_instrumentations: [\n          {\n            route(route) {\n              route.instrument({\n                async loader(loader) {\n                  loader();\n                },\n              });\n            },\n          },\n        ],\n      });\n\n      let A = await t.navigate(\"/page\");\n      await A.loaders.page.resolve(\"PAGE\");\n      expect(t.router.state).toMatchObject({\n        navigation: { state: \"idle\" },\n        location: { pathname: \"/page\" },\n        loaderData: { page: \"PAGE\" },\n        errors: null,\n      });\n      expect(A.loaders.page.stub).toHaveBeenCalledTimes(1);\n    });\n\n    it(\"does not let you call handlers more than once\", async () => {\n      let errorSpy = jest.spyOn(console, \"error\").mockImplementation(() => {});\n      let t = setup({\n        routes: [\n          {\n            index: true,\n          },\n          {\n            id: \"page\",\n            path: \"/page\",\n            loader: true,\n          },\n        ],\n        unstable_instrumentations: [\n          {\n            route(route) {\n              route.instrument({\n                async loader(loader) {\n                  await loader();\n                  await loader();\n                },\n              });\n            },\n          },\n        ],\n      });\n\n      let A = await t.navigate(\"/page\");\n      await A.loaders.page.resolve(\"PAGE\");\n      expect(t.router.state).toMatchObject({\n        navigation: { state: \"idle\" },\n        location: { pathname: \"/page\" },\n        loaderData: { page: \"PAGE\" },\n        errors: null,\n      });\n      expect(A.loaders.page.stub).toHaveBeenCalledTimes(1);\n      expect(errorSpy).toHaveBeenCalledWith(\n        \"You cannot call instrumented handlers more than once\",\n      );\n      errorSpy.mockRestore();\n    });\n\n    it(\"provides read-only information to instrumentation wrappers\", async () => {\n      let spy = jest.fn();\n      let t = setup({\n        routes: [\n          {\n            index: true,\n          },\n          {\n            id: \"slug\",\n            path: \"/:slug\",\n            loader: true,\n          },\n        ],\n        unstable_instrumentations: [\n          {\n            route(route) {\n              route.instrument({\n                async loader(loader, info) {\n                  spy(info);\n                  Object.assign(info.params, { extra: \"extra\" });\n                  await loader();\n                },\n              });\n            },\n          },\n        ],\n      });\n\n      let A = await t.navigate(\"/a\");\n      await A.loaders.slug.resolve(\"A\");\n      let args = spy.mock.calls[0][0];\n      expect(args.request.method).toBe(\"GET\");\n      expect(args.request.url).toBe(\"http://localhost/a\");\n      expect(args.request.url).toBe(\"http://localhost/a\");\n      expect(args.request.headers.get).toBeDefined();\n      expect(args.request.headers.set).not.toBeDefined();\n      expect(args.params).toEqual({ slug: \"a\", extra: \"extra\" });\n      expect(args.unstable_pattern).toBe(\"/:slug\");\n      expect(args.context.get).toBeDefined();\n      expect(args.context.set).not.toBeDefined();\n      expect(t.router.state.matches[0].params).toEqual({ slug: \"a\" });\n    });\n\n    it(\"allows composition of multiple instrumentations\", async () => {\n      let spy = jest.fn();\n      let t = setup({\n        routes: [\n          {\n            index: true,\n          },\n          {\n            id: \"page\",\n            path: \"/page\",\n            loader: true,\n          },\n        ],\n        unstable_instrumentations: [\n          {\n            route(route) {\n              route.instrument({\n                async loader(loader) {\n                  spy(\"start inner\");\n                  await loader();\n                  spy(\"end inner\");\n                },\n              });\n              route.instrument({\n                async loader(loader) {\n                  spy(\"start outer\");\n                  await loader();\n                  spy(\"end outer\");\n                },\n              });\n            },\n          },\n        ],\n      });\n\n      let A = await t.navigate(\"/page\");\n      await A.loaders.page.resolve(\"PAGE\");\n      expect(spy.mock.calls).toEqual([\n        [\"start outer\"],\n        [\"start inner\"],\n        [\"end inner\"],\n        [\"end outer\"],\n      ]);\n      expect(t.router.state).toMatchObject({\n        navigation: { state: \"idle\" },\n        location: { pathname: \"/page\" },\n        loaderData: { page: \"PAGE\" },\n      });\n    });\n\n    it(\"allows instrumentation of navigations\", async () => {\n      let spy = jest.fn();\n      let router = createMemoryRouter(\n        [\n          {\n            index: true,\n          },\n          {\n            id: \"page\",\n            path: \"/page\",\n            loader: () => \"PAGE\",\n          },\n        ],\n        {\n          unstable_instrumentations: [\n            {\n              router(router) {\n                router.instrument({\n                  async navigate(navigate, info) {\n                    spy(\"start\", info);\n                    await navigate();\n                    spy(\"end\", info);\n                  },\n                });\n              },\n            },\n          ],\n        },\n      );\n\n      await router.navigate(\"/page\");\n      expect(spy.mock.calls).toEqual([\n        [\"start\", { currentUrl: \"/\", to: \"/page\" }],\n        [\"end\", { currentUrl: \"/\", to: \"/page\" }],\n      ]);\n      expect(router.state).toMatchObject({\n        navigation: { state: \"idle\" },\n        location: { pathname: \"/page\" },\n        loaderData: { page: \"PAGE\" },\n      });\n    });\n\n    it(\"allows instrumentation of fetchers\", async () => {\n      let spy = jest.fn();\n      let router = createMemoryRouter(\n        [\n          {\n            index: true,\n          },\n          {\n            id: \"page\",\n            path: \"/page\",\n            loader: () => \"PAGE\",\n          },\n        ],\n        {\n          unstable_instrumentations: [\n            {\n              router(router) {\n                router.instrument({\n                  async fetch(fetch, info) {\n                    spy(\"start\", info);\n                    await fetch();\n                    spy(\"end\", info);\n                  },\n                });\n              },\n            },\n          ],\n        },\n      );\n\n      let data: unknown;\n      router.subscribe((state) => {\n        data = data ?? state.fetchers.get(\"key\")?.data;\n      });\n      await router.fetch(\"key\", \"0\", \"/page\");\n      expect(spy.mock.calls).toEqual([\n        [\"start\", { href: \"/page\", currentUrl: \"/\", fetcherKey: \"key\" }],\n        [\"end\", { href: \"/page\", currentUrl: \"/\", fetcherKey: \"key\" }],\n      ]);\n      expect(router.state).toMatchObject({\n        navigation: { state: \"idle\" },\n        location: { pathname: \"/\" },\n      });\n      expect(data).toBe(\"PAGE\");\n    });\n  });\n\n  describe(\"static handler\", () => {\n    it(\"allows instrumentation of lazy\", async () => {\n      let spy = jest.fn();\n      let { query } = createStaticHandler(\n        [\n          {\n            id: \"index\",\n            index: true,\n            lazy: async () => {\n              spy(\"lazy\");\n              return {\n                loader: () => {\n                  spy(\"loader\");\n                  return new Response(\"INDEX\");\n                },\n              };\n            },\n          },\n        ],\n        {\n          unstable_instrumentations: [\n            {\n              route(route) {\n                route.instrument({\n                  async lazy(loader) {\n                    spy(\"start\");\n                    await loader();\n                    spy(\"end\");\n                  },\n                });\n              },\n            },\n          ],\n        },\n      );\n\n      let context = await query(new Request(\"http://localhost/\"));\n      expect(spy.mock.calls).toEqual([\n        [\"start\"],\n        [\"lazy\"],\n        [\"end\"],\n        [\"loader\"],\n      ]);\n      expect(context).toMatchObject({\n        location: { pathname: \"/\" },\n        loaderData: { index: \"INDEX\" },\n      });\n      spy.mockClear();\n\n      // Recreate to get a fresh execution of lazy\n      let { queryRoute } = createStaticHandler(\n        [\n          {\n            id: \"index\",\n            index: true,\n            lazy: async () => {\n              spy(\"lazy\");\n              return {\n                loader: () => {\n                  spy(\"loader\");\n                  return new Response(\"INDEX\");\n                },\n              };\n            },\n          },\n        ],\n        {\n          unstable_instrumentations: [\n            {\n              route(route) {\n                route.instrument({\n                  async lazy(loader) {\n                    spy(\"start\");\n                    await loader();\n                    spy(\"end\");\n                  },\n                });\n              },\n            },\n          ],\n        },\n      );\n      let response = await queryRoute(new Request(\"http://localhost/\"));\n      expect(spy.mock.calls).toEqual([\n        [\"start\"],\n        [\"lazy\"],\n        [\"end\"],\n        [\"loader\"],\n      ]);\n      expect(await response.text()).toBe(\"INDEX\");\n    });\n\n    it(\"allows instrumentation of middleware\", async () => {\n      let spy = jest.fn();\n      let { query, queryRoute } = createStaticHandler(\n        [\n          {\n            id: \"index\",\n            index: true,\n            middleware: [\n              async (_: unknown, next: MiddlewareNextFunction) => {\n                spy(\"middleware\");\n                return await next();\n              },\n            ],\n            loader: () => {\n              spy(\"loader\");\n              return new Response(\"INDEX\");\n            },\n          },\n        ],\n        {\n          unstable_instrumentations: [\n            {\n              route(route) {\n                route.instrument({\n                  async middleware(middleware) {\n                    spy(\"start\");\n                    await middleware();\n                    spy(\"end\");\n                  },\n                });\n              },\n            },\n          ],\n        },\n      );\n\n      let request = new Request(\"http://localhost/\");\n      let response = (await query(request, {\n        async generateMiddlewareResponse(query) {\n          let ctx = (await query(request)) as StaticHandlerContext;\n          return new Response(\n            JSON.stringify({\n              location: ctx.location,\n              loaderData: ctx.loaderData,\n            }),\n          );\n        },\n      })) as Response;\n      expect(spy.mock.calls).toEqual([\n        [\"start\"],\n        [\"middleware\"],\n        [\"loader\"],\n        [\"end\"],\n      ]);\n      expect(JSON.parse(await response.text())).toMatchObject({\n        location: { pathname: \"/\" },\n        loaderData: { index: \"INDEX\" },\n      });\n      spy.mockClear();\n\n      response = await queryRoute(request, {\n        generateMiddlewareResponse: async (queryRoute) => {\n          return await queryRoute(request);\n        },\n      });\n      expect(spy.mock.calls).toEqual([\n        [\"start\"],\n        [\"middleware\"],\n        [\"loader\"],\n        [\"end\"],\n      ]);\n      expect(await response.text()).toBe(\"INDEX\");\n    });\n\n    it(\"allows instrumentation of loaders\", async () => {\n      let spy = jest.fn();\n      let { query, queryRoute } = createStaticHandler(\n        [\n          {\n            id: \"index\",\n            index: true,\n            loader: () => {\n              spy(\"loader\");\n              return new Response(\"INDEX\");\n            },\n          },\n        ],\n        {\n          unstable_instrumentations: [\n            {\n              route(route) {\n                route.instrument({\n                  async loader(loader) {\n                    spy(\"start\");\n                    await loader();\n                    spy(\"end\");\n                  },\n                });\n              },\n            },\n          ],\n        },\n      );\n\n      let context = await query(new Request(\"http://localhost/\"));\n      expect(spy.mock.calls).toEqual([[\"start\"], [\"loader\"], [\"end\"]]);\n      expect(context).toMatchObject({\n        location: { pathname: \"/\" },\n        loaderData: { index: \"INDEX\" },\n      });\n      spy.mockClear();\n\n      let response = await queryRoute(new Request(\"http://localhost/\"));\n      expect(spy.mock.calls).toEqual([[\"start\"], [\"loader\"], [\"end\"]]);\n      expect(await response.text()).toBe(\"INDEX\");\n    });\n\n    it(\"allows instrumentation of actions\", async () => {\n      let spy = jest.fn();\n      let { query, queryRoute } = createStaticHandler(\n        [\n          {\n            id: \"index\",\n            index: true,\n            action: () => {\n              spy(\"action\");\n              return new Response(\"INDEX\");\n            },\n          },\n        ],\n        {\n          unstable_instrumentations: [\n            {\n              route(route) {\n                route.instrument({\n                  async action(action) {\n                    spy(\"start\");\n                    await action();\n                    spy(\"end\");\n                  },\n                });\n              },\n            },\n          ],\n        },\n      );\n\n      let context = await query(\n        new Request(\"http://localhost/\", { method: \"post\", body: \"data\" }),\n      );\n      expect(spy.mock.calls).toEqual([[\"start\"], [\"action\"], [\"end\"]]);\n      expect(context).toMatchObject({\n        location: { pathname: \"/\" },\n        actionData: { index: \"INDEX\" },\n      });\n      spy.mockClear();\n\n      let response = await queryRoute(\n        new Request(\"http://localhost/\", { method: \"post\", body: \"data\" }),\n      );\n      expect(spy.mock.calls).toEqual([[\"start\"], [\"action\"], [\"end\"]]);\n      expect(await response.text()).toBe(\"INDEX\");\n    });\n  });\n\n  describe(\"request handler\", () => {\n    it(\"allows instrumentation of the request handler\", async () => {\n      let spy = jest.fn();\n      let build = mockServerBuild(\n        {\n          root: {\n            path: \"\",\n            loader: () => {\n              spy(\"loader\");\n              return \"ROOT\";\n            },\n            default: () => \"COMPONENT\",\n          },\n        },\n        {\n          handleDocumentRequest(request) {\n            return new Response(`${request.method} ${request.url} COMPONENT`);\n          },\n          unstable_instrumentations: [\n            {\n              handler(handler) {\n                handler.instrument({\n                  async request(handler, info) {\n                    spy(\"start\", info);\n                    await handler();\n                    spy(\"end\", info);\n                  },\n                });\n              },\n            },\n          ],\n        },\n      );\n      let handler = createRequestHandler(build);\n      let response = await handler(new Request(\"http://localhost/\"), {});\n\n      expect(await response.text()).toBe(\"GET http://localhost/ COMPONENT\");\n      expect(spy.mock.calls).toEqual([\n        [\n          \"start\",\n          {\n            request: {\n              method: \"GET\",\n              url: \"http://localhost/\",\n              headers: {\n                get: expect.any(Function),\n              },\n            },\n            context: {},\n          },\n        ],\n        [\"loader\"],\n        [\n          \"end\",\n          {\n            request: {\n              method: \"GET\",\n              url: \"http://localhost/\",\n              headers: {\n                get: expect.any(Function),\n              },\n            },\n            context: {},\n          },\n        ],\n      ]);\n    });\n\n    it(\"allows instrumentation of middleware\", async () => {\n      let spy = jest.fn();\n      let build = mockServerBuild(\n        {\n          root: {\n            path: \"/\",\n            middleware: [\n              (_: unknown, next: MiddlewareNextFunction<Response>) => {\n                spy(\"middleware\");\n                return next();\n              },\n            ],\n            loader: () => {\n              spy(\"loader\");\n              return \"ROOT\";\n            },\n            default: () => \"COMPONENT\",\n          },\n        },\n        {\n          future: {\n            v8_middleware: true,\n          },\n          handleDocumentRequest(request) {\n            return new Response(`${request.method} ${request.url} COMPONENT`);\n          },\n          unstable_instrumentations: [\n            {\n              route(route) {\n                route.instrument({\n                  async middleware(middleware, info) {\n                    spy(\"start\", info);\n                    await middleware();\n                    spy(\"end\", info);\n                  },\n                });\n              },\n            },\n          ],\n        },\n      );\n      let handler = createRequestHandler(build);\n      let response = await handler(new Request(\"http://localhost/\"));\n\n      expect(await response.text()).toBe(\"GET http://localhost/ COMPONENT\");\n      expect(spy.mock.calls).toEqual([\n        [\n          \"start\",\n          {\n            request: {\n              method: \"GET\",\n              url: \"http://localhost/\",\n              headers: {\n                get: expect.any(Function),\n              },\n            },\n            params: {},\n            unstable_pattern: \"/\",\n            context: {\n              get: expect.any(Function),\n            },\n          },\n        ],\n        [\"middleware\"],\n        [\"loader\"],\n        [\n          \"end\",\n          {\n            request: {\n              method: \"GET\",\n              url: \"http://localhost/\",\n              headers: {\n                get: expect.any(Function),\n              },\n            },\n            params: {},\n            unstable_pattern: \"/\",\n            context: {\n              get: expect.any(Function),\n            },\n          },\n        ],\n      ]);\n    });\n\n    it(\"allows instrumentation of loaders\", async () => {\n      let spy = jest.fn();\n      let build = mockServerBuild(\n        {\n          root: {\n            path: \"/\",\n            loader: () => {\n              spy(\"loader\");\n              return \"ROOT\";\n            },\n            default: () => \"COMPONENT\",\n          },\n        },\n        {\n          handleDocumentRequest(request) {\n            return new Response(`${request.method} ${request.url} COMPONENT`);\n          },\n          unstable_instrumentations: [\n            {\n              route(route) {\n                route.instrument({\n                  async loader(loader, info) {\n                    spy(\"start\", info);\n                    await loader();\n                    spy(\"end\", info);\n                  },\n                });\n              },\n            },\n          ],\n        },\n      );\n      let handler = createRequestHandler(build);\n      let response = await handler(new Request(\"http://localhost/\"));\n\n      expect(await response.text()).toBe(\"GET http://localhost/ COMPONENT\");\n      expect(spy.mock.calls).toEqual([\n        [\n          \"start\",\n          {\n            request: {\n              method: \"GET\",\n              url: \"http://localhost/\",\n              headers: {\n                get: expect.any(Function),\n              },\n            },\n            params: {},\n            unstable_pattern: \"/\",\n            context: {},\n          },\n        ],\n        [\"loader\"],\n        [\n          \"end\",\n          {\n            request: {\n              method: \"GET\",\n              url: \"http://localhost/\",\n              headers: {\n                get: expect.any(Function),\n              },\n            },\n            params: {},\n            unstable_pattern: \"/\",\n            context: {},\n          },\n        ],\n      ]);\n    });\n\n    it(\"allows instrumentation of actions\", async () => {\n      let spy = jest.fn();\n      let build = mockServerBuild(\n        {\n          root: {\n            path: \"/\",\n            action: () => {\n              spy(\"action\");\n              return \"ROOT\";\n            },\n            default: () => \"COMPONENT\",\n          },\n        },\n        {\n          handleDocumentRequest(request) {\n            return new Response(`${request.method} ${request.url} COMPONENT`);\n          },\n          unstable_instrumentations: [\n            {\n              route(route) {\n                route.instrument({\n                  async action(action, info) {\n                    spy(\"start\", info);\n                    await action();\n                    spy(\"end\", info);\n                  },\n                });\n              },\n            },\n          ],\n        },\n      );\n      let handler = createRequestHandler(build);\n      let response = await handler(\n        new Request(\"http://localhost/\", { method: \"post\", body: \"data\" }),\n      );\n\n      expect(await response.text()).toBe(\"POST http://localhost/ COMPONENT\");\n      expect(spy.mock.calls).toEqual([\n        [\n          \"start\",\n          {\n            request: {\n              method: \"POST\",\n              url: \"http://localhost/\",\n              headers: {\n                get: expect.any(Function),\n              },\n            },\n            params: {},\n            unstable_pattern: \"/\",\n            context: {},\n          },\n        ],\n        [\"action\"],\n        [\n          \"end\",\n          {\n            request: {\n              method: \"POST\",\n              url: \"http://localhost/\",\n              headers: {\n                get: expect.any(Function),\n              },\n            },\n            params: {},\n            unstable_pattern: \"/\",\n            context: {},\n          },\n        ],\n      ]);\n    });\n  });\n});\n"
  },
  {
    "path": "packages/react-router/__tests__/router/interruptions-test.ts",
    "content": "/* eslint-disable jest/valid-title */\nimport type { HydrationState } from \"../../lib/router/router\";\nimport { IDLE_NAVIGATION } from \"../../lib/router/router\";\nimport { cleanup, setup } from \"./utils/data-router-setup\";\nimport { createFormData } from \"./utils/utils\";\n\nfunction initializeTest(init?: {\n  url?: string;\n  hydrationData?: HydrationState;\n}) {\n  return setup({\n    routes: [\n      {\n        path: \"\",\n        id: \"root\",\n        hasErrorBoundary: true,\n        loader: true,\n        children: [\n          {\n            path: \"/\",\n            id: \"index\",\n            loader: true,\n            action: true,\n          },\n          {\n            path: \"/foo\",\n            id: \"foo\",\n            loader: true,\n            action: true,\n          },\n          {\n            path: \"/bar\",\n            id: \"bar\",\n            loader: true,\n            action: true,\n          },\n          {\n            path: \"/baz\",\n            id: \"baz\",\n            loader: true,\n            action: true,\n          },\n        ],\n      },\n    ],\n    hydrationData: init?.hydrationData || {\n      loaderData: { root: \"ROOT\", index: \"INDEX\" },\n    },\n    ...(init?.url ? { initialEntries: [init.url] } : {}),\n  });\n}\n\nbeforeEach(() => {\n  jest.spyOn(console, \"warn\").mockImplementation(() => {});\n});\n\n// Detect any failures inside the router navigate code\nafterEach(() => {\n  cleanup();\n\n  // @ts-ignore\n  console.warn.mockReset();\n});\n\ndescribe(\"interruptions\", () => {\n  describe(`\n      A) GET /foo |---X\n      B) GET /bar     |---O\n    `, () => {\n    it(\"aborts previous load\", async () => {\n      let t = initializeTest();\n      let A = await t.navigate(\"/foo\");\n      t.navigate(\"/bar\");\n      expect(A.loaders.foo.stub.mock.calls.length).toBe(1);\n    });\n  });\n\n  describe(`\n      A) GET  /foo |---X\n      B) POST /bar     |---O\n    `, () => {\n    it(\"aborts previous load\", async () => {\n      let t = initializeTest();\n      let A = await t.navigate(\"/foo\");\n      await t.navigate(\"/bar\", {\n        formMethod: \"post\",\n        formData: new FormData(),\n      });\n      expect(A.loaders.foo.signal.aborted).toBe(true);\n    });\n  });\n\n  describe(`\n      A) POST /foo |---X\n      B) POST /bar     |---O\n    `, () => {\n    it(\"aborts previous action\", async () => {\n      let t = initializeTest();\n      let A = await t.navigate(\"/foo\", {\n        formMethod: \"post\",\n        formData: new FormData(),\n      });\n      await t.navigate(\"/bar\", {\n        formMethod: \"post\",\n        formData: new FormData(),\n      });\n      expect(A.actions.foo.signal.aborted).toBe(true);\n    });\n  });\n\n  describe(`\n      A) POST /foo |--|--X\n      B) GET  /bar       |---O\n    `, () => {\n    it(\"aborts previous action reload\", async () => {\n      let t = initializeTest();\n      let A = await t.navigate(\"/foo\", {\n        formMethod: \"post\",\n        formData: new FormData(),\n      });\n      await A.actions.foo.resolve(\"A ACTION\");\n      await t.navigate(\"/bar\");\n      expect(A.loaders.foo.signal.aborted).toBe(true);\n    });\n  });\n\n  describe(`\n      A) POST /foo |--|--X\n      B) POST /bar       |---O\n    `, () => {\n    it(\"aborts previous action reload\", async () => {\n      let t = initializeTest();\n      let A = await t.navigate(\"/foo\", {\n        formMethod: \"post\",\n        formData: new FormData(),\n      });\n      await A.actions.foo.resolve(\"A ACTION\");\n      await t.navigate(\"/bar\", {\n        formMethod: \"post\",\n        formData: new FormData(),\n      });\n      expect(A.loaders.foo.signal.aborted).toBe(true);\n    });\n  });\n\n  describe(`\n      A) GET /foo |--/bar--X\n      B) GET /baz          |---O\n    `, () => {\n    it(\"aborts previous action redirect load\", async () => {\n      let t = initializeTest();\n      let A = await t.navigate(\"/foo\");\n      let AR = await A.loaders.foo.redirect(\"/bar\");\n      t.navigate(\"/baz\");\n      expect(AR.loaders.bar.stub.mock.calls.length).toBe(1);\n    });\n  });\n\n  describe(`\n      A) POST /foo |--/bar--X\n      B) GET  /baz          |---O\n    `, () => {\n    it(\"aborts previous action redirect load\", async () => {\n      let t = initializeTest();\n      let A = await t.navigate(\"/foo\", {\n        formMethod: \"post\",\n        formData: new FormData(),\n      });\n      let AR = await A.actions.foo.redirect(\"/bar\");\n      await t.navigate(\"/baz\");\n      expect(AR.loaders.bar.signal.aborted).toBe(true);\n    });\n  });\n\n  describe(`\n      A) GET /foo |---X\n      B) GET /bar     |---X\n      C) GET /baz         |---O\n    `, () => {\n    it(\"aborts multiple subsequent loads\", async () => {\n      let t = initializeTest();\n      // Start A navigation and immediately interrupt\n      let A = await t.navigate(\"/foo\");\n      let B = await t.navigate(\"/bar\");\n      // resolve A then interrupt B - ensure the A resolution doesn't clear\n      // the new pendingNavigationController which is now reflecting B's nav\n      await A.loaders.foo.resolve(\"A\");\n      let C = await t.navigate(\"/baz\");\n      await B.loaders.bar.resolve(\"B\");\n      await C.loaders.baz.resolve(\"C\");\n\n      expect(A.loaders.foo.stub.mock.calls.length).toBe(1);\n      expect(A.loaders.foo.signal.aborted).toBe(true);\n\n      expect(B.loaders.bar.stub.mock.calls.length).toBe(1);\n      expect(B.loaders.bar.signal.aborted).toBe(true);\n\n      expect(C.loaders.baz.stub.mock.calls.length).toBe(1);\n      expect(C.loaders.baz.signal.aborted).toBe(false);\n\n      expect(t.router.state.loaderData).toEqual({\n        root: \"ROOT\",\n        baz: \"C\",\n      });\n    });\n  });\n\n  describe(`\n      A) POST /foo |---X\n      B) POST /bar     |---X\n      C) POST /baz         |---O\n    `, () => {\n    it(\"aborts previous load\", async () => {\n      let t = initializeTest();\n      // Start A navigation and immediately interrupt\n      let A = await t.navigate(\"/foo\", {\n        formMethod: \"post\",\n        formData: new FormData(),\n      });\n      let B = await t.navigate(\"/bar\", {\n        formMethod: \"post\",\n        formData: new FormData(),\n      });\n      // resolve A then interrupt B - ensure the A resolution doesn't clear\n      // the new pendingNavigationController which is now reflecting B's nav\n      await A.actions.foo.resolve(\"A\");\n      let C = await t.navigate(\"/baz\", {\n        formMethod: \"post\",\n        formData: new FormData(),\n      });\n      await B.actions.bar.resolve(\"B\");\n      await C.actions.baz.resolve(\"C\");\n\n      expect(A.actions.foo.stub.mock.calls.length).toBe(1);\n      expect(A.actions.foo.signal.aborted).toBe(true);\n\n      expect(B.actions.bar.stub.mock.calls.length).toBe(1);\n      expect(B.actions.bar.signal.aborted).toBe(true);\n\n      expect(C.actions.baz.stub.mock.calls.length).toBe(1);\n      expect(C.actions.baz.signal.aborted).toBe(false);\n\n      expect(t.router.state.actionData).toEqual({\n        baz: \"C\",\n      });\n    });\n  });\n\n  describe(`\n      A) POST /foo |--X\n      B) GET  /bar    |-----O\n    `, () => {\n    it(\"forces all loaders to revalidate on interrupted submission\", async () => {\n      let t = initializeTest();\n      let A = await t.navigate(\"/foo\", {\n        formMethod: \"post\",\n        formData: createFormData({ key: \"value\" }),\n      });\n      // Interrupting the submission should cause the next load to call all loaders\n      let B = await t.navigate(\"/bar\");\n      await A.actions.foo.resolve(\"A ACTION\");\n      await B.loaders.root.resolve(\"ROOT*\");\n      await B.loaders.bar.resolve(\"BAR\");\n      expect(t.router.state).toMatchObject({\n        navigation: IDLE_NAVIGATION,\n        location: { pathname: \"/bar\" },\n        actionData: null,\n        loaderData: {\n          root: \"ROOT*\",\n          bar: \"BAR\",\n        },\n      });\n    });\n  });\n\n  describe(`\n      A) POST /foo |--|--X\n      B) GET  /bar       |-----O\n    `, () => {\n    it(\"forces all loaders to revalidate on interrupted actionReload\", async () => {\n      let t = initializeTest();\n      let A = await t.navigate(\"/foo\", {\n        formMethod: \"post\",\n        formData: createFormData({ key: \"value\" }),\n      });\n      await A.actions.foo.resolve(\"A ACTION\");\n      expect(t.router.state.navigation.state).toBe(\"loading\");\n      // Interrupting the actionReload should cause the next load to call all loaders\n      let B = await t.navigate(\"/bar\");\n      await B.loaders.root.resolve(\"ROOT*\");\n      await B.loaders.bar.resolve(\"BAR\");\n      expect(t.router.state).toMatchObject({\n        navigation: IDLE_NAVIGATION,\n        location: { pathname: \"/bar\" },\n        actionData: null,\n        loaderData: {\n          root: \"ROOT*\",\n          bar: \"BAR\",\n        },\n      });\n    });\n\n    it(\"forces all loaders to revalidate on interrupted submissionRedirect\", async () => {\n      let t = initializeTest();\n      let A = await t.navigate(\"/foo\", {\n        formMethod: \"post\",\n        formData: createFormData({ key: \"value\" }),\n      });\n      await A.actions.foo.redirect(\"/baz\");\n      expect(t.router.state.navigation.state).toBe(\"loading\");\n      // Interrupting the submissionRedirect should cause the next load to call all loaders\n      let B = await t.navigate(\"/bar\");\n      await B.loaders.root.resolve(\"ROOT*\");\n      await B.loaders.bar.resolve(\"BAR\");\n      expect(t.router.state).toMatchObject({\n        navigation: IDLE_NAVIGATION,\n        location: { pathname: \"/bar\" },\n        loaderData: {\n          root: \"ROOT*\",\n          bar: \"BAR\",\n        },\n      });\n    });\n  });\n});\n"
  },
  {
    "path": "packages/react-router/__tests__/router/lazy-discovery-test.ts",
    "content": "import type { Router } from \"../../lib/router/router\";\nimport type { AgnosticDataRouteObject } from \"../../lib/router/utils\";\nimport { createMemoryHistory } from \"../../lib/router/history\";\nimport { IDLE_NAVIGATION, createRouter } from \"../../lib/router/router\";\nimport { ErrorResponseImpl } from \"../../lib/router/utils\";\nimport { getFetcherData } from \"./utils/data-router-setup\";\nimport { createDeferred, createFormData, tick } from \"./utils/utils\";\n\nlet router: Router;\n\nfunction last(array: any[]) {\n  return array[array.length - 1];\n}\n\ndescribe(\"Lazy Route Discovery (Fog of War)\", () => {\n  afterEach(() => {\n    router.dispose();\n    // @ts-expect-error\n    router = null;\n  });\n\n  it(\"discovers child route at a depth of 1 (GET navigation)\", async () => {\n    let childrenDfd = createDeferred<AgnosticDataRouteObject[]>();\n    let loaderDfd = createDeferred();\n    let childLoaderDfd = createDeferred();\n\n    router = createRouter({\n      history: createMemoryHistory(),\n      routes: [\n        {\n          path: \"/\",\n        },\n        {\n          id: \"parent\",\n          path: \"parent\",\n          loader: () => loaderDfd.promise,\n        },\n      ],\n      async patchRoutesOnNavigation({ patch }) {\n        let children = await childrenDfd.promise;\n        patch(\"parent\", children);\n      },\n    });\n\n    router.navigate(\"/parent/child\");\n    expect(router.state.navigation).toMatchObject({\n      state: \"loading\",\n      location: { pathname: \"/parent/child\" },\n    });\n\n    loaderDfd.resolve(\"PARENT\");\n    expect(router.state.navigation).toMatchObject({\n      state: \"loading\",\n      location: { pathname: \"/parent/child\" },\n    });\n\n    childrenDfd.resolve([\n      {\n        id: \"child\",\n        path: \"child\",\n        loader: () => childLoaderDfd.promise,\n      },\n    ]);\n    expect(router.state.navigation).toMatchObject({\n      state: \"loading\",\n      location: { pathname: \"/parent/child\" },\n    });\n\n    childLoaderDfd.resolve(\"CHILD\");\n    await tick();\n\n    expect(router.state.location.pathname).toBe(\"/parent/child\");\n    expect(router.state.loaderData).toEqual({\n      parent: \"PARENT\",\n      child: \"CHILD\",\n    });\n    expect(router.state.matches.map((m) => m.route.id)).toEqual([\n      \"parent\",\n      \"child\",\n    ]);\n  });\n\n  it(\"discovers child routes at a depth >1 (GET navigation)\", async () => {\n    router = createRouter({\n      history: createMemoryHistory(),\n      routes: [\n        {\n          path: \"/\",\n        },\n        {\n          id: \"a\",\n          path: \"a\",\n        },\n      ],\n      async patchRoutesOnNavigation({ patch, matches }) {\n        await tick();\n        if (last(matches).route.id === \"a\") {\n          patch(\"a\", [\n            {\n              id: \"b\",\n              path: \"b\",\n            },\n          ]);\n        }\n\n        if (last(matches).route.id === \"b\") {\n          patch(\"b\", [\n            {\n              id: \"c\",\n              path: \"c\",\n              async loader() {\n                await tick();\n                return \"C\";\n              },\n            },\n          ]);\n        }\n      },\n    });\n\n    await router.navigate(\"/a/b/c\");\n    expect(router.state.location.pathname).toBe(\"/a/b/c\");\n    expect(router.state.loaderData).toEqual({\n      c: \"C\",\n    });\n    expect(router.state.matches.map((m) => m.route.id)).toEqual([\n      \"a\",\n      \"b\",\n      \"c\",\n    ]);\n  });\n\n  it(\"discovers child routes at a depth >1 when a separate matching param route exists (GET navigation)\", async () => {\n    router = createRouter({\n      history: createMemoryHistory(),\n      routes: [\n        {\n          path: \"/\",\n        },\n        {\n          id: \"a\",\n          path: \"a\",\n          handle: {\n            async lazyChildren() {\n              await tick();\n              return [\n                {\n                  id: \"b\",\n                  path: \"b\",\n                  handle: {\n                    async lazyChildren() {\n                      await tick();\n                      return [{ id: \"c\", path: \"c\" }];\n                    },\n                  },\n                },\n              ];\n            },\n          },\n        },\n        {\n          id: \"splat\",\n          path: \"*\",\n        },\n      ],\n      async patchRoutesOnNavigation({ patch, matches }) {\n        await tick();\n        const leafRoute = last(matches).route;\n        if (leafRoute.handle?.lazyChildren) {\n          const children = await leafRoute.handle.lazyChildren();\n          patch(leafRoute.id, children);\n        }\n      },\n    });\n\n    await router.navigate(\"/a/b/c\");\n    expect(router.state.location.pathname).toBe(\"/a/b/c\");\n    expect(router.state.matches.map((m) => m.route.id)).toEqual([\n      \"a\",\n      \"b\",\n      \"c\",\n    ]);\n  });\n\n  it(\"discovers child route at a depth of 1 (POST navigation)\", async () => {\n    let childrenDfd = createDeferred<AgnosticDataRouteObject[]>();\n    let loaderDfd = createDeferred();\n    let childActionDfd = createDeferred();\n    let childLoaderDfd = createDeferred();\n\n    router = createRouter({\n      history: createMemoryHistory(),\n      routes: [\n        {\n          path: \"/\",\n        },\n        {\n          id: \"parent\",\n          path: \"parent\",\n          loader: () => loaderDfd.promise,\n        },\n      ],\n      async patchRoutesOnNavigation({ patch }) {\n        let children = await childrenDfd.promise;\n        patch(\"parent\", children);\n      },\n    });\n\n    router.navigate(\"/parent/child\", {\n      formMethod: \"POST\",\n      formData: createFormData({}),\n    });\n    expect(router.state.navigation).toMatchObject({\n      state: \"submitting\",\n      location: { pathname: \"/parent/child\" },\n    });\n\n    childrenDfd.resolve([\n      {\n        id: \"child\",\n        path: \"child\",\n        action: () => childActionDfd.promise,\n        loader: () => childLoaderDfd.promise,\n      },\n    ]);\n    expect(router.state.navigation).toMatchObject({\n      state: \"submitting\",\n      location: { pathname: \"/parent/child\" },\n    });\n\n    childActionDfd.resolve(\"CHILD ACTION\");\n    await tick();\n    expect(router.state.navigation).toMatchObject({\n      state: \"loading\",\n      location: { pathname: \"/parent/child\" },\n    });\n    expect(router.state.actionData?.child).toBe(\"CHILD ACTION\");\n\n    loaderDfd.resolve(\"PARENT\");\n    expect(router.state.navigation).toMatchObject({\n      state: \"loading\",\n      location: { pathname: \"/parent/child\" },\n    });\n\n    childLoaderDfd.resolve(\"CHILD\");\n    await tick();\n\n    expect(router.state).toMatchObject({\n      location: { pathname: \"/parent/child\" },\n      actionData: {\n        child: \"CHILD ACTION\",\n      },\n      loaderData: {\n        parent: \"PARENT\",\n        child: \"CHILD\",\n      },\n      navigation: { state: \"idle\" },\n    });\n    expect(router.state.matches.map((m) => m.route.id)).toEqual([\n      \"parent\",\n      \"child\",\n    ]);\n  });\n\n  it(\"discovers child routes at a depth >1 (POST navigation)\", async () => {\n    router = createRouter({\n      history: createMemoryHistory(),\n      routes: [\n        {\n          path: \"/\",\n        },\n        {\n          id: \"a\",\n          path: \"a\",\n        },\n      ],\n      async patchRoutesOnNavigation({ patch, matches }) {\n        await tick();\n        if (last(matches).route.id === \"a\") {\n          patch(\"a\", [\n            {\n              id: \"b\",\n              path: \"b\",\n            },\n          ]);\n        }\n\n        if (last(matches).route.id === \"b\") {\n          patch(\"b\", [\n            {\n              id: \"c\",\n              path: \"c\",\n              async action() {\n                await tick();\n                return \"C ACTION\";\n              },\n              async loader() {\n                await tick();\n                return \"C\";\n              },\n            },\n          ]);\n        }\n      },\n    });\n\n    await router.navigate(\"/a/b/c\", {\n      formMethod: \"POST\",\n      formData: createFormData({}),\n    });\n    expect(router.state).toMatchObject({\n      location: { pathname: \"/a/b/c\" },\n      actionData: {\n        c: \"C ACTION\",\n      },\n      loaderData: {\n        c: \"C\",\n      },\n    });\n    expect(router.state.matches.map((m) => m.route.id)).toEqual([\n      \"a\",\n      \"b\",\n      \"c\",\n    ]);\n  });\n\n  it(\"discovers child routes at a depth >1 when a separate matching param route exists (POST navigation)\", async () => {\n    router = createRouter({\n      history: createMemoryHistory(),\n      routes: [\n        {\n          path: \"/\",\n        },\n        {\n          id: \"a\",\n          path: \"a\",\n          handle: {\n            async lazyChildren() {\n              await tick();\n              return [\n                {\n                  id: \"b\",\n                  path: \"b\",\n                  handle: {\n                    async lazyChildren() {\n                      await tick();\n                      return [{ id: \"c\", path: \"c\" }];\n                    },\n                  },\n                },\n              ];\n            },\n          },\n        },\n        {\n          id: \"splat\",\n          path: \"*\",\n        },\n      ],\n      async patchRoutesOnNavigation({ patch, matches }) {\n        await tick();\n        const leafRoute = last(matches).route;\n        if (leafRoute.handle?.lazyChildren) {\n          const children = await leafRoute.handle.lazyChildren();\n          patch(leafRoute.id, children);\n        }\n      },\n    });\n\n    await router.navigate(\"/a/b/c\", {\n      formMethod: \"POST\",\n      formData: createFormData({}),\n    });\n    expect(router.state.location.pathname).toBe(\"/a/b/c\");\n    expect(router.state.matches.map((m) => m.route.id)).toEqual([\n      \"a\",\n      \"b\",\n      \"c\",\n    ]);\n  });\n\n  it(\"does not reuse former calls to patchRoutes on interruptions\", async () => {\n    let aDfd = createDeferred<AgnosticDataRouteObject[]>();\n    let calls: string[][] = [];\n    router = createRouter({\n      history: createMemoryHistory(),\n      routes: [\n        {\n          path: \"/\",\n        },\n        {\n          id: \"a\",\n          path: \"a\",\n        },\n      ],\n      async patchRoutesOnNavigation({ path, matches, patch }) {\n        let routeId = last(matches).route.id;\n        calls.push([path, routeId]);\n        patch(\"a\", await aDfd.promise);\n      },\n    });\n\n    router.navigate(\"/a/b\");\n    await tick();\n    expect(router.state).toMatchObject({\n      navigation: { state: \"loading\", location: { pathname: \"/a/b\" } },\n    });\n    expect(calls).toEqual([[\"/a/b\", \"a\"]]);\n\n    router.navigate(\"/a/b\", {\n      formMethod: \"POST\",\n      formData: createFormData({}),\n    });\n    await tick();\n    expect(router.state).toMatchObject({\n      navigation: { state: \"submitting\", location: { pathname: \"/a/b\" } },\n    });\n    expect(calls).toEqual([\n      [\"/a/b\", \"a\"],\n      [\"/a/b\", \"a\"],\n    ]);\n\n    aDfd.resolve([\n      {\n        id: \"b\",\n        path: \"b\",\n        action: () => \"A ACTION\",\n        loader: () => \"A\",\n      },\n    ]);\n    await tick();\n    expect(router.state).toMatchObject({\n      navigation: { state: \"idle\" },\n      location: { pathname: \"/a/b\" },\n    });\n    expect(calls).toEqual([\n      [\"/a/b\", \"a\"],\n      [\"/a/b\", \"a\"],\n    ]);\n  });\n\n  it(\"handles interruptions when navigating to the same route\", async () => {\n    let dfd1 = createDeferred<AgnosticDataRouteObject[]>();\n    let dfd2 = createDeferred<AgnosticDataRouteObject[]>();\n    let called = false;\n    router = createRouter({\n      history: createMemoryHistory(),\n      routes: [\n        {\n          path: \"/\",\n        },\n      ],\n      async patchRoutesOnNavigation({ patch }) {\n        if (!called) {\n          called = true;\n          patch(null, await dfd1.promise);\n        } else {\n          patch(null, await dfd2.promise);\n        }\n      },\n    });\n\n    router.navigate(\"/a\");\n    await tick();\n    expect(router.state).toMatchObject({\n      navigation: { state: \"loading\", location: { pathname: \"/a\" } },\n    });\n\n    router.navigate(\"/a\");\n    await tick();\n    expect(router.state).toMatchObject({\n      navigation: { state: \"loading\", location: { pathname: \"/a\" } },\n    });\n\n    dfd1.resolve([\n      {\n        id: \"a1\",\n        path: \"/a\",\n      },\n    ]);\n    await tick();\n    expect(router.state).toMatchObject({\n      navigation: { state: \"loading\", location: { pathname: \"/a\" } },\n    });\n\n    dfd2.resolve([\n      {\n        id: \"a2\",\n        path: \"/a\",\n      },\n    ]);\n    await tick();\n    expect(router.state).toMatchObject({\n      location: { pathname: \"/a\" },\n      navigation: IDLE_NAVIGATION,\n      matches: [{ route: { id: \"a2\" } }],\n    });\n  });\n\n  it(\"handles interruptions when navigating to a new route\", async () => {\n    let aDfd = createDeferred<AgnosticDataRouteObject[]>();\n    let bDfd = createDeferred<AgnosticDataRouteObject[]>();\n    router = createRouter({\n      history: createMemoryHistory(),\n      routes: [\n        {\n          path: \"/\",\n        },\n        {\n          id: \"a\",\n          path: \"a\",\n        },\n      ],\n      async patchRoutesOnNavigation({ path, matches, patch }) {\n        let routeId = last(matches).route.id;\n        if (!path) {\n          return;\n        }\n        if (routeId === \"a\") {\n          patch(\"a\", await aDfd.promise);\n        } else if (routeId === \"b\") {\n          patch(\"b\", await bDfd.promise);\n        }\n      },\n    });\n\n    router.navigate(\"/a/b/c\");\n    await tick();\n    expect(router.state).toMatchObject({\n      navigation: { state: \"loading\", location: { pathname: \"/a/b/c\" } },\n    });\n\n    aDfd.resolve([\n      {\n        id: \"b\",\n        path: \"b\",\n      },\n    ]);\n    await tick();\n    expect(router.state).toMatchObject({\n      navigation: { state: \"loading\", location: { pathname: \"/a/b/c\" } },\n    });\n\n    router.navigate(\"/a/b/d\");\n    await tick();\n    expect(router.state).toMatchObject({\n      navigation: { state: \"loading\", location: { pathname: \"/a/b/d\" } },\n    });\n\n    bDfd.resolve([\n      {\n        id: \"c\",\n        path: \"c\",\n        loader() {\n          return \"C\";\n        },\n      },\n      {\n        id: \"d\",\n        path: \"d\",\n        loader() {\n          return \"D\";\n        },\n      },\n    ]);\n    await tick();\n\n    expect(router.state.location.pathname).toBe(\"/a/b/d\");\n    expect(router.state.loaderData).toEqual({\n      d: \"D\",\n    });\n    expect(router.state.matches.map((m) => m.route.id)).toEqual([\n      \"a\",\n      \"b\",\n      \"d\",\n    ]);\n  });\n\n  it(\"allows folks to implement at the route level via handle.children()\", async () => {\n    router = createRouter({\n      history: createMemoryHistory(),\n      routes: [\n        {\n          path: \"/\",\n        },\n        {\n          id: \"a\",\n          path: \"a\",\n          handle: {\n            async loadChildren() {\n              await tick();\n              return [\n                {\n                  id: \"b\",\n                  path: \"b\",\n                  handle: {\n                    async loadChildren() {\n                      await tick();\n                      return [\n                        {\n                          id: \"c\",\n                          path: \"c\",\n                          async loader() {\n                            await tick();\n                            return \"C\";\n                          },\n                        },\n                      ];\n                    },\n                  },\n                },\n              ];\n            },\n          },\n        },\n      ],\n      async patchRoutesOnNavigation({ matches, patch }) {\n        let leafRoute = last(matches).route;\n        patch(leafRoute.id, await leafRoute.handle.loadChildren?.());\n      },\n    });\n\n    await router.navigate(\"/a/b/c\");\n    expect(router.state.location.pathname).toBe(\"/a/b/c\");\n    expect(router.state.loaderData).toEqual({\n      c: \"C\",\n    });\n    expect(router.state.matches.map((m) => m.route.id)).toEqual([\n      \"a\",\n      \"b\",\n      \"c\",\n    ]);\n  });\n\n  it(\"discovers child routes through pathless routes\", async () => {\n    router = createRouter({\n      history: createMemoryHistory(),\n      routes: [\n        {\n          path: \"/\",\n        },\n        {\n          id: \"a\",\n          path: \"a\",\n        },\n      ],\n      async patchRoutesOnNavigation({ matches, patch }) {\n        await tick();\n        if (last(matches).route.id === \"a\") {\n          patch(\"a\", [\n            {\n              id: \"pathless\",\n              path: \"\",\n            },\n          ]);\n        } else if (last(matches).route.id === \"pathless\") {\n          patch(\"pathless\", [\n            {\n              id: \"b\",\n              path: \"b\",\n              async loader() {\n                await tick();\n                return \"B\";\n              },\n            },\n          ]);\n        }\n      },\n    });\n\n    await router.navigate(\"/a/b\");\n    expect(router.state.location.pathname).toBe(\"/a/b\");\n    expect(router.state.loaderData).toEqual({\n      b: \"B\",\n    });\n    expect(router.state.matches.map((m) => m.route.id)).toEqual([\n      \"a\",\n      \"pathless\",\n      \"b\",\n    ]);\n  });\n\n  it(\"de-prioritizes dynamic param routes in favor of looking for better async matches\", async () => {\n    router = createRouter({\n      history: createMemoryHistory(),\n      routes: [\n        {\n          path: \"/\",\n        },\n        {\n          id: \"slug\",\n          path: \"/:slug\",\n        },\n      ],\n      async patchRoutesOnNavigation({ patch }) {\n        await tick();\n        patch(null, [\n          {\n            id: \"static\",\n            path: \"/static\",\n          },\n        ]);\n      },\n    });\n\n    await router.navigate(\"/static\");\n    expect(router.state.location.pathname).toBe(\"/static\");\n    expect(router.state.matches.map((m) => m.route.id)).toEqual([\"static\"]);\n  });\n\n  it(\"de-prioritizes dynamic param routes in favor of looking for better async matches (product/:slug)\", async () => {\n    router = createRouter({\n      history: createMemoryHistory(),\n      routes: [\n        {\n          path: \"/\",\n        },\n        {\n          id: \"slug\",\n          path: \"/product/:slug\",\n        },\n      ],\n      async patchRoutesOnNavigation({ patch }) {\n        await tick();\n        patch(null, [\n          {\n            id: \"static\",\n            path: \"/product/static\",\n          },\n        ]);\n      },\n    });\n\n    await router.navigate(\"/product/static\");\n    expect(router.state.location.pathname).toBe(\"/product/static\");\n    expect(router.state.matches.map((m) => m.route.id)).toEqual([\"static\"]);\n  });\n\n  it(\"de-prioritizes dynamic param routes in favor of looking for better async matches (child route)\", async () => {\n    router = createRouter({\n      history: createMemoryHistory(),\n      routes: [\n        {\n          path: \"/\",\n        },\n        {\n          id: \"product\",\n          path: \"/product\",\n          children: [\n            {\n              id: \"slug\",\n              path: \":slug\",\n            },\n          ],\n        },\n      ],\n      async patchRoutesOnNavigation({ patch }) {\n        await tick();\n        patch(\"product\", [\n          {\n            id: \"static\",\n            path: \"static\",\n          },\n        ]);\n      },\n    });\n\n    await router.navigate(\"/product/static\");\n    expect(router.state.location.pathname).toBe(\"/product/static\");\n    expect(router.state.matches.map((m) => m.route.id)).toEqual([\n      \"product\",\n      \"static\",\n    ]);\n  });\n\n  it(\"matches dynamic params when other paths don't pan out\", async () => {\n    router = createRouter({\n      history: createMemoryHistory(),\n      routes: [\n        {\n          path: \"/\",\n        },\n        {\n          id: \"slug\",\n          path: \"/:slug\",\n        },\n      ],\n      async patchRoutesOnNavigation({ matches, patch }) {\n        await tick();\n      },\n    });\n\n    await router.navigate(\"/a\");\n    expect(router.state.location.pathname).toBe(\"/a\");\n    expect(router.state.matches.map((m) => m.route.id)).toEqual([\"slug\"]);\n  });\n\n  it(\"de-prioritizes splat routes in favor of looking for better async matches\", async () => {\n    router = createRouter({\n      history: createMemoryHistory(),\n      routes: [\n        {\n          path: \"/\",\n        },\n        {\n          id: \"splat\",\n          path: \"*\",\n        },\n        {\n          id: \"a\",\n          path: \"a\",\n        },\n      ],\n      async patchRoutesOnNavigation({ matches, patch }) {\n        await tick();\n        if (last(matches).route.id === \"a\") {\n          patch(\"a\", [\n            {\n              id: \"b\",\n              path: \"b\",\n            },\n          ]);\n        }\n      },\n    });\n\n    await router.navigate(\"/a/b\");\n    expect(router.state.location.pathname).toBe(\"/a/b\");\n    expect(router.state.matches.map((m) => m.route.id)).toEqual([\"a\", \"b\"]);\n  });\n\n  it(\"de-prioritizes splat routes in favor of looking for better async matches (splat/*)\", async () => {\n    router = createRouter({\n      history: createMemoryHistory(),\n      routes: [\n        {\n          path: \"/\",\n        },\n        {\n          id: \"splat\",\n          path: \"/splat/*\",\n        },\n      ],\n      async patchRoutesOnNavigation({ matches, patch }) {\n        await tick();\n        patch(null, [\n          {\n            id: \"static\",\n            path: \"/splat/static\",\n          },\n        ]);\n      },\n    });\n\n    await router.navigate(\"/splat/static\");\n    expect(router.state.location.pathname).toBe(\"/splat/static\");\n    expect(router.state.matches.map((m) => m.route.id)).toEqual([\"static\"]);\n  });\n\n  it(\"de-prioritizes splat routes in favor of looking for better async matches (child route)\", async () => {\n    router = createRouter({\n      history: createMemoryHistory(),\n      routes: [\n        {\n          path: \"/\",\n        },\n        {\n          id: \"product\",\n          path: \"/product\",\n          children: [\n            {\n              id: \"splat\",\n              path: \"*\",\n            },\n          ],\n        },\n      ],\n      async patchRoutesOnNavigation({ patch }) {\n        await tick();\n        patch(\"product\", [\n          {\n            id: \"static\",\n            path: \"static\",\n          },\n        ]);\n      },\n    });\n\n    await router.navigate(\"/product/static\");\n    expect(router.state.location.pathname).toBe(\"/product/static\");\n    expect(router.state.matches.map((m) => m.route.id)).toEqual([\n      \"product\",\n      \"static\",\n    ]);\n  });\n\n  it(\"matches splats when other paths don't pan out\", async () => {\n    router = createRouter({\n      history: createMemoryHistory(),\n      routes: [\n        {\n          path: \"/\",\n        },\n        {\n          id: \"splat\",\n          path: \"*\",\n        },\n        {\n          id: \"a\",\n          path: \"a\",\n        },\n      ],\n      async patchRoutesOnNavigation({ matches, patch }) {\n        await tick();\n        if (last(matches).route.id === \"a\") {\n          patch(\"a\", [\n            {\n              id: \"b\",\n              path: \"b\",\n            },\n          ]);\n        }\n      },\n    });\n\n    await router.navigate(\"/a/nope\");\n    expect(router.state.location.pathname).toBe(\"/a/nope\");\n    expect(router.state.matches.map((m) => m.route.id)).toEqual([\"splat\"]);\n  });\n\n  it(\"recurses patchRoutesOnNavigation until a match is found\", async () => {\n    let count = 0;\n    router = createRouter({\n      history: createMemoryHistory(),\n      routes: [\n        {\n          path: \"/\",\n        },\n        {\n          id: \"a\",\n          path: \"a\",\n        },\n      ],\n      async patchRoutesOnNavigation({ matches, patch }) {\n        await tick();\n        count++;\n        if (last(matches).route.id === \"a\") {\n          patch(\"a\", [\n            {\n              id: \"b\",\n              path: \"b\",\n            },\n          ]);\n        } else if (last(matches).route.id === \"b\") {\n          patch(\"b\", [\n            {\n              id: \"c\",\n              path: \"c\",\n            },\n          ]);\n        }\n      },\n    });\n\n    await router.navigate(\"/a/b/c\");\n    expect(router.state.location.pathname).toBe(\"/a/b/c\");\n    expect(router.state.matches.map((m) => m.route.id)).toEqual([\n      \"a\",\n      \"b\",\n      \"c\",\n    ]);\n    expect(count).toBe(2);\n  });\n\n  it(\"discovers routes during initial hydration\", async () => {\n    let childrenDfd = createDeferred<AgnosticDataRouteObject[]>();\n    let loaderDfd = createDeferred();\n    let childLoaderDfd = createDeferred();\n\n    router = createRouter({\n      history: createMemoryHistory({ initialEntries: [\"/parent/child\"] }),\n      routes: [\n        {\n          path: \"/\",\n        },\n        {\n          id: \"parent\",\n          path: \"parent\",\n          loader: () => loaderDfd.promise,\n        },\n      ],\n      async patchRoutesOnNavigation({ patch }) {\n        let children = await childrenDfd.promise;\n        patch(\"parent\", children);\n      },\n    });\n    router.initialize();\n\n    expect(router.state.initialized).toBe(false);\n    expect(router.state.matches.map((m) => m.route.id)).toEqual([\"parent\"]);\n\n    loaderDfd.resolve(\"PARENT\");\n    expect(router.state.initialized).toBe(false);\n    expect(router.state.matches.map((m) => m.route.id)).toEqual([\"parent\"]);\n\n    childrenDfd.resolve([\n      {\n        id: \"child\",\n        path: \"child\",\n        loader: () => childLoaderDfd.promise,\n      },\n    ]);\n    expect(router.state.initialized).toBe(false);\n    expect(router.state.matches.map((m) => m.route.id)).toEqual([\"parent\"]);\n\n    childLoaderDfd.resolve(\"CHILD\");\n    await tick();\n    expect(router.state.initialized).toBe(true);\n    expect(router.state.location.pathname).toBe(\"/parent/child\");\n    expect(router.state.loaderData).toEqual({\n      parent: \"PARENT\",\n      child: \"CHILD\",\n    });\n    expect(router.state.matches.map((m) => m.route.id)).toEqual([\n      \"parent\",\n      \"child\",\n    ]);\n  });\n\n  it(\"discovers routes during initial SPA renders when a splat route matches\", async () => {\n    let childrenDfd = createDeferred<AgnosticDataRouteObject[]>();\n\n    router = createRouter({\n      history: createMemoryHistory({ initialEntries: [\"/test\"] }),\n      routes: [\n        {\n          path: \"/\",\n        },\n        {\n          path: \"*\",\n        },\n      ],\n      async patchRoutesOnNavigation({ patch }) {\n        let children = await childrenDfd.promise;\n        patch(null, children);\n      },\n    });\n    router.initialize();\n    expect(router.state.initialized).toBe(false);\n\n    childrenDfd.resolve([\n      {\n        id: \"test\",\n        path: \"/test\",\n      },\n    ]);\n    await tick();\n    expect(router.state.initialized).toBe(true);\n    expect(router.state.location.pathname).toBe(\"/test\");\n    expect(router.state.matches.map((m) => m.route.id)).toEqual([\"test\"]);\n  });\n\n  it(\"does not discover routes during initial SSR hydration when a splat route matches\", async () => {\n    router = createRouter({\n      history: createMemoryHistory({ initialEntries: [\"/test\"] }),\n      routes: [\n        {\n          path: \"/\",\n        },\n        {\n          id: \"splat\",\n          loader: () => \"SPLAT 2\",\n          path: \"*\",\n        },\n      ],\n      hydrationData: {\n        loaderData: {\n          splat: \"SPLAT 1\",\n        },\n      },\n      async patchRoutesOnNavigation() {\n        throw new Error(\"Should not be called\");\n      },\n    });\n    router.initialize();\n\n    await tick();\n    expect(router.state.initialized).toBe(true);\n    expect(router.state.location.pathname).toBe(\"/test\");\n    expect(router.state.loaderData.splat).toBe(\"SPLAT 1\");\n    expect(router.state.matches.map((m) => m.route.id)).toEqual([\"splat\"]);\n  });\n\n  it(\"discovers new root routes\", async () => {\n    let childrenDfd = createDeferred<AgnosticDataRouteObject[]>();\n    let childLoaderDfd = createDeferred();\n\n    router = createRouter({\n      history: createMemoryHistory(),\n      routes: [\n        {\n          path: \"/\",\n        },\n        {\n          path: \"/parent\",\n        },\n      ],\n      async patchRoutesOnNavigation({ patch }) {\n        patch(null, await childrenDfd.promise);\n      },\n    });\n\n    router.navigate(\"/parent/child\");\n    expect(router.state.navigation).toMatchObject({\n      state: \"loading\",\n      location: { pathname: \"/parent/child\" },\n    });\n\n    childrenDfd.resolve([\n      {\n        id: \"parent-child\",\n        path: \"/parent/child\",\n        loader: () => childLoaderDfd.promise,\n      },\n    ]);\n    expect(router.state.navigation).toMatchObject({\n      state: \"loading\",\n      location: { pathname: \"/parent/child\" },\n    });\n\n    childLoaderDfd.resolve(\"CHILD\");\n    await tick();\n\n    expect(router.state.location.pathname).toBe(\"/parent/child\");\n    expect(router.state.loaderData).toEqual({\n      \"parent-child\": \"CHILD\",\n    });\n    expect(router.state.matches.map((m) => m.route.id)).toEqual([\n      \"parent-child\",\n    ]);\n  });\n\n  it(\"lets you patch elsewhere in the tree (dynamic param)\", async () => {\n    let childrenDfd = createDeferred<AgnosticDataRouteObject[]>();\n    let childLoaderDfd = createDeferred();\n\n    router = createRouter({\n      history: createMemoryHistory(),\n      routes: [\n        {\n          id: \"root\",\n          path: \"/\",\n        },\n        {\n          id: \"param\",\n          path: \"/:param\",\n        },\n      ],\n      async patchRoutesOnNavigation({ matches, patch }) {\n        // We matched for the param but we want to patch in under root\n        expect(matches.length).toBe(1);\n        expect(matches[0].route.id).toBe(\"param\");\n        patch(\"root\", await childrenDfd.promise);\n      },\n    });\n\n    router.navigate(\"/parent/child\");\n    expect(router.state.navigation).toMatchObject({\n      state: \"loading\",\n      location: { pathname: \"/parent/child\" },\n    });\n\n    childrenDfd.resolve([\n      {\n        id: \"parent-child\",\n        path: \"/parent/child\",\n        loader: () => childLoaderDfd.promise,\n      },\n    ]);\n    expect(router.state.navigation).toMatchObject({\n      state: \"loading\",\n      location: { pathname: \"/parent/child\" },\n    });\n\n    childLoaderDfd.resolve(\"CHILD\");\n    await tick();\n\n    expect(router.state.location.pathname).toBe(\"/parent/child\");\n    expect(router.state.loaderData).toEqual({\n      \"parent-child\": \"CHILD\",\n    });\n    expect(router.state.matches.map((m) => m.route.id)).toEqual([\n      \"root\",\n      \"parent-child\",\n    ]);\n  });\n\n  it(\"lets you patch elsewhere in the tree (splat)\", async () => {\n    let childrenDfd = createDeferred<AgnosticDataRouteObject[]>();\n    let childLoaderDfd = createDeferred();\n\n    router = createRouter({\n      history: createMemoryHistory({ initialEntries: [\"/other\"] }),\n      routes: [\n        {\n          id: \"other\",\n          path: \"/other\",\n        },\n        {\n          id: \"splat\",\n          path: \"*\",\n        },\n      ],\n      async patchRoutesOnNavigation({ matches, patch }) {\n        // We matched for the splat but we want to patch in at the top\n        expect(matches.length).toBe(1);\n        expect(matches[0].route.id).toBe(\"splat\");\n        let children = await childrenDfd.promise;\n        patch(null, children);\n      },\n    });\n\n    router.navigate(\"/parent/child\");\n    expect(router.state.navigation).toMatchObject({\n      state: \"loading\",\n      location: { pathname: \"/parent/child\" },\n    });\n\n    childrenDfd.resolve([\n      {\n        id: \"parent-child\",\n        path: \"/parent/child\",\n        loader: () => childLoaderDfd.promise,\n      },\n    ]);\n    expect(router.state.navigation).toMatchObject({\n      state: \"loading\",\n      location: { pathname: \"/parent/child\" },\n    });\n\n    childLoaderDfd.resolve(\"CHILD\");\n    await tick();\n\n    expect(router.state.location.pathname).toBe(\"/parent/child\");\n    expect(router.state.loaderData).toEqual({\n      \"parent-child\": \"CHILD\",\n    });\n    expect(router.state.matches.map((m) => m.route.id)).toEqual([\n      \"parent-child\",\n    ]);\n  });\n\n  it(\"works when there are no partial matches\", async () => {\n    let childrenDfd = createDeferred<AgnosticDataRouteObject[]>();\n    let childLoaderDfd = createDeferred();\n\n    router = createRouter({\n      history: createMemoryHistory(),\n      routes: [\n        {\n          path: \"/nope\",\n        },\n      ],\n      async patchRoutesOnNavigation({ matches, patch }) {\n        expect(matches.length).toBe(0);\n        let children = await childrenDfd.promise;\n        patch(null, children);\n      },\n    });\n\n    router.navigate(\"/parent/child\");\n    expect(router.state.navigation).toMatchObject({\n      state: \"loading\",\n      location: { pathname: \"/parent/child\" },\n    });\n\n    childrenDfd.resolve([\n      {\n        id: \"parent-child\",\n        path: \"/parent/child\",\n        loader: () => childLoaderDfd.promise,\n      },\n    ]);\n    expect(router.state.navigation).toMatchObject({\n      state: \"loading\",\n      location: { pathname: \"/parent/child\" },\n    });\n\n    childLoaderDfd.resolve(\"CHILD\");\n    await tick();\n\n    expect(router.state.location.pathname).toBe(\"/parent/child\");\n    expect(router.state.loaderData).toEqual({\n      \"parent-child\": \"CHILD\",\n    });\n    expect(router.state.matches.map((m) => m.route.id)).toEqual([\n      \"parent-child\",\n    ]);\n  });\n\n  it(\"creates a new router.routes identity when patching routes\", async () => {\n    let childrenDfd = createDeferred<AgnosticDataRouteObject[]>();\n\n    router = createRouter({\n      history: createMemoryHistory(),\n      routes: [\n        {\n          path: \"/\",\n        },\n        {\n          id: \"parent\",\n          path: \"parent\",\n        },\n      ],\n      async patchRoutesOnNavigation({ patch }) {\n        let children = await childrenDfd.promise;\n        patch(\"parent\", children);\n      },\n    });\n    let originalRoutes = router.routes;\n\n    router.navigate(\"/parent/child\");\n    childrenDfd.resolve([\n      {\n        id: \"child\",\n        path: \"child\",\n      },\n    ]);\n    await tick();\n\n    expect(router.state.location.pathname).toBe(\"/parent/child\");\n    expect(router.state.matches.map((m) => m.route.id)).toEqual([\n      \"parent\",\n      \"child\",\n    ]);\n\n    expect(router.routes).not.toBe(originalRoutes);\n  });\n\n  it(\"allows patching externally/eagerly and triggers a reflow\", async () => {\n    router = createRouter({\n      history: createMemoryHistory(),\n      routes: [\n        {\n          path: \"/\",\n        },\n        {\n          id: \"parent\",\n          path: \"parent\",\n        },\n      ],\n    });\n    let spy = jest.fn();\n    let unsubscribe = router.subscribe(spy);\n    let originalRoutes = router.routes;\n    router.patchRoutes(\"parent\", [\n      {\n        id: \"child\",\n        path: \"child\",\n      },\n    ]);\n    expect(spy).toHaveBeenCalled();\n    expect(router.routes).not.toBe(originalRoutes);\n\n    await router.navigate(\"/parent/child\");\n    expect(router.state.location.pathname).toBe(\"/parent/child\");\n    expect(router.state.matches.map((m) => m.route.id)).toEqual([\n      \"parent\",\n      \"child\",\n    ]);\n\n    unsubscribe();\n  });\n\n  it(\"does not re-patch previously patched routes\", async () => {\n    let count = 0;\n    router = createRouter({\n      history: createMemoryHistory(),\n      routes: [\n        {\n          path: \"/\",\n        },\n      ],\n      async patchRoutesOnNavigation({ patch }) {\n        count++;\n        patch(null, [\n          {\n            id: \"param\",\n            path: \":param\",\n          },\n        ]);\n        await tick();\n      },\n    });\n\n    await router.navigate(\"/a\");\n    expect(router.state.location.pathname).toBe(\"/a\");\n    expect(router.state.matches.map((m) => m.route.id)).toEqual([\"param\"]);\n    expect(count).toBe(1);\n    expect(router.routes).toMatchInlineSnapshot(`\n      [\n        {\n          \"children\": undefined,\n          \"hasErrorBoundary\": false,\n          \"id\": \"0\",\n          \"path\": \"/\",\n        },\n        {\n          \"children\": undefined,\n          \"hasErrorBoundary\": false,\n          \"id\": \"param\",\n          \"path\": \":param\",\n        },\n      ]\n    `);\n\n    await router.navigate(\"/\");\n    expect(router.state.location.pathname).toBe(\"/\");\n    expect(count).toBe(1);\n\n    await router.navigate(\"/b\");\n    expect(router.state.location.pathname).toBe(\"/b\");\n    expect(router.state.matches.map((m) => m.route.id)).toEqual([\"param\"]);\n    expect(router.state.errors).toBeNull();\n    // Called again\n    expect(count).toBe(2);\n    // But not patched again\n    expect(router.routes).toMatchInlineSnapshot(`\n      [\n        {\n          \"children\": undefined,\n          \"hasErrorBoundary\": false,\n          \"id\": \"0\",\n          \"path\": \"/\",\n        },\n        {\n          \"children\": undefined,\n          \"hasErrorBoundary\": false,\n          \"id\": \"param\",\n          \"path\": \":param\",\n        },\n      ]\n    `);\n  });\n\n  it(\"distinguishes sibling pathless layout routes in idempotent patch check (via id)\", async () => {\n    let count = 0;\n    router = createRouter({\n      history: createMemoryHistory(),\n      routes: [\n        {\n          id: \"root\",\n          path: \"/\",\n          children: [\n            {\n              id: \"a-layout\",\n              children: [\n                {\n                  id: \"a\",\n                  path: \"a\",\n                },\n              ],\n            },\n          ],\n        },\n      ],\n      async patchRoutesOnNavigation({ patch, path }) {\n        count++;\n        if (path === \"/b\") {\n          patch(\"root\", [\n            {\n              id: \"b-layout\",\n              children: [\n                {\n                  id: \"b\",\n                  path: \"b\",\n                },\n              ],\n            },\n          ]);\n        }\n        await tick();\n      },\n    });\n\n    await router.navigate(\"/a\");\n    expect(router.state.location.pathname).toBe(\"/a\");\n    expect(router.state.matches.map((m) => m.route.id)).toEqual([\n      \"root\",\n      \"a-layout\",\n      \"a\",\n    ]);\n    expect(router.state.errors).toBeNull();\n    expect(count).toBe(0);\n\n    await router.navigate(\"/b\");\n    expect(router.state.location.pathname).toBe(\"/b\");\n    expect(router.state.matches.map((m) => m.route.id)).toEqual([\n      \"root\",\n      \"b-layout\",\n      \"b\",\n    ]);\n    expect(router.state.errors).toBeNull();\n    expect(count).toBe(1);\n\n    expect(router.routes).toMatchInlineSnapshot(`\n      [\n        {\n          \"children\": [\n            {\n              \"children\": [\n                {\n                  \"children\": undefined,\n                  \"hasErrorBoundary\": false,\n                  \"id\": \"a\",\n                  \"path\": \"a\",\n                },\n              ],\n              \"hasErrorBoundary\": false,\n              \"id\": \"a-layout\",\n            },\n            {\n              \"children\": [\n                {\n                  \"children\": undefined,\n                  \"hasErrorBoundary\": false,\n                  \"id\": \"b\",\n                  \"path\": \"b\",\n                },\n              ],\n              \"hasErrorBoundary\": false,\n              \"id\": \"b-layout\",\n            },\n          ],\n          \"hasErrorBoundary\": false,\n          \"id\": \"root\",\n          \"path\": \"/\",\n        },\n      ]\n    `);\n  });\n\n  it(\"distinguishes sibling pathless layout routes in idempotent patch check (via children)\", async () => {\n    let count = 0;\n    router = createRouter({\n      history: createMemoryHistory(),\n      routes: [\n        {\n          id: \"root\",\n          path: \"/\",\n          children: [\n            {\n              children: [\n                {\n                  path: \"a\",\n                },\n              ],\n            },\n          ],\n        },\n      ],\n      async patchRoutesOnNavigation({ patch, path }) {\n        count++;\n        if (path === \"/b\") {\n          patch(\"root\", [\n            {\n              children: [\n                {\n                  path: \"b\",\n                },\n              ],\n            },\n          ]);\n        }\n        await tick();\n      },\n    });\n\n    await router.navigate(\"/a\");\n    expect(router.state.location.pathname).toBe(\"/a\");\n    expect(router.state.matches.map((m) => m.route.id)).toEqual([\n      \"root\",\n      \"0-0\",\n      \"0-0-0\",\n    ]);\n    expect(router.state.errors).toBeNull();\n    expect(count).toBe(0);\n\n    await router.navigate(\"/b\");\n    expect(router.state.location.pathname).toBe(\"/b\");\n    expect(router.state.matches.map((m) => m.route.id)).toEqual([\n      \"root\",\n      \"root-patch-1-0\",\n      \"root-patch-1-0-0\",\n    ]);\n    expect(router.state.errors).toBeNull();\n    expect(count).toBe(1);\n\n    expect(router.routes).toMatchInlineSnapshot(`\n      [\n        {\n          \"children\": [\n            {\n              \"children\": [\n                {\n                  \"children\": undefined,\n                  \"hasErrorBoundary\": false,\n                  \"id\": \"0-0-0\",\n                  \"path\": \"a\",\n                },\n              ],\n              \"hasErrorBoundary\": false,\n              \"id\": \"0-0\",\n            },\n            {\n              \"children\": [\n                {\n                  \"children\": undefined,\n                  \"hasErrorBoundary\": false,\n                  \"id\": \"root-patch-1-0-0\",\n                  \"path\": \"b\",\n                },\n              ],\n              \"hasErrorBoundary\": false,\n              \"id\": \"root-patch-1-0\",\n            },\n          ],\n          \"hasErrorBoundary\": false,\n          \"id\": \"root\",\n          \"path\": \"/\",\n        },\n      ]\n    `);\n  });\n\n  describe(\"errors\", () => {\n    it(\"lazy 404s (GET navigation)\", async () => {\n      let childrenDfd = createDeferred<AgnosticDataRouteObject[]>();\n\n      router = createRouter({\n        history: createMemoryHistory(),\n        routes: [\n          {\n            path: \"/\",\n          },\n          {\n            id: \"parent\",\n            path: \"parent\",\n          },\n        ],\n        async patchRoutesOnNavigation({ patch }) {\n          let children = await childrenDfd.promise;\n          patch(\"parent\", children);\n        },\n      });\n\n      router.navigate(\"/parent/junk\");\n      expect(router.state.navigation).toMatchObject({\n        state: \"loading\",\n      });\n\n      childrenDfd.resolve([{ id: \"child\", path: \"child\" }]);\n      await tick();\n\n      expect(router.state).toMatchObject({\n        location: { pathname: \"/parent/junk\" },\n        loaderData: {},\n        errors: {\n          \"0\": new ErrorResponseImpl(\n            404,\n            \"Not Found\",\n            new Error('No route matches URL \"/parent/junk\"'),\n            true,\n          ),\n        },\n      });\n      expect(router.state.matches).toEqual([\n        {\n          params: {},\n          pathname: \"\",\n          pathnameBase: \"\",\n          route: {\n            children: undefined,\n            hasErrorBoundary: false,\n            id: \"0\",\n            path: \"/\",\n          },\n        },\n      ]);\n    });\n\n    it(\"lazy 404s (POST navigation)\", async () => {\n      let childrenDfd = createDeferred<AgnosticDataRouteObject[]>();\n\n      router = createRouter({\n        history: createMemoryHistory(),\n        routes: [\n          {\n            path: \"/\",\n          },\n          {\n            id: \"parent\",\n            path: \"parent\",\n          },\n        ],\n        async patchRoutesOnNavigation({ patch }) {\n          let children = await childrenDfd.promise;\n          patch(\"parent\", children);\n        },\n      });\n\n      router.navigate(\"/parent/junk\", {\n        formMethod: \"POST\",\n        formData: createFormData({}),\n      });\n      expect(router.state.navigation).toMatchObject({\n        state: \"submitting\",\n      });\n\n      childrenDfd.resolve([{ id: \"child\", path: \"child\" }]);\n      await tick();\n\n      expect(router.state).toMatchObject({\n        location: { pathname: \"/parent/junk\" },\n        actionData: null,\n        loaderData: {},\n        errors: {\n          \"0\": new ErrorResponseImpl(\n            404,\n            \"Not Found\",\n            new Error('No route matches URL \"/parent/junk\"'),\n            true,\n          ),\n        },\n      });\n      expect(router.state.matches).toEqual([\n        {\n          params: {},\n          pathname: \"\",\n          pathnameBase: \"\",\n          route: {\n            children: undefined,\n            hasErrorBoundary: false,\n            id: \"0\",\n            path: \"/\",\n          },\n        },\n      ]);\n    });\n\n    it(\"errors thrown at lazy boundary route (GET navigation)\", async () => {\n      router = createRouter({\n        history: createMemoryHistory(),\n        routes: [\n          {\n            path: \"/\",\n          },\n          {\n            id: \"a\",\n            path: \"a\",\n          },\n        ],\n        async patchRoutesOnNavigation({ matches, patch }) {\n          await tick();\n          if (last(matches).route.id === \"a\") {\n            patch(\"a\", [\n              {\n                id: \"b\",\n                path: \"b\",\n              },\n            ]);\n          } else if (last(matches).route.id === \"b\") {\n            await tick();\n            patch(\"b\", [\n              {\n                id: \"c\",\n                path: \"c\",\n                hasErrorBoundary: true,\n                async loader() {\n                  await tick();\n                  throw new Error(\"C ERROR\");\n                },\n              },\n            ]);\n          }\n        },\n      });\n\n      await router.navigate(\"/a/b/c\");\n      expect(router.state).toMatchObject({\n        location: { pathname: \"/a/b/c\" },\n        loaderData: {},\n        errors: {\n          c: new Error(\"C ERROR\"),\n        },\n      });\n      expect(router.state.matches.map((m) => m.route.id)).toEqual([\n        \"a\",\n        \"b\",\n        \"c\",\n      ]);\n    });\n\n    it(\"errors bubbled to lazy parent route (GET navigation)\", async () => {\n      router = createRouter({\n        history: createMemoryHistory(),\n        routes: [\n          {\n            path: \"/\",\n          },\n          {\n            id: \"a\",\n            path: \"a\",\n          },\n        ],\n        async patchRoutesOnNavigation({ matches, patch }) {\n          await tick();\n          if (last(matches).route.id === \"a\") {\n            patch(\"a\", [\n              {\n                id: \"b\",\n                path: \"b\",\n                hasErrorBoundary: true,\n              },\n            ]);\n          } else if (last(matches).route.id === \"b\") {\n            await tick();\n            patch(\"b\", [\n              {\n                id: \"c\",\n                path: \"c\",\n                async loader() {\n                  await tick();\n                  throw new Error(\"C ERROR\");\n                },\n              },\n            ]);\n          }\n        },\n      });\n\n      await router.navigate(\"/a/b/c\");\n      expect(router.state).toMatchObject({\n        location: { pathname: \"/a/b/c\" },\n        loaderData: {},\n        errors: {\n          b: new Error(\"C ERROR\"),\n        },\n      });\n      expect(router.state.matches.map((m) => m.route.id)).toEqual([\n        \"a\",\n        \"b\",\n        \"c\",\n      ]);\n    });\n\n    it(\"errors bubbled when no boundary exists (GET navigation)\", async () => {\n      router = createRouter({\n        history: createMemoryHistory(),\n        routes: [\n          {\n            path: \"/\",\n          },\n          {\n            id: \"a\",\n            path: \"a\",\n          },\n        ],\n        async patchRoutesOnNavigation({ matches, patch }) {\n          await tick();\n          if (last(matches).route.id === \"a\") {\n            patch(\"a\", [\n              {\n                id: \"b\",\n                path: \"b\",\n              },\n            ]);\n          } else if (last(matches).route.id === \"b\") {\n            await tick();\n            patch(\"b\", [\n              {\n                id: \"c\",\n                path: \"c\",\n                async loader() {\n                  await tick();\n                  throw new Error(\"C ERROR\");\n                },\n              },\n            ]);\n          }\n        },\n      });\n\n      await router.navigate(\"/a/b/c\");\n      expect(router.state).toMatchObject({\n        location: { pathname: \"/a/b/c\" },\n        loaderData: {},\n        errors: {\n          a: new Error(\"C ERROR\"),\n        },\n      });\n      expect(router.state.matches.map((m) => m.route.id)).toEqual([\n        \"a\",\n        \"b\",\n        \"c\",\n      ]);\n    });\n\n    it(\"errors thrown at lazy boundary route (POST navigation)\", async () => {\n      router = createRouter({\n        history: createMemoryHistory(),\n        routes: [\n          {\n            path: \"/\",\n          },\n          {\n            id: \"a\",\n            path: \"a\",\n          },\n        ],\n        async patchRoutesOnNavigation({ matches, patch }) {\n          await tick();\n          if (last(matches).route.id === \"a\") {\n            patch(\"a\", [\n              {\n                id: \"b\",\n                path: \"b\",\n              },\n            ]);\n          } else if (last(matches).route.id === \"b\") {\n            await tick();\n            patch(\"b\", [\n              {\n                id: \"c\",\n                path: \"c\",\n                hasErrorBoundary: true,\n                async action() {\n                  await tick();\n                  throw new Error(\"C ERROR\");\n                },\n              },\n            ]);\n          }\n        },\n      });\n\n      await router.navigate(\"/a/b/c\", {\n        formMethod: \"POST\",\n        formData: createFormData({}),\n      });\n      expect(router.state).toMatchObject({\n        location: { pathname: \"/a/b/c\" },\n        actionData: null,\n        loaderData: {},\n        errors: {\n          c: new Error(\"C ERROR\"),\n        },\n      });\n      expect(router.state.matches.map((m) => m.route.id)).toEqual([\n        \"a\",\n        \"b\",\n        \"c\",\n      ]);\n    });\n\n    it(\"errors bubbled to lazy parent route (POST navigation)\", async () => {\n      router = createRouter({\n        history: createMemoryHistory(),\n        routes: [\n          {\n            path: \"/\",\n          },\n          {\n            id: \"a\",\n            path: \"a\",\n          },\n        ],\n        async patchRoutesOnNavigation({ matches, patch }) {\n          await tick();\n          if (last(matches).route.id === \"a\") {\n            patch(\"a\", [\n              {\n                id: \"b\",\n                path: \"b\",\n                hasErrorBoundary: true,\n              },\n            ]);\n          } else if (last(matches).route.id === \"b\") {\n            await tick();\n            patch(\"b\", [\n              {\n                id: \"c\",\n                path: \"c\",\n                async action() {\n                  await tick();\n                  throw new Error(\"C ERROR\");\n                },\n              },\n            ]);\n          }\n        },\n      });\n\n      await router.navigate(\"/a/b/c\", {\n        formMethod: \"POST\",\n        formData: createFormData({}),\n      });\n      expect(router.state).toMatchObject({\n        location: { pathname: \"/a/b/c\" },\n        actionData: null,\n        loaderData: {},\n        errors: {\n          b: new Error(\"C ERROR\"),\n        },\n      });\n      expect(router.state.matches.map((m) => m.route.id)).toEqual([\n        \"a\",\n        \"b\",\n        \"c\",\n      ]);\n    });\n\n    it(\"errors bubbled when no boundary exists (POST navigation)\", async () => {\n      router = createRouter({\n        history: createMemoryHistory(),\n        routes: [\n          {\n            path: \"/\",\n          },\n          {\n            id: \"a\",\n            path: \"a\",\n          },\n        ],\n        async patchRoutesOnNavigation({ matches, patch }) {\n          await tick();\n          if (last(matches).route.id === \"a\") {\n            patch(\"a\", [\n              {\n                id: \"b\",\n                path: \"b\",\n              },\n            ]);\n          } else if (last(matches).route.id === \"b\") {\n            await tick();\n            patch(\"b\", [\n              {\n                id: \"c\",\n                path: \"c\",\n                async action() {\n                  await tick();\n                  throw new Error(\"C ERROR\");\n                },\n              },\n            ]);\n          }\n        },\n      });\n\n      await router.navigate(\"/a/b/c\", {\n        formMethod: \"POST\",\n        formData: createFormData({}),\n      });\n      expect(router.state).toMatchObject({\n        location: { pathname: \"/a/b/c\" },\n        actionData: null,\n        loaderData: {},\n        errors: {\n          a: new Error(\"C ERROR\"),\n        },\n      });\n      expect(router.state.matches.map((m) => m.route.id)).toEqual([\n        \"a\",\n        \"b\",\n        \"c\",\n      ]);\n    });\n\n    it(\"handles errors thrown from patchRoutesOnNavigation() (GET navigation)\", async () => {\n      let shouldThrow = true;\n      router = createRouter({\n        history: createMemoryHistory(),\n        routes: [\n          {\n            id: \"index\",\n            path: \"/\",\n          },\n          {\n            id: \"a\",\n            path: \"a\",\n          },\n        ],\n        async patchRoutesOnNavigation({ patch }) {\n          await tick();\n          if (shouldThrow) {\n            shouldThrow = false;\n            throw new Error(\"broke!\");\n          }\n          patch(\"a\", [\n            {\n              id: \"b\",\n              path: \"b\",\n              loader() {\n                return \"B\";\n              },\n            },\n          ]);\n        },\n      });\n\n      await router.navigate(\"/a/b\");\n      expect(router.state).toMatchObject({\n        location: { pathname: \"/a/b\" },\n        actionData: null,\n        loaderData: {},\n        errors: {\n          a: new Error(\"broke!\"),\n        },\n      });\n      expect(router.state.matches.map((m) => m.route.id)).toEqual([\"a\"]);\n\n      await router.navigate(\"/\");\n      expect(router.state).toMatchObject({\n        location: { pathname: \"/\" },\n        actionData: null,\n        loaderData: {},\n        errors: null,\n      });\n      expect(router.state.matches.map((m) => m.route.id)).toEqual([\"index\"]);\n\n      await router.navigate(\"/a/b\");\n      expect(router.state).toMatchObject({\n        location: { pathname: \"/a/b\" },\n        actionData: null,\n        loaderData: {\n          b: \"B\",\n        },\n        errors: null,\n      });\n      expect(router.state.matches.map((m) => m.route.id)).toEqual([\"a\", \"b\"]);\n    });\n\n    it(\"handles errors thrown from patchRoutesOnNavigation() (POST navigation)\", async () => {\n      let shouldThrow = true;\n      router = createRouter({\n        history: createMemoryHistory(),\n        routes: [\n          {\n            id: \"index\",\n            path: \"/\",\n          },\n          {\n            id: \"a\",\n            path: \"a\",\n          },\n        ],\n        async patchRoutesOnNavigation({ patch }) {\n          await tick();\n          if (shouldThrow) {\n            shouldThrow = false;\n            throw new Error(\"broke!\");\n          }\n          patch(\"a\", [\n            {\n              id: \"b\",\n              path: \"b\",\n              action() {\n                return \"B\";\n              },\n            },\n          ]);\n        },\n      });\n\n      await router.navigate(\"/a/b\", {\n        formMethod: \"POST\",\n        formData: createFormData({}),\n      });\n      expect(router.state).toMatchObject({\n        location: { pathname: \"/a/b\" },\n        actionData: null,\n        loaderData: {},\n        errors: {\n          a: new Error(\"broke!\"),\n        },\n      });\n      expect(router.state.matches.map((m) => m.route.id)).toEqual([\"a\"]);\n\n      await router.navigate(\"/\");\n      expect(router.state).toMatchObject({\n        location: { pathname: \"/\" },\n        actionData: null,\n        loaderData: {},\n        errors: null,\n      });\n      expect(router.state.matches.map((m) => m.route.id)).toEqual([\"index\"]);\n\n      await router.navigate(\"/a/b\", {\n        formMethod: \"POST\",\n        formData: createFormData({}),\n      });\n      expect(router.state).toMatchObject({\n        location: { pathname: \"/a/b\" },\n        actionData: {\n          b: \"B\",\n        },\n        loaderData: {},\n        errors: null,\n      });\n      expect(router.state.matches.map((m) => m.route.id)).toEqual([\"a\", \"b\"]);\n    });\n\n    it(\"handles errors thrown from patchRoutesOnNavigation() when there are no partial matches (GET navigation)\", async () => {\n      router = createRouter({\n        history: createMemoryHistory(),\n        routes: [\n          {\n            id: \"a\",\n            path: \"a\",\n          },\n        ],\n        async patchRoutesOnNavigation({ patch }) {\n          await tick();\n          throw new Error(\"broke!\");\n        },\n      });\n\n      await router.navigate(\"/b\");\n      expect(router.state).toMatchObject({\n        // A bit odd but this is a result of our best attempt to display some form\n        // of error UI to the user - follows the same logic we use on 404s\n        matches: [\n          {\n            params: {},\n            pathname: \"\",\n            pathnameBase: \"\",\n            route: {\n              children: undefined,\n              hasErrorBoundary: false,\n              id: \"a\",\n              path: \"a\",\n            },\n          },\n        ],\n        location: { pathname: \"/b\" },\n        actionData: null,\n        loaderData: {},\n        errors: {\n          a: new Error(\"broke!\"),\n        },\n      });\n    });\n\n    it(\"handles errors thrown from patchRoutesOnNavigation() when there are no partial matches (POST navigation)\", async () => {\n      router = createRouter({\n        history: createMemoryHistory(),\n        routes: [\n          {\n            id: \"a\",\n            path: \"a\",\n          },\n        ],\n        async patchRoutesOnNavigation({ patch }) {\n          await tick();\n          throw new Error(\"broke!\");\n        },\n      });\n\n      await router.navigate(\"/b\", {\n        formMethod: \"POST\",\n        formData: createFormData({}),\n      });\n      expect(router.state).toMatchObject({\n        // A bit odd but this is a result of our best attempt to display some form\n        // of error UI to the user - follows the same logic we use on 404s\n        matches: [\n          {\n            params: {},\n            pathname: \"\",\n            pathnameBase: \"\",\n            route: {\n              children: undefined,\n              hasErrorBoundary: false,\n              id: \"a\",\n              path: \"a\",\n            },\n          },\n        ],\n        location: { pathname: \"/b\" },\n        actionData: null,\n        loaderData: {},\n        errors: {\n          a: new Error(\"broke!\"),\n        },\n      });\n    });\n\n    it(\"bubbles errors thrown from patchRoutesOnNavigation() during hydration\", async () => {\n      router = createRouter({\n        history: createMemoryHistory({\n          initialEntries: [\"/parent/child/grandchild\"],\n        }),\n        routes: [\n          {\n            id: \"parent\",\n            path: \"parent\",\n            hasErrorBoundary: true,\n            children: [\n              {\n                id: \"child\",\n                path: \"child\",\n              },\n            ],\n          },\n        ],\n        async patchRoutesOnNavigation() {\n          await tick();\n          throw new Error(\"broke!\");\n        },\n      }).initialize();\n\n      expect(router.state).toMatchObject({\n        location: { pathname: \"/parent/child/grandchild\" },\n        initialized: false,\n        errors: null,\n      });\n      expect(router.state.matches.map((m) => m.route.id)).toEqual([\n        \"parent\",\n        \"child\",\n      ]);\n\n      await tick();\n      expect(router.state).toMatchObject({\n        location: { pathname: \"/parent/child/grandchild\" },\n        actionData: null,\n        loaderData: {},\n        errors: {\n          parent: new Error(\"broke!\"),\n        },\n      });\n      expect(router.state.matches.map((m) => m.route.id)).toEqual([\n        \"parent\",\n        \"child\",\n      ]);\n    });\n  });\n\n  describe(\"fetchers\", () => {\n    it(\"discovers child route at a depth of 1 (fetcher.load)\", async () => {\n      let childrenDfd = createDeferred<AgnosticDataRouteObject[]>();\n      let childLoaderDfd = createDeferred();\n\n      router = createRouter({\n        history: createMemoryHistory(),\n        routes: [\n          {\n            path: \"/\",\n          },\n          {\n            id: \"parent\",\n            path: \"parent\",\n          },\n        ],\n        async patchRoutesOnNavigation({ patch }) {\n          let children = await childrenDfd.promise;\n          patch(\"parent\", children);\n        },\n      });\n      let fetcherData = getFetcherData(router);\n\n      let key = \"key\";\n      router.fetch(key, \"0\", \"/parent/child\");\n      expect(router.getFetcher(key).state).toBe(\"loading\");\n\n      childrenDfd.resolve([\n        {\n          id: \"child\",\n          path: \"child\",\n          loader: () => childLoaderDfd.promise,\n        },\n      ]);\n      expect(router.getFetcher(key).state).toBe(\"loading\");\n\n      childLoaderDfd.resolve(\"CHILD\");\n      await tick();\n\n      expect(router.getFetcher(key).state).toBe(\"idle\");\n      expect(fetcherData.get(key)).toBe(\"CHILD\");\n    });\n\n    it(\"discovers child routes at a depth >1 (fetcher.load)\", async () => {\n      router = createRouter({\n        history: createMemoryHistory(),\n        routes: [\n          {\n            path: \"/\",\n          },\n          {\n            id: \"a\",\n            path: \"a\",\n          },\n        ],\n        async patchRoutesOnNavigation({ matches, patch }) {\n          await tick();\n          if (last(matches).route.id === \"a\") {\n            patch(\"a\", [\n              {\n                id: \"b\",\n                path: \"b\",\n              },\n            ]);\n          } else if (last(matches).route.id === \"b\") {\n            patch(\"b\", [\n              {\n                id: \"c\",\n                path: \"c\",\n                async loader() {\n                  await tick();\n                  return \"C\";\n                },\n              },\n            ]);\n          }\n        },\n      });\n      let fetcherData = getFetcherData(router);\n\n      let key = \"key\";\n      await router.fetch(key, \"0\", \"/a/b/c\");\n      // Needed for now since router.fetch is not async until v7\n      await new Promise((r) => setTimeout(r, 10));\n      expect(router.getFetcher(key).state).toBe(\"idle\");\n      expect(fetcherData.get(key)).toBe(\"C\");\n    });\n\n    it(\"discovers child route at a depth of 1 (fetcher.submit)\", async () => {\n      let childrenDfd = createDeferred<AgnosticDataRouteObject[]>();\n      let childActionDfd = createDeferred();\n\n      router = createRouter({\n        history: createMemoryHistory(),\n        routes: [\n          {\n            path: \"/\",\n          },\n          {\n            id: \"parent\",\n            path: \"parent\",\n          },\n        ],\n        async patchRoutesOnNavigation({ patch }) {\n          let children = await childrenDfd.promise;\n          patch(\"parent\", children);\n        },\n      });\n      let fetcherData = getFetcherData(router);\n\n      let key = \"key\";\n      router.fetch(key, \"0\", \"/parent/child\", {\n        formMethod: \"post\",\n        formData: createFormData({}),\n      });\n      expect(router.getFetcher(key).state).toBe(\"submitting\");\n\n      childrenDfd.resolve([\n        {\n          id: \"child\",\n          path: \"child\",\n          action: () => childActionDfd.promise,\n        },\n      ]);\n      expect(router.getFetcher(key).state).toBe(\"submitting\");\n\n      childActionDfd.resolve(\"CHILD\");\n      await tick();\n\n      expect(router.getFetcher(key).state).toBe(\"idle\");\n      expect(fetcherData.get(key)).toBe(\"CHILD\");\n    });\n\n    it(\"discovers child routes at a depth >1 (fetcher.submit)\", async () => {\n      router = createRouter({\n        history: createMemoryHistory(),\n        routes: [\n          {\n            path: \"/\",\n          },\n          {\n            id: \"a\",\n            path: \"a\",\n          },\n        ],\n        async patchRoutesOnNavigation({ matches, patch }) {\n          await tick();\n          if (last(matches).route.id === \"a\") {\n            patch(\"a\", [\n              {\n                id: \"b\",\n                path: \"b\",\n              },\n            ]);\n          } else if (last(matches).route.id === \"b\") {\n            patch(\"b\", [\n              {\n                id: \"c\",\n                path: \"c\",\n                async action() {\n                  await tick();\n                  return \"C ACTION\";\n                },\n              },\n            ]);\n          }\n        },\n      });\n      let fetcherData = getFetcherData(router);\n\n      let key = \"key\";\n      await router.fetch(key, \"0\", \"/a/b/c\", {\n        formMethod: \"POST\",\n        formData: createFormData({}),\n      });\n      // Needed for now since router.fetch is not async until v7\n      await new Promise((r) => setTimeout(r, 10));\n      expect(router.getFetcher(key).state).toBe(\"idle\");\n      expect(fetcherData.get(key)).toBe(\"C ACTION\");\n    });\n\n    it(\"does not include search params in the `path` (fetcher.load)\", async () => {\n      let capturedPath;\n\n      router = createRouter({\n        history: createMemoryHistory(),\n        routes: [\n          {\n            path: \"/\",\n          },\n          {\n            id: \"parent\",\n            path: \"parent\",\n          },\n        ],\n        async patchRoutesOnNavigation({ path, patch }) {\n          capturedPath = path;\n          patch(\"parent\", [\n            {\n              id: \"child\",\n              path: \"child\",\n              loader: () => \"CHILD\",\n            },\n          ]);\n        },\n      });\n\n      let key = \"key\";\n\n      let data;\n      router.subscribe((state) => {\n        if (state.fetchers.has(\"key\")) {\n          data = state.fetchers.get(\"key\")!.data;\n        }\n      });\n\n      router.fetch(key, \"0\", \"/parent/child?a=b\");\n      await tick();\n      expect(router.getFetcher(key).state).toBe(\"idle\");\n      expect(data).toBe(\"CHILD\");\n      expect(capturedPath).toBe(\"/parent/child\");\n    });\n\n    it(\"does not include search params in the `path` (fetcher.submit)\", async () => {\n      let capturedPath;\n\n      router = createRouter({\n        history: createMemoryHistory(),\n        routes: [\n          {\n            path: \"/\",\n          },\n          {\n            id: \"parent\",\n            path: \"parent\",\n          },\n        ],\n        async patchRoutesOnNavigation({ path, patch }) {\n          capturedPath = path;\n          patch(\"parent\", [\n            {\n              id: \"child\",\n              path: \"child\",\n              action: () => \"CHILD\",\n            },\n          ]);\n        },\n      });\n\n      let key = \"key\";\n\n      let data;\n      router.subscribe((state) => {\n        if (state.fetchers.has(\"key\")) {\n          data = state.fetchers.get(\"key\")!.data;\n        }\n      });\n\n      router.fetch(key, \"0\", \"/parent/child?a=b\", {\n        formMethod: \"post\",\n        formData: createFormData({}),\n      });\n      await tick();\n      expect(router.getFetcher(key).state).toBe(\"idle\");\n      expect(data).toBe(\"CHILD\");\n      expect(capturedPath).toBe(\"/parent/child\");\n    });\n  });\n});\n"
  },
  {
    "path": "packages/react-router/__tests__/router/lazy-test.ts",
    "content": "import { createMemoryHistory } from \"../../lib/router/history\";\nimport { createRouter, createStaticHandler } from \"../../lib/router/router\";\nimport {\n  createMemoryRouter,\n  hydrationRouteProperties,\n} from \"../../lib/components\";\n\nimport type {\n  TestNonIndexRouteObject,\n  TestRouteObject,\n} from \"./utils/data-router-setup\";\nimport {\n  cleanup,\n  createDeferred,\n  createAsyncStub,\n  setup,\n} from \"./utils/data-router-setup\";\nimport {\n  createFormData,\n  createRequest,\n  findRouteById,\n  invariant,\n  tick,\n} from \"./utils/utils\";\n\ndescribe(\"lazily loaded route modules\", () => {\n  beforeEach(() => {\n    jest.spyOn(console, \"warn\").mockImplementation(() => {});\n  });\n\n  // Detect any failures inside the router navigate code\n  afterEach(() => {\n    cleanup();\n\n    // @ts-ignore\n    console.warn.mockReset();\n  });\n\n  const createBasicLazyRoutes = (\n    lazy: TestNonIndexRouteObject[\"lazy\"],\n  ): TestRouteObject[] => {\n    return [\n      {\n        id: \"root\",\n        path: \"/\",\n        children: [\n          {\n            id: \"lazy\",\n            path: \"/lazy\",\n            lazy,\n          },\n        ],\n      },\n    ];\n  };\n\n  const createBasicLazyFunctionRoutes = (): {\n    routes: TestRouteObject[];\n    lazy: jest.Mock;\n    lazyDeferred: ReturnType<typeof createDeferred>;\n  } => {\n    let [lazy, lazyDeferred] = createAsyncStub();\n    return {\n      routes: createBasicLazyRoutes(lazy),\n      lazy,\n      lazyDeferred,\n    };\n  };\n\n  describe(\"initialization\", () => {\n    it(\"fetches lazy route functions on router initialization\", async () => {\n      let lazyDeferred = createDeferred();\n      let router = createRouter({\n        routes: [\n          {\n            path: \"/lazy\",\n            lazy: () => lazyDeferred.promise,\n          },\n        ],\n        history: createMemoryHistory({ initialEntries: [\"/lazy\"] }),\n      });\n\n      expect(router.state.initialized).toBe(false);\n\n      router.initialize();\n\n      let route = { Component: () => null };\n      await lazyDeferred.resolve(route);\n\n      expect(router.state.location.pathname).toBe(\"/lazy\");\n      expect(router.state.navigation.state).toBe(\"idle\");\n      expect(router.state.initialized).toBe(true);\n      expect(router.state.matches[0].route).toMatchObject(route);\n    });\n\n    it(\"resolves lazy route properties on router initialization\", async () => {\n      let lazyLoaderDeferred = createDeferred();\n      let lazyActionDeferred = createDeferred();\n      let router = createRouter({\n        routes: [\n          {\n            path: \"/lazy\",\n            lazy: {\n              loader: () => lazyLoaderDeferred.promise,\n              action: () => lazyActionDeferred.promise,\n            },\n          },\n        ],\n        history: createMemoryHistory({ initialEntries: [\"/lazy\"] }),\n      });\n\n      expect(router.state.initialized).toBe(false);\n\n      router.initialize();\n\n      let loader = jest.fn(() => null);\n      await lazyLoaderDeferred.resolve(loader);\n\n      // Ensure loader is called as soon as it's loaded\n      expect(loader).toHaveBeenCalledTimes(1);\n\n      // Finish loading all lazy properties\n      let action = jest.fn(() => null);\n      await lazyActionDeferred.resolve(action);\n      expect(action).toHaveBeenCalledTimes(0);\n\n      expect(router.state.location.pathname).toBe(\"/lazy\");\n      expect(router.state.navigation.state).toBe(\"idle\");\n      expect(router.state.initialized).toBe(true);\n      expect(router.state.matches[0].route).toMatchObject({\n        loader,\n        action,\n      });\n    });\n\n    it(\"ignores falsy lazy route properties on router initialization\", async () => {\n      let lazyLoaderDeferred = createDeferred();\n      let lazyActionDeferred = createDeferred();\n      let router = createRouter({\n        routes: [\n          {\n            path: \"/lazy\",\n            lazy: {\n              loader: () => lazyLoaderDeferred.promise,\n              action: () => lazyActionDeferred.promise,\n            },\n          },\n        ],\n        history: createMemoryHistory({ initialEntries: [\"/lazy\"] }),\n      });\n\n      expect(router.state.initialized).toBe(false);\n\n      router.initialize();\n\n      await lazyLoaderDeferred.resolve(null);\n      await lazyActionDeferred.resolve(undefined);\n\n      expect(router.state.location.pathname).toBe(\"/lazy\");\n      expect(router.state.navigation.state).toBe(\"idle\");\n      expect(router.state.initialized).toBe(true);\n      expect(router.state.loaderData).toEqual({});\n      expect(router.state.matches[0].route.loader).toBeUndefined();\n      expect(router.state.matches[0].route.action).toBeUndefined();\n    });\n\n    it(\"ignores and warns on unsupported lazy route function properties on router initialization\", async () => {\n      let consoleWarn = jest.spyOn(console, \"warn\");\n      let loaderDeferred = createDeferred();\n      let router = createRouter({\n        routes: [\n          {\n            path: \"/lazy\",\n            // @ts-expect-error\n            lazy: async () => {\n              return {\n                loader: () => loaderDeferred.promise,\n                lazy: async () => {\n                  throw new Error(\"SHOULD NOT BE CALLED\");\n                },\n                caseSensitive: async () => true,\n                path: async () => \"/lazy/path\",\n                id: async () => \"lazy\",\n                index: async () => true,\n                children: async () => [],\n              };\n            },\n          },\n        ],\n        history: createMemoryHistory({ initialEntries: [\"/lazy\"] }),\n      });\n\n      expect(router.state.initialized).toBe(false);\n\n      router.initialize();\n\n      let LOADER_DATA = 123;\n      await loaderDeferred.resolve(LOADER_DATA);\n\n      expect(router.state.location.pathname).toBe(\"/lazy\");\n      expect(router.state.navigation.state).toBe(\"idle\");\n      expect(router.state.initialized).toBe(true);\n      expect(router.state.loaderData).toEqual({\n        \"0\": LOADER_DATA,\n      });\n\n      expect(consoleWarn.mock.calls.map((call) => call[0]).sort())\n        .toMatchInlineSnapshot(`\n        [\n          \"Route property caseSensitive is not a supported property to be returned from a lazy route function. This property will be ignored.\",\n          \"Route property children is not a supported property to be returned from a lazy route function. This property will be ignored.\",\n          \"Route property id is not a supported property to be returned from a lazy route function. This property will be ignored.\",\n          \"Route property index is not a supported property to be returned from a lazy route function. This property will be ignored.\",\n          \"Route property lazy is not a supported property to be returned from a lazy route function. This property will be ignored.\",\n          \"Route property path is not a supported property to be returned from a lazy route function. This property will be ignored.\",\n        ]\n      `);\n      consoleWarn.mockReset();\n    });\n\n    it(\"ignores and warns on unsupported lazy route properties on router initialization\", async () => {\n      let consoleWarn = jest.spyOn(console, \"warn\");\n      let loaderDeferred = createDeferred();\n      let router = createRouter({\n        routes: [\n          {\n            path: \"/lazy\",\n            lazy: {\n              loader: () => loaderDeferred.promise,\n              // @ts-expect-error\n              lazy: async () => {\n                throw new Error(\"SHOULD NOT BE CALLED\");\n              },\n              caseSensitive: async () => true,\n              path: async () => \"/lazy/path\",\n              id: async () => \"lazy\",\n              index: async () => true,\n              children: async () => [],\n            },\n          },\n        ],\n        history: createMemoryHistory({ initialEntries: [\"/lazy\"] }),\n      });\n\n      expect(router.state.initialized).toBe(false);\n\n      router.initialize();\n\n      let LOADER_DATA = 123;\n      let loader = () => LOADER_DATA;\n      await loaderDeferred.resolve(loader);\n\n      expect(router.state.location.pathname).toBe(\"/lazy\");\n      expect(router.state.navigation.state).toBe(\"idle\");\n      expect(router.state.initialized).toBe(true);\n      expect(router.state.loaderData).toEqual({\n        \"0\": LOADER_DATA,\n      });\n      expect(router.state.matches[0].route.loader).toBe(loader);\n\n      expect(consoleWarn.mock.calls.map((call) => call[0]).sort())\n        .toMatchInlineSnapshot(`\n        [\n          \"Route property caseSensitive is not a supported lazy route property. This property will be ignored.\",\n          \"Route property children is not a supported lazy route property. This property will be ignored.\",\n          \"Route property id is not a supported lazy route property. This property will be ignored.\",\n          \"Route property index is not a supported lazy route property. This property will be ignored.\",\n          \"Route property lazy is not a supported lazy route property. This property will be ignored.\",\n          \"Route property path is not a supported lazy route property. This property will be ignored.\",\n        ]\n      `);\n      consoleWarn.mockReset();\n    });\n\n    it(\"fetches lazy route functions and executes loaders on router initialization\", async () => {\n      let lazyDeferred = createDeferred();\n      let router = createRouter({\n        routes: [\n          {\n            path: \"/lazy\",\n            lazy: () => lazyDeferred.promise,\n          },\n        ],\n        history: createMemoryHistory({ initialEntries: [\"/lazy\"] }),\n      });\n\n      expect(router.state.initialized).toBe(false);\n\n      router.initialize();\n\n      let loaderDeferred = createDeferred();\n      let route = {\n        Component: () => null,\n        loader: () => loaderDeferred.promise,\n      };\n      await lazyDeferred.resolve(route);\n      expect(router.state.initialized).toBe(false);\n\n      await loaderDeferred.resolve(\"LOADER\");\n      expect(router.state.location.pathname).toBe(\"/lazy\");\n      expect(router.state.navigation.state).toBe(\"idle\");\n      expect(router.state.initialized).toBe(true);\n      expect(router.state.loaderData).toEqual({\n        \"0\": \"LOADER\",\n      });\n      expect(router.state.matches[0].route).toMatchObject(route);\n    });\n\n    it(\"resolves lazy route properties and executes loaders on router initialization\", async () => {\n      let lazyLoaderDeferred = createDeferred();\n      let lazyActionDeferred = createDeferred();\n      let router = createRouter({\n        routes: [\n          {\n            path: \"/lazy\",\n            lazy: {\n              loader: () => lazyLoaderDeferred.promise,\n              action: () => lazyActionDeferred.promise,\n            },\n          },\n        ],\n        history: createMemoryHistory({ initialEntries: [\"/lazy\"] }),\n      });\n\n      expect(router.state.initialized).toBe(false);\n\n      router.initialize();\n\n      // Ensure loader is called as soon as it's loaded\n      let [loader, loaderDeferred] = createAsyncStub();\n      await lazyLoaderDeferred.resolve(loader);\n      expect(loader).toHaveBeenCalledTimes(1);\n      expect(router.state.initialized).toBe(false);\n\n      // Finish loading all lazy properties\n      let action = jest.fn(() => null);\n      await lazyActionDeferred.resolve(action);\n      expect(action).toHaveBeenCalledTimes(0);\n\n      await loaderDeferred.resolve(\"LOADER\");\n      expect(router.state.location.pathname).toBe(\"/lazy\");\n      expect(router.state.navigation.state).toBe(\"idle\");\n      expect(router.state.initialized).toBe(true);\n      expect(router.state.loaderData).toEqual({\n        \"0\": \"LOADER\",\n      });\n      expect(router.state.matches[0].route).toMatchObject({ loader });\n    });\n  });\n\n  describe(\"happy path\", () => {\n    it(\"fetches lazy route functions on loading navigation\", async () => {\n      let { routes, lazy, lazyDeferred } = createBasicLazyFunctionRoutes();\n      let t = setup({ routes });\n      expect(lazy).not.toHaveBeenCalled();\n\n      await t.navigate(\"/lazy\");\n      expect(t.router.state.location.pathname).toBe(\"/\");\n      expect(t.router.state.navigation.state).toBe(\"loading\");\n      expect(lazy).toHaveBeenCalledTimes(1);\n\n      let loaderDeferred = createDeferred();\n      lazyDeferred.resolve({\n        loader: () => loaderDeferred.promise,\n      });\n      expect(t.router.state.location.pathname).toBe(\"/\");\n      expect(t.router.state.navigation.state).toBe(\"loading\");\n      expect(lazy).toHaveBeenCalledTimes(1);\n\n      await loaderDeferred.resolve(\"LAZY LOADER\");\n\n      expect(t.router.state.location.pathname).toBe(\"/lazy\");\n      expect(t.router.state.navigation.state).toBe(\"idle\");\n      expect(t.router.state.loaderData).toEqual({\n        lazy: \"LAZY LOADER\",\n      });\n      expect(lazy).toHaveBeenCalledTimes(1);\n    });\n\n    it(\"resolves lazy route properties on loading navigation\", async () => {\n      let [lazyLoader, lazyLoaderDeferred] = createAsyncStub();\n      let [lazyAction, lazyActionDeferred] = createAsyncStub();\n      let routes = createBasicLazyRoutes({\n        loader: lazyLoader,\n        action: lazyAction,\n      });\n      let t = setup({ routes });\n      expect(lazyLoader).not.toHaveBeenCalled();\n\n      await t.navigate(\"/lazy\");\n      expect(t.router.state.location.pathname).toBe(\"/\");\n      expect(t.router.state.navigation.state).toBe(\"loading\");\n      expect(lazyLoader).toHaveBeenCalledTimes(1);\n\n      // Ensure loader is called as soon as it's loaded\n      let [loader, loaderDeferred] = createAsyncStub();\n      await lazyLoaderDeferred.resolve(loader);\n      expect(loader).toHaveBeenCalledTimes(1);\n      expect(t.router.state.location.pathname).toBe(\"/\");\n      expect(t.router.state.navigation.state).toBe(\"loading\");\n\n      // Ensure we're still loading if other lazy properties are not loaded yet\n      await loaderDeferred.resolve(\"LAZY LOADER\");\n      expect(t.router.state.location.pathname).toBe(\"/\");\n      expect(t.router.state.navigation.state).toBe(\"loading\");\n\n      // Finish loading all lazy properties\n      let action = jest.fn(() => null);\n      await lazyActionDeferred.resolve(action);\n      expect(action).toHaveBeenCalledTimes(0);\n\n      expect(t.router.state.location.pathname).toBe(\"/lazy\");\n      expect(t.router.state.navigation.state).toBe(\"idle\");\n      expect(t.router.state.loaderData).toEqual({\n        lazy: \"LAZY LOADER\",\n      });\n      expect(lazyLoader).toHaveBeenCalledTimes(1);\n      expect(lazyAction).toHaveBeenCalledTimes(1);\n    });\n\n    it(\"ignores falsy lazy route properties on loading navigation\", async () => {\n      let [lazyLoader, lazyLoaderDeferred] = createAsyncStub();\n      let routes = createBasicLazyRoutes({ loader: lazyLoader });\n      let t = setup({ routes });\n      expect(lazyLoader).not.toHaveBeenCalled();\n\n      await t.navigate(\"/lazy\");\n      expect(t.router.state.location.pathname).toBe(\"/\");\n      expect(t.router.state.navigation.state).toBe(\"loading\");\n      expect(lazyLoader).toHaveBeenCalledTimes(1);\n\n      await lazyLoaderDeferred.resolve(null);\n      expect(t.router.state.matches[0].route.loader).toBeUndefined();\n      expect(t.router.state.location.pathname).toBe(\"/lazy\");\n      expect(t.router.state.navigation.state).toBe(\"idle\");\n      expect(t.router.state.loaderData).toEqual({});\n      expect(lazyLoader).toHaveBeenCalledTimes(1);\n    });\n\n    it(\"fetches lazy route functions on submission navigation\", async () => {\n      let { routes, lazy, lazyDeferred } = createBasicLazyFunctionRoutes();\n      let t = setup({ routes });\n      expect(lazy).not.toHaveBeenCalled();\n\n      await t.navigate(\"/lazy\", {\n        formMethod: \"post\",\n        formData: createFormData({}),\n      });\n      expect(t.router.state.location.pathname).toBe(\"/\");\n      expect(t.router.state.navigation.state).toBe(\"submitting\");\n      expect(lazy).toHaveBeenCalledTimes(1);\n\n      let actionDeferred = createDeferred();\n      let loaderDeferred = createDeferred();\n      lazyDeferred.resolve({\n        action: () => actionDeferred.promise,\n        loader: () => loaderDeferred.promise,\n      });\n      expect(t.router.state.location.pathname).toBe(\"/\");\n      expect(t.router.state.navigation.state).toBe(\"submitting\");\n\n      await actionDeferred.resolve(\"LAZY ACTION\");\n      expect(t.router.state.location.pathname).toBe(\"/\");\n      expect(t.router.state.navigation.state).toBe(\"loading\");\n      expect(t.router.state.actionData).toEqual({\n        lazy: \"LAZY ACTION\",\n      });\n      expect(t.router.state.loaderData).toEqual({});\n\n      await loaderDeferred.resolve(\"LAZY LOADER\");\n      expect(t.router.state.location.pathname).toBe(\"/lazy\");\n      expect(t.router.state.navigation.state).toBe(\"idle\");\n      expect(t.router.state.actionData).toEqual({\n        lazy: \"LAZY ACTION\",\n      });\n      expect(t.router.state.loaderData).toEqual({\n        lazy: \"LAZY LOADER\",\n      });\n\n      expect(lazy).toHaveBeenCalledTimes(1);\n    });\n\n    it(\"resolves lazy route properties on submission navigation\", async () => {\n      let [lazyLoader, lazyLoaderDeferred] = createAsyncStub();\n      let [lazyAction, lazyActionDeferred] = createAsyncStub();\n      let routes = createBasicLazyRoutes({\n        loader: lazyLoader,\n        action: lazyAction,\n      });\n      let t = setup({ routes });\n      expect(lazyLoader).not.toHaveBeenCalled();\n      expect(lazyAction).not.toHaveBeenCalled();\n\n      await t.navigate(\"/lazy\", {\n        formMethod: \"post\",\n        formData: createFormData({}),\n      });\n      expect(t.router.state.location.pathname).toBe(\"/\");\n      expect(t.router.state.navigation.state).toBe(\"submitting\");\n      expect(lazyLoader).toHaveBeenCalledTimes(1);\n      expect(lazyAction).toHaveBeenCalledTimes(1);\n\n      let [action, actionDeferred] = createAsyncStub();\n      let [loader, loaderDeferred] = createAsyncStub();\n\n      // Ensure action is called as soon as it's loaded\n      await lazyActionDeferred.resolve(action);\n      await actionDeferred.resolve(\"LAZY ACTION\");\n      expect(action).toHaveBeenCalledTimes(1);\n      expect(loader).toHaveBeenCalledTimes(0);\n      expect(t.router.state.location.pathname).toBe(\"/\");\n      expect(t.router.state.navigation.state).toBe(\"submitting\");\n      expect(t.router.state.actionData).toEqual(null);\n      expect(t.router.state.loaderData).toEqual({});\n\n      // Finish loading all lazy properties\n      await lazyLoaderDeferred.resolve(loader);\n      expect(loader).toHaveBeenCalledTimes(1);\n      expect(action).toHaveBeenCalledTimes(1);\n      expect(t.router.state.location.pathname).toBe(\"/\");\n      expect(t.router.state.navigation.state).toBe(\"loading\");\n      await loaderDeferred.resolve(\"LAZY LOADER\");\n      expect(t.router.state.location.pathname).toBe(\"/lazy\");\n      expect(t.router.state.navigation.state).toBe(\"idle\");\n      expect(t.router.state.actionData).toEqual({\n        lazy: \"LAZY ACTION\",\n      });\n      expect(t.router.state.loaderData).toEqual({\n        lazy: \"LAZY LOADER\",\n      });\n\n      expect(lazyLoader).toHaveBeenCalledTimes(1);\n      expect(lazyAction).toHaveBeenCalledTimes(1);\n    });\n\n    it(\"ignores falsy lazy route properties on submission navigation\", async () => {\n      let [lazyLoader, lazyLoaderDeferred] = createAsyncStub();\n      let [lazyAction, lazyActionDeferred] = createAsyncStub();\n      let routes = createBasicLazyRoutes({\n        loader: lazyLoader,\n        action: lazyAction,\n      });\n      let t = setup({ routes });\n      expect(lazyLoader).not.toHaveBeenCalled();\n      expect(lazyAction).not.toHaveBeenCalled();\n\n      await t.navigate(\"/lazy\", {\n        formMethod: \"post\",\n        formData: createFormData({}),\n      });\n      expect(t.router.state.location.pathname).toBe(\"/\");\n      expect(t.router.state.navigation.state).toBe(\"submitting\");\n      expect(lazyLoader).toHaveBeenCalledTimes(1);\n      expect(lazyAction).toHaveBeenCalledTimes(1);\n\n      await lazyLoaderDeferred.resolve(undefined);\n      await lazyActionDeferred.resolve(null);\n      expect(t.router.state.location.pathname).toBe(\"/lazy\");\n      expect(t.router.state.navigation.state).toBe(\"idle\");\n      expect(t.router.state.actionData).toEqual(null);\n      expect(t.router.state.loaderData).toEqual({});\n      expect(lazyLoader).toHaveBeenCalledTimes(1);\n      expect(lazyAction).toHaveBeenCalledTimes(1);\n      expect(t.router.state.matches[0].route.loader).toBeUndefined();\n      expect(t.router.state.matches[0].route.action).toBeUndefined();\n    });\n\n    it(\"only resolves lazy hydration route properties on hydration\", async () => {\n      let [lazyLoaderForHydration, lazyLoaderDeferredForHydration] =\n        createAsyncStub();\n      let [lazyLoaderForNavigation, lazyLoaderDeferredForNavigation] =\n        createAsyncStub();\n      let [\n        lazyHydrateFallbackForHydration,\n        lazyHydrateFallbackDeferredForHydration,\n      ] = createAsyncStub();\n      let [\n        lazyHydrateFallbackElementForHydration,\n        lazyHydrateFallbackElementDeferredForHydration,\n      ] = createAsyncStub();\n      let lazyHydrateFallbackForNavigation = jest.fn(async () => null);\n      let lazyHydrateFallbackElementForNavigation = jest.fn(async () => null);\n      let router = createMemoryRouter(\n        [\n          {\n            path: \"/hydration\",\n            lazy: {\n              HydrateFallback: lazyHydrateFallbackForHydration,\n              hydrateFallbackElement: lazyHydrateFallbackElementForHydration,\n              loader: lazyLoaderForHydration,\n            },\n          },\n          {\n            path: \"/navigation\",\n            lazy: {\n              HydrateFallback: lazyHydrateFallbackForNavigation,\n              hydrateFallbackElement: lazyHydrateFallbackElementForNavigation,\n              loader: lazyLoaderForNavigation,\n            },\n          },\n        ],\n        {\n          initialEntries: [\"/hydration\"],\n        },\n      );\n      expect(router.state.initialized).toBe(false);\n\n      expect(lazyHydrateFallbackForHydration).toHaveBeenCalledTimes(1);\n      expect(lazyHydrateFallbackElementForHydration).toHaveBeenCalledTimes(1);\n      expect(lazyLoaderForHydration).toHaveBeenCalledTimes(1);\n      await lazyHydrateFallbackDeferredForHydration.resolve(null);\n      await lazyHydrateFallbackElementDeferredForHydration.resolve(null);\n      await lazyLoaderDeferredForHydration.resolve(null);\n\n      expect(router.state.location.pathname).toBe(\"/hydration\");\n      expect(router.state.navigation.state).toBe(\"idle\");\n      expect(router.state.initialized).toBe(true);\n\n      let navigationPromise = router.navigate(\"/navigation\");\n      expect(router.state.location.pathname).toBe(\"/hydration\");\n      expect(router.state.navigation.state).toBe(\"loading\");\n      expect(lazyHydrateFallbackForNavigation).not.toHaveBeenCalled();\n      expect(lazyHydrateFallbackElementForNavigation).not.toHaveBeenCalled();\n      expect(lazyLoaderForNavigation).toHaveBeenCalledTimes(1);\n      await lazyLoaderDeferredForNavigation.resolve(null);\n      await navigationPromise;\n      expect(router.state.location.pathname).toBe(\"/navigation\");\n      expect(router.state.navigation.state).toBe(\"idle\");\n    });\n\n    it(\"fetches lazy route functions on fetcher.load\", async () => {\n      let { routes, lazy, lazyDeferred } = createBasicLazyFunctionRoutes();\n      let t = setup({ routes });\n      expect(lazy).not.toHaveBeenCalled();\n\n      let key = \"key\";\n      await t.fetch(\"/lazy\", key);\n      expect(t.router.state.fetchers.get(key)?.state).toBe(\"loading\");\n      expect(lazy).toHaveBeenCalledTimes(1);\n\n      let loaderDeferred = createDeferred();\n      lazyDeferred.resolve({\n        loader: () => loaderDeferred.promise,\n      });\n      expect(t.router.state.fetchers.get(key)?.state).toBe(\"loading\");\n\n      await loaderDeferred.resolve(\"LAZY LOADER\");\n      expect(t.fetchers[key].state).toBe(\"idle\");\n      expect(t.fetchers[key].data).toBe(\"LAZY LOADER\");\n\n      expect(lazy).toHaveBeenCalledTimes(1);\n    });\n\n    it(\"resolves lazy route properties on fetcher.load\", async () => {\n      let [lazyLoader, lazyLoaderDeferred] = createAsyncStub();\n      let routes = createBasicLazyRoutes({ loader: lazyLoader });\n      let t = setup({ routes });\n      expect(lazyLoader).not.toHaveBeenCalled();\n\n      let key = \"key\";\n      await t.fetch(\"/lazy\", key);\n      expect(t.router.state.fetchers.get(key)?.state).toBe(\"loading\");\n      expect(lazyLoader).toHaveBeenCalledTimes(1);\n\n      let loaderDeferred = createDeferred();\n      lazyLoaderDeferred.resolve(() => loaderDeferred.promise);\n      expect(t.router.state.fetchers.get(key)?.state).toBe(\"loading\");\n\n      await loaderDeferred.resolve(\"LAZY LOADER\");\n      expect(t.fetchers[key].state).toBe(\"idle\");\n      expect(t.fetchers[key].data).toBe(\"LAZY LOADER\");\n\n      expect(lazyLoader).toHaveBeenCalledTimes(1);\n    });\n\n    it(\"skips lazy hydration route properties on fetcher.load\", async () => {\n      let [lazyLoader, lazyLoaderDeferred] = createAsyncStub();\n      let lazyHydrateFallback = jest.fn(async () => null);\n      let lazyHydrateFallbackElement = jest.fn(async () => null);\n      let routes = createBasicLazyRoutes({\n        loader: lazyLoader,\n        // @ts-expect-error\n        HydrateFallback: lazyHydrateFallback,\n        hydrateFallbackElement: lazyHydrateFallbackElement,\n      });\n      let t = setup({ routes, hydrationRouteProperties });\n      expect(lazyHydrateFallback).not.toHaveBeenCalled();\n      expect(lazyHydrateFallbackElement).not.toHaveBeenCalled();\n\n      let key = \"key\";\n      await t.fetch(\"/lazy\", key);\n      expect(t.router.state.fetchers.get(key)?.state).toBe(\"loading\");\n      expect(lazyLoader).toHaveBeenCalledTimes(1);\n      expect(lazyHydrateFallback).not.toHaveBeenCalled();\n      expect(lazyHydrateFallbackElement).not.toHaveBeenCalled();\n\n      let loaderDeferred = createDeferred();\n      lazyLoaderDeferred.resolve(() => loaderDeferred.promise);\n      expect(t.router.state.fetchers.get(key)?.state).toBe(\"loading\");\n\n      await loaderDeferred.resolve(\"LAZY LOADER\");\n      expect(t.fetchers[key].state).toBe(\"idle\");\n      expect(t.fetchers[key].data).toBe(\"LAZY LOADER\");\n\n      expect(lazyLoader).toHaveBeenCalledTimes(1);\n      expect(lazyHydrateFallback).not.toHaveBeenCalled();\n      expect(lazyHydrateFallbackElement).not.toHaveBeenCalled();\n    });\n\n    it(\"fetches lazy route functions on fetcher.submit\", async () => {\n      let { routes, lazy, lazyDeferred } = createBasicLazyFunctionRoutes();\n      let t = setup({ routes });\n      expect(lazy).not.toHaveBeenCalled();\n\n      let key = \"key\";\n      await t.fetch(\"/lazy\", key, {\n        formMethod: \"post\",\n        formData: createFormData({}),\n      });\n      expect(t.router.state.fetchers.get(key)?.state).toBe(\"submitting\");\n      expect(lazy).toHaveBeenCalledTimes(1);\n\n      let actionDeferred = createDeferred();\n      lazyDeferred.resolve({\n        action: () => actionDeferred.promise,\n      });\n      expect(t.router.state.fetchers.get(key)?.state).toBe(\"submitting\");\n\n      await actionDeferred.resolve(\"LAZY ACTION\");\n      expect(t.fetchers[key]?.state).toBe(\"idle\");\n      expect(t.fetchers[key]?.data).toBe(\"LAZY ACTION\");\n\n      expect(lazy).toHaveBeenCalledTimes(1);\n    });\n\n    it(\"resolves lazy route properties on fetcher.submit\", async () => {\n      let [lazyLoader, lazyLoaderDeferred] = createAsyncStub();\n      let [lazyAction, lazyActionDeferred] = createAsyncStub();\n      let routes = createBasicLazyRoutes({\n        loader: lazyLoader,\n        action: lazyAction,\n      });\n      let t = setup({ routes });\n      expect(lazyLoader).not.toHaveBeenCalled();\n      expect(lazyAction).not.toHaveBeenCalled();\n\n      let key = \"key\";\n      await t.fetch(\"/lazy\", key, {\n        formMethod: \"post\",\n        formData: createFormData({}),\n      });\n      expect(t.router.state.fetchers.get(key)?.state).toBe(\"submitting\");\n      expect(lazyLoader).toHaveBeenCalledTimes(1);\n      expect(lazyAction).toHaveBeenCalledTimes(1);\n\n      let actionDeferred = createDeferred();\n      let loaderDeferred = createDeferred();\n      lazyLoaderDeferred.resolve(() => loaderDeferred.promise);\n      lazyActionDeferred.resolve(() => actionDeferred.promise);\n      expect(t.router.state.fetchers.get(key)?.state).toBe(\"submitting\");\n\n      await actionDeferred.resolve(\"LAZY ACTION\");\n      expect(t.fetchers[key]?.state).toBe(\"idle\");\n      expect(t.fetchers[key]?.data).toBe(\"LAZY ACTION\");\n\n      expect(lazyLoader).toHaveBeenCalledTimes(1);\n      expect(lazyAction).toHaveBeenCalledTimes(1);\n    });\n\n    it(\"skips lazy hydration route properties on fetcher.submit\", async () => {\n      let [lazyLoaderStub, lazyLoaderDeferred] = createAsyncStub();\n      let [lazyActionStub, lazyActionDeferred] = createAsyncStub();\n      let lazyHydrateFallback = jest.fn(async () => null);\n      let lazyHydrateFallbackElement = jest.fn(async () => null);\n      let routes = createBasicLazyRoutes({\n        loader: lazyLoaderStub,\n        action: lazyActionStub,\n        // @ts-expect-error\n        HydrateFallback: lazyHydrateFallback,\n        hydrateFallbackElement: lazyHydrateFallbackElement,\n      });\n      let t = setup({ routes, hydrationRouteProperties });\n      expect(lazyLoaderStub).not.toHaveBeenCalled();\n      expect(lazyActionStub).not.toHaveBeenCalled();\n\n      let key = \"key\";\n      await t.fetch(\"/lazy\", key, {\n        formMethod: \"post\",\n        formData: createFormData({}),\n      });\n      expect(t.router.state.fetchers.get(key)?.state).toBe(\"submitting\");\n      expect(lazyLoaderStub).toHaveBeenCalledTimes(1);\n      expect(lazyActionStub).toHaveBeenCalledTimes(1);\n      expect(lazyHydrateFallback).not.toHaveBeenCalled();\n      expect(lazyHydrateFallbackElement).not.toHaveBeenCalled();\n\n      let actionDeferred = createDeferred();\n      let loaderDeferred = createDeferred();\n      lazyLoaderDeferred.resolve(() => loaderDeferred.promise);\n      lazyActionDeferred.resolve(() => actionDeferred.promise);\n      expect(t.router.state.fetchers.get(key)?.state).toBe(\"submitting\");\n\n      await actionDeferred.resolve(\"LAZY ACTION\");\n      expect(t.fetchers[key]?.state).toBe(\"idle\");\n      expect(t.fetchers[key]?.data).toBe(\"LAZY ACTION\");\n\n      expect(lazyLoaderStub).toHaveBeenCalledTimes(1);\n      expect(lazyActionStub).toHaveBeenCalledTimes(1);\n      expect(lazyHydrateFallback).not.toHaveBeenCalled();\n      expect(lazyHydrateFallbackElement).not.toHaveBeenCalled();\n    });\n\n    it(\"fetches lazy route functions on staticHandler.query()\", async () => {\n      let { query } = createStaticHandler([\n        {\n          id: \"lazy\",\n          path: \"/lazy\",\n          lazy: async () => {\n            await tick();\n            return {\n              async loader() {\n                return Response.json({ value: \"LAZY LOADER\" });\n              },\n            };\n          },\n        },\n      ]);\n\n      let context = await query(createRequest(\"/lazy\"));\n      invariant(\n        !(context instanceof Response),\n        \"Expected a StaticContext instance\",\n      );\n      expect(context.loaderData).toEqual({ lazy: { value: \"LAZY LOADER\" } });\n    });\n\n    it(\"resolves lazy route properties on staticHandler.query()\", async () => {\n      let { query } = createStaticHandler([\n        {\n          id: \"lazy\",\n          path: \"/lazy\",\n          lazy: {\n            loader: async () => {\n              await tick();\n              return () => Response.json({ value: \"LAZY LOADER\" });\n            },\n          },\n        },\n      ]);\n\n      let context = await query(createRequest(\"/lazy\"));\n      invariant(\n        !(context instanceof Response),\n        \"Expected a StaticContext instance\",\n      );\n      expect(context.loaderData).toEqual({ lazy: { value: \"LAZY LOADER\" } });\n    });\n\n    it(\"fetches lazy route functions on staticHandler.queryRoute()\", async () => {\n      let { queryRoute } = createStaticHandler([\n        {\n          id: \"lazy\",\n          path: \"/lazy\",\n          lazy: async () => {\n            await tick();\n            return {\n              async loader() {\n                return Response.json({ value: \"LAZY LOADER\" });\n              },\n            };\n          },\n        },\n      ]);\n\n      let response = await queryRoute(createRequest(\"/lazy\"));\n      let data = await response.json();\n      expect(data).toEqual({ value: \"LAZY LOADER\" });\n    });\n\n    it(\"resolves lazy route properties on staticHandler.queryRoute()\", async () => {\n      let { queryRoute } = createStaticHandler([\n        {\n          id: \"lazy\",\n          path: \"/lazy\",\n          lazy: {\n            loader: async () => {\n              await tick();\n              return () => Response.json({ value: \"LAZY LOADER\" });\n            },\n          },\n        },\n      ]);\n\n      let response = await queryRoute(createRequest(\"/lazy\"));\n      let data = await response.json();\n      expect(data).toEqual({ value: \"LAZY LOADER\" });\n    });\n\n    it(\"resolves lazy hydration route properties on staticHandler.queryRoute()\", async () => {\n      let lazyHydrateFallback = jest.fn(async () => null);\n      let lazyHydrateFallbackElement = jest.fn(async () => null);\n      let { queryRoute } = createStaticHandler(\n        [\n          {\n            id: \"lazy\",\n            path: \"/lazy\",\n            lazy: {\n              loader: async () => {\n                await tick();\n                return () => Response.json({ value: \"LAZY LOADER\" });\n              },\n              // @ts-expect-error\n              HydrateFallback: lazyHydrateFallback,\n              hydrateFallbackElement: lazyHydrateFallbackElement,\n            },\n          },\n        ],\n        { hydrationRouteProperties },\n      );\n\n      let response = await queryRoute(createRequest(\"/lazy\"));\n      let data = await response.json();\n      expect(data).toEqual({ value: \"LAZY LOADER\" });\n      expect(lazyHydrateFallback).toHaveBeenCalled();\n      expect(lazyHydrateFallbackElement).toHaveBeenCalled();\n    });\n  });\n\n  describe(\"statically defined fields\", () => {\n    it(\"prefers statically defined loader over lazily defined loader via lazy function\", async () => {\n      let consoleWarn = jest.spyOn(console, \"warn\");\n      let [lazy, lazyDeferred] = createAsyncStub();\n      let t = setup({\n        routes: [\n          {\n            id: \"lazy\",\n            path: \"/lazy\",\n            loader: true,\n            lazy,\n          },\n        ],\n      });\n\n      let A = await t.navigate(\"/lazy\");\n      expect(t.router.state.location.pathname).toBe(\"/\");\n      expect(t.router.state.navigation.state).toBe(\"loading\");\n      // Execute in parallel\n      expect(A.loaders.lazy.stub).toHaveBeenCalled();\n      expect(lazy).toHaveBeenCalledTimes(1);\n\n      let loader = jest.fn(() => \"LAZY LOADER\");\n      lazyDeferred.resolve({ loader });\n      expect(t.router.state.location.pathname).toBe(\"/\");\n      expect(t.router.state.navigation.state).toBe(\"loading\");\n\n      await A.loaders.lazy.resolve(\"STATIC LOADER\");\n      expect(t.router.state.location.pathname).toBe(\"/lazy\");\n      expect(t.router.state.navigation.state).toBe(\"idle\");\n      expect(t.router.state.loaderData).toEqual({\n        lazy: \"STATIC LOADER\",\n      });\n\n      let route = findRouteById(t.router.routes, \"lazy\");\n      expect(route.lazy).toBeUndefined();\n      expect(route.loader).toEqual(expect.any(Function));\n      expect(route.loader).not.toBe(loader);\n      expect(loader).not.toHaveBeenCalled();\n      expect(lazy).toHaveBeenCalledTimes(1);\n\n      expect(consoleWarn).toHaveBeenCalledTimes(1);\n      expect(consoleWarn.mock.calls[0][0]).toMatchInlineSnapshot(\n        `\"Route \"lazy\" has a static property \"loader\" defined but its lazy function is also returning a value for this property. The lazy route property \"loader\" will be ignored.\"`,\n      );\n      consoleWarn.mockReset();\n    });\n\n    it(\"prefers statically defined loader over lazily defined loader via lazy property\", async () => {\n      let consoleWarn = jest.spyOn(console, \"warn\");\n      let [lazyLoader, lazyLoaderDeferred] = createAsyncStub();\n      let t = setup({\n        routes: [\n          {\n            id: \"lazy\",\n            path: \"/lazy\",\n            loader: true,\n            lazy: {\n              loader: lazyLoader,\n            },\n          },\n        ],\n      });\n\n      let A = await t.navigate(\"/lazy\");\n      expect(t.router.state.location.pathname).toBe(\"/\");\n      expect(t.router.state.navigation.state).toBe(\"loading\");\n      // Execute in parallel\n      expect(A.loaders.lazy.stub).toHaveBeenCalled();\n      expect(lazyLoader).toHaveBeenCalledTimes(0);\n\n      let loader = jest.fn(() => \"LAZY LOADER\");\n      lazyLoaderDeferred.resolve(loader);\n      expect(t.router.state.location.pathname).toBe(\"/\");\n      expect(t.router.state.navigation.state).toBe(\"loading\");\n\n      await A.loaders.lazy.resolve(\"STATIC LOADER\");\n      expect(t.router.state.location.pathname).toBe(\"/lazy\");\n      expect(t.router.state.navigation.state).toBe(\"idle\");\n      expect(t.router.state.loaderData).toEqual({\n        lazy: \"STATIC LOADER\",\n      });\n\n      let route = findRouteById(t.router.routes, \"lazy\");\n      expect(route.lazy).toBeUndefined();\n      expect(route.loader).toEqual(expect.any(Function));\n      expect(route.loader).not.toBe(loader);\n      expect(loader).not.toHaveBeenCalled();\n      expect(lazyLoader).toHaveBeenCalledTimes(0);\n\n      expect(consoleWarn).toHaveBeenCalledTimes(1);\n      expect(consoleWarn.mock.calls[0][0]).toMatchInlineSnapshot(\n        `\"Route \"lazy\" has a static property \"loader\" defined. The lazy property will be ignored.\"`,\n      );\n      consoleWarn.mockReset();\n    });\n\n    it(\"prefers statically defined loader over lazily defined falsy loader via lazy property\", async () => {\n      let consoleWarn = jest.spyOn(console, \"warn\");\n      let [lazyLoader, lazyLoaderDeferred] = createAsyncStub();\n      let t = setup({\n        routes: [\n          {\n            id: \"lazy\",\n            path: \"/lazy\",\n            loader: true,\n            lazy: {\n              loader: lazyLoader,\n            },\n          },\n        ],\n      });\n\n      let A = await t.navigate(\"/lazy\");\n      expect(t.router.state.location.pathname).toBe(\"/\");\n      expect(t.router.state.navigation.state).toBe(\"loading\");\n      // Execute in parallel\n      expect(A.loaders.lazy.stub).toHaveBeenCalled();\n      expect(lazyLoader).toHaveBeenCalledTimes(0);\n\n      lazyLoaderDeferred.resolve(null);\n      expect(t.router.state.location.pathname).toBe(\"/\");\n      expect(t.router.state.navigation.state).toBe(\"loading\");\n\n      await A.loaders.lazy.resolve(\"STATIC LOADER\");\n      expect(t.router.state.location.pathname).toBe(\"/lazy\");\n      expect(t.router.state.navigation.state).toBe(\"idle\");\n      expect(t.router.state.loaderData).toEqual({\n        lazy: \"STATIC LOADER\",\n      });\n\n      let route = findRouteById(t.router.routes, \"lazy\");\n      expect(route.lazy).toBeUndefined();\n      expect(route.loader).toEqual(expect.any(Function));\n      expect(route.loader).toBeInstanceOf(Function);\n      expect(lazyLoader).toHaveBeenCalledTimes(0);\n\n      expect(consoleWarn).toHaveBeenCalledTimes(1);\n      expect(consoleWarn.mock.calls[0][0]).toMatchInlineSnapshot(\n        `\"Route \"lazy\" has a static property \"loader\" defined. The lazy property will be ignored.\"`,\n      );\n      consoleWarn.mockReset();\n    });\n\n    it(\"prefers statically defined action over lazily loaded action via lazy function\", async () => {\n      let consoleWarn = jest.spyOn(console, \"warn\");\n      let [lazy, lazyDeferred] = createAsyncStub();\n      let t = setup({\n        routes: [\n          {\n            id: \"lazy\",\n            path: \"/lazy\",\n            action: true,\n            lazy,\n          },\n        ],\n      });\n\n      let A = await t.navigate(\"/lazy\", {\n        formMethod: \"post\",\n        formData: createFormData({}),\n      });\n      expect(t.router.state.location.pathname).toBe(\"/\");\n      expect(t.router.state.navigation.state).toBe(\"submitting\");\n      // Execute in parallel\n      expect(A.actions.lazy.stub).toHaveBeenCalled();\n      expect(lazy).toHaveBeenCalledTimes(1);\n\n      let lazyAction = jest.fn(() => \"LAZY ACTION\");\n      let loaderDeferred = createDeferred();\n      await lazyDeferred.resolve({\n        action: lazyAction,\n        loader: () => loaderDeferred.promise,\n      });\n      expect(t.router.state.location.pathname).toBe(\"/\");\n      expect(t.router.state.navigation.state).toBe(\"submitting\");\n\n      await A.actions.lazy.resolve(\"STATIC ACTION\");\n      expect(t.router.state.location.pathname).toBe(\"/\");\n      expect(t.router.state.navigation.state).toBe(\"loading\");\n      expect(t.router.state.actionData).toEqual({\n        lazy: \"STATIC ACTION\",\n      });\n      expect(t.router.state.loaderData).toEqual({});\n\n      await loaderDeferred.resolve(\"LAZY LOADER\");\n      expect(t.router.state.location.pathname).toBe(\"/lazy\");\n      expect(t.router.state.navigation.state).toBe(\"idle\");\n      expect(t.router.state.actionData).toEqual({\n        lazy: \"STATIC ACTION\",\n      });\n      expect(t.router.state.loaderData).toEqual({\n        lazy: \"LAZY LOADER\",\n      });\n\n      let route = findRouteById(t.router.routes, \"lazy\");\n      expect(route.lazy).toBeUndefined();\n      expect(route.action).toEqual(expect.any(Function));\n      expect(route.action).not.toBe(lazyAction);\n      expect(lazyAction).not.toHaveBeenCalled();\n      expect(lazy).toHaveBeenCalledTimes(1);\n\n      expect(consoleWarn).toHaveBeenCalledTimes(1);\n      expect(consoleWarn.mock.calls[0][0]).toMatchInlineSnapshot(\n        `\"Route \"lazy\" has a static property \"action\" defined but its lazy function is also returning a value for this property. The lazy route property \"action\" will be ignored.\"`,\n      );\n      consoleWarn.mockReset();\n    });\n\n    it(\"prefers statically defined action over lazily loaded action via lazy property\", async () => {\n      let consoleWarn = jest.spyOn(console, \"warn\");\n      let [lazyLoader, lazyLoaderDeferred] = createAsyncStub();\n      let [lazyAction, lazyActionDeferred] = createAsyncStub();\n      let t = setup({\n        routes: [\n          {\n            id: \"lazy\",\n            path: \"/lazy\",\n            action: true,\n            lazy: {\n              action: lazyAction,\n              loader: lazyLoader,\n            },\n          },\n        ],\n      });\n\n      let A = await t.navigate(\"/lazy\", {\n        formMethod: \"post\",\n        formData: createFormData({}),\n      });\n      expect(t.router.state.location.pathname).toBe(\"/\");\n      expect(t.router.state.navigation.state).toBe(\"submitting\");\n      // Execute in parallel\n      expect(A.actions.lazy.stub).toHaveBeenCalled();\n      expect(lazyAction).toHaveBeenCalledTimes(0);\n      expect(lazyLoader).toHaveBeenCalledTimes(1);\n\n      let action = jest.fn(() => \"LAZY ACTION\");\n      let loaderDeferred = createDeferred();\n      lazyActionDeferred.resolve(action);\n      lazyLoaderDeferred.resolve(() => loaderDeferred.promise);\n      expect(t.router.state.location.pathname).toBe(\"/\");\n      expect(t.router.state.navigation.state).toBe(\"submitting\");\n\n      await A.actions.lazy.resolve(\"STATIC ACTION\");\n      expect(t.router.state.location.pathname).toBe(\"/\");\n      expect(t.router.state.navigation.state).toBe(\"loading\");\n      expect(t.router.state.actionData).toEqual({\n        lazy: \"STATIC ACTION\",\n      });\n      expect(t.router.state.loaderData).toEqual({});\n\n      await loaderDeferred.resolve(\"LAZY LOADER\");\n      expect(t.router.state.location.pathname).toBe(\"/lazy\");\n      expect(t.router.state.navigation.state).toBe(\"idle\");\n      expect(t.router.state.actionData).toEqual({\n        lazy: \"STATIC ACTION\",\n      });\n      expect(t.router.state.loaderData).toEqual({\n        lazy: \"LAZY LOADER\",\n      });\n\n      let route = findRouteById(t.router.routes, \"lazy\");\n      expect(route.lazy).toBeUndefined();\n      expect(route.action).toEqual(expect.any(Function));\n      expect(route.action).not.toBe(action);\n      expect(action).not.toHaveBeenCalled();\n      expect(lazyAction).toHaveBeenCalledTimes(0);\n      expect(lazyLoader).toHaveBeenCalledTimes(1);\n\n      expect(consoleWarn).toHaveBeenCalledTimes(1);\n      expect(consoleWarn.mock.calls[0][0]).toMatchInlineSnapshot(\n        `\"Route \"lazy\" has a static property \"action\" defined. The lazy property will be ignored.\"`,\n      );\n      consoleWarn.mockReset();\n    });\n\n    it(\"prefers statically defined action/loader over lazily defined action/loader via lazy function\", async () => {\n      let consoleWarn = jest.spyOn(console, \"warn\");\n      let [lazy, lazyDeferred] = createAsyncStub();\n      let t = setup({\n        routes: [\n          {\n            id: \"lazy\",\n            path: \"/lazy\",\n            action: true,\n            loader: true,\n            lazy,\n          },\n        ],\n      });\n\n      let A = await t.navigate(\"/lazy\", {\n        formMethod: \"post\",\n        formData: createFormData({}),\n      });\n      expect(t.router.state.location.pathname).toBe(\"/\");\n      expect(t.router.state.navigation.state).toBe(\"submitting\");\n      expect(lazy).toHaveBeenCalledTimes(1);\n\n      let action = jest.fn(() => \"LAZY ACTION\");\n      let loader = jest.fn(() => \"LAZY LOADER\");\n      await lazyDeferred.resolve({ action, loader });\n      expect(t.router.state.location.pathname).toBe(\"/\");\n      expect(t.router.state.navigation.state).toBe(\"submitting\");\n\n      await A.actions.lazy.resolve(\"STATIC ACTION\");\n      expect(t.router.state.location.pathname).toBe(\"/\");\n      expect(t.router.state.navigation.state).toBe(\"loading\");\n      expect(t.router.state.actionData).toEqual({\n        lazy: \"STATIC ACTION\",\n      });\n      expect(t.router.state.loaderData).toEqual({});\n\n      await A.loaders.lazy.resolve(\"STATIC LOADER\");\n      expect(t.router.state.location.pathname).toBe(\"/lazy\");\n      expect(t.router.state.navigation.state).toBe(\"idle\");\n      expect(t.router.state.actionData).toEqual({\n        lazy: \"STATIC ACTION\",\n      });\n      expect(t.router.state.loaderData).toEqual({\n        lazy: \"STATIC LOADER\",\n      });\n\n      let route = findRouteById(t.router.routes, \"lazy\");\n      expect(route.lazy).toBeUndefined();\n      expect(route.action).toEqual(expect.any(Function));\n      expect(route.loader).toEqual(expect.any(Function));\n      expect(route.action).not.toBe(action);\n      expect(route.loader).not.toBe(loader);\n      expect(action).not.toHaveBeenCalled();\n      expect(loader).not.toHaveBeenCalled();\n      expect(lazy).toHaveBeenCalledTimes(1);\n\n      expect(consoleWarn).toHaveBeenCalledTimes(2);\n      expect(consoleWarn.mock.calls[0][0]).toMatchInlineSnapshot(\n        `\"Route \"lazy\" has a static property \"action\" defined but its lazy function is also returning a value for this property. The lazy route property \"action\" will be ignored.\"`,\n      );\n      expect(consoleWarn.mock.calls[1][0]).toMatchInlineSnapshot(\n        `\"Route \"lazy\" has a static property \"loader\" defined but its lazy function is also returning a value for this property. The lazy route property \"loader\" will be ignored.\"`,\n      );\n      consoleWarn.mockReset();\n    });\n\n    it(\"prefers statically defined action/loader over lazily defined action/loader via lazy property\", async () => {\n      let consoleWarn = jest.spyOn(console, \"warn\");\n      let [lazyAction, lazyActionDeferred] = createAsyncStub();\n      let [lazyLoader, lazyLoaderDeferred] = createAsyncStub();\n      let t = setup({\n        routes: [\n          {\n            id: \"lazy\",\n            path: \"/lazy\",\n            action: true,\n            loader: true,\n            lazy: {\n              action: lazyAction,\n              loader: lazyLoader,\n            },\n          },\n        ],\n      });\n\n      let A = await t.navigate(\"/lazy\", {\n        formMethod: \"post\",\n        formData: createFormData({}),\n      });\n      expect(t.router.state.location.pathname).toBe(\"/\");\n      expect(t.router.state.navigation.state).toBe(\"submitting\");\n      expect(lazyLoader).toHaveBeenCalledTimes(0);\n\n      expect(lazyAction).toHaveBeenCalledTimes(0);\n      let action = jest.fn(() => \"LAZY ACTION\");\n      let loader = jest.fn(() => \"LAZY LOADER\");\n      lazyActionDeferred.resolve(action);\n      lazyLoaderDeferred.resolve(loader);\n      expect(t.router.state.location.pathname).toBe(\"/\");\n      expect(t.router.state.navigation.state).toBe(\"submitting\");\n\n      await A.actions.lazy.resolve(\"STATIC ACTION\");\n      expect(t.router.state.location.pathname).toBe(\"/\");\n      expect(t.router.state.navigation.state).toBe(\"loading\");\n      expect(t.router.state.actionData).toEqual({\n        lazy: \"STATIC ACTION\",\n      });\n      expect(t.router.state.loaderData).toEqual({});\n\n      await A.loaders.lazy.resolve(\"STATIC LOADER\");\n      expect(t.router.state.location.pathname).toBe(\"/lazy\");\n      expect(t.router.state.navigation.state).toBe(\"idle\");\n      expect(t.router.state.actionData).toEqual({\n        lazy: \"STATIC ACTION\",\n      });\n      expect(t.router.state.loaderData).toEqual({\n        lazy: \"STATIC LOADER\",\n      });\n\n      let route = findRouteById(t.router.routes, \"lazy\");\n      expect(route.lazy).toBeUndefined();\n      expect(route.action).toEqual(expect.any(Function));\n      expect(route.loader).toEqual(expect.any(Function));\n      expect(route.action).not.toBe(action);\n      expect(route.loader).not.toBe(loader);\n      expect(action).not.toHaveBeenCalled();\n      expect(loader).not.toHaveBeenCalled();\n      expect(lazyAction).toHaveBeenCalledTimes(0);\n      expect(lazyLoader).toHaveBeenCalledTimes(0);\n\n      expect(consoleWarn).toHaveBeenCalledTimes(2);\n      expect(consoleWarn.mock.calls[0][0]).toMatchInlineSnapshot(\n        `\"Route \"lazy\" has a static property \"action\" defined. The lazy property will be ignored.\"`,\n      );\n      expect(consoleWarn.mock.calls[1][0]).toMatchInlineSnapshot(\n        `\"Route \"lazy\" has a static property \"loader\" defined. The lazy property will be ignored.\"`,\n      );\n      consoleWarn.mockReset();\n    });\n\n    it(\"prefers statically defined loader over lazily defined loader via lazy function (staticHandler.query)\", async () => {\n      let consoleWarn = jest.spyOn(console, \"warn\");\n      let loader = jest.fn(async () => {\n        await tick();\n        return Response.json({ value: \"LAZY LOADER\" });\n      });\n      let lazy = jest.fn(async () => {\n        await tick();\n        return {\n          loader,\n        };\n      });\n\n      let { query } = createStaticHandler([\n        {\n          id: \"lazy\",\n          path: \"/lazy\",\n          loader: async () => {\n            await tick();\n            return Response.json({ value: \"STATIC LOADER\" });\n          },\n          lazy,\n        },\n      ]);\n\n      let context = await query(createRequest(\"/lazy\"));\n      invariant(\n        !(context instanceof Response),\n        \"Expected a StaticContext instance\",\n      );\n      expect(context.loaderData).toEqual({\n        lazy: { value: \"STATIC LOADER\" },\n      });\n      expect(lazy).toHaveBeenCalledTimes(1);\n      expect(loader).not.toHaveBeenCalled();\n\n      expect(consoleWarn).toHaveBeenCalledTimes(1);\n      expect(consoleWarn.mock.calls[0][0]).toMatchInlineSnapshot(\n        `\"Route \"lazy\" has a static property \"loader\" defined but its lazy function is also returning a value for this property. The lazy route property \"loader\" will be ignored.\"`,\n      );\n      consoleWarn.mockReset();\n    });\n\n    it(\"prefers statically defined loader over lazily defined loader via lazy property (staticHandler.query)\", async () => {\n      let consoleWarn = jest.spyOn(console, \"warn\");\n      let loader = jest.fn(async () => {\n        await tick();\n        return Response.json({ value: \"LAZY LOADER\" });\n      });\n      let lazyLoader = jest.fn(async () => {\n        await tick();\n        return loader;\n      });\n\n      let { query } = createStaticHandler([\n        {\n          id: \"lazy\",\n          path: \"/lazy\",\n          loader: async () => {\n            await tick();\n            return Response.json({ value: \"STATIC LOADER\" });\n          },\n          lazy: {\n            loader: lazyLoader,\n          },\n        },\n      ]);\n\n      let context = await query(createRequest(\"/lazy\"));\n      invariant(\n        !(context instanceof Response),\n        \"Expected a StaticContext instance\",\n      );\n      expect(context.loaderData).toEqual({\n        lazy: { value: \"STATIC LOADER\" },\n      });\n      expect(lazyLoader).not.toHaveBeenCalled();\n      expect(loader).not.toHaveBeenCalled();\n\n      expect(consoleWarn).toHaveBeenCalledTimes(1);\n      expect(consoleWarn.mock.calls[0][0]).toMatchInlineSnapshot(\n        `\"Route \"lazy\" has a static property \"loader\" defined. The lazy property will be ignored.\"`,\n      );\n      consoleWarn.mockReset();\n    });\n\n    it(\"prefers statically defined loader over lazily defined loader via lazy function (staticHandler.queryRoute)\", async () => {\n      let consoleWarn = jest.spyOn(console, \"warn\");\n      let loader = jest.fn(async () => {\n        await tick();\n        return Response.json({ value: \"LAZY LOADER\" });\n      });\n\n      let { query } = createStaticHandler([\n        {\n          id: \"lazy\",\n          path: \"/lazy\",\n          loader: async () => {\n            await tick();\n            return Response.json({ value: \"STATIC LOADER\" });\n          },\n          lazy: async () => {\n            await tick();\n            return {\n              loader,\n            };\n          },\n        },\n      ]);\n\n      let context = await query(createRequest(\"/lazy\"));\n      invariant(\n        !(context instanceof Response),\n        \"Expected a StaticContext instance\",\n      );\n      expect(context.loaderData).toEqual({\n        lazy: { value: \"STATIC LOADER\" },\n      });\n      expect(loader).not.toHaveBeenCalled();\n\n      expect(consoleWarn).toHaveBeenCalledTimes(1);\n      expect(consoleWarn.mock.calls[0][0]).toMatchInlineSnapshot(\n        `\"Route \"lazy\" has a static property \"loader\" defined but its lazy function is also returning a value for this property. The lazy route property \"loader\" will be ignored.\"`,\n      );\n      consoleWarn.mockReset();\n    });\n\n    it(\"prefers statically defined loader over lazily defined loader via lazy property (staticHandler.queryRoute)\", async () => {\n      let consoleWarn = jest.spyOn(console, \"warn\");\n      let loader = jest.fn(async () => {\n        await tick();\n        return Response.json({ value: \"LAZY LOADER\" });\n      });\n      let lazyLoader = jest.fn(async () => {\n        await tick();\n        return loader;\n      });\n\n      let { query } = createStaticHandler([\n        {\n          id: \"lazy\",\n          path: \"/lazy\",\n          loader: async () => {\n            await tick();\n            return Response.json({ value: \"STATIC LOADER\" });\n          },\n          lazy: {\n            loader: lazyLoader,\n          },\n        },\n      ]);\n\n      let context = await query(createRequest(\"/lazy\"));\n      invariant(\n        !(context instanceof Response),\n        \"Expected a StaticContext instance\",\n      );\n      expect(context.loaderData).toEqual({\n        lazy: { value: \"STATIC LOADER\" },\n      });\n      expect(loader).not.toHaveBeenCalled();\n      expect(lazyLoader).not.toHaveBeenCalled();\n\n      expect(consoleWarn).toHaveBeenCalledTimes(1);\n      expect(consoleWarn.mock.calls[0][0]).toMatchInlineSnapshot(\n        `\"Route \"lazy\" has a static property \"loader\" defined. The lazy property will be ignored.\"`,\n      );\n      consoleWarn.mockReset();\n    });\n\n    it(\"handles errors thrown from static loaders before lazy function has completed\", async () => {\n      let consoleWarn = jest.spyOn(console, \"warn\");\n      let [lazy, lazyDeferred] = createAsyncStub();\n      let t = setup({\n        routes: [\n          {\n            id: \"root\",\n            path: \"/\",\n            children: [\n              {\n                id: \"lazy\",\n                path: \"lazy\",\n                loader: true,\n                lazy,\n              },\n            ],\n          },\n        ],\n      });\n      expect(lazy).not.toHaveBeenCalled();\n\n      let A = await t.navigate(\"/lazy\");\n\n      await A.loaders.lazy.reject(\"STATIC LOADER ERROR\");\n      expect(t.router.state.navigation.state).toBe(\"loading\");\n\n      // We shouldn't bubble the loader error until after this resolves\n      // so we know if it has a boundary or not\n      await lazyDeferred.resolve({\n        hasErrorBoundary: true,\n      });\n      expect(t.router.state.location.pathname).toBe(\"/lazy\");\n      expect(t.router.state.navigation.state).toBe(\"idle\");\n      expect(t.router.state.loaderData).toEqual({});\n      expect(t.router.state.errors).toEqual({\n        lazy: \"STATIC LOADER ERROR\",\n      });\n      expect(lazy).toHaveBeenCalledTimes(1);\n      consoleWarn.mockReset();\n    });\n\n    it(\"handles errors thrown from static loaders before lazy property has resolved\", async () => {\n      let consoleWarn = jest.spyOn(console, \"warn\");\n      let [lazyHasErrorBoundary, lazyHasErrorBoundaryDeferred] =\n        createAsyncStub();\n      let t = setup({\n        routes: [\n          {\n            id: \"root\",\n            path: \"/\",\n            children: [\n              {\n                id: \"lazy\",\n                path: \"lazy\",\n                loader: true,\n                lazy: {\n                  hasErrorBoundary: lazyHasErrorBoundary,\n                },\n              },\n            ],\n          },\n        ],\n      });\n      expect(lazyHasErrorBoundary).not.toHaveBeenCalled();\n\n      let A = await t.navigate(\"/lazy\");\n\n      await A.loaders.lazy.reject(\"STATIC LOADER ERROR\");\n      expect(t.router.state.navigation.state).toBe(\"loading\");\n\n      // We shouldn't bubble the loader error until after this resolves\n      // so we know if it has a boundary or not\n      await lazyHasErrorBoundaryDeferred.resolve(true);\n      expect(t.router.state.location.pathname).toBe(\"/lazy\");\n      expect(t.router.state.navigation.state).toBe(\"idle\");\n      expect(t.router.state.loaderData).toEqual({});\n      expect(t.router.state.errors).toEqual({\n        lazy: \"STATIC LOADER ERROR\",\n      });\n      expect(lazyHasErrorBoundary).toHaveBeenCalledTimes(1);\n      consoleWarn.mockReset();\n    });\n  });\n\n  it(\"bubbles errors thrown from static loaders before lazy property has resolved if lazy 'hasErrorBoundary' is falsy\", async () => {\n    let consoleWarn = jest.spyOn(console, \"warn\");\n    let [lazyHasErrorBoundary, lazyHasErrorBoundaryDeferred] =\n      createAsyncStub();\n    let t = setup({\n      routes: [\n        {\n          id: \"root\",\n          path: \"/\",\n          children: [\n            {\n              id: \"lazy\",\n              path: \"lazy\",\n              loader: true,\n              lazy: {\n                hasErrorBoundary: lazyHasErrorBoundary,\n              },\n            },\n          ],\n        },\n      ],\n    });\n    expect(lazyHasErrorBoundary).not.toHaveBeenCalled();\n\n    let A = await t.navigate(\"/lazy\");\n\n    await A.loaders.lazy.reject(\"STATIC LOADER ERROR\");\n    expect(t.router.state.navigation.state).toBe(\"loading\");\n\n    // We shouldn't bubble the loader error until after this resolves\n    // so we know if it has a boundary or not\n    await lazyHasErrorBoundaryDeferred.resolve(null);\n    expect(t.router.state.location.pathname).toBe(\"/lazy\");\n    expect(t.router.state.navigation.state).toBe(\"idle\");\n    expect(t.router.state.loaderData).toEqual({});\n    expect(t.router.state.errors).toEqual({\n      root: \"STATIC LOADER ERROR\",\n    });\n    expect(lazyHasErrorBoundary).toHaveBeenCalledTimes(1);\n    consoleWarn.mockReset();\n  });\n\n  describe(\"interruptions\", () => {\n    it(\"runs lazily loaded route loader even if lazy function is interrupted\", async () => {\n      let { routes, lazy, lazyDeferred } = createBasicLazyFunctionRoutes();\n      let t = setup({ routes });\n      expect(lazy).not.toHaveBeenCalled();\n\n      await t.navigate(\"/lazy\");\n      expect(t.router.state.location.pathname).toBe(\"/\");\n      expect(t.router.state.navigation.state).toBe(\"loading\");\n\n      await t.navigate(\"/\");\n      expect(t.router.state.location.pathname).toBe(\"/\");\n      expect(t.router.state.navigation.state).toBe(\"idle\");\n\n      let loader = jest.fn(() => \"LAZY LOADER\");\n      await lazyDeferred.resolve({ loader });\n      expect(t.router.state.location.pathname).toBe(\"/\");\n      expect(t.router.state.navigation.state).toBe(\"idle\");\n      expect(loader).toHaveBeenCalledTimes(1);\n\n      // Ensure the lazy route object update still happened\n      let route = findRouteById(t.router.routes, \"lazy\");\n      expect(route.lazy).toBeUndefined();\n      expect(route.loader).toBe(loader);\n\n      expect(lazy).toHaveBeenCalledTimes(1);\n    });\n\n    it(\"runs lazily loaded route loader even if lazy property is interrupted\", async () => {\n      let [lazyLoader, lazyLoaderDeferred] = createAsyncStub();\n      let routes = createBasicLazyRoutes({ loader: lazyLoader });\n      let t = setup({ routes });\n      expect(lazyLoader).not.toHaveBeenCalled();\n\n      await t.navigate(\"/lazy\");\n      expect(t.router.state.location.pathname).toBe(\"/\");\n      expect(t.router.state.navigation.state).toBe(\"loading\");\n\n      await t.navigate(\"/\");\n      expect(t.router.state.location.pathname).toBe(\"/\");\n      expect(t.router.state.navigation.state).toBe(\"idle\");\n\n      let loader = jest.fn(() => \"LAZY LOADER\");\n      await lazyLoaderDeferred.resolve(loader);\n      expect(t.router.state.location.pathname).toBe(\"/\");\n      expect(t.router.state.navigation.state).toBe(\"idle\");\n      expect(loader).toHaveBeenCalledTimes(1);\n\n      // Ensure the lazy route object update still happened\n      let route = findRouteById(t.router.routes, \"lazy\");\n      expect(route.lazy).toBeUndefined();\n      expect(route.loader).toBe(loader);\n\n      expect(lazyLoader).toHaveBeenCalledTimes(1);\n    });\n\n    it(\"runs lazily loaded route action even if lazy function is interrupted\", async () => {\n      let { routes, lazy, lazyDeferred } = createBasicLazyFunctionRoutes();\n      let t = setup({ routes });\n      expect(lazy).not.toHaveBeenCalled();\n\n      await t.navigate(\"/lazy\", {\n        formMethod: \"post\",\n        formData: createFormData({}),\n      });\n      expect(t.router.state.location.pathname).toBe(\"/\");\n      expect(t.router.state.navigation.state).toBe(\"submitting\");\n\n      await t.navigate(\"/\");\n      expect(t.router.state.location.pathname).toBe(\"/\");\n      expect(t.router.state.navigation.state).toBe(\"idle\");\n\n      let action = jest.fn(() => \"LAZY ACTION\");\n      let loader = jest.fn(() => \"LAZY LOADER\");\n      await lazyDeferred.resolve({ action, loader });\n\n      let route = findRouteById(t.router.routes, \"lazy\");\n      expect(action).toHaveBeenCalledTimes(1);\n      expect(loader).not.toHaveBeenCalled();\n      expect(route.lazy).toBeUndefined();\n      expect(route.action).toBe(action);\n      expect(route.loader).toBe(loader);\n      expect(lazy).toHaveBeenCalledTimes(1);\n    });\n\n    it(\"runs lazily loaded route action even if lazy property is interrupted\", async () => {\n      let [lazyAction, lazyActionDeferred] = createAsyncStub();\n      let [lazyLoader, lazyLoaderDeferred] = createAsyncStub();\n      let routes = createBasicLazyRoutes({\n        action: lazyAction,\n        loader: lazyLoader,\n      });\n      let t = setup({ routes });\n      expect(lazyAction).not.toHaveBeenCalled();\n      expect(lazyLoader).not.toHaveBeenCalled();\n\n      await t.navigate(\"/lazy\", {\n        formMethod: \"post\",\n        formData: createFormData({}),\n      });\n      expect(t.router.state.location.pathname).toBe(\"/\");\n      expect(t.router.state.navigation.state).toBe(\"submitting\");\n\n      await t.navigate(\"/\");\n      expect(t.router.state.location.pathname).toBe(\"/\");\n      expect(t.router.state.navigation.state).toBe(\"idle\");\n\n      let action = jest.fn(() => \"LAZY ACTION\");\n      let loader = jest.fn(() => \"LAZY LOADER\");\n      await lazyActionDeferred.resolve(action);\n      await lazyLoaderDeferred.resolve(loader);\n\n      let route = findRouteById(t.router.routes, \"lazy\");\n      expect(action).toHaveBeenCalledTimes(1);\n      expect(loader).not.toHaveBeenCalled();\n      expect(route.lazy).toBeUndefined();\n      expect(route.action).toBe(action);\n      expect(route.loader).toBe(loader);\n      expect(lazyAction).toHaveBeenCalledTimes(1);\n      expect(lazyLoader).toHaveBeenCalledTimes(1);\n    });\n\n    it(\"runs lazily loaded route loader on fetcher.load() even if lazy function is interrupted\", async () => {\n      let { routes, lazy, lazyDeferred } = createBasicLazyFunctionRoutes();\n      let t = setup({ routes });\n      expect(lazy).not.toHaveBeenCalled();\n\n      let key = \"key\";\n      await t.fetch(\"/lazy\", key);\n      expect(t.router.state.fetchers.get(key)?.state).toBe(\"loading\");\n      expect(lazy).toHaveBeenCalledTimes(1);\n\n      await t.fetch(\"/lazy\", key);\n      expect(t.router.state.fetchers.get(key)?.state).toBe(\"loading\");\n      expect(lazy).toHaveBeenCalledTimes(1);\n\n      let [loader, loaderDeferred] = createAsyncStub();\n      await lazyDeferred.resolve({ loader });\n      expect(t.router.state.fetchers.get(key)?.state).toBe(\"loading\");\n\n      await loaderDeferred.resolve(\"LAZY LOADER\");\n\n      expect(t.fetchers[key].state).toBe(\"idle\");\n      expect(t.fetchers[key].data).toBe(\"LAZY LOADER\");\n      expect(loader).toHaveBeenCalledTimes(2);\n      expect(lazy).toHaveBeenCalledTimes(1);\n    });\n\n    it(\"runs lazily loaded route loader on fetcher.load() even if lazy property is interrupted\", async () => {\n      let [lazyLoader, lazyLoaderDeferred] = createAsyncStub();\n      let routes = createBasicLazyRoutes({ loader: lazyLoader });\n      let t = setup({ routes });\n      expect(lazyLoader).not.toHaveBeenCalled();\n\n      let key = \"key\";\n      await t.fetch(\"/lazy\", key);\n      expect(t.router.state.fetchers.get(key)?.state).toBe(\"loading\");\n      expect(lazyLoader).toHaveBeenCalledTimes(1);\n\n      await t.fetch(\"/lazy\", key);\n      expect(t.router.state.fetchers.get(key)?.state).toBe(\"loading\");\n      expect(lazyLoader).toHaveBeenCalledTimes(1);\n\n      let [loader, loaderDeferred] = createAsyncStub();\n      await lazyLoaderDeferred.resolve(loader);\n      expect(t.router.state.fetchers.get(key)?.state).toBe(\"loading\");\n\n      await loaderDeferred.resolve(\"LAZY LOADER\");\n\n      expect(t.fetchers[key].state).toBe(\"idle\");\n      expect(t.fetchers[key].data).toBe(\"LAZY LOADER\");\n      expect(loader).toHaveBeenCalledTimes(2);\n      expect(lazyLoader).toHaveBeenCalledTimes(1);\n    });\n\n    it(\"runs lazily loaded route action on fetcher.submit() even if lazy function is interrupted\", async () => {\n      let { routes, lazy, lazyDeferred } = createBasicLazyFunctionRoutes();\n      let t = setup({ routes });\n      expect(lazy).not.toHaveBeenCalled();\n\n      let key = \"key\";\n      await t.fetch(\"/lazy\", key, {\n        formMethod: \"post\",\n        formData: createFormData({}),\n      });\n      expect(t.router.state.fetchers.get(key)?.state).toBe(\"submitting\");\n      expect(lazy).toHaveBeenCalledTimes(1);\n\n      await t.fetch(\"/lazy\", key, {\n        formMethod: \"post\",\n        formData: createFormData({}),\n      });\n      expect(t.router.state.fetchers.get(key)?.state).toBe(\"submitting\");\n      expect(lazy).toHaveBeenCalledTimes(1);\n\n      let [action, actionDeferred] = createAsyncStub();\n      await lazyDeferred.resolve({ action });\n      expect(t.router.state.fetchers.get(key)?.state).toBe(\"submitting\");\n\n      await actionDeferred.resolve(\"LAZY ACTION\");\n\n      expect(t.fetchers[key].state).toBe(\"idle\");\n      expect(t.fetchers[key].data).toBe(\"LAZY ACTION\");\n      expect(action).toHaveBeenCalledTimes(2);\n      expect(lazy).toHaveBeenCalledTimes(1);\n    });\n\n    it(\"runs lazily loaded route action on fetcher.submit() even if lazy property is interrupted\", async () => {\n      let [lazyAction, lazyActionDeferred] = createAsyncStub();\n      let routes = createBasicLazyRoutes({ action: lazyAction });\n      let t = setup({ routes });\n      expect(lazyAction).not.toHaveBeenCalled();\n\n      let key = \"key\";\n      await t.fetch(\"/lazy\", key, {\n        formMethod: \"post\",\n        formData: createFormData({}),\n      });\n      expect(t.router.state.fetchers.get(key)?.state).toBe(\"submitting\");\n      expect(lazyAction).toHaveBeenCalledTimes(1);\n\n      await t.fetch(\"/lazy\", key, {\n        formMethod: \"post\",\n        formData: createFormData({}),\n      });\n      expect(t.router.state.fetchers.get(key)?.state).toBe(\"submitting\");\n      expect(lazyAction).toHaveBeenCalledTimes(1);\n\n      let [action, actionDeferred] = createAsyncStub();\n      await lazyActionDeferred.resolve(action);\n      expect(t.router.state.fetchers.get(key)?.state).toBe(\"submitting\");\n\n      await actionDeferred.resolve(\"LAZY ACTION\");\n\n      expect(t.fetchers[key].state).toBe(\"idle\");\n      expect(t.fetchers[key].data).toBe(\"LAZY ACTION\");\n      expect(action).toHaveBeenCalledTimes(2);\n      expect(lazyAction).toHaveBeenCalledTimes(1);\n    });\n\n    it(\"uses the first-called lazy function execution on repeated loading navigations\", async () => {\n      let { routes, lazy, lazyDeferred } = createBasicLazyFunctionRoutes();\n      let t = setup({ routes });\n      expect(lazy).not.toHaveBeenCalled();\n\n      await t.navigate(\"/lazy\");\n      expect(t.router.state.location.pathname).toBe(\"/\");\n      expect(t.router.state.navigation.state).toBe(\"loading\");\n      expect(lazy).toHaveBeenCalledTimes(1);\n\n      await t.navigate(\"/lazy\");\n      expect(t.router.state.location.pathname).toBe(\"/\");\n      expect(t.router.state.navigation.state).toBe(\"loading\");\n      expect(lazy).toHaveBeenCalledTimes(1);\n\n      let [loader, loaderDeferred] = createAsyncStub();\n      await lazyDeferred.resolve({ loader });\n\n      await loaderDeferred.resolve(\"LAZY LOADER\");\n\n      expect(t.router.state.location.pathname).toBe(\"/lazy\");\n      expect(t.router.state.navigation.state).toBe(\"idle\");\n\n      expect(t.router.state.loaderData).toEqual({ lazy: \"LAZY LOADER\" });\n\n      expect(loader).toHaveBeenCalledTimes(2);\n      expect(lazy).toHaveBeenCalledTimes(1);\n    });\n\n    it(\"uses the first-called lazy property execution on repeated loading navigations\", async () => {\n      let [lazyLoader, lazyLoaderDeferred] = createAsyncStub();\n      let routes = createBasicLazyRoutes({ loader: lazyLoader });\n      let t = setup({ routes });\n      expect(lazyLoader).not.toHaveBeenCalled();\n\n      await t.navigate(\"/lazy\");\n      expect(t.router.state.location.pathname).toBe(\"/\");\n      expect(t.router.state.navigation.state).toBe(\"loading\");\n      expect(lazyLoader).toHaveBeenCalledTimes(1);\n\n      await t.navigate(\"/lazy\");\n      expect(t.router.state.location.pathname).toBe(\"/\");\n      expect(t.router.state.navigation.state).toBe(\"loading\");\n      expect(lazyLoader).toHaveBeenCalledTimes(1);\n\n      let [loader, loaderDeferred] = createAsyncStub();\n      await lazyLoaderDeferred.resolve(loader);\n\n      await loaderDeferred.resolve(\"LAZY LOADER\");\n\n      expect(t.router.state.location.pathname).toBe(\"/lazy\");\n      expect(t.router.state.navigation.state).toBe(\"idle\");\n\n      expect(t.router.state.loaderData).toEqual({ lazy: \"LAZY LOADER\" });\n\n      expect(loader).toHaveBeenCalledTimes(2);\n      expect(lazyLoader).toHaveBeenCalledTimes(1);\n    });\n\n    it(\"uses the first-called lazy function execution on repeated submission navigations\", async () => {\n      let { routes, lazy, lazyDeferred } = createBasicLazyFunctionRoutes();\n      let t = setup({ routes });\n      expect(lazy).not.toHaveBeenCalled();\n\n      await t.navigate(\"/lazy\", {\n        formMethod: \"post\",\n        formData: createFormData({}),\n      });\n      expect(t.router.state.location.pathname).toBe(\"/\");\n      expect(t.router.state.navigation.state).toBe(\"submitting\");\n      expect(lazy).toHaveBeenCalledTimes(1);\n\n      await t.navigate(\"/lazy\", {\n        formMethod: \"post\",\n        formData: createFormData({}),\n      });\n      expect(t.router.state.location.pathname).toBe(\"/\");\n      expect(t.router.state.navigation.state).toBe(\"submitting\");\n      expect(lazy).toHaveBeenCalledTimes(1);\n\n      let [action, actionDeferred] = createAsyncStub();\n      let [loader, loaderDeferred] = createAsyncStub();\n      await lazyDeferred.resolve({ action, loader });\n\n      await actionDeferred.resolve(\"LAZY ACTION\");\n      await loaderDeferred.resolve(\"LAZY LOADER\");\n\n      expect(t.router.state.location.pathname).toBe(\"/lazy\");\n      expect(t.router.state.navigation.state).toBe(\"idle\");\n\n      expect(t.router.state.actionData).toEqual({ lazy: \"LAZY ACTION\" });\n      expect(t.router.state.loaderData).toEqual({ lazy: \"LAZY LOADER\" });\n\n      expect(action).toHaveBeenCalledTimes(2);\n      expect(loader).toHaveBeenCalledTimes(1);\n      expect(lazy).toHaveBeenCalledTimes(1);\n    });\n\n    it(\"uses the first-called lazy function property on repeated submission navigations\", async () => {\n      let [lazyAction, lazyActionDeferred] = createAsyncStub();\n      let [lazyLoader, lazyLoaderDeferred] = createAsyncStub();\n      let routes = createBasicLazyRoutes({\n        action: lazyAction,\n        loader: lazyLoader,\n      });\n      let t = setup({ routes });\n      expect(lazyAction).not.toHaveBeenCalled();\n      expect(lazyLoader).not.toHaveBeenCalled();\n\n      await t.navigate(\"/lazy\", {\n        formMethod: \"post\",\n        formData: createFormData({}),\n      });\n      expect(t.router.state.location.pathname).toBe(\"/\");\n      expect(t.router.state.navigation.state).toBe(\"submitting\");\n      expect(lazyAction).toHaveBeenCalledTimes(1);\n      expect(lazyLoader).toHaveBeenCalledTimes(1);\n\n      await t.navigate(\"/lazy\", {\n        formMethod: \"post\",\n        formData: createFormData({}),\n      });\n      expect(t.router.state.location.pathname).toBe(\"/\");\n      expect(t.router.state.navigation.state).toBe(\"submitting\");\n      expect(lazyAction).toHaveBeenCalledTimes(1);\n      expect(lazyLoader).toHaveBeenCalledTimes(1);\n\n      let [action, actionDeferred] = createAsyncStub();\n      let [loader, loaderDeferred] = createAsyncStub();\n      await lazyActionDeferred.resolve(action);\n      await lazyLoaderDeferred.resolve(loader);\n\n      await actionDeferred.resolve(\"LAZY ACTION\");\n      await loaderDeferred.resolve(\"LAZY LOADER\");\n\n      expect(t.router.state.location.pathname).toBe(\"/lazy\");\n      expect(t.router.state.navigation.state).toBe(\"idle\");\n\n      expect(t.router.state.actionData).toEqual({ lazy: \"LAZY ACTION\" });\n      expect(t.router.state.loaderData).toEqual({ lazy: \"LAZY LOADER\" });\n\n      expect(action).toHaveBeenCalledTimes(2);\n      expect(loader).toHaveBeenCalledTimes(1);\n      expect(lazyAction).toHaveBeenCalledTimes(1);\n      expect(lazyLoader).toHaveBeenCalledTimes(1);\n    });\n\n    it(\"uses the first-called lazy function execution on repeated fetcher.load calls\", async () => {\n      let { routes, lazy, lazyDeferred } = createBasicLazyFunctionRoutes();\n      let t = setup({ routes });\n      expect(lazy).not.toHaveBeenCalled();\n\n      let key = \"key\";\n      await t.fetch(\"/lazy\", key);\n      expect(t.router.state.fetchers.get(key)?.state).toBe(\"loading\");\n      expect(lazy).toHaveBeenCalledTimes(1);\n\n      await t.fetch(\"/lazy\", key);\n      expect(t.router.state.fetchers.get(key)?.state).toBe(\"loading\");\n      expect(lazy).toHaveBeenCalledTimes(1);\n\n      let [loader, loaderDeferred] = createAsyncStub();\n      await lazyDeferred.resolve({ loader });\n\n      expect(t.fetchers[key].state).toBe(\"loading\");\n\n      await loaderDeferred.resolve(\"LAZY LOADER\");\n\n      expect(t.fetchers[key].state).toBe(\"idle\");\n      expect(t.fetchers[key].data).toBe(\"LAZY LOADER\");\n      expect(loader).toHaveBeenCalledTimes(2);\n      expect(lazy).toHaveBeenCalledTimes(1);\n    });\n\n    it(\"uses the first-called lazy property execution on repeated fetcher.load calls\", async () => {\n      let [lazyLoader, lazyLoaderDeferred] = createAsyncStub();\n      let routes = createBasicLazyRoutes({ loader: lazyLoader });\n      let t = setup({ routes });\n      expect(lazyLoader).not.toHaveBeenCalled();\n\n      let key = \"key\";\n      await t.fetch(\"/lazy\", key);\n      expect(t.router.state.fetchers.get(key)?.state).toBe(\"loading\");\n      expect(lazyLoader).toHaveBeenCalledTimes(1);\n\n      await t.fetch(\"/lazy\", key);\n      expect(t.router.state.fetchers.get(key)?.state).toBe(\"loading\");\n      expect(lazyLoader).toHaveBeenCalledTimes(1);\n\n      let [loader, loaderDeferred] = createAsyncStub();\n      await lazyLoaderDeferred.resolve(loader);\n\n      expect(t.fetchers[key].state).toBe(\"loading\");\n\n      await loaderDeferred.resolve(\"LAZY LOADER\");\n\n      expect(t.fetchers[key].state).toBe(\"idle\");\n      expect(t.fetchers[key].data).toBe(\"LAZY LOADER\");\n      expect(loader).toHaveBeenCalledTimes(2);\n      expect(lazyLoader).toHaveBeenCalledTimes(1);\n    });\n  });\n\n  describe(\"errors\", () => {\n    it(\"handles errors when failing to resolve lazy route function on initialization\", async () => {\n      let lazyDeferred = createDeferred();\n      let router = createRouter({\n        history: createMemoryHistory({ initialEntries: [\"/lazy\"] }),\n        routes: [\n          {\n            id: \"root\",\n            path: \"/\",\n            hasErrorBoundary: true,\n            children: [\n              {\n                id: \"lazy\",\n                path: \"lazy\",\n                lazy: () => lazyDeferred.promise,\n              },\n            ],\n          },\n        ],\n      }).initialize();\n\n      expect(router.state.initialized).toBe(false);\n      lazyDeferred.reject(new Error(\"LAZY FUNCTION ERROR\"));\n      await tick();\n      expect(router.state.errors).toEqual({\n        root: new Error(\"LAZY FUNCTION ERROR\"),\n      });\n      expect(router.state.initialized).toBe(true);\n    });\n\n    it(\"handles errors when failing to resolve lazy route property on initialization\", async () => {\n      let lazyLoaderDeferred = createDeferred();\n      let router = createRouter({\n        history: createMemoryHistory({ initialEntries: [\"/lazy\"] }),\n        routes: [\n          {\n            id: \"root\",\n            path: \"/\",\n            hasErrorBoundary: true,\n            children: [\n              {\n                id: \"lazy\",\n                path: \"lazy\",\n                lazy: {\n                  loader: () => lazyLoaderDeferred.promise,\n                },\n              },\n            ],\n          },\n        ],\n      }).initialize();\n\n      expect(router.state.initialized).toBe(false);\n      lazyLoaderDeferred.reject(new Error(\"LAZY PROPERTY ERROR\"));\n      await tick();\n      expect(router.state.errors).toEqual({\n        root: new Error(\"LAZY PROPERTY ERROR\"),\n      });\n      expect(router.state.initialized).toBe(true);\n    });\n\n    it(\"handles errors when failing to resolve lazy route function on loading navigation\", async () => {\n      let { routes, lazy, lazyDeferred } = createBasicLazyFunctionRoutes();\n      let t = setup({ routes });\n      expect(lazy).not.toHaveBeenCalled();\n\n      await t.navigate(\"/lazy\");\n      expect(t.router.state.location.pathname).toBe(\"/\");\n      expect(t.router.state.navigation.state).toBe(\"loading\");\n      expect(lazy).toHaveBeenCalledTimes(1);\n\n      await lazyDeferred.reject(new Error(\"LAZY FUNCTION ERROR\"));\n      expect(t.router.state.location.pathname).toBe(\"/lazy\");\n      expect(t.router.state.navigation.state).toBe(\"idle\");\n\n      expect(t.router.state.loaderData).toEqual({});\n      expect(t.router.state.errors).toEqual({\n        root: new Error(\"LAZY FUNCTION ERROR\"),\n      });\n      expect(lazy).toHaveBeenCalledTimes(1);\n    });\n\n    it(\"handles errors when failing to resolve lazy route loader property on loading navigation\", async () => {\n      let [lazyLoader, lazyLoaderDeferred] = createAsyncStub();\n      let routes = createBasicLazyRoutes({ loader: lazyLoader });\n      let t = setup({ routes });\n      expect(lazyLoader).not.toHaveBeenCalled();\n\n      await t.navigate(\"/lazy\");\n      expect(t.router.state.location.pathname).toBe(\"/\");\n      expect(t.router.state.navigation.state).toBe(\"loading\");\n      expect(lazyLoader).toHaveBeenCalledTimes(1);\n\n      await lazyLoaderDeferred.reject(new Error(\"LAZY PROPERTY ERROR\"));\n      expect(t.router.state.location.pathname).toBe(\"/lazy\");\n      expect(t.router.state.navigation.state).toBe(\"idle\");\n\n      expect(t.router.state.loaderData).toEqual({});\n      expect(t.router.state.errors).toEqual({\n        root: new Error(\"LAZY PROPERTY ERROR\"),\n      });\n      expect(lazyLoader).toHaveBeenCalledTimes(1);\n    });\n\n    it(\"handles errors when failing to resolve other lazy route properties on loading navigation\", async () => {\n      let [lazyLoader, lazyLoaderDeferred] = createAsyncStub();\n      let [lazyAction, lazyActionDeferred] = createAsyncStub();\n      let routes = createBasicLazyRoutes({\n        loader: lazyLoader,\n        action: lazyAction,\n      });\n      let t = setup({ routes });\n      expect(lazyLoader).not.toHaveBeenCalled();\n\n      await t.navigate(\"/lazy\");\n      expect(t.router.state.location.pathname).toBe(\"/\");\n      expect(t.router.state.navigation.state).toBe(\"loading\");\n      expect(lazyLoader).toHaveBeenCalledTimes(1);\n\n      // Ensure loader is called as soon as it's loaded\n      let loader = jest.fn(() => null);\n      await lazyLoaderDeferred.resolve(loader);\n      expect(t.router.state.location.pathname).toBe(\"/\");\n      expect(t.router.state.navigation.state).toBe(\"loading\");\n      expect(loader).toHaveBeenCalledTimes(1);\n\n      // Reject remaining lazy properties\n      await lazyActionDeferred.reject(new Error(\"LAZY PROPERTY ERROR\"));\n      expect(t.router.state.location.pathname).toBe(\"/lazy\");\n      expect(t.router.state.navigation.state).toBe(\"idle\");\n\n      expect(t.router.state.loaderData).toEqual({});\n      expect(t.router.state.errors).toEqual({\n        root: new Error(\"LAZY PROPERTY ERROR\"),\n      });\n      expect(lazyLoader).toHaveBeenCalledTimes(1);\n      expect(lazyAction).toHaveBeenCalledTimes(1);\n    });\n\n    it(\"handles loader errors from lazy route functions when the route has an error boundary\", async () => {\n      let { routes, lazy, lazyDeferred } = createBasicLazyFunctionRoutes();\n      let t = setup({ routes });\n      expect(lazy).not.toHaveBeenCalled();\n\n      await t.navigate(\"/lazy\");\n      expect(t.router.state.location.pathname).toBe(\"/\");\n      expect(t.router.state.navigation.state).toBe(\"loading\");\n      expect(lazy).toHaveBeenCalledTimes(1);\n\n      let loaderDeferred = createDeferred();\n      await lazyDeferred.resolve({\n        loader: () => loaderDeferred.promise,\n        hasErrorBoundary: true,\n      });\n      expect(t.router.state.location.pathname).toBe(\"/\");\n      expect(t.router.state.navigation.state).toBe(\"loading\");\n      expect(lazy).toHaveBeenCalledTimes(1);\n      await loaderDeferred.reject(new Error(\"LAZY LOADER ERROR\"));\n\n      expect(t.router.state.location.pathname).toBe(\"/lazy\");\n      expect(t.router.state.navigation.state).toBe(\"idle\");\n      expect(t.router.state.errors).toEqual({\n        lazy: new Error(\"LAZY LOADER ERROR\"),\n      });\n      expect(lazy).toHaveBeenCalledTimes(1);\n    });\n\n    it(\"handles loader errors from lazy route properties when the route has an error boundary\", async () => {\n      let [lazyLoader, lazyLoaderDeferred] = createAsyncStub();\n      let [lazyHasErrorBoundary, lazyHasErrorBoundaryDeferred] =\n        createAsyncStub();\n      let routes = createBasicLazyRoutes({\n        loader: lazyLoader,\n        hasErrorBoundary: lazyHasErrorBoundary,\n      });\n      let t = setup({ routes });\n      expect(lazyLoader).not.toHaveBeenCalled();\n      expect(lazyHasErrorBoundary).not.toHaveBeenCalled();\n\n      await t.navigate(\"/lazy\");\n      expect(t.router.state.location.pathname).toBe(\"/\");\n      expect(t.router.state.navigation.state).toBe(\"loading\");\n      expect(lazyLoader).toHaveBeenCalledTimes(1);\n      expect(lazyHasErrorBoundary).toHaveBeenCalledTimes(1);\n\n      let loaderDeferred = createDeferred();\n      await lazyLoaderDeferred.resolve(() => loaderDeferred.promise);\n      await lazyHasErrorBoundaryDeferred.resolve(() => true);\n      expect(t.router.state.location.pathname).toBe(\"/\");\n      expect(t.router.state.navigation.state).toBe(\"loading\");\n      expect(lazyLoader).toHaveBeenCalledTimes(1);\n      await loaderDeferred.reject(new Error(\"LAZY LOADER ERROR\"));\n\n      expect(t.router.state.location.pathname).toBe(\"/lazy\");\n      expect(t.router.state.navigation.state).toBe(\"idle\");\n      expect(t.router.state.errors).toEqual({\n        lazy: new Error(\"LAZY LOADER ERROR\"),\n      });\n      expect(lazyLoader).toHaveBeenCalledTimes(1);\n      expect(lazyHasErrorBoundary).toHaveBeenCalledTimes(1);\n    });\n\n    it(\"bubbles loader errors from in lazy route functions when the route does not specify an error boundary\", async () => {\n      let { routes, lazy, lazyDeferred } = createBasicLazyFunctionRoutes();\n      let t = setup({ routes });\n      expect(lazy).not.toHaveBeenCalled();\n\n      await t.navigate(\"/lazy\");\n      expect(t.router.state.location.pathname).toBe(\"/\");\n      expect(t.router.state.navigation.state).toBe(\"loading\");\n      expect(lazy).toHaveBeenCalledTimes(1);\n\n      let loaderDeferred = createDeferred();\n      await lazyDeferred.resolve({\n        loader: () => loaderDeferred.promise,\n      });\n      expect(t.router.state.location.pathname).toBe(\"/\");\n      expect(t.router.state.navigation.state).toBe(\"loading\");\n\n      await loaderDeferred.reject(new Error(\"LAZY LOADER ERROR\"));\n\n      expect(t.router.state.location.pathname).toBe(\"/lazy\");\n      expect(t.router.state.navigation.state).toBe(\"idle\");\n      expect(t.router.state.errors).toEqual({\n        root: new Error(\"LAZY LOADER ERROR\"),\n      });\n      expect(lazy).toHaveBeenCalledTimes(1);\n    });\n\n    it(\"bubbles loader errors from in lazy route properties when the route does not specify an error boundary\", async () => {\n      let [lazyLoader, lazyLoaderDeferred] = createAsyncStub();\n      let routes = createBasicLazyRoutes({ loader: lazyLoader });\n      let t = setup({ routes });\n      expect(lazyLoader).not.toHaveBeenCalled();\n\n      await t.navigate(\"/lazy\");\n      expect(t.router.state.location.pathname).toBe(\"/\");\n      expect(t.router.state.navigation.state).toBe(\"loading\");\n      expect(lazyLoader).toHaveBeenCalledTimes(1);\n\n      let loaderDeferred = createDeferred();\n      await lazyLoaderDeferred.resolve(() => loaderDeferred.promise);\n      expect(t.router.state.location.pathname).toBe(\"/\");\n      expect(t.router.state.navigation.state).toBe(\"loading\");\n\n      await loaderDeferred.reject(new Error(\"LAZY LOADER ERROR\"));\n\n      expect(t.router.state.location.pathname).toBe(\"/lazy\");\n      expect(t.router.state.navigation.state).toBe(\"idle\");\n      expect(t.router.state.errors).toEqual({\n        root: new Error(\"LAZY LOADER ERROR\"),\n      });\n      expect(lazyLoader).toHaveBeenCalledTimes(1);\n    });\n\n    it(\"bubbles loader errors from lazy route functions when the route specifies hasErrorBoundary:false\", async () => {\n      let { routes, lazy, lazyDeferred } = createBasicLazyFunctionRoutes();\n      let t = setup({ routes });\n      expect(lazy).not.toHaveBeenCalled();\n\n      await t.navigate(\"/lazy\");\n      expect(t.router.state.location.pathname).toBe(\"/\");\n      expect(t.router.state.navigation.state).toBe(\"loading\");\n      expect(lazy).toHaveBeenCalledTimes(1);\n\n      let loaderDeferred = createDeferred();\n      await lazyDeferred.resolve({\n        loader: () => loaderDeferred.promise,\n        hasErrorBoundary: false,\n      });\n      expect(t.router.state.location.pathname).toBe(\"/\");\n      expect(t.router.state.navigation.state).toBe(\"loading\");\n\n      await loaderDeferred.reject(new Error(\"LAZY LOADER ERROR\"));\n\n      expect(t.router.state.location.pathname).toBe(\"/lazy\");\n      expect(t.router.state.navigation.state).toBe(\"idle\");\n      expect(t.router.state.errors).toEqual({\n        root: new Error(\"LAZY LOADER ERROR\"),\n      });\n      expect(lazy).toHaveBeenCalledTimes(1);\n    });\n\n    it(\"bubbles loader errors from lazy route properties when the route specifies hasErrorBoundary:false\", async () => {\n      let [lazyLoader, lazyLoaderDeferred] = createAsyncStub();\n      let [lazyHasErrorBoundary, lazyHasErrorBoundaryDeferred] =\n        createAsyncStub();\n      let routes = createBasicLazyRoutes({\n        loader: lazyLoader,\n        hasErrorBoundary: lazyHasErrorBoundary,\n      });\n      let t = setup({ routes });\n      expect(lazyLoader).not.toHaveBeenCalled();\n      expect(lazyHasErrorBoundary).not.toHaveBeenCalled();\n\n      await t.navigate(\"/lazy\");\n      expect(t.router.state.location.pathname).toBe(\"/\");\n      expect(t.router.state.navigation.state).toBe(\"loading\");\n      expect(lazyLoader).toHaveBeenCalledTimes(1);\n      expect(lazyHasErrorBoundary).toHaveBeenCalledTimes(1);\n\n      let loaderDeferred = createDeferred();\n      await lazyLoaderDeferred.resolve(() => loaderDeferred.promise);\n      await lazyHasErrorBoundaryDeferred.resolve(false);\n      expect(t.router.state.location.pathname).toBe(\"/\");\n      expect(t.router.state.navigation.state).toBe(\"loading\");\n\n      await loaderDeferred.reject(new Error(\"LAZY LOADER ERROR\"));\n\n      expect(t.router.state.location.pathname).toBe(\"/lazy\");\n      expect(t.router.state.navigation.state).toBe(\"idle\");\n      expect(t.router.state.errors).toEqual({\n        root: new Error(\"LAZY LOADER ERROR\"),\n      });\n      expect(lazyLoader).toHaveBeenCalledTimes(1);\n      expect(lazyHasErrorBoundary).toHaveBeenCalledTimes(1);\n    });\n\n    it(\"bubbles loader errors from lazy route properties when the route specifies hasErrorBoundary:null\", async () => {\n      let [lazyLoader, lazyLoaderDeferred] = createAsyncStub();\n      let [lazyHasErrorBoundary, lazyHasErrorBoundaryDeferred] =\n        createAsyncStub();\n      let routes = createBasicLazyRoutes({\n        loader: lazyLoader,\n        hasErrorBoundary: lazyHasErrorBoundary,\n      });\n      let t = setup({ routes });\n      expect(lazyLoader).not.toHaveBeenCalled();\n      expect(lazyHasErrorBoundary).not.toHaveBeenCalled();\n\n      await t.navigate(\"/lazy\");\n      expect(t.router.state.location.pathname).toBe(\"/\");\n      expect(t.router.state.navigation.state).toBe(\"loading\");\n      expect(lazyLoader).toHaveBeenCalledTimes(1);\n      expect(lazyHasErrorBoundary).toHaveBeenCalledTimes(1);\n\n      let loaderDeferred = createDeferred();\n      await lazyLoaderDeferred.resolve(() => loaderDeferred.promise);\n      await lazyHasErrorBoundaryDeferred.resolve(null);\n      expect(t.router.state.location.pathname).toBe(\"/\");\n      expect(t.router.state.navigation.state).toBe(\"loading\");\n\n      await loaderDeferred.reject(new Error(\"LAZY LOADER ERROR\"));\n\n      expect(t.router.state.location.pathname).toBe(\"/lazy\");\n      expect(t.router.state.navigation.state).toBe(\"idle\");\n      expect(t.router.state.errors).toEqual({\n        root: new Error(\"LAZY LOADER ERROR\"),\n      });\n      expect(lazyLoader).toHaveBeenCalledTimes(1);\n      expect(lazyHasErrorBoundary).toHaveBeenCalledTimes(1);\n    });\n\n    it(\"handles errors when failing to resolve lazy route functions on submission navigation\", async () => {\n      let { routes, lazy, lazyDeferred } = createBasicLazyFunctionRoutes();\n      let t = setup({ routes });\n      expect(lazy).not.toHaveBeenCalled();\n\n      await t.navigate(\"/lazy\", {\n        formMethod: \"post\",\n        formData: createFormData({}),\n      });\n      expect(t.router.state.location.pathname).toBe(\"/\");\n      expect(t.router.state.navigation.state).toBe(\"submitting\");\n      expect(lazy).toHaveBeenCalledTimes(1);\n\n      await lazyDeferred.reject(new Error(\"LAZY FUNCTION ERROR\"));\n      expect(t.router.state.location.pathname).toBe(\"/lazy\");\n      expect(t.router.state.navigation.state).toBe(\"idle\");\n\n      expect(t.router.state.errors).toEqual({\n        root: new Error(\"LAZY FUNCTION ERROR\"),\n      });\n      expect(t.router.state.actionData).toEqual(null);\n      expect(t.router.state.loaderData).toEqual({});\n      expect(lazy).toHaveBeenCalledTimes(1);\n    });\n\n    it(\"handles errors when failing to resolve lazy route properties on submission navigation\", async () => {\n      let [lazyAction, lazyActionDeferred] = createAsyncStub();\n      let routes = createBasicLazyRoutes({ action: lazyAction });\n      let t = setup({ routes });\n      expect(lazyAction).not.toHaveBeenCalled();\n\n      await t.navigate(\"/lazy\", {\n        formMethod: \"post\",\n        formData: createFormData({}),\n      });\n      expect(t.router.state.location.pathname).toBe(\"/\");\n      expect(t.router.state.navigation.state).toBe(\"submitting\");\n      expect(lazyAction).toHaveBeenCalledTimes(1);\n\n      await lazyActionDeferred.reject(new Error(\"LAZY FUNCTION ERROR\"));\n      expect(t.router.state.location.pathname).toBe(\"/lazy\");\n      expect(t.router.state.navigation.state).toBe(\"idle\");\n\n      expect(t.router.state.errors).toEqual({\n        root: new Error(\"LAZY FUNCTION ERROR\"),\n      });\n      expect(t.router.state.actionData).toEqual(null);\n      expect(t.router.state.loaderData).toEqual({});\n      expect(lazyAction).toHaveBeenCalledTimes(1);\n    });\n\n    it(\"handles action errors from lazy route functions on submission navigation\", async () => {\n      let { routes, lazy, lazyDeferred } = createBasicLazyFunctionRoutes();\n      let t = setup({ routes });\n      expect(lazy).not.toHaveBeenCalled();\n\n      await t.navigate(\"/lazy\", {\n        formMethod: \"post\",\n        formData: createFormData({}),\n      });\n      expect(t.router.state.location.pathname).toBe(\"/\");\n      expect(t.router.state.navigation.state).toBe(\"submitting\");\n      expect(lazy).toHaveBeenCalledTimes(1);\n\n      let actionDeferred = createDeferred();\n      await lazyDeferred.resolve({\n        action: () => actionDeferred.promise,\n        hasErrorBoundary: true,\n      });\n      expect(t.router.state.location.pathname).toBe(\"/\");\n      expect(t.router.state.navigation.state).toBe(\"submitting\");\n\n      await actionDeferred.reject(new Error(\"LAZY ACTION ERROR\"));\n      expect(t.router.state.location.pathname).toBe(\"/lazy\");\n      expect(t.router.state.navigation.state).toBe(\"idle\");\n      expect(t.router.state.actionData).toEqual(null);\n      expect(t.router.state.errors).toEqual({\n        lazy: new Error(\"LAZY ACTION ERROR\"),\n      });\n      expect(lazy).toHaveBeenCalledTimes(1);\n    });\n\n    it(\"handles action errors from lazy route properties on submission navigation\", async () => {\n      let [lazyAction, lazyActionDeferred] = createAsyncStub();\n      let [lazyErrorBoundaryStub, lazyErrorBoundaryDeferred] =\n        createAsyncStub();\n      let routes = createBasicLazyRoutes({\n        action: lazyAction,\n        hasErrorBoundary: lazyErrorBoundaryStub,\n      });\n      let t = setup({ routes });\n      expect(lazyAction).not.toHaveBeenCalled();\n      expect(lazyErrorBoundaryStub).not.toHaveBeenCalled();\n\n      await t.navigate(\"/lazy\", {\n        formMethod: \"post\",\n        formData: createFormData({}),\n      });\n      expect(t.router.state.location.pathname).toBe(\"/\");\n      expect(t.router.state.navigation.state).toBe(\"submitting\");\n      expect(lazyAction).toHaveBeenCalledTimes(1);\n      expect(lazyErrorBoundaryStub).toHaveBeenCalledTimes(1);\n\n      let actionDeferred = createDeferred();\n      await lazyActionDeferred.resolve(() => actionDeferred.promise);\n      await lazyErrorBoundaryDeferred.resolve(true);\n      expect(t.router.state.location.pathname).toBe(\"/\");\n      expect(t.router.state.navigation.state).toBe(\"submitting\");\n\n      await actionDeferred.reject(new Error(\"LAZY ACTION ERROR\"));\n      expect(t.router.state.location.pathname).toBe(\"/lazy\");\n      expect(t.router.state.navigation.state).toBe(\"idle\");\n      expect(t.router.state.actionData).toEqual(null);\n      expect(t.router.state.errors).toEqual({\n        lazy: new Error(\"LAZY ACTION ERROR\"),\n      });\n      expect(lazyAction).toHaveBeenCalledTimes(1);\n      expect(lazyErrorBoundaryStub).toHaveBeenCalledTimes(1);\n    });\n\n    it(\"bubbles action errors from lazy route functions when the route specifies hasErrorBoundary:false\", async () => {\n      let { routes, lazy, lazyDeferred } = createBasicLazyFunctionRoutes();\n      let t = setup({ routes });\n      expect(lazy).not.toHaveBeenCalled();\n\n      await t.navigate(\"/lazy\", {\n        formMethod: \"post\",\n        formData: createFormData({}),\n      });\n      expect(t.router.state.location.pathname).toBe(\"/\");\n      expect(t.router.state.navigation.state).toBe(\"submitting\");\n      expect(lazy).toHaveBeenCalledTimes(1);\n\n      let actionDeferred = createDeferred();\n      await lazyDeferred.resolve({\n        action: () => actionDeferred.promise,\n        hasErrorBoundary: false,\n      });\n      expect(t.router.state.location.pathname).toBe(\"/\");\n      expect(t.router.state.navigation.state).toBe(\"submitting\");\n\n      await actionDeferred.reject(new Error(\"LAZY ACTION ERROR\"));\n      expect(t.router.state.location.pathname).toBe(\"/lazy\");\n      expect(t.router.state.navigation.state).toBe(\"idle\");\n      expect(t.router.state.actionData).toEqual(null);\n      expect(t.router.state.errors).toEqual({\n        root: new Error(\"LAZY ACTION ERROR\"),\n      });\n      expect(lazy).toHaveBeenCalledTimes(1);\n    });\n\n    it(\"bubbles action errors from lazy route properties when the route specifies hasErrorBoundary:false\", async () => {\n      let [lazyAction, lazyActionDeferred] = createAsyncStub();\n      let [lazyErrorBoundaryStub, lazyErrorBoundaryDeferred] =\n        createAsyncStub();\n      let routes = createBasicLazyRoutes({\n        action: lazyAction,\n        hasErrorBoundary: lazyErrorBoundaryStub,\n      });\n      let t = setup({ routes });\n      expect(lazyAction).not.toHaveBeenCalled();\n      expect(lazyErrorBoundaryStub).not.toHaveBeenCalled();\n\n      await t.navigate(\"/lazy\", {\n        formMethod: \"post\",\n        formData: createFormData({}),\n      });\n      expect(t.router.state.location.pathname).toBe(\"/\");\n      expect(t.router.state.navigation.state).toBe(\"submitting\");\n      expect(lazyAction).toHaveBeenCalledTimes(1);\n      expect(lazyErrorBoundaryStub).toHaveBeenCalledTimes(1);\n\n      let actionDeferred = createDeferred();\n      await lazyActionDeferred.resolve(() => actionDeferred.promise);\n      await lazyErrorBoundaryDeferred.resolve(false);\n      expect(t.router.state.location.pathname).toBe(\"/\");\n      expect(t.router.state.navigation.state).toBe(\"submitting\");\n\n      await actionDeferred.reject(new Error(\"LAZY ACTION ERROR\"));\n      expect(t.router.state.location.pathname).toBe(\"/lazy\");\n      expect(t.router.state.navigation.state).toBe(\"idle\");\n      expect(t.router.state.actionData).toEqual(null);\n      expect(t.router.state.errors).toEqual({\n        root: new Error(\"LAZY ACTION ERROR\"),\n      });\n      expect(lazyAction).toHaveBeenCalledTimes(1);\n      expect(lazyErrorBoundaryStub).toHaveBeenCalledTimes(1);\n    });\n\n    it(\"bubbles action errors from lazy route properties when the route specifies hasErrorBoundary:null\", async () => {\n      let [lazyAction, lazyActionDeferred] = createAsyncStub();\n      let [lazyErrorBoundaryStub, lazyErrorBoundaryDeferred] =\n        createAsyncStub();\n      let routes = createBasicLazyRoutes({\n        action: lazyAction,\n        hasErrorBoundary: lazyErrorBoundaryStub,\n      });\n      let t = setup({ routes });\n      expect(lazyAction).not.toHaveBeenCalled();\n      expect(lazyErrorBoundaryStub).not.toHaveBeenCalled();\n\n      await t.navigate(\"/lazy\", {\n        formMethod: \"post\",\n        formData: createFormData({}),\n      });\n      expect(t.router.state.location.pathname).toBe(\"/\");\n      expect(t.router.state.navigation.state).toBe(\"submitting\");\n      expect(lazyAction).toHaveBeenCalledTimes(1);\n      expect(lazyErrorBoundaryStub).toHaveBeenCalledTimes(1);\n\n      let actionDeferred = createDeferred();\n      await lazyActionDeferred.resolve(() => actionDeferred.promise);\n      await lazyErrorBoundaryDeferred.resolve(null);\n      expect(t.router.state.location.pathname).toBe(\"/\");\n      expect(t.router.state.navigation.state).toBe(\"submitting\");\n\n      await actionDeferred.reject(new Error(\"LAZY ACTION ERROR\"));\n      expect(t.router.state.location.pathname).toBe(\"/lazy\");\n      expect(t.router.state.navigation.state).toBe(\"idle\");\n      expect(t.router.state.actionData).toEqual(null);\n      expect(t.router.state.errors).toEqual({\n        root: new Error(\"LAZY ACTION ERROR\"),\n      });\n      expect(lazyAction).toHaveBeenCalledTimes(1);\n      expect(lazyErrorBoundaryStub).toHaveBeenCalledTimes(1);\n    });\n\n    it(\"handles errors when failing to load lazy route functions on fetcher.load\", async () => {\n      let { routes, lazy, lazyDeferred } = createBasicLazyFunctionRoutes();\n      let t = setup({ routes });\n      expect(lazy).not.toHaveBeenCalled();\n\n      let key = \"key\";\n      await t.fetch(\"/lazy\", key);\n      expect(t.router.state.fetchers.get(key)?.state).toBe(\"loading\");\n      expect(lazy).toHaveBeenCalledTimes(1);\n\n      await lazyDeferred.reject(new Error(\"LAZY FUNCTION ERROR\"));\n      expect(t.router.state.fetchers.get(key)).toBeUndefined();\n      expect(t.router.state.errors).toEqual({\n        root: new Error(\"LAZY FUNCTION ERROR\"),\n      });\n      expect(lazy).toHaveBeenCalledTimes(1);\n    });\n\n    it(\"handles errors when failing to load lazy route properties on fetcher.load\", async () => {\n      let [lazyLoader, lazyLoaderDeferred] = createAsyncStub();\n      let routes = createBasicLazyRoutes({ loader: lazyLoader });\n      let t = setup({ routes });\n      expect(lazyLoader).not.toHaveBeenCalled();\n\n      let key = \"key\";\n      await t.fetch(\"/lazy\", key);\n      expect(t.router.state.fetchers.get(key)?.state).toBe(\"loading\");\n      expect(lazyLoader).toHaveBeenCalledTimes(1);\n\n      await lazyLoaderDeferred.reject(new Error(\"LAZY FUNCTION ERROR\"));\n      expect(t.router.state.fetchers.get(key)).toBeUndefined();\n      expect(t.router.state.errors).toEqual({\n        root: new Error(\"LAZY FUNCTION ERROR\"),\n      });\n      expect(lazyLoader).toHaveBeenCalledTimes(1);\n    });\n\n    it(\"handles loader errors in lazy route functions on fetcher.load\", async () => {\n      let { routes, lazy, lazyDeferred } = createBasicLazyFunctionRoutes();\n      let t = setup({ routes });\n      expect(lazy).not.toHaveBeenCalled();\n\n      let key = \"key\";\n      await t.fetch(\"/lazy\", key);\n      expect(t.router.state.fetchers.get(key)?.state).toBe(\"loading\");\n      expect(lazy).toHaveBeenCalledTimes(1);\n\n      let loaderDeferred = createDeferred();\n      await lazyDeferred.resolve({\n        loader: () => loaderDeferred.promise,\n      });\n      expect(t.router.state.fetchers.get(key)?.state).toBe(\"loading\");\n\n      await loaderDeferred.reject(new Error(\"LAZY LOADER ERROR\"));\n      expect(t.router.state.fetchers.get(key)).toBeUndefined();\n      expect(t.router.state.errors).toEqual({\n        root: new Error(\"LAZY LOADER ERROR\"),\n      });\n      expect(lazy).toHaveBeenCalledTimes(1);\n    });\n\n    it(\"handles loader errors in lazy route properties on fetcher.load\", async () => {\n      let [lazyLoader, lazyLoaderDeferred] = createAsyncStub();\n      let routes = createBasicLazyRoutes({ loader: lazyLoader });\n      let t = setup({ routes });\n      expect(lazyLoader).not.toHaveBeenCalled();\n\n      let key = \"key\";\n      await t.fetch(\"/lazy\", key);\n      expect(t.router.state.fetchers.get(key)?.state).toBe(\"loading\");\n      expect(lazyLoader).toHaveBeenCalledTimes(1);\n\n      let loaderDeferred = createDeferred();\n      await lazyLoaderDeferred.resolve(() => loaderDeferred.promise);\n      expect(t.router.state.fetchers.get(key)?.state).toBe(\"loading\");\n\n      await loaderDeferred.reject(new Error(\"LAZY LOADER ERROR\"));\n      expect(t.router.state.fetchers.get(key)).toBeUndefined();\n      expect(t.router.state.errors).toEqual({\n        root: new Error(\"LAZY LOADER ERROR\"),\n      });\n      expect(lazyLoader).toHaveBeenCalledTimes(1);\n    });\n\n    it(\"handles errors when failing to load lazy route functions on fetcher.submit\", async () => {\n      let { routes, lazy, lazyDeferred } = createBasicLazyFunctionRoutes();\n      let t = setup({ routes });\n      expect(lazy).not.toHaveBeenCalled();\n\n      let key = \"key\";\n      await t.fetch(\"/lazy\", key, {\n        formMethod: \"post\",\n        formData: createFormData({}),\n      });\n      expect(t.router.state.fetchers.get(key)?.state).toBe(\"submitting\");\n      expect(lazy).toHaveBeenCalledTimes(1);\n\n      await lazyDeferred.reject(new Error(\"LAZY FUNCTION ERROR\"));\n      expect(t.router.state.fetchers.get(key)).toBeUndefined();\n      expect(t.router.state.errors).toEqual({\n        root: new Error(\"LAZY FUNCTION ERROR\"),\n      });\n      expect(lazy).toHaveBeenCalledTimes(1);\n    });\n\n    it(\"handles errors when failing to load lazy route properties on fetcher.submit\", async () => {\n      let [lazyAction, lazyActionDeferred] = createAsyncStub();\n      let routes = createBasicLazyRoutes({ action: lazyAction });\n      let t = setup({ routes });\n      expect(lazyAction).not.toHaveBeenCalled();\n\n      let key = \"key\";\n      await t.fetch(\"/lazy\", key, {\n        formMethod: \"post\",\n        formData: createFormData({}),\n      });\n      expect(t.router.state.fetchers.get(key)?.state).toBe(\"submitting\");\n      expect(lazyAction).toHaveBeenCalledTimes(1);\n\n      await lazyActionDeferred.reject(new Error(\"LAZY FUNCTION ERROR\"));\n      expect(t.router.state.fetchers.get(key)).toBeUndefined();\n      expect(t.router.state.errors).toEqual({\n        root: new Error(\"LAZY FUNCTION ERROR\"),\n      });\n      expect(lazyAction).toHaveBeenCalledTimes(1);\n    });\n\n    it(\"handles action errors in lazy route functions on fetcher.submit\", async () => {\n      let { routes, lazy, lazyDeferred } = createBasicLazyFunctionRoutes();\n      let t = setup({ routes });\n      expect(lazy).not.toHaveBeenCalled();\n\n      let key = \"key\";\n      await t.fetch(\"/lazy\", key, {\n        formMethod: \"post\",\n        formData: createFormData({}),\n      });\n      expect(t.router.state.fetchers.get(key)?.state).toBe(\"submitting\");\n      expect(lazy).toHaveBeenCalledTimes(1);\n\n      let actionDeferred = createDeferred();\n      await lazyDeferred.resolve({\n        action: () => actionDeferred.promise,\n      });\n      expect(t.router.state.fetchers.get(key)?.state).toBe(\"submitting\");\n\n      await actionDeferred.reject(new Error(\"LAZY ACTION ERROR\"));\n      await tick();\n      expect(t.router.state.fetchers.get(key)).toBeUndefined();\n      expect(t.router.state.errors).toEqual({\n        root: new Error(\"LAZY ACTION ERROR\"),\n      });\n      expect(lazy).toHaveBeenCalledTimes(1);\n    });\n\n    it(\"handles action errors in lazy route properties on fetcher.submit\", async () => {\n      let [lazyAction, lazyActionDeferred] = createAsyncStub();\n      let routes = createBasicLazyRoutes({ action: lazyAction });\n      let t = setup({ routes });\n      expect(lazyAction).not.toHaveBeenCalled();\n\n      let key = \"key\";\n      await t.fetch(\"/lazy\", key, {\n        formMethod: \"post\",\n        formData: createFormData({}),\n      });\n      expect(t.router.state.fetchers.get(key)?.state).toBe(\"submitting\");\n      expect(lazyAction).toHaveBeenCalledTimes(1);\n\n      let actionDeferred = createDeferred();\n      await lazyActionDeferred.resolve(() => actionDeferred.promise);\n      expect(t.router.state.fetchers.get(key)?.state).toBe(\"submitting\");\n\n      await actionDeferred.reject(new Error(\"LAZY ACTION ERROR\"));\n      await tick();\n      expect(t.router.state.fetchers.get(key)).toBeUndefined();\n      expect(t.router.state.errors).toEqual({\n        root: new Error(\"LAZY ACTION ERROR\"),\n      });\n      expect(lazyAction).toHaveBeenCalledTimes(1);\n    });\n\n    it(\"throws when failing to resolve lazy route functions on staticHandler.query()\", async () => {\n      let { query } = createStaticHandler([\n        {\n          id: \"root\",\n          path: \"/\",\n          children: [\n            {\n              id: \"lazy\",\n              path: \"/lazy\",\n              lazy: async () => {\n                throw new Error(\"LAZY FUNCTION ERROR\");\n              },\n            },\n          ],\n        },\n      ]);\n\n      let context = await query(createRequest(\"/lazy\"));\n      invariant(\n        !(context instanceof Response),\n        \"Expected a StaticContext instance\",\n      );\n      expect(context.errors).toEqual({\n        root: new Error(\"LAZY FUNCTION ERROR\"),\n      });\n    });\n\n    it(\"throws when failing to resolve lazy route properties on staticHandler.query()\", async () => {\n      let { query } = createStaticHandler([\n        {\n          id: \"root\",\n          path: \"/\",\n          children: [\n            {\n              id: \"lazy\",\n              path: \"/lazy\",\n              lazy: {\n                loader: async () => {\n                  throw new Error(\"LAZY PROPERTY ERROR\");\n                },\n              },\n            },\n          ],\n        },\n      ]);\n\n      let context = await query(createRequest(\"/lazy\"));\n      invariant(\n        !(context instanceof Response),\n        \"Expected a StaticContext instance\",\n      );\n      expect(context.errors).toEqual({\n        root: new Error(\"LAZY PROPERTY ERROR\"),\n      });\n    });\n\n    it(\"handles loader errors from lazy route functions on staticHandler.query()\", async () => {\n      let { query } = createStaticHandler([\n        {\n          id: \"root\",\n          path: \"/\",\n          children: [\n            {\n              id: \"lazy\",\n              path: \"/lazy\",\n              lazy: async () => {\n                await tick();\n                return {\n                  async loader() {\n                    throw new Error(\"LAZY LOADER ERROR\");\n                  },\n                  hasErrorBoundary: true,\n                };\n              },\n            },\n          ],\n        },\n      ]);\n\n      let context = await query(createRequest(\"/lazy\"));\n      invariant(\n        !(context instanceof Response),\n        \"Expected a StaticContext instance\",\n      );\n      expect(context.loaderData).toEqual({});\n      expect(context.errors).toEqual({\n        lazy: new Error(\"LAZY LOADER ERROR\"),\n      });\n    });\n\n    it(\"handles loader errors from lazy route properties on staticHandler.query()\", async () => {\n      let { query } = createStaticHandler([\n        {\n          id: \"root\",\n          path: \"/\",\n          children: [\n            {\n              id: \"lazy\",\n              path: \"/lazy\",\n              lazy: {\n                loader: async () => {\n                  await tick();\n                  return async () => {\n                    throw new Error(\"LAZY LOADER ERROR\");\n                  };\n                },\n                hasErrorBoundary: async () => {\n                  await tick();\n                  return true;\n                },\n              },\n            },\n          ],\n        },\n      ]);\n\n      let context = await query(createRequest(\"/lazy\"));\n      invariant(\n        !(context instanceof Response),\n        \"Expected a StaticContext instance\",\n      );\n      expect(context.loaderData).toEqual({});\n      expect(context.errors).toEqual({\n        lazy: new Error(\"LAZY LOADER ERROR\"),\n      });\n    });\n\n    it(\"bubbles loader errors from lazy route functions on staticHandler.query() when hasErrorBoundary is resolved as false\", async () => {\n      let { query } = createStaticHandler([\n        {\n          id: \"root\",\n          path: \"/\",\n          children: [\n            {\n              id: \"lazy\",\n              path: \"/lazy\",\n              lazy: async () => {\n                await tick();\n                return {\n                  async loader() {\n                    await tick();\n                    throw new Error(\"LAZY LOADER ERROR\");\n                  },\n                  hasErrorBoundary: false,\n                };\n              },\n            },\n          ],\n        },\n      ]);\n\n      let context = await query(createRequest(\"/lazy\"));\n      invariant(\n        !(context instanceof Response),\n        \"Expected a StaticContext instance\",\n      );\n      expect(context.loaderData).toEqual({});\n      expect(context.errors).toEqual({\n        root: new Error(\"LAZY LOADER ERROR\"),\n      });\n    });\n\n    it(\"bubbles loader errors from lazy route properties on staticHandler.query() when hasErrorBoundary is resolved as false\", async () => {\n      let { query } = createStaticHandler([\n        {\n          id: \"root\",\n          path: \"/\",\n          children: [\n            {\n              id: \"lazy\",\n              path: \"/lazy\",\n              lazy: {\n                loader: async () => {\n                  await tick();\n                  return async () => {\n                    throw new Error(\"LAZY LOADER ERROR\");\n                  };\n                },\n                hasErrorBoundary: async () => {\n                  await tick();\n                  return false;\n                },\n              },\n            },\n          ],\n        },\n      ]);\n\n      let context = await query(createRequest(\"/lazy\"));\n      invariant(\n        !(context instanceof Response),\n        \"Expected a StaticContext instance\",\n      );\n      expect(context.loaderData).toEqual({});\n      expect(context.errors).toEqual({\n        root: new Error(\"LAZY LOADER ERROR\"),\n      });\n    });\n\n    it(\"bubbles loader errors from lazy route properties on staticHandler.query() when hasErrorBoundary is resolved as null\", async () => {\n      let { query } = createStaticHandler([\n        {\n          id: \"root\",\n          path: \"/\",\n          children: [\n            {\n              id: \"lazy\",\n              path: \"/lazy\",\n              lazy: {\n                loader: async () => {\n                  await tick();\n                  return async () => {\n                    throw new Error(\"LAZY LOADER ERROR\");\n                  };\n                },\n                hasErrorBoundary: async () => {\n                  await tick();\n                  return null;\n                },\n              },\n            },\n          ],\n        },\n      ]);\n\n      let context = await query(createRequest(\"/lazy\"));\n      invariant(\n        !(context instanceof Response),\n        \"Expected a StaticContext instance\",\n      );\n      expect(context.loaderData).toEqual({});\n      expect(context.errors).toEqual({\n        root: new Error(\"LAZY LOADER ERROR\"),\n      });\n    });\n\n    it(\"throws when failing to resolve lazy route functions on staticHandler.queryRoute()\", async () => {\n      let { queryRoute } = createStaticHandler([\n        {\n          id: \"lazy\",\n          path: \"/lazy\",\n          lazy: async () => {\n            throw new Error(\"LAZY FUNCTION ERROR\");\n          },\n        },\n      ]);\n\n      let err;\n      try {\n        await queryRoute(createRequest(\"/lazy\"));\n      } catch (_err) {\n        err = _err;\n      }\n\n      expect(err?.message).toBe(\"LAZY FUNCTION ERROR\");\n    });\n\n    it(\"throws when failing to resolve lazy route properties on staticHandler.queryRoute()\", async () => {\n      let { queryRoute } = createStaticHandler([\n        {\n          id: \"lazy\",\n          path: \"/lazy\",\n          lazy: {\n            loader: async () => {\n              throw new Error(\"LAZY PROPERTY ERROR\");\n            },\n          },\n        },\n      ]);\n\n      let err;\n      try {\n        await queryRoute(createRequest(\"/lazy\"));\n      } catch (_err) {\n        err = _err;\n      }\n\n      expect(err?.message).toBe(\"LAZY PROPERTY ERROR\");\n    });\n\n    it(\"handles loader errors in lazy route functions on staticHandler.queryRoute()\", async () => {\n      let { queryRoute } = createStaticHandler([\n        {\n          id: \"lazy\",\n          path: \"/lazy\",\n          lazy: async () => {\n            await tick();\n            return {\n              async loader() {\n                throw new Error(\"LAZY LOADER ERROR\");\n              },\n            };\n          },\n        },\n      ]);\n\n      let err;\n      try {\n        await queryRoute(createRequest(\"/lazy\"));\n      } catch (_err) {\n        err = _err;\n      }\n\n      expect(err?.message).toBe(\"LAZY LOADER ERROR\");\n    });\n\n    it(\"handles loader errors in lazy route properties on staticHandler.queryRoute()\", async () => {\n      let { queryRoute } = createStaticHandler([\n        {\n          id: \"lazy\",\n          path: \"/lazy\",\n          lazy: {\n            loader: async () => {\n              await tick();\n              return async () => {\n                throw new Error(\"LAZY LOADER ERROR\");\n              };\n            },\n          },\n        },\n      ]);\n\n      let err;\n      try {\n        await queryRoute(createRequest(\"/lazy\"));\n      } catch (_err) {\n        err = _err;\n      }\n\n      expect(err?.message).toBe(\"LAZY LOADER ERROR\");\n    });\n  });\n});\n"
  },
  {
    "path": "packages/react-router/__tests__/router/mask-test.ts",
    "content": "import { cleanup, setup } from \"./utils/data-router-setup\";\nimport { createFormData } from \"./utils/utils\";\n\ndescribe(\"location masking\", () => {\n  // Detect any failures inside the router navigate code\n  afterEach(() => cleanup());\n\n  it(\"navigates to router location and masks the browser URL\", async () => {\n    let t = setup({\n      routes: [\n        { path: \"/\" },\n        { id: \"gallery\", path: \"/gallery\", loader: true },\n        { id: \"images\", path: \"/images/:id\", loader: true },\n      ],\n    });\n\n    // Navigate to /gallery?photo=123 but mask browser URL as /images/123\n    let A = await t.navigate(\"/gallery?photo=123\", {\n      unstable_mask: \"/images/123\",\n    });\n\n    // The loader should receive the router location URL in the request\n    expect(A.loaders.gallery.stub.mock.calls[0][0].request.url).toMatch(\n      /\\/gallery\\?photo=123$/,\n    );\n\n    await A.loaders.gallery.resolve(\"GALLERY DATA\");\n\n    expect(t.router.state.location).toMatchObject({\n      pathname: \"/gallery\",\n      search: \"?photo=123\",\n      unstable_mask: {\n        pathname: \"/images/123\",\n        search: \"\",\n        hash: \"\",\n      },\n    });\n\n    // The matched routes should be for the router location\n    expect(t.router.state.matches.map((m) => m.route.id)).toEqual([\"gallery\"]);\n\n    // The loader data should be present\n    expect(t.router.state.loaderData.gallery).toBe(\"GALLERY DATA\");\n  });\n\n  it(\"preserves mask on POP navigation\", async () => {\n    let t = setup({\n      routes: [\n        { path: \"/\" },\n        { id: \"gallery\", path: \"/gallery\", loader: true },\n        { id: \"images\", path: \"/images/:id\", loader: true },\n      ],\n    });\n\n    // Navigate to /gallery?photo=123 with mask\n    let A = await t.navigate(\"/gallery?photo=123\", {\n      unstable_mask: \"/images/123\",\n    });\n    await A.loaders.gallery.resolve(\"GALLERY DATA\");\n\n    // Navigate to home\n    await t.navigate(\"/\");\n    expect(t.router.state.location.pathname).toBe(\"/\");\n\n    // Go back - should preserve the mask\n    let promise = t.router.navigate(-1);\n\n    // Wait for navigation to complete\n    await promise;\n\n    expect(t.router.state.location).toMatchObject({\n      pathname: \"/gallery\",\n      search: \"?photo=123\",\n      unstable_mask: {\n        pathname: \"/images/123\",\n        search: \"\",\n        hash: \"\",\n      },\n    });\n\n    expect(t.router.state.matches.map((m) => m.route.id)).toEqual([\"gallery\"]);\n  });\n\n  it(\"supports mask with replace\", async () => {\n    let t = setup({\n      routes: [\n        { path: \"/\" },\n        { id: \"gallery\", path: \"/gallery\", loader: true },\n        { id: \"images\", path: \"/images/:id\", loader: true },\n      ],\n    });\n\n    let A = await t.navigate(\"/gallery?photo=123\", {\n      unstable_mask: \"/images/123\",\n      replace: true,\n    });\n\n    expect(A.loaders.gallery).toBeDefined();\n    await A.loaders.gallery.resolve(\"GALLERY DATA\");\n\n    expect(t.router.state.location).toMatchObject({\n      pathname: \"/gallery\",\n      search: \"?photo=123\",\n      unstable_mask: {\n        pathname: \"/images/123\",\n        search: \"\",\n        hash: \"\",\n      },\n    });\n  });\n\n  it(\"supports mask with hash\", async () => {\n    let t = setup({\n      routes: [\n        { path: \"/\" },\n        { id: \"gallery\", path: \"/gallery\", loader: true },\n        { id: \"images\", path: \"/images/:id\", loader: true },\n      ],\n    });\n\n    let A = await t.navigate(\"/gallery?photo=123#header\", {\n      unstable_mask: \"/images/123#preview\",\n    });\n\n    expect(A.loaders.gallery).toBeDefined();\n\n    // The loader should receive the router location URL (hash is not preserved in Request URL)\n    expect(A.loaders.gallery.stub.mock.calls[0][0].request.url).toMatch(\n      /\\/gallery\\?photo=123$/,\n    );\n\n    await A.loaders.gallery.resolve(\"GALLERY DATA\");\n\n    expect(t.router.state.location).toMatchObject({\n      pathname: \"/gallery\",\n      search: \"?photo=123\",\n      hash: \"#header\",\n      unstable_mask: {\n        pathname: \"/images/123\",\n        search: \"\",\n        hash: \"#preview\",\n      },\n    });\n  });\n\n  it(\"supports mask with state\", async () => {\n    let t = setup({\n      routes: [\n        { path: \"/\" },\n        { id: \"gallery\", path: \"/gallery\", loader: true },\n        { id: \"images\", path: \"/images/:id\", loader: true },\n      ],\n    });\n\n    let A = await t.navigate(\"/gallery?photo=123\", {\n      unstable_mask: \"/images/123\",\n      state: { customData: \"test\" },\n    });\n\n    await A.loaders.gallery.resolve(\"GALLERY DATA\");\n\n    expect(t.router.state.location).toMatchObject({\n      pathname: \"/gallery\",\n      search: \"?photo=123\",\n      state: { customData: \"test\" },\n      unstable_mask: {\n        pathname: \"/images/123\",\n        search: \"\",\n        hash: \"\",\n      },\n    });\n  });\n\n  it(\"supports mask with action submission\", async () => {\n    let t = setup({\n      routes: [\n        { path: \"/\" },\n        { id: \"gallery\", path: \"/gallery\", action: true, loader: true },\n        { id: \"images\", path: \"/images/:id\", action: true },\n      ],\n    });\n\n    let A = await t.navigate(\"/gallery?photo=123\", {\n      unstable_mask: \"/images/123\",\n      formMethod: \"post\",\n      formData: createFormData({ test: \"value\" }),\n    });\n\n    // Should call the gallery action, not the images action\n    expect(A.actions.gallery).toBeDefined();\n    expect(A.actions.images).toBeUndefined();\n\n    // The action should receive the router location URL\n    expect(A.actions.gallery.stub.mock.calls[0][0].request.url).toMatch(\n      /\\/gallery\\?photo=123$/,\n    );\n\n    await A.actions.gallery.resolve(\"ACTION DATA\");\n\n    // Should then call the gallery loader\n    expect(A.loaders.gallery).toBeDefined();\n    await A.loaders.gallery.resolve(\"LOADER DATA\");\n\n    expect(t.router.state.location).toMatchObject({\n      pathname: \"/gallery\",\n      search: \"?photo=123\",\n      unstable_mask: {\n        pathname: \"/images/123\",\n        search: \"\",\n        hash: \"\",\n      },\n    });\n  });\n\n  it(\"handles mask with relative paths\", async () => {\n    let t = setup({\n      routes: [\n        {\n          path: \"/\",\n          children: [\n            { id: \"gallery\", path: \"gallery\", loader: true },\n            { id: \"images\", path: \"images/:id\", loader: true },\n          ],\n        },\n      ],\n    });\n\n    let A = await t.navigate(\"/gallery?photo=123\", {\n      unstable_mask: \"/images/123\",\n    });\n\n    expect(A.loaders.gallery).toBeDefined();\n    await A.loaders.gallery.resolve(\"GALLERY DATA\");\n\n    expect(t.router.state.location).toMatchObject({\n      pathname: \"/gallery\",\n      search: \"?photo=123\",\n      unstable_mask: {\n        pathname: \"/images/123\",\n        search: \"\",\n        hash: \"\",\n      },\n    });\n    expect(t.router.state.matches.map((m) => m.route.id)).toEqual([\n      \"route-0\",\n      \"gallery\",\n    ]);\n  });\n\n  it(\"handles 404 on router location\", async () => {\n    let t = setup({\n      routes: [\n        { path: \"/\" },\n        { id: \"images\", path: \"/images/:id\", loader: true },\n      ],\n    });\n\n    // Navigate to non-existent route with mask\n    await t.navigate(\"/nonexistent\", {\n      unstable_mask: \"/images/123\",\n    });\n\n    // Should get a 404 error\n    expect(t.router.state.errors).toBeDefined();\n    expect(t.router.state.location).toMatchObject({\n      pathname: \"/nonexistent\",\n      unstable_mask: {\n        pathname: \"/images/123\",\n        search: \"\",\n        hash: \"\",\n      },\n    });\n  });\n});\n"
  },
  {
    "path": "packages/react-router/__tests__/router/memory-test.ts",
    "content": "/* eslint-disable jest/expect-expect */\n\nimport type { MemoryHistory } from \"../../lib/router/history\";\nimport { createMemoryHistory } from \"../../lib/router/history\";\n\nimport Listen from \"./TestSequences/Listen\";\nimport InitialLocationHasKey from \"./TestSequences/InitialLocationHasKey\";\nimport PushNewLocation from \"./TestSequences/PushNewLocation\";\nimport PushSamePath from \"./TestSequences/PushSamePath\";\nimport PushState from \"./TestSequences/PushState\";\nimport PushMissingPathname from \"./TestSequences/PushMissingPathname\";\nimport PushRelativePathnameWarning from \"./TestSequences/PushRelativePathnameWarning\";\nimport ReplaceNewLocation from \"./TestSequences/ReplaceNewLocation\";\nimport ReplaceSamePath from \"./TestSequences/ReplaceSamePath\";\nimport ReplaceState from \"./TestSequences/ReplaceState\";\nimport EncodedReservedCharacters from \"./TestSequences/EncodedReservedCharacters\";\nimport GoBack from \"./TestSequences/GoBack\";\nimport GoForward from \"./TestSequences/GoForward\";\nimport ListenPopOnly from \"./TestSequences/ListenPopOnly\";\n\ndescribe(\"a memory history\", () => {\n  let history: MemoryHistory;\n\n  beforeEach(() => {\n    history = createMemoryHistory();\n  });\n\n  it(\"has an index property\", () => {\n    expect(typeof history.index).toBe(\"number\");\n  });\n\n  it(\"knows how to create hrefs\", () => {\n    const href = history.createHref({\n      pathname: \"/the/path\",\n      search: \"?the=query\",\n      hash: \"#the-hash\",\n    });\n\n    expect(href).toEqual(\"/the/path?the=query#the-hash\");\n  });\n\n  it(\"knows how to create hrefs from strings\", () => {\n    const href = history.createHref(\"/the/path?the=query#the-hash\");\n    expect(href).toEqual(\"/the/path?the=query#the-hash\");\n  });\n\n  it(\"does not encode the generated path\", () => {\n    const encodedHref = history.createHref({\n      pathname: \"/%23abc\",\n    });\n    expect(encodedHref).toEqual(\"/%23abc\");\n\n    const unencodedHref = history.createHref({\n      pathname: \"/#abc\",\n    });\n    expect(unencodedHref).toEqual(\"/#abc\");\n  });\n\n  describe(\"the initial location\", () => {\n    it(\"has a key\", () => {\n      InitialLocationHasKey(history);\n    });\n  });\n\n  describe(\"listen\", () => {\n    it(\"does not immediately call listeners\", () => {\n      Listen(history);\n    });\n\n    it(\"calls listeners only for POP actions\", () => {\n      ListenPopOnly(history);\n    });\n  });\n\n  describe(\"push\", () => {\n    it(\"pushes the new location\", () => {\n      PushNewLocation(history);\n    });\n\n    it(\"pushes the same location\", async () => {\n      await PushSamePath(history);\n    });\n\n    it(\"pushes with state\", () => {\n      PushState(history);\n    });\n\n    it(\"reuses the current location pathname\", () => {\n      PushMissingPathname(history);\n    });\n\n    it(\"issues a warning on relative pathnames\", () => {\n      PushRelativePathnameWarning(history);\n    });\n  });\n\n  describe(\"replace\", () => {\n    it(\"replaces with a new location\", () => {\n      ReplaceNewLocation(history);\n    });\n  });\n\n  describe(\"replace the same path\", () => {\n    it(\"replaces with the same location\", () => {\n      ReplaceSamePath(history);\n    });\n\n    it(\"replaces the state\", () => {\n      ReplaceState(history);\n    });\n  });\n\n  describe(\"location created with encoded/unencoded reserved characters\", () => {\n    it(\"produces different location objects\", () => {\n      EncodedReservedCharacters(history);\n    });\n  });\n\n  describe(\"go\", () => {\n    it(\"goes back\", async () => {\n      await GoBack(history);\n    });\n    it(\"goes forward\", async () => {\n      await GoForward(history);\n    });\n  });\n});\n\ndescribe(\"a memory history without an onPopState callback\", () => {\n  it(\"fails gracefully on go() calls\", () => {\n    let history = createMemoryHistory();\n    history.push(\"/page1\");\n    history.push(\"/page2\");\n    expect(history.location.pathname).toBe(\"/page2\");\n    history.go(-2);\n    expect(history.location.pathname).toBe(\"/\");\n    history.go(1);\n    expect(history.location.pathname).toBe(\"/page1\");\n  });\n});\n\ndescribe(\"a memory history with some initial entries\", () => {\n  it(\"clamps the initial index to a valid value\", () => {\n    let history = createMemoryHistory({\n      initialEntries: [\"/one\", \"/two\", \"/three\"],\n      initialIndex: 3, // invalid\n    });\n\n    expect(history.index).toBe(2);\n  });\n\n  it(\"starts at the last entry by default\", () => {\n    let history = createMemoryHistory({\n      initialEntries: [\"/one\", \"/two\", \"/three\"],\n    });\n\n    expect(history.index).toBe(2);\n    expect(history.location).toMatchObject({\n      pathname: \"/three\",\n      search: \"\",\n      hash: \"\",\n      state: null,\n      key: expect.any(String),\n    });\n\n    history.go(-1);\n    expect(history.index).toBe(1);\n    expect(history.location).toMatchObject({\n      pathname: \"/two\",\n      search: \"\",\n      hash: \"\",\n      state: null,\n      key: expect.any(String),\n    });\n  });\n\n  it(\"allows initial entries to have state and keys\", () => {\n    let history = createMemoryHistory({\n      initialEntries: [\n        { pathname: \"/one\", state: \"1\", key: \"1\" },\n        { pathname: \"/two\", state: \"2\", key: \"2\" },\n      ],\n    });\n\n    expect(history.index).toBe(1);\n    expect(history.location).toMatchObject({\n      pathname: \"/two\",\n      search: \"\",\n      hash: \"\",\n      state: \"2\",\n      key: \"2\",\n    });\n\n    history.go(-1);\n    expect(history.index).toBe(0);\n    expect(history.location).toMatchObject({\n      pathname: \"/one\",\n      search: \"\",\n      hash: \"\",\n      state: \"1\",\n      key: \"1\",\n    });\n  });\n});\n"
  },
  {
    "path": "packages/react-router/__tests__/router/navigation-blocking-test.ts",
    "content": "import { createMemoryHistory } from \"../../lib/router/history\";\nimport type { Router } from \"../../lib/router/router\";\nimport { createRouter } from \"../../lib/router/router\";\n\nconst LOADER_LATENCY_MS = 100;\nconst routes = [\n  { path: \"/\" },\n  {\n    path: \"/about\",\n    loader: () => sleep(LOADER_LATENCY_MS),\n  },\n  { path: \"/contact\" },\n  { path: \"/help\" },\n];\n\ndescribe(\"navigation blocking\", () => {\n  let router: Router;\n  it(\"initializes an 'unblocked' blocker\", () => {\n    router = createRouter({\n      history: createMemoryHistory({\n        initialEntries: [\"/\"],\n        initialIndex: 0,\n      }),\n      routes,\n    });\n    router.initialize();\n\n    let fn = () => true;\n    router.getBlocker(\"KEY\", fn);\n    expect(router.getBlocker(\"KEY\", fn)).toEqual({\n      state: \"unblocked\",\n      proceed: undefined,\n      reset: undefined,\n      location: undefined,\n    });\n  });\n\n  describe(\"on history push\", () => {\n    let initialEntries = [\"/\", \"/about\"];\n    let initialIndex = 0;\n    beforeEach(() => {\n      router = createRouter({\n        history: createMemoryHistory({\n          initialEntries,\n          initialIndex,\n        }),\n        routes,\n      });\n      router.initialize();\n    });\n\n    describe(\"blocker returns false\", () => {\n      let fn = () => false;\n      it(\"navigates\", async () => {\n        router.getBlocker(\"KEY\", fn);\n        await router.navigate(\"/about\");\n        expect(router.state.location.pathname).toBe(\"/about\");\n      });\n\n      it(\"gets an 'unblocked' blocker after navigation starts\", async () => {\n        router.getBlocker(\"KEY\", fn);\n        router.navigate(\"/about\");\n        expect(router.getBlocker(\"KEY\", fn)).toEqual({\n          state: \"unblocked\",\n          proceed: undefined,\n          reset: undefined,\n          location: undefined,\n        });\n      });\n\n      it(\"gets an 'unblocked' blocker after navigation completes\", async () => {\n        router.getBlocker(\"KEY\", fn);\n        await router.navigate(\"/about\");\n        expect(router.getBlocker(\"KEY\", fn)).toEqual({\n          state: \"unblocked\",\n          proceed: undefined,\n          reset: undefined,\n          location: undefined,\n        });\n      });\n    });\n\n    describe(\"blocker returns true\", () => {\n      let fn = () => true;\n\n      it(\"does not navigate\", async () => {\n        router.getBlocker(\"KEY\", fn);\n        await router.navigate(\"/about\");\n        expect(router.state.location.pathname).toBe(\n          initialEntries[initialIndex],\n        );\n      });\n\n      it(\"gets a 'blocked' blocker after navigation starts\", async () => {\n        router.getBlocker(\"KEY\", fn);\n        router.navigate(\"/about\");\n        expect(router.getBlocker(\"KEY\", fn)).toEqual({\n          state: \"blocked\",\n          proceed: expect.any(Function),\n          reset: expect.any(Function),\n          location: expect.any(Object),\n        });\n      });\n\n      it(\"gets a 'blocked' blocker after navigation promise resolves\", async () => {\n        router.getBlocker(\"KEY\", fn);\n        await router.navigate(\"/about\");\n        expect(router.getBlocker(\"KEY\", fn)).toEqual({\n          state: \"blocked\",\n          proceed: expect.any(Function),\n          reset: expect.any(Function),\n          location: expect.any(Object),\n        });\n      });\n    });\n\n    describe(\"proceeds from blocked state\", () => {\n      let fn = () => true;\n      it(\"gets a 'proceeding' blocker after proceeding navigation starts\", async () => {\n        router.getBlocker(\"KEY\", fn);\n        await router.navigate(\"/about\");\n        router.getBlocker(\"KEY\", fn).proceed?.();\n        expect(router.getBlocker(\"KEY\", fn)).toEqual({\n          state: \"proceeding\",\n          proceed: undefined,\n          reset: undefined,\n          location: expect.any(Object),\n        });\n      });\n\n      it(\"gets an 'unblocked' blocker after proceeding navigation completes\", async () => {\n        router.getBlocker(\"KEY\", fn);\n        await router.navigate(\"/about\");\n        router.getBlocker(\"KEY\", fn).proceed?.();\n        await sleep(LOADER_LATENCY_MS);\n        expect(router.getBlocker(\"KEY\", fn)).toEqual({\n          state: \"unblocked\",\n          proceed: undefined,\n          reset: undefined,\n          location: undefined,\n        });\n      });\n\n      it(\"navigates after proceeding navigation completes\", async () => {\n        router.getBlocker(\"KEY\", fn);\n        await router.navigate(\"/about\");\n        router.getBlocker(\"KEY\", fn).proceed?.();\n        await sleep(LOADER_LATENCY_MS);\n        expect(router.state.location.pathname).toBe(\"/about\");\n      });\n    });\n\n    describe(\"resets from blocked state\", () => {\n      let fn = () => true;\n      it(\"gets an 'unblocked' blocker after resetting navigation\", async () => {\n        router.getBlocker(\"KEY\", fn);\n        await router.navigate(\"/about\");\n        router.getBlocker(\"KEY\", fn).reset?.();\n        expect(router.getBlocker(\"KEY\", fn)).toEqual({\n          state: \"unblocked\",\n          proceed: undefined,\n          reset: undefined,\n          location: undefined,\n        });\n      });\n\n      it(\"stays at current location after resetting\", async () => {\n        router.getBlocker(\"KEY\", fn);\n        let pathnameBeforeNavigation = router.state.location.pathname;\n        await router.navigate(\"/about\");\n        router.getBlocker(\"KEY\", fn).reset?.();\n\n        // wait for '/about' loader so we catch failure if navigation proceeds\n        await sleep(LOADER_LATENCY_MS);\n        expect(router.state.location.pathname).toBe(pathnameBeforeNavigation);\n      });\n    });\n  });\n\n  describe(\"on history replace\", () => {\n    let initialEntries = [\"/\", \"/about\"];\n    let initialIndex = 0;\n    beforeEach(() => {\n      router = createRouter({\n        history: createMemoryHistory({\n          initialEntries,\n          initialIndex,\n        }),\n        routes,\n      });\n      router.initialize();\n    });\n\n    describe(\"blocker returns false\", () => {\n      let fn = () => false;\n      it(\"navigates\", async () => {\n        router.getBlocker(\"KEY\", fn);\n        await router.navigate(\"/about\", { replace: true });\n        expect(router.state.location.pathname).toBe(\"/about\");\n      });\n\n      it(\"gets an 'unblocked' blocker after navigation starts\", async () => {\n        router.getBlocker(\"KEY\", fn);\n        router.navigate(\"/about\", { replace: true });\n        expect(router.getBlocker(\"KEY\", fn)).toEqual({\n          state: \"unblocked\",\n          proceed: undefined,\n          reset: undefined,\n          location: undefined,\n        });\n      });\n\n      it(\"gets an 'unblocked' blocker after navigation completes\", async () => {\n        router.getBlocker(\"KEY\", fn);\n        await router.navigate(\"/about\", { replace: true });\n        expect(router.getBlocker(\"KEY\", fn)).toEqual({\n          state: \"unblocked\",\n          proceed: undefined,\n          reset: undefined,\n          location: undefined,\n        });\n      });\n    });\n\n    describe(\"blocker returns true\", () => {\n      let fn = () => true;\n\n      it(\"does not navigate\", async () => {\n        router.getBlocker(\"KEY\", fn);\n        await router.navigate(\"/about\", { replace: true });\n        expect(router.state.location.pathname).toBe(\n          initialEntries[initialIndex],\n        );\n      });\n\n      it(\"gets a 'blocked' blocker after navigation starts\", async () => {\n        router.getBlocker(\"KEY\", fn);\n        router.navigate(\"/about\", { replace: true });\n        expect(router.getBlocker(\"KEY\", fn)).toEqual({\n          state: \"blocked\",\n          proceed: expect.any(Function),\n          reset: expect.any(Function),\n          location: expect.any(Object),\n        });\n      });\n\n      it(\"gets a 'blocked' blocker after navigation promise resolves\", async () => {\n        router.getBlocker(\"KEY\", fn);\n        await router.navigate(\"/about\", { replace: true });\n        expect(router.getBlocker(\"KEY\", fn)).toEqual({\n          state: \"blocked\",\n          proceed: expect.any(Function),\n          reset: expect.any(Function),\n          location: expect.any(Object),\n        });\n      });\n    });\n\n    describe(\"proceeds from blocked state\", () => {\n      let fn = () => true;\n      it(\"gets a 'proceeding' blocker after proceeding navigation starts\", async () => {\n        router.getBlocker(\"KEY\", fn);\n        await router.navigate(\"/about\", { replace: true });\n        router.getBlocker(\"KEY\", fn).proceed?.();\n        expect(router.getBlocker(\"KEY\", fn)).toEqual({\n          state: \"proceeding\",\n          proceed: undefined,\n          reset: undefined,\n          location: expect.any(Object),\n        });\n      });\n\n      it(\"gets an 'unblocked' blocker after proceeding navigation completes\", async () => {\n        router.getBlocker(\"KEY\", fn);\n        await router.navigate(\"/about\", { replace: true });\n        router.getBlocker(\"KEY\", fn).proceed?.();\n        await sleep(LOADER_LATENCY_MS);\n        expect(router.getBlocker(\"KEY\", fn)).toEqual({\n          state: \"unblocked\",\n          proceed: undefined,\n          reset: undefined,\n          location: undefined,\n        });\n      });\n\n      it(\"navigates after proceeding navigation completes\", async () => {\n        router.getBlocker(\"KEY\", fn);\n        await router.navigate(\"/about\", { replace: true });\n        router.getBlocker(\"KEY\", fn).proceed?.();\n        await sleep(LOADER_LATENCY_MS);\n        expect(router.state.location.pathname).toBe(\"/about\");\n      });\n\n      it(\"replaces the current history entry after proceeding completes\", async () => {\n        router.getBlocker(\"KEY\", fn);\n        let historyLengthBeforeNavigation = window.history.length;\n        await router.navigate(\"/about\", { replace: true });\n        router.getBlocker(\"KEY\", fn).proceed?.();\n        await sleep(LOADER_LATENCY_MS);\n        expect(window.history.length).toBe(historyLengthBeforeNavigation);\n      });\n    });\n\n    describe(\"resets from blocked state\", () => {\n      let fn = () => true;\n      it(\"gets an 'unblocked' blocker after resetting navigation\", async () => {\n        router.getBlocker(\"KEY\", fn);\n        await router.navigate(\"/about\", { replace: true });\n        router.getBlocker(\"KEY\", fn).reset?.();\n        expect(router.getBlocker(\"KEY\", fn)).toEqual({\n          state: \"unblocked\",\n          proceed: undefined,\n          reset: undefined,\n          location: undefined,\n        });\n      });\n\n      it(\"stays at current location after resetting\", async () => {\n        router.getBlocker(\"KEY\", fn);\n        let pathnameBeforeNavigation = router.state.location.pathname;\n        await router.navigate(\"/about\", { replace: true });\n        router.getBlocker(\"KEY\", fn).reset?.();\n\n        // wait for '/about' loader so we catch failure if navigation proceeds\n        await sleep(LOADER_LATENCY_MS);\n        expect(router.state.location.pathname).toBe(pathnameBeforeNavigation);\n      });\n    });\n  });\n\n  describe(\"on history pop\", () => {\n    let initialEntries = [\"/\", \"/about\", \"/contact\", \"/help\"];\n    let initialIndex = 1;\n    beforeEach(() => {\n      router = createRouter({\n        history: createMemoryHistory({\n          initialEntries,\n          initialIndex,\n        }),\n        routes,\n      });\n      router.initialize();\n    });\n\n    describe(\"blocker returns false\", () => {\n      let fn = () => false;\n      it(\"navigates\", async () => {\n        router.getBlocker(\"KEY\", fn);\n        await router.navigate(-1);\n        expect(router.state.location.pathname).toBe(\n          initialEntries[initialIndex - 1],\n        );\n      });\n\n      it(\"gets an 'unblocked' blocker after navigation starts\", async () => {\n        router.getBlocker(\"KEY\", fn);\n        router.navigate(-1);\n        expect(router.getBlocker(\"KEY\", fn)).toEqual({\n          state: \"unblocked\",\n          proceed: undefined,\n          reset: undefined,\n          location: undefined,\n        });\n      });\n\n      it(\"gets an 'unblocked' blocker after navigation completes\", async () => {\n        router.getBlocker(\"KEY\", fn);\n        await router.navigate(-1);\n        expect(router.getBlocker(\"KEY\", fn)).toEqual({\n          state: \"unblocked\",\n          proceed: undefined,\n          reset: undefined,\n          location: undefined,\n        });\n      });\n    });\n\n    describe(\"blocker returns true\", () => {\n      let fn = () => true;\n\n      it(\"does not navigate\", async () => {\n        router.getBlocker(\"KEY\", fn);\n        await router.navigate(-1);\n        expect(router.state.location.pathname).toBe(\n          initialEntries[initialIndex],\n        );\n      });\n\n      it(\"gets a 'blocked' blocker after navigation starts\", async () => {\n        router.getBlocker(\"KEY\", fn);\n        router.navigate(-1);\n        expect(router.getBlocker(\"KEY\", fn)).toEqual({\n          state: \"blocked\",\n          proceed: expect.any(Function),\n          reset: expect.any(Function),\n          location: expect.any(Object),\n        });\n      });\n\n      it(\"gets a 'blocked' blocker after navigation promise resolves\", async () => {\n        router.getBlocker(\"KEY\", fn);\n        await router.navigate(-1);\n        expect(router.getBlocker(\"KEY\", fn)).toEqual({\n          state: \"blocked\",\n          proceed: expect.any(Function),\n          reset: expect.any(Function),\n          location: expect.any(Object),\n        });\n      });\n    });\n\n    describe(\"proceeds from blocked state\", () => {\n      let fn = () => true;\n\n      // we want to navigate so that `/about` is the previous entry in the\n      // stack here since it has a loader that won't resolve immediately\n      let initialEntries = [\"/\", \"/about\", \"/contact\"];\n      let initialIndex = 2;\n      beforeEach(() => {\n        router = createRouter({\n          history: createMemoryHistory({\n            initialEntries,\n            initialIndex,\n          }),\n          routes,\n        });\n        router.initialize();\n      });\n\n      it(\"gets a 'proceeding' blocker after proceeding navigation starts\", async () => {\n        router.getBlocker(\"KEY\", fn);\n        await router.navigate(-1);\n        router.getBlocker(\"KEY\", fn).proceed?.();\n        expect(router.getBlocker(\"KEY\", fn)).toEqual({\n          state: \"proceeding\",\n          proceed: undefined,\n          reset: undefined,\n          location: expect.any(Object),\n        });\n      });\n\n      it(\"gets an 'unblocked' blocker after proceeding navigation completes\", async () => {\n        router.getBlocker(\"KEY\", fn);\n        await router.navigate(-1);\n        router.getBlocker(\"KEY\", fn).proceed?.();\n        await sleep(LOADER_LATENCY_MS + 10);\n        expect(router.getBlocker(\"KEY\", fn)).toEqual({\n          state: \"unblocked\",\n          proceed: undefined,\n          reset: undefined,\n          location: undefined,\n        });\n      });\n\n      it(\"navigates after proceeding navigation completes\", async () => {\n        router.getBlocker(\"KEY\", fn);\n        await router.navigate(-1);\n        router.getBlocker(\"KEY\", fn).proceed?.();\n        await sleep(LOADER_LATENCY_MS + 10);\n        expect(router.state.location.pathname).toBe(\"/about\");\n      });\n    });\n\n    describe(\"resets from blocked state\", () => {\n      let fn = () => true;\n      it(\"gets an 'unblocked' blocker after resetting navigation\", async () => {\n        router.getBlocker(\"KEY\", fn);\n        await router.navigate(-1);\n        router.getBlocker(\"KEY\", fn).reset?.();\n        expect(router.getBlocker(\"KEY\", fn)).toEqual({\n          state: \"unblocked\",\n          proceed: undefined,\n          reset: undefined,\n          location: undefined,\n        });\n      });\n\n      it(\"stays at current location after resetting\", async () => {\n        router.getBlocker(\"KEY\", fn);\n        let pathnameBeforeNavigation = router.state.location.pathname;\n        await router.navigate(-1);\n        router.getBlocker(\"KEY\", fn).reset?.();\n\n        // wait for '/about' loader so we catch failure if navigation proceeds\n        await sleep(LOADER_LATENCY_MS);\n        expect(router.state.location.pathname).toBe(pathnameBeforeNavigation);\n      });\n    });\n  });\n});\n\nfunction sleep(n: number = 500) {\n  return new Promise<void>((r) => setTimeout(r, n));\n}\n"
  },
  {
    "path": "packages/react-router/__tests__/router/navigation-test.ts",
    "content": "import { createBrowserRouter } from \"../../lib/dom/lib\";\nimport type { HydrationState } from \"../../lib/router/router\";\nimport getWindow from \"../utils/getWindow\";\nimport { cleanup, setup } from \"./utils/data-router-setup\";\nimport { createFormData } from \"./utils/utils\";\n\nfunction initializeTest(init?: {\n  url?: string;\n  hydrationData?: HydrationState;\n}) {\n  return setup({\n    routes: [\n      {\n        path: \"\",\n        id: \"root\",\n        hasErrorBoundary: true,\n        loader: true,\n        children: [\n          {\n            path: \"/\",\n            id: \"index\",\n            loader: true,\n            action: true,\n          },\n          {\n            path: \"/foo\",\n            id: \"foo\",\n            loader: true,\n            action: true,\n          },\n          {\n            path: \"/foo/bar\",\n            id: \"foobar\",\n            loader: true,\n            action: true,\n          },\n          {\n            path: \"/bar\",\n            id: \"bar\",\n            loader: true,\n            action: true,\n          },\n          {\n            path: \"/baz\",\n            id: \"baz\",\n            loader: true,\n            action: true,\n          },\n          {\n            path: \"/p/:param\",\n            id: \"param\",\n            loader: true,\n            action: true,\n          },\n        ],\n      },\n    ],\n    hydrationData: init?.hydrationData || {\n      loaderData: { root: \"ROOT\", index: \"INDEX\" },\n    },\n    ...(init?.url ? { initialEntries: [init.url] } : {}),\n  });\n}\n\ndescribe(\"navigations\", () => {\n  afterEach(() => cleanup());\n\n  describe(\"normal navigation\", () => {\n    it(\"fetches data on navigation\", async () => {\n      let t = initializeTest();\n      let A = await t.navigate(\"/foo\");\n      await A.loaders.foo.resolve(\"FOO\");\n      expect(t.router.state.loaderData).toMatchInlineSnapshot(`\n        {\n          \"foo\": \"FOO\",\n          \"root\": \"ROOT\",\n        }\n      `);\n    });\n\n    it(\"allows `null` as a valid data value\", async () => {\n      let t = initializeTest();\n      let A = await t.navigate(\"/foo\");\n      await A.loaders.foo.resolve(null);\n      expect(t.router.state.loaderData).toMatchObject({\n        root: \"ROOT\",\n        foo: null,\n      });\n    });\n\n    it(\"unwraps non-redirect json Responses\", async () => {\n      let t = initializeTest();\n      let A = await t.navigate(\"/foo\");\n      await A.loaders.foo.resolve(\n        new Response(JSON.stringify({ key: \"value\" }), {\n          status: 200,\n          headers: {\n            \"Content-Type\": \"application/json\",\n          },\n        }),\n      );\n      expect(t.router.state.loaderData).toMatchObject({\n        root: \"ROOT\",\n        foo: { key: \"value\" },\n      });\n    });\n\n    it(\"unwraps non-redirect json Responses (Response.json() helper)\", async () => {\n      let t = initializeTest();\n      let A = await t.navigate(\"/foo\");\n      await A.loaders.foo.resolve(\n        Response.json({ key: \"value\" }, { status: 200 }),\n      );\n      expect(t.router.state.loaderData).toMatchObject({\n        root: \"ROOT\",\n        foo: { key: \"value\" },\n      });\n    });\n\n    // See: https://github.com/remix-run/react-router/issues/11145\n    it(\"does not attempt to deserialize empty json responses\", async () => {\n      let t = initializeTest();\n      let A = await t.navigate(\"/foo\");\n      await A.loaders.foo.resolve(\n        new Response(null, {\n          headers: {\n            \"Content-Type\": \"application/json\",\n          },\n        }),\n      );\n      expect(t.router.state.errors).toBeNull();\n      expect(t.router.state.loaderData).toMatchObject({\n        root: \"ROOT\",\n        foo: null,\n      });\n    });\n\n    it(\"unwraps non-redirect text Responses\", async () => {\n      let t = initializeTest();\n      let A = await t.navigate(\"/foo\");\n      await A.loaders.foo.resolve(new Response(\"FOO\", { status: 200 }));\n      expect(t.router.state.loaderData).toMatchObject({\n        root: \"ROOT\",\n        foo: \"FOO\",\n      });\n    });\n\n    it(\"handles errors when unwrapping Responses\", async () => {\n      let t = setup({\n        routes: [\n          {\n            path: \"/\",\n            children: [\n              {\n                id: \"foo\",\n                path: \"foo\",\n                hasErrorBoundary: true,\n                loader: true,\n              },\n            ],\n          },\n        ],\n      });\n      let A = await t.navigate(\"/foo\");\n      await A.loaders.foo.resolve(\n        // Invalid JSON\n        new Response('{\"key\":\"value\"}}}}}', {\n          status: 200,\n          headers: {\n            \"Content-Type\": \"application/json\",\n          },\n        }),\n      );\n      expect(t.router.state.loaderData).toEqual({});\n\n      expect(t.router.state.errors?.foo).toBeInstanceOf(SyntaxError);\n      expect(t.router.state.errors?.foo.message).toContain(\n        \"Unexpected non-whitespace character after JSON at position 15\",\n      );\n    });\n\n    it(\"bubbles errors when unwrapping Responses\", async () => {\n      let t = setup({\n        routes: [\n          {\n            id: \"root\",\n            path: \"/\",\n            hasErrorBoundary: true,\n            children: [\n              {\n                id: \"foo\",\n                path: \"foo\",\n                loader: true,\n              },\n            ],\n          },\n        ],\n      });\n      let A = await t.navigate(\"/foo\");\n      await A.loaders.foo.resolve(\n        // Invalid JSON\n        new Response('{\"key\":\"value\"}}}}}', {\n          status: 200,\n          headers: {\n            \"Content-Type\": \"application/json\",\n          },\n        }),\n      );\n      expect(t.router.state.loaderData).toEqual({});\n\n      expect(t.router.state.errors?.root).toBeInstanceOf(SyntaxError);\n      expect(t.router.state.errors?.root.message).toContain(\n        \"Unexpected non-whitespace character after JSON at position 15\",\n      );\n    });\n\n    it(\"does not fetch unchanging layout data\", async () => {\n      let t = initializeTest();\n      let A = await t.navigate(\"/foo\");\n      await A.loaders.foo.resolve(\"FOO\");\n      expect(A.loaders.root.stub.mock.calls.length).toBe(0);\n      expect(t.router.state.loaderData.root).toBe(\"ROOT\");\n    });\n\n    it(\"reloads all routes on search changes\", async () => {\n      let t = initializeTest();\n      let A = await t.navigate(\"/foo?q=1\");\n      await A.loaders.root.resolve(\"ROOT1\");\n      await A.loaders.foo.resolve(\"1\");\n      expect(A.loaders.root.stub.mock.calls.length).toBe(1);\n      expect(t.router.state.loaderData).toMatchObject({\n        root: \"ROOT1\",\n        foo: \"1\",\n      });\n\n      let B = await t.navigate(\"/foo?q=2\");\n      await B.loaders.root.resolve(\"ROOT2\");\n      await B.loaders.foo.resolve(\"2\");\n      expect(B.loaders.root.stub.mock.calls.length).toBe(1);\n      expect(t.router.state.loaderData).toMatchObject({\n        root: \"ROOT2\",\n        foo: \"2\",\n      });\n    });\n\n    it(\"does not reload all routes when search does not change\", async () => {\n      let t = initializeTest();\n      expect(t.router.state.loaderData).toMatchObject({\n        root: \"ROOT\",\n      });\n\n      let A = await t.navigate(\"/foo?q=1\");\n      await A.loaders.root.resolve(\"ROOT1\");\n      await A.loaders.foo.resolve(\"1\");\n      expect(A.loaders.root.stub.mock.calls.length).toBe(1);\n      expect(t.router.state.loaderData).toMatchObject({\n        root: \"ROOT1\",\n        foo: \"1\",\n      });\n\n      let B = await t.navigate(\"/foo/bar?q=1\");\n      await B.loaders.foobar.resolve(\"2\");\n      expect(B.loaders.root.stub.mock.calls.length).toBe(0);\n\n      expect(t.router.state.loaderData).toMatchObject({\n        root: \"ROOT1\",\n        foobar: \"2\",\n      });\n    });\n\n    it(\"reloads only routes with changed params\", async () => {\n      let t = initializeTest();\n\n      let A = await t.navigate(\"/p/one\");\n      await A.loaders.param.resolve(\"one\");\n      expect(A.loaders.root.stub.mock.calls.length).toBe(0);\n      expect(t.router.state.loaderData).toMatchObject({\n        root: \"ROOT\",\n        param: \"one\",\n      });\n\n      let B = await t.navigate(\"/p/two\");\n      await B.loaders.param.resolve(\"two\");\n      expect(B.loaders.root.stub.mock.calls.length).toBe(0);\n      expect(t.router.state.loaderData).toMatchObject({\n        root: \"ROOT\",\n        param: \"two\",\n      });\n    });\n\n    it(\"reloads all routes on refresh\", async () => {\n      let t = initializeTest();\n      let url = \"/p/same\";\n\n      let A = await t.navigate(url);\n      await A.loaders.param.resolve(\"1\");\n      expect(A.loaders.root.stub.mock.calls.length).toBe(0);\n      expect(t.router.state.loaderData).toMatchObject({\n        root: \"ROOT\",\n        param: \"1\",\n      });\n\n      let B = await t.navigate(url);\n      await B.loaders.root.resolve(\"ROOT2\");\n      await B.loaders.param.resolve(\"2\");\n      expect(B.loaders.root.stub.mock.calls.length).toBe(1);\n      expect(t.router.state.loaderData).toMatchObject({\n        root: \"ROOT2\",\n        param: \"2\",\n      });\n    });\n\n    it(\"does not run loaders on hash change only navigations (no hash -> hash)\", async () => {\n      let t = initializeTest();\n      expect(t.router.state.loaderData).toMatchObject({ root: \"ROOT\" });\n      let A = await t.navigate(\"/#bar\");\n      expect(A.loaders.root.stub.mock.calls.length).toBe(0);\n      expect(t.router.state.loaderData).toMatchObject({ root: \"ROOT\" });\n    });\n\n    it(\"does not run loaders on hash change only navigations (hash -> new hash)\", async () => {\n      let t = initializeTest({ url: \"/#foo\" });\n      expect(t.router.state.loaderData).toMatchObject({ root: \"ROOT\" });\n      let A = await t.navigate(\"/#bar\");\n      expect(A.loaders.root.stub.mock.calls.length).toBe(0);\n      expect(t.router.state.loaderData).toMatchObject({ root: \"ROOT\" });\n    });\n\n    it(\"does not run loaders on same-hash navigations\", async () => {\n      let t = initializeTest({ url: \"/#bar\" });\n      expect(t.router.state.loaderData).toMatchObject({ root: \"ROOT\" });\n      let A = await t.navigate(\"/#bar\");\n      expect(A.loaders.root.stub.mock.calls.length).toBe(0);\n      expect(A.loaders.index.stub.mock.calls.length).toBe(0);\n    });\n\n    it(\"runs loaders on same-hash navigations to new paths\", async () => {\n      let t = initializeTest({ url: \"/#bar\" });\n      expect(t.router.state.loaderData).toMatchObject({ root: \"ROOT\" });\n      let A = await t.navigate(\"/foo#bar\");\n      expect(A.loaders.root.stub.mock.calls.length).toBe(0);\n      expect(A.loaders.foo.stub.mock.calls.length).toBe(1);\n    });\n\n    it(\"runs loaders on hash removal navigations (same path)\", async () => {\n      let t = initializeTest({ url: \"/#bar\" });\n      expect(t.router.state.loaderData).toMatchObject({ root: \"ROOT\" });\n      let A = await t.navigate(\"/\");\n      expect(A.loaders.root.stub.mock.calls.length).toBe(1);\n      expect(A.loaders.index.stub.mock.calls.length).toBe(1);\n    });\n\n    it(\"runs loaders on hash removal navigations (nested path)\", async () => {\n      let t = initializeTest({ url: \"/#bar\" });\n      expect(t.router.state.loaderData).toMatchObject({ root: \"ROOT\" });\n      let A = await t.navigate(\"/foo\");\n      expect(A.loaders.root.stub.mock.calls.length).toBe(0);\n      expect(A.loaders.foo.stub.mock.calls.length).toBe(1);\n    });\n\n    it('does not load anything on hash change only empty <Form method=\"get\"> navigations', async () => {\n      let t = initializeTest();\n      expect(t.router.state.loaderData).toMatchObject({ root: \"ROOT\" });\n      let A = await t.navigate(\"/#bar\", {\n        formData: createFormData({}),\n      });\n      expect(A.loaders.root.stub.mock.calls.length).toBe(0);\n      expect(t.router.state.loaderData).toMatchObject({ root: \"ROOT\" });\n    });\n\n    it('runs loaders on hash change only non-empty <Form method=\"get\"> navigations', async () => {\n      let t = initializeTest();\n      expect(t.router.state.loaderData).toMatchObject({ root: \"ROOT\" });\n      let A = await t.navigate(\"/#bar\", {\n        formData: createFormData({ key: \"value\" }),\n      });\n      await A.loaders.root.resolve(\"ROOT 2\");\n      await A.loaders.index.resolve(\"INDEX 2\");\n      expect(t.router.state.location.search).toBe(\"?key=value\");\n      expect(t.router.state.loaderData).toMatchObject({\n        root: \"ROOT 2\",\n        index: \"INDEX 2\",\n      });\n    });\n\n    it('runs action/loaders on hash change only <Form method=\"post\"> navigations', async () => {\n      let t = initializeTest();\n      let A = await t.navigate(\"/foo#bar\");\n      expect(t.router.state.navigation.state).toBe(\"loading\");\n      await A.loaders.foo.resolve(\"A\");\n      expect(t.router.state.loaderData).toMatchObject({\n        root: \"ROOT\",\n        foo: \"A\",\n      });\n\n      // Submit while we have an active hash causing us to lose it\n      let B = await t.navigate(\"/foo\", {\n        formMethod: \"post\",\n        formData: createFormData({}),\n      });\n      expect(t.router.state.navigation.state).toBe(\"submitting\");\n      await B.actions.foo.resolve(\"ACTION\");\n      await B.loaders.root.resolve(\"ROOT 2\");\n      await B.loaders.foo.resolve(\"B\");\n      expect(t.router.state.navigation.state).toBe(\"idle\");\n      expect(t.router.state.actionData).toMatchObject({\n        foo: \"ACTION\",\n      });\n      expect(t.router.state.loaderData).toMatchObject({\n        root: \"ROOT 2\",\n        foo: \"B\",\n      });\n    });\n\n    it(\"sets all right states on hash change only\", async () => {\n      let t = initializeTest();\n      let key = t.router.state.location.key;\n      t.navigate(\"/#bar\");\n      // hash changes are synchronous but force a key change\n      expect(t.router.state.location.key).not.toBe(key);\n      expect(t.router.state.location.hash).toBe(\"#bar\");\n      expect(t.router.state.navigation.state).toBe(\"idle\");\n    });\n\n    it(\"loads new data on new routes even if there's also a hash change\", async () => {\n      let t = initializeTest();\n      let A = await t.navigate(\"/foo#bar\");\n      expect(t.router.state.navigation.state).toBe(\"loading\");\n      await A.loaders.foo.resolve(\"A\");\n      expect(t.router.state.loaderData).toMatchObject({\n        root: \"ROOT\",\n        foo: \"A\",\n      });\n    });\n\n    it(\"does not use fog of war partial matches for hash change only navigations\", async () => {\n      let router = createBrowserRouter(\n        [\n          {\n            path: \"/\",\n            children: [\n              {\n                path: \"*\",\n              },\n            ],\n          },\n        ],\n        {\n          window: getWindow(\"/\"),\n          // This is what enables the partialMatches logic\n          patchRoutesOnNavigation: () => {},\n        },\n      );\n      expect(router.state.location).toMatchObject({\n        pathname: \"/\",\n        hash: \"\",\n      });\n      expect(router.state.matches).toMatchObject([{ route: { path: \"/\" } }]);\n      await router.navigate(\"/foo\");\n      expect(router.state.location).toMatchObject({\n        pathname: \"/foo\",\n        hash: \"\",\n      });\n      expect(router.state.matches).toMatchObject([\n        { route: { path: \"/\" } },\n        { route: { path: \"*\" } },\n      ]);\n      await router.navigate(\"/foo#bar\");\n      expect(router.state.location).toMatchObject({\n        pathname: \"/foo\",\n        hash: \"#bar\",\n      });\n      expect(router.state.matches).toMatchObject([\n        { route: { path: \"/\" } },\n        { route: { path: \"*\" } },\n      ]);\n    });\n\n    it(\"redirects from loaders (throw)\", async () => {\n      let t = initializeTest();\n\n      let A = await t.navigate(\"/bar\");\n      expect(t.router.state.navigation.state).toBe(\"loading\");\n      expect(t.router.state.navigation.location?.pathname).toBe(\"/bar\");\n      expect(t.router.state.loaderData).toMatchObject({\n        root: \"ROOT\",\n      });\n\n      let B = await A.loaders.bar.redirect(\"/baz\");\n      expect(t.router.state.navigation.state).toBe(\"loading\");\n      expect(t.router.state.navigation.location?.pathname).toBe(\"/baz\");\n      expect(t.router.state.loaderData).toMatchObject({\n        root: \"ROOT\",\n      });\n\n      await B.loaders.baz.resolve(\"B\");\n      expect(t.router.state.navigation.state).toBe(\"idle\");\n      expect(t.router.state.location.pathname).toBe(\"/baz\");\n      expect(t.router.state.loaderData).toMatchObject({\n        root: \"ROOT\",\n        baz: \"B\",\n      });\n    });\n\n    it(\"redirects from loaders (return)\", async () => {\n      let t = initializeTest();\n\n      let A = await t.navigate(\"/bar\");\n      expect(t.router.state.navigation.state).toBe(\"loading\");\n      expect(t.router.state.navigation.location?.pathname).toBe(\"/bar\");\n      expect(t.router.state.loaderData).toMatchObject({\n        root: \"ROOT\",\n      });\n\n      let B = await A.loaders.bar.redirectReturn(\"/baz\");\n      expect(t.router.state.navigation.state).toBe(\"loading\");\n      expect(t.router.state.navigation.location?.pathname).toBe(\"/baz\");\n      expect(t.router.state.loaderData).toMatchObject({\n        root: \"ROOT\",\n      });\n\n      await B.loaders.baz.resolve(\"B\");\n      expect(t.router.state.navigation.state).toBe(\"idle\");\n      expect(t.router.state.location.pathname).toBe(\"/baz\");\n      expect(t.router.state.loaderData).toMatchObject({\n        root: \"ROOT\",\n        baz: \"B\",\n      });\n    });\n\n    it(\"reloads all routes if X-Remix-Revalidate was set in a loader redirect header\", async () => {\n      let t = initializeTest();\n\n      let A = await t.navigate(\"/foo\");\n      expect(t.router.state.navigation.state).toBe(\"loading\");\n      expect(t.router.state.navigation.location?.pathname).toBe(\"/foo\");\n      expect(t.router.state.loaderData).toMatchObject({\n        root: \"ROOT\",\n      });\n\n      let B = await A.loaders.foo.redirectReturn(\"/bar\", undefined, {\n        \"X-Remix-Revalidate\": \"yes\",\n      });\n      expect(t.router.state.navigation.state).toBe(\"loading\");\n      expect(t.router.state.navigation.location?.pathname).toBe(\"/bar\");\n      expect(t.router.state.loaderData).toMatchObject({\n        root: \"ROOT\",\n      });\n\n      await B.loaders.root.resolve(\"ROOT*\");\n      await B.loaders.bar.resolve(\"BAR\");\n      expect(t.router.state.navigation.state).toBe(\"idle\");\n      expect(t.router.state.location.pathname).toBe(\"/bar\");\n      expect(t.router.state.loaderData).toMatchObject({\n        root: \"ROOT*\",\n        bar: \"BAR\",\n      });\n    });\n\n    it(\"reloads all routes if X-Remix-Revalidate was set in a loader redirect header (chained redirects)\", async () => {\n      let t = initializeTest();\n\n      let A = await t.navigate(\"/foo\");\n      expect(A.loaders.root.stub.mock.calls.length).toBe(0); // Reused on navigation\n\n      let B = await A.loaders.foo.redirectReturn(\"/bar\", undefined, {\n        \"X-Remix-Revalidate\": \"yes\",\n      });\n      await B.loaders.root.resolve(\"ROOT*\");\n      expect(B.loaders.root.stub.mock.calls.length).toBe(1);\n\n      // No cookie on second redirect\n      let C = await B.loaders.bar.redirectReturn(\"/baz\");\n      expect(C.loaders.root.stub.mock.calls.length).toBe(1);\n      await C.loaders.root.resolve(\"ROOT**\");\n      await C.loaders.baz.resolve(\"BAZ\");\n\n      expect(t.router.state.navigation.state).toBe(\"idle\");\n      expect(t.router.state.location.pathname).toBe(\"/baz\");\n      expect(t.router.state.loaderData).toMatchObject({\n        root: \"ROOT**\",\n        baz: \"BAZ\",\n      });\n    });\n  });\n\n  describe(\"errors on navigation\", () => {\n    describe(\"with an error boundary in the throwing route\", () => {\n      it(\"uses the throwing route's error boundary\", async () => {\n        let t = setup({\n          routes: [\n            {\n              path: \"/\",\n              id: \"parent\",\n              children: [\n                {\n                  path: \"/child\",\n                  id: \"child\",\n                  hasErrorBoundary: true,\n                  loader: true,\n                },\n              ],\n            },\n          ],\n        });\n        let nav = await t.navigate(\"/child\");\n        await nav.loaders.child.reject(new Error(\"Kaboom!\"));\n        expect(t.router.state.errors).toEqual({\n          child: new Error(\"Kaboom!\"),\n        });\n      });\n\n      it(\"clears previous loaderData at that route\", async () => {\n        let t = setup({\n          routes: [\n            {\n              path: \"/\",\n              id: \"parent\",\n              loader: true,\n              children: [\n                {\n                  path: \"/child\",\n                  id: \"child\",\n                  hasErrorBoundary: true,\n                  loader: true,\n                },\n              ],\n            },\n          ],\n        });\n        let nav = await t.navigate(\"/child\");\n        await nav.loaders.parent.resolve(\"PARENT\");\n        await nav.loaders.child.resolve(\"CHILD\");\n        expect(t.router.state.loaderData).toEqual({\n          parent: \"PARENT\",\n          child: \"CHILD\",\n        });\n        expect(t.router.state.errors).toEqual(null);\n\n        let nav2 = await t.navigate(\"/child\");\n        await nav2.loaders.parent.resolve(\"PARENT2\");\n        await nav2.loaders.child.reject(new Error(\"Kaboom!\"));\n        expect(t.router.state.loaderData).toEqual({\n          parent: \"PARENT2\",\n        });\n        expect(t.router.state.errors).toEqual({\n          child: new Error(\"Kaboom!\"),\n        });\n      });\n    });\n\n    describe(\"with an error boundary above the throwing route\", () => {\n      it(\"uses the nearest error boundary\", async () => {\n        let t = setup({\n          routes: [\n            {\n              path: \"/\",\n              id: \"parent\",\n              hasErrorBoundary: true,\n              children: [\n                {\n                  path: \"/child\",\n                  id: \"child\",\n                  loader: true,\n                },\n              ],\n            },\n          ],\n          hydrationData: { loaderData: { parent: \"stuff\" } },\n        });\n        let nav = await t.navigate(\"/child\");\n        await nav.loaders.child.reject(new Error(\"Kaboom!\"));\n        expect(t.router.state.errors).toEqual({\n          parent: new Error(\"Kaboom!\"),\n        });\n      });\n\n      it(\"clears out the error on new locations\", async () => {\n        let t = setup({\n          routes: [\n            {\n              path: \"\",\n              id: \"root\",\n              loader: true,\n              children: [\n                {\n                  path: \"/\",\n                  id: \"parent\",\n                  children: [\n                    {\n                      path: \"/child\",\n                      id: \"child\",\n                      hasErrorBoundary: true,\n                      loader: true,\n                    },\n                  ],\n                },\n              ],\n            },\n          ],\n          hydrationData: { loaderData: { root: \"ROOT\" } },\n        });\n\n        let nav = await t.navigate(\"/child\");\n        await nav.loaders.child.reject(\"Kaboom!\");\n        expect(t.router.state.loaderData).toEqual({ root: \"ROOT\" });\n        expect(t.router.state.errors).toEqual({ child: \"Kaboom!\" });\n\n        await t.navigate(\"/\");\n        expect(t.router.state.loaderData).toEqual({ root: \"ROOT\" });\n        expect(t.router.state.errors).toBe(null);\n      });\n    });\n\n    it(\"loads data above error boundary route\", async () => {\n      let t = setup({\n        routes: [\n          {\n            path: \"/\",\n            id: \"a\",\n            loader: true,\n            children: [\n              {\n                path: \"/b\",\n                id: \"b\",\n                loader: true,\n                hasErrorBoundary: true,\n                children: [\n                  {\n                    path: \"/b/c\",\n                    id: \"c\",\n                    loader: true,\n                  },\n                ],\n              },\n            ],\n          },\n        ],\n        hydrationData: { loaderData: { a: \"LOADER A\" } },\n      });\n      let nav = await t.navigate(\"/b/c\");\n      await nav.loaders.b.resolve(\"LOADER B\");\n      await nav.loaders.c.reject(\"Kaboom!\");\n      expect(t.router.state.loaderData).toEqual({\n        a: \"LOADER A\",\n        b: \"LOADER B\",\n      });\n      expect(t.router.state.errors).toEqual({\n        b: \"Kaboom!\",\n      });\n    });\n  });\n\n  describe(\"POP navigations\", () => {\n    it(\"does a normal load when backing into an action redirect\", async () => {\n      // start at / (history stack: [/])\n      let t = initializeTest();\n\n      // POST /foo, redirect /bar (history stack: [/, /bar])\n      let A = await t.navigate(\"/foo\", {\n        formMethod: \"post\",\n        formData: createFormData({ gosh: \"dang\" }),\n      });\n      let B = await A.actions.foo.redirect(\"/bar\");\n      await B.loaders.root.resolve(\"ROOT DATA\");\n      await B.loaders.bar.resolve(\"B LOADER\");\n      expect(t.router.state.historyAction).toEqual(\"PUSH\");\n      expect(t.router.state.location.pathname).toEqual(\"/bar\");\n      expect(B.loaders.root.stub.mock.calls.length).toBe(1);\n      expect(t.router.state.loaderData).toEqual({\n        root: \"ROOT DATA\",\n        bar: \"B LOADER\",\n      });\n\n      // Link to /baz (history stack: [/, /bar, /baz])\n      let C = await t.navigate(\"/baz\");\n      await C.loaders.baz.resolve(\"C LOADER\");\n      expect(t.router.state.historyAction).toEqual(\"PUSH\");\n      expect(t.router.state.location.pathname).toEqual(\"/baz\");\n      expect(C.loaders.root.stub.mock.calls.length).toBe(0);\n      expect(t.router.state.loaderData).toEqual({\n        root: \"ROOT DATA\",\n        baz: \"C LOADER\",\n      });\n\n      // POP /bar (history stack: [/, /bar])\n      let D = await t.navigate(-1);\n      await D.loaders.bar.resolve(\"D LOADER\");\n      expect(t.router.state.historyAction).toEqual(\"POP\");\n      expect(t.router.state.location.pathname).toEqual(\"/bar\");\n      expect(D.loaders.root.stub.mock.calls.length).toBe(0);\n      expect(t.router.state.loaderData).toMatchObject({\n        root: \"ROOT DATA\",\n        bar: \"D LOADER\",\n      });\n\n      // POP / (history stack: [/])\n      let E = await t.navigate(-1);\n      await E.loaders.index.resolve(\"E LOADER\");\n      expect(t.router.state.historyAction).toEqual(\"POP\");\n      expect(t.router.state.location.pathname).toEqual(\"/\");\n      expect(E.loaders.root.stub.mock.calls.length).toBe(0);\n      expect(t.router.state.loaderData).toMatchObject({\n        root: \"ROOT DATA\",\n        index: \"E LOADER\",\n      });\n    });\n\n    it(\"navigates correctly using POP navigations\", async () => {\n      let t = initializeTest();\n\n      let A = await t.navigate(\"/foo\");\n      await A.loaders.foo.resolve(\"FOO\");\n      expect(t.router.state.location.pathname).toEqual(\"/foo\");\n\n      let B = await t.navigate(\"/bar\");\n      await B.loaders.bar.resolve(\"BAR\");\n      expect(t.router.state.location.pathname).toEqual(\"/bar\");\n\n      let C = await t.navigate(-1);\n      await C.loaders.foo.resolve(\"FOO*\");\n      expect(t.router.state.location.pathname).toEqual(\"/foo\");\n\n      let D = await t.navigate(\"/baz\", { replace: true });\n      await D.loaders.baz.resolve(\"BAZ\");\n      expect(t.router.state.location.pathname).toEqual(\"/baz\");\n\n      // POP to /\n      let E = await t.navigate(-1);\n      await E.loaders.index.resolve(\"INDEX*\");\n      expect(t.router.state.location.pathname).toEqual(\"/\");\n    });\n\n    it(\"navigates correctly using POP navigations across actions\", async () => {\n      let t = initializeTest();\n\n      // Navigate to /foo\n      let A = await t.navigate(\"/foo\");\n      await A.loaders.foo.resolve(\"FOO\");\n      expect(t.router.state.location.pathname).toEqual(\"/foo\");\n\n      // Navigate to /bar\n      let B = await t.navigate(\"/bar\");\n      await B.loaders.bar.resolve(\"BAR\");\n      expect(t.router.state.location.pathname).toEqual(\"/bar\");\n\n      // Post to /bar (should replace)\n      let C = await t.navigate(\"/bar\", {\n        formMethod: \"post\",\n        formData: createFormData({ key: \"value\" }),\n      });\n      await C.actions.bar.resolve(\"BAR ACTION\");\n      await C.loaders.root.resolve(\"ROOT\");\n      await C.loaders.bar.resolve(\"BAR\");\n      expect(t.router.state.location.pathname).toEqual(\"/bar\");\n\n      // POP to /foo\n      let D = await t.navigate(-1);\n      await D.loaders.foo.resolve(\"FOO\");\n      expect(t.router.state.location.pathname).toEqual(\"/foo\");\n    });\n\n    it(\"navigates correctly using POP navigations across actions to new locations\", async () => {\n      let t = initializeTest();\n\n      // Navigate to /foo\n      let A = await t.navigate(\"/foo\");\n      await A.loaders.foo.resolve(\"FOO\");\n      expect(t.router.state.location.pathname).toEqual(\"/foo\");\n\n      // Navigate to /bar\n      let B = await t.navigate(\"/bar\");\n      await B.loaders.bar.resolve(\"BAR\");\n      expect(t.router.state.location.pathname).toEqual(\"/bar\");\n\n      // Post to /baz (should not replace)\n      let C = await t.navigate(\"/baz\", {\n        formMethod: \"post\",\n        formData: createFormData({ key: \"value\" }),\n      });\n      await C.actions.baz.resolve(\"BAZ ACTION\");\n      await C.loaders.root.resolve(\"ROOT\");\n      await C.loaders.baz.resolve(\"BAZ\");\n      expect(t.router.state.location.pathname).toEqual(\"/baz\");\n\n      // POP to /bar\n      let D = await t.navigate(-1);\n      await D.loaders.bar.resolve(\"BAR\");\n      expect(t.router.state.location.pathname).toEqual(\"/bar\");\n    });\n\n    it(\"navigates correctly using POP navigations across action errors\", async () => {\n      let t = initializeTest();\n\n      // Navigate to /foo\n      let A = await t.navigate(\"/foo\");\n      await A.loaders.foo.resolve(\"FOO\");\n      expect(t.router.state.location.pathname).toEqual(\"/foo\");\n\n      // Navigate to /bar\n      let B = await t.navigate(\"/bar\");\n      await B.loaders.bar.resolve(\"BAR\");\n      expect(t.router.state.location.pathname).toEqual(\"/bar\");\n\n      // Post to /bar (should push due to our error)\n      let C = await t.navigate(\"/bar\", {\n        formMethod: \"post\",\n        formData: createFormData({ key: \"value\" }),\n      });\n      await C.actions.bar.reject(\"BAR ERROR\");\n      await C.loaders.root.resolve(\"ROOT\");\n      await C.loaders.bar.resolve(\"BAR\");\n      expect(t.router.state.location.pathname).toEqual(\"/bar\");\n\n      // POP to /bar\n      let D = await t.navigate(-1);\n      await D.loaders.bar.resolve(\"BAR\");\n      expect(t.router.state.location.pathname).toEqual(\"/bar\");\n    });\n\n    it(\"navigates correctly using POP navigations across loader redirects\", async () => {\n      // Start at / (history stack: [/])\n      let t = initializeTest();\n\n      // Navigate to /foo (history stack: [/, /foo])\n      let A = await t.navigate(\"/foo\");\n      await A.loaders.foo.resolve(\"FOO\");\n      expect(t.router.state.location.pathname).toEqual(\"/foo\");\n      let fooKey = t.router.state.location?.key;\n\n      // Navigate to /bar, redirect to /baz (history stack: [/, /foo, /baz])\n      let B = await t.navigate(\"/bar\");\n      let C = await B.loaders.bar.redirect(\"/baz\");\n      await C.loaders.root.resolve(\"ROOT\");\n      await C.loaders.baz.resolve(\"BAZ\");\n      expect(t.router.state.location.pathname).toEqual(\"/baz\");\n\n      // POP to /foo (history stack: [/, /foo])\n      let E = await t.navigate(-1);\n      await E.loaders.foo.resolve(\"FOO\");\n      expect(t.router.state.location.pathname).toEqual(\"/foo\");\n      expect(t.router.state.location.key).toBe(fooKey);\n    });\n\n    it(\"navigates correctly using POP navigations across loader redirects with replace:true\", async () => {\n      // Start at / (history stack: [/])\n      let t = initializeTest();\n      let indexKey = t.router.state.location?.key;\n\n      // Navigate to /foo (history stack: [/, /foo])\n      let A = await t.navigate(\"/foo\");\n      await A.loaders.foo.resolve(\"FOO\");\n      expect(t.router.state.historyAction).toEqual(\"PUSH\");\n      expect(t.router.state.location.pathname).toEqual(\"/foo\");\n\n      // Navigate to /bar, redirect to /baz (history stack: [/, /baz])\n      let B = await t.navigate(\"/bar\", { replace: true });\n      let C = await B.loaders.bar.redirect(\"/baz\");\n      await C.loaders.root.resolve(\"ROOT\");\n      await C.loaders.baz.resolve(\"BAZ\");\n      expect(t.router.state.historyAction).toEqual(\"REPLACE\");\n      expect(t.router.state.location.pathname).toEqual(\"/baz\");\n\n      // POP to / (history stack: [/])\n      let E = await t.navigate(-1);\n      await E.loaders.index.resolve(\"INDEX\");\n      expect(t.router.state.historyAction).toEqual(\"POP\");\n      expect(t.router.state.location.pathname).toEqual(\"/\");\n      expect(t.router.state.location.key).toBe(indexKey);\n    });\n\n    it(\"navigates correctly using POP navigations across action redirects\", async () => {\n      let t = initializeTest();\n\n      // Navigate to /foo\n      let A = await t.navigate(\"/foo\");\n      await A.loaders.foo.resolve(\"FOO\");\n      expect(t.router.state.location.pathname).toEqual(\"/foo\");\n\n      // Navigate to /bar\n      let B = await t.navigate(\"/bar\");\n      let getBarKey = t.router.state.navigation.location?.key;\n      await B.loaders.bar.resolve(\"BAR\");\n      expect(t.router.state.historyAction).toEqual(\"PUSH\");\n      expect(t.router.state.location.pathname).toEqual(\"/bar\");\n\n      // Post to /bar, redirect to /baz\n      let C = await t.navigate(\"/bar\", {\n        formMethod: \"post\",\n        formData: createFormData({ key: \"value\" }),\n      });\n      let postBarKey = t.router.state.navigation.location?.key;\n      let D = await C.actions.bar.redirect(\"/baz\");\n      await D.loaders.root.resolve(\"ROOT\");\n      await D.loaders.baz.resolve(\"BAZ\");\n      expect(t.router.state.historyAction).toEqual(\"PUSH\");\n      expect(t.router.state.location.pathname).toEqual(\"/baz\");\n\n      // POP to /bar\n      let E = await t.navigate(-1);\n      await E.loaders.bar.resolve(\"BAR\");\n      expect(t.router.state.historyAction).toEqual(\"POP\");\n      expect(t.router.state.location.pathname).toEqual(\"/bar\");\n      expect(t.router.state.location.key).toBe(getBarKey);\n      expect(t.router.state.location.key).not.toBe(postBarKey);\n    });\n\n    it(\"navigates correctly using POP navigations across action redirects to the same location\", async () => {\n      let t = initializeTest();\n\n      // Navigate to /foo\n      let A = await t.navigate(\"/foo\");\n      let fooKey = t.router.state.navigation.location?.key;\n      await A.loaders.foo.resolve(\"FOO\");\n      expect(t.router.state.location.pathname).toEqual(\"/foo\");\n\n      // Navigate to /bar\n      let B = await t.navigate(\"/bar\");\n      await B.loaders.bar.resolve(\"BAR\");\n      expect(t.router.state.historyAction).toEqual(\"PUSH\");\n      expect(t.router.state.location.pathname).toEqual(\"/bar\");\n\n      // Post to /bar, redirect to /bar\n      let C = await t.navigate(\"/bar\", {\n        formMethod: \"post\",\n        formData: createFormData({ key: \"value\" }),\n      });\n      let postBarKey = t.router.state.navigation.location?.key;\n      let D = await C.actions.bar.redirect(\"/bar\");\n      await D.loaders.root.resolve(\"ROOT\");\n      await D.loaders.bar.resolve(\"BAR\");\n      expect(t.router.state.historyAction).toEqual(\"REPLACE\");\n      expect(t.router.state.location.pathname).toEqual(\"/bar\");\n\n      // POP to /foo\n      let E = await t.navigate(-1);\n      await E.loaders.foo.resolve(\"FOO\");\n      expect(t.router.state.historyAction).toEqual(\"POP\");\n      expect(t.router.state.location.pathname).toEqual(\"/foo\");\n      expect(t.router.state.location.key).toBe(fooKey);\n      expect(t.router.state.location.key).not.toBe(postBarKey);\n    });\n\n    it(\"navigates correctly using POP navigations across <Form replace> redirects\", async () => {\n      let t = initializeTest();\n\n      // Navigate to /foo\n      let A = await t.navigate(\"/foo\");\n      await A.loaders.foo.resolve(\"FOO\");\n      expect(t.router.state.location.pathname).toEqual(\"/foo\");\n\n      // Navigate to /bar\n      let B = await t.navigate(\"/bar\");\n      await B.loaders.bar.resolve(\"BAR\");\n      expect(t.router.state.historyAction).toEqual(\"PUSH\");\n      expect(t.router.state.location.pathname).toEqual(\"/bar\");\n\n      // Post to /bar, redirect to /baz\n      let C = await t.navigate(\"/bar\", {\n        formMethod: \"post\",\n        formData: createFormData({ key: \"value\" }),\n        replace: true,\n      });\n      let D = await C.actions.bar.redirect(\"/baz\");\n      await D.loaders.root.resolve(\"ROOT\");\n      await D.loaders.baz.resolve(\"BAZ\");\n      expect(t.router.state.historyAction).toEqual(\"REPLACE\");\n      expect(t.router.state.location.pathname).toEqual(\"/baz\");\n\n      // POP to /foo\n      let E = await t.navigate(-1);\n      await E.loaders.foo.resolve(\"FOO\");\n      expect(t.router.state.historyAction).toEqual(\"POP\");\n      expect(t.router.state.location.pathname).toEqual(\"/foo\");\n    });\n\n    it(\"should respect explicit replace:false on non-redirected actions to new locations\", async () => {\n      // start at / (history stack: [/])\n      let t = initializeTest();\n\n      // Link to /foo (history stack: [/, /foo])\n      let A = await t.navigate(\"/foo\");\n      await A.loaders.root.resolve(\"ROOT\");\n      await A.loaders.foo.resolve(\"FOO\");\n      expect(t.router.state.historyAction).toEqual(\"PUSH\");\n      expect(t.router.state.location.pathname).toEqual(\"/foo\");\n\n      // POST /bar (history stack: [/, /foo, /bar])\n      let B = await t.navigate(\"/bar\", {\n        formMethod: \"post\",\n        formData: createFormData({ gosh: \"dang\" }),\n        replace: false,\n      });\n      await B.actions.bar.resolve(\"BAR\");\n      await B.loaders.root.resolve(\"ROOT\");\n      await B.loaders.bar.resolve(\"BAR\");\n      expect(t.router.state.historyAction).toEqual(\"PUSH\");\n      expect(t.router.state.location.pathname).toEqual(\"/bar\");\n\n      // POP /foo (history stack: [GET /, GET /foo])\n      let C = await t.navigate(-1);\n      await C.loaders.foo.resolve(\"FOO\");\n      expect(t.router.state.historyAction).toEqual(\"POP\");\n      expect(t.router.state.location.pathname).toEqual(\"/foo\");\n    });\n\n    it(\"should respect explicit replace:false on non-redirected actions to the same location\", async () => {\n      // start at / (history stack: [/])\n      let t = initializeTest();\n\n      // Link to /foo (history stack: [/, /foo])\n      let A = await t.navigate(\"/foo\");\n      await A.loaders.root.resolve(\"ROOT\");\n      await A.loaders.foo.resolve(\"FOO\");\n      expect(t.router.state.historyAction).toEqual(\"PUSH\");\n      expect(t.router.state.location.pathname).toEqual(\"/foo\");\n\n      // POST /foo (history stack: [/, /foo, /foo])\n      let B = await t.navigate(\"/foo\", {\n        formMethod: \"post\",\n        formData: createFormData({ gosh: \"dang\" }),\n        replace: false,\n      });\n      await B.actions.foo.resolve(\"FOO2 ACTION\");\n      await B.loaders.root.resolve(\"ROOT2\");\n      await B.loaders.foo.resolve(\"FOO2\");\n      expect(t.router.state.historyAction).toEqual(\"PUSH\");\n      expect(t.router.state.location.pathname).toEqual(\"/foo\");\n\n      // POP /foo (history stack: [/, /foo])\n      let C = await t.navigate(-1);\n      await C.loaders.root.resolve(\"ROOT3\");\n      await C.loaders.foo.resolve(\"FOO3\");\n      expect(t.router.state.historyAction).toEqual(\"POP\");\n      expect(t.router.state.location.pathname).toEqual(\"/foo\");\n    });\n  });\n\n  describe(\"navigation states\", () => {\n    it(\"initialization\", async () => {\n      let t = initializeTest();\n      let navigation = t.router.state.navigation;\n      expect(navigation.state).toBe(\"idle\");\n      expect(navigation.formData).toBeUndefined();\n      expect(navigation.location).toBeUndefined();\n    });\n\n    it(\"get\", async () => {\n      let t = initializeTest();\n      let A = await t.navigate(\"/foo\");\n      let navigation = t.router.state.navigation;\n      expect(navigation.state).toBe(\"loading\");\n      expect(navigation.formData).toBeUndefined();\n      expect(navigation.location).toMatchObject({\n        pathname: \"/foo\",\n        search: \"\",\n        hash: \"\",\n      });\n\n      await A.loaders.foo.resolve(\"A\");\n      navigation = t.router.state.navigation;\n      expect(navigation.state).toBe(\"idle\");\n      expect(navigation.formData).toBeUndefined();\n      expect(navigation.location).toBeUndefined();\n    });\n\n    it(\"get + redirect\", async () => {\n      let t = initializeTest();\n\n      let A = await t.navigate(\"/foo\");\n      let B = await A.loaders.foo.redirect(\"/bar\");\n\n      let navigation = t.router.state.navigation;\n      expect(navigation.state).toBe(\"loading\");\n      expect(navigation.formData).toBeUndefined();\n      expect(navigation.location?.pathname).toBe(\"/bar\");\n\n      await B.loaders.bar.resolve(\"B\");\n      navigation = t.router.state.navigation;\n      expect(navigation.state).toBe(\"idle\");\n      expect(navigation.formData).toBeUndefined();\n      expect(navigation.location).toBeUndefined();\n    });\n\n    it(\"action submission\", async () => {\n      let t = initializeTest();\n\n      let A = await t.navigate(\"/foo\", {\n        formMethod: \"post\",\n        formData: createFormData({ gosh: \"dang\" }),\n      });\n      let navigation = t.router.state.navigation;\n      expect(navigation.state).toBe(\"submitting\");\n\n      expect(\n        // @ts-expect-error\n        new URLSearchParams(navigation.formData).toString(),\n      ).toBe(\"gosh=dang\");\n      expect(navigation.formMethod).toBe(\"POST\");\n      expect(navigation.formEncType).toBe(\"application/x-www-form-urlencoded\");\n      expect(navigation.location).toMatchObject({\n        pathname: \"/foo\",\n        search: \"\",\n        hash: \"\",\n      });\n\n      await A.actions.foo.resolve(\"A\");\n      navigation = t.router.state.navigation;\n      expect(navigation.state).toBe(\"loading\");\n      expect(\n        // @ts-expect-error\n        new URLSearchParams(navigation.formData).toString(),\n      ).toBe(\"gosh=dang\");\n      expect(navigation.formMethod).toBe(\"POST\");\n      expect(navigation.formEncType).toBe(\"application/x-www-form-urlencoded\");\n      expect(navigation.location).toMatchObject({\n        pathname: \"/foo\",\n        search: \"\",\n        hash: \"\",\n      });\n\n      await A.loaders.foo.resolve(\"A\");\n      navigation = t.router.state.navigation;\n      expect(navigation.state).toBe(\"loading\");\n\n      await A.loaders.root.resolve(\"B\");\n      navigation = t.router.state.navigation;\n      expect(navigation.state).toBe(\"idle\");\n      expect(navigation.formData).toBeUndefined();\n      expect(navigation.location).toBeUndefined();\n    });\n\n    it(\"action submission + redirect\", async () => {\n      let t = initializeTest();\n\n      let A = await t.navigate(\"/foo\", {\n        formMethod: \"post\",\n        formData: createFormData({ gosh: \"dang\" }),\n      });\n      let B = await A.actions.foo.redirect(\"/bar\");\n\n      let navigation = t.router.state.navigation;\n      expect(navigation.state).toBe(\"loading\");\n      expect(\n        // @ts-expect-error\n        new URLSearchParams(navigation.formData).toString(),\n      ).toBe(\"gosh=dang\");\n      expect(navigation.formMethod).toBe(\"POST\");\n      expect(navigation.location).toMatchObject({\n        pathname: \"/bar\",\n        search: \"\",\n        hash: \"\",\n      });\n\n      await B.loaders.bar.resolve(\"B\");\n      navigation = t.router.state.navigation;\n      expect(navigation.state).toBe(\"loading\");\n\n      await B.loaders.root.resolve(\"C\");\n      navigation = t.router.state.navigation;\n      expect(navigation.state).toBe(\"idle\");\n      expect(navigation.formData).toBeUndefined();\n      expect(navigation.location).toBeUndefined();\n    });\n\n    it(\"loader submission\", async () => {\n      let t = initializeTest();\n      let A = await t.navigate(\"/foo\", {\n        formData: createFormData({ gosh: \"dang\" }),\n      });\n      let navigation = t.router.state.navigation;\n      expect(navigation.state).toBe(\"loading\");\n      expect(navigation.formData).toEqual(createFormData({ gosh: \"dang\" }));\n      expect(navigation.formMethod).toBe(\"GET\");\n      expect(navigation.formEncType).toBe(\"application/x-www-form-urlencoded\");\n      expect(navigation.location).toMatchObject({\n        pathname: \"/foo\",\n        search: \"?gosh=dang\",\n        hash: \"\",\n      });\n\n      await A.loaders.root.resolve(\"ROOT\");\n      await A.loaders.foo.resolve(\"A\");\n      navigation = t.router.state.navigation;\n      expect(navigation.state).toBe(\"idle\");\n      expect(navigation.formData).toBeUndefined();\n      expect(navigation.location).toBeUndefined();\n    });\n\n    it(\"loader submission + redirect\", async () => {\n      let t = initializeTest();\n\n      let A = await t.navigate(\"/foo\", {\n        formData: createFormData({ gosh: \"dang\" }),\n      });\n      await A.loaders.root.resolve(\"ROOT\");\n      let B = await A.loaders.foo.redirect(\"/bar\");\n\n      let navigation = t.router.state.navigation;\n      expect(navigation.state).toBe(\"loading\");\n      expect(navigation.formData).toEqual(createFormData({ gosh: \"dang\" }));\n      expect(navigation.formMethod).toBe(\"GET\");\n      expect(navigation.formEncType).toBe(\"application/x-www-form-urlencoded\");\n      expect(navigation.location?.pathname).toBe(\"/bar\");\n\n      await B.loaders.bar.resolve(\"B\");\n      navigation = t.router.state.navigation;\n      expect(navigation.state).toBe(\"idle\");\n      expect(navigation.formData).toBeUndefined();\n      expect(navigation.location).toBeUndefined();\n    });\n  });\n});\n"
  },
  {
    "path": "packages/react-router/__tests__/router/path-resolution-test.ts",
    "content": "import { createMemoryHistory, createPath } from \"../../lib/router/history\";\nimport { createRouter } from \"../../lib/router/router\";\nimport { cleanup } from \"./utils/data-router-setup\";\nimport { createFormData } from \"./utils/utils\";\n\ndescribe(\"path resolution\", () => {\n  // Detect any failures inside the router navigate code\n  afterEach(() => cleanup());\n\n  describe(\"routing to self\", () => {\n    // Utility that accepts children of /foo routes and executes the same\n    // routing tests starting at /foo/bar/?a=b#hash\n    function assertRoutingToSelf(fooChildren, expectedPath, expectIndex) {\n      const getRouter = () =>\n        createRouter({\n          routes: [\n            {\n              path: \"/\",\n              children: [\n                {\n                  path: \"foo\",\n                  children: fooChildren,\n                },\n              ],\n            },\n          ],\n          history: createMemoryHistory({\n            initialEntries: [\"/foo/bar?a=1#hash\"],\n          }),\n        }).initialize();\n\n      // Null should preserve the search/hash\n      let router = getRouter();\n      router.navigate(null, { fromRouteId: \"activeRoute\" });\n      expect(createPath(router.state.location)).toBe(\n        expectedPath + (expectIndex ? \"?index&a=1#hash\" : \"?a=1#hash\"),\n      );\n      router.dispose();\n\n      // \".\" and \"\" should not preserve the search and hash\n      router = getRouter();\n      router.navigate(\".\", { fromRouteId: \"activeRoute\" });\n      expect(createPath(router.state.location)).toBe(\n        expectedPath + (expectIndex ? \"?index\" : \"\"),\n      );\n      router.dispose();\n\n      router = getRouter();\n      router.navigate(\"\", { fromRouteId: \"activeRoute\" });\n      expect(createPath(router.state.location)).toBe(\n        expectedPath + (expectIndex ? \"?index\" : \"\"),\n      );\n      router.dispose();\n    }\n\n    /* eslint-disable jest/expect-expect */\n    it(\"from a static route\", () => {\n      assertRoutingToSelf(\n        [\n          {\n            id: \"activeRoute\",\n            path: \"bar\",\n          },\n        ],\n        \"/foo/bar\",\n        false,\n      );\n    });\n\n    it(\"from a layout route\", () => {\n      assertRoutingToSelf(\n        [\n          {\n            id: \"activeRoute\",\n            path: \"bar\",\n            children: [\n              {\n                index: true,\n              },\n            ],\n          },\n        ],\n        \"/foo/bar\",\n        false,\n      );\n    });\n\n    it(\"from an index route\", () => {\n      assertRoutingToSelf(\n        [\n          {\n            path: \"bar\",\n            children: [\n              {\n                id: \"activeRoute\",\n                index: true,\n              },\n            ],\n          },\n        ],\n        \"/foo/bar\",\n        true,\n      );\n    });\n\n    it(\"from an index route with a path\", () => {\n      assertRoutingToSelf(\n        [\n          {\n            id: \"activeRoute\",\n            path: \"bar\",\n            index: true,\n          },\n        ],\n        \"/foo/bar\",\n        true,\n      );\n    });\n\n    it(\"from a dynamic param route\", () => {\n      assertRoutingToSelf(\n        [\n          {\n            id: \"activeRoute\",\n            path: \":param\",\n          },\n        ],\n        \"/foo/bar\",\n        false,\n      );\n    });\n\n    it(\"from a splat route\", () => {\n      assertRoutingToSelf(\n        [\n          {\n            id: \"activeRoute\",\n            path: \"*\",\n          },\n        ],\n        \"/foo/bar\",\n        false,\n      );\n    });\n    /* eslint-enable jest/expect-expect */\n  });\n\n  describe(\"routing to parent\", () => {\n    function assertRoutingToParent(fooChildren) {\n      let router = createRouter({\n        routes: [\n          {\n            path: \"/\",\n            children: [\n              {\n                path: \"foo\",\n                children: fooChildren,\n              },\n            ],\n          },\n        ],\n        history: createMemoryHistory({\n          initialEntries: [\"/foo/bar?a=1#hash\"],\n        }),\n      }).initialize();\n\n      // Null should preserve the search/hash\n      router.navigate(\"..\", { fromRouteId: \"activeRoute\" });\n      expect(createPath(router.state.location)).toBe(\"/foo\");\n    }\n\n    /* eslint-disable jest/expect-expect */\n    it(\"from a static route\", () => {\n      assertRoutingToParent([\n        {\n          id: \"activeRoute\",\n          path: \"bar\",\n        },\n      ]);\n    });\n\n    it(\"from a layout route\", () => {\n      assertRoutingToParent([\n        {\n          id: \"activeRoute\",\n          path: \"bar\",\n          children: [\n            {\n              index: true,\n            },\n          ],\n        },\n      ]);\n    });\n\n    it(\"from an index route\", () => {\n      assertRoutingToParent([\n        {\n          path: \"bar\",\n          children: [\n            {\n              id: \"activeRoute\",\n              index: true,\n            },\n          ],\n        },\n      ]);\n    });\n\n    it(\"from an index route with a path\", () => {\n      assertRoutingToParent([\n        {\n          id: \"activeRoute\",\n          path: \"bar\",\n          index: true,\n        },\n      ]);\n    });\n\n    it(\"from a dynamic param route\", () => {\n      assertRoutingToParent([\n        {\n          id: \"activeRoute\",\n          path: \":param\",\n        },\n      ]);\n    });\n\n    it(\"from a splat route\", () => {\n      assertRoutingToParent([\n        {\n          id: \"activeRoute\",\n          path: \"*\",\n        },\n      ]);\n    });\n    /* eslint-enable jest/expect-expect */\n  });\n\n  describe(\"routing to sibling\", () => {\n    function assertRoutingToSibling(fooChildren) {\n      let router = createRouter({\n        routes: [\n          {\n            path: \"/\",\n            children: [\n              {\n                path: \"foo\",\n                children: [\n                  ...fooChildren,\n                  {\n                    path: \"bar-sibling\",\n                  },\n                ],\n              },\n            ],\n          },\n        ],\n        history: createMemoryHistory({\n          initialEntries: [\"/foo/bar?a=1#hash\"],\n        }),\n      }).initialize();\n\n      // Null should preserve the search/hash\n      router.navigate(\"../bar-sibling\", { fromRouteId: \"activeRoute\" });\n      expect(createPath(router.state.location)).toBe(\"/foo/bar-sibling\");\n    }\n\n    /* eslint-disable jest/expect-expect */\n    it(\"from a static route\", () => {\n      assertRoutingToSibling([\n        {\n          id: \"activeRoute\",\n          path: \"bar\",\n        },\n      ]);\n    });\n\n    it(\"from a layout route\", () => {\n      assertRoutingToSibling([\n        {\n          id: \"activeRoute\",\n          path: \"bar\",\n          children: [\n            {\n              index: true,\n            },\n          ],\n        },\n      ]);\n    });\n\n    it(\"from an index route\", () => {\n      assertRoutingToSibling([\n        {\n          path: \"bar\",\n          children: [\n            {\n              id: \"activeRoute\",\n              index: true,\n            },\n          ],\n        },\n      ]);\n    });\n\n    it(\"from an index route with a path\", () => {\n      assertRoutingToSibling([\n        {\n          id: \"activeRoute\",\n          path: \"bar\",\n          index: true,\n        },\n      ]);\n    });\n\n    it(\"from a dynamic param route\", () => {\n      assertRoutingToSibling([\n        {\n          id: \"activeRoute\",\n          path: \":param\",\n        },\n      ]);\n    });\n\n    it(\"from a splat route\", () => {\n      assertRoutingToSibling([\n        {\n          id: \"activeRoute\",\n          path: \"*\",\n        },\n      ]);\n    });\n    /* eslint-enable jest/expect-expect */\n  });\n\n  describe(\"routing to child\", () => {\n    function assertRoutingToChild(fooChildren) {\n      const getRouter = () =>\n        createRouter({\n          routes: [\n            {\n              path: \"/\",\n              children: [\n                {\n                  path: \"foo\",\n                  children: [...fooChildren],\n                },\n              ],\n            },\n          ],\n          history: createMemoryHistory({\n            initialEntries: [\"/foo/bar?a=1#hash\"],\n          }),\n        }).initialize();\n\n      let router = getRouter();\n      router.navigate(\"baz\", { fromRouteId: \"activeRoute\" });\n      expect(createPath(router.state.location)).toBe(\"/foo/bar/baz\");\n      router.dispose();\n\n      router = getRouter();\n      router.navigate(\"./baz\", { fromRouteId: \"activeRoute\" });\n      expect(createPath(router.state.location)).toBe(\"/foo/bar/baz\");\n      router.dispose();\n    }\n\n    /* eslint-disable jest/expect-expect */\n    it(\"from a static route\", () => {\n      assertRoutingToChild([\n        {\n          id: \"activeRoute\",\n          path: \"bar\",\n          children: [{ path: \"baz\" }],\n        },\n      ]);\n    });\n\n    it(\"from a layout route\", () => {\n      assertRoutingToChild([\n        {\n          id: \"activeRoute\",\n          path: \"bar\",\n          children: [\n            {\n              index: true,\n            },\n            { path: \"baz\" },\n          ],\n        },\n      ]);\n    });\n\n    it(\"from a dynamic param route\", () => {\n      assertRoutingToChild([\n        {\n          id: \"activeRoute\",\n          path: \":param\",\n          children: [{ path: \"baz\" }],\n        },\n      ]);\n    });\n    /* eslint-enable jest/expect-expect */\n  });\n\n  it(\"resolves relative routes when using relative:path\", () => {\n    let history = createMemoryHistory({\n      initialEntries: [\"/a/b/c/d/e/f\"],\n    });\n    let routes = [\n      {\n        id: \"a\",\n        path: \"/a\",\n        children: [\n          {\n            id: \"bc\",\n            path: \"b/c\",\n            children: [\n              {\n                id: \"de\",\n                path: \"d/e\",\n                children: [\n                  {\n                    id: \"f\",\n                    path: \"f\",\n                  },\n                ],\n              },\n            ],\n          },\n        ],\n      },\n    ];\n\n    // Navigating without relative:path\n    let router = createRouter({ routes, history }).initialize();\n    router.navigate(\"..\");\n    expect(router.state.location.pathname).toBe(\"/a/b/c/d/e\");\n    router.navigate(\"/a/b/c/d/e/f\");\n\n    router.navigate(\"../..\");\n    expect(router.state.location.pathname).toBe(\"/a/b/c\");\n    router.navigate(\"/a/b/c/d/e/f\");\n\n    // Navigating with relative:path\n    router.navigate(\"..\", { relative: \"path\" });\n    expect(router.state.location.pathname).toBe(\"/a/b/c/d/e\");\n    router.navigate(\"/a/b/c/d/e/f\");\n\n    router.navigate(\"../..\", { relative: \"path\" });\n    expect(router.state.location.pathname).toBe(\"/a/b/c/d\");\n    router.navigate(\"/a/b/c/d/e/f\");\n\n    // Navigating with relative:path from mid-route-hierarchy\n    router.navigate(\"..\", { relative: \"path\", fromRouteId: \"f\" });\n    expect(router.state.location.pathname).toBe(\"/a/b/c/d/e\");\n    router.navigate(\"/a/b/c/d/e/f\");\n\n    router.navigate(\"../..\", { relative: \"path\", fromRouteId: \"de\" });\n    expect(router.state.location.pathname).toBe(\"/a/b/c\");\n    router.navigate(\"/a/b/c/d/e/f\");\n\n    router.navigate(\"../..\", { relative: \"path\", fromRouteId: \"bc\" });\n    expect(router.state.location.pathname).toBe(\"/a\");\n    router.navigate(\"/a/b/c/d/e/f\");\n\n    // Go up farther than # of URL segments\n    router.navigate(\"../../../../../../../../..\", {\n      relative: \"path\",\n      fromRouteId: \"f\",\n    });\n    expect(router.state.location.pathname).toBe(\"/\");\n    router.navigate(\"/a/b/c/d/e/f\");\n\n    router.dispose();\n  });\n\n  it(\"should not append ?index to get submission navigations to self from index route\", () => {\n    let router = createRouter({\n      routes: [\n        {\n          path: \"/\",\n          children: [\n            {\n              path: \"path\",\n              children: [\n                {\n                  id: \"activeRouteId\",\n                  index: true,\n                },\n              ],\n            },\n          ],\n        },\n      ],\n      history: createMemoryHistory({ initialEntries: [\"/path\"] }),\n    }).initialize();\n\n    router.navigate(null, {\n      fromRouteId: \"activeRouteId\",\n      formData: createFormData({}),\n    });\n    expect(createPath(router.state.location)).toBe(\"/path\");\n    expect(router.state.matches[2].route.index).toBe(true);\n\n    router.navigate(\".\", {\n      fromRouteId: \"activeRouteId\",\n      formData: createFormData({}),\n    });\n    expect(createPath(router.state.location)).toBe(\"/path\");\n    expect(router.state.matches[2].route.index).toBe(true);\n\n    router.navigate(\"\", {\n      fromRouteId: \"activeRouteId\",\n      formData: createFormData({}),\n    });\n    expect(createPath(router.state.location)).toBe(\"/path\");\n    expect(router.state.matches[2].route.index).toBe(true);\n  });\n\n  it(\"handles pathless relative routing when a basename is present\", () => {\n    let router = createRouter({\n      routes: [{ path: \"/path\" }],\n      history: createMemoryHistory({ initialEntries: [\"/base/path\"] }),\n      basename: \"/base\",\n    }).initialize();\n\n    expect(createPath(router.state.location)).toBe(\"/base/path\");\n    expect(router.state.matches[0].route.path).toBe(\"/path\");\n\n    router.navigate(\".?a=1\");\n    expect(createPath(router.state.location)).toBe(\"/base/path?a=1\");\n    expect(router.state.matches[0].route.path).toBe(\"/path\");\n\n    router.navigate(\"?b=2\");\n    expect(createPath(router.state.location)).toBe(\"/base/path?b=2\");\n    expect(router.state.matches[0].route.path).toBe(\"/path\");\n\n    router.navigate(\"/path?c=3\");\n    expect(createPath(router.state.location)).toBe(\"/base/path?c=3\");\n    expect(router.state.matches[0].route.path).toBe(\"/path\");\n  });\n});\n"
  },
  {
    "path": "packages/react-router/__tests__/router/redirects-test.ts",
    "content": "import { createMemoryHistory } from \"../../lib/router/history\";\nimport { IDLE_NAVIGATION, createRouter } from \"../../lib/router/router\";\nimport { replace } from \"../../lib/router/utils\";\nimport type { TestRouteObject } from \"./utils/data-router-setup\";\nimport { cleanup, setup } from \"./utils/data-router-setup\";\nimport { createFormData, tick } from \"./utils/utils\";\n\ndescribe(\"redirects\", () => {\n  afterEach(() => cleanup());\n\n  let REDIRECT_ROUTES: TestRouteObject[] = [\n    {\n      id: \"root\",\n      path: \"/\",\n      children: [\n        {\n          id: \"parent\",\n          path: \"parent\",\n          action: true,\n          loader: true,\n          children: [\n            {\n              id: \"child\",\n              path: \"child\",\n              action: true,\n              loader: true,\n              children: [\n                {\n                  id: \"index\",\n                  index: true,\n                  action: true,\n                  loader: true,\n                },\n              ],\n            },\n          ],\n        },\n      ],\n    },\n  ];\n\n  it(\"applies the basename to redirects returned from loaders\", async () => {\n    let t = setup({\n      routes: REDIRECT_ROUTES,\n      basename: \"/base/name\",\n      initialEntries: [\"/base/name\"],\n    });\n\n    let nav1 = await t.navigate(\"/parent\");\n\n    let nav2 = await nav1.loaders.parent.redirectReturn(\"/parent/child\");\n    await nav2.loaders.parent.resolve(\"PARENT\");\n    await nav2.loaders.child.resolve(\"CHILD\");\n    await nav2.loaders.index.resolve(\"INDEX\");\n    expect(t.router.state).toMatchObject({\n      historyAction: \"PUSH\",\n      location: {\n        pathname: \"/base/name/parent/child\",\n      },\n      navigation: IDLE_NAVIGATION,\n      loaderData: {\n        parent: \"PARENT\",\n        child: \"CHILD\",\n        index: \"INDEX\",\n      },\n      errors: null,\n    });\n    expect(t.history.action).toEqual(\"PUSH\");\n    expect(t.history.location.pathname).toEqual(\"/base/name/parent/child\");\n  });\n\n  it(\"supports relative routing in redirects (from parent navigation loader)\", async () => {\n    let t = setup({ routes: REDIRECT_ROUTES });\n\n    let nav1 = await t.navigate(\"/parent/child\");\n\n    await nav1.loaders.child.resolve(\"CHILD\");\n    await nav1.loaders.index.resolve(\"INDEX\");\n    await nav1.loaders.parent.redirectReturn(\"..\");\n    // No root loader so redirect lands immediately\n    expect(t.router.state).toMatchObject({\n      location: {\n        pathname: \"/\",\n      },\n      navigation: IDLE_NAVIGATION,\n      loaderData: {},\n      errors: null,\n    });\n  });\n\n  it(\"supports relative routing in redirects (from child navigation loader)\", async () => {\n    let t = setup({ routes: REDIRECT_ROUTES });\n\n    let nav1 = await t.navigate(\"/parent/child\");\n\n    await nav1.loaders.parent.resolve(\"PARENT\");\n    await nav1.loaders.index.resolve(\"INDEX\");\n    let nav2 = await nav1.loaders.child.redirectReturn(\n      \"..\",\n      undefined,\n      undefined,\n      [\"parent\"],\n    );\n    await nav2.loaders.parent.resolve(\"PARENT 2\");\n    expect(t.router.state).toMatchObject({\n      location: {\n        pathname: \"/parent\",\n      },\n      navigation: IDLE_NAVIGATION,\n      loaderData: {\n        parent: \"PARENT 2\",\n      },\n      errors: null,\n    });\n  });\n\n  it(\"supports relative routing in redirects (from index navigation loader)\", async () => {\n    let t = setup({ routes: REDIRECT_ROUTES });\n\n    let nav1 = await t.navigate(\"/parent/child\");\n\n    await nav1.loaders.parent.resolve(\"PARENT\");\n    await nav1.loaders.child.resolve(\"INDEX\");\n    let nav2 = await nav1.loaders.index.redirectReturn(\n      \"..\",\n      undefined,\n      undefined,\n      [\"parent\"],\n    );\n    await nav2.loaders.parent.resolve(\"PARENT 2\");\n    expect(t.router.state).toMatchObject({\n      location: {\n        pathname: \"/parent\",\n      },\n      navigation: IDLE_NAVIGATION,\n      loaderData: {\n        parent: \"PARENT 2\",\n      },\n      errors: null,\n    });\n  });\n\n  it(\"supports relative routing in redirects (from parent fetch loader)\", async () => {\n    let t = setup({ routes: REDIRECT_ROUTES });\n\n    let fetch = await t.fetch(\"/parent\", \"key\");\n\n    await fetch.loaders.parent.redirectReturn(\"..\", undefined, undefined, [\n      \"parent\",\n    ]);\n\n    // No root loader so redirect lands immediately\n    expect(t.router.state).toMatchObject({\n      location: {\n        pathname: \"/\",\n      },\n      navigation: IDLE_NAVIGATION,\n      loaderData: {},\n      errors: null,\n    });\n    // There's never any data so it just ends up being removed from state.fetchers\n    expect(t.fetchers[\"key\"]).toBeUndefined();\n  });\n\n  it(\"supports relative routing in redirects (from child fetch loader)\", async () => {\n    let t = setup({ routes: REDIRECT_ROUTES });\n\n    let fetch = await t.fetch(\"/parent/child\");\n    let nav = await fetch.loaders.child.redirectReturn(\n      \"..\",\n      undefined,\n      undefined,\n      [\"parent\"],\n    );\n\n    await nav.loaders.parent.resolve(\"PARENT\");\n    expect(t.router.state).toMatchObject({\n      location: {\n        pathname: \"/parent\",\n      },\n      navigation: IDLE_NAVIGATION,\n      loaderData: {\n        parent: \"PARENT\",\n      },\n      errors: null,\n    });\n  });\n\n  it(\"supports relative routing in redirects (from index fetch loader)\", async () => {\n    let t = setup({ routes: REDIRECT_ROUTES });\n\n    let fetch = await t.fetch(\"/parent/child?index\");\n    let nav = await fetch.loaders.index.redirectReturn(\n      \"..\",\n      undefined,\n      undefined,\n      [\"parent\"],\n    );\n\n    await nav.loaders.parent.resolve(\"PARENT\");\n    expect(t.router.state).toMatchObject({\n      location: {\n        pathname: \"/parent\",\n      },\n      navigation: IDLE_NAVIGATION,\n      loaderData: {\n        parent: \"PARENT\",\n      },\n      errors: null,\n    });\n  });\n\n  it(\"supports . redirects\", async () => {\n    let t = setup({ routes: REDIRECT_ROUTES });\n\n    let nav1 = await t.navigate(\"/parent\");\n\n    let nav2 = await nav1.loaders.parent.redirectReturn(\n      \"./child\",\n      undefined,\n      undefined,\n      [\"parent\", \"child\", \"index\"],\n    );\n    await nav2.loaders.parent.resolve(\"PARENT\");\n    await nav2.loaders.child.resolve(\"CHILD\");\n    await nav2.loaders.index.resolve(\"INDEX\");\n    expect(t.router.state).toMatchObject({\n      location: {\n        pathname: \"/parent/child\",\n      },\n      navigation: IDLE_NAVIGATION,\n      loaderData: {\n        parent: \"PARENT\",\n        child: \"CHILD\",\n        index: \"INDEX\",\n      },\n      errors: null,\n    });\n  });\n\n  it(\"supports relative routing in navigation action redirects\", async () => {\n    let t = setup({ routes: REDIRECT_ROUTES });\n\n    let nav1 = await t.navigate(\"/parent/child\", {\n      formMethod: \"post\",\n      formData: createFormData({}),\n    });\n\n    let nav2 = await nav1.actions.child.redirectReturn(\n      \"..\",\n      undefined,\n      undefined,\n      [\"parent\"],\n    );\n    await nav2.loaders.parent.resolve(\"PARENT\");\n    expect(t.router.state).toMatchObject({\n      location: {\n        pathname: \"/parent\",\n      },\n      navigation: IDLE_NAVIGATION,\n      loaderData: {\n        parent: \"PARENT\",\n      },\n      errors: null,\n    });\n  });\n\n  it(\"supports relative routing in fetch action redirects\", async () => {\n    let t = setup({ routes: REDIRECT_ROUTES });\n\n    let nav1 = await t.fetch(\"/parent/child\", {\n      formMethod: \"post\",\n      formData: createFormData({}),\n    });\n\n    let nav2 = await nav1.actions.child.redirectReturn(\n      \"..\",\n      undefined,\n      undefined,\n      [\"parent\"],\n    );\n    await nav2.loaders.parent.resolve(\"PARENT\");\n    expect(t.router.state).toMatchObject({\n      location: {\n        pathname: \"/parent\",\n      },\n      navigation: IDLE_NAVIGATION,\n      loaderData: {\n        parent: \"PARENT\",\n      },\n      errors: null,\n    });\n  });\n\n  it(\"preserves query and hash in redirects\", async () => {\n    let t = setup({ routes: REDIRECT_ROUTES });\n\n    let nav1 = await t.navigate(\"/parent/child\", {\n      formMethod: \"post\",\n      formData: createFormData({}),\n    });\n\n    let nav2 = await nav1.actions.child.redirectReturn(\n      \"/parent?key=value#hash\",\n    );\n    await nav2.loaders.parent.resolve(\"PARENT\");\n    expect(t.router.state).toMatchObject({\n      location: {\n        pathname: \"/parent\",\n        search: \"?key=value\",\n        hash: \"#hash\",\n      },\n      navigation: IDLE_NAVIGATION,\n      loaderData: {\n        parent: \"PARENT\",\n      },\n      errors: null,\n    });\n  });\n\n  it(\"preserves query and hash in relative redirects\", async () => {\n    let t = setup({ routes: REDIRECT_ROUTES });\n\n    let nav1 = await t.navigate(\"/parent/child\", {\n      formMethod: \"post\",\n      formData: createFormData({}),\n    });\n\n    let nav2 = await nav1.actions.child.redirectReturn(\n      \"..?key=value#hash\",\n      undefined,\n      undefined,\n      [\"parent\"],\n    );\n    await nav2.loaders.parent.resolve(\"PARENT\");\n    expect(t.router.state).toMatchObject({\n      location: {\n        pathname: \"/parent\",\n        search: \"?key=value\",\n        hash: \"#hash\",\n      },\n      navigation: IDLE_NAVIGATION,\n      loaderData: {\n        parent: \"PARENT\",\n      },\n      errors: null,\n    });\n  });\n\n  it(\"processes external redirects if window is present (push)\", async () => {\n    let urls = [\n      \"http://remix.run/blog\",\n      \"https://remix.run/blog\",\n      \"//remix.run/blog\",\n      \"app://whatever\",\n    ];\n\n    for (let url of urls) {\n      let t = setup({ routes: REDIRECT_ROUTES });\n\n      let A = await t.navigate(\"/parent/child\", {\n        formMethod: \"post\",\n        formData: createFormData({}),\n      });\n\n      await A.actions.child.redirectReturn(url);\n      expect(t.window.location.assign).toHaveBeenCalledWith(url);\n      expect(t.window.location.replace).not.toHaveBeenCalled();\n    }\n  });\n\n  it(\"processes external redirects if window is present (replace)\", async () => {\n    let urls = [\n      \"http://remix.run/blog\",\n      \"https://remix.run/blog\",\n      \"//remix.run/blog\",\n      \"app://whatever\",\n    ];\n\n    for (let url of urls) {\n      let t = setup({ routes: REDIRECT_ROUTES });\n\n      let A = await t.navigate(\"/parent/child\", {\n        formMethod: \"post\",\n        formData: createFormData({}),\n        replace: true,\n      });\n\n      await A.actions.child.redirectReturn(url);\n      expect(t.window.location.replace).toHaveBeenCalledWith(url);\n      expect(t.window.location.assign).not.toHaveBeenCalled();\n    }\n  });\n\n  it(\"processes redirects with document reload if header is present (assign)\", async () => {\n    let t = setup({ routes: REDIRECT_ROUTES });\n\n    let A = await t.navigate(\"/parent/child\", {\n      formMethod: \"post\",\n      formData: createFormData({}),\n    });\n\n    await A.actions.child.redirectReturn(\"/redirect\", 301, {\n      \"X-Remix-Reload-Document\": \"true\",\n    });\n    expect(t.window.location.assign).toHaveBeenCalledWith(\"/redirect\");\n    expect(t.window.location.replace).not.toHaveBeenCalled();\n  });\n\n  it(\"processes redirects with document reload if header is present (replace)\", async () => {\n    let t = setup({ routes: REDIRECT_ROUTES });\n\n    let A = await t.navigate(\"/parent/child\", {\n      formMethod: \"post\",\n      formData: createFormData({}),\n      replace: true,\n    });\n\n    await A.actions.child.redirectReturn(\"/redirect\", 301, {\n      \"X-Remix-Reload-Document\": \"true\",\n    });\n    expect(t.window.location.replace).toHaveBeenCalledWith(\"/redirect\");\n    expect(t.window.location.assign).not.toHaveBeenCalled();\n  });\n\n  it(\"properly handles same-origin absolute URLs\", async () => {\n    let t = setup({ routes: REDIRECT_ROUTES });\n\n    let A = await t.navigate(\"/parent/child\", {\n      formMethod: \"post\",\n      formData: createFormData({}),\n    });\n\n    let B = await A.actions.child.redirectReturn(\n      \"http://localhost/parent\",\n      undefined,\n      undefined,\n      [\"parent\"],\n    );\n    await B.loaders.parent.resolve(\"PARENT\");\n    expect(t.router.state.location).toMatchObject({\n      hash: \"\",\n      pathname: \"/parent\",\n      search: \"\",\n      state: {\n        _isRedirect: true,\n      },\n    });\n  });\n\n  it(\"properly handles same-origin absolute URLs when using a basename\", async () => {\n    let t = setup({ routes: REDIRECT_ROUTES, basename: \"/base\" });\n\n    let A = await t.navigate(\"/parent/child\", {\n      formMethod: \"post\",\n      formData: createFormData({}),\n    });\n\n    let B = await A.actions.child.redirectReturn(\n      \"http://localhost/base/parent\",\n      undefined,\n      undefined,\n      [\"parent\"],\n    );\n    await B.loaders.parent.resolve(\"PARENT\");\n    expect(t.router.state.location).toMatchObject({\n      hash: \"\",\n      pathname: \"/base/parent\",\n      search: \"\",\n      state: {\n        _isRedirect: true,\n      },\n    });\n  });\n\n  it(\"treats same-origin absolute URLs as external if they don't match the basename\", async () => {\n    let t = setup({ routes: REDIRECT_ROUTES, basename: \"/base\" });\n\n    let A = await t.navigate(\"/parent/child\", {\n      formMethod: \"post\",\n      formData: createFormData({}),\n    });\n\n    let url = \"http://localhost/not/the/same/basename\";\n    await A.actions.child.redirectReturn(url);\n    expect(t.window.location.assign).toHaveBeenCalledWith(url);\n    expect(t.window.location.replace).not.toHaveBeenCalled();\n  });\n\n  it(\"automatically replaces in the history stack if you redirect to the same location (root relative)\", async () => {\n    let t = setup({ routes: REDIRECT_ROUTES });\n\n    let A = await t.navigate(\"/parent\");\n    await A.loaders.parent.resolve(\"PARENT\");\n\n    let B = await t.navigate(\"/parent\", {\n      formMethod: \"post\",\n      formData: createFormData({}),\n    });\n    let C = await B.actions.parent.redirectReturn(\"/parent\");\n    await C.loaders.parent.resolve(\"PARENT*\");\n\n    expect(t.router.state.location).toMatchObject({ pathname: \"/parent\" });\n\n    // Because we redirected to the current location it defaults to a replace\n    await t.router.navigate(-1);\n    expect(t.router.state.location).toMatchObject({ pathname: \"/\" });\n  });\n\n  it(\"automatically replaces in the history stack if you redirect to the same location (absolute)\", async () => {\n    let t = setup({ routes: REDIRECT_ROUTES });\n\n    let A = await t.navigate(\"/parent\");\n    await A.loaders.parent.resolve(\"PARENT\");\n\n    let B = await t.navigate(\"/parent\", {\n      formMethod: \"post\",\n      formData: createFormData({}),\n    });\n    let C = await B.actions.parent.redirectReturn(\n      \"http://localhost/parent\",\n      undefined,\n      undefined,\n      [\"parent\"],\n    );\n    await C.loaders.parent.resolve(\"PARENT*\");\n\n    expect(t.router.state.location).toMatchObject({ pathname: \"/parent\" });\n\n    // Because we redirected to the current location it defaults to a replace\n    await t.router.navigate(-1);\n    expect(t.router.state.location).toMatchObject({ pathname: \"/\" });\n  });\n\n  it(\"preserves action revalidation across multiple redirects\", async () => {\n    let t = setup({\n      initialEntries: [\"/action\"],\n      routes: [\n        {\n          id: \"action\",\n          path: \"/action\",\n          loader: true,\n          children: [\n            {\n              id: \"index\",\n              index: true,\n              action: true,\n              loader: true,\n            },\n            {\n              id: \"one\",\n              path: \"1\",\n              loader: true,\n            },\n            {\n              path: \"2\",\n            },\n          ],\n        },\n      ],\n      hydrationData: {\n        loaderData: {\n          action: \"ACTION 0\",\n          index: \"INDEX\",\n        },\n      },\n    });\n\n    let A = await t.navigate(\"/action?index\", {\n      formMethod: \"post\",\n      formData: createFormData({}),\n    });\n\n    let B = await A.actions.index.redirectReturn(\"/action/1\");\n    await B.loaders.action.resolve(\"ACTION 1\");\n    let C = await B.loaders.one.redirectReturn(\"/action/2\");\n    await C.loaders.action.resolve(\"ACTION 2\");\n\n    expect(t.router.state).toMatchObject({\n      location: {\n        pathname: \"/action/2\",\n      },\n      navigation: IDLE_NAVIGATION,\n      loaderData: {\n        action: \"ACTION 2\",\n      },\n    });\n  });\n\n  it(\"preserves X-Remix-Revalidate revalidation across multiple redirects\", async () => {\n    let t = setup({\n      initialEntries: [\"/loader\"],\n      routes: [\n        {\n          id: \"loader\",\n          path: \"/loader\",\n          loader: true,\n          children: [\n            {\n              id: \"index\",\n              index: true,\n            },\n            {\n              id: \"one\",\n              path: \"1\",\n              loader: true,\n            },\n            {\n              id: \"two\",\n              path: \"2\",\n              loader: true,\n            },\n            {\n              path: \"3\",\n            },\n          ],\n        },\n      ],\n      hydrationData: {\n        loaderData: {\n          loader: \"LOADER 0\",\n        },\n      },\n    });\n\n    let A = await t.navigate(\"/loader/1\");\n    let B = await A.loaders.one.redirectReturn(\"/loader/2\", undefined, {\n      \"X-Remix-Revalidate\": \"true\",\n    });\n    await B.loaders.loader.resolve(\"LOADER 3\");\n    let C = await B.loaders.two.redirectReturn(\"/loader/3\");\n    await C.loaders.loader.resolve(\"LOADER 3\");\n\n    expect(t.router.state).toMatchObject({\n      location: {\n        pathname: \"/loader/3\",\n      },\n      navigation: IDLE_NAVIGATION,\n      loaderData: {\n        loader: \"LOADER 3\",\n      },\n    });\n  });\n\n  it(\"supports replace() redirects\", async () => {\n    let router = createRouter({\n      history: createMemoryHistory(),\n      routes: [\n        {\n          path: \"/\",\n        },\n        {\n          path: \"/a\",\n        },\n        {\n          path: \"/b\",\n          loader: () => replace(\"/c\"),\n        },\n        {\n          path: \"/c\",\n        },\n      ],\n    });\n    router.initialize();\n    await tick();\n\n    // ['/']\n    expect(router.state).toMatchObject({\n      historyAction: \"POP\",\n      location: {\n        pathname: \"/\",\n        state: null,\n      },\n    });\n\n    // Push /a: ['/', '/a']\n    await router.navigate(\"/a\");\n    expect(router.state).toMatchObject({\n      historyAction: \"PUSH\",\n      location: {\n        pathname: \"/a\",\n        state: null,\n      },\n    });\n\n    // Push /b which calls replace('/c'): ['/', '/c']\n    await router.navigate(\"/b\");\n    expect(router.state).toMatchObject({\n      historyAction: \"REPLACE\",\n      location: {\n        pathname: \"/c\",\n        state: {\n          _isRedirect: true,\n        },\n      },\n    });\n\n    // Pop: ['/']\n    await router.navigate(-1);\n    expect(router.state).toMatchObject({\n      historyAction: \"POP\",\n      location: {\n        pathname: \"/\",\n        state: null,\n      },\n    });\n  });\n\n  describe(\"redirect status code handling\", () => {\n    it(\"should not treat 300 as a redirect\", async () => {\n      let t = setup({ routes: REDIRECT_ROUTES });\n\n      let A = await t.navigate(\"/parent\");\n      await A.loaders.parent.redirectReturn(\"/idk\", 300);\n      expect(t.router.state).toMatchObject({\n        loaderData: {},\n        location: {\n          pathname: \"/parent\",\n        },\n        navigation: {\n          state: \"idle\",\n        },\n      });\n    });\n\n    it(\"should not preserve the method on 301 redirects\", async () => {\n      let t = setup({ routes: REDIRECT_ROUTES });\n\n      let A = await t.navigate(\"/parent/child\", {\n        formMethod: \"post\",\n        formData: createFormData({ key: \"value\" }),\n      });\n      // Triggers a GET redirect\n      let B = await A.actions.child.redirectReturn(\"/parent\", 301);\n      await B.loaders.parent.resolve(\"PARENT\");\n      expect(t.router.state).toMatchObject({\n        loaderData: {\n          parent: \"PARENT\",\n        },\n        location: {\n          pathname: \"/parent\",\n        },\n        navigation: {\n          state: \"idle\",\n        },\n      });\n    });\n\n    it(\"should not preserve the method on 302 redirects\", async () => {\n      let t = setup({ routes: REDIRECT_ROUTES });\n\n      let A = await t.navigate(\"/parent/child\", {\n        formMethod: \"post\",\n        formData: createFormData({ key: \"value\" }),\n      });\n      // Triggers a GET redirect\n      let B = await A.actions.child.redirectReturn(\"/parent\", 302);\n      await B.loaders.parent.resolve(\"PARENT\");\n      expect(t.router.state).toMatchObject({\n        loaderData: {\n          parent: \"PARENT\",\n        },\n        location: {\n          pathname: \"/parent\",\n        },\n        navigation: {\n          state: \"idle\",\n        },\n      });\n    });\n\n    it(\"should not preserve the method on 303 redirects\", async () => {\n      let t = setup({ routes: REDIRECT_ROUTES });\n\n      let A = await t.navigate(\"/parent/child\", {\n        formMethod: \"post\",\n        formData: createFormData({ key: \"value\" }),\n      });\n      // Triggers a GET redirect\n      let B = await A.actions.child.redirectReturn(\"/parent\", 303);\n      await B.loaders.parent.resolve(\"PARENT\");\n      expect(t.router.state).toMatchObject({\n        loaderData: {\n          parent: \"PARENT\",\n        },\n        location: {\n          pathname: \"/parent\",\n        },\n        navigation: {\n          state: \"idle\",\n        },\n      });\n    });\n\n    it(\"should not treat 304 as a redirect\", async () => {\n      let t = setup({ routes: REDIRECT_ROUTES });\n\n      let A = await t.navigate(\"/parent\");\n      await A.loaders.parent.resolve(new Response(null, { status: 304 }));\n      expect(t.router.state).toMatchObject({\n        loaderData: {},\n        location: {\n          pathname: \"/parent\",\n        },\n        navigation: {\n          state: \"idle\",\n        },\n      });\n    });\n\n    it(\"should preserve the method on 307 redirects\", async () => {\n      let t = setup({ routes: REDIRECT_ROUTES });\n\n      let A = await t.navigate(\"/parent/child\", {\n        formMethod: \"post\",\n        formData: createFormData({ key: \"value\" }),\n      });\n      // Triggers a POST redirect\n      let B = await A.actions.child.redirectReturn(\"/parent\", 307);\n      await B.actions.parent.resolve(\"PARENT ACTION\");\n      await B.loaders.parent.resolve(\"PARENT\");\n      expect(t.router.state).toMatchObject({\n        actionData: {\n          parent: \"PARENT ACTION\",\n        },\n        loaderData: {\n          parent: \"PARENT\",\n        },\n        location: {\n          pathname: \"/parent\",\n        },\n        navigation: {\n          state: \"idle\",\n        },\n      });\n\n      let request = B.actions.parent.stub.mock.calls[0][0].request;\n      expect(request.method).toBe(\"POST\");\n      let fd = await request.formData();\n      expect(Array.from(fd.entries())).toEqual([[\"key\", \"value\"]]);\n    });\n\n    it(\"should preserve the method on 308 redirects\", async () => {\n      let t = setup({ routes: REDIRECT_ROUTES });\n\n      let A = await t.navigate(\"/parent/child\", {\n        formMethod: \"post\",\n        formData: createFormData({ key: \"value\" }),\n      });\n      // Triggers a POST redirect\n      let B = await A.actions.child.redirectReturn(\"/parent\", 308);\n      await B.actions.parent.resolve(\"PARENT ACTION\");\n      await B.loaders.parent.resolve(\"PARENT\");\n      expect(t.router.state).toMatchObject({\n        actionData: {\n          parent: \"PARENT ACTION\",\n        },\n        loaderData: {\n          parent: \"PARENT\",\n        },\n        location: {\n          pathname: \"/parent\",\n        },\n        navigation: {\n          state: \"idle\",\n        },\n      });\n\n      let request = B.actions.parent.stub.mock.calls[0][0].request;\n      expect(request.method).toBe(\"POST\");\n      let fd = await request.formData();\n      expect(Array.from(fd.entries())).toEqual([[\"key\", \"value\"]]);\n    });\n  });\n});\n"
  },
  {
    "path": "packages/react-router/__tests__/router/resolveTo-test.tsx",
    "content": "import { resolveTo } from \"../../lib/router/utils\";\n\ndescribe(\"resolveTo\", () => {\n  it('resolve path without mutating the \"to\" argument', () => {\n    const toArg = {\n      pathname: \"../../create\",\n    };\n\n    const routePathnames = [\"/\", \"/user\", \"/user/1\", \"/user/1/edit\"];\n    const locationPathname = \"/user/1/edit/\";\n\n    let resolvedPath = resolveTo(toArg, routePathnames, locationPathname);\n    expect(resolvedPath).toEqual({\n      pathname: \"/user/create\",\n      search: \"\",\n      hash: \"\",\n    });\n\n    resolvedPath = resolveTo(toArg, routePathnames, locationPathname);\n    expect(resolvedPath).toEqual({\n      pathname: \"/user/create\",\n      search: \"\",\n      hash: \"\",\n    });\n  });\n});\n"
  },
  {
    "path": "packages/react-router/__tests__/router/revalidate-test.ts",
    "content": "import { createMemoryRouter } from \"../../lib/components\";\nimport { IDLE_NAVIGATION } from \"../../lib/router/router\";\nimport {\n  cleanup,\n  setup,\n  createDeferred,\n  TASK_ROUTES,\n} from \"./utils/data-router-setup\";\nimport { createFormData } from \"./utils/utils\";\n\ndescribe(\"router.revalidate\", () => {\n  // Detect any failures inside the router navigate code\n  afterEach(() => cleanup());\n\n  it(\"handles uninterrupted revalidation in an idle state (from POP)\", async () => {\n    let t = setup({\n      routes: TASK_ROUTES,\n      initialEntries: [\"/\"],\n      hydrationData: {\n        loaderData: {\n          root: \"ROOT_DATA\",\n          index: \"INDEX_DATA\",\n        },\n      },\n    });\n\n    let key = t.router.state.location.key;\n    let R = await t.revalidate();\n    expect(t.router.state).toMatchObject({\n      historyAction: \"POP\",\n      location: { pathname: \"/\" },\n      navigation: IDLE_NAVIGATION,\n      revalidation: \"loading\",\n      loaderData: {\n        root: \"ROOT_DATA\",\n        index: \"INDEX_DATA\",\n      },\n    });\n\n    await R.loaders.root.resolve(\"ROOT_DATA*\");\n    await R.loaders.index.resolve(\"INDEX_DATA*\");\n    expect(t.router.state).toMatchObject({\n      historyAction: \"POP\",\n      location: { pathname: \"/\" },\n      navigation: IDLE_NAVIGATION,\n      revalidation: \"idle\",\n      loaderData: {\n        root: \"ROOT_DATA*\",\n        index: \"INDEX_DATA*\",\n      },\n    });\n    expect(t.router.state.location.key).toBe(key);\n    expect(t.history.push).not.toHaveBeenCalled();\n    expect(t.history.replace).not.toHaveBeenCalled();\n  });\n\n  it(\"handles uninterrupted revalidation in an idle state (from PUSH)\", async () => {\n    let t = setup({\n      routes: TASK_ROUTES,\n      initialEntries: [\"/\"],\n      hydrationData: {\n        loaderData: {\n          root: \"ROOT_DATA\",\n          index: \"INDEX_DATA\",\n        },\n      },\n    });\n\n    let N = await t.navigate(\"/\");\n    await N.loaders.root.resolve(\"ROOT_DATA\");\n    await N.loaders.index.resolve(\"INDEX_DATA\");\n    expect(t.router.state).toMatchObject({\n      historyAction: \"PUSH\",\n      location: { pathname: \"/\" },\n      navigation: IDLE_NAVIGATION,\n      revalidation: \"idle\",\n      loaderData: {\n        root: \"ROOT_DATA\",\n        index: \"INDEX_DATA\",\n      },\n    });\n    // @ts-expect-error\n    expect(t.history.push.mock.calls.length).toBe(1);\n\n    let key = t.router.state.location.key;\n    let R = await t.revalidate();\n    expect(t.router.state).toMatchObject({\n      historyAction: \"PUSH\",\n      location: { pathname: \"/\" },\n      navigation: IDLE_NAVIGATION,\n      revalidation: \"loading\",\n      loaderData: {\n        root: \"ROOT_DATA\",\n        index: \"INDEX_DATA\",\n      },\n    });\n\n    await R.loaders.root.resolve(\"ROOT_DATA*\");\n    await R.loaders.index.resolve(\"INDEX_DATA*\");\n    expect(t.router.state).toMatchObject({\n      historyAction: \"PUSH\",\n      location: { pathname: \"/\" },\n      navigation: IDLE_NAVIGATION,\n      revalidation: \"idle\",\n      loaderData: {\n        root: \"ROOT_DATA*\",\n        index: \"INDEX_DATA*\",\n      },\n    });\n    expect(t.router.state.location.key).toBe(key);\n    // @ts-ignore\n    expect(t.history.push.mock.calls.length).toBe(1);\n    expect(t.history.replace).not.toHaveBeenCalled();\n  });\n\n  it(\"handles revalidation when a hash is present\", async () => {\n    let t = setup({\n      routes: TASK_ROUTES,\n      initialEntries: [\"/#hash\"],\n      hydrationData: {\n        loaderData: {\n          root: \"ROOT_DATA\",\n          index: \"INDEX_DATA\",\n        },\n      },\n    });\n\n    let key = t.router.state.location.key;\n    let R = await t.revalidate();\n    expect(t.router.state).toMatchObject({\n      historyAction: \"POP\",\n      location: { pathname: \"/\" },\n      navigation: IDLE_NAVIGATION,\n      revalidation: \"loading\",\n      loaderData: {\n        root: \"ROOT_DATA\",\n        index: \"INDEX_DATA\",\n      },\n    });\n\n    await R.loaders.root.resolve(\"ROOT_DATA*\");\n    await R.loaders.index.resolve(\"INDEX_DATA*\");\n    expect(t.router.state).toMatchObject({\n      historyAction: \"POP\",\n      location: { pathname: \"/\" },\n      navigation: IDLE_NAVIGATION,\n      revalidation: \"idle\",\n      loaderData: {\n        root: \"ROOT_DATA*\",\n        index: \"INDEX_DATA*\",\n      },\n    });\n    expect(t.router.state.location.hash).toBe(\"#hash\");\n    expect(t.router.state.location.key).toBe(key);\n    expect(t.history.push).not.toHaveBeenCalled();\n    expect(t.history.replace).not.toHaveBeenCalled();\n  });\n\n  it(\"handles revalidation interrupted by a <Link> navigation\", async () => {\n    let t = setup({\n      routes: TASK_ROUTES,\n      initialEntries: [\"/\"],\n      hydrationData: {\n        loaderData: {\n          root: \"ROOT_DATA\",\n          index: \"INDEX_DATA\",\n        },\n      },\n    });\n\n    let R = await t.revalidate();\n    expect(t.router.state).toMatchObject({\n      historyAction: \"POP\",\n      location: { pathname: \"/\" },\n      navigation: IDLE_NAVIGATION,\n      revalidation: \"loading\",\n      loaderData: {\n        root: \"ROOT_DATA\",\n        index: \"INDEX_DATA\",\n      },\n    });\n\n    let N = await t.navigate(\"/tasks\");\n    // Revalidation was aborted\n    expect(R.loaders.root.signal.aborted).toBe(true);\n    expect(R.loaders.index.signal.aborted).toBe(true);\n    expect(t.router.state).toMatchObject({\n      historyAction: \"POP\",\n      location: { pathname: \"/\" },\n      navigation: {\n        state: \"loading\",\n        location: { pathname: \"/tasks\" },\n      },\n      revalidation: \"loading\",\n      loaderData: {\n        root: \"ROOT_DATA\",\n        index: \"INDEX_DATA\",\n      },\n    });\n\n    // Land the revalidation calls - should no-op\n    await R.loaders.root.resolve(\"ROOT_DATA interrupted\");\n    await R.loaders.index.resolve(\"INDEX_DATA interrupted\");\n    expect(t.router.state).toMatchObject({\n      historyAction: \"POP\",\n      location: { pathname: \"/\" },\n      navigation: {\n        state: \"loading\",\n        location: { pathname: \"/tasks\" },\n      },\n      revalidation: \"loading\",\n      loaderData: {\n        root: \"ROOT_DATA\",\n        index: \"INDEX_DATA\",\n      },\n    });\n\n    // Land the navigation calls - should update state and end the revalidation\n    await N.loaders.root.resolve(\"ROOT_DATA*\");\n    await N.loaders.tasks.resolve(\"TASKS_DATA\");\n    expect(t.router.state).toMatchObject({\n      historyAction: \"PUSH\",\n      location: { pathname: \"/tasks\" },\n      navigation: IDLE_NAVIGATION,\n      revalidation: \"idle\",\n      loaderData: {\n        root: \"ROOT_DATA*\",\n        tasks: \"TASKS_DATA\",\n      },\n    });\n    expect(t.history.push).toHaveBeenCalledWith(\n      t.router.state.location,\n      t.router.state.location.state,\n    );\n  });\n\n  it(\"handles revalidation interrupted by a <Form method=get> navigation\", async () => {\n    let t = setup({\n      routes: TASK_ROUTES,\n      initialEntries: [\"/\"],\n      hydrationData: {\n        loaderData: {\n          root: \"ROOT_DATA\",\n          index: \"INDEX_DATA\",\n        },\n      },\n    });\n\n    let R = await t.revalidate();\n    expect(t.router.state).toMatchObject({\n      historyAction: \"POP\",\n      location: { pathname: \"/\" },\n      navigation: IDLE_NAVIGATION,\n      revalidation: \"loading\",\n      loaderData: {\n        root: \"ROOT_DATA\",\n        index: \"INDEX_DATA\",\n      },\n    });\n\n    let N = await t.navigate(\"/tasks\", {\n      formMethod: \"get\",\n      formData: createFormData({ key: \"value\" }),\n    });\n    expect(t.router.state).toMatchObject({\n      historyAction: \"POP\",\n      location: { pathname: \"/\" },\n      navigation: {\n        state: \"loading\",\n        location: {\n          pathname: \"/tasks\",\n          search: \"?key=value\",\n        },\n        formMethod: \"GET\",\n        formData: createFormData({ key: \"value\" }),\n      },\n      revalidation: \"loading\",\n      loaderData: {\n        root: \"ROOT_DATA\",\n        index: \"INDEX_DATA\",\n      },\n    });\n    await R.loaders.root.resolve(\"ROOT_DATA interrupted\");\n    await R.loaders.index.resolve(\"INDEX_DATA interrupted\");\n    await N.loaders.root.resolve(\"ROOT_DATA*\");\n    await N.loaders.tasks.resolve(\"TASKS_DATA\");\n    expect(t.router.state).toMatchObject({\n      historyAction: \"PUSH\",\n      location: { pathname: \"/tasks\" },\n      navigation: IDLE_NAVIGATION,\n      revalidation: \"idle\",\n      loaderData: {\n        root: \"ROOT_DATA*\",\n        tasks: \"TASKS_DATA\",\n      },\n    });\n    expect(t.history.push).toHaveBeenCalledWith(\n      t.router.state.location,\n      t.router.state.location.state,\n    );\n  });\n\n  it(\"handles revalidation interrupted by a <Form method=post> navigation\", async () => {\n    let t = setup({\n      routes: TASK_ROUTES,\n      initialEntries: [\"/\"],\n      hydrationData: {\n        loaderData: {\n          root: \"ROOT_DATA\",\n          index: \"INDEX_DATA\",\n        },\n      },\n    });\n\n    let R = await t.revalidate();\n    expect(t.router.state).toMatchObject({\n      historyAction: \"POP\",\n      location: { pathname: \"/\" },\n      navigation: IDLE_NAVIGATION,\n      revalidation: \"loading\",\n      loaderData: {\n        root: \"ROOT_DATA\",\n        index: \"INDEX_DATA\",\n      },\n    });\n\n    let N = await t.navigate(\"/tasks\", {\n      formMethod: \"post\",\n      formData: createFormData({ key: \"value\" }),\n    });\n    expect(t.router.state).toMatchObject({\n      historyAction: \"POP\",\n      location: { pathname: \"/\" },\n      navigation: {\n        state: \"submitting\",\n        location: { pathname: \"/tasks\" },\n      },\n      revalidation: \"loading\",\n      loaderData: {\n        root: \"ROOT_DATA\",\n        index: \"INDEX_DATA\",\n      },\n    });\n\n    // Aborted by the navigation, resolving should no-op\n    expect(R.loaders.root.signal.aborted).toBe(true);\n    expect(R.loaders.index.signal.aborted).toBe(true);\n    await R.loaders.root.resolve(\"ROOT_DATA interrupted\");\n    await R.loaders.index.resolve(\"INDEX_DATA interrupted\");\n\n    await N.actions.tasks.resolve(\"TASKS_ACTION\");\n    expect(t.router.state).toMatchObject({\n      historyAction: \"POP\",\n      location: { pathname: \"/\" },\n      navigation: {\n        state: \"loading\",\n        location: { pathname: \"/tasks\" },\n      },\n      revalidation: \"loading\",\n      loaderData: {\n        root: \"ROOT_DATA\",\n        index: \"INDEX_DATA\",\n      },\n    });\n\n    await N.loaders.root.resolve(\"ROOT_DATA*\");\n    await N.loaders.tasks.resolve(\"TASKS_DATA\");\n    expect(t.router.state).toMatchObject({\n      historyAction: \"PUSH\",\n      location: { pathname: \"/tasks\" },\n      navigation: IDLE_NAVIGATION,\n      revalidation: \"idle\",\n      loaderData: {\n        root: \"ROOT_DATA*\",\n        tasks: \"TASKS_DATA\",\n      },\n      actionData: {\n        tasks: \"TASKS_ACTION\",\n      },\n    });\n    expect(t.history.push).toHaveBeenCalledWith(\n      t.router.state.location,\n      t.router.state.location.state,\n    );\n  });\n\n  it(\"handles <Link> navigation interrupted by a revalidation\", async () => {\n    let t = setup({\n      routes: TASK_ROUTES,\n      initialEntries: [\"/\"],\n      hydrationData: {\n        loaderData: {\n          root: \"ROOT_DATA\",\n          index: \"INDEX_DATA\",\n        },\n      },\n    });\n\n    let N = await t.navigate(\"/tasks\");\n    expect(N.loaders.root.stub).not.toHaveBeenCalled();\n    expect(N.loaders.tasks.stub).toHaveBeenCalled();\n    expect(t.router.state).toMatchObject({\n      historyAction: \"POP\",\n      location: { pathname: \"/\" },\n      navigation: { state: \"loading\" },\n      revalidation: \"idle\",\n      loaderData: {\n        root: \"ROOT_DATA\",\n        index: \"INDEX_DATA\",\n      },\n    });\n\n    let R = await t.revalidate();\n    expect(R.loaders.root.stub).toHaveBeenCalled();\n    expect(R.loaders.tasks.stub).toHaveBeenCalled();\n    expect(t.router.state).toMatchObject({\n      historyAction: \"POP\",\n      location: { pathname: \"/\" },\n      navigation: { state: \"loading\" },\n      revalidation: \"loading\",\n      loaderData: {\n        root: \"ROOT_DATA\",\n        index: \"INDEX_DATA\",\n      },\n    });\n\n    await N.loaders.tasks.resolve(\"TASKS_DATA interrupted\");\n    await R.loaders.root.resolve(\"ROOT_DATA*\");\n    await R.loaders.tasks.resolve(\"TASKS_DATA*\");\n    expect(t.router.state).toMatchObject({\n      historyAction: \"PUSH\",\n      location: { pathname: \"/tasks\" },\n      navigation: IDLE_NAVIGATION,\n      revalidation: \"idle\",\n      loaderData: {\n        root: \"ROOT_DATA*\",\n        tasks: \"TASKS_DATA*\",\n      },\n    });\n    expect(t.history.push).toHaveBeenCalledWith(\n      t.router.state.location,\n      t.router.state.location.state,\n    );\n  });\n\n  it(\"handles <Form method=get> navigation interrupted by a revalidation\", async () => {\n    let t = setup({\n      routes: TASK_ROUTES,\n      initialEntries: [\"/\"],\n      hydrationData: {\n        loaderData: {\n          root: \"ROOT_DATA\",\n          index: \"INDEX_DATA\",\n        },\n      },\n    });\n\n    let N = await t.navigate(\"/tasks\", {\n      formMethod: \"get\",\n      formData: createFormData({ key: \"value\" }),\n    });\n    // Called due to search param changing\n    expect(N.loaders.root.stub).toHaveBeenCalled();\n    expect(N.loaders.tasks.stub).toHaveBeenCalled();\n    expect(t.router.state).toMatchObject({\n      historyAction: \"POP\",\n      location: { pathname: \"/\" },\n      navigation: {\n        state: \"loading\",\n        location: {\n          pathname: \"/tasks\",\n          search: \"?key=value\",\n        },\n      },\n      revalidation: \"idle\",\n      loaderData: {\n        root: \"ROOT_DATA\",\n        index: \"INDEX_DATA\",\n      },\n    });\n\n    let R = await t.revalidate();\n    expect(R.loaders.root.stub).toHaveBeenCalled();\n    expect(R.loaders.tasks.stub).toHaveBeenCalled();\n    expect(t.router.state).toMatchObject({\n      historyAction: \"POP\",\n      location: { pathname: \"/\" },\n      navigation: {\n        state: \"loading\",\n        location: {\n          pathname: \"/tasks\",\n          search: \"?key=value\",\n        },\n      },\n      revalidation: \"loading\",\n      loaderData: {\n        root: \"ROOT_DATA\",\n        index: \"INDEX_DATA\",\n      },\n    });\n\n    await N.loaders.root.resolve(\"ROOT_DATA interrupted\");\n    await N.loaders.tasks.resolve(\"TASKS_DATA interrupted\");\n    await R.loaders.root.resolve(\"ROOT_DATA*\");\n    await R.loaders.tasks.resolve(\"TASKS_DATA*\");\n    expect(t.router.state).toMatchObject({\n      historyAction: \"PUSH\",\n      location: { pathname: \"/tasks\" },\n      navigation: IDLE_NAVIGATION,\n      revalidation: \"idle\",\n      loaderData: {\n        root: \"ROOT_DATA*\",\n        tasks: \"TASKS_DATA*\",\n      },\n    });\n    expect(t.history.push).toHaveBeenCalledWith(\n      t.router.state.location,\n      t.router.state.location.state,\n    );\n  });\n\n  it(\"handles <Form method=post> navigation interrupted by a revalidation during action phase\", async () => {\n    let t = setup({\n      routes: TASK_ROUTES,\n      initialEntries: [\"/\"],\n      hydrationData: {\n        loaderData: {\n          root: \"ROOT_DATA\",\n          index: \"INDEX_DATA\",\n        },\n      },\n    });\n\n    let N = await t.navigate(\"/tasks\", {\n      formMethod: \"post\",\n      formData: createFormData({ key: \"value\" }),\n    });\n    expect(t.router.state).toMatchObject({\n      historyAction: \"POP\",\n      location: { pathname: \"/\" },\n      navigation: { state: \"submitting\" },\n      revalidation: \"idle\",\n      loaderData: {\n        root: \"ROOT_DATA\",\n        index: \"INDEX_DATA\",\n      },\n    });\n\n    let R = await t.revalidate();\n    expect(t.router.state).toMatchObject({\n      historyAction: \"POP\",\n      location: { pathname: \"/\" },\n      navigation: { state: \"submitting\" },\n      revalidation: \"loading\",\n      loaderData: {\n        root: \"ROOT_DATA\",\n        index: \"INDEX_DATA\",\n      },\n    });\n\n    await N.actions.tasks.resolve(\"TASKS_ACTION\");\n    expect(t.router.state).toMatchObject({\n      historyAction: \"POP\",\n      location: { pathname: \"/\" },\n      navigation: { state: \"loading\" },\n      revalidation: \"loading\",\n      loaderData: {\n        root: \"ROOT_DATA\",\n        index: \"INDEX_DATA\",\n      },\n      actionData: {\n        tasks: \"TASKS_ACTION\",\n      },\n    });\n\n    await N.loaders.root.resolve(\"ROOT_DATA interrupted\");\n    await N.loaders.tasks.resolve(\"TASKS_DATA interrupted\");\n    await R.loaders.root.resolve(\"ROOT_DATA*\");\n    await R.loaders.tasks.resolve(\"TASKS_DATA*\");\n    expect(t.router.state).toMatchObject({\n      historyAction: \"PUSH\",\n      location: { pathname: \"/tasks\" },\n      navigation: IDLE_NAVIGATION,\n      revalidation: \"idle\",\n      loaderData: {\n        root: \"ROOT_DATA*\",\n        tasks: \"TASKS_DATA*\",\n      },\n    });\n    expect(t.history.push).toHaveBeenCalledWith(\n      t.router.state.location,\n      t.router.state.location.state,\n    );\n\n    // Action was not resubmitted\n    expect(N.actions.tasks.stub.mock.calls.length).toBe(1);\n    // This is sort of an implementation detail.  Internally we do not start\n    // a new navigation, but our helpers return the new \"loaders\" from the\n    // revalidate.  The key here is that together, loaders only got called once\n    expect(N.loaders.root.stub.mock.calls.length).toBe(0);\n    expect(N.loaders.tasks.stub.mock.calls.length).toBe(0);\n    expect(R.loaders.root.stub.mock.calls.length).toBe(1);\n    expect(R.loaders.tasks.stub.mock.calls.length).toBe(1);\n  });\n\n  it(\"handles <Form method=post> navigation interrupted by a revalidation during loading phase\", async () => {\n    let t = setup({\n      routes: TASK_ROUTES,\n      initialEntries: [\"/\"],\n      hydrationData: {\n        loaderData: {\n          root: \"ROOT_DATA\",\n          index: \"INDEX_DATA\",\n        },\n      },\n    });\n\n    let N = await t.navigate(\"/tasks\", {\n      formMethod: \"post\",\n      formData: createFormData({ key: \"value\" }),\n    });\n    expect(t.router.state).toMatchObject({\n      historyAction: \"POP\",\n      location: { pathname: \"/\" },\n      navigation: { state: \"submitting\" },\n      revalidation: \"idle\",\n      loaderData: {\n        root: \"ROOT_DATA\",\n        index: \"INDEX_DATA\",\n      },\n    });\n\n    await N.actions.tasks.resolve(\"TASKS_ACTION\");\n    expect(t.router.state).toMatchObject({\n      historyAction: \"POP\",\n      location: { pathname: \"/\" },\n      navigation: { state: \"loading\" },\n      revalidation: \"idle\",\n      loaderData: {\n        root: \"ROOT_DATA\",\n        index: \"INDEX_DATA\",\n      },\n      actionData: {\n        tasks: \"TASKS_ACTION\",\n      },\n    });\n\n    let R = await t.revalidate();\n    expect(t.router.state).toMatchObject({\n      historyAction: \"POP\",\n      location: { pathname: \"/\" },\n      navigation: { state: \"loading\" },\n      revalidation: \"loading\",\n      loaderData: {\n        root: \"ROOT_DATA\",\n        index: \"INDEX_DATA\",\n      },\n      actionData: {\n        tasks: \"TASKS_ACTION\",\n      },\n    });\n\n    await N.loaders.root.resolve(\"ROOT_DATA interrupted\");\n    await N.loaders.tasks.resolve(\"TASKS_DATA interrupted\");\n    await R.loaders.root.resolve(\"ROOT_DATA*\");\n    await R.loaders.tasks.resolve(\"TASKS_DATA*\");\n    expect(t.router.state).toMatchObject({\n      historyAction: \"PUSH\",\n      location: { pathname: \"/tasks\" },\n      navigation: IDLE_NAVIGATION,\n      revalidation: \"idle\",\n      loaderData: {\n        root: \"ROOT_DATA*\",\n        tasks: \"TASKS_DATA*\",\n      },\n      actionData: {\n        tasks: \"TASKS_ACTION\",\n      },\n    });\n    expect(t.history.push).toHaveBeenCalledWith(\n      t.router.state.location,\n      t.router.state.location.state,\n    );\n\n    // Action was not resubmitted\n    expect(N.actions.tasks.stub.mock.calls.length).toBe(1);\n    // Because we interrupted during the loading phase, all loaders got re-called\n    expect(N.loaders.root.stub.mock.calls.length).toBe(1);\n    expect(N.loaders.tasks.stub.mock.calls.length).toBe(1);\n    expect(R.loaders.root.stub.mock.calls.length).toBe(1);\n    expect(R.loaders.tasks.stub.mock.calls.length).toBe(1);\n  });\n\n  it(\"handles redirects returned from revalidations\", async () => {\n    let t = setup({\n      routes: TASK_ROUTES,\n      initialEntries: [\"/\"],\n      hydrationData: {\n        loaderData: {\n          root: \"ROOT_DATA\",\n          index: \"INDEX_DATA\",\n        },\n      },\n    });\n\n    let key = t.router.state.location.key;\n    let R = await t.revalidate();\n    expect(t.router.state).toMatchObject({\n      historyAction: \"POP\",\n      location: { pathname: \"/\" },\n      navigation: IDLE_NAVIGATION,\n      revalidation: \"loading\",\n      loaderData: {\n        root: \"ROOT_DATA\",\n        index: \"INDEX_DATA\",\n      },\n    });\n\n    await R.loaders.root.resolve(\"ROOT_DATA*\");\n    let N = await R.loaders.index.redirectReturn(\"/tasks\");\n    expect(t.router.state).toMatchObject({\n      historyAction: \"POP\",\n      location: { pathname: \"/\" },\n      navigation: {\n        state: \"loading\",\n      },\n      revalidation: \"loading\",\n      loaderData: {\n        root: \"ROOT_DATA\",\n        index: \"INDEX_DATA\",\n      },\n    });\n    expect(t.router.state.location.key).toBe(key);\n\n    await N.loaders.root.resolve(\"ROOT_DATA redirect\");\n    await N.loaders.tasks.resolve(\"TASKS_DATA\");\n    expect(t.router.state).toMatchObject({\n      historyAction: \"PUSH\",\n      location: { pathname: \"/tasks\" },\n      navigation: IDLE_NAVIGATION,\n      revalidation: \"idle\",\n      loaderData: {\n        root: \"ROOT_DATA redirect\",\n        tasks: \"TASKS_DATA\",\n      },\n    });\n    expect(t.router.state.location.key).not.toBe(key);\n\n    let B = await t.navigate(-1);\n    await B.loaders.index.resolve(\"INDEX_DATA 2\");\n    // PUSH on the revalidation redirect means back button takes us back to\n    // the page that triggered the revalidation redirect\n    expect(t.router.state).toMatchObject({\n      historyAction: \"POP\",\n      location: { pathname: \"/\" },\n      navigation: IDLE_NAVIGATION,\n      revalidation: \"idle\",\n      loaderData: {\n        root: \"ROOT_DATA redirect\",\n        index: \"INDEX_DATA 2\",\n      },\n    });\n    expect(t.router.state.location.key).toBe(key);\n  });\n\n  it(\"handles errors from revalidations\", async () => {\n    let t = setup({\n      routes: TASK_ROUTES,\n      initialEntries: [\"/\"],\n      hydrationData: {\n        loaderData: {\n          root: \"ROOT_DATA\",\n          index: \"INDEX_DATA\",\n        },\n      },\n    });\n\n    let key = t.router.state.location.key;\n    let R = await t.revalidate();\n    expect(t.router.state).toMatchObject({\n      historyAction: \"POP\",\n      location: { pathname: \"/\" },\n      navigation: IDLE_NAVIGATION,\n      revalidation: \"loading\",\n      loaderData: {\n        root: \"ROOT_DATA\",\n        index: \"INDEX_DATA\",\n      },\n    });\n\n    await R.loaders.root.reject(\"ROOT_ERROR\");\n    await R.loaders.index.resolve(\"INDEX_DATA*\");\n    expect(t.router.state).toMatchObject({\n      historyAction: \"POP\",\n      location: { pathname: \"/\" },\n      navigation: IDLE_NAVIGATION,\n      revalidation: \"idle\",\n      loaderData: {\n        index: \"INDEX_DATA*\",\n      },\n      errors: {\n        root: \"ROOT_ERROR\",\n      },\n    });\n    expect(t.router.state.location.key).toBe(key);\n  });\n\n  it(\"leverages shouldRevalidate on revalidation routes\", async () => {\n    let shouldRevalidate = jest.fn(({ nextUrl }) => {\n      return nextUrl.searchParams.get(\"reload\") === \"1\";\n    });\n    let t = setup({\n      routes: [\n        {\n          id: \"root\",\n          loader: true,\n          shouldRevalidate: (...args) => shouldRevalidate(...args),\n          children: [\n            {\n              id: \"index\",\n              index: true,\n              loader: true,\n              shouldRevalidate: (...args) => shouldRevalidate(...args),\n            },\n          ],\n        },\n      ],\n      initialEntries: [\"/?reload=0\"],\n      hydrationData: {\n        loaderData: {\n          root: \"ROOT_DATA\",\n          index: \"INDEX_DATA\",\n        },\n      },\n    });\n\n    let R = await t.revalidate();\n    expect(R.loaders.root.stub).not.toHaveBeenCalled();\n    expect(R.loaders.index.stub).not.toHaveBeenCalled();\n    expect(t.router.state).toMatchObject({\n      historyAction: \"POP\",\n      location: { pathname: \"/\" },\n      navigation: IDLE_NAVIGATION,\n      revalidation: \"idle\",\n      loaderData: {\n        root: \"ROOT_DATA\",\n        index: \"INDEX_DATA\",\n      },\n    });\n\n    let N = await t.navigate(\"/?reload=1\");\n    await N.loaders.root.resolve(\"ROOT_DATA*\");\n    await N.loaders.index.resolve(\"INDEX_DATA*\");\n    expect(t.router.state).toMatchObject({\n      historyAction: \"PUSH\",\n      location: { pathname: \"/\" },\n      navigation: IDLE_NAVIGATION,\n      revalidation: \"idle\",\n      loaderData: {\n        root: \"ROOT_DATA*\",\n        index: \"INDEX_DATA*\",\n      },\n    });\n\n    let R2 = await t.revalidate();\n    expect(t.router.state).toMatchObject({\n      historyAction: \"PUSH\",\n      location: { pathname: \"/\" },\n      navigation: IDLE_NAVIGATION,\n      revalidation: \"loading\",\n      loaderData: {\n        root: \"ROOT_DATA*\",\n        index: \"INDEX_DATA*\",\n      },\n    });\n\n    await R2.loaders.root.resolve(\"ROOT_DATA**\");\n    await R2.loaders.index.resolve(\"INDEX_DATA**\");\n    expect(t.router.state).toMatchObject({\n      historyAction: \"PUSH\",\n      location: { pathname: \"/\" },\n      navigation: IDLE_NAVIGATION,\n      revalidation: \"idle\",\n      loaderData: {\n        root: \"ROOT_DATA**\",\n        index: \"INDEX_DATA**\",\n      },\n    });\n  });\n\n  it(\"triggers revalidation on fetcher loads\", async () => {\n    let t = setup({\n      routes: TASK_ROUTES,\n      initialEntries: [\"/\"],\n      hydrationData: {\n        loaderData: {\n          root: \"ROOT_DATA\",\n          index: \"INDEX_DATA\",\n        },\n      },\n    });\n\n    let key = \"key\";\n    let F = await t.fetch(\"/\", key);\n    await F.loaders.root.resolve(\"ROOT_DATA*\");\n    expect(t.fetchers[key]).toMatchObject({\n      state: \"idle\",\n      data: \"ROOT_DATA*\",\n    });\n\n    let R = await t.revalidate();\n    await R.loaders.root.resolve(\"ROOT_DATA**\");\n    await R.loaders.index.resolve(\"INDEX_DATA\");\n    expect(t.fetchers[key]).toMatchObject({\n      state: \"idle\",\n      data: \"ROOT_DATA**\",\n    });\n  });\n\n  it(\"exposes a revalidation promise across navigations\", async () => {\n    let revalidationDfd = createDeferred();\n    let navigationDfd = createDeferred();\n    let isFirstCall = true;\n    let router = createMemoryRouter(\n      [\n        {\n          id: \"root\",\n          loader() {\n            if (isFirstCall) {\n              isFirstCall = false;\n              return revalidationDfd.promise;\n            } else {\n              return navigationDfd.promise;\n            }\n          },\n          children: [\n            {\n              index: true,\n            },\n            {\n              path: \"/page\",\n            },\n          ],\n        },\n      ],\n      {\n        hydrationData: {\n          loaderData: { root: \"ROOT\" },\n        },\n      },\n    );\n\n    let revalidationValue = null;\n    let navigationValue = null;\n\n    await router.initialize();\n    expect(router.state).toMatchObject({\n      location: { pathname: \"/\" },\n      navigation: { state: \"idle\" },\n      revalidation: \"idle\",\n      loaderData: { root: \"ROOT\" },\n    });\n    expect(isFirstCall).toBe(true);\n\n    // Start a revalidation, but don't resolve the revalidation loader call\n    router.revalidate().then(() => {\n      revalidationValue = router.state.loaderData.root;\n    });\n    expect(router.state).toMatchObject({\n      location: { pathname: \"/\" },\n      navigation: { state: \"idle\" },\n      revalidation: \"loading\",\n      loaderData: { root: \"ROOT\" },\n    });\n    expect(isFirstCall).toBe(false);\n\n    // Interrupt with a GET navigation\n    router.navigate(\"/page\").then(() => {\n      navigationValue = router.state.loaderData.root;\n    });\n    expect(router.state).toMatchObject({\n      location: { pathname: \"/\" },\n      navigation: { state: \"loading\" },\n      revalidation: \"loading\",\n      loaderData: { root: \"ROOT\" },\n    });\n    expect(revalidationValue).toBeNull();\n    expect(navigationValue).toBeNull();\n\n    // Complete the navigation, which should resolve the revalidation\n    await navigationDfd.resolve(\"ROOT*\");\n    expect(router.state).toMatchObject({\n      location: { pathname: \"/page\" },\n      navigation: { state: \"idle\" },\n      revalidation: \"idle\",\n      loaderData: { root: \"ROOT*\" },\n    });\n    expect(revalidationValue).toBe(\"ROOT*\");\n    expect(navigationValue).toBe(\"ROOT*\");\n\n    // no-op\n    revalidationDfd.reject();\n    expect(router.state).toMatchObject({\n      location: { pathname: \"/page\" },\n      navigation: { state: \"idle\" },\n      revalidation: \"idle\",\n      loaderData: { root: \"ROOT*\" },\n    });\n    expect(revalidationValue).toBe(\"ROOT*\");\n    expect(navigationValue).toBe(\"ROOT*\");\n  });\n\n  it(\"exposes a revalidation promise across multiple navigations\", async () => {\n    let revalidationDfd = createDeferred();\n    let navigationDfd = createDeferred();\n    let navigationDfd2 = createDeferred();\n    let count = 0;\n    let router = createMemoryRouter(\n      [\n        {\n          id: \"root\",\n          loader() {\n            count++;\n            if (count === 1) {\n              return revalidationDfd.promise;\n            } else if (count === 2) {\n              return navigationDfd.promise;\n            } else {\n              return navigationDfd2.promise;\n            }\n          },\n          children: [\n            {\n              index: true,\n            },\n            {\n              path: \"/page\",\n            },\n            {\n              path: \"/page2\",\n            },\n          ],\n        },\n      ],\n      {\n        hydrationData: {\n          loaderData: { root: \"ROOT\" },\n        },\n      },\n    );\n\n    let revalidationValue = null;\n    let navigationValue = null;\n    let navigationValue2 = null;\n\n    await router.initialize();\n    expect(router.state).toMatchObject({\n      location: { pathname: \"/\" },\n      navigation: { state: \"idle\" },\n      revalidation: \"idle\",\n      loaderData: { root: \"ROOT\" },\n    });\n    expect(count).toBe(0);\n\n    // Start a revalidation, but don't resolve the revalidation loader call\n    router.revalidate().then(() => {\n      revalidationValue = router.state.loaderData.root;\n    });\n    expect(router.state).toMatchObject({\n      location: { pathname: \"/\" },\n      navigation: { state: \"idle\" },\n      revalidation: \"loading\",\n      loaderData: { root: \"ROOT\" },\n    });\n    expect(count).toBe(1);\n\n    // Interrupt with a navigation\n    router.navigate(\"/page\").then(() => {\n      navigationValue = router.state.loaderData.root;\n    });\n    expect(router.state).toMatchObject({\n      location: { pathname: \"/\" },\n      navigation: { state: \"loading\" },\n      revalidation: \"loading\",\n      loaderData: { root: \"ROOT\" },\n    });\n    expect(count).toBe(2);\n    expect(revalidationValue).toBeNull();\n    expect(navigationValue).toBeNull();\n    expect(navigationValue2).toBeNull();\n\n    // Interrupt with another navigation\n    router.navigate(\"/page2\").then(() => {\n      navigationValue2 = router.state.loaderData.root;\n    });\n    expect(router.state).toMatchObject({\n      location: { pathname: \"/\" },\n      navigation: { state: \"loading\" },\n      revalidation: \"loading\",\n      loaderData: { root: \"ROOT\" },\n    });\n    expect(count).toBe(3);\n    expect(revalidationValue).toBeNull();\n    expect(navigationValue).toBeNull();\n    expect(navigationValue2).toBeNull();\n\n    // Complete the navigation, which should resolve the revalidation\n    await navigationDfd2.resolve(\"ROOT**\");\n    expect(router.state).toMatchObject({\n      location: { pathname: \"/page2\" },\n      navigation: { state: \"idle\" },\n      revalidation: \"idle\",\n      loaderData: { root: \"ROOT**\" },\n    });\n    // The interrupted revalidation hooks into the next completed navigation\n    // so it reflects the end state value\n    expect(revalidationValue).toBe(\"ROOT**\");\n    // The interim navigation gets interrupted and ends while the router still\n    // reflects the original value\n    expect(navigationValue).toBe(\"ROOT\");\n    expect(navigationValue2).toBe(\"ROOT**\");\n\n    // no-op\n    navigationDfd.reject();\n    revalidationDfd.reject();\n    expect(router.state).toMatchObject({\n      location: { pathname: \"/page2\" },\n      navigation: { state: \"idle\" },\n      revalidation: \"idle\",\n      loaderData: { root: \"ROOT**\" },\n    });\n    expect(revalidationValue).toBe(\"ROOT**\");\n    expect(navigationValue).toBe(\"ROOT\");\n    expect(navigationValue2).toBe(\"ROOT**\");\n  });\n});\n"
  },
  {
    "path": "packages/react-router/__tests__/router/route-fallback-test.ts",
    "content": "import type { LoaderFunction } from \"../../lib/router/utils\";\nimport type { Router } from \"../../lib/router/router\";\nimport { createMemoryHistory } from \"../../lib/router/history\";\nimport { IDLE_NAVIGATION, createRouter } from \"../../lib/router/router\";\n\nimport { urlMatch } from \"./utils/custom-matchers\";\nimport { createDeferred } from \"./utils/data-router-setup\";\nimport { tick } from \"./utils/utils\";\n\ninterface CustomMatchers<R = jest.Expect> {\n  urlMatch(url: string);\n  trackedPromise(data?: any, error?: any, aborted?: boolean): R;\n  deferredData(\n    done: boolean,\n    status?: number,\n    headers?: Record<string, string>,\n  ): R;\n}\n\ndeclare global {\n  namespace jest {\n    interface Expect extends CustomMatchers {}\n    interface Matchers<R> extends CustomMatchers<R> {}\n    interface InverseAsymmetricMatchers extends CustomMatchers {}\n  }\n}\n\nexpect.extend({\n  urlMatch,\n});\n\nlet router: Router;\n\n// Detect any failures inside the router navigate code\nafterEach(() => {\n  router.dispose();\n});\n\ndescribe(\"route HydrateFallback\", () => {\n  it(\"starts with initialized=false, runs unhydrated loaders with partial hydrationData\", async () => {\n    let spy = jest.fn();\n    let shouldRevalidateSpy = jest.fn((args) => args.defaultShouldRevalidate);\n    let dfd = createDeferred();\n    router = createRouter({\n      routes: [\n        {\n          id: \"root\",\n          path: \"/\",\n          loader: spy,\n          shouldRevalidate: shouldRevalidateSpy,\n          children: [\n            {\n              id: \"index\",\n              index: true,\n              loader: () => dfd.promise,\n            },\n          ],\n        },\n      ],\n      history: createMemoryHistory(),\n      hydrationData: {\n        loaderData: {\n          root: \"LOADER DATA\",\n          // No loaderData provided for index route\n        },\n      },\n    });\n\n    let subscriberSpy = jest.fn();\n    router.subscribe(subscriberSpy);\n\n    // Start with initialized:false\n    expect(router.state).toMatchObject({\n      historyAction: \"POP\",\n      location: { pathname: \"/\" },\n      loaderData: { root: \"LOADER DATA\" },\n      initialized: false,\n      navigation: { state: \"idle\" },\n    });\n\n    // Initialize/kick off data loads due to partial hydrationData\n    router.initialize();\n    await dfd.resolve(\"INDEX DATA\");\n    expect(router.state).toMatchObject({\n      historyAction: \"POP\",\n      location: { pathname: \"/\" },\n      loaderData: { root: \"LOADER DATA\", index: \"INDEX DATA\" },\n      initialized: true,\n      navigation: { state: \"idle\" },\n    });\n\n    // Root was not re-called\n    expect(shouldRevalidateSpy).not.toHaveBeenCalled();\n    expect(spy).not.toHaveBeenCalled();\n\n    // Ensure we don't go into a navigating state during initial calls of\n    // the loaders\n    expect(subscriberSpy).toHaveBeenCalledTimes(1);\n    expect(subscriberSpy.mock.calls[0][0]).toMatchObject({\n      loaderData: {\n        index: \"INDEX DATA\",\n        root: \"LOADER DATA\",\n      },\n      navigation: IDLE_NAVIGATION,\n    });\n  });\n\n  it(\"starts with initialized=false, runs hydrated loaders when loader.hydrate=true\", async () => {\n    let spy = jest.fn();\n    let shouldRevalidateSpy = jest.fn((args) => args.defaultShouldRevalidate);\n    let dfd = createDeferred();\n    let indexLoader: LoaderFunction = () => dfd.promise;\n    indexLoader.hydrate = true;\n    router = createRouter({\n      routes: [\n        {\n          id: \"root\",\n          path: \"/\",\n          loader: spy,\n          shouldRevalidate: shouldRevalidateSpy,\n          children: [\n            {\n              id: \"index\",\n              index: true,\n              loader: indexLoader,\n            },\n          ],\n        },\n      ],\n      history: createMemoryHistory(),\n      hydrationData: {\n        loaderData: {\n          root: \"LOADER DATA\",\n          index: \"INDEX INITIAL\",\n        },\n      },\n    });\n\n    let subscriberSpy = jest.fn();\n    router.subscribe(subscriberSpy);\n\n    // Start with initialized:false\n    expect(router.state).toMatchObject({\n      historyAction: \"POP\",\n      location: { pathname: \"/\" },\n      loaderData: {\n        root: \"LOADER DATA\",\n        index: \"INDEX INITIAL\",\n      },\n      initialized: false,\n      navigation: { state: \"idle\" },\n    });\n\n    // Initialize/kick off data loads due to partial hydrationData\n    router.initialize();\n    await dfd.resolve(\"INDEX UPDATED\");\n    expect(router.state).toMatchObject({\n      historyAction: \"POP\",\n      location: { pathname: \"/\" },\n      loaderData: {\n        root: \"LOADER DATA\",\n        index: \"INDEX UPDATED\",\n      },\n      initialized: true,\n      navigation: { state: \"idle\" },\n    });\n\n    // Root was not re-called\n    expect(shouldRevalidateSpy).not.toHaveBeenCalled();\n    expect(spy).not.toHaveBeenCalled();\n\n    // Ensure we don't go into a navigating state during initial calls of\n    // the loaders\n    expect(subscriberSpy).toHaveBeenCalledTimes(1);\n    expect(subscriberSpy.mock.calls[0][0]).toMatchObject({\n      loaderData: {\n        index: \"INDEX UPDATED\",\n        root: \"LOADER DATA\",\n      },\n      navigation: IDLE_NAVIGATION,\n    });\n  });\n\n  it(\"does not kick off initial data load if errors exist (parent error)\", async () => {\n    let consoleWarnSpy = jest\n      .spyOn(console, \"warn\")\n      .mockImplementation(() => {});\n    let parentDfd = createDeferred();\n    let parentSpy = jest.fn(() => parentDfd.promise);\n    let childDfd = createDeferred();\n    let childSpy = jest.fn(() => childDfd.promise);\n    router = createRouter({\n      history: createMemoryHistory({ initialEntries: [\"/child\"] }),\n      routes: [\n        {\n          path: \"/\",\n          loader: parentSpy,\n          children: [\n            {\n              path: \"child\",\n              loader: childSpy,\n            },\n          ],\n        },\n      ],\n      hydrationData: {\n        errors: {\n          \"0\": \"PARENT ERROR\",\n        },\n        loaderData: {\n          \"0-0\": \"CHILD_DATA\",\n        },\n      },\n    });\n    router.initialize();\n\n    expect(consoleWarnSpy).not.toHaveBeenCalled();\n    expect(parentSpy).not.toHaveBeenCalled();\n    expect(childSpy).not.toHaveBeenCalled();\n    expect(router.state).toMatchObject({\n      historyAction: \"POP\",\n      location: expect.objectContaining({ pathname: \"/child\" }),\n      matches: [{ route: { path: \"/\" } }, { route: { path: \"child\" } }],\n      initialized: true,\n      navigation: IDLE_NAVIGATION,\n      errors: {\n        \"0\": \"PARENT ERROR\",\n      },\n      loaderData: {\n        \"0-0\": \"CHILD_DATA\",\n      },\n    });\n\n    consoleWarnSpy.mockReset();\n  });\n\n  it(\"does not kick off initial data load if errors exist (bubbled child error)\", async () => {\n    let consoleWarnSpy = jest\n      .spyOn(console, \"warn\")\n      .mockImplementation(() => {});\n    let parentDfd = createDeferred();\n    let parentSpy = jest.fn(() => parentDfd.promise);\n    let childDfd = createDeferred();\n    let childSpy = jest.fn(() => childDfd.promise);\n    router = createRouter({\n      history: createMemoryHistory({ initialEntries: [\"/child\"] }),\n      routes: [\n        {\n          path: \"/\",\n          loader: parentSpy,\n          children: [\n            {\n              path: \"child\",\n              loader: childSpy,\n            },\n          ],\n        },\n      ],\n      hydrationData: {\n        errors: {\n          \"0\": \"CHILD ERROR\",\n        },\n        loaderData: {\n          \"0\": \"PARENT DATA\",\n        },\n      },\n    });\n    router.initialize();\n\n    expect(consoleWarnSpy).not.toHaveBeenCalled();\n    expect(parentSpy).not.toHaveBeenCalled();\n    expect(childSpy).not.toHaveBeenCalled();\n    expect(router.state).toMatchObject({\n      historyAction: \"POP\",\n      location: expect.objectContaining({ pathname: \"/child\" }),\n      matches: [{ route: { path: \"/\" } }, { route: { path: \"child\" } }],\n      initialized: true,\n      navigation: IDLE_NAVIGATION,\n      errors: {\n        \"0\": \"CHILD ERROR\",\n      },\n      loaderData: {\n        \"0\": \"PARENT DATA\",\n      },\n    });\n\n    consoleWarnSpy.mockReset();\n  });\n\n  it(\"does not kick off initial data load for routes that don't have loaders\", async () => {\n    let consoleWarnSpy = jest\n      .spyOn(console, \"warn\")\n      .mockImplementation(() => {});\n    let parentDfd = createDeferred();\n    let parentSpy = jest.fn(() => parentDfd.promise);\n    router = createRouter({\n      history: createMemoryHistory({ initialEntries: [\"/child\"] }),\n      routes: [\n        {\n          path: \"/\",\n          loader: parentSpy,\n          children: [\n            {\n              path: \"child\",\n            },\n          ],\n        },\n      ],\n      hydrationData: {\n        loaderData: {\n          \"0\": \"PARENT DATA\",\n        },\n      },\n    });\n    expect(router.state).toMatchObject({\n      // already initialized so calling initialize() won't kick off loaders\n      initialized: true,\n      navigation: IDLE_NAVIGATION,\n      loaderData: {\n        \"0\": \"PARENT DATA\",\n      },\n    });\n\n    consoleWarnSpy.mockReset();\n  });\n\n  it(\"does not kick off initial data loads below SSR error boundaries (child throw)\", async () => {\n    let parentCount = 0;\n    let childCount = 0;\n    let routes = [\n      {\n        id: \"parent\",\n        path: \"/\",\n        loader: () => `PARENT ${++parentCount}`,\n        hasErrorBoundary: true,\n        children: [\n          {\n            path: \"child\",\n            loader: () => `CHILD ${++childCount}`,\n          },\n        ],\n      },\n    ];\n\n    // @ts-expect-error\n    routes[0].loader.hydrate = true;\n    // @ts-expect-error\n    routes[0].children[0].loader.hydrate = true;\n\n    router = createRouter({\n      history: createMemoryHistory({ initialEntries: [\"/child\"] }),\n      routes,\n      hydrationData: {\n        loaderData: {\n          parent: \"PARENT 0\",\n        },\n        errors: {\n          // Child threw and bubbled to parent\n          parent: \"CHILD SSR ERROR\",\n        },\n      },\n    }).initialize();\n    expect(router.state).toMatchObject({\n      initialized: false,\n      navigation: IDLE_NAVIGATION,\n      loaderData: {\n        parent: \"PARENT 0\",\n      },\n      errors: {\n        parent: \"CHILD SSR ERROR\",\n      },\n    });\n    await tick();\n    expect(router.state).toMatchObject({\n      initialized: true,\n      navigation: IDLE_NAVIGATION,\n      loaderData: {\n        parent: \"PARENT 1\",\n      },\n      errors: {\n        parent: \"CHILD SSR ERROR\",\n      },\n    });\n\n    expect(parentCount).toBe(1);\n    expect(childCount).toBe(0);\n  });\n\n  it(\"does not kick off initial data loads at SSR error boundaries (boundary throw)\", async () => {\n    let parentCount = 0;\n    let childCount = 0;\n    let routes = [\n      {\n        id: \"parent\",\n        path: \"/\",\n        loader: () => `PARENT ${++parentCount}`,\n        hasErrorBoundary: true,\n        children: [\n          {\n            path: \"child\",\n            loader: () => `CHILD ${++childCount}`,\n          },\n        ],\n      },\n    ];\n\n    // @ts-expect-error\n    routes[0].loader.hydrate = true;\n    // @ts-expect-error\n    routes[0].children[0].loader.hydrate = true;\n\n    router = createRouter({\n      history: createMemoryHistory({ initialEntries: [\"/child\"] }),\n      routes,\n      hydrationData: {\n        loaderData: {},\n        errors: {\n          // Parent threw\n          parent: \"PARENT SSR ERROR\",\n        },\n      },\n    }).initialize();\n\n    expect(router.state).toMatchObject({\n      initialized: true,\n      navigation: IDLE_NAVIGATION,\n      loaderData: {},\n      errors: {\n        parent: \"PARENT SSR ERROR\",\n      },\n    });\n\n    expect(parentCount).toBe(0);\n    expect(childCount).toBe(0);\n  });\n});\n"
  },
  {
    "path": "packages/react-router/__tests__/router/router-memory-test.ts",
    "content": "/**\n * @jest-environment node\n */\n\nimport { createMemoryHistory } from \"../../lib/router/history\";\nimport { createRouter } from \"../../lib/router/router\";\n\n// This suite of tests specifically runs in the node jest environment to catch\n// issues when window is not present\ndescribe(\"a memory router\", () => {\n  it(\"initializes properly\", async () => {\n    let router = createRouter({\n      routes: [\n        {\n          path: \"/\",\n        },\n      ],\n      history: createMemoryHistory(),\n    });\n    expect(router.state).toEqual({\n      historyAction: \"POP\",\n      loaderData: {},\n      actionData: null,\n      errors: null,\n      location: {\n        hash: \"\",\n        key: expect.any(String),\n        pathname: \"/\",\n        search: \"\",\n        state: null,\n      },\n      matches: [\n        {\n          params: {},\n          pathname: \"/\",\n          pathnameBase: \"/\",\n          route: {\n            id: \"0\",\n            hasErrorBoundary: false,\n            path: \"/\",\n          },\n        },\n      ],\n      initialized: true,\n      renderFallback: false,\n      navigation: {\n        location: undefined,\n        state: \"idle\",\n      },\n      preventScrollReset: false,\n      restoreScrollPosition: null,\n      revalidation: \"idle\",\n      fetchers: new Map(),\n      blockers: new Map(),\n    });\n    router.dispose();\n  });\n\n  it(\"can create Requests without window\", async () => {\n    let loaderSpy = jest.fn();\n    let router = createRouter({\n      routes: [\n        {\n          path: \"/\",\n        },\n        {\n          path: \"/a\",\n          loader: loaderSpy,\n        },\n      ],\n      history: createMemoryHistory(),\n    });\n\n    router.navigate(\"/a\");\n    expect(loaderSpy.mock.calls[0][0].request.url).toBe(\"http://localhost/a\");\n    router.dispose();\n  });\n\n  it(\"can create URLs without window\", async () => {\n    let shouldRevalidateSpy = jest.fn();\n\n    let router = createRouter({\n      routes: [\n        {\n          path: \"/\",\n          loader: () => \"ROOT\",\n          shouldRevalidate: shouldRevalidateSpy,\n          children: [\n            {\n              index: true,\n            },\n            {\n              path: \"a\",\n            },\n          ],\n        },\n      ],\n      history: createMemoryHistory(),\n      hydrationData: { loaderData: { \"0\": \"ROOT\" } },\n    });\n\n    router.navigate(\"/a\");\n    expect(shouldRevalidateSpy.mock.calls[0][0].currentUrl.toString()).toBe(\n      \"http://localhost/\",\n    );\n    expect(shouldRevalidateSpy.mock.calls[0][0].nextUrl.toString()).toBe(\n      \"http://localhost/a\",\n    );\n    router.dispose();\n  });\n\n  it(\"properly handles same-origin absolute URLs\", async () => {\n    let router = createRouter({\n      routes: [\n        {\n          path: \"/\",\n          children: [\n            {\n              index: true,\n            },\n            {\n              path: \"a\",\n              loader: () =>\n                new Response(null, {\n                  status: 302,\n                  headers: {\n                    Location: \"http://localhost/b\",\n                  },\n                }),\n            },\n            {\n              path: \"b\",\n            },\n          ],\n        },\n      ],\n      history: createMemoryHistory(),\n    });\n\n    await router.navigate(\"/a\");\n    expect(router.state.location).toMatchObject({\n      hash: \"\",\n      pathname: \"/b\",\n      search: \"\",\n    });\n  });\n\n  it(\"properly handles protocol-less same-origin absolute URLs\", async () => {\n    let router = createRouter({\n      routes: [\n        {\n          path: \"/\",\n          children: [\n            {\n              index: true,\n            },\n            {\n              path: \"a\",\n              loader: () =>\n                new Response(null, {\n                  status: 302,\n                  headers: {\n                    Location: \"//localhost/b\",\n                  },\n                }),\n            },\n            {\n              path: \"b\",\n            },\n          ],\n        },\n      ],\n      history: createMemoryHistory(),\n    });\n\n    await router.navigate(\"/a\");\n    expect(router.state.location).toMatchObject({\n      hash: \"\",\n      pathname: \"/b\",\n      search: \"\",\n    });\n  });\n\n  it(\"can submit non-FormData values without window\", async () => {\n    let actionSpy = jest.fn();\n\n    let router = createRouter({\n      routes: [\n        {\n          path: \"/\",\n          action: actionSpy,\n        },\n      ],\n      history: createMemoryHistory(),\n    });\n\n    router.navigate(\"/\", {\n      formMethod: \"post\",\n      formEncType: \"application/json\",\n      body: { key: \"value\" },\n    });\n    let request = actionSpy.mock.calls[0][0].request;\n    expect(await request.json()).toEqual({ key: \"value\" });\n\n    router.navigate(\"/\", {\n      formMethod: \"post\",\n      formEncType: \"text/plain\",\n      body: \"body\",\n    });\n    request = actionSpy.mock.calls[1][0].request;\n    expect(await request.text()).toEqual(\"body\");\n\n    router.dispose();\n  });\n});\n"
  },
  {
    "path": "packages/react-router/__tests__/router/router-test.ts",
    "content": "import type { HydrationState } from \"../../lib/router/router\";\nimport { createMemoryHistory } from \"../../lib/router/history\";\nimport { createRouter, IDLE_NAVIGATION } from \"../../lib/router/router\";\nimport type {\n  AgnosticDataRouteObject,\n  AgnosticRouteObject,\n} from \"../../lib/router/utils\";\nimport { data, ErrorResponseImpl, redirect } from \"../../lib/router/utils\";\n\nimport { urlMatch } from \"./utils/custom-matchers\";\nimport {\n  cleanup,\n  createDeferred,\n  getFetcherData,\n  setup,\n  TASK_ROUTES,\n} from \"./utils/data-router-setup\";\nimport { createFormData, tick } from \"./utils/utils\";\n\ninterface CustomMatchers<R = jest.Expect> {\n  urlMatch(url: string): R;\n}\n\ndeclare global {\n  namespace jest {\n    interface Expect extends CustomMatchers {}\n    interface Matchers<R> extends CustomMatchers<R> {}\n    interface InverseAsymmetricMatchers extends CustomMatchers {}\n  }\n}\n\nexpect.extend({\n  urlMatch,\n});\n\nfunction initializeTest(init?: {\n  url?: string;\n  hydrationData?: HydrationState;\n}) {\n  return setup({\n    routes: [\n      {\n        path: \"\",\n        id: \"root\",\n        hasErrorBoundary: true,\n        loader: true,\n        children: [\n          {\n            path: \"/\",\n            id: \"index\",\n            loader: true,\n            action: true,\n          },\n          {\n            path: \"/foo\",\n            id: \"foo\",\n            loader: true,\n            action: true,\n          },\n          {\n            path: \"/bar\",\n            id: \"bar\",\n            loader: true,\n            action: true,\n          },\n          {\n            path: \"/no-loader\",\n            id: \"noLoader\",\n          },\n        ],\n      },\n    ],\n    hydrationData: init?.hydrationData || {\n      loaderData: { root: \"ROOT\", index: \"INDEX\" },\n    },\n    ...(init?.url ? { initialEntries: [init.url] } : {}),\n  });\n}\n\nbeforeEach(() => {\n  jest.spyOn(console, \"warn\").mockImplementation(() => {});\n});\n\n// Detect any failures inside the router navigate code\nafterEach(() => {\n  cleanup();\n\n  // @ts-ignore\n  console.warn.mockReset();\n});\n\ndescribe(\"a router\", () => {\n  describe(\"init\", () => {\n    it(\"requires routes\", async () => {\n      let history = createMemoryHistory({ initialEntries: [\"/\"] });\n      expect(() =>\n        createRouter({\n          routes: [],\n          history,\n          hydrationData: {},\n        }),\n      ).toThrowErrorMatchingInlineSnapshot(\n        `\"You must provide a non-empty routes array to createRouter\"`,\n      );\n    });\n\n    it(\"converts routes to data routes\", async () => {\n      let history = createMemoryHistory({\n        initialEntries: [\"/child/grandchild\"],\n      });\n      let routes = [\n        {\n          path: \"/\",\n          children: [\n            {\n              id: \"child-keep-me\",\n              path: \"child\",\n              children: [\n                {\n                  path: \"grandchild\",\n                },\n              ],\n            },\n          ],\n        },\n      ];\n      let originalRoutes = JSON.parse(JSON.stringify(routes));\n      let router = createRouter({\n        routes,\n        history,\n        hydrationData: {},\n      });\n      // routes are not mutated in place\n      expect(routes).toEqual(originalRoutes);\n      expect(router.state.matches).toMatchObject([\n        {\n          route: {\n            id: \"0\",\n          },\n        },\n        {\n          route: {\n            id: \"child-keep-me\",\n          },\n        },\n        {\n          route: {\n            id: \"0-0-0\",\n          },\n        },\n      ]);\n    });\n\n    it(\"throws if it finds duplicate route ids\", async () => {\n      let history = createMemoryHistory({\n        initialEntries: [\"/child/grandchild\"],\n      });\n      let routes = [\n        {\n          path: \"/\",\n          children: [\n            {\n              id: \"child\",\n              path: \"child\",\n              children: [\n                {\n                  id: \"child\",\n                  path: \"grandchild\",\n                },\n              ],\n            },\n          ],\n        },\n      ];\n      expect(() =>\n        createRouter({\n          routes,\n          history,\n          hydrationData: {},\n        }),\n      ).toThrowErrorMatchingInlineSnapshot(\n        `\"Found a route id collision on id \"child\".  Route id's must be globally unique within Data Router usages\"`,\n      );\n    });\n\n    it(\"throws if it finds index routes with children\", async () => {\n      let routes: AgnosticRouteObject[] = [\n        // @ts-expect-error\n        {\n          index: true,\n          children: [\n            {\n              path: \"nope\",\n            },\n          ],\n        },\n      ];\n      expect(() =>\n        createRouter({\n          routes,\n          history: createMemoryHistory(),\n        }),\n      ).toThrowErrorMatchingInlineSnapshot(\n        `\"Cannot specify children on an index route\"`,\n      );\n    });\n\n    it(\"supports a basename prop for route matching\", async () => {\n      let history = createMemoryHistory({\n        initialEntries: [\"/base/name/path\"],\n      });\n      let router = createRouter({\n        basename: \"/base/name\",\n        routes: [{ path: \"path\" }],\n        history,\n      });\n      expect(router.state).toMatchObject({\n        location: {\n          hash: \"\",\n          key: expect.any(String),\n          pathname: \"/base/name/path\",\n          search: \"\",\n          state: null,\n        },\n        matches: [\n          {\n            params: {},\n            pathname: \"/path\",\n            pathnameBase: \"/path\",\n            route: {\n              id: \"0\",\n              path: \"path\",\n            },\n          },\n        ],\n        initialized: true,\n      });\n    });\n\n    it(\"supports a basename prop for route matching without a leading slash\", async () => {\n      let history = createMemoryHistory({\n        initialEntries: [\"/base/name/path\"],\n      });\n      let router = createRouter({\n        basename: \"base/name\",\n        routes: [{ path: \"path\" }],\n        history,\n      });\n      expect(router.state).toMatchObject({\n        location: {\n          hash: \"\",\n          key: expect.any(String),\n          pathname: \"/base/name/path\",\n          search: \"\",\n          state: null,\n        },\n        matches: [\n          {\n            params: {},\n            pathname: \"/path\",\n            pathnameBase: \"/path\",\n            route: {\n              id: \"0\",\n              path: \"path\",\n            },\n          },\n        ],\n        initialized: true,\n      });\n    });\n\n    it(\"supports subscribers\", async () => {\n      let history = createMemoryHistory({ initialEntries: [\"/\"] });\n      let count = 0;\n      let router = createRouter({\n        routes: [\n          {\n            id: \"root\",\n            path: \"/\",\n            hasErrorBoundary: true,\n            loader: () => ++count,\n          },\n        ],\n        history,\n        hydrationData: {\n          loaderData: { root: 0 },\n        },\n      }).initialize();\n      expect(router.state.loaderData).toEqual({\n        root: 0,\n      });\n\n      let subscriber = jest.fn();\n      let unsubscribe = router.subscribe(subscriber);\n      let subscriber2 = jest.fn();\n      let unsubscribe2 = router.subscribe(subscriber2);\n\n      await router.navigate(\"/?key=a\");\n      expect(subscriber.mock.calls[0][0].navigation.state).toBe(\"loading\");\n      expect(subscriber.mock.calls[0][0].navigation.location.search).toBe(\n        \"?key=a\",\n      );\n      expect(subscriber.mock.calls[1][0].navigation.state).toBe(\"idle\");\n      expect(subscriber.mock.calls[1][0].location.search).toBe(\"?key=a\");\n      expect(subscriber2.mock.calls[0][0].navigation.state).toBe(\"loading\");\n      expect(subscriber2.mock.calls[0][0].navigation.location.search).toBe(\n        \"?key=a\",\n      );\n      expect(subscriber2.mock.calls[1][0].navigation.state).toBe(\"idle\");\n      expect(subscriber2.mock.calls[1][0].location.search).toBe(\"?key=a\");\n\n      unsubscribe2();\n      await router.navigate(\"/?key=b\");\n      expect(subscriber.mock.calls[2][0].navigation.state).toBe(\"loading\");\n      expect(subscriber.mock.calls[2][0].navigation.location.search).toBe(\n        \"?key=b\",\n      );\n      expect(subscriber.mock.calls[3][0].navigation.state).toBe(\"idle\");\n      expect(subscriber.mock.calls[3][0].location.search).toBe(\"?key=b\");\n\n      unsubscribe();\n      await router.navigate(\"/?key=c\");\n      expect(subscriber).toHaveBeenCalledTimes(4);\n      expect(subscriber2).toHaveBeenCalledTimes(2);\n    });\n  });\n\n  describe(\"no route match\", () => {\n    it(\"navigations to root catch\", () => {\n      let t = initializeTest();\n      t.navigate(\"/not-found\");\n      expect(t.router.state.loaderData).toEqual({\n        root: \"ROOT\",\n      });\n      expect(t.router.state.errors).toEqual({\n        root: new ErrorResponseImpl(\n          404,\n          \"Not Found\",\n          new Error('No route matches URL \"/not-found\"'),\n          true,\n        ),\n      });\n      expect(t.router.state.matches).toMatchObject([\n        {\n          params: {},\n          pathname: \"\",\n          route: {\n            hasErrorBoundary: true,\n            children: expect.any(Array),\n            id: \"root\",\n            loader: expect.any(Function),\n            path: \"\",\n          },\n        },\n      ]);\n    });\n\n    it(\"matches root pathless route\", () => {\n      let t = setup({\n        routes: [{ id: \"root\", children: [{ path: \"foo\" }] }],\n      });\n\n      t.navigate(\"/not-found\");\n      expect(t.router.state.errors).toEqual({\n        root: new ErrorResponseImpl(\n          404,\n          \"Not Found\",\n          new Error('No route matches URL \"/not-found\"'),\n          true,\n        ),\n      });\n      expect(t.router.state.matches).toMatchObject([\n        {\n          params: {},\n          pathname: \"\",\n          route: {\n            id: \"root\",\n            children: expect.any(Array),\n          },\n        },\n      ]);\n    });\n\n    it(\"clears prior loader/action data\", async () => {\n      let t = initializeTest();\n      expect(t.router.state.loaderData).toEqual({\n        root: \"ROOT\",\n        index: \"INDEX\",\n      });\n\n      let A = await t.navigate(\"/foo\", {\n        formMethod: \"post\",\n        formData: createFormData({ key: \"value\" }),\n      });\n      await A.actions.foo.resolve(\"ACTION\");\n      await A.loaders.root.resolve(\"ROOT*\");\n      await A.loaders.foo.resolve(\"LOADER\");\n      expect(t.router.state.actionData).toEqual({\n        foo: \"ACTION\",\n      });\n      expect(t.router.state.loaderData).toEqual({\n        root: \"ROOT*\",\n        foo: \"LOADER\",\n      });\n\n      t.navigate(\"/not-found\");\n      expect(t.router.state.actionData).toBe(null);\n      expect(t.router.state.loaderData).toEqual({\n        root: \"ROOT*\",\n      });\n      expect(t.router.state.errors).toEqual({\n        root: new ErrorResponseImpl(\n          404,\n          \"Not Found\",\n          new Error('No route matches URL \"/not-found\"'),\n          true,\n        ),\n      });\n      expect(t.router.state.matches).toMatchObject([\n        {\n          params: {},\n          pathname: \"\",\n          route: {\n            hasErrorBoundary: true,\n            children: expect.any(Array),\n            id: \"root\",\n            loader: expect.any(Function),\n            path: \"\",\n          },\n        },\n      ]);\n    });\n  });\n\n  describe(\"navigation (new)\", () => {\n    it(\"navigates through a history stack without data loading\", async () => {\n      let t = setup({\n        routes: [\n          {\n            id: \"index\",\n            index: true,\n          },\n          {\n            id: \"tasks\",\n            path: \"tasks\",\n          },\n          {\n            id: \"tasksId\",\n            path: \"tasks/:id\",\n          },\n        ],\n        initialEntries: [\"/\"],\n      });\n\n      expect(t.router.state).toMatchObject({\n        historyAction: \"POP\",\n        location: {\n          pathname: \"/\",\n          search: \"\",\n          hash: \"\",\n          state: null,\n          key: expect.any(String),\n        },\n        navigation: IDLE_NAVIGATION,\n        loaderData: {},\n        matches: [expect.objectContaining({ pathname: \"/\" })],\n      });\n      expect(t.history.action).toEqual(\"POP\");\n      expect(t.history.location.pathname).toEqual(\"/\");\n\n      await t.navigate(\"/tasks\");\n      expect(t.router.state).toMatchObject({\n        historyAction: \"PUSH\",\n        location: {\n          pathname: \"/tasks\",\n          search: \"\",\n          hash: \"\",\n          state: null,\n          key: expect.any(String),\n        },\n        navigation: IDLE_NAVIGATION,\n        loaderData: {},\n        matches: [expect.objectContaining({ pathname: \"/tasks\" })],\n      });\n      expect(t.history.action).toEqual(\"PUSH\");\n      expect(t.history.location.pathname).toEqual(\"/tasks\");\n\n      await t.navigate(\"/tasks/1\", { replace: true });\n      expect(t.router.state).toMatchObject({\n        historyAction: \"REPLACE\",\n        location: {\n          pathname: \"/tasks/1\",\n          search: \"\",\n          hash: \"\",\n          state: null,\n          key: expect.any(String),\n        },\n        navigation: IDLE_NAVIGATION,\n        loaderData: {},\n        matches: [expect.objectContaining({ pathname: \"/tasks/1\" })],\n      });\n      expect(t.history.action).toEqual(\"REPLACE\");\n      expect(t.history.location.pathname).toEqual(\"/tasks/1\");\n\n      t.router.navigate(-1);\n      await tick();\n      expect(t.router.state).toMatchObject({\n        historyAction: \"POP\",\n        location: {\n          pathname: \"/\",\n          search: \"\",\n          hash: \"\",\n          state: null,\n          key: expect.any(String),\n        },\n        navigation: IDLE_NAVIGATION,\n        loaderData: {},\n        matches: [expect.objectContaining({ pathname: \"/\" })],\n      });\n      expect(t.history.action).toEqual(\"POP\");\n      expect(t.history.location.pathname).toEqual(\"/\");\n\n      await t.navigate(\"/tasks?foo=bar#hash\");\n      expect(t.router.state).toMatchObject({\n        historyAction: \"PUSH\",\n        location: {\n          pathname: \"/tasks\",\n          search: \"?foo=bar\",\n          hash: \"#hash\",\n          state: null,\n          key: expect.any(String),\n        },\n        navigation: IDLE_NAVIGATION,\n        loaderData: {},\n        matches: [expect.objectContaining({ pathname: \"/tasks\" })],\n      });\n      expect(t.history.action).toEqual(\"PUSH\");\n      expect(t.history.location).toEqual({\n        pathname: \"/tasks\",\n        search: \"?foo=bar\",\n        hash: \"#hash\",\n        state: null,\n        key: expect.any(String),\n      });\n    });\n\n    it(\"navigates through a history stack without data loading (with a basename)\", async () => {\n      let t = setup({\n        basename: \"/base/name\",\n        routes: [\n          {\n            id: \"index\",\n            index: true,\n          },\n          {\n            id: \"tasks\",\n            path: \"tasks\",\n          },\n          {\n            id: \"tasksId\",\n            path: \"tasks/:id\",\n          },\n        ],\n        initialEntries: [\"/base/name\"],\n      });\n\n      expect(t.router.state).toMatchObject({\n        location: {\n          pathname: \"/base/name\",\n        },\n        matches: [{ route: { id: \"index\" } }],\n      });\n      expect(t.history.action).toEqual(\"POP\");\n      expect(t.history.location.pathname).toEqual(\"/base/name\");\n\n      await t.navigate(\"/tasks\");\n      expect(t.router.state).toMatchObject({\n        location: {\n          pathname: \"/base/name/tasks\",\n        },\n        matches: [{ route: { id: \"tasks\" } }],\n      });\n      expect(t.history.action).toEqual(\"PUSH\");\n      expect(t.history.location.pathname).toEqual(\"/base/name/tasks\");\n\n      await t.navigate(\"/tasks/1\");\n      expect(t.router.state).toMatchObject({\n        location: {\n          pathname: \"/base/name/tasks/1\",\n        },\n        matches: [{ route: { id: \"tasksId\" } }],\n      });\n      expect(t.history.location.pathname).toEqual(\"/base/name/tasks/1\");\n    });\n\n    it(\"handles 404 routes\", () => {\n      let t = setup({\n        routes: TASK_ROUTES,\n        initialEntries: [\"/\"],\n      });\n      t.navigate(\"/junk\");\n      expect(t.router.state).toMatchObject({\n        location: {\n          pathname: \"/junk\",\n        },\n        navigation: IDLE_NAVIGATION,\n        loaderData: {},\n        errors: {\n          root: new ErrorResponseImpl(\n            404,\n            \"Not Found\",\n            new Error('No route matches URL \"/junk\"'),\n            true,\n          ),\n        },\n      });\n    });\n\n    it(\"handles 404 routes when the root route contains a path (initialization)\", () => {\n      let t = setup({\n        routes: [\n          {\n            id: \"root\",\n            path: \"/path\",\n            children: [\n              {\n                index: true,\n              },\n            ],\n          },\n        ],\n        initialEntries: [\"/junk\"],\n      });\n      expect(t.router.state).toMatchObject({\n        errors: {\n          root: new ErrorResponseImpl(\n            404,\n            \"Not Found\",\n            new Error('No route matches URL \"/junk\"'),\n            true,\n          ),\n        },\n        initialized: true,\n        location: {\n          pathname: \"/junk\",\n        },\n        matches: [\n          {\n            route: {\n              id: \"root\",\n            },\n          },\n        ],\n      });\n    });\n\n    it(\"handles 404 routes when the root route contains a path (navigation)\", () => {\n      let t = setup({\n        routes: [\n          {\n            id: \"root\",\n            path: \"/path\",\n            children: [\n              {\n                index: true,\n              },\n            ],\n          },\n        ],\n        initialEntries: [\"/path\"],\n      });\n      expect(t.router.state).toMatchObject({\n        errors: null,\n      });\n      t.navigate(\"/junk\");\n      expect(t.router.state).toMatchObject({\n        errors: {\n          root: new ErrorResponseImpl(\n            404,\n            \"Not Found\",\n            new Error('No route matches URL \"/junk\"'),\n            true,\n          ),\n        },\n        location: {\n          pathname: \"/junk\",\n        },\n        matches: [\n          {\n            route: {\n              id: \"root\",\n            },\n          },\n        ],\n      });\n    });\n\n    it(\"converts formData to URLSearchParams for unspecified formMethod\", async () => {\n      let t = setup({\n        routes: TASK_ROUTES,\n        initialEntries: [\"/\"],\n      });\n      await t.navigate(\"/tasks\", {\n        formData: createFormData({ key: \"value\" }),\n      });\n      expect(t.router.state.navigation.state).toBe(\"loading\");\n      expect(t.router.state.navigation.location).toMatchObject({\n        pathname: \"/tasks\",\n        search: \"?key=value\",\n      });\n      expect(t.router.state.navigation.formMethod).toBe(\"GET\");\n      expect(t.router.state.navigation.formData).toEqual(\n        createFormData({ key: \"value\" }),\n      );\n    });\n\n    it(\"converts formData to URLSearchParams for formMethod=get\", async () => {\n      let t = setup({\n        routes: TASK_ROUTES,\n        initialEntries: [\"/\"],\n      });\n      await t.navigate(\"/tasks\", {\n        formMethod: \"get\",\n        formData: createFormData({ key: \"value\" }),\n      });\n      expect(t.router.state.navigation.state).toBe(\"loading\");\n      expect(t.router.state.navigation.location).toMatchObject({\n        pathname: \"/tasks\",\n        search: \"?key=value\",\n      });\n      expect(t.router.state.navigation.formMethod).toBe(\"GET\");\n      expect(t.router.state.navigation.formData).toEqual(\n        createFormData({ key: \"value\" }),\n      );\n    });\n\n    it(\"does not preserve existing 'action' URLSearchParams for formMethod='get'\", async () => {\n      let t = setup({\n        routes: TASK_ROUTES,\n        initialEntries: [\"/\"],\n      });\n      await t.navigate(\"/tasks?key=1\", {\n        formMethod: \"get\",\n        formData: createFormData({ key: \"2\" }),\n      });\n      expect(t.router.state.navigation.state).toBe(\"loading\");\n      expect(t.router.state.navigation.location).toMatchObject({\n        pathname: \"/tasks\",\n        search: \"?key=2\",\n      });\n      expect(t.router.state.navigation.formMethod).toBe(\"GET\");\n      expect(t.router.state.navigation.formData).toEqual(\n        createFormData({ key: \"2\" }),\n      );\n    });\n\n    it(\"preserves existing 'action' URLSearchParams for formMethod='post'\", async () => {\n      let t = setup({\n        routes: TASK_ROUTES,\n        initialEntries: [\"/\"],\n      });\n      await t.navigate(\"/tasks?key=1\", {\n        formMethod: \"post\",\n        formData: createFormData({ key: \"2\" }),\n      });\n      expect(t.router.state.navigation.state).toBe(\"submitting\");\n      expect(t.router.state.navigation.location).toMatchObject({\n        pathname: \"/tasks\",\n        search: \"?key=1\",\n      });\n      expect(t.router.state.navigation.formMethod).toBe(\"POST\");\n      expect(t.router.state.navigation.formData).toEqual(\n        createFormData({ key: \"2\" }),\n      );\n    });\n\n    it(\"url-encodes File names on GET submissions\", async () => {\n      let t = setup({\n        routes: TASK_ROUTES,\n        initialEntries: [\"/\"],\n        hydrationData: {\n          loaderData: {\n            root: \"ROOT DATA\",\n            index: \"INDEX DATA\",\n          },\n        },\n      });\n\n      let formData = new FormData();\n      formData.append(\n        \"blob\",\n        new Blob([\"<h1>Some html file contents</h1>\"], {\n          type: \"text/html\",\n        }),\n        \"blob.html\",\n      );\n\n      let A = await t.navigate(\"/tasks\", {\n        formMethod: \"get\",\n        formData: formData,\n      });\n      let params = new URL(A.loaders.tasks.stub.mock.calls[0][0].request.url)\n        .searchParams;\n      expect(params.get(\"blob\")).toEqual(\"blob.html\");\n    });\n\n    it(\"returns a 405 error if attempting to submit with method=HEAD\", async () => {\n      let t = setup({\n        routes: TASK_ROUTES,\n        initialEntries: [\"/\"],\n        hydrationData: {\n          loaderData: {\n            root: \"ROOT DATA\",\n            index: \"INDEX DATA\",\n          },\n        },\n      });\n\n      let formData = new FormData();\n      formData.append(\n        \"blob\",\n        new Blob([\"<h1>Some html file contents</h1>\"], {\n          type: \"text/html\",\n        }),\n      );\n\n      await t.navigate(\"/tasks\", {\n        // @ts-expect-error\n        formMethod: \"head\",\n        formData,\n      });\n      expect(t.router.state.navigation.state).toBe(\"idle\");\n      expect(t.router.state.location).toMatchObject({\n        pathname: \"/tasks\",\n        search: \"\",\n      });\n      expect(t.router.state.errors).toEqual({\n        tasks: new ErrorResponseImpl(\n          405,\n          \"Method Not Allowed\",\n          new Error('Invalid request method \"HEAD\"'),\n          true,\n        ),\n      });\n    });\n\n    it(\"returns a 405 error if attempting to submit with method=OPTIONS\", async () => {\n      let t = setup({\n        routes: TASK_ROUTES,\n        initialEntries: [\"/\"],\n        hydrationData: {\n          loaderData: {\n            root: \"ROOT DATA\",\n            index: \"INDEX DATA\",\n          },\n        },\n      });\n\n      let formData = new FormData();\n      formData.append(\n        \"blob\",\n        new Blob([\"<h1>Some html file contents</h1>\"], {\n          type: \"text/html\",\n        }),\n      );\n\n      await t.navigate(\"/tasks\", {\n        // @ts-expect-error\n        formMethod: \"options\",\n        formData: formData,\n      });\n      expect(t.router.state.navigation.state).toBe(\"idle\");\n      expect(t.router.state.location).toMatchObject({\n        pathname: \"/tasks\",\n        search: \"\",\n      });\n      expect(t.router.state.errors).toEqual({\n        tasks: new ErrorResponseImpl(\n          405,\n          \"Method Not Allowed\",\n          new Error('Invalid request method \"OPTIONS\"'),\n          true,\n        ),\n      });\n    });\n\n    it(\"handles promises for navigations\", async () => {\n      let aDfd = createDeferred();\n\n      let router = createRouter({\n        history: createMemoryHistory(),\n        routes: [\n          {\n            id: \"index\",\n            path: \"/\",\n          },\n          {\n            id: \"a\",\n            path: \"/a\",\n            loader: () => aDfd.promise,\n          },\n        ],\n      });\n\n      let sequence: string[] = [];\n      router.navigate(\"/a\").then(() => sequence.push(\"/a complete\"));\n      await tick();\n      expect(sequence).toEqual([]);\n      aDfd.resolve(\"A DATA\");\n      await tick();\n      expect(router.state.navigation).toBe(IDLE_NAVIGATION);\n      expect(sequence).toEqual([\"/a complete\"]);\n    });\n\n    it(\"handles promises for popstate navigations\", async () => {\n      let indexDfd = createDeferred();\n\n      let router = createRouter({\n        history: createMemoryHistory(),\n        routes: [\n          {\n            id: \"index\",\n            path: \"/\",\n            loader: () => indexDfd.promise,\n          },\n          {\n            id: \"a\",\n            path: \"/a\",\n          },\n        ],\n        hydrationData: {\n          loaderData: {\n            index: \"INDEX DATA\",\n          },\n        },\n      }).initialize();\n\n      let sequence: string[] = [];\n      await router.navigate(\"/a\");\n      expect(router.state.location.pathname).toBe(\"/a\");\n      expect(router.state.navigation).toBe(IDLE_NAVIGATION);\n\n      router.navigate(-1).then(() => sequence.push(\"back complete\"));\n      await tick();\n      expect(sequence).toEqual([]);\n\n      indexDfd.resolve(\"INDEX DATA\");\n      await tick();\n      expect(router.state.navigation).toBe(IDLE_NAVIGATION);\n      expect(sequence).toEqual([\"back complete\"]);\n    });\n\n    it(\"handles promises for interrupted navigations\", async () => {\n      let indexDfd = createDeferred();\n      let aDfd = createDeferred();\n      let bDfd = createDeferred();\n      let cDfd = createDeferred();\n\n      let router = createRouter({\n        history: createMemoryHistory(),\n        routes: [\n          {\n            id: \"index\",\n            path: \"/\",\n            loader: () => indexDfd.promise,\n          },\n          {\n            id: \"a\",\n            path: \"/a\",\n            loader: () => aDfd.promise,\n          },\n          {\n            id: \"b\",\n            path: \"/b\",\n            loader: () => bDfd.promise,\n          },\n          {\n            id: \"c\",\n            path: \"/c\",\n            loader: () => cDfd.promise,\n          },\n        ],\n        hydrationData: {\n          loaderData: {\n            index: \"INDEX DATA\",\n          },\n        },\n      });\n\n      let sequence: string[] = [];\n      router.navigate(\"/a\").then(() => sequence.push(\"/a complete\"));\n      aDfd.resolve(\"A DATA\");\n      await tick();\n      expect(router.state.navigation).toBe(IDLE_NAVIGATION);\n\n      router.navigate(\"/b\").then(() => sequence.push(\"/b complete\"));\n      await tick();\n      expect(sequence).toEqual([\"/a complete\"]);\n\n      router.navigate(\"/c\").then(() => sequence.push(\"/c complete\"));\n      await tick();\n      expect(sequence).toEqual([\"/a complete\", \"/b complete\"]);\n\n      bDfd.resolve(\"B DATA\"); // no-op\n      await tick();\n      expect(router.state.navigation.state).toBe(\"loading\");\n      expect(sequence).toEqual([\"/a complete\", \"/b complete\"]);\n\n      cDfd.resolve(\"C DATA\");\n      await tick();\n      expect(router.state.navigation).toBe(IDLE_NAVIGATION);\n      expect(sequence).toEqual([\"/a complete\", \"/b complete\", \"/c complete\"]);\n    });\n\n    it(\"handles promises for interrupted popstate navigations\", async () => {\n      let indexDfd = createDeferred();\n      let aDfd = createDeferred();\n      let bDfd = createDeferred();\n\n      let router = createRouter({\n        history: createMemoryHistory(),\n        routes: [\n          {\n            id: \"index\",\n            path: \"/\",\n            loader: () => indexDfd.promise,\n          },\n          {\n            id: \"a\",\n            path: \"/a\",\n            loader: () => aDfd.promise,\n          },\n          {\n            id: \"b\",\n            path: \"/b\",\n            loader: () => bDfd.promise,\n          },\n        ],\n        hydrationData: {\n          loaderData: {\n            index: \"INDEX DATA\",\n          },\n        },\n      });\n\n      let sequence: string[] = [];\n      router.navigate(\"/a\").then(() => sequence.push(\"/a complete\"));\n      aDfd.resolve(\"A DATA\");\n      await tick();\n      expect(router.state.navigation).toBe(IDLE_NAVIGATION);\n\n      router.navigate(-1).then(() => sequence.push(\"back complete\"));\n      await tick();\n      expect(sequence).toEqual([\"/a complete\"]);\n\n      router.navigate(\"/b\").then(() => sequence.push(\"/b complete\"));\n      await tick();\n      expect(sequence).toEqual([\"/a complete\", \"back complete\"]);\n\n      indexDfd.resolve(\"A DATA\");\n      await tick();\n      expect(router.state.navigation.state).toBe(\"loading\");\n      expect(sequence).toEqual([\"/a complete\", \"back complete\"]);\n\n      bDfd.resolve(\"B DATA\");\n      await tick();\n      expect(router.state.navigation).toBe(IDLE_NAVIGATION);\n      expect(sequence).toEqual([\"/a complete\", \"back complete\", \"/b complete\"]);\n    });\n\n    it(\"handles promises for fetcher redirect interrupted popstate navigations\", async () => {\n      let indexDfd = createDeferred();\n      let bDfd = createDeferred();\n\n      let router = createRouter({\n        history: createMemoryHistory(),\n        routes: [\n          {\n            id: \"index\",\n            path: \"/\",\n            loader: () => indexDfd.promise,\n          },\n          {\n            id: \"a\",\n            path: \"/a\",\n          },\n          {\n            id: \"b\",\n            path: \"/b\",\n            loader: () => bDfd.promise,\n          },\n          {\n            id: \"fetch\",\n            path: \"/fetch\",\n            loader: () => redirect(\"/b\"),\n          },\n        ],\n        hydrationData: {\n          loaderData: {\n            index: \"INDEX DATA\",\n          },\n        },\n      });\n\n      let sequence: string[] = [];\n      router.navigate(\"/a\").then(() => sequence.push(\"/a complete\"));\n      await tick();\n      expect(router.state.navigation).toBe(IDLE_NAVIGATION);\n\n      router.navigate(-1).then(() => sequence.push(\"back complete\"));\n      await tick();\n      expect(sequence).toEqual([\"/a complete\"]);\n\n      router\n        .fetch(\"key\", \"a\", \"/fetch\")\n        .then(() => sequence.push(\"fetch redirect complete\"));\n      await tick();\n      expect(sequence).toEqual([\"/a complete\", \"back complete\"]);\n\n      indexDfd.resolve(\"A DATA\"); // no-op\n      await tick();\n      expect(router.state.navigation.state).toBe(\"loading\");\n      expect(sequence).toEqual([\"/a complete\", \"back complete\"]);\n\n      bDfd.resolve(\"B DATA\");\n      await tick();\n      expect(router.state.navigation).toBe(IDLE_NAVIGATION);\n      expect(sequence).toEqual([\n        \"/a complete\",\n        \"back complete\",\n        \"fetch redirect complete\",\n      ]);\n    });\n  });\n\n  describe(\"data loading (new)\", () => {\n    it(\"marks as initialized immediately when no loaders are present\", async () => {\n      let t = setup({\n        routes: [\n          {\n            id: \"root\",\n            path: \"/\",\n          },\n        ],\n        initialEntries: [\"/\"],\n      });\n\n      expect(console.warn).not.toHaveBeenCalled();\n      expect(t.router.state).toMatchObject({\n        historyAction: \"POP\",\n        location: {\n          pathname: \"/\",\n        },\n        initialized: true,\n        navigation: IDLE_NAVIGATION,\n        loaderData: {},\n      });\n    });\n\n    it(\"hydrates initial data\", async () => {\n      let t = setup({\n        routes: TASK_ROUTES,\n        initialEntries: [\"/\"],\n        hydrationData: {\n          loaderData: {\n            root: \"ROOT DATA\",\n            index: \"INDEX DATA\",\n          },\n        },\n      });\n\n      expect(console.warn).not.toHaveBeenCalled();\n      expect(t.router.state).toMatchObject({\n        historyAction: \"POP\",\n        location: {\n          pathname: \"/\",\n        },\n        initialized: true,\n        navigation: IDLE_NAVIGATION,\n        loaderData: {\n          root: \"ROOT DATA\",\n          index: \"INDEX DATA\",\n        },\n      });\n    });\n\n    it(\"does not run middlewares when complete hydrationData exists\", async () => {\n      let middlewareSpy = jest.fn();\n      let loaderSpy = jest.fn();\n      let router = createRouter({\n        history: createMemoryHistory(),\n        routes: [\n          {\n            id: \"index\",\n            path: \"/\",\n            middleware: [middlewareSpy],\n            loader: loaderSpy,\n          },\n        ],\n        hydrationData: {\n          loaderData: {\n            index: \"INDEX DATA\",\n          },\n        },\n      });\n      router.initialize();\n\n      expect(router.state).toMatchObject({\n        historyAction: \"POP\",\n        location: {\n          pathname: \"/\",\n        },\n        initialized: true,\n        navigation: IDLE_NAVIGATION,\n        loaderData: {\n          index: \"INDEX DATA\",\n        },\n      });\n      expect(middlewareSpy).not.toHaveBeenCalled();\n      expect(loaderSpy).not.toHaveBeenCalled();\n    });\n\n    it(\"kicks off initial data load if no hydration data is provided\", async () => {\n      let parentDfd = createDeferred();\n      let parentSpy = jest.fn(() => parentDfd.promise);\n      let childDfd = createDeferred();\n      let childSpy = jest.fn(() => childDfd.promise);\n      let router = createRouter({\n        history: createMemoryHistory({ initialEntries: [\"/child\"] }),\n        routes: [\n          {\n            path: \"/\",\n            loader: parentSpy,\n            children: [\n              {\n                path: \"child\",\n                loader: childSpy,\n              },\n            ],\n          },\n        ],\n      });\n      router.initialize();\n\n      expect(console.warn).not.toHaveBeenCalled();\n      expect(parentSpy.mock.calls.length).toBe(1);\n      expect(childSpy.mock.calls.length).toBe(1);\n      expect(router.state).toMatchObject({\n        historyAction: \"POP\",\n        location: expect.objectContaining({ pathname: \"/child\" }),\n        initialized: false,\n        navigation: IDLE_NAVIGATION,\n      });\n      expect(router.state.loaderData).toEqual({});\n\n      await parentDfd.resolve(\"PARENT DATA\");\n      expect(router.state).toMatchObject({\n        historyAction: \"POP\",\n        location: expect.objectContaining({ pathname: \"/child\" }),\n        initialized: false,\n        navigation: IDLE_NAVIGATION,\n      });\n      expect(router.state.loaderData).toEqual({});\n\n      await childDfd.resolve(\"CHILD DATA\");\n      expect(router.state).toMatchObject({\n        historyAction: \"POP\",\n        location: expect.objectContaining({ pathname: \"/child\" }),\n        initialized: true,\n        navigation: IDLE_NAVIGATION,\n        loaderData: {\n          \"0\": \"PARENT DATA\",\n          \"0-0\": \"CHILD DATA\",\n        },\n      });\n\n      router.dispose();\n    });\n\n    it(\"run middlewares without loaders on initial load if no hydration data is provided\", async () => {\n      let parentDfd = createDeferred();\n      let parentSpy = jest.fn(() => parentDfd.promise);\n      let childDfd = createDeferred();\n      let childSpy = jest.fn(() => childDfd.promise);\n      let router = createRouter({\n        history: createMemoryHistory(),\n        routes: [\n          {\n            path: \"/\",\n            middleware: [parentSpy],\n            children: [\n              {\n                index: true,\n                middleware: [childSpy],\n              },\n            ],\n          },\n        ],\n      });\n      router.initialize();\n      await tick();\n\n      expect(console.warn).not.toHaveBeenCalled();\n      expect(parentSpy.mock.calls.length).toBe(1);\n      expect(childSpy.mock.calls.length).toBe(0);\n      expect(router.state).toMatchObject({\n        historyAction: \"POP\",\n        location: expect.objectContaining({ pathname: \"/\" }),\n        initialized: false,\n        navigation: IDLE_NAVIGATION,\n      });\n      expect(router.state.loaderData).toEqual({});\n\n      await parentDfd.resolve(undefined);\n      expect(router.state).toMatchObject({\n        historyAction: \"POP\",\n        location: expect.objectContaining({ pathname: \"/\" }),\n        initialized: false,\n        navigation: IDLE_NAVIGATION,\n      });\n      expect(router.state.loaderData).toEqual({});\n\n      await childDfd.resolve(undefined);\n      expect(router.state).toMatchObject({\n        historyAction: \"POP\",\n        location: expect.objectContaining({ pathname: \"/\" }),\n        initialized: true,\n        navigation: IDLE_NAVIGATION,\n        loaderData: {},\n      });\n\n      router.dispose();\n    });\n\n    it(\"allows routes to be initialized with undefined loaderData\", async () => {\n      let t = setup({\n        routes: [\n          {\n            id: \"root\",\n            path: \"/\",\n            loader: true,\n          },\n        ],\n        hydrationData: {\n          loaderData: {\n            root: undefined,\n          },\n        },\n      });\n\n      expect(t.router.state).toMatchObject({\n        historyAction: \"POP\",\n        location: {\n          pathname: \"/\",\n        },\n        initialized: true,\n        navigation: IDLE_NAVIGATION,\n        loaderData: {\n          root: undefined,\n        },\n      });\n    });\n\n    it(\"handles interruptions of initial data load\", async () => {\n      let parentDfd = createDeferred();\n      let parentSpy = jest.fn(() => parentDfd.promise);\n      let childDfd = createDeferred();\n      let childSpy = jest.fn(() => childDfd.promise);\n      let child2Dfd = createDeferred();\n      let child2Spy = jest.fn(() => child2Dfd.promise);\n      let router = createRouter({\n        history: createMemoryHistory({ initialEntries: [\"/child\"] }),\n        routes: [\n          {\n            path: \"/\",\n            loader: parentSpy,\n            children: [\n              {\n                path: \"child\",\n                loader: childSpy,\n              },\n              {\n                path: \"child2\",\n                loader: child2Spy,\n              },\n            ],\n          },\n        ],\n      });\n      router.initialize();\n\n      expect(console.warn).not.toHaveBeenCalled();\n      expect(parentSpy.mock.calls.length).toBe(1);\n      expect(childSpy.mock.calls.length).toBe(1);\n      expect(router.state).toMatchObject({\n        historyAction: \"POP\",\n        location: expect.objectContaining({ pathname: \"/child\" }),\n        initialized: false,\n        navigation: IDLE_NAVIGATION,\n      });\n      expect(router.state.loaderData).toEqual({});\n\n      await parentDfd.resolve(\"PARENT DATA\");\n      expect(router.state).toMatchObject({\n        historyAction: \"POP\",\n        location: expect.objectContaining({ pathname: \"/child\" }),\n        initialized: false,\n        navigation: IDLE_NAVIGATION,\n      });\n      expect(router.state.loaderData).toEqual({});\n\n      router.navigate(\"/child2\");\n      await childDfd.resolve(\"CHILD DATA\");\n      expect(router.state).toMatchObject({\n        historyAction: \"POP\",\n        location: expect.objectContaining({ pathname: \"/child\" }),\n        initialized: false,\n        navigation: {\n          state: \"loading\",\n          location: { pathname: \"/child2\" },\n        },\n      });\n      expect(router.state.loaderData).toEqual({});\n\n      await child2Dfd.resolve(\"CHILD2 DATA\");\n      expect(router.state).toMatchObject({\n        historyAction: \"PUSH\",\n        location: expect.objectContaining({ pathname: \"/child2\" }),\n        initialized: true,\n        navigation: IDLE_NAVIGATION,\n        loaderData: {\n          \"0\": \"PARENT DATA\",\n          \"0-1\": \"CHILD2 DATA\",\n        },\n      });\n\n      router.dispose();\n    });\n\n    it(\"handles errors in initial data load\", async () => {\n      let router = createRouter({\n        history: createMemoryHistory({ initialEntries: [\"/child\"] }),\n        routes: [\n          {\n            path: \"/\",\n            loader: () => Promise.reject(\"Kaboom!\"),\n            children: [\n              {\n                path: \"child\",\n                loader: () => Promise.resolve(\"child\"),\n              },\n            ],\n          },\n        ],\n      });\n      router.initialize();\n\n      await tick();\n      expect(router.state).toMatchObject({\n        historyAction: \"POP\",\n        location: expect.objectContaining({ pathname: \"/child\" }),\n        initialized: true,\n        navigation: IDLE_NAVIGATION,\n        loaderData: {\n          \"0-0\": \"child\",\n        },\n        errors: {\n          \"0\": \"Kaboom!\",\n        },\n      });\n\n      router.dispose();\n    });\n\n    it(\"handles initial load 404s when the error boundary router has a loader\", async () => {\n      let router = createRouter({\n        history: createMemoryHistory({ initialEntries: [\"/404\"] }),\n        routes: [\n          {\n            path: \"/\",\n            hasErrorBoundary: true,\n            loader: () => {},\n          },\n        ],\n      });\n\n      expect(router.state).toMatchObject({\n        historyAction: \"POP\",\n        location: expect.objectContaining({ pathname: \"/404\" }),\n        initialized: true,\n        navigation: IDLE_NAVIGATION,\n        loaderData: {},\n        errors: {\n          \"0\": new ErrorResponseImpl(\n            404,\n            \"Not Found\",\n            new Error('No route matches URL \"/404\"'),\n            true,\n          ),\n        },\n      });\n\n      router.dispose();\n    });\n\n    it(\"kicks off initial data load when hash is present\", async () => {\n      let loaderDfd = createDeferred();\n      let loaderSpy = jest.fn(() => loaderDfd.promise);\n      let router = createRouter({\n        history: createMemoryHistory({ initialEntries: [\"/#hash\"] }),\n        routes: [\n          {\n            path: \"/\",\n            loader: loaderSpy,\n          },\n        ],\n      });\n      router.initialize();\n\n      expect(console.warn).not.toHaveBeenCalled();\n      expect(loaderSpy.mock.calls.length).toBe(1);\n      expect(router.state).toMatchObject({\n        historyAction: \"POP\",\n        location: expect.objectContaining({ pathname: \"/\", hash: \"#hash\" }),\n        initialized: false,\n        navigation: IDLE_NAVIGATION,\n      });\n      expect(router.state.loaderData).toEqual({});\n\n      await loaderDfd.resolve(\"DATA\");\n      expect(router.state).toMatchObject({\n        historyAction: \"POP\",\n        location: expect.objectContaining({ pathname: \"/\", hash: \"#hash\" }),\n        initialized: true,\n        navigation: IDLE_NAVIGATION,\n        loaderData: {\n          \"0\": \"DATA\",\n        },\n      });\n\n      router.dispose();\n    });\n\n    it(\"executes loaders on push navigations\", async () => {\n      let t = setup({\n        routes: TASK_ROUTES,\n        initialEntries: [\"/\"],\n        hydrationData: {\n          loaderData: {\n            root: \"ROOT_DATA\",\n            index: \"INDEX_DATA\",\n          },\n        },\n      });\n\n      let nav1 = await t.navigate(\"/tasks\");\n      expect(t.router.state).toMatchObject({\n        historyAction: \"POP\",\n        location: {\n          pathname: \"/\",\n        },\n        navigation: {\n          location: {\n            pathname: \"/tasks\",\n          },\n          state: \"loading\",\n        },\n        loaderData: {\n          root: \"ROOT_DATA\",\n          index: \"INDEX_DATA\",\n        },\n      });\n      expect(t.history.action).toEqual(\"POP\");\n      expect(t.history.location.pathname).toEqual(\"/\");\n\n      await nav1.loaders.tasks.resolve(\"TASKS_DATA\");\n      expect(t.router.state).toMatchObject({\n        historyAction: \"PUSH\",\n        location: {\n          pathname: \"/tasks\",\n        },\n        navigation: IDLE_NAVIGATION,\n        loaderData: {\n          root: \"ROOT_DATA\",\n          tasks: \"TASKS_DATA\",\n        },\n      });\n      expect(t.history.action).toEqual(\"PUSH\");\n      expect(t.history.location.pathname).toEqual(\"/tasks\");\n\n      let nav2 = await t.navigate(\"/tasks/1\");\n      await nav2.loaders.tasksId.resolve(\"TASKS_ID_DATA\");\n      expect(t.router.state).toMatchObject({\n        historyAction: \"PUSH\",\n        location: {\n          pathname: \"/tasks/1\",\n        },\n        navigation: IDLE_NAVIGATION,\n        loaderData: {\n          root: \"ROOT_DATA\",\n          tasksId: \"TASKS_ID_DATA\",\n        },\n      });\n      expect(t.history.action).toEqual(\"PUSH\");\n      expect(t.history.location.pathname).toEqual(\"/tasks/1\");\n    });\n\n    it(\"executes loaders on replace navigations\", async () => {\n      let t = setup({\n        routes: TASK_ROUTES,\n        initialEntries: [\"/\"],\n        hydrationData: {\n          loaderData: {\n            root: \"ROOT_DATA\",\n            index: \"INDEX_DATA\",\n          },\n        },\n      });\n\n      let nav = await t.navigate(\"/tasks\", { replace: true });\n      expect(t.router.state).toMatchObject({\n        historyAction: \"POP\",\n        location: {\n          pathname: \"/\",\n        },\n        navigation: {\n          location: {\n            pathname: \"/tasks\",\n          },\n          state: \"loading\",\n        },\n        loaderData: {\n          root: \"ROOT_DATA\",\n          index: \"INDEX_DATA\",\n        },\n      });\n      expect(t.history.action).toEqual(\"POP\");\n      expect(t.history.location.pathname).toEqual(\"/\");\n\n      await nav.loaders.tasks.resolve(\"TASKS_DATA\");\n      expect(t.router.state).toMatchObject({\n        historyAction: \"REPLACE\",\n        location: {\n          pathname: \"/tasks\",\n        },\n        navigation: IDLE_NAVIGATION,\n        loaderData: {\n          root: \"ROOT_DATA\",\n          tasks: \"TASKS_DATA\",\n        },\n      });\n      expect(t.history.action).toEqual(\"REPLACE\");\n      expect(t.history.location.pathname).toEqual(\"/tasks\");\n    });\n\n    it(\"executes loaders on go navigations\", async () => {\n      let t = setup({\n        routes: TASK_ROUTES,\n        initialEntries: [\"/\", \"/tasks\"],\n        initialIndex: 0,\n        hydrationData: {\n          loaderData: {\n            root: \"ROOT_DATA\",\n            index: \"INDEX_DATA\",\n          },\n        },\n      });\n\n      // pop forward to /tasks\n      let nav2 = await t.navigate(1);\n      expect(t.router.state).toMatchObject({\n        historyAction: \"POP\",\n        location: {\n          pathname: \"/\",\n        },\n        navigation: {\n          location: {\n            pathname: \"/tasks\",\n          },\n          state: \"loading\",\n        },\n        loaderData: {\n          root: \"ROOT_DATA\",\n          index: \"INDEX_DATA\",\n        },\n      });\n      expect(t.history.action).toEqual(\"POP\");\n      expect(t.history.location.pathname).toEqual(\"/tasks\");\n\n      await nav2.loaders.tasks.resolve(\"TASKS_DATA\");\n      expect(t.router.state).toMatchObject({\n        historyAction: \"POP\",\n        location: {\n          pathname: \"/tasks\",\n        },\n        navigation: IDLE_NAVIGATION,\n        loaderData: {\n          root: \"ROOT_DATA\",\n          tasks: \"TASKS_DATA\",\n        },\n      });\n      expect(t.history.action).toEqual(\"POP\");\n      expect(t.history.location.pathname).toEqual(\"/tasks\");\n    });\n\n    it(\"persists location keys throughout navigations\", async () => {\n      let t = setup({\n        routes: TASK_ROUTES,\n        initialEntries: [\"/\"],\n        hydrationData: {\n          loaderData: {\n            root: \"ROOT_DATA\",\n            index: \"INDEX_DATA\",\n          },\n        },\n      });\n\n      expect(t.router.state.location.key).toBe(\"default\");\n\n      let A = await t.navigate(\"/tasks\");\n      let navigationKey = t.router.state.navigation.location?.key;\n      expect(t.router.state.location.key).toBe(\"default\");\n      expect(t.router.state.navigation.state).toBe(\"loading\");\n      expect(navigationKey).not.toBe(\"default\");\n      expect(Number(navigationKey?.length) > 0).toBe(true);\n\n      await A.loaders.tasks.resolve(\"TASKS\");\n      expect(t.router.state.navigation.state).toBe(\"idle\");\n\n      // Make sure we keep the same location.key throughout the navigation and\n      // history isn't creating a new one in history.push\n      expect(t.router.state.location.key).toBe(navigationKey);\n      expect(t.history.location.key).toBe(navigationKey);\n    });\n\n    it(\"sends proper arguments to loaders\", async () => {\n      let t = setup({\n        routes: TASK_ROUTES,\n        initialEntries: [\"/\"],\n        hydrationData: {\n          loaderData: {\n            root: \"ROOT_DATA\",\n            index: \"INDEX_DATA\",\n          },\n        },\n      });\n\n      let nav = await t.navigate(\"/tasks\");\n      expect(nav.loaders.tasks.stub).toHaveBeenCalledWith({\n        params: {},\n        request: new Request(\"http://localhost/tasks\", {\n          signal: nav.loaders.tasks.stub.mock.calls[0][0].request.signal,\n        }),\n        unstable_pattern: \"/tasks\",\n        context: {},\n      });\n\n      let nav2 = await t.navigate(\"/tasks/1\");\n      expect(nav2.loaders.tasksId.stub).toHaveBeenCalledWith({\n        params: { id: \"1\" },\n        request: new Request(\"http://localhost/tasks/1\", {\n          signal: nav2.loaders.tasksId.stub.mock.calls[0][0].request.signal,\n        }),\n        unstable_pattern: \"/tasks/:id\",\n        context: {},\n      });\n\n      let nav3 = await t.navigate(\"/tasks?foo=bar#hash\");\n      expect(nav3.loaders.tasks.stub).toHaveBeenCalledWith({\n        params: {},\n        request: new Request(\"http://localhost/tasks?foo=bar\", {\n          signal: nav3.loaders.tasks.stub.mock.calls[0][0].request.signal,\n        }),\n        unstable_pattern: \"/tasks\",\n        context: {},\n      });\n\n      let nav4 = await t.navigate(\"/tasks#hash\", {\n        formData: createFormData({ foo: \"bar\" }),\n      });\n      expect(nav4.loaders.tasks.stub).toHaveBeenCalledWith({\n        params: {},\n        request: new Request(\"http://localhost/tasks?foo=bar\", {\n          signal: nav4.loaders.tasks.stub.mock.calls[0][0].request.signal,\n        }),\n        unstable_pattern: \"/tasks\",\n        context: {},\n      });\n\n      expect(t.router.state.navigation.formAction).toBe(\"/tasks\");\n      expect(t.router.state.navigation?.location?.pathname).toBe(\"/tasks\");\n      expect(t.router.state.navigation?.location?.search).toBe(\"?foo=bar\");\n    });\n\n    it(\"handles errors thrown from loaders\", async () => {\n      let t = setup({\n        routes: TASK_ROUTES,\n        initialEntries: [\"/\"],\n        hydrationData: {\n          loaderData: {\n            root: \"ROOT_DATA\",\n            index: \"INDEX_DATA\",\n          },\n        },\n      });\n\n      // Throw from tasks, handled by tasks\n      let nav = await t.navigate(\"/tasks\");\n      await nav.loaders.tasks.reject(\"TASKS_ERROR\");\n      expect(t.router.state.navigation).toEqual(IDLE_NAVIGATION);\n      expect(t.router.state.loaderData).toEqual({\n        root: \"ROOT_DATA\",\n      });\n      expect(t.router.state.errors).toEqual({\n        tasks: \"TASKS_ERROR\",\n      });\n\n      // Throw from index, handled by root\n      let nav2 = await t.navigate(\"/\");\n      await nav2.loaders.index.reject(\"INDEX_ERROR\");\n      expect(t.router.state.navigation).toEqual(IDLE_NAVIGATION);\n      expect(t.router.state.loaderData).toEqual({\n        root: \"ROOT_DATA\",\n      });\n      expect(t.router.state.errors).toEqual({\n        root: \"INDEX_ERROR\",\n      });\n    });\n\n    it(\"re-runs loaders on post-error navigations\", async () => {\n      let t = setup({\n        routes: TASK_ROUTES,\n        initialEntries: [\"/\"],\n        hydrationData: {\n          errors: {\n            root: \"ROOT_ERROR\",\n          },\n        },\n      });\n\n      // If a route has an error, we should call the loader if that route is\n      // re-used on a navigation\n      let nav = await t.navigate(\"/tasks\");\n      await nav.loaders.tasks.resolve(\"TASKS_DATA\");\n      expect(t.router.state.navigation.state).toEqual(\"loading\");\n      expect(t.router.state.loaderData).toEqual({});\n      expect(t.router.state.errors).toEqual({\n        root: \"ROOT_ERROR\",\n      });\n\n      await nav.loaders.root.resolve(\"ROOT_DATA\");\n      expect(t.router.state.navigation).toEqual(IDLE_NAVIGATION);\n      expect(t.router.state.loaderData).toEqual({\n        root: \"ROOT_DATA\",\n        tasks: \"TASKS_DATA\",\n      });\n      expect(t.router.state.errors).toBe(null);\n    });\n\n    it(\"handles interruptions during navigations\", async () => {\n      let t = setup({\n        routes: TASK_ROUTES,\n        initialEntries: [\"/\"],\n        hydrationData: {\n          loaderData: {\n            root: \"ROOT_DATA\",\n            index: \"INDEX_DATA\",\n          },\n        },\n      });\n\n      let historySpy = jest.spyOn(t.history, \"push\");\n\n      let nav = await t.navigate(\"/tasks\");\n      expect(t.router.state.navigation.state).toEqual(\"loading\");\n      expect(t.router.state.location.pathname).toEqual(\"/\");\n      expect(nav.loaders.tasks.signal.aborted).toBe(false);\n      expect(t.history.action).toEqual(\"POP\");\n      expect(t.history.location.pathname).toEqual(\"/\");\n\n      // Interrupt and confirm prior loader was aborted\n      let nav2 = await t.navigate(\"/tasks/1\");\n      expect(t.router.state.navigation.state).toEqual(\"loading\");\n      expect(t.router.state.location.pathname).toEqual(\"/\");\n      expect(nav.loaders.tasks.signal.aborted).toBe(true);\n      expect(t.history.action).toEqual(\"POP\");\n      expect(t.history.location.pathname).toEqual(\"/\");\n\n      // Complete second navigation\n      await nav2.loaders.tasksId.resolve(\"TASKS_ID_DATA\");\n      expect(t.router.state.navigation).toEqual(IDLE_NAVIGATION);\n      expect(t.router.state.location.pathname).toEqual(\"/tasks/1\");\n      expect(t.history.location.pathname).toEqual(\"/tasks/1\");\n      expect(t.router.state.loaderData).toEqual({\n        root: \"ROOT_DATA\",\n        tasksId: \"TASKS_ID_DATA\",\n      });\n      expect(t.history.action).toEqual(\"PUSH\");\n      expect(t.history.location.pathname).toEqual(\"/tasks/1\");\n\n      // Resolve first navigation - should no-op\n      await nav.loaders.tasks.resolve(\"TASKS_DATA\");\n      expect(t.router.state.navigation).toEqual(IDLE_NAVIGATION);\n      expect(t.router.state.location.pathname).toEqual(\"/tasks/1\");\n      expect(t.history.location.pathname).toEqual(\"/tasks/1\");\n      expect(t.router.state.loaderData).toEqual({\n        root: \"ROOT_DATA\",\n        tasksId: \"TASKS_ID_DATA\",\n      });\n      expect(t.history.action).toEqual(\"PUSH\");\n      expect(t.history.location.pathname).toEqual(\"/tasks/1\");\n\n      expect(historySpy.mock.calls).toEqual([\n        [\n          expect.objectContaining({\n            pathname: \"/tasks/1\",\n          }),\n          null,\n        ],\n      ]);\n    });\n\n    it(\"handles redirects thrown from loaders\", async () => {\n      let t = setup({\n        routes: TASK_ROUTES,\n        initialEntries: [\"/\"],\n        hydrationData: {\n          loaderData: {\n            root: \"ROOT_DATA\",\n            index: \"INDEX_DATA\",\n          },\n        },\n      });\n\n      let nav1 = await t.navigate(\"/tasks\");\n      expect(t.router.state).toMatchObject({\n        historyAction: \"POP\",\n        location: {\n          pathname: \"/\",\n        },\n        navigation: {\n          location: {\n            pathname: \"/tasks\",\n          },\n          state: \"loading\",\n        },\n        loaderData: {\n          root: \"ROOT_DATA\",\n        },\n        errors: null,\n      });\n      expect(t.history.action).toEqual(\"POP\");\n      expect(t.history.location.pathname).toEqual(\"/\");\n\n      let nav2 = await nav1.loaders.tasks.redirect(\"/tasks/1\");\n\n      // Should not abort if it redirected\n      expect(nav1.loaders.tasks.signal.aborted).toBe(false);\n      expect(t.router.state).toMatchObject({\n        historyAction: \"POP\",\n        location: {\n          pathname: \"/\",\n        },\n        navigation: {\n          location: {\n            pathname: \"/tasks/1\",\n          },\n          state: \"loading\",\n        },\n        loaderData: {\n          root: \"ROOT_DATA\",\n        },\n        errors: null,\n      });\n      expect(t.history.action).toEqual(\"POP\");\n      expect(t.history.location.pathname).toEqual(\"/\");\n\n      await nav2.loaders.tasksId.resolve(\"TASKS_ID_DATA\");\n      expect(t.router.state).toMatchObject({\n        historyAction: \"PUSH\",\n        location: {\n          pathname: \"/tasks/1\",\n        },\n        navigation: IDLE_NAVIGATION,\n        loaderData: {\n          root: \"ROOT_DATA\",\n          tasksId: \"TASKS_ID_DATA\",\n        },\n        errors: null,\n      });\n      expect(t.history.action).toEqual(\"PUSH\");\n      expect(t.history.location.pathname).toEqual(\"/tasks/1\");\n    });\n\n    it(\"handles redirects returned from loaders\", async () => {\n      let t = setup({\n        routes: TASK_ROUTES,\n        initialEntries: [\"/\"],\n        hydrationData: {\n          loaderData: {\n            root: \"ROOT_DATA\",\n            index: \"INDEX_DATA\",\n          },\n        },\n      });\n\n      let nav1 = await t.navigate(\"/tasks\");\n      expect(t.router.state).toMatchObject({\n        historyAction: \"POP\",\n        location: {\n          pathname: \"/\",\n        },\n        navigation: {\n          location: {\n            pathname: \"/tasks\",\n          },\n          state: \"loading\",\n        },\n        loaderData: {\n          root: \"ROOT_DATA\",\n        },\n        errors: null,\n      });\n      expect(t.history.action).toEqual(\"POP\");\n      expect(t.history.location.pathname).toEqual(\"/\");\n\n      let nav2 = await nav1.loaders.tasks.redirectReturn(\"/tasks/1\");\n\n      // Should not abort if it redirected\n      expect(nav1.loaders.tasks.signal.aborted).toBe(false);\n      expect(t.router.state).toMatchObject({\n        historyAction: \"POP\",\n        location: {\n          pathname: \"/\",\n        },\n        navigation: {\n          location: {\n            pathname: \"/tasks/1\",\n          },\n          state: \"loading\",\n        },\n        loaderData: {\n          root: \"ROOT_DATA\",\n        },\n        errors: null,\n      });\n      expect(t.history.action).toEqual(\"POP\");\n      expect(t.history.location.pathname).toEqual(\"/\");\n\n      await nav2.loaders.tasksId.resolve(\"TASKS_ID_DATA\");\n      expect(t.router.state).toMatchObject({\n        historyAction: \"PUSH\",\n        location: {\n          pathname: \"/tasks/1\",\n        },\n        navigation: IDLE_NAVIGATION,\n        loaderData: {\n          root: \"ROOT_DATA\",\n          tasksId: \"TASKS_ID_DATA\",\n        },\n        errors: null,\n      });\n      expect(t.history.action).toEqual(\"PUSH\");\n      expect(t.history.location.pathname).toEqual(\"/tasks/1\");\n    });\n\n    it(\"handles thrown non-redirect Responses as ErrorResponse's (text)\", async () => {\n      let t = setup({\n        routes: TASK_ROUTES,\n        initialEntries: [\"/\"],\n        hydrationData: {\n          loaderData: {\n            root: \"ROOT_DATA\",\n            index: \"INDEX_DATA\",\n          },\n        },\n      });\n\n      // Throw from tasks, handled by tasks\n      let nav = await t.navigate(\"/tasks\");\n      await nav.loaders.tasks.reject(\n        new Response(\"broken\", { status: 400, statusText: \"Bad Request\" }),\n      );\n      expect(t.router.state).toMatchObject({\n        navigation: IDLE_NAVIGATION,\n        loaderData: {\n          root: \"ROOT_DATA\",\n        },\n        actionData: null,\n        errors: {\n          tasks: new ErrorResponseImpl(400, \"Bad Request\", \"broken\"),\n        },\n      });\n    });\n\n    it(\"handles thrown non-redirect Responses as ErrorResponse's (json)\", async () => {\n      let t = setup({\n        routes: TASK_ROUTES,\n        initialEntries: [\"/\"],\n        hydrationData: {\n          loaderData: {\n            root: \"ROOT_DATA\",\n            index: \"INDEX_DATA\",\n          },\n        },\n      });\n\n      // Throw from tasks, handled by tasks\n      let nav = await t.navigate(\"/tasks\");\n      await nav.loaders.tasks.reject(\n        new Response(JSON.stringify({ key: \"value\" }), {\n          status: 400,\n          statusText: \"Bad Request\",\n          headers: {\n            \"Content-Type\": \"application/json\",\n          },\n        }),\n      );\n      expect(t.router.state).toMatchObject({\n        navigation: IDLE_NAVIGATION,\n        loaderData: {\n          root: \"ROOT_DATA\",\n        },\n        actionData: null,\n        errors: {\n          tasks: new ErrorResponseImpl(400, \"Bad Request\", { key: \"value\" }),\n        },\n      });\n    });\n\n    it(\"handles thrown non-redirect Responses as ErrorResponse's (json utf8)\", async () => {\n      let t = setup({\n        routes: TASK_ROUTES,\n        initialEntries: [\"/\"],\n        hydrationData: {\n          loaderData: {\n            root: \"ROOT_DATA\",\n            index: \"INDEX_DATA\",\n          },\n        },\n      });\n\n      // Throw from tasks, handled by tasks\n      let nav = await t.navigate(\"/tasks\");\n      await nav.loaders.tasks.reject(\n        new Response(JSON.stringify({ key: \"value\" }), {\n          status: 400,\n          statusText: \"Bad Request\",\n          headers: {\n            \"Content-Type\": \"application/json; charset=utf-8\",\n          },\n        }),\n      );\n      expect(t.router.state).toMatchObject({\n        navigation: IDLE_NAVIGATION,\n        loaderData: {\n          root: \"ROOT_DATA\",\n        },\n        actionData: null,\n        errors: {\n          tasks: new ErrorResponseImpl(400, \"Bad Request\", { key: \"value\" }),\n        },\n      });\n    });\n\n    it(\"handles thrown data() values as ErrorResponse's\", async () => {\n      let t = setup({\n        routes: TASK_ROUTES,\n        initialEntries: [\"/\"],\n        hydrationData: {\n          loaderData: {\n            root: \"ROOT_DATA\",\n            index: \"INDEX_DATA\",\n          },\n        },\n      });\n\n      let nav = await t.navigate(\"/tasks\");\n      await nav.loaders.tasks.reject(\n        data(\"broken\", { status: 400, statusText: \"Bad Request\" }),\n      );\n      expect(t.router.state).toMatchObject({\n        navigation: IDLE_NAVIGATION,\n        loaderData: {\n          root: \"ROOT_DATA\",\n        },\n        errors: {\n          tasks: new ErrorResponseImpl(400, \"Bad Request\", \"broken\"),\n        },\n      });\n    });\n\n    it(\"sends proper arguments to actions\", async () => {\n      let t = setup({\n        routes: TASK_ROUTES,\n        initialEntries: [\"/\"],\n        hydrationData: {\n          loaderData: {\n            root: \"ROOT_DATA\",\n            index: \"INDEX_DATA\",\n          },\n        },\n      });\n\n      let nav = await t.navigate(\"/tasks\", {\n        formMethod: \"post\",\n        formData: createFormData({ query: \"params\" }),\n      });\n      expect(nav.actions.tasks.stub).toHaveBeenCalledWith({\n        params: {},\n        request: expect.any(Request),\n        unstable_pattern: \"/tasks\",\n        context: {},\n      });\n\n      // Assert request internals, cannot do a deep comparison above since some\n      // internals aren't the same on separate creations\n      let request = nav.actions.tasks.stub.mock.calls[0][0].request;\n      expect(request.method).toBe(\"POST\");\n      expect(request.url).toBe(\"http://localhost/tasks\");\n      expect(request.headers.get(\"Content-Type\")).toBe(\n        \"application/x-www-form-urlencoded;charset=UTF-8\",\n      );\n      expect((await request.formData()).get(\"query\")).toBe(\"params\");\n\n      await nav.actions.tasks.resolve(\"TASKS ACTION\");\n      let rootLoaderRequest = nav.loaders.root.stub.mock.calls[0][0].request;\n      expect(rootLoaderRequest.method).toBe(\"GET\");\n      expect(rootLoaderRequest.url).toBe(\"http://localhost/tasks\");\n\n      let tasksLoaderRequest = nav.loaders.tasks.stub.mock.calls[0][0].request;\n      expect(tasksLoaderRequest.method).toBe(\"GET\");\n      expect(tasksLoaderRequest.url).toBe(\"http://localhost/tasks\");\n    });\n\n    it(\"sends proper arguments to actions (using query string)\", async () => {\n      let t = setup({\n        routes: TASK_ROUTES,\n        initialEntries: [\"/\"],\n        hydrationData: {\n          loaderData: {\n            root: \"ROOT_DATA\",\n            index: \"INDEX_DATA\",\n          },\n        },\n      });\n\n      let formData = createFormData({ query: \"params\" });\n\n      let nav = await t.navigate(\"/tasks?foo=bar\", {\n        formMethod: \"post\",\n        formData,\n      });\n      expect(nav.actions.tasks.stub).toHaveBeenCalledWith({\n        params: {},\n        request: expect.any(Request),\n        unstable_pattern: expect.any(String),\n        context: {},\n      });\n      // Assert request internals, cannot do a deep comparison above since some\n      // internals aren't the same on separate creations\n      let request = nav.actions.tasks.stub.mock.calls[0][0].request;\n      expect(request.url).toBe(\"http://localhost/tasks?foo=bar\");\n      expect(request.method).toBe(\"POST\");\n      expect(request.headers.get(\"Content-Type\")).toBe(\n        \"application/x-www-form-urlencoded;charset=UTF-8\",\n      );\n      expect((await request.formData()).get(\"query\")).toBe(\"params\");\n    });\n\n    // https://fetch.spec.whatwg.org/#concept-method\n    it(\"properly handles method=PATCH weirdness\", async () => {\n      let t = setup({\n        routes: TASK_ROUTES,\n        initialEntries: [\"/\"],\n        hydrationData: {\n          loaderData: {\n            root: \"ROOT_DATA\",\n            index: \"INDEX_DATA\",\n          },\n        },\n      });\n\n      let nav = await t.navigate(\"/tasks\", {\n        formMethod: \"patch\",\n        formData: createFormData({ query: \"params\" }),\n      });\n      expect(nav.actions.tasks.stub).toHaveBeenCalledWith({\n        params: {},\n        request: expect.any(Request),\n        unstable_pattern: expect.any(String),\n        context: {},\n      });\n\n      // Assert request internals, cannot do a deep comparison above since some\n      // internals aren't the same on separate creations\n      let request = nav.actions.tasks.stub.mock.calls[0][0].request;\n      expect(request.method).toBe(\"PATCH\");\n      expect(request.url).toBe(\"http://localhost/tasks\");\n      expect(request.headers.get(\"Content-Type\")).toBe(\n        \"application/x-www-form-urlencoded;charset=UTF-8\",\n      );\n      expect((await request.formData()).get(\"query\")).toBe(\"params\");\n\n      await nav.actions.tasks.resolve(\"TASKS ACTION\");\n      let rootLoaderRequest = nav.loaders.root.stub.mock.calls[0][0].request;\n      expect(rootLoaderRequest.method).toBe(\"GET\");\n      expect(rootLoaderRequest.url).toBe(\"http://localhost/tasks\");\n\n      let tasksLoaderRequest = nav.loaders.tasks.stub.mock.calls[0][0].request;\n      expect(tasksLoaderRequest.method).toBe(\"GET\");\n      expect(tasksLoaderRequest.url).toBe(\"http://localhost/tasks\");\n    });\n\n    it(\"handles multipart/form-data submissions\", async () => {\n      let t = setup({\n        routes: [\n          {\n            id: \"root\",\n            path: \"/\",\n            action: true,\n          },\n        ],\n        initialEntries: [\"/\"],\n        hydrationData: {\n          loaderData: {\n            root: \"ROOT_DATA\",\n          },\n        },\n      });\n\n      let fd = new FormData();\n      fd.append(\"key\", \"value\");\n      fd.append(\"file\", new Blob([\"1\", \"2\", \"3\"]), \"file.txt\");\n\n      let A = await t.navigate(\"/\", {\n        formMethod: \"post\",\n        formEncType: \"multipart/form-data\",\n        formData: fd,\n      });\n\n      expect(\n        A.actions.root.stub.mock.calls[0][0].request.headers.get(\n          \"Content-Type\",\n        ),\n      ).toMatch(\n        /^multipart\\/form-data; boundary=----formdata-undici-[a-z0-9]+/,\n      );\n    });\n\n    it(\"url-encodes File names on x-www-form-urlencoded submissions\", async () => {\n      let t = setup({\n        routes: [\n          {\n            id: \"root\",\n            path: \"/\",\n            action: true,\n          },\n        ],\n        initialEntries: [\"/\"],\n        hydrationData: {\n          loaderData: {\n            root: \"ROOT_DATA\",\n          },\n        },\n      });\n\n      let fd = new FormData();\n      fd.append(\"key\", \"value\");\n      fd.append(\"file\", new Blob([\"1\", \"2\", \"3\"]), \"file.txt\");\n\n      let A = await t.navigate(\"/\", {\n        formMethod: \"post\",\n        formEncType: \"application/x-www-form-urlencoded\",\n        formData: fd,\n      });\n\n      let req = A.actions.root.stub.mock.calls[0][0].request.clone();\n      expect((await req.formData()).get(\"file\")).toEqual(\"file.txt\");\n    });\n\n    it(\"races actions and loaders against abort signals\", async () => {\n      let loaderDfd = createDeferred();\n      let actionDfd = createDeferred();\n      let router = createRouter({\n        routes: [\n          {\n            index: true,\n          },\n          {\n            path: \"foo\",\n            loader: () => loaderDfd.promise,\n            action: () => actionDfd.promise,\n          },\n          {\n            path: \"bar\",\n          },\n        ],\n        hydrationData: { loaderData: { \"0\": null } },\n        history: createMemoryHistory(),\n      });\n\n      expect(router.state.initialized).toBe(true);\n\n      let fooPromise = router.navigate(\"/foo\");\n      expect(router.state.navigation.state).toBe(\"loading\");\n\n      let barPromise = router.navigate(\"/bar\");\n\n      // This should resolve _without_ us resolving the loader\n      await fooPromise;\n      await barPromise;\n\n      expect(router.state.navigation.state).toBe(\"idle\");\n      expect(router.state.location.pathname).toBe(\"/bar\");\n\n      let fooPromise2 = router.navigate(\"/foo\", {\n        formMethod: \"post\",\n        formData: createFormData({ key: \"value\" }),\n      });\n      expect(router.state.navigation.state).toBe(\"submitting\");\n\n      let barPromise2 = router.navigate(\"/bar\");\n\n      // This should resolve _without_ us resolving the action\n      await fooPromise2;\n      await barPromise2;\n\n      expect(router.state.navigation.state).toBe(\"idle\");\n      expect(router.state.location.pathname).toBe(\"/bar\");\n\n      router.dispose();\n    });\n\n    it(\"allows returning undefined from actions/loaders\", async () => {\n      let t = setup({\n        routes: [\n          {\n            index: true,\n          },\n          {\n            id: \"path\",\n            path: \"/path\",\n            loader: true,\n            action: true,\n          },\n        ],\n      });\n\n      let nav1 = await t.navigate(\"/path\");\n      await nav1.loaders.path.resolve(undefined);\n      expect(t.router.state).toMatchObject({\n        location: {\n          pathname: \"/path\",\n        },\n        loaderData: {\n          path: undefined,\n        },\n        errors: null,\n      });\n\n      await t.navigate(\"/\");\n      expect(t.router.state).toMatchObject({\n        location: {\n          pathname: \"/\",\n        },\n        errors: {},\n      });\n\n      let nav3 = await t.navigate(\"/path\", {\n        formMethod: \"post\",\n        formData: createFormData({}),\n      });\n      await nav3.actions.path.resolve(undefined);\n      await nav3.loaders.path.resolve(\"PATH\");\n      expect(t.router.state).toMatchObject({\n        location: {\n          pathname: \"/path\",\n        },\n        actionData: {\n          path: undefined,\n        },\n        loaderData: {\n          path: \"PATH\",\n        },\n        errors: null,\n      });\n    });\n  });\n\n  describe(\"router.enhanceRoutes\", () => {\n    // Detect any failures inside the router navigate code\n    afterEach(() => cleanup());\n\n    it(\"should retain existing routes until revalidation completes on loader removal\", async () => {\n      let t = initializeTest();\n      let ogRoutes = t.router.routes;\n      let A = await t.navigate(\"/foo\");\n      await A.loaders.foo.resolve(\"foo\");\n      expect(t.router.state.loaderData).toMatchObject({\n        root: \"ROOT\",\n        foo: \"foo\",\n      });\n\n      let newRoutes = t.enhanceRoutes([\n        {\n          path: \"\",\n          id: \"root\",\n          hasErrorBoundary: true,\n          loader: true,\n          children: [\n            {\n              path: \"/\",\n              id: \"index\",\n              loader: true,\n              action: true,\n              hasErrorBoundary: false,\n            },\n            {\n              path: \"/foo\",\n              id: \"foo\",\n              loader: false,\n              action: true,\n              hasErrorBoundary: false,\n            },\n          ],\n        },\n      ]);\n      t._internalSetRoutes(newRoutes);\n\n      // Get a new revalidation helper that should use the updated routes\n      let R = await t.revalidate();\n      expect(t.router.state.revalidation).toBe(\"loading\");\n\n      // Should still expose be the og routes until revalidation completes\n      expect(t.router.routes).toBe(ogRoutes);\n\n      // Resolve any loaders that should have ran (foo's loader has been removed)\n      await R.loaders.root.resolve(\"ROOT*\");\n      expect(t.router.state.revalidation).toBe(\"idle\");\n\n      // Routes should be updated\n      expect(t.router.routes).not.toBe(ogRoutes);\n      expect(t.router.routes).toEqual(newRoutes);\n\n      // Loader data should be updated and foo removed\n      expect(t.router.state.loaderData).toEqual({\n        root: \"ROOT*\",\n      });\n      expect(t.router.state.errors).toEqual(null);\n    });\n\n    it(\"should retain existing routes until revalidation completes on loader addition\", async () => {\n      let t = initializeTest();\n      let ogRoutes = t.router.routes;\n      await t.navigate(\"/no-loader\");\n      expect(t.router.state.loaderData).toMatchObject({\n        root: \"ROOT\",\n      });\n\n      let newRoutes = t.enhanceRoutes([\n        {\n          path: \"\",\n          id: \"root\",\n          hasErrorBoundary: true,\n          loader: true,\n          children: [\n            {\n              path: \"/no-loader\",\n              id: \"noLoader\",\n              loader: true,\n              action: true,\n              hasErrorBoundary: false,\n            },\n          ],\n        },\n      ]);\n      t._internalSetRoutes(newRoutes);\n      // Get a new revalidation helper that should use the updated routes\n      let R = await t.revalidate();\n      expect(t.router.state.revalidation).toBe(\"loading\");\n      expect(t.router.routes).toBe(ogRoutes);\n\n      // Should still expose be the og routes until revalidation completes\n      expect(t.router.routes).toBe(ogRoutes);\n\n      // Resolve any loaders that should have ran\n      await R.loaders.root.resolve(\"ROOT*\");\n      await R.loaders.noLoader.resolve(\"NO_LOADER*\");\n      expect(t.router.state.revalidation).toBe(\"idle\");\n\n      // Routes should be updated\n      expect(t.router.routes).not.toBe(ogRoutes);\n      expect(t.router.routes).toEqual(newRoutes);\n\n      // Loader data should be updated\n      expect(t.router.state.loaderData).toEqual({\n        root: \"ROOT*\",\n        noLoader: \"NO_LOADER*\",\n      });\n      expect(t.router.state.errors).toEqual(null);\n    });\n\n    it(\"should retain existing routes until interrupting navigation completes\", async () => {\n      let t = initializeTest();\n      let ogRoutes = t.router.routes;\n      let A = await t.navigate(\"/foo\");\n      await A.loaders.foo.resolve(\"foo\");\n      expect(t.router.state.loaderData).toMatchObject({\n        root: \"ROOT\",\n        foo: \"foo\",\n      });\n\n      let newRoutes = t.enhanceRoutes([\n        {\n          path: \"\",\n          id: \"root\",\n          hasErrorBoundary: true,\n          loader: true,\n          children: [\n            {\n              path: \"/\",\n              id: \"index\",\n              loader: false,\n              action: true,\n              hasErrorBoundary: false,\n            },\n            {\n              path: \"/foo\",\n              id: \"foo\",\n              loader: false,\n              action: true,\n              hasErrorBoundary: false,\n            },\n          ],\n        },\n      ]);\n      t._internalSetRoutes(newRoutes);\n\n      // Revalidate and interrupt with a navigation\n      let R = await t.revalidate();\n      let N = await t.navigate(\"/?revalidate\");\n\n      expect(t.router.state.navigation.state).toBe(\"loading\");\n      expect(t.router.state.revalidation).toBe(\"loading\");\n\n      // Should still expose be the og routes until navigation completes\n      expect(t.router.routes).toBe(ogRoutes);\n\n      // Revalidation cancelled so this shouldn't make it through\n      await R.loaders.root.resolve(\"ROOT STALE\");\n\n      // Resolve any loaders that should have ran (foo's loader has been removed)\n      await N.loaders.root.resolve(\"ROOT*\");\n      expect(t.router.state.navigation.state).toBe(\"idle\");\n      expect(t.router.state.revalidation).toBe(\"idle\");\n\n      // Routes should be updated\n      expect(t.router.routes).not.toBe(ogRoutes);\n      expect(t.router.routes).toEqual(newRoutes);\n\n      // Loader data should be updated\n      expect(t.router.state.loaderData).toEqual({\n        root: \"ROOT*\",\n      });\n      expect(t.router.state.errors).toEqual(null);\n    });\n\n    it(\"should retain existing routes until interrupted navigation completes\", async () => {\n      let t = initializeTest();\n      let ogRoutes = t.router.routes;\n\n      let N = await t.navigate(\"/foo\");\n\n      let newRoutes = t.enhanceRoutes([\n        {\n          path: \"\",\n          id: \"root\",\n          hasErrorBoundary: true,\n          loader: true,\n          children: [\n            {\n              path: \"/\",\n              id: \"index\",\n              loader: false,\n              action: true,\n              hasErrorBoundary: false,\n            },\n            {\n              path: \"/foo\",\n              id: \"foo\",\n              loader: false,\n              action: true,\n              hasErrorBoundary: false,\n            },\n          ],\n        },\n      ]);\n      t._internalSetRoutes(newRoutes);\n\n      // Interrupt /foo navigation with a revalidation\n      let R = await t.revalidate();\n\n      expect(t.router.state.navigation.state).toBe(\"loading\");\n      expect(t.router.state.revalidation).toBe(\"loading\");\n\n      // Should still expose be the og routes until navigation completes\n      expect(t.router.routes).toBe(ogRoutes);\n\n      // NAvigation interrupted so this shouldn't make it through\n      await N.loaders.root.resolve(\"ROOT STALE\");\n\n      // Resolve any loaders that should have ran (foo's loader has been removed)\n      await R.loaders.root.resolve(\"ROOT*\");\n      expect(t.router.state.navigation.state).toBe(\"idle\");\n      expect(t.router.state.revalidation).toBe(\"idle\");\n\n      // Routes should be updated\n      expect(t.router.routes).not.toBe(ogRoutes);\n      expect(t.router.routes).toEqual(newRoutes);\n\n      // Loader data should be updated\n      expect(t.router.state.loaderData).toEqual({\n        root: \"ROOT*\",\n      });\n      expect(t.router.state.errors).toEqual(null);\n    });\n\n    it(\"should retain existing routes until revalidation completes on loader removal (fetch)\", async () => {\n      let rootDfd = createDeferred();\n      let fooDfd = createDeferred();\n      let ogRoutes: AgnosticDataRouteObject[] = [\n        {\n          path: \"/\",\n          id: \"root\",\n          hasErrorBoundary: true,\n          loader: () => rootDfd.promise,\n          children: [\n            {\n              index: true,\n              id: \"index\",\n              hasErrorBoundary: false,\n            },\n            {\n              path: \"foo\",\n              id: \"foo\",\n              loader: () => fooDfd.promise,\n              children: undefined,\n              hasErrorBoundary: false,\n            },\n          ],\n        },\n      ];\n      let router = createRouter({\n        routes: ogRoutes,\n        history: createMemoryHistory(),\n        hydrationData: {\n          loaderData: {\n            root: \"ROOT INITIAL\",\n          },\n        },\n      });\n      let fetcherData = getFetcherData(router);\n      router.initialize();\n\n      let key = \"key\";\n      router.fetch(key, \"root\", \"/foo\");\n      await fooDfd.resolve(\"FOO\");\n      await tick();\n      expect(fetcherData.get(key)).toBe(\"FOO\");\n\n      let rootDfd2 = createDeferred();\n      let newRoutes: AgnosticDataRouteObject[] = [\n        {\n          path: \"/\",\n          id: \"root\",\n          loader: () => rootDfd2.promise,\n          hasErrorBoundary: true,\n          children: [\n            {\n              index: true,\n              id: \"index\",\n              hasErrorBoundary: false,\n            },\n            {\n              path: \"foo\",\n              id: \"foo\",\n              children: undefined,\n              hasErrorBoundary: false,\n            },\n          ],\n        },\n      ];\n\n      router._internalSetRoutes(newRoutes);\n\n      // Interrupt /foo navigation with a revalidation\n      router.revalidate();\n\n      expect(router.state.revalidation).toBe(\"loading\");\n\n      // Should still expose be the og routes until navigation completes\n      expect(router.routes).toEqual(ogRoutes);\n\n      // Resolve any loaders that should have ran (foo's loader has been removed)\n      await rootDfd2.resolve(\"ROOT*\");\n      await tick();\n      expect(router.state.revalidation).toBe(\"idle\");\n\n      // Routes should be updated\n      expect(router.routes).not.toEqual(ogRoutes);\n      expect(router.routes).toEqual(newRoutes);\n\n      // Loader data should be updated\n      expect(router.state.loaderData).toEqual({\n        root: \"ROOT*\",\n      });\n\n      // Fetcher should have been revalidated but throw an error since the\n      // loader was removed\n      // The data remains in the UI layer in this test setup since it hasn't\n      // unmounted - but normally it would unmount and the data would be removed\n      expect(fetcherData.get(\"key\")).toBe(\"FOO\");\n      expect(router.state.errors).toMatchInlineSnapshot(`\n        {\n          \"root\": ErrorResponseImpl {\n            \"data\": \"Error: No route matches URL \"/foo\"\",\n            \"error\": [Error: No route matches URL \"/foo\"],\n            \"internal\": true,\n            \"status\": 404,\n            \"statusText\": \"Not Found\",\n          },\n        }\n      `);\n\n      cleanup(router);\n    });\n\n    it(\"should retain existing routes until revalidation completes on route removal (fetch)\", async () => {\n      let rootDfd = createDeferred();\n      let fooDfd = createDeferred();\n      let ogRoutes: AgnosticDataRouteObject[] = [\n        {\n          path: \"/\",\n          id: \"root\",\n          hasErrorBoundary: true,\n          loader: () => rootDfd.promise,\n          children: [\n            {\n              index: true,\n              id: \"index\",\n              hasErrorBoundary: false,\n            },\n            {\n              path: \"foo\",\n              id: \"foo\",\n              loader: () => fooDfd.promise,\n              children: undefined,\n              hasErrorBoundary: false,\n            },\n          ],\n        },\n      ];\n      let router = createRouter({\n        routes: ogRoutes,\n        history: createMemoryHistory(),\n        hydrationData: {\n          loaderData: {\n            root: \"ROOT INITIAL\",\n          },\n        },\n      });\n      let fetcherData = getFetcherData(router);\n      router.initialize();\n\n      let key = \"key\";\n      router.fetch(key, \"root\", \"/foo\");\n      await fooDfd.resolve(\"FOO\");\n      expect(fetcherData.get(key)).toBe(\"FOO\");\n\n      let rootDfd2 = createDeferred();\n      let newRoutes: AgnosticDataRouteObject[] = [\n        {\n          path: \"/\",\n          id: \"root\",\n          loader: () => rootDfd2.promise,\n          hasErrorBoundary: true,\n          children: [\n            {\n              index: true,\n              id: \"index\",\n              hasErrorBoundary: false,\n            },\n          ],\n        },\n      ];\n\n      router._internalSetRoutes(newRoutes);\n\n      // Interrupt /foo navigation with a revalidation\n      router.revalidate();\n\n      expect(router.state.revalidation).toBe(\"loading\");\n\n      // Should still expose be the og routes until navigation completes\n      expect(router.routes).toEqual(ogRoutes);\n\n      // Resolve any loaders that should have ran (foo's loader has been removed)\n      await rootDfd2.resolve(\"ROOT*\");\n      expect(router.state.revalidation).toBe(\"idle\");\n\n      // Routes should be updated\n      expect(router.routes).not.toEqual(ogRoutes);\n      expect(router.routes).toEqual(newRoutes);\n\n      // Loader data should be updated\n      expect(router.state.loaderData).toEqual({\n        root: \"ROOT*\",\n      });\n      // Fetcher should have been revalidated but thrown a 404 wince the route was removed\n      // The data remains in the UI layer in this test setup since it hasn't\n      // unmounted - but normally it would unmount and the data would be removed\n      expect(fetcherData.get(key)).toBe(\"FOO\");\n      expect(router.state.errors).toEqual({\n        root: new ErrorResponseImpl(\n          404,\n          \"Not Found\",\n          new Error('No route matches URL \"/foo\"'),\n          true,\n        ),\n      });\n\n      cleanup(router);\n    });\n  });\n\n  describe(\"router.dispose\", () => {\n    it(\"should cancel pending navigations\", async () => {\n      let t = setup({\n        routes: TASK_ROUTES,\n        initialEntries: [\"/\"],\n        hydrationData: {\n          loaderData: {\n            root: \"ROOT DATA\",\n            index: \"INDEX DATA\",\n          },\n        },\n      });\n\n      let A = await t.navigate(\"/tasks\");\n      expect(t.router.state.navigation.state).toBe(\"loading\");\n\n      t.router.dispose();\n      expect(A.loaders.tasks.signal.aborted).toBe(true);\n    });\n\n    it(\"should cancel pending fetchers\", async () => {\n      let t = setup({\n        routes: TASK_ROUTES,\n        initialEntries: [\"/\"],\n        hydrationData: {\n          loaderData: {\n            root: \"ROOT DATA\",\n            index: \"INDEX DATA\",\n          },\n        },\n      });\n\n      let A = await t.fetch(\"/tasks\");\n      let B = await t.fetch(\"/tasks\");\n\n      t.router.dispose();\n      expect(A.loaders.tasks.signal.aborted).toBe(true);\n      expect(B.loaders.tasks.signal.aborted).toBe(true);\n    });\n  });\n});\n"
  },
  {
    "path": "packages/react-router/__tests__/router/scroll-restoration-test.ts",
    "content": "import type { TestRouteObject } from \"./utils/data-router-setup\";\nimport { cleanup, setup } from \"./utils/data-router-setup\";\nimport { createFormData } from \"./utils/utils\";\n\ndescribe(\"scroll restoration\", () => {\n  afterEach(() => cleanup());\n\n  // Version of TASK_ROUTES with no root loader to allow for initialized\n  // hydrationData:null usage\n  const SCROLL_ROUTES: TestRouteObject[] = [\n    {\n      path: \"/\",\n      children: [\n        {\n          id: \"index\",\n          index: true,\n          loader: true,\n        },\n        {\n          id: \"tasks\",\n          path: \"tasks\",\n          loader: true,\n          action: true,\n        },\n        {\n          path: \"no-loader\",\n        },\n      ],\n    },\n  ];\n\n  describe(\"scroll restoration\", () => {\n    it(\"restores scroll on initial load (w/o hydrationData)\", async () => {\n      let t = setup({\n        routes: SCROLL_ROUTES,\n        initialEntries: [\"/no-loader\"],\n      });\n\n      expect(t.router.state.restoreScrollPosition).toBe(null);\n      expect(t.router.state.preventScrollReset).toBe(false);\n\n      // Assume initial location had a saved position\n      let positions = { default: 50 };\n      t.router.enableScrollRestoration(positions, () => 0);\n      expect(t.router.state.restoreScrollPosition).toBe(50);\n    });\n\n    it(\"restores scroll on initial load (w/ hydrationData)\", async () => {\n      let t = setup({\n        routes: SCROLL_ROUTES,\n        initialEntries: [\"/\"],\n        hydrationData: {\n          loaderData: {\n            index: \"INDEX\",\n          },\n        },\n      });\n\n      expect(t.router.state.restoreScrollPosition).toBe(false);\n      expect(t.router.state.preventScrollReset).toBe(false);\n\n      // Assume initial location had a saved position\n      let positions = { default: 50 };\n      t.router.enableScrollRestoration(positions, () => 0);\n      expect(t.router.state.restoreScrollPosition).toBe(false);\n    });\n\n    it(\"restores scroll on navigations\", async () => {\n      let t = setup({\n        routes: SCROLL_ROUTES,\n        initialEntries: [\"/\"],\n        hydrationData: {\n          loaderData: {\n            index: \"INDEX_DATA\",\n          },\n        },\n      });\n\n      expect(t.router.state.restoreScrollPosition).toBe(false);\n      expect(t.router.state.preventScrollReset).toBe(false);\n\n      let positions = {};\n\n      // Simulate scrolling to 100 on /\n      let activeScrollPosition = 100;\n      t.router.enableScrollRestoration(positions, () => activeScrollPosition);\n\n      // No restoration on first click to /tasks\n      let nav1 = await t.navigate(\"/tasks\");\n      await nav1.loaders.tasks.resolve(\"TASKS\");\n      expect(t.router.state.restoreScrollPosition).toBe(null);\n      expect(t.router.state.preventScrollReset).toBe(false);\n\n      // Simulate scrolling down on /tasks\n      activeScrollPosition = 200;\n\n      // Restore on pop back to /\n      let nav2 = await t.navigate(-1);\n      expect(t.router.state.restoreScrollPosition).toBe(null);\n      await nav2.loaders.index.resolve(\"INDEX\");\n      expect(t.router.state.restoreScrollPosition).toBe(100);\n      expect(t.router.state.preventScrollReset).toBe(false);\n\n      // Restore on pop forward to /tasks\n      let nav3 = await t.navigate(1);\n      await nav3.loaders.tasks.resolve(\"TASKS\");\n      expect(t.router.state.restoreScrollPosition).toBe(200);\n      expect(t.router.state.preventScrollReset).toBe(false);\n    });\n\n    it(\"restores scroll using custom key\", async () => {\n      let t = setup({\n        routes: SCROLL_ROUTES,\n        initialEntries: [\"/\"],\n        hydrationData: {\n          loaderData: {\n            index: \"INDEX_DATA\",\n          },\n        },\n      });\n\n      expect(t.router.state.restoreScrollPosition).toBe(false);\n      expect(t.router.state.preventScrollReset).toBe(false);\n\n      let positions = { \"/tasks\": 100 };\n      let activeScrollPosition = 0;\n      t.router.enableScrollRestoration(\n        positions,\n        () => activeScrollPosition,\n        (l) => l.pathname,\n      );\n\n      let nav1 = await t.navigate(\"/tasks\");\n      await nav1.loaders.tasks.resolve(\"TASKS\");\n      expect(t.router.state.restoreScrollPosition).toBe(100);\n      expect(t.router.state.preventScrollReset).toBe(false);\n    });\n\n    it(\"does not strip the basename from the location provided to getKey\", async () => {\n      let t = setup({\n        routes: SCROLL_ROUTES,\n        basename: \"/base\",\n        initialEntries: [\"/base\"],\n        hydrationData: {\n          loaderData: {\n            index: \"INDEX_DATA\",\n          },\n        },\n      });\n\n      let positions = { \"/base/tasks\": 100 };\n      let activeScrollPosition = 0;\n      let pathname;\n      t.router.enableScrollRestoration(\n        positions,\n        () => activeScrollPosition,\n        (l) => {\n          pathname = l.pathname;\n          return l.pathname;\n        },\n      );\n\n      let nav1 = await t.navigate(\"/tasks\");\n      await nav1.loaders.tasks.resolve(\"TASKS\");\n      expect(pathname).toBe(\"/base/tasks\");\n      expect(t.router.state.restoreScrollPosition).toBe(100);\n      expect(t.router.state.preventScrollReset).toBe(false);\n    });\n\n    it(\"restores scroll on GET submissions\", async () => {\n      let t = setup({\n        routes: SCROLL_ROUTES,\n        initialEntries: [\"/tasks\"],\n        hydrationData: {\n          loaderData: {\n            tasks: \"TASKS\",\n          },\n        },\n      });\n\n      expect(t.router.state.restoreScrollPosition).toBe(false);\n      expect(t.router.state.preventScrollReset).toBe(false);\n      // We were previously on tasks at 100\n      let positions = { \"/tasks\": 100 };\n      // But we've scrolled up to 50 to submit.  We'll save this overtop of\n      // the 100 when we start this submission navigation and then restore to\n      // 50 below\n      let activeScrollPosition = 50;\n      t.router.enableScrollRestoration(\n        positions,\n        () => activeScrollPosition,\n        (l) => l.pathname,\n      );\n\n      let nav1 = await t.navigate(\"/tasks\", {\n        formMethod: \"get\",\n        formData: createFormData({ key: \"value\" }),\n      });\n      await nav1.loaders.tasks.resolve(\"TASKS2\");\n      expect(t.router.state.restoreScrollPosition).toBe(50);\n      expect(t.router.state.preventScrollReset).toBe(false);\n    });\n\n    it(\"restores scroll on POST submissions\", async () => {\n      let t = setup({\n        routes: SCROLL_ROUTES,\n        initialEntries: [\"/tasks\"],\n        hydrationData: {\n          loaderData: {\n            index: \"INDEX_DATA\",\n          },\n        },\n      });\n\n      expect(t.router.state.restoreScrollPosition).toBe(false);\n      expect(t.router.state.preventScrollReset).toBe(false);\n      // We were previously on tasks at 100\n      let positions = { \"/tasks\": 100 };\n      // But we've scrolled up to 50 to submit.  We'll save this overtop of\n      // the 100 when we start this submission navigation and then restore to\n      // 50 below\n      let activeScrollPosition = 50;\n      t.router.enableScrollRestoration(\n        positions,\n        () => activeScrollPosition,\n        (l) => l.pathname,\n      );\n\n      let nav1 = await t.navigate(\"/tasks\", {\n        formMethod: \"post\",\n        formData: createFormData({}),\n      });\n      const nav2 = await nav1.actions.tasks.redirectReturn(\"/tasks\");\n      await nav2.loaders.tasks.resolve(\"TASKS\");\n      expect(t.router.state.restoreScrollPosition).toBe(50);\n      expect(t.router.state.preventScrollReset).toBe(false);\n    });\n\n    it(\"does not restore scroll on revalidation\", async () => {\n      let t = setup({\n        routes: SCROLL_ROUTES,\n        initialEntries: [\"/\"],\n      });\n\n      expect(t.router.state.restoreScrollPosition).toBe(null);\n      expect(t.router.state.preventScrollReset).toBe(false);\n\n      let positions = {};\n\n      // Simulate scrolling to 100 on /\n      let activeScrollPosition = 100;\n      t.router.enableScrollRestoration(positions, () => activeScrollPosition);\n\n      // Revalidate\n      let R = await t.revalidate();\n      await R.loaders.index.resolve(\"INDEX\");\n\n      expect(t.router.state.restoreScrollPosition).toBe(false);\n      expect(t.router.state.preventScrollReset).toBe(false);\n\n      // Scroll to 200\n      activeScrollPosition = 200;\n\n      // Go to /tasks\n      let nav1 = await t.navigate(\"/tasks\");\n      await nav1.loaders.tasks.resolve(\"TASKS\");\n\n      expect(t.router.state.restoreScrollPosition).toBe(null);\n      expect(t.router.state.preventScrollReset).toBe(false);\n\n      // Restore on pop back to /\n      let nav2 = await t.navigate(-1);\n      expect(t.router.state.restoreScrollPosition).toBe(null);\n      await nav2.loaders.index.resolve(\"INDEX\");\n      expect(t.router.state.restoreScrollPosition).toBe(200);\n      expect(t.router.state.preventScrollReset).toBe(false);\n    });\n  });\n\n  describe(\"scroll reset\", () => {\n    describe(\"default behavior\", () => {\n      it(\"resets on navigations\", async () => {\n        let t = setup({\n          routes: SCROLL_ROUTES,\n          initialEntries: [\"/\"],\n          hydrationData: {\n            loaderData: {\n              index: \"INDEX_DATA\",\n            },\n          },\n        });\n\n        expect(t.router.state.restoreScrollPosition).toBe(false);\n        expect(t.router.state.preventScrollReset).toBe(false);\n\n        let positions = {};\n        let activeScrollPosition = 0;\n        t.router.enableScrollRestoration(positions, () => activeScrollPosition);\n\n        let nav1 = await t.navigate(\"/tasks\");\n        await nav1.loaders.tasks.resolve(\"TASKS\");\n        expect(t.router.state.restoreScrollPosition).toBe(null);\n        expect(t.router.state.preventScrollReset).toBe(false);\n      });\n\n      it(\"resets on navigations that redirect\", async () => {\n        let t = setup({\n          routes: SCROLL_ROUTES,\n          initialEntries: [\"/\"],\n          hydrationData: {\n            loaderData: {\n              index: \"INDEX_DATA\",\n            },\n          },\n        });\n\n        expect(t.router.state.restoreScrollPosition).toBe(false);\n        expect(t.router.state.preventScrollReset).toBe(false);\n\n        let positions = {};\n        let activeScrollPosition = 0;\n        t.router.enableScrollRestoration(positions, () => activeScrollPosition);\n\n        let nav1 = await t.navigate(\"/tasks\");\n        let nav2 = await nav1.loaders.tasks.redirectReturn(\"/\");\n        await nav2.loaders.index.resolve(\"INDEX_DATA 2\");\n        expect(t.router.state.restoreScrollPosition).toBe(null);\n        expect(t.router.state.preventScrollReset).toBe(false);\n      });\n\n      it(\"does not reset on submission navigations\", async () => {\n        let t = setup({\n          routes: SCROLL_ROUTES,\n          initialEntries: [\"/\"],\n          hydrationData: {\n            loaderData: {\n              index: \"INDEX_DATA\",\n            },\n          },\n        });\n\n        expect(t.router.state.restoreScrollPosition).toBe(false);\n        expect(t.router.state.preventScrollReset).toBe(false);\n\n        let positions = {};\n        let activeScrollPosition = 0;\n        t.router.enableScrollRestoration(positions, () => activeScrollPosition);\n\n        let nav1 = await t.navigate(\"/tasks\", {\n          formMethod: \"post\",\n          formData: createFormData({}),\n        });\n        await nav1.actions.tasks.resolve(\"ACTION\");\n        await nav1.loaders.tasks.resolve(\"TASKS\");\n        expect(t.router.state.restoreScrollPosition).toBe(null);\n        expect(t.router.state.preventScrollReset).toBe(true);\n      });\n\n      it(\"resets on submission navigations that redirect\", async () => {\n        let t = setup({\n          routes: SCROLL_ROUTES,\n          initialEntries: [\"/\"],\n          hydrationData: {\n            loaderData: {\n              index: \"INDEX_DATA\",\n            },\n          },\n        });\n\n        expect(t.router.state.restoreScrollPosition).toBe(false);\n        expect(t.router.state.preventScrollReset).toBe(false);\n\n        let positions = {};\n        let activeScrollPosition = 0;\n        t.router.enableScrollRestoration(positions, () => activeScrollPosition);\n\n        let nav1 = await t.navigate(\"/tasks\", {\n          formMethod: \"post\",\n          formData: createFormData({}),\n        });\n        let nav2 = await nav1.actions.tasks.redirectReturn(\"/\");\n        await nav2.loaders.index.resolve(\"INDEX_DATA2\");\n        expect(t.router.state.restoreScrollPosition).toBe(null);\n        expect(t.router.state.preventScrollReset).toBe(false);\n      });\n\n      it(\"resets on fetch submissions that redirect\", async () => {\n        let t = setup({\n          routes: SCROLL_ROUTES,\n          initialEntries: [\"/tasks\"],\n          hydrationData: {\n            loaderData: {\n              tasks: \"TASKS\",\n            },\n          },\n        });\n\n        expect(t.router.state.restoreScrollPosition).toBe(false);\n        expect(t.router.state.preventScrollReset).toBe(false);\n\n        let positions = {};\n        let activeScrollPosition = 0;\n        t.router.enableScrollRestoration(positions, () => activeScrollPosition);\n\n        let nav1 = await t.fetch(\"/tasks\", {\n          formMethod: \"post\",\n          formData: createFormData({}),\n        });\n        let nav2 = await nav1.actions.tasks.redirectReturn(\"/tasks\");\n        await nav2.loaders.tasks.resolve(\"TASKS 2\");\n        expect(t.router.state.restoreScrollPosition).toBe(null);\n        expect(t.router.state.preventScrollReset).toBe(false);\n      });\n    });\n\n    describe(\"user-specified flag preventScrollReset flag\", () => {\n      it(\"prevents scroll reset on navigations\", async () => {\n        let t = setup({\n          routes: SCROLL_ROUTES,\n          initialEntries: [\"/\"],\n          hydrationData: {\n            loaderData: {\n              index: \"INDEX_DATA\",\n            },\n          },\n        });\n\n        expect(t.router.state.restoreScrollPosition).toBe(false);\n        expect(t.router.state.preventScrollReset).toBe(false);\n\n        let positions = {};\n        let activeScrollPosition = 0;\n        t.router.enableScrollRestoration(positions, () => activeScrollPosition);\n\n        let nav1 = await t.navigate(\"/tasks\", { preventScrollReset: true });\n        await nav1.loaders.tasks.resolve(\"TASKS\");\n        expect(t.router.state.restoreScrollPosition).toBe(null);\n        expect(t.router.state.preventScrollReset).toBe(true);\n      });\n\n      it(\"prevents scroll reset on navigations that redirect\", async () => {\n        let t = setup({\n          routes: SCROLL_ROUTES,\n          initialEntries: [\"/\"],\n          hydrationData: {\n            loaderData: {\n              index: \"INDEX_DATA\",\n            },\n          },\n        });\n\n        expect(t.router.state.restoreScrollPosition).toBe(false);\n        expect(t.router.state.preventScrollReset).toBe(false);\n\n        let positions = {};\n        let activeScrollPosition = 0;\n        t.router.enableScrollRestoration(positions, () => activeScrollPosition);\n\n        let nav1 = await t.navigate(\"/tasks\", { preventScrollReset: true });\n        let nav2 = await nav1.loaders.tasks.redirectReturn(\"/\");\n        await nav2.loaders.index.resolve(\"INDEX_DATA 2\");\n        expect(t.router.state.restoreScrollPosition).toBe(null);\n        expect(t.router.state.preventScrollReset).toBe(true);\n      });\n\n      it(\"prevents scroll reset on submission navigations\", async () => {\n        let t = setup({\n          routes: SCROLL_ROUTES,\n          initialEntries: [\"/\"],\n          hydrationData: {\n            loaderData: {\n              index: \"INDEX_DATA\",\n            },\n          },\n        });\n\n        expect(t.router.state.restoreScrollPosition).toBe(false);\n        expect(t.router.state.preventScrollReset).toBe(false);\n\n        let positions = {};\n        let activeScrollPosition = 0;\n        t.router.enableScrollRestoration(positions, () => activeScrollPosition);\n\n        let nav1 = await t.navigate(\"/tasks\", {\n          formMethod: \"post\",\n          formData: createFormData({}),\n          preventScrollReset: true,\n        });\n        await nav1.actions.tasks.resolve(\"ACTION\");\n        await nav1.loaders.tasks.resolve(\"TASKS\");\n        expect(t.router.state.restoreScrollPosition).toBe(null);\n        expect(t.router.state.preventScrollReset).toBe(true);\n      });\n\n      it(\"prevents scroll reset on submission navigations that redirect\", async () => {\n        let t = setup({\n          routes: SCROLL_ROUTES,\n          initialEntries: [\"/\"],\n          hydrationData: {\n            loaderData: {\n              index: \"INDEX_DATA\",\n            },\n          },\n        });\n\n        expect(t.router.state.restoreScrollPosition).toBe(false);\n        expect(t.router.state.preventScrollReset).toBe(false);\n\n        let positions = {};\n        let activeScrollPosition = 0;\n        t.router.enableScrollRestoration(positions, () => activeScrollPosition);\n\n        let nav1 = await t.navigate(\"/tasks\", {\n          formMethod: \"post\",\n          formData: createFormData({}),\n          preventScrollReset: true,\n        });\n        let nav2 = await nav1.actions.tasks.redirectReturn(\"/\");\n        await nav2.loaders.index.resolve(\"INDEX_DATA2\");\n        expect(t.router.state.restoreScrollPosition).toBe(null);\n        expect(t.router.state.preventScrollReset).toBe(true);\n      });\n\n      it(\"prevents scroll reset on fetch submissions that redirect\", async () => {\n        let t = setup({\n          routes: SCROLL_ROUTES,\n          initialEntries: [\"/tasks\"],\n          hydrationData: {\n            loaderData: {\n              tasks: \"TASKS\",\n            },\n          },\n        });\n\n        expect(t.router.state.restoreScrollPosition).toBe(false);\n        expect(t.router.state.preventScrollReset).toBe(false);\n\n        let positions = {};\n        let activeScrollPosition = 0;\n        t.router.enableScrollRestoration(positions, () => activeScrollPosition);\n\n        let nav1 = await t.fetch(\"/tasks\", {\n          formMethod: \"post\",\n          formData: createFormData({}),\n          preventScrollReset: true,\n        });\n        let nav2 = await nav1.actions.tasks.redirectReturn(\"/tasks\");\n        await nav2.loaders.tasks.resolve(\"TASKS 2\");\n        expect(t.router.state.restoreScrollPosition).toBe(null);\n        expect(t.router.state.preventScrollReset).toBe(true);\n      });\n\n      it(\"persists through concurrent redirecting fetchers\", async () => {\n        let t = setup({\n          routes: SCROLL_ROUTES,\n          initialEntries: [\"/tasks\"],\n          hydrationData: {\n            loaderData: {\n              tasks: \"TASKS\",\n            },\n          },\n        });\n\n        expect(t.router.state.restoreScrollPosition).toBe(false);\n        expect(t.router.state.preventScrollReset).toBe(false);\n\n        let positions = {};\n        let activeScrollPosition = 0;\n        t.router.enableScrollRestoration(positions, () => activeScrollPosition);\n\n        let nav1 = await t.fetch(\"/tasks\", {\n          formMethod: \"post\",\n          formData: createFormData({}),\n          preventScrollReset: true,\n        });\n\n        let nav2 = await t.fetch(\"/tasks\", {\n          formMethod: \"post\",\n          formData: createFormData({}),\n          preventScrollReset: true,\n        });\n\n        let nav3 = await nav1.actions.tasks.redirectReturn(\"/tasks\");\n        await nav3.loaders.tasks.resolve(\"TASKS 2\");\n        expect(t.router.state.restoreScrollPosition).toBe(null);\n        expect(t.router.state.preventScrollReset).toBe(true);\n\n        let nav4 = await nav2.actions.tasks.redirectReturn(\"/tasks\");\n        await nav4.loaders.tasks.resolve(\"TASKS 3\");\n        expect(t.router.state.restoreScrollPosition).toBe(null);\n        expect(t.router.state.preventScrollReset).toBe(true);\n      });\n    });\n  });\n});\n"
  },
  {
    "path": "packages/react-router/__tests__/router/should-revalidate-test.ts",
    "content": "import { createMemoryHistory } from \"../../lib/router/history\";\nimport { IDLE_NAVIGATION, createRouter } from \"../../lib/router/router\";\nimport { ErrorResponseImpl, redirect } from \"../../lib/router/utils\";\nimport type { ShouldRevalidateFunctionArgs } from \"../../lib/router/utils\";\nimport { urlMatch } from \"./utils/custom-matchers\";\nimport { cleanup, getFetcherData, setup } from \"./utils/data-router-setup\";\nimport { createFormData, tick } from \"./utils/utils\";\n\ninterface CustomMatchers {\n  urlMatch(url: string);\n}\n\ndeclare global {\n  namespace jest {\n    interface Expect extends CustomMatchers {}\n    interface Matchers extends CustomMatchers {}\n    interface InverseAsymmetricMatchers extends CustomMatchers {}\n  }\n}\n\nexpect.extend({\n  urlMatch,\n});\n\ndescribe(\"shouldRevalidate\", () => {\n  afterEach(() => cleanup());\n\n  it(\"provides a default implementation\", async () => {\n    let rootLoader = jest.fn((...args) => \"ROOT\");\n\n    let history = createMemoryHistory();\n    let router = createRouter({\n      history,\n      routes: [\n        {\n          path: \"\",\n          loader: async (...args) => rootLoader(...args),\n          children: [\n            {\n              path: \"/\",\n              id: \"index\",\n            },\n            {\n              path: \"/child\",\n              action: async () => null,\n            },\n            {\n              path: \"/redirect\",\n              action: async () =>\n                new Response(null, {\n                  status: 301,\n                  headers: { location: \"/\" },\n                }),\n            },\n            {\n              path: \"/cookie\",\n              loader: async () =>\n                new Response(null, {\n                  status: 301,\n                  headers: {\n                    location: \"/\",\n                    \"X-Remix-Revalidate\": \"1\",\n                  },\n                }),\n            },\n          ],\n        },\n      ],\n    });\n    router.initialize();\n\n    // Initial load - no existing data, should always call loader\n    await tick();\n    expect(rootLoader.mock.calls.length).toBe(1);\n    rootLoader.mockClear();\n\n    // Should not re-run on normal navigations re-using the loader\n    router.navigate(\"/child\");\n    await tick();\n    router.navigate(\"/\");\n    await tick();\n    router.navigate(\"/child\");\n    await tick();\n    expect(rootLoader.mock.calls.length).toBe(0);\n    rootLoader.mockClear();\n\n    // Should call on same-path navigations\n    router.navigate(\"/child\");\n    await tick();\n    expect(rootLoader.mock.calls.length).toBe(1);\n    rootLoader.mockClear();\n\n    // Should call on query string changes\n    router.navigate(\"/child?key=value\");\n    await tick();\n    expect(rootLoader.mock.calls.length).toBe(1);\n    rootLoader.mockClear();\n\n    // Should call after form submission revalidation\n    router.navigate(\"/child\", {\n      formMethod: \"post\",\n      formData: createFormData({ gosh: \"dang\" }),\n    });\n    await tick();\n    expect(rootLoader.mock.calls.length).toBe(1);\n    rootLoader.mockClear();\n\n    // Should call after form submission redirect\n    router.navigate(\"/redirect\", {\n      formMethod: \"post\",\n      formData: createFormData({ gosh: \"dang\" }),\n    });\n    await tick();\n    expect(rootLoader.mock.calls.length).toBe(1);\n    rootLoader.mockClear();\n\n    // Should call after loader redirect with X-Remix-Revalidate\n    router.navigate(\"/cookie\");\n    await tick();\n    expect(rootLoader.mock.calls.length).toBe(1);\n    rootLoader.mockClear();\n\n    router.dispose();\n  });\n\n  it(\"delegates to the route if it should reload or not\", async () => {\n    let rootLoader = jest.fn((...args) => \"ROOT\");\n    let childLoader = jest.fn((...args) => \"CHILD\");\n    let paramsLoader = jest.fn((...args) => \"PARAMS\");\n    let shouldRevalidate = jest.fn((args) => false);\n\n    let history = createMemoryHistory();\n    let router = createRouter({\n      history,\n      routes: [\n        {\n          path: \"\",\n          id: \"root\",\n          loader: async (...args) => rootLoader(...args),\n          shouldRevalidate: (args) => shouldRevalidate(args) === true,\n\n          children: [\n            {\n              path: \"/\",\n              id: \"index\",\n            },\n            {\n              path: \"/child\",\n              id: \"child\",\n              loader: async (...args) => childLoader(...args),\n              action: async () => ({ ok: false }),\n            },\n            {\n              path: \"/params/:a/:b\",\n              id: \"params\",\n              loader: async (...args) => paramsLoader(...args),\n            },\n          ],\n        },\n      ],\n    });\n    router.initialize();\n\n    // Initial load - no existing data, should always call loader and should\n    // not give use ability to opt-out\n    await tick();\n    expect(rootLoader.mock.calls.length).toBe(1);\n    expect(shouldRevalidate.mock.calls.length).toBe(0);\n    rootLoader.mockClear();\n    shouldRevalidate.mockClear();\n\n    // Should not re-run on normal navigations re-using the loader\n    router.navigate(\"/child\");\n    await tick();\n    router.navigate(\"/\");\n    await tick();\n    router.navigate(\"/child\");\n    await tick();\n    expect(rootLoader.mock.calls.length).toBe(0);\n    expect(shouldRevalidate.mock.calls.length).toBe(3);\n    rootLoader.mockClear();\n    shouldRevalidate.mockClear();\n\n    // Check that we pass the right args to shouldRevalidate and respect it's answer\n    shouldRevalidate.mockImplementation(() => true);\n    router.navigate(\"/params/aValue/bValue\");\n    await tick();\n    expect(rootLoader.mock.calls.length).toBe(1);\n    let expectedArg: ShouldRevalidateFunctionArgs = {\n      currentParams: {},\n      currentUrl: expect.urlMatch(\"http://localhost/child\"),\n      nextParams: {\n        a: \"aValue\",\n        b: \"bValue\",\n      },\n      nextUrl: expect.urlMatch(\"http://localhost/params/aValue/bValue\"),\n      defaultShouldRevalidate: false,\n      actionResult: undefined,\n    };\n    expect(shouldRevalidate.mock.calls[0][0]).toMatchObject(expectedArg);\n    rootLoader.mockClear();\n    shouldRevalidate.mockClear();\n\n    // On actions we send along the action result\n    shouldRevalidate.mockImplementation(\n      ({ actionResult }) => actionResult.ok === true,\n    );\n    router.navigate(\"/child\", {\n      formMethod: \"post\",\n      formData: createFormData({}),\n    });\n    await tick();\n    expect(rootLoader.mock.calls.length).toBe(0);\n\n    router.dispose();\n  });\n\n  it(\"includes submissions on actions that return data\", async () => {\n    let shouldRevalidate = jest.fn(() => true);\n\n    let history = createMemoryHistory({ initialEntries: [\"/child\"] });\n    let router = createRouter({\n      history,\n      routes: [\n        {\n          path: \"/\",\n          id: \"root\",\n          loader: () => \"ROOT\",\n          shouldRevalidate,\n          children: [\n            {\n              path: \"child\",\n              id: \"child\",\n              loader: () => \"CHILD\",\n              action: () => \"ACTION\",\n            },\n          ],\n        },\n      ],\n    });\n    router.initialize();\n\n    // Initial load - no existing data, should always call loader and should\n    // not give use ability to opt-out\n    await tick();\n    router.navigate(\"/child\", {\n      formMethod: \"post\",\n      formData: createFormData({ key: \"value\" }),\n    });\n    await tick();\n    expect(shouldRevalidate.mock.calls.length).toBe(1);\n    // @ts-expect-error\n    let arg = shouldRevalidate.mock.calls[0][0];\n    let expectedArg: ShouldRevalidateFunctionArgs = {\n      currentParams: {},\n      currentUrl: expect.urlMatch(\"http://localhost/child\"),\n      nextParams: {},\n      nextUrl: expect.urlMatch(\"http://localhost/child\"),\n      defaultShouldRevalidate: true,\n      formMethod: \"POST\",\n      formAction: \"/child\",\n      formEncType: \"application/x-www-form-urlencoded\",\n      actionResult: \"ACTION\",\n    };\n    expect(arg).toMatchObject(expectedArg);\n    // @ts-expect-error\n    expect(Object.fromEntries(arg.formData)).toEqual({ key: \"value\" });\n\n    router.dispose();\n  });\n\n  it(\"includes submission on actions that return redirects\", async () => {\n    let shouldRevalidate = jest.fn(() => true);\n\n    let history = createMemoryHistory({ initialEntries: [\"/child\"] });\n    let router = createRouter({\n      history,\n      routes: [\n        {\n          path: \"/\",\n          id: \"root\",\n          loader: () => \"ROOT\",\n          shouldRevalidate,\n          children: [\n            {\n              path: \"child\",\n              id: \"child\",\n              loader: () => \"CHILD\",\n              action: () => redirect(\"/\"),\n            },\n          ],\n        },\n      ],\n    });\n    router.initialize();\n\n    // Initial load - no existing data, should always call loader and should\n    // not give use ability to opt-out\n    await tick();\n    router.navigate(\"/child\", {\n      formMethod: \"post\",\n      formData: createFormData({ key: \"value\" }),\n    });\n    await tick();\n    expect(shouldRevalidate.mock.calls.length).toBe(1);\n    // @ts-expect-error\n    let arg = shouldRevalidate.mock.calls[0][0];\n    let expectedArg: ShouldRevalidateFunctionArgs = {\n      currentParams: {},\n      currentUrl: expect.urlMatch(\"http://localhost/child\"),\n      nextParams: {},\n      nextUrl: expect.urlMatch(\"http://localhost/\"),\n      defaultShouldRevalidate: true,\n      formMethod: \"POST\",\n      formAction: \"/child\",\n      formEncType: \"application/x-www-form-urlencoded\",\n      actionResult: undefined,\n    };\n    expect(arg).toMatchObject(expectedArg);\n    // @ts-expect-error\n    expect(Object.fromEntries(arg.formData)).toEqual({ key: \"value\" });\n\n    router.dispose();\n  });\n\n  it(\"includes submissions and acton status on actions that return Responses\", async () => {\n    let shouldRevalidate = jest.fn(() => true);\n\n    let history = createMemoryHistory({ initialEntries: [\"/child\"] });\n    let router = createRouter({\n      history,\n      routes: [\n        {\n          path: \"/\",\n          id: \"root\",\n          loader: () => \"ROOT\",\n          shouldRevalidate,\n          children: [\n            {\n              path: \"child\",\n              id: \"child\",\n              loader: () => \"CHILD\",\n              action: () => new Response(\"ACTION\", { status: 201 }),\n            },\n          ],\n        },\n      ],\n    });\n    router.initialize();\n\n    // Initial load - no existing data, should always call loader and should\n    // not give use ability to opt-out\n    await tick();\n    router.navigate(\"/child\", {\n      formMethod: \"post\",\n      formData: createFormData({ key: \"value\" }),\n    });\n    await tick();\n    expect(shouldRevalidate.mock.calls.length).toBe(1);\n    // @ts-expect-error\n    let arg = shouldRevalidate.mock.calls[0][0];\n    let expectedArg: ShouldRevalidateFunctionArgs = {\n      currentParams: {},\n      currentUrl: expect.urlMatch(\"http://localhost/child\"),\n      nextParams: {},\n      nextUrl: expect.urlMatch(\"http://localhost/child\"),\n      defaultShouldRevalidate: true,\n      formMethod: \"POST\",\n      formAction: \"/child\",\n      formEncType: \"application/x-www-form-urlencoded\",\n      actionResult: \"ACTION\",\n      actionStatus: 201,\n    };\n    expect(arg).toMatchObject(expectedArg);\n    // @ts-expect-error\n    expect(Object.fromEntries(arg.formData)).toEqual({ key: \"value\" });\n\n    router.dispose();\n  });\n\n  it(\"includes json submissions\", async () => {\n    let shouldRevalidate = jest.fn(() => true);\n\n    let history = createMemoryHistory();\n    let router = createRouter({\n      history,\n      routes: [\n        {\n          path: \"/\",\n          id: \"root\",\n          loader: () => \"ROOT*\",\n          action: () => \"ACTION\",\n          shouldRevalidate,\n        },\n      ],\n      hydrationData: { loaderData: { root: \"ROOT\" } },\n    }).initialize();\n\n    await tick();\n    router.navigate(null, {\n      formMethod: \"post\",\n      formEncType: \"application/json\",\n      body: { key: \"value\" },\n    });\n    await tick();\n    expect(shouldRevalidate.mock.calls.length).toBe(1);\n    // @ts-expect-error\n    let arg = shouldRevalidate.mock.calls[0][0];\n    let expectedArg: Partial<ShouldRevalidateFunctionArgs> = {\n      formMethod: \"POST\",\n      formAction: \"/\",\n      formEncType: \"application/json\",\n      text: undefined,\n      formData: undefined,\n      json: { key: \"value\" },\n      actionResult: \"ACTION\",\n    };\n    expect(arg).toMatchObject(expectedArg);\n\n    router.dispose();\n  });\n\n  it(\"includes text submissions\", async () => {\n    let shouldRevalidate = jest.fn(() => true);\n\n    let history = createMemoryHistory();\n    let router = createRouter({\n      history,\n      routes: [\n        {\n          path: \"/\",\n          id: \"root\",\n          loader: () => \"ROOT*\",\n          action: () => \"ACTION\",\n          shouldRevalidate,\n        },\n      ],\n      hydrationData: { loaderData: { root: \"ROOT\" } },\n    }).initialize();\n\n    await tick();\n    router.navigate(null, {\n      formMethod: \"post\",\n      formEncType: \"text/plain\",\n      body: \"hello world\",\n    });\n    await tick();\n    expect(shouldRevalidate.mock.calls.length).toBe(1);\n    // @ts-expect-error\n    let arg = shouldRevalidate.mock.calls[0][0];\n    let expectedArg: Partial<ShouldRevalidateFunctionArgs> = {\n      formMethod: \"POST\",\n      formAction: \"/\",\n      formEncType: \"text/plain\",\n      text: \"hello world\",\n      formData: undefined,\n      json: undefined,\n      actionResult: \"ACTION\",\n    };\n    expect(arg).toMatchObject(expectedArg);\n\n    router.dispose();\n  });\n\n  it(\"provides the default implementation to the route function\", async () => {\n    let rootLoader = jest.fn((...args) => \"ROOT\");\n\n    let history = createMemoryHistory();\n    let router = createRouter({\n      history,\n      routes: [\n        {\n          path: \"\",\n          loader: async (...args) => rootLoader(...args),\n          shouldRevalidate: ({ defaultShouldRevalidate }) =>\n            defaultShouldRevalidate,\n          children: [\n            {\n              path: \"/\",\n              id: \"index\",\n            },\n            {\n              path: \"/child\",\n              action: async () => null,\n            },\n            {\n              path: \"/redirect\",\n              action: async () =>\n                new Response(null, {\n                  status: 301,\n                  headers: { location: \"/\" },\n                }),\n            },\n            {\n              path: \"/cookie\",\n              loader: async () =>\n                new Response(null, {\n                  status: 301,\n                  headers: {\n                    location: \"/\",\n                    \"X-Remix-Revalidate\": \"1\",\n                  },\n                }),\n            },\n          ],\n        },\n      ],\n    });\n    router.initialize();\n\n    // Initial load - no existing data, should always call loader\n    await tick();\n    expect(rootLoader.mock.calls.length).toBe(1);\n    rootLoader.mockClear();\n\n    // Should not re-run on normal navigations re-using the loader\n    router.navigate(\"/child\");\n    await tick();\n    router.navigate(\"/\");\n    await tick();\n    router.navigate(\"/child\");\n    await tick();\n    expect(rootLoader.mock.calls.length).toBe(0);\n    rootLoader.mockClear();\n\n    // Should call on same-path navigations\n    router.navigate(\"/child\");\n    await tick();\n    expect(rootLoader.mock.calls.length).toBe(1);\n    rootLoader.mockClear();\n\n    // Should call on query string changes\n    router.navigate(\"/child?key=value\");\n    await tick();\n    expect(rootLoader.mock.calls.length).toBe(1);\n    rootLoader.mockClear();\n\n    // Should call after form submission revalidation\n    router.navigate(\"/child\", {\n      formMethod: \"post\",\n      formData: createFormData({ gosh: \"dang\" }),\n    });\n    await tick();\n    expect(rootLoader.mock.calls.length).toBe(1);\n    rootLoader.mockClear();\n\n    // Should call after form submission redirect\n    router.navigate(\"/redirect\", {\n      formMethod: \"post\",\n      formData: createFormData({ gosh: \"dang\" }),\n    });\n    await tick();\n    expect(rootLoader.mock.calls.length).toBe(1);\n    rootLoader.mockClear();\n\n    // Should call after loader redirect with X-Remix-Revalidate\n    router.navigate(\"/cookie\");\n    await tick();\n    expect(rootLoader.mock.calls.length).toBe(1);\n    rootLoader.mockClear();\n\n    router.dispose();\n  });\n\n  it(\"applies to fetcher loads\", async () => {\n    let count = 0;\n    let fetchLoader = jest.fn((...args) => `FETCH ${++count}`);\n    let shouldRevalidate = jest.fn((args) => false);\n\n    let history = createMemoryHistory();\n    let router = createRouter({\n      history,\n      routes: [\n        {\n          path: \"\",\n          id: \"root\",\n\n          children: [\n            {\n              path: \"/\",\n              id: \"index\",\n            },\n            {\n              path: \"/child\",\n              id: \"child\",\n            },\n            {\n              path: \"/fetch\",\n              id: \"fetch\",\n              loader: async (...args) => fetchLoader(...args),\n              shouldRevalidate: (args) => shouldRevalidate(args) === true,\n            },\n          ],\n        },\n      ],\n    });\n    let fetcherData = getFetcherData(router);\n    router.initialize();\n    await tick();\n\n    let key = \"key\";\n    router.fetch(key, \"root\", \"/fetch\");\n    await tick();\n    expect(router.getFetcher(key).state).toBe(\"idle\");\n    expect(fetcherData.get(key)).toBe(\"FETCH 1\");\n    expect(shouldRevalidate.mock.calls.length).toBe(0);\n\n    // Normal navigations should trigger fetcher shouldRevalidate with\n    // defaultShouldRevalidate=false\n    router.navigate(\"/child\");\n    await tick();\n    expect(shouldRevalidate.mock.calls.length).toBe(1);\n    expect(shouldRevalidate.mock.calls[0][0]).toMatchObject({\n      currentParams: {},\n      currentUrl: expect.urlMatch(\"http://localhost/\"),\n      nextParams: {},\n      nextUrl: expect.urlMatch(\"http://localhost/child\"),\n      defaultShouldRevalidate: false,\n    });\n    expect(router.getFetcher(key).state).toBe(\"idle\");\n    expect(fetcherData.get(key)).toBe(\"FETCH 1\");\n\n    router.navigate(\"/\");\n    await tick();\n    expect(shouldRevalidate.mock.calls.length).toBe(2);\n    expect(shouldRevalidate.mock.calls[1][0]).toMatchObject({\n      currentParams: {},\n      currentUrl: expect.urlMatch(\"http://localhost/child\"),\n      nextParams: {},\n      nextUrl: expect.urlMatch(\"http://localhost/\"),\n      defaultShouldRevalidate: false,\n    });\n    expect(router.getFetcher(key).state).toBe(\"idle\");\n    expect(fetcherData.get(key)).toBe(\"FETCH 1\");\n\n    // Submission navigations should trigger fetcher shouldRevalidate with\n    // defaultShouldRevalidate=true\n    router.navigate(\"/child\", {\n      formMethod: \"post\",\n      formData: createFormData({}),\n    });\n    await tick();\n    expect(router.getFetcher(key).state).toBe(\"idle\");\n    expect(fetcherData.get(key)).toBe(\"FETCH 1\");\n    expect(shouldRevalidate.mock.calls.length).toBe(3);\n    expect(shouldRevalidate.mock.calls[2][0]).toMatchObject({\n      currentParams: {},\n      currentUrl: expect.urlMatch(\"http://localhost/\"),\n      nextParams: {},\n      nextUrl: expect.urlMatch(\"http://localhost/child\"),\n      formAction: \"/child\",\n      formData: createFormData({}),\n      formEncType: \"application/x-www-form-urlencoded\",\n      formMethod: \"POST\",\n      defaultShouldRevalidate: true,\n    });\n\n    router.dispose();\n  });\n\n  it(\"applies to fetcher submissions and sends fetcher actionResult through\", async () => {\n    let shouldRevalidate = jest.fn((args) => true);\n\n    let history = createMemoryHistory();\n    let router = createRouter({\n      history,\n      routes: [\n        {\n          path: \"\",\n          id: \"root\",\n\n          children: [\n            {\n              path: \"/\",\n              id: \"index\",\n              loader: () => \"INDEX\",\n              shouldRevalidate,\n            },\n            {\n              path: \"/fetch\",\n              id: \"fetch\",\n              action: () => \"FETCH\",\n            },\n          ],\n        },\n      ],\n    });\n    let fetcherData = getFetcherData(router);\n    router.initialize();\n    await tick();\n\n    let key = \"key\";\n    router.fetch(key, \"root\", \"/fetch\", {\n      formMethod: \"post\",\n      formData: createFormData({ key: \"value\" }),\n    });\n    await tick();\n    expect(router.getFetcher(key).state).toBe(\"idle\");\n    expect(fetcherData.get(key)).toBe(\"FETCH\");\n\n    let arg = shouldRevalidate.mock.calls[0][0];\n    expect(arg).toMatchInlineSnapshot(`\n      {\n        \"actionResult\": \"FETCH\",\n        \"actionStatus\": undefined,\n        \"currentParams\": {},\n        \"currentUrl\": \"http://localhost/\",\n        \"defaultShouldRevalidate\": true,\n        \"formAction\": \"/fetch\",\n        \"formData\": FormData {},\n        \"formEncType\": \"application/x-www-form-urlencoded\",\n        \"formMethod\": \"POST\",\n        \"json\": undefined,\n        \"nextParams\": {},\n        \"nextUrl\": \"http://localhost/\",\n        \"text\": undefined,\n      }\n    `);\n    expect(Object.fromEntries(arg.formData)).toEqual({ key: \"value\" });\n\n    router.dispose();\n  });\n\n  it(\"applies to fetcher submissions when action redirects\", async () => {\n    let shouldRevalidate = jest.fn((args) => true);\n\n    let history = createMemoryHistory();\n    let router = createRouter({\n      history,\n      routes: [\n        {\n          path: \"\",\n          id: \"root\",\n\n          children: [\n            {\n              path: \"/\",\n              id: \"index\",\n              loader: () => \"INDEX\",\n              shouldRevalidate,\n            },\n            {\n              path: \"/fetch\",\n              id: \"fetch\",\n              action: () => redirect(\"/\"),\n            },\n          ],\n        },\n      ],\n    });\n    let fetcherData = getFetcherData(router);\n    router.initialize();\n    await tick();\n\n    let key = \"key\";\n    router.fetch(key, \"root\", \"/fetch\", {\n      formMethod: \"post\",\n      formData: createFormData({ key: \"value\" }),\n    });\n    await tick();\n    expect(router.getFetcher(key).state).toBe(\"idle\");\n    expect(fetcherData.get(key)).toBe(undefined);\n\n    let arg = shouldRevalidate.mock.calls[0][0];\n    expect(arg).toMatchInlineSnapshot(`\n      {\n        \"actionResult\": undefined,\n        \"actionStatus\": undefined,\n        \"currentParams\": {},\n        \"currentUrl\": \"http://localhost/\",\n        \"defaultShouldRevalidate\": true,\n        \"formAction\": \"/fetch\",\n        \"formData\": FormData {},\n        \"formEncType\": \"application/x-www-form-urlencoded\",\n        \"formMethod\": \"POST\",\n        \"json\": undefined,\n        \"nextParams\": {},\n        \"nextUrl\": \"http://localhost/\",\n        \"text\": undefined,\n      }\n    `);\n\n    router.dispose();\n  });\n\n  it(\"preserves non-revalidated loaderData on navigations\", async () => {\n    let count = 0;\n    let history = createMemoryHistory();\n    let router = createRouter({\n      history,\n      routes: [\n        {\n          path: \"\",\n          id: \"root\",\n          loader: () => `ROOT ${++count}`,\n\n          children: [\n            {\n              path: \"/\",\n              id: \"index\",\n              loader: (args) => \"SHOULD NOT GET CALLED\",\n              shouldRevalidate: () => false,\n            },\n          ],\n        },\n      ],\n      hydrationData: {\n        loaderData: {\n          root: \"ROOT 0\",\n          index: \"INDEX\",\n        },\n      },\n    });\n    router.initialize();\n    await tick();\n\n    // Navigating to the same link would normally cause all loaders to re-run\n    router.navigate(\"/\");\n    await tick();\n    expect(router.state.loaderData).toEqual({\n      root: \"ROOT 1\",\n      index: \"INDEX\",\n    });\n\n    router.navigate(\"/\");\n    await tick();\n    expect(router.state.loaderData).toEqual({\n      root: \"ROOT 2\",\n      index: \"INDEX\",\n    });\n\n    router.dispose();\n  });\n\n  it(\"preserves non-revalidated loaderData on fetches\", async () => {\n    let count = 0;\n    let history = createMemoryHistory();\n    let router = createRouter({\n      history,\n      routes: [\n        {\n          path: \"\",\n          id: \"root\",\n\n          children: [\n            {\n              path: \"/\",\n              id: \"index\",\n              loader: () => \"SHOULD NOT GET CALLED\",\n              shouldRevalidate: () => false,\n            },\n            {\n              path: \"/fetch\",\n              id: \"fetch\",\n              action: () => `FETCH ${++count}`,\n            },\n          ],\n        },\n      ],\n      hydrationData: {\n        loaderData: {\n          index: \"INDEX\",\n        },\n      },\n    });\n    let fetcherData = getFetcherData(router);\n    router.initialize();\n    await tick();\n\n    let key = \"key\";\n\n    router.fetch(key, \"root\", \"/fetch\", {\n      formMethod: \"post\",\n      formData: createFormData({ key: \"value\" }),\n    });\n    await tick();\n    expect(router.getFetcher(key).state).toBe(\"idle\");\n    expect(fetcherData.get(key)).toBe(\"FETCH 1\");\n    expect(router.state.loaderData).toMatchObject({\n      index: \"INDEX\",\n    });\n\n    router.fetch(key, \"root\", \"/fetch\", {\n      formMethod: \"post\",\n      formData: createFormData({ key: \"value\" }),\n    });\n    await tick();\n    expect(router.getFetcher(key).state).toBe(\"idle\");\n    expect(fetcherData.get(key)).toBe(\"FETCH 2\");\n    expect(router.state.loaderData).toMatchObject({\n      index: \"INDEX\",\n    });\n\n    router.dispose();\n  });\n\n  it(\"requires an explicit false return value to override default true behavior\", async () => {\n    let count = 0;\n    let returnValue = true;\n    let history = createMemoryHistory();\n    let router = createRouter({\n      history,\n      routes: [\n        {\n          path: \"\",\n          id: \"root\",\n          loader: () => ++count,\n          shouldRevalidate: () => returnValue,\n        },\n      ],\n      hydrationData: {\n        loaderData: {\n          root: 0,\n        },\n      },\n    });\n    router.initialize();\n\n    await tick();\n    expect(router.state.loaderData).toEqual({\n      root: 0,\n    });\n\n    router.revalidate();\n    await tick();\n    expect(router.state.loaderData).toEqual({\n      root: 1,\n    });\n\n    // @ts-expect-error\n    returnValue = undefined;\n    router.revalidate();\n    await tick();\n    expect(router.state.loaderData).toEqual({\n      root: 2,\n    });\n\n    // @ts-expect-error\n    returnValue = null;\n    router.revalidate();\n    await tick();\n    expect(router.state.loaderData).toEqual({\n      root: 3,\n    });\n\n    // @ts-expect-error\n    returnValue = \"\";\n    router.revalidate();\n    await tick();\n    expect(router.state.loaderData).toEqual({\n      root: 4,\n    });\n\n    returnValue = false;\n    router.revalidate();\n    await tick();\n    expect(router.state.loaderData).toEqual({\n      root: 4, // No revalidation\n    });\n\n    router.dispose();\n  });\n\n  it(\"requires an explicit true return value to override default false behavior\", async () => {\n    let count = 0;\n    let returnValue = false;\n    let history = createMemoryHistory({ initialEntries: [\"/a\"] });\n    let router = createRouter({\n      history,\n      routes: [\n        {\n          path: \"/\",\n          id: \"root\",\n          loader: () => ++count,\n          shouldRevalidate: () => returnValue,\n\n          children: [\n            {\n              path: \"a\",\n              id: \"a\",\n            },\n            {\n              path: \"b\",\n              id: \"b\",\n            },\n          ],\n        },\n      ],\n      hydrationData: {\n        loaderData: {\n          root: 0,\n        },\n      },\n    });\n    router.initialize();\n\n    await tick();\n    expect(router.state.loaderData).toEqual({\n      root: 0,\n    });\n\n    router.navigate(\"/b\");\n    await tick();\n    expect(router.state.loaderData).toEqual({\n      root: 0,\n    });\n\n    // @ts-expect-error\n    returnValue = undefined;\n    router.navigate(\"/a\");\n    await tick();\n    expect(router.state.loaderData).toEqual({\n      root: 0,\n    });\n\n    // @ts-expect-error\n    returnValue = null;\n    router.navigate(\"/b\");\n    await tick();\n    expect(router.state.loaderData).toEqual({\n      root: 0,\n    });\n\n    // @ts-expect-error\n    returnValue = \"truthy\";\n    router.navigate(\"/a\");\n    await tick();\n    expect(router.state.loaderData).toEqual({\n      root: 0,\n    });\n\n    returnValue = true;\n    router.navigate(\"/b\");\n    await tick();\n    expect(router.state.loaderData).toEqual({\n      root: 1,\n    });\n\n    router.dispose();\n  });\n\n  it(\"does not revalidates after actions returning 4xx/5xx responses with flag\", async () => {\n    let count = -1;\n    let responses = [\n      new Response(\"DATA 400\", { status: 400 }),\n      new Response(\"DATA 500\", { status: 500 }),\n    ];\n    let rootLoader = jest.fn(() => \"ROOT \" + count);\n\n    let history = createMemoryHistory();\n    let router = createRouter({\n      history,\n      routes: [\n        {\n          id: \"root\",\n          path: \"/\",\n          loader: async () => rootLoader(),\n          children: [\n            {\n              id: \"index\",\n              index: true,\n              hasErrorBoundary: true,\n              action: () => responses[++count],\n            },\n          ],\n        },\n      ],\n      hydrationData: {\n        loaderData: {\n          root: \"ROOT\",\n        },\n      },\n    });\n    router.initialize();\n\n    router.navigate(\"/?index\", {\n      formMethod: \"post\",\n      formData: createFormData({ gosh: \"dang\" }),\n    });\n    await tick();\n    expect(rootLoader.mock.calls.length).toBe(0);\n    expect(router.state).toMatchObject({\n      location: { pathname: \"/\" },\n      navigation: { state: \"idle\" },\n      loaderData: { root: \"ROOT\" },\n      actionData: { index: \"DATA 400\" },\n      errors: null,\n    });\n\n    router.navigate(\"/?index\", {\n      formMethod: \"post\",\n      formData: createFormData({ gosh: \"dang\" }),\n    });\n    await tick();\n    expect(rootLoader.mock.calls.length).toBe(0);\n    expect(router.state).toMatchObject({\n      location: { pathname: \"/\" },\n      navigation: { state: \"idle\" },\n      loaderData: { root: \"ROOT\" },\n      actionData: { index: \"DATA 500\" },\n      errors: null,\n    });\n\n    router.dispose();\n  });\n\n  it(\"does not revalidate after actions throwing 4xx/5xx responses with flag\", async () => {\n    let count = -1;\n    let responses = [\n      new Response(\"ERROR 400\", { status: 400 }),\n      new Response(\"ERROR 500\", { status: 500 }),\n    ];\n    let rootLoader = jest.fn(() => \"ROOT \" + count);\n\n    let history = createMemoryHistory();\n    let router = createRouter({\n      history,\n      routes: [\n        {\n          id: \"root\",\n          path: \"/\",\n          loader: async () => rootLoader(),\n          children: [\n            {\n              id: \"index\",\n              index: true,\n              hasErrorBoundary: true,\n              action: () => {\n                throw responses[++count];\n              },\n            },\n          ],\n        },\n      ],\n      hydrationData: {\n        loaderData: {\n          root: \"ROOT\",\n        },\n      },\n    });\n    router.initialize();\n\n    router.navigate(\"/?index\", {\n      formMethod: \"post\",\n      formData: createFormData({ gosh: \"dang\" }),\n    });\n    await tick();\n    expect(rootLoader.mock.calls.length).toBe(0);\n    expect(router.state).toMatchObject({\n      location: { pathname: \"/\" },\n      navigation: { state: \"idle\" },\n      loaderData: { root: \"ROOT\" },\n      actionData: null,\n      errors: { index: new ErrorResponseImpl(400, \"\", \"ERROR 400\") },\n    });\n\n    router.navigate(\"/?index\", {\n      formMethod: \"post\",\n      formData: createFormData({ gosh: \"dang\" }),\n    });\n    await tick();\n    expect(rootLoader.mock.calls.length).toBe(0);\n    expect(router.state).toMatchObject({\n      location: { pathname: \"/\" },\n      navigation: { state: \"idle\" },\n      loaderData: { root: \"ROOT\" },\n      actionData: null,\n      errors: { index: new ErrorResponseImpl(500, \"\", \"ERROR 500\") },\n    });\n\n    router.dispose();\n  });\n\n  it(\"preserves ancestor loaderData on bubbled action errors when not revalidating with a custom data strategy\", async () => {\n    let history = createMemoryHistory();\n    let router = createRouter({\n      history,\n      routes: [\n        {\n          id: \"root\",\n          path: \"/\",\n          hasErrorBoundary: true,\n          loader: () => \"NOPE\",\n          children: [\n            {\n              id: \"index\",\n              index: true,\n              action: () => {\n                throw new Response(\"ERROR 400\", { status: 400 });\n              },\n            },\n          ],\n        },\n      ],\n      hydrationData: {\n        loaderData: {\n          root: \"ROOT\",\n        },\n      },\n      async dataStrategy({ request, matches }) {\n        let keyedResults = {};\n        let matchesToLoad = matches.filter((match) =>\n          match.shouldCallHandler(\n            request.method === \"POST\"\n              ? undefined\n              : !match.shouldRevalidateArgs?.actionStatus ||\n                  match.shouldRevalidateArgs.actionStatus < 400,\n          ),\n        );\n        await Promise.all(\n          matchesToLoad.map(async (match) => {\n            keyedResults[match.route.id] = await match.resolve();\n          }),\n        );\n        return keyedResults;\n      },\n    });\n    router.initialize();\n\n    router.navigate(\"/?index\", {\n      formMethod: \"post\",\n      formData: createFormData({ gosh: \"dang\" }),\n    });\n    await tick();\n    expect(router.state).toMatchObject({\n      location: { pathname: \"/\" },\n      navigation: { state: \"idle\" },\n      loaderData: { root: \"ROOT\" },\n      actionData: null,\n      errors: { root: new ErrorResponseImpl(400, \"\", \"ERROR 400\") },\n    });\n\n    router.dispose();\n  });\n\n  describe(\"call-site revalidation opt out\", () => {\n    it(\"skips revalidation on loading navigation\", async () => {\n      let t = setup({\n        routes: [\n          {\n            id: \"index\",\n            path: \"/\",\n            loader: true,\n          },\n        ],\n        hydrationData: {\n          loaderData: {\n            index: \"INDEX\",\n          },\n        },\n      });\n\n      let A = await t.navigate(\"/?foo=bar\", {\n        unstable_defaultShouldRevalidate: false,\n      });\n\n      A.loaders.index.resolve(\"SHOULD NOT BE CALLED\");\n\n      expect(t.router.state).toMatchObject({\n        location: expect.objectContaining({\n          pathname: \"/\",\n          search: \"?foo=bar\",\n        }),\n        navigation: IDLE_NAVIGATION,\n        loaderData: {\n          index: \"INDEX\",\n        },\n      });\n    });\n\n    it(\"passes value through to route shouldRevalidate for loading navigations\", async () => {\n      let calledWithValue: boolean | undefined = undefined;\n      let t = setup({\n        routes: [\n          {\n            id: \"index\",\n            path: \"/\",\n            loader: true,\n            shouldRevalidate: ({ defaultShouldRevalidate }) => {\n              calledWithValue = defaultShouldRevalidate;\n              return defaultShouldRevalidate;\n            },\n          },\n        ],\n        hydrationData: {\n          loaderData: {\n            index: \"INDEX\",\n          },\n        },\n      });\n\n      let A = await t.navigate(\"/?foo=bar\", {\n        unstable_defaultShouldRevalidate: false,\n      });\n\n      A.loaders.index.resolve(\"SHOULD NOT BE CALLED\");\n\n      expect(calledWithValue).toBe(false);\n      expect(t.router.state).toMatchObject({\n        location: expect.objectContaining({\n          pathname: \"/\",\n          search: \"?foo=bar\",\n        }),\n        navigation: IDLE_NAVIGATION,\n        loaderData: {\n          index: \"INDEX\",\n        },\n      });\n    });\n\n    it(\"skips revalidation on submission navigation\", async () => {\n      let key = \"key\";\n      let t = setup({\n        routes: [\n          {\n            id: \"index\",\n            path: \"/\",\n            loader: true,\n            action: true,\n          },\n          {\n            id: \"fetch\",\n            path: \"/fetch\",\n            loader: true,\n          },\n        ],\n        hydrationData: {\n          loaderData: {\n            index: \"INDEX\",\n          },\n        },\n      });\n\n      // preload a fetcher\n      let A = await t.fetch(\"/fetch\", key);\n      await A.loaders.fetch.resolve(\"LOAD\");\n      expect(t.fetchers[key]).toMatchObject({\n        state: \"idle\",\n        data: \"LOAD\",\n      });\n\n      // submit action with shouldRevalidate=false\n      let B = await t.navigate(\n        \"/\",\n        {\n          formMethod: \"post\",\n          formData: createFormData({}),\n          unstable_defaultShouldRevalidate: false,\n        },\n        [\"fetch\"],\n      );\n\n      // resolve action — no loaders should trigger\n      await B.actions.index.resolve(\"ACTION\");\n\n      B.loaders.index.resolve(\"SHOULD NOT BE CALLED\");\n      B.loaders.fetch.resolve(\"SHOULD NOT BE CALLED\");\n\n      expect(t.router.state).toMatchObject({\n        navigation: IDLE_NAVIGATION,\n        actionData: {\n          index: \"ACTION\",\n        },\n        loaderData: {\n          index: \"INDEX\",\n        },\n      });\n      expect(t.fetchers[key]).toMatchObject({\n        state: \"idle\",\n        data: \"LOAD\",\n      });\n    });\n\n    it(\"passes value through to route shouldRevalidate on submission navigation\", async () => {\n      let key = \"key\";\n      let calledWithValue1: boolean | undefined = undefined;\n      let calledWithValue2: boolean | undefined = undefined;\n      let t = setup({\n        routes: [\n          {\n            id: \"index\",\n            path: \"/\",\n            loader: true,\n            action: true,\n            shouldRevalidate: ({ defaultShouldRevalidate }) => {\n              calledWithValue1 = defaultShouldRevalidate;\n              return defaultShouldRevalidate;\n            },\n          },\n          {\n            id: \"fetch\",\n            path: \"/fetch\",\n            loader: true,\n            shouldRevalidate: ({ defaultShouldRevalidate }) => {\n              calledWithValue2 = defaultShouldRevalidate;\n              return defaultShouldRevalidate;\n            },\n          },\n        ],\n        hydrationData: {\n          loaderData: {\n            index: \"INDEX\",\n          },\n        },\n      });\n\n      // preload a fetcher\n      let A = await t.fetch(\"/fetch\", key);\n      await A.loaders.fetch.resolve(\"LOAD\");\n      expect(t.fetchers[key]).toMatchObject({\n        state: \"idle\",\n        data: \"LOAD\",\n      });\n\n      // submit action with shouldRevalidate=false\n      let B = await t.navigate(\n        \"/\",\n        {\n          formMethod: \"post\",\n          formData: createFormData({}),\n          unstable_defaultShouldRevalidate: false,\n        },\n        [\"fetch\"],\n      );\n\n      // resolve action — no loaders should trigger\n      await B.actions.index.resolve(\"ACTION\");\n\n      B.loaders.index.resolve(\"SHOULD NOT BE CALLED\");\n      B.loaders.fetch.resolve(\"SHOULD NOT BE CALLED\");\n\n      expect(calledWithValue1).toBe(false);\n      expect(calledWithValue2).toBe(false);\n\n      expect(t.router.state).toMatchObject({\n        navigation: IDLE_NAVIGATION,\n        actionData: {\n          index: \"ACTION\",\n        },\n        loaderData: {\n          index: \"INDEX\",\n        },\n      });\n      expect(t.fetchers[key]).toMatchObject({\n        state: \"idle\",\n        data: \"LOAD\",\n      });\n    });\n\n    it(\"skips revalidation on fetcher.submit\", async () => {\n      let key = \"key\";\n      let actionKey = \"actionKey\";\n      let t = setup({\n        routes: [\n          {\n            id: \"index\",\n            path: \"/\",\n            loader: true,\n          },\n          {\n            id: \"fetch\",\n            path: \"/fetch\",\n            action: true,\n            loader: true,\n          },\n        ],\n        hydrationData: {\n          loaderData: {\n            index: \"INDEX\",\n          },\n        },\n      });\n\n      // preload a fetcher\n      let A = await t.fetch(\"/fetch\", key);\n      await A.loaders.fetch.resolve(\"LOAD\");\n      expect(t.fetchers[key]).toMatchObject({\n        state: \"idle\",\n        data: \"LOAD\",\n      });\n\n      // submit action with shouldRevalidate=false\n      let B = await t.fetch(\"/fetch\", actionKey, \"index\", {\n        formMethod: \"post\",\n        formData: createFormData({}),\n        unstable_defaultShouldRevalidate: false,\n      });\n      t.shimHelper(B.loaders, \"fetch\", \"loader\", \"fetch\");\n\n      // resolve action — no loaders should trigger\n      await B.actions.fetch.resolve(\"ACTION\");\n\n      B.loaders.index.resolve(\"SHOULD NOT BE CALLED\");\n      B.loaders.fetch.resolve(\"SHOULD NOT BE CALLED\");\n\n      expect(t.router.state.loaderData).toEqual({\n        index: \"INDEX\",\n      });\n      expect(t.fetchers[key]).toMatchObject({\n        state: \"idle\",\n        data: \"LOAD\",\n      });\n      expect(t.fetchers[actionKey]).toMatchObject({\n        state: \"idle\",\n        data: \"ACTION\",\n      });\n    });\n\n    it(\"passes through value on fetcher.submit\", async () => {\n      let key = \"key\";\n      let actionKey = \"actionKey\";\n      let calledWithValue1: boolean | undefined = undefined;\n      let calledWithValue2: boolean | undefined = undefined;\n      let t = setup({\n        routes: [\n          {\n            id: \"index\",\n            path: \"/\",\n            loader: true,\n            shouldRevalidate: ({ defaultShouldRevalidate }) => {\n              calledWithValue1 = defaultShouldRevalidate;\n              return defaultShouldRevalidate;\n            },\n          },\n          {\n            id: \"fetch\",\n            path: \"/fetch\",\n            action: true,\n            loader: true,\n            shouldRevalidate: ({ defaultShouldRevalidate }) => {\n              calledWithValue2 = defaultShouldRevalidate;\n              return defaultShouldRevalidate;\n            },\n          },\n        ],\n        hydrationData: {\n          loaderData: {\n            index: \"INDEX\",\n          },\n        },\n      });\n\n      // preload a fetcher\n      let A = await t.fetch(\"/fetch\", key);\n      await A.loaders.fetch.resolve(\"LOAD\");\n      expect(t.fetchers[key]).toMatchObject({\n        state: \"idle\",\n        data: \"LOAD\",\n      });\n\n      // submit action with shouldRevalidate=false\n      let B = await t.fetch(\"/fetch\", actionKey, \"index\", {\n        formMethod: \"post\",\n        formData: createFormData({}),\n        unstable_defaultShouldRevalidate: false,\n      });\n      t.shimHelper(B.loaders, \"fetch\", \"loader\", \"fetch\");\n\n      // resolve action — no loaders should trigger\n      await B.actions.fetch.resolve(\"ACTION\");\n\n      B.loaders.index.resolve(\"SHOULD NOT BE CALLED\");\n      B.loaders.fetch.resolve(\"SHOULD NOT BE CALLED\");\n\n      expect(calledWithValue1).toBe(false);\n      expect(calledWithValue2).toBe(false);\n      expect(t.router.state.loaderData).toEqual({\n        index: \"INDEX\",\n      });\n      expect(t.fetchers[key]).toMatchObject({\n        state: \"idle\",\n        data: \"LOAD\",\n      });\n      expect(t.fetchers[actionKey]).toMatchObject({\n        state: \"idle\",\n        data: \"ACTION\",\n      });\n    });\n\n    it(\"allows route to override call-site value\", async () => {\n      let key = \"key\";\n      let actionKey = \"actionKey\";\n      let calledWithValue1: boolean | undefined = undefined;\n      let calledWithValue2: boolean | undefined = undefined;\n      let t = setup({\n        routes: [\n          {\n            id: \"index\",\n            path: \"/\",\n            loader: true,\n            shouldRevalidate: ({ defaultShouldRevalidate }) => {\n              calledWithValue1 = defaultShouldRevalidate;\n              return true;\n            },\n          },\n          {\n            id: \"fetch\",\n            path: \"/fetch\",\n            action: true,\n            loader: true,\n            shouldRevalidate: ({ defaultShouldRevalidate }) => {\n              calledWithValue2 = defaultShouldRevalidate;\n              return defaultShouldRevalidate;\n            },\n          },\n        ],\n        hydrationData: {\n          loaderData: {\n            index: \"INDEX\",\n          },\n        },\n      });\n\n      // preload a fetcher\n      let A = await t.fetch(\"/fetch\", key);\n      await A.loaders.fetch.resolve(\"LOAD\");\n      expect(t.fetchers[key]).toMatchObject({\n        state: \"idle\",\n        data: \"LOAD\",\n      });\n\n      // submit action with shouldRevalidate=false\n      let B = await t.fetch(\"/fetch\", actionKey, \"index\", {\n        formMethod: \"post\",\n        formData: createFormData({}),\n        unstable_defaultShouldRevalidate: false,\n      });\n      t.shimHelper(B.loaders, \"fetch\", \"loader\", \"fetch\");\n\n      await B.actions.fetch.resolve(\"ACTION\");\n      await B.loaders.index.resolve(\"INDEX*\");\n      B.loaders.fetch.resolve(\"SHOULD NOT BE CALLED\");\n\n      expect(calledWithValue1).toBe(false);\n      expect(calledWithValue2).toBe(false);\n      expect(t.router.state.loaderData).toEqual({\n        index: \"INDEX*\",\n      });\n      expect(t.fetchers[key]).toMatchObject({\n        state: \"idle\",\n        data: \"LOAD\",\n      });\n      expect(t.fetchers[actionKey]).toMatchObject({\n        state: \"idle\",\n        data: \"ACTION\",\n      });\n    });\n  });\n});\n"
  },
  {
    "path": "packages/react-router/__tests__/router/ssr-test.ts",
    "content": "/**\n * @jest-environment node\n */\n\nimport urlDataStrategy from \"./utils/urlDataStrategy\";\nimport type {\n  StaticHandler,\n  StaticHandlerContext,\n} from \"../../lib/router/router\";\nimport {\n  createStaticHandler,\n  getStaticContextFromError,\n} from \"../../lib/router/router\";\nimport {\n  ErrorResponseImpl,\n  isRouteErrorResponse,\n  redirect,\n} from \"../../lib/router/utils\";\nimport { createDeferred } from \"./utils/data-router-setup\";\nimport {\n  createRequest,\n  createSubmitRequest,\n  invariant,\n  sleep,\n} from \"./utils/utils\";\n\nprocess.on(\"unhandledRejection\", (e) => {\n  console.error(\"unhandledRejection\", e);\n});\nprocess.on(\"uncaughtException\", (e) => {\n  console.error(\"uncaughtException\", e);\n});\n\ndescribe(\"ssr\", () => {\n  const SSR_ROUTES = [\n    {\n      id: \"index\",\n      path: \"/\",\n      loader: () => \"INDEX LOADER\",\n    },\n    {\n      id: \"parent\",\n      path: \"/parent\",\n      loader: () => \"PARENT LOADER\",\n      action: () => \"PARENT ACTION\",\n      children: [\n        {\n          id: \"parentIndex\",\n          index: true,\n          loader: () => \"PARENT INDEX LOADER\",\n          action: () => \"PARENT INDEX ACTION\",\n        },\n        {\n          id: \"child\",\n          path: \"child\",\n          loader: () => \"CHILD LOADER\",\n          action: () => \"CHILD ACTION\",\n        },\n        {\n          id: \"json\",\n          path: \"json\",\n          loader: () => Response.json({ type: \"loader\" }),\n          action: () => Response.json({ type: \"action\" }),\n        },\n        {\n          id: \"deferred\",\n          path: \"deferred\",\n          loader: ({ request }) => {\n            if (new URL(request.url).searchParams.has(\"reject\")) {\n              let promise = new Promise((_, r) =>\n                setTimeout(() => r(\"broken!\"), 10),\n              );\n              promise.catch(() => {});\n              return {\n                critical: \"loader\",\n                lazy: promise,\n              };\n            }\n            if (new URL(request.url).searchParams.has(\"undefined\")) {\n              return {\n                critical: \"loader\",\n                lazy: new Promise((r) => setTimeout(() => r(undefined), 10)),\n              };\n            }\n            if (new URL(request.url).searchParams.has(\"status\")) {\n              return {\n                critical: \"loader\",\n                lazy: new Promise((r) => setTimeout(() => r(\"lazy\"), 10)),\n              };\n            }\n            return {\n              critical: \"loader\",\n              lazy: new Promise((r) => setTimeout(() => r(\"lazy\"), 10)),\n            };\n          },\n        },\n        {\n          id: \"error\",\n          path: \"error\",\n          loader: () => Promise.reject(\"ERROR LOADER ERROR\"),\n          action: () => Promise.reject(\"ERROR ACTION ERROR\"),\n        },\n        {\n          id: \"errorBoundary\",\n          path: \"error-boundary\",\n          hasErrorBoundary: true,\n          loader: () => Promise.reject(\"ERROR BOUNDARY LOADER ERROR\"),\n          action: () => Promise.reject(\"ERROR BOUNDARY ACTION ERROR\"),\n        },\n      ],\n    },\n    {\n      id: \"redirect\",\n      path: \"/redirect\",\n      loader: () => redirect(\"/\"),\n    },\n    {\n      id: \"custom\",\n      path: \"/custom\",\n      loader: () =>\n        new Response(new URLSearchParams([[\"foo\", \"bar\"]]).toString(), {\n          headers: { \"Content-Type\": \"application/x-www-form-urlencoded\" },\n        }),\n    },\n  ];\n\n  // Regardless of if the URL is internal or external - all absolute URL\n  // responses should return untouched during SSR so the browser can handle\n  // them\n  let ABSOLUTE_URLS = [\n    \"http://localhost/\",\n    \"https://localhost/about\",\n    \"http://remix.run/blog\",\n    \"https://remix.run/blog\",\n    \"//remix.run/blog\",\n    \"app://whatever\",\n    \"mailto:hello@remix.run\",\n    \"web+remix:whatever\",\n  ];\n\n  describe(\"document requests\", () => {\n    it(\"should support document load navigations\", async () => {\n      let { query } = createStaticHandler(SSR_ROUTES);\n      let context = await query(createRequest(\"/parent/child\"));\n      expect(context).toMatchObject({\n        actionData: null,\n        loaderData: {\n          parent: \"PARENT LOADER\",\n          child: \"CHILD LOADER\",\n        },\n        errors: null,\n        location: { pathname: \"/parent/child\" },\n        matches: [{ route: { id: \"parent\" } }, { route: { id: \"child\" } }],\n      });\n    });\n\n    it(\"should support document load navigations with HEAD requests\", async () => {\n      let { query } = createStaticHandler(SSR_ROUTES);\n      let context = await query(\n        createRequest(\"/parent/child\", { method: \"HEAD\" }),\n      );\n      expect(context).toMatchObject({\n        actionData: null,\n        loaderData: {\n          parent: \"PARENT LOADER\",\n          child: \"CHILD LOADER\",\n        },\n        errors: null,\n        location: { pathname: \"/parent/child\" },\n        matches: [{ route: { id: \"parent\" } }, { route: { id: \"child\" } }],\n      });\n    });\n\n    it(\"should support document load navigations with a basename\", async () => {\n      let { query } = createStaticHandler(SSR_ROUTES, { basename: \"/base\" });\n      let context = await query(createRequest(\"/base/parent/child\"));\n      expect(context).toMatchObject({\n        actionData: null,\n        loaderData: {\n          parent: \"PARENT LOADER\",\n          child: \"CHILD LOADER\",\n        },\n        errors: null,\n        location: { pathname: \"/base/parent/child\" },\n        matches: [{ route: { id: \"parent\" } }, { route: { id: \"child\" } }],\n      });\n    });\n\n    it(\"should not fill in null loaderData values for routes without loaders\", async () => {\n      let { query } = createStaticHandler([\n        {\n          id: \"root\",\n          path: \"/\",\n          children: [\n            {\n              id: \"none\",\n              path: \"none\",\n            },\n            {\n              id: \"a\",\n              path: \"a\",\n              loader: () => \"A\",\n              children: [\n                {\n                  id: \"b\",\n                  path: \"b\",\n                },\n              ],\n            },\n          ],\n        },\n      ]);\n\n      // No loaders at all\n      let context = await query(createRequest(\"/none\"));\n      expect(context).toMatchObject({\n        actionData: null,\n        loaderData: {},\n        errors: null,\n        location: { pathname: \"/none\" },\n      });\n\n      // Mix of loaders and no loaders\n      context = await query(createRequest(\"/a/b\"));\n      expect(context).toMatchObject({\n        actionData: null,\n        loaderData: {\n          a: \"A\",\n        },\n        errors: null,\n        location: { pathname: \"/a/b\" },\n      });\n    });\n\n    it(\"should support document load navigations returning responses\", async () => {\n      let { query } = createStaticHandler(SSR_ROUTES);\n      let context = await query(createRequest(\"/parent/json\"));\n      expect(context).toMatchObject({\n        actionData: null,\n        loaderData: {\n          parent: \"PARENT LOADER\",\n          json: { type: \"loader\" },\n        },\n        errors: null,\n        matches: [{ route: { id: \"parent\" } }, { route: { id: \"json\" } }],\n      });\n    });\n\n    it(\"should support document load navigations returning deferred\", async () => {\n      let { query } = createStaticHandler(SSR_ROUTES);\n      let context = (await query(\n        createRequest(\"/parent/deferred\"),\n      )) as StaticHandlerContext;\n      expect(context).toMatchObject({\n        actionData: null,\n        loaderData: {\n          parent: \"PARENT LOADER\",\n          deferred: {\n            critical: \"loader\",\n            lazy: expect.any(Promise),\n          },\n        },\n        errors: null,\n        location: { pathname: \"/parent/deferred\" },\n        matches: [{ route: { id: \"parent\" } }, { route: { id: \"deferred\" } }],\n      });\n\n      await new Promise((r) => setTimeout(r, 10));\n\n      await expect(context.loaderData.deferred.lazy).resolves.toBe(\"lazy\");\n    });\n\n    it(\"should support route.lazy\", async () => {\n      let { query } = createStaticHandler([\n        {\n          id: \"root\",\n          path: \"/\",\n          async lazy() {\n            await sleep(100);\n            return {\n              async loader() {\n                await sleep(100);\n                return \"ROOT LOADER\";\n              },\n            };\n          },\n        },\n        {\n          id: \"parent\",\n          path: \"/parent\",\n          async lazy() {\n            await sleep(100);\n            return {\n              async loader() {\n                await sleep(100);\n                return \"PARENT LOADER\";\n              },\n            };\n          },\n          children: [\n            {\n              id: \"child\",\n              path: \"child\",\n              async lazy() {\n                await sleep(100);\n                return {\n                  async loader() {\n                    await sleep(100);\n                    return \"CHILD LOADER\";\n                  },\n                };\n              },\n            },\n          ],\n        },\n      ]);\n\n      let context = await query(createRequest(\"/\"));\n      expect(context).toMatchObject({\n        loaderData: {\n          root: \"ROOT LOADER\",\n        },\n        errors: null,\n        location: { pathname: \"/\" },\n        matches: [{ route: { id: \"root\" } }],\n      });\n\n      context = await query(createRequest(\"/parent/child\"));\n      expect(context).toMatchObject({\n        actionData: null,\n        loaderData: {\n          parent: \"PARENT LOADER\",\n          child: \"CHILD LOADER\",\n        },\n        errors: null,\n        location: { pathname: \"/parent/child\" },\n        matches: [{ route: { id: \"parent\" } }, { route: { id: \"child\" } }],\n      });\n    });\n\n    it(\"should support document submit navigations\", async () => {\n      let { query } = createStaticHandler(SSR_ROUTES);\n      let context = await query(createSubmitRequest(\"/parent/child\"));\n      expect(context).toMatchObject({\n        actionData: {\n          child: \"CHILD ACTION\",\n        },\n        loaderData: {\n          parent: \"PARENT LOADER\",\n          child: \"CHILD LOADER\",\n        },\n        errors: null,\n        location: { pathname: \"/parent/child\" },\n        matches: [{ route: { id: \"parent\" } }, { route: { id: \"child\" } }],\n      });\n    });\n\n    it(\"should support alternative submission methods\", async () => {\n      let { query } = createStaticHandler(SSR_ROUTES);\n      let context;\n\n      let expected = {\n        actionData: {\n          child: \"CHILD ACTION\",\n        },\n        loaderData: {\n          parent: \"PARENT LOADER\",\n          child: \"CHILD LOADER\",\n        },\n        errors: null,\n        location: { pathname: \"/parent/child\" },\n        matches: [{ route: { id: \"parent\" } }, { route: { id: \"child\" } }],\n      };\n\n      context = await query(\n        createSubmitRequest(\"/parent/child\", { method: \"PUT\" }),\n      );\n      expect(context).toMatchObject(expected);\n\n      context = await query(\n        createSubmitRequest(\"/parent/child\", { method: \"PATCH\" }),\n      );\n      expect(context).toMatchObject(expected);\n\n      context = await query(\n        createSubmitRequest(\"/parent/child\", { method: \"DELETE\" }),\n      );\n      expect(context).toMatchObject(expected);\n    });\n\n    it(\"should support document submit navigations returning responses\", async () => {\n      let { query } = createStaticHandler(SSR_ROUTES);\n      let context = await query(createSubmitRequest(\"/parent/json\"));\n      expect(context).toMatchObject({\n        actionData: {\n          json: { type: \"action\" },\n        },\n        loaderData: {\n          parent: \"PARENT LOADER\",\n          json: { type: \"loader\" },\n        },\n        errors: null,\n        matches: [{ route: { id: \"parent\" } }, { route: { id: \"json\" } }],\n      });\n    });\n\n    it(\"should support document submit navigations to layout routes\", async () => {\n      let { query } = createStaticHandler(SSR_ROUTES);\n      let context = await query(createSubmitRequest(\"/parent\"));\n      expect(context).toMatchObject({\n        actionData: {\n          parent: \"PARENT ACTION\",\n        },\n        loaderData: {\n          parent: \"PARENT LOADER\",\n          parentIndex: \"PARENT INDEX LOADER\",\n        },\n        errors: null,\n        matches: [\n          { route: { id: \"parent\" } },\n          { route: { id: \"parentIndex\" } },\n        ],\n      });\n    });\n\n    it(\"should support document submit navigations to index routes\", async () => {\n      let { query } = createStaticHandler(SSR_ROUTES);\n      let context = await query(createSubmitRequest(\"/parent?index\"));\n      expect(context).toMatchObject({\n        actionData: {\n          parentIndex: \"PARENT INDEX ACTION\",\n        },\n        loaderData: {\n          parent: \"PARENT LOADER\",\n          parentIndex: \"PARENT INDEX LOADER\",\n        },\n        errors: null,\n        matches: [\n          { route: { id: \"parent\" } },\n          { route: { id: \"parentIndex\" } },\n        ],\n      });\n    });\n\n    it(\"should handle redirect Responses\", async () => {\n      let { query } = createStaticHandler(SSR_ROUTES);\n      let response = await query(createRequest(\"/redirect\"));\n      expect(response instanceof Response).toBe(true);\n      expect((response as Response).status).toBe(302);\n      expect((response as Response).headers.get(\"Location\")).toBe(\"/\");\n    });\n\n    it(\"should handle relative redirect responses (loader)\", async () => {\n      let { query } = createStaticHandler([\n        {\n          path: \"/\",\n          children: [\n            {\n              path: \"parent\",\n              children: [\n                {\n                  path: \"child\",\n                  loader: () => redirect(\"..\"),\n                },\n              ],\n            },\n          ],\n        },\n      ]);\n      let response = await query(createRequest(\"/parent/child\"));\n      expect(response instanceof Response).toBe(true);\n      expect((response as Response).status).toBe(302);\n      expect((response as Response).headers.get(\"Location\")).toBe(\"/parent\");\n    });\n\n    it(\"should handle relative redirect responses (action)\", async () => {\n      let { query } = createStaticHandler([\n        {\n          path: \"/\",\n          children: [\n            {\n              path: \"parent\",\n              children: [\n                {\n                  path: \"child\",\n                  action: () => redirect(\"..\"),\n                },\n              ],\n            },\n          ],\n        },\n      ]);\n      let response = await query(createSubmitRequest(\"/parent/child\"));\n      expect(response instanceof Response).toBe(true);\n      expect((response as Response).status).toBe(302);\n      expect((response as Response).headers.get(\"Location\")).toBe(\"/parent\");\n    });\n\n    it(\"should handle absolute redirect Responses\", async () => {\n      for (let url of ABSOLUTE_URLS) {\n        let handler = createStaticHandler([\n          {\n            path: \"/\",\n            loader: () => redirect(url),\n          },\n        ]);\n        let response = await handler.query(createRequest(\"/\"));\n        expect(response instanceof Response).toBe(true);\n        expect((response as Response).status).toBe(302);\n        expect((response as Response).headers.get(\"Location\")).toBe(url);\n      }\n    });\n\n    it(\"should handle 404 navigations\", async () => {\n      let { query } = createStaticHandler(SSR_ROUTES);\n      let context = await query(createRequest(\"/not/found\"));\n\n      expect(context).toMatchObject({\n        loaderData: {},\n        actionData: null,\n        errors: {\n          index: new ErrorResponseImpl(\n            404,\n            \"Not Found\",\n            new Error('No route matches URL \"/not/found\"'),\n            true,\n          ),\n        },\n        matches: [{ route: { id: \"index\" } }],\n      });\n    });\n\n    it(\"should handle load error responses\", async () => {\n      let { query } = createStaticHandler(SSR_ROUTES);\n      let context;\n\n      // Error handled by child\n      context = await query(createRequest(\"/parent/error-boundary\"));\n      expect(context).toMatchObject({\n        actionData: null,\n        loaderData: {\n          parent: \"PARENT LOADER\",\n        },\n        errors: {\n          errorBoundary: \"ERROR BOUNDARY LOADER ERROR\",\n        },\n        matches: [\n          { route: { id: \"parent\" } },\n          { route: { id: \"errorBoundary\" } },\n        ],\n      });\n\n      // Error propagates to parent\n      context = await query(createRequest(\"/parent/error\"));\n      expect(context).toMatchObject({\n        actionData: null,\n        loaderData: {\n          parent: \"PARENT LOADER\",\n        },\n        errors: {\n          parent: \"ERROR LOADER ERROR\",\n        },\n        matches: [{ route: { id: \"parent\" } }, { route: { id: \"error\" } }],\n      });\n    });\n\n    it(\"should handle submit error responses\", async () => {\n      let { query } = createStaticHandler(SSR_ROUTES);\n      let context;\n\n      // Error handled by child\n      context = await query(createSubmitRequest(\"/parent/error-boundary\"));\n      expect(context).toMatchObject({\n        actionData: null,\n        loaderData: {\n          parent: \"PARENT LOADER\",\n        },\n        errors: {\n          errorBoundary: \"ERROR BOUNDARY ACTION ERROR\",\n        },\n        matches: [\n          { route: { id: \"parent\" } },\n          { route: { id: \"errorBoundary\" } },\n        ],\n      });\n\n      // Error propagates to parent\n      context = await query(createSubmitRequest(\"/parent/error\"));\n      expect(context).toMatchObject({\n        actionData: null,\n        loaderData: {},\n        errors: {\n          parent: \"ERROR ACTION ERROR\",\n        },\n        matches: [{ route: { id: \"parent\" } }, { route: { id: \"error\" } }],\n      });\n    });\n\n    it(\"should handle multiple errors at separate boundaries\", async () => {\n      let routes = [\n        {\n          id: \"root\",\n          path: \"/\",\n          loader: () => Promise.reject(\"ROOT\"),\n          hasErrorBoundary: true,\n          children: [\n            {\n              id: \"child\",\n              path: \"child\",\n              loader: () => Promise.reject(\"CHILD\"),\n              hasErrorBoundary: true,\n            },\n          ],\n        },\n      ];\n\n      let { query } = createStaticHandler(routes);\n      let context;\n\n      context = await query(createRequest(\"/child\"));\n      expect(context.errors).toEqual({\n        root: \"ROOT\",\n        child: \"CHILD\",\n      });\n    });\n\n    it(\"should handle multiple errors at the same boundary\", async () => {\n      let routes = [\n        {\n          id: \"root\",\n          path: \"/\",\n          loader: () => Promise.reject(\"ROOT\"),\n          hasErrorBoundary: true,\n          children: [\n            {\n              id: \"child\",\n              path: \"child\",\n              loader: () => Promise.reject(\"CHILD\"),\n            },\n          ],\n        },\n      ];\n\n      let { query } = createStaticHandler(routes);\n      let context;\n\n      context = await query(createRequest(\"/child\"));\n      expect(context.errors).toEqual({\n        // higher error value wins\n        root: \"ROOT\",\n      });\n    });\n\n    it(\"should skip bubbling loader errors when skipLoaderErrorBubbling is passed\", async () => {\n      let routes = [\n        {\n          id: \"root\",\n          path: \"/\",\n          hasErrorBoundary: true,\n          children: [\n            {\n              id: \"child\",\n              path: \"child\",\n              loader: () => Promise.reject(\"CHILD\"),\n            },\n          ],\n        },\n      ];\n\n      let { query } = createStaticHandler(routes);\n      let context;\n\n      context = await query(createRequest(\"/child\"), {\n        skipLoaderErrorBubbling: true,\n      });\n      expect(context.errors).toEqual({\n        child: \"CHILD\",\n      });\n    });\n\n    it(\"should handle aborted load requests\", async () => {\n      let dfd = createDeferred();\n      let controller = new AbortController();\n      let { query } = createStaticHandler([\n        {\n          id: \"root\",\n          path: \"/path\",\n          loader: () => dfd.promise,\n        },\n      ]);\n      let request = createRequest(\"/path?key=value\", {\n        signal: controller.signal,\n      });\n      let e;\n      try {\n        let contextPromise = query(request);\n        controller.abort();\n        // This should resolve even though we never resolved the loader\n        await contextPromise;\n      } catch (_e) {\n        e = _e;\n      }\n      expect(e).toBeInstanceOf(DOMException);\n      expect(e.name).toBe(\"AbortError\");\n      expect(e.message).toBe(\"This operation was aborted\");\n    });\n\n    it(\"should handle aborted submit requests\", async () => {\n      let dfd = createDeferred();\n      let controller = new AbortController();\n      let { query } = createStaticHandler([\n        {\n          id: \"root\",\n          path: \"/path\",\n          action: () => dfd.promise,\n        },\n      ]);\n      let request = createSubmitRequest(\"/path?key=value\", {\n        signal: controller.signal,\n      });\n      let e;\n      try {\n        let contextPromise = query(request);\n        controller.abort();\n        // This should resolve even though we never resolved the loader\n        await contextPromise;\n      } catch (_e) {\n        e = _e;\n      }\n      expect(e).toBeInstanceOf(DOMException);\n      expect(e.name).toBe(\"AbortError\");\n      expect(e.message).toBe(\"This operation was aborted\");\n    });\n\n    it(\"should handle aborted requests\", async () => {\n      let dfd = createDeferred();\n      let controller = new AbortController();\n      let { query } = createStaticHandler([\n        {\n          id: \"root\",\n          path: \"/path\",\n          loader: () => dfd.promise,\n        },\n      ]);\n      let request = createRequest(\"/path?key=value\", {\n        signal: controller.signal,\n      });\n      let e;\n      try {\n        let contextPromise = query(request);\n        controller.abort(new Error(\"Oh no!\"));\n        // This should resolve even though we never resolved the loader\n        await contextPromise;\n      } catch (_e) {\n        e = _e;\n      }\n      expect(e).toBeInstanceOf(Error);\n      expect(e.message).toBe(\"Oh no!\");\n    });\n\n    it(\"should assign signals to requests by default (per the\", async () => {\n      let { query } = createStaticHandler(SSR_ROUTES);\n      let request = createRequest(\"/\", { signal: undefined });\n      let context = await query(request);\n      expect((context as StaticHandlerContext).loaderData.index).toBe(\n        \"INDEX LOADER\",\n      );\n    });\n\n    it(\"should handle not found action submissions with a 405 error\", async () => {\n      let { query } = createStaticHandler([\n        {\n          id: \"root\",\n          path: \"/\",\n        },\n      ]);\n      let request = createSubmitRequest(\"/\");\n      let context = await query(request);\n      expect(context).toMatchObject({\n        actionData: null,\n        loaderData: {},\n        errors: {\n          root: new ErrorResponseImpl(\n            405,\n            \"Method Not Allowed\",\n            new Error(\n              'You made a POST request to \"/\" but did not provide an `action` ' +\n                'for route \"root\", so there is no way to handle the request.',\n            ),\n            true,\n          ),\n        },\n        matches: [{ route: { id: \"root\" } }],\n      });\n    });\n\n    it(\"should handle unsupported methods with a 405 error\", async () => {\n      let { query } = createStaticHandler([\n        {\n          id: \"root\",\n          path: \"/\",\n        },\n      ]);\n      let request = createRequest(\"/\", { method: \"OPTIONS\" });\n      let context = await query(request);\n      expect(context).toMatchObject({\n        actionData: null,\n        loaderData: {},\n        errors: {\n          root: new ErrorResponseImpl(\n            405,\n            \"Method Not Allowed\",\n            new Error('Invalid request method \"OPTIONS\"'),\n            true,\n          ),\n        },\n        matches: [{ route: { id: \"root\" } }],\n      });\n    });\n\n    it(\"should send proper arguments to loaders\", async () => {\n      let rootLoaderStub = jest.fn(() => \"ROOT\");\n      let childLoaderStub = jest.fn(() => \"CHILD\");\n      let { query } = createStaticHandler([\n        {\n          id: \"root\",\n          path: \"/\",\n          loader: rootLoaderStub,\n          children: [\n            {\n              id: \"child\",\n              path: \"child\",\n              loader: childLoaderStub,\n            },\n          ],\n        },\n      ]);\n      await query(createRequest(\"/child\"));\n\n      // @ts-expect-error\n      let rootLoaderRequest = rootLoaderStub.mock.calls[0][0]?.request;\n      // @ts-expect-error\n      let childLoaderRequest = childLoaderStub.mock.calls[0][0]?.request;\n      expect(rootLoaderRequest.method).toBe(\"GET\");\n      expect(rootLoaderRequest.url).toBe(\"http://localhost/child\");\n      expect(childLoaderRequest.method).toBe(\"GET\");\n      expect(childLoaderRequest.url).toBe(\"http://localhost/child\");\n    });\n\n    it(\"should send proper arguments to actions\", async () => {\n      let actionStub = jest.fn(() => \"ACTION\");\n      let rootLoaderStub = jest.fn(() => \"ROOT\");\n      let childLoaderStub = jest.fn(() => \"CHILD\");\n      let { query } = createStaticHandler([\n        {\n          id: \"root\",\n          path: \"/\",\n          loader: rootLoaderStub,\n          children: [\n            {\n              id: \"child\",\n              path: \"child\",\n              action: actionStub,\n              loader: childLoaderStub,\n            },\n          ],\n        },\n      ]);\n      await query(\n        createSubmitRequest(\"/child\", {\n          headers: {\n            test: \"value\",\n          },\n        }),\n      );\n\n      // @ts-expect-error\n      let actionRequest = actionStub.mock.calls[0][0]?.request;\n      expect(actionRequest.method).toBe(\"POST\");\n      expect(actionRequest.url).toBe(\"http://localhost/child\");\n      expect(actionRequest.headers.get(\"Content-Type\")).toBe(\n        \"application/x-www-form-urlencoded;charset=UTF-8\",\n      );\n      expect((await actionRequest.formData()).get(\"key\")).toBe(\"value\");\n\n      // @ts-expect-error\n      let rootLoaderRequest = rootLoaderStub.mock.calls[0][0]?.request;\n      // @ts-expect-error\n      let childLoaderRequest = childLoaderStub.mock.calls[0][0]?.request;\n      expect(rootLoaderRequest.method).toBe(\"GET\");\n      expect(rootLoaderRequest.url).toBe(\"http://localhost/child\");\n      expect(rootLoaderRequest.headers.get(\"test\")).toBe(\"value\");\n      expect(await rootLoaderRequest.text()).toBe(\"\");\n      expect(childLoaderRequest.method).toBe(\"GET\");\n      expect(childLoaderRequest.url).toBe(\"http://localhost/child\");\n      expect(childLoaderRequest.headers.get(\"test\")).toBe(\"value\");\n      // Can't re-read body here since it's the same request as the root\n    });\n\n    it(\"should send proper arguments to loaders after an action errors\", async () => {\n      let actionStub = jest.fn(() => Promise.reject(\"ACTION ERROR\"));\n      let rootLoaderStub = jest.fn(() => \"ROOT\");\n      let childLoaderStub = jest.fn(() => \"CHILD\");\n      let { query } = createStaticHandler([\n        {\n          id: \"root\",\n          path: \"/\",\n          loader: rootLoaderStub,\n          children: [\n            {\n              id: \"child\",\n              path: \"child\",\n              action: actionStub,\n              loader: childLoaderStub,\n              hasErrorBoundary: true,\n            },\n          ],\n        },\n      ]);\n      await query(\n        createSubmitRequest(\"/child\", {\n          headers: {\n            test: \"value\",\n          },\n        }),\n      );\n\n      // @ts-expect-error\n      let actionRequest = actionStub.mock.calls[0][0]?.request;\n      expect(actionRequest.method).toBe(\"POST\");\n      expect(actionRequest.url).toBe(\"http://localhost/child\");\n      expect(actionRequest.headers.get(\"Content-Type\")).toBe(\n        \"application/x-www-form-urlencoded;charset=UTF-8\",\n      );\n      expect((await actionRequest.formData()).get(\"key\")).toBe(\"value\");\n\n      // @ts-expect-error\n      let rootLoaderRequest = rootLoaderStub.mock.calls[0][0]?.request;\n      expect(rootLoaderRequest.method).toBe(\"GET\");\n      expect(rootLoaderRequest.url).toBe(\"http://localhost/child\");\n      expect(rootLoaderRequest.headers.get(\"test\")).toBe(\"value\");\n      expect(await rootLoaderRequest.text()).toBe(\"\");\n      expect(childLoaderStub).not.toHaveBeenCalled();\n    });\n\n    it(\"should support a requestContext passed to loaders and actions\", async () => {\n      let requestContext = { sessionId: \"12345\" };\n      let rootStub = jest.fn(() => \"ROOT\");\n      let childStub = jest.fn(() => \"CHILD\");\n      let actionStub = jest.fn(() => \"CHILD ACTION\");\n      let arg = (s) => s.mock.calls[0][0];\n      let { query } = createStaticHandler([\n        {\n          id: \"root\",\n          path: \"/\",\n          loader: rootStub,\n          children: [\n            {\n              id: \"child\",\n              path: \"child\",\n              action: actionStub,\n              loader: childStub,\n            },\n          ],\n        },\n      ]);\n\n      await query(createRequest(\"/child\"), { requestContext });\n      expect(arg(rootStub).context.sessionId).toBe(\"12345\");\n      expect(arg(childStub).context.sessionId).toBe(\"12345\");\n\n      actionStub.mockClear();\n      rootStub.mockClear();\n      childStub.mockClear();\n\n      await query(createSubmitRequest(\"/child\"), { requestContext });\n      expect(arg(actionStub).context.sessionId).toBe(\"12345\");\n      expect(arg(rootStub).context.sessionId).toBe(\"12345\");\n      expect(arg(childStub).context.sessionId).toBe(\"12345\");\n    });\n\n    describe(\"deferred\", () => {\n      let { query } = createStaticHandler(SSR_ROUTES);\n\n      it(\"should return DeferredData on symbol\", async () => {\n        let context = (await query(\n          createRequest(\"/parent/deferred\"),\n        )) as StaticHandlerContext;\n        expect(context).toMatchObject({\n          loaderData: {\n            parent: \"PARENT LOADER\",\n            deferred: {\n              critical: \"loader\",\n              lazy: expect.any(Promise),\n            },\n          },\n        });\n        await new Promise((r) => setTimeout(r, 10));\n        await expect(context.loaderData.deferred.lazy).resolves.toBe(\"lazy\");\n        expect(context).toMatchObject({\n          loaderData: {\n            parent: \"PARENT LOADER\",\n            deferred: {\n              critical: \"loader\",\n              lazy: expect.any(Promise),\n            },\n          },\n        });\n      });\n\n      it(\"should return rejected promises\", async () => {\n        let context = (await query(\n          createRequest(\"/parent/deferred?reject\"),\n        )) as StaticHandlerContext;\n        expect(context).toMatchObject({\n          loaderData: {\n            parent: \"PARENT LOADER\",\n            deferred: {\n              critical: \"loader\",\n              lazy: expect.any(Promise),\n            },\n          },\n        });\n        await new Promise((r) => setTimeout(r, 10));\n        await expect(context.loaderData.deferred.lazy).rejects.toBe(\"broken!\");\n      });\n\n      it(\"should return resolved undefined\", async () => {\n        let context = (await query(\n          createRequest(\"/parent/deferred?undefined\"),\n        )) as StaticHandlerContext;\n        expect(context).toMatchObject({\n          loaderData: {\n            parent: \"PARENT LOADER\",\n            deferred: {\n              critical: \"loader\",\n              lazy: expect.any(Promise),\n            },\n          },\n        });\n        await new Promise((r) => setTimeout(r, 10));\n        await expect(context.loaderData.deferred.lazy).resolves.toBeUndefined();\n      });\n    });\n\n    describe(\"statusCode\", () => {\n      it(\"should expose a 200 status code by default\", async () => {\n        let { query } = createStaticHandler([\n          {\n            id: \"root\",\n            path: \"/\",\n          },\n        ]);\n        let context = (await query(createRequest(\"/\"))) as StaticHandlerContext;\n        expect(context.statusCode).toBe(200);\n      });\n\n      it(\"should expose a 500 status code on loader errors\", async () => {\n        let { query } = createStaticHandler([\n          {\n            id: \"root\",\n            path: \"/\",\n            loader: () => Response.json({ data: \"ROOT\" }, { status: 201 }),\n            children: [\n              {\n                id: \"child\",\n                index: true,\n                loader: () => {\n                  throw new Error(\"💥\");\n                },\n              },\n            ],\n          },\n        ]);\n        let context = (await query(createRequest(\"/\"))) as StaticHandlerContext;\n        expect(context.statusCode).toBe(500);\n      });\n\n      it(\"should expose a 500 status code on action errors\", async () => {\n        let { query } = createStaticHandler([\n          {\n            id: \"root\",\n            path: \"/\",\n            loader: () => Response.json({ data: \"ROOT\" }, { status: 201 }),\n            children: [\n              {\n                id: \"child\",\n                index: true,\n                loader: () => Response.json({ data: \"CHILD\" }, { status: 202 }),\n                action: () => {\n                  throw new Error(\"💥\");\n                },\n              },\n            ],\n          },\n        ]);\n        let context = (await query(\n          createSubmitRequest(\"/?index\"),\n        )) as StaticHandlerContext;\n        expect(context.statusCode).toBe(500);\n      });\n\n      it(\"should expose a 4xx status code on thrown loader responses\", async () => {\n        let { query } = createStaticHandler([\n          {\n            id: \"root\",\n            path: \"/\",\n            loader: () => Response.json({ data: \"ROOT\" }, { status: 201 }),\n            children: [\n              {\n                id: \"child\",\n                index: true,\n                loader: () => {\n                  throw new Response(null, { status: 400 });\n                },\n              },\n            ],\n          },\n        ]);\n        let context = (await query(createRequest(\"/\"))) as StaticHandlerContext;\n        expect(context.statusCode).toBe(400);\n      });\n\n      it(\"should expose a 4xx status code on thrown action responses\", async () => {\n        let { query } = createStaticHandler([\n          {\n            id: \"root\",\n            path: \"/\",\n            loader: () => Response.json({ data: \"ROOT\" }, { status: 201 }),\n            children: [\n              {\n                id: \"child\",\n                index: true,\n                loader: () => Response.json({ data: \"CHILD\" }, { status: 202 }),\n                action: () => {\n                  throw new Response(null, { status: 400 });\n                },\n              },\n            ],\n          },\n        ]);\n        let context = (await query(\n          createSubmitRequest(\"/?index\"),\n        )) as StaticHandlerContext;\n        expect(context.statusCode).toBe(400);\n      });\n\n      it(\"should expose the action status on submissions\", async () => {\n        let { query } = createStaticHandler([\n          {\n            id: \"root\",\n            path: \"/\",\n            loader: () => Response.json({ data: \"ROOT\" }, { status: 201 }),\n            children: [\n              {\n                id: \"child\",\n                index: true,\n                loader: () => Response.json({ data: \"ROOT\" }, { status: 202 }),\n                action: () => Response.json({ data: \"ROOT\" }, { status: 203 }),\n              },\n            ],\n          },\n        ]);\n        let context = (await query(\n          createSubmitRequest(\"/?index\"),\n        )) as StaticHandlerContext;\n        expect(context.statusCode).toBe(203);\n      });\n\n      it(\"should expose the deepest 2xx status\", async () => {\n        let { query } = createStaticHandler([\n          {\n            id: \"root\",\n            path: \"/\",\n            loader: () => Response.json({ data: \"ROOT\" }, { status: 201 }),\n            children: [\n              {\n                id: \"child\",\n                index: true,\n                loader: () => Response.json({ data: \"ROOT\" }, { status: 202 }),\n              },\n            ],\n          },\n        ]);\n        let context = (await query(createRequest(\"/\"))) as StaticHandlerContext;\n        expect(context.statusCode).toBe(202);\n      });\n\n      it(\"should expose the shallowest 4xx/5xx status\", async () => {\n        let context;\n        let query: StaticHandler[\"query\"];\n\n        query = createStaticHandler([\n          {\n            id: \"root\",\n            path: \"/\",\n            loader: () => {\n              throw new Response(null, { status: 400 });\n            },\n            children: [\n              {\n                id: \"child\",\n                index: true,\n                loader: () => {\n                  throw new Response(null, { status: 401 });\n                },\n              },\n            ],\n          },\n        ]).query;\n        context = (await query(createRequest(\"/\"))) as StaticHandlerContext;\n        expect(context.statusCode).toBe(400);\n\n        query = createStaticHandler([\n          {\n            id: \"root\",\n            path: \"/\",\n            loader: () => {\n              throw new Response(null, { status: 400 });\n            },\n            children: [\n              {\n                id: \"child\",\n                index: true,\n                loader: () => {\n                  throw new Response(null, { status: 500 });\n                },\n              },\n            ],\n          },\n        ]).query;\n        context = (await query(createRequest(\"/\"))) as StaticHandlerContext;\n        expect(context.statusCode).toBe(400);\n\n        query = createStaticHandler([\n          {\n            id: \"root\",\n            path: \"/\",\n            loader: () => {\n              throw new Response(null, { status: 400 });\n            },\n            children: [\n              {\n                id: \"child\",\n                index: true,\n                loader: () => {\n                  throw new Error(\"💥\");\n                },\n              },\n            ],\n          },\n        ]).query;\n        context = (await query(createRequest(\"/\"))) as StaticHandlerContext;\n        expect(context.statusCode).toBe(400);\n      });\n    });\n\n    describe(\"headers\", () => {\n      it(\"should expose headers from action/loader responses\", async () => {\n        let { query } = createStaticHandler([\n          {\n            id: \"root\",\n            path: \"/\",\n            loader: () => new Response(null, { headers: { two: \"2\" } }),\n            children: [\n              {\n                id: \"child\",\n                index: true,\n                action: () => new Response(null, { headers: { one: \"1\" } }),\n                loader: () => new Response(null, { headers: { three: \"3\" } }),\n              },\n            ],\n          },\n        ]);\n        let context = (await query(\n          createSubmitRequest(\"/?index\"),\n        )) as StaticHandlerContext;\n        expect(Array.from(context.actionHeaders.child.entries())).toEqual([\n          [\"one\", \"1\"],\n        ]);\n        expect(Array.from(context.loaderHeaders.root.entries())).toEqual([\n          [\"two\", \"2\"],\n        ]);\n        expect(Array.from(context.loaderHeaders.child.entries())).toEqual([\n          [\"three\", \"3\"],\n        ]);\n      });\n\n      it(\"should expose headers from loader error responses\", async () => {\n        let { query } = createStaticHandler([\n          {\n            id: \"root\",\n            path: \"/\",\n            loader: () => new Response(null, { headers: { one: \"1\" } }),\n            children: [\n              {\n                id: \"child\",\n                index: true,\n                loader: () => {\n                  throw new Response(null, { headers: { two: \"2\" } });\n                },\n              },\n            ],\n          },\n        ]);\n        let context = (await query(createRequest(\"/\"))) as StaticHandlerContext;\n        expect(Array.from(context.loaderHeaders.root.entries())).toEqual([\n          [\"one\", \"1\"],\n        ]);\n        expect(Array.from(context.loaderHeaders.child.entries())).toEqual([\n          [\"two\", \"2\"],\n        ]);\n      });\n\n      it(\"should expose headers from action error responses\", async () => {\n        let { query } = createStaticHandler([\n          {\n            id: \"root\",\n            path: \"/\",\n            children: [\n              {\n                id: \"child\",\n                index: true,\n                action: () => {\n                  throw new Response(null, { headers: { one: \"1\" } });\n                },\n              },\n            ],\n          },\n        ]);\n        let context = (await query(\n          createSubmitRequest(\"/?index\"),\n        )) as StaticHandlerContext;\n        expect(Array.from(context.actionHeaders.child.entries())).toEqual([\n          [\"one\", \"1\"],\n        ]);\n      });\n    });\n\n    describe(\"getStaticContextFromError\", () => {\n      it(\"should provide a context for a second-pass render for a thrown error\", async () => {\n        let { query } = createStaticHandler(SSR_ROUTES);\n        let context = await query(createRequest(\"/\"));\n        expect(context).toMatchObject({\n          errors: null,\n          loaderData: {\n            index: \"INDEX LOADER\",\n          },\n          statusCode: 200,\n        });\n\n        let error = new Error(\"💥\");\n        invariant(!(context instanceof Response), \"Uh oh\");\n        context = getStaticContextFromError(SSR_ROUTES, context, error);\n        expect(context).toMatchObject({\n          errors: {\n            index: error,\n          },\n          loaderData: {\n            index: \"INDEX LOADER\",\n          },\n          statusCode: 500,\n        });\n      });\n\n      it(\"should accept a thrown response from entry.server\", async () => {\n        let { query } = createStaticHandler(SSR_ROUTES);\n        let context = await query(createRequest(\"/\"));\n        expect(context).toMatchObject({\n          errors: null,\n          loaderData: {\n            index: \"INDEX LOADER\",\n          },\n          statusCode: 200,\n        });\n\n        let errorResponse = new ErrorResponseImpl(400, \"Bad Request\", \"Oops!\");\n        invariant(!(context instanceof Response), \"Uh oh\");\n        context = getStaticContextFromError(SSR_ROUTES, context, errorResponse);\n        expect(context).toMatchObject({\n          errors: {\n            index: errorResponse,\n          },\n          loaderData: {\n            index: \"INDEX LOADER\",\n          },\n          statusCode: 400,\n        });\n      });\n    });\n\n    describe(\"router dataStrategy\", () => {\n      it(\"should support document load navigations with custom dataStrategy\", async () => {\n        let { query } = createStaticHandler(SSR_ROUTES);\n\n        let context = await query(createRequest(\"/custom\"), {\n          dataStrategy: urlDataStrategy,\n        });\n        expect(context).toMatchObject({\n          actionData: null,\n          loaderData: {\n            custom: expect.any(URLSearchParams),\n          },\n          errors: null,\n          location: { pathname: \"/custom\" },\n          matches: [{ route: { id: \"custom\" } }],\n        });\n        expect(\n          (context as StaticHandlerContext).loaderData.custom.get(\"foo\"),\n        ).toEqual(\"bar\");\n      });\n    });\n  });\n\n  describe(\"singular route requests\", () => {\n    function setupFlexRouteTest() {\n      function queryRoute(\n        req: Request,\n        routeId: string,\n        type: \"loader\" | \"action\",\n        data: any,\n        isError = false,\n      ) {\n        let handler = createStaticHandler([\n          {\n            id: \"flex\",\n            path: \"/flex\",\n            [type]: () =>\n              isError ? Promise.reject(data) : Promise.resolve(data),\n          },\n        ]);\n        return handler.queryRoute(req, { routeId });\n      }\n\n      return {\n        resolveLoader(data: any) {\n          return queryRoute(\n            createRequest(\"/flex\"),\n            \"flex\",\n            \"loader\",\n            data,\n            false,\n          );\n        },\n        rejectLoader(data: any) {\n          return queryRoute(\n            createRequest(\"/flex\"),\n            \"flex\",\n            \"loader\",\n            data,\n            true,\n          );\n        },\n        resolveAction(data: any) {\n          return queryRoute(\n            createSubmitRequest(\"/flex\"),\n            \"flex\",\n            \"action\",\n            data,\n            false,\n          );\n        },\n        rejectAction(data: any) {\n          return queryRoute(\n            createSubmitRequest(\"/flex\"),\n            \"flex\",\n            \"action\",\n            data,\n            true,\n          );\n        },\n      };\n    }\n\n    it(\"should match routes automatically if no routeId is provided\", async () => {\n      let { queryRoute } = createStaticHandler(SSR_ROUTES);\n      let data;\n\n      data = await queryRoute(createRequest(\"/parent\"));\n      expect(data).toBe(\"PARENT LOADER\");\n\n      data = await queryRoute(createRequest(\"/parent?index\"));\n      expect(data).toBe(\"PARENT INDEX LOADER\");\n\n      data = await queryRoute(createRequest(\"/parent/child\"), {\n        routeId: \"child\",\n      });\n      expect(data).toBe(\"CHILD LOADER\");\n    });\n\n    it(\"should support HEAD requests\", async () => {\n      let { queryRoute } = createStaticHandler(SSR_ROUTES);\n      let data = await queryRoute(createRequest(\"/parent\", { method: \"HEAD\" }));\n      expect(data).toBe(\"PARENT LOADER\");\n    });\n\n    it(\"should support OPTIONS requests\", async () => {\n      let { queryRoute } = createStaticHandler(SSR_ROUTES);\n      let data = await queryRoute(\n        createRequest(\"/parent\", { method: \"OPTIONS\" }),\n      );\n      expect(data).toBe(\"PARENT LOADER\");\n    });\n\n    it(\"should support singular route load navigations (primitives)\", async () => {\n      let { queryRoute } = createStaticHandler(SSR_ROUTES);\n      let data;\n\n      // Layout route\n      data = await queryRoute(createRequest(\"/parent\"), {\n        routeId: \"parent\",\n      });\n      expect(data).toBe(\"PARENT LOADER\");\n\n      // Index route\n      data = await queryRoute(createRequest(\"/parent\"), {\n        routeId: \"parentIndex\",\n      });\n      expect(data).toBe(\"PARENT INDEX LOADER\");\n\n      // Parent in nested route\n      data = await queryRoute(createRequest(\"/parent/child\"), {\n        routeId: \"parent\",\n      });\n      expect(data).toBe(\"PARENT LOADER\");\n\n      // Child in nested route\n      data = await queryRoute(createRequest(\"/parent/child\"), {\n        routeId: \"child\",\n      });\n      expect(data).toBe(\"CHILD LOADER\");\n\n      // Non-undefined falsey values should count\n      let T = setupFlexRouteTest();\n      data = await T.resolveLoader(null);\n      expect(data).toBeNull();\n      data = await T.resolveLoader(false);\n      expect(data).toBe(false);\n      data = await T.resolveLoader(\"\");\n      expect(data).toBe(\"\");\n    });\n\n    it(\"should support singular route load navigations (Responses)\", async () => {\n      /* eslint-disable jest/no-conditional-expect */\n      let T = setupFlexRouteTest();\n      let data;\n\n      // When Responses are returned or thrown, it should always resolve the\n      // raw Response from queryRoute\n\n      // Returned Success Response\n      data = await T.resolveLoader(new Response(\"Created!\", { status: 201 }));\n      expect(data.status).toBe(201);\n      expect(await data.text()).toBe(\"Created!\");\n\n      // Thrown Success Response\n      try {\n        await T.rejectLoader(new Response(\"Created!\", { status: 201 }));\n        expect(false).toBe(true);\n      } catch (data) {\n        expect(data.status).toBe(201);\n        expect(await data.text()).toBe(\"Created!\");\n      }\n\n      // Returned Redirect Response\n      data = await T.resolveLoader(\n        new Response(null, {\n          status: 302,\n          headers: { Location: \"/\" },\n        }),\n      );\n      expect(data.status).toBe(302);\n      expect(data.headers.get(\"Location\")).toBe(\"/\");\n\n      // Thrown Redirect Response\n      data = await T.rejectLoader(\n        new Response(null, {\n          status: 301,\n          headers: { Location: \"/\" },\n        }),\n      );\n      expect(data.status).toBe(301);\n      expect(data.headers.get(\"Location\")).toBe(\"/\");\n\n      // Returned Error Response\n      data = await T.resolveLoader(new Response(\"Why?\", { status: 400 }));\n      expect(data.status).toBe(400);\n      expect(await data.text()).toBe(\"Why?\");\n\n      // Thrown Error Response\n      try {\n        await T.rejectLoader(new Response(\"Oh no!\", { status: 401 }));\n        expect(false).toBe(true);\n      } catch (data) {\n        expect(data.status).toBe(401);\n        expect(await data.text()).toBe(\"Oh no!\");\n      }\n      /* eslint-enable jest/no-conditional-expect */\n    });\n\n    it(\"should support singular route load navigations (Errors)\", async () => {\n      let T = setupFlexRouteTest();\n      let data;\n\n      // Returned Error instance is treated as data since it was not thrown\n      data = await T.resolveLoader(new Error(\"Why?\"));\n      expect(data).toEqual(new Error(\"Why?\"));\n\n      // Anything thrown (Error instance or not) will throw from queryRoute\n      // so we know to handle it as an errorPath in the server.  Generally\n      // though in queryRoute, we would expect responses to be coming back -\n      // not\n\n      // Thrown Error\n      try {\n        await T.rejectLoader(new Error(\"Oh no!\"));\n      } catch (e) {\n        data = e;\n      }\n      expect(data).toEqual(new Error(\"Oh no!\"));\n\n      // Thrown non-Error\n      try {\n        await T.rejectLoader(\"This is weird?\");\n      } catch (e) {\n        data = e;\n      }\n      expect(data).toEqual(\"This is weird?\");\n\n      // Non-undefined falsey values should count\n      try {\n        await T.rejectLoader(null);\n      } catch (e) {\n        data = e;\n      }\n      expect(data).toBeNull();\n      try {\n        await T.rejectLoader(false);\n      } catch (e) {\n        data = e;\n      }\n      expect(data).toBe(false);\n      try {\n        await T.rejectLoader(\"\");\n      } catch (e) {\n        data = e;\n      }\n      expect(data).toBe(\"\");\n    });\n\n    it(\"should support singular route load navigations (with a basename)\", async () => {\n      let { queryRoute } = createStaticHandler(SSR_ROUTES, {\n        basename: \"/base\",\n      });\n      let data;\n\n      // Layout route\n      data = await queryRoute(createRequest(\"/base/parent\"), {\n        routeId: \"parent\",\n      });\n      expect(data).toBe(\"PARENT LOADER\");\n\n      // Index route\n      data = await queryRoute(createRequest(\"/base/parent\"), {\n        routeId: \"parentIndex\",\n      });\n      expect(data).toBe(\"PARENT INDEX LOADER\");\n\n      // Parent in nested route\n      data = await queryRoute(createRequest(\"/base/parent/child\"), {\n        routeId: \"parent\",\n      });\n      expect(data).toBe(\"PARENT LOADER\");\n\n      // Child in nested route\n      data = await queryRoute(createRequest(\"/base/parent/child\"), {\n        routeId: \"child\",\n      });\n      expect(data).toBe(\"CHILD LOADER\");\n\n      // Non-undefined falsey values should count\n      let T = setupFlexRouteTest();\n      data = await T.resolveLoader(null);\n      expect(data).toBeNull();\n      data = await T.resolveLoader(false);\n      expect(data).toBe(false);\n      data = await T.resolveLoader(\"\");\n      expect(data).toBe(\"\");\n    });\n\n    it(\"should support singular route submit navigations (primitives)\", async () => {\n      let { queryRoute } = createStaticHandler(SSR_ROUTES);\n      let data;\n\n      // Layout route\n      data = await queryRoute(createSubmitRequest(\"/parent\"), {\n        routeId: \"parent\",\n      });\n      expect(data).toBe(\"PARENT ACTION\");\n\n      // Index route\n      data = await queryRoute(createSubmitRequest(\"/parent\"), {\n        routeId: \"parentIndex\",\n      });\n      expect(data).toBe(\"PARENT INDEX ACTION\");\n\n      // Parent in nested route\n      data = await queryRoute(createSubmitRequest(\"/parent/child\"), {\n        routeId: \"parent\",\n      });\n      expect(data).toBe(\"PARENT ACTION\");\n\n      // Child in nested route\n      data = await queryRoute(createSubmitRequest(\"/parent/child\"), {\n        routeId: \"child\",\n      });\n      expect(data).toBe(\"CHILD ACTION\");\n\n      // Non-undefined falsey values should count\n      let T = setupFlexRouteTest();\n      data = await T.resolveAction(null);\n      expect(data).toBeNull();\n      data = await T.resolveAction(false);\n      expect(data).toBe(false);\n      data = await T.resolveAction(\"\");\n      expect(data).toBe(\"\");\n    });\n\n    it(\"should support alternative submission methods\", async () => {\n      let { queryRoute } = createStaticHandler(SSR_ROUTES);\n      let data;\n\n      data = await queryRoute(\n        createSubmitRequest(\"/parent\", { method: \"PUT\" }),\n        { routeId: \"parent\" },\n      );\n      expect(data).toBe(\"PARENT ACTION\");\n\n      data = await queryRoute(\n        createSubmitRequest(\"/parent\", { method: \"PATCH\" }),\n        { routeId: \"parent\" },\n      );\n      expect(data).toBe(\"PARENT ACTION\");\n\n      data = await queryRoute(\n        createSubmitRequest(\"/parent\", { method: \"DELETE\" }),\n        { routeId: \"parent\" },\n      );\n      expect(data).toBe(\"PARENT ACTION\");\n    });\n\n    it(\"should support singular route submit navigations (Responses)\", async () => {\n      /* eslint-disable jest/no-conditional-expect */\n      let T = setupFlexRouteTest();\n      let data;\n\n      // When Responses are returned or thrown, it should always resolve the\n      // raw Response from queryRoute\n\n      // Returned Success Response\n      data = await T.resolveAction(new Response(\"Created!\", { status: 201 }));\n      expect(data.status).toBe(201);\n      expect(await data.text()).toBe(\"Created!\");\n\n      // Thrown Success Response\n      try {\n        await T.rejectAction(new Response(\"Created!\", { status: 201 }));\n        expect(false).toBe(true);\n      } catch (data) {\n        expect(data.status).toBe(201);\n        expect(await data.text()).toBe(\"Created!\");\n      }\n\n      // Returned Redirect Response\n      data = await T.resolveAction(\n        new Response(null, {\n          status: 302,\n          headers: { Location: \"/\" },\n        }),\n      );\n      expect(data.status).toBe(302);\n      expect(data.headers.get(\"Location\")).toBe(\"/\");\n\n      // Thrown Redirect Response\n      data = await T.rejectAction(\n        new Response(null, {\n          status: 301,\n          headers: { Location: \"/\" },\n        }),\n      );\n      expect(data.status).toBe(301);\n      expect(data.headers.get(\"Location\")).toBe(\"/\");\n\n      // Returned Error Response\n      data = await T.resolveAction(new Response(\"Why?\", { status: 400 }));\n      expect(data.status).toBe(400);\n      expect(await data.text()).toBe(\"Why?\");\n\n      // Thrown Error Response\n      try {\n        await T.rejectAction(new Response(\"Oh no!\", { status: 401 }));\n        expect(false).toBe(true);\n      } catch (data) {\n        expect(data.status).toBe(401);\n        expect(await data.text()).toBe(\"Oh no!\");\n      }\n      /* eslint-enable jest/no-conditional-expect */\n    });\n\n    it(\"should support singular route submit navigations (Errors)\", async () => {\n      let T = setupFlexRouteTest();\n      let data;\n\n      // Returned Error instance is treated as data since it was not thrown\n      data = await T.resolveAction(new Error(\"Why?\"));\n      expect(data).toEqual(new Error(\"Why?\"));\n\n      // Anything thrown (Error instance or not) will throw from queryRoute\n      // so we know to handle it as an errorPath in the server.  Generally\n      // though in queryRoute, we would expect responses to be coming back -\n      // not\n\n      // Thrown Error\n      try {\n        await T.rejectAction(new Error(\"Oh no!\"));\n      } catch (e) {\n        data = e;\n      }\n      expect(data).toEqual(new Error(\"Oh no!\"));\n\n      // Thrown non-Error\n      try {\n        await T.rejectAction(\"This is weird?\");\n      } catch (e) {\n        data = e;\n      }\n      expect(data).toEqual(\"This is weird?\");\n\n      // Non-undefined falsey values should count\n      try {\n        await T.rejectAction(null);\n      } catch (e) {\n        data = e;\n      }\n      expect(data).toBeNull();\n      try {\n        await T.rejectAction(false);\n      } catch (e) {\n        data = e;\n      }\n      expect(data).toBe(false);\n      try {\n        await T.rejectAction(\"\");\n      } catch (e) {\n        data = e;\n      }\n      expect(data).toBe(\"\");\n    });\n\n    it(\"should allow returning undefined from an action/loader\", async () => {\n      let T = setupFlexRouteTest();\n\n      expect(await T.resolveLoader(undefined)).toBeUndefined();\n      expect(await T.resolveAction(undefined)).toBeUndefined();\n    });\n\n    it(\"should handle relative redirect responses (loader)\", async () => {\n      let { queryRoute } = createStaticHandler([\n        {\n          path: \"/\",\n          children: [\n            {\n              path: \"parent\",\n              children: [\n                {\n                  id: \"child\",\n                  path: \"child\",\n                  loader: () => redirect(\"..\"),\n                },\n              ],\n            },\n          ],\n        },\n      ]);\n      let response = await queryRoute(createRequest(\"/parent/child\"), {\n        routeId: \"child\",\n      });\n      expect(response instanceof Response).toBe(true);\n      expect((response as Response).status).toBe(302);\n      expect((response as Response).headers.get(\"Location\")).toBe(\"/parent\");\n    });\n\n    it(\"should handle relative redirect responses (action)\", async () => {\n      let { queryRoute } = createStaticHandler([\n        {\n          path: \"/\",\n          children: [\n            {\n              path: \"parent\",\n              children: [\n                {\n                  id: \"child\",\n                  path: \"child\",\n                  action: () => redirect(\"..\"),\n                },\n              ],\n            },\n          ],\n        },\n      ]);\n      let response = await queryRoute(createSubmitRequest(\"/parent/child\"), {\n        routeId: \"child\",\n      });\n      expect(response instanceof Response).toBe(true);\n      expect((response as Response).status).toBe(302);\n      expect((response as Response).headers.get(\"Location\")).toBe(\"/parent\");\n    });\n\n    it(\"should handle absolute redirect Responses\", async () => {\n      for (let url of ABSOLUTE_URLS) {\n        let handler = createStaticHandler([\n          {\n            id: \"root\",\n            path: \"/\",\n            loader: () => redirect(url),\n          },\n        ]);\n        let response = await handler.queryRoute(createRequest(\"/\"), {\n          routeId: \"root\",\n        });\n        expect(response instanceof Response).toBe(true);\n        expect((response as Response).status).toBe(302);\n        expect((response as Response).headers.get(\"Location\")).toBe(url);\n      }\n    });\n\n    it(\"should not unwrap responses returned from loaders\", async () => {\n      let response = Response.json({ key: \"value\" });\n      let { queryRoute } = createStaticHandler([\n        {\n          id: \"root\",\n          path: \"/\",\n          loader: () => Promise.resolve(response),\n        },\n      ]);\n      let request = createRequest(\"/\");\n      let data = await queryRoute(request, { routeId: \"root\" });\n      expect(data instanceof Response).toBe(true);\n      expect(await data.json()).toEqual({ key: \"value\" });\n    });\n\n    it(\"should not unwrap responses returned from actions\", async () => {\n      let response = Response.json({ key: \"value\" });\n      let { queryRoute } = createStaticHandler([\n        {\n          id: \"root\",\n          path: \"/\",\n          action: () => Promise.resolve(response),\n        },\n      ]);\n      let request = createSubmitRequest(\"/\");\n      let data = await queryRoute(request, { routeId: \"root\" });\n      expect(data instanceof Response).toBe(true);\n      expect(await data.json()).toEqual({ key: \"value\" });\n    });\n\n    it(\"should not unwrap responses thrown from loaders\", async () => {\n      let response = Response.json({ key: \"value\" });\n      let { queryRoute } = createStaticHandler([\n        {\n          id: \"root\",\n          path: \"/\",\n          loader: () => Promise.reject(response),\n        },\n      ]);\n      let request = createRequest(\"/\");\n      let data;\n      try {\n        await queryRoute(request, { routeId: \"root\" });\n      } catch (e) {\n        data = e;\n      }\n      expect(data instanceof Response).toBe(true);\n      expect(await data.json()).toEqual({ key: \"value\" });\n    });\n\n    it(\"should not unwrap responses thrown from actions\", async () => {\n      let response = Response.json({ key: \"value\" });\n      let { queryRoute } = createStaticHandler([\n        {\n          id: \"root\",\n          path: \"/\",\n          action: () => Promise.reject(response),\n        },\n      ]);\n      let request = createSubmitRequest(\"/\");\n      let data;\n      try {\n        await queryRoute(request, { routeId: \"root\" });\n      } catch (e) {\n        data = e;\n      }\n      expect(data instanceof Response).toBe(true);\n      expect(await data.json()).toEqual({ key: \"value\" });\n    });\n\n    it(\"should handle aborted load requests\", async () => {\n      let dfd = createDeferred();\n      let controller = new AbortController();\n      let { queryRoute } = createStaticHandler([\n        {\n          id: \"root\",\n          path: \"/path\",\n          loader: () => dfd.promise,\n        },\n      ]);\n      let request = createRequest(\"/path?key=value\", {\n        signal: controller.signal,\n      });\n      let e;\n      try {\n        let statePromise = queryRoute(request, { routeId: \"root\" });\n        controller.abort();\n        // This should resolve even though we never resolved the loader\n        await statePromise;\n      } catch (_e) {\n        e = _e;\n      }\n      expect(e).toBeInstanceOf(DOMException);\n      expect(e.name).toBe(\"AbortError\");\n      expect(e.message).toBe(\"This operation was aborted\");\n    });\n\n    it(\"should handle aborted submit requests - custom reason\", async () => {\n      let dfd = createDeferred();\n      let controller = new AbortController();\n      let { queryRoute } = createStaticHandler([\n        {\n          id: \"root\",\n          path: \"/path\",\n          action: () => dfd.promise,\n        },\n      ]);\n      let request = createSubmitRequest(\"/path?key=value\", {\n        signal: controller.signal,\n      });\n      let e;\n      try {\n        let statePromise = queryRoute(request, { routeId: \"root\" });\n        controller.abort();\n        // This should resolve even though we never resolved the loader\n        await statePromise;\n      } catch (_e) {\n        e = _e;\n      }\n      expect(e).toBeInstanceOf(DOMException);\n      expect(e.name).toBe(\"AbortError\");\n      expect(e.message).toBe(\"This operation was aborted\");\n    });\n\n    it(\"should handle aborted load requests - custom reason\", async () => {\n      let dfd = createDeferred();\n      let controller = new AbortController();\n      let { queryRoute } = createStaticHandler([\n        {\n          id: \"root\",\n          path: \"/path\",\n          loader: () => dfd.promise,\n        },\n      ]);\n      let request = createRequest(\"/path?key=value\", {\n        signal: controller.signal,\n      });\n      let e;\n      try {\n        let statePromise = queryRoute(request, { routeId: \"root\" });\n        controller.abort(new Error(\"Oh no!\"));\n        // This should resolve even though we never resolved the loader\n        await statePromise;\n      } catch (_e) {\n        e = _e;\n      }\n      expect(e).toBeInstanceOf(Error);\n      expect(e.message).toBe(\"Oh no!\");\n    });\n\n    it(\"should assign signals to requests by default (per the spec)\", async () => {\n      let { queryRoute } = createStaticHandler(SSR_ROUTES);\n      let request = createRequest(\"/\", { signal: undefined });\n      let data = await queryRoute(request, { routeId: \"index\" });\n      expect(data).toBe(\"INDEX LOADER\");\n    });\n\n    it(\"should support a requestContext passed to loaders and actions\", async () => {\n      let requestContext = { sessionId: \"12345\" };\n      let childStub = jest.fn(() => \"CHILD\");\n      let actionStub = jest.fn(() => \"CHILD ACTION\");\n      let arg = (s) => s.mock.calls[0][0];\n      let { queryRoute } = createStaticHandler([\n        {\n          path: \"/\",\n          children: [\n            {\n              id: \"child\",\n              path: \"child\",\n              action: actionStub,\n              loader: childStub,\n            },\n          ],\n        },\n      ]);\n\n      await queryRoute(createRequest(\"/child\"), {\n        routeId: \"child\",\n        requestContext,\n      });\n      expect(arg(childStub).context.sessionId).toBe(\"12345\");\n\n      await queryRoute(createSubmitRequest(\"/child\"), {\n        routeId: \"child\",\n        requestContext,\n      });\n      expect(arg(actionStub).context.sessionId).toBe(\"12345\");\n    });\n\n    describe(\"Errors with Status Codes\", () => {\n      /* eslint-disable jest/no-conditional-expect */\n      let { queryRoute } = createStaticHandler([\n        {\n          id: \"root\",\n          path: \"/\",\n        },\n      ]);\n\n      it(\"should handle not found paths with a 404 Response\", async () => {\n        try {\n          await queryRoute(createRequest(\"/junk\"));\n          expect(false).toBe(true);\n        } catch (data) {\n          expect(isRouteErrorResponse(data)).toBe(true);\n          expect(data.status).toBe(404);\n          expect(data.error).toEqual(new Error('No route matches URL \"/junk\"'));\n          expect(data.internal).toBe(true);\n        }\n\n        try {\n          await queryRoute(createSubmitRequest(\"/junk\"));\n          expect(false).toBe(true);\n        } catch (data) {\n          expect(isRouteErrorResponse(data)).toBe(true);\n          expect(data.status).toBe(404);\n          expect(data.error).toEqual(new Error('No route matches URL \"/junk\"'));\n          expect(data.internal).toBe(true);\n        }\n      });\n\n      it(\"should handle not found routeIds with a 403 Response\", async () => {\n        try {\n          await queryRoute(createRequest(\"/\"), { routeId: \"junk\" });\n          expect(false).toBe(true);\n        } catch (data) {\n          expect(isRouteErrorResponse(data)).toBe(true);\n          expect(data.status).toBe(403);\n          expect(data.error).toEqual(\n            new Error('Route \"junk\" does not match URL \"/\"'),\n          );\n          expect(data.internal).toBe(true);\n        }\n\n        try {\n          await queryRoute(createSubmitRequest(\"/\"), { routeId: \"junk\" });\n          expect(false).toBe(true);\n        } catch (data) {\n          expect(isRouteErrorResponse(data)).toBe(true);\n          expect(data.status).toBe(403);\n          expect(data.error).toEqual(\n            new Error('Route \"junk\" does not match URL \"/\"'),\n          );\n          expect(data.internal).toBe(true);\n        }\n      });\n\n      it(\"should handle missing loaders with a 400 Response\", async () => {\n        try {\n          await queryRoute(createRequest(\"/\"), { routeId: \"root\" });\n          expect(false).toBe(true);\n        } catch (data) {\n          expect(isRouteErrorResponse(data)).toBe(true);\n          expect(data.status).toBe(400);\n          expect(data.error).toEqual(\n            new Error(\n              'You made a GET request to \"/\" but did not provide a `loader` ' +\n                'for route \"root\", so there is no way to handle the request.',\n            ),\n          );\n          expect(data.internal).toBe(true);\n        }\n      });\n\n      it(\"should handle missing actions with a 405 Response\", async () => {\n        try {\n          await queryRoute(createSubmitRequest(\"/\"), { routeId: \"root\" });\n          expect(false).toBe(true);\n        } catch (data) {\n          expect(isRouteErrorResponse(data)).toBe(true);\n          expect(data.status).toBe(405);\n          expect(data.error).toEqual(\n            new Error(\n              'You made a POST request to \"/\" but did not provide an `action` ' +\n                'for route \"root\", so there is no way to handle the request.',\n            ),\n          );\n          expect(data.internal).toBe(true);\n        }\n      });\n\n      it(\"should handle unsupported methods with a 405 Response\", async () => {\n        try {\n          await queryRoute(createRequest(\"/\", { method: \"CHICKEN\" }), {\n            routeId: \"root\",\n          });\n          expect(false).toBe(true);\n        } catch (data) {\n          expect(isRouteErrorResponse(data)).toBe(true);\n          expect(data.status).toBe(405);\n          expect(data.error).toEqual(\n            new Error('Invalid request method \"CHICKEN\"'),\n          );\n          expect(data.internal).toBe(true);\n        }\n      });\n\n      /* eslint-enable jest/no-conditional-expect */\n    });\n\n    describe(\"router dataStrategy\", () => {\n      it(\"should apply a custom data strategy\", async () => {\n        let { queryRoute } = createStaticHandler(SSR_ROUTES);\n        let data;\n\n        data = await queryRoute(createRequest(\"/custom\"), {\n          dataStrategy: urlDataStrategy,\n        });\n        expect(data).toBeInstanceOf(URLSearchParams);\n        expect((data as URLSearchParams).get(\"foo\")).toBe(\"bar\");\n      });\n    });\n  });\n});\n"
  },
  {
    "path": "packages/react-router/__tests__/router/submission-test.ts",
    "content": "import type { HydrationState } from \"../../lib/router/router\";\nimport { ErrorResponseImpl } from \"../../lib/router/utils\";\nimport { cleanup, setup } from \"./utils/data-router-setup\";\nimport { createFormData } from \"./utils/utils\";\n\nfunction initializeTest(init?: {\n  url?: string;\n  hydrationData?: HydrationState;\n}) {\n  return setup({\n    routes: [\n      {\n        path: \"\",\n        id: \"root\",\n        hasErrorBoundary: true,\n        loader: true,\n        children: [\n          {\n            path: \"/\",\n            id: \"index\",\n            loader: true,\n            action: true,\n          },\n          {\n            path: \"/foo\",\n            id: \"foo\",\n            loader: true,\n            action: true,\n          },\n          {\n            path: \"/bar\",\n            id: \"bar\",\n            loader: true,\n            action: true,\n          },\n          {\n            path: \"/baz\",\n            id: \"baz\",\n            loader: true,\n            action: true,\n          },\n        ],\n      },\n    ],\n    hydrationData: init?.hydrationData || {\n      loaderData: { root: \"ROOT\", index: \"INDEX\" },\n    },\n    ...(init?.url ? { initialEntries: [init.url] } : {}),\n  });\n}\n\ndescribe(\"submissions\", () => {\n  // Detect any failures inside the router navigate code\n  afterEach(() => cleanup());\n\n  describe(\"submission navigations\", () => {\n    it(\"reloads all routes when a loader during an actionReload redirects\", async () => {\n      let t = initializeTest();\n      let A = await t.navigate(\"/foo\", {\n        formMethod: \"post\",\n        formData: createFormData({ gosh: \"dang\" }),\n      });\n      expect(A.loaders.root.stub.mock.calls.length).toBe(0);\n\n      await A.actions.foo.resolve(\"FOO ACTION\");\n      expect(A.loaders.root.stub.mock.calls.length).toBe(1);\n      expect(t.router.state.actionData).toEqual({\n        foo: \"FOO ACTION\",\n      });\n\n      let B = await A.loaders.foo.redirect(\"/bar\");\n      await A.loaders.root.reject(\"ROOT ERROR\");\n      await B.loaders.root.resolve(\"ROOT LOADER 2\");\n      await B.loaders.bar.resolve(\"BAR LOADER\");\n      expect(B.loaders.root.stub.mock.calls.length).toBe(1);\n      expect(t.router.state).toMatchObject({\n        actionData: null,\n        loaderData: {\n          root: \"ROOT LOADER 2\",\n          bar: \"BAR LOADER\",\n        },\n        errors: {},\n      });\n    });\n\n    it(\"commits action data as soon as it lands\", async () => {\n      let t = initializeTest();\n\n      let A = await t.navigate(\"/foo\", {\n        formMethod: \"post\",\n        formData: createFormData({ gosh: \"dang\" }),\n      });\n      expect(t.router.state.actionData).toBeNull();\n\n      await A.actions.foo.resolve(\"A\");\n      expect(t.router.state.actionData).toEqual({\n        foo: \"A\",\n      });\n    });\n\n    it(\"reloads all routes after the action\", async () => {\n      let t = initializeTest();\n      let A = await t.navigate(\"/foo\", {\n        formMethod: \"post\",\n        formData: createFormData({ gosh: \"dang\" }),\n      });\n      expect(A.loaders.root.stub.mock.calls.length).toBe(0);\n\n      await A.actions.foo.resolve(\"FOO ACTION\");\n      expect(A.loaders.root.stub.mock.calls.length).toBe(1);\n      expect(t.router.state.actionData).toEqual({\n        foo: \"FOO ACTION\",\n      });\n\n      await A.loaders.foo.resolve(\"A LOADER\");\n      expect(t.router.state.navigation.state).toBe(\"loading\");\n      expect(t.router.state.loaderData).toEqual({\n        root: \"ROOT\", // old data\n        index: \"INDEX\", // old data\n      });\n\n      await A.loaders.root.resolve(\"ROOT LOADER\");\n      expect(t.router.state.navigation.state).toBe(\"idle\");\n      expect(t.router.state.actionData).toEqual({\n        foo: \"FOO ACTION\", // kept around on action reload\n      });\n      expect(t.router.state.loaderData).toEqual({\n        foo: \"A LOADER\",\n        root: \"ROOT LOADER\",\n      });\n    });\n\n    it(\"reloads all routes after action redirect (throw)\", async () => {\n      let t = initializeTest();\n      let A = await t.navigate(\"/foo\", {\n        formMethod: \"post\",\n        formData: createFormData({ gosh: \"dang\" }),\n      });\n      expect(A.loaders.root.stub.mock.calls.length).toBe(0);\n\n      let B = await A.actions.foo.redirect(\"/bar\");\n      expect(A.loaders.root.stub.mock.calls.length).toBe(0);\n      expect(B.loaders.root.stub.mock.calls.length).toBe(1);\n\n      await B.loaders.root.resolve(\"ROOT LOADER\");\n      expect(t.router.state.navigation.state).toBe(\"loading\");\n      expect(t.router.state.loaderData).toEqual({\n        root: \"ROOT\", // old data\n        index: \"INDEX\", // old data\n      });\n\n      await B.loaders.bar.resolve(\"B LOADER\");\n      expect(t.router.state.navigation.state).toBe(\"idle\");\n      expect(t.router.state.loaderData).toEqual({\n        bar: \"B LOADER\",\n        root: \"ROOT LOADER\",\n      });\n    });\n\n    it(\"reloads all routes after action redirect (return)\", async () => {\n      let t = initializeTest();\n      let A = await t.navigate(\"/foo\", {\n        formMethod: \"post\",\n        formData: createFormData({ gosh: \"dang\" }),\n      });\n      expect(A.loaders.root.stub.mock.calls.length).toBe(0);\n\n      let B = await A.actions.foo.redirectReturn(\"/bar\");\n      expect(A.loaders.root.stub.mock.calls.length).toBe(0);\n      expect(B.loaders.root.stub.mock.calls.length).toBe(1);\n\n      await B.loaders.root.resolve(\"ROOT LOADER\");\n      expect(t.router.state.navigation.state).toBe(\"loading\");\n      expect(t.router.state.loaderData).toEqual({\n        root: \"ROOT\", // old data\n        index: \"INDEX\", // old data\n      });\n\n      await B.loaders.bar.resolve(\"B LOADER\");\n      expect(t.router.state.navigation.state).toBe(\"idle\");\n      expect(t.router.state.loaderData).toEqual({\n        bar: \"B LOADER\",\n        root: \"ROOT LOADER\",\n      });\n    });\n\n    it(\"reloads all routes after action redirect (chained redirects)\", async () => {\n      let t = initializeTest();\n      let A = await t.navigate(\"/foo\", {\n        formMethod: \"post\",\n        formData: createFormData({ gosh: \"dang\" }),\n      });\n      expect(A.loaders.root.stub.mock.calls.length).toBe(0);\n\n      let B = await A.actions.foo.redirectReturn(\"/bar\");\n      expect(B.loaders.root.stub.mock.calls.length).toBe(1);\n\n      await B.loaders.root.resolve(\"ROOT*\");\n      let C = await B.loaders.bar.redirectReturn(\"/baz\");\n      expect(C.loaders.root.stub.mock.calls.length).toBe(1);\n\n      await C.loaders.root.resolve(\"ROOT**\");\n      await C.loaders.baz.resolve(\"BAZ\");\n      expect(t.router.state.navigation.state).toBe(\"idle\");\n      expect(t.router.state.loaderData).toEqual({\n        baz: \"BAZ\",\n        root: \"ROOT**\",\n      });\n    });\n\n    it(\"removes action data at new locations\", async () => {\n      let t = initializeTest();\n      let A = await t.navigate(\"/foo\", {\n        formMethod: \"post\",\n        formData: createFormData({ gosh: \"dang\" }),\n      });\n      await A.actions.foo.resolve(\"A ACTION\");\n      await A.loaders.root.resolve(\"A ROOT\");\n      await A.loaders.foo.resolve(\"A LOADER\");\n      expect(t.router.state.actionData).toEqual({ foo: \"A ACTION\" });\n\n      let B = await t.navigate(\"/bar\");\n      await B.loaders.bar.resolve(\"B LOADER\");\n      expect(t.router.state.actionData).toBeNull();\n    });\n\n    it(\"removes action data after action redirect (w/o loaders to run)\", async () => {\n      let t = setup({\n        routes: [\n          {\n            index: true,\n            id: \"index\",\n            action: true,\n          },\n          {\n            path: \"/other\",\n            id: \"other\",\n          },\n        ],\n      });\n      let A = await t.navigate(\"/\", {\n        formMethod: \"post\",\n        formData: createFormData({ gosh: \"\" }),\n      });\n      await A.actions.index.resolve({ error: \"invalid\" });\n      expect(t.router.state.actionData).toEqual({\n        index: { error: \"invalid\" },\n      });\n\n      let B = await t.navigate(\"/\", {\n        formMethod: \"post\",\n        formData: createFormData({ gosh: \"dang\" }),\n      });\n      await B.actions.index.redirectReturn(\"/other\");\n\n      expect(t.router.state.actionData).toBeNull();\n    });\n\n    it(\"removes action data after action redirect (w/ loaders to run)\", async () => {\n      let t = setup({\n        routes: [\n          {\n            index: true,\n            id: \"index\",\n            action: true,\n          },\n          {\n            path: \"/other\",\n            id: \"other\",\n            loader: true,\n          },\n        ],\n      });\n      let A = await t.navigate(\"/\", {\n        formMethod: \"post\",\n        formData: createFormData({ gosh: \"\" }),\n      });\n      await A.actions.index.resolve({ error: \"invalid\" });\n      expect(t.router.state.actionData).toEqual({\n        index: { error: \"invalid\" },\n      });\n\n      let B = await t.navigate(\"/\", {\n        formMethod: \"post\",\n        formData: createFormData({ gosh: \"dang\" }),\n      });\n\n      let C = await B.actions.index.redirectReturn(\"/other\");\n      expect(t.router.state.actionData).toEqual({\n        index: { error: \"invalid\" },\n      });\n      expect(t.router.state.loaderData).toEqual({});\n\n      await C.loaders.other.resolve(\"OTHER\");\n\n      expect(t.router.state.actionData).toBeNull();\n      expect(t.router.state.loaderData).toEqual({\n        other: \"OTHER\",\n      });\n    });\n\n    it(\"removes action data after action redirect to current location\", async () => {\n      let t = setup({\n        routes: [\n          {\n            path: \"/\",\n            id: \"index\",\n            action: true,\n            loader: true,\n          },\n        ],\n      });\n      let A = await t.navigate(\"/\", {\n        formMethod: \"post\",\n        formData: createFormData({ gosh: \"\" }),\n      });\n      await A.actions.index.resolve({ error: \"invalid\" });\n      expect(t.router.state.actionData).toEqual({\n        index: { error: \"invalid\" },\n      });\n\n      let B = await t.navigate(\"/\", {\n        formMethod: \"post\",\n        formData: createFormData({ gosh: \"dang\" }),\n      });\n\n      let C = await B.actions.index.redirectReturn(\"/\");\n      expect(t.router.state.actionData).toEqual({\n        index: { error: \"invalid\" },\n      });\n      expect(t.router.state.loaderData).toEqual({});\n\n      await C.loaders.index.resolve(\"NEW\");\n\n      expect(t.router.state.actionData).toBeNull();\n      expect(t.router.state.loaderData).toEqual({\n        index: \"NEW\",\n      });\n    });\n\n    it(\"uses the proper action for index routes\", async () => {\n      let t = setup({\n        routes: [\n          {\n            path: \"/\",\n            id: \"parent\",\n            children: [\n              {\n                path: \"/child\",\n                id: \"child\",\n                hasErrorBoundary: true,\n                action: true,\n                children: [\n                  {\n                    index: true,\n                    id: \"childIndex\",\n                    hasErrorBoundary: true,\n                    action: true,\n                  },\n                ],\n              },\n            ],\n          },\n        ],\n      });\n      let A = await t.navigate(\"/child\", {\n        formMethod: \"post\",\n        formData: createFormData({ gosh: \"dang\" }),\n      });\n      await A.actions.child.resolve(\"CHILD\");\n      expect(t.router.state.actionData).toEqual({\n        child: \"CHILD\",\n      });\n\n      let B = await t.navigate(\"/child?index\", {\n        formMethod: \"post\",\n        formData: createFormData({ gosh: \"dang\" }),\n      });\n      await B.actions.childIndex.resolve(\"CHILD INDEX\");\n      expect(t.router.state.actionData).toEqual({\n        childIndex: \"CHILD INDEX\",\n      });\n    });\n\n    it(\"uses the proper action for pathless layout routes\", async () => {\n      let t = setup({\n        routes: [\n          {\n            id: \"parent\",\n            path: \"/parent\",\n            action: true,\n            children: [\n              {\n                hasErrorBoundary: true,\n                children: [\n                  {\n                    id: \"index\",\n                    index: true,\n                    action: true,\n                  },\n                ],\n              },\n            ],\n          },\n        ],\n      });\n      let A = await t.navigate(\"/parent\", {\n        formMethod: \"post\",\n        formData: createFormData({ gosh: \"dang\" }),\n      });\n      await A.actions.parent.resolve(\"PARENT\");\n      expect(t.router.state).toMatchObject({\n        location: { pathname: \"/parent\" },\n        actionData: {\n          parent: \"PARENT\",\n        },\n        errors: null,\n      });\n\n      let B = await t.navigate(\"/parent?index\", {\n        formMethod: \"post\",\n        formData: createFormData({ gosh: \"dang\" }),\n      });\n      await B.actions.index.resolve(\"INDEX\");\n      expect(t.router.state).toMatchObject({\n        location: { pathname: \"/parent\", search: \"?index\" },\n        actionData: {\n          index: \"INDEX\",\n        },\n        errors: null,\n      });\n    });\n\n    it(\"retains the index match when submitting to a layout route\", async () => {\n      let t = setup({\n        routes: [\n          {\n            path: \"/\",\n            id: \"parent\",\n            loader: true,\n            action: true,\n            children: [\n              {\n                path: \"/child\",\n                id: \"child\",\n                loader: true,\n                action: true,\n                children: [\n                  {\n                    index: true,\n                    id: \"childIndex\",\n                    loader: true,\n                    action: true,\n                  },\n                ],\n              },\n            ],\n          },\n        ],\n      });\n      let A = await t.navigate(\"/child\", {\n        formMethod: \"post\",\n        formData: new FormData(),\n      });\n      await A.actions.child.resolve(\"CHILD ACTION\");\n      await A.loaders.parent.resolve(\"PARENT LOADER\");\n      await A.loaders.child.resolve(\"CHILD LOADER\");\n      await A.loaders.childIndex.resolve(\"CHILD INDEX LOADER\");\n      expect(t.router.state.navigation.state).toBe(\"idle\");\n      expect(t.router.state.loaderData).toEqual({\n        parent: \"PARENT LOADER\",\n        child: \"CHILD LOADER\",\n        childIndex: \"CHILD INDEX LOADER\",\n      });\n      expect(t.router.state.actionData).toEqual({\n        child: \"CHILD ACTION\",\n      });\n      expect(t.router.state.matches.map((m) => m.route.id)).toEqual([\n        \"parent\",\n        \"child\",\n        \"childIndex\",\n      ]);\n    });\n\n    describe(\"formMethod casing\", () => {\n      it(\"normalizes to lowercase in v6\", async () => {\n        let t = setup({\n          routes: [\n            {\n              id: \"root\",\n              path: \"/\",\n              children: [\n                {\n                  id: \"child\",\n                  path: \"child\",\n                  loader: true,\n                  action: true,\n                },\n              ],\n            },\n          ],\n        });\n        let A = await t.navigate(\"/child\", {\n          formMethod: \"get\",\n          formData: createFormData({}),\n        });\n        expect(t.router.state.navigation.formMethod).toBe(\"GET\");\n        await A.loaders.child.resolve(\"LOADER\");\n        expect(t.router.state.navigation.formMethod).toBeUndefined();\n        await t.router.navigate(\"/\");\n\n        let B = await t.navigate(\"/child\", {\n          formMethod: \"post\",\n          formData: createFormData({}),\n        });\n        expect(t.router.state.navigation.formMethod).toBe(\"POST\");\n        await B.actions.child.resolve(\"ACTION\");\n        await B.loaders.child.resolve(\"LOADER\");\n        expect(t.router.state.navigation.formMethod).toBeUndefined();\n        await t.router.navigate(\"/\");\n\n        let C = await t.fetch(\"/child\", \"key\", {\n          formMethod: \"GET\",\n          formData: createFormData({}),\n        });\n        expect(t.router.state.fetchers.get(\"key\")?.formMethod).toBe(\"GET\");\n        await C.loaders.child.resolve(\"LOADER FETCH\");\n        expect(t.router.state.fetchers.get(\"key\")?.formMethod).toBeUndefined();\n\n        let D = await t.fetch(\"/child\", \"key\", {\n          formMethod: \"POST\",\n          formData: createFormData({}),\n        });\n        expect(t.router.state.fetchers.get(\"key\")?.formMethod).toBe(\"POST\");\n        await D.actions.child.resolve(\"ACTION FETCH\");\n        expect(t.router.state.fetchers.get(\"key\")?.formMethod).toBeUndefined();\n      });\n\n      it(\"normalizes to uppercase\", async () => {\n        let t = setup({\n          routes: [\n            {\n              id: \"root\",\n              path: \"/\",\n              children: [\n                {\n                  id: \"child\",\n                  path: \"child\",\n                  loader: true,\n                  action: true,\n                },\n              ],\n            },\n          ],\n        });\n        let A = await t.navigate(\"/child\", {\n          formMethod: \"get\",\n          formData: createFormData({}),\n        });\n        expect(t.router.state.navigation.formMethod).toBe(\"GET\");\n        await A.loaders.child.resolve(\"LOADER\");\n        expect(t.router.state.navigation.formMethod).toBeUndefined();\n        await t.router.navigate(\"/\");\n\n        let B = await t.navigate(\"/child\", {\n          formMethod: \"POST\",\n          formData: createFormData({}),\n        });\n        expect(t.router.state.navigation.formMethod).toBe(\"POST\");\n        await B.actions.child.resolve(\"ACTION\");\n        await B.loaders.child.resolve(\"LOADER\");\n        expect(t.router.state.navigation.formMethod).toBeUndefined();\n        await t.router.navigate(\"/\");\n\n        let C = await t.fetch(\"/child\", \"key\", {\n          formMethod: \"GET\",\n          formData: createFormData({}),\n        });\n        expect(t.router.state.fetchers.get(\"key\")?.formMethod).toBe(\"GET\");\n        await C.loaders.child.resolve(\"LOADER FETCH\");\n        expect(t.router.state.fetchers.get(\"key\")?.formMethod).toBeUndefined();\n\n        let D = await t.fetch(\"/child\", \"key\", {\n          formMethod: \"post\",\n          formData: createFormData({}),\n        });\n        expect(t.router.state.fetchers.get(\"key\")?.formMethod).toBe(\"POST\");\n        await D.actions.child.resolve(\"ACTION FETCH\");\n        expect(t.router.state.fetchers.get(\"key\")?.formMethod).toBeUndefined();\n      });\n    });\n  });\n\n  describe(\"action errors\", () => {\n    describe(\"with an error boundary in the action route\", () => {\n      it(\"uses the action route's error boundary\", async () => {\n        let t = setup({\n          routes: [\n            {\n              path: \"/\",\n              id: \"parent\",\n              children: [\n                {\n                  path: \"/child\",\n                  id: \"child\",\n                  hasErrorBoundary: true,\n                  action: true,\n                },\n              ],\n            },\n          ],\n        });\n        let A = await t.navigate(\"/child\", {\n          formMethod: \"post\",\n          formData: createFormData({ gosh: \"dang\" }),\n        });\n        await A.actions.child.reject(new Error(\"Kaboom!\"));\n        expect(t.router.state.errors).toEqual({\n          child: new Error(\"Kaboom!\"),\n        });\n      });\n\n      it(\"loads parent data, but not action data\", async () => {\n        let t = setup({\n          routes: [\n            {\n              path: \"/\",\n              id: \"parent\",\n              loader: true,\n              children: [\n                {\n                  path: \"/child\",\n                  id: \"child\",\n                  hasErrorBoundary: true,\n                  loader: true,\n                  action: true,\n                },\n              ],\n            },\n          ],\n        });\n        let A = await t.navigate(\"/child\", {\n          formMethod: \"post\",\n          formData: createFormData({ gosh: \"dang\" }),\n        });\n        await A.actions.child.reject(new Error(\"Kaboom!\"));\n        expect(A.loaders.parent.stub.mock.calls.length).toBe(1);\n        expect(A.loaders.child.stub.mock.calls.length).toBe(0);\n        await A.loaders.parent.resolve(\"PARENT LOADER\");\n        expect(t.router.state).toMatchObject({\n          loaderData: {\n            parent: \"PARENT LOADER\",\n          },\n          actionData: null,\n          errors: {\n            child: new Error(\"Kaboom!\"),\n          },\n        });\n      });\n    });\n\n    describe(\"with an error boundary above the action route\", () => {\n      it(\"uses the nearest error boundary\", async () => {\n        let t = setup({\n          routes: [\n            {\n              path: \"/\",\n              id: \"parent\",\n              hasErrorBoundary: true,\n              children: [\n                {\n                  path: \"/child\",\n                  id: \"child\",\n                  action: true,\n                },\n              ],\n            },\n          ],\n        });\n        let A = await t.navigate(\"/child\", {\n          formMethod: \"post\",\n          formData: createFormData({ gosh: \"dang\" }),\n        });\n        await A.actions.child.reject(new Error(\"Kaboom!\"));\n        expect(t.router.state.errors).toEqual({\n          parent: new Error(\"Kaboom!\"),\n        });\n      });\n    });\n\n    describe(\"with a parent loader that throws also, good grief!\", () => {\n      it(\"uses action error but nearest errorBoundary to parent\", async () => {\n        let t = setup({\n          routes: [\n            {\n              path: \"/\",\n              id: \"root\",\n              hasErrorBoundary: true,\n              children: [\n                {\n                  path: \"/parent\",\n                  id: \"parent\",\n                  loader: true,\n                  children: [\n                    {\n                      path: \"/parent/child\",\n                      id: \"child\",\n                      action: true,\n                      hasErrorBoundary: true,\n                    },\n                  ],\n                },\n              ],\n            },\n          ],\n        });\n\n        let A = await t.navigate(\"/parent/child\", {\n          formMethod: \"post\",\n          formData: createFormData({ gosh: \"dang\" }),\n        });\n        await A.actions.child.reject(new Error(\"Kaboom!\"));\n        await A.loaders.parent.reject(new Error(\"Should not see this!\"));\n        expect(t.router.state).toMatchObject({\n          loaderData: {},\n          actionData: {},\n          errors: {\n            root: new Error(\"Kaboom!\"),\n          },\n        });\n      });\n    });\n\n    describe(\"with no corresponding action\", () => {\n      it(\"throws a 405 ErrorResponse\", async () => {\n        let t = setup({\n          routes: [\n            {\n              path: \"/\",\n              id: \"parent\",\n              children: [\n                {\n                  path: \"/child\",\n                  id: \"child\",\n                  hasErrorBoundary: true,\n                },\n              ],\n            },\n          ],\n        });\n        let spy = jest.spyOn(console, \"warn\").mockImplementation(() => {});\n        await t.navigate(\"/child\", {\n          formMethod: \"post\",\n          formData: createFormData({ gosh: \"dang\" }),\n        });\n        expect(t.router.state.errors).toEqual({\n          child: new ErrorResponseImpl(\n            405,\n            \"Method Not Allowed\",\n            new Error(\n              'You made a POST request to \"/child\" but did not provide an ' +\n                '`action` for route \"child\", so there is no way to handle the request.',\n            ),\n            true,\n          ),\n        });\n        spy.mockReset();\n      });\n\n      it(\"still calls appropriate loaders after 405 ErrorResponse\", async () => {\n        let t = setup({\n          routes: [\n            {\n              path: \"/\",\n              id: \"parent\",\n              loader: true,\n              children: [\n                {\n                  path: \"child\",\n                  id: \"child\",\n                  loader: true,\n                  children: [\n                    {\n                      path: \"grandchild\",\n                      id: \"grandchild\",\n                      loader: true,\n                      // no action to post to\n                      hasErrorBoundary: true,\n                    },\n                  ],\n                },\n              ],\n            },\n          ],\n          hydrationData: {\n            loaderData: {\n              parent: \"PARENT DATA\",\n            },\n          },\n        });\n        let A = await t.navigate(\"/child/grandchild\", {\n          formMethod: \"post\",\n          formData: createFormData({ gosh: \"dang\" }),\n        });\n        expect(t.router.state.errors).toBe(null);\n        expect(A.loaders.parent.stub.mock.calls.length).toBe(1); // called again for revalidation\n        expect(A.loaders.child.stub.mock.calls.length).toBe(1); // called because it's above error\n        expect(A.loaders.grandchild.stub.mock.calls.length).toBe(0); // don't call due to error\n        await A.loaders.parent.resolve(\"PARENT DATA*\");\n        await A.loaders.child.resolve(\"CHILD DATA\");\n        expect(t.router.state.loaderData).toEqual({\n          parent: \"PARENT DATA*\",\n          child: \"CHILD DATA\",\n        });\n        expect(t.router.state.actionData).toBe(null);\n        expect(t.router.state.errors).toEqual({\n          grandchild: new ErrorResponseImpl(\n            405,\n            \"Method Not Allowed\",\n            new Error(\n              'You made a POST request to \"/child/grandchild\" but did not ' +\n                'provide an `action` for route \"grandchild\", so there is no way ' +\n                \"to handle the request.\",\n            ),\n            true,\n          ),\n        });\n      });\n    });\n\n    it(\"clears previous actionData at the throwing route\", async () => {\n      let t = setup({\n        routes: [\n          {\n            path: \"/\",\n            id: \"parent\",\n            loader: true,\n            children: [\n              {\n                path: \"/child\",\n                id: \"child\",\n                hasErrorBoundary: true,\n                action: true,\n                loader: true,\n              },\n            ],\n          },\n        ],\n      });\n      let nav = await t.navigate(\"/child\", {\n        formMethod: \"post\",\n        formData: createFormData({ key: \"value\" }),\n      });\n      await nav.actions.child.resolve(\"ACTION\");\n      await nav.loaders.parent.resolve(\"PARENT\");\n      await nav.loaders.child.resolve(\"CHILD\");\n      expect(t.router.state.actionData).toEqual({\n        child: \"ACTION\",\n      });\n      expect(t.router.state.loaderData).toEqual({\n        parent: \"PARENT\",\n        child: \"CHILD\",\n      });\n      expect(t.router.state.errors).toEqual(null);\n\n      let nav2 = await t.navigate(\"/child\", {\n        formMethod: \"post\",\n        formData: createFormData({ key2: \"value2\" }),\n      });\n      await nav2.actions.child.reject(new Error(\"Kaboom!\"));\n      await nav2.loaders.parent.resolve(\"PARENT2\");\n      expect(t.router.state.actionData).toEqual(null);\n      expect(t.router.state.loaderData).toEqual({\n        parent: \"PARENT2\",\n      });\n      expect(t.router.state.errors).toEqual({\n        child: new Error(\"Kaboom!\"),\n      });\n    });\n\n    it(\"does not clear previous loaderData at the handling route\", async () => {\n      let t = setup({\n        routes: [\n          {\n            path: \"/\",\n            id: \"parent\",\n            loader: true,\n            hasErrorBoundary: true,\n            children: [\n              {\n                path: \"/child\",\n                id: \"child\",\n                action: true,\n                loader: true,\n              },\n            ],\n          },\n        ],\n      });\n      let nav = await t.navigate(\"/child\");\n      await nav.loaders.parent.resolve(\"PARENT\");\n      await nav.loaders.child.resolve(\"CHILD\");\n      expect(t.router.state.actionData).toEqual(null);\n      expect(t.router.state.loaderData).toEqual({\n        parent: \"PARENT\",\n        child: \"CHILD\",\n      });\n      expect(t.router.state.errors).toEqual(null);\n\n      let nav2 = await t.navigate(\"/child\", {\n        formMethod: \"post\",\n        formData: createFormData({ key2: \"value2\" }),\n      });\n      await nav2.actions.child.reject(new Error(\"Kaboom!\"));\n      expect(t.router.state.actionData).toEqual(null);\n      expect(t.router.state.loaderData).toEqual({\n        parent: \"PARENT\",\n      });\n      expect(t.router.state.errors).toEqual({\n        parent: new Error(\"Kaboom!\"),\n      });\n    });\n  });\n\n  describe(\"submission encTypes\", () => {\n    async function validateFormDataSubmission(\n      body: any,\n      includeFormEncType: boolean,\n    ) {\n      let t = setup({\n        routes: [{ id: \"root\", path: \"/\", action: true }],\n      });\n\n      let nav = await t.navigate(\"/\", {\n        formMethod: \"post\",\n        ...(includeFormEncType\n          ? { formEncType: \"application/x-www-form-urlencoded\" }\n          : {}),\n        body,\n      });\n      expect(t.router.state.navigation.text).toBeUndefined();\n      expect(t.router.state.navigation.formData?.get(\"a\")).toBe(\"1\");\n      expect(t.router.state.navigation.formData?.get(\"b\")).toBe(\"2\");\n      expect(t.router.state.navigation.json).toBeUndefined();\n\n      await nav.actions.root.resolve(\"ACTION\");\n\n      expect(nav.actions.root.stub).toHaveBeenCalledWith({\n        params: {},\n        request: expect.any(Request),\n        unstable_pattern: expect.any(String),\n        context: {},\n      });\n\n      let request = nav.actions.root.stub.mock.calls[0][0].request;\n      expect(request.method).toBe(\"POST\");\n      expect(request.url).toBe(\"http://localhost/\");\n      expect(request.headers.get(\"Content-Type\")).toBe(\n        \"application/x-www-form-urlencoded;charset=UTF-8\",\n      );\n      let fd = await request.formData();\n      expect(fd.get(\"a\")).toBe(\"1\");\n      expect(fd.get(\"b\")).toBe(\"2\");\n    }\n\n    async function validateJsonObjectSubmission(body: any) {\n      let t = setup({\n        routes: [{ id: \"root\", path: \"/\", action: true }],\n      });\n\n      let nav = await t.navigate(\"/\", {\n        formMethod: \"post\",\n        formEncType: \"application/json\",\n        body,\n      });\n      expect(t.router.state.navigation.text).toBeUndefined();\n      expect(t.router.state.navigation.json?.[\"a\"]).toBe(1);\n      expect(t.router.state.navigation.json?.[\"b\"]).toBe(2);\n      expect(t.router.state.navigation.formData).toBeUndefined();\n\n      await nav.actions.root.resolve(\"ACTION\");\n\n      expect(nav.actions.root.stub).toHaveBeenCalledWith({\n        params: {},\n        request: expect.any(Request),\n        unstable_pattern: expect.any(String),\n        context: {},\n      });\n\n      let request = nav.actions.root.stub.mock.calls[0][0].request;\n      expect(request.method).toBe(\"POST\");\n      expect(request.url).toBe(\"http://localhost/\");\n      expect(request.headers.get(\"Content-Type\")).toBe(\"application/json\");\n      let json = await request.json();\n      expect(json[\"a\"]).toBe(1);\n      expect(json[\"b\"]).toBe(2);\n    }\n\n    async function validateJsonArraySubmission(body: any) {\n      let t = setup({\n        routes: [{ id: \"root\", path: \"/\", action: true }],\n      });\n\n      let nav = await t.navigate(\"/\", {\n        formMethod: \"post\",\n        formEncType: \"application/json\",\n        body,\n      });\n      expect(t.router.state.navigation.text).toBeUndefined();\n      expect(t.router.state.navigation.json?.[0]).toBe(1);\n      expect(t.router.state.navigation.json?.[1]).toBe(2);\n      expect(t.router.state.navigation.formData).toBeUndefined();\n\n      await nav.actions.root.resolve(\"ACTION\");\n\n      expect(nav.actions.root.stub).toHaveBeenCalledWith({\n        params: {},\n        request: expect.any(Request),\n        unstable_pattern: expect.any(String),\n        context: {},\n      });\n\n      let request = nav.actions.root.stub.mock.calls[0][0].request;\n      expect(request.method).toBe(\"POST\");\n      expect(request.url).toBe(\"http://localhost/\");\n      expect(request.headers.get(\"Content-Type\")).toBe(\"application/json\");\n      let json = await request.json();\n      expect(json[0]).toBe(1);\n      expect(json[1]).toBe(2);\n    }\n\n    // eslint-disable-next-line jest/expect-expect\n    it(\"serializes body as implicit application/x-www-form-urlencoded (object)\", async () => {\n      await validateFormDataSubmission({ a: \"1\", b: \"2\" }, false);\n    });\n\n    // eslint-disable-next-line jest/expect-expect\n    it(\"serializes body as implicit application/x-www-form-urlencoded (string)\", async () => {\n      await validateFormDataSubmission(\"a=1&b=2\", false);\n    });\n\n    // eslint-disable-next-line jest/expect-expect\n    it(\"serializes body as implicit application/x-www-form-urlencoded (string with leading ?)\", async () => {\n      await validateFormDataSubmission(\"?a=1&b=2\", false);\n    });\n\n    // eslint-disable-next-line jest/expect-expect\n    it(\"serializes body as implicit application/x-www-form-urlencoded (entries array)\", async () => {\n      await validateFormDataSubmission(\n        [\n          [\"a\", \"1\"],\n          [\"b\", \"2\"],\n        ],\n        false,\n      );\n    });\n\n    // eslint-disable-next-line jest/expect-expect\n    it(\"serializes body as explicit application/x-www-form-urlencoded (object)\", async () => {\n      await validateFormDataSubmission({ a: \"1\", b: \"2\" }, true);\n    });\n\n    // eslint-disable-next-line jest/expect-expect\n    it(\"serializes body as explicit application/x-www-form-urlencoded (string)\", async () => {\n      await validateFormDataSubmission(\"a=1&b=2\", true);\n    });\n\n    // eslint-disable-next-line jest/expect-expect\n    it(\"serializes body as explicit application/x-www-form-urlencoded (string with leading ?)\", async () => {\n      await validateFormDataSubmission(\"?a=1&b=2\", true);\n    });\n\n    // eslint-disable-next-line jest/expect-expect\n    it(\"serializes body as explicit application/x-www-form-urlencoded (entries array)\", async () => {\n      await validateFormDataSubmission(\n        [\n          [\"a\", \"1\"],\n          [\"b\", \"2\"],\n        ],\n        true,\n      );\n    });\n\n    // eslint-disable-next-line jest/expect-expect\n    it(\"serializes body as application/json (object)\", async () => {\n      await validateJsonObjectSubmission({ a: 1, b: 2 });\n    });\n\n    // eslint-disable-next-line jest/expect-expect\n    it(\"serializes body as application/json (object string)\", async () => {\n      await validateJsonObjectSubmission('{\"a\":1,\"b\":2}');\n    });\n\n    // eslint-disable-next-line jest/expect-expect\n    it(\"serializes body as application/json (array)\", async () => {\n      await validateJsonArraySubmission([1, 2]);\n    });\n\n    // eslint-disable-next-line jest/expect-expect\n    it(\"serializes body as application/json (array string)\", async () => {\n      await validateJsonArraySubmission(\"[1,2]\");\n    });\n\n    it(\"serializes body as text/plain (string)\", async () => {\n      let t = setup({\n        routes: [{ id: \"root\", path: \"/\", action: true }],\n      });\n\n      let body = \"plain text\";\n      let nav = await t.navigate(\"/\", {\n        formMethod: \"post\",\n        formEncType: \"text/plain\",\n        body,\n      });\n      expect(t.router.state.navigation.text).toBe(body);\n      expect(t.router.state.navigation.formData).toBeUndefined();\n      expect(t.router.state.navigation.json).toBeUndefined();\n\n      await nav.actions.root.resolve(\"ACTION\");\n\n      expect(nav.actions.root.stub).toHaveBeenCalledWith({\n        params: {},\n        request: expect.any(Request),\n        unstable_pattern: expect.any(String),\n        context: {},\n      });\n\n      let request = nav.actions.root.stub.mock.calls[0][0].request;\n      expect(request.method).toBe(\"POST\");\n      expect(request.url).toBe(\"http://localhost/\");\n      expect(request.headers.get(\"Content-Type\")).toBe(\n        \"text/plain;charset=UTF-8\",\n      );\n      expect(await request.text()).toEqual(body);\n    });\n\n    it(\"serializes body as text/plain (FormData)\", async () => {\n      let t = setup({\n        routes: [{ id: \"root\", path: \"/\", action: true }],\n      });\n\n      let body = new FormData();\n      body.append(\"a\", \"1\");\n      body.append(\"b\", \"2\");\n      let nav = await t.navigate(\"/\", {\n        formMethod: \"post\",\n        formEncType: \"text/plain\",\n        body,\n      });\n      expect(t.router.state.navigation.text).toMatchInlineSnapshot(`\n      \"a=1\n      b=2\n      \"\n    `);\n      expect(t.router.state.navigation.formData).toBeUndefined();\n      expect(t.router.state.navigation.json).toBeUndefined();\n\n      await nav.actions.root.resolve(\"ACTION\");\n\n      expect(nav.actions.root.stub).toHaveBeenCalledWith({\n        params: {},\n        request: expect.any(Request),\n        unstable_pattern: expect.any(String),\n        context: {},\n      });\n\n      let request = nav.actions.root.stub.mock.calls[0][0].request;\n      expect(request.method).toBe(\"POST\");\n      expect(request.url).toBe(\"http://localhost/\");\n      expect(request.headers.get(\"Content-Type\")).toBe(\n        \"text/plain;charset=UTF-8\",\n      );\n      expect(await request.text()).toMatchInlineSnapshot(`\n      \"a=1\n      b=2\n      \"\n    `);\n    });\n\n    it(\"serializes body as FormData when encType=undefined\", async () => {\n      let t = setup({\n        routes: [{ id: \"root\", path: \"/\", action: true }],\n      });\n\n      let body = { a: \"1\" };\n      let nav = await t.navigate(\"/\", {\n        formMethod: \"post\",\n        body,\n      });\n      expect(t.router.state.navigation.text).toBeUndefined();\n      expect(t.router.state.navigation.formData?.get(\"a\")).toBe(\"1\");\n      expect(t.router.state.navigation.json).toBeUndefined();\n\n      await nav.actions.root.resolve(\"ACTION\");\n\n      expect(nav.actions.root.stub).toHaveBeenCalledWith({\n        params: {},\n        request: expect.any(Request),\n        unstable_pattern: expect.any(String),\n        context: {},\n      });\n\n      let request = nav.actions.root.stub.mock.calls[0][0].request;\n      expect(request.method).toBe(\"POST\");\n      expect((await request.formData()).get(\"a\")).toBe(\"1\");\n      expect(request.headers.get(\"Content-Type\")).toBe(\n        \"application/x-www-form-urlencoded;charset=UTF-8\",\n      );\n    });\n\n    it(\"throws on invalid URLSearchParams submissions\", async () => {\n      let t = setup({\n        routes: [{ id: \"root\", path: \"/\", action: true }],\n      });\n\n      await t.navigate(\"/\", {\n        formMethod: \"post\",\n        formEncType: \"application/x-www-form-urlencoded\",\n        body: [\"you\", \"cant\", \"do\", \"this\"],\n      });\n      expect(t.router.state.errors).toMatchInlineSnapshot(`\n      {\n        \"root\": ErrorResponseImpl {\n          \"data\": \"Error: Unable to encode submission body\",\n          \"error\": [Error: Unable to encode submission body],\n          \"internal\": true,\n          \"status\": 400,\n          \"statusText\": \"Bad Request\",\n        },\n      }\n    `);\n    });\n\n    it(\"throws on invalid JSON submissions\", async () => {\n      let t = setup({\n        routes: [{ id: \"root\", path: \"/\", action: true }],\n      });\n\n      await t.navigate(\"/\", {\n        formMethod: \"post\",\n        formEncType: \"application/json\",\n        body: '{ not: \"valid }',\n      });\n      expect(t.router.state.errors).toMatchInlineSnapshot(`\n      {\n        \"root\": ErrorResponseImpl {\n          \"data\": \"Error: Unable to encode submission body\",\n          \"error\": [Error: Unable to encode submission body],\n          \"internal\": true,\n          \"status\": 400,\n          \"statusText\": \"Bad Request\",\n        },\n      }\n    `);\n    });\n  });\n});\n"
  },
  {
    "path": "packages/react-router/__tests__/router/utils/custom-matchers.ts",
    "content": "interface CustomMatchers<R = jest.Expect> {\n  URL(url: string);\n  trackedPromise(data?: any, error?: any, aborted?: boolean): R;\n  deferredData(\n    done: boolean,\n    status?: number,\n    headers?: Record<string, string>,\n  ): R;\n}\n\ndeclare global {\n  namespace jest {\n    interface Expect extends CustomMatchers {}\n    interface Matchers<R> extends CustomMatchers<R> {}\n    interface InverseAsymmetricMatchers extends CustomMatchers {}\n  }\n}\n\n// Custom matcher for asserting against URLs\nexport function urlMatch(received, url) {\n  return {\n    message: () => `expected URL ${received.toString()} to equal URL ${url}`,\n    pass: received instanceof URL && received.toString() === url,\n  };\n}\n"
  },
  {
    "path": "packages/react-router/__tests__/router/utils/data-router-setup.ts",
    "content": "import type { InitialEntry } from \"../../../lib/router/history\";\nimport type {\n  Fetcher,\n  RouterFetchOptions,\n  Router,\n  RouterNavigateOptions,\n  RouterInit,\n} from \"../../../lib/router/router\";\nimport type {\n  AgnosticDataRouteObject,\n  AgnosticRouteMatch,\n} from \"../../../lib/router/utils\";\nimport { createRouter, IDLE_FETCHER } from \"../../../lib/router/router\";\nimport {\n  createMemoryHistory,\n  invariant,\n  parsePath,\n} from \"../../../lib/router/history\";\nimport type {\n  AgnosticIndexRouteObject,\n  AgnosticNonIndexRouteObject,\n} from \"../../../lib/router/utils\";\nimport {\n  matchRoutes,\n  redirect,\n  stripBasename,\n} from \"../../../lib/router/utils\";\n\nimport { isRedirect, tick } from \"./utils\";\n\n// Routes passed into setup() should just have a boolean for loader/action\n// indicating they want a stub.  They get enhanced back to AgnosticRouteObjects\n// by our test harness\nexport type TestIndexRouteObject = Pick<\n  AgnosticIndexRouteObject,\n  | \"id\"\n  | \"index\"\n  | \"path\"\n  | \"shouldRevalidate\"\n  | \"handle\"\n  | \"lazy\"\n  | \"middleware\"\n> & {\n  loader?: boolean;\n  action?: boolean;\n  hasErrorBoundary?: boolean;\n};\n\nexport type TestNonIndexRouteObject = Pick<\n  AgnosticNonIndexRouteObject,\n  | \"id\"\n  | \"index\"\n  | \"path\"\n  | \"shouldRevalidate\"\n  | \"handle\"\n  | \"lazy\"\n  | \"middleware\"\n> & {\n  loader?: boolean;\n  action?: boolean;\n  hasErrorBoundary?: boolean;\n  children?: TestRouteObject[];\n};\n\nexport type TestRouteObject = TestIndexRouteObject | TestNonIndexRouteObject;\n\n// A helper that includes the Deferred and stubs for any loaders/actions for the\n// route allowing fine-grained test execution\nexport type InternalHelpers = {\n  navigationId: number;\n  dfd: ReturnType<typeof createDeferred>;\n  stub: jest.Mock;\n  _signal?: AbortSignal;\n};\n\nexport type Helpers = InternalHelpers & {\n  get signal(): AbortSignal;\n  resolve: (d: any) => Promise<void>;\n  reject: (d: any) => Promise<void>;\n  redirect: (\n    href: string,\n    status?: number,\n    headers?: Record<string, string>,\n    shims?: string[],\n  ) => Promise<NavigationHelpers>;\n  redirectReturn: (\n    href: string,\n    status?: number,\n    headers?: Record<string, string>,\n    shims?: string[],\n  ) => Promise<NavigationHelpers>;\n};\n\n// Helpers returned from a TestHarness.navigate call, allowing fine grained\n// control and assertions over the loaders/actions\nexport type NavigationHelpers = {\n  navigationId: number;\n  loaders: Record<string, Helpers>;\n  actions: Record<string, Helpers>;\n};\n\nexport type FetcherHelpers = NavigationHelpers & {\n  key: string;\n  fetcher: Fetcher;\n};\n\n// Router created by setup() - used for automatic cleanup\nlet currentRouter: Router | null = null;\n// A set of to-be-garbage-collected Deferred's to clean up at the end of a test\nlet gcDfds = new Set<ReturnType<typeof createDeferred>>();\n\n// Reusable routes for a simple tasks app, for test cases that don't want\n// to create their own more complex routes\nexport const TASK_ROUTES: TestRouteObject[] = [\n  {\n    id: \"root\",\n    path: \"/\",\n    loader: true,\n    hasErrorBoundary: true,\n    children: [\n      {\n        id: \"index\",\n        index: true,\n        loader: true,\n      },\n      {\n        id: \"tasks\",\n        path: \"tasks\",\n        loader: true,\n        action: true,\n        hasErrorBoundary: true,\n      },\n      {\n        id: \"tasksId\",\n        path: \"tasks/:id\",\n        loader: true,\n        action: true,\n        hasErrorBoundary: true,\n      },\n      {\n        id: \"noLoader\",\n        path: \"no-loader\",\n      },\n    ],\n  },\n];\n\ntype SetupOpts = Omit<RouterInit, \"routes\" | \"history\" | \"window\"> & {\n  routes: TestRouteObject[];\n  initialEntries?: InitialEntry[];\n  initialIndex?: number;\n};\n\n// We use a slightly modified version of createDeferred here that includes the\n// tick() calls to let the router finish updating\nexport function createDeferred<T = any>() {\n  let resolve: (val: T) => Promise<void>;\n  let reject: (error?: Error) => Promise<void>;\n  let promise = new Promise<T>((res, rej) => {\n    resolve = async (val: T) => {\n      res(val);\n      await tick();\n      await promise;\n    };\n    reject = async (error?: Error) => {\n      rej(error);\n      await promise.catch(() => tick());\n    };\n  });\n  return {\n    promise,\n    //@ts-ignore\n    resolve,\n    //@ts-ignore\n    reject,\n  };\n}\n\nexport function createAsyncStub(): [\n  asyncStub: jest.Mock,\n  deferred: ReturnType<typeof createDeferred>,\n] {\n  let deferred = createDeferred();\n  let asyncStub = jest.fn(() => deferred.promise);\n\n  return [asyncStub, deferred];\n}\n\nexport function getFetcherData(router: Router) {\n  let fetcherData = new Map<string, unknown>();\n  router.subscribe((state, { deletedFetchers }) => {\n    deletedFetchers.forEach((k) => {\n      fetcherData.delete(k);\n    });\n    state.fetchers.forEach((fetcher, key) => {\n      if (\n        fetcher.data !== undefined &&\n        // action fetch\n        ((fetcher.formMethod !== \"GET\" && fetcher.state === \"loading\") ||\n          // normal fetch\n          fetcher.state === \"idle\")\n      ) {\n        fetcherData.set(key, fetcher.data);\n      }\n    });\n  });\n  return fetcherData;\n}\n\nexport function setup({\n  routes,\n  initialEntries,\n  initialIndex,\n  ...routerInit\n}: SetupOpts) {\n  let guid = 0;\n  // Global \"active\" helpers, keyed by navType:guid:loaderOrAction:routeId.\n  // For example, the first navigation for /parent/foo would generate:\n  //   navigation:1:action:parent -> helpers\n  //   navigation:1:action:foo -> helpers\n  //   navigation:1:loader:parent -> helpers\n  //   navigation:1:loader:foo -> helpers\n  //\n  let activeHelpers = new Map<string, InternalHelpers>();\n  // \"Active\" flags to indicate which helpers should be used the next time a\n  // router calls an action or loader internally.\n  let activeActionType: \"navigation\" | \"fetch\" = \"navigation\";\n  let activeLoaderType: \"navigation\" | \"fetch\" = \"navigation\";\n  let activeLoaderNavigationId = guid;\n  let activeActionNavigationId = guid;\n  let activeLoaderFetchId = guid;\n  let activeActionFetchId = guid;\n\n  // Enhance routes with loaders/actions as requested that will call the\n  // active navigation loader/action\n  function enhanceRoutes(_routes: TestRouteObject[]) {\n    return _routes.map((r) => {\n      let enhancedRoute: AgnosticDataRouteObject = {\n        middleware: undefined,\n        ...r,\n        loader: undefined,\n        action: undefined,\n        children: undefined,\n        id: r.id || `route-${guid++}`,\n      };\n      if (r.loader) {\n        enhancedRoute.loader = (...args) => {\n          let navigationId =\n            activeLoaderType === \"fetch\"\n              ? activeLoaderFetchId\n              : activeLoaderNavigationId;\n          let helperKey = `${activeLoaderType}:${navigationId}:loader:${enhancedRoute.id}`;\n          let helpers = activeHelpers.get(helperKey);\n          invariant(helpers, `No helpers found for: ${helperKey}`);\n          helpers.stub(...args);\n          helpers._signal = args[0].request.signal;\n          return helpers.dfd.promise;\n        };\n      }\n      if (r.action) {\n        enhancedRoute.action = (...args) => {\n          let type = activeActionType;\n          let navigationId =\n            activeActionType === \"fetch\"\n              ? activeActionFetchId\n              : activeActionNavigationId;\n          let helperKey = `${activeActionType}:${navigationId}:action:${enhancedRoute.id}`;\n          let helpers = activeHelpers.get(helperKey);\n          invariant(helpers, `No helpers found for: ${helperKey}`);\n          helpers.stub(...args);\n          helpers._signal = args[0].request.signal;\n          return helpers.dfd.promise.then(\n            (result) => {\n              // After a successful non-redirect action, ensure we call the right\n              // loaders as a follow up.  In the case of a redirect, ths navigation\n              // is aborted and we will use whatever new navigationId the redirect\n              // already assigned\n              if (!isRedirect(result)) {\n                if (type === \"navigation\") {\n                  activeLoaderType = \"navigation\";\n                  activeLoaderNavigationId = navigationId;\n                } else {\n                  activeLoaderType = \"fetch\";\n                  activeLoaderFetchId = navigationId;\n                }\n              }\n              return result;\n            },\n            (result) => {\n              // After a non-redirect rejected navigation action, we may still call\n              // ancestor loaders so set the right values to ensure we trigger the\n              // right ones.\n              if (type === \"navigation\" && !isRedirect(result)) {\n                activeLoaderType = \"navigation\";\n                activeLoaderNavigationId = navigationId;\n              }\n              return Promise.reject(result);\n            },\n          );\n        };\n      }\n      if (!r.index && r.children) {\n        enhancedRoute.children = enhanceRoutes(r.children);\n      }\n      return enhancedRoute;\n    });\n  }\n\n  // jsdom is making more and more properties non-configurable, so we inject\n  // our own jest-friendly window.\n  let testWindow = {\n    ...window,\n    location: {\n      ...window.location,\n      assign: jest.fn(),\n      replace: jest.fn(),\n    },\n  } as unknown as Window;\n  // ^ Spread makes TS sad - `window.NaN` conflicts with `[index: number]: Window`\n\n  let history = createMemoryHistory({ initialEntries, initialIndex });\n  jest.spyOn(history, \"push\");\n  jest.spyOn(history, \"replace\");\n  currentRouter = createRouter({\n    history,\n    routes: enhanceRoutes(routes),\n    window: testWindow,\n    ...routerInit,\n  });\n\n  let fetcherData = getFetcherData(currentRouter);\n  currentRouter.initialize();\n\n  function getRouteHelpers(\n    routeId: string,\n    navigationId: number,\n    addHelpers: (routeId: string, helpers: InternalHelpers) => void,\n  ): Helpers {\n    // Internal methods we need access to from the route loader execution\n    let internalHelpers: InternalHelpers = {\n      navigationId,\n      dfd: createDeferred(),\n      stub: jest.fn(),\n    };\n    // Allow the caller to store off the helpers in the right spot so eventual\n    // executions by the router can access the right ones\n    addHelpers(routeId, internalHelpers);\n    gcDfds.add(internalHelpers.dfd);\n\n    async function _redirect(\n      isRejection: boolean,\n      href: string,\n      status = 301,\n      headers = {},\n      shims: string[] = [],\n    ) {\n      let redirectNavigationId = ++guid;\n      activeLoaderType = \"navigation\";\n      activeLoaderNavigationId = redirectNavigationId;\n      if (status === 307 || status === 308) {\n        activeActionType = \"navigation\";\n        activeActionNavigationId = redirectNavigationId;\n      }\n\n      let helpers = getNavigationHelpers(href, redirectNavigationId);\n\n      // Since a redirect kicks off and awaits a new navigation we can't shim\n      // these _after_ the redirect, so we allow the caller to pass in loader\n      // shims with the redirect\n      shims.forEach((routeId) =>\n        shimHelper(helpers.loaders, \"navigation\", \"loader\", routeId),\n      );\n\n      try {\n        let redirectResponse = redirect(href, { status, headers });\n        if (isRejection) {\n          // @ts-ignore\n          await internalHelpers.dfd.reject(redirectResponse);\n        } else {\n          await internalHelpers.dfd.resolve(redirectResponse);\n        }\n        await tick();\n      } catch (e) {}\n      return helpers;\n    }\n\n    let routeHelpers: Helpers = {\n      get signal() {\n        return internalHelpers._signal!;\n      },\n      // Note: This spread has to come _after_ the above getter, otherwise\n      // we lose the getter nature of it somewhere in the babel/typescript\n      // transform.  Doesn't seem ot be an issue in ts-jest but that's a\n      // bit large of a change to look into at the moment\n      ...internalHelpers,\n      // Public APIs only needed for test execution\n      async resolve(value) {\n        await internalHelpers.dfd.resolve(value);\n      },\n      async reject(value) {\n        try {\n          await internalHelpers.dfd.reject(value);\n        } catch (e) {}\n      },\n      async redirect(href, status = 301, headers = {}, shims = []) {\n        return _redirect(true, href, status, headers, shims);\n      },\n      async redirectReturn(href, status = 301, headers = {}, shims = []) {\n        return _redirect(false, href, status, headers, shims);\n      },\n    };\n    return routeHelpers;\n  }\n\n  function getHelpers(\n    matches: AgnosticRouteMatch<string, AgnosticDataRouteObject>[],\n    navigationId: number,\n    addHelpers: (routeId: string, helpers: InternalHelpers) => void,\n  ): Record<string, Helpers> {\n    return matches.reduce(\n      (acc, m) =>\n        Object.assign(acc, {\n          [m.route.id]: getRouteHelpers(m.route.id, navigationId, addHelpers),\n        }),\n      {},\n    );\n  }\n\n  let inFlightRoutes: AgnosticDataRouteObject[] | undefined;\n  function _internalSetRoutes(routes: AgnosticDataRouteObject[]) {\n    inFlightRoutes = routes;\n    currentRouter?._internalSetRoutes(routes);\n  }\n\n  function getNavigationHelpers(\n    href: string,\n    navigationId: number,\n  ): NavigationHelpers {\n    invariant(\n      currentRouter?.routes,\n      \"No currentRouter.routes available in getNavigationHelpers\",\n    );\n    let matches = matchRoutes(inFlightRoutes || currentRouter.routes, href);\n\n    let loaderHelpers = getHelpers(\n      (matches || []).filter((m) => m.route.loader),\n      navigationId,\n      (routeId, helpers) =>\n        activeHelpers.set(\n          `navigation:${navigationId}:loader:${routeId}`,\n          helpers,\n        ),\n    );\n    let actionHelpers = getHelpers(\n      (matches || []).filter((m) => m.route.action),\n      navigationId,\n      (routeId, helpers) =>\n        activeHelpers.set(\n          `navigation:${navigationId}:action:${routeId}`,\n          helpers,\n        ),\n    );\n\n    return {\n      navigationId,\n      loaders: loaderHelpers,\n      actions: actionHelpers,\n    };\n  }\n\n  function getFetcherHelpers(\n    key: string,\n    href: string,\n    navigationId: number,\n    opts?: RouterNavigateOptions,\n  ): FetcherHelpers {\n    invariant(\n      currentRouter?.routes,\n      \"No currentRouter.routes available in getFetcherHelpers\",\n    );\n    let matches = matchRoutes(inFlightRoutes || currentRouter.routes, href);\n    invariant(currentRouter, \"No currentRouter available\");\n    let search = parsePath(href).search || \"\";\n    let hasNakedIndexQuery = new URLSearchParams(search)\n      .getAll(\"index\")\n      .some((v) => v === \"\");\n\n    // Let fetcher 404s go right through\n    if (!matches) {\n      return {\n        key,\n        navigationId,\n        get fetcher() {\n          invariant(currentRouter, \"No currentRouter available\");\n          return currentRouter.getFetcher(key);\n        },\n        loaders: {},\n        actions: {},\n      };\n    }\n\n    let match =\n      matches[matches.length - 1].route.index && !hasNakedIndexQuery\n        ? matches.slice(-2)[0]\n        : matches.slice(-1)[0];\n\n    // If this is an action submission we need loaders for all current matches.\n    // Otherwise we should only need a loader for the leaf match\n    let activeLoaderMatches = [match];\n    // @ts-expect-error\n    if (opts?.formMethod != null && opts.formMethod.toUpperCase() !== \"GET\") {\n      if (currentRouter.state.navigation?.location) {\n        let matches = matchRoutes(\n          inFlightRoutes || currentRouter.routes,\n          currentRouter.state.navigation.location,\n        );\n        invariant(matches, \"No matches found for fetcher\");\n        activeLoaderMatches = matches;\n      } else {\n        activeLoaderMatches = currentRouter.state.matches;\n      }\n    }\n\n    let loaderHelpers = getHelpers(\n      activeLoaderMatches.filter((m) => m.route.loader),\n      navigationId,\n      (routeId, helpers) =>\n        activeHelpers.set(`fetch:${navigationId}:loader:${routeId}`, helpers),\n    );\n    let actionHelpers = getHelpers(\n      match.route.action ? [match] : [],\n      navigationId,\n      (routeId, helpers) =>\n        activeHelpers.set(`fetch:${navigationId}:action:${routeId}`, helpers),\n    );\n\n    return {\n      key,\n      navigationId,\n      get fetcher() {\n        invariant(currentRouter, \"No currentRouter available\");\n        return {\n          ...currentRouter.getFetcher(key),\n          data: fetcherData.get(key),\n        };\n      },\n      loaders: loaderHelpers,\n      actions: actionHelpers,\n    };\n  }\n\n  // Simulate a navigation, returning a series of helpers to manually\n  // control/assert loader/actions\n  function navigate(n: number): Promise<NavigationHelpers>;\n  function navigate(\n    href: string,\n    opts?: RouterNavigateOptions,\n    shims?: string[],\n  ): Promise<NavigationHelpers>;\n  async function navigate(\n    href: number | string,\n    opts?: RouterNavigateOptions,\n    shims?: string[],\n  ): Promise<NavigationHelpers> {\n    let navigationId = ++guid;\n    let helpers: NavigationHelpers;\n\n    invariant(currentRouter, \"No currentRouter available\");\n\n    // @ts-expect-error\n    if (opts?.formMethod != null && opts.formMethod.toUpperCase() !== \"GET\") {\n      activeActionType = \"navigation\";\n      activeActionNavigationId = navigationId;\n      // Assume happy path and mark this navigations loaders as active.  Even if\n      // we never call them from the router (if the action rejects) we'll want\n      // this to be accurate so we can assert against the stubs\n      activeLoaderType = \"navigation\";\n      activeLoaderNavigationId = navigationId;\n    } else {\n      activeLoaderType = \"navigation\";\n      activeLoaderNavigationId = navigationId;\n    }\n\n    if (typeof href === \"number\") {\n      let promise = new Promise<void>((r) => {\n        invariant(currentRouter, \"No currentRouter available\");\n        let unsubscribe = currentRouter.subscribe(() => {\n          let popHref = history.createHref(history.location);\n          if (currentRouter?.basename) {\n            popHref = stripBasename(popHref, currentRouter.basename) as string;\n            invariant(\n              popHref,\n              \"href passed to navigate should start with basename\",\n            );\n          }\n          helpers = getNavigationHelpers(popHref, navigationId);\n          unsubscribe();\n          r();\n        });\n      });\n      currentRouter.navigate(href);\n      await promise;\n      //@ts-ignore\n      return helpers;\n    }\n\n    helpers = getNavigationHelpers(href, navigationId);\n    shims?.forEach((routeId) =>\n      shimHelper(helpers.loaders, \"navigation\", \"loader\", routeId),\n    );\n    currentRouter.navigate(href, opts);\n    return helpers;\n  }\n\n  // Simulate a fetcher call, returning a series of helpers to manually\n  // control/assert loader/actions\n  async function fetch(\n    href: string,\n    opts?: RouterFetchOptions,\n  ): Promise<FetcherHelpers>;\n  async function fetch(\n    href: string,\n    key: string,\n    opts?: RouterFetchOptions,\n  ): Promise<FetcherHelpers>;\n  async function fetch(\n    href: string,\n    key: string,\n    routeId: string,\n    opts?: RouterFetchOptions,\n  ): Promise<FetcherHelpers>;\n  async function fetch(\n    href: string,\n    keyOrOpts?: string | RouterFetchOptions,\n    routeIdOrOpts?: string | RouterFetchOptions,\n    opts?: RouterFetchOptions,\n  ): Promise<FetcherHelpers> {\n    let navigationId = ++guid;\n    let key = typeof keyOrOpts === \"string\" ? keyOrOpts : String(navigationId);\n    let routeId =\n      typeof routeIdOrOpts === \"string\"\n        ? routeIdOrOpts\n        : String(currentRouter?.routes[0].id);\n    opts =\n      typeof keyOrOpts === \"object\"\n        ? keyOrOpts\n        : typeof routeIdOrOpts === \"object\"\n          ? routeIdOrOpts\n          : opts;\n    invariant(currentRouter, \"No currentRouter available\");\n\n    // @ts-expect-error\n    if (opts?.formMethod != null && opts.formMethod.toUpperCase() !== \"GET\") {\n      activeActionType = \"fetch\";\n      activeActionFetchId = navigationId;\n    } else {\n      activeLoaderType = \"fetch\";\n      activeLoaderFetchId = navigationId;\n    }\n\n    let helpers = getFetcherHelpers(key, href, navigationId, opts);\n    currentRouter.fetch(key, routeId, href, opts);\n    return helpers;\n  }\n\n  // Simulate a revalidation, returning a series of helpers to manually\n  // control/assert loader/actions\n  async function revalidate(\n    type: \"navigation\" | \"fetch\" = \"navigation\",\n    shimRouteId?: string,\n  ): Promise<NavigationHelpers> {\n    invariant(currentRouter, \"No currentRouter available\");\n    let navigationId;\n    if (type === \"fetch\") {\n      // This is a special case for when we want to test revalidation against\n      // fetchers, so that our A.loaders.routeId will trigger the fetcher loader,\n      // not the route loader\n      navigationId = ++guid;\n      activeLoaderType = \"fetch\";\n      activeLoaderFetchId = navigationId;\n    } else {\n      // if a revalidation interrupts an action submission, we don't actually\n      // start a new navigation so don't increment here\n      navigationId =\n        currentRouter.state.navigation.state === \"submitting\" &&\n        currentRouter.state.navigation.formMethod !== \"GET\"\n          ? guid\n          : ++guid;\n      activeLoaderType = \"navigation\";\n      activeLoaderNavigationId = navigationId;\n    }\n    let href = currentRouter.createHref(\n      currentRouter.state.navigation.location || currentRouter.state.location,\n    );\n    let helpers = getNavigationHelpers(href, navigationId);\n    if (shimRouteId) {\n      shimHelper(helpers.loaders, type, \"loader\", shimRouteId);\n    }\n    currentRouter.revalidate();\n    return helpers;\n  }\n\n  function shimHelper(\n    navHelpers: Record<string, Helpers>,\n    type: \"navigation\" | \"fetch\",\n    type2: \"loader\" | \"action\",\n    routeId: string,\n  ) {\n    invariant(!navHelpers[routeId], \"Can't overwrite existing helpers\");\n    navHelpers[routeId] = getRouteHelpers(routeId, guid, (routeId, helpers) =>\n      activeHelpers.set(`${type}:${guid}:${type2}:${routeId}`, helpers),\n    );\n  }\n\n  return {\n    window: testWindow,\n    history,\n    router: currentRouter,\n    get fetchers() {\n      let fetchers: Record<string, Fetcher> = {};\n      currentRouter?.state.fetchers.forEach((f, key) => {\n        fetchers[key] = {\n          ...f,\n          data: fetcherData.get(key),\n        };\n      });\n      fetcherData.forEach((data, key) => {\n        if (!fetchers[key]) {\n          fetchers[key] = {\n            ...IDLE_FETCHER,\n            data: fetcherData.get(key),\n          };\n        }\n      });\n      return fetchers;\n    },\n    navigate,\n    fetch,\n    revalidate,\n    shimHelper,\n    enhanceRoutes,\n    _internalSetRoutes,\n  };\n}\n\nexport function cleanup(_router?: Router) {\n  let router = _router || currentRouter;\n\n  // Cleanup any routers created using setup()\n  if (router) {\n    expect(router._internalFetchControllers.size).toBe(0);\n  }\n  router?.dispose();\n  currentRouter = null;\n\n  // Reject any lingering deferreds and remove\n  for (let dfd of gcDfds.values()) {\n    dfd.reject();\n    gcDfds.delete(dfd);\n  }\n  expect(gcDfds.size).toBe(0);\n}\n"
  },
  {
    "path": "packages/react-router/__tests__/router/utils/urlDataStrategy.ts",
    "content": "import type { DataStrategyFunction } from \"../../../lib/router/utils\";\nimport { invariant } from \"./utils\";\n\nconst urlDataStrategy: DataStrategyFunction = async ({ matches }) => {\n  let results: Record<string, { type: \"data\" | \"error\"; result: unknown }> = {};\n  await Promise.all(\n    matches.map((match) =>\n      match.resolve(async (handler) => {\n        let response = await handler();\n        invariant(response instanceof Response, \"Expected a response\");\n        let contentType = response.headers.get(\"Content-Type\");\n        invariant(\n          contentType === \"application/x-www-form-urlencoded\",\n          \"Invalid Response\",\n        );\n        results[match.route.id] = {\n          type: \"data\",\n          result: new URLSearchParams(await response.text()),\n        };\n      }),\n    ),\n  );\n  return results;\n};\n\nexport default urlDataStrategy;\n"
  },
  {
    "path": "packages/react-router/__tests__/router/utils/utils.ts",
    "content": "import type { AgnosticDataRouteObject } from \"../../utils\";\n\nexport async function sleep(n: number) {\n  await new Promise((r) => setTimeout(r, n));\n}\n\nexport async function tick() {\n  await sleep(0);\n}\n\nexport function invariant(value: boolean, message?: string): asserts value;\nexport function invariant<T>(\n  value: T | null | undefined,\n  message?: string,\n): asserts value is T;\nexport function invariant(value: any, message?: string) {\n  if (value === false || value === null || typeof value === \"undefined\") {\n    console.warn(\"Test invariant failed:\", message);\n    throw new Error(message);\n  }\n}\n\nexport function createFormData(obj: Record<string, string>): FormData {\n  let formData = new FormData();\n  Object.entries(obj).forEach((e) => formData.append(e[0], e[1]));\n  return formData;\n}\n\nexport function isRedirect(result: any) {\n  return (\n    result instanceof Response && result.status >= 300 && result.status <= 399\n  );\n}\n\nexport function createDeferred<T = unknown>() {\n  let resolve: (val?: any) => Promise<void>;\n  let reject: (error?: Error) => Promise<void>;\n  let promise = new Promise<T>((res, rej) => {\n    resolve = async (val: T) => {\n      res(val);\n      try {\n        await promise;\n      } catch (e) {}\n    };\n    reject = async (error?: Error) => {\n      rej(error);\n      try {\n        await promise;\n      } catch (e) {}\n    };\n  });\n  return {\n    promise,\n    //@ts-ignore\n    resolve,\n    //@ts-ignore\n    reject,\n  };\n}\n\nexport function findRouteById(\n  routes: AgnosticDataRouteObject[],\n  id: string,\n): AgnosticDataRouteObject {\n  let foundRoute: AgnosticDataRouteObject | null = null;\n  for (const route of routes) {\n    if (route.id === id) {\n      foundRoute = route;\n      break;\n    }\n    if (route.children) {\n      foundRoute = findRouteById(route.children, id);\n      if (foundRoute) {\n        break;\n      }\n    }\n  }\n\n  invariant(foundRoute, `Route not found with id \"${id}\".`);\n\n  return foundRoute;\n}\n\nexport function createRequest(path: string, opts?: RequestInit) {\n  return new Request(`http://localhost${path}`, opts);\n}\n\nexport function createSubmitRequest(path: string, opts?: RequestInit) {\n  let searchParams = new URLSearchParams();\n  searchParams.append(\"key\", \"value\");\n\n  return createRequest(path, {\n    method: \"post\",\n    body: searchParams,\n    ...opts,\n  });\n}\n"
  },
  {
    "path": "packages/react-router/__tests__/router/view-transition-test.ts",
    "content": "import { IDLE_NAVIGATION } from \"../../lib/router/router\";\nimport { cleanup, setup } from \"./utils/data-router-setup\";\nimport { createFormData } from \"./utils/utils\";\n\ndescribe(\"view transitions\", () => {\n  // Detect any failures inside the router navigate code\n  afterEach(() => cleanup());\n\n  it(\"only enables view transitions when specified for the navigation\", () => {\n    let t = setup({\n      routes: [{ path: \"/\" }, { path: \"/a\" }, { path: \"/b\" }],\n    });\n    let spy = jest.fn();\n    let unsubscribe = t.router.subscribe(spy);\n\n    // PUSH / -> /a - w/o transition\n    t.navigate(\"/a\");\n    expect(spy).toHaveBeenLastCalledWith(\n      expect.objectContaining({\n        navigation: IDLE_NAVIGATION,\n        location: expect.objectContaining({ pathname: \"/a\" }),\n      }),\n      expect.objectContaining({ viewTransitionOpts: undefined }),\n    );\n\n    // PUSH /a -> /b - w/ transition\n    t.navigate(\"/b\", { viewTransition: true });\n    expect(spy).toHaveBeenLastCalledWith(\n      expect.objectContaining({\n        navigation: IDLE_NAVIGATION,\n        location: expect.objectContaining({ pathname: \"/b\" }),\n      }),\n      expect.objectContaining({\n        viewTransitionOpts: {\n          currentLocation: expect.objectContaining({ pathname: \"/a\" }),\n          nextLocation: expect.objectContaining({ pathname: \"/b\" }),\n        },\n      }),\n    );\n\n    // POP /b -> /a - w/ transition (cached from above)\n    t.navigate(-1);\n    expect(spy).toHaveBeenLastCalledWith(\n      expect.objectContaining({\n        navigation: IDLE_NAVIGATION,\n        location: expect.objectContaining({ pathname: \"/a\" }),\n      }),\n      expect.objectContaining({\n        viewTransitionOpts: {\n          // Args reversed on POP so same hooks apply\n          currentLocation: expect.objectContaining({ pathname: \"/a\" }),\n          nextLocation: expect.objectContaining({ pathname: \"/b\" }),\n        },\n      }),\n    );\n\n    // POP /a -> / - No transition\n    t.navigate(-1);\n    expect(spy).toHaveBeenLastCalledWith(\n      expect.objectContaining({\n        navigation: IDLE_NAVIGATION,\n        location: expect.objectContaining({ pathname: \"/\" }),\n      }),\n      expect.objectContaining({ viewTransitionOpts: undefined }),\n    );\n\n    unsubscribe();\n    t.router.dispose();\n  });\n\n  it(\"preserves pending view transitions through router.revalidate()\", async () => {\n    let t = setup({\n      routes: [{ path: \"/\" }, { id: \"a\", path: \"/a\", loader: true }],\n    });\n    let spy = jest.fn();\n    let unsubscribe = t.router.subscribe(spy);\n\n    let A = await t.navigate(\"/a\", { viewTransition: true });\n    expect(spy).toHaveBeenCalledTimes(1);\n    expect(spy.mock.calls[0]).toEqual([\n      expect.objectContaining({\n        navigation: expect.objectContaining({ state: \"loading\" }),\n      }),\n      expect.objectContaining({ viewTransitionOpts: undefined }),\n    ]);\n    expect(A.loaders.a.stub).toHaveBeenCalledTimes(1);\n\n    // Interrupt the navigation loading state with a revalidation\n    let B = await t.revalidate();\n    expect(spy).toHaveBeenCalledTimes(3);\n    expect(spy.mock.calls[1]).toEqual([\n      expect.objectContaining({\n        revalidation: \"loading\",\n      }),\n      expect.objectContaining({\n        viewTransitionOpts: undefined,\n      }),\n    ]);\n    expect(spy.mock.calls[2]).toEqual([\n      expect.objectContaining({\n        navigation: expect.objectContaining({ state: \"loading\" }),\n      }),\n      expect.objectContaining({\n        viewTransitionOpts: undefined,\n      }),\n    ]);\n    expect(spy).toHaveBeenLastCalledWith(\n      expect.objectContaining({\n        navigation: expect.objectContaining({ state: \"loading\" }),\n      }),\n      expect.objectContaining({\n        viewTransitionOpts: undefined,\n      }),\n    );\n    expect(B.loaders.a.stub).toHaveBeenCalledTimes(1);\n\n    await A.loaders.a.resolve(\"A\");\n    await B.loaders.a.resolve(\"A*\");\n\n    expect(spy).toHaveBeenCalledTimes(4);\n    expect(spy.mock.calls[3]).toEqual([\n      expect.objectContaining({\n        navigation: IDLE_NAVIGATION,\n        location: expect.objectContaining({ pathname: \"/a\" }),\n        loaderData: {\n          a: \"A*\",\n        },\n      }),\n      expect.objectContaining({\n        viewTransitionOpts: {\n          currentLocation: expect.objectContaining({ pathname: \"/\" }),\n          nextLocation: expect.objectContaining({ pathname: \"/a\" }),\n        },\n      }),\n    ]);\n\n    unsubscribe();\n    t.router.dispose();\n  });\n\n  it(\"preserves pending view transitions through redirects\", async () => {\n    let t = setup({\n      routes: [\n        { path: \"/\" },\n        { id: \"a\", path: \"/a\", action: true },\n        { path: \"/b\" },\n      ],\n    });\n    let spy = jest.fn();\n    let unsubscribe = t.router.subscribe(spy);\n\n    let A = await t.navigate(\"/a\", {\n      formMethod: \"post\",\n      formData: createFormData({}),\n      viewTransition: true,\n    });\n\n    await A.actions.a.redirect(\"/b\");\n    expect(spy).toHaveBeenLastCalledWith(\n      expect.objectContaining({\n        navigation: IDLE_NAVIGATION,\n        location: expect.objectContaining({ pathname: \"/b\" }),\n      }),\n      expect.objectContaining({\n        viewTransitionOpts: {\n          currentLocation: expect.objectContaining({ pathname: \"/\" }),\n          nextLocation: expect.objectContaining({ pathname: \"/b\" }),\n        },\n      }),\n    );\n\n    unsubscribe();\n    t.router.dispose();\n  });\n});\n"
  },
  {
    "path": "packages/react-router/__tests__/same-component-lifecycle-test.tsx",
    "content": "import * as React from \"react\";\nimport * as TestRenderer from \"react-test-renderer\";\nimport { MemoryRouter, Routes, Route } from \"react-router\";\n\ndescribe(\"when the same component is mounted by two different routes\", () => {\n  it(\"mounts only once\", () => {\n    let mountCount = 0;\n\n    class Home extends React.Component {\n      componentDidMount() {\n        mountCount += 1;\n      }\n      render() {\n        return <h1>Home</h1>;\n      }\n    }\n\n    let renderer: TestRenderer.ReactTestRenderer;\n    TestRenderer.act(() => {\n      renderer = TestRenderer.create(\n        <MemoryRouter initialEntries={[\"/home\"]}>\n          <Routes>\n            <Route path=\"home\" element={<Home />} />\n            <Route path=\"another-home\" element={<Home />} />\n          </Routes>\n        </MemoryRouter>,\n      );\n    });\n\n    expect(renderer.toJSON()).toMatchInlineSnapshot(`\n      <h1>\n        Home\n      </h1>\n    `);\n\n    expect(mountCount).toBe(1);\n\n    TestRenderer.act(() => {\n      renderer.update(\n        <MemoryRouter initialEntries={[\"/another-home\"]}>\n          <Routes>\n            <Route path=\"home\" element={<Home />} />\n            <Route path=\"another-home\" element={<Home />} />\n          </Routes>\n        </MemoryRouter>,\n      );\n    });\n\n    expect(renderer.toJSON()).toMatchInlineSnapshot(`\n      <h1>\n        Home\n      </h1>\n    `);\n\n    expect(mountCount).toBe(1);\n  });\n});\n"
  },
  {
    "path": "packages/react-router/__tests__/server-runtime/actions-test.ts",
    "content": "/**\n * @jest-environment node\n */\n\nimport { throwIfPotentialCSRFAttack } from \"../../lib/actions\";\n\ndescribe(\"throwIfPotentialCSRFAttack\", () => {\n  describe(\"when origin matches host\", () => {\n    it(\"should not throw when origin matches host header\", () => {\n      const headers = new Headers({\n        origin: \"https://example.com\",\n        host: \"example.com\",\n      });\n      expect(() =>\n        throwIfPotentialCSRFAttack(headers, undefined),\n      ).not.toThrow();\n    });\n\n    it(\"should not throw when origin matches x-forwarded-host header\", () => {\n      const headers = new Headers({\n        origin: \"https://example.com\",\n        \"x-forwarded-host\": \"example.com\",\n      });\n      expect(() =>\n        throwIfPotentialCSRFAttack(headers, undefined),\n      ).not.toThrow();\n    });\n\n    it(\"should prefer x-forwarded-host over host header\", () => {\n      const headers = new Headers({\n        origin: \"https://example.com\",\n        \"x-forwarded-host\": \"example.com\",\n        host: \"different.com\",\n      });\n      expect(() =>\n        throwIfPotentialCSRFAttack(headers, undefined),\n      ).not.toThrow();\n    });\n\n    it(\"should use first value from comma-separated x-forwarded-host\", () => {\n      const headers = new Headers({\n        origin: \"https://example.com\",\n        \"x-forwarded-host\": \"example.com, other.com, another.com\",\n      });\n      expect(() =>\n        throwIfPotentialCSRFAttack(headers, undefined),\n      ).not.toThrow();\n    });\n  });\n\n  describe(\"when origin does not match host\", () => {\n    it(\"should throw when origin does not match host header\", () => {\n      const headers = new Headers({\n        origin: \"https://untrusted.com\",\n        host: \"example.com\",\n      });\n      expect(() => throwIfPotentialCSRFAttack(headers, undefined)).toThrow(\n        \"host header does not match `origin` header from a forwarded action request\",\n      );\n    });\n\n    it(\"should throw when origin does not match x-forwarded-host header\", () => {\n      const headers = new Headers({\n        origin: \"https://untrusted.com\",\n        \"x-forwarded-host\": \"example.com\",\n      });\n      expect(() => throwIfPotentialCSRFAttack(headers, undefined)).toThrow(\n        \"x-forwarded-host header does not match `origin` header from a forwarded action request\",\n      );\n    });\n\n    it(\"should throw when origin is present but host headers are missing\", () => {\n      const headers = new Headers({\n        origin: \"https://untrusted.com\",\n      });\n      expect(() => throwIfPotentialCSRFAttack(headers, undefined)).toThrow(\n        \"`x-forwarded-host` or `host` headers are not provided\",\n      );\n    });\n  });\n\n  describe(\"with allowed origins\", () => {\n    it(\"should not throw when origin matches an allowed origin exactly\", () => {\n      const headers = new Headers({\n        origin: \"https://trusted.com\",\n        host: \"example.com\",\n      });\n      expect(() =>\n        throwIfPotentialCSRFAttack(headers, [\"trusted.com\"]),\n      ).not.toThrow();\n    });\n\n    it(\"should not throw when origin matches a wildcard pattern\", () => {\n      const headers = new Headers({\n        origin: \"https://sub.trusted.com\",\n        host: \"example.com\",\n      });\n      expect(() =>\n        throwIfPotentialCSRFAttack(headers, [\"*.trusted.com\"]),\n      ).not.toThrow();\n    });\n\n    it(\"should not throw when origin matches a multi-level wildcard pattern\", () => {\n      const headers = new Headers({\n        origin: \"https://sub.domain.trusted.com\",\n        host: \"example.com\",\n      });\n      expect(() =>\n        throwIfPotentialCSRFAttack(headers, [\"**.trusted.com\"]),\n      ).not.toThrow();\n    });\n\n    it(\"should throw when origin does not match any allowed origin\", () => {\n      const headers = new Headers({\n        origin: \"https://untrusted.com\",\n        host: \"example.com\",\n      });\n      expect(() =>\n        throwIfPotentialCSRFAttack(headers, [\"trusted.com\", \"*.safe.com\"]),\n      ).toThrow(\n        \"host header does not match `origin` header from a forwarded action request\",\n      );\n    });\n\n    it(\"should handle multiple allowed origins\", () => {\n      const headers = new Headers({\n        origin: \"https://partner2.com\",\n        host: \"example.com\",\n      });\n      expect(() =>\n        throwIfPotentialCSRFAttack(headers, [\n          \"partner1.com\",\n          \"partner2.com\",\n          \"*.trusted.com\",\n        ]),\n      ).not.toThrow();\n    });\n  });\n\n  describe(\"edge cases\", () => {\n    it(\"should not throw when origin is not present\", () => {\n      const headers = new Headers({\n        host: \"example.com\",\n      });\n      expect(() =>\n        throwIfPotentialCSRFAttack(headers, undefined),\n      ).not.toThrow();\n    });\n\n    it(\"should throw when origin is null string without host\", () => {\n      const headers = new Headers({\n        origin: \"null\",\n      });\n      expect(() => throwIfPotentialCSRFAttack(headers, undefined)).toThrow(\n        \"`x-forwarded-host` or `host` headers are not provided\",\n      );\n    });\n\n    it(\"should throw when origin is not a valid URL\", () => {\n      let invalidHeaders = [\n        \"not-a-valid-url\",\n        \"ht!tp://example.com\",\n        \"http://exam ple.com\",\n        \"example.com\",\n        \"\",\n        \"    \",\n      ];\n\n      for (const invalidHeader of invalidHeaders) {\n        const headers = new Headers({\n          origin: invalidHeader,\n          host: \"example.com\",\n        });\n        expect(() => throwIfPotentialCSRFAttack(headers, undefined)).toThrow(\n          \"`origin` header is not a valid URL. Aborting the action.\",\n        );\n      }\n    });\n\n    it(\"should handle origin with port number\", () => {\n      const headers = new Headers({\n        origin: \"https://example.com:8080\",\n        host: \"example.com:8080\",\n      });\n      expect(() =>\n        throwIfPotentialCSRFAttack(headers, undefined),\n      ).not.toThrow();\n    });\n\n    it(\"should throw when origin port differs from host port\", () => {\n      const headers = new Headers({\n        origin: \"https://example.com:8080\",\n        host: \"example.com:3000\",\n      });\n      expect(() => throwIfPotentialCSRFAttack(headers, undefined)).toThrow(\n        \"host header does not match `origin` header from a forwarded action request\",\n      );\n    });\n\n    it(\"should handle x-forwarded-host with whitespace\", () => {\n      const headers = new Headers({\n        origin: \"https://example.com\",\n        \"x-forwarded-host\": \"  example.com  \",\n      });\n      expect(() =>\n        throwIfPotentialCSRFAttack(headers, undefined),\n      ).not.toThrow();\n    });\n\n    it(\"should ignore empty string in allowed origins\", () => {\n      const headers = new Headers({\n        origin: \"https://different.com\",\n        host: \"example.com\",\n      });\n      expect(() =>\n        throwIfPotentialCSRFAttack(headers, [\"\", \"other.com\"]),\n      ).toThrow(\n        \"host header does not match `origin` header from a forwarded action request\",\n      );\n    });\n\n    it(\"should throw when origin is null string but has matching host\", () => {\n      const headers = new Headers({\n        origin: \"null\",\n        host: \"null\",\n      });\n      expect(() =>\n        throwIfPotentialCSRFAttack(headers, undefined),\n      ).not.toThrow();\n    });\n\n    it(\"should handle subdomain in origin vs base domain in host\", () => {\n      const headers = new Headers({\n        origin: \"https://api.example.com\",\n        host: \"example.com\",\n      });\n      expect(() => throwIfPotentialCSRFAttack(headers, undefined)).toThrow(\n        \"host header does not match `origin` header from a forwarded action request\",\n      );\n    });\n\n    it(\"should not throw when wildcard allows subdomain\", () => {\n      const headers = new Headers({\n        origin: \"https://api.example.com\",\n        host: \"main.com\",\n      });\n      expect(() =>\n        throwIfPotentialCSRFAttack(headers, [\"*.example.com\"]),\n      ).not.toThrow();\n    });\n\n    it(\"should throw on * wildcard patterns because they only match one segment\", () => {\n      const headers = new Headers({\n        origin: \"https://different.com\",\n        host: \"example.com\",\n      });\n      expect(() => throwIfPotentialCSRFAttack(headers, [\"*\"])).toThrow(\n        \"host header does not match `origin` header from a forwarded action request\",\n      );\n    });\n\n    it(\"** should match anything\", () => {\n      const headers = new Headers({\n        origin: \"https://different.com\",\n        host: \"example.com\",\n      });\n      expect(() => throwIfPotentialCSRFAttack(headers, [\"**\"])).not.toThrow();\n    });\n  });\n});\n"
  },
  {
    "path": "packages/react-router/__tests__/server-runtime/cookies-test.ts",
    "content": "/**\n * @jest-environment node\n */\n\nimport { createCookie, isCookie } from \"../../lib/server-runtime/cookies\";\n\nfunction getCookieFromSetCookie(setCookie: string): string {\n  return setCookie.split(/;\\s*/)[0];\n}\n\ndescribe(\"isCookie\", () => {\n  it(\"returns `true` for Cookie objects\", () => {\n    expect(isCookie(createCookie(\"my-cookie\"))).toBe(true);\n  });\n\n  it(\"returns `false` for non-Cookie objects\", () => {\n    expect(isCookie({})).toBe(false);\n    expect(isCookie([])).toBe(false);\n    expect(isCookie(\"\")).toBe(false);\n    expect(isCookie(true)).toBe(false);\n  });\n});\n\ndescribe(\"cookies\", () => {\n  it(\"parses/serializes empty string values\", async () => {\n    let cookie = createCookie(\"my-cookie\");\n    let setCookie = await cookie.serialize(\"\");\n    let value = await cookie.parse(getCookieFromSetCookie(setCookie));\n\n    expect(value).toMatchInlineSnapshot(`\"\"`);\n  });\n\n  it(\"parses/serializes unsigned string values\", async () => {\n    let cookie = createCookie(\"my-cookie\");\n    let setCookie = await cookie.serialize(\"hello world\");\n    let value = await cookie.parse(getCookieFromSetCookie(setCookie));\n\n    expect(value).toEqual(\"hello world\");\n  });\n\n  it(\"parses/serializes unsigned boolean values\", async () => {\n    let cookie = createCookie(\"my-cookie\");\n    let setCookie = await cookie.serialize(true);\n    let value = await cookie.parse(getCookieFromSetCookie(setCookie));\n\n    expect(value).toBe(true);\n  });\n\n  it(\"parses/serializes signed string values\", async () => {\n    let cookie = createCookie(\"my-cookie\", {\n      secrets: [\"secret1\"],\n    });\n    let setCookie = await cookie.serialize(\"hello michael\");\n    let value = await cookie.parse(getCookieFromSetCookie(setCookie));\n\n    expect(value).toMatchInlineSnapshot(`\"hello michael\"`);\n  });\n\n  it(\"parses/serializes string values containing utf8 characters\", async () => {\n    let cookie = createCookie(\"my-cookie\");\n    let setCookie = await cookie.serialize(\"日本語\");\n    let value = await cookie.parse(getCookieFromSetCookie(setCookie));\n\n    expect(value).toBe(\"日本語\");\n  });\n\n  it(\"fails to parses signed string values with invalid signature\", async () => {\n    let cookie = createCookie(\"my-cookie\", {\n      secrets: [\"secret1\"],\n    });\n    let setCookie = await cookie.serialize(\"hello michael\");\n    let cookie2 = createCookie(\"my-cookie\", {\n      secrets: [\"secret2\"],\n    });\n    let value = await cookie2.parse(getCookieFromSetCookie(setCookie));\n\n    expect(value).toBe(null);\n  });\n\n  it(\"fails to parse signed string values with invalid signature encoding\", async () => {\n    let cookie = createCookie(\"my-cookie\", {\n      secrets: [\"secret1\"],\n    });\n    let setCookie = await cookie.serialize(\"hello michael\");\n    let cookie2 = createCookie(\"my-cookie\", {\n      secrets: [\"secret2\"],\n    });\n    // use characters that are invalid for base64 encoding\n    let value = await cookie2.parse(getCookieFromSetCookie(setCookie) + \"%^&\");\n\n    expect(value).toBe(null);\n  });\n\n  it(\"parses/serializes signed object values\", async () => {\n    let cookie = createCookie(\"my-cookie\", {\n      secrets: [\"secret1\"],\n    });\n    let setCookie = await cookie.serialize({ hello: \"mjackson\" });\n    let value = await cookie.parse(getCookieFromSetCookie(setCookie));\n\n    expect(value).toMatchInlineSnapshot(`\n      {\n        \"hello\": \"mjackson\",\n      }\n    `);\n  });\n\n  it(\"fails to parse signed object values with invalid signature\", async () => {\n    let cookie = createCookie(\"my-cookie\", {\n      secrets: [\"secret1\"],\n    });\n    let setCookie = await cookie.serialize({ hello: \"mjackson\" });\n    let cookie2 = createCookie(\"my-cookie\", {\n      secrets: [\"secret2\"],\n    });\n    let value = await cookie2.parse(getCookieFromSetCookie(setCookie));\n\n    expect(value).toBeNull();\n  });\n\n  it(\"supports secret rotation\", async () => {\n    let cookie = createCookie(\"my-cookie\", {\n      secrets: [\"secret1\"],\n    });\n    let setCookie = await cookie.serialize({ hello: \"mjackson\" });\n    let value = await cookie.parse(getCookieFromSetCookie(setCookie));\n\n    expect(value).toMatchInlineSnapshot(`\n      {\n        \"hello\": \"mjackson\",\n      }\n    `);\n\n    // A new secret enters the rotation...\n    cookie = createCookie(\"my-cookie\", {\n      secrets: [\"secret2\", \"secret1\"],\n    });\n\n    // cookie should still be able to parse old cookies.\n    let oldValue = await cookie.parse(getCookieFromSetCookie(setCookie));\n    expect(oldValue).toMatchObject(value);\n\n    // New Set-Cookie should be different, it uses a different secret.\n    let setCookie2 = await cookie.serialize(value);\n    expect(setCookie).not.toEqual(setCookie2);\n  });\n\n  it(\"makes the default secrets to be an empty array\", async () => {\n    let cookie = createCookie(\"my-cookie\");\n\n    expect(cookie.isSigned).toBe(false);\n\n    let cookie2 = createCookie(\"my-cookie2\", {\n      secrets: undefined,\n    });\n\n    expect(cookie2.isSigned).toBe(false);\n  });\n\n  it(\"makes the default path of cookies to be /\", async () => {\n    let cookie = createCookie(\"my-cookie\");\n\n    let setCookie = await cookie.serialize(\"hello world\");\n    expect(setCookie).toContain(\"Path=/\");\n\n    let cookie2 = createCookie(\"my-cookie2\");\n\n    let setCookie2 = await cookie2.serialize(\"hello world\", {\n      path: \"/about\",\n    });\n    expect(setCookie2).toContain(\"Path=/about\");\n  });\n\n  it(\"supports the Priority attribute\", async () => {\n    let cookie = createCookie(\"my-cookie\");\n\n    let setCookie = await cookie.serialize(\"hello world\");\n    expect(setCookie).not.toContain(\"Priority\");\n\n    let cookie2 = createCookie(\"my-cookie2\");\n\n    let setCookie2 = await cookie2.serialize(\"hello world\", {\n      priority: \"high\",\n    });\n    expect(setCookie2).toContain(\"Priority=High\");\n  });\n\n  describe(\"warnings when providing options you may not want to\", () => {\n    let spy = spyConsole();\n\n    it(\"warns against using `expires` when creating the cookie instance\", async () => {\n      createCookie(\"my-cookie\", { expires: new Date(Date.now() + 60_000) });\n      expect(spy.console).toHaveBeenCalledTimes(1);\n      expect(spy.console).toHaveBeenCalledWith(\n        'The \"my-cookie\" cookie has an \"expires\" property set. This will cause the expires value to not be updated when the session is committed. Instead, you should set the expires value when serializing the cookie. You can use `commitSession(session, { expires })` if using a session storage object, or `cookie.serialize(\"value\", { expires })` if you\\'re using the cookie directly.',\n      );\n    });\n  });\n});\n\nfunction spyConsole() {\n  // https://github.com/facebook/react/issues/7047\n  let spy: any = {};\n\n  beforeAll(() => {\n    spy.console = jest.spyOn(console, \"warn\").mockImplementation(() => {});\n  });\n\n  beforeEach(() => {\n    spy.console.mockClear();\n  });\n\n  afterAll(() => {\n    spy.console.mockRestore();\n  });\n\n  return spy;\n}\n"
  },
  {
    "path": "packages/react-router/__tests__/server-runtime/data-test.ts",
    "content": "import { decodeViaTurboStream } from \"../../lib/dom/ssr/single-fetch\";\nimport { createRequestHandler } from \"../../lib/server-runtime/server\";\nimport { mockServerBuild } from \"./utils\";\n\ndescribe(\"loaders\", () => {\n  // so that HTML/Fetch requests are the same, and so redirects don't hang on to\n  // this param for no reason\n  it(\"removes .data from request.url\", async () => {\n    let loader = async ({ request }) => {\n      return new URL(request.url).pathname;\n    };\n\n    let routeId = \"routes/random\";\n    let build = mockServerBuild({\n      [routeId]: {\n        path: \"/random\",\n        default: {},\n        loader,\n      },\n    });\n\n    let handler = createRequestHandler(build);\n\n    let request = new Request(\"http://example.com/random.data\", {\n      headers: {\n        \"Content-Type\": \"application/json\",\n      },\n    });\n\n    let res = await handler(request);\n    if (!res.body) throw new Error(\"No body\");\n    const decoded = await decodeViaTurboStream(res.body, global);\n    expect((decoded.value as any)[routeId].data).toEqual(\"/random\");\n  });\n});\n"
  },
  {
    "path": "packages/react-router/__tests__/server-runtime/handle-error-test.ts",
    "content": "import { createRequestHandler } from \"../../lib/server-runtime/server\";\nimport { ErrorResponseImpl } from \"../../lib/router/utils\";\nimport { mockServerBuild } from \"./utils\";\nimport type { HandleDocumentRequestFunction } from \"../../lib/server-runtime/build\";\n\nfunction getHandler(\n  routeModule = {},\n  opts: {\n    handleDocumentRequest?: HandleDocumentRequestFunction;\n  } = {},\n) {\n  let handleErrorSpy = jest.fn();\n  let build = mockServerBuild(\n    {\n      root: {\n        path: \"/\",\n        default() {},\n        ...routeModule,\n      },\n    },\n    {\n      handleError: handleErrorSpy,\n      handleDocumentRequest: opts.handleDocumentRequest,\n    },\n  );\n\n  return {\n    handler: createRequestHandler(build),\n    handleErrorSpy,\n  };\n}\n\ndescribe(\"handleError\", () => {\n  describe(\"document request\", () => {\n    it(\"provides user-thrown Error\", async () => {\n      let error = new Error(\"💥\");\n      let { handler, handleErrorSpy } = getHandler({\n        loader() {\n          throw error;\n        },\n      });\n      let request = new Request(\"http://example.com/\");\n      await handler(request);\n      expect(handleErrorSpy).toHaveBeenCalledWith(error, {\n        request,\n        params: {},\n        context: {},\n      });\n    });\n\n    it(\"provides router-thrown ErrorResponse\", async () => {\n      let { handler, handleErrorSpy } = getHandler({});\n      let request = new Request(\"http://example.com/\", { method: \"post\" });\n      await handler(request);\n      expect(handleErrorSpy).toHaveBeenCalledWith(\n        new ErrorResponseImpl(\n          405,\n          \"Method Not Allowed\",\n          new Error(\n            'You made a POST request to \"/\" but did not provide an `action` for route \"root\", so there is no way to handle the request.',\n          ),\n          true,\n        ),\n        {\n          request,\n          params: {},\n          context: {},\n        },\n      );\n    });\n\n    it(\"provides render-thrown Error\", async () => {\n      let { handler, handleErrorSpy } = getHandler(undefined, {\n        handleDocumentRequest() {\n          throw new Error(\"Render error\");\n        },\n      });\n      let request = new Request(\"http://example.com/\");\n      await handler(request);\n      expect(handleErrorSpy).toHaveBeenCalledWith(new Error(\"Render error\"), {\n        request,\n        params: {},\n        context: {},\n      });\n    });\n\n    it(\"does not provide user-thrown Responses to handleError\", async () => {\n      let { handler, handleErrorSpy } = getHandler({\n        loader() {\n          throw Response.json(\n            { message: \"not found\" },\n            { status: 404, statusText: \"Not Found\" },\n          );\n        },\n      });\n      let request = new Request(\"http://example.com/\");\n      await handler(request);\n      expect(handleErrorSpy).not.toHaveBeenCalled();\n    });\n  });\n\n  describe(\"data request\", () => {\n    it(\"provides user-thrown Error\", async () => {\n      let error = new Error(\"💥\");\n      let { handler, handleErrorSpy } = getHandler({\n        loader() {\n          throw error;\n        },\n      });\n      let request = new Request(\"http://example.com/_root.data\");\n      await handler(request);\n      expect(handleErrorSpy).toHaveBeenCalledWith(error, {\n        request,\n        params: {},\n        context: {},\n      });\n    });\n\n    it(\"provides router-thrown ErrorResponse\", async () => {\n      let { handler, handleErrorSpy } = getHandler({});\n      let request = new Request(\"http://example.com/_root.data\", {\n        method: \"post\",\n      });\n      await handler(request);\n      expect(handleErrorSpy).toHaveBeenCalledWith(\n        new ErrorResponseImpl(\n          405,\n          \"Method Not Allowed\",\n          new Error(\n            'You made a POST request to \"/\" but did not provide an `action` for route \"root\", so there is no way to handle the request.',\n          ),\n          true,\n        ),\n        {\n          request,\n          params: {},\n          context: {},\n        },\n      );\n    });\n\n    it(\"does not provide user-thrown Responses to handleError\", async () => {\n      let { handler, handleErrorSpy } = getHandler({\n        loader() {\n          throw Response.json(\n            { message: \"not found\" },\n            { status: 404, statusText: \"Not Found\" },\n          );\n        },\n      });\n      let request = new Request(\"http://example.com/_root.data\");\n      await handler(request);\n      expect(handleErrorSpy).not.toHaveBeenCalled();\n    });\n\n    it(\"provides proper params to handleError\", async () => {\n      let error = new Error(\"💥\");\n\n      let handleErrorSpy = jest.fn();\n      let build = mockServerBuild(\n        {\n          root: {\n            path: \"\",\n            default() {\n              return null;\n            },\n          },\n          param: {\n            path: \":param\",\n            parentId: \"root\",\n            default() {\n              return null;\n            },\n            loader() {\n              throw error;\n            },\n          },\n        },\n        {\n          handleError: handleErrorSpy,\n        },\n      );\n\n      let handler = createRequestHandler(build);\n      let request = new Request(\"http://example.com/a.data\");\n      await handler(request);\n      expect(handleErrorSpy).toHaveBeenCalledWith(error, {\n        request,\n        params: { param: \"a\" },\n        context: {},\n      });\n    });\n  });\n\n  describe(\"resource request\", () => {\n    it(\"provides user-thrown Error\", async () => {\n      let error = new Error(\"💥\");\n      let { handler, handleErrorSpy } = getHandler({\n        loader() {\n          throw error;\n        },\n        default: null,\n      });\n      let request = new Request(\"http://example.com/\");\n      await handler(request);\n      expect(handleErrorSpy).toHaveBeenCalledWith(error, {\n        request,\n        params: {},\n        context: {},\n      });\n    });\n\n    it(\"provides router-thrown ErrorResponse\", async () => {\n      let { handler, handleErrorSpy } = getHandler({ default: null });\n      let request = new Request(\"http://example.com/\", {\n        method: \"post\",\n      });\n      await handler(request);\n      expect(handleErrorSpy).toHaveBeenCalledWith(\n        new ErrorResponseImpl(\n          405,\n          \"Method Not Allowed\",\n          new Error(\n            'You made a POST request to \"/\" but did not provide an `action` for route \"root\", so there is no way to handle the request.',\n          ),\n          true,\n        ),\n        {\n          request,\n          params: {},\n          context: {},\n        },\n      );\n    });\n\n    it(\"does not provide user-thrown Responses to handleError\", async () => {\n      let { handler, handleErrorSpy } = getHandler({\n        loader() {\n          throw Response.json(\n            { message: \"not found\" },\n            { status: 404, statusText: \"Not Found\" },\n          );\n        },\n        default: null,\n      });\n      let request = new Request(\"http://example.com/\");\n      await handler(request);\n      expect(handleErrorSpy).not.toHaveBeenCalled();\n    });\n  });\n});\n\n// let request = new Request(\n//   \"http://example.com/random?_data=routes/random&foo=bar\",\n//   {\n//     method: \"post\",\n//     // headers: {\n//     //   \"Content-Type\": \"application/json\",\n//     // },\n//   }\n// );\n"
  },
  {
    "path": "packages/react-router/__tests__/server-runtime/handler-test.ts",
    "content": "import { createRequestHandler } from \"../../lib/server-runtime/server\";\nimport { mockServerBuild } from \"./utils\";\n\ndescribe(\"createRequestHandler\", () => {\n  it(\"retains request headers when stripping body off for loaders\", async () => {\n    let build = mockServerBuild({\n      root: {\n        path: \"/test\",\n        loader: ({ request }) => Response.json(request.headers.get(\"X-Foo\")),\n      },\n    });\n    let handler = createRequestHandler(build);\n\n    let response = await handler(\n      new Request(\"http://.../test\", {\n        headers: {\n          \"X-Foo\": \"bar\",\n        },\n        signal: new AbortController().signal,\n      }),\n    );\n\n    expect(await response.json()).toBe(\"bar\");\n  });\n});\n"
  },
  {
    "path": "packages/react-router/__tests__/server-runtime/markup-test.ts",
    "content": "import vm from \"vm\";\n\nimport { escapeHtml } from \"../../lib/dom/ssr/markup\";\n\ndescribe(\"escapeHtml\", () => {\n  // These tests are based on https://github.com/zertosh/htmlescape/blob/3e6cf0614dd0f778fd0131e69070b77282150c15/test/htmlescape-test.js\n  // License: https://github.com/zertosh/htmlescape/blob/0527ca7156a524d256101bb310a9f970f63078ad/LICENSE\n\n  test(\"with angle brackets should escape\", () => {\n    let evilObj = { evil: \"<script></script>\" };\n    expect(escapeHtml(JSON.stringify(evilObj))).toBe(\n      '{\"evil\":\"\\\\u003cscript\\\\u003e\\\\u003c/script\\\\u003e\"}',\n    );\n  });\n\n  test(\"with angle brackets should parse back\", () => {\n    let evilObj = { evil: \"<script></script>\" };\n    expect(JSON.parse(escapeHtml(JSON.stringify(evilObj)))).toMatchObject(\n      evilObj,\n    );\n  });\n\n  test(\"with ampersands should escape\", () => {\n    let evilObj = { evil: \"&\" };\n    expect(escapeHtml(JSON.stringify(evilObj))).toBe('{\"evil\":\"\\\\u0026\"}');\n  });\n\n  test(\"with ampersands should parse back\", () => {\n    let evilObj = { evil: \"&\" };\n    expect(JSON.parse(escapeHtml(JSON.stringify(evilObj)))).toMatchObject(\n      evilObj,\n    );\n  });\n\n  test('with \"LINE SEPARATOR\" and \"PARAGRAPH SEPARATOR\" should escape', () => {\n    let evilObj = { evil: \"\\u2028\\u2029\" };\n    expect(escapeHtml(JSON.stringify(evilObj))).toBe(\n      '{\"evil\":\"\\\\u2028\\\\u2029\"}',\n    );\n  });\n\n  test('with \"LINE SEPARATOR\" and \"PARAGRAPH SEPARATOR\" should parse back', () => {\n    let evilObj = { evil: \"\\u2028\\u2029\" };\n    expect(JSON.parse(escapeHtml(JSON.stringify(evilObj)))).toMatchObject(\n      evilObj,\n    );\n  });\n\n  test(\"escaped line terminators should work\", () => {\n    expect(() => {\n      vm.runInNewContext(\n        \"(\" + escapeHtml(JSON.stringify({ evil: \"\\u2028\\u2029\" })) + \")\",\n      );\n    }).not.toThrow();\n  });\n});\n"
  },
  {
    "path": "packages/react-router/__tests__/server-runtime/responses-test.ts",
    "content": "/**\n * @jest-environment node\n */\n\nimport { redirect } from \"../../lib/router/utils\";\n\ndescribe(\"json\", () => {\n  it(\"sets the Content-Type header\", () => {\n    let response = Response.json({});\n    expect(response.headers.get(\"Content-Type\")).toEqual(\"application/json\");\n  });\n\n  it(\"preserves existing headers, including Content-Type\", () => {\n    let response = Response.json(\n      {},\n      {\n        headers: {\n          \"Content-Type\": \"application/json; charset=iso-8859-1\",\n          \"X-Remix\": \"is awesome\",\n        },\n      },\n    );\n\n    expect(response.headers.get(\"Content-Type\")).toEqual(\n      \"application/json; charset=iso-8859-1\",\n    );\n    expect(response.headers.get(\"X-Remix\")).toEqual(\"is awesome\");\n  });\n\n  it(\"encodes the response body\", async () => {\n    let response = Response.json({ hello: \"remix\" });\n    expect(await response.json()).toEqual({ hello: \"remix\" });\n  });\n\n  it(\"accepts status as a second parameter\", () => {\n    let response = Response.json({}, { status: 201 });\n    expect(response.status).toEqual(201);\n  });\n\n  it(\"infers input type\", async () => {\n    let response = Response.json({ hello: \"remix\" });\n    let result = await response.json();\n    expect(result).toMatchObject({ hello: \"remix\" });\n  });\n\n  it(\"disallows unserializables\", () => {\n    // @ts-expect-error\n    expect(() => Response.json(124n)).toThrow();\n    // @ts-expect-error\n    expect(() => Response.json({ field: 124n })).toThrow();\n  });\n});\n\ndescribe(\"redirect\", () => {\n  it(\"sets the status to 302 by default\", () => {\n    let response = redirect(\"/login\");\n    expect(response.status).toEqual(302);\n  });\n\n  it(\"sets the status to 302 when only headers are given\", () => {\n    let response = redirect(\"/login\", {\n      headers: {\n        \"X-Remix\": \"is awesome\",\n      },\n    });\n    expect(response.status).toEqual(302);\n  });\n\n  it(\"sets the Location header\", () => {\n    let response = redirect(\"/login\");\n    expect(response.headers.get(\"Location\")).toEqual(\"/login\");\n  });\n\n  it(\"preserves existing headers, but not Location\", () => {\n    let response = redirect(\"/login\", {\n      headers: {\n        Location: \"/\",\n        \"X-Remix\": \"is awesome\",\n      },\n    });\n\n    expect(response.headers.get(\"Location\")).toEqual(\"/login\");\n    expect(response.headers.get(\"X-Remix\")).toEqual(\"is awesome\");\n  });\n\n  it(\"accepts status as a second parameter\", () => {\n    let response = redirect(\"/profile\", 301);\n    expect(response.status).toEqual(301);\n  });\n});\n"
  },
  {
    "path": "packages/react-router/__tests__/server-runtime/server-test.ts",
    "content": "/**\n * @jest-environment node\n */\n\nimport { createContext, type StaticHandlerContext } from \"react-router\";\n\nimport { createRequestHandler } from \"../../lib/server-runtime/server\";\nimport { ServerMode } from \"../../lib/server-runtime/mode\";\nimport { mockServerBuild } from \"./utils\";\n\nfunction spyConsole() {\n  // https://github.com/facebook/react/issues/7047\n  let spy: any = {};\n\n  beforeAll(() => {\n    spy.console = jest.spyOn(console, \"error\").mockImplementation(() => {});\n  });\n\n  afterAll(() => {\n    spy.console.mockRestore();\n  });\n\n  return spy;\n}\n\ndescribe(\"server\", () => {\n  let routeId = \"root\";\n  let build = mockServerBuild(\n    {\n      [routeId]: {\n        path: \"\",\n        action: ({ request }) =>\n          new Response(`${request.method} ${request.url} ACTION`),\n        loader: ({ request }) =>\n          new Response(`${request.method} ${request.url} LOADER`),\n        default: () => \"COMPONENT\",\n      },\n    },\n    {\n      handleDocumentRequest(request) {\n        return new Response(`${request.method}, ${request.url} COMPONENT`);\n      },\n    },\n  );\n\n  describe(\"createRequestHandler\", () => {\n    let spy = spyConsole();\n\n    beforeEach(() => {\n      spy.console.mockClear();\n    });\n\n    let allowThrough = [\n      [\"GET\", \"/\", \"COMPONENT\"],\n      [\"GET\", \"/_root.data\", \"LOADER\"],\n      [\"POST\", \"/\", \"COMPONENT\"],\n      [\"POST\", \"/_root.data\", \"ACTION\"],\n      [\"PUT\", \"/\", \"COMPONENT\"],\n      [\"PUT\", \"/_root.data\", \"ACTION\"],\n      [\"DELETE\", \"/\", \"COMPONENT\"],\n      [\"DELETE\", \"/_root.data\", \"ACTION\"],\n      [\"PATCH\", \"/\", \"COMPONENT\"],\n      [\"PATCH\", \"/_root.data\", \"ACTION\"],\n    ];\n    it.each(allowThrough)(\n      `allows through %s request to %s`,\n      async (method, to, expected) => {\n        let handler = createRequestHandler(build);\n        let response = await handler(\n          new Request(`http://localhost:3000${to}`, {\n            method,\n          }),\n        );\n\n        expect(response.status).toBe(200);\n        let text = await response.text();\n        expect(text).toContain(method);\n        expect(text).toContain(expected);\n        expect(spy.console).not.toHaveBeenCalled();\n      },\n    );\n\n    it(\"strips body for HEAD requests\", async () => {\n      let handler = createRequestHandler(build);\n      let response = await handler(\n        new Request(\"http://localhost:3000/\", {\n          method: \"HEAD\",\n        }),\n      );\n\n      expect(await response.text()).toBe(\"\");\n    });\n\n    it(\"accepts proper values from getLoadContext (without middleware)\", async () => {\n      let build = mockServerBuild(\n        {\n          root: {\n            path: \"\",\n            loader: ({ context }) => context.foo,\n            default: () => \"COMPONENT\",\n          },\n        },\n        {\n          handleDocumentRequest(request) {\n            return new Response(`${request.method}, ${request.url} COMPONENT`);\n          },\n        },\n      );\n      let handler = createRequestHandler(build);\n      let response = await handler(\n        new Request(\"http://localhost:3000/_root.data\"),\n        {\n          foo: \"FOO\",\n        },\n      );\n\n      expect(await response.text()).toContain(\"FOO\");\n    });\n\n    it(\"accepts proper values from getLoadContext (with middleware)\", async () => {\n      let fooContext = createContext<string>();\n      let build = mockServerBuild(\n        {\n          root: {\n            path: \"\",\n            loader: ({ context }) => context.get(fooContext),\n            default: () => \"COMPONENT\",\n          },\n        },\n        {\n          handleDocumentRequest(request) {\n            return new Response(`${request.method}, ${request.url} COMPONENT`);\n          },\n        },\n      );\n      let handler = createRequestHandler(build);\n      let response = await handler(\n        new Request(\"http://localhost:3000/_root.data\"),\n        // @ts-expect-error In apps the expected type is handled via the Future interface\n        new Map([[fooContext, \"FOO\"]]),\n      );\n\n      expect(await response.text()).toContain(\"FOO\");\n    });\n\n    it(\"errors if an invalid value is returned from getLoadContext (with middleware)\", async () => {\n      let handleErrorSpy = jest.fn();\n      let build = mockServerBuild(\n        {\n          root: {\n            path: \"\",\n            loader: ({ context }) => context.foo,\n            default: () => \"COMPONENT\",\n          },\n        },\n        {\n          future: {\n            v8_middleware: true,\n          },\n          handleError: handleErrorSpy,\n          handleDocumentRequest(request) {\n            return new Response(`${request.method}, ${request.url} COMPONENT`);\n          },\n        },\n      );\n      let handler = createRequestHandler(build);\n      let response = await handler(\n        new Request(\"http://localhost:3000/_root.data\"),\n        {\n          foo: \"FOO\",\n        },\n      );\n\n      expect(response.status).toBe(500);\n      expect(await response.text()).toContain(\"Unexpected Server Error\");\n      expect(handleErrorSpy).toHaveBeenCalledTimes(1);\n      expect(handleErrorSpy.mock.calls[0][0].message).toBe(\n        \"Invalid `context` value provided to `handleRequest`. When middleware is \" +\n          \"enabled you must return an instance of `RouterContextProvider` \" +\n          \"from your `getLoadContext` function.\",\n      );\n      handleErrorSpy.mockRestore();\n    });\n  });\n});\n\ndescribe(\"shared server runtime\", () => {\n  let spy = spyConsole();\n\n  beforeEach(() => {\n    spy.console.mockClear();\n  });\n\n  let baseUrl = \"http://test.com\";\n\n  describe(\"resource routes\", () => {\n    test(\"calls resource route loader\", async () => {\n      let rootLoader = jest.fn(() => {\n        return \"root\";\n      });\n      let resourceLoader = jest.fn(() => {\n        return Response.json(\"resource\");\n      });\n      let build = mockServerBuild({\n        root: {\n          default: {},\n          loader: rootLoader,\n        },\n        \"routes/resource\": {\n          loader: resourceLoader,\n          path: \"resource\",\n        },\n      });\n      let handler = createRequestHandler(build, ServerMode.Test);\n\n      let request = new Request(`${baseUrl}/resource`, {\n        method: \"get\",\n      });\n\n      let result = await handler(request);\n      expect(result.status).toBe(200);\n      expect(await result.json()).toBe(\"resource\");\n      expect(rootLoader.mock.calls.length).toBe(0);\n      expect(resourceLoader.mock.calls.length).toBe(1);\n    });\n\n    test(\"calls sub resource route loader\", async () => {\n      let rootLoader = jest.fn(() => {\n        return \"root\";\n      });\n      let resourceLoader = jest.fn(() => {\n        return Response.json(\"resource\");\n      });\n      let subResourceLoader = jest.fn(() => {\n        return Response.json(\"sub\");\n      });\n      let build = mockServerBuild({\n        root: {\n          default: {},\n          loader: rootLoader,\n        },\n        \"routes/resource\": {\n          loader: resourceLoader,\n          path: \"resource\",\n        },\n        \"routes/resource.sub\": {\n          loader: subResourceLoader,\n          path: \"resource/sub\",\n        },\n      });\n      let handler = createRequestHandler(build, ServerMode.Test);\n\n      let request = new Request(`${baseUrl}/resource/sub`, {\n        method: \"get\",\n      });\n\n      let result = await handler(request);\n      expect(result.status).toBe(200);\n      expect(await result.json()).toBe(\"sub\");\n      expect(rootLoader.mock.calls.length).toBe(0);\n      expect(resourceLoader.mock.calls.length).toBe(0);\n      expect(subResourceLoader.mock.calls.length).toBe(1);\n    });\n\n    test(\"resource route loader allows thrown responses\", async () => {\n      let rootLoader = jest.fn(() => {\n        return \"root\";\n      });\n      let resourceLoader = jest.fn(() => {\n        throw new Response(\"resource\");\n      });\n      let build = mockServerBuild({\n        root: {\n          default: {},\n          loader: rootLoader,\n        },\n        \"routes/resource\": {\n          loader: resourceLoader,\n          path: \"resource\",\n        },\n      });\n      let handler = createRequestHandler(build, ServerMode.Test);\n\n      let request = new Request(`${baseUrl}/resource`, {\n        method: \"get\",\n      });\n\n      let result = await handler(request);\n      expect(result.status).toBe(200);\n      expect(await result.text()).toBe(\"resource\");\n      expect(rootLoader.mock.calls.length).toBe(0);\n      expect(resourceLoader.mock.calls.length).toBe(1);\n    });\n\n    test(\"resource route loader responds with generic error when thrown\", async () => {\n      let error = new Error(\"should be logged when resource loader throws\");\n      let loader = jest.fn(() => {\n        throw error;\n      });\n      let build = mockServerBuild({\n        \"routes/resource\": {\n          loader,\n          path: \"resource\",\n        },\n      });\n      let handler = createRequestHandler(build, ServerMode.Test);\n\n      let request = new Request(`${baseUrl}/resource`, {\n        method: \"get\",\n      });\n\n      let result = await handler(request);\n      expect(await result.text()).toBe(\n        \"Unexpected Server Error\\n\\nError: should be logged when resource loader throws\",\n      );\n    });\n\n    test(\"resource route loader responds with detailed error when thrown in development\", async () => {\n      let error = new Error(\"should be logged when resource loader throws\");\n      let loader = jest.fn(() => {\n        throw error;\n      });\n      let build = mockServerBuild({\n        \"routes/resource\": {\n          loader,\n          path: \"resource\",\n        },\n      });\n      let handler = createRequestHandler(build, ServerMode.Development);\n\n      let request = new Request(`${baseUrl}/resource`, {\n        method: \"get\",\n      });\n\n      let result = await handler(request);\n      expect((await result.text()).includes(error.message)).toBe(true);\n      expect(spy.console.mock.calls.length).toBe(1);\n    });\n\n    test(\"calls resource route action\", async () => {\n      let rootAction = jest.fn(() => {\n        return \"root\";\n      });\n      let resourceAction = jest.fn(() => {\n        return Response.json(\"resource\");\n      });\n      let build = mockServerBuild({\n        root: {\n          default: {},\n          action: rootAction,\n        },\n        \"routes/resource\": {\n          action: resourceAction,\n          path: \"resource\",\n        },\n      });\n      let handler = createRequestHandler(build, ServerMode.Test);\n\n      let request = new Request(`${baseUrl}/resource`, {\n        method: \"post\",\n      });\n\n      let result = await handler(request);\n      expect(result.status).toBe(200);\n      expect(await result.json()).toBe(\"resource\");\n      expect(rootAction.mock.calls.length).toBe(0);\n      expect(resourceAction.mock.calls.length).toBe(1);\n    });\n\n    test(\"calls sub resource route action\", async () => {\n      let rootAction = jest.fn(() => {\n        return \"root\";\n      });\n      let resourceAction = jest.fn(() => {\n        return Response.json(\"resource\");\n      });\n      let subResourceAction = jest.fn(() => {\n        return Response.json(\"sub\");\n      });\n      let build = mockServerBuild({\n        root: {\n          default: {},\n          action: rootAction,\n        },\n        \"routes/resource\": {\n          action: resourceAction,\n          path: \"resource\",\n        },\n        \"routes/resource.sub\": {\n          action: subResourceAction,\n          path: \"resource/sub\",\n        },\n      });\n      let handler = createRequestHandler(build, ServerMode.Test);\n\n      let request = new Request(`${baseUrl}/resource/sub`, {\n        method: \"post\",\n      });\n\n      let result = await handler(request);\n      expect(result.status).toBe(200);\n      expect(await result.json()).toBe(\"sub\");\n      expect(rootAction.mock.calls.length).toBe(0);\n      expect(resourceAction.mock.calls.length).toBe(0);\n      expect(subResourceAction.mock.calls.length).toBe(1);\n    });\n\n    test(\"resource route action allows thrown responses\", async () => {\n      let rootAction = jest.fn(() => {\n        return \"root\";\n      });\n      let resourceAction = jest.fn(() => {\n        throw new Response(\"resource\");\n      });\n      let build = mockServerBuild({\n        root: {\n          default: {},\n          action: rootAction,\n        },\n        \"routes/resource\": {\n          action: resourceAction,\n          path: \"resource\",\n        },\n      });\n      let handler = createRequestHandler(build, ServerMode.Test);\n\n      let request = new Request(`${baseUrl}/resource`, {\n        method: \"post\",\n      });\n\n      let result = await handler(request);\n      expect(result.status).toBe(200);\n      expect(await result.text()).toBe(\"resource\");\n      expect(rootAction.mock.calls.length).toBe(0);\n      expect(resourceAction.mock.calls.length).toBe(1);\n    });\n\n    test(\"resource route action responds with generic error when thrown\", async () => {\n      let error = new Error(\"should be logged when resource loader throws\");\n      let action = jest.fn(() => {\n        throw error;\n      });\n      let build = mockServerBuild({\n        \"routes/resource\": {\n          action,\n          path: \"resource\",\n        },\n      });\n      let handler = createRequestHandler(build, ServerMode.Test);\n\n      let request = new Request(`${baseUrl}/resource`, {\n        method: \"post\",\n      });\n\n      let result = await handler(request);\n      expect(await result.text()).toBe(\n        \"Unexpected Server Error\\n\\nError: should be logged when resource loader throws\",\n      );\n    });\n\n    test(\"resource route action responds with detailed error when thrown in development\", async () => {\n      let message = \"should be logged when resource loader throws\";\n      let action = jest.fn(() => {\n        throw new Error(message);\n      });\n      let build = mockServerBuild({\n        \"routes/resource\": {\n          action,\n          path: \"resource\",\n        },\n      });\n      let handler = createRequestHandler(build, ServerMode.Development);\n\n      let request = new Request(`${baseUrl}/resource`, {\n        method: \"post\",\n      });\n\n      let result = await handler(request);\n      expect((await result.text()).includes(message)).toBe(true);\n      expect(spy.console.mock.calls.length).toBe(1);\n    });\n\n    test(\"aborts request with reason\", async () => {\n      let rootLoader = jest.fn(() => {\n        return \"root\";\n      });\n      let resourceLoader = jest.fn(async () => {\n        await new Promise((r) => setTimeout(r, 10));\n        return \"resource\";\n      });\n      let handleErrorSpy = jest.fn();\n      let build = mockServerBuild(\n        {\n          root: {\n            default: {},\n            loader: rootLoader,\n          },\n          \"routes/resource\": {\n            loader: resourceLoader,\n            path: \"resource\",\n          },\n        },\n        {\n          handleError: handleErrorSpy,\n        },\n      );\n      let handler = createRequestHandler(build, ServerMode.Test);\n\n      let controller = new AbortController();\n      let request = new Request(`${baseUrl}/resource`, {\n        method: \"get\",\n        signal: controller.signal,\n      });\n\n      let resultPromise = handler(request);\n      controller.abort();\n      let result = await resultPromise;\n      expect(result.status).toBe(500);\n      expect(await result.text()).toMatchInlineSnapshot(`\n        \"Unexpected Server Error\n\n        AbortError: This operation was aborted\"\n      `);\n      expect(handleErrorSpy).toHaveBeenCalledTimes(1);\n      expect(handleErrorSpy.mock.calls[0][0] instanceof DOMException).toBe(\n        true,\n      );\n      expect(handleErrorSpy.mock.calls[0][0].name).toBe(\"AbortError\");\n      expect(handleErrorSpy.mock.calls[0][0].message).toBe(\n        \"This operation was aborted\",\n      );\n      expect(handleErrorSpy.mock.calls[0][1].request.method).toBe(\"GET\");\n      expect(handleErrorSpy.mock.calls[0][1].request.url).toBe(\n        \"http://test.com/resource\",\n      );\n    });\n  });\n\n  describe.skip(\"data requests\", () => {\n    test(\"data request that does not match loader surfaces 400 error for boundary\", async () => {\n      let build = mockServerBuild({\n        root: {\n          default: {},\n        },\n        \"routes/_index\": {\n          parentId: \"root\",\n          index: true,\n        },\n      });\n      let handler = createRequestHandler(build, ServerMode.Test);\n\n      let request = new Request(`${baseUrl}/?_data=routes/_index`, {\n        method: \"get\",\n      });\n\n      let result = await handler(request);\n      expect(result.status).toBe(400);\n      expect(headers.has(\"X-Remix-Response\")).toBe(true);\n      expect((await result.json()).message).toBeTruthy();\n    });\n\n    test(\"data request that does not match routeId surfaces 403 error for boundary\", async () => {\n      let build = mockServerBuild({\n        root: {\n          default: {},\n        },\n        \"routes/_index\": {\n          parentId: \"root\",\n          index: true,\n          loader: () => null,\n        },\n      });\n      let handler = createRequestHandler(build, ServerMode.Test);\n\n      // This bug wasn't that the router wasn't returning a 404 (it was), but\n      // that we weren't defensive when looking at match.params when we went\n      // to call handleDataRequest(), - and that threw it's own uncaught\n      // exception triggering a 500.  We need to ensure that this build has a\n      // handleDataRequest implementation for this test to mean anything\n      expect(build.entry.module.handleDataRequest).toBeDefined();\n\n      let request = new Request(`${baseUrl}/?_data=routes/junk`, {\n        method: \"get\",\n      });\n\n      let result = await handler(request);\n      expect(result.status).toBe(403);\n      expect(headers.has(\"X-Remix-Response\")).toBe(true);\n      expect((await result.json()).message).toBeTruthy();\n    });\n\n    test(\"data request that does not match route surfaces 404 error for boundary\", async () => {\n      let build = mockServerBuild({\n        root: {\n          default: {},\n        },\n        \"routes/_index\": {\n          parentId: \"root\",\n          index: true,\n          loader: () => null,\n        },\n      });\n      let handler = createRequestHandler(build, ServerMode.Test);\n\n      let request = new Request(`${baseUrl}/junk?_data=routes/junk`, {\n        method: \"get\",\n      });\n\n      let result = await handler(request);\n      expect(result.status).toBe(404);\n      expect(headers.has(\"X-Remix-Response\")).toBe(true);\n      expect((await result.json()).message).toBeTruthy();\n    });\n\n    test(\"data request calls loader\", async () => {\n      let rootLoader = jest.fn(() => {\n        return \"root\";\n      });\n      let indexLoader = jest.fn(() => {\n        return \"index\";\n      });\n      let build = mockServerBuild({\n        root: {\n          default: {},\n          loader: rootLoader,\n        },\n        \"routes/_index\": {\n          parentId: \"root\",\n          loader: indexLoader,\n          index: true,\n        },\n      });\n      let handler = createRequestHandler(build, ServerMode.Test);\n\n      let request = new Request(`${baseUrl}/?_data=routes/_index`, {\n        method: \"get\",\n      });\n\n      let result = await handler(request);\n      expect(result.status).toBe(200);\n      expect(await result.json()).toBe(\"index\");\n      expect(rootLoader.mock.calls.length).toBe(0);\n      expect(indexLoader.mock.calls.length).toBe(1);\n    });\n\n    test(\"data request calls loader and responds with generic message and error header\", async () => {\n      let rootLoader = jest.fn(() => {\n        throw new Error(\"test\");\n      });\n      let testAction = jest.fn(() => {\n        return \"root\";\n      });\n      let build = mockServerBuild({\n        root: {\n          default: {},\n          loader: rootLoader,\n        },\n        \"routes/test\": {\n          parentId: \"root\",\n          action: testAction,\n          path: \"test\",\n        },\n      });\n      let handler = createRequestHandler(build, ServerMode.Test);\n\n      let request = new Request(`${baseUrl}/test?_data=root`, {\n        method: \"get\",\n      });\n\n      let result = await handler(request);\n      expect(result.status).toBe(500);\n      expect((await result.json()).message).toBe(\"Unexpected Server Error\");\n      expect(headers.has(\"X-Remix-Response\")).toBe(true);\n      expect(rootLoader.mock.calls.length).toBe(1);\n      expect(testAction.mock.calls.length).toBe(0);\n    });\n\n    test(\"data request calls loader and responds with detailed info and error header in development mode\", async () => {\n      let message =\n        \"data request loader error logged to console once in dev mode\";\n      let rootLoader = jest.fn(() => {\n        throw new Error(message);\n      });\n      let testAction = jest.fn(() => {\n        return \"root\";\n      });\n      let build = mockServerBuild({\n        root: {\n          default: {},\n          loader: rootLoader,\n        },\n        \"routes/test\": {\n          parentId: \"root\",\n          action: testAction,\n          path: \"test\",\n        },\n      });\n      let handler = createRequestHandler(build, ServerMode.Development);\n\n      let request = new Request(`${baseUrl}/test?_data=root`, {\n        method: \"get\",\n      });\n\n      let result = await handler(request);\n      expect(result.status).toBe(500);\n      expect((await result.json()).message).toBe(message);\n      expect(headers.has(\"X-Remix-Response\")).toBe(true);\n      expect(rootLoader.mock.calls.length).toBe(1);\n      expect(testAction.mock.calls.length).toBe(0);\n      expect(spy.console.mock.calls.length).toBe(1);\n    });\n\n    test(\"data request calls loader and responds with catch header\", async () => {\n      let rootLoader = jest.fn(() => {\n        throw new Response(\"test\", { status: 400 });\n      });\n      let testAction = jest.fn(() => {\n        return \"root\";\n      });\n      let build = mockServerBuild({\n        root: {\n          default: {},\n          loader: rootLoader,\n        },\n        \"routes/test\": {\n          parentId: \"root\",\n          action: testAction,\n          path: \"test\",\n        },\n      });\n      let handler = createRequestHandler(build, ServerMode.Test);\n\n      let request = new Request(`${baseUrl}/test?_data=root`, {\n        method: \"get\",\n      });\n\n      let result = await handler(request);\n      expect(result.status).toBe(400);\n      expect(await result.text()).toBe(\"test\");\n      expect(rootLoader.mock.calls.length).toBe(1);\n      expect(testAction.mock.calls.length).toBe(0);\n    });\n\n    test(\"data request calls action\", async () => {\n      let rootLoader = jest.fn(() => {\n        return \"root\";\n      });\n      let testAction = jest.fn(() => {\n        return \"test\";\n      });\n      let build = mockServerBuild({\n        root: {\n          default: {},\n          loader: rootLoader,\n        },\n        \"routes/test\": {\n          parentId: \"root\",\n          action: testAction,\n          path: \"test\",\n        },\n      });\n      let handler = createRequestHandler(build, ServerMode.Test);\n\n      let request = new Request(`${baseUrl}/test?_data=routes/test`, {\n        method: \"post\",\n      });\n\n      let result = await handler(request);\n      expect(result.status).toBe(200);\n      expect(await result.json()).toBe(\"test\");\n      expect(rootLoader.mock.calls.length).toBe(0);\n      expect(testAction.mock.calls.length).toBe(1);\n    });\n\n    test(\"data request calls action and responds with generic message and error header\", async () => {\n      let rootLoader = jest.fn(() => {\n        return \"root\";\n      });\n      let testAction = jest.fn(() => {\n        throw new Error(\"test\");\n      });\n      let build = mockServerBuild({\n        root: {\n          default: {},\n          loader: rootLoader,\n        },\n        \"routes/test\": {\n          parentId: \"root\",\n          action: testAction,\n          path: \"test\",\n        },\n      });\n      let handler = createRequestHandler(build, ServerMode.Test);\n\n      let request = new Request(`${baseUrl}/test?_data=routes/test`, {\n        method: \"post\",\n      });\n\n      let result = await handler(request);\n      expect(result.status).toBe(500);\n      expect((await result.json()).message).toBe(\"Unexpected Server Error\");\n      expect(headers.has(\"X-Remix-Response\")).toBe(true);\n      expect(rootLoader.mock.calls.length).toBe(0);\n      expect(testAction.mock.calls.length).toBe(1);\n    });\n\n    test(\"data request calls action and responds with detailed info and error header in development mode\", async () => {\n      let message =\n        \"data request action error logged to console once in dev mode\";\n      let rootLoader = jest.fn(() => {\n        return \"root\";\n      });\n      let testAction = jest.fn(() => {\n        throw new Error(message);\n      });\n      let build = mockServerBuild({\n        root: {\n          default: {},\n          loader: rootLoader,\n        },\n        \"routes/test\": {\n          parentId: \"root\",\n          action: testAction,\n          path: \"test\",\n        },\n      });\n      let handler = createRequestHandler(build, ServerMode.Development);\n\n      let request = new Request(`${baseUrl}/test?_data=routes/test`, {\n        method: \"post\",\n      });\n\n      let result = await handler(request);\n      expect(result.status).toBe(500);\n      expect((await result.json()).message).toBe(message);\n      expect(headers.has(\"X-Remix-Response\")).toBe(true);\n      expect(rootLoader.mock.calls.length).toBe(0);\n      expect(testAction.mock.calls.length).toBe(1);\n      expect(spy.console.mock.calls.length).toBe(1);\n    });\n\n    test(\"data request calls action and responds with catch header\", async () => {\n      let rootLoader = jest.fn(() => {\n        return \"root\";\n      });\n      let testAction = jest.fn(() => {\n        throw new Response(\"test\", { status: 400 });\n      });\n      let build = mockServerBuild({\n        root: {\n          default: {},\n          loader: rootLoader,\n        },\n        \"routes/test\": {\n          parentId: \"root\",\n          action: testAction,\n          path: \"test\",\n        },\n      });\n      let handler = createRequestHandler(build, ServerMode.Test);\n\n      let request = new Request(`${baseUrl}/test?_data=routes/test`, {\n        method: \"post\",\n      });\n\n      let result = await handler(request);\n      expect(result.status).toBe(400);\n      expect(await result.text()).toBe(\"test\");\n      expect(rootLoader.mock.calls.length).toBe(0);\n      expect(testAction.mock.calls.length).toBe(1);\n    });\n\n    test(\"data request calls layout action\", async () => {\n      let rootLoader = jest.fn(() => {\n        return \"root\";\n      });\n      let rootAction = jest.fn(() => {\n        return \"root\";\n      });\n      let build = mockServerBuild({\n        root: {\n          default: {},\n          loader: rootLoader,\n          action: rootAction,\n        },\n        \"routes/_index\": {\n          parentId: \"root\",\n          index: true,\n        },\n      });\n      let handler = createRequestHandler(build, ServerMode.Test);\n\n      let request = new Request(`${baseUrl}/?_data=root`, {\n        method: \"post\",\n      });\n\n      let result = await handler(request);\n      expect(result.status).toBe(200);\n      expect(await result.json()).toBe(\"root\");\n      expect(rootLoader.mock.calls.length).toBe(0);\n      expect(rootAction.mock.calls.length).toBe(1);\n    });\n\n    test(\"data request calls index action\", async () => {\n      let rootLoader = jest.fn(() => {\n        return \"root\";\n      });\n      let indexAction = jest.fn(() => {\n        return \"index\";\n      });\n      let build = mockServerBuild({\n        root: {\n          default: {},\n          loader: rootLoader,\n        },\n        \"routes/_index\": {\n          parentId: \"root\",\n          action: indexAction,\n          index: true,\n        },\n      });\n      let handler = createRequestHandler(build, ServerMode.Test);\n\n      let request = new Request(`${baseUrl}/?index&_data=routes/_index`, {\n        method: \"post\",\n      });\n\n      let result = await handler(request);\n      expect(result.status).toBe(200);\n      expect(await result.json()).toBe(\"index\");\n      expect(rootLoader.mock.calls.length).toBe(0);\n      expect(indexAction.mock.calls.length).toBe(1);\n    });\n\n    test(\"data request handleDataRequest redirects are handled\", async () => {\n      let rootLoader = jest.fn(() => {\n        return \"root\";\n      });\n      let indexLoader = jest.fn(() => {\n        return \"index\";\n      });\n      let build = mockServerBuild({\n        root: {\n          default: {},\n          loader: rootLoader,\n        },\n        \"routes/_index\": {\n          parentId: \"root\",\n          loader: indexLoader,\n          index: true,\n        },\n      });\n      build.entry.module.handleDataRequest.mockImplementation(async () => {\n        return new Response(null, {\n          status: 302,\n          headers: {\n            Location: \"/redirect\",\n          },\n        });\n      });\n      let handler = createRequestHandler(build, ServerMode.Test);\n\n      let request = new Request(`${baseUrl}/?_data=routes/_index`, {\n        method: \"get\",\n      });\n\n      let result = await handler(request);\n      expect(result.status).toBe(204);\n      expect(result.headers.get(\"X-Remix-Redirect\")).toBe(\"/redirect\");\n      expect(result.headers.get(\"X-Remix-Status\")).toBe(\"302\");\n      expect(rootLoader.mock.calls.length).toBe(0);\n      expect(indexLoader.mock.calls.length).toBe(1);\n    });\n\n    test(\"aborts request\", async () => {\n      let rootLoader = jest.fn(() => {\n        return \"root\";\n      });\n      let indexLoader = jest.fn(() => {\n        return \"index\";\n      });\n      let handleErrorSpy = jest.fn();\n      let build = mockServerBuild(\n        {\n          root: {\n            default: {},\n            loader: rootLoader,\n          },\n          \"routes/_index\": {\n            parentId: \"root\",\n            loader: indexLoader,\n            index: true,\n          },\n        },\n        {\n          handleError: handleErrorSpy,\n        },\n      );\n      let handler = createRequestHandler(build, ServerMode.Test);\n\n      let controller = new AbortController();\n      let request = new Request(`${baseUrl}/?_data=routes/_index`, {\n        method: \"get\",\n        signal: controller.signal,\n      });\n\n      let resultPromise = handler(request);\n      controller.abort();\n      let result = await resultPromise;\n      expect(result.status).toBe(500);\n      let error = await result.json();\n      expect(error.message).toBe(\"This operation was aborted\");\n      expect(\n        error.stack.startsWith(\"AbortError: This operation was aborted\"),\n      ).toBe(true);\n      expect(handleErrorSpy).toHaveBeenCalledTimes(1);\n      expect(handleErrorSpy.mock.calls[0][0] instanceof DOMException).toBe(\n        true,\n      );\n      expect(handleErrorSpy.mock.calls[0][0].name).toBe(\"AbortError\");\n      expect(handleErrorSpy.mock.calls[0][0].message).toBe(\n        \"This operation was aborted\",\n      );\n      expect(handleErrorSpy.mock.calls[0][1].request.method).toBe(\"GET\");\n      expect(handleErrorSpy.mock.calls[0][1].request.url).toBe(\n        \"http://test.com/?_data=routes/_index\",\n      );\n    });\n  });\n\n  describe(\"document requests\", () => {\n    test(\"not found document request for no matches and no ErrorBoundary\", async () => {\n      let rootLoader = jest.fn(() => {\n        return \"root\";\n      });\n      let build = mockServerBuild({\n        root: {\n          default: {},\n          loader: rootLoader,\n        },\n      });\n      let handler = createRequestHandler(build, ServerMode.Test);\n\n      let request = new Request(`${baseUrl}/`, {\n        method: \"get\",\n      });\n\n      let result = await handler(request);\n      expect(result.status).toBe(404);\n      expect(rootLoader.mock.calls.length).toBe(0);\n      expect(build.entry.module.default.mock.calls.length).toBe(1);\n\n      let calls = build.entry.module.default.mock.calls;\n      expect(calls.length).toBe(1);\n      let context = calls[0][3].staticHandlerContext as StaticHandlerContext;\n      expect(context.errors).toBeTruthy();\n      expect(context.errors!.root.status).toBe(404);\n    });\n\n    test(\"sets root as catch boundary for not found document request\", async () => {\n      let rootLoader = jest.fn(() => {\n        return \"root\";\n      });\n      let build = mockServerBuild({\n        root: {\n          default: {},\n          loader: rootLoader,\n          ErrorBoundary: {},\n        },\n      });\n      let handler = createRequestHandler(build, ServerMode.Test);\n\n      let request = new Request(`${baseUrl}/`, { method: \"get\" });\n\n      let result = await handler(request);\n      expect(result.status).toBe(404);\n      expect(rootLoader.mock.calls.length).toBe(0);\n      expect(build.entry.module.default.mock.calls.length).toBe(1);\n\n      let calls = build.entry.module.default.mock.calls;\n      expect(calls.length).toBe(1);\n      let context = calls[0][3].staticHandlerContext as StaticHandlerContext;\n      expect(context.errors).toBeTruthy();\n      expect(context.errors!.root.status).toBe(404);\n      expect(context.loaderData).toEqual({});\n    });\n\n    test(\"thrown loader responses bubble up\", async () => {\n      let rootLoader = jest.fn(() => {\n        return \"root\";\n      });\n      let indexLoader = jest.fn(() => {\n        throw new Response(null, { status: 400 });\n      });\n      let build = mockServerBuild({\n        root: {\n          default: {},\n          loader: rootLoader,\n          ErrorBoundary: {},\n        },\n        \"routes/_index\": {\n          parentId: \"root\",\n          index: true,\n          default: {},\n          loader: indexLoader,\n        },\n      });\n      let handler = createRequestHandler(build, ServerMode.Test);\n\n      let request = new Request(`${baseUrl}/`, { method: \"get\" });\n\n      let result = await handler(request);\n      expect(result.status).toBe(400);\n      expect(rootLoader.mock.calls.length).toBe(1);\n      expect(indexLoader.mock.calls.length).toBe(1);\n      expect(build.entry.module.default.mock.calls.length).toBe(1);\n\n      let calls = build.entry.module.default.mock.calls;\n      expect(calls.length).toBe(1);\n      let context = calls[0][3].staticHandlerContext as StaticHandlerContext;\n      expect(context.errors).toBeTruthy();\n      expect(context.errors!.root.status).toBe(400);\n      expect(context.loaderData).toEqual({\n        root: \"root\",\n      });\n    });\n\n    test(\"thrown loader responses catch deep\", async () => {\n      let rootLoader = jest.fn(() => {\n        return \"root\";\n      });\n      let indexLoader = jest.fn(() => {\n        throw new Response(null, { status: 400 });\n      });\n      let build = mockServerBuild({\n        root: {\n          default: {},\n          loader: rootLoader,\n          ErrorBoundary: {},\n        },\n        \"routes/_index\": {\n          parentId: \"root\",\n          index: true,\n          default: {},\n          loader: indexLoader,\n          ErrorBoundary: {},\n        },\n      });\n      let handler = createRequestHandler(build, ServerMode.Test);\n\n      let request = new Request(`${baseUrl}/`, { method: \"get\" });\n\n      let result = await handler(request);\n      expect(result.status).toBe(400);\n      expect(rootLoader.mock.calls.length).toBe(1);\n      expect(indexLoader.mock.calls.length).toBe(1);\n      expect(build.entry.module.default.mock.calls.length).toBe(1);\n\n      let calls = build.entry.module.default.mock.calls;\n      expect(calls.length).toBe(1);\n      let context = calls[0][3].staticHandlerContext as StaticHandlerContext;\n      expect(context.errors).toBeTruthy();\n      expect(context.errors![\"routes/_index\"].status).toBe(400);\n      expect(context.loaderData).toEqual({\n        root: \"root\",\n      });\n    });\n\n    test(\"thrown action responses bubble up\", async () => {\n      let rootLoader = jest.fn(() => {\n        return \"root\";\n      });\n      let testAction = jest.fn(() => {\n        throw new Response(null, { status: 400 });\n      });\n      let testLoader = jest.fn(() => {\n        return \"test\";\n      });\n      let build = mockServerBuild({\n        root: {\n          default: {},\n          loader: rootLoader,\n          ErrorBoundary: {},\n        },\n        \"routes/test\": {\n          parentId: \"root\",\n          path: \"test\",\n          default: {},\n          loader: testLoader,\n          action: testAction,\n        },\n      });\n      let handler = createRequestHandler(build, ServerMode.Test);\n\n      let request = new Request(`${baseUrl}/test`, { method: \"post\" });\n\n      let result = await handler(request);\n      expect(result.status).toBe(400);\n      expect(testAction.mock.calls.length).toBe(1);\n      // Should not call root loader since it is the boundary route\n      expect(rootLoader.mock.calls.length).toBe(0);\n      expect(testLoader.mock.calls.length).toBe(0);\n      expect(build.entry.module.default.mock.calls.length).toBe(1);\n\n      let calls = build.entry.module.default.mock.calls;\n      expect(calls.length).toBe(1);\n      let context = calls[0][3].staticHandlerContext as StaticHandlerContext;\n      expect(context.errors).toBeTruthy();\n      expect(context.errors!.root.status).toBe(400);\n      expect(context.loaderData).toEqual({});\n    });\n\n    test(\"thrown action responses bubble up for index routes\", async () => {\n      let rootLoader = jest.fn(() => {\n        return \"root\";\n      });\n      let indexAction = jest.fn(() => {\n        throw new Response(null, { status: 400 });\n      });\n      let indexLoader = jest.fn(() => {\n        return \"index\";\n      });\n      let build = mockServerBuild({\n        root: {\n          default: {},\n          loader: rootLoader,\n          ErrorBoundary: {},\n        },\n        \"routes/_index\": {\n          parentId: \"root\",\n          index: true,\n          default: {},\n          loader: indexLoader,\n          action: indexAction,\n        },\n      });\n      let handler = createRequestHandler(build, ServerMode.Test);\n\n      let request = new Request(`${baseUrl}/?index`, { method: \"post\" });\n\n      let result = await handler(request);\n      expect(result.status).toBe(400);\n      expect(indexAction.mock.calls.length).toBe(1);\n      // Should not call root loader since it is the boundary route\n      expect(rootLoader.mock.calls.length).toBe(0);\n      expect(indexLoader.mock.calls.length).toBe(0);\n      expect(build.entry.module.default.mock.calls.length).toBe(1);\n\n      let calls = build.entry.module.default.mock.calls;\n      expect(calls.length).toBe(1);\n      let context = calls[0][3].staticHandlerContext as StaticHandlerContext;\n      expect(context.errors).toBeTruthy();\n      expect(context.errors!.root.status).toBe(400);\n      expect(context.loaderData).toEqual({});\n    });\n\n    test(\"thrown action responses catch deep\", async () => {\n      let rootLoader = jest.fn(() => {\n        return \"root\";\n      });\n      let testAction = jest.fn(() => {\n        throw new Response(null, { status: 400 });\n      });\n      let testLoader = jest.fn(() => {\n        return \"test\";\n      });\n      let build = mockServerBuild({\n        root: {\n          default: {},\n          loader: rootLoader,\n          ErrorBoundary: {},\n        },\n        \"routes/test\": {\n          parentId: \"root\",\n          path: \"test\",\n          default: {},\n          loader: testLoader,\n          action: testAction,\n          ErrorBoundary: {},\n        },\n      });\n      let handler = createRequestHandler(build, ServerMode.Test);\n\n      let request = new Request(`${baseUrl}/test`, { method: \"post\" });\n\n      let result = await handler(request);\n      expect(result.status).toBe(400);\n      expect(testAction.mock.calls.length).toBe(1);\n      expect(rootLoader.mock.calls.length).toBe(1);\n      expect(testLoader.mock.calls.length).toBe(0);\n      expect(build.entry.module.default.mock.calls.length).toBe(1);\n\n      let calls = build.entry.module.default.mock.calls;\n      expect(calls.length).toBe(1);\n      let context = calls[0][3].staticHandlerContext as StaticHandlerContext;\n      expect(context.errors).toBeTruthy();\n      expect(context.errors![\"routes/test\"].status).toBe(400);\n      expect(context.loaderData).toEqual({\n        root: \"root\",\n      });\n    });\n\n    test(\"thrown action responses catch deep for index routes\", async () => {\n      let rootLoader = jest.fn(() => {\n        return \"root\";\n      });\n      let indexAction = jest.fn(() => {\n        throw new Response(null, { status: 400 });\n      });\n      let indexLoader = jest.fn(() => {\n        return \"index\";\n      });\n      let build = mockServerBuild({\n        root: {\n          default: {},\n          loader: rootLoader,\n          ErrorBoundary: {},\n        },\n        \"routes/_index\": {\n          parentId: \"root\",\n          index: true,\n          default: {},\n          loader: indexLoader,\n          action: indexAction,\n          ErrorBoundary: {},\n        },\n      });\n      let handler = createRequestHandler(build, ServerMode.Test);\n\n      let request = new Request(`${baseUrl}/?index`, { method: \"post\" });\n\n      let result = await handler(request);\n      expect(result.status).toBe(400);\n      expect(indexAction.mock.calls.length).toBe(1);\n      expect(rootLoader.mock.calls.length).toBe(1);\n      expect(indexLoader.mock.calls.length).toBe(0);\n      expect(build.entry.module.default.mock.calls.length).toBe(1);\n\n      let calls = build.entry.module.default.mock.calls;\n      expect(calls.length).toBe(1);\n      let context = calls[0][3].staticHandlerContext as StaticHandlerContext;\n      expect(context.errors).toBeTruthy();\n      expect(context.errors![\"routes/_index\"].status).toBe(400);\n      expect(context.loaderData).toEqual({\n        root: \"root\",\n      });\n    });\n\n    test(\"thrown loader response after thrown action response bubble up action throw to deepest loader boundary\", async () => {\n      let rootLoader = jest.fn(() => {\n        return \"root\";\n      });\n      let layoutLoader = jest.fn(() => {\n        throw new Response(\"layout\", { status: 401 });\n      });\n      let testAction = jest.fn(() => {\n        throw new Response(\"action\", { status: 400 });\n      });\n      let testLoader = jest.fn(() => {\n        return \"test\";\n      });\n      let build = mockServerBuild({\n        root: {\n          default: {},\n          loader: rootLoader,\n          ErrorBoundary: {},\n        },\n        \"routes/__layout\": {\n          parentId: \"root\",\n          default: {},\n          loader: layoutLoader,\n          ErrorBoundary: {},\n        },\n        \"routes/__layout/test\": {\n          parentId: \"routes/__layout\",\n          path: \"test\",\n          default: {},\n          loader: testLoader,\n          action: testAction,\n        },\n      });\n      let handler = createRequestHandler(build, ServerMode.Test);\n\n      let request = new Request(`${baseUrl}/test`, { method: \"post\" });\n\n      let result = await handler(request);\n      expect(result.status).toBe(400);\n      expect(testAction.mock.calls.length).toBe(1);\n      expect(rootLoader.mock.calls.length).toBe(1);\n      expect(testLoader.mock.calls.length).toBe(0);\n      expect(build.entry.module.default.mock.calls.length).toBe(1);\n\n      let calls = build.entry.module.default.mock.calls;\n      expect(calls.length).toBe(1);\n      let context = calls[0][3].staticHandlerContext as StaticHandlerContext;\n      expect(context.errors).toBeTruthy();\n      expect(context.errors![\"routes/__layout\"].data).toBe(\"action\");\n      expect(context.loaderData).toEqual({\n        root: \"root\",\n      });\n    });\n\n    test(\"thrown loader response after thrown index action response bubble up action throw to deepest loader boundary\", async () => {\n      let rootLoader = jest.fn(() => {\n        return \"root\";\n      });\n      let layoutLoader = jest.fn(() => {\n        throw new Response(\"layout\", { status: 401 });\n      });\n      let indexAction = jest.fn(() => {\n        throw new Response(\"action\", { status: 400 });\n      });\n      let indexLoader = jest.fn(() => {\n        return \"index\";\n      });\n      let build = mockServerBuild({\n        root: {\n          default: {},\n          loader: rootLoader,\n          ErrorBoundary: {},\n        },\n        \"routes/__layout\": {\n          parentId: \"root\",\n          default: {},\n          loader: layoutLoader,\n          ErrorBoundary: {},\n        },\n        \"routes/__layout/index\": {\n          parentId: \"routes/__layout\",\n          index: true,\n          default: {},\n          loader: indexLoader,\n          action: indexAction,\n        },\n      });\n      let handler = createRequestHandler(build, ServerMode.Test);\n\n      let request = new Request(`${baseUrl}/?index`, { method: \"post\" });\n\n      let result = await handler(request);\n      expect(result.status).toBe(400);\n      expect(indexAction.mock.calls.length).toBe(1);\n      expect(rootLoader.mock.calls.length).toBe(1);\n      expect(indexLoader.mock.calls.length).toBe(0);\n      expect(build.entry.module.default.mock.calls.length).toBe(1);\n\n      let calls = build.entry.module.default.mock.calls;\n      expect(calls.length).toBe(1);\n      let context = calls[0][3].staticHandlerContext as StaticHandlerContext;\n      expect(context.errors).toBeTruthy();\n      expect(context.errors![\"routes/__layout\"].data).toBe(\"action\");\n      expect(context.loaderData).toEqual({\n        root: \"root\",\n      });\n    });\n\n    test(\"loader errors bubble up\", async () => {\n      let rootLoader = jest.fn(() => {\n        return \"root\";\n      });\n      let indexLoader = jest.fn(() => {\n        throw new Error(\"index\");\n      });\n      let build = mockServerBuild({\n        root: {\n          default: {},\n          loader: rootLoader,\n          ErrorBoundary: {},\n        },\n        \"routes/_index\": {\n          parentId: \"root\",\n          index: true,\n          default: {},\n          loader: indexLoader,\n        },\n      });\n      let handler = createRequestHandler(build, ServerMode.Test);\n\n      let request = new Request(`${baseUrl}/`, { method: \"get\" });\n\n      let result = await handler(request);\n      expect(result.status).toBe(500);\n      expect(rootLoader.mock.calls.length).toBe(1);\n      expect(indexLoader.mock.calls.length).toBe(1);\n      expect(build.entry.module.default.mock.calls.length).toBe(1);\n\n      let calls = build.entry.module.default.mock.calls;\n      expect(calls.length).toBe(1);\n      let context = calls[0][3].staticHandlerContext as StaticHandlerContext;\n      expect(context.errors).toBeTruthy();\n      expect(context.errors!.root).toBeInstanceOf(Error);\n      expect(context.errors!.root.message).toBe(\"Unexpected Server Error\");\n      expect(context.errors!.root.stack).toBeUndefined();\n      expect(context.loaderData).toEqual({\n        root: \"root\",\n      });\n    });\n\n    test(\"loader errors catch deep\", async () => {\n      let rootLoader = jest.fn(() => {\n        return \"root\";\n      });\n      let indexLoader = jest.fn(() => {\n        throw new Error(\"index\");\n      });\n      let build = mockServerBuild({\n        root: {\n          default: {},\n          loader: rootLoader,\n          ErrorBoundary: {},\n        },\n        \"routes/_index\": {\n          parentId: \"root\",\n          index: true,\n          default: {},\n          loader: indexLoader,\n          ErrorBoundary: {},\n        },\n      });\n      let handler = createRequestHandler(build, ServerMode.Test);\n\n      let request = new Request(`${baseUrl}/`, { method: \"get\" });\n\n      let result = await handler(request);\n      expect(result.status).toBe(500);\n      expect(rootLoader.mock.calls.length).toBe(1);\n      expect(indexLoader.mock.calls.length).toBe(1);\n      expect(build.entry.module.default.mock.calls.length).toBe(1);\n\n      let calls = build.entry.module.default.mock.calls;\n      expect(calls.length).toBe(1);\n      let context = calls[0][3].staticHandlerContext as StaticHandlerContext;\n      expect(context.errors).toBeTruthy();\n      expect(context.errors![\"routes/_index\"]).toBeInstanceOf(Error);\n      expect(context.errors![\"routes/_index\"].message).toBe(\n        \"Unexpected Server Error\",\n      );\n      expect(context.errors![\"routes/_index\"].stack).toBeUndefined();\n      expect(context.loaderData).toEqual({\n        root: \"root\",\n      });\n    });\n\n    test(\"action errors bubble up\", async () => {\n      let rootLoader = jest.fn(() => {\n        return \"root\";\n      });\n      let testAction = jest.fn(() => {\n        throw new Error(\"test\");\n      });\n      let testLoader = jest.fn(() => {\n        return \"test\";\n      });\n      let build = mockServerBuild({\n        root: {\n          default: {},\n          loader: rootLoader,\n          ErrorBoundary: {},\n        },\n        \"routes/test\": {\n          parentId: \"root\",\n          path: \"test\",\n          default: {},\n          loader: testLoader,\n          action: testAction,\n        },\n      });\n      let handler = createRequestHandler(build, ServerMode.Test);\n\n      let request = new Request(`${baseUrl}/test`, { method: \"post\" });\n\n      let result = await handler(request);\n      expect(result.status).toBe(500);\n      expect(testAction.mock.calls.length).toBe(1);\n      // Should not call root loader since it is the boundary route\n      expect(rootLoader.mock.calls.length).toBe(0);\n      expect(testLoader.mock.calls.length).toBe(0);\n      expect(build.entry.module.default.mock.calls.length).toBe(1);\n\n      let calls = build.entry.module.default.mock.calls;\n      expect(calls.length).toBe(1);\n      let context = calls[0][3].staticHandlerContext as StaticHandlerContext;\n      expect(context.errors).toBeTruthy();\n      expect(context.errors!.root).toBeInstanceOf(Error);\n      expect(context.errors!.root.message).toBe(\"Unexpected Server Error\");\n      expect(context.errors!.root.stack).toBeUndefined();\n      expect(context.loaderData).toEqual({});\n    });\n\n    test(\"action errors bubble up for index routes\", async () => {\n      let rootLoader = jest.fn(() => {\n        return \"root\";\n      });\n      let indexAction = jest.fn(() => {\n        throw new Error(\"index\");\n      });\n      let indexLoader = jest.fn(() => {\n        return \"index\";\n      });\n      let build = mockServerBuild({\n        root: {\n          default: {},\n          loader: rootLoader,\n          ErrorBoundary: {},\n        },\n        \"routes/_index\": {\n          parentId: \"root\",\n          index: true,\n          default: {},\n          loader: indexLoader,\n          action: indexAction,\n        },\n      });\n      let handler = createRequestHandler(build, ServerMode.Test);\n\n      let request = new Request(`${baseUrl}/?index`, { method: \"post\" });\n\n      let result = await handler(request);\n      expect(result.status).toBe(500);\n      expect(indexAction.mock.calls.length).toBe(1);\n      // Should not call root loader since it is the boundary route\n      expect(rootLoader.mock.calls.length).toBe(0);\n      expect(indexLoader.mock.calls.length).toBe(0);\n      expect(build.entry.module.default.mock.calls.length).toBe(1);\n\n      let calls = build.entry.module.default.mock.calls;\n      expect(calls.length).toBe(1);\n      let context = calls[0][3].staticHandlerContext as StaticHandlerContext;\n      expect(context.errors).toBeTruthy();\n      expect(context.errors!.root).toBeInstanceOf(Error);\n      expect(context.errors!.root.message).toBe(\"Unexpected Server Error\");\n      expect(context.errors!.root.stack).toBeUndefined();\n      expect(context.loaderData).toEqual({});\n    });\n\n    test(\"action errors catch deep\", async () => {\n      let rootLoader = jest.fn(() => {\n        return \"root\";\n      });\n      let testAction = jest.fn(() => {\n        throw new Error(\"test\");\n      });\n      let testLoader = jest.fn(() => {\n        return \"test\";\n      });\n      let build = mockServerBuild({\n        root: {\n          default: {},\n          loader: rootLoader,\n          ErrorBoundary: {},\n        },\n        \"routes/test\": {\n          parentId: \"root\",\n          path: \"test\",\n          default: {},\n          loader: testLoader,\n          action: testAction,\n          ErrorBoundary: {},\n        },\n      });\n      let handler = createRequestHandler(build, ServerMode.Test);\n\n      let request = new Request(`${baseUrl}/test`, { method: \"post\" });\n\n      let result = await handler(request);\n      expect(result.status).toBe(500);\n      expect(testAction.mock.calls.length).toBe(1);\n      expect(rootLoader.mock.calls.length).toBe(1);\n      expect(testLoader.mock.calls.length).toBe(0);\n      expect(build.entry.module.default.mock.calls.length).toBe(1);\n\n      let calls = build.entry.module.default.mock.calls;\n      expect(calls.length).toBe(1);\n      let context = calls[0][3].staticHandlerContext as StaticHandlerContext;\n      expect(context.errors).toBeTruthy();\n      expect(context.errors![\"routes/test\"]).toBeInstanceOf(Error);\n      expect(context.errors![\"routes/test\"].message).toBe(\n        \"Unexpected Server Error\",\n      );\n      expect(context.errors![\"routes/test\"].stack).toBeUndefined();\n      expect(context.loaderData).toEqual({\n        root: \"root\",\n      });\n    });\n\n    test(\"action errors catch deep for index routes\", async () => {\n      let rootLoader = jest.fn(() => {\n        return \"root\";\n      });\n      let indexAction = jest.fn(() => {\n        throw new Error(\"index\");\n      });\n      let indexLoader = jest.fn(() => {\n        return \"index\";\n      });\n      let build = mockServerBuild({\n        root: {\n          default: {},\n          loader: rootLoader,\n          ErrorBoundary: {},\n        },\n        \"routes/_index\": {\n          parentId: \"root\",\n          index: true,\n          default: {},\n          loader: indexLoader,\n          action: indexAction,\n          ErrorBoundary: {},\n        },\n      });\n      let handler = createRequestHandler(build, ServerMode.Test);\n\n      let request = new Request(`${baseUrl}/?index`, { method: \"post\" });\n\n      let result = await handler(request);\n      expect(result.status).toBe(500);\n      expect(indexAction.mock.calls.length).toBe(1);\n      expect(rootLoader.mock.calls.length).toBe(1);\n      expect(indexLoader.mock.calls.length).toBe(0);\n      expect(build.entry.module.default.mock.calls.length).toBe(1);\n\n      let calls = build.entry.module.default.mock.calls;\n      expect(calls.length).toBe(1);\n      let context = calls[0][3].staticHandlerContext as StaticHandlerContext;\n      expect(context.errors).toBeTruthy();\n      expect(context.errors![\"routes/_index\"]).toBeInstanceOf(Error);\n      expect(context.errors![\"routes/_index\"].message).toBe(\n        \"Unexpected Server Error\",\n      );\n      expect(context.errors![\"routes/_index\"].stack).toBeUndefined();\n      expect(context.loaderData).toEqual({\n        root: \"root\",\n      });\n    });\n\n    test(\"loader errors after action error bubble up action error to deepest loader boundary\", async () => {\n      let rootLoader = jest.fn(() => {\n        return \"root\";\n      });\n      let layoutLoader = jest.fn(() => {\n        throw new Error(\"layout\");\n      });\n      let testAction = jest.fn(() => {\n        throw new Error(\"action\");\n      });\n      let testLoader = jest.fn(() => {\n        return \"test\";\n      });\n      let build = mockServerBuild({\n        root: {\n          default: {},\n          loader: rootLoader,\n          ErrorBoundary: {},\n        },\n        \"routes/__layout\": {\n          parentId: \"root\",\n          default: {},\n          loader: layoutLoader,\n          ErrorBoundary: {},\n        },\n        \"routes/__layout/test\": {\n          parentId: \"routes/__layout\",\n          path: \"test\",\n          default: {},\n          loader: testLoader,\n          action: testAction,\n        },\n      });\n      let handler = createRequestHandler(build, ServerMode.Test);\n\n      let request = new Request(`${baseUrl}/test`, { method: \"post\" });\n\n      let result = await handler(request);\n      expect(result.status).toBe(500);\n      expect(testAction.mock.calls.length).toBe(1);\n      expect(rootLoader.mock.calls.length).toBe(1);\n      expect(testLoader.mock.calls.length).toBe(0);\n      expect(build.entry.module.default.mock.calls.length).toBe(1);\n\n      let calls = build.entry.module.default.mock.calls;\n      expect(calls.length).toBe(1);\n      let context = calls[0][3].staticHandlerContext as StaticHandlerContext;\n      expect(context.errors).toBeTruthy();\n      expect(context.errors![\"routes/__layout\"]).toBeInstanceOf(Error);\n      expect(context.errors![\"routes/__layout\"].message).toBe(\n        \"Unexpected Server Error\",\n      );\n      expect(context.errors![\"routes/__layout\"].stack).toBeUndefined();\n      expect(context.loaderData).toEqual({\n        root: \"root\",\n      });\n    });\n\n    test(\"loader errors after index action error bubble up action error to deepest loader boundary\", async () => {\n      let rootLoader = jest.fn(() => {\n        return \"root\";\n      });\n      let layoutLoader = jest.fn(() => {\n        throw new Error(\"layout\");\n      });\n      let indexAction = jest.fn(() => {\n        throw new Error(\"action\");\n      });\n      let indexLoader = jest.fn(() => {\n        return \"index\";\n      });\n      let build = mockServerBuild({\n        root: {\n          default: {},\n          loader: rootLoader,\n          ErrorBoundary: {},\n        },\n        \"routes/__layout\": {\n          parentId: \"root\",\n          default: {},\n          loader: layoutLoader,\n          ErrorBoundary: {},\n        },\n        \"routes/__layout/index\": {\n          parentId: \"routes/__layout\",\n          index: true,\n          default: {},\n          loader: indexLoader,\n          action: indexAction,\n        },\n      });\n      let handler = createRequestHandler(build, ServerMode.Test);\n\n      let request = new Request(`${baseUrl}/?index`, { method: \"post\" });\n\n      let result = await handler(request);\n      expect(result.status).toBe(500);\n      expect(indexAction.mock.calls.length).toBe(1);\n      expect(rootLoader.mock.calls.length).toBe(1);\n      expect(indexLoader.mock.calls.length).toBe(0);\n      expect(build.entry.module.default.mock.calls.length).toBe(1);\n\n      let calls = build.entry.module.default.mock.calls;\n      expect(calls.length).toBe(1);\n      let context = calls[0][3].staticHandlerContext as StaticHandlerContext;\n      expect(context.errors).toBeTruthy();\n      expect(context.errors![\"routes/__layout\"]).toBeInstanceOf(Error);\n      expect(context.errors![\"routes/__layout\"].message).toBe(\n        \"Unexpected Server Error\",\n      );\n      expect(context.errors![\"routes/__layout\"].stack).toBeUndefined();\n      expect(context.loaderData).toEqual({\n        root: \"root\",\n      });\n    });\n\n    test(\"calls handleDocumentRequest again with new error when handleDocumentRequest throws\", async () => {\n      let rootLoader = jest.fn(() => {\n        return \"root\";\n      });\n      let indexLoader = jest.fn(() => {\n        return \"index\";\n      });\n      let build = mockServerBuild({\n        root: {\n          default: {},\n          loader: rootLoader,\n          ErrorBoundary: {},\n        },\n        \"routes/_index\": {\n          parentId: \"root\",\n          index: true,\n          default: {},\n          loader: indexLoader,\n        },\n      });\n      let calledBefore = false;\n      let ogHandleDocumentRequest = build.entry.module.default;\n      build.entry.module.default = jest.fn(function () {\n        if (!calledBefore) {\n          throw new Error(\"thrown\");\n        }\n        calledBefore = true;\n        return ogHandleDocumentRequest.call(null, ...arguments);\n      }) as any;\n      let handler = createRequestHandler(build, ServerMode.Development);\n\n      let request = new Request(`${baseUrl}/404`, { method: \"get\" });\n\n      let result = await handler(request);\n      expect(result.status).toBe(500);\n      expect(rootLoader.mock.calls.length).toBe(0);\n      expect(indexLoader.mock.calls.length).toBe(0);\n\n      let calls = build.entry.module.default.mock.calls;\n      expect(calls.length).toBe(2);\n      let context = calls[1][3].staticHandlerContext;\n      expect(context.errors.root).toBeTruthy();\n      expect(context.errors!.root.message).toBe(\"thrown\");\n      expect(context.loaderData).toEqual({});\n    });\n\n    test(\"unwraps responses thrown from handleDocumentRequest\", async () => {\n      let rootLoader = jest.fn(() => {\n        return \"root\";\n      });\n      let indexLoader = jest.fn(() => {\n        return \"index\";\n      });\n      let build = mockServerBuild({\n        root: {\n          default: {},\n          loader: rootLoader,\n          ErrorBoundary: {},\n        },\n        \"routes/_index\": {\n          parentId: \"root\",\n          index: true,\n          default: {},\n          loader: indexLoader,\n        },\n      });\n      let ogHandleDocumentRequest = build.entry.module.default;\n      build.entry.module.default = function (\n        _: Request,\n        responseStatusCode: number,\n      ) {\n        if (responseStatusCode === 200) {\n          throw new Response(\"Uh oh!\", {\n            status: 400,\n            statusText: \"Bad Request\",\n          });\n        }\n        return ogHandleDocumentRequest.call(null, ...arguments);\n      } as any;\n      let handler = createRequestHandler(build, ServerMode.Development);\n\n      let request = new Request(`${baseUrl}/`, { method: \"get\" });\n\n      let result = await handler(request);\n      expect(result.status).toBe(400);\n    });\n\n    test(\"returns generic message if handleDocumentRequest throws a second time\", async () => {\n      let rootLoader = jest.fn(() => {\n        return \"root\";\n      });\n      let indexLoader = jest.fn(() => {\n        return \"index\";\n      });\n      let build = mockServerBuild({\n        root: {\n          default: {},\n          loader: rootLoader,\n          ErrorBoundary: {},\n        },\n        \"routes/_index\": {\n          parentId: \"root\",\n          default: {},\n          loader: indexLoader,\n        },\n      });\n      let lastThrownError;\n      build.entry.module.default = jest.fn(function () {\n        lastThrownError = new Error(\"rofl\");\n        throw lastThrownError;\n      }) as any;\n      let handler = createRequestHandler(build, ServerMode.Test);\n\n      let request = new Request(`${baseUrl}/`, { method: \"get\" });\n\n      let result = await handler(request);\n      expect(result.status).toBe(500);\n      expect(await result.text()).toBe(\n        \"Unexpected Server Error\\n\\nError: rofl\",\n      );\n      expect(rootLoader.mock.calls.length).toBe(0);\n      expect(indexLoader.mock.calls.length).toBe(0);\n\n      let calls = build.entry.module.default.mock.calls;\n      expect(calls.length).toBe(2);\n    });\n\n    test(\"returns more detailed message if handleDocumentRequest throws a second time in development mode\", async () => {\n      let rootLoader = jest.fn(() => {\n        return \"root\";\n      });\n      let indexLoader = jest.fn(() => {\n        return \"index\";\n      });\n      let build = mockServerBuild({\n        root: {\n          path: \"/\",\n          default: {},\n          loader: rootLoader,\n          ErrorBoundary: {},\n        },\n        \"routes/_index\": {\n          parentId: \"root\",\n          index: true,\n          default: {},\n          loader: indexLoader,\n        },\n      });\n      let errorMessage =\n        \"thrown from handleDocumentRequest and expected to be logged in console only once\";\n      let lastThrownError;\n      build.entry.module.default = jest.fn(function () {\n        lastThrownError = new Error(errorMessage);\n        errorMessage = \"second error thrown from handleDocumentRequest\";\n        throw lastThrownError;\n      }) as any;\n      let handler = createRequestHandler(build, ServerMode.Development);\n\n      let request = new Request(`${baseUrl}/`);\n\n      let result = await handler(request);\n      expect(result.status).toBe(500);\n      expect((await result.text()).includes(errorMessage)).toBe(true);\n      expect(rootLoader.mock.calls.length).toBe(1);\n      expect(indexLoader.mock.calls.length).toBe(1);\n\n      let calls = build.entry.module.default.mock.calls;\n      expect(calls.length).toBe(2);\n      expect(spy.console.mock.calls).toEqual([\n        [\n          new Error(\n            \"thrown from handleDocumentRequest and expected to be logged in console only once\",\n          ),\n        ],\n        [new Error(\"second error thrown from handleDocumentRequest\")],\n      ]);\n    });\n\n    test(\"aborts request\", async () => {\n      let rootLoader = jest.fn(() => {\n        return \"root\";\n      });\n      let indexLoader = jest.fn(async () => {\n        await new Promise((r) => setTimeout(r, 10));\n        return \"index\";\n      });\n      let handleErrorSpy = jest.fn();\n      let build = mockServerBuild(\n        {\n          root: {\n            default: {},\n            loader: rootLoader,\n          },\n          \"routes/resource\": {\n            loader: indexLoader,\n            index: true,\n            default: {},\n          },\n        },\n        {\n          handleError: handleErrorSpy,\n        },\n      );\n      let handler = createRequestHandler(build, ServerMode.Test);\n\n      let controller = new AbortController();\n      let request = new Request(`${baseUrl}/`, {\n        method: \"get\",\n        signal: controller.signal,\n      });\n\n      let resultPromise = handler(request);\n      controller.abort();\n      let result = await resultPromise;\n      expect(result.status).toBe(500);\n      expect(build.entry.module.default.mock.calls.length).toBe(0);\n\n      expect(handleErrorSpy).toHaveBeenCalledTimes(1);\n      expect(handleErrorSpy.mock.calls[0][0] instanceof DOMException).toBe(\n        true,\n      );\n      expect(handleErrorSpy.mock.calls[0][0].name).toBe(\"AbortError\");\n      expect(handleErrorSpy.mock.calls[0][0].message).toBe(\n        \"This operation was aborted\",\n      );\n      expect(handleErrorSpy.mock.calls[0][1].request.method).toBe(\"GET\");\n      expect(handleErrorSpy.mock.calls[0][1].request.url).toBe(\n        \"http://test.com/\",\n      );\n    });\n  });\n\n  test(\"provides load context to server entrypoint\", async () => {\n    let rootLoader = jest.fn(() => {\n      return \"root\";\n    });\n    let indexLoader = jest.fn(() => {\n      return \"index\";\n    });\n    let build = mockServerBuild(\n      {\n        root: {\n          default: {},\n          loader: rootLoader,\n          ErrorBoundary: {},\n        },\n        \"routes/_index\": {\n          parentId: \"root\",\n          default: {},\n          loader: indexLoader,\n        },\n      },\n      {\n        handleDocumentRequest(request, responseStatusCode, responseHeaders) {\n          return new Response(JSON.stringify(loadContext), {\n            status: responseStatusCode,\n            headers: responseHeaders,\n          });\n        },\n      },\n    );\n\n    let handler = createRequestHandler(build, ServerMode.Development);\n    let request = new Request(`${baseUrl}/`, { method: \"get\" });\n    let loadContext = { \"load-context\": \"load-value\" };\n\n    let result = await handler(request, loadContext);\n    expect(await result.text()).toBe(JSON.stringify(loadContext));\n  });\n});\n"
  },
  {
    "path": "packages/react-router/__tests__/server-runtime/sessions-test.ts",
    "content": "/**\n * @jest-environment node\n */\n\nimport { createSession, isSession } from \"../../lib/server-runtime/sessions\";\nimport { createCookieSessionStorage } from \"../../lib/server-runtime/sessions/cookieStorage\";\nimport { createMemorySessionStorage } from \"../../lib/server-runtime/sessions/memoryStorage\";\n\nfunction getCookieFromSetCookie(setCookie: string): string {\n  return setCookie.split(/;\\s*/)[0];\n}\n\ndescribe(\"Session\", () => {\n  it(\"has an empty id by default\", () => {\n    expect(createSession().id).toEqual(\"\");\n  });\n\n  it(\"correctly stores and retrieves values\", () => {\n    let session = createSession();\n\n    session.set(\"user\", \"mjackson\");\n    session.flash(\"error\", \"boom\");\n\n    expect(session.has(\"user\")).toBe(true);\n    expect(session.get(\"user\")).toBe(\"mjackson\");\n    // Normal values should remain in the session after get()\n    expect(session.has(\"user\")).toBe(true);\n    expect(session.get(\"user\")).toBe(\"mjackson\");\n\n    expect(session.has(\"error\")).toBe(true);\n    expect(session.get(\"error\")).toBe(\"boom\");\n    // Flash values disappear after the first get()\n    expect(session.has(\"error\")).toBe(false);\n    expect(session.get(\"error\")).toBeUndefined();\n\n    session.unset(\"user\");\n\n    expect(session.has(\"user\")).toBe(false);\n    expect(session.get(\"user\")).toBeUndefined();\n  });\n});\n\ndescribe(\"isSession\", () => {\n  it(\"returns `true` for Session objects\", () => {\n    expect(isSession(createSession())).toBe(true);\n  });\n\n  it(\"returns `false` for non-Session objects\", () => {\n    expect(isSession({})).toBe(false);\n    expect(isSession([])).toBe(false);\n    expect(isSession(\"\")).toBe(false);\n    expect(isSession(true)).toBe(false);\n  });\n});\n\ndescribe(\"In-memory session storage\", () => {\n  it(\"persists session data across requests\", async () => {\n    let { getSession, commitSession } = createMemorySessionStorage({\n      cookie: { secrets: [\"secret1\"] },\n    });\n    let session = await getSession();\n    session.set(\"user\", \"mjackson\");\n    let setCookie = await commitSession(session);\n    session = await getSession(getCookieFromSetCookie(setCookie));\n\n    expect(session.get(\"user\")).toEqual(\"mjackson\");\n  });\n\n  it(\"uses random hash keys as session ids\", async () => {\n    let { getSession, commitSession } = createMemorySessionStorage({\n      cookie: { secrets: [\"secret1\"] },\n    });\n    let session = await getSession();\n    session.set(\"user\", \"mjackson\");\n    let setCookie = await commitSession(session);\n    session = await getSession(getCookieFromSetCookie(setCookie));\n    expect(session.id).toMatch(/^[a-z0-9]{8}$/);\n  });\n});\n\ndescribe(\"Cookie session storage\", () => {\n  it(\"persists session data across requests\", async () => {\n    let { getSession, commitSession } = createCookieSessionStorage({\n      cookie: { secrets: [\"secret1\"] },\n    });\n    let session = await getSession();\n    session.set(\"user\", \"mjackson\");\n    let setCookie = await commitSession(session);\n    session = await getSession(getCookieFromSetCookie(setCookie));\n\n    expect(session.get(\"user\")).toEqual(\"mjackson\");\n  });\n\n  it(\"returns an empty session for cookies that are not signed properly\", async () => {\n    let { getSession, commitSession } = createCookieSessionStorage({\n      cookie: { secrets: [\"secret1\"] },\n    });\n    let session = await getSession();\n    session.set(\"user\", \"mjackson\");\n\n    expect(session.get(\"user\")).toEqual(\"mjackson\");\n\n    let setCookie = await commitSession(session);\n    session = await getSession(\n      // Tamper with the session cookie...\n      getCookieFromSetCookie(setCookie).slice(0, -1),\n    );\n\n    expect(session.get(\"user\")).toBeUndefined();\n  });\n\n  it('\"makes the default path of cookies to be /', async () => {\n    let { getSession, commitSession } = createCookieSessionStorage({\n      cookie: { secrets: [\"secret1\"] },\n    });\n    let session = await getSession();\n    session.set(\"user\", \"mjackson\");\n    let setCookie = await commitSession(session);\n    expect(setCookie).toContain(\"Path=/\");\n  });\n\n  it(\"throws an error when the cookie size exceeds 4096 bytes\", async () => {\n    let { getSession, commitSession } = createCookieSessionStorage({\n      cookie: { secrets: [\"secret1\"] },\n    });\n    let session = await getSession();\n    let longString = new Array(4097).fill(\"a\").join(\"\");\n    session.set(\"over4096bytes\", longString);\n    await expect(() => commitSession(session)).rejects.toThrow();\n  });\n\n  it(\"destroys sessions using a past date\", async () => {\n    let spy = jest.spyOn(console, \"warn\").mockImplementation(() => {});\n    let { getSession, destroySession } = createCookieSessionStorage({\n      cookie: {\n        secrets: [\"secret1\"],\n      },\n    });\n    let session = await getSession();\n    let setCookie = await destroySession(session);\n    expect(setCookie).toMatchInlineSnapshot(\n      `\"__session=; Path=/; Expires=Thu, 01 Jan 1970 00:00:00 GMT; SameSite=Lax\"`,\n    );\n    spy.mockRestore();\n  });\n\n  it(\"destroys sessions that leverage maxAge\", async () => {\n    let spy = jest.spyOn(console, \"warn\").mockImplementation(() => {});\n    let { getSession, destroySession } = createCookieSessionStorage({\n      cookie: {\n        maxAge: 60 * 60, // 1 hour\n        secrets: [\"secret1\"],\n      },\n    });\n    let session = await getSession();\n    let setCookie = await destroySession(session);\n    expect(setCookie).toMatchInlineSnapshot(\n      `\"__session=; Path=/; Expires=Thu, 01 Jan 1970 00:00:00 GMT; SameSite=Lax\"`,\n    );\n    spy.mockRestore();\n  });\n\n  describe(\"warnings when providing options you may not want to\", () => {\n    let spy = spyConsole();\n\n    it(\"warns against using `expires` when creating the session\", async () => {\n      createCookieSessionStorage({\n        cookie: {\n          secrets: [\"secret1\"],\n          expires: new Date(Date.now() + 60_000),\n        },\n      });\n\n      expect(spy.console).toHaveBeenCalledTimes(1);\n      expect(spy.console).toHaveBeenCalledWith(\n        'The \"__session\" cookie has an \"expires\" property set. This will cause the expires value to not be updated when the session is committed. Instead, you should set the expires value when serializing the cookie. You can use `commitSession(session, { expires })` if using a session storage object, or `cookie.serialize(\"value\", { expires })` if you\\'re using the cookie directly.',\n      );\n    });\n\n    it(\"warns when not passing secrets when creating the session\", async () => {\n      createCookieSessionStorage({ cookie: {} });\n\n      expect(spy.console).toHaveBeenCalledTimes(1);\n      expect(spy.console).toHaveBeenCalledWith(\n        'The \"__session\" cookie is not signed, but session cookies should be signed to prevent tampering on the client before they are sent back to the server. See https://reactrouter.com/explanation/sessions-and-cookies#signing-cookies for more information.',\n      );\n    });\n  });\n\n  describe(\"when a new secret shows up in the rotation\", () => {\n    it(\"unsigns old session cookies using the old secret and encodes new cookies using the new secret\", async () => {\n      let { getSession, commitSession } = createCookieSessionStorage({\n        cookie: { secrets: [\"secret1\"] },\n      });\n      let session = await getSession();\n      session.set(\"user\", \"mjackson\");\n      let setCookie = await commitSession(session);\n      session = await getSession(getCookieFromSetCookie(setCookie));\n\n      expect(session.get(\"user\")).toEqual(\"mjackson\");\n\n      // A new secret enters the rotation...\n      let storage = createCookieSessionStorage({\n        cookie: { secrets: [\"secret2\", \"secret1\"] },\n      });\n      getSession = storage.getSession;\n      commitSession = storage.commitSession;\n\n      // Old cookies should still work with the old secret.\n      session = await storage.getSession(getCookieFromSetCookie(setCookie));\n      expect(session.get(\"user\")).toEqual(\"mjackson\");\n\n      // New cookies should be signed using the new secret.\n      let setCookie2 = await storage.commitSession(session);\n      expect(setCookie2).not.toEqual(setCookie);\n    });\n  });\n});\n\nfunction spyConsole() {\n  // https://github.com/facebook/react/issues/7047\n  let spy: any = {};\n\n  beforeAll(() => {\n    spy.console = jest.spyOn(console, \"warn\").mockImplementation(() => {});\n  });\n\n  beforeEach(() => {\n    spy.console.mockClear();\n  });\n\n  afterAll(() => {\n    spy.console.mockRestore();\n  });\n\n  return spy;\n}\n"
  },
  {
    "path": "packages/react-router/__tests__/server-runtime/utils.ts",
    "content": "import type { FutureConfig } from \"../../lib/dom/ssr/entry\";\nimport type {\n  ServerRoute,\n  ServerRouteManifest,\n} from \"../../lib/server-runtime/routes\";\nimport type {\n  HandleDocumentRequestFunction,\n  HandleErrorFunction,\n  ServerBuild,\n} from \"../../lib/server-runtime/build\";\nimport type { HeadersFunction } from \"../../lib/dom/ssr/routeModules\";\nimport type { EntryRoute } from \"../../lib/dom/ssr/routes\";\nimport type {\n  ActionFunction,\n  LoaderFunction,\n  MiddlewareFunction,\n} from \"../../lib/router/utils\";\nimport type { unstable_ServerInstrumentation } from \"../../lib/router/instrumentation\";\n\nexport function mockServerBuild(\n  routes: Record<\n    string,\n    {\n      parentId?: string;\n      index?: true;\n      path?: string;\n      default?: any;\n      ErrorBoundary?: any;\n      action?: ActionFunction;\n      headers?: HeadersFunction;\n      loader?: LoaderFunction;\n      middleware?: MiddlewareFunction<Response>[];\n    }\n  >,\n  opts: {\n    future?: Partial<FutureConfig>;\n    handleError?: HandleErrorFunction;\n    handleDocumentRequest?: HandleDocumentRequestFunction;\n    unstable_instrumentations?: unstable_ServerInstrumentation[];\n  } = {},\n): ServerBuild {\n  return {\n    ssr: true,\n    future: {\n      v8_middleware: false,\n      unstable_subResourceIntegrity: false,\n      ...opts.future,\n    },\n    prerender: [],\n    isSpaMode: false,\n    routeDiscovery: {\n      mode: \"lazy\",\n      manifestPath: \"/__manifest\",\n    },\n    assetsBuildDirectory: \"\",\n    publicPath: \"\",\n    assets: {\n      entry: {\n        imports: [\"\"],\n        module: \"\",\n      },\n      routes: Object.entries(routes).reduce((p, [id, config]) => {\n        let route: EntryRoute = {\n          hasAction: !!config.action,\n          hasErrorBoundary: !!config.ErrorBoundary,\n          hasLoader: !!config.loader,\n          hasClientAction: false,\n          hasClientLoader: false,\n          hasClientMiddleware: false,\n          clientActionModule: undefined,\n          clientLoaderModule: undefined,\n          clientMiddlewareModule: undefined,\n          hydrateFallbackModule: undefined,\n          id,\n          module: \"\",\n          index: config.index,\n          path: config.path,\n          parentId: config.parentId,\n        };\n        return {\n          ...p,\n          [id]: route,\n        };\n      }, {}),\n      url: \"\",\n      version: \"\",\n    },\n    entry: {\n      module: {\n        default:\n          opts.handleDocumentRequest ||\n          jest.fn(\n            async (request, responseStatusCode, responseHeaders) =>\n              new Response(null, {\n                status: responseStatusCode,\n                headers: responseHeaders,\n              }),\n          ),\n        handleDataRequest: jest.fn(async (response) => response),\n        handleError: opts.handleError,\n        unstable_instrumentations: opts.unstable_instrumentations,\n      },\n    },\n    routes: Object.entries(routes).reduce<ServerRouteManifest>(\n      (p, [id, config]) => {\n        let route: Omit<ServerRoute, \"children\"> = {\n          id,\n          index: config.index,\n          path: config.path,\n          parentId: config.parentId,\n          module: {\n            default: config.default,\n            ErrorBoundary: config.ErrorBoundary,\n            action: config.action,\n            loader: config.loader,\n            middleware: config.middleware,\n          },\n        };\n        return {\n          ...p,\n          [id]: route,\n        };\n      },\n      {},\n    ),\n  };\n}\n"
  },
  {
    "path": "packages/react-router/__tests__/setup.ts",
    "content": "// https://reactjs.org/blog/2022/03/08/react-18-upgrade-guide.html#configuring-your-testing-environment\nglobalThis.IS_REACT_ACT_ENVIRONMENT = true;\n\nif (!globalThis.TextEncoder || !globalThis.TextDecoder) {\n  const { TextDecoder, TextEncoder } = require(\"node:util\");\n  globalThis.TextEncoder = TextEncoder;\n  globalThis.TextDecoder = TextDecoder;\n}\n\nif (!globalThis.ReadableStream || !globalThis.WritableStream) {\n  const { ReadableStream, WritableStream } = require(\"node:stream/web\");\n  globalThis.ReadableStream = ReadableStream;\n  globalThis.WritableStream = WritableStream;\n}\n\nif (!globalThis.fetch) {\n  const { fetch, FormData, Request, Response, Headers } = require(\"undici\");\n\n  globalThis.fetch = fetch;\n  globalThis.Request = Request;\n  globalThis.Response = Response;\n  globalThis.Headers = Headers;\n\n  globalThis.FormData = globalThis.FormData || FormData;\n}\n\nif (!globalThis.TextEncoderStream) {\n  const { TextEncoderStream } = require(\"node:stream/web\");\n  globalThis.TextEncoderStream = TextEncoderStream;\n}\n\nif (!globalThis.TextDecoderStream) {\n  const { TextDecoderStream } = require(\"node:stream/web\");\n  globalThis.TextDecoderStream = TextDecoderStream;\n}\n\nif (!globalThis.TransformStream) {\n  const { TransformStream } = require(\"node:stream/web\");\n  globalThis.TransformStream = TransformStream;\n}\n\nconst consoleError = console.error;\nconsole.error = (msg, ...args) => {\n  if (\n    typeof msg === \"string\" &&\n    msg.includes(\"react-test-renderer is deprecated\")\n  ) {\n    return;\n  }\n  consoleError.call(console, msg, ...args);\n};\n"
  },
  {
    "path": "packages/react-router/__tests__/use-revalidator-test.tsx",
    "content": "import \"@testing-library/jest-dom\";\nimport {\n  fireEvent,\n  queryByText,\n  render,\n  screen,\n  waitFor,\n} from \"@testing-library/react\";\nimport * as React from \"react\";\nimport {\n  Outlet,\n  Route,\n  RouterProvider,\n  createMemoryRouter,\n  createRoutesFromElements,\n  useLoaderData,\n  useNavigation,\n  useRevalidator,\n  useRouteError,\n} from \"react-router\";\nimport MemoryNavigate from \"./utils/MemoryNavigate\";\nimport getHtml from \"./utils/getHtml\";\n\ndescribe(\"useRevalidator\", () => {\n  it(\"reloads data using useRevalidator\", async () => {\n    let count = 1;\n    let router = createMemoryRouter(\n      createRoutesFromElements(\n        <Route path=\"/\" element={<Layout />}>\n          <Route\n            path=\"foo\"\n            loader={async () => `count=${++count}`}\n            element={<Foo />}\n          />\n        </Route>,\n      ),\n      {\n        initialEntries: [\"/foo\"],\n        hydrationData: {\n          loaderData: {\n            \"0-0\": \"count=1\",\n          },\n        },\n      },\n    );\n    let { container } = render(<RouterProvider router={router} />);\n\n    function Layout() {\n      let navigation = useNavigation();\n      let { revalidate, state } = useRevalidator();\n      return (\n        <div>\n          <button onClick={() => revalidate()}>Revalidate</button>\n          <p>{navigation.state}</p>\n          <p>{state}</p>\n          <Outlet />\n        </div>\n      );\n    }\n\n    function Foo() {\n      let data = useLoaderData() as string;\n      return <p>{data}</p>;\n    }\n\n    expect(getHtml(container)).toMatchInlineSnapshot(`\n    \"<div>\n      <div>\n        <button>\n          Revalidate\n        </button>\n        <p>\n          idle\n        </p>\n        <p>\n          idle\n        </p>\n        <p>\n          count=1\n        </p>\n      </div>\n    </div>\"\n  `);\n\n    fireEvent.click(screen.getByText(\"Revalidate\"));\n    expect(getHtml(container)).toMatchInlineSnapshot(`\n    \"<div>\n      <div>\n        <button>\n          Revalidate\n        </button>\n        <p>\n          idle\n        </p>\n        <p>\n          loading\n        </p>\n        <p>\n          count=1\n        </p>\n      </div>\n    </div>\"\n  `);\n\n    await waitFor(() => screen.getByText(\"count=2\"));\n    expect(getHtml(container)).toMatchInlineSnapshot(`\n    \"<div>\n      <div>\n        <button>\n          Revalidate\n        </button>\n        <p>\n          idle\n        </p>\n        <p>\n          idle\n        </p>\n        <p>\n          count=2\n        </p>\n      </div>\n    </div>\"\n  `);\n  });\n\n  it(\"allows a successful useRevalidator to resolve the error boundary (loader + child boundary)\", async () => {\n    let shouldFail = true;\n    let router = createMemoryRouter(\n      createRoutesFromElements(\n        <Route\n          path=\"/\"\n          Component={() => (\n            <>\n              <MemoryNavigate to=\"child\">/child</MemoryNavigate>\n              <Outlet />\n            </>\n          )}\n        >\n          <Route\n            path=\"child\"\n            loader={() => {\n              if (shouldFail) {\n                shouldFail = false;\n                throw new Error(\"Broken\");\n              } else {\n                return \"Fixed\";\n              }\n            }}\n            Component={() => <p>{(\"Child:\" + useLoaderData()) as string}</p>}\n            ErrorBoundary={() => {\n              let { revalidate } = useRevalidator();\n              return (\n                <>\n                  <p>{\"Error:\" + (useRouteError() as Error).message}</p>\n                  <button onClick={() => revalidate()}>Try again</button>\n                </>\n              );\n            }}\n          />\n        </Route>,\n      ),\n    );\n\n    let { container } = render(\n      <div>\n        <RouterProvider router={router} />\n      </div>,\n    );\n\n    fireEvent.click(screen.getByText(\"/child\"));\n    await waitFor(() => screen.getByText(\"Error:Broken\"));\n    expect(getHtml(container)).toMatch(\"Error:Broken\");\n    expect(router.state.errors).not.toBe(null);\n\n    fireEvent.click(screen.getByText(\"Try again\"));\n    await waitFor(() => {\n      expect(queryByText(container, \"Child:Fixed\")).toBeInTheDocument();\n    });\n    expect(getHtml(container)).toMatch(\"Child:Fixed\");\n    expect(router.state.errors).toBe(null);\n  });\n\n  it(\"allows a successful useRevalidator to resolve the error boundary (loader + parent boundary)\", async () => {\n    let shouldFail = true;\n    let router = createMemoryRouter(\n      createRoutesFromElements(\n        <Route\n          path=\"/\"\n          Component={() => (\n            <>\n              <MemoryNavigate to=\"child\">/child</MemoryNavigate>\n              <Outlet />\n            </>\n          )}\n          ErrorBoundary={() => {\n            let { revalidate } = useRevalidator();\n            return (\n              <>\n                <p>{\"Error:\" + (useRouteError() as Error).message}</p>\n                <button onClick={() => revalidate()}>Try again</button>\n              </>\n            );\n          }}\n        >\n          <Route\n            path=\"child\"\n            loader={() => {\n              if (shouldFail) {\n                shouldFail = false;\n                throw new Error(\"Broken\");\n              } else {\n                return \"Fixed\";\n              }\n            }}\n            Component={() => <p>{(\"Child:\" + useLoaderData()) as string}</p>}\n          />\n        </Route>,\n      ),\n    );\n\n    let { container } = render(\n      <div>\n        <RouterProvider router={router} />\n      </div>,\n    );\n\n    fireEvent.click(screen.getByText(\"/child\"));\n    await waitFor(() => screen.getByText(\"Error:Broken\"));\n    expect(getHtml(container)).toMatch(\"Error:Broken\");\n    expect(router.state.errors).not.toBe(null);\n\n    fireEvent.click(screen.getByText(\"Try again\"));\n    await waitFor(() => {\n      expect(queryByText(container, \"Child:Fixed\")).toBeInTheDocument();\n    });\n    expect(getHtml(container)).toMatch(\"Child:Fixed\");\n    expect(router.state.errors).toBe(null);\n  });\n\n  it(\"is stable across location changes\", async () => {\n    let count = 0;\n    let router = createMemoryRouter([\n      {\n        path: \"/\",\n        Component() {\n          let revalidator = useRevalidator();\n\n          React.useEffect(() => {\n            count++;\n          }, [revalidator]);\n          return (\n            <div>\n              <MemoryNavigate to=\"/\">Link to Home</MemoryNavigate>{\" \"}\n              <MemoryNavigate to=\"/foo\">Link to Foo</MemoryNavigate>\n              <Outlet />\n            </div>\n          );\n        },\n        children: [\n          {\n            index: true,\n            Component() {\n              return <h1>Home Page</h1>;\n            },\n          },\n          {\n            path: \"foo\",\n            Component() {\n              return <h1>Foo Page</h1>;\n            },\n          },\n        ],\n      },\n    ]);\n\n    render(<RouterProvider router={router} />);\n\n    fireEvent.click(screen.getByText(\"Link to Foo\"));\n    await waitFor(() => screen.getByText(\"Foo Page\"));\n\n    fireEvent.click(screen.getByText(\"Link to Home\"));\n    await waitFor(() => screen.getByText(\"Home Page\"));\n\n    expect(count).toBe(1);\n  });\n\n  it(\"is stable across revalidation changes\", async () => {\n    let uiCount = 0;\n    let stableCount = 0;\n    let unstableCount = 0;\n    let router = createMemoryRouter(\n      [\n        {\n          id: \"root\",\n          path: \"/\",\n          loader: () => ++uiCount,\n          Component() {\n            let { revalidate, state } = useRevalidator();\n\n            React.useEffect(() => void stableCount++, [revalidate]);\n            React.useEffect(() => void unstableCount++, [state]);\n\n            return (\n              <button onClick={() => revalidate()}>\n                Revalidate: {useLoaderData()}\n              </button>\n            );\n          },\n        },\n      ],\n      {\n        hydrationData: {\n          loaderData: { root: 0 },\n        },\n      },\n    );\n\n    render(<RouterProvider router={router} />);\n\n    fireEvent.click(screen.getByText(\"Revalidate: 0\"));\n    await waitFor(() => screen.getByText(\"Revalidate: 1\"));\n\n    fireEvent.click(screen.getByText(\"Revalidate: 1\"));\n    await waitFor(() => screen.getByText(\"Revalidate: 2\"));\n\n    await new Promise((resolve) => setTimeout(resolve, 0));\n\n    // idle -> loading -> idle -> loading -> idle\n    expect(unstableCount).toBe(5);\n    expect(stableCount).toBe(1);\n  });\n});\n"
  },
  {
    "path": "packages/react-router/__tests__/useHref-basename-test.tsx",
    "content": "import * as React from \"react\";\nimport * as TestRenderer from \"react-test-renderer\";\nimport { MemoryRouter, Routes, Route, useHref } from \"react-router\";\n\nfunction ShowHref({ to }: { to: string }) {\n  return <p>{useHref(to)}</p>;\n}\n\ndescribe(\"useHref under a basename\", () => {\n  describe(\"to an absolute route\", () => {\n    it(\"returns the correct href\", () => {\n      let renderer: TestRenderer.ReactTestRenderer;\n      TestRenderer.act(() => {\n        renderer = TestRenderer.create(\n          <MemoryRouter basename=\"/app\" initialEntries={[\"/app/admin\"]}>\n            <Routes>\n              <Route path=\"admin\" element={<ShowHref to=\"/invoices\" />} />\n            </Routes>\n          </MemoryRouter>,\n        );\n      });\n\n      expect(renderer.toJSON()).toMatchInlineSnapshot(`\n        <p>\n          /app/invoices\n        </p>\n      `);\n    });\n  });\n\n  describe(\"to a child route\", () => {\n    it(\"returns the correct href\", () => {\n      let renderer: TestRenderer.ReactTestRenderer;\n      TestRenderer.act(() => {\n        renderer = TestRenderer.create(\n          <MemoryRouter basename=\"/app\" initialEntries={[\"/app/admin\"]}>\n            <Routes>\n              <Route path=\"admin\" element={<ShowHref to=\"invoices\" />} />\n            </Routes>\n          </MemoryRouter>,\n        );\n      });\n\n      expect(renderer.toJSON()).toMatchInlineSnapshot(`\n        <p>\n          /app/admin/invoices\n        </p>\n      `);\n    });\n\n    describe(\"when the URL has a trailing slash\", () => {\n      it(\"returns the correct href\", () => {\n        let renderer: TestRenderer.ReactTestRenderer;\n        TestRenderer.act(() => {\n          renderer = TestRenderer.create(\n            <MemoryRouter basename=\"/app\" initialEntries={[\"/app/admin/\"]}>\n              <Routes>\n                <Route path=\"admin\" element={<ShowHref to=\"invoices\" />} />\n              </Routes>\n            </MemoryRouter>,\n          );\n        });\n\n        expect(renderer.toJSON()).toMatchInlineSnapshot(`\n          <p>\n            /app/admin/invoices\n          </p>\n        `);\n      });\n    });\n\n    describe(\"when the href has a trailing slash\", () => {\n      it(\"returns the correct href\", () => {\n        let renderer: TestRenderer.ReactTestRenderer;\n        TestRenderer.act(() => {\n          renderer = TestRenderer.create(\n            <MemoryRouter basename=\"/app\" initialEntries={[\"/app/admin\"]}>\n              <Routes>\n                <Route path=\"admin\" element={<ShowHref to=\"invoices/\" />} />\n              </Routes>\n            </MemoryRouter>,\n          );\n        });\n\n        expect(renderer.toJSON()).toMatchInlineSnapshot(`\n          <p>\n            /app/admin/invoices/\n          </p>\n        `);\n      });\n    });\n  });\n\n  describe(\"to a sibling route\", () => {\n    it(\"returns the correct href\", () => {\n      let renderer: TestRenderer.ReactTestRenderer;\n      TestRenderer.act(() => {\n        renderer = TestRenderer.create(\n          <MemoryRouter basename=\"/app\" initialEntries={[\"/app/admin\"]}>\n            <Routes>\n              <Route path=\"admin\" element={<ShowHref to=\"../dashboard\" />} />\n            </Routes>\n          </MemoryRouter>,\n        );\n      });\n\n      expect(renderer.toJSON()).toMatchInlineSnapshot(`\n        <p>\n          /app/dashboard\n        </p>\n      `);\n    });\n\n    describe(\"when the URL has a trailing slash\", () => {\n      it(\"returns the correct href\", () => {\n        let renderer: TestRenderer.ReactTestRenderer;\n        TestRenderer.act(() => {\n          renderer = TestRenderer.create(\n            <MemoryRouter basename=\"/app\" initialEntries={[\"/app/admin/\"]}>\n              <Routes>\n                <Route path=\"admin\" element={<ShowHref to=\"../dashboard\" />} />\n              </Routes>\n            </MemoryRouter>,\n          );\n        });\n\n        expect(renderer.toJSON()).toMatchInlineSnapshot(`\n          <p>\n            /app/dashboard\n          </p>\n        `);\n      });\n    });\n\n    describe(\"when the href has a trailing slash\", () => {\n      it(\"returns the correct href\", () => {\n        let renderer: TestRenderer.ReactTestRenderer;\n        TestRenderer.act(() => {\n          renderer = TestRenderer.create(\n            <MemoryRouter basename=\"/app\" initialEntries={[\"/app/admin\"]}>\n              <Routes>\n                <Route path=\"admin\" element={<ShowHref to=\"../dashboard/\" />} />\n              </Routes>\n            </MemoryRouter>,\n          );\n        });\n\n        expect(renderer.toJSON()).toMatchInlineSnapshot(`\n          <p>\n            /app/dashboard/\n          </p>\n        `);\n      });\n    });\n  });\n\n  describe(\"to a parent route\", () => {\n    it(\"returns the correct href\", () => {\n      let renderer: TestRenderer.ReactTestRenderer;\n      TestRenderer.act(() => {\n        renderer = TestRenderer.create(\n          <MemoryRouter basename=\"/app\" initialEntries={[\"/app/admin\"]}>\n            <Routes>\n              <Route path=\"admin\" element={<ShowHref to=\"..\" />} />\n            </Routes>\n          </MemoryRouter>,\n        );\n      });\n\n      expect(renderer.toJSON()).toMatchInlineSnapshot(`\n        <p>\n          /app\n        </p>\n      `);\n    });\n\n    describe(\"when the URL has a trailing slash\", () => {\n      it(\"returns the correct href\", () => {\n        let renderer: TestRenderer.ReactTestRenderer;\n        TestRenderer.act(() => {\n          renderer = TestRenderer.create(\n            <MemoryRouter basename=\"/app\" initialEntries={[\"/app/admin/\"]}>\n              <Routes>\n                <Route path=\"admin\" element={<ShowHref to=\"..\" />} />\n              </Routes>\n            </MemoryRouter>,\n          );\n        });\n\n        expect(renderer.toJSON()).toMatchInlineSnapshot(`\n          <p>\n            /app\n          </p>\n        `);\n      });\n    });\n\n    describe(\"when the href has a trailing slash\", () => {\n      it(\"returns the correct href\", () => {\n        let renderer: TestRenderer.ReactTestRenderer;\n        TestRenderer.act(() => {\n          renderer = TestRenderer.create(\n            <MemoryRouter basename=\"/app\" initialEntries={[\"/app/admin\"]}>\n              <Routes>\n                <Route path=\"admin\" element={<ShowHref to=\"../\" />} />\n              </Routes>\n            </MemoryRouter>,\n          );\n        });\n\n        expect(renderer.toJSON()).toMatchInlineSnapshot(`\n          <p>\n            /app\n          </p>\n        `);\n      });\n    });\n  });\n\n  describe(\"with a to value that has more .. segments than the current URL\", () => {\n    it(\"returns the correct href\", () => {\n      let renderer: TestRenderer.ReactTestRenderer;\n      TestRenderer.act(() => {\n        renderer = TestRenderer.create(\n          <MemoryRouter basename=\"/app\" initialEntries={[\"/app/admin\"]}>\n            <Routes>\n              <Route\n                path=\"admin\"\n                element={<ShowHref to=\"../../../dashboard\" />}\n              />\n            </Routes>\n          </MemoryRouter>,\n        );\n      });\n\n      // This is correct because the basename works like a chroot \"jail\".\n      // Relative <Link to> values cannot \"escape\" into a higher level URL since\n      // they would be linking to a URL that the <Router> cannot render. To link\n      // to a higher URL path, use a plain <a>.\n      expect(renderer.toJSON()).toMatchInlineSnapshot(`\n        <p>\n          /app/dashboard\n        </p>\n      `);\n    });\n\n    describe(\"and no additional segments\", () => {\n      it(\"returns the correct href\", () => {\n        let renderer: TestRenderer.ReactTestRenderer;\n        TestRenderer.act(() => {\n          renderer = TestRenderer.create(\n            <MemoryRouter basename=\"/app\" initialEntries={[\"/app/admin\"]}>\n              <Routes>\n                <Route path=\"admin\" element={<ShowHref to=\"../../..\" />} />\n              </Routes>\n            </MemoryRouter>,\n          );\n        });\n\n        // This is correct because the basename works like a chroot \"jail\".\n        // Relative <Link to> values cannot \"escape\" into a higher level URL\n        // since they would be linking to a URL that the <Router> cannot render.\n        // To link to a higher URL path, use a plain <a>.\n        expect(renderer.toJSON()).toMatchInlineSnapshot(`\n          <p>\n            /app\n          </p>\n        `);\n      });\n    });\n\n    describe(\"when the URL has a trailing slash\", () => {\n      it(\"returns the correct href\", () => {\n        let renderer: TestRenderer.ReactTestRenderer;\n        TestRenderer.act(() => {\n          renderer = TestRenderer.create(\n            <MemoryRouter basename=\"/app\" initialEntries={[\"/app/admin/\"]}>\n              <Routes>\n                <Route\n                  path=\"admin\"\n                  element={<ShowHref to=\"../../../dashboard\" />}\n                />\n              </Routes>\n            </MemoryRouter>,\n          );\n        });\n\n        // This is correct because the basename works like a chroot \"jail\".\n        // Relative <Link to> values cannot \"escape\" into a higher level URL\n        // since they would be linking to a URL that the <Router> cannot render.\n        // To link to a higher URL path, use a plain <a>.\n        expect(renderer.toJSON()).toMatchInlineSnapshot(`\n          <p>\n            /app/dashboard\n          </p>\n        `);\n      });\n    });\n\n    describe(\"when the href has a trailing slash\", () => {\n      it(\"returns the correct href\", () => {\n        let renderer: TestRenderer.ReactTestRenderer;\n        TestRenderer.act(() => {\n          renderer = TestRenderer.create(\n            <MemoryRouter basename=\"/app\" initialEntries={[\"/app/admin\"]}>\n              <Routes>\n                <Route\n                  path=\"admin\"\n                  element={<ShowHref to=\"../../../dashboard/\" />}\n                />\n              </Routes>\n            </MemoryRouter>,\n          );\n        });\n\n        expect(renderer.toJSON()).toMatchInlineSnapshot(`\n          <p>\n            /app/dashboard/\n          </p>\n        `);\n      });\n    });\n  });\n\n  describe(\"after an update\", () => {\n    it(\"does not change\", () => {\n      let element = (\n        <MemoryRouter basename=\"/app\" initialEntries={[\"/app/admin\"]}>\n          <Routes>\n            <Route path=\"admin\" element={<ShowHref to=\"/invoices\" />} />\n          </Routes>\n        </MemoryRouter>\n      );\n\n      let renderer: TestRenderer.ReactTestRenderer;\n      TestRenderer.act(() => {\n        renderer = TestRenderer.create(element);\n      });\n\n      TestRenderer.act(() => {\n        renderer.update(element);\n      });\n\n      expect(renderer.toJSON()).toMatchInlineSnapshot(`\n        <p>\n          /app/invoices\n        </p>\n      `);\n    });\n  });\n});\n"
  },
  {
    "path": "packages/react-router/__tests__/useHref-test.tsx",
    "content": "import * as React from \"react\";\nimport * as TestRenderer from \"react-test-renderer\";\nimport { MemoryRouter, Routes, Route, useHref } from \"react-router\";\n\nfunction ShowHref({ to }: { to: string }) {\n  return <pre>{useHref(to)}</pre>;\n}\n\ndescribe(\"useHref\", () => {\n  describe(\"to a child route\", () => {\n    it(\"returns the correct href\", () => {\n      let renderer: TestRenderer.ReactTestRenderer;\n      TestRenderer.act(() => {\n        renderer = TestRenderer.create(\n          <MemoryRouter initialEntries={[\"/courses\"]}>\n            <Routes>\n              <Route\n                path=\"courses\"\n                element={<ShowHref to=\"advanced-react\" />}\n              />\n            </Routes>\n          </MemoryRouter>,\n        );\n      });\n\n      expect(renderer.toJSON()).toMatchInlineSnapshot(`\n        <pre>\n          /courses/advanced-react\n        </pre>\n      `);\n    });\n\n    describe(\"when the URL has a trailing slash\", () => {\n      it(\"returns the correct href\", () => {\n        let renderer: TestRenderer.ReactTestRenderer;\n        TestRenderer.act(() => {\n          renderer = TestRenderer.create(\n            <MemoryRouter initialEntries={[\"/courses/\"]}>\n              <Routes>\n                <Route\n                  path=\"courses\"\n                  element={<ShowHref to=\"advanced-react\" />}\n                />\n              </Routes>\n            </MemoryRouter>,\n          );\n        });\n\n        expect(renderer.toJSON()).toMatchInlineSnapshot(`\n          <pre>\n            /courses/advanced-react\n          </pre>\n        `);\n      });\n    });\n\n    describe(\"when the href has a trailing slash\", () => {\n      it(\"returns the correct href\", () => {\n        let renderer: TestRenderer.ReactTestRenderer;\n        TestRenderer.act(() => {\n          renderer = TestRenderer.create(\n            <MemoryRouter initialEntries={[\"/courses\"]}>\n              <Routes>\n                <Route\n                  path=\"courses\"\n                  element={<ShowHref to=\"advanced-react/\" />}\n                />\n              </Routes>\n            </MemoryRouter>,\n          );\n        });\n\n        expect(renderer.toJSON()).toMatchInlineSnapshot(`\n          <pre>\n            /courses/advanced-react/\n          </pre>\n        `);\n      });\n    });\n  });\n\n  describe(\"to a sibling route\", () => {\n    it(\"returns the correct href\", () => {\n      let renderer: TestRenderer.ReactTestRenderer;\n      TestRenderer.act(() => {\n        renderer = TestRenderer.create(\n          <MemoryRouter initialEntries={[\"/courses\"]}>\n            <Routes>\n              <Route path=\"courses\" element={<ShowHref to=\"../about\" />} />\n            </Routes>\n          </MemoryRouter>,\n        );\n      });\n\n      expect(renderer.toJSON()).toMatchInlineSnapshot(`\n        <pre>\n          /about\n        </pre>\n      `);\n    });\n\n    describe(\"when the URL has a trailing slash\", () => {\n      it(\"returns the correct href\", () => {\n        let renderer: TestRenderer.ReactTestRenderer;\n        TestRenderer.act(() => {\n          renderer = TestRenderer.create(\n            <MemoryRouter initialEntries={[\"/courses/\"]}>\n              <Routes>\n                <Route path=\"/courses/\" element={<ShowHref to=\"../about\" />} />\n              </Routes>\n            </MemoryRouter>,\n          );\n        });\n\n        expect(renderer.toJSON()).toMatchInlineSnapshot(`\n          <pre>\n            /about\n          </pre>\n        `);\n      });\n    });\n\n    describe(\"when the href has a trailing slash\", () => {\n      it(\"returns the correct href\", () => {\n        let renderer: TestRenderer.ReactTestRenderer;\n        TestRenderer.act(() => {\n          renderer = TestRenderer.create(\n            <MemoryRouter initialEntries={[\"/courses\"]}>\n              <Routes>\n                <Route path=\"courses\" element={<ShowHref to=\"../about/\" />} />\n              </Routes>\n            </MemoryRouter>,\n          );\n        });\n\n        expect(renderer.toJSON()).toMatchInlineSnapshot(`\n          <pre>\n            /about/\n          </pre>\n        `);\n      });\n    });\n  });\n\n  describe(\"to a parent route\", () => {\n    it(\"returns the correct href\", () => {\n      let renderer: TestRenderer.ReactTestRenderer;\n      TestRenderer.act(() => {\n        renderer = TestRenderer.create(\n          <MemoryRouter initialEntries={[\"/courses/advanced-react\"]}>\n            <Routes>\n              <Route path=\"courses\">\n                <Route path=\"advanced-react\" element={<ShowHref to=\"..\" />} />\n              </Route>\n            </Routes>\n          </MemoryRouter>,\n        );\n      });\n\n      expect(renderer.toJSON()).toMatchInlineSnapshot(`\n        <pre>\n          /courses\n        </pre>\n      `);\n    });\n\n    describe(\"when the URL has a trailing slash\", () => {\n      it(\"returns the correct href\", () => {\n        let renderer: TestRenderer.ReactTestRenderer;\n        TestRenderer.act(() => {\n          renderer = TestRenderer.create(\n            <MemoryRouter initialEntries={[\"/courses/advanced-react/\"]}>\n              <Routes>\n                <Route path=\"courses\">\n                  <Route path=\"advanced-react\" element={<ShowHref to=\"..\" />} />\n                </Route>\n              </Routes>\n            </MemoryRouter>,\n          );\n        });\n\n        expect(renderer.toJSON()).toMatchInlineSnapshot(`\n          <pre>\n            /courses\n          </pre>\n        `);\n      });\n    });\n\n    describe(\"when the href has a trailing slash\", () => {\n      it(\"returns the correct href\", () => {\n        let renderer: TestRenderer.ReactTestRenderer;\n        TestRenderer.act(() => {\n          renderer = TestRenderer.create(\n            <MemoryRouter initialEntries={[\"/courses/advanced-react\"]}>\n              <Routes>\n                <Route path=\"courses\">\n                  <Route\n                    path=\"advanced-react\"\n                    element={<ShowHref to=\"../\" />}\n                  />\n                </Route>\n              </Routes>\n            </MemoryRouter>,\n          );\n        });\n\n        expect(renderer.toJSON()).toMatchInlineSnapshot(`\n          <pre>\n            /courses/\n          </pre>\n        `);\n      });\n    });\n  });\n\n  describe(\"to an absolute route\", () => {\n    it(\"returns the correct href\", () => {\n      let renderer: TestRenderer.ReactTestRenderer;\n      TestRenderer.act(() => {\n        renderer = TestRenderer.create(\n          <MemoryRouter initialEntries={[\"/courses/advanced-react\"]}>\n            <Routes>\n              <Route\n                path=\"courses/advanced-react\"\n                element={<ShowHref to=\"/users\" />}\n              />\n            </Routes>\n          </MemoryRouter>,\n        );\n      });\n\n      expect(renderer.toJSON()).toMatchInlineSnapshot(`\n        <pre>\n          /users\n        </pre>\n      `);\n    });\n  });\n\n  describe(\"with a to value that has more .. segments than are in the URL\", () => {\n    it(\"returns the correct href\", () => {\n      let renderer: TestRenderer.ReactTestRenderer;\n      TestRenderer.act(() => {\n        renderer = TestRenderer.create(\n          <MemoryRouter initialEntries={[\"/courses/react-fundamentals\"]}>\n            <Routes>\n              <Route path=\"courses\">\n                <Route\n                  path=\"react-fundamentals\"\n                  element={<ShowHref to=\"../../../courses\" />}\n                />\n              </Route>\n            </Routes>\n          </MemoryRouter>,\n        );\n      });\n\n      expect(renderer.toJSON()).toMatchInlineSnapshot(`\n        <pre>\n          /courses\n        </pre>\n      `);\n    });\n\n    describe(\"and no additional segments\", () => {\n      it(\"links to the root /\", () => {\n        let renderer: TestRenderer.ReactTestRenderer;\n        TestRenderer.act(() => {\n          renderer = TestRenderer.create(\n            <MemoryRouter initialEntries={[\"/home\"]}>\n              <Routes>\n                <Route path=\"/home\" element={<ShowHref to=\"../../..\" />} />\n              </Routes>\n            </MemoryRouter>,\n          );\n        });\n\n        expect(renderer.toJSON()).toMatchInlineSnapshot(`\n          <pre>\n            /\n          </pre>\n        `);\n      });\n    });\n  });\n});\n"
  },
  {
    "path": "packages/react-router/__tests__/useLocation-test.tsx",
    "content": "import * as React from \"react\";\nimport * as TestRenderer from \"react-test-renderer\";\nimport { MemoryRouter, Routes, Route, useLocation } from \"react-router\";\n\nfunction ShowLocation() {\n  let location = useLocation();\n  return <pre>{JSON.stringify(location)}</pre>;\n}\n\ndescribe(\"useLocation\", () => {\n  it(\"returns the current location object\", () => {\n    let renderer: TestRenderer.ReactTestRenderer;\n    TestRenderer.act(() => {\n      renderer = TestRenderer.create(\n        <MemoryRouter initialEntries={[\"/home?the=search#the-hash\"]}>\n          <Routes>\n            <Route path=\"/home\" element={<ShowLocation />} />\n          </Routes>\n        </MemoryRouter>,\n      );\n    });\n\n    expect(renderer.toJSON()).toMatchInlineSnapshot(`\n      <pre>\n        {\"pathname\":\"/home\",\"search\":\"?the=search\",\"hash\":\"#the-hash\",\"state\":null,\"key\":\"default\"}\n      </pre>\n    `);\n  });\n\n  it(\"returns the scoped location object when nested in <Routes location>\", () => {\n    let renderer: TestRenderer.ReactTestRenderer;\n    TestRenderer.act(() => {\n      renderer = TestRenderer.create(\n        <MemoryRouter initialEntries={[\"/home?the=search#the-hash\"]}>\n          <App />\n        </MemoryRouter>,\n      );\n    });\n\n    function App() {\n      return (\n        <div>\n          <Routes>\n            <Route path=\"/home\" element={<ShowLocation />} />\n          </Routes>\n          <Routes location=\"/scoped?scoped=search#scoped-hash\">\n            <Route path=\"/scoped\" element={<ShowLocation />} />\n          </Routes>\n        </div>\n      );\n    }\n\n    expect(renderer.toJSON()).toMatchInlineSnapshot(`\n      <div>\n        <pre>\n          {\"pathname\":\"/home\",\"search\":\"?the=search\",\"hash\":\"#the-hash\",\"state\":null,\"key\":\"default\"}\n        </pre>\n        <pre>\n          {\"pathname\":\"/scoped\",\"search\":\"?scoped=search\",\"hash\":\"#scoped-hash\",\"state\":null,\"key\":\"default\"}\n        </pre>\n      </div>\n    `);\n  });\n\n  it(\"preserves state from initialEntries\", () => {\n    let renderer: TestRenderer.ReactTestRenderer;\n    TestRenderer.act(() => {\n      renderer = TestRenderer.create(\n        <MemoryRouter\n          initialEntries={[\n            { pathname: \"/example\", state: { my: \"state\" }, key: \"my-key\" },\n          ]}\n        >\n          <Routes>\n            <Route path={\"/example\"} element={<ShowLocation />} />\n          </Routes>\n        </MemoryRouter>,\n      );\n    });\n\n    expect(renderer.toJSON()).toMatchInlineSnapshot(`\n      <pre>\n        {\"pathname\":\"/example\",\"search\":\"\",\"hash\":\"\",\"state\":{\"my\":\"state\"},\"key\":\"my-key\"}\n      </pre>\n    `);\n  });\n});\n"
  },
  {
    "path": "packages/react-router/__tests__/useMatch-test.tsx",
    "content": "import * as React from \"react\";\nimport * as TestRenderer from \"react-test-renderer\";\nimport type { PathMatch } from \"react-router\";\nimport { MemoryRouter, Routes, Route, useMatch } from \"react-router\";\n\nfunction ShowMatch({ pattern }: { pattern: string }) {\n  return <pre>{JSON.stringify(useMatch(pattern), null, 2)}</pre>;\n}\n\ndescribe(\"useMatch\", () => {\n  describe(\"when the path matches the current URL\", () => {\n    it(\"returns the match\", () => {\n      let renderer: TestRenderer.ReactTestRenderer;\n      TestRenderer.act(() => {\n        renderer = TestRenderer.create(\n          <MemoryRouter initialEntries={[\"/home\"]}>\n            <Routes>\n              <Route path=\"/\" element={<ShowMatch pattern=\"home\" />}>\n                <Route path=\"/home\" element={<h1>Home</h1>} />\n              </Route>\n            </Routes>\n          </MemoryRouter>,\n        );\n      });\n\n      expect(renderer.toJSON()).toMatchInlineSnapshot(`\n        <pre>\n          {\n          \"params\": {},\n          \"pathname\": \"/home\",\n          \"pathnameBase\": \"/home\",\n          \"pattern\": {\n            \"path\": \"home\",\n            \"caseSensitive\": false,\n            \"end\": true\n          }\n        }\n        </pre>\n      `);\n    });\n  });\n\n  describe(\"when the current URL ends with a slash\", () => {\n    it(\"returns the match.pathname with the trailing slash\", () => {\n      let renderer: TestRenderer.ReactTestRenderer;\n      TestRenderer.act(() => {\n        renderer = TestRenderer.create(\n          <MemoryRouter initialEntries={[\"/home/\"]}>\n            <Routes>\n              <Route path=\"/\" element={<ShowMatch pattern=\"home\" />}>\n                <Route path=\"/home\" element={<h1>Home</h1>} />\n              </Route>\n            </Routes>\n          </MemoryRouter>,\n        );\n      });\n\n      expect(renderer.toJSON()).toMatchInlineSnapshot(`\n        <pre>\n          {\n          \"params\": {},\n          \"pathname\": \"/home/\",\n          \"pathnameBase\": \"/home\",\n          \"pattern\": {\n            \"path\": \"home\",\n            \"caseSensitive\": false,\n            \"end\": true\n          }\n        }\n        </pre>\n      `);\n    });\n  });\n\n  describe(\"when the path does not match the current URL\", () => {\n    it(\"returns null\", () => {\n      let renderer: TestRenderer.ReactTestRenderer;\n      TestRenderer.act(() => {\n        renderer = TestRenderer.create(\n          <MemoryRouter initialEntries={[\"/home\"]}>\n            <Routes>\n              <Route path=\"/\" element={<ShowMatch pattern=\"about\" />}>\n                <Route path=\"/home\" element={<h1>Home</h1>} />\n              </Route>\n            </Routes>\n          </MemoryRouter>,\n        );\n      });\n\n      expect(renderer.toJSON()).toMatchInlineSnapshot(`\n        <pre>\n          null\n        </pre>\n      `);\n    });\n  });\n\n  describe(\"when re-rendered with the same URL\", () => {\n    it(\"returns the memoized match\", () => {\n      let path = \"/home\";\n      let match: PathMatch<string>;\n      let firstMatch: PathMatch<string>;\n\n      function HomePage() {\n        match = useMatch(path);\n\n        if (!firstMatch) {\n          firstMatch = match;\n        }\n\n        return null;\n      }\n\n      let renderer: TestRenderer.ReactTestRenderer;\n      TestRenderer.act(() => {\n        renderer = TestRenderer.create(\n          <MemoryRouter initialEntries={[path]}>\n            <Routes>\n              <Route path={path} element={<HomePage />} />\n            </Routes>\n          </MemoryRouter>,\n        );\n      });\n\n      TestRenderer.act(() => {\n        renderer.update(\n          <MemoryRouter initialEntries={[path]}>\n            <Routes>\n              <Route path={path} element={<HomePage />} />\n            </Routes>\n          </MemoryRouter>,\n        );\n      });\n\n      expect(match).toBe(firstMatch);\n    });\n  });\n});\n"
  },
  {
    "path": "packages/react-router/__tests__/useNavigate-test.tsx",
    "content": "import * as React from \"react\";\nimport * as TestRenderer from \"react-test-renderer\";\nimport type { RelativeRoutingType, To } from \"react-router\";\nimport {\n  MemoryRouter,\n  Routes,\n  Route,\n  useNavigate,\n  useLocation,\n  createMemoryRouter,\n  createRoutesFromElements,\n  Outlet,\n  RouterProvider,\n} from \"react-router\";\n\ndescribe(\"useNavigate\", () => {\n  it(\"navigates to the new location\", async () => {\n    function Home() {\n      let navigate = useNavigate();\n\n      function handleClick() {\n        navigate(\"/about\");\n      }\n\n      return (\n        <div>\n          <h1>Home</h1>\n          <button onClick={handleClick}>click me</button>\n        </div>\n      );\n    }\n\n    let renderer: TestRenderer.ReactTestRenderer;\n    TestRenderer.act(() => {\n      renderer = TestRenderer.create(\n        <MemoryRouter initialEntries={[\"/home\"]}>\n          <Routes>\n            <Route path=\"home\" element={<Home />} />\n            <Route path=\"about\" element={<h1>About</h1>} />\n          </Routes>\n        </MemoryRouter>,\n      );\n    });\n\n    // @ts-expect-error\n    let button = renderer.root.findByType(\"button\");\n    await TestRenderer.act(() => button.props.onClick());\n\n    // @ts-expect-error\n    expect(renderer.toJSON()).toMatchInlineSnapshot(`\n      <h1>\n        About\n      </h1>\n    `);\n  });\n\n  it(\"navigates to the new location when no pathname is provided\", async () => {\n    function Home() {\n      let location = useLocation();\n      let navigate = useNavigate();\n\n      return (\n        <>\n          <p>{location.pathname + location.search}</p>\n          <button onClick={() => navigate(\"?key=value\")}>click me</button>\n        </>\n      );\n    }\n\n    let renderer: TestRenderer.ReactTestRenderer;\n    TestRenderer.act(() => {\n      renderer = TestRenderer.create(\n        <MemoryRouter initialEntries={[\"/home\"]}>\n          <Routes>\n            <Route path=\"home\" element={<Home />} />\n          </Routes>\n        </MemoryRouter>,\n      );\n    });\n\n    // @ts-expect-error\n    expect(renderer.toJSON()).toMatchInlineSnapshot(`\n      [\n        <p>\n          /home\n        </p>,\n        <button\n          onClick={[Function]}\n        >\n          click me\n        </button>,\n      ]\n    `);\n\n    // @ts-expect-error\n    let button = renderer.root.findByType(\"button\");\n    await TestRenderer.act(() => button.props.onClick());\n\n    // @ts-expect-error\n    expect(renderer.toJSON()).toMatchInlineSnapshot(`\n      [\n        <p>\n          /home?key=value\n        </p>,\n        <button\n          onClick={[Function]}\n        >\n          click me\n        </button>,\n      ]\n    `);\n  });\n\n  it(\"navigates to the new location when no pathname is provided (with a basename)\", async () => {\n    function Home() {\n      let location = useLocation();\n      let navigate = useNavigate();\n\n      return (\n        <>\n          <p>{location.pathname + location.search}</p>\n          <button onClick={() => navigate(\"?key=value\")}>click me</button>\n        </>\n      );\n    }\n\n    let renderer: TestRenderer.ReactTestRenderer;\n    TestRenderer.act(() => {\n      renderer = TestRenderer.create(\n        <MemoryRouter basename=\"/basename\" initialEntries={[\"/basename/home\"]}>\n          <Routes>\n            <Route path=\"home\" element={<Home />} />\n          </Routes>\n        </MemoryRouter>,\n      );\n    });\n\n    // @ts-expect-error\n    expect(renderer.toJSON()).toMatchInlineSnapshot(`\n      [\n        <p>\n          /home\n        </p>,\n        <button\n          onClick={[Function]}\n        >\n          click me\n        </button>,\n      ]\n    `);\n\n    // @ts-expect-error\n    let button = renderer.root.findByType(\"button\");\n    await TestRenderer.act(() => button.props.onClick());\n\n    // @ts-expect-error\n    expect(renderer.toJSON()).toMatchInlineSnapshot(`\n      [\n        <p>\n          /home?key=value\n        </p>,\n        <button\n          onClick={[Function]}\n        >\n          click me\n        </button>,\n      ]\n    `);\n  });\n\n  it(\"navigates to the new location with empty query string when no query string is provided\", async () => {\n    function Home() {\n      let location = useLocation();\n      let navigate = useNavigate();\n\n      return (\n        <>\n          <p>{location.pathname + location.search}</p>\n          <button onClick={() => navigate(\"/home\")}>click me</button>\n        </>\n      );\n    }\n\n    let renderer: TestRenderer.ReactTestRenderer;\n    TestRenderer.act(() => {\n      renderer = TestRenderer.create(\n        <MemoryRouter initialEntries={[\"/home?key=value\"]}>\n          <Routes>\n            <Route path=\"home\" element={<Home />} />\n          </Routes>\n        </MemoryRouter>,\n      );\n    });\n\n    // @ts-expect-error\n    expect(renderer.toJSON()).toMatchInlineSnapshot(`\n      [\n        <p>\n          /home?key=value\n        </p>,\n        <button\n          onClick={[Function]}\n        >\n          click me\n        </button>,\n      ]\n    `);\n\n    // @ts-expect-error\n    let button = renderer.root.findByType(\"button\");\n    await TestRenderer.act(() => button.props.onClick());\n\n    // @ts-expect-error\n    expect(renderer.toJSON()).toMatchInlineSnapshot(`\n      [\n        <p>\n          /home\n        </p>,\n        <button\n          onClick={[Function]}\n        >\n          click me\n        </button>,\n      ]\n    `);\n  });\n\n  it(\"throws on invalid destination path objects\", () => {\n    function Home() {\n      let navigate = useNavigate();\n\n      return (\n        <div>\n          <h1>Home</h1>\n          <button onClick={() => navigate({ pathname: \"/about/thing?search\" })}>\n            click 1\n          </button>\n          <button onClick={() => navigate({ pathname: \"/about/thing#hash\" })}>\n            click 2\n          </button>\n          <button\n            onClick={() => navigate({ pathname: \"/about/thing?search#hash\" })}\n          >\n            click 3\n          </button>\n          <button\n            onClick={() =>\n              navigate({\n                pathname: \"/about/thing\",\n                search: \"?search#hash\",\n              })\n            }\n          >\n            click 4\n          </button>\n        </div>\n      );\n    }\n\n    let renderer: TestRenderer.ReactTestRenderer;\n    TestRenderer.act(() => {\n      renderer = TestRenderer.create(\n        <MemoryRouter initialEntries={[\"/home\"]}>\n          <Routes>\n            <Route path=\"home\" element={<Home />} />\n          </Routes>\n        </MemoryRouter>,\n      );\n    });\n\n    expect(() =>\n      TestRenderer.act(() => {\n        renderer.root.findAllByType(\"button\")[0].props.onClick();\n      }),\n    ).toThrowErrorMatchingInlineSnapshot(\n      `\"Cannot include a '?' character in a manually specified \\`to.pathname\\` field [{\"pathname\":\"/about/thing?search\"}].  Please separate it out to the \\`to.search\\` field. Alternatively you may provide the full path as a string in <Link to=\"...\"> and the router will parse it for you.\"`,\n    );\n\n    expect(() =>\n      TestRenderer.act(() => {\n        renderer.root.findAllByType(\"button\")[1].props.onClick();\n      }),\n    ).toThrowErrorMatchingInlineSnapshot(\n      `\"Cannot include a '#' character in a manually specified \\`to.pathname\\` field [{\"pathname\":\"/about/thing#hash\"}].  Please separate it out to the \\`to.hash\\` field. Alternatively you may provide the full path as a string in <Link to=\"...\"> and the router will parse it for you.\"`,\n    );\n\n    expect(() =>\n      TestRenderer.act(() => {\n        renderer.root.findAllByType(\"button\")[2].props.onClick();\n      }),\n    ).toThrowErrorMatchingInlineSnapshot(\n      `\"Cannot include a '?' character in a manually specified \\`to.pathname\\` field [{\"pathname\":\"/about/thing?search#hash\"}].  Please separate it out to the \\`to.search\\` field. Alternatively you may provide the full path as a string in <Link to=\"...\"> and the router will parse it for you.\"`,\n    );\n\n    expect(() =>\n      TestRenderer.act(() => {\n        renderer.root.findAllByType(\"button\")[3].props.onClick();\n      }),\n    ).toThrowErrorMatchingInlineSnapshot(\n      `\"Cannot include a '#' character in a manually specified \\`to.search\\` field [{\"pathname\":\"/about/thing\",\"search\":\"?search#hash\"}].  Please separate it out to the \\`to.hash\\` field. Alternatively you may provide the full path as a string in <Link to=\"...\"> and the router will parse it for you.\"`,\n    );\n  });\n\n  it(\"allows useNavigate usage in a mixed RouterProvider/<Routes> scenario\", async () => {\n    const router = createMemoryRouter([\n      {\n        path: \"/*\",\n        Component() {\n          let navigate = useNavigate();\n          let location = useLocation();\n          return (\n            <>\n              <button\n                onClick={() =>\n                  navigate(location.pathname === \"/\" ? \"/page\" : \"/\")\n                }\n              >\n                Navigate from RouterProvider\n              </button>\n              <Routes>\n                <Route path=\"/\" element={<Home />} />\n                <Route path=\"/page\" element={<Page />} />\n              </Routes>\n            </>\n          );\n        },\n      },\n    ]);\n\n    function Home() {\n      let navigate = useNavigate();\n      return (\n        <>\n          <h1>Home</h1>\n          <button onClick={() => navigate(\"/page\")}>\n            Navigate /page from Routes\n          </button>\n        </>\n      );\n    }\n\n    function Page() {\n      let navigate = useNavigate();\n      return (\n        <>\n          <h1>Page</h1>\n          <button onClick={() => navigate(\"/\")}>\n            Navigate /home from Routes\n          </button>\n        </>\n      );\n    }\n\n    let renderer: TestRenderer.ReactTestRenderer;\n    TestRenderer.act(() => {\n      renderer = TestRenderer.create(<RouterProvider router={router} />);\n    });\n\n    expect(router.state.location.pathname).toBe(\"/\");\n    expect(renderer.toJSON()).toMatchInlineSnapshot(`\n      [\n        <button\n          onClick={[Function]}\n        >\n          Navigate from RouterProvider\n        </button>,\n        <h1>\n          Home\n        </h1>,\n        <button\n          onClick={[Function]}\n        >\n          Navigate /page from Routes\n        </button>,\n      ]\n    `);\n\n    let button = renderer.root.findByProps({\n      children: \"Navigate from RouterProvider\",\n    });\n    await TestRenderer.act(() => button.props.onClick());\n\n    expect(router.state.location.pathname).toBe(\"/page\");\n    expect(renderer.toJSON()).toMatchInlineSnapshot(`\n      [\n        <button\n          onClick={[Function]}\n        >\n          Navigate from RouterProvider\n        </button>,\n        <h1>\n          Page\n        </h1>,\n        <button\n          onClick={[Function]}\n        >\n          Navigate /home from Routes\n        </button>,\n      ]\n    `);\n\n    button = renderer.root.findByProps({\n      children: \"Navigate from RouterProvider\",\n    });\n    await TestRenderer.act(() => button.props.onClick());\n\n    expect(router.state.location.pathname).toBe(\"/\");\n    expect(renderer.toJSON()).toMatchInlineSnapshot(`\n      [\n        <button\n          onClick={[Function]}\n        >\n          Navigate from RouterProvider\n        </button>,\n        <h1>\n          Home\n        </h1>,\n        <button\n          onClick={[Function]}\n        >\n          Navigate /page from Routes\n        </button>,\n      ]\n    `);\n\n    button = renderer.root.findByProps({\n      children: \"Navigate /page from Routes\",\n    });\n    await TestRenderer.act(() => button.props.onClick());\n\n    expect(router.state.location.pathname).toBe(\"/page\");\n    expect(renderer.toJSON()).toMatchInlineSnapshot(`\n      [\n        <button\n          onClick={[Function]}\n        >\n          Navigate from RouterProvider\n        </button>,\n        <h1>\n          Page\n        </h1>,\n        <button\n          onClick={[Function]}\n        >\n          Navigate /home from Routes\n        </button>,\n      ]\n    `);\n\n    button = renderer.root.findByProps({\n      children: \"Navigate /home from Routes\",\n    });\n    await TestRenderer.act(() => button.props.onClick());\n\n    expect(router.state.location.pathname).toBe(\"/\");\n    expect(renderer.toJSON()).toMatchInlineSnapshot(`\n      [\n        <button\n          onClick={[Function]}\n        >\n          Navigate from RouterProvider\n        </button>,\n        <h1>\n          Home\n        </h1>,\n        <button\n          onClick={[Function]}\n        >\n          Navigate /page from Routes\n        </button>,\n      ]\n    `);\n  });\n\n  describe(\"navigating in effects versus render\", () => {\n    let warnSpy: jest.SpyInstance;\n\n    beforeEach(() => {\n      warnSpy = jest.spyOn(console, \"warn\").mockImplementation(() => {});\n    });\n\n    afterEach(() => {\n      warnSpy.mockRestore();\n    });\n\n    describe(\"MemoryRouter\", () => {\n      it(\"does not allow navigation from the render cycle\", () => {\n        let renderer: TestRenderer.ReactTestRenderer;\n        TestRenderer.act(() => {\n          renderer = TestRenderer.create(\n            <MemoryRouter>\n              <Routes>\n                <Route index element={<Home />} />\n                <Route path=\"about\" element={<h1>About</h1>} />\n              </Routes>\n            </MemoryRouter>,\n          );\n        });\n\n        function Home() {\n          let navigate = useNavigate();\n          navigate(\"/about\");\n          return <h1>Home</h1>;\n        }\n\n        // @ts-expect-error\n        expect(renderer.toJSON()).toMatchInlineSnapshot(`\n          <h1>\n            Home\n          </h1>\n        `);\n        expect(warnSpy).toHaveBeenCalledWith(\n          \"You should call navigate() in a React.useEffect(), not when your component is first rendered.\",\n        );\n      });\n\n      it(\"allows navigation from effects\", () => {\n        let renderer: TestRenderer.ReactTestRenderer;\n        TestRenderer.act(() => {\n          renderer = TestRenderer.create(\n            <MemoryRouter>\n              <Routes>\n                <Route index element={<Home />} />\n                <Route path=\"about\" element={<h1>About</h1>} />\n              </Routes>\n            </MemoryRouter>,\n          );\n        });\n\n        function Home() {\n          let navigate = useNavigate();\n          React.useEffect(() => {\n            navigate(\"/about\");\n          }, [navigate]);\n          return <h1>Home</h1>;\n        }\n\n        // @ts-expect-error\n        expect(renderer.toJSON()).toMatchInlineSnapshot(`\n          <h1>\n            About\n          </h1>\n        `);\n        expect(warnSpy).not.toHaveBeenCalled();\n      });\n\n      it(\"allows navigation in child useEffects\", () => {\n        let renderer: TestRenderer.ReactTestRenderer;\n        TestRenderer.act(() => {\n          renderer = TestRenderer.create(\n            <MemoryRouter initialEntries={[\"/home\"]}>\n              <Routes>\n                <Route path=\"home\" element={<Parent />} />\n                <Route path=\"about\" element={<h1>About</h1>} />\n              </Routes>\n            </MemoryRouter>,\n          );\n        });\n\n        function Parent() {\n          let navigate = useNavigate();\n          let onChildRendered = React.useCallback(\n            () => navigate(\"/about\"),\n            [navigate],\n          );\n          return <Child onChildRendered={onChildRendered} />;\n        }\n\n        function Child({ onChildRendered }) {\n          React.useEffect(() => {\n            onChildRendered();\n          });\n          return null;\n        }\n\n        // @ts-expect-error\n        expect(renderer.toJSON()).toMatchInlineSnapshot(`\n          <h1>\n            About\n          </h1>\n        `);\n      });\n    });\n\n    describe(\"RouterProvider\", () => {\n      it(\"does not allow navigation from the render cycle\", async () => {\n        let router = createMemoryRouter([\n          {\n            index: true,\n            Component() {\n              let navigate = useNavigate();\n              navigate(\"/about\");\n              return <h1>Home</h1>;\n            },\n          },\n          {\n            path: \"about\",\n            element: <h1>About</h1>,\n          },\n        ]);\n        let renderer: TestRenderer.ReactTestRenderer;\n        TestRenderer.act(() => {\n          renderer = TestRenderer.create(<RouterProvider router={router} />);\n        });\n\n        // @ts-expect-error\n        expect(renderer.toJSON()).toMatchInlineSnapshot(`\n          <h1>\n            Home\n          </h1>\n        `);\n        expect(warnSpy).toHaveBeenCalledWith(\n          \"You should call navigate() in a React.useEffect(), not when your component is first rendered.\",\n        );\n      });\n\n      it(\"allows navigation from effects\", () => {\n        let router = createMemoryRouter([\n          {\n            index: true,\n            Component() {\n              let navigate = useNavigate();\n              React.useEffect(() => {\n                navigate(\"/about\");\n              }, [navigate]);\n              return <h1>Home</h1>;\n            },\n          },\n          {\n            path: \"about\",\n            element: <h1>About</h1>,\n          },\n        ]);\n        let renderer: TestRenderer.ReactTestRenderer;\n        TestRenderer.act(() => {\n          renderer = TestRenderer.create(<RouterProvider router={router} />);\n        });\n\n        // @ts-expect-error\n        expect(renderer.toJSON()).toMatchInlineSnapshot(`\n          <h1>\n            About\n          </h1>\n        `);\n        expect(warnSpy).not.toHaveBeenCalled();\n      });\n\n      it(\"allows navigation in child useEffects\", () => {\n        let router = createMemoryRouter([\n          {\n            index: true,\n            Component() {\n              let navigate = useNavigate();\n              let onChildRendered = React.useCallback(\n                () => navigate(\"/about\"),\n                [navigate],\n              );\n              return <Child onChildRendered={onChildRendered} />;\n            },\n          },\n          {\n            path: \"about\",\n            element: <h1>About</h1>,\n          },\n        ]);\n        let renderer: TestRenderer.ReactTestRenderer;\n        TestRenderer.act(() => {\n          renderer = TestRenderer.create(<RouterProvider router={router} />);\n        });\n\n        function Child({ onChildRendered }) {\n          React.useEffect(() => {\n            onChildRendered();\n          });\n          return null;\n        }\n\n        // @ts-expect-error\n        expect(renderer.toJSON()).toMatchInlineSnapshot(`\n          <h1>\n            About\n          </h1>\n        `);\n      });\n    });\n  });\n\n  describe(\"with state\", () => {\n    it(\"adds the state to location.state\", async () => {\n      function Home() {\n        let navigate = useNavigate();\n\n        function handleClick() {\n          navigate(\"/about\", { state: { from: \"home\" } });\n        }\n\n        return (\n          <div>\n            <h1>Home</h1>\n            <button onClick={handleClick}>click me</button>\n          </div>\n        );\n      }\n\n      function ShowLocationState() {\n        return <p>location.state:{JSON.stringify(useLocation().state)}</p>;\n      }\n\n      let renderer: TestRenderer.ReactTestRenderer;\n      TestRenderer.act(() => {\n        renderer = TestRenderer.create(\n          <MemoryRouter initialEntries={[\"/home\"]}>\n            <Routes>\n              <Route path=\"home\" element={<Home />} />\n              <Route path=\"about\" element={<ShowLocationState />} />\n            </Routes>\n          </MemoryRouter>,\n        );\n      });\n\n      // @ts-expect-error\n      let button = renderer.root.findByType(\"button\");\n      await TestRenderer.act(() => button.props.onClick());\n\n      // @ts-expect-error\n      expect(renderer.toJSON()).toMatchInlineSnapshot(`\n        <p>\n          location.state:\n          {\"from\":\"home\"}\n        </p>\n      `);\n    });\n  });\n\n  describe(\"when relative navigation is handled via React Context\", () => {\n    describe(\"with an absolute href\", () => {\n      it(\"navigates to the correct URL\", async () => {\n        let renderer: TestRenderer.ReactTestRenderer;\n        TestRenderer.act(() => {\n          renderer = TestRenderer.create(\n            <MemoryRouter initialEntries={[\"/home\"]}>\n              <Routes>\n                <Route\n                  path=\"home\"\n                  element={<UseNavigateButton to=\"/about\" />}\n                />\n                <Route path=\"about\" element={<h1>About</h1>} />\n              </Routes>\n            </MemoryRouter>,\n          );\n        });\n\n        // @ts-expect-error\n        let button = renderer.root.findByType(\"button\");\n        await TestRenderer.act(() => button.props.onClick());\n\n        // @ts-expect-error\n        expect(renderer.toJSON()).toMatchInlineSnapshot(`\n          <h1>\n            About\n          </h1>\n        `);\n      });\n    });\n\n    describe(\"with a relative href (relative=route)\", () => {\n      it(\"navigates to the correct URL\", async () => {\n        let renderer: TestRenderer.ReactTestRenderer;\n        TestRenderer.act(() => {\n          renderer = TestRenderer.create(\n            <MemoryRouter initialEntries={[\"/home\"]}>\n              <Routes>\n                <Route\n                  path=\"home\"\n                  element={<UseNavigateButton to=\"../about\" />}\n                />\n                <Route path=\"about\" element={<h1>About</h1>} />\n              </Routes>\n            </MemoryRouter>,\n          );\n        });\n\n        // @ts-expect-error\n        let button = renderer.root.findByType(\"button\");\n        await TestRenderer.act(() => button.props.onClick());\n\n        // @ts-expect-error\n        expect(renderer.toJSON()).toMatchInlineSnapshot(`\n          <h1>\n            About\n          </h1>\n        `);\n      });\n\n      it(\"handles upward navigation from an index routes\", async () => {\n        let renderer: TestRenderer.ReactTestRenderer;\n        TestRenderer.act(() => {\n          renderer = TestRenderer.create(\n            <MemoryRouter initialEntries={[\"/home\"]}>\n              <Routes>\n                <Route path=\"home\">\n                  <Route index element={<UseNavigateButton to=\"../about\" />} />\n                </Route>\n                <Route path=\"about\" element={<h1>About</h1>} />\n              </Routes>\n            </MemoryRouter>,\n          );\n        });\n\n        // @ts-expect-error\n        let button = renderer.root.findByType(\"button\");\n        await TestRenderer.act(() => button.props.onClick());\n\n        // @ts-expect-error\n        expect(renderer.toJSON()).toMatchInlineSnapshot(`\n          <h1>\n            About\n          </h1>\n        `);\n      });\n\n      it(\"handles upward navigation from inside a pathless layout route\", async () => {\n        let renderer: TestRenderer.ReactTestRenderer;\n        TestRenderer.act(() => {\n          renderer = TestRenderer.create(\n            <MemoryRouter initialEntries={[\"/home\"]}>\n              <Routes>\n                <Route element={<Outlet />}>\n                  <Route\n                    path=\"home\"\n                    element={<UseNavigateButton to=\"../about\" />}\n                  />\n                </Route>\n                <Route path=\"about\" element={<h1>About</h1>} />\n              </Routes>\n            </MemoryRouter>,\n          );\n        });\n\n        // @ts-expect-error\n        let button = renderer.root.findByType(\"button\");\n        await TestRenderer.act(() => button.props.onClick());\n\n        // @ts-expect-error\n        expect(renderer.toJSON()).toMatchInlineSnapshot(`\n          <h1>\n            About\n          </h1>\n        `);\n      });\n\n      it(\"handles upward navigation from inside multiple pathless layout routes + index route\", async () => {\n        let renderer: TestRenderer.ReactTestRenderer;\n        TestRenderer.act(() => {\n          renderer = TestRenderer.create(\n            <MemoryRouter initialEntries={[\"/home\"]}>\n              <Routes>\n                <Route path=\"home\">\n                  <Route element={<Outlet />}>\n                    <Route element={<Outlet />}>\n                      <Route element={<Outlet />}>\n                        <Route\n                          index\n                          element={<UseNavigateButton to=\"../about\" />}\n                        />\n                      </Route>\n                    </Route>\n                  </Route>\n                </Route>\n                <Route path=\"about\" element={<h1>About</h1>} />\n              </Routes>\n            </MemoryRouter>,\n          );\n        });\n\n        // @ts-expect-error\n        let button = renderer.root.findByType(\"button\");\n        await TestRenderer.act(() => button.props.onClick());\n\n        // @ts-expect-error\n        expect(renderer.toJSON()).toMatchInlineSnapshot(`\n          <h1>\n            About\n          </h1>\n        `);\n      });\n\n      it(\"handles upward navigation from inside multiple pathless layout routes + path route\", async () => {\n        let renderer: TestRenderer.ReactTestRenderer;\n        TestRenderer.act(() => {\n          renderer = TestRenderer.create(\n            <MemoryRouter initialEntries={[\"/home/page\"]}>\n              <Routes>\n                <Route path=\"home\" element={<Outlet />}>\n                  <Route element={<Outlet />}>\n                    <Route element={<Outlet />}>\n                      <Route element={<Outlet />}>\n                        <Route\n                          path=\"page\"\n                          element={<UseNavigateButton to=\"../../about\" />}\n                        />\n                      </Route>\n                    </Route>\n                  </Route>\n                </Route>\n                <Route path=\"about\" element={<h1>About</h1>} />\n              </Routes>\n            </MemoryRouter>,\n          );\n        });\n\n        // @ts-expect-error\n        let button = renderer.root.findByType(\"button\");\n        await TestRenderer.act(() => button.props.onClick());\n\n        // @ts-expect-error\n        expect(renderer.toJSON()).toMatchInlineSnapshot(`\n          <h1>\n            About\n          </h1>\n        `);\n      });\n\n      it(\"handles parent navigation from inside multiple pathless layout routes\", async () => {\n        let renderer: TestRenderer.ReactTestRenderer;\n        TestRenderer.act(() => {\n          renderer = TestRenderer.create(\n            <MemoryRouter initialEntries={[\"/home/page\"]}>\n              <Routes>\n                <Route\n                  path=\"home\"\n                  element={\n                    <>\n                      <h1>Home</h1>\n                      <Outlet />\n                    </>\n                  }\n                >\n                  <Route element={<Outlet />}>\n                    <Route element={<Outlet />}>\n                      <Route element={<Outlet />}>\n                        <Route\n                          path=\"page\"\n                          element={\n                            <>\n                              <h2>Page</h2>\n                              <UseNavigateButton to=\"..\" />\n                            </>\n                          }\n                        />\n                      </Route>\n                    </Route>\n                  </Route>\n                </Route>\n                <Route path=\"about\" element={<h1>About</h1>} />\n              </Routes>\n            </MemoryRouter>,\n          );\n        });\n\n        // @ts-expect-error\n        let button = renderer.root.findByType(\"button\");\n        await TestRenderer.act(() => button.props.onClick());\n\n        // @ts-expect-error\n        expect(renderer.toJSON()).toMatchInlineSnapshot(`\n          <h1>\n            Home\n          </h1>\n        `);\n      });\n\n      it(\"handles relative navigation from nested index route\", async () => {\n        let renderer: TestRenderer.ReactTestRenderer;\n        TestRenderer.act(() => {\n          renderer = TestRenderer.create(\n            <MemoryRouter initialEntries={[\"/layout/thing\"]}>\n              <Routes>\n                <Route path=\"layout\">\n                  <Route path=\":param\">\n                    {/* redirect /layout/:param/ index routes to /layout/:param/dest */}\n                    <Route index element={<UseNavigateButton to=\"dest\" />} />\n                    <Route path=\"dest\" element={<h1>Destination</h1>} />\n                  </Route>\n                </Route>\n              </Routes>\n            </MemoryRouter>,\n          );\n        });\n\n        // @ts-expect-error\n        let button = renderer.root.findByType(\"button\");\n        await TestRenderer.act(() => button.props.onClick());\n\n        // @ts-expect-error\n        expect(renderer.toJSON()).toMatchInlineSnapshot(`\n          <h1>\n            Destination\n          </h1>\n        `);\n      });\n    });\n\n    describe(\"with a relative href (relative=path)\", () => {\n      it(\"navigates to the correct URL\", async () => {\n        let renderer: TestRenderer.ReactTestRenderer;\n        TestRenderer.act(() => {\n          renderer = TestRenderer.create(\n            <MemoryRouter initialEntries={[\"/contacts/1\"]}>\n              <Routes>\n                <Route path=\"contacts\" element={<h1>Contacts</h1>} />\n                <Route\n                  path=\"contacts/:id\"\n                  element={<UseNavigateButton to=\"..\" relative=\"path\" />}\n                />\n              </Routes>\n            </MemoryRouter>,\n          );\n        });\n\n        // @ts-expect-error\n        let button = renderer.root.findByType(\"button\");\n        await TestRenderer.act(() => button.props.onClick());\n\n        // @ts-expect-error\n        expect(renderer.toJSON()).toMatchInlineSnapshot(`\n          <h1>\n            Contacts\n          </h1>\n        `);\n      });\n\n      it(\"handles upward navigation from an index routes\", async () => {\n        let renderer: TestRenderer.ReactTestRenderer;\n        TestRenderer.act(() => {\n          renderer = TestRenderer.create(\n            <MemoryRouter initialEntries={[\"/contacts/1\"]}>\n              <Routes>\n                <Route path=\"contacts\" element={<h1>Contacts</h1>} />\n                <Route path=\"contacts/:id\">\n                  <Route\n                    index\n                    element={<UseNavigateButton to=\"..\" relative=\"path\" />}\n                  />\n                </Route>\n              </Routes>\n            </MemoryRouter>,\n          );\n        });\n\n        // @ts-expect-error\n        let button = renderer.root.findByType(\"button\");\n        await TestRenderer.act(() => button.props.onClick());\n\n        // @ts-expect-error\n        expect(renderer.toJSON()).toMatchInlineSnapshot(`\n          <h1>\n            Contacts\n          </h1>\n        `);\n      });\n\n      it(\"handles upward navigation from inside a pathless layout route\", async () => {\n        let renderer: TestRenderer.ReactTestRenderer;\n        TestRenderer.act(() => {\n          renderer = TestRenderer.create(\n            <MemoryRouter initialEntries={[\"/contacts/1\"]}>\n              <Routes>\n                <Route path=\"contacts\" element={<h1>Contacts</h1>} />\n                <Route element={<Outlet />}>\n                  <Route\n                    path=\"contacts/:id\"\n                    element={<UseNavigateButton to=\"..\" relative=\"path\" />}\n                  />\n                </Route>\n              </Routes>\n            </MemoryRouter>,\n          );\n        });\n\n        // @ts-expect-error\n        let button = renderer.root.findByType(\"button\");\n        await TestRenderer.act(() => button.props.onClick());\n\n        // @ts-expect-error\n        expect(renderer.toJSON()).toMatchInlineSnapshot(`\n          <h1>\n            Contacts\n          </h1>\n        `);\n      });\n\n      it(\"handles upward navigation from inside multiple pathless layout routes + index route\", async () => {\n        let renderer: TestRenderer.ReactTestRenderer;\n        TestRenderer.act(() => {\n          renderer = TestRenderer.create(\n            <MemoryRouter initialEntries={[\"/contacts/1\"]}>\n              <Routes>\n                <Route path=\"contacts\" element={<h1>Contacts</h1>} />\n                <Route path=\"contacts/:id\">\n                  <Route element={<Outlet />}>\n                    <Route element={<Outlet />}>\n                      <Route element={<Outlet />}>\n                        <Route\n                          index\n                          element={\n                            <UseNavigateButton to=\"..\" relative=\"path\" />\n                          }\n                        />\n                      </Route>\n                    </Route>\n                  </Route>\n                </Route>\n              </Routes>\n            </MemoryRouter>,\n          );\n        });\n\n        // @ts-expect-error\n        let button = renderer.root.findByType(\"button\");\n        await TestRenderer.act(() => button.props.onClick());\n\n        // @ts-expect-error\n        expect(renderer.toJSON()).toMatchInlineSnapshot(`\n          <h1>\n            Contacts\n          </h1>\n        `);\n      });\n\n      it(\"handles upward navigation from inside multiple pathless layout routes + path route\", async () => {\n        let renderer: TestRenderer.ReactTestRenderer;\n        TestRenderer.act(() => {\n          renderer = TestRenderer.create(\n            <MemoryRouter initialEntries={[\"/contacts/1\"]}>\n              <Routes>\n                <Route path=\"contacts\" element={<Outlet />}>\n                  <Route index element={<h1>Contacts</h1>} />\n                  <Route element={<Outlet />}>\n                    <Route element={<Outlet />}>\n                      <Route element={<Outlet />}>\n                        <Route\n                          path=\":id\"\n                          element={\n                            <UseNavigateButton to=\"..\" relative=\"path\" />\n                          }\n                        />\n                      </Route>\n                    </Route>\n                  </Route>\n                </Route>\n              </Routes>\n            </MemoryRouter>,\n          );\n        });\n\n        // @ts-expect-error\n        let button = renderer.root.findByType(\"button\");\n        await TestRenderer.act(() => button.props.onClick());\n\n        // @ts-expect-error\n        expect(renderer.toJSON()).toMatchInlineSnapshot(`\n          <h1>\n            Contacts\n          </h1>\n        `);\n      });\n\n      it(\"handles relative navigation from nested index route\", async () => {\n        let renderer: TestRenderer.ReactTestRenderer;\n        TestRenderer.act(() => {\n          renderer = TestRenderer.create(\n            <MemoryRouter initialEntries={[\"/layout/thing\"]}>\n              <Routes>\n                <Route path=\"layout\">\n                  <Route path=\":param\">\n                    {/* redirect /layout/:param/ index routes to /layout/:param/dest */}\n                    <Route\n                      index\n                      element={<UseNavigateButton to=\"dest\" relative=\"path\" />}\n                    />\n                    <Route path=\"dest\" element={<h1>Destination</h1>} />\n                  </Route>\n                </Route>\n              </Routes>\n            </MemoryRouter>,\n          );\n        });\n\n        // @ts-expect-error\n        let button = renderer.root.findByType(\"button\");\n        await TestRenderer.act(() => button.props.onClick());\n\n        // @ts-expect-error\n        expect(renderer.toJSON()).toMatchInlineSnapshot(`\n          <h1>\n            Destination\n          </h1>\n        `);\n      });\n\n      it(\"preserves search params and hash\", async () => {\n        let renderer: TestRenderer.ReactTestRenderer;\n        TestRenderer.act(() => {\n          renderer = TestRenderer.create(\n            <MemoryRouter initialEntries={[\"/contacts/1\"]}>\n              <Routes>\n                <Route path=\"contacts\" element={<Contacts />} />\n                <Route\n                  path=\"contacts/:id\"\n                  element={\n                    <UseNavigateButton to=\"..?foo=bar#hash\" relative=\"path\" />\n                  }\n                />\n              </Routes>\n            </MemoryRouter>,\n          );\n        });\n\n        function Contacts() {\n          let { search, hash } = useLocation();\n          return (\n            <>\n              <h1>Contacts</h1>\n              <p>\n                {search}\n                {hash}\n              </p>\n            </>\n          );\n        }\n\n        // @ts-expect-error\n        let button = renderer.root.findByType(\"button\");\n        await TestRenderer.act(() => button.props.onClick());\n\n        // @ts-expect-error\n        expect(renderer.toJSON()).toMatchInlineSnapshot(`\n          [\n            <h1>\n              Contacts\n            </h1>,\n            <p>\n              ?foo=bar\n              #hash\n            </p>,\n          ]\n        `);\n      });\n    });\n\n    it(\"is not stable across location changes\", () => {\n      let renderer: TestRenderer.ReactTestRenderer;\n      TestRenderer.act(() => {\n        renderer = TestRenderer.create(\n          <MemoryRouter initialEntries={[\"/home\"]}>\n            <Routes>\n              <Route\n                path=\"/\"\n                element={\n                  <>\n                    <NavBar />\n                    <Outlet />\n                  </>\n                }\n              >\n                <Route path=\"home\" element={<h1>Home</h1>} />\n                <Route path=\"about\" element={<h1>About</h1>} />\n              </Route>\n            </Routes>\n          </MemoryRouter>,\n        );\n      });\n\n      function NavBar() {\n        let count = React.useRef(0);\n        let navigate = useNavigate();\n        React.useEffect(() => {\n          count.current++;\n        }, [navigate]);\n        return (\n          <nav>\n            <button onClick={() => navigate(\"/home\")}>Home</button>\n            <button onClick={() => navigate(\"/about\")}>About</button>\n            <p>{`count:${count.current}`}</p>\n          </nav>\n        );\n      }\n\n      // @ts-expect-error\n      expect(renderer.toJSON()).toMatchInlineSnapshot(`\n        [\n          <nav>\n            <button\n              onClick={[Function]}\n            >\n              Home\n            </button>\n            <button\n              onClick={[Function]}\n            >\n              About\n            </button>\n            <p>\n              count:0\n            </p>\n          </nav>,\n          <h1>\n            Home\n          </h1>,\n        ]\n      `);\n\n      // @ts-expect-error\n      let buttons = renderer.root.findAllByType(\"button\");\n      TestRenderer.act(() => {\n        buttons[1].props.onClick(); // link to /about\n      });\n\n      // @ts-expect-error\n      expect(renderer.toJSON()).toMatchInlineSnapshot(`\n        [\n          <nav>\n            <button\n              onClick={[Function]}\n            >\n              Home\n            </button>\n            <button\n              onClick={[Function]}\n            >\n              About\n            </button>\n            <p>\n              count:1\n            </p>\n          </nav>,\n          <h1>\n            About\n          </h1>,\n        ]\n      `);\n\n      // @ts-expect-error\n      buttons = renderer.root.findAllByType(\"button\");\n      TestRenderer.act(() => {\n        buttons[0].props.onClick(); // link back to /home\n      });\n\n      // @ts-expect-error\n      expect(renderer.toJSON()).toMatchInlineSnapshot(`\n        [\n          <nav>\n            <button\n              onClick={[Function]}\n            >\n              Home\n            </button>\n            <button\n              onClick={[Function]}\n            >\n              About\n            </button>\n            <p>\n              count:2\n            </p>\n          </nav>,\n          <h1>\n            Home\n          </h1>,\n        ]\n      `);\n    });\n  });\n\n  describe(\"when relative navigation is handled via @remix-run/router\", () => {\n    describe(\"with an absolute href\", () => {\n      it(\"navigates to the correct URL\", async () => {\n        let router = createMemoryRouter(\n          createRoutesFromElements(\n            <>\n              <Route path=\"home\" element={<UseNavigateButton to=\"/about\" />} />\n              <Route path=\"about\" element={<h1>About</h1>} />\n            </>,\n          ),\n          { initialEntries: [\"/home\"] },\n        );\n\n        let renderer: TestRenderer.ReactTestRenderer;\n        TestRenderer.act(() => {\n          renderer = TestRenderer.create(<RouterProvider router={router} />);\n        });\n\n        // @ts-expect-error\n        let button = renderer.root.findByType(\"button\");\n        await TestRenderer.act(() => button.props.onClick());\n\n        // @ts-expect-error\n        expect(renderer.toJSON()).toMatchInlineSnapshot(`\n          <h1>\n            About\n          </h1>\n        `);\n      });\n    });\n\n    describe(\"with a relative href (relative=route)\", () => {\n      it(\"navigates to the correct URL\", async () => {\n        let router = createMemoryRouter(\n          createRoutesFromElements(\n            <>\n              <Route\n                path=\"home\"\n                element={<UseNavigateButton to=\"../about\" />}\n              />\n              <Route path=\"about\" element={<h1>About</h1>} />\n            </>,\n          ),\n          { initialEntries: [\"/home\"] },\n        );\n\n        let renderer: TestRenderer.ReactTestRenderer;\n        TestRenderer.act(() => {\n          renderer = TestRenderer.create(<RouterProvider router={router} />);\n        });\n\n        // @ts-expect-error\n        let button = renderer.root.findByType(\"button\");\n        await TestRenderer.act(() => button.props.onClick());\n\n        // @ts-expect-error\n        expect(renderer.toJSON()).toMatchInlineSnapshot(`\n          <h1>\n            About\n          </h1>\n        `);\n      });\n\n      it(\"handles upward navigation from an index routes\", async () => {\n        let router = createMemoryRouter(\n          createRoutesFromElements(\n            <>\n              <Route path=\"home\">\n                <Route index element={<UseNavigateButton to=\"../about\" />} />\n              </Route>\n              <Route path=\"about\" element={<h1>About</h1>} />\n            </>,\n          ),\n          { initialEntries: [\"/home\"] },\n        );\n\n        let renderer: TestRenderer.ReactTestRenderer;\n        TestRenderer.act(() => {\n          renderer = TestRenderer.create(<RouterProvider router={router} />);\n        });\n\n        // @ts-expect-error\n        let button = renderer.root.findByType(\"button\");\n        await TestRenderer.act(() => button.props.onClick());\n\n        // @ts-expect-error\n        expect(renderer.toJSON()).toMatchInlineSnapshot(`\n          <h1>\n            About\n          </h1>\n        `);\n      });\n\n      it(\"handles upward navigation from inside a pathless layout route\", async () => {\n        let router = createMemoryRouter(\n          createRoutesFromElements(\n            <>\n              <Route element={<Outlet />}>\n                <Route\n                  path=\"home\"\n                  element={<UseNavigateButton to=\"../about\" />}\n                />\n              </Route>\n              <Route path=\"about\" element={<h1>About</h1>} />\n            </>,\n          ),\n          { initialEntries: [\"/home\"] },\n        );\n\n        let renderer: TestRenderer.ReactTestRenderer;\n        TestRenderer.act(() => {\n          renderer = TestRenderer.create(<RouterProvider router={router} />);\n        });\n\n        // @ts-expect-error\n        let button = renderer.root.findByType(\"button\");\n        await TestRenderer.act(() => button.props.onClick());\n\n        // @ts-expect-error\n        expect(renderer.toJSON()).toMatchInlineSnapshot(`\n          <h1>\n            About\n          </h1>\n        `);\n      });\n\n      it(\"handles upward navigation from inside multiple pathless layout routes + index route\", async () => {\n        let router = createMemoryRouter(\n          createRoutesFromElements(\n            <>\n              <Route path=\"home\">\n                <Route element={<Outlet />}>\n                  <Route element={<Outlet />}>\n                    <Route element={<Outlet />}>\n                      <Route\n                        index\n                        element={<UseNavigateButton to=\"../about\" />}\n                      />\n                    </Route>\n                  </Route>\n                </Route>\n              </Route>\n              <Route path=\"about\" element={<h1>About</h1>} />\n            </>,\n          ),\n          { initialEntries: [\"/home\"] },\n        );\n\n        let renderer: TestRenderer.ReactTestRenderer;\n        TestRenderer.act(() => {\n          renderer = TestRenderer.create(<RouterProvider router={router} />);\n        });\n\n        // @ts-expect-error\n        let button = renderer.root.findByType(\"button\");\n        await TestRenderer.act(() => button.props.onClick());\n\n        // @ts-expect-error\n        expect(renderer.toJSON()).toMatchInlineSnapshot(`\n          <h1>\n            About\n          </h1>\n        `);\n      });\n\n      it(\"handles upward navigation from inside multiple pathless layout routes + path route\", async () => {\n        let router = createMemoryRouter(\n          createRoutesFromElements(\n            <>\n              <Route path=\"home\" element={<Outlet />}>\n                <Route element={<Outlet />}>\n                  <Route element={<Outlet />}>\n                    <Route element={<Outlet />}>\n                      <Route\n                        path=\"page\"\n                        element={<UseNavigateButton to=\"../../about\" />}\n                      />\n                    </Route>\n                  </Route>\n                </Route>\n              </Route>\n              <Route path=\"about\" element={<h1>About</h1>} />\n            </>,\n          ),\n          { initialEntries: [\"/home/page\"] },\n        );\n\n        let renderer: TestRenderer.ReactTestRenderer;\n        TestRenderer.act(() => {\n          renderer = TestRenderer.create(<RouterProvider router={router} />);\n        });\n\n        // @ts-expect-error\n        let button = renderer.root.findByType(\"button\");\n        await TestRenderer.act(() => button.props.onClick());\n\n        // @ts-expect-error\n        expect(renderer.toJSON()).toMatchInlineSnapshot(`\n          <h1>\n            About\n          </h1>\n        `);\n      });\n\n      it(\"handles parent navigation from inside multiple pathless layout routes\", async () => {\n        let router = createMemoryRouter(\n          createRoutesFromElements(\n            <>\n              <Route\n                path=\"home\"\n                element={\n                  <>\n                    <h1>Home</h1>\n                    <Outlet />\n                  </>\n                }\n              >\n                <Route element={<Outlet />}>\n                  <Route element={<Outlet />}>\n                    <Route element={<Outlet />}>\n                      <Route\n                        path=\"page\"\n                        element={\n                          <>\n                            <h2>Page</h2>\n                            <UseNavigateButton to=\"..\" />\n                          </>\n                        }\n                      />\n                    </Route>\n                  </Route>\n                </Route>\n              </Route>\n              <Route path=\"about\" element={<h1>About</h1>} />\n            </>,\n          ),\n          { initialEntries: [\"/home/page\"] },\n        );\n\n        let renderer: TestRenderer.ReactTestRenderer;\n        TestRenderer.act(() => {\n          renderer = TestRenderer.create(<RouterProvider router={router} />);\n        });\n\n        // @ts-expect-error\n        let button = renderer.root.findByType(\"button\");\n        await TestRenderer.act(() => button.props.onClick());\n\n        // @ts-expect-error\n        expect(renderer.toJSON()).toMatchInlineSnapshot(`\n          <h1>\n            Home\n          </h1>\n        `);\n      });\n\n      it(\"handles relative navigation from nested index route\", async () => {\n        let router = createMemoryRouter(\n          createRoutesFromElements(\n            <>\n              <Route path=\"layout\">\n                <Route path=\":param\">\n                  {/* redirect /layout/:param/ index routes to /layout/:param/dest */}\n                  <Route index element={<UseNavigateButton to=\"dest\" />} />\n                  <Route path=\"dest\" element={<h1>Destination</h1>} />\n                </Route>\n              </Route>\n            </>,\n          ),\n          { initialEntries: [\"/layout/thing\"] },\n        );\n\n        let renderer: TestRenderer.ReactTestRenderer;\n        TestRenderer.act(() => {\n          renderer = TestRenderer.create(<RouterProvider router={router} />);\n        });\n\n        // @ts-expect-error\n        let button = renderer.root.findByType(\"button\");\n        await TestRenderer.act(() => button.props.onClick());\n\n        // @ts-expect-error\n        expect(renderer.toJSON()).toMatchInlineSnapshot(`\n          <h1>\n            Destination\n          </h1>\n        `);\n      });\n    });\n\n    describe(\"with a relative href (relative=path)\", () => {\n      it(\"navigates to the correct URL\", async () => {\n        let router = createMemoryRouter(\n          createRoutesFromElements(\n            <>\n              <Route path=\"contacts\" element={<h1>Contacts</h1>} />\n              <Route\n                path=\"contacts/:id\"\n                element={<UseNavigateButton to=\"..\" relative=\"path\" />}\n              />\n            </>,\n          ),\n          { initialEntries: [\"/contacts/1\"] },\n        );\n\n        let renderer: TestRenderer.ReactTestRenderer;\n        TestRenderer.act(() => {\n          renderer = TestRenderer.create(<RouterProvider router={router} />);\n        });\n\n        // @ts-expect-error\n        let button = renderer.root.findByType(\"button\");\n        await TestRenderer.act(() => button.props.onClick());\n\n        // @ts-expect-error\n        expect(renderer.toJSON()).toMatchInlineSnapshot(`\n          <h1>\n            Contacts\n          </h1>\n        `);\n      });\n\n      it(\"handles upward navigation from an index routes\", async () => {\n        let router = createMemoryRouter(\n          createRoutesFromElements(\n            <>\n              <Route path=\"contacts\" element={<h1>Contacts</h1>} />\n              <Route path=\"contacts/:id\">\n                <Route\n                  index\n                  element={<UseNavigateButton to=\"..\" relative=\"path\" />}\n                />\n              </Route>\n            </>,\n          ),\n          { initialEntries: [\"/contacts/1\"] },\n        );\n\n        let renderer: TestRenderer.ReactTestRenderer;\n        TestRenderer.act(() => {\n          renderer = TestRenderer.create(<RouterProvider router={router} />);\n        });\n\n        // @ts-expect-error\n        let button = renderer.root.findByType(\"button\");\n        await TestRenderer.act(() => button.props.onClick());\n\n        // @ts-expect-error\n        expect(renderer.toJSON()).toMatchInlineSnapshot(`\n          <h1>\n            Contacts\n          </h1>\n        `);\n      });\n\n      it(\"handles upward navigation from inside a pathless layout route\", async () => {\n        let router = createMemoryRouter(\n          createRoutesFromElements(\n            <>\n              <Route path=\"contacts\" element={<h1>Contacts</h1>} />\n              <Route element={<Outlet />}>\n                <Route\n                  path=\"contacts/:id\"\n                  element={<UseNavigateButton to=\"..\" relative=\"path\" />}\n                />\n              </Route>\n            </>,\n          ),\n          { initialEntries: [\"/contacts/1\"] },\n        );\n\n        let renderer: TestRenderer.ReactTestRenderer;\n        TestRenderer.act(() => {\n          renderer = TestRenderer.create(<RouterProvider router={router} />);\n        });\n\n        // @ts-expect-error\n        let button = renderer.root.findByType(\"button\");\n        await TestRenderer.act(() => button.props.onClick());\n\n        // @ts-expect-error\n        expect(renderer.toJSON()).toMatchInlineSnapshot(`\n          <h1>\n            Contacts\n          </h1>\n        `);\n      });\n\n      it(\"handles upward navigation from inside multiple pathless layout routes + index route\", async () => {\n        let router = createMemoryRouter(\n          createRoutesFromElements(\n            <>\n              <Route path=\"contacts\" element={<h1>Contacts</h1>} />\n              <Route path=\"contacts/:id\">\n                <Route element={<Outlet />}>\n                  <Route element={<Outlet />}>\n                    <Route element={<Outlet />}>\n                      <Route\n                        index\n                        element={<UseNavigateButton to=\"..\" relative=\"path\" />}\n                      />\n                    </Route>\n                  </Route>\n                </Route>\n              </Route>\n            </>,\n          ),\n          { initialEntries: [\"/contacts/1\"] },\n        );\n\n        let renderer: TestRenderer.ReactTestRenderer;\n        TestRenderer.act(() => {\n          renderer = TestRenderer.create(<RouterProvider router={router} />);\n        });\n\n        // @ts-expect-error\n        let button = renderer.root.findByType(\"button\");\n        await TestRenderer.act(() => button.props.onClick());\n\n        // @ts-expect-error\n        expect(renderer.toJSON()).toMatchInlineSnapshot(`\n          <h1>\n            Contacts\n          </h1>\n        `);\n      });\n\n      it(\"handles upward navigation from inside multiple pathless layout routes + path route\", async () => {\n        let router = createMemoryRouter(\n          createRoutesFromElements(\n            <>\n              <Route path=\"contacts\" element={<Outlet />}>\n                <Route index element={<h1>Contacts</h1>} />\n                <Route element={<Outlet />}>\n                  <Route element={<Outlet />}>\n                    <Route element={<Outlet />}>\n                      <Route\n                        path=\":id\"\n                        element={<UseNavigateButton to=\"..\" relative=\"path\" />}\n                      />\n                    </Route>\n                  </Route>\n                </Route>\n              </Route>\n            </>,\n          ),\n          { initialEntries: [\"/contacts/1\"] },\n        );\n\n        let renderer: TestRenderer.ReactTestRenderer;\n        TestRenderer.act(() => {\n          renderer = TestRenderer.create(<RouterProvider router={router} />);\n        });\n\n        // @ts-expect-error\n        let button = renderer.root.findByType(\"button\");\n        await TestRenderer.act(() => button.props.onClick());\n\n        // @ts-expect-error\n        expect(renderer.toJSON()).toMatchInlineSnapshot(`\n          <h1>\n            Contacts\n          </h1>\n        `);\n      });\n\n      it(\"handles relative navigation from nested index route\", async () => {\n        let router = createMemoryRouter(\n          createRoutesFromElements(\n            <>\n              <Route path=\"layout\">\n                <Route path=\":param\">\n                  {/* redirect /layout/:param/ index routes to /layout/:param/dest */}\n                  <Route\n                    index\n                    element={<UseNavigateButton to=\"dest\" relative=\"path\" />}\n                  />\n                  <Route path=\"dest\" element={<h1>Destination</h1>} />\n                </Route>\n              </Route>\n            </>,\n          ),\n          { initialEntries: [\"/layout/thing\"] },\n        );\n\n        let renderer: TestRenderer.ReactTestRenderer;\n        TestRenderer.act(() => {\n          renderer = TestRenderer.create(<RouterProvider router={router} />);\n        });\n\n        // @ts-expect-error\n        let button = renderer.root.findByType(\"button\");\n        await TestRenderer.act(() => button.props.onClick());\n\n        // @ts-expect-error\n        expect(renderer.toJSON()).toMatchInlineSnapshot(`\n          <h1>\n            Destination\n          </h1>\n        `);\n      });\n\n      it(\"preserves search params and hash\", async () => {\n        let router = createMemoryRouter(\n          createRoutesFromElements(\n            <>\n              <Route path=\"contacts\" element={<Contacts />} />\n              <Route\n                path=\"contacts/:id\"\n                element={\n                  <UseNavigateButton to=\"..?foo=bar#hash\" relative=\"path\" />\n                }\n              />\n            </>,\n          ),\n          { initialEntries: [\"/contacts/1\"] },\n        );\n\n        function Contacts() {\n          let { search, hash } = useLocation();\n          return (\n            <>\n              <h1>Contacts</h1>\n              <p>\n                {search}\n                {hash}\n              </p>\n            </>\n          );\n        }\n\n        let renderer: TestRenderer.ReactTestRenderer;\n        TestRenderer.act(() => {\n          renderer = TestRenderer.create(<RouterProvider router={router} />);\n        });\n\n        // @ts-expect-error\n        let button = renderer.root.findByType(\"button\");\n        await TestRenderer.act(() => button.props.onClick());\n\n        // @ts-expect-error\n        expect(renderer.toJSON()).toMatchInlineSnapshot(`\n          [\n            <h1>\n              Contacts\n            </h1>,\n            <p>\n              ?foo=bar\n              #hash\n            </p>,\n          ]\n        `);\n      });\n    });\n\n    it(\"is stable across location changes\", () => {\n      let router = createMemoryRouter(\n        [\n          {\n            path: \"/\",\n            Component: () => (\n              <>\n                <NavBar />\n                <Outlet />\n              </>\n            ),\n            children: [\n              {\n                path: \"home\",\n                element: <h1>Home</h1>,\n              },\n              {\n                path: \"about\",\n                element: <h1>About</h1>,\n              },\n            ],\n          },\n        ],\n        { initialEntries: [\"/home\"] },\n      );\n\n      let renderer: TestRenderer.ReactTestRenderer;\n      TestRenderer.act(() => {\n        renderer = TestRenderer.create(<RouterProvider router={router} />);\n      });\n\n      function NavBar() {\n        let count = React.useRef(0);\n        let navigate = useNavigate();\n        React.useEffect(() => {\n          count.current++;\n        }, [navigate]);\n        return (\n          <nav>\n            <button onClick={() => router.navigate(\"/home\")}>Home</button>\n            <button onClick={() => router.navigate(\"/about\")}>About</button>\n            <p>{`count:${count.current}`}</p>\n          </nav>\n        );\n      }\n\n      // @ts-expect-error\n      expect(renderer.toJSON()).toMatchInlineSnapshot(`\n        [\n          <nav>\n            <button\n              onClick={[Function]}\n            >\n              Home\n            </button>\n            <button\n              onClick={[Function]}\n            >\n              About\n            </button>\n            <p>\n              count:0\n            </p>\n          </nav>,\n          <h1>\n            Home\n          </h1>,\n        ]\n      `);\n\n      // @ts-expect-error\n      let buttons = renderer.root.findAllByType(\"button\");\n      TestRenderer.act(() => {\n        buttons[1].props.onClick(); // link to /about\n      });\n\n      // @ts-expect-error\n      expect(renderer.toJSON()).toMatchInlineSnapshot(`\n        [\n          <nav>\n            <button\n              onClick={[Function]}\n            >\n              Home\n            </button>\n            <button\n              onClick={[Function]}\n            >\n              About\n            </button>\n            <p>\n              count:1\n            </p>\n          </nav>,\n          <h1>\n            About\n          </h1>,\n        ]\n      `);\n\n      // @ts-expect-error\n      buttons = renderer.root.findAllByType(\"button\");\n      TestRenderer.act(() => {\n        buttons[0].props.onClick(); // link back to /home\n      });\n\n      // @ts-expect-error\n      expect(renderer.toJSON()).toMatchInlineSnapshot(`\n        [\n          <nav>\n            <button\n              onClick={[Function]}\n            >\n              Home\n            </button>\n            <button\n              onClick={[Function]}\n            >\n              About\n            </button>\n            <p>\n              count:1\n            </p>\n          </nav>,\n          <h1>\n            Home\n          </h1>,\n        ]\n      `);\n    });\n  });\n\n  describe(\"with a basename\", () => {\n    describe(\"in a MemoryRouter\", () => {\n      it(\"in a root route\", async () => {\n        let renderer: TestRenderer.ReactTestRenderer;\n        TestRenderer.act(() => {\n          renderer = TestRenderer.create(\n            <MemoryRouter basename=\"/base\" initialEntries={[\"/base\"]}>\n              <Routes>\n                <Route path=\"/\" element={<Home />} />\n                <Route path=\"/path\" element={<h1>Path</h1>} />\n              </Routes>\n            </MemoryRouter>,\n          );\n        });\n\n        function Home() {\n          let navigate = useNavigate();\n          return <button onClick={() => navigate(\"/path\")} />;\n        }\n\n        // @ts-expect-error\n        expect(renderer.toJSON()).toMatchInlineSnapshot(`\n          <button\n            onClick={[Function]}\n          />\n        `);\n\n        // @ts-expect-error\n        let button = renderer.root.findByType(\"button\");\n        await TestRenderer.act(() => button.props.onClick());\n\n        // @ts-expect-error\n        expect(renderer.toJSON()).toMatchInlineSnapshot(`\n          <h1>\n            Path\n          </h1>\n        `);\n      });\n\n      it(\"in a descendant route\", async () => {\n        let renderer: TestRenderer.ReactTestRenderer;\n        TestRenderer.act(() => {\n          renderer = TestRenderer.create(\n            <MemoryRouter basename=\"/base\" initialEntries={[\"/base\"]}>\n              <Routes>\n                <Route\n                  path=\"/*\"\n                  element={\n                    <Routes>\n                      <Route index element={<Home />} />\n                    </Routes>\n                  }\n                />\n                <Route path=\"/path\" element={<h1>Path</h1>} />\n              </Routes>\n            </MemoryRouter>,\n          );\n        });\n\n        function Home() {\n          let navigate = useNavigate();\n          return <button onClick={() => navigate(\"/path\")} />;\n        }\n\n        // @ts-expect-error\n        expect(renderer.toJSON()).toMatchInlineSnapshot(`\n          <button\n            onClick={[Function]}\n          />\n        `);\n\n        // @ts-expect-error\n        let button = renderer.root.findByType(\"button\");\n        await TestRenderer.act(() => button.props.onClick());\n\n        // @ts-expect-error\n        expect(renderer.toJSON()).toMatchInlineSnapshot(`\n          <h1>\n            Path\n          </h1>\n        `);\n      });\n    });\n\n    describe(\"in a RouterProvider\", () => {\n      it(\"in a root route\", async () => {\n        let router = createMemoryRouter(\n          [\n            {\n              path: \"/\",\n              Component: Home,\n            },\n            { path: \"/path\", Component: () => <h1>Path</h1> },\n          ],\n          { basename: \"/base\", initialEntries: [\"/base\"] },\n        );\n\n        function Home() {\n          let navigate = useNavigate();\n          return <button onClick={() => navigate(\"/path\")} />;\n        }\n\n        let renderer: TestRenderer.ReactTestRenderer;\n        TestRenderer.act(() => {\n          renderer = TestRenderer.create(<RouterProvider router={router} />);\n        });\n\n        // @ts-expect-error\n        expect(renderer.toJSON()).toMatchInlineSnapshot(`\n          <button\n            onClick={[Function]}\n          />\n        `);\n\n        // @ts-expect-error\n        let button = renderer.root.findByType(\"button\");\n        await TestRenderer.act(() => button.props.onClick());\n\n        // @ts-expect-error\n        expect(renderer.toJSON()).toMatchInlineSnapshot(`\n          <h1>\n            Path\n          </h1>\n        `);\n      });\n\n      it(\"in a descendant route\", async () => {\n        let router = createMemoryRouter(\n          [\n            {\n              path: \"/*\",\n              Component() {\n                return (\n                  <Routes>\n                    <Route index element={<Home />} />\n                  </Routes>\n                );\n              },\n            },\n            { path: \"/path\", Component: () => <h1>Path</h1> },\n          ],\n          { basename: \"/base\", initialEntries: [\"/base\"] },\n        );\n\n        function Home() {\n          let navigate = useNavigate();\n          return <button onClick={() => navigate(\"/path\")} />;\n        }\n\n        let renderer: TestRenderer.ReactTestRenderer;\n        TestRenderer.act(() => {\n          renderer = TestRenderer.create(<RouterProvider router={router} />);\n        });\n\n        // @ts-expect-error\n        expect(renderer.toJSON()).toMatchInlineSnapshot(`\n          <button\n            onClick={[Function]}\n          />\n        `);\n\n        // @ts-expect-error\n        let button = renderer.root.findByType(\"button\");\n        await TestRenderer.act(() => button.props.onClick());\n\n        // @ts-expect-error\n        expect(renderer.toJSON()).toMatchInlineSnapshot(`\n          <h1>\n            Path\n          </h1>\n        `);\n      });\n    });\n  });\n});\n\nfunction UseNavigateButton({\n  to,\n  relative,\n}: {\n  to: To;\n  relative?: RelativeRoutingType;\n}) {\n  let navigate = useNavigate();\n  return <button onClick={() => navigate(to, { relative })}>Navigate</button>;\n}\n"
  },
  {
    "path": "packages/react-router/__tests__/useOutlet-test.tsx",
    "content": "import * as React from \"react\";\nimport * as TestRenderer from \"react-test-renderer\";\nimport {\n  MemoryRouter,\n  Routes,\n  Route,\n  useOutlet,\n  useOutletContext,\n} from \"react-router\";\n\ndescribe(\"useOutlet\", () => {\n  describe(\"when there is no child route\", () => {\n    it(\"returns null\", () => {\n      function Home() {\n        return useOutlet();\n      }\n\n      let renderer: TestRenderer.ReactTestRenderer;\n      TestRenderer.act(() => {\n        renderer = TestRenderer.create(\n          <MemoryRouter initialEntries={[\"/home\"]}>\n            <Routes>\n              <Route path=\"/home\" element={<Home />} />\n            </Routes>\n          </MemoryRouter>,\n        );\n      });\n\n      expect(renderer.toJSON()).toBeNull();\n    });\n\n    it(\"renders the fallback\", () => {\n      function Home() {\n        let outlet = useOutlet();\n        return <div>{outlet ? \"outlet\" : \"no outlet\"}</div>;\n      }\n\n      let renderer: TestRenderer.ReactTestRenderer;\n      TestRenderer.act(() => {\n        renderer = TestRenderer.create(\n          <MemoryRouter initialEntries={[\"/home\"]}>\n            <Routes>\n              <Route path=\"/home\" element={<Home />} />\n            </Routes>\n          </MemoryRouter>,\n        );\n      });\n\n      expect(renderer.toJSON()).toMatchInlineSnapshot(`\n        <div>\n          no outlet\n        </div>\n      `);\n    });\n\n    it(\"renders the fallback with context provided\", () => {\n      function Home() {\n        let outlet = useOutlet({ some: \"context\" });\n        return <div>{outlet ? \"outlet\" : \"no outlet\"}</div>;\n      }\n\n      let renderer: TestRenderer.ReactTestRenderer;\n      TestRenderer.act(() => {\n        renderer = TestRenderer.create(\n          <MemoryRouter initialEntries={[\"/home\"]}>\n            <Routes>\n              <Route path=\"/home\" element={<Home />} />\n            </Routes>\n          </MemoryRouter>,\n        );\n      });\n\n      expect(renderer.toJSON()).toMatchInlineSnapshot(`\n        <div>\n          no outlet\n        </div>\n      `);\n    });\n  });\n\n  describe(\"when there is a child route\", () => {\n    it(\"returns an element\", () => {\n      function Users() {\n        return useOutlet();\n      }\n\n      let renderer: TestRenderer.ReactTestRenderer;\n      TestRenderer.act(() => {\n        renderer = TestRenderer.create(\n          <MemoryRouter initialEntries={[\"/users/profile\"]}>\n            <Routes>\n              <Route path=\"users\" element={<Users />}>\n                <Route path=\"profile\" element={<h1>Profile</h1>} />\n              </Route>\n            </Routes>\n          </MemoryRouter>,\n        );\n      });\n\n      expect(renderer.toJSON()).toMatchInlineSnapshot(`\n        <h1>\n          Profile\n        </h1>\n      `);\n    });\n\n    it(\"returns an element when no context\", () => {\n      function Home() {\n        let outlet = useOutlet();\n        return <div>{outlet ? \"outlet\" : \"no outlet\"}</div>;\n      }\n\n      let renderer: TestRenderer.ReactTestRenderer;\n      TestRenderer.act(() => {\n        renderer = TestRenderer.create(\n          <MemoryRouter initialEntries={[\"/home\"]}>\n            <Routes>\n              <Route path=\"/home\" element={<Home />}>\n                <Route index element={<div>index</div>} />\n              </Route>\n            </Routes>\n          </MemoryRouter>,\n        );\n      });\n\n      expect(renderer.toJSON()).toMatchInlineSnapshot(`\n          <div>\n            outlet\n          </div>\n        `);\n    });\n\n    it(\"returns an element when context\", () => {\n      function Home() {\n        let outlet = useOutlet({ some: \"context\" });\n        return <div>{outlet ? \"outlet\" : \"no outlet\"}</div>;\n      }\n\n      let renderer: TestRenderer.ReactTestRenderer;\n      TestRenderer.act(() => {\n        renderer = TestRenderer.create(\n          <MemoryRouter initialEntries={[\"/home\"]}>\n            <Routes>\n              <Route path=\"/home\" element={<Home />}>\n                <Route index element={<div>index</div>} />\n              </Route>\n            </Routes>\n          </MemoryRouter>,\n        );\n      });\n\n      expect(renderer.toJSON()).toMatchInlineSnapshot(`\n          <div>\n            outlet\n          </div>\n        `);\n    });\n  });\n\n  describe(\"OutletContext when there is no context\", () => {\n    it(\"returns null\", () => {\n      function Users() {\n        return useOutlet();\n      }\n\n      function Profile() {\n        let outletContext = useOutletContext();\n\n        return (\n          <div>\n            <h1>Profile</h1>\n            <pre>{outletContext}</pre>\n          </div>\n        );\n      }\n\n      let renderer: TestRenderer.ReactTestRenderer;\n      TestRenderer.act(() => {\n        renderer = TestRenderer.create(\n          <MemoryRouter initialEntries={[\"/users/profile\"]}>\n            <Routes>\n              <Route path=\"users\" element={<Users />}>\n                <Route path=\"profile\" element={<Profile />} />\n              </Route>\n            </Routes>\n          </MemoryRouter>,\n        );\n      });\n\n      expect(renderer.toJSON()).toMatchInlineSnapshot(`\n        <div>\n          <h1>\n            Profile\n          </h1>\n          <pre />\n        </div>\n      `);\n    });\n  });\n\n  describe(\"OutletContext when there is context\", () => {\n    it(\"returns the context\", () => {\n      function Users() {\n        return useOutlet([\n          \"Mary\",\n          \"Jane\",\n          \"Michael\",\n          \"Bert\",\n          \"Winifred\",\n          \"George\",\n        ]);\n      }\n\n      function Profile() {\n        let outletContext = useOutletContext<string[]>();\n\n        return (\n          <div>\n            <h1>Profile</h1>\n            <ul>\n              {outletContext.map((name) => (\n                <li key={name}>{name}</li>\n              ))}\n            </ul>\n          </div>\n        );\n      }\n\n      let renderer: TestRenderer.ReactTestRenderer;\n      TestRenderer.act(() => {\n        renderer = TestRenderer.create(\n          <MemoryRouter initialEntries={[\"/users/profile\"]}>\n            <Routes>\n              <Route path=\"users\" element={<Users />}>\n                <Route path=\"profile\" element={<Profile />} />\n              </Route>\n            </Routes>\n          </MemoryRouter>,\n        );\n      });\n\n      expect(renderer.toJSON()).toMatchInlineSnapshot(`\n        <div>\n          <h1>\n            Profile\n          </h1>\n          <ul>\n            <li>\n              Mary\n            </li>\n            <li>\n              Jane\n            </li>\n            <li>\n              Michael\n            </li>\n            <li>\n              Bert\n            </li>\n            <li>\n              Winifred\n            </li>\n            <li>\n              George\n            </li>\n          </ul>\n        </div>\n      `);\n    });\n  });\n\n  describe(\"when child route without element prop\", () => {\n    it(\"returns nested route element\", () => {\n      function Layout() {\n        return useOutlet();\n      }\n\n      let renderer: TestRenderer.ReactTestRenderer;\n      TestRenderer.act(() => {\n        renderer = TestRenderer.create(\n          <MemoryRouter initialEntries={[\"/users/profile\"]}>\n            <Routes>\n              <Route path=\"/\" element={<Layout />}>\n                <Route path=\"users\">\n                  <Route path=\"profile\" element={<h1>Profile</h1>} />\n                </Route>\n              </Route>\n            </Routes>\n          </MemoryRouter>,\n        );\n      });\n\n      expect(renderer.toJSON()).toMatchInlineSnapshot(`\n        <h1>\n          Profile\n        </h1>\n      `);\n    });\n\n    it(\"returns the context\", () => {\n      function Layout() {\n        return useOutlet([\"Mary\", \"Jane\", \"Michael\"]);\n      }\n\n      function Profile() {\n        let outletContext = useOutletContext<string[]>();\n\n        return (\n          <div>\n            <h1>Profile</h1>\n            <ul>\n              {outletContext.map((name) => (\n                <li key={name}>{name}</li>\n              ))}\n            </ul>\n          </div>\n        );\n      }\n\n      let renderer: TestRenderer.ReactTestRenderer;\n      TestRenderer.act(() => {\n        renderer = TestRenderer.create(\n          <MemoryRouter initialEntries={[\"/users/profile\"]}>\n            <Routes>\n              <Route path=\"/\" element={<Layout />}>\n                <Route path=\"users\">\n                  <Route path=\"profile\" element={<Profile />} />\n                </Route>\n              </Route>\n            </Routes>\n          </MemoryRouter>,\n        );\n      });\n\n      expect(renderer.toJSON()).toMatchInlineSnapshot(`\n        <div>\n          <h1>\n            Profile\n          </h1>\n          <ul>\n            <li>\n              Mary\n            </li>\n            <li>\n              Jane\n            </li>\n            <li>\n              Michael\n            </li>\n          </ul>\n        </div>\n      `);\n    });\n  });\n});\n"
  },
  {
    "path": "packages/react-router/__tests__/useParams-test.tsx",
    "content": "import * as React from \"react\";\nimport * as TestRenderer from \"react-test-renderer\";\nimport {\n  MemoryRouter,\n  Outlet,\n  Routes,\n  Route,\n  useParams,\n  useLocation,\n  generatePath,\n} from \"react-router\";\n\nfunction ShowParams() {\n  return <pre>{JSON.stringify(useParams())}</pre>;\n}\n\ndescribe(\"useParams\", () => {\n  describe(\"when the route isn't matched\", () => {\n    it(\"returns an empty object\", () => {\n      let renderer: TestRenderer.ReactTestRenderer;\n      TestRenderer.act(() => {\n        renderer = TestRenderer.create(\n          <MemoryRouter initialEntries={[\"/home\"]}>\n            <ShowParams />\n          </MemoryRouter>,\n        );\n      });\n\n      expect(renderer.toJSON()).toMatchInlineSnapshot(`\n        <pre>\n          {}\n        </pre>\n      `);\n    });\n  });\n\n  describe(\"when the path has no params\", () => {\n    it(\"returns an empty object\", () => {\n      let renderer: TestRenderer.ReactTestRenderer;\n      TestRenderer.act(() => {\n        renderer = TestRenderer.create(\n          <MemoryRouter initialEntries={[\"/home\"]}>\n            <Routes>\n              <Route path=\"/home\" element={<ShowParams />} />\n            </Routes>\n          </MemoryRouter>,\n        );\n      });\n\n      expect(renderer.toJSON()).toMatchInlineSnapshot(`\n        <pre>\n          {}\n        </pre>\n      `);\n    });\n  });\n\n  describe(\"when the path has some params\", () => {\n    it(\"returns an object of the URL params\", () => {\n      let renderer: TestRenderer.ReactTestRenderer;\n      TestRenderer.act(() => {\n        renderer = TestRenderer.create(\n          <MemoryRouter initialEntries={[\"/blog/react-router\"]}>\n            <Routes>\n              <Route path=\"/blog/:slug\" element={<ShowParams />} />\n            </Routes>\n          </MemoryRouter>,\n        );\n      });\n\n      expect(renderer.toJSON()).toMatchInlineSnapshot(`\n        <pre>\n          {\"slug\":\"react-router\"}\n        </pre>\n      `);\n    });\n\n    describe(\"a child route\", () => {\n      it(\"returns a combined hash of the parent and child params\", () => {\n        function UserDashboard() {\n          return (\n            <div>\n              <h1>User Dashboard</h1>\n              <Outlet />\n            </div>\n          );\n        }\n\n        let renderer: TestRenderer.ReactTestRenderer;\n        TestRenderer.act(() => {\n          renderer = TestRenderer.create(\n            <MemoryRouter\n              initialEntries={[\"/users/mjackson/courses/react-router\"]}\n            >\n              <Routes>\n                <Route path=\"users/:username\" element={<UserDashboard />}>\n                  <Route path=\"courses/:course\" element={<ShowParams />} />\n                </Route>\n              </Routes>\n            </MemoryRouter>,\n          );\n        });\n\n        expect(renderer.toJSON()).toMatchInlineSnapshot(`\n          <div>\n            <h1>\n              User Dashboard\n            </h1>\n            <pre>\n              {\"username\":\"mjackson\",\"course\":\"react-router\"}\n            </pre>\n          </div>\n        `);\n      });\n    });\n  });\n\n  describe(\"when the path has percent-encoded params\", () => {\n    it(\"returns an object of the decoded params\", () => {\n      let renderer: TestRenderer.ReactTestRenderer;\n      TestRenderer.act(() => {\n        renderer = TestRenderer.create(\n          <MemoryRouter initialEntries={[\"/blog/react%20router\"]}>\n            <Routes>\n              <Route path=\"/blog/:slug\" element={<ShowParams />} />\n            </Routes>\n          </MemoryRouter>,\n        );\n      });\n\n      expect(renderer.toJSON()).toMatchInlineSnapshot(`\n        <pre>\n          {\"slug\":\"react router\"}\n        </pre>\n      `);\n    });\n  });\n\n  describe(\"when the path has a + character\", () => {\n    it(\"returns an object of the decoded params\", () => {\n      let renderer: TestRenderer.ReactTestRenderer;\n      TestRenderer.act(() => {\n        renderer = TestRenderer.create(\n          <MemoryRouter initialEntries={[\"/blog/react+router+is%20awesome\"]}>\n            <Routes>\n              <Route path=\"/blog/:slug\" element={<ShowParams />} />\n            </Routes>\n          </MemoryRouter>,\n        );\n      });\n\n      expect(renderer.toJSON()).toMatchInlineSnapshot(`\n        <pre>\n          {\"slug\":\"react+router+is awesome\"}\n        </pre>\n      `);\n    });\n  });\n\n  describe(\"when the path has a malformed param\", () => {\n    let consoleWarn: jest.SpyInstance<\n      ReturnType<typeof console.warn>,\n      Parameters<typeof console.warn>\n    >;\n\n    beforeEach(() => {\n      consoleWarn = jest.spyOn(console, \"warn\").mockImplementation(() => {});\n    });\n\n    afterEach(() => {\n      consoleWarn.mockRestore();\n    });\n\n    it(\"returns the raw value and warns\", () => {\n      let renderer: TestRenderer.ReactTestRenderer;\n      TestRenderer.act(() => {\n        renderer = TestRenderer.create(\n          <MemoryRouter initialEntries={[\"/blog/react%2router\"]}>\n            <Routes>\n              <Route path=\"/blog/:slug\" element={<ShowParams />} />\n            </Routes>\n          </MemoryRouter>,\n        );\n      });\n\n      expect(renderer.toJSON()).toMatchInlineSnapshot(`\n        <pre>\n          {\"slug\":\"react%2router\"}\n        </pre>\n      `);\n\n      expect(consoleWarn).toHaveBeenCalledWith(\n        expect.stringMatching(\"malformed URL segment\"),\n      );\n    });\n  });\n\n  describe(\"when the params match in a child route\", () => {\n    it(\"renders params in the parent\", () => {\n      let renderer: TestRenderer.ReactTestRenderer;\n      TestRenderer.act(() => {\n        renderer = TestRenderer.create(\n          <MemoryRouter initialEntries={[\"/blog/react-router\"]}>\n            <Routes>\n              <Route path=\"/blog\" element={<ShowParams />}>\n                <Route path=\":slug\" element={null} />\n              </Route>\n            </Routes>\n          </MemoryRouter>,\n        );\n      });\n\n      expect(renderer.toJSON()).toMatchInlineSnapshot(`\n        <pre>\n          {\"slug\":\"react-router\"}\n        </pre>\n      `);\n    });\n  });\n\n  test(\"maintains compatibility with generatePath\", () => {\n    let tests = [\n      {\n        path: \"/books/42\",\n        url: \"/books/42\",\n        params: {},\n      },\n      {\n        path: \"/books/:id\",\n        url: \"/books/42\",\n        params: { id: \"42\" },\n      },\n      {\n        path: \"/books/:id.json\",\n        url: \"/books/42.json\",\n        params: { id: \"42\" },\n      },\n      {\n        path: \"/books/:id/comments\",\n        url: \"/books/42/comments\",\n        params: { id: \"42\" },\n      },\n      {\n        path: \"/books/:id.json/comments\",\n        url: \"/books/42.json/comments\",\n        params: { id: \"42\" },\n      },\n    ];\n\n    function ShowParamsAndPath({ path }: { path: string }) {\n      return (\n        <>\n          <p>{JSON.stringify(useParams())}</p>\n          <p>{useLocation().pathname}</p>\n          <p>{generatePath(path, useParams())}</p>\n        </>\n      );\n    }\n\n    for (let { path, url, params } of tests) {\n      let renderer: TestRenderer.ReactTestRenderer;\n      TestRenderer.act(() => {\n        renderer = TestRenderer.create(\n          <MemoryRouter initialEntries={[url]}>\n            <Routes>\n              <Route path={path} element={<ShowParamsAndPath path={path} />} />\n            </Routes>\n          </MemoryRouter>,\n        );\n      });\n\n      // eslint-disable-next-line jest/no-interpolation-in-snapshots\n      expect(renderer.toJSON()).toMatchInlineSnapshot(`\n        [\n          <p>\n            ${JSON.stringify(params)}\n          </p>,\n          <p>\n            ${url}\n          </p>,\n          <p>\n            ${url}\n          </p>,\n        ]\n      `);\n    }\n  });\n});\n"
  },
  {
    "path": "packages/react-router/__tests__/useResolvedPath-test.tsx",
    "content": "import * as React from \"react\";\nimport * as TestRenderer from \"react-test-renderer\";\nimport type { Path } from \"react-router\";\nimport {\n  MemoryRouter,\n  Routes,\n  Route,\n  useResolvedPath,\n  useLocation,\n} from \"react-router\";\nimport { prettyDOM, render } from \"@testing-library/react\";\n\nfunction ShowResolvedPath({ path }: { path: string | Path }) {\n  return <pre>{JSON.stringify(useResolvedPath(path))}</pre>;\n}\n\ndescribe(\"useResolvedPath\", () => {\n  it(\"path string resolves correctly\", () => {\n    let renderer: TestRenderer.ReactTestRenderer;\n    TestRenderer.act(() => {\n      renderer = TestRenderer.create(\n        <MemoryRouter initialEntries={[\"/\"]}>\n          <Routes>\n            <Route\n              path=\"/\"\n              element={<ShowResolvedPath path=\"/home?user=mj#welcome\" />}\n            />\n          </Routes>\n        </MemoryRouter>,\n      );\n    });\n\n    expect(renderer.toJSON()).toMatchInlineSnapshot(`\n      <pre>\n        {\"pathname\":\"/home\",\"search\":\"?user=mj\",\"hash\":\"#welcome\"}\n      </pre>\n    `);\n  });\n\n  it(\"partial path object resolves correctly\", () => {\n    let renderer: TestRenderer.ReactTestRenderer;\n    TestRenderer.act(() => {\n      renderer = TestRenderer.create(\n        <MemoryRouter initialEntries={[\"/\"]}>\n          <Routes>\n            <Route\n              path=\"/\"\n              element={\n                <ShowResolvedPath\n                  path={{\n                    pathname: \"/home\",\n                    search: new URLSearchParams({ user: \"mj\" }).toString(),\n                    hash: \"#welcome\",\n                  }}\n                />\n              }\n            />\n          </Routes>\n        </MemoryRouter>,\n      );\n    });\n\n    expect(renderer.toJSON()).toMatchInlineSnapshot(`\n      <pre>\n        {\"pathname\":\"/home\",\"search\":\"?user=mj\",\"hash\":\"#welcome\"}\n      </pre>\n    `);\n  });\n\n  describe(\"given a hash with a ? character\", () => {\n    it(\"hash is not parsed as a search string\", () => {\n      let renderer: TestRenderer.ReactTestRenderer;\n      TestRenderer.act(() => {\n        renderer = TestRenderer.create(\n          <MemoryRouter initialEntries={[\"/\"]}>\n            <Routes>\n              <Route\n                path=\"/\"\n                element={<ShowResolvedPath path=\"/home#welcome?user=mj\" />}\n              />\n            </Routes>\n          </MemoryRouter>,\n        );\n      });\n\n      expect(renderer.toJSON()).toMatchInlineSnapshot(`\n        <pre>\n          {\"pathname\":\"/home\",\"search\":\"\",\"hash\":\"#welcome?user=mj\"}\n        </pre>\n      `);\n    });\n  });\n\n  describe(\"in a splat route\", () => {\n    it(\"resolves . to the route path\", () => {\n      let renderer: TestRenderer.ReactTestRenderer;\n      TestRenderer.act(() => {\n        renderer = TestRenderer.create(\n          <MemoryRouter initialEntries={[\"/users/mj\"]}>\n            <Routes>\n              <Route path=\"/users\">\n                <Route path=\"*\" element={<ShowResolvedPath path=\".\" />} />\n              </Route>\n            </Routes>\n          </MemoryRouter>,\n        );\n      });\n\n      expect(renderer.toJSON()).toMatchInlineSnapshot(`\n        <pre>\n          {\"pathname\":\"/users/mj\",\"search\":\"\",\"hash\":\"\"}\n        </pre>\n      `);\n    });\n\n    it(\"resolves .. to the parent route path\", () => {\n      let renderer: TestRenderer.ReactTestRenderer;\n      TestRenderer.act(() => {\n        renderer = TestRenderer.create(\n          <MemoryRouter initialEntries={[\"/users/mj\"]}>\n            <Routes>\n              <Route path=\"/users\">\n                <Route path=\"*\" element={<ShowResolvedPath path=\"..\" />} />\n              </Route>\n            </Routes>\n          </MemoryRouter>,\n        );\n      });\n\n      expect(renderer.toJSON()).toMatchInlineSnapshot(`\n        <pre>\n          {\"pathname\":\"/users\",\"search\":\"\",\"hash\":\"\"}\n        </pre>\n      `);\n    });\n\n    it(\"resolves . to the route path (descendant route)\", () => {\n      let renderer: TestRenderer.ReactTestRenderer;\n      TestRenderer.act(() => {\n        renderer = TestRenderer.create(\n          <MemoryRouter initialEntries={[\"/users/mj\"]}>\n            <Routes>\n              <Route\n                path=\"/users/*\"\n                element={\n                  <Routes>\n                    <Route path=\"mj\" element={<ShowResolvedPath path=\".\" />} />\n                  </Routes>\n                }\n              />\n            </Routes>\n          </MemoryRouter>,\n        );\n      });\n\n      expect(renderer.toJSON()).toMatchInlineSnapshot(`\n        <pre>\n          {\"pathname\":\"/users/mj\",\"search\":\"\",\"hash\":\"\"}\n        </pre>\n      `);\n    });\n\n    it(\"resolves .. to the parent route path (descendant route)\", () => {\n      let renderer: TestRenderer.ReactTestRenderer;\n      TestRenderer.act(() => {\n        renderer = TestRenderer.create(\n          <MemoryRouter initialEntries={[\"/users/mj\"]}>\n            <Routes>\n              <Route\n                path=\"/users/*\"\n                element={\n                  <Routes>\n                    <Route path=\"mj\" element={<ShowResolvedPath path=\"..\" />} />\n                  </Routes>\n                }\n              />\n            </Routes>\n          </MemoryRouter>,\n        );\n      });\n\n      expect(renderer.toJSON()).toMatchInlineSnapshot(`\n        <pre>\n          {\"pathname\":\"/users\",\"search\":\"\",\"hash\":\"\"}\n        </pre>\n      `);\n    });\n  });\n\n  describe(\"in a param route\", () => {\n    it(\"resolves . to the route path\", () => {\n      let renderer: TestRenderer.ReactTestRenderer;\n      TestRenderer.act(() => {\n        renderer = TestRenderer.create(\n          <MemoryRouter initialEntries={[\"/users/mj\"]}>\n            <Routes>\n              <Route path=\"/users\">\n                <Route path=\":name\" element={<ShowResolvedPath path=\".\" />} />\n              </Route>\n            </Routes>\n          </MemoryRouter>,\n        );\n      });\n\n      expect(renderer.toJSON()).toMatchInlineSnapshot(`\n        <pre>\n          {\"pathname\":\"/users/mj\",\"search\":\"\",\"hash\":\"\"}\n        </pre>\n      `);\n    });\n\n    it(\"resolves .. to the parent route\", () => {\n      let renderer: TestRenderer.ReactTestRenderer;\n      TestRenderer.act(() => {\n        renderer = TestRenderer.create(\n          <MemoryRouter initialEntries={[\"/users/mj\"]}>\n            <Routes>\n              <Route path=\"/users\">\n                <Route path=\":name\" element={<ShowResolvedPath path=\"..\" />} />\n              </Route>\n            </Routes>\n          </MemoryRouter>,\n        );\n      });\n\n      expect(renderer.toJSON()).toMatchInlineSnapshot(`\n        <pre>\n          {\"pathname\":\"/users\",\"search\":\"\",\"hash\":\"\"}\n        </pre>\n      `);\n    });\n  });\n\n  function LogResolvedPathInfo({ desc }) {\n    return (\n      <>\n        {`--- Routes: ${desc} ---`}\n        {`useLocation(): ${useLocation().pathname}`}\n        {`useResolvedPath('.'): ${useResolvedPath(\".\").pathname}`}\n        {`useResolvedPath('..'): ${useResolvedPath(\"..\").pathname}`}\n        {`useResolvedPath('..', { relative: 'path' }): ${\n          useResolvedPath(\"..\", { relative: \"path\" }).pathname\n        }`}\n        {`useResolvedPath('baz/qux'): ${useResolvedPath(\"baz/qux\").pathname}`}\n        {`useResolvedPath('./baz/qux'): ${\n          useResolvedPath(\"./baz/qux\").pathname\n        }\\n`}\n      </>\n    );\n  }\n\n  // See: https://github.com/remix-run/react-router/issues/11052#issuecomment-1836589329\n  it(\"resolves splat route relative paths the same as other routes\", async () => {\n    function App({ enableFlag }: { enableFlag: boolean }) {\n      let routeConfigs = [\n        {\n          routes: (\n            <Route\n              path=\"/foo/bar\"\n              element={<LogResolvedPathInfo desc='<Route path=\"/foo/bar\" />' />}\n            />\n          ),\n        },\n        {\n          routes: (\n            <Route\n              path=\"/foo/:param\"\n              element={\n                <LogResolvedPathInfo desc='<Route path=\"/foo/:param\" />' />\n              }\n            />\n          ),\n        },\n        {\n          routes: (\n            <Route path=\"/foo\">\n              <Route\n                path=\":param\"\n                element={\n                  <LogResolvedPathInfo desc='<Route path=\"/foo\"><Route path=\":param\" />' />\n                }\n              />\n            </Route>\n          ),\n        },\n        {\n          routes: (\n            <Route\n              path=\"/foo/*\"\n              element={<LogResolvedPathInfo desc='<Route path=\"/foo/*\" />' />}\n            />\n          ),\n        },\n        {\n          routes: (\n            <Route path=\"foo\">\n              <Route\n                path=\"*\"\n                element={\n                  <LogResolvedPathInfo desc='<Route path=\"/foo\"><Route path=\"*\" />' />\n                }\n              />\n            </Route>\n          ),\n        },\n      ];\n\n      return (\n        <>\n          {routeConfigs.map((config, idx) => (\n            <MemoryRouter initialEntries={[\"/foo/bar\"]} key={idx}>\n              <Routes>{config.routes}</Routes>\n            </MemoryRouter>\n          ))}\n        </>\n      );\n    }\n\n    let { container } = render(<App enableFlag={true} />);\n    let html = getHtml(container);\n    html = html ? html.replace(/&lt;/g, \"<\").replace(/&gt;/g, \">\") : html;\n    expect(html).toMatchInlineSnapshot(`\n      \"<div>\n        --- Routes: <Route path=\"/foo/bar\" /> ---\n        useLocation(): /foo/bar\n        useResolvedPath('.'): /foo/bar\n        useResolvedPath('..'): /\n        useResolvedPath('..', { relative: 'path' }): /foo\n        useResolvedPath('baz/qux'): /foo/bar/baz/qux\n        useResolvedPath('./baz/qux'): /foo/bar/baz/qux\n\n        --- Routes: <Route path=\"/foo/:param\" /> ---\n        useLocation(): /foo/bar\n        useResolvedPath('.'): /foo/bar\n        useResolvedPath('..'): /\n        useResolvedPath('..', { relative: 'path' }): /foo\n        useResolvedPath('baz/qux'): /foo/bar/baz/qux\n        useResolvedPath('./baz/qux'): /foo/bar/baz/qux\n\n        --- Routes: <Route path=\"/foo\"><Route path=\":param\" /> ---\n        useLocation(): /foo/bar\n        useResolvedPath('.'): /foo/bar\n        useResolvedPath('..'): /foo\n        useResolvedPath('..', { relative: 'path' }): /foo\n        useResolvedPath('baz/qux'): /foo/bar/baz/qux\n        useResolvedPath('./baz/qux'): /foo/bar/baz/qux\n\n        --- Routes: <Route path=\"/foo/*\" /> ---\n        useLocation(): /foo/bar\n        useResolvedPath('.'): /foo/bar\n        useResolvedPath('..'): /\n        useResolvedPath('..', { relative: 'path' }): /foo\n        useResolvedPath('baz/qux'): /foo/bar/baz/qux\n        useResolvedPath('./baz/qux'): /foo/bar/baz/qux\n\n        --- Routes: <Route path=\"/foo\"><Route path=\"*\" /> ---\n        useLocation(): /foo/bar\n        useResolvedPath('.'): /foo/bar\n        useResolvedPath('..'): /foo\n        useResolvedPath('..', { relative: 'path' }): /foo\n        useResolvedPath('baz/qux'): /foo/bar/baz/qux\n        useResolvedPath('./baz/qux'): /foo/bar/baz/qux\n\n      </div>\"\n    `);\n  });\n\n  // gh-issue #11629\n  it(\"'.' resolves to the current path including any splat paths nested in pathless routes\", () => {\n    let { container } = render(\n      <MemoryRouter initialEntries={[\"/foo/bar\"]}>\n        <Routes>\n          <Route path=\"foo\">\n            <Route>\n              <Route\n                path=\"*\"\n                element={\n                  <LogResolvedPathInfo desc='<Route path=\"/foo\"><Route><Route path=\"*\" /></Route></Route>' />\n                }\n              />\n            </Route>\n          </Route>\n        </Routes>\n      </MemoryRouter>,\n    );\n    let html = getHtml(container);\n    html = html ? html.replace(/&lt;/g, \"<\").replace(/&gt;/g, \">\") : html;\n    expect(html).toMatchInlineSnapshot(`\n      \"<div>\n        --- Routes: <Route path=\"/foo\"><Route><Route path=\"*\" /></Route></Route> ---\n        useLocation(): /foo/bar\n        useResolvedPath('.'): /foo/bar\n        useResolvedPath('..'): /foo\n        useResolvedPath('..', { relative: 'path' }): /foo\n        useResolvedPath('baz/qux'): /foo/bar/baz/qux\n        useResolvedPath('./baz/qux'): /foo/bar/baz/qux\n\n      </div>\"\n    `);\n  });\n});\n\nfunction getHtml(container: HTMLElement) {\n  return prettyDOM(container, undefined, {\n    highlight: false,\n  });\n}\n"
  },
  {
    "path": "packages/react-router/__tests__/useRoutes-test.tsx",
    "content": "import * as React from \"react\";\nimport * as TestRenderer from \"react-test-renderer\";\nimport type { RouteObject } from \"react-router\";\nimport { MemoryRouter, useRoutes } from \"react-router\";\n\ndescribe(\"useRoutes\", () => {\n  it(\"returns the matching element from a route config\", () => {\n    let routes = [\n      { path: \"home\", element: <h1>home</h1> },\n      { path: \"about\", element: <h1>about</h1> },\n    ];\n\n    let renderer: TestRenderer.ReactTestRenderer;\n    TestRenderer.act(() => {\n      renderer = TestRenderer.create(\n        <MemoryRouter initialEntries={[\"/home\"]}>\n          <RoutesRenderer routes={routes} />\n        </MemoryRouter>,\n      );\n    });\n\n    expect(renderer.toJSON()).toMatchInlineSnapshot(`\n      <h1>\n        home\n      </h1>\n    `);\n  });\n\n  describe(\"when some routes are missing elements\", () => {\n    it(\"defaults to rendering their children\", () => {\n      let routes = [\n        {\n          path: \"users\",\n          children: [{ path: \":id\", element: <h1>user profile</h1> }],\n        },\n        { path: \"about\", element: <h1>about</h1> },\n      ];\n\n      let renderer: TestRenderer.ReactTestRenderer;\n      TestRenderer.act(() => {\n        renderer = TestRenderer.create(\n          <MemoryRouter initialEntries={[\"/users/mj\"]}>\n            <RoutesRenderer routes={routes} />\n          </MemoryRouter>,\n        );\n      });\n\n      expect(renderer.toJSON()).toMatchInlineSnapshot(`\n        <h1>\n          user profile\n        </h1>\n      `);\n    });\n  });\n\n  it(\"Uses the `location` prop instead of context location`\", () => {\n    let routes = [\n      { path: \"one\", element: <h1>one</h1> },\n      { path: \"two\", element: <h1>two</h1> },\n    ];\n\n    let renderer: TestRenderer.ReactTestRenderer;\n    TestRenderer.act(() => {\n      renderer = TestRenderer.create(\n        <MemoryRouter initialEntries={[\"/one\"]}>\n          <RoutesRenderer routes={routes} location={{ pathname: \"/two\" }} />\n        </MemoryRouter>,\n      );\n    });\n\n    expect(renderer.toJSON()).toMatchInlineSnapshot(`\n      <h1>\n        two\n      </h1>\n    `);\n  });\n\n  it(\"returns null when no route matches\", () => {\n    let spy = jest.spyOn(console, \"warn\").mockImplementation(() => {});\n\n    let routes = [{ path: \"one\", element: <h1>one</h1> }];\n\n    const NullRenderer = (props: { routes: RouteObject[] }) => {\n      const element = useRoutes(props.routes);\n      return element === null ? <div>is null</div> : <div>is not null</div>;\n    };\n\n    let renderer: TestRenderer.ReactTestRenderer;\n    TestRenderer.act(() => {\n      renderer = TestRenderer.create(\n        <MemoryRouter initialEntries={[\"/two\"]}>\n          <NullRenderer routes={routes} />\n        </MemoryRouter>,\n      );\n    });\n\n    expect(renderer.toJSON()).toMatchInlineSnapshot(`\n      <div>\n        is null\n      </div>\n    `);\n\n    spy.mockRestore();\n  });\n\n  it(\"returns null when no route matches and a `location` prop is passed\", () => {\n    let spy = jest.spyOn(console, \"warn\").mockImplementation(() => {});\n\n    let routes = [{ path: \"one\", element: <h1>one</h1> }];\n\n    const NullRenderer = (props: {\n      routes: RouteObject[];\n      location?: Partial<Location> & { pathname: string };\n    }) => {\n      const element = useRoutes(props.routes, props.location);\n      return element === null ? <div>is null</div> : <div>is not null</div>;\n    };\n\n    let renderer: TestRenderer.ReactTestRenderer;\n    TestRenderer.act(() => {\n      renderer = TestRenderer.create(\n        <MemoryRouter initialEntries={[\"/two\"]}>\n          <NullRenderer\n            routes={routes}\n            location={{ pathname: \"/three\", search: \"\", hash: \"\" }}\n          />\n        </MemoryRouter>,\n      );\n    });\n\n    expect(renderer.toJSON()).toMatchInlineSnapshot(`\n      <div>\n        is null\n      </div>\n    `);\n\n    spy.mockRestore();\n  });\n\n  describe(\"warns\", () => {\n    let consoleWarn: jest.SpyInstance;\n    beforeEach(() => {\n      consoleWarn = jest.spyOn(console, \"warn\").mockImplementation(() => {});\n    });\n\n    afterEach(() => {\n      consoleWarn.mockRestore();\n    });\n\n    it(\"for no element on leaf route\", () => {\n      let routes = [\n        {\n          path: \"layout\",\n          children: [{ path: \"two\", element: <h1>two</h1> }],\n        },\n      ];\n\n      TestRenderer.act(() => {\n        TestRenderer.create(\n          <MemoryRouter initialEntries={[\"/layout\"]}>\n            <RoutesRenderer routes={routes} />\n          </MemoryRouter>,\n        );\n      });\n\n      expect(consoleWarn).toHaveBeenCalledTimes(1);\n      expect(consoleWarn).toHaveBeenCalledWith(\n        expect.stringContaining(\n          `Matched leaf route at location \"/layout\" does not have an element`,\n        ),\n      );\n    });\n  });\n});\n\nfunction RoutesRenderer({\n  routes,\n  location,\n}: {\n  routes: RouteObject[];\n  location?: Partial<Location> & { pathname: string };\n}) {\n  return useRoutes(routes, location);\n}\n"
  },
  {
    "path": "packages/react-router/__tests__/utils/MemoryNavigate.tsx",
    "content": "import type { HTMLFormMethod } from \"../../lib/router/utils\";\nimport { joinPaths } from \"../../lib/router/utils\";\nimport * as React from \"react\";\nimport { UNSAFE_DataRouterContext } from \"../../index\";\n\nexport default function MemoryNavigate({\n  to,\n  formMethod,\n  formData,\n  children,\n}: {\n  to: string;\n  formMethod?: HTMLFormMethod;\n  formData?: FormData;\n  children: React.ReactNode;\n}) {\n  let dataRouterContext = React.useContext(UNSAFE_DataRouterContext);\n\n  let onClickHandler = React.useCallback(\n    async (event: React.MouseEvent) => {\n      event.preventDefault();\n      if (formMethod && formData) {\n        dataRouterContext?.router.navigate(to, { formMethod, formData });\n      } else {\n        dataRouterContext?.router.navigate(to);\n      }\n    },\n    [dataRouterContext, to, formMethod, formData],\n  );\n\n  // Only prepend the basename to the rendered href, send the non-prefixed `to`\n  // value into the router since it will prepend the basename\n  let basename = dataRouterContext?.basename;\n  let href = to;\n  if (basename && basename !== \"/\") {\n    href = to === \"/\" ? basename : joinPaths([basename, to]);\n  }\n\n  return formData ? (\n    <form onClick={onClickHandler} children={children} />\n  ) : (\n    <a href={href} onClick={onClickHandler} children={children} />\n  );\n}\n"
  },
  {
    "path": "packages/react-router/__tests__/utils/framework.ts",
    "content": "import type {\n  EntryContext,\n  FrameworkContextObject,\n} from \"../../lib/dom/ssr/entry\";\n\nexport function mockFrameworkContext(\n  overrides?: Partial<FrameworkContextObject>,\n): FrameworkContextObject {\n  return {\n    routeModules: { root: { default: () => null } },\n    manifest: {\n      routes: {\n        root: {\n          id: \"root\",\n          module: \"root.js\",\n          hasClientMiddleware: false,\n          hasLoader: false,\n          hasClientLoader: false,\n          hasAction: false,\n          hasClientAction: false,\n          hasErrorBoundary: false,\n          clientActionModule: undefined,\n          clientLoaderModule: undefined,\n          clientMiddlewareModule: undefined,\n          hydrateFallbackModule: undefined,\n        },\n      },\n      entry: { imports: [], module: \"/dummy-entry-module.js\" },\n      url: \"\",\n      version: \"\",\n    },\n    future: {\n      v8_middleware: false,\n      unstable_subResourceIntegrity: false,\n    },\n    ssr: true,\n    isSpaMode: false,\n    routeDiscovery: {\n      mode: \"lazy\",\n      manifestPath: \"/__manifest\",\n    },\n    ...overrides,\n  };\n}\n\nexport function mockEntryContext(\n  overrides?: Partial<EntryContext>,\n): EntryContext {\n  return {\n    ...mockFrameworkContext(overrides),\n    staticHandlerContext: {\n      location: {\n        pathname: \"/\",\n        search: \"\",\n        hash: \"\",\n        state: null,\n        key: \"default\",\n      },\n      basename: \"\",\n      loaderData: {},\n      actionData: null,\n      errors: null,\n      matches: [],\n      statusCode: 200,\n      actionHeaders: {},\n      loaderHeaders: {},\n    },\n    serverHandoffStream: undefined,\n    ...overrides,\n  };\n}\n"
  },
  {
    "path": "packages/react-router/__tests__/utils/getHtml.ts",
    "content": "import { prettyDOM } from \"@testing-library/react\";\n\nexport default function getHtml(container: HTMLElement) {\n  return prettyDOM(container, undefined, {\n    highlight: false,\n  });\n}\n"
  },
  {
    "path": "packages/react-router/__tests__/utils/getWindow.ts",
    "content": "import { JSDOM } from \"jsdom\";\n\nexport default function getWindow(initialUrl: string, isHash = false): Window {\n  // Need to use our own custom DOM in order to get a working history\n  const dom = new JSDOM(`<!DOCTYPE html>`, { url: \"http://localhost/\" });\n  dom.window.history.replaceState(null, \"\", (isHash ? \"#\" : \"\") + initialUrl);\n  return dom.window as unknown as Window;\n}\n"
  },
  {
    "path": "packages/react-router/__tests__/utils/renderStrict.tsx",
    "content": "import * as React from \"react\";\nimport * as ReactDOM from \"react-dom\";\n\nlet StrictMode: React.FC = function (props) {\n  return props.children || (null as any);\n};\n\nif (React.StrictMode) {\n  StrictMode = React.StrictMode;\n}\n\nfunction renderStrict(\n  element:\n    | React.FunctionComponentElement<any>\n    | React.FunctionComponentElement<any>[],\n  node: ReactDOM.Container,\n): void {\n  ReactDOM.render(<StrictMode>{element}</StrictMode>, node);\n}\n\nexport default renderStrict;\n"
  },
  {
    "path": "packages/react-router/__tests__/utils/tick.ts",
    "content": "export default async function tick() {\n  await new Promise((r) => setTimeout(r, 0));\n}\n"
  },
  {
    "path": "packages/react-router/__tests__/utils/waitForRedirect.tsx",
    "content": "export default function waitForRedirect(fn: (...args: any[]) => void) {\n  // TODO: Hook into <Redirect> so we can know when\n  // the redirect actually happens instead of guessing.\n  setTimeout(fn, 100);\n}\n"
  },
  {
    "path": "packages/react-router/__tests__/vendor/turbo-stream-test.ts",
    "content": "import { decode, encode } from \"../../vendor/turbo-stream-v2/turbo-stream\";\nimport {\n  Deferred,\n  type EncodePlugin,\n} from \"../../vendor/turbo-stream-v2/utils\";\n\nasync function quickDecode(stream: ReadableStream<Uint8Array>) {\n  const decoded = await decode(stream);\n  await decoded.done;\n  return decoded.value;\n}\n\ntest(\"should encode and decode undefined\", async () => {\n  const input = undefined;\n  const output = await quickDecode(encode(input));\n  expect(output).toEqual(input);\n});\n\ntest(\"should encode and decode null\", async () => {\n  const input = null;\n  const output = await quickDecode(encode(input));\n  expect(output).toEqual(input);\n});\n\ntest(\"should encode and decode boolean\", async () => {\n  const input = true;\n  const output = await quickDecode(encode(input));\n  expect(output).toEqual(input);\n\n  const input2 = false;\n  const output2 = await quickDecode(encode(input2));\n  expect(output2).toEqual(input2);\n});\n\ntest(\"should encode and decode number\", async () => {\n  const input = 42;\n  const output = await quickDecode(encode(input));\n  expect(output).toEqual(input);\n});\n\ntest(\"should encode and decode string\", async () => {\n  const input = \"Hello World\";\n  const output = await quickDecode(encode(input));\n  expect(output).toEqual(input);\n});\n\ntest(\"should encode and decode Date\", async () => {\n  const input = new Date();\n  const output = await quickDecode(encode(input));\n  expect(output).toEqual(input);\n});\n\ntest(\"should encode and decode invalid Date\", async () => {\n  const input = new Date(\"invalid\");\n  const output = await quickDecode(encode(input));\n  expect(isNaN(input.getTime())).toBe(true);\n  expect(isNaN(output.getTime())).toBe(true);\n});\n\ntest(\"should encode and decode NaN\", async () => {\n  const input = NaN;\n  const output = await quickDecode(encode(input));\n  expect(output).toEqual(input);\n});\n\ntest(\"should encode and decode Number.NaN\", async () => {\n  const input = Number.NaN;\n  const output = await quickDecode(encode(input));\n  expect(output).toEqual(input);\n});\n\ntest(\"should encode and decode Infinity\", async () => {\n  const input = Infinity;\n  const output = await quickDecode(encode(input));\n  expect(output).toEqual(input);\n});\n\ntest(\"should encode and decode -Infinity\", async () => {\n  const input = -Infinity;\n  const output = await quickDecode(encode(input));\n  expect(output).toEqual(input);\n});\n\ntest(\"should encode and decode -0\", async () => {\n  const input = -0;\n  const output = await quickDecode(encode(input));\n  expect(output).toEqual(input);\n});\n\ntest(\"should encode and decode BigInt\", async () => {\n  const input = BigInt(42);\n  const output = await quickDecode(encode(input));\n  expect(output).toEqual(input);\n});\n\ntest(\"should encode and decode RegExp\", async () => {\n  const input = /foo/g;\n  const output = await quickDecode(encode(input));\n  expect(output).toEqual(input);\n});\n\ntest(\"should encode and decode Symbol\", async () => {\n  const input = Symbol.for(\"foo\");\n  const output = await quickDecode(encode(input));\n  expect(output).toEqual(input);\n});\n\ntest(\"should encode and decode URL\", async () => {\n  const input = new URL(\"https://example.com\");\n  const output = await quickDecode(encode(input));\n  expect(output).toEqual(input);\n});\n\ntest(\"should encode and decode object with null prototype\", async () => {\n  const input = Object.create(null);\n  input.foo = \"bar\";\n  const output = await quickDecode(encode(input));\n  expect(output).toEqual(input);\n});\n\ntest(\"should encode and decode Map\", async () => {\n  const input = new Map([\n    [\"foo\", \"bar\"],\n    [\"baz\", \"qux\"],\n  ]);\n  const output = await quickDecode(encode(input));\n  expect(output).toEqual(input);\n});\n\ntest(\"should maintain order of Map entries\", async () => {\n  const input = new Map([\n    [\"foo\", \"bar\"],\n    [\"baz\", \"qux\"],\n  ]);\n  const output = await quickDecode(encode(input));\n  expect(Array.from(output as typeof input)).toEqual(Array.from(input));\n});\n\ntest(\"should encode and decode empty Map\", async () => {\n  const input = new Map();\n  const output = await quickDecode(encode(input));\n  expect(output).toEqual(input);\n});\n\ntest(\"should encode and decode Set\", async () => {\n  const input = new Set([\"foo\", \"bar\"]);\n  const output = await quickDecode(encode(input));\n  expect(output).toEqual(input);\n});\n\ntest(\"should maintain order of Set entries\", async () => {\n  const input = new Set([\"foo\", \"bar\"]);\n  const output = await quickDecode(encode(input));\n  expect(Array.from(output as typeof input)).toEqual(Array.from(input));\n});\n\ntest(\"should encode and decode empty Set\", async () => {\n  const input = new Set();\n  const output = await quickDecode(encode(input));\n  expect(output).toEqual(input);\n});\n\ntest(\"should encode and decode an Error\", async () => {\n  const input = new Error(\"foo\");\n  const output = await quickDecode(encode(input));\n  expect(output).toEqual(input);\n});\n\ntest(\"should encode and decode an EvalError\", async () => {\n  const input = new EvalError(\"foo\");\n  const output = await quickDecode(encode(input));\n  expect(output).toEqual(input);\n});\n\ntest(\"should encode and decode array\", async () => {\n  const input = [1, 2, 3];\n  const output = await quickDecode(encode(input));\n  expect(output).toEqual(input);\n});\n\ntest(\"should encode and decode array with holes\", async () => {\n  // eslint-disable-next-line\n  const input = [1, , 3];\n  const output = await quickDecode(encode(input));\n  expect(output).toEqual(input);\n});\n\ntest(\"should encode and decode object\", async () => {\n  const input = { foo: \"bar\" };\n  const output = await quickDecode(encode(input));\n  expect(output).toEqual(input);\n});\n\ntest(\"should encode and decode large payload\", async () => {\n  const input: unknown[] = [];\n  for (let i = 0; i < 100000; i++) {\n    input.push({\n      [Math.random().toString(36).slice(2)]: Math.random()\n        .toString(36)\n        .slice(2),\n    });\n  }\n  const output = await quickDecode(encode(input));\n  expect(output).toEqual(input);\n});\n\ntest(\"should encode and decode object maintaining property order for re-used keys\", async () => {\n  const input = [\n    { a: \"a value 1\", b: \"b value\" },\n    { c: \"c value\", a: \"a value 2\" },\n  ];\n  const output = await quickDecode(encode(input));\n  expect(JSON.stringify(output)).toEqual(JSON.stringify(input));\n});\n\ntest(\"should encode and decode null prototype object maintaining property order for re-used keys\", async () => {\n  const input = Object.create(null);\n  const test1 = Object.create(null);\n  test1.a = \"a value 1\";\n  test1.b = \"b value\";\n  input.test1 = test1;\n  const test2 = Object.create(null);\n  test2.c = \"c value\";\n  test2.a = \"a value 2\";\n  input.test2 = test2;\n\n  const output = await quickDecode(encode(input));\n  expect(JSON.stringify(output)).toEqual(JSON.stringify(input));\n});\n\ntest(\"should encode and decode object and dedupe object key, value, and promise value\", async () => {\n  const input = { foo: \"bar\", bar: \"bar\", baz: Promise.resolve(\"bar\") };\n  const output = await quickDecode(encode(input));\n  const { baz: bazResult, ...partialResult } = output as typeof input;\n  const { baz: bazInput, ...partialInput } = input;\n\n  expect(partialResult).toEqual(partialInput);\n  expect(await bazResult).toEqual(await bazInput);\n\n  let encoded = \"\";\n  const stream = encode(input);\n  await stream.pipeThrough(new TextDecoderStream()).pipeTo(\n    new WritableStream({\n      write(chunk) {\n        encoded += chunk;\n      },\n    }),\n  );\n\n  expect(Array.from(encoded.matchAll(/\"foo\"/g))).toHaveLength(1);\n  expect(Array.from(encoded.matchAll(/\"bar\"/g))).toHaveLength(1);\n  expect(Array.from(encoded.matchAll(/\"baz\"/g))).toHaveLength(1);\n});\n\ntest(\"should encode and decode object with undefined\", async () => {\n  const input = { foo: undefined };\n  const output = (await quickDecode(encode(input))) as typeof input;\n  expect(output).toEqual(input);\n  expect(\"foo\" in output).toBe(true);\n});\n\ntest(\"should encode and decode promise\", async () => {\n  const input = Promise.resolve(\"foo\");\n  const decoded = await decode(encode(input));\n  expect(decoded.value).toBeInstanceOf(Promise);\n  expect(await decoded.value).toEqual(await input);\n  await decoded.done;\n});\n\ntest(\"should encode and decode subsequent null from promise in object value\", async () => {\n  const input = { root: null, promise: Promise.resolve(null) };\n  const decoded = await decode(encode(input));\n  const value = decoded.value as typeof input;\n  expect(await value.promise).toEqual(await input.promise);\n  await decoded.done;\n});\n\ntest(\"should encode and decode subsequent undefined from promise in object value\", async () => {\n  const input = { root: undefined, promise: Promise.resolve(undefined) };\n  const decoded = await decode(encode(input));\n  const value = decoded.value as typeof input;\n  expect(await value.promise).toEqual(await input.promise);\n  await decoded.done;\n});\n\ntest(\"should encode and decode rejected promise\", async () => {\n  const input = Promise.reject(new Error(\"foo\"));\n  const decoded = await decode(encode(input));\n  expect(decoded.value).toBeInstanceOf(Promise);\n  await expect(decoded.value).rejects.toEqual(\n    await input.catch((reason) => reason),\n  );\n  await decoded.done;\n});\n\ntest(\"should encode and decode object with promises as values\", async () => {\n  const input = { foo: Promise.resolve(\"bar\") };\n  const decoded = await decode(encode(input));\n  const value = decoded.value as typeof input;\n  expect(value).toEqual({ foo: expect.any(Promise) });\n  expect(await value.foo).toEqual(await input.foo);\n  await decoded.done;\n});\n\ntest(\"should encode and decode object with rejected promise\", async () => {\n  const input = { foo: Promise.reject(new Error(\"bar\")) };\n  const decoded = await decode(encode(input));\n  const value = decoded.value as typeof input;\n  expect(value.foo).toBeInstanceOf(Promise);\n  await expect(value.foo).rejects.toEqual(\n    await input.foo.catch((reason) => reason),\n  );\n  return decoded.done;\n});\n\ntest(\"should encode and decode set with promises as values\", async () => {\n  const prom = Promise.resolve(\"foo\");\n  const input = new Set([prom, prom]);\n  const decoded = await decode(encode(input));\n  const value = decoded.value as typeof input;\n  expect(value).toEqual(new Set([expect.any(Promise)]));\n  const proms = Array.from(value);\n  expect(await proms[0]).toEqual(await Array.from(input)[0]);\n  await decoded.done;\n});\n\ntest(\"should encode and decode custom type\", async () => {\n  class Custom {\n    child: Custom | undefined;\n    constructor(public foo: string) {}\n  }\n  const input = new Custom(\"bar\");\n  input.child = new Custom(\"baz\");\n\n  const decoder = jest.fn((type, foo, child) => {\n    if (type === \"Custom\") {\n      const value = new Custom(foo as string);\n      value.child = child as Custom | undefined;\n      return { value };\n    }\n  });\n\n  const encoder = jest.fn((value) => {\n    if (value instanceof Custom) {\n      return [\"Custom\", value.foo, value.child];\n    }\n  });\n\n  const decoded = await decode(\n    encode(input, {\n      plugins: [encoder as EncodePlugin],\n    }),\n    {\n      plugins: [decoder],\n    },\n  );\n  const value = decoded.value as Custom;\n  expect(value).toBeInstanceOf(Custom);\n  expect(value.foo).toEqual(input.foo);\n  expect(value.child).toBeInstanceOf(Custom);\n  expect(value.child?.foo).toEqual(input.child.foo);\n\n  expect(encoder.mock.calls.length).toBe(2);\n  expect(decoder.mock.calls.length).toBe(2);\n});\n\ntest(\"should encode and decode custom type when nested alongside Promise\", async () => {\n  class Custom {\n    constructor(public foo: string) {}\n  }\n  const input = {\n    number: 1,\n    array: [2, \"foo\", 3],\n    set: new Set([\"bar\", \"baz\"]),\n    custom: new Custom(\"qux\"),\n    promise: Promise.resolve(\"resolved\"),\n  };\n  const decoded = (await decode(\n    encode(input, {\n      plugins: [\n        (value) => {\n          if (value instanceof Custom) {\n            return [\"Custom\", value.foo];\n          }\n        },\n      ],\n    }),\n    {\n      plugins: [\n        (type, foo) => {\n          if (type === \"Custom\") {\n            return { value: new Custom(foo as string) };\n          }\n        },\n      ],\n    },\n  )) as unknown as {\n    value: {\n      number: number;\n      array: [];\n      set: Set<string>;\n      custom: Custom;\n      promise: Promise<string>;\n    };\n  };\n  expect(decoded.value.number).toBe(input.number);\n  expect(decoded.value.array).toEqual(input.array);\n  expect(decoded.value.set).toEqual(input.set);\n  expect(decoded.value.custom).toBeInstanceOf(Custom);\n  expect(decoded.value.custom.foo).toBe(\"qux\");\n  expect(await decoded.value.promise).toBe(\"resolved\");\n});\n\ntest(\"should allow plugins to encode and decode functions\", async () => {\n  const input = () => \"foo\";\n  const decoded = await decode(\n    encode(input, {\n      plugins: [\n        (value) => {\n          if (typeof value === \"function\") {\n            return [\"Function\"];\n          }\n        },\n      ],\n    }),\n    {\n      plugins: [\n        (type) => {\n          if (type === \"Function\") {\n            return { value: () => \"foo\" };\n          }\n        },\n      ],\n    },\n  );\n  expect(decoded.value).toBeInstanceOf(Function);\n  expect((decoded.value as typeof input)()).toBe(\"foo\");\n  await decoded.done;\n});\n\ntest(\"should allow postPlugins to handle values that would otherwise throw\", async () => {\n  class Class {}\n  const input = {\n    func: () => null,\n    class: new Class(),\n  };\n  const decoded = await decode(\n    encode(input, {\n      postPlugins: [\n        (value) => {\n          return [\"u\"];\n        },\n      ],\n    }),\n    {\n      plugins: [\n        (type) => {\n          if (type === \"u\") {\n            return { value: undefined };\n          }\n        },\n      ],\n    },\n  );\n  expect(decoded.value).toEqual({ func: undefined, class: undefined });\n  await decoded.done;\n});\n\ntest(\"should propagate abort reason to deferred promises for sync resolved promise\", async () => {\n  const abortController = new AbortController();\n  const reason = new Error(\"reason\");\n  abortController.abort(reason);\n  const decoded = await decode(\n    encode(Promise.resolve(\"foo\"), { signal: abortController.signal }),\n  );\n  await expect(decoded.value).rejects.toEqual(reason);\n});\n\ntest(\"should propagate abort reason to deferred promises for async resolved promise\", async () => {\n  const abortController = new AbortController();\n  const deferred = new Deferred();\n  const reason = new Error(\"reason\");\n  const decoded = await decode(\n    encode(deferred.promise, { signal: abortController.signal }),\n  );\n  abortController.abort(reason);\n  await expect(decoded.value).rejects.toEqual(reason);\n});\n\ntest(\"should encode and decode objects with multiple promises resolving to the same values\", async () => {\n  const input = {\n    foo: Promise.resolve(\"baz\"),\n    bar: Promise.resolve(\"baz\"),\n  };\n\n  const decoded = await decode(encode(input));\n  const value = decoded.value as typeof input;\n  expect(value).toEqual({\n    foo: expect.any(Promise),\n    bar: expect.any(Promise),\n  });\n  expect(await value.foo).toEqual(await input.foo);\n  expect(await value.bar).toEqual(await input.bar);\n  await decoded.done;\n\n  // Ensure we aren't duplicating values in the stream\n  let encoded = \"\";\n  const stream = encode(input);\n  await stream.pipeThrough(new TextDecoderStream()).pipeTo(\n    new WritableStream({\n      write(chunk) {\n        encoded += chunk;\n      },\n    }),\n  );\n  expect(Array.from(encoded.matchAll(/\"baz\"/g))).toHaveLength(1);\n});\n\ntest(\"should encode and decode objects with reused values\", async () => {\n  const input = {\n    foo: Promise.resolve({ use: \"baz\" }),\n    bar: Promise.resolve(\"baz\"),\n    data: Promise.resolve({ quux: \"quux\" }),\n  };\n\n  const decoded = await decode(encode(input));\n  const value = decoded.value as typeof input;\n  expect(value).toEqual({\n    foo: expect.any(Promise),\n    bar: expect.any(Promise),\n    data: expect.any(Promise),\n  });\n  expect(await value.foo).toEqual(await input.foo);\n  expect(await value.bar).toEqual(await input.bar);\n  expect(await value.data).toEqual(await input.data);\n\n  // Ensure we aren't duplicating values in the stream\n  let encoded = \"\";\n  const stream = encode(input);\n  await stream.pipeThrough(new TextDecoderStream()).pipeTo(\n    new WritableStream({\n      write(chunk) {\n        encoded += chunk;\n      },\n    }),\n  );\n  expect(Array.from(encoded.matchAll(/\"baz\"/g))).toHaveLength(1);\n  await decoded.done;\n});\n\ntest(\"should encode and decode objects with multiple promises rejecting to the same values\", async () => {\n  const err = new Error(\"baz\");\n  const input = {\n    foo: Promise.reject(err),\n    bar: Promise.reject(err),\n  };\n\n  const decoded = await decode(encode(input));\n  const value = decoded.value as typeof input;\n  expect(value).toEqual({\n    foo: expect.any(Promise),\n    bar: expect.any(Promise),\n  });\n  await expect(value.foo).rejects.toEqual(err);\n  await expect(value.bar).rejects.toEqual(err);\n  await decoded.done;\n\n  // Ensure we aren't duplicating values in the stream\n  let encoded = \"\";\n  const stream = encode(input);\n  await stream.pipeThrough(new TextDecoderStream()).pipeTo(\n    new WritableStream({\n      write(chunk) {\n        encoded += chunk;\n      },\n    }),\n  );\n  expect(Array.from(encoded.matchAll(/\"baz\"/g))).toHaveLength(1);\n});\n\ntest(\"should allow many nested promises without a memory leak\", async () => {\n  const depth = 2000;\n  type Nested = { i: number; next: Promise<Nested> | null };\n  const input: Nested = { i: 0, next: null };\n  let current: Nested = input;\n  for (let i = 1; i < depth; i++) {\n    const next = { i, next: null };\n    current.next = Promise.resolve(next);\n    current = next;\n  }\n\n  const decoded = await decode(encode(input));\n  let currentDecoded: Nested = decoded.value as Nested;\n  while (currentDecoded.next) {\n    currentDecoded = await currentDecoded.next;\n  }\n  expect(currentDecoded.i).toBe(depth - 1);\n  await decoded.done;\n});\n"
  },
  {
    "path": "packages/react-router/dom-export.ts",
    "content": "/**\n * @module dom\n */\n\n\"use client\";\n\nexport type { RouterProviderProps } from \"./lib/dom-export/dom-router-provider\";\nexport { RouterProvider } from \"./lib/dom-export/dom-router-provider\";\nexport type { HydratedRouterProps } from \"./lib/dom-export/hydrated-router\";\nexport { HydratedRouter } from \"./lib/dom-export/hydrated-router\";\n\n// RSC\nexport {\n  createCallServer as unstable_createCallServer,\n  RSCHydratedRouter as unstable_RSCHydratedRouter,\n} from \"./lib/rsc/browser\";\nexport { getRSCStream as unstable_getRSCStream } from \"./lib/rsc/html-stream/browser\";\n\nexport type {\n  DecodeActionFunction as unstable_DecodeActionFunction,\n  DecodeFormStateFunction as unstable_DecodeFormStateFunction,\n  DecodeReplyFunction as unstable_DecodeReplyFunction,\n  RSCManifestPayload as unstable_RSCManifestPayload,\n  RSCPayload as unstable_RSCPayload,\n  RSCRenderPayload as unstable_RSCRenderPayload,\n} from \"./lib/rsc/server.rsc\";\n"
  },
  {
    "path": "packages/react-router/index-react-server-client.ts",
    "content": "\"use client\";\n\nexport { AwaitContextProvider as UNSAFE_AwaitContextProvider } from \"./lib/context\";\nexport {\n  MemoryRouter,\n  Navigate,\n  Outlet,\n  Route,\n  Router,\n  RouterProvider,\n  Routes,\n  WithComponentProps as UNSAFE_WithComponentProps,\n  WithErrorBoundaryProps as UNSAFE_WithErrorBoundaryProps,\n  WithHydrateFallbackProps as UNSAFE_WithHydrateFallbackProps,\n} from \"./lib/components\";\nexport {\n  BrowserRouter,\n  HashRouter,\n  Link,\n  HistoryRouter as unstable_HistoryRouter,\n  NavLink,\n  Form,\n  ScrollRestoration,\n} from \"./lib/dom/lib\";\nexport { StaticRouter, StaticRouterProvider } from \"./lib/dom/server\";\nexport { Meta, Links } from \"./lib/dom/ssr/components\";\n"
  },
  {
    "path": "packages/react-router/index-react-server.ts",
    "content": "// RSC APIs\nexport {\n  getRequest as unstable_getRequest,\n  matchRSCServerRequest as unstable_matchRSCServerRequest,\n} from \"./lib/rsc/server.rsc\";\n\nexport type {\n  DecodeActionFunction as unstable_DecodeActionFunction,\n  DecodeFormStateFunction as unstable_DecodeFormStateFunction,\n  DecodeReplyFunction as unstable_DecodeReplyFunction,\n  LoadServerActionFunction as unstable_LoadServerActionFunction,\n  RSCManifestPayload as unstable_RSCManifestPayload,\n  RSCMatch as unstable_RSCMatch,\n  RSCPayload as unstable_RSCPayload,\n  RSCRenderPayload as unstable_RSCRenderPayload,\n  RSCRouteManifest as unstable_RSCRouteManifest,\n  RSCRouteMatch as unstable_RSCRouteMatch,\n  RSCRouteConfigEntry as unstable_RSCRouteConfigEntry,\n  RSCRouteConfig as unstable_RSCRouteConfig,\n} from \"./lib/rsc/server.rsc\";\n\n// RSC implementation of agnostic APIs\nexport {\n  Await,\n  redirect,\n  redirectDocument,\n  replace,\n} from \"./lib/rsc/server.rsc\";\n\n// Client references\nexport {\n  BrowserRouter,\n  Form,\n  HashRouter,\n  Link,\n  Links,\n  MemoryRouter,\n  Meta,\n  Navigate,\n  NavLink,\n  Outlet,\n  Route,\n  Router,\n  RouterProvider,\n  Routes,\n  ScrollRestoration,\n  StaticRouter,\n  StaticRouterProvider,\n  unstable_HistoryRouter,\n} from \"react-router/internal/react-server-client\";\n\n// Shared implementation of agnostic APIs\nexport { createStaticHandler } from \"./lib/router/router\";\nexport {\n  data,\n  matchRoutes,\n  isRouteErrorResponse,\n  createContext,\n  RouterContextProvider,\n} from \"./lib/router/utils\";\nexport { href } from \"./lib/href\";\n\nexport { createCookie, isCookie } from \"./lib/server-runtime/cookies\";\nexport {\n  createSession,\n  createSessionStorage,\n  isSession,\n} from \"./lib/server-runtime/sessions\";\nexport { createCookieSessionStorage } from \"./lib/server-runtime/sessions/cookieStorage\";\nexport { createMemorySessionStorage } from \"./lib/server-runtime/sessions/memoryStorage\";\n\nexport type {\n  MiddlewareFunction,\n  MiddlewareNextFunction,\n  RouterContext,\n} from \"./lib/router/utils\";\n\nexport type {\n  Cookie,\n  CookieOptions,\n  CookieParseOptions,\n  CookieSerializeOptions,\n  CookieSignatureOptions,\n  IsCookieFunction,\n} from \"./lib/server-runtime/cookies\";\nexport type {\n  IsSessionFunction,\n  Session,\n  SessionData,\n  SessionIdStorageStrategy,\n  SessionStorage,\n  FlashSessionData,\n} from \"./lib/server-runtime/sessions\";\n"
  },
  {
    "path": "packages/react-router/index.ts",
    "content": "/**\n * @module index\n * @mergeModuleWith react-router\n */\n\n\"use client\";\n\n// Expose old @remix-run/router API\nexport type { InitialEntry, Location, Path, To } from \"./lib/router/history\";\nexport type {\n  HydrationState,\n  StaticHandler,\n  GetScrollPositionFunction,\n  GetScrollRestorationKeyFunction,\n  StaticHandlerContext,\n  Fetcher,\n  Navigation,\n  NavigationStates,\n  RelativeRoutingType,\n  Blocker,\n  BlockerFunction,\n  Router as DataRouter,\n  RouterState,\n  RouterInit,\n  RouterSubscriber,\n  RouterNavigateOptions,\n  RouterFetchOptions,\n  RevalidationState,\n} from \"./lib/router/router\";\nexport type {\n  ActionFunction,\n  ActionFunctionArgs,\n  DataStrategyFunction,\n  DataStrategyFunctionArgs,\n  DataStrategyMatch,\n  DataStrategyResult,\n  DataWithResponseInit as UNSAFE_DataWithResponseInit,\n  ErrorResponse,\n  FormEncType,\n  FormMethod,\n  HTMLFormMethod,\n  LazyRouteFunction,\n  LoaderFunction,\n  LoaderFunctionArgs,\n  MiddlewareFunction,\n  ParamParseKey,\n  Params,\n  PathMatch,\n  PathParam,\n  PathPattern,\n  RedirectFunction,\n  RouterContext,\n  ShouldRevalidateFunction,\n  ShouldRevalidateFunctionArgs,\n  UIMatch,\n} from \"./lib/router/utils\";\nexport { createContext, RouterContextProvider } from \"./lib/router/utils\";\n\nexport {\n  Action as NavigationType,\n  createPath,\n  parsePath,\n} from \"./lib/router/history\";\nexport type {\n  unstable_ServerInstrumentation,\n  unstable_ClientInstrumentation,\n  unstable_InstrumentRequestHandlerFunction,\n  unstable_InstrumentRouterFunction,\n  unstable_InstrumentRouteFunction,\n  unstable_InstrumentationHandlerResult,\n} from \"./lib/router/instrumentation\";\nexport {\n  IDLE_NAVIGATION,\n  IDLE_FETCHER,\n  IDLE_BLOCKER,\n} from \"./lib/router/router\";\nexport {\n  data,\n  generatePath,\n  isRouteErrorResponse,\n  matchPath,\n  matchRoutes,\n  redirect,\n  redirectDocument,\n  replace,\n  resolvePath,\n} from \"./lib/router/utils\";\n\n// Expose react-router public API\nexport type {\n  DataRouteMatch,\n  DataRouteObject,\n  IndexRouteObject,\n  NavigateOptions,\n  Navigator,\n  NonIndexRouteObject,\n  PatchRoutesOnNavigationFunction,\n  PatchRoutesOnNavigationFunctionArgs,\n  RouteMatch,\n  RouteObject,\n} from \"./lib/context\";\nexport { AwaitContextProvider as UNSAFE_AwaitContextProvider } from \"./lib/context\";\nexport type {\n  AwaitProps,\n  IndexRouteProps,\n  ClientOnErrorFunction,\n  LayoutRouteProps,\n  MemoryRouterOpts,\n  MemoryRouterProps,\n  NavigateProps,\n  OutletProps,\n  PathRouteProps,\n  RouteProps,\n  RouterProps,\n  RouterProviderProps,\n  RoutesProps,\n} from \"./lib/components\";\nexport {\n  Await,\n  MemoryRouter,\n  Navigate,\n  Outlet,\n  Route,\n  Router,\n  RouterProvider,\n  Routes,\n  createMemoryRouter,\n  createRoutesFromChildren,\n  createRoutesFromElements,\n  renderMatches,\n} from \"./lib/components\";\nexport type { NavigateFunction } from \"./lib/hooks\";\nexport {\n  useBlocker,\n  useActionData,\n  useAsyncError,\n  useAsyncValue,\n  useHref,\n  useInRouterContext,\n  useLoaderData,\n  useLocation,\n  useMatch,\n  useMatches,\n  useNavigate,\n  useNavigation,\n  useNavigationType,\n  useOutlet,\n  useOutletContext,\n  useParams,\n  useResolvedPath,\n  useRevalidator,\n  useRouteError,\n  useRouteLoaderData,\n  useRoutes,\n  useRoute as unstable_useRoute,\n} from \"./lib/hooks\";\n\n// Expose old RR DOM API\nexport type {\n  BrowserRouterProps,\n  DOMRouterOpts,\n  HashRouterProps,\n  HistoryRouterProps,\n  LinkProps,\n  NavLinkProps,\n  NavLinkRenderProps,\n  FetcherFormProps,\n  FormProps,\n  ScrollRestorationProps,\n  SetURLSearchParams,\n  SubmitFunction,\n  FetcherSubmitFunction,\n  FetcherWithComponents,\n} from \"./lib/dom/lib\";\nexport {\n  createBrowserRouter,\n  createHashRouter,\n  BrowserRouter,\n  HashRouter,\n  Link,\n  HistoryRouter as unstable_HistoryRouter,\n  NavLink,\n  Form,\n  ScrollRestoration,\n  useLinkClickHandler,\n  useSearchParams,\n  useSubmit,\n  useFormAction,\n  useFetcher,\n  useFetchers,\n  useBeforeUnload,\n  usePrompt as unstable_usePrompt,\n  useViewTransitionState,\n} from \"./lib/dom/lib\";\nexport type {\n  FetcherSubmitOptions,\n  ParamKeyValuePair,\n  SubmitOptions,\n  URLSearchParamsInit,\n  SubmitTarget,\n} from \"./lib/dom/dom\";\nexport { createSearchParams } from \"./lib/dom/dom\";\nexport type {\n  StaticRouterProps,\n  StaticRouterProviderProps,\n} from \"./lib/dom/server\";\nexport {\n  createStaticHandler,\n  createStaticRouter,\n  StaticRouter,\n  StaticRouterProvider,\n} from \"./lib/dom/server\";\nexport {\n  Meta,\n  Links,\n  Scripts,\n  PrefetchPageLinks,\n} from \"./lib/dom/ssr/components\";\nexport type {\n  LinksProps,\n  ScriptsProps,\n  PrefetchBehavior,\n  DiscoverBehavior,\n} from \"./lib/dom/ssr/components\";\nexport type { EntryContext } from \"./lib/dom/ssr/entry\";\nexport type {\n  ClientActionFunction,\n  ClientActionFunctionArgs,\n  ClientLoaderFunction,\n  ClientLoaderFunctionArgs,\n  HeadersArgs,\n  HeadersFunction,\n  MetaArgs,\n  MetaDescriptor,\n  MetaFunction,\n  LinksFunction,\n} from \"./lib/dom/ssr/routeModules\";\nexport type { ServerRouterProps } from \"./lib/dom/ssr/server\";\nexport { ServerRouter } from \"./lib/dom/ssr/server\";\nexport type { RoutesTestStubProps } from \"./lib/dom/ssr/routes-test-stub\";\nexport { createRoutesStub } from \"./lib/dom/ssr/routes-test-stub\";\n\n// Expose old @remix-run/server-runtime API, minus duplicate APIs\nexport { createCookie, isCookie } from \"./lib/server-runtime/cookies\";\n\nexport { createRequestHandler } from \"./lib/server-runtime/server\";\nexport {\n  createSession,\n  createSessionStorage,\n  isSession,\n} from \"./lib/server-runtime/sessions\";\nexport { createCookieSessionStorage } from \"./lib/server-runtime/sessions/cookieStorage\";\nexport { createMemorySessionStorage } from \"./lib/server-runtime/sessions/memoryStorage\";\nexport { setDevServerHooks as unstable_setDevServerHooks } from \"./lib/server-runtime/dev\";\n\nexport type { IsCookieFunction } from \"./lib/server-runtime/cookies\";\nexport type { CreateRequestHandlerFunction } from \"./lib/server-runtime/server\";\nexport type { IsSessionFunction } from \"./lib/server-runtime/sessions\";\n\nexport type {\n  HandleDataRequestFunction,\n  HandleDocumentRequestFunction,\n  HandleErrorFunction,\n  ServerBuild,\n  ServerEntryModule,\n} from \"./lib/server-runtime/build\";\n\nexport type {\n  Cookie,\n  CookieOptions,\n  CookieParseOptions,\n  CookieSerializeOptions,\n  CookieSignatureOptions,\n} from \"./lib/server-runtime/cookies\";\n\nexport type { AppLoadContext } from \"./lib/server-runtime/data\";\n\nexport type {\n  PageLinkDescriptor,\n  HtmlLinkDescriptor,\n  LinkDescriptor,\n} from \"./lib/router/links\";\n\nexport type { RequestHandler } from \"./lib/server-runtime/server\";\n\nexport type {\n  Session,\n  SessionData,\n  SessionIdStorageStrategy,\n  SessionStorage,\n  FlashSessionData,\n} from \"./lib/server-runtime/sessions\";\n\nexport type {\n  Future,\n  MiddlewareEnabled as UNSAFE_MiddlewareEnabled,\n} from \"./lib/types/future.ts\";\nexport type { unstable_SerializesTo } from \"./lib/types/serializes-to.ts\";\nexport type { Register } from \"./lib/types/register\";\nexport { href } from \"./lib/href\";\n\n// RSC\nexport type {\n  BrowserCreateFromReadableStreamFunction as unstable_BrowserCreateFromReadableStreamFunction,\n  EncodeReplyFunction as unstable_EncodeReplyFunction,\n  RSCHydratedRouterProps as unstable_RSCHydratedRouterProps,\n} from \"./lib/rsc/browser\";\nexport type {\n  SSRCreateFromReadableStreamFunction as unstable_SSRCreateFromReadableStreamFunction,\n  RSCStaticRouterProps as unstable_RSCStaticRouterProps,\n} from \"./lib/rsc/server.ssr\";\nexport {\n  routeRSCServerRequest as unstable_routeRSCServerRequest,\n  RSCStaticRouter as unstable_RSCStaticRouter,\n} from \"./lib/rsc/server.ssr\";\nexport { RSCDefaultRootErrorBoundary as UNSAFE_RSCDefaultRootErrorBoundary } from \"./lib/rsc/errorBoundaries\";\n\n// Re-export of RSC types\nimport type { getRequest, matchRSCServerRequest } from \"./lib/rsc/server.rsc\";\nexport declare const unstable_getRequest: typeof getRequest;\nexport declare const unstable_matchRSCServerRequest: typeof matchRSCServerRequest;\n\nexport type {\n  DecodeActionFunction as unstable_DecodeActionFunction,\n  DecodeFormStateFunction as unstable_DecodeFormStateFunction,\n  DecodeReplyFunction as unstable_DecodeReplyFunction,\n  LoadServerActionFunction as unstable_LoadServerActionFunction,\n  RSCManifestPayload as unstable_RSCManifestPayload,\n  RSCMatch as unstable_RSCMatch,\n  RSCPayload as unstable_RSCPayload,\n  RSCRenderPayload as unstable_RSCRenderPayload,\n  RSCRouteManifest as unstable_RSCRouteManifest,\n  RSCRouteMatch as unstable_RSCRouteMatch,\n  RSCRouteConfigEntry as unstable_RSCRouteConfigEntry,\n  RSCRouteConfig as unstable_RSCRouteConfig,\n} from \"./lib/rsc/server.rsc\";\n\n///////////////////////////////////////////////////////////////////////////////\n// DANGER! PLEASE READ ME!\n// We provide these exports as an escape hatch in the event that you need any\n// routing data that we don't provide an explicit API for. With that said, we\n// want to cover your use case if we can, so if you feel the need to use these\n// we want to hear from you. Let us know what you're building and we'll do our\n// best to make sure we can support you!\n//\n// We consider these exports an implementation detail and do not guarantee\n// against any breaking changes, regardless of the semver release. Use with\n// extreme caution and only if you understand the consequences. Godspeed.\n///////////////////////////////////////////////////////////////////////////////\n\n/** @internal */\nexport {\n  createMemoryHistory as UNSAFE_createMemoryHistory,\n  createBrowserHistory as UNSAFE_createBrowserHistory,\n  createHashHistory as UNSAFE_createHashHistory,\n  invariant as UNSAFE_invariant,\n} from \"./lib/router/history\";\n\n/** @internal */\nexport { createRouter as UNSAFE_createRouter } from \"./lib/router/router\";\n\n/** @internal */\nexport { ErrorResponseImpl as UNSAFE_ErrorResponseImpl } from \"./lib/router/utils\";\n\n/** @internal */\nexport {\n  DataRouterContext as UNSAFE_DataRouterContext,\n  DataRouterStateContext as UNSAFE_DataRouterStateContext,\n  FetchersContext as UNSAFE_FetchersContext,\n  LocationContext as UNSAFE_LocationContext,\n  NavigationContext as UNSAFE_NavigationContext,\n  RouteContext as UNSAFE_RouteContext,\n  ViewTransitionContext as UNSAFE_ViewTransitionContext,\n} from \"./lib/context\";\n\n/** @internal */\nexport {\n  hydrationRouteProperties as UNSAFE_hydrationRouteProperties,\n  mapRouteProperties as UNSAFE_mapRouteProperties,\n  WithComponentProps as UNSAFE_WithComponentProps,\n  withComponentProps as UNSAFE_withComponentProps,\n  WithHydrateFallbackProps as UNSAFE_WithHydrateFallbackProps,\n  withHydrateFallbackProps as UNSAFE_withHydrateFallbackProps,\n  WithErrorBoundaryProps as UNSAFE_WithErrorBoundaryProps,\n  withErrorBoundaryProps as UNSAFE_withErrorBoundaryProps,\n} from \"./lib/components\";\n\n/** @internal */\nexport { FrameworkContext as UNSAFE_FrameworkContext } from \"./lib/dom/ssr/components\";\n\n/** @internal */\nexport type { AssetsManifest as UNSAFE_AssetsManifest } from \"./lib/dom/ssr/entry\";\n\n/** @internal */\nexport { deserializeErrors as UNSAFE_deserializeErrors } from \"./lib/dom/ssr/errors\";\n\n/** @internal */\nexport { RemixErrorBoundary as UNSAFE_RemixErrorBoundary } from \"./lib/dom/ssr/errorBoundaries\";\n\n/** @internal */\nexport {\n  getPatchRoutesOnNavigationFunction as UNSAFE_getPatchRoutesOnNavigationFunction,\n  useFogOFWarDiscovery as UNSAFE_useFogOFWarDiscovery,\n} from \"./lib/dom/ssr/fog-of-war\";\n\n/** @internal */\nexport { getHydrationData as UNSAFE_getHydrationData } from \"./lib/dom/ssr/hydration\";\n\n/** @internal */\nexport type { RouteModules as UNSAFE_RouteModules } from \"./lib/dom/ssr/routeModules\";\n\n/** @internal */\nexport {\n  createClientRoutes as UNSAFE_createClientRoutes,\n  createClientRoutesWithHMRRevalidationOptOut as UNSAFE_createClientRoutesWithHMRRevalidationOptOut,\n  shouldHydrateRouteLoader as UNSAFE_shouldHydrateRouteLoader,\n} from \"./lib/dom/ssr/routes\";\n\n/** @internal */\nexport { getTurboStreamSingleFetchDataStrategy as UNSAFE_getTurboStreamSingleFetchDataStrategy } from \"./lib/dom/ssr/single-fetch\";\n\n/** @internal */\nexport {\n  decodeViaTurboStream as UNSAFE_decodeViaTurboStream,\n  SingleFetchRedirectSymbol as UNSAFE_SingleFetchRedirectSymbol,\n} from \"./lib/dom/ssr/single-fetch\";\n\n/** @internal */\nexport { ServerMode as UNSAFE_ServerMode } from \"./lib/server-runtime/mode\";\n\n/** @internal */\nexport { useScrollRestoration as UNSAFE_useScrollRestoration } from \"./lib/dom/lib\";\n"
  },
  {
    "path": "packages/react-router/jest.config.js",
    "content": "/** @type {import('jest').Config} */\nmodule.exports = {\n  ...require(\"../../jest/jest.config.shared\"),\n  setupFiles: [\"<rootDir>/__tests__/setup.ts\"],\n  setupFilesAfterEnv: [\"@testing-library/jest-dom\"],\n  testEnvironment: \"jsdom\",\n};\n"
  },
  {
    "path": "packages/react-router/lib/actions.ts",
    "content": "export function throwIfPotentialCSRFAttack(\n  headers: Headers,\n  allowedActionOrigins: string[] | undefined,\n) {\n  let originHeader = headers.get(\"origin\");\n  let originDomain: string | null = null;\n\n  try {\n    originDomain =\n      typeof originHeader === \"string\" && originHeader !== \"null\"\n        ? new URL(originHeader).host\n        : originHeader;\n  } catch {\n    throw new Error(\n      `\\`origin\\` header is not a valid URL. Aborting the action.`,\n    );\n  }\n  let host = parseHostHeader(headers);\n\n  if (originDomain && (!host || originDomain !== host.value)) {\n    if (!isAllowedOrigin(originDomain, allowedActionOrigins)) {\n      if (host) {\n        // This seems to be an CSRF attack. We should not proceed with the action.\n        throw new Error(\n          `${host.type} header does not match \\`origin\\` header from a forwarded ` +\n            `action request. Aborting the action.`,\n        );\n      } else {\n        // This is an attack. We should not proceed with the action.\n        throw new Error(\n          \"`x-forwarded-host` or `host` headers are not provided. One of these \" +\n            \"is needed to compare the `origin` header from a forwarded action \" +\n            \"request. Aborting the action.\",\n        );\n      }\n    }\n  }\n}\n\n// Implementation of micromatch by Next.js https://github.com/vercel/next.js/blob/ea927b583d24f42e538001bf13370e38c91d17bf/packages/next/src/server/app-render/csrf-protection.ts#L6\nfunction matchWildcardDomain(domain: string, pattern: string) {\n  const domainParts = domain.split(\".\");\n  const patternParts = pattern.split(\".\");\n\n  if (patternParts.length < 1) {\n    // pattern is empty and therefore invalid to match against\n    return false;\n  }\n\n  if (domainParts.length < patternParts.length) {\n    // domain has too few segments and thus cannot match\n    return false;\n  }\n\n  while (patternParts.length) {\n    const patternPart = patternParts.pop();\n    const domainPart = domainParts.pop();\n\n    switch (patternPart) {\n      case \"\": {\n        // invalid pattern. pattern segments must be non empty\n        return false;\n      }\n      case \"*\": {\n        // wildcard matches anything so we continue if the domain part is non-empty\n        if (domainPart) {\n          continue;\n        } else {\n          return false;\n        }\n      }\n      case \"**\": {\n        // if this is not the last item in the pattern the pattern is invalid\n        if (patternParts.length > 0) {\n          return false;\n        }\n        // recursive wildcard matches anything so we terminate here if the domain part is non empty\n        return domainPart !== undefined;\n      }\n      case undefined:\n      default: {\n        if (domainPart !== patternPart) {\n          return false;\n        }\n      }\n    }\n  }\n\n  // We exhausted the pattern. If we also exhausted the domain we have a match\n  return domainParts.length === 0;\n}\n\nfunction isAllowedOrigin(\n  originDomain: string,\n  allowedActionOrigins: string[] | undefined = [],\n) {\n  return allowedActionOrigins.some(\n    (allowedOrigin) =>\n      allowedOrigin &&\n      (allowedOrigin === originDomain ||\n        matchWildcardDomain(originDomain, allowedOrigin)),\n  );\n}\n\nfunction parseHostHeader(headers: Headers) {\n  let forwardedHostHeader = headers.get(\"x-forwarded-host\");\n  let forwardedHostValue = forwardedHostHeader?.split(\",\")[0]?.trim();\n  let hostHeader = headers.get(\"host\");\n\n  return forwardedHostValue\n    ? {\n        type: \"x-forwarded-host\",\n        value: forwardedHostValue,\n      }\n    : hostHeader\n      ? {\n          type: \"host\",\n          value: hostHeader,\n        }\n      : undefined;\n}\n"
  },
  {
    "path": "packages/react-router/lib/components.tsx",
    "content": "import * as React from \"react\";\n\nimport type {\n  InitialEntry,\n  Location,\n  MemoryHistory,\n  To,\n} from \"./router/history\";\nimport {\n  Action as NavigationType,\n  createMemoryHistory,\n  invariant,\n  parsePath,\n  warning,\n} from \"./router/history\";\nimport type {\n  FutureConfig,\n  HydrationState,\n  RelativeRoutingType,\n  Router as DataRouter,\n  RouterState,\n  RouterSubscriber,\n  RouterInit,\n} from \"./router/router\";\nimport { createRouter } from \"./router/router\";\nimport type {\n  DataStrategyFunction,\n  LazyRouteFunction,\n  Params,\n  TrackedPromise,\n} from \"./router/utils\";\nimport {\n  getResolveToMatches,\n  getRoutePattern,\n  resolveTo,\n  stripBasename,\n} from \"./router/utils\";\n\nimport type {\n  DataRouteObject,\n  IndexRouteObject,\n  Navigator,\n  NonIndexRouteObject,\n  PatchRoutesOnNavigationFunction,\n  RouteMatch,\n  RouteObject,\n  ViewTransitionContextObject,\n} from \"./context\";\nimport {\n  AwaitContext,\n  DataRouterContext,\n  DataRouterStateContext,\n  ENABLE_DEV_WARNINGS,\n  FetchersContext,\n  LocationContext,\n  NavigationContext,\n  RouteContext,\n  ViewTransitionContext,\n  useIsRSCRouterContext,\n} from \"./context\";\nimport {\n  _renderMatches,\n  useActionData,\n  useAsyncValue,\n  useInRouterContext,\n  useLoaderData,\n  useLocation,\n  useMatches,\n  useNavigate,\n  useOutlet,\n  useParams,\n  useRouteError,\n  useRoutes,\n  useRoutesImpl,\n} from \"./hooks\";\nimport type { ViewTransition } from \"./dom/global\";\nimport { warnOnce } from \"./server-runtime/warnings\";\nimport type { unstable_ClientInstrumentation } from \"./router/instrumentation\";\n\n/**\n * Webpack can fail to compile against react versions without this export -\n * it complains that `useOptimistic` doesn't exist in `React`.\n *\n * Using the string constant directly at runtime fixes the webpack build issue\n * but can result in terser stripping the actual call at minification time.\n *\n * Grabbing an exported reference once up front resolves that issue.\n *\n * See https://github.com/remix-run/react-router/issues/10579\n */\nconst USE_OPTIMISTIC = \"useOptimistic\";\n// @ts-expect-error Needs React 19 types but we develop against 18\nconst useOptimisticImpl = React[USE_OPTIMISTIC];\nconst stableUseOptimisticSetter = () => undefined;\n\nfunction useOptimisticSafe<T>(\n  val: T,\n): [T, React.Dispatch<React.SetStateAction<T>>] {\n  if (useOptimisticImpl) {\n    // eslint-disable-next-line react-hooks/rules-of-hooks\n    return useOptimisticImpl(val);\n  } else {\n    return [val, stableUseOptimisticSetter];\n  }\n}\n\nexport function mapRouteProperties(route: RouteObject) {\n  let updates: Partial<RouteObject> & { hasErrorBoundary: boolean } = {\n    // Note: this check also occurs in createRoutesFromChildren so update\n    // there if you change this -- please and thank you!\n    hasErrorBoundary:\n      route.hasErrorBoundary ||\n      route.ErrorBoundary != null ||\n      route.errorElement != null,\n  };\n\n  if (route.Component) {\n    if (ENABLE_DEV_WARNINGS) {\n      if (route.element) {\n        warning(\n          false,\n          \"You should not include both `Component` and `element` on your route - \" +\n            \"`Component` will be used.\",\n        );\n      }\n    }\n    Object.assign(updates, {\n      element: React.createElement(route.Component),\n      Component: undefined,\n    });\n  }\n\n  if (route.HydrateFallback) {\n    if (ENABLE_DEV_WARNINGS) {\n      if (route.hydrateFallbackElement) {\n        warning(\n          false,\n          \"You should not include both `HydrateFallback` and `hydrateFallbackElement` on your route - \" +\n            \"`HydrateFallback` will be used.\",\n        );\n      }\n    }\n    Object.assign(updates, {\n      hydrateFallbackElement: React.createElement(route.HydrateFallback),\n      HydrateFallback: undefined,\n    });\n  }\n\n  if (route.ErrorBoundary) {\n    if (ENABLE_DEV_WARNINGS) {\n      if (route.errorElement) {\n        warning(\n          false,\n          \"You should not include both `ErrorBoundary` and `errorElement` on your route - \" +\n            \"`ErrorBoundary` will be used.\",\n        );\n      }\n    }\n    Object.assign(updates, {\n      errorElement: React.createElement(route.ErrorBoundary),\n      ErrorBoundary: undefined,\n    });\n  }\n\n  return updates;\n}\n\nexport const hydrationRouteProperties: (keyof RouteObject)[] = [\n  \"HydrateFallback\",\n  \"hydrateFallbackElement\",\n];\n\n/**\n * @category Data Routers\n */\nexport interface MemoryRouterOpts {\n  /**\n   * Basename path for the application.\n   */\n  basename?: string;\n  /**\n   * A function that returns an {@link RouterContextProvider} instance\n   * which is provided as the `context` argument to client [`action`](../../start/data/route-object#action)s,\n   * [`loader`](../../start/data/route-object#loader)s and [middleware](../../how-to/middleware).\n   * This function is called to generate a fresh `context` instance on each\n   * navigation or fetcher call.\n   */\n  getContext?: RouterInit[\"getContext\"];\n  /**\n   * Future flags to enable for the router.\n   */\n  future?: Partial<FutureConfig>;\n  /**\n   * Hydration data to initialize the router with if you have already performed\n   * data loading on the server.\n   */\n  hydrationData?: HydrationState;\n  /**\n   * Initial entries in the in-memory history stack\n   */\n  initialEntries?: InitialEntry[];\n  /**\n   * Index of `initialEntries` the application should initialize to\n   */\n  initialIndex?: number;\n  /**\n   * Array of instrumentation objects allowing you to instrument the router and\n   * individual routes prior to router initialization (and on any subsequently\n   * added routes via `route.lazy` or `patchRoutesOnNavigation`).  This is\n   * mostly useful for observability such as wrapping navigations, fetches,\n   * as well as route loaders/actions/middlewares with logging and/or performance\n   * tracing.  See the [docs](../../how-to/instrumentation) for more information.\n   *\n   * ```tsx\n   * let router = createBrowserRouter(routes, {\n   *   unstable_instrumentations: [logging]\n   * });\n   *\n   *\n   * let logging = {\n   *   router({ instrument }) {\n   *     instrument({\n   *       navigate: (impl, info) => logExecution(`navigate ${info.to}`, impl),\n   *       fetch: (impl, info) => logExecution(`fetch ${info.to}`, impl)\n   *     });\n   *   },\n   *   route({ instrument, id }) {\n   *     instrument({\n   *       middleware: (impl, info) => logExecution(\n   *         `middleware ${info.request.url} (route ${id})`,\n   *         impl\n   *       ),\n   *       loader: (impl, info) => logExecution(\n   *         `loader ${info.request.url} (route ${id})`,\n   *         impl\n   *       ),\n   *       action: (impl, info) => logExecution(\n   *         `action ${info.request.url} (route ${id})`,\n   *         impl\n   *       ),\n   *     })\n   *   }\n   * };\n   *\n   * async function logExecution(label: string, impl: () => Promise<void>) {\n   *   let start = performance.now();\n   *   console.log(`start ${label}`);\n   *   await impl();\n   *   let duration = Math.round(performance.now() - start);\n   *   console.log(`end ${label} (${duration}ms)`);\n   * }\n   * ```\n   */\n  unstable_instrumentations?: unstable_ClientInstrumentation[];\n  /**\n   * Override the default data strategy of running loaders in parallel -\n   * see the [docs](../../how-to/data-strategy) for more information.\n   *\n   * ```tsx\n   * let router = createBrowserRouter(routes, {\n   *   async dataStrategy({\n   *     matches,\n   *     request,\n   *     runClientMiddleware,\n   *   }) {\n   *     const matchesToLoad = matches.filter((m) =>\n   *       m.shouldCallHandler(),\n   *     );\n   *\n   *     const results: Record<string, DataStrategyResult> = {};\n   *     await runClientMiddleware(() =>\n   *       Promise.all(\n   *         matchesToLoad.map(async (match) => {\n   *           results[match.route.id] = await match.resolve();\n   *         }),\n   *       ),\n   *     );\n   *     return results;\n   *   },\n   * });\n   * ```\n   */\n  dataStrategy?: DataStrategyFunction;\n  /**\n   * Lazily define portions of the route tree on navigations.\n   */\n  patchRoutesOnNavigation?: PatchRoutesOnNavigationFunction;\n}\n\n/**\n * Create a new {@link DataRouter} that manages the application path using an\n * in-memory [`History`](https://developer.mozilla.org/en-US/docs/Web/API/History)\n * stack. Useful for non-browser environments without a DOM API.\n *\n * @public\n * @category Data Routers\n * @mode data\n * @param routes Application routes\n * @param opts Options\n * @param {MemoryRouterOpts.basename} opts.basename n/a\n * @param {MemoryRouterOpts.dataStrategy} opts.dataStrategy n/a\n * @param {MemoryRouterOpts.future} opts.future n/a\n * @param {MemoryRouterOpts.getContext} opts.getContext n/a\n * @param {MemoryRouterOpts.hydrationData} opts.hydrationData n/a\n * @param {MemoryRouterOpts.initialEntries} opts.initialEntries n/a\n * @param {MemoryRouterOpts.initialIndex} opts.initialIndex n/a\n * @param {MemoryRouterOpts.unstable_instrumentations} opts.unstable_instrumentations n/a\n * @param {MemoryRouterOpts.patchRoutesOnNavigation} opts.patchRoutesOnNavigation n/a\n * @returns An initialized {@link DataRouter} to pass to {@link RouterProvider | `<RouterProvider>`}\n */\nexport function createMemoryRouter(\n  routes: RouteObject[],\n  opts?: MemoryRouterOpts,\n): DataRouter {\n  return createRouter({\n    basename: opts?.basename,\n    getContext: opts?.getContext,\n    future: opts?.future,\n    history: createMemoryHistory({\n      initialEntries: opts?.initialEntries,\n      initialIndex: opts?.initialIndex,\n    }),\n    hydrationData: opts?.hydrationData,\n    routes,\n    hydrationRouteProperties,\n    mapRouteProperties,\n    dataStrategy: opts?.dataStrategy,\n    patchRoutesOnNavigation: opts?.patchRoutesOnNavigation,\n    unstable_instrumentations: opts?.unstable_instrumentations,\n  }).initialize();\n}\n\nclass Deferred<T> {\n  status: \"pending\" | \"resolved\" | \"rejected\" = \"pending\";\n  promise: Promise<T>;\n  // @ts-expect-error - no initializer\n  resolve: (value: T) => void;\n  // @ts-expect-error - no initializer\n  reject: (reason?: unknown) => void;\n  constructor() {\n    this.promise = new Promise((resolve, reject) => {\n      this.resolve = (value) => {\n        if (this.status === \"pending\") {\n          this.status = \"resolved\";\n          resolve(value);\n        }\n      };\n      this.reject = (reason) => {\n        if (this.status === \"pending\") {\n          this.status = \"rejected\";\n          reject(reason);\n        }\n      };\n    });\n  }\n}\n\n/**\n * Function signature for client side error handling for loader/actions errors\n * and rendering errors via `componentDidCatch`\n */\nexport interface ClientOnErrorFunction {\n  (\n    error: unknown,\n    info: {\n      location: Location;\n      params: Params;\n      unstable_pattern: string;\n      errorInfo?: React.ErrorInfo;\n    },\n  ): void;\n}\n\n/**\n * @category Types\n */\nexport interface RouterProviderProps {\n  /**\n   * The {@link DataRouter} instance to use for navigation and data fetching.\n   */\n  router: DataRouter;\n  /**\n   * The [`ReactDOM.flushSync`](https://react.dev/reference/react-dom/flushSync)\n   * implementation to use for flushing updates.\n   *\n   * You usually don't have to worry about this:\n   * - The `RouterProvider` exported from `react-router/dom` handles this internally for you\n   * - If you are rendering in a non-DOM environment, you can import\n   *   `RouterProvider` from `react-router` and ignore this prop\n   */\n  flushSync?: (fn: () => unknown) => undefined;\n  /**\n   * An error handler function that will be called for any middleware, loader, action,\n   * or render errors that are encountered in your application.  This is useful for\n   * logging or reporting errors instead of in the {@link ErrorBoundary} because it's not\n   * subject to re-rendering and will only run one time per error.\n   *\n   * The `errorInfo` parameter is passed along from\n   * [`componentDidCatch`](https://react.dev/reference/react/Component#componentdidcatch)\n   * and is only present for render errors.\n   *\n   * ```tsx\n   * <RouterProvider onError=(error, info) => {\n   *   let { location, params, unstable_pattern, errorInfo } = info;\n   *   console.error(error, location, errorInfo);\n   *   reportToErrorService(error, location, errorInfo);\n   * }} />\n   * ```\n   */\n  onError?: ClientOnErrorFunction;\n  /**\n   * Control whether router state updates are internally wrapped in\n   * [`React.startTransition`](https://react.dev/reference/react/startTransition).\n   *\n   * - When left `undefined`, all state updates are wrapped in\n   *   `React.startTransition`\n   *   - This can lead to buggy behaviors if you are wrapping your own\n   *     navigations/fetchers in `startTransition`.\n   * - When set to `true`, {@link Link} and {@link Form} navigations will be wrapped\n   *   in `React.startTransition` and router state changes will be wrapped in\n   *   `React.startTransition` and also sent through\n   *   [`useOptimistic`](https://react.dev/reference/react/useOptimistic) to\n   *   surface mid-navigation router state changes to the UI.\n   * - When set to `false`, the router will not leverage `React.startTransition` or\n   *   `React.useOptimistic` on any navigations or state changes.\n   *\n   * For more information, please see the [docs](https://reactrouter.com/explanation/react-transitions).\n   */\n  unstable_useTransitions?: boolean;\n}\n\n/**\n * Render the UI for the given {@link DataRouter}. This component should\n * typically be at the top of an app's element tree.\n *\n * ```tsx\n * import { createBrowserRouter } from \"react-router\";\n * import { RouterProvider } from \"react-router/dom\";\n * import { createRoot } from \"react-dom/client\";\n *\n * const router = createBrowserRouter(routes);\n * createRoot(document.getElementById(\"root\")).render(\n *   <RouterProvider router={router} />\n * );\n * ```\n *\n * <docs-info>Please note that this component is exported both from\n * `react-router` and `react-router/dom` with the only difference being that the\n * latter automatically wires up `react-dom`'s [`flushSync`](https://react.dev/reference/react-dom/flushSync)\n * implementation. You _almost always_ want to use the version from\n * `react-router/dom` unless you're running in a non-DOM environment.</docs-info>\n *\n *\n * @public\n * @category Data Routers\n * @mode data\n * @param props Props\n * @param {RouterProviderProps.flushSync} props.flushSync n/a\n * @param {RouterProviderProps.onError} props.onError n/a\n * @param {RouterProviderProps.router} props.router n/a\n * @param {RouterProviderProps.unstable_useTransitions} props.unstable_useTransitions n/a\n * @returns React element for the rendered router\n */\nexport function RouterProvider({\n  router,\n  flushSync: reactDomFlushSyncImpl,\n  onError,\n  unstable_useTransitions,\n}: RouterProviderProps): React.ReactElement {\n  let unstable_rsc = useIsRSCRouterContext();\n  unstable_useTransitions = unstable_rsc || unstable_useTransitions;\n\n  let [_state, setStateImpl] = React.useState(router.state);\n  let [state, setOptimisticState] = useOptimisticSafe(_state);\n  let [pendingState, setPendingState] = React.useState<RouterState>();\n  let [vtContext, setVtContext] = React.useState<ViewTransitionContextObject>({\n    isTransitioning: false,\n  });\n  let [renderDfd, setRenderDfd] = React.useState<Deferred<void>>();\n  let [transition, setTransition] = React.useState<ViewTransition>();\n  let [interruption, setInterruption] = React.useState<{\n    state: RouterState;\n    currentLocation: Location;\n    nextLocation: Location;\n  }>();\n  let fetcherData = React.useRef<Map<string, any>>(new Map());\n\n  let setState = React.useCallback<RouterSubscriber>(\n    (\n      newState: RouterState,\n      { deletedFetchers, newErrors, flushSync, viewTransitionOpts },\n    ) => {\n      // Send router errors through onError\n      if (newErrors && onError) {\n        Object.values(newErrors).forEach((error) =>\n          onError(error, {\n            location: newState.location,\n            params: newState.matches[0]?.params ?? {},\n            unstable_pattern: getRoutePattern(newState.matches),\n          }),\n        );\n      }\n\n      newState.fetchers.forEach((fetcher, key) => {\n        if (fetcher.data !== undefined) {\n          fetcherData.current.set(key, fetcher.data);\n        }\n      });\n      deletedFetchers.forEach((key) => fetcherData.current.delete(key));\n\n      warnOnce(\n        flushSync === false || reactDomFlushSyncImpl != null,\n        \"You provided the `flushSync` option to a router update, \" +\n          \"but you are not using the `<RouterProvider>` from `react-router/dom` \" +\n          \"so `ReactDOM.flushSync()` is unavailable.  Please update your app \" +\n          'to `import { RouterProvider } from \"react-router/dom\"` and ensure ' +\n          \"you have `react-dom` installed as a dependency to use the \" +\n          \"`flushSync` option.\",\n      );\n\n      let isViewTransitionAvailable =\n        router.window != null &&\n        router.window.document != null &&\n        typeof router.window.document.startViewTransition === \"function\";\n\n      warnOnce(\n        viewTransitionOpts == null || isViewTransitionAvailable,\n        \"You provided the `viewTransition` option to a router update, \" +\n          \"but you do not appear to be running in a DOM environment as \" +\n          \"`window.startViewTransition` is not available.\",\n      );\n\n      // If this isn't a view transition or it's not available in this browser,\n      // just update and be done with it\n      if (!viewTransitionOpts || !isViewTransitionAvailable) {\n        if (reactDomFlushSyncImpl && flushSync) {\n          reactDomFlushSyncImpl(() => setStateImpl(newState));\n        } else if (unstable_useTransitions === false) {\n          setStateImpl(newState);\n        } else {\n          React.startTransition(() => {\n            if (unstable_useTransitions === true) {\n              setOptimisticState((s) => getOptimisticRouterState(s, newState));\n            }\n            setStateImpl(newState);\n          });\n        }\n        return;\n      }\n\n      // flushSync + startViewTransition\n      if (reactDomFlushSyncImpl && flushSync) {\n        // Flush through the context to mark DOM elements as transition=ing\n        reactDomFlushSyncImpl(() => {\n          // Cancel any pending transitions\n          if (transition) {\n            renderDfd?.resolve();\n            transition.skipTransition();\n          }\n          setVtContext({\n            isTransitioning: true,\n            flushSync: true,\n            currentLocation: viewTransitionOpts.currentLocation,\n            nextLocation: viewTransitionOpts.nextLocation,\n          });\n        });\n\n        // Update the DOM\n        let t = router.window!.document.startViewTransition(() => {\n          reactDomFlushSyncImpl(() => setStateImpl(newState));\n        });\n\n        // Clean up after the animation completes\n        t.finished.finally(() => {\n          reactDomFlushSyncImpl(() => {\n            setRenderDfd(undefined);\n            setTransition(undefined);\n            setPendingState(undefined);\n            setVtContext({ isTransitioning: false });\n          });\n        });\n\n        reactDomFlushSyncImpl(() => setTransition(t));\n        return;\n      }\n\n      // startTransition + startViewTransition\n      if (transition) {\n        // Interrupting an in-progress transition, cancel and let everything flush\n        // out, and then kick off a new transition from the interruption state\n        renderDfd?.resolve();\n        transition.skipTransition();\n        setInterruption({\n          state: newState,\n          currentLocation: viewTransitionOpts.currentLocation,\n          nextLocation: viewTransitionOpts.nextLocation,\n        });\n      } else {\n        // Completed navigation update with opted-in view transitions, let 'er rip\n        setPendingState(newState);\n        setVtContext({\n          isTransitioning: true,\n          flushSync: false,\n          currentLocation: viewTransitionOpts.currentLocation,\n          nextLocation: viewTransitionOpts.nextLocation,\n        });\n      }\n    },\n    [\n      router.window,\n      reactDomFlushSyncImpl,\n      transition,\n      renderDfd,\n      unstable_useTransitions,\n      setOptimisticState,\n      onError,\n    ],\n  );\n\n  // Need to use a layout effect here so we are subscribed early enough to\n  // pick up on any render-driven redirects/navigations (useEffect/<Navigate>)\n  React.useLayoutEffect(() => router.subscribe(setState), [router, setState]);\n\n  // When we start a view transition, create a Deferred we can use for the\n  // eventual \"completed\" render\n  React.useEffect(() => {\n    if (vtContext.isTransitioning && !vtContext.flushSync) {\n      setRenderDfd(new Deferred<void>());\n    }\n  }, [vtContext]);\n\n  // Once the deferred is created, kick off startViewTransition() to update the\n  // DOM and then wait on the Deferred to resolve (indicating the DOM update has\n  // happened)\n  React.useEffect(() => {\n    if (renderDfd && pendingState && router.window) {\n      let newState = pendingState;\n      let renderPromise = renderDfd.promise;\n      let transition = router.window.document.startViewTransition(async () => {\n        if (unstable_useTransitions === false) {\n          setStateImpl(newState);\n        } else {\n          React.startTransition(() => {\n            if (unstable_useTransitions === true) {\n              setOptimisticState((s) => getOptimisticRouterState(s, newState));\n            }\n            setStateImpl(newState);\n          });\n        }\n        await renderPromise;\n      });\n      transition.finished.finally(() => {\n        setRenderDfd(undefined);\n        setTransition(undefined);\n        setPendingState(undefined);\n        setVtContext({ isTransitioning: false });\n      });\n      setTransition(transition);\n    }\n  }, [\n    pendingState,\n    renderDfd,\n    router.window,\n    unstable_useTransitions,\n    setOptimisticState,\n  ]);\n\n  // When the new location finally renders and is committed to the DOM, this\n  // effect will run to resolve the transition\n  React.useEffect(() => {\n    if (\n      renderDfd &&\n      pendingState &&\n      state.location.key === pendingState.location.key\n    ) {\n      renderDfd.resolve();\n    }\n  }, [renderDfd, transition, state.location, pendingState]);\n\n  // If we get interrupted with a new navigation during a transition, we skip\n  // the active transition, let it cleanup, then kick it off again here\n  React.useEffect(() => {\n    if (!vtContext.isTransitioning && interruption) {\n      setPendingState(interruption.state);\n      setVtContext({\n        isTransitioning: true,\n        flushSync: false,\n        currentLocation: interruption.currentLocation,\n        nextLocation: interruption.nextLocation,\n      });\n      setInterruption(undefined);\n    }\n  }, [vtContext.isTransitioning, interruption]);\n\n  let navigator = React.useMemo((): Navigator => {\n    return {\n      createHref: router.createHref,\n      encodeLocation: router.encodeLocation,\n      go: (n) => router.navigate(n),\n      push: (to, state, opts) =>\n        router.navigate(to, {\n          state,\n          preventScrollReset: opts?.preventScrollReset,\n        }),\n      replace: (to, state, opts) =>\n        router.navigate(to, {\n          replace: true,\n          state,\n          preventScrollReset: opts?.preventScrollReset,\n        }),\n    };\n  }, [router]);\n\n  let basename = router.basename || \"/\";\n\n  let dataRouterContext = React.useMemo(\n    () => ({\n      router,\n      navigator,\n      static: false,\n      basename,\n      onError,\n    }),\n    [router, navigator, basename, onError],\n  );\n\n  // The fragment and {null} here are important!  We need them to keep React 18's\n  // useId happy when we are server-rendering since we may have a <script> here\n  // containing the hydrated server-side staticContext (from StaticRouterProvider).\n  // useId relies on the component tree structure to generate deterministic id's\n  // so we need to ensure it remains the same on the client even though\n  // we don't need the <script> tag\n  return (\n    <>\n      <DataRouterContext.Provider value={dataRouterContext}>\n        <DataRouterStateContext.Provider value={state}>\n          <FetchersContext.Provider value={fetcherData.current}>\n            <ViewTransitionContext.Provider value={vtContext}>\n              <Router\n                basename={basename}\n                location={state.location}\n                navigationType={state.historyAction}\n                navigator={navigator}\n                unstable_useTransitions={unstable_useTransitions}\n              >\n                <MemoizedDataRoutes\n                  routes={router.routes}\n                  future={router.future}\n                  state={state}\n                  isStatic={false}\n                  onError={onError}\n                />\n              </Router>\n            </ViewTransitionContext.Provider>\n          </FetchersContext.Provider>\n        </DataRouterStateContext.Provider>\n      </DataRouterContext.Provider>\n      {null}\n    </>\n  );\n}\n\nfunction getOptimisticRouterState(\n  currentState: RouterState,\n  newState: RouterState,\n): RouterState {\n  return {\n    // Don't surface \"current location specific\" stuff mid-navigation\n    // (historyAction, location, matches, loaderData, errors, initialized,\n    // restoreScroll, preventScrollReset, blockers, etc.)\n    ...currentState,\n    // Only surface \"pending/in-flight stuff\"\n    // (navigation, revalidation, actionData, fetchers, )\n    navigation:\n      newState.navigation.state !== \"idle\"\n        ? newState.navigation\n        : currentState.navigation,\n    revalidation:\n      newState.revalidation !== \"idle\"\n        ? newState.revalidation\n        : currentState.revalidation,\n    actionData:\n      newState.navigation.state !== \"submitting\"\n        ? newState.actionData\n        : currentState.actionData,\n    fetchers: newState.fetchers,\n  };\n}\n\n// Memoize to avoid re-renders when updating `ViewTransitionContext`\nconst MemoizedDataRoutes = React.memo(DataRoutes);\n\nexport function DataRoutes({\n  routes,\n  future,\n  state,\n  isStatic,\n  onError,\n}: {\n  routes: DataRouteObject[];\n  future: DataRouter[\"future\"];\n  state: RouterState;\n  isStatic: boolean;\n  onError?: ClientOnErrorFunction;\n}): React.ReactElement | null {\n  return useRoutesImpl(routes, undefined, { state, isStatic, onError, future });\n}\n\n/**\n * @category Types\n */\nexport interface MemoryRouterProps {\n  /**\n   * Application basename\n   */\n  basename?: string;\n  /**\n   * Nested {@link Route} elements describing the route tree\n   */\n  children?: React.ReactNode;\n  /**\n   * Initial entries in the in-memory history stack\n   */\n  initialEntries?: InitialEntry[];\n  /**\n   * Index of `initialEntries` the application should initialize to\n   */\n  initialIndex?: number;\n  /**\n   * Control whether router state updates are internally wrapped in\n   * [`React.startTransition`](https://react.dev/reference/react/startTransition).\n   *\n   * - When left `undefined`, all router state updates are wrapped in\n   *   `React.startTransition`\n   * - When set to `true`, {@link Link} and {@link Form} navigations will be wrapped\n   *   in `React.startTransition` and all router state updates are wrapped in\n   *   `React.startTransition`\n   * - When set to `false`, the router will not leverage `React.startTransition`\n   *   on any navigations or state changes.\n   *\n   * For more information, please see the [docs](https://reactrouter.com/explanation/react-transitions).\n   */\n  unstable_useTransitions?: boolean;\n}\n\n/**\n * A declarative {@link Router | `<Router>`} that stores all entries in memory.\n *\n * @public\n * @category Declarative Routers\n * @mode declarative\n * @param props Props\n * @param {MemoryRouterProps.basename} props.basename n/a\n * @param {MemoryRouterProps.children} props.children n/a\n * @param {MemoryRouterProps.initialEntries} props.initialEntries n/a\n * @param {MemoryRouterProps.initialIndex} props.initialIndex n/a\n * @param {MemoryRouterProps.unstable_useTransitions} props.unstable_useTransitions n/a\n * @returns A declarative in-memory {@link Router | `<Router>`} for client-side\n * routing.\n */\nexport function MemoryRouter({\n  basename,\n  children,\n  initialEntries,\n  initialIndex,\n  unstable_useTransitions,\n}: MemoryRouterProps): React.ReactElement {\n  let historyRef = React.useRef<MemoryHistory>();\n  if (historyRef.current == null) {\n    historyRef.current = createMemoryHistory({\n      initialEntries,\n      initialIndex,\n      v5Compat: true,\n    });\n  }\n\n  let history = historyRef.current;\n  let [state, setStateImpl] = React.useState({\n    action: history.action,\n    location: history.location,\n  });\n  let setState = React.useCallback(\n    (newState: { action: NavigationType; location: Location }) => {\n      if (unstable_useTransitions === false) {\n        setStateImpl(newState);\n      } else {\n        React.startTransition(() => setStateImpl(newState));\n      }\n    },\n    [unstable_useTransitions],\n  );\n\n  React.useLayoutEffect(() => history.listen(setState), [history, setState]);\n\n  return (\n    <Router\n      basename={basename}\n      children={children}\n      location={state.location}\n      navigationType={state.action}\n      navigator={history}\n      unstable_useTransitions={unstable_useTransitions}\n    />\n  );\n}\n\n/**\n * @category Types\n */\nexport interface NavigateProps {\n  /**\n   * The path to navigate to. This can be a string or a {@link Path} object\n   */\n  to: To;\n  /**\n   * Whether to replace the current entry in the [`History`](https://developer.mozilla.org/en-US/docs/Web/API/History)\n   * stack\n   */\n  replace?: boolean;\n  /**\n   * State to pass to the new {@link Location} to store in [`history.state`](https://developer.mozilla.org/en-US/docs/Web/API/History/state).\n   */\n  state?: any;\n  /**\n   * How to interpret relative routing in the `to` prop.\n   * See {@link RelativeRoutingType}.\n   */\n  relative?: RelativeRoutingType;\n}\n\n/**\n * A component-based version of {@link useNavigate} to use in a\n * [`React.Component` class](https://react.dev/reference/react/Component) where\n * hooks cannot be used.\n *\n * It's recommended to avoid using this component in favor of {@link useNavigate}.\n *\n * @example\n * <Navigate to=\"/tasks\" />\n *\n * @public\n * @category Components\n * @param props Props\n * @param {NavigateProps.relative} props.relative n/a\n * @param {NavigateProps.replace} props.replace n/a\n * @param {NavigateProps.state} props.state n/a\n * @param {NavigateProps.to} props.to n/a\n * @returns {void}\n *\n */\nexport function Navigate({\n  to,\n  replace,\n  state,\n  relative,\n}: NavigateProps): null {\n  invariant(\n    useInRouterContext(),\n    // TODO: This error is probably because they somehow have 2 versions of\n    // the router loaded. We can help them understand how to avoid that.\n    `<Navigate> may be used only in the context of a <Router> component.`,\n  );\n\n  let { static: isStatic } = React.useContext(NavigationContext);\n\n  warning(\n    !isStatic,\n    `<Navigate> must not be used on the initial render in a <StaticRouter>. ` +\n      `This is a no-op, but you should modify your code so the <Navigate> is ` +\n      `only ever rendered in response to some user interaction or state change.`,\n  );\n\n  let { matches } = React.useContext(RouteContext);\n  let { pathname: locationPathname } = useLocation();\n  let navigate = useNavigate();\n\n  // Resolve the path outside of the effect so that when effects run twice in\n  // StrictMode they navigate to the same place\n  let path = resolveTo(\n    to,\n    getResolveToMatches(matches),\n    locationPathname,\n    relative === \"path\",\n  );\n  let jsonPath = JSON.stringify(path);\n\n  React.useEffect(() => {\n    navigate(JSON.parse(jsonPath), { replace, state, relative });\n  }, [navigate, jsonPath, relative, replace, state]);\n\n  return null;\n}\n\n/**\n * @category Types\n */\nexport interface OutletProps {\n  /**\n   * Provides a context value to the element tree below the outlet. Use when\n   * the parent route needs to provide values to child routes.\n   *\n   * ```tsx\n   * <Outlet context={myContextValue} />\n   * ```\n   *\n   * Access the context with {@link useOutletContext}.\n   */\n  context?: unknown;\n}\n\n/**\n * Renders the matching child route of a parent route or nothing if no child\n * route matches.\n *\n * @example\n * import { Outlet } from \"react-router\";\n *\n * export default function SomeParent() {\n *   return (\n *     <div>\n *       <h1>Parent Content</h1>\n *       <Outlet />\n *     </div>\n *   );\n * }\n *\n * @public\n * @category Components\n * @param props Props\n * @param {OutletProps.context} props.context n/a\n * @returns React element for the rendered outlet or `null` if no child route matches.\n */\nexport function Outlet(props: OutletProps): React.ReactElement | null {\n  return useOutlet(props.context);\n}\n\n/**\n * @category Types\n */\nexport interface PathRouteProps {\n  /**\n   * Whether the path should be case-sensitive. Defaults to `false`.\n   */\n  caseSensitive?: NonIndexRouteObject[\"caseSensitive\"];\n  /**\n   * The path pattern to match. If unspecified or empty, then this becomes a\n   * layout route.\n   */\n  path?: NonIndexRouteObject[\"path\"];\n  /**\n   * The unique identifier for this route (for use with {@link DataRouter}s)\n   */\n  id?: NonIndexRouteObject[\"id\"];\n  /**\n   * A function that returns a promise that resolves to the route object.\n   * Used for code-splitting routes.\n   * See [`lazy`](../../start/data/route-object#lazy).\n   */\n  lazy?: LazyRouteFunction<NonIndexRouteObject>;\n  /**\n   * The route middleware.\n   * See [`middleware`](../../start/data/route-object#middleware).\n   */\n  middleware?: NonIndexRouteObject[\"middleware\"];\n  /**\n   * The route loader.\n   * See [`loader`](../../start/data/route-object#loader).\n   */\n  loader?: NonIndexRouteObject[\"loader\"];\n  /**\n   * The route action.\n   * See [`action`](../../start/data/route-object#action).\n   */\n  action?: NonIndexRouteObject[\"action\"];\n  hasErrorBoundary?: NonIndexRouteObject[\"hasErrorBoundary\"];\n  /**\n   * The route shouldRevalidate function.\n   * See [`shouldRevalidate`](../../start/data/route-object#shouldRevalidate).\n   */\n  shouldRevalidate?: NonIndexRouteObject[\"shouldRevalidate\"];\n  /**\n   * The route handle.\n   */\n  handle?: NonIndexRouteObject[\"handle\"];\n  /**\n   * Whether this is an index route.\n   */\n  index?: false;\n  /**\n   * Child Route components\n   */\n  children?: React.ReactNode;\n  /**\n   * The React element to render when this Route matches.\n   * Mutually exclusive with `Component`.\n   */\n  element?: React.ReactNode | null;\n  /**\n   * The React element to render while this router is loading data.\n   * Mutually exclusive with `HydrateFallback`.\n   */\n  hydrateFallbackElement?: React.ReactNode | null;\n  /**\n   * The React element to render at this route if an error occurs.\n   * Mutually exclusive with `ErrorBoundary`.\n   */\n  errorElement?: React.ReactNode | null;\n  /**\n   * The React Component to render when this route matches.\n   * Mutually exclusive with `element`.\n   */\n  Component?: React.ComponentType | null;\n  /**\n   * The React Component to render while this router is loading data.\n   * Mutually exclusive with `hydrateFallbackElement`.\n   */\n  HydrateFallback?: React.ComponentType | null;\n  /**\n   * The React Component to render at this route if an error occurs.\n   * Mutually exclusive with `errorElement`.\n   */\n  ErrorBoundary?: React.ComponentType | null;\n}\n\n/**\n * @category Types\n */\nexport interface LayoutRouteProps extends PathRouteProps {}\n\n/**\n * @category Types\n */\nexport interface IndexRouteProps {\n  /**\n   * Whether the path should be case-sensitive. Defaults to `false`.\n   */\n  caseSensitive?: IndexRouteObject[\"caseSensitive\"];\n  /**\n   * The path pattern to match. If unspecified or empty, then this becomes a\n   * layout route.\n   */\n  path?: IndexRouteObject[\"path\"];\n  /**\n   * The unique identifier for this route (for use with {@link DataRouter}s)\n   */\n  id?: IndexRouteObject[\"id\"];\n  /**\n   * A function that returns a promise that resolves to the route object.\n   * Used for code-splitting routes.\n   * See [`lazy`](../../start/data/route-object#lazy).\n   */\n  lazy?: LazyRouteFunction<IndexRouteObject>;\n  /**\n   * The route middleware.\n   * See [`middleware`](../../start/data/route-object#middleware).\n   */\n  middleware?: IndexRouteObject[\"middleware\"];\n  /**\n   * The route loader.\n   * See [`loader`](../../start/data/route-object#loader).\n   */\n  loader?: IndexRouteObject[\"loader\"];\n  /**\n   * The route action.\n   * See [`action`](../../start/data/route-object#action).\n   */\n  action?: IndexRouteObject[\"action\"];\n  hasErrorBoundary?: IndexRouteObject[\"hasErrorBoundary\"];\n  /**\n   * The route shouldRevalidate function.\n   * See [`shouldRevalidate`](../../start/data/route-object#shouldRevalidate).\n   */\n  shouldRevalidate?: IndexRouteObject[\"shouldRevalidate\"];\n  /**\n   * The route handle.\n   */\n  handle?: IndexRouteObject[\"handle\"];\n  /**\n   * Whether this is an index route.\n   */\n  index: true;\n  /**\n   * Child Route components\n   */\n  children?: undefined;\n  /**\n   * The React element to render when this Route matches.\n   * Mutually exclusive with `Component`.\n   */\n  element?: React.ReactNode | null;\n  /**\n   * The React element to render while this router is loading data.\n   * Mutually exclusive with `HydrateFallback`.\n   */\n  hydrateFallbackElement?: React.ReactNode | null;\n  /**\n   * The React element to render at this route if an error occurs.\n   * Mutually exclusive with `ErrorBoundary`.\n   */\n  errorElement?: React.ReactNode | null;\n  /**\n   * The React Component to render when this route matches.\n   * Mutually exclusive with `element`.\n   */\n  Component?: React.ComponentType | null;\n  /**\n   * The React Component to render while this router is loading data.\n   * Mutually exclusive with `hydrateFallbackElement`.\n   */\n  HydrateFallback?: React.ComponentType | null;\n  /**\n   * The React Component to render at this route if an error occurs.\n   * Mutually exclusive with `errorElement`.\n   */\n  ErrorBoundary?: React.ComponentType | null;\n}\n\nexport type RouteProps = PathRouteProps | LayoutRouteProps | IndexRouteProps;\n\n/**\n * Configures an element to render when a pattern matches the current location.\n * It must be rendered within a {@link Routes} element. Note that these routes\n * do not participate in data loading, actions, code splitting, or any other\n * route module features.\n *\n * @example\n * // Usually used in a declarative router\n * function App() {\n *   return (\n *     <BrowserRouter>\n *       <Routes>\n *         <Route index element={<StepOne />} />\n *         <Route path=\"step-2\" element={<StepTwo />} />\n *         <Route path=\"step-3\" element={<StepThree />} />\n *       </Routes>\n *    </BrowserRouter>\n *   );\n * }\n *\n * // But can be used with a data router as well if you prefer the JSX notation\n * const routes = createRoutesFromElements(\n *   <>\n *     <Route index loader={step1Loader} Component={StepOne} />\n *     <Route path=\"step-2\" loader={step2Loader} Component={StepTwo} />\n *     <Route path=\"step-3\" loader={step3Loader} Component={StepThree} />\n *   </>\n * );\n *\n * const router = createBrowserRouter(routes);\n *\n * function App() {\n *   return <RouterProvider router={router} />;\n * }\n *\n * @public\n * @category Components\n * @param props Props\n * @param {PathRouteProps.action} props.action n/a\n * @param {PathRouteProps.caseSensitive} props.caseSensitive n/a\n * @param {PathRouteProps.Component} props.Component n/a\n * @param {PathRouteProps.children} props.children n/a\n * @param {PathRouteProps.element} props.element n/a\n * @param {PathRouteProps.ErrorBoundary} props.ErrorBoundary n/a\n * @param {PathRouteProps.errorElement} props.errorElement n/a\n * @param {PathRouteProps.handle} props.handle n/a\n * @param {PathRouteProps.HydrateFallback} props.HydrateFallback n/a\n * @param {PathRouteProps.hydrateFallbackElement} props.hydrateFallbackElement n/a\n * @param {PathRouteProps.id} props.id n/a\n * @param {PathRouteProps.index} props.index n/a\n * @param {PathRouteProps.lazy} props.lazy n/a\n * @param {PathRouteProps.loader} props.loader n/a\n * @param {PathRouteProps.path} props.path n/a\n * @param {PathRouteProps.shouldRevalidate} props.shouldRevalidate n/a\n * @returns {void}\n */\nexport function Route(props: RouteProps): React.ReactElement | null {\n  invariant(\n    false,\n    `A <Route> is only ever to be used as the child of <Routes> element, ` +\n      `never rendered directly. Please wrap your <Route> in a <Routes>.`,\n  );\n}\n\n/**\n * @category Types\n */\nexport interface RouterProps {\n  /**\n   * The base path for the application. This is prepended to all locations\n   */\n  basename?: string;\n  /**\n   * Nested {@link Route} elements describing the route tree\n   */\n  children?: React.ReactNode;\n  /**\n   * The location to match against. Defaults to the current location.\n   * This can be a string or a {@link Location} object.\n   */\n  location: Partial<Location> | string;\n  /**\n   * The type of navigation that triggered this `location` change.\n   * Defaults to {@link NavigationType.Pop}.\n   */\n  navigationType?: NavigationType;\n  /**\n   * The navigator to use for navigation. This is usually a history object\n   * or a custom navigator that implements the {@link Navigator} interface.\n   */\n  navigator: Navigator;\n  /**\n   * Whether this router is static or not (used for SSR). If `true`, the router\n   * will not be reactive to location changes.\n   */\n  static?: boolean;\n  /**\n   * Control whether router state updates are internally wrapped in\n   * [`React.startTransition`](https://react.dev/reference/react/startTransition).\n   *\n   * - When left `undefined`, all router state updates are wrapped in\n   *   `React.startTransition`\n   * - When set to `true`, {@link Link} and {@link Form} navigations will be wrapped\n   *   in `React.startTransition` and all router state updates are wrapped in\n   *   `React.startTransition`\n   * - When set to `false`, the router will not leverage `React.startTransition`\n   *   on any navigations or state changes.\n   *\n   * For more information, please see the [docs](https://reactrouter.com/explanation/react-transitions).\n   */\n  unstable_useTransitions?: boolean;\n}\n\n/**\n * Provides location context for the rest of the app.\n *\n * Note: You usually won't render a `<Router>` directly. Instead, you'll render a\n * router that is more specific to your environment such as a {@link BrowserRouter}\n * in web browsers or a {@link ServerRouter} for server rendering.\n *\n * @public\n * @category Declarative Routers\n * @mode declarative\n * @param props Props\n * @param {RouterProps.basename} props.basename n/a\n * @param {RouterProps.children} props.children n/a\n * @param {RouterProps.location} props.location n/a\n * @param {RouterProps.navigationType} props.navigationType n/a\n * @param {RouterProps.navigator} props.navigator n/a\n * @param {RouterProps.static} props.static n/a\n * @param {RouterProps.unstable_useTransitions} props.unstable_useTransitions n/a\n * @returns React element for the rendered router or `null` if the location does\n * not match the {@link props.basename}\n */\nexport function Router({\n  basename: basenameProp = \"/\",\n  children = null,\n  location: locationProp,\n  navigationType = NavigationType.Pop,\n  navigator,\n  static: staticProp = false,\n  unstable_useTransitions,\n}: RouterProps): React.ReactElement | null {\n  invariant(\n    !useInRouterContext(),\n    `You cannot render a <Router> inside another <Router>.` +\n      ` You should never have more than one in your app.`,\n  );\n\n  // Preserve trailing slashes on basename, so we can let the user control\n  // the enforcement of trailing slashes throughout the app\n  let basename = basenameProp.replace(/^\\/*/, \"/\");\n  let navigationContext = React.useMemo(\n    () => ({\n      basename,\n      navigator,\n      static: staticProp,\n      unstable_useTransitions,\n      future: {},\n    }),\n    [basename, navigator, staticProp, unstable_useTransitions],\n  );\n\n  if (typeof locationProp === \"string\") {\n    locationProp = parsePath(locationProp);\n  }\n\n  let {\n    pathname = \"/\",\n    search = \"\",\n    hash = \"\",\n    state = null,\n    key = \"default\",\n    unstable_mask,\n  } = locationProp;\n\n  let locationContext = React.useMemo(() => {\n    let trailingPathname = stripBasename(pathname, basename);\n\n    if (trailingPathname == null) {\n      return null;\n    }\n\n    return {\n      location: {\n        pathname: trailingPathname,\n        search,\n        hash,\n        state,\n        key,\n        unstable_mask,\n      },\n      navigationType,\n    };\n  }, [\n    basename,\n    pathname,\n    search,\n    hash,\n    state,\n    key,\n    navigationType,\n    unstable_mask,\n  ]);\n\n  warning(\n    locationContext != null,\n    `<Router basename=\"${basename}\"> is not able to match the URL ` +\n      `\"${pathname}${search}${hash}\" because it does not start with the ` +\n      `basename, so the <Router> won't render anything.`,\n  );\n\n  if (locationContext == null) {\n    return null;\n  }\n\n  return (\n    <NavigationContext.Provider value={navigationContext}>\n      <LocationContext.Provider children={children} value={locationContext} />\n    </NavigationContext.Provider>\n  );\n}\n\n/**\n * @category Types\n */\nexport interface RoutesProps {\n  /**\n   * Nested {@link Route} elements\n   */\n  children?: React.ReactNode;\n  /**\n   * The {@link Location} to match against. Defaults to the current location.\n   */\n  location?: Partial<Location> | string;\n}\n\n/**\n * Renders a branch of {@link Route | `<Route>`s} that best matches the current\n * location. Note that these routes do not participate in [data loading](../../start/framework/route-module#loader),\n * [`action`](../../start/framework/route-module#action), code splitting, or\n * any other [route module](../../start/framework/route-module) features.\n *\n * @example\n * import { Route, Routes } from \"react-router\";\n *\n * <Routes>\n *   <Route index element={<StepOne />} />\n *   <Route path=\"step-2\" element={<StepTwo />} />\n *   <Route path=\"step-3\" element={<StepThree />} />\n * </Routes>\n *\n * @public\n * @category Components\n * @param props Props\n * @param {RoutesProps.children} props.children n/a\n * @param {RoutesProps.location} props.location n/a\n * @returns React element for the rendered routes or `null` if no route matches\n */\nexport function Routes({\n  children,\n  location,\n}: RoutesProps): React.ReactElement | null {\n  return useRoutes(createRoutesFromChildren(children), location);\n}\n\nexport interface AwaitResolveRenderFunction<Resolve = any> {\n  (data: Awaited<Resolve>): React.ReactNode;\n}\n\n/**\n * @category Types\n */\nexport interface AwaitProps<Resolve> {\n  /**\n   * When using a function, the resolved value is provided as the parameter.\n   *\n   * ```tsx [2]\n   * <Await resolve={reviewsPromise}>\n   *   {(resolvedReviews) => <Reviews items={resolvedReviews} />}\n   * </Await>\n   * ```\n   *\n   * When using React elements, {@link useAsyncValue} will provide the\n   * resolved value:\n   *\n   * ```tsx [2]\n   * <Await resolve={reviewsPromise}>\n   *   <Reviews />\n   * </Await>\n   *\n   * function Reviews() {\n   *   const resolvedReviews = useAsyncValue();\n   *   return <div>...</div>;\n   * }\n   * ```\n   */\n  children: React.ReactNode | AwaitResolveRenderFunction<Resolve>;\n\n  /**\n   * The error element renders instead of the `children` when the [`Promise`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise)\n   * rejects.\n   *\n   * ```tsx\n   * <Await\n   *   errorElement={<div>Oops</div>}\n   *   resolve={reviewsPromise}\n   * >\n   *   <Reviews />\n   * </Await>\n   * ```\n   *\n   * To provide a more contextual error, you can use the {@link useAsyncError} in a\n   * child component\n   *\n   * ```tsx\n   * <Await\n   *   errorElement={<ReviewsError />}\n   *   resolve={reviewsPromise}\n   * >\n   *   <Reviews />\n   * </Await>\n   *\n   * function ReviewsError() {\n   *   const error = useAsyncError();\n   *   return <div>Error loading reviews: {error.message}</div>;\n   * }\n   * ```\n   *\n   * If you do not provide an `errorElement`, the rejected value will bubble up\n   * to the nearest route-level [`ErrorBoundary`](../../start/framework/route-module#errorboundary)\n   * and be accessible via the {@link useRouteError} hook.\n   */\n  errorElement?: React.ReactNode;\n\n  /**\n   * Takes a [`Promise`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise)\n   * returned from a [`loader`](../../start/framework/route-module#loader) to be\n   * resolved and rendered.\n   *\n   * ```tsx\n   * import { Await, useLoaderData } from \"react-router\";\n   *\n   * export async function loader() {\n   *   let reviews = getReviews(); // not awaited\n   *   let book = await getBook();\n   *   return {\n   *     book,\n   *     reviews, // this is a promise\n   *   };\n   * }\n   *\n   * export default function Book() {\n   *   const {\n   *     book,\n   *     reviews, // this is the same promise\n   *   } = useLoaderData();\n   *\n   *   return (\n   *     <div>\n   *       <h1>{book.title}</h1>\n   *       <p>{book.description}</p>\n   *       <React.Suspense fallback={<ReviewsSkeleton />}>\n   *         <Await\n   *           // and is the promise we pass to Await\n   *           resolve={reviews}\n   *         >\n   *           <Reviews />\n   *         </Await>\n   *       </React.Suspense>\n   *     </div>\n   *   );\n   * }\n   * ```\n   */\n  resolve: Resolve;\n}\n\n/**\n * Used to render promise values with automatic error handling.\n *\n * **Note:** `<Await>` expects to be rendered inside a [`<React.Suspense>`](https://react.dev/reference/react/Suspense)\n *\n * @example\n * import { Await, useLoaderData } from \"react-router\";\n *\n * export async function loader() {\n *   // not awaited\n *   const reviews = getReviews();\n *   // awaited (blocks the transition)\n *   const book = await fetch(\"/api/book\").then((res) => res.json());\n *   return { book, reviews };\n * }\n *\n * function Book() {\n *   const { book, reviews } = useLoaderData();\n *   return (\n *     <div>\n *       <h1>{book.title}</h1>\n *       <p>{book.description}</p>\n *       <React.Suspense fallback={<ReviewsSkeleton />}>\n *         <Await\n *           resolve={reviews}\n *           errorElement={\n *             <div>Could not load reviews 😬</div>\n *           }\n *           children={(resolvedReviews) => (\n *             <Reviews items={resolvedReviews} />\n *           )}\n *         />\n *       </React.Suspense>\n *     </div>\n *   );\n * }\n *\n * @public\n * @category Components\n * @mode framework\n * @mode data\n * @param props Props\n * @param {AwaitProps.children} props.children n/a\n * @param {AwaitProps.errorElement} props.errorElement n/a\n * @param {AwaitProps.resolve} props.resolve n/a\n * @returns React element for the rendered awaited value\n */\nexport function Await<Resolve>({\n  children,\n  errorElement,\n  resolve,\n}: AwaitProps<Resolve>) {\n  let dataRouterContext = React.useContext(DataRouterContext);\n  let dataRouterStateContext = React.useContext(DataRouterStateContext);\n\n  let onError = React.useCallback(\n    (error: unknown, errorInfo?: React.ErrorInfo) => {\n      if (\n        dataRouterContext &&\n        dataRouterContext.onError &&\n        dataRouterStateContext\n      ) {\n        dataRouterContext.onError(error, {\n          location: dataRouterStateContext.location,\n          params: dataRouterStateContext.matches[0]?.params || {},\n          unstable_pattern: getRoutePattern(dataRouterStateContext.matches),\n          errorInfo,\n        });\n      }\n    },\n    [dataRouterContext, dataRouterStateContext],\n  );\n\n  return (\n    <AwaitErrorBoundary\n      resolve={resolve}\n      errorElement={errorElement}\n      onError={onError}\n    >\n      <ResolveAwait>{children}</ResolveAwait>\n    </AwaitErrorBoundary>\n  );\n}\n\ntype AwaitErrorBoundaryProps = React.PropsWithChildren<{\n  errorElement?: React.ReactNode;\n  resolve: TrackedPromise | any;\n  onError?: (error: unknown, errorInfo?: React.ErrorInfo) => void;\n}>;\n\ntype AwaitErrorBoundaryState = {\n  error: any;\n};\n\nenum AwaitRenderStatus {\n  pending,\n  success,\n  error,\n}\n\nclass AwaitErrorBoundary extends React.Component<\n  AwaitErrorBoundaryProps,\n  AwaitErrorBoundaryState\n> {\n  constructor(props: AwaitErrorBoundaryProps) {\n    super(props);\n    this.state = { error: null };\n  }\n\n  static getDerivedStateFromError(error: any) {\n    return { error };\n  }\n\n  componentDidCatch(error: any, errorInfo: React.ErrorInfo) {\n    if (this.props.onError) {\n      // Log render errors\n      this.props.onError(error, errorInfo);\n    } else {\n      console.error(\n        \"<Await> caught the following error during render\",\n        error,\n        errorInfo,\n      );\n    }\n  }\n\n  render() {\n    let { children, errorElement, resolve } = this.props;\n\n    let promise: TrackedPromise | null = null;\n    let status: AwaitRenderStatus = AwaitRenderStatus.pending;\n\n    if (!(resolve instanceof Promise)) {\n      // Didn't get a promise - provide as a resolved promise\n      status = AwaitRenderStatus.success;\n      promise = Promise.resolve();\n      Object.defineProperty(promise, \"_tracked\", { get: () => true });\n      Object.defineProperty(promise, \"_data\", { get: () => resolve });\n    } else if (this.state.error) {\n      // Caught a render error, provide it as a rejected promise\n      status = AwaitRenderStatus.error;\n      let renderError = this.state.error;\n      promise = Promise.reject().catch(() => {}); // Avoid unhandled rejection warnings\n      Object.defineProperty(promise, \"_tracked\", { get: () => true });\n      Object.defineProperty(promise, \"_error\", { get: () => renderError });\n    } else if ((resolve as TrackedPromise)._tracked) {\n      // Already tracked promise - check contents\n      promise = resolve;\n      status =\n        \"_error\" in promise\n          ? AwaitRenderStatus.error\n          : \"_data\" in promise\n            ? AwaitRenderStatus.success\n            : AwaitRenderStatus.pending;\n    } else {\n      // Raw (untracked) promise - track it\n      status = AwaitRenderStatus.pending;\n      Object.defineProperty(resolve, \"_tracked\", { get: () => true });\n      promise = resolve.then(\n        (data: any) =>\n          Object.defineProperty(resolve, \"_data\", { get: () => data }),\n        (error: any) => {\n          // Log promise rejections\n          this.props.onError?.(error);\n          Object.defineProperty(resolve, \"_error\", { get: () => error });\n        },\n      );\n    }\n\n    if (status === AwaitRenderStatus.error && !errorElement) {\n      // No errorElement, throw to the nearest route-level error boundary\n      throw promise._error;\n    }\n\n    if (status === AwaitRenderStatus.error) {\n      // Render via our errorElement\n      return <AwaitContext.Provider value={promise} children={errorElement} />;\n    }\n\n    if (status === AwaitRenderStatus.success) {\n      // Render children with resolved value\n      return <AwaitContext.Provider value={promise} children={children} />;\n    }\n\n    // Throw to the suspense boundary\n    throw promise;\n  }\n}\n\n// Indirection to leverage useAsyncValue for a render-prop API on `<Await>`\nfunction ResolveAwait({\n  children,\n}: {\n  children: React.ReactNode | AwaitResolveRenderFunction;\n}) {\n  let data = useAsyncValue();\n  let toRender = typeof children === \"function\" ? children(data) : children;\n  return <>{toRender}</>;\n}\n\n///////////////////////////////////////////////////////////////////////////////\n// UTILS\n///////////////////////////////////////////////////////////////////////////////\n\n/**\n * Creates a route config from a React \"children\" object, which is usually\n * either a `<Route>` element or an array of them. Used internally by\n * `<Routes>` to create a route config from its children.\n *\n * @category Utils\n * @mode data\n * @param children The React children to convert into a route config\n * @param parentPath The path of the parent route, used to generate unique IDs.\n * @returns An array of {@link RouteObject}s that can be used with a {@link DataRouter}\n */\nexport function createRoutesFromChildren(\n  children: React.ReactNode,\n  parentPath: number[] = [],\n): RouteObject[] {\n  let routes: RouteObject[] = [];\n\n  React.Children.forEach(children, (element, index) => {\n    if (!React.isValidElement(element)) {\n      // Ignore non-elements. This allows people to more easily inline\n      // conditionals in their route config.\n      return;\n    }\n\n    let treePath = [...parentPath, index];\n\n    if (element.type === React.Fragment) {\n      // Transparently support React.Fragment and its children.\n      routes.push.apply(\n        routes,\n        createRoutesFromChildren(element.props.children, treePath),\n      );\n      return;\n    }\n\n    invariant(\n      element.type === Route,\n      `[${\n        typeof element.type === \"string\" ? element.type : element.type.name\n      }] is not a <Route> component. All component children of <Routes> must be a <Route> or <React.Fragment>`,\n    );\n\n    invariant(\n      !element.props.index || !element.props.children,\n      \"An index route cannot have child routes.\",\n    );\n\n    let route: RouteObject = {\n      id: element.props.id || treePath.join(\"-\"),\n      caseSensitive: element.props.caseSensitive,\n      element: element.props.element,\n      Component: element.props.Component,\n      index: element.props.index,\n      path: element.props.path,\n      middleware: element.props.middleware,\n      loader: element.props.loader,\n      action: element.props.action,\n      hydrateFallbackElement: element.props.hydrateFallbackElement,\n      HydrateFallback: element.props.HydrateFallback,\n      errorElement: element.props.errorElement,\n      ErrorBoundary: element.props.ErrorBoundary,\n      hasErrorBoundary:\n        element.props.hasErrorBoundary === true ||\n        element.props.ErrorBoundary != null ||\n        element.props.errorElement != null,\n      shouldRevalidate: element.props.shouldRevalidate,\n      handle: element.props.handle,\n      lazy: element.props.lazy,\n    };\n\n    if (element.props.children) {\n      route.children = createRoutesFromChildren(\n        element.props.children,\n        treePath,\n      );\n    }\n\n    routes.push(route);\n  });\n\n  return routes;\n}\n\n/**\n * Create route objects from JSX elements instead of arrays of objects.\n *\n * @example\n * const routes = createRoutesFromElements(\n *   <>\n *     <Route index loader={step1Loader} Component={StepOne} />\n *     <Route path=\"step-2\" loader={step2Loader} Component={StepTwo} />\n *     <Route path=\"step-3\" loader={step3Loader} Component={StepThree} />\n *   </>\n * );\n *\n * const router = createBrowserRouter(routes);\n *\n * function App() {\n *   return <RouterProvider router={router} />;\n * }\n *\n * @name createRoutesFromElements\n * @public\n * @category Utils\n * @mode data\n * @param children The React children to convert into a route config\n * @param parentPath The path of the parent route, used to generate unique IDs.\n * This is used for internal recursion and is not intended to be used by the\n * application developer.\n * @returns An array of {@link RouteObject}s that can be used with a {@link DataRouter}\n */\nexport const createRoutesFromElements = createRoutesFromChildren;\n\n/**\n * Renders the result of {@link matchRoutes} into a React element.\n *\n * @public\n * @category Utils\n * @param matches The array of {@link RouteMatch | route matches} to render\n * @returns A React element that renders the matched routes or `null` if no matches\n */\nexport function renderMatches(\n  matches: RouteMatch[] | null,\n): React.ReactElement | null {\n  return _renderMatches(matches);\n}\n\nfunction useRouteComponentProps() {\n  return {\n    params: useParams(),\n    loaderData: useLoaderData(),\n    actionData: useActionData(),\n    matches: useMatches(),\n  };\n}\n\nexport type RouteComponentProps = ReturnType<typeof useRouteComponentProps>;\nexport type RouteComponentType = React.ComponentType<RouteComponentProps>;\n\nexport function WithComponentProps({\n  children,\n}: {\n  children: React.ReactElement;\n}) {\n  const props = useRouteComponentProps();\n  return React.cloneElement(children, props);\n}\n\nexport function withComponentProps(Component: RouteComponentType) {\n  return function WithComponentProps() {\n    const props = useRouteComponentProps();\n    return React.createElement(Component, props);\n  };\n}\n\nfunction useHydrateFallbackProps() {\n  return {\n    params: useParams(),\n    loaderData: useLoaderData(),\n    actionData: useActionData(),\n  };\n}\n\nexport type HydrateFallbackProps = ReturnType<typeof useHydrateFallbackProps>;\nexport type HydrateFallbackType = React.ComponentType<HydrateFallbackProps>;\n\nexport function WithHydrateFallbackProps({\n  children,\n}: {\n  children: React.ReactElement;\n}) {\n  const props = useHydrateFallbackProps();\n  return React.cloneElement(children, props);\n}\n\nexport function withHydrateFallbackProps(HydrateFallback: HydrateFallbackType) {\n  return function WithHydrateFallbackProps() {\n    const props = useHydrateFallbackProps();\n    return React.createElement(HydrateFallback, props);\n  };\n}\n\nfunction useErrorBoundaryProps() {\n  return {\n    params: useParams(),\n    loaderData: useLoaderData(),\n    actionData: useActionData(),\n    error: useRouteError(),\n  };\n}\n\nexport type ErrorBoundaryProps = ReturnType<typeof useErrorBoundaryProps>;\nexport type ErrorBoundaryType = React.ComponentType<ErrorBoundaryProps>;\n\nexport function WithErrorBoundaryProps({\n  children,\n}: {\n  children: React.ReactElement;\n}) {\n  const props = useErrorBoundaryProps();\n  return React.cloneElement(children, props);\n}\n\nexport function withErrorBoundaryProps(ErrorBoundary: ErrorBoundaryType) {\n  return function WithErrorBoundaryProps() {\n    const props = useErrorBoundaryProps();\n    return React.createElement(ErrorBoundary, props);\n  };\n}\n"
  },
  {
    "path": "packages/react-router/lib/context.ts",
    "content": "import * as React from \"react\";\nimport type { ClientOnErrorFunction } from \"./components\";\nimport type {\n  History,\n  Location,\n  Action as NavigationType,\n  To,\n} from \"./router/history\";\nimport type {\n  RelativeRoutingType,\n  Router,\n  StaticHandlerContext,\n} from \"./router/router\";\nimport type {\n  AgnosticIndexRouteObject,\n  AgnosticNonIndexRouteObject,\n  AgnosticPatchRoutesOnNavigationFunction,\n  AgnosticPatchRoutesOnNavigationFunctionArgs,\n  AgnosticRouteMatch,\n  LazyRouteDefinition,\n  TrackedPromise,\n} from \"./router/utils\";\n\n// Create react-specific types from the agnostic types in @remix-run/router to\n// export from react-router\nexport interface IndexRouteObject {\n  caseSensitive?: AgnosticIndexRouteObject[\"caseSensitive\"];\n  path?: AgnosticIndexRouteObject[\"path\"];\n  id?: AgnosticIndexRouteObject[\"id\"];\n  middleware?: AgnosticIndexRouteObject[\"middleware\"];\n  loader?: AgnosticIndexRouteObject[\"loader\"];\n  action?: AgnosticIndexRouteObject[\"action\"];\n  hasErrorBoundary?: AgnosticIndexRouteObject[\"hasErrorBoundary\"];\n  shouldRevalidate?: AgnosticIndexRouteObject[\"shouldRevalidate\"];\n  handle?: AgnosticIndexRouteObject[\"handle\"];\n  index: true;\n  children?: undefined;\n  element?: React.ReactNode | null;\n  hydrateFallbackElement?: React.ReactNode | null;\n  errorElement?: React.ReactNode | null;\n  Component?: React.ComponentType | null;\n  HydrateFallback?: React.ComponentType | null;\n  ErrorBoundary?: React.ComponentType | null;\n  lazy?: LazyRouteDefinition<RouteObject>;\n}\n\nexport interface NonIndexRouteObject {\n  caseSensitive?: AgnosticNonIndexRouteObject[\"caseSensitive\"];\n  path?: AgnosticNonIndexRouteObject[\"path\"];\n  id?: AgnosticNonIndexRouteObject[\"id\"];\n  middleware?: AgnosticNonIndexRouteObject[\"middleware\"];\n  loader?: AgnosticNonIndexRouteObject[\"loader\"];\n  action?: AgnosticNonIndexRouteObject[\"action\"];\n  hasErrorBoundary?: AgnosticNonIndexRouteObject[\"hasErrorBoundary\"];\n  shouldRevalidate?: AgnosticNonIndexRouteObject[\"shouldRevalidate\"];\n  handle?: AgnosticNonIndexRouteObject[\"handle\"];\n  index?: false;\n  children?: RouteObject[];\n  element?: React.ReactNode | null;\n  hydrateFallbackElement?: React.ReactNode | null;\n  errorElement?: React.ReactNode | null;\n  Component?: React.ComponentType | null;\n  HydrateFallback?: React.ComponentType | null;\n  ErrorBoundary?: React.ComponentType | null;\n  lazy?: LazyRouteDefinition<RouteObject>;\n}\n\nexport type RouteObject = IndexRouteObject | NonIndexRouteObject;\n\nexport type DataRouteObject = RouteObject & {\n  children?: DataRouteObject[];\n  id: string;\n};\n\nexport interface RouteMatch<\n  ParamKey extends string = string,\n  RouteObjectType extends RouteObject = RouteObject,\n> extends AgnosticRouteMatch<ParamKey, RouteObjectType> {}\n\nexport interface DataRouteMatch extends RouteMatch<string, DataRouteObject> {}\n\nexport type PatchRoutesOnNavigationFunctionArgs =\n  AgnosticPatchRoutesOnNavigationFunctionArgs<RouteObject, RouteMatch>;\n\nexport type PatchRoutesOnNavigationFunction =\n  AgnosticPatchRoutesOnNavigationFunction<RouteObject, RouteMatch>;\n\nexport interface DataRouterContextObject\n  // Omit `future` since those can be pulled from the `router`\n  // `NavigationContext` needs `future`/`unstable_useTransitions` since it doesn't\n  // have a `router` in all cases\n  extends Omit<NavigationContextObject, \"future\" | \"unstable_useTransitions\"> {\n  router: Router;\n  staticContext?: StaticHandlerContext;\n  onError?: ClientOnErrorFunction;\n}\n\nexport const DataRouterContext =\n  React.createContext<DataRouterContextObject | null>(null);\nDataRouterContext.displayName = \"DataRouter\";\n\nexport const DataRouterStateContext = React.createContext<\n  Router[\"state\"] | null\n>(null);\nDataRouterStateContext.displayName = \"DataRouterState\";\n\nexport const RSCRouterContext = React.createContext<boolean>(false);\n\nexport function useIsRSCRouterContext(): boolean {\n  return React.useContext(RSCRouterContext);\n}\n\nexport type ViewTransitionContextObject =\n  | {\n      isTransitioning: false;\n    }\n  | {\n      isTransitioning: true;\n      flushSync: boolean;\n      currentLocation: Location;\n      nextLocation: Location;\n    };\n\nexport const ViewTransitionContext =\n  React.createContext<ViewTransitionContextObject>({\n    isTransitioning: false,\n  });\nViewTransitionContext.displayName = \"ViewTransition\";\n\n// TODO: (v7) Change the useFetcher data from `any` to `unknown`\nexport type FetchersContextObject = Map<string, any>;\n\nexport const FetchersContext = React.createContext<FetchersContextObject>(\n  new Map(),\n);\nFetchersContext.displayName = \"Fetchers\";\n\nexport const AwaitContext = React.createContext<TrackedPromise | null>(null);\nAwaitContext.displayName = \"Await\";\n\nexport const AwaitContextProvider = (\n  props: React.ComponentProps<typeof AwaitContext.Provider>,\n) => React.createElement(AwaitContext.Provider, props);\n\nexport interface NavigateOptions {\n  /** Replace the current entry in the history stack instead of pushing a new one */\n  replace?: boolean;\n  /** Masked URL */\n  unstable_mask?: To;\n  /** Adds persistent client side routing state to the next location */\n  state?: any;\n  /** If you are using {@link ScrollRestoration `<ScrollRestoration>`}, prevent the scroll position from being reset to the top of the window when navigating */\n  preventScrollReset?: boolean;\n  /** Defines the relative path behavior for the link. \"route\" will use the route hierarchy so \"..\" will remove all URL segments of the current route pattern while \"path\" will use the URL path so \"..\" will remove one URL segment. */\n  relative?: RelativeRoutingType;\n  /** Wraps the initial state update for this navigation in a {@link https://react.dev/reference/react-dom/flushSync ReactDOM.flushSync} call instead of the default {@link https://react.dev/reference/react/startTransition React.startTransition} */\n  flushSync?: boolean;\n  /** Enables a {@link https://developer.mozilla.org/en-US/docs/Web/API/View_Transitions_API View Transition} for this navigation by wrapping the final state update in `document.startViewTransition()`. If you need to apply specific styles for this view transition, you will also need to leverage the {@link useViewTransitionState `useViewTransitionState()`} hook.  */\n  viewTransition?: boolean;\n  /** Specifies the default revalidation behavior after this submission */\n  unstable_defaultShouldRevalidate?: boolean;\n}\n\n/**\n * A Navigator is a \"location changer\"; it's how you get to different locations.\n *\n * Every history instance conforms to the Navigator interface, but the\n * distinction is useful primarily when it comes to the low-level `<Router>` API\n * where both the location and a navigator must be provided separately in order\n * to avoid \"tearing\" that may occur in a suspense-enabled app if the action\n * and/or location were to be read directly from the history instance.\n */\nexport interface Navigator {\n  createHref: History[\"createHref\"];\n  // Optional for backwards-compat with Router/HistoryRouter usage (edge case)\n  encodeLocation?: History[\"encodeLocation\"];\n  go: History[\"go\"];\n  push(to: To, state?: any, opts?: NavigateOptions): void;\n  replace(to: To, state?: any, opts?: NavigateOptions): void;\n}\n\ninterface NavigationContextObject {\n  basename: string;\n  navigator: Navigator;\n  static: boolean;\n  unstable_useTransitions: boolean | undefined;\n  // TODO: Re-introduce a singular `FutureConfig` once we land our first\n  // future.unstable_ or future.v8_ flag\n  future: {};\n}\n\nexport const NavigationContext = React.createContext<NavigationContextObject>(\n  null!,\n);\nNavigationContext.displayName = \"Navigation\";\n\ninterface LocationContextObject {\n  location: Location;\n  navigationType: NavigationType;\n}\n\nexport const LocationContext = React.createContext<LocationContextObject>(\n  null!,\n);\nLocationContext.displayName = \"Location\";\n\nexport interface RouteContextObject {\n  outlet: React.ReactElement | null;\n  matches: RouteMatch[];\n  isDataRoute: boolean;\n}\n\nexport const RouteContext = React.createContext<RouteContextObject>({\n  outlet: null,\n  matches: [],\n  isDataRoute: false,\n});\nRouteContext.displayName = \"Route\";\n\nexport const RouteErrorContext = React.createContext<any>(null);\nRouteErrorContext.displayName = \"RouteError\";\n\n// Provided by the build system\ndeclare const __DEV__: boolean;\nexport const ENABLE_DEV_WARNINGS = __DEV__;\n"
  },
  {
    "path": "packages/react-router/lib/dom/dom.ts",
    "content": "import { warning } from \"../router/history\";\nimport type { RelativeRoutingType } from \"../router/router\";\nimport type { FormEncType, HTMLFormMethod } from \"../router/utils\";\nimport { stripBasename } from \"../router/utils\";\n\nexport const defaultMethod: HTMLFormMethod = \"get\";\nconst defaultEncType: FormEncType = \"application/x-www-form-urlencoded\";\n\nexport function isHtmlElement(object: any): object is HTMLElement {\n  return typeof HTMLElement !== \"undefined\" && object instanceof HTMLElement;\n}\n\nexport function isButtonElement(object: any): object is HTMLButtonElement {\n  return isHtmlElement(object) && object.tagName.toLowerCase() === \"button\";\n}\n\nexport function isFormElement(object: any): object is HTMLFormElement {\n  return isHtmlElement(object) && object.tagName.toLowerCase() === \"form\";\n}\n\nexport function isInputElement(object: any): object is HTMLInputElement {\n  return isHtmlElement(object) && object.tagName.toLowerCase() === \"input\";\n}\n\ntype LimitedMouseEvent = Pick<\n  MouseEvent,\n  \"button\" | \"metaKey\" | \"altKey\" | \"ctrlKey\" | \"shiftKey\"\n>;\n\nfunction isModifiedEvent(event: LimitedMouseEvent) {\n  return !!(event.metaKey || event.altKey || event.ctrlKey || event.shiftKey);\n}\n\nexport function shouldProcessLinkClick(\n  event: LimitedMouseEvent,\n  target?: string,\n) {\n  return (\n    event.button === 0 && // Ignore everything but left clicks\n    (!target || target === \"_self\") && // Let browser handle \"target=_blank\" etc.\n    !isModifiedEvent(event) // Ignore clicks with modifier keys\n  );\n}\n\nexport type ParamKeyValuePair = [string, string];\n\nexport type URLSearchParamsInit =\n  | string\n  | ParamKeyValuePair[]\n  | Record<string, string | string[]>\n  | URLSearchParams;\n\n/**\n  Creates a URLSearchParams object using the given initializer.\n\n  This is identical to `new URLSearchParams(init)` except it also\n  supports arrays as values in the object form of the initializer\n  instead of just strings. This is convenient when you need multiple\n  values for a given key, but don't want to use an array initializer.\n\n  For example, instead of:\n\n  ```tsx\n  let searchParams = new URLSearchParams([\n    ['sort', 'name'],\n    ['sort', 'price']\n  ]);\n  ```\n  you can do:\n\n  ```\n  let searchParams = createSearchParams({\n    sort: ['name', 'price']\n  });\n  ```\n\n  @category Utils\n */\nexport function createSearchParams(\n  init: URLSearchParamsInit = \"\",\n): URLSearchParams {\n  return new URLSearchParams(\n    typeof init === \"string\" ||\n    Array.isArray(init) ||\n    init instanceof URLSearchParams\n      ? init\n      : Object.keys(init).reduce((memo, key) => {\n          let value = init[key];\n          return memo.concat(\n            Array.isArray(value) ? value.map((v) => [key, v]) : [[key, value]],\n          );\n        }, [] as ParamKeyValuePair[]),\n  );\n}\n\nexport function getSearchParamsForLocation(\n  locationSearch: string,\n  defaultSearchParams: URLSearchParams | null,\n) {\n  let searchParams = createSearchParams(locationSearch);\n\n  if (defaultSearchParams) {\n    // Use `defaultSearchParams.forEach(...)` here instead of iterating of\n    // `defaultSearchParams.keys()` to work-around a bug in Firefox related to\n    // web extensions. Relevant Bugzilla tickets:\n    // https://bugzilla.mozilla.org/show_bug.cgi?id=1414602\n    // https://bugzilla.mozilla.org/show_bug.cgi?id=1023984\n    defaultSearchParams.forEach((_, key) => {\n      if (!searchParams.has(key)) {\n        defaultSearchParams.getAll(key).forEach((value) => {\n          searchParams.append(key, value);\n        });\n      }\n    });\n  }\n\n  return searchParams;\n}\n\n// Thanks https://github.com/sindresorhus/type-fest!\ntype JsonObject = { [Key in string]: JsonValue } & {\n  [Key in string]?: JsonValue | undefined;\n};\ntype JsonArray = JsonValue[] | readonly JsonValue[];\ntype JsonPrimitive = string | number | boolean | null;\ntype JsonValue = JsonPrimitive | JsonObject | JsonArray;\n\nexport type SubmitTarget =\n  | HTMLFormElement\n  | HTMLButtonElement\n  | HTMLInputElement\n  | FormData\n  | URLSearchParams\n  | JsonValue\n  | null;\n\n// One-time check for submitter support\nlet _formDataSupportsSubmitter: boolean | null = null;\n\nfunction isFormDataSubmitterSupported() {\n  if (_formDataSupportsSubmitter === null) {\n    try {\n      new FormData(\n        document.createElement(\"form\"),\n        // @ts-expect-error if FormData supports the submitter parameter, this will throw\n        0,\n      );\n      _formDataSupportsSubmitter = false;\n    } catch (e) {\n      _formDataSupportsSubmitter = true;\n    }\n  }\n  return _formDataSupportsSubmitter;\n}\n\n/**\n * Submit options shared by both navigations and fetchers\n */\ninterface SharedSubmitOptions {\n  /**\n   * The HTTP method used to submit the form. Overrides `<form method>`.\n   * Defaults to \"GET\".\n   */\n  method?: HTMLFormMethod;\n\n  /**\n   * The action URL path used to submit the form. Overrides `<form action>`.\n   * Defaults to the path of the current route.\n   */\n  action?: string;\n\n  /**\n   * The encoding used to submit the form. Overrides `<form encType>`.\n   * Defaults to \"application/x-www-form-urlencoded\".\n   */\n  encType?: FormEncType;\n\n  /**\n   * Determines whether the form action is relative to the route hierarchy or\n   * the pathname.  Use this if you want to opt out of navigating the route\n   * hierarchy and want to instead route based on /-delimited URL segments\n   */\n  relative?: RelativeRoutingType;\n\n  /**\n   * In browser-based environments, prevent resetting scroll after this\n   * navigation when using the <ScrollRestoration> component\n   */\n  preventScrollReset?: boolean;\n\n  /**\n   * Enable flushSync for this submission's state updates\n   */\n  flushSync?: boolean;\n\n  /**\n   * Specify the default revalidation behavior after this submission\n   *\n   * If no `shouldRevalidate` functions are present on the active routes, then this\n   * value will be used directly.  Otherwise it will be passed into `shouldRevalidate`\n   * so the route can make the final determination on revalidation. This can be\n   * useful when updating search params and you don't want to trigger a revalidation.\n   *\n   * By default (when not specified), loaders will revalidate according to the routers\n   * standard revalidation behavior.\n   */\n  unstable_defaultShouldRevalidate?: boolean;\n}\n\n/**\n * Submit options available to fetchers\n */\nexport interface FetcherSubmitOptions extends SharedSubmitOptions {}\n\n/**\n * Submit options available to navigations\n */\nexport interface SubmitOptions extends FetcherSubmitOptions {\n  /**\n   * Set `true` to replace the current entry in the browser's history stack\n   * instead of creating a new one (i.e. stay on \"the same page\"). Defaults\n   * to `false`.\n   */\n  replace?: boolean;\n\n  /**\n   * State object to add to the history stack entry for this navigation\n   */\n  state?: any;\n\n  /**\n   * Indicate a specific fetcherKey to use when using navigate=false\n   */\n  fetcherKey?: string;\n\n  /**\n   * navigate=false will use a fetcher instead of a navigation\n   */\n  navigate?: boolean;\n\n  /**\n   * Enable view transitions on this submission navigation\n   */\n  viewTransition?: boolean;\n}\n\nconst supportedFormEncTypes: Set<FormEncType> = new Set([\n  \"application/x-www-form-urlencoded\",\n  \"multipart/form-data\",\n  \"text/plain\",\n]);\n\nfunction getFormEncType(encType: string | null) {\n  if (encType != null && !supportedFormEncTypes.has(encType as FormEncType)) {\n    warning(\n      false,\n      `\"${encType}\" is not a valid \\`encType\\` for \\`<Form>\\`/\\`<fetcher.Form>\\` ` +\n        `and will default to \"${defaultEncType}\"`,\n    );\n\n    return null;\n  }\n  return encType;\n}\n\nexport function getFormSubmissionInfo(\n  target: SubmitTarget,\n  basename: string,\n): {\n  action: string | null;\n  method: string;\n  encType: string;\n  formData: FormData | undefined;\n  body: any;\n} {\n  let method: string;\n  let action: string | null;\n  let encType: string;\n  let formData: FormData | undefined;\n  let body: any;\n\n  if (isFormElement(target)) {\n    // When grabbing the action from the element, it will have had the basename\n    // prefixed to ensure non-JS scenarios work, so strip it since we'll\n    // re-prefix in the router\n    let attr = target.getAttribute(\"action\");\n    action = attr ? stripBasename(attr, basename) : null;\n    method = target.getAttribute(\"method\") || defaultMethod;\n    encType = getFormEncType(target.getAttribute(\"enctype\")) || defaultEncType;\n\n    formData = new FormData(target);\n  } else if (\n    isButtonElement(target) ||\n    (isInputElement(target) &&\n      (target.type === \"submit\" || target.type === \"image\"))\n  ) {\n    let form = target.form;\n\n    if (form == null) {\n      throw new Error(\n        `Cannot submit a <button> or <input type=\"submit\"> without a <form>`,\n      );\n    }\n\n    // <button>/<input type=\"submit\"> may override attributes of <form>\n\n    // When grabbing the action from the element, it will have had the basename\n    // prefixed to ensure non-JS scenarios work, so strip it since we'll\n    // re-prefix in the router\n    let attr = target.getAttribute(\"formaction\") || form.getAttribute(\"action\");\n    action = attr ? stripBasename(attr, basename) : null;\n\n    method =\n      target.getAttribute(\"formmethod\") ||\n      form.getAttribute(\"method\") ||\n      defaultMethod;\n    encType =\n      getFormEncType(target.getAttribute(\"formenctype\")) ||\n      getFormEncType(form.getAttribute(\"enctype\")) ||\n      defaultEncType;\n\n    // Build a FormData object populated from a form and submitter\n    formData = new FormData(form, target);\n\n    // If this browser doesn't support the `FormData(el, submitter)` format,\n    // then tack on the submitter value at the end.  This is a lightweight\n    // solution that is not 100% spec compliant.  For complete support in older\n    // browsers, consider using the `formdata-submitter-polyfill` package\n    if (!isFormDataSubmitterSupported()) {\n      let { name, type, value } = target;\n      if (type === \"image\") {\n        let prefix = name ? `${name}.` : \"\";\n        formData.append(`${prefix}x`, \"0\");\n        formData.append(`${prefix}y`, \"0\");\n      } else if (name) {\n        formData.append(name, value);\n      }\n    }\n  } else if (isHtmlElement(target)) {\n    throw new Error(\n      `Cannot submit element that is not <form>, <button>, or ` +\n        `<input type=\"submit|image\">`,\n    );\n  } else {\n    method = defaultMethod;\n    action = null;\n    encType = defaultEncType;\n    body = target;\n  }\n\n  // Send body for <Form encType=\"text/plain\" so we encode it into text\n  if (formData && encType === \"text/plain\") {\n    body = formData;\n    formData = undefined;\n  }\n\n  return { action, method: method.toLowerCase(), encType, formData, body };\n}\n"
  },
  {
    "path": "packages/react-router/lib/dom/global.ts",
    "content": "import type { HydrationState, Router as DataRouter } from \"../router/router\";\nimport type { ServerHandoff } from \"../server-runtime/serverHandoff\";\nimport type { AssetsManifest } from \"./ssr/entry\";\nimport type { RouteModules } from \"./ssr/routeModules\";\n\nexport type WindowReactRouterContext = ServerHandoff & {\n  state: HydrationState; // Deserialized via the stream\n  stream: ReadableStream<Uint8Array> | undefined;\n  streamController: ReadableStreamDefaultController<Uint8Array>;\n};\n\nexport interface ViewTransition {\n  finished: Promise<void>;\n  ready: Promise<void>;\n  updateCallbackDone: Promise<void>;\n  skipTransition(): void;\n}\n\ndeclare global {\n  // TODO: v7 - Can this go away in favor of \"just use remix\"?\n  var __staticRouterHydrationData: HydrationState | undefined;\n  // v6 SPA info\n  var __reactRouterVersion: string;\n  interface Document {\n    startViewTransition(cb: () => Promise<void> | void): ViewTransition;\n  }\n  var __reactRouterContext: WindowReactRouterContext | undefined;\n  var __reactRouterManifest: AssetsManifest | undefined;\n  var __reactRouterRouteModules: RouteModules | undefined;\n  var __reactRouterDataRouter: DataRouter | undefined;\n  var __reactRouterHdrActive: boolean;\n  var $RefreshRuntime$:\n    | {\n        performReactRefresh: () => void;\n      }\n    | undefined;\n}\n\n// https://stackoverflow.com/a/59499895\nexport {};\n"
  },
  {
    "path": "packages/react-router/lib/dom/lib.tsx",
    "content": "import * as React from \"react\";\n\nimport type {\n  BrowserHistory,\n  HashHistory,\n  History,\n  Action as NavigationType,\n  Location,\n  To,\n} from \"../router/history\";\nimport {\n  createBrowserHistory,\n  createHashHistory,\n  createPath,\n  invariant,\n  warning,\n} from \"../router/history\";\nimport type {\n  BlockerFunction,\n  Fetcher,\n  FutureConfig,\n  GetScrollRestorationKeyFunction,\n  HydrationState,\n  RelativeRoutingType,\n  Router as DataRouter,\n  RouterInit,\n} from \"../router/router\";\nimport { IDLE_FETCHER, createRouter } from \"../router/router\";\nimport type {\n  DataStrategyFunction,\n  FormEncType,\n  HTMLFormMethod,\n  UIMatch,\n} from \"../router/utils\";\nimport {\n  ErrorResponseImpl,\n  joinPaths,\n  matchPath,\n  parseToInfo,\n  resolveTo,\n  stripBasename,\n} from \"../router/utils\";\n\n// eslint-disable-next-line @typescript-eslint/no-unused-vars\nimport type * as _ from \"./global\";\nimport type {\n  SubmitOptions,\n  URLSearchParamsInit,\n  SubmitTarget,\n  FetcherSubmitOptions,\n} from \"./dom\";\nimport {\n  createSearchParams,\n  defaultMethod,\n  getFormSubmissionInfo,\n  getSearchParamsForLocation,\n  shouldProcessLinkClick,\n} from \"./dom\";\n\nimport type {\n  DiscoverBehavior,\n  PrefetchBehavior,\n  ScriptsProps,\n} from \"./ssr/components\";\nimport {\n  PrefetchPageLinks,\n  FrameworkContext,\n  mergeRefs,\n  usePrefetchBehavior,\n} from \"./ssr/components\";\nimport {\n  Router,\n  mapRouteProperties,\n  hydrationRouteProperties,\n} from \"../components\";\nimport type {\n  RouteObject,\n  NavigateOptions,\n  PatchRoutesOnNavigationFunction,\n} from \"../context\";\nimport {\n  DataRouterContext,\n  DataRouterStateContext,\n  FetchersContext,\n  NavigationContext,\n  RouteContext,\n  ViewTransitionContext,\n} from \"../context\";\nimport {\n  useBlocker,\n  useHref,\n  useLocation,\n  useMatches,\n  useNavigate,\n  useNavigation,\n  useResolvedPath,\n  useRouteId,\n} from \"../hooks\";\nimport type { SerializeFrom } from \"../types/route-data\";\nimport type { unstable_ClientInstrumentation } from \"../router/instrumentation\";\nimport { escapeHtml } from \"./ssr/markup\";\n\n////////////////////////////////////////////////////////////////////////////////\n//#region Global Stuff\n////////////////////////////////////////////////////////////////////////////////\n\nconst isBrowser =\n  typeof window !== \"undefined\" &&\n  typeof window.document !== \"undefined\" &&\n  typeof window.document.createElement !== \"undefined\";\n\n// HEY YOU! DON'T TOUCH THIS VARIABLE!\n//\n// It is replaced with the proper version at build time via a babel plugin in\n// the rollup config.\n//\n// Export a global property onto the window for React Router detection by the\n// Core Web Vitals Technology Report.  This way they can configure the `wappalyzer`\n// to detect and properly classify live websites as being built with React Router:\n// https://github.com/HTTPArchive/wappalyzer/blob/main/src/technologies/r.json\ntry {\n  if (isBrowser) {\n    window.__reactRouterVersion =\n      // @ts-expect-error\n      REACT_ROUTER_VERSION;\n  }\n} catch (e) {\n  // no-op\n}\n//#endregion\n\n////////////////////////////////////////////////////////////////////////////////\n//#region Routers\n////////////////////////////////////////////////////////////////////////////////\n\n/**\n * @category Data Routers\n */\nexport interface DOMRouterOpts {\n  /**\n   * Basename path for the application.\n   */\n  basename?: string;\n  /**\n   * A function that returns an {@link RouterContextProvider} instance\n   * which is provided as the `context` argument to client [`action`](../../start/data/route-object#action)s,\n   * [`loader`](../../start/data/route-object#loader)s and [middleware](../../how-to/middleware).\n   * This function is called to generate a fresh `context` instance on each\n   * navigation or fetcher call.\n   *\n   * ```tsx\n   * import {\n   *   createContext,\n   *   RouterContextProvider,\n   * } from \"react-router\";\n   *\n   * const apiClientContext = createContext<APIClient>();\n   *\n   * function createBrowserRouter(routes, {\n   *   getContext() {\n   *     let context = new RouterContextProvider();\n   *     context.set(apiClientContext, getApiClient());\n   *     return context;\n   *   }\n   * })\n   * ```\n   */\n  getContext?: RouterInit[\"getContext\"];\n  /**\n   * Future flags to enable for the router.\n   */\n  future?: Partial<FutureConfig>;\n  /**\n   * When Server-Rendering and opting-out of automatic hydration, the\n   * `hydrationData` option allows you to pass in hydration data from your\n   * server-render. This will almost always be a subset of data from the\n   * {@link StaticHandlerContext} value you get back from the {@link StaticHandler}'s\n   * `query` method:\n   *\n   * ```tsx\n   * const router = createBrowserRouter(routes, {\n   *   hydrationData: {\n   *     loaderData: {\n   *       // [routeId]: serverLoaderData\n   *     },\n   *     // may also include `errors` and/or `actionData`\n   *   },\n   * });\n   * ```\n   *\n   * **Partial Hydration Data**\n   *\n   * You will almost always include a complete set of `loaderData` to hydrate a\n   * server-rendered app. But in advanced use-cases (such as Framework Mode's\n   * [`clientLoader`](../../start/framework/route-module#clientLoader)), you may\n   * want to include `loaderData` for only some routes that were loaded/rendered\n   * on the server. This allows you to hydrate _some_ of the routes (such as the\n   * app layout/shell) while showing a `HydrateFallback` component and running\n   * the [`loader`](../../start/data/route-object#loader)s for other routes\n   * during hydration.\n   *\n   * A route [`loader`](../../start/data/route-object#loader) will run during\n   * hydration in two scenarios:\n   *\n   *  1. No hydration data is provided\n   *     In these cases the `HydrateFallback` component will render on initial\n   *     hydration\n   *  2. The `loader.hydrate` property is set to `true`\n   *     This allows you to run the [`loader`](../../start/data/route-object#loader)\n   *     even if you did not render a fallback on initial hydration (i.e., to\n   *     prime a cache with hydration data)\n   *\n   * ```tsx\n   * const router = createBrowserRouter(\n   *   [\n   *     {\n   *       id: \"root\",\n   *       loader: rootLoader,\n   *       Component: Root,\n   *       children: [\n   *         {\n   *           id: \"index\",\n   *           loader: indexLoader,\n   *           HydrateFallback: IndexSkeleton,\n   *           Component: Index,\n   *         },\n   *       ],\n   *     },\n   *   ],\n   *   {\n   *     hydrationData: {\n   *       loaderData: {\n   *         root: \"ROOT DATA\",\n   *         // No index data provided\n   *       },\n   *     },\n   *   }\n   * );\n   * ```\n   */\n  hydrationData?: HydrationState;\n  /**\n   * Array of instrumentation objects allowing you to instrument the router and\n   * individual routes prior to router initialization (and on any subsequently\n   * added routes via `route.lazy` or `patchRoutesOnNavigation`).  This is\n   * mostly useful for observability such as wrapping navigations, fetches,\n   * as well as route loaders/actions/middlewares with logging and/or performance\n   * tracing.  See the [docs](../../how-to/instrumentation) for more information.\n   *\n   * ```tsx\n   * let router = createBrowserRouter(routes, {\n   *   unstable_instrumentations: [logging]\n   * });\n   *\n   *\n   * let logging = {\n   *   router({ instrument }) {\n   *     instrument({\n   *       navigate: (impl, info) => logExecution(`navigate ${info.to}`, impl),\n   *       fetch: (impl, info) => logExecution(`fetch ${info.to}`, impl)\n   *     });\n   *   },\n   *   route({ instrument, id }) {\n   *     instrument({\n   *       middleware: (impl, info) => logExecution(\n   *         `middleware ${info.request.url} (route ${id})`,\n   *         impl\n   *       ),\n   *       loader: (impl, info) => logExecution(\n   *         `loader ${info.request.url} (route ${id})`,\n   *         impl\n   *       ),\n   *       action: (impl, info) => logExecution(\n   *         `action ${info.request.url} (route ${id})`,\n   *         impl\n   *       ),\n   *     })\n   *   }\n   * };\n   *\n   * async function logExecution(label: string, impl: () => Promise<void>) {\n   *   let start = performance.now();\n   *   console.log(`start ${label}`);\n   *   await impl();\n   *   let duration = Math.round(performance.now() - start);\n   *   console.log(`end ${label} (${duration}ms)`);\n   * }\n   * ```\n   */\n  unstable_instrumentations?: unstable_ClientInstrumentation[];\n  /**\n   * Override the default data strategy of running loaders in parallel -\n   * see the [docs](../../how-to/data-strategy) for more information.\n   *\n   * ```tsx\n   * let router = createBrowserRouter(routes, {\n   *   async dataStrategy({\n   *     matches,\n   *     request,\n   *     runClientMiddleware,\n   *   }) {\n   *     const matchesToLoad = matches.filter((m) =>\n   *       m.shouldCallHandler(),\n   *     );\n   *\n   *     const results: Record<string, DataStrategyResult> = {};\n   *     await runClientMiddleware(() =>\n   *       Promise.all(\n   *         matchesToLoad.map(async (match) => {\n   *           results[match.route.id] = await match.resolve();\n   *         }),\n   *       ),\n   *     );\n   *     return results;\n   *   },\n   * });\n   * ```\n   */\n  dataStrategy?: DataStrategyFunction;\n  /**\n   * Lazily define portions of the route tree on navigations.\n   * See {@link PatchRoutesOnNavigationFunction}.\n   *\n   * By default, React Router wants you to provide a full route tree up front via\n   * `createBrowserRouter(routes)`. This allows React Router to perform synchronous\n   * route matching, execute loaders, and then render route components in the most\n   * optimistic manner without introducing waterfalls. The tradeoff is that your\n   * initial JS bundle is larger by definition — which may slow down application\n   * start-up times as your application grows.\n   *\n   * To combat this, we introduced [`route.lazy`](../../start/data/route-object#lazy)\n   * in [v6.9.0](https://github.com/remix-run/react-router/blob/main/CHANGELOG.md#v690)\n   * which lets you lazily load the route _implementation_ ([`loader`](../../start/data/route-object#loader),\n   * [`Component`](../../start/data/route-object#Component), etc.) while still\n   * providing the route _definition_ aspects up front (`path`, `index`, etc.).\n   * This is a good middle ground. React Router still knows about your route\n   * definitions (the lightweight part) up front and can perform synchronous\n   * route matching, but then delay loading any of the route implementation\n   * aspects (the heavier part) until the route is actually navigated to.\n   *\n   * In some cases, even this doesn't go far enough. For huge applications,\n   * providing all route definitions up front can be prohibitively expensive.\n   * Additionally, it might not even be possible to provide all route definitions\n   * up front in certain Micro-Frontend or Module-Federation architectures.\n   *\n   * This is where `patchRoutesOnNavigation` comes in ([RFC](https://github.com/remix-run/react-router/discussions/11113)).\n   * This API is for advanced use-cases where you are unable to provide the full\n   * route tree up-front and need a way to lazily \"discover\" portions of the route\n   * tree at runtime. This feature is often referred to as [\"Fog of War\"](https://en.wikipedia.org/wiki/Fog_of_war),\n   * because similar to how video games expand the \"world\" as you move around -\n   * the router would be expanding its routing tree as the user navigated around\n   * the app - but would only ever end up loading portions of the tree that the\n   * user visited.\n   *\n   * `patchRoutesOnNavigation` will be called anytime React Router is unable to\n   * match a `path`. The arguments include the `path`, any partial `matches`,\n   * and a `patch` function you can call to patch new routes into the tree at a\n   * specific location. This method is executed during the `loading` portion of\n   * the navigation for `GET` requests and during the `submitting` portion of\n   * the navigation for non-`GET` requests.\n   *\n   * <details>\n   *   <summary><b>Example <code>patchRoutesOnNavigation</code> Use Cases</b></summary>\n   *\n   *   **Patching children into an existing route**\n   *\n   *   ```tsx\n   *   const router = createBrowserRouter(\n   *     [\n   *       {\n   *         id: \"root\",\n   *         path: \"/\",\n   *         Component: RootComponent,\n   *       },\n   *     ],\n   *     {\n   *       async patchRoutesOnNavigation({ patch, path }) {\n   *         if (path === \"/a\") {\n   *           // Load/patch the `a` route as a child of the route with id `root`\n   *           let route = await getARoute();\n   *           //  ^ { path: 'a', Component: A }\n   *           patch(\"root\", [route]);\n   *         }\n   *       },\n   *     }\n   *   );\n   *   ```\n   *\n   *   In the above example, if the user clicks a link to `/a`, React Router\n   *   won't match any routes initially and will call `patchRoutesOnNavigation`\n   *   with a `path = \"/a\"` and a `matches` array containing the root route\n   *   match. By calling `patch('root', [route])`, the new route will be added\n   *   to the route tree as a child of the `root` route and React Router will\n   *   perform matching on the updated routes. This time it will successfully\n   *   match the `/a` path and the navigation will complete successfully.\n   *\n   *   **Patching new root-level routes**\n   *\n   *   If you need to patch a new route to the top of the tree (i.e., it doesn't\n   *   have a parent), you can pass `null` as the `routeId`:\n   *\n   *   ```tsx\n   *   const router = createBrowserRouter(\n   *     [\n   *       {\n   *         id: \"root\",\n   *         path: \"/\",\n   *         Component: RootComponent,\n   *       },\n   *     ],\n   *     {\n   *       async patchRoutesOnNavigation({ patch, path }) {\n   *         if (path === \"/root-sibling\") {\n   *           // Load/patch the `/root-sibling` route as a sibling of the root route\n   *           let route = await getRootSiblingRoute();\n   *           //  ^ { path: '/root-sibling', Component: RootSibling }\n   *           patch(null, [route]);\n   *         }\n   *       },\n   *     }\n   *   );\n   *   ```\n   *\n   *   **Patching subtrees asynchronously**\n   *\n   *   You can also perform asynchronous matching to lazily fetch entire sections\n   *   of your application:\n   *\n   *   ```tsx\n   *   let router = createBrowserRouter(\n   *     [\n   *       {\n   *         path: \"/\",\n   *         Component: Home,\n   *       },\n   *     ],\n   *     {\n   *       async patchRoutesOnNavigation({ patch, path }) {\n   *         if (path.startsWith(\"/dashboard\")) {\n   *           let children = await import(\"./dashboard\");\n   *           patch(null, children);\n   *         }\n   *         if (path.startsWith(\"/account\")) {\n   *           let children = await import(\"./account\");\n   *           patch(null, children);\n   *         }\n   *       },\n   *     }\n   *   );\n   *   ```\n   *\n   *   <docs-info>If in-progress execution of `patchRoutesOnNavigation` is\n   *   interrupted by a later navigation, then any remaining `patch` calls in\n   *   the interrupted execution will not update the route tree because the\n   *   operation was cancelled.</docs-info>\n   *\n   *   **Co-locating route discovery with route definition**\n   *\n   *   If you don't wish to perform your own pseudo-matching, you can leverage\n   *   the partial `matches` array and the [`handle`](../../start/data/route-object#handle)\n   *   field on a route to keep the children definitions co-located:\n   *\n   *   ```tsx\n   *   let router = createBrowserRouter(\n   *     [\n   *       {\n   *         path: \"/\",\n   *         Component: Home,\n   *       },\n   *       {\n   *         path: \"/dashboard\",\n   *         children: [\n   *           {\n   *             // If we want to include /dashboard in the critical routes, we need to\n   *             // also include it's index route since patchRoutesOnNavigation will not be\n   *             // called on a navigation to `/dashboard` because it will have successfully\n   *             // matched the `/dashboard` parent route\n   *             index: true,\n   *             // ...\n   *           },\n   *         ],\n   *         handle: {\n   *           lazyChildren: () => import(\"./dashboard\"),\n   *         },\n   *       },\n   *       {\n   *         path: \"/account\",\n   *         children: [\n   *           {\n   *             index: true,\n   *             // ...\n   *           },\n   *         ],\n   *         handle: {\n   *           lazyChildren: () => import(\"./account\"),\n   *         },\n   *       },\n   *     ],\n   *     {\n   *       async patchRoutesOnNavigation({ matches, patch }) {\n   *         let leafRoute = matches[matches.length - 1]?.route;\n   *         if (leafRoute?.handle?.lazyChildren) {\n   *           let children =\n   *             await leafRoute.handle.lazyChildren();\n   *           patch(leafRoute.id, children);\n   *         }\n   *       },\n   *     }\n   *   );\n   *   ```\n   *\n   *   **A note on routes with parameters**\n   *\n   *   Because React Router uses ranked routes to find the best match for a\n   *   given path, there is an interesting ambiguity introduced when only a\n   *   partial route tree is known at any given point in time. If we match a\n   *   fully static route such as `path: \"/about/contact-us\"` then we know we've\n   *   found the right match since it's composed entirely of static URL segments.\n   *   Thus, we do not need to bother asking for any other potentially\n   *   higher-scoring routes.\n   *\n   *   However, routes with parameters (dynamic or splat) can't make this\n   *   assumption because there might be a not-yet-discovered route that scores\n   *   higher. Consider a full route tree such as:\n   *\n   *   ```tsx\n   *   // Assume this is the full route tree for your app\n   *   const routes = [\n   *     {\n   *       path: \"/\",\n   *       Component: Home,\n   *     },\n   *     {\n   *       id: \"blog\",\n   *       path: \"/blog\",\n   *       Component: BlogLayout,\n   *       children: [\n   *         { path: \"new\", Component: NewPost },\n   *         { path: \":slug\", Component: BlogPost },\n   *       ],\n   *     },\n   *   ];\n   *   ```\n   *\n   *   And then assume we want to use `patchRoutesOnNavigation` to fill this in\n   *   as the user navigates around:\n   *\n   *   ```tsx\n   *   // Start with only the index route\n   *   const router = createBrowserRouter(\n   *     [\n   *       {\n   *         path: \"/\",\n   *         Component: Home,\n   *       },\n   *     ],\n   *     {\n   *       async patchRoutesOnNavigation({ patch, path }) {\n   *         if (path === \"/blog/new\") {\n   *           patch(\"blog\", [\n   *             {\n   *               path: \"new\",\n   *               Component: NewPost,\n   *             },\n   *           ]);\n   *         } else if (path.startsWith(\"/blog\")) {\n   *           patch(\"blog\", [\n   *             {\n   *               path: \":slug\",\n   *               Component: BlogPost,\n   *             },\n   *           ]);\n   *         }\n   *       },\n   *     }\n   *   );\n   *   ```\n   *\n   *   If the user were to a blog post first (i.e., `/blog/my-post`) we would\n   *   patch in the `:slug` route. Then, if the user navigated to `/blog/new` to\n   *   write a new post, we'd match `/blog/:slug` but it wouldn't be the _right_\n   *   match! We need to call `patchRoutesOnNavigation` just in case there\n   *   exists a higher-scoring route we've not yet discovered, which in this\n   *   case there is.\n   *\n   *   So, anytime React Router matches a path that contains at least one param,\n   *   it will call `patchRoutesOnNavigation` and match routes again just to\n   *   confirm it has found the best match.\n   *\n   *   If your `patchRoutesOnNavigation` implementation is expensive or making\n   *   side effect [`fetch`](https://developer.mozilla.org/en-US/docs/Web/API/fetch)\n   *   calls to a backend server, you may want to consider tracking previously\n   *   seen routes to avoid over-fetching in cases where you know the proper\n   *   route has already been found. This can usually be as simple as\n   *   maintaining a small cache of prior `path` values for which you've already\n   *   patched in the right routes:\n   *\n   *   ```tsx\n   *   let discoveredRoutes = new Set();\n   *\n   *   const router = createBrowserRouter(routes, {\n   *     async patchRoutesOnNavigation({ patch, path }) {\n   *       if (discoveredRoutes.has(path)) {\n   *         // We've seen this before so nothing to patch in and we can let the router\n   *         // use the routes it already knows about\n   *         return;\n   *       }\n   *\n   *       discoveredRoutes.add(path);\n   *\n   *       // ... patch routes in accordingly\n   *     },\n   *   });\n   *   ```\n   * </details>\n   */\n  patchRoutesOnNavigation?: PatchRoutesOnNavigationFunction;\n  /**\n   * [`Window`](https://developer.mozilla.org/en-US/docs/Web/API/Window) object\n   * override. Defaults to the global `window` instance.\n   */\n  window?: Window;\n}\n\n/**\n * Create a new {@link DataRouter| data router} that manages the application\n * path via [`history.pushState`](https://developer.mozilla.org/en-US/docs/Web/API/History/pushState)\n * and [`history.replaceState`](https://developer.mozilla.org/en-US/docs/Web/API/History/replaceState).\n *\n * @public\n * @category Data Routers\n * @mode data\n * @param routes Application routes\n * @param opts Options\n * @param {DOMRouterOpts.basename} opts.basename n/a\n * @param {DOMRouterOpts.dataStrategy} opts.dataStrategy n/a\n * @param {DOMRouterOpts.future} opts.future n/a\n * @param {DOMRouterOpts.getContext} opts.getContext n/a\n * @param {DOMRouterOpts.hydrationData} opts.hydrationData n/a\n * @param {DOMRouterOpts.unstable_instrumentations} opts.unstable_instrumentations n/a\n * @param {DOMRouterOpts.patchRoutesOnNavigation} opts.patchRoutesOnNavigation n/a\n * @param {DOMRouterOpts.window} opts.window n/a\n * @returns An initialized {@link DataRouter| data router} to pass to {@link RouterProvider | `<RouterProvider>`}\n */\nexport function createBrowserRouter(\n  routes: RouteObject[],\n  opts?: DOMRouterOpts,\n): DataRouter {\n  return createRouter({\n    basename: opts?.basename,\n    getContext: opts?.getContext,\n    future: opts?.future,\n    history: createBrowserHistory({ window: opts?.window }),\n    hydrationData: opts?.hydrationData || parseHydrationData(),\n    routes,\n    mapRouteProperties,\n    hydrationRouteProperties,\n    dataStrategy: opts?.dataStrategy,\n    patchRoutesOnNavigation: opts?.patchRoutesOnNavigation,\n    window: opts?.window,\n    unstable_instrumentations: opts?.unstable_instrumentations,\n  }).initialize();\n}\n\n/**\n * Create a new {@link DataRouter| data router} that manages the application\n * path via the URL [`hash`](https://developer.mozilla.org/en-US/docs/Web/API/URL/hash).\n *\n * @public\n * @category Data Routers\n * @mode data\n * @param routes Application routes\n * @param opts Options\n * @param {DOMRouterOpts.basename} opts.basename n/a\n * @param {DOMRouterOpts.future} opts.future n/a\n * @param {DOMRouterOpts.getContext} opts.getContext n/a\n * @param {DOMRouterOpts.hydrationData} opts.hydrationData n/a\n * @param {DOMRouterOpts.unstable_instrumentations} opts.unstable_instrumentations n/a\n * @param {DOMRouterOpts.dataStrategy} opts.dataStrategy n/a\n * @param {DOMRouterOpts.patchRoutesOnNavigation} opts.patchRoutesOnNavigation n/a\n * @param {DOMRouterOpts.window} opts.window n/a\n * @returns An initialized {@link DataRouter| data router} to pass to {@link RouterProvider | `<RouterProvider>`}\n */\nexport function createHashRouter(\n  routes: RouteObject[],\n  opts?: DOMRouterOpts,\n): DataRouter {\n  return createRouter({\n    basename: opts?.basename,\n    getContext: opts?.getContext,\n    future: opts?.future,\n    history: createHashHistory({ window: opts?.window }),\n    hydrationData: opts?.hydrationData || parseHydrationData(),\n    routes,\n    mapRouteProperties,\n    hydrationRouteProperties,\n    dataStrategy: opts?.dataStrategy,\n    patchRoutesOnNavigation: opts?.patchRoutesOnNavigation,\n    window: opts?.window,\n    unstable_instrumentations: opts?.unstable_instrumentations,\n  }).initialize();\n}\n\nfunction parseHydrationData(): HydrationState | undefined {\n  let state = window?.__staticRouterHydrationData;\n  if (state && state.errors) {\n    state = {\n      ...state,\n      errors: deserializeErrors(state.errors),\n    };\n  }\n  return state;\n}\n\nfunction deserializeErrors(\n  errors: DataRouter[\"state\"][\"errors\"],\n): DataRouter[\"state\"][\"errors\"] {\n  if (!errors) return null;\n  let entries = Object.entries(errors);\n  let serialized: DataRouter[\"state\"][\"errors\"] = {};\n  for (let [key, val] of entries) {\n    // Hey you!  If you change this, please change the corresponding logic in\n    // serializeErrors in react-router-dom/server.tsx :)\n    if (val && val.__type === \"RouteErrorResponse\") {\n      serialized[key] = new ErrorResponseImpl(\n        val.status,\n        val.statusText,\n        val.data,\n        val.internal === true,\n      );\n    } else if (val && val.__type === \"Error\") {\n      // Attempt to reconstruct the right type of Error (i.e., ReferenceError)\n      if (val.__subType) {\n        let ErrorConstructor = window[val.__subType];\n        if (typeof ErrorConstructor === \"function\") {\n          try {\n            // @ts-expect-error\n            let error = new ErrorConstructor(val.message);\n            // Wipe away the client-side stack trace.  Nothing to fill it in with\n            // because we don't serialize SSR stack traces for security reasons\n            error.stack = \"\";\n            serialized[key] = error;\n          } catch (e) {\n            // no-op - fall through and create a normal Error\n          }\n        }\n      }\n\n      if (serialized[key] == null) {\n        let error = new Error(val.message);\n        // Wipe away the client-side stack trace.  Nothing to fill it in with\n        // because we don't serialize SSR stack traces for security reasons\n        error.stack = \"\";\n        serialized[key] = error;\n      }\n    } else {\n      serialized[key] = val;\n    }\n  }\n  return serialized;\n}\n\n//#endregion\n\n////////////////////////////////////////////////////////////////////////////////\n//#region Components\n////////////////////////////////////////////////////////////////////////////////\n\n/**\n * @category Types\n */\nexport interface BrowserRouterProps {\n  /**\n   * Application basename\n   */\n  basename?: string;\n  /**\n   * {@link Route | `<Route>`} components describing your route configuration\n   */\n  children?: React.ReactNode;\n  /**\n   * Control whether router state updates are internally wrapped in\n   * [`React.startTransition`](https://react.dev/reference/react/startTransition).\n   *\n   * - When left `undefined`, all router state updates are wrapped in\n   *   `React.startTransition`\n   * - When set to `true`, {@link Link} and {@link Form} navigations will be wrapped\n   *   in `React.startTransition` and all router state updates are wrapped in\n   *   `React.startTransition`\n   * - When set to `false`, the router will not leverage `React.startTransition`\n   *   on any navigations or state changes.\n   *\n   * For more information, please see the [docs](https://reactrouter.com/explanation/react-transitions).\n   */\n  unstable_useTransitions?: boolean;\n  /**\n   * [`Window`](https://developer.mozilla.org/en-US/docs/Web/API/Window) object\n   * override. Defaults to the global `window` instance\n   */\n  window?: Window;\n}\n\n/**\n * A declarative {@link Router | `<Router>`} using the browser [`History`](https://developer.mozilla.org/en-US/docs/Web/API/History)\n * API for client-side routing.\n *\n * @public\n * @category Declarative Routers\n * @mode declarative\n * @param props Props\n * @param {BrowserRouterProps.basename} props.basename n/a\n * @param {BrowserRouterProps.children} props.children n/a\n * @param {BrowserRouterProps.unstable_useTransitions} props.unstable_useTransitions n/a\n * @param {BrowserRouterProps.window} props.window n/a\n * @returns A declarative {@link Router | `<Router>`} using the browser [`History`](https://developer.mozilla.org/en-US/docs/Web/API/History)\n * API for client-side routing.\n */\nexport function BrowserRouter({\n  basename,\n  children,\n  unstable_useTransitions,\n  window,\n}: BrowserRouterProps) {\n  let historyRef = React.useRef<BrowserHistory>();\n  if (historyRef.current == null) {\n    historyRef.current = createBrowserHistory({ window, v5Compat: true });\n  }\n\n  let history = historyRef.current;\n  let [state, setStateImpl] = React.useState({\n    action: history.action,\n    location: history.location,\n  });\n  let setState = React.useCallback(\n    (newState: { action: NavigationType; location: Location }) => {\n      if (unstable_useTransitions === false) {\n        setStateImpl(newState);\n      } else {\n        React.startTransition(() => setStateImpl(newState));\n      }\n    },\n    [unstable_useTransitions],\n  );\n\n  React.useLayoutEffect(() => history.listen(setState), [history, setState]);\n\n  return (\n    <Router\n      basename={basename}\n      children={children}\n      location={state.location}\n      navigationType={state.action}\n      navigator={history}\n      unstable_useTransitions={unstable_useTransitions}\n    />\n  );\n}\n\n/**\n * @category Types\n */\nexport interface HashRouterProps {\n  /**\n   * Application basename\n   */\n  basename?: string;\n  /**\n   * {@link Route | `<Route>`} components describing your route configuration\n   */\n  children?: React.ReactNode;\n  /**\n   * Control whether router state updates are internally wrapped in\n   * [`React.startTransition`](https://react.dev/reference/react/startTransition).\n   *\n   * - When left `undefined`, all router state updates are wrapped in\n   *   `React.startTransition`\n   * - When set to `true`, {@link Link} and {@link Form} navigations will be wrapped\n   *   in `React.startTransition` and all router state updates are wrapped in\n   *   `React.startTransition`\n   * - When set to `false`, the router will not leverage `React.startTransition`\n   *   on any navigations or state changes.\n   *\n   * For more information, please see the [docs](https://reactrouter.com/explanation/react-transitions).\n   */\n  unstable_useTransitions?: boolean;\n  /**\n   * [`Window`](https://developer.mozilla.org/en-US/docs/Web/API/Window) object\n   * override. Defaults to the global `window` instance\n   */\n  window?: Window;\n}\n\n/**\n * A declarative {@link Router | `<Router>`} that stores the location in the\n * [`hash`](https://developer.mozilla.org/en-US/docs/Web/API/URL/hash) portion\n * of the URL so it is not sent to the server.\n *\n * @public\n * @category Declarative Routers\n * @mode declarative\n * @param props Props\n * @param {HashRouterProps.basename} props.basename n/a\n * @param {HashRouterProps.children} props.children n/a\n * @param {HashRouterProps.unstable_useTransitions} props.unstable_useTransitions n/a\n * @param {HashRouterProps.window} props.window n/a\n * @returns A declarative {@link Router | `<Router>`} using the URL [`hash`](https://developer.mozilla.org/en-US/docs/Web/API/URL/hash)\n * for client-side routing.\n */\nexport function HashRouter({\n  basename,\n  children,\n  unstable_useTransitions,\n  window,\n}: HashRouterProps) {\n  let historyRef = React.useRef<HashHistory>();\n  if (historyRef.current == null) {\n    historyRef.current = createHashHistory({ window, v5Compat: true });\n  }\n\n  let history = historyRef.current;\n  let [state, setStateImpl] = React.useState({\n    action: history.action,\n    location: history.location,\n  });\n  let setState = React.useCallback(\n    (newState: { action: NavigationType; location: Location }) => {\n      if (unstable_useTransitions === false) {\n        setStateImpl(newState);\n      } else {\n        React.startTransition(() => setStateImpl(newState));\n      }\n    },\n    [unstable_useTransitions],\n  );\n\n  React.useLayoutEffect(() => history.listen(setState), [history, setState]);\n\n  return (\n    <Router\n      basename={basename}\n      children={children}\n      location={state.location}\n      navigationType={state.action}\n      navigator={history}\n      unstable_useTransitions={unstable_useTransitions}\n    />\n  );\n}\n\n/**\n * @category Types\n */\nexport interface HistoryRouterProps {\n  /**\n   * Application basename\n   */\n  basename?: string;\n  /**\n   * {@link Route | `<Route>`} components describing your route configuration\n   */\n  children?: React.ReactNode;\n  /**\n   *  A {@link History} implementation for use by the router\n   */\n  history: History;\n  /**\n   * Control whether router state updates are internally wrapped in\n   * [`React.startTransition`](https://react.dev/reference/react/startTransition).\n   *\n   * - When left `undefined`, all router state updates are wrapped in\n   *   `React.startTransition`\n   * - When set to `true`, {@link Link} and {@link Form} navigations will be wrapped\n   *   in `React.startTransition` and all router state updates are wrapped in\n   *   `React.startTransition`\n   * - When set to `false`, the router will not leverage `React.startTransition`\n   *   on any navigations or state changes.\n   *\n   * For more information, please see the [docs](https://reactrouter.com/explanation/react-transitions).\n   */\n  unstable_useTransitions?: boolean;\n}\n\n/**\n * A declarative {@link Router | `<Router>`} that accepts a pre-instantiated\n * `history` object.\n * It's important to note that using your own `history` object is highly discouraged\n * and may add two versions of the `history` library to your bundles unless you use\n * the same version of the `history` library that React Router uses internally.\n *\n * @name unstable_HistoryRouter\n * @public\n * @category Declarative Routers\n * @mode declarative\n * @param props Props\n * @param {HistoryRouterProps.basename} props.basename n/a\n * @param {HistoryRouterProps.children} props.children n/a\n * @param {HistoryRouterProps.history} props.history n/a\n * @param {HistoryRouterProps.unstable_useTransitions} props.unstable_useTransitions n/a\n * @returns A declarative {@link Router | `<Router>`} using the provided history\n * implementation for client-side routing.\n */\nexport function HistoryRouter({\n  basename,\n  children,\n  history,\n  unstable_useTransitions,\n}: HistoryRouterProps) {\n  let [state, setStateImpl] = React.useState({\n    action: history.action,\n    location: history.location,\n  });\n  let setState = React.useCallback(\n    (newState: { action: NavigationType; location: Location }) => {\n      if (unstable_useTransitions === false) {\n        setStateImpl(newState);\n      } else {\n        React.startTransition(() => setStateImpl(newState));\n      }\n    },\n    [unstable_useTransitions],\n  );\n\n  React.useLayoutEffect(() => history.listen(setState), [history, setState]);\n\n  return (\n    <Router\n      basename={basename}\n      children={children}\n      location={state.location}\n      navigationType={state.action}\n      navigator={history}\n      unstable_useTransitions={unstable_useTransitions}\n    />\n  );\n}\nHistoryRouter.displayName = \"unstable_HistoryRouter\";\n\n/**\n * @category Types\n */\nexport interface LinkProps\n  extends Omit<React.AnchorHTMLAttributes<HTMLAnchorElement>, \"href\"> {\n  /**\n   * Defines the link [lazy route discovery](../../explanation/lazy-route-discovery) behavior.\n   *\n   * - **render** — default, discover the route when the link renders\n   * - **none** — don't eagerly discover, only discover if the link is clicked\n   *\n   * ```tsx\n   * <Link /> // default (\"render\")\n   * <Link discover=\"render\" />\n   * <Link discover=\"none\" />\n   * ```\n   */\n  discover?: DiscoverBehavior;\n\n  /**\n   * Defines the data and module prefetching behavior for the link.\n   *\n   * ```tsx\n   * <Link /> // default\n   * <Link prefetch=\"none\" />\n   * <Link prefetch=\"intent\" />\n   * <Link prefetch=\"render\" />\n   * <Link prefetch=\"viewport\" />\n   * ```\n   *\n   * - **none** — default, no prefetching\n   * - **intent** — prefetches when the user hovers or focuses the link\n   * - **render** — prefetches when the link renders\n   * - **viewport** — prefetches when the link is in the viewport, very useful for mobile\n   *\n   * Prefetching is done with HTML [`<link rel=\"prefetch\">`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/link)\n   * tags. They are inserted after the link.\n   *\n   * ```tsx\n   * <a href=\"...\" />\n   * <a href=\"...\" />\n   * <link rel=\"prefetch\" /> // might conditionally render\n   * ```\n   *\n   * Because of this, if you are using `nav :last-child` you will need to use\n   * `nav :last-of-type` so the styles don't conditionally fall off your last link\n   * (and any other similar selectors).\n   */\n  prefetch?: PrefetchBehavior;\n\n  /**\n   * Will use document navigation instead of client side routing when the link is\n   * clicked: the browser will handle the transition normally (as if it were an\n   * [`<a href>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/a)).\n   *\n   * ```tsx\n   * <Link to=\"/logout\" reloadDocument />\n   * ```\n   */\n  reloadDocument?: boolean;\n\n  /**\n   * Replaces the current entry in the [`History`](https://developer.mozilla.org/en-US/docs/Web/API/History)\n   * stack instead of pushing a new one onto it.\n   *\n   * ```tsx\n   * <Link replace />\n   * ```\n   *\n   * ```\n   * # with a history stack like this\n   * A -> B\n   *\n   * # normal link click pushes a new entry\n   * A -> B -> C\n   *\n   * # but with `replace`, B is replaced by C\n   * A -> C\n   * ```\n   */\n  replace?: boolean;\n\n  /**\n   * Adds persistent client side routing state to the next location.\n   *\n   * ```tsx\n   * <Link to=\"/somewhere/else\" state={{ some: \"value\" }} />\n   * ```\n   *\n   * The location state is accessed from the `location`.\n   *\n   * ```tsx\n   * function SomeComp() {\n   *   const location = useLocation();\n   *   location.state; // { some: \"value\" }\n   * }\n   * ```\n   *\n   * This state is inaccessible on the server as it is implemented on top of\n   * [`history.state`](https://developer.mozilla.org/en-US/docs/Web/API/History/state)\n   */\n  state?: any;\n\n  /**\n   * Prevents the scroll position from being reset to the top of the window when\n   * the link is clicked and the app is using {@link ScrollRestoration}. This only\n   * prevents new locations resetting scroll to the top, scroll position will be\n   * restored for back/forward button navigation.\n   *\n   * ```tsx\n   * <Link to=\"?tab=one\" preventScrollReset />\n   * ```\n   */\n  preventScrollReset?: boolean;\n\n  /**\n   * Defines the relative path behavior for the link.\n   *\n   * ```tsx\n   * <Link to=\"..\" /> // default: \"route\"\n   * <Link relative=\"route\" />\n   * <Link relative=\"path\" />\n   * ```\n   *\n   * Consider a route hierarchy where a parent route pattern is `\"blog\"` and a child\n   * route pattern is `\"blog/:slug/edit\"`.\n   *\n   * - **route** — default, resolves the link relative to the route pattern. In the\n   * example above, a relative link of `\"...\"` will remove both `:slug/edit` segments\n   * back to `\"/blog\"`.\n   * - **path** — relative to the path so `\"...\"` will only remove one URL segment up\n   * to `\"/blog/:slug\"`\n   *\n   * Note that index routes and layout routes do not have paths so they are not\n   * included in the relative path calculation.\n   */\n  relative?: RelativeRoutingType;\n\n  /**\n   * Can be a string or a partial {@link Path}:\n   *\n   * ```tsx\n   * <Link to=\"/some/path\" />\n   *\n   * <Link\n   *   to={{\n   *     pathname: \"/some/path\",\n   *     search: \"?query=string\",\n   *     hash: \"#hash\",\n   *   }}\n   * />\n   * ```\n   */\n  to: To;\n\n  /**\n   * Enables a [View Transition](https://developer.mozilla.org/en-US/docs/Web/API/View_Transitions_API)\n   * for this navigation.\n   *\n   * ```jsx\n   * <Link to={to} viewTransition>\n   *   Click me\n   * </Link>\n   * ```\n   *\n   * To apply specific styles for the transition, see {@link useViewTransitionState}\n   */\n  viewTransition?: boolean;\n\n  /**\n   * Specify the default revalidation behavior for the navigation.\n   *\n   * ```tsx\n   * <Link to=\"/some/path\" unstable_defaultShouldRevalidate={false} />\n   * ```\n   *\n   * If no `shouldRevalidate` functions are present on the active routes, then this\n   * value will be used directly.  Otherwise it will be passed into `shouldRevalidate`\n   * so the route can make the final determination on revalidation. This can be\n   * useful when updating search params and you don't want to trigger a revalidation.\n   *\n   * By default (when not specified), loaders will revalidate according to the routers\n   * standard revalidation behavior.\n   */\n  unstable_defaultShouldRevalidate?: boolean;\n\n  /**\n   * Masked path for for this navigation, when you want to navigate the router to\n   * one location but display a separate location in the URL bar.\n   *\n   * This is useful for contextual navigations such as opening an image in a modal\n   * on top of a gallery while keeping the underlying gallery active. If a user\n   * shares the masked URL, or opens the link in a new tab, they will only load\n   * the masked location without the underlying contextual location.\n   *\n   * This feature relies on `history.state` and is thus only intended for SPA uses\n   * and SSR renders will not respect the masking.\n   *\n   * ```tsx\n   * // routes/gallery.tsx\n   * export function clientLoader({ request }: Route.LoaderArgs) {\n   *   let sp = new URL(request.url).searchParams;\n   *   return {\n   *     images: getImages(),\n   *     modalImage: sp.has(\"image\") ? getImage(sp.get(\"image\")!) : null,\n   *   };\n   * }\n   *\n   * export default function Gallery({ loaderData }: Route.ComponentProps) {\n   *   return (\n   *     <>\n   *       <GalleryGrid>\n   *        {loaderData.images.map((image) => (\n   *          <Link\n   *            key={image.id}\n   *            to={`/gallery?image=${image.id}`}\n   *            unstable_mask={`/images/${image.id}`}\n   *          >\n   *            <img src={image.url} alt={image.alt} />\n   *          </Link>\n   *        ))}\n   *       </GalleryGrid>\n   *\n   *       {data.modalImage ? (\n   *         <dialog open>\n   *           <img src={data.modalImage.url} alt={data.modalImage.alt} />\n   *         </dialog>\n   *       ) : null}\n   *     </>\n   *   );\n   * }\n   * ```\n   */\n  unstable_mask?: To;\n}\n\nconst ABSOLUTE_URL_REGEX = /^(?:[a-z][a-z0-9+.-]*:|\\/\\/)/i;\n\n/**\n * A progressively enhanced [`<a href>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/a)\n * wrapper to enable navigation with client-side routing.\n *\n * @example\n * import { Link } from \"react-router\";\n *\n * <Link to=\"/dashboard\">Dashboard</Link>;\n *\n * <Link\n *   to={{\n *     pathname: \"/some/path\",\n *     search: \"?query=string\",\n *     hash: \"#hash\",\n *   }}\n * />;\n *\n * @public\n * @category Components\n * @param {LinkProps.discover} props.discover [modes: framework] n/a\n * @param {LinkProps.prefetch} props.prefetch [modes: framework] n/a\n * @param {LinkProps.preventScrollReset} props.preventScrollReset [modes: framework, data] n/a\n * @param {LinkProps.relative} props.relative n/a\n * @param {LinkProps.reloadDocument} props.reloadDocument n/a\n * @param {LinkProps.replace} props.replace n/a\n * @param {LinkProps.state} props.state n/a\n * @param {LinkProps.to} props.to n/a\n * @param {LinkProps.viewTransition} props.viewTransition [modes: framework, data] n/a\n * @param {LinkProps.unstable_defaultShouldRevalidate} props.unstable_defaultShouldRevalidate n/a\n * @param {LinkProps.unstable_mask} props.unstable_mask [modes: framework, data] n/a\n */\nexport const Link = React.forwardRef<HTMLAnchorElement, LinkProps>(\n  function LinkWithRef(\n    {\n      onClick,\n      discover = \"render\",\n      prefetch = \"none\",\n      relative,\n      reloadDocument,\n      replace,\n      unstable_mask,\n      state,\n      target,\n      to,\n      preventScrollReset,\n      viewTransition,\n      unstable_defaultShouldRevalidate,\n      ...rest\n    },\n    forwardedRef,\n  ) {\n    let { basename, navigator, unstable_useTransitions } =\n      React.useContext(NavigationContext);\n    let isAbsolute = typeof to === \"string\" && ABSOLUTE_URL_REGEX.test(to);\n\n    let parsed = parseToInfo(to, basename);\n    to = parsed.to;\n\n    // Rendered into <a href> for relative URLs\n    let href = useHref(to, { relative });\n    let location = useLocation();\n\n    let maskedHref: string | null = null;\n\n    if (unstable_mask) {\n      // Inlined version of the `useHref` logic operating off the masked location\n      // instead of the current location\n      let resolved = resolveTo(\n        unstable_mask,\n        [],\n        location.unstable_mask ? location.unstable_mask.pathname : \"/\",\n        true,\n      );\n\n      // If we're operating within a basename, prepend it to the pathname prior\n      // to creating the href.  If this is a root navigation, then just use the raw\n      // basename which allows the basename to have full control over the presence\n      // of a trailing slash on root links\n      if (basename !== \"/\") {\n        resolved.pathname =\n          resolved.pathname === \"/\"\n            ? basename\n            : joinPaths([basename, resolved.pathname]);\n      }\n\n      maskedHref = navigator.createHref(resolved);\n    }\n\n    let [shouldPrefetch, prefetchRef, prefetchHandlers] = usePrefetchBehavior(\n      prefetch,\n      rest,\n    );\n\n    let internalOnClick = useLinkClickHandler(to, {\n      replace,\n      unstable_mask,\n      state,\n      target,\n      preventScrollReset,\n      relative,\n      viewTransition,\n      unstable_defaultShouldRevalidate,\n      unstable_useTransitions,\n    });\n    function handleClick(\n      event: React.MouseEvent<HTMLAnchorElement, MouseEvent>,\n    ) {\n      if (onClick) onClick(event);\n      if (!event.defaultPrevented) {\n        internalOnClick(event);\n      }\n    }\n\n    let isSpaLink = !(parsed.isExternal || reloadDocument);\n    let link = (\n      // eslint-disable-next-line jsx-a11y/anchor-has-content\n      <a\n        {...rest}\n        {...prefetchHandlers}\n        href={\n          (isSpaLink ? maskedHref : undefined) || parsed.absoluteURL || href\n        }\n        onClick={isSpaLink ? handleClick : onClick}\n        ref={mergeRefs(forwardedRef, prefetchRef)}\n        target={target}\n        data-discover={\n          !isAbsolute && discover === \"render\" ? \"true\" : undefined\n        }\n      />\n    );\n\n    return shouldPrefetch && !isAbsolute ? (\n      <>\n        {link}\n        <PrefetchPageLinks page={href} />\n      </>\n    ) : (\n      link\n    );\n  },\n);\nLink.displayName = \"Link\";\n\n/**\n * The object passed to {@link NavLink} `children`, `className`, and `style` prop\n * callbacks to render and style the link based on its state.\n *\n * ```\n * // className\n * <NavLink\n *   to=\"/messages\"\n *   className={({ isActive, isPending }) =>\n *     isPending ? \"pending\" : isActive ? \"active\" : \"\"\n *   }\n * >\n *   Messages\n * </NavLink>\n *\n * // style\n * <NavLink\n *   to=\"/messages\"\n *   style={({ isActive, isPending }) => {\n *     return {\n *       fontWeight: isActive ? \"bold\" : \"\",\n *       color: isPending ? \"red\" : \"black\",\n *     }\n *   )}\n * />\n *\n * // children\n * <NavLink to=\"/tasks\">\n *   {({ isActive, isPending }) => (\n *     <span className={isActive ? \"active\" : \"\"}>Tasks</span>\n *   )}\n * </NavLink>\n * ```\n *\n */\nexport type NavLinkRenderProps = {\n  /**\n   * Indicates if the link's URL matches the current {@link Location}.\n   */\n  isActive: boolean;\n\n  /**\n   * Indicates if the pending {@link Location} matches the link's URL. Only\n   * available in Framework/Data modes.\n   */\n  isPending: boolean;\n\n  /**\n   * Indicates if a view transition to the link's URL is in progress.\n   * See {@link useViewTransitionState}\n   */\n  isTransitioning: boolean;\n};\n\n/**\n * @category Types\n */\nexport interface NavLinkProps\n  extends Omit<LinkProps, \"className\" | \"style\" | \"children\"> {\n  /**\n   *  Can be regular React children or a function that receives an object with the\n   * `active` and `pending` states of the link.\n   *\n   *  ```tsx\n   *  <NavLink to=\"/tasks\">\n   *    {({ isActive }) => (\n   *      <span className={isActive ? \"active\" : \"\"}>Tasks</span>\n   *    )}\n   *  </NavLink>\n   *  ```\n   */\n  children?: React.ReactNode | ((props: NavLinkRenderProps) => React.ReactNode);\n\n  /**\n   * Changes the matching logic to make it case-sensitive:\n   *\n   * | Link                                         | URL           | isActive |\n   * | -------------------------------------------- | ------------- | -------- |\n   * | `<NavLink to=\"/SpOnGe-bOB\" />`               | `/sponge-bob` | true     |\n   * | `<NavLink to=\"/SpOnGe-bOB\" caseSensitive />` | `/sponge-bob` | false    |\n   */\n  caseSensitive?: boolean;\n\n  /**\n   * Classes are automatically applied to `NavLink` that correspond to the state.\n   *\n   * ```css\n   * a.active {\n   *   color: red;\n   * }\n   * a.pending {\n   *   color: blue;\n   * }\n   * a.transitioning {\n   *   view-transition-name: my-transition;\n   * }\n   * ```\n   *\n   * Or you can specify a function that receives {@link NavLinkRenderProps} and\n   * returns the `className`:\n   *\n   * ```tsx\n   * <NavLink className={({ isActive, isPending }) => (\n   *   isActive ? \"my-active-class\" :\n   *   isPending ? \"my-pending-class\" :\n   *   \"\"\n   * )} />\n   * ```\n   */\n  className?: string | ((props: NavLinkRenderProps) => string | undefined);\n\n  /**\n   * Changes the matching logic for the `active` and `pending` states to only match\n   * to the \"end\" of the {@link NavLinkProps.to}. If the URL is longer, it will no\n   * longer be considered active.\n   *\n   * | Link                          | URL          | isActive |\n   * | ----------------------------- | ------------ | -------- |\n   * | `<NavLink to=\"/tasks\" />`     | `/tasks`     | true     |\n   * | `<NavLink to=\"/tasks\" />`     | `/tasks/123` | true     |\n   * | `<NavLink to=\"/tasks\" end />` | `/tasks`     | true     |\n   * | `<NavLink to=\"/tasks\" end />` | `/tasks/123` | false    |\n   *\n   * `<NavLink to=\"/\">` is an exceptional case because _every_ URL matches `/`.\n   * To avoid this matching every single route by default, it effectively ignores\n   * the `end` prop and only matches when you're at the root route.\n   */\n  end?: boolean;\n\n  /**\n   * Styles can also be applied dynamically via a function that receives\n   * {@link NavLinkRenderProps} and returns the styles:\n   *\n   * ```tsx\n   * <NavLink to=\"/tasks\" style={{ color: \"red\" }} />\n   * <NavLink to=\"/tasks\" style={({ isActive, isPending }) => ({\n   *   color:\n   *     isActive ? \"red\" :\n   *     isPending ? \"blue\" : \"black\"\n   * })} />\n   * ```\n   */\n  style?:\n    | React.CSSProperties\n    | ((props: NavLinkRenderProps) => React.CSSProperties | undefined);\n}\n\n/**\n * Wraps {@link Link | `<Link>`} with additional props for styling active and\n * pending states.\n *\n * - Automatically applies classes to the link based on its `active` and `pending`\n * states, see {@link NavLinkProps.className}\n *   - Note that `pending` is only available with Framework and Data modes.\n * - Automatically applies `aria-current=\"page\"` to the link when the link is active.\n * See [`aria-current`](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Attributes/aria-current)\n * on MDN.\n * - States are additionally available through the className, style, and children\n * render props. See {@link NavLinkRenderProps}.\n *\n * @example\n * <NavLink to=\"/message\">Messages</NavLink>\n *\n * // Using render props\n * <NavLink\n *   to=\"/messages\"\n *   className={({ isActive, isPending }) =>\n *     isPending ? \"pending\" : isActive ? \"active\" : \"\"\n *   }\n * >\n *   Messages\n * </NavLink>\n *\n * @public\n * @category Components\n * @param {NavLinkProps.caseSensitive} props.caseSensitive n/a\n * @param {NavLinkProps.children} props.children n/a\n * @param {NavLinkProps.className} props.className n/a\n * @param {NavLinkProps.discover} props.discover [modes: framework] n/a\n * @param {NavLinkProps.end} props.end n/a\n * @param {NavLinkProps.prefetch} props.prefetch [modes: framework] n/a\n * @param {NavLinkProps.preventScrollReset} props.preventScrollReset [modes: framework, data] n/a\n * @param {NavLinkProps.relative} props.relative n/a\n * @param {NavLinkProps.reloadDocument} props.reloadDocument n/a\n * @param {NavLinkProps.replace} props.replace n/a\n * @param {NavLinkProps.state} props.state n/a\n * @param {NavLinkProps.style} props.style n/a\n * @param {NavLinkProps.to} props.to n/a\n * @param {NavLinkProps.viewTransition} props.viewTransition [modes: framework, data] n/a\n */\nexport const NavLink = React.forwardRef<HTMLAnchorElement, NavLinkProps>(\n  function NavLinkWithRef(\n    {\n      \"aria-current\": ariaCurrentProp = \"page\",\n      caseSensitive = false,\n      className: classNameProp = \"\",\n      end = false,\n      style: styleProp,\n      to,\n      viewTransition,\n      children,\n      ...rest\n    },\n    ref,\n  ) {\n    let path = useResolvedPath(to, { relative: rest.relative });\n    let location = useLocation();\n    let routerState = React.useContext(DataRouterStateContext);\n    let { navigator, basename } = React.useContext(NavigationContext);\n    let isTransitioning =\n      routerState != null &&\n      // Conditional usage is OK here because the usage of a data router is static\n      // eslint-disable-next-line react-hooks/rules-of-hooks\n      useViewTransitionState(path) &&\n      viewTransition === true;\n\n    let toPathname = navigator.encodeLocation\n      ? navigator.encodeLocation(path).pathname\n      : path.pathname;\n    let locationPathname = location.pathname;\n    let nextLocationPathname =\n      routerState && routerState.navigation && routerState.navigation.location\n        ? routerState.navigation.location.pathname\n        : null;\n\n    if (!caseSensitive) {\n      locationPathname = locationPathname.toLowerCase();\n      nextLocationPathname = nextLocationPathname\n        ? nextLocationPathname.toLowerCase()\n        : null;\n      toPathname = toPathname.toLowerCase();\n    }\n\n    if (nextLocationPathname && basename) {\n      nextLocationPathname =\n        stripBasename(nextLocationPathname, basename) || nextLocationPathname;\n    }\n\n    // If the `to` has a trailing slash, look at that exact spot.  Otherwise,\n    // we're looking for a slash _after_ what's in `to`.  For example:\n    //\n    // <NavLink to=\"/users\"> and <NavLink to=\"/users/\">\n    // both want to look for a / at index 6 to match URL `/users/matt`\n    const endSlashPosition =\n      toPathname !== \"/\" && toPathname.endsWith(\"/\")\n        ? toPathname.length - 1\n        : toPathname.length;\n    let isActive =\n      locationPathname === toPathname ||\n      (!end &&\n        locationPathname.startsWith(toPathname) &&\n        locationPathname.charAt(endSlashPosition) === \"/\");\n\n    let isPending =\n      nextLocationPathname != null &&\n      (nextLocationPathname === toPathname ||\n        (!end &&\n          nextLocationPathname.startsWith(toPathname) &&\n          nextLocationPathname.charAt(toPathname.length) === \"/\"));\n\n    let renderProps = {\n      isActive,\n      isPending,\n      isTransitioning,\n    };\n\n    let ariaCurrent = isActive ? ariaCurrentProp : undefined;\n\n    let className: string | undefined;\n    if (typeof classNameProp === \"function\") {\n      className = classNameProp(renderProps);\n    } else {\n      // If the className prop is not a function, we use a default `active`\n      // class for <NavLink />s that are active. In v5 `active` was the default\n      // value for `activeClassName`, but we are removing that API and can still\n      // use the old default behavior for a cleaner upgrade path and keep the\n      // simple styling rules working as they currently do.\n      className = [\n        classNameProp,\n        isActive ? \"active\" : null,\n        isPending ? \"pending\" : null,\n        isTransitioning ? \"transitioning\" : null,\n      ]\n        .filter(Boolean)\n        .join(\" \");\n    }\n\n    let style =\n      typeof styleProp === \"function\" ? styleProp(renderProps) : styleProp;\n\n    return (\n      <Link\n        {...rest}\n        aria-current={ariaCurrent}\n        className={className}\n        ref={ref}\n        style={style}\n        to={to}\n        viewTransition={viewTransition}\n      >\n        {typeof children === \"function\" ? children(renderProps) : children}\n      </Link>\n    );\n  },\n);\nNavLink.displayName = \"NavLink\";\n\n/**\n * Form props shared by navigations and fetchers\n */\ninterface SharedFormProps extends React.FormHTMLAttributes<HTMLFormElement> {\n  /**\n   * The HTTP verb to use when the form is submitted. Supports `\"delete\"`,\n   * `\"get\"`, `\"patch\"`, `\"post\"`, and `\"put\"`.\n   *\n   * Native [`<form>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/form)\n   * only supports `\"get\"` and `\"post\"`, avoid the other verbs if you'd like to\n   * support progressive enhancement\n   */\n  method?: HTMLFormMethod;\n\n  /**\n   * The encoding type to use for the form submission.\n   *\n   * ```tsx\n   * <Form encType=\"application/x-www-form-urlencoded\"/>  // Default\n   * <Form encType=\"multipart/form-data\"/>\n   * <Form encType=\"text/plain\"/>\n   * ```\n   */\n  encType?:\n    | \"application/x-www-form-urlencoded\"\n    | \"multipart/form-data\"\n    | \"text/plain\";\n\n  /**\n   * The URL to submit the form data to. If `undefined`, this defaults to the\n   * closest route in context.\n   */\n  action?: string;\n\n  /**\n   * Determines whether the form action is relative to the route hierarchy or\n   * the pathname. Use this if you want to opt out of navigating the route\n   * hierarchy and want to instead route based on slash-delimited URL segments.\n   * See {@link RelativeRoutingType}.\n   */\n  relative?: RelativeRoutingType;\n\n  /**\n   * Prevent the scroll position from resetting to the top of the viewport on\n   * completion of the navigation when using the\n   * {@link ScrollRestoration | `<ScrollRestoration>`} component\n   */\n  preventScrollReset?: boolean;\n\n  /**\n   * A function to call when the form is submitted. If you call\n   * [`event.preventDefault()`](https://developer.mozilla.org/en-US/docs/Web/API/Event/preventDefault)\n   * then this form will not do anything.\n   */\n  onSubmit?: React.FormEventHandler<HTMLFormElement>;\n\n  /**\n   * Specify the default revalidation behavior after this submission\n   *\n   * If no `shouldRevalidate` functions are present on the active routes, then this\n   * value will be used directly.  Otherwise it will be passed into `shouldRevalidate`\n   * so the route can make the final determination on revalidation. This can be\n   * useful when updating search params and you don't want to trigger a revalidation.\n   *\n   * By default (when not specified), loaders will revalidate according to the routers\n   * standard revalidation behavior.\n   */\n  unstable_defaultShouldRevalidate?: boolean;\n}\n\n/**\n * Form props available to fetchers\n * @category Types\n */\nexport interface FetcherFormProps extends SharedFormProps {}\n\n/**\n * Form props available to navigations\n * @category Types\n */\nexport interface FormProps extends SharedFormProps {\n  /**\n   * Defines the form [lazy route discovery](../../explanation/lazy-route-discovery) behavior.\n   *\n   * - **render** — default, discover the route when the form renders\n   * - **none** — don't eagerly discover, only discover if the form is submitted\n   *\n   * ```tsx\n   * <Form /> // default (\"render\")\n   * <Form discover=\"render\" />\n   * <Form discover=\"none\" />\n   * ```\n   */\n  discover?: DiscoverBehavior;\n\n  /**\n   * Indicates a specific fetcherKey to use when using `navigate={false}` so you\n   * can pick up the fetcher's state in a different component in a {@link useFetcher}.\n   */\n  fetcherKey?: string;\n\n  /**\n   * When `false`, skips the navigation and submits via a fetcher internally.\n   * This is essentially a shorthand for {@link useFetcher} + `<fetcher.Form>` where\n   * you don't care about the resulting data in this component.\n   */\n  navigate?: boolean;\n\n  /**\n   * Forces a full document navigation instead of client side routing and data\n   * fetch.\n   */\n  reloadDocument?: boolean;\n\n  /**\n   * Replaces the current entry in the browser [`History`](https://developer.mozilla.org/en-US/docs/Web/API/History)\n   * stack when the form navigates. Use this if you don't want the user to be\n   * able to click \"back\" to the page with the form on it.\n   */\n  replace?: boolean;\n\n  /**\n   * State object to add to the [`History`](https://developer.mozilla.org/en-US/docs/Web/API/History)\n   * stack entry for this navigation\n   */\n  state?: any;\n\n  /**\n   * Enables a [View Transition](https://developer.mozilla.org/en-US/docs/Web/API/View_Transitions_API)\n   * for this navigation. To apply specific styles during the transition, see\n   * {@link useViewTransitionState}.\n   */\n  viewTransition?: boolean;\n}\n\ntype HTMLSubmitEvent = React.BaseSyntheticEvent<\n  SubmitEvent,\n  Event,\n  HTMLFormElement\n>;\n\ntype HTMLFormSubmitter = HTMLButtonElement | HTMLInputElement;\n\n/**\n * A progressively enhanced HTML [`<form>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/form)\n * that submits data to actions via [`fetch`](https://developer.mozilla.org/en-US/docs/Web/API/fetch),\n * activating pending states in {@link useNavigation} which enables advanced\n * user interfaces beyond a basic HTML [`<form>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/form).\n * After a form's `action` completes, all data on the page is automatically\n * revalidated to keep the UI in sync with the data.\n *\n * Because it uses the HTML form API, server rendered pages are interactive at a\n * basic level before JavaScript loads. Instead of React Router managing the\n * submission, the browser manages the submission as well as the pending states\n * (like the spinning favicon). After JavaScript loads, React Router takes over\n * enabling web application user experiences.\n *\n * `Form` is most useful for submissions that should also change the URL or\n * otherwise add an entry to the browser history stack. For forms that shouldn't\n * manipulate the browser [`History`](https://developer.mozilla.org/en-US/docs/Web/API/History)\n * stack, use {@link FetcherWithComponents.Form | `<fetcher.Form>`}.\n *\n * @example\n * import { Form } from \"react-router\";\n *\n * function NewEvent() {\n *   return (\n *     <Form action=\"/events\" method=\"post\">\n *       <input name=\"title\" type=\"text\" />\n *       <input name=\"description\" type=\"text\" />\n *     </Form>\n *   );\n * }\n *\n * @public\n * @category Components\n * @mode framework\n * @mode data\n * @param {FormProps.action} action n/a\n * @param {FormProps.discover} discover n/a\n * @param {FormProps.encType} encType n/a\n * @param {FormProps.fetcherKey} fetcherKey n/a\n * @param {FormProps.method} method n/a\n * @param {FormProps.navigate} navigate n/a\n * @param {FormProps.onSubmit} onSubmit n/a\n * @param {FormProps.preventScrollReset} preventScrollReset n/a\n * @param {FormProps.relative} relative n/a\n * @param {FormProps.reloadDocument} reloadDocument n/a\n * @param {FormProps.replace} replace n/a\n * @param {FormProps.state} state n/a\n * @param {FormProps.viewTransition} viewTransition n/a\n * @param {FormProps.unstable_defaultShouldRevalidate} unstable_defaultShouldRevalidate n/a\n * @returns A progressively enhanced [`<form>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/form) component\n */\nexport const Form = React.forwardRef<HTMLFormElement, FormProps>(\n  (\n    {\n      discover = \"render\",\n      fetcherKey,\n      navigate,\n      reloadDocument,\n      replace,\n      state,\n      method = defaultMethod,\n      action,\n      onSubmit,\n      relative,\n      preventScrollReset,\n      viewTransition,\n      unstable_defaultShouldRevalidate,\n      ...props\n    },\n    forwardedRef,\n  ) => {\n    let { unstable_useTransitions } = React.useContext(NavigationContext);\n    let submit = useSubmit();\n    let formAction = useFormAction(action, { relative });\n    let formMethod: HTMLFormMethod =\n      method.toLowerCase() === \"get\" ? \"get\" : \"post\";\n    let isAbsolute =\n      typeof action === \"string\" && ABSOLUTE_URL_REGEX.test(action);\n\n    let submitHandler: React.FormEventHandler<HTMLFormElement> = (event) => {\n      onSubmit && onSubmit(event);\n      if (event.defaultPrevented) return;\n      event.preventDefault();\n\n      let submitter = (event as unknown as HTMLSubmitEvent).nativeEvent\n        .submitter as HTMLFormSubmitter | null;\n\n      let submitMethod =\n        (submitter?.getAttribute(\"formmethod\") as HTMLFormMethod | undefined) ||\n        method;\n\n      let doSubmit = () =>\n        submit(submitter || event.currentTarget, {\n          fetcherKey,\n          method: submitMethod,\n          navigate,\n          replace,\n          state,\n          relative,\n          preventScrollReset,\n          viewTransition,\n          unstable_defaultShouldRevalidate,\n        });\n\n      if (unstable_useTransitions && navigate !== false) {\n        // @ts-expect-error Needs React 19 types\n        React.startTransition(() => doSubmit());\n      } else {\n        doSubmit();\n      }\n    };\n\n    return (\n      <form\n        ref={forwardedRef}\n        method={formMethod}\n        action={formAction}\n        onSubmit={reloadDocument ? onSubmit : submitHandler}\n        {...props}\n        data-discover={\n          !isAbsolute && discover === \"render\" ? \"true\" : undefined\n        }\n      />\n    );\n  },\n);\nForm.displayName = \"Form\";\n\nexport type ScrollRestorationProps = ScriptsProps & {\n  /**\n   * A function that returns a key to use for scroll restoration. This is useful\n   * for custom scroll restoration logic, such as using only the pathname so\n   * that later navigations to prior paths will restore the scroll. Defaults to\n   * `location.key`. See {@link GetScrollRestorationKeyFunction}.\n   *\n   * ```tsx\n   * <ScrollRestoration\n   *   getKey={(location, matches) => {\n   *     // Restore based on a unique location key (default behavior)\n   *     return location.key\n   *\n   *     // Restore based on pathname\n   *     return location.pathname\n   *   }}\n   * />\n   * ```\n   */\n  getKey?: GetScrollRestorationKeyFunction;\n\n  /**\n   * The key to use for storing scroll positions in [`sessionStorage`](https://developer.mozilla.org/en-US/docs/Web/API/Window/sessionStorage).\n   * Defaults to `\"react-router-scroll-positions\"`.\n   */\n  storageKey?: string;\n};\n\n/**\n * Emulates the browser's scroll restoration on location changes. Apps should only render one of these, right before the {@link Scripts} component.\n *\n * ```tsx\n * import { ScrollRestoration } from \"react-router\";\n *\n * export default function Root() {\n *   return (\n *     <html>\n *       <body>\n *         <ScrollRestoration />\n *         <Scripts />\n *       </body>\n *     </html>\n *   );\n * }\n * ```\n *\n * This component renders an inline `<script>` to prevent scroll flashing. The `nonce` prop will be passed down to the script tag to allow CSP nonce usage.\n *\n * ```tsx\n * <ScrollRestoration nonce={cspNonce} />\n * ```\n *\n * @public\n * @category Components\n * @mode framework\n * @mode data\n * @param props Props\n * @param {ScrollRestorationProps.getKey} props.getKey n/a\n * @param {ScriptsProps.nonce} props.nonce n/a\n * @param {ScrollRestorationProps.storageKey} props.storageKey n/a\n * @returns A [`<script>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script)\n * tag that restores scroll positions on navigation.\n */\nexport function ScrollRestoration({\n  getKey,\n  storageKey,\n  ...props\n}: ScrollRestorationProps) {\n  let remixContext = React.useContext(FrameworkContext);\n  let { basename } = React.useContext(NavigationContext);\n  let location = useLocation();\n  let matches = useMatches();\n  useScrollRestoration({ getKey, storageKey });\n\n  // In order to support `getKey`, we need to compute a \"key\" here so we can\n  // hydrate that up so that SSR scroll restoration isn't waiting on React to\n  // hydrate. *However*, our key on the server is not the same as our key on\n  // the client!  So if the user's getKey implementation returns the SSR\n  // location key, then let's ignore it and let our inline <script> below pick\n  // up the client side history state key\n  let ssrKey = React.useMemo(\n    () => {\n      if (!remixContext || !getKey) return null;\n      let userKey = getScrollRestorationKey(\n        location,\n        matches,\n        basename,\n        getKey,\n      );\n      return userKey !== location.key ? userKey : null;\n    },\n    // Nah, we only need this the first time for the SSR render\n    // eslint-disable-next-line react-hooks/exhaustive-deps\n    [],\n  );\n\n  // In SPA Mode, there's nothing to restore on initial render since we didn't\n  // render anything on the server\n  if (!remixContext || remixContext.isSpaMode) {\n    return null;\n  }\n\n  let restoreScroll = ((storageKey: string, restoreKey: string) => {\n    if (!window.history.state || !window.history.state.key) {\n      let key = Math.random().toString(32).slice(2);\n      window.history.replaceState({ key }, \"\");\n    }\n    try {\n      let positions = JSON.parse(sessionStorage.getItem(storageKey) || \"{}\");\n      let storedY = positions[restoreKey || window.history.state.key];\n      if (typeof storedY === \"number\") {\n        window.scrollTo(0, storedY);\n      }\n    } catch (error: unknown) {\n      console.error(error);\n      sessionStorage.removeItem(storageKey);\n    }\n  }).toString();\n\n  return (\n    <script\n      {...props}\n      suppressHydrationWarning\n      dangerouslySetInnerHTML={{\n        __html: `(${restoreScroll})(${escapeHtml(\n          JSON.stringify(storageKey || SCROLL_RESTORATION_STORAGE_KEY),\n        )}, ${escapeHtml(JSON.stringify(ssrKey))})`,\n      }}\n    />\n  );\n}\nScrollRestoration.displayName = \"ScrollRestoration\";\n//#endregion\n\n////////////////////////////////////////////////////////////////////////////////\n//#region Hooks\n////////////////////////////////////////////////////////////////////////////////\n\nenum DataRouterHook {\n  UseScrollRestoration = \"useScrollRestoration\",\n  UseSubmit = \"useSubmit\",\n  UseSubmitFetcher = \"useSubmitFetcher\",\n  UseFetcher = \"useFetcher\",\n  useViewTransitionState = \"useViewTransitionState\",\n}\n\nenum DataRouterStateHook {\n  UseFetcher = \"useFetcher\",\n  UseFetchers = \"useFetchers\",\n  UseScrollRestoration = \"useScrollRestoration\",\n}\n\n// Internal hooks\n\nfunction getDataRouterConsoleError(\n  hookName: DataRouterHook | DataRouterStateHook,\n) {\n  return `${hookName} must be used within a data router.  See https://reactrouter.com/en/main/routers/picking-a-router.`;\n}\n\nfunction useDataRouterContext(hookName: DataRouterHook) {\n  let ctx = React.useContext(DataRouterContext);\n  invariant(ctx, getDataRouterConsoleError(hookName));\n  return ctx;\n}\n\nfunction useDataRouterState(hookName: DataRouterStateHook) {\n  let state = React.useContext(DataRouterStateContext);\n  invariant(state, getDataRouterConsoleError(hookName));\n  return state;\n}\n\n// External hooks\n\n/**\n * Handles the click behavior for router {@link Link | `<Link>`} components.This\n * is useful if you need to create custom {@link Link | `<Link>`} components with\n * the same click behavior we use in our exported {@link Link | `<Link>`}.\n *\n * @public\n * @category Hooks\n * @param to The URL to navigate to, can be a string or a partial {@link Path}.\n * @param options Options\n * @param options.preventScrollReset Whether to prevent the scroll position from\n * being reset to the top of the viewport on completion of the navigation when\n * using the {@link ScrollRestoration} component. Defaults to `false`.\n * @param options.relative The {@link RelativeRoutingType | relative routing type}\n * to use for the link. Defaults to `\"route\"`.\n * @param options.replace Whether to replace the current [`History`](https://developer.mozilla.org/en-US/docs/Web/API/History)\n * entry instead of pushing a new one. Defaults to `false`.\n * @param options.state The state to add to the [`History`](https://developer.mozilla.org/en-US/docs/Web/API/History)\n * entry for this navigation. Defaults to `undefined`.\n * @param options.target The target attribute for the link. Defaults to `undefined`.\n * @param options.viewTransition Enables a [View Transition](https://developer.mozilla.org/en-US/docs/Web/API/View_Transitions_API)\n * for this navigation. To apply specific styles during the transition, see\n * {@link useViewTransitionState}. Defaults to `false`.\n * @param options.unstable_defaultShouldRevalidate Specify the default revalidation\n * behavior for the navigation. Defaults to `true`.\n * @param options.unstable_mask Masked location to display in the browser instead\n * of the router location. Defaults to `undefined`.\n * @param options.unstable_useTransitions Wraps the navigation in\n * [`React.startTransition`](https://react.dev/reference/react/startTransition)\n * for concurrent rendering. Defaults to `false`.\n * @returns A click handler function that can be used in a custom {@link Link} component.\n */\nexport function useLinkClickHandler<E extends Element = HTMLAnchorElement>(\n  to: To,\n  {\n    target,\n    replace: replaceProp,\n    unstable_mask,\n    state,\n    preventScrollReset,\n    relative,\n    viewTransition,\n    unstable_defaultShouldRevalidate,\n    unstable_useTransitions,\n  }: {\n    target?: React.HTMLAttributeAnchorTarget;\n    replace?: boolean;\n    unstable_mask?: To;\n    state?: any;\n    preventScrollReset?: boolean;\n    relative?: RelativeRoutingType;\n    viewTransition?: boolean;\n    unstable_defaultShouldRevalidate?: boolean;\n    unstable_useTransitions?: boolean;\n  } = {},\n): (event: React.MouseEvent<E, MouseEvent>) => void {\n  let navigate = useNavigate();\n  let location = useLocation();\n  let path = useResolvedPath(to, { relative });\n\n  return React.useCallback(\n    (event: React.MouseEvent<E, MouseEvent>) => {\n      if (shouldProcessLinkClick(event, target)) {\n        event.preventDefault();\n\n        // If the URL hasn't changed, a regular <a> will do a replace instead of\n        // a push, so do the same here unless the replace prop is explicitly set\n        let replace =\n          replaceProp !== undefined\n            ? replaceProp\n            : createPath(location) === createPath(path);\n\n        let doNavigate = () =>\n          navigate(to, {\n            replace,\n            unstable_mask,\n            state,\n            preventScrollReset,\n            relative,\n            viewTransition,\n            unstable_defaultShouldRevalidate,\n          });\n\n        if (unstable_useTransitions) {\n          // @ts-expect-error Needs React 19 types\n          React.startTransition(() => doNavigate());\n        } else {\n          doNavigate();\n        }\n      }\n    },\n    [\n      location,\n      navigate,\n      path,\n      replaceProp,\n      unstable_mask,\n      state,\n      target,\n      to,\n      preventScrollReset,\n      relative,\n      viewTransition,\n      unstable_defaultShouldRevalidate,\n      unstable_useTransitions,\n    ],\n  );\n}\n\n/**\n * Returns a tuple of the current URL's [`URLSearchParams`](https://developer.mozilla.org/en-US/docs/Web/API/URLSearchParams)\n * and a function to update them. Setting the search params causes a navigation.\n *\n * ```tsx\n * import { useSearchParams } from \"react-router\";\n *\n * export function SomeComponent() {\n *   const [searchParams, setSearchParams] = useSearchParams();\n *   // ...\n * }\n * ```\n *\n * ### `setSearchParams` function\n *\n * The second element of the tuple is a function that can be used to update the\n * search params. It accepts the same types as `defaultInit` and will cause a\n * navigation to the new URL.\n *\n * ```tsx\n * let [searchParams, setSearchParams] = useSearchParams();\n *\n * // a search param string\n * setSearchParams(\"?tab=1\");\n *\n * // a shorthand object\n * setSearchParams({ tab: \"1\" });\n *\n * // object keys can be arrays for multiple values on the key\n * setSearchParams({ brand: [\"nike\", \"reebok\"] });\n *\n * // an array of tuples\n * setSearchParams([[\"tab\", \"1\"]]);\n *\n * // a `URLSearchParams` object\n * setSearchParams(new URLSearchParams(\"?tab=1\"));\n * ```\n *\n * It also supports a function callback like React's\n * [`setState`](https://react.dev/reference/react/useState#setstate):\n *\n * ```tsx\n * setSearchParams((searchParams) => {\n *   searchParams.set(\"tab\", \"2\");\n *   return searchParams;\n * });\n * ```\n *\n * <docs-warning>The function callback version of `setSearchParams` does not support\n * the [queueing](https://react.dev/reference/react/useState#setstate-parameters)\n * logic that React's `setState` implements.  Multiple calls to `setSearchParams`\n * in the same tick will not build on the prior value.  If you need this behavior,\n * you can use `setState` manually.</docs-warning>\n *\n * ### Notes\n *\n * Note that `searchParams` is a stable reference, so you can reliably use it\n * as a dependency in React's [`useEffect`](https://react.dev/reference/react/useEffect)\n * hooks.\n *\n * ```tsx\n * useEffect(() => {\n *   console.log(searchParams.get(\"tab\"));\n * }, [searchParams]);\n * ```\n *\n * However, this also means it's mutable. If you change the object without\n * calling `setSearchParams`, its values will change between renders if some\n * other state causes the component to re-render and URL will not reflect the\n * values.\n *\n * @public\n * @category Hooks\n * @param defaultInit\n * You can initialize the search params with a default value, though it **will\n * not** change the URL on the first render.\n *\n * ```tsx\n * // a search param string\n * useSearchParams(\"?tab=1\");\n *\n * // a shorthand object\n * useSearchParams({ tab: \"1\" });\n *\n * // object keys can be arrays for multiple values on the key\n * useSearchParams({ brand: [\"nike\", \"reebok\"] });\n *\n * // an array of tuples\n * useSearchParams([[\"tab\", \"1\"]]);\n *\n * // a `URLSearchParams` object\n * useSearchParams(new URLSearchParams(\"?tab=1\"));\n * ```\n * @returns A tuple of the current [`URLSearchParams`](https://developer.mozilla.org/en-US/docs/Web/API/URLSearchParams)\n * and a function to update them.\n */\nexport function useSearchParams(\n  defaultInit?: URLSearchParamsInit,\n): [URLSearchParams, SetURLSearchParams] {\n  warning(\n    typeof URLSearchParams !== \"undefined\",\n    `You cannot use the \\`useSearchParams\\` hook in a browser that does not ` +\n      `support the URLSearchParams API. If you need to support Internet ` +\n      `Explorer 11, we recommend you load a polyfill such as ` +\n      `https://github.com/ungap/url-search-params.`,\n  );\n\n  let defaultSearchParamsRef = React.useRef(createSearchParams(defaultInit));\n  let hasSetSearchParamsRef = React.useRef(false);\n\n  let location = useLocation();\n  let searchParams = React.useMemo(\n    () =>\n      // Only merge in the defaults if we haven't yet called setSearchParams.\n      // Once we call that we want those to take precedence, otherwise you can't\n      // remove a param with setSearchParams({}) if it has an initial value\n      getSearchParamsForLocation(\n        location.search,\n        hasSetSearchParamsRef.current ? null : defaultSearchParamsRef.current,\n      ),\n    [location.search],\n  );\n\n  let navigate = useNavigate();\n  let setSearchParams = React.useCallback<SetURLSearchParams>(\n    (nextInit, navigateOptions) => {\n      const newSearchParams = createSearchParams(\n        typeof nextInit === \"function\"\n          ? nextInit(new URLSearchParams(searchParams))\n          : nextInit,\n      );\n      hasSetSearchParamsRef.current = true;\n      navigate(\"?\" + newSearchParams, navigateOptions);\n    },\n    [navigate, searchParams],\n  );\n\n  return [searchParams, setSearchParams];\n}\n\n/**\n *  Sets new search params and causes a navigation when called.\n *\n *  ```tsx\n *  <button\n *    onClick={() => {\n *      const params = new URLSearchParams();\n *      params.set(\"someKey\", \"someValue\");\n *      setSearchParams(params, {\n *        preventScrollReset: true,\n *      });\n *    }}\n *  />\n *  ```\n *\n *  It also supports a function for setting new search params.\n *\n *  ```tsx\n *  <button\n *    onClick={() => {\n *      setSearchParams((prev) => {\n *        prev.set(\"someKey\", \"someValue\");\n *        return prev;\n *      });\n *    }}\n *  />\n *  ```\n */\nexport type SetURLSearchParams = (\n  nextInit?:\n    | URLSearchParamsInit\n    | ((prev: URLSearchParams) => URLSearchParamsInit),\n  navigateOpts?: NavigateOptions,\n) => void;\n\n/**\n * Submits a HTML [`<form>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/form)\n * to the server without reloading the page.\n */\nexport interface SubmitFunction {\n  (\n    /**\n     * Can be multiple types of elements and objects\n     *\n     * **[`HTMLFormElement`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLFormElement)**\n     *\n     * ```tsx\n     * <Form\n     *   onSubmit={(event) => {\n     *     submit(event.currentTarget);\n     *   }}\n     * />\n     * ```\n     *\n     * **[`FormData`](https://developer.mozilla.org/en-US/docs/Web/API/FormData)**\n     *\n     * ```tsx\n     * const formData = new FormData();\n     * formData.append(\"myKey\", \"myValue\");\n     * submit(formData, { method: \"post\" });\n     * ```\n     *\n     * **Plain object that will be serialized as [`FormData`](https://developer.mozilla.org/en-US/docs/Web/API/FormData)**\n     *\n     * ```tsx\n     * submit({ myKey: \"myValue\" }, { method: \"post\" });\n     * ```\n     *\n     * **Plain object that will be serialized as JSON**\n     *\n     * ```tsx\n     * submit(\n     *   { myKey: \"myValue\" },\n     *   { method: \"post\", encType: \"application/json\" }\n     * );\n     * ```\n     */\n    target: SubmitTarget,\n\n    /**\n     * Options that override the [`<form>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/form)'s\n     * own attributes. Required when submitting arbitrary data without a backing\n     * [`<form>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/form).\n     */\n    options?: SubmitOptions,\n  ): Promise<void>;\n}\n\n/**\n * Submits a fetcher [`<form>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/form) to the server without reloading the page.\n */\nexport interface FetcherSubmitFunction {\n  (\n    /**\n     * Can be multiple types of elements and objects\n     *\n     * **[`HTMLFormElement`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLFormElement)**\n     *\n     * ```tsx\n     * <fetcher.Form\n     *   onSubmit={(event) => {\n     *     fetcher.submit(event.currentTarget);\n     *   }}\n     * />\n     * ```\n     *\n     * **[`FormData`](https://developer.mozilla.org/en-US/docs/Web/API/FormData)**\n     *\n     * ```tsx\n     * const formData = new FormData();\n     * formData.append(\"myKey\", \"myValue\");\n     * fetcher.submit(formData, { method: \"post\" });\n     * ```\n     *\n     * **Plain object that will be serialized as [`FormData`](https://developer.mozilla.org/en-US/docs/Web/API/FormData)**\n     *\n     * ```tsx\n     * fetcher.submit({ myKey: \"myValue\" }, { method: \"post\" });\n     * ```\n     *\n     * **Plain object that will be serialized as JSON**\n     *\n     * ```tsx\n     * fetcher.submit(\n     *   { myKey: \"myValue\" },\n     *   { method: \"post\", encType: \"application/json\" }\n     * );\n     * ```\n     */\n    target: SubmitTarget,\n\n    // Fetchers cannot replace or set state because they are not navigation events\n    options?: FetcherSubmitOptions,\n  ): Promise<void>;\n}\n\nlet fetcherId = 0;\nlet getUniqueFetcherId = () => `__${String(++fetcherId)}__`;\n\n/**\n * The imperative version of {@link Form | `<Form>`} that lets you submit a form\n * from code instead of a user interaction.\n *\n * @example\n * import { useSubmit } from \"react-router\";\n *\n * function SomeComponent() {\n *   const submit = useSubmit();\n *   return (\n *     <Form onChange={(event) => submit(event.currentTarget)} />\n *   );\n * }\n *\n * @public\n * @category Hooks\n * @mode framework\n * @mode data\n * @returns A function that can be called to submit a {@link Form} imperatively.\n */\nexport function useSubmit(): SubmitFunction {\n  let { router } = useDataRouterContext(DataRouterHook.UseSubmit);\n  let { basename } = React.useContext(NavigationContext);\n  let currentRouteId = useRouteId();\n\n  let routerFetch = router.fetch;\n  let routerNavigate = router.navigate;\n\n  return React.useCallback<SubmitFunction>(\n    async (target, options = {}) => {\n      let { action, method, encType, formData, body } = getFormSubmissionInfo(\n        target,\n        basename,\n      );\n\n      if (options.navigate === false) {\n        let key = options.fetcherKey || getUniqueFetcherId();\n        await routerFetch(key, currentRouteId, options.action || action, {\n          unstable_defaultShouldRevalidate:\n            options.unstable_defaultShouldRevalidate,\n          preventScrollReset: options.preventScrollReset,\n          formData,\n          body,\n          formMethod: options.method || (method as HTMLFormMethod),\n          formEncType: options.encType || (encType as FormEncType),\n          flushSync: options.flushSync,\n        });\n      } else {\n        await routerNavigate(options.action || action, {\n          unstable_defaultShouldRevalidate:\n            options.unstable_defaultShouldRevalidate,\n          preventScrollReset: options.preventScrollReset,\n          formData,\n          body,\n          formMethod: options.method || (method as HTMLFormMethod),\n          formEncType: options.encType || (encType as FormEncType),\n          replace: options.replace,\n          state: options.state,\n          fromRouteId: currentRouteId,\n          flushSync: options.flushSync,\n          viewTransition: options.viewTransition,\n        });\n      }\n    },\n    [routerFetch, routerNavigate, basename, currentRouteId],\n  );\n}\n\n// v7: Eventually we should deprecate this entirely in favor of using the\n// router method directly?\n/**\n * Resolves the URL to the closest route in the component hierarchy instead of\n * the current URL of the app.\n *\n * This is used internally by {@link Form} to resolve the `action` to the closest\n * route, but can be used generically as well.\n *\n * @example\n * import { useFormAction } from \"react-router\";\n *\n * function SomeComponent() {\n *   // closest route URL\n *   let action = useFormAction();\n *\n *   // closest route URL + \"destroy\"\n *   let destroyAction = useFormAction(\"destroy\");\n * }\n *\n * @public\n * @category Hooks\n * @mode framework\n * @mode data\n * @param action The action to append to the closest route URL. Defaults to the\n * closest route URL.\n * @param options Options\n * @param options.relative The relative routing type to use when resolving the\n * action. Defaults to `\"route\"`.\n * @returns The resolved action URL.\n */\nexport function useFormAction(\n  action?: string,\n  { relative }: { relative?: RelativeRoutingType } = {},\n): string {\n  let { basename } = React.useContext(NavigationContext);\n  let routeContext = React.useContext(RouteContext);\n  invariant(routeContext, \"useFormAction must be used inside a RouteContext\");\n\n  let [match] = routeContext.matches.slice(-1);\n  // Shallow clone path so we can modify it below, otherwise we modify the\n  // object referenced by useMemo inside useResolvedPath\n  let path = { ...useResolvedPath(action ? action : \".\", { relative }) };\n\n  // If no action was specified, browsers will persist current search params\n  // when determining the path, so match that behavior\n  // https://github.com/remix-run/remix/issues/927\n  let location = useLocation();\n  if (action == null) {\n    // Safe to write to this directly here since if action was undefined, we\n    // would have called useResolvedPath(\".\") which will never include a search\n    path.search = location.search;\n\n    // When grabbing search params from the URL, remove any included ?index param\n    // since it might not apply to our contextual route.  We add it back based\n    // on match.route.index below\n    let params = new URLSearchParams(path.search);\n    let indexValues = params.getAll(\"index\");\n    let hasNakedIndexParam = indexValues.some((v) => v === \"\");\n    if (hasNakedIndexParam) {\n      params.delete(\"index\");\n      indexValues.filter((v) => v).forEach((v) => params.append(\"index\", v));\n      let qs = params.toString();\n      path.search = qs ? `?${qs}` : \"\";\n    }\n  }\n\n  if ((!action || action === \".\") && match.route.index) {\n    path.search = path.search\n      ? path.search.replace(/^\\?/, \"?index&\")\n      : \"?index\";\n  }\n\n  // If we're operating within a basename, prepend it to the pathname prior\n  // to creating the form action.  If this is a root navigation, then just use\n  // the raw basename which allows the basename to have full control over the\n  // presence of a trailing slash on root actions\n  if (basename !== \"/\") {\n    path.pathname =\n      path.pathname === \"/\" ? basename : joinPaths([basename, path.pathname]);\n  }\n\n  return createPath(path);\n}\n\n/**\n * The return value {@link useFetcher} that keeps track of the state of a fetcher.\n *\n * ```tsx\n * let fetcher = useFetcher();\n * ```\n */\nexport type FetcherWithComponents<TData> = Fetcher<TData> & {\n  /**\n   * Just like {@link Form} except it doesn't cause a navigation.\n   *\n   * ```tsx\n   * function SomeComponent() {\n   *   const fetcher = useFetcher()\n   *   return (\n   *     <fetcher.Form method=\"post\" action=\"/some/route\">\n   *       <input type=\"text\" />\n   *     </fetcher.Form>\n   *   )\n   * }\n   * ```\n   */\n  Form: React.ForwardRefExoticComponent<\n    FetcherFormProps & React.RefAttributes<HTMLFormElement>\n  >;\n\n  /**\n   * Loads data from a route. Useful for loading data imperatively inside user\n   * events outside a normal button or form, like a combobox or search input.\n   *\n   * ```tsx\n   * let fetcher = useFetcher()\n   *\n   * <input onChange={e => {\n   *   fetcher.load(`/search?q=${e.target.value}`)\n   * }} />\n   * ```\n   */\n  load: (\n    href: string,\n    opts?: {\n      /**\n       * Wraps the initial state update for this `fetcher.load` in a\n       * [`ReactDOM.flushSync`](https://react.dev/reference/react-dom/flushSync)\n       * call instead of the default [`React.startTransition`](https://react.dev/reference/react/startTransition).\n       * This allows you to perform synchronous DOM actions immediately after the\n       * update is flushed to the DOM.\n       */\n      flushSync?: boolean;\n    },\n  ) => Promise<void>;\n\n  /**\n   * Reset a fetcher back to an empty/idle state.\n   *\n   * If the fetcher is currently in-flight, the\n   * [`AbortController`](https://developer.mozilla.org/en-US/docs/Web/API/AbortController)\n   * will be aborted with the `reason`, if provided.\n   *\n   * @param reason Optional `reason` to provide to [`AbortController.abort()`](https://developer.mozilla.org/en-US/docs/Web/API/AbortController/abort)\n   * @returns void\n   */\n  reset: (opts?: { reason?: unknown }) => void;\n\n  /**\n   *  Submits form data to a route. While multiple nested routes can match a URL, only the leaf route will be called.\n   *\n   *  The `formData` can be multiple types:\n   *\n   *  - [`FormData`](https://developer.mozilla.org/en-US/docs/Web/API/FormData)\n   *    A `FormData` instance.\n   *  - [`HTMLFormElement`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLFormElement)\n   *    A [`<form>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/form) DOM element.\n   *  - `Object`\n   *    An object of key/value-pairs that will be converted to a [`FormData`](https://developer.mozilla.org/en-US/docs/Web/API/FormData)\n   *    instance by default. You can pass a more complex object and serialize it\n   *    as JSON by specifying `encType: \"application/json\"`. See\n   *    {@link useSubmit} for more details.\n   *\n   *  If the method is `GET`, then the route [`loader`](../../start/framework/route-module#loader)\n   *  is being called and with the `formData` serialized to the url as [`URLSearchParams`](https://developer.mozilla.org/en-US/docs/Web/API/URLSearchParams).\n   *  If `DELETE`, `PATCH`, `POST`, or `PUT`, then the route [`action`](../../start/framework/route-module#action)\n   *  is being called with `formData` as the body.\n   *\n   *  ```tsx\n   *  // Submit a FormData instance (GET request)\n   *  const formData = new FormData();\n   *  fetcher.submit(formData);\n   *\n   *  // Submit the HTML form element\n   *  fetcher.submit(event.currentTarget.form, {\n   *    method: \"POST\",\n   *  });\n   *\n   *  // Submit key/value JSON as a FormData instance\n   *  fetcher.submit(\n   *    { serialized: \"values\" },\n   *    { method: \"POST\" }\n   *  );\n   *\n   *  // Submit raw JSON\n   *  fetcher.submit(\n   *    {\n   *      deeply: {\n   *        nested: {\n   *          json: \"values\",\n   *        },\n   *      },\n   *    },\n   *    {\n   *      method: \"POST\",\n   *      encType: \"application/json\",\n   *    }\n   *  );\n   *  ```\n   */\n  submit: FetcherSubmitFunction;\n};\n\n// TODO: (v7) Change the useFetcher generic default from `any` to `unknown`\n\n/**\n * Useful for creating complex, dynamic user interfaces that require multiple,\n * concurrent data interactions without causing a navigation.\n *\n * Fetchers track their own, independent state and can be used to load data, submit\n * forms, and generally interact with [`action`](../../start/framework/route-module#action)\n * and [`loader`](../../start/framework/route-module#loader) functions.\n *\n * @example\n * import { useFetcher } from \"react-router\"\n *\n * function SomeComponent() {\n *   let fetcher = useFetcher()\n *\n *   // states are available on the fetcher\n *   fetcher.state // \"idle\" | \"loading\" | \"submitting\"\n *   fetcher.data // the data returned from the action or loader\n *\n *   // render a form\n *   <fetcher.Form method=\"post\" />\n *\n *   // load data\n *   fetcher.load(\"/some/route\")\n *\n *   // submit data\n *   fetcher.submit(someFormRef, { method: \"post\" })\n *   fetcher.submit(someData, {\n *     method: \"post\",\n *     encType: \"application/json\"\n *   })\n *\n *   // reset fetcher\n *   fetcher.reset()\n * }\n *\n * @public\n * @category Hooks\n * @mode framework\n * @mode data\n * @param options Options\n * @param options.key A unique key to identify the fetcher.\n *\n *\n * By default, `useFetcher` generates a unique fetcher scoped to that component.\n * If you want to identify a fetcher with your own key such that you can access\n * it from elsewhere in your app, you can do that with the `key` option:\n *\n * ```tsx\n * function SomeComp() {\n *   let fetcher = useFetcher({ key: \"my-key\" })\n *   // ...\n * }\n *\n * // Somewhere else\n * function AnotherComp() {\n *   // this will be the same fetcher, sharing the state across the app\n *   let fetcher = useFetcher({ key: \"my-key\" });\n *   // ...\n * }\n * ```\n * @returns A {@link FetcherWithComponents} object that contains the fetcher's state, data, and components for submitting forms and loading data.\n */\nexport function useFetcher<T = any>({\n  key,\n}: {\n  key?: string;\n} = {}): FetcherWithComponents<SerializeFrom<T>> {\n  let { router } = useDataRouterContext(DataRouterHook.UseFetcher);\n  let state = useDataRouterState(DataRouterStateHook.UseFetcher);\n  let fetcherData = React.useContext(FetchersContext);\n  let route = React.useContext(RouteContext);\n  let routeId = route.matches[route.matches.length - 1]?.route.id;\n\n  invariant(fetcherData, `useFetcher must be used inside a FetchersContext`);\n  invariant(route, `useFetcher must be used inside a RouteContext`);\n  invariant(\n    routeId != null,\n    `useFetcher can only be used on routes that contain a unique \"id\"`,\n  );\n\n  // Fetcher key handling\n  let defaultKey = React.useId();\n  let [fetcherKey, setFetcherKey] = React.useState<string>(key || defaultKey);\n  if (key && key !== fetcherKey) {\n    setFetcherKey(key);\n  }\n\n  let { deleteFetcher, getFetcher, resetFetcher, fetch: routerFetch } = router;\n\n  // Registration/cleanup\n  React.useEffect(() => {\n    getFetcher(fetcherKey);\n    return () => deleteFetcher(fetcherKey);\n  }, [deleteFetcher, getFetcher, fetcherKey]);\n\n  // Fetcher additions\n  let load = React.useCallback(\n    async (href: string, opts?: { flushSync?: boolean }) => {\n      invariant(routeId, \"No routeId available for fetcher.load()\");\n      await routerFetch(fetcherKey, routeId, href, opts);\n    },\n    [fetcherKey, routeId, routerFetch],\n  );\n\n  let submitImpl = useSubmit();\n  let submit = React.useCallback<FetcherSubmitFunction>(\n    async (target, opts) => {\n      await submitImpl(target, {\n        ...opts,\n        navigate: false,\n        fetcherKey,\n      });\n    },\n    [fetcherKey, submitImpl],\n  );\n\n  let reset = React.useCallback<FetcherWithComponents<T>[\"reset\"]>(\n    (opts) => resetFetcher(fetcherKey, opts),\n    [resetFetcher, fetcherKey],\n  );\n\n  let FetcherForm = React.useMemo(() => {\n    let FetcherForm = React.forwardRef<HTMLFormElement, FetcherFormProps>(\n      (props, ref) => {\n        return (\n          <Form {...props} navigate={false} fetcherKey={fetcherKey} ref={ref} />\n        );\n      },\n    );\n    FetcherForm.displayName = \"fetcher.Form\";\n    return FetcherForm;\n  }, [fetcherKey]);\n\n  // Exposed FetcherWithComponents\n  let fetcher = state.fetchers.get(fetcherKey) || IDLE_FETCHER;\n  let data = fetcherData.get(fetcherKey);\n  let fetcherWithComponents = React.useMemo(\n    () => ({\n      Form: FetcherForm,\n      submit,\n      load,\n      reset,\n      ...fetcher,\n      data,\n    }),\n    [FetcherForm, submit, load, reset, fetcher, data],\n  );\n\n  return fetcherWithComponents;\n}\n\n/**\n * Returns an array of all in-flight {@link Fetcher}s. This is useful for components\n * throughout the app that didn't create the fetchers but want to use their submissions\n * to participate in optimistic UI.\n *\n * @example\n * import { useFetchers } from \"react-router\";\n *\n * function SomeComponent() {\n *   const fetchers = useFetchers();\n *   fetchers[0].formData; // FormData\n *   fetchers[0].state; // etc.\n *   // ...\n * }\n *\n * @public\n * @category Hooks\n * @mode framework\n * @mode data\n * @returns An array of all in-flight {@link Fetcher}s, each with a unique `key`\n * property.\n */\nexport function useFetchers(): (Fetcher & { key: string })[] {\n  let state = useDataRouterState(DataRouterStateHook.UseFetchers);\n  return Array.from(state.fetchers.entries()).map(([key, fetcher]) => ({\n    ...fetcher,\n    key,\n  }));\n}\n\nconst SCROLL_RESTORATION_STORAGE_KEY = \"react-router-scroll-positions\";\nlet savedScrollPositions: Record<string, number> = {};\n\nfunction getScrollRestorationKey(\n  location: Location,\n  matches: UIMatch[],\n  basename: string,\n  getKey?: GetScrollRestorationKeyFunction,\n) {\n  let key: string | null = null;\n  if (getKey) {\n    if (basename !== \"/\") {\n      key = getKey(\n        {\n          ...location,\n          pathname:\n            stripBasename(location.pathname, basename) || location.pathname,\n        },\n        matches,\n      );\n    } else {\n      key = getKey(location, matches);\n    }\n  }\n  if (key == null) {\n    key = location.key;\n  }\n  return key;\n}\n\n/**\n * When rendered inside a {@link RouterProvider}, will restore scroll positions\n * on navigations\n *\n * <!--\n * Not marked `@public` because we only export as UNSAFE_ and therefore we don't\n * maintain an .md file for this hook\n * -->\n *\n * @name UNSAFE_useScrollRestoration\n * @category Hooks\n * @mode framework\n * @mode data\n * @param options Options\n * @param options.getKey A function that returns a key to use for scroll restoration.\n * This is useful for custom scroll restoration logic, such as using only the pathname\n * so that subsequent navigations to prior paths will restore the scroll. Defaults\n * to `location.key`.\n * @param options.storageKey The key to use for storing scroll positions in\n * `sessionStorage`. Defaults to `\"react-router-scroll-positions\"`.\n * @returns {void}\n */\nexport function useScrollRestoration({\n  getKey,\n  storageKey,\n}: {\n  getKey?: GetScrollRestorationKeyFunction;\n  storageKey?: string;\n} = {}): void {\n  let { router } = useDataRouterContext(DataRouterHook.UseScrollRestoration);\n  let { restoreScrollPosition, preventScrollReset } = useDataRouterState(\n    DataRouterStateHook.UseScrollRestoration,\n  );\n  let { basename } = React.useContext(NavigationContext);\n  let location = useLocation();\n  let matches = useMatches();\n  let navigation = useNavigation();\n\n  // Trigger manual scroll restoration while we're active\n  React.useEffect(() => {\n    window.history.scrollRestoration = \"manual\";\n    return () => {\n      window.history.scrollRestoration = \"auto\";\n    };\n  }, []);\n\n  // Save positions on pagehide\n  usePageHide(\n    React.useCallback(() => {\n      if (navigation.state === \"idle\") {\n        let key = getScrollRestorationKey(location, matches, basename, getKey);\n        savedScrollPositions[key] = window.scrollY;\n      }\n      try {\n        sessionStorage.setItem(\n          storageKey || SCROLL_RESTORATION_STORAGE_KEY,\n          JSON.stringify(savedScrollPositions),\n        );\n      } catch (error) {\n        warning(\n          false,\n          `Failed to save scroll positions in sessionStorage, <ScrollRestoration /> will not work properly (${error}).`,\n        );\n      }\n      window.history.scrollRestoration = \"auto\";\n    }, [navigation.state, getKey, basename, location, matches, storageKey]),\n  );\n\n  // Read in any saved scroll locations\n  if (typeof document !== \"undefined\") {\n    // eslint-disable-next-line react-hooks/rules-of-hooks\n    React.useLayoutEffect(() => {\n      try {\n        let sessionPositions = sessionStorage.getItem(\n          storageKey || SCROLL_RESTORATION_STORAGE_KEY,\n        );\n        if (sessionPositions) {\n          savedScrollPositions = JSON.parse(sessionPositions);\n        }\n      } catch (e) {\n        // no-op, use default empty object\n      }\n    }, [storageKey]);\n\n    // Enable scroll restoration in the router\n    // eslint-disable-next-line react-hooks/rules-of-hooks\n    React.useLayoutEffect(() => {\n      let disableScrollRestoration = router?.enableScrollRestoration(\n        savedScrollPositions,\n        () => window.scrollY,\n        getKey\n          ? (location, matches) =>\n              getScrollRestorationKey(location, matches, basename, getKey)\n          : undefined,\n      );\n      return () => disableScrollRestoration && disableScrollRestoration();\n    }, [router, basename, getKey]);\n\n    // Restore scrolling when state.restoreScrollPosition changes\n    // eslint-disable-next-line react-hooks/rules-of-hooks\n    React.useLayoutEffect(() => {\n      // Explicit false means don't do anything (used for submissions or revalidations)\n      if (restoreScrollPosition === false) {\n        return;\n      }\n\n      // been here before, scroll to it\n      if (typeof restoreScrollPosition === \"number\") {\n        window.scrollTo(0, restoreScrollPosition);\n        return;\n      }\n\n      // try to scroll to the hash\n      try {\n        if (location.hash) {\n          let el = document.getElementById(\n            decodeURIComponent(location.hash.slice(1)),\n          );\n          if (el) {\n            el.scrollIntoView();\n            return;\n          }\n        }\n      } catch {\n        warning(\n          false,\n          `\"${location.hash.slice(\n            1,\n          )}\" is not a decodable element ID. The view will not scroll to it.`,\n        );\n      }\n\n      // Don't reset if this navigation opted out\n      if (preventScrollReset === true) {\n        return;\n      }\n\n      // otherwise go to the top on new locations\n      window.scrollTo(0, 0);\n    }, [location, restoreScrollPosition, preventScrollReset]);\n  }\n}\n\n/**\n * Set up a callback to be fired on [Window's `beforeunload` event](https://developer.mozilla.org/en-US/docs/Web/API/Window/beforeunload_event).\n *\n * @public\n * @category Hooks\n * @param callback The callback to be called when the [`beforeunload` event](https://developer.mozilla.org/en-US/docs/Web/API/Window/beforeunload_event)\n * is fired.\n * @param options Options\n * @param options.capture If `true`, the event will be captured during the capture\n * phase. Defaults to `false`.\n * @returns {void}\n */\nexport function useBeforeUnload(\n  callback: (event: BeforeUnloadEvent) => any,\n  options?: { capture?: boolean },\n): void {\n  let { capture } = options || {};\n  React.useEffect(() => {\n    let opts = capture != null ? { capture } : undefined;\n    window.addEventListener(\"beforeunload\", callback, opts);\n    return () => {\n      window.removeEventListener(\"beforeunload\", callback, opts);\n    };\n  }, [callback, capture]);\n}\n\n/*\n * Setup a callback to be fired on the window's `pagehide` event. This is\n * useful for saving some data to `window.localStorage` just before the page\n * refreshes.  This event is better supported than beforeunload across browsers.\n *\n * Note: The `callback` argument should be a function created with\n * `React.useCallback()`.\n */\nfunction usePageHide(\n  callback: (event: PageTransitionEvent) => any,\n  options?: { capture?: boolean },\n): void {\n  let { capture } = options || {};\n  React.useEffect(() => {\n    let opts = capture != null ? { capture } : undefined;\n    window.addEventListener(\"pagehide\", callback, opts);\n    return () => {\n      window.removeEventListener(\"pagehide\", callback, opts);\n    };\n  }, [callback, capture]);\n}\n\n/**\n * Wrapper around {@link useBlocker} to show a [`window.confirm`](https://developer.mozilla.org/en-US/docs/Web/API/Window/confirm)\n * prompt to users instead of building a custom UI with {@link useBlocker}.\n *\n * The `unstable_` flag will not be removed because this technique has a lot of\n * rough edges and behaves very differently (and incorrectly sometimes) across\n * browsers if users click addition back/forward navigations while the\n * confirmation is open. Use at your own risk.\n *\n * @example\n * function ImportantForm() {\n *   let [value, setValue] = React.useState(\"\");\n *\n *   // Block navigating elsewhere when data has been entered into the input\n *   unstable_usePrompt({\n *     message: \"Are you sure?\",\n *     when: ({ currentLocation, nextLocation }) =>\n *       value !== \"\" &&\n *       currentLocation.pathname !== nextLocation.pathname,\n *   });\n *\n *   return (\n *     <Form method=\"post\">\n *       <label>\n *         Enter some important data:\n *         <input\n *           name=\"data\"\n *           value={value}\n *           onChange={(e) => setValue(e.target.value)}\n *         />\n *       </label>\n *       <button type=\"submit\">Save</button>\n *     </Form>\n *   );\n * }\n *\n * @name unstable_usePrompt\n * @public\n * @category Hooks\n * @mode framework\n * @mode data\n * @param options Options\n * @param options.message The message to show in the confirmation dialog.\n * @param options.when A boolean or a function that returns a boolean indicating\n * whether to block the navigation. If a function is provided, it will receive an\n * object with `currentLocation` and `nextLocation` properties.\n * @returns {void}\n */\nexport function usePrompt({\n  when,\n  message,\n}: {\n  when: boolean | BlockerFunction;\n  message: string;\n}): void {\n  let blocker = useBlocker(when);\n\n  React.useEffect(() => {\n    if (blocker.state === \"blocked\") {\n      let proceed = window.confirm(message);\n      if (proceed) {\n        // This timeout is needed to avoid a weird \"race\" on POP navigations\n        // between the `window.history` revert navigation and the result of\n        // `window.confirm`\n        setTimeout(blocker.proceed, 0);\n      } else {\n        blocker.reset();\n      }\n    }\n  }, [blocker, message]);\n\n  React.useEffect(() => {\n    if (blocker.state === \"blocked\" && !when) {\n      blocker.reset();\n    }\n  }, [blocker, when]);\n}\n\n/**\n * This hook returns `true` when there is an active [View Transition](https://developer.mozilla.org/en-US/docs/Web/API/View_Transitions_API)\n * to the specified location. This can be used to apply finer-grained styles to\n * elements to further customize the view transition. This requires that view\n * transitions have been enabled for the given navigation via {@link LinkProps.viewTransition}\n * (or the `Form`, `submit`, or `navigate` call)\n *\n * @public\n * @category Hooks\n * @mode framework\n * @mode data\n * @param to The {@link To} location to check for an active [View Transition](https://developer.mozilla.org/en-US/docs/Web/API/View_Transitions_API).\n * @param options Options\n * @param options.relative The relative routing type to use when resolving the\n * `to` location, defaults to `\"route\"`. See {@link RelativeRoutingType} for\n * more details.\n * @returns `true` if there is an active [View Transition](https://developer.mozilla.org/en-US/docs/Web/API/View_Transitions_API)\n * to the specified {@link Location}, otherwise `false`.\n */\nexport function useViewTransitionState(\n  to: To,\n  { relative }: { relative?: RelativeRoutingType } = {},\n) {\n  let vtContext = React.useContext(ViewTransitionContext);\n\n  invariant(\n    vtContext != null,\n    \"`useViewTransitionState` must be used within `react-router-dom`'s `RouterProvider`.  \" +\n      \"Did you accidentally import `RouterProvider` from `react-router`?\",\n  );\n\n  let { basename } = useDataRouterContext(\n    DataRouterHook.useViewTransitionState,\n  );\n  let path = useResolvedPath(to, { relative });\n  if (!vtContext.isTransitioning) {\n    return false;\n  }\n\n  let currentPath =\n    stripBasename(vtContext.currentLocation.pathname, basename) ||\n    vtContext.currentLocation.pathname;\n  let nextPath =\n    stripBasename(vtContext.nextLocation.pathname, basename) ||\n    vtContext.nextLocation.pathname;\n\n  // Transition is active if we're going to or coming from the indicated\n  // destination.  This ensures that other PUSH navigations that reverse\n  // an indicated transition apply.  I.e., on the list view you have:\n  //\n  //   <NavLink to=\"/details/1\" viewTransition>\n  //\n  // If you click the breadcrumb back to the list view:\n  //\n  //   <NavLink to=\"/list\" viewTransition>\n  //\n  // We should apply the transition because it's indicated as active going\n  // from /list -> /details/1 and therefore should be active on the reverse\n  // (even though this isn't strictly a POP reverse)\n  return (\n    matchPath(path.pathname, nextPath) != null ||\n    matchPath(path.pathname, currentPath) != null\n  );\n}\n\n//#endregion\n"
  },
  {
    "path": "packages/react-router/lib/dom/node-main.js",
    "content": "/* eslint-env node */\n\nif (process.env.NODE_ENV === \"production\") {\n  module.exports = require(\"./umd/react-router-dom.production.min.js\");\n} else {\n  module.exports = require(\"./umd/react-router-dom.development.js\");\n}\n"
  },
  {
    "path": "packages/react-router/lib/dom/server.tsx",
    "content": "import * as React from \"react\";\n\nimport type { Location, Path, To } from \"../router/history\";\nimport {\n  Action as NavigationType,\n  createPath,\n  invariant,\n  parsePath,\n} from \"../router/history\";\nimport type {\n  FutureConfig,\n  Router as DataRouter,\n  RevalidationState,\n  CreateStaticHandlerOptions as RouterCreateStaticHandlerOptions,\n  StaticHandlerContext,\n} from \"../router/router\";\nimport {\n  IDLE_BLOCKER,\n  IDLE_FETCHER,\n  IDLE_NAVIGATION,\n  createStaticHandler as routerCreateStaticHandler,\n} from \"../router/router\";\nimport type { RouteManifest } from \"../router/utils\";\nimport {\n  convertRoutesToDataRoutes,\n  isRouteErrorResponse,\n} from \"../router/utils\";\nimport { DataRoutes, Router, mapRouteProperties } from \"../components\";\nimport type { RouteObject } from \"../context\";\nimport {\n  DataRouterContext,\n  DataRouterStateContext,\n  FetchersContext,\n  ViewTransitionContext,\n} from \"../context\";\nimport { escapeHtml } from \"./ssr/markup\";\n\n/**\n * @category Types\n */\nexport interface StaticRouterProps {\n  /**\n   * The base URL for the static router (default: `/`)\n   */\n  basename?: string;\n  /**\n   * The child elements to render inside the static router\n   */\n  children?: React.ReactNode;\n  /**\n   * The {@link Location} to render the static router at (default: `/`)\n   */\n  location: Partial<Location> | string;\n}\n\n/**\n * A {@link Router | `<Router>`} that may not navigate to any other {@link Location}.\n * This is useful on the server where there is no stateful UI.\n *\n * @public\n * @category Declarative Routers\n * @mode declarative\n * @param props Props\n * @param {StaticRouterProps.basename} props.basename n/a\n * @param {StaticRouterProps.children} props.children n/a\n * @param {StaticRouterProps.location} props.location n/a\n * @returns A React element that renders the static {@link Router | `<Router>`}\n */\nexport function StaticRouter({\n  basename,\n  children,\n  location: locationProp = \"/\",\n}: StaticRouterProps) {\n  if (typeof locationProp === \"string\") {\n    locationProp = parsePath(locationProp);\n  }\n\n  let action = NavigationType.Pop;\n  let location: Location = {\n    pathname: locationProp.pathname || \"/\",\n    search: locationProp.search || \"\",\n    hash: locationProp.hash || \"\",\n    state: locationProp.state != null ? locationProp.state : null,\n    key: locationProp.key || \"default\",\n    unstable_mask: undefined,\n  };\n\n  let staticNavigator = getStatelessNavigator();\n  return (\n    <Router\n      basename={basename}\n      children={children}\n      location={location}\n      navigationType={action}\n      navigator={staticNavigator}\n      static={true}\n      unstable_useTransitions={false}\n    />\n  );\n}\n\n/**\n * @category Types\n */\nexport interface StaticRouterProviderProps {\n  /**\n   * The {@link StaticHandlerContext} returned from {@link StaticHandler}'s\n   * `query`\n   */\n  context: StaticHandlerContext;\n  /**\n   * The static {@link DataRouter} from {@link createStaticRouter}\n   */\n  router: DataRouter;\n  /**\n   * Whether to hydrate the router on the client (default `true`)\n   */\n  hydrate?: boolean;\n  /**\n   * The [`nonce`](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Global_attributes/nonce)\n   * to use for the hydration [`<script>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script)\n   * tag\n   */\n  nonce?: string;\n}\n\n/**\n * A {@link DataRouter} that may not navigate to any other {@link Location}.\n * This is useful on the server where there is no stateful UI.\n *\n * @example\n * export async function handleRequest(request: Request) {\n *   let { query, dataRoutes } = createStaticHandler(routes);\n *   let context = await query(request));\n *\n *   if (context instanceof Response) {\n *     return context;\n *   }\n *\n *   let router = createStaticRouter(dataRoutes, context);\n *   return new Response(\n *     ReactDOMServer.renderToString(<StaticRouterProvider ... />),\n *     { headers: { \"Content-Type\": \"text/html\" } }\n *   );\n * }\n *\n * @public\n * @category Data Routers\n * @mode data\n * @param props Props\n * @param {StaticRouterProviderProps.context} props.context n/a\n * @param {StaticRouterProviderProps.hydrate} props.hydrate n/a\n * @param {StaticRouterProviderProps.nonce} props.nonce n/a\n * @param {StaticRouterProviderProps.router} props.router n/a\n * @returns A React element that renders the static router provider\n */\nexport function StaticRouterProvider({\n  context,\n  router,\n  hydrate = true,\n  nonce,\n}: StaticRouterProviderProps) {\n  invariant(\n    router && context,\n    \"You must provide `router` and `context` to <StaticRouterProvider>\",\n  );\n\n  let dataRouterContext = {\n    router,\n    navigator: getStatelessNavigator(),\n    static: true,\n    staticContext: context,\n    basename: context.basename || \"/\",\n  };\n\n  let fetchersContext = new Map();\n\n  let hydrateScript = \"\";\n\n  if (hydrate !== false) {\n    let data = {\n      loaderData: context.loaderData,\n      actionData: context.actionData,\n      errors: serializeErrors(context.errors),\n    };\n    // Use JSON.parse here instead of embedding a raw JS object here to speed\n    // up parsing on the client.  Dual-stringify is needed to ensure all quotes\n    // are properly escaped in the resulting string.  See:\n    //   https://v8.dev/blog/cost-of-javascript-2019#json\n    let json = escapeHtml(JSON.stringify(JSON.stringify(data)));\n    hydrateScript = `window.__staticRouterHydrationData = JSON.parse(${json});`;\n  }\n\n  let { state } = dataRouterContext.router;\n\n  return (\n    <>\n      <DataRouterContext.Provider value={dataRouterContext}>\n        <DataRouterStateContext.Provider value={state}>\n          <FetchersContext.Provider value={fetchersContext}>\n            <ViewTransitionContext.Provider value={{ isTransitioning: false }}>\n              <Router\n                basename={dataRouterContext.basename}\n                location={state.location}\n                navigationType={state.historyAction}\n                navigator={dataRouterContext.navigator}\n                static={dataRouterContext.static}\n                unstable_useTransitions={false}\n              >\n                <DataRoutes\n                  routes={router.routes}\n                  future={router.future}\n                  state={state}\n                  isStatic={true}\n                />\n              </Router>\n            </ViewTransitionContext.Provider>\n          </FetchersContext.Provider>\n        </DataRouterStateContext.Provider>\n      </DataRouterContext.Provider>\n      {hydrateScript ? (\n        <script\n          suppressHydrationWarning\n          nonce={nonce}\n          dangerouslySetInnerHTML={{ __html: hydrateScript }}\n        />\n      ) : null}\n    </>\n  );\n}\n\nfunction serializeErrors(\n  errors: StaticHandlerContext[\"errors\"],\n): StaticHandlerContext[\"errors\"] {\n  if (!errors) return null;\n  let entries = Object.entries(errors);\n  let serialized: StaticHandlerContext[\"errors\"] = {};\n  for (let [key, val] of entries) {\n    // Hey you!  If you change this, please change the corresponding logic in\n    // deserializeErrors in react-router-dom/index.tsx :)\n    if (isRouteErrorResponse(val)) {\n      serialized[key] = { ...val, __type: \"RouteErrorResponse\" };\n    } else if (val instanceof Error) {\n      // Do not serialize stack traces from SSR for security reasons\n      serialized[key] = {\n        message: val.message,\n        __type: \"Error\",\n        // If this is a subclass (i.e., ReferenceError), send up the type so we\n        // can re-create the same type during hydration.\n        ...(val.name !== \"Error\"\n          ? {\n              __subType: val.name,\n            }\n          : {}),\n      };\n    } else {\n      serialized[key] = val;\n    }\n  }\n  return serialized;\n}\n\nfunction getStatelessNavigator() {\n  return {\n    createHref,\n    encodeLocation,\n    push(to: To) {\n      throw new Error(\n        `You cannot use navigator.push() on the server because it is a stateless ` +\n          `environment. This error was probably triggered when you did a ` +\n          `\\`navigate(${JSON.stringify(to)})\\` somewhere in your app.`,\n      );\n    },\n    replace(to: To) {\n      throw new Error(\n        `You cannot use navigator.replace() on the server because it is a stateless ` +\n          `environment. This error was probably triggered when you did a ` +\n          `\\`navigate(${JSON.stringify(to)}, { replace: true })\\` somewhere ` +\n          `in your app.`,\n      );\n    },\n    go(delta: number) {\n      throw new Error(\n        `You cannot use navigator.go() on the server because it is a stateless ` +\n          `environment. This error was probably triggered when you did a ` +\n          `\\`navigate(${delta})\\` somewhere in your app.`,\n      );\n    },\n    back() {\n      throw new Error(\n        `You cannot use navigator.back() on the server because it is a stateless ` +\n          `environment.`,\n      );\n    },\n    forward() {\n      throw new Error(\n        `You cannot use navigator.forward() on the server because it is a stateless ` +\n          `environment.`,\n      );\n    },\n  };\n}\n\ntype CreateStaticHandlerOptions = Omit<\n  RouterCreateStaticHandlerOptions,\n  \"mapRouteProperties\"\n>;\n\n/**\n * Create a static handler to perform server-side data loading\n *\n * @example\n * export async function handleRequest(request: Request) {\n *   let { query, dataRoutes } = createStaticHandler(routes);\n *   let context = await query(request);\n *\n *   if (context instanceof Response) {\n *     return context;\n *   }\n *\n *   let router = createStaticRouter(dataRoutes, context);\n *   return new Response(\n *     ReactDOMServer.renderToString(<StaticRouterProvider ... />),\n *     { headers: { \"Content-Type\": \"text/html\" } }\n *   );\n * }\n *\n * @public\n * @category Data Routers\n * @mode data\n * @param routes The {@link RouteObject | route objects} to create a static\n * handler for\n * @param opts Options\n * @param opts.basename The base URL for the static handler (default: `/`)\n * @param opts.future Future flags for the static handler\n * @returns A static handler that can be used to query data for the provided\n * routes\n */\nexport function createStaticHandler(\n  routes: RouteObject[],\n  opts?: CreateStaticHandlerOptions,\n) {\n  return routerCreateStaticHandler(routes, {\n    ...opts,\n    mapRouteProperties,\n  });\n}\n\n/**\n * Create a static {@link DataRouter} for server-side rendering\n *\n * @example\n * export async function handleRequest(request: Request) {\n *   let { query, dataRoutes } = createStaticHandler(routes);\n *   let context = await query(request);\n *\n *   if (context instanceof Response) {\n *     return context;\n *   }\n *\n *   let router = createStaticRouter(dataRoutes, context);\n *   return new Response(\n *     ReactDOMServer.renderToString(<StaticRouterProvider ... />),\n *     { headers: { \"Content-Type\": \"text/html\" } }\n *   );\n * }\n *\n * @public\n * @category Data Routers\n * @mode data\n * @param routes The route objects to create a static {@link DataRouter} for\n * @param context The {@link StaticHandlerContext} returned from {@link StaticHandler}'s\n * `query`\n * @param opts Options\n * @param opts.future Future flags for the static {@link DataRouter}\n * @returns A static {@link DataRouter} that can be used to render the provided routes\n */\nexport function createStaticRouter(\n  routes: RouteObject[],\n  context: StaticHandlerContext,\n  opts: {\n    future?: Partial<FutureConfig>;\n  } = {},\n): DataRouter {\n  let manifest: RouteManifest = {};\n  let dataRoutes = convertRoutesToDataRoutes(\n    routes,\n    mapRouteProperties,\n    undefined,\n    manifest,\n  );\n\n  // Because our context matches may be from a framework-agnostic set of\n  // routes passed to createStaticHandler(), we update them here with our\n  // newly created/enhanced data routes\n  let matches = context.matches.map((match) => {\n    let route = manifest[match.route.id] || match.route;\n    return {\n      ...match,\n      route,\n    };\n  });\n\n  let msg = (method: string) =>\n    `You cannot use router.${method}() on the server because it is a stateless environment`;\n\n  return {\n    get basename() {\n      return context.basename;\n    },\n    get future() {\n      return {\n        v8_middleware: false,\n        ...opts?.future,\n      };\n    },\n    get state() {\n      return {\n        historyAction: NavigationType.Pop,\n        location: context.location,\n        matches,\n        loaderData: context.loaderData,\n        actionData: context.actionData,\n        errors: context.errors,\n        initialized: true,\n        renderFallback: false,\n        navigation: IDLE_NAVIGATION,\n        restoreScrollPosition: null,\n        preventScrollReset: false,\n        revalidation: \"idle\" as RevalidationState,\n        fetchers: new Map(),\n        blockers: new Map(),\n      };\n    },\n    get routes() {\n      return dataRoutes;\n    },\n    get window() {\n      return undefined;\n    },\n    initialize() {\n      throw msg(\"initialize\");\n    },\n    subscribe() {\n      throw msg(\"subscribe\");\n    },\n    enableScrollRestoration() {\n      throw msg(\"enableScrollRestoration\");\n    },\n    navigate() {\n      throw msg(\"navigate\");\n    },\n    fetch() {\n      throw msg(\"fetch\");\n    },\n    revalidate() {\n      throw msg(\"revalidate\");\n    },\n    createHref,\n    encodeLocation,\n    getFetcher() {\n      return IDLE_FETCHER;\n    },\n    deleteFetcher() {\n      throw msg(\"deleteFetcher\");\n    },\n    resetFetcher() {\n      throw msg(\"resetFetcher\");\n    },\n    dispose() {\n      throw msg(\"dispose\");\n    },\n    getBlocker() {\n      return IDLE_BLOCKER;\n    },\n    deleteBlocker() {\n      throw msg(\"deleteBlocker\");\n    },\n    patchRoutes() {\n      throw msg(\"patchRoutes\");\n    },\n    _internalFetchControllers: new Map(),\n    _internalSetRoutes() {\n      throw msg(\"_internalSetRoutes\");\n    },\n    _internalSetStateDoNotUseOrYouWillBreakYourApp() {\n      throw msg(\"_internalSetStateDoNotUseOrYouWillBreakYourApp\");\n    },\n  };\n}\n\nfunction createHref(to: To) {\n  return typeof to === \"string\" ? to : createPath(to);\n}\n\nfunction encodeLocation(to: To): Path {\n  let href = typeof to === \"string\" ? to : createPath(to);\n  // Treating this as a full URL will strip any trailing spaces so we need to\n  // pre-encode them since they might be part of a matching splat param from\n  // an ancestor route\n  href = href.replace(/ $/, \"%20\");\n  let encoded = ABSOLUTE_URL_REGEX.test(href)\n    ? new URL(href)\n    : new URL(href, \"http://localhost\");\n  return {\n    pathname: encoded.pathname,\n    search: encoded.search,\n    hash: encoded.hash,\n  };\n}\n\nconst ABSOLUTE_URL_REGEX = /^(?:[a-z][a-z0-9+.-]*:|\\/\\/)/i;\n"
  },
  {
    "path": "packages/react-router/lib/dom/ssr/components.tsx",
    "content": "import type {\n  FocusEventHandler,\n  MouseEventHandler,\n  TouchEventHandler,\n} from \"react\";\nimport * as React from \"react\";\n\nimport type { RouterState } from \"../../router/router\";\nimport type { AgnosticDataRouteMatch } from \"../../router/utils\";\nimport { matchRoutes } from \"../../router/utils\";\n\nimport type { FrameworkContextObject } from \"./entry\";\nimport invariant from \"./invariant\";\nimport {\n  getKeyedLinksForMatches,\n  getKeyedPrefetchLinks,\n  getModuleLinkHrefs,\n  getNewMatchesForLinks,\n  isPageLinkDescriptor,\n} from \"./links\";\nimport type { KeyedHtmlLinkDescriptor } from \"./links\";\nimport { escapeHtml } from \"./markup\";\nimport type {\n  MetaFunction,\n  MetaDescriptor,\n  MetaMatch,\n  MetaMatches,\n} from \"./routeModules\";\nimport { singleFetchUrl } from \"./single-fetch\";\nimport {\n  DataRouterContext,\n  DataRouterStateContext,\n  useIsRSCRouterContext,\n} from \"../../context\";\nimport { warnOnce } from \"../../server-runtime/warnings\";\nimport { useLocation } from \"../../hooks\";\nimport { getPartialManifest, isFogOfWarEnabled } from \"./fog-of-war\";\nimport type { PageLinkDescriptor } from \"../../router/links\";\n\nfunction useDataRouterContext() {\n  let context = React.useContext(DataRouterContext);\n  invariant(\n    context,\n    \"You must render this element inside a <DataRouterContext.Provider> element\",\n  );\n  return context;\n}\n\nfunction useDataRouterStateContext() {\n  let context = React.useContext(DataRouterStateContext);\n  invariant(\n    context,\n    \"You must render this element inside a <DataRouterStateContext.Provider> element\",\n  );\n  return context;\n}\n\n////////////////////////////////////////////////////////////////////////////////\n// FrameworkContext\n\nexport const FrameworkContext = React.createContext<\n  FrameworkContextObject | undefined\n>(undefined);\nFrameworkContext.displayName = \"FrameworkContext\";\n\nexport function useFrameworkContext(): FrameworkContextObject {\n  let context = React.useContext(FrameworkContext);\n  invariant(\n    context,\n    \"You must render this element inside a <HydratedRouter> element\",\n  );\n  return context;\n}\n\n////////////////////////////////////////////////////////////////////////////////\n// Public API\n\n/**\n * Defines the [lazy route discovery](../../explanation/lazy-route-discovery)\n * behavior of the link/form:\n *\n * - \"render\" - default, discover the route when the link renders\n * - \"none\" - don't eagerly discover, only discover if the link is clicked\n */\nexport type DiscoverBehavior = \"render\" | \"none\";\n\n/**\n * Defines the prefetching behavior of the link:\n *\n * - \"none\": Never fetched\n * - \"intent\": Fetched when the user focuses or hovers the link\n * - \"render\": Fetched when the link is rendered\n * - \"viewport\": Fetched when the link is in the viewport\n */\nexport type PrefetchBehavior = \"intent\" | \"render\" | \"none\" | \"viewport\";\n\ninterface PrefetchHandlers {\n  onFocus?: FocusEventHandler;\n  onBlur?: FocusEventHandler;\n  onMouseEnter?: MouseEventHandler;\n  onMouseLeave?: MouseEventHandler;\n  onTouchStart?: TouchEventHandler;\n}\n\nexport function usePrefetchBehavior<T extends HTMLAnchorElement>(\n  prefetch: PrefetchBehavior,\n  theirElementProps: PrefetchHandlers,\n): [boolean, React.RefObject<T>, PrefetchHandlers] {\n  let frameworkContext = React.useContext(FrameworkContext);\n  let [maybePrefetch, setMaybePrefetch] = React.useState(false);\n  let [shouldPrefetch, setShouldPrefetch] = React.useState(false);\n  let { onFocus, onBlur, onMouseEnter, onMouseLeave, onTouchStart } =\n    theirElementProps;\n\n  let ref = React.useRef<T>(null);\n\n  React.useEffect(() => {\n    if (prefetch === \"render\") {\n      setShouldPrefetch(true);\n    }\n\n    if (prefetch === \"viewport\") {\n      let callback: IntersectionObserverCallback = (entries) => {\n        entries.forEach((entry) => {\n          setShouldPrefetch(entry.isIntersecting);\n        });\n      };\n      let observer = new IntersectionObserver(callback, { threshold: 0.5 });\n      if (ref.current) observer.observe(ref.current);\n\n      return () => {\n        observer.disconnect();\n      };\n    }\n  }, [prefetch]);\n\n  React.useEffect(() => {\n    if (maybePrefetch) {\n      let id = setTimeout(() => {\n        setShouldPrefetch(true);\n      }, 100);\n      return () => {\n        clearTimeout(id);\n      };\n    }\n  }, [maybePrefetch]);\n\n  let setIntent = () => {\n    setMaybePrefetch(true);\n  };\n\n  let cancelIntent = () => {\n    setMaybePrefetch(false);\n    setShouldPrefetch(false);\n  };\n\n  // No prefetching if not using SSR\n  if (!frameworkContext) {\n    return [false, ref, {}];\n  }\n\n  if (prefetch !== \"intent\") {\n    return [shouldPrefetch, ref, {}];\n  }\n\n  // When using prefetch=\"intent\" we need to attach focus/hover listeners\n  return [\n    shouldPrefetch,\n    ref,\n    {\n      onFocus: composeEventHandlers(onFocus, setIntent),\n      onBlur: composeEventHandlers(onBlur, cancelIntent),\n      onMouseEnter: composeEventHandlers(onMouseEnter, setIntent),\n      onMouseLeave: composeEventHandlers(onMouseLeave, cancelIntent),\n      onTouchStart: composeEventHandlers(onTouchStart, setIntent),\n    },\n  ];\n}\n\nexport function composeEventHandlers<\n  EventType extends React.SyntheticEvent | Event,\n>(\n  theirHandler: ((event: EventType) => any) | undefined,\n  ourHandler: (event: EventType) => any,\n): (event: EventType) => any {\n  return (event) => {\n    theirHandler && theirHandler(event);\n    if (!event.defaultPrevented) {\n      ourHandler(event);\n    }\n  };\n}\n\n// Return the matches actively being displayed:\n// - In SPA Mode we only SSR/hydrate the root match, and include all matches\n//   after hydration. This lets the router handle initial match loads via lazy().\n// - When an error boundary is rendered, we slice off matches up to the\n//   boundary for <Links>/<Meta>\nfunction getActiveMatches(\n  matches: RouterState[\"matches\"],\n  errors: RouterState[\"errors\"],\n  isSpaMode: boolean,\n) {\n  if (isSpaMode && !isHydrated) {\n    return [matches[0]];\n  }\n\n  if (errors) {\n    let errorIdx = matches.findIndex((m) => errors[m.route.id] !== undefined);\n    return matches.slice(0, errorIdx + 1);\n  }\n\n  return matches;\n}\n\nexport const CRITICAL_CSS_DATA_ATTRIBUTE = \"data-react-router-critical-css\";\n\n/**\n * Props for the {@link Links} component.\n *\n * @category Types\n */\nexport interface LinksProps {\n  /**\n   * A [`nonce`](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Global_attributes/nonce)\n   * attribute to render on the [`<link>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/link)\n   * element\n   */\n  nonce?: string | undefined;\n  /**\n   * A [`crossOrigin`](https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/crossorigin)\n   * attribute to render on the [`<link>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/link)\n   * element\n   */\n  crossOrigin?: \"anonymous\" | \"use-credentials\";\n}\n\n/**\n * Renders all the [`<link>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/link)\n * tags created by the route module's [`links`](../../start/framework/route-module#links)\n * export. You should render it inside the [`<head>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/head)\n * of your document.\n *\n * @example\n * import { Links } from \"react-router\";\n *\n * export default function Root() {\n *   return (\n *     <html>\n *       <head>\n *         <Links />\n *       </head>\n *       <body></body>\n *     </html>\n *   );\n * }\n *\n * @public\n * @category Components\n * @mode framework\n * @param props Props\n * @param {LinksProps.nonce} props.nonce n/a\n * @param {LinksProps.crossOrigin} props.crossOrigin n/a\n * @returns A collection of React elements for [`<link>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/link)\n * tags\n */\nexport function Links({ nonce, crossOrigin }: LinksProps): React.JSX.Element {\n  let { isSpaMode, manifest, routeModules, criticalCss } =\n    useFrameworkContext();\n  let { errors, matches: routerMatches } = useDataRouterStateContext();\n\n  let matches = getActiveMatches(routerMatches, errors, isSpaMode);\n\n  let keyedLinks = React.useMemo(\n    () => getKeyedLinksForMatches(matches, routeModules, manifest),\n    [matches, routeModules, manifest],\n  );\n\n  return (\n    <>\n      {typeof criticalCss === \"string\" ? (\n        <style\n          {...{ [CRITICAL_CSS_DATA_ATTRIBUTE]: \"\" }}\n          nonce={nonce}\n          dangerouslySetInnerHTML={{ __html: criticalCss }}\n        />\n      ) : null}\n      {typeof criticalCss === \"object\" ? (\n        <link\n          {...{ [CRITICAL_CSS_DATA_ATTRIBUTE]: \"\" }}\n          rel=\"stylesheet\"\n          href={criticalCss.href}\n          nonce={nonce}\n          crossOrigin={crossOrigin}\n        />\n      ) : null}\n      {keyedLinks.map(({ key, link }) =>\n        isPageLinkDescriptor(link) ? (\n          <PrefetchPageLinks\n            key={key}\n            nonce={nonce}\n            {...link}\n            crossOrigin={link.crossOrigin ?? crossOrigin}\n          />\n        ) : (\n          <link\n            key={key}\n            nonce={nonce}\n            {...link}\n            crossOrigin={link.crossOrigin ?? crossOrigin}\n          />\n        ),\n      )}\n    </>\n  );\n}\n\n/**\n * Renders [`<link rel=prefetch|modulepreload>`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLLinkElement/rel)\n * tags for modules and data of another page to enable an instant navigation to\n * that page. [`<Link prefetch>`](./Link#prefetch) uses this internally, but you\n * can render it to prefetch a page for any other reason.\n *\n * For example, you may render one of this as the user types into a search field\n * to prefetch search results before they click through to their selection.\n *\n * @example\n * import { PrefetchPageLinks } from \"react-router\";\n *\n * <PrefetchPageLinks page=\"/absolute/path\" />\n *\n * @public\n * @category Components\n * @mode framework\n * @param props Props\n * @param {PageLinkDescriptor.page} props.page n/a\n * @param props.linkProps Additional props to spread onto the [`<link>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/link)\n * tags, such as [`crossOrigin`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLLinkElement/crossOrigin),\n * [`integrity`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLLinkElement/integrity),\n * [`rel`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLLinkElement/rel),\n * etc.\n * @returns A collection of React elements for [`<link>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/link)\n * tags\n */\nexport function PrefetchPageLinks({ page, ...linkProps }: PageLinkDescriptor) {\n  let { router } = useDataRouterContext();\n  let matches = React.useMemo(\n    () => matchRoutes(router.routes, page, router.basename),\n    [router.routes, page, router.basename],\n  );\n\n  if (!matches) {\n    return null;\n  }\n\n  return <PrefetchPageLinksImpl page={page} matches={matches} {...linkProps} />;\n}\n\nfunction useKeyedPrefetchLinks(matches: AgnosticDataRouteMatch[]) {\n  let { manifest, routeModules } = useFrameworkContext();\n\n  let [keyedPrefetchLinks, setKeyedPrefetchLinks] = React.useState<\n    KeyedHtmlLinkDescriptor[]\n  >([]);\n\n  React.useEffect(() => {\n    let interrupted: boolean = false;\n\n    void getKeyedPrefetchLinks(matches, manifest, routeModules).then(\n      (links) => {\n        if (!interrupted) {\n          setKeyedPrefetchLinks(links);\n        }\n      },\n    );\n\n    return () => {\n      interrupted = true;\n    };\n  }, [matches, manifest, routeModules]);\n\n  return keyedPrefetchLinks;\n}\n\nfunction PrefetchPageLinksImpl({\n  page,\n  matches: nextMatches,\n  ...linkProps\n}: PageLinkDescriptor & {\n  matches: AgnosticDataRouteMatch[];\n}) {\n  let location = useLocation();\n  let { future, manifest, routeModules } = useFrameworkContext();\n  let { basename } = useDataRouterContext();\n  let { loaderData, matches } = useDataRouterStateContext();\n\n  let newMatchesForData = React.useMemo(\n    () =>\n      getNewMatchesForLinks(\n        page,\n        nextMatches,\n        matches,\n        manifest,\n        location,\n        \"data\",\n      ),\n    [page, nextMatches, matches, manifest, location],\n  );\n\n  let newMatchesForAssets = React.useMemo(\n    () =>\n      getNewMatchesForLinks(\n        page,\n        nextMatches,\n        matches,\n        manifest,\n        location,\n        \"assets\",\n      ),\n    [page, nextMatches, matches, manifest, location],\n  );\n\n  let dataHrefs = React.useMemo(() => {\n    if (page === location.pathname + location.search + location.hash) {\n      // Because we opt-into revalidation, don't compute this for the current page\n      // since it would always trigger a prefetch of the existing loaders\n      return [];\n    }\n\n    // Single-fetch is harder :)\n    // This parallels the logic in the single fetch data strategy\n    let routesParams = new Set<string>();\n    let foundOptOutRoute = false;\n    nextMatches.forEach((m) => {\n      let manifestRoute = manifest.routes[m.route.id];\n      if (!manifestRoute || !manifestRoute.hasLoader) {\n        return;\n      }\n\n      if (\n        !newMatchesForData.some((m2) => m2.route.id === m.route.id) &&\n        m.route.id in loaderData &&\n        routeModules[m.route.id]?.shouldRevalidate\n      ) {\n        foundOptOutRoute = true;\n      } else if (manifestRoute.hasClientLoader) {\n        foundOptOutRoute = true;\n      } else {\n        routesParams.add(m.route.id);\n      }\n    });\n\n    if (routesParams.size === 0) {\n      return [];\n    }\n\n    let url = singleFetchUrl(\n      page,\n      basename,\n      future.unstable_trailingSlashAwareDataRequests,\n      \"data\",\n    );\n    // When one or more routes have opted out, we add a _routes param to\n    // limit the loaders to those that have a server loader and did not\n    // opt out\n    if (foundOptOutRoute && routesParams.size > 0) {\n      url.searchParams.set(\n        \"_routes\",\n        nextMatches\n          .filter((m) => routesParams.has(m.route.id))\n          .map((m) => m.route.id)\n          .join(\",\"),\n      );\n    }\n\n    return [url.pathname + url.search];\n  }, [\n    basename,\n    future.unstable_trailingSlashAwareDataRequests,\n    loaderData,\n    location,\n    manifest,\n    newMatchesForData,\n    nextMatches,\n    page,\n    routeModules,\n  ]);\n\n  let moduleHrefs = React.useMemo(\n    () => getModuleLinkHrefs(newMatchesForAssets, manifest),\n    [newMatchesForAssets, manifest],\n  );\n\n  // needs to be a hook with async behavior because we need the modules, not\n  // just the manifest like the other links in here.\n  let keyedPrefetchLinks = useKeyedPrefetchLinks(newMatchesForAssets);\n\n  return (\n    <>\n      {dataHrefs.map((href) => (\n        <link key={href} rel=\"prefetch\" as=\"fetch\" href={href} {...linkProps} />\n      ))}\n      {moduleHrefs.map((href) => (\n        <link key={href} rel=\"modulepreload\" href={href} {...linkProps} />\n      ))}\n      {keyedPrefetchLinks.map(({ key, link }) => (\n        // these don't spread `linkProps` because they are full link descriptors\n        // already with their own props\n        <link\n          key={key}\n          nonce={linkProps.nonce}\n          {...link}\n          crossOrigin={link.crossOrigin ?? linkProps.crossOrigin}\n        />\n      ))}\n    </>\n  );\n}\n\n/**\n * Renders all the [`<meta>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/meta)\n * tags created by the route module's [`meta`](../../start/framework/route-module#meta)\n * export. You should render it inside the [`<head>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/head)\n * of your document.\n *\n * @example\n * import { Meta } from \"react-router\";\n *\n * export default function Root() {\n *   return (\n *     <html>\n *       <head>\n *         <Meta />\n *       </head>\n *     </html>\n *   );\n * }\n *\n * @public\n * @category Components\n * @mode framework\n * @returns A collection of React elements for [`<meta>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/meta)\n * tags\n */\nexport function Meta(): React.JSX.Element {\n  let { isSpaMode, routeModules } = useFrameworkContext();\n  let {\n    errors,\n    matches: routerMatches,\n    loaderData,\n  } = useDataRouterStateContext();\n  let location = useLocation();\n\n  let _matches = getActiveMatches(routerMatches, errors, isSpaMode);\n\n  let error: any = null;\n  if (errors) {\n    error = errors[_matches[_matches.length - 1].route.id];\n  }\n\n  let meta: MetaDescriptor[] = [];\n  let leafMeta: MetaDescriptor[] | null = null;\n  let matches: MetaMatches = [];\n  for (let i = 0; i < _matches.length; i++) {\n    let _match = _matches[i];\n    let routeId = _match.route.id;\n    let data = loaderData[routeId];\n    let params = _match.params;\n    let routeModule = routeModules[routeId];\n    let routeMeta: MetaDescriptor[] | undefined = [];\n\n    let match: MetaMatch = {\n      id: routeId,\n      data,\n      loaderData: data,\n      meta: [],\n      params: _match.params,\n      pathname: _match.pathname,\n      handle: _match.route.handle,\n      error,\n    };\n    matches[i] = match;\n\n    if (routeModule?.meta) {\n      routeMeta =\n        typeof routeModule.meta === \"function\"\n          ? (routeModule.meta as MetaFunction)({\n              data,\n              loaderData: data,\n              params,\n              location,\n              matches,\n              error,\n            })\n          : Array.isArray(routeModule.meta)\n            ? [...routeModule.meta]\n            : routeModule.meta;\n    } else if (leafMeta) {\n      // We only assign the route's meta to the nearest leaf if there is no meta\n      // export in the route. The meta function may return a falsy value which\n      // is effectively the same as an empty array.\n      routeMeta = [...leafMeta];\n    }\n\n    routeMeta = routeMeta || [];\n    if (!Array.isArray(routeMeta)) {\n      throw new Error(\n        \"The route at \" +\n          _match.route.path +\n          \" returns an invalid value. All route meta functions must \" +\n          \"return an array of meta objects.\" +\n          \"\\n\\nTo reference the meta function API, see https://reactrouter.com/start/framework/route-module#meta\",\n      );\n    }\n\n    match.meta = routeMeta;\n    matches[i] = match;\n    meta = [...routeMeta];\n    leafMeta = meta;\n  }\n\n  return (\n    <>\n      {meta.flat().map((metaProps) => {\n        if (!metaProps) {\n          return null;\n        }\n\n        if (\"tagName\" in metaProps) {\n          let { tagName, ...rest } = metaProps;\n          if (!isValidMetaTag(tagName)) {\n            console.warn(\n              `A meta object uses an invalid tagName: ${tagName}. Expected either 'link' or 'meta'`,\n            );\n            return null;\n          }\n          let Comp = tagName;\n          return <Comp key={JSON.stringify(rest)} {...rest} />;\n        }\n\n        if (\"title\" in metaProps) {\n          return <title key=\"title\">{String(metaProps.title)}</title>;\n        }\n\n        if (\"charset\" in metaProps) {\n          metaProps.charSet ??= metaProps.charset;\n          delete metaProps.charset;\n        }\n\n        if (\"charSet\" in metaProps && metaProps.charSet != null) {\n          return typeof metaProps.charSet === \"string\" ? (\n            <meta key=\"charSet\" charSet={metaProps.charSet} />\n          ) : null;\n        }\n\n        if (\"script:ld+json\" in metaProps) {\n          try {\n            let json = JSON.stringify(metaProps[\"script:ld+json\"]);\n            return (\n              <script\n                key={`script:ld+json:${json}`}\n                type=\"application/ld+json\"\n                dangerouslySetInnerHTML={{ __html: escapeHtml(json) }}\n              />\n            );\n          } catch (err) {\n            return null;\n          }\n        }\n        return <meta key={JSON.stringify(metaProps)} {...metaProps} />;\n      })}\n    </>\n  );\n}\n\nfunction isValidMetaTag(tagName: unknown): tagName is \"meta\" | \"link\" {\n  return typeof tagName === \"string\" && /^(meta|link)$/.test(tagName);\n}\n\n/**\n * Tracks whether hydration is finished, so scripts can be skipped\n * during client-side updates.\n */\nlet isHydrated = false;\nexport function setIsHydrated() {\n  isHydrated = true;\n}\n\n/**\n * A couple common attributes:\n *\n * - `<Scripts crossOrigin>` for hosting your static assets on a different\n *   server than your app.\n * - `<Scripts nonce>` to support a [content security policy for scripts](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/script-src)\n * with [nonce-sources](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/Sources#sources)\n * for your [`<script>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script)\n * tags.\n *\n * You cannot pass through attributes such as [`async`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLScriptElement/async),\n * [`defer`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLScriptElement/defer),\n * [`noModule`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLScriptElement/noModule),\n * [`src`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLScriptElement/src),\n * or [`type`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLScriptElement/type),\n * because they are managed by React Router internally.\n *\n * @category Types\n */\nexport type ScriptsProps = Omit<\n  React.HTMLProps<HTMLScriptElement>,\n  | \"async\"\n  | \"children\"\n  | \"dangerouslySetInnerHTML\"\n  | \"defer\"\n  | \"noModule\"\n  | \"src\"\n  | \"suppressHydrationWarning\"\n  | \"type\"\n> & {\n  /**\n   * A [`nonce`](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Global_attributes/nonce)\n   * attribute to render on the [`<script>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script)\n   * element\n   */\n  nonce?: string | undefined;\n};\n\n/**\n * Renders the client runtime of your app. It should be rendered inside the\n * [`<body>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/body)\n *  of the document.\n *\n * If server rendering, you can omit `<Scripts/>` and the app will work as a\n * traditional web app without JavaScript, relying solely on HTML and browser\n * behaviors.\n *\n * @example\n * import { Scripts } from \"react-router\";\n *\n * export default function Root() {\n *   return (\n *     <html>\n *       <head />\n *       <body>\n *         <Scripts />\n *       </body>\n *     </html>\n *   );\n * }\n *\n * @public\n * @category Components\n * @mode framework\n * @param scriptProps Additional props to spread onto the [`<script>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script)\n * tags, such as [`crossOrigin`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLScriptElement/crossOrigin),\n * [`nonce`](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Global_attributes/nonce),\n * etc.\n * @returns A collection of React elements for [`<script>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script)\n * tags\n */\nexport function Scripts(scriptProps: ScriptsProps): React.JSX.Element | null {\n  let {\n    manifest,\n    serverHandoffString,\n    isSpaMode,\n    renderMeta,\n    routeDiscovery,\n    ssr,\n  } = useFrameworkContext();\n  let { router, static: isStatic, staticContext } = useDataRouterContext();\n  let { matches: routerMatches } = useDataRouterStateContext();\n  let isRSCRouterContext = useIsRSCRouterContext();\n  let enableFogOfWar = isFogOfWarEnabled(routeDiscovery, ssr);\n\n  // Let <ServerRouter> know that we hydrated and we should render the single\n  // fetch streaming scripts\n  if (renderMeta) {\n    renderMeta.didRenderScripts = true;\n  }\n\n  let matches = getActiveMatches(routerMatches, null, isSpaMode);\n\n  React.useEffect(() => {\n    setIsHydrated();\n  }, []);\n\n  let initialScripts = React.useMemo(() => {\n    if (isRSCRouterContext) {\n      return null;\n    }\n\n    let streamScript =\n      \"window.__reactRouterContext.stream = new ReadableStream({\" +\n      \"start(controller){\" +\n      \"window.__reactRouterContext.streamController = controller;\" +\n      \"}\" +\n      \"}).pipeThrough(new TextEncoderStream());\";\n\n    let contextScript = staticContext\n      ? `window.__reactRouterContext = ${serverHandoffString};${streamScript}`\n      : \" \";\n\n    let routeModulesScript = !isStatic\n      ? \" \"\n      : `${\n          manifest.hmr?.runtime\n            ? `import ${JSON.stringify(manifest.hmr.runtime)};`\n            : \"\"\n        }${!enableFogOfWar ? `import ${JSON.stringify(manifest.url)}` : \"\"};\n${matches\n  .map((match, routeIndex) => {\n    let routeVarName = `route${routeIndex}`;\n    let manifestEntry = manifest.routes[match.route.id];\n    invariant(manifestEntry, `Route ${match.route.id} not found in manifest`);\n    let {\n      clientActionModule,\n      clientLoaderModule,\n      clientMiddlewareModule,\n      hydrateFallbackModule,\n      module,\n    } = manifestEntry;\n\n    let chunks: Array<{ module: string; varName: string }> = [\n      ...(clientActionModule\n        ? [\n            {\n              module: clientActionModule,\n              varName: `${routeVarName}_clientAction`,\n            },\n          ]\n        : []),\n      ...(clientLoaderModule\n        ? [\n            {\n              module: clientLoaderModule,\n              varName: `${routeVarName}_clientLoader`,\n            },\n          ]\n        : []),\n      ...(clientMiddlewareModule\n        ? [\n            {\n              module: clientMiddlewareModule,\n              varName: `${routeVarName}_clientMiddleware`,\n            },\n          ]\n        : []),\n      ...(hydrateFallbackModule\n        ? [\n            {\n              module: hydrateFallbackModule,\n              varName: `${routeVarName}_HydrateFallback`,\n            },\n          ]\n        : []),\n      { module, varName: `${routeVarName}_main` },\n    ];\n\n    if (chunks.length === 1) {\n      return `import * as ${routeVarName} from ${JSON.stringify(module)};`;\n    }\n\n    let chunkImportsSnippet = chunks\n      .map((chunk) => `import * as ${chunk.varName} from \"${chunk.module}\";`)\n      .join(\"\\n\");\n\n    let mergedChunksSnippet = `const ${routeVarName} = {${chunks\n      .map((chunk) => `...${chunk.varName}`)\n      .join(\",\")}};`;\n\n    return [chunkImportsSnippet, mergedChunksSnippet].join(\"\\n\");\n  })\n  .join(\"\\n\")}\n  ${\n    enableFogOfWar\n      ? // Inline a minimal manifest with the SSR matches\n        `window.__reactRouterManifest = ${JSON.stringify(\n          getPartialManifest(manifest, router),\n          null,\n          2,\n        )};`\n      : \"\"\n  }\n  window.__reactRouterRouteModules = {${matches\n    .map((match, index) => `${JSON.stringify(match.route.id)}:route${index}`)\n    .join(\",\")}};\n\nimport(${JSON.stringify(manifest.entry.module)});`;\n\n    return (\n      <>\n        <script\n          {...scriptProps}\n          suppressHydrationWarning\n          dangerouslySetInnerHTML={{ __html: contextScript }}\n          type={undefined}\n        />\n        <script\n          {...scriptProps}\n          suppressHydrationWarning\n          dangerouslySetInnerHTML={{ __html: routeModulesScript }}\n          type=\"module\"\n          async\n        />\n      </>\n    );\n    // disabled deps array because we are purposefully only rendering this once\n    // for hydration, after that we want to just continue rendering the initial\n    // scripts as they were when the page first loaded\n    // eslint-disable-next-line\n  }, []);\n\n  let preloads =\n    isHydrated || isRSCRouterContext\n      ? []\n      : dedupe(\n          manifest.entry.imports.concat(\n            getModuleLinkHrefs(matches, manifest, {\n              includeHydrateFallback: true,\n            }),\n          ),\n        );\n\n  let sri = typeof manifest.sri === \"object\" ? manifest.sri : {};\n\n  warnOnce(\n    !isRSCRouterContext,\n    \"The <Scripts /> element is a no-op when using RSC and can be safely removed.\",\n  );\n\n  return isHydrated || isRSCRouterContext ? null : (\n    <>\n      {typeof manifest.sri === \"object\" ? (\n        <script\n          {...scriptProps}\n          rr-importmap=\"\"\n          type=\"importmap\"\n          suppressHydrationWarning\n          dangerouslySetInnerHTML={{\n            __html: JSON.stringify({\n              integrity: sri,\n            }),\n          }}\n        />\n      ) : null}\n      {!enableFogOfWar ? (\n        <link\n          rel=\"modulepreload\"\n          href={manifest.url}\n          crossOrigin={scriptProps.crossOrigin}\n          integrity={sri[manifest.url]}\n          suppressHydrationWarning\n        />\n      ) : null}\n      <link\n        rel=\"modulepreload\"\n        href={manifest.entry.module}\n        crossOrigin={scriptProps.crossOrigin}\n        integrity={sri[manifest.entry.module]}\n        suppressHydrationWarning\n      />\n      {preloads.map((path) => (\n        <link\n          key={path}\n          rel=\"modulepreload\"\n          href={path}\n          crossOrigin={scriptProps.crossOrigin}\n          integrity={sri[path]}\n          suppressHydrationWarning\n        />\n      ))}\n      {initialScripts}\n    </>\n  );\n}\n\nfunction dedupe(array: any[]) {\n  return [...new Set(array)];\n}\n\nexport function mergeRefs<T = any>(\n  ...refs: Array<React.MutableRefObject<T> | React.LegacyRef<T>>\n): React.RefCallback<T> {\n  return (value) => {\n    refs.forEach((ref) => {\n      if (typeof ref === \"function\") {\n        ref(value);\n      } else if (ref != null) {\n        (ref as React.MutableRefObject<T | null>).current = value;\n      }\n    });\n  };\n}\n"
  },
  {
    "path": "packages/react-router/lib/dom/ssr/data.ts",
    "content": "// eslint-disable-next-line @typescript-eslint/no-unused-vars\nimport type * as _ from \"../global\";\n\nexport async function createRequestInit(\n  request: Request,\n): Promise<RequestInit> {\n  let init: RequestInit = { signal: request.signal };\n\n  if (request.method !== \"GET\") {\n    init.method = request.method;\n\n    let contentType = request.headers.get(\"Content-Type\");\n\n    // Check between word boundaries instead of startsWith() due to the last\n    // paragraph of https://httpwg.org/specs/rfc9110.html#field.content-type\n    if (contentType && /\\bapplication\\/json\\b/.test(contentType)) {\n      init.headers = { \"Content-Type\": contentType };\n      init.body = JSON.stringify(await request.json());\n    } else if (contentType && /\\btext\\/plain\\b/.test(contentType)) {\n      init.headers = { \"Content-Type\": contentType };\n      init.body = await request.text();\n    } else if (\n      contentType &&\n      /\\bapplication\\/x-www-form-urlencoded\\b/.test(contentType)\n    ) {\n      init.body = new URLSearchParams(await request.text());\n    } else {\n      init.body = await request.formData();\n    }\n  }\n\n  return init;\n}\n"
  },
  {
    "path": "packages/react-router/lib/dom/ssr/entry.ts",
    "content": "import type { StaticHandlerContext } from \"../../router/router\";\n\nimport type { EntryRoute } from \"./routes\";\nimport type { RouteModules } from \"./routeModules\";\nimport type { RouteManifest } from \"../../router/utils\";\nimport type { ServerBuild } from \"../../server-runtime/build\";\n\ntype SerializedError = {\n  message: string;\n  stack?: string;\n};\n\n// Object passed to RemixContext.Provider\nexport interface FrameworkContextObject {\n  manifest: AssetsManifest;\n  routeModules: RouteModules;\n  criticalCss?: CriticalCss;\n  serverHandoffString?: string;\n  future: FutureConfig;\n  ssr: boolean;\n  isSpaMode: boolean;\n  routeDiscovery: ServerBuild[\"routeDiscovery\"];\n  serializeError?(error: Error): SerializedError;\n  renderMeta?: {\n    didRenderScripts?: boolean;\n    streamCache?: Record<\n      number,\n      Promise<void> & {\n        result?: {\n          done: boolean;\n          value: string;\n        };\n        error?: unknown;\n      }\n    >;\n  };\n}\n\n// Additional React-Router information needed at runtime, but not hydrated\n// through RemixContext\nexport interface EntryContext extends FrameworkContextObject {\n  staticHandlerContext: StaticHandlerContext;\n  serverHandoffStream?: ReadableStream<Uint8Array>;\n}\n\nexport interface FutureConfig {\n  unstable_subResourceIntegrity: boolean;\n  unstable_trailingSlashAwareDataRequests: boolean;\n  v8_middleware: boolean;\n}\n\nexport type CriticalCss = string | { rel: \"stylesheet\"; href: string };\n\nexport interface AssetsManifest {\n  entry: {\n    imports: string[];\n    module: string;\n  };\n  routes: RouteManifest<EntryRoute>;\n  url: string;\n  version: string;\n  hmr?: {\n    timestamp?: number;\n    runtime: string;\n  };\n  sri?: Record<string, string> | true;\n}\n"
  },
  {
    "path": "packages/react-router/lib/dom/ssr/errorBoundaries.tsx",
    "content": "import * as React from \"react\";\n\nimport { Scripts, useFrameworkContext } from \"./components\";\nimport type { Location } from \"../../router/history\";\nimport { isRouteErrorResponse } from \"../../router/utils\";\nimport { ENABLE_DEV_WARNINGS } from \"../../context\";\n\ntype RemixErrorBoundaryProps = React.PropsWithChildren<{\n  location: Location;\n  isOutsideRemixApp?: boolean;\n  error?: Error;\n}>;\n\ntype RemixErrorBoundaryState = {\n  error: null | Error;\n  location: Location;\n};\n\nexport class RemixErrorBoundary extends React.Component<\n  RemixErrorBoundaryProps,\n  RemixErrorBoundaryState\n> {\n  constructor(props: RemixErrorBoundaryProps) {\n    super(props);\n    this.state = { error: props.error || null, location: props.location };\n  }\n\n  static getDerivedStateFromError(error: Error) {\n    return { error };\n  }\n\n  static getDerivedStateFromProps(\n    props: RemixErrorBoundaryProps,\n    state: RemixErrorBoundaryState,\n  ) {\n    // When we get into an error state, the user will likely click \"back\" to the\n    // previous page that didn't have an error. Because this wraps the entire\n    // application (even the HTML!) that will have no effect--the error page\n    // continues to display. This gives us a mechanism to recover from the error\n    // when the location changes.\n    //\n    // Whether we're in an error state or not, we update the location in state\n    // so that when we are in an error state, it gets reset when a new location\n    // comes in and the user recovers from the error.\n    if (state.location !== props.location) {\n      return { error: props.error || null, location: props.location };\n    }\n\n    // If we're not changing locations, preserve the location but still surface\n    // any new errors that may come through. We retain the existing error, we do\n    // this because the error provided from the app state may be cleared without\n    // the location changing.\n    return { error: props.error || state.error, location: state.location };\n  }\n\n  render() {\n    if (this.state.error) {\n      return (\n        <RemixRootDefaultErrorBoundary\n          error={this.state.error}\n          isOutsideRemixApp={true}\n        />\n      );\n    } else {\n      return this.props.children;\n    }\n  }\n}\n\n/**\n * When app's don't provide a root level ErrorBoundary, we default to this.\n */\nexport function RemixRootDefaultErrorBoundary({\n  error,\n  isOutsideRemixApp,\n}: {\n  error: unknown;\n  isOutsideRemixApp?: boolean;\n}) {\n  console.error(error);\n\n  let heyDeveloper = (\n    <script\n      dangerouslySetInnerHTML={{\n        __html: `\n        console.log(\n          \"💿 Hey developer 👋. You can provide a way better UX than this when your app throws errors. Check out https://reactrouter.com/how-to/error-boundary for more information.\"\n        );\n      `,\n      }}\n    />\n  );\n\n  if (isRouteErrorResponse(error)) {\n    return (\n      <BoundaryShell title=\"Unhandled Thrown Response!\">\n        <h1 style={{ fontSize: \"24px\" }}>\n          {error.status} {error.statusText}\n        </h1>\n        {ENABLE_DEV_WARNINGS ? heyDeveloper : null}\n      </BoundaryShell>\n    );\n  }\n\n  let errorInstance: Error;\n  if (error instanceof Error) {\n    errorInstance = error;\n  } else {\n    let errorString =\n      error == null\n        ? \"Unknown Error\"\n        : typeof error === \"object\" && \"toString\" in error\n          ? error.toString()\n          : JSON.stringify(error);\n    errorInstance = new Error(errorString);\n  }\n\n  return (\n    <BoundaryShell\n      title=\"Application Error!\"\n      isOutsideRemixApp={isOutsideRemixApp}\n    >\n      <h1 style={{ fontSize: \"24px\" }}>Application Error</h1>\n      <pre\n        style={{\n          padding: \"2rem\",\n          background: \"hsla(10, 50%, 50%, 0.1)\",\n          color: \"red\",\n          overflow: \"auto\",\n        }}\n      >\n        {errorInstance.stack}\n      </pre>\n      {heyDeveloper}\n    </BoundaryShell>\n  );\n}\n\nexport function BoundaryShell({\n  title,\n  renderScripts,\n  isOutsideRemixApp,\n  children,\n}: {\n  title: string;\n  renderScripts?: boolean;\n  isOutsideRemixApp?: boolean;\n  children: React.ReactNode | React.ReactNode[];\n}) {\n  let { routeModules } = useFrameworkContext();\n\n  // Generally speaking, when the root route has a Layout we want to use that\n  // as the app shell instead of the default `BoundaryShell` wrapper markup below.\n  // This is true for `loader`/`action` errors, most render errors, and\n  // `HydrateFallback` scenarios.\n\n  // However, render errors thrown from the `Layout` present a bit of an issue\n  // because if the `Layout` itself throws during the `ErrorBoundary` pass and\n  // we bubble outside the `RouterProvider` to the wrapping `RemixErrorBoundary`,\n  // by returning only `children` here we'll be trying to append a `<div>` to\n  // the `document` and the DOM will throw, putting React into an error/hydration\n  // loop.\n\n  // Instead, if we're ever rendering from the outermost `RemixErrorBoundary`\n  // during hydration that wraps `RouterProvider`, then we can't trust the\n  // `Layout` and should fallback to the default app shell so we're always\n  // returning an `<html>` document.\n  if (routeModules.root?.Layout && !isOutsideRemixApp) {\n    return children;\n  }\n\n  return (\n    <html lang=\"en\">\n      <head>\n        <meta charSet=\"utf-8\" />\n        <meta\n          name=\"viewport\"\n          content=\"width=device-width,initial-scale=1,viewport-fit=cover\"\n        />\n        <title>{title}</title>\n      </head>\n      <body>\n        <main style={{ fontFamily: \"system-ui, sans-serif\", padding: \"2rem\" }}>\n          {children}\n          {renderScripts ? <Scripts /> : null}\n        </main>\n      </body>\n    </html>\n  );\n}\n"
  },
  {
    "path": "packages/react-router/lib/dom/ssr/errors.ts",
    "content": "import type { RouterState } from \"../../router/router\";\nimport { ErrorResponseImpl } from \"../../router/utils\";\n\nexport function deserializeErrors(\n  errors: RouterState[\"errors\"],\n): RouterState[\"errors\"] {\n  if (!errors) return null;\n  let entries = Object.entries(errors);\n  let serialized: RouterState[\"errors\"] = {};\n  for (let [key, val] of entries) {\n    // Hey you!  If you change this, please change the corresponding logic in\n    // serializeErrors in react-router/lib/server-runtime/errors.ts :)\n    if (val && val.__type === \"RouteErrorResponse\") {\n      serialized[key] = new ErrorResponseImpl(\n        val.status,\n        val.statusText,\n        val.data,\n        val.internal === true,\n      );\n    } else if (val && val.__type === \"Error\") {\n      // Attempt to reconstruct the right type of Error (i.e., ReferenceError)\n      if (val.__subType) {\n        let ErrorConstructor = window[val.__subType];\n        if (typeof ErrorConstructor === \"function\") {\n          try {\n            // @ts-expect-error\n            let error = new ErrorConstructor(val.message);\n            error.stack = val.stack;\n            serialized[key] = error;\n          } catch (e) {\n            // no-op - fall through and create a normal Error\n          }\n        }\n      }\n\n      if (serialized[key] == null) {\n        let error = new Error(val.message);\n        error.stack = val.stack;\n        serialized[key] = error;\n      }\n    } else {\n      serialized[key] = val;\n    }\n  }\n  return serialized;\n}\n"
  },
  {
    "path": "packages/react-router/lib/dom/ssr/fallback.tsx",
    "content": "import * as React from \"react\";\n\nimport { BoundaryShell } from \"./errorBoundaries\";\nimport { ENABLE_DEV_WARNINGS } from \"../../context\";\n\n// If the user sets `clientLoader.hydrate=true` somewhere but does not\n// provide a `HydrateFallback` at any level of the tree, then we need to at\n// least include `<Scripts>` in the SSR so we can hydrate the app and call the\n// `clientLoader` functions\nexport function RemixRootDefaultHydrateFallback() {\n  return (\n    <BoundaryShell title=\"Loading...\" renderScripts>\n      {ENABLE_DEV_WARNINGS ? (\n        <script\n          dangerouslySetInnerHTML={{\n            __html: `\n              console.log(\n                \"💿 Hey developer 👋. You can provide a way better UX than this \" +\n                \"when your app is loading JS modules and/or running \\`clientLoader\\` \" +\n                \"functions. Check out https://reactrouter.com/start/framework/route-module#hydratefallback \" +\n                \"for more information.\"\n              );\n            `,\n          }}\n        />\n      ) : null}\n    </BoundaryShell>\n  );\n}\n"
  },
  {
    "path": "packages/react-router/lib/dom/ssr/fog-of-war.ts",
    "content": "import * as React from \"react\";\nimport type { PatchRoutesOnNavigationFunction } from \"../../context\";\nimport type { Router as DataRouter } from \"../../router/router\";\nimport type { RouteManifest } from \"../../router/utils\";\nimport { matchRoutes } from \"../../router/utils\";\nimport type { AssetsManifest } from \"./entry\";\nimport type { RouteModules } from \"./routeModules\";\nimport type { EntryRoute } from \"./routes\";\nimport { createClientRoutes } from \"./routes\";\nimport type { ServerBuild } from \"../../server-runtime/build\";\nimport { createPath } from \"../../router/history\";\n\n// Currently rendered links that may need prefetching\nconst nextPaths = new Set<string>();\n\n// FIFO queue of previously discovered routes to prevent re-calling on\n// subsequent navigations to the same path\nconst discoveredPathsMaxSize = 1000;\nconst discoveredPaths = new Set<string>();\n\n// 7.5k to come in under the ~8k limit for most browsers\n// https://stackoverflow.com/a/417184\nconst URL_LIMIT = 7680;\n\nexport function isFogOfWarEnabled(\n  routeDiscovery: ServerBuild[\"routeDiscovery\"],\n  ssr: boolean,\n) {\n  return routeDiscovery.mode === \"lazy\" && ssr === true;\n}\n\nexport function getPartialManifest(\n  { sri, ...manifest }: AssetsManifest,\n  router: DataRouter,\n) {\n  // Start with our matches for this pathname\n  let routeIds = new Set(router.state.matches.map((m) => m.route.id));\n\n  let segments = router.state.location.pathname.split(\"/\").filter(Boolean);\n  let paths: string[] = [\"/\"];\n\n  // We've already matched to the last segment\n  segments.pop();\n\n  // Traverse each path for our parents and match in case they have pathless/index\n  // children we need to include in the initial manifest\n  while (segments.length > 0) {\n    paths.push(`/${segments.join(\"/\")}`);\n    segments.pop();\n  }\n\n  paths.forEach((path) => {\n    let matches = matchRoutes(router.routes, path, router.basename);\n    if (matches) {\n      matches.forEach((m) => routeIds.add(m.route.id));\n    }\n  });\n\n  let initialRoutes = [...routeIds].reduce(\n    (acc, id) => Object.assign(acc, { [id]: manifest.routes[id] }),\n    {},\n  );\n  return {\n    ...manifest,\n    routes: initialRoutes,\n    sri: sri ? true : undefined,\n  };\n}\n\nexport function getPatchRoutesOnNavigationFunction(\n  getRouter: () => DataRouter,\n  manifest: AssetsManifest,\n  routeModules: RouteModules,\n  ssr: boolean,\n  routeDiscovery: ServerBuild[\"routeDiscovery\"],\n  isSpaMode: boolean,\n  basename: string | undefined,\n): PatchRoutesOnNavigationFunction | undefined {\n  if (!isFogOfWarEnabled(routeDiscovery, ssr)) {\n    return undefined;\n  }\n\n  return async ({ path, patch, signal, fetcherKey }) => {\n    if (discoveredPaths.has(path)) {\n      return;\n    }\n    let { state } = getRouter();\n    await fetchAndApplyManifestPatches(\n      [path],\n      // If we're patching for a fetcher call, reload the current location\n      // Otherwise prefer any ongoing navigation location\n      fetcherKey\n        ? window.location.href\n        : createPath(state.navigation.location || state.location),\n      manifest,\n      routeModules,\n      ssr,\n      isSpaMode,\n      basename,\n      routeDiscovery.manifestPath,\n      patch,\n      signal,\n    );\n  };\n}\n\nexport function useFogOFWarDiscovery(\n  router: DataRouter,\n  manifest: AssetsManifest,\n  routeModules: RouteModules,\n  ssr: boolean,\n  routeDiscovery: ServerBuild[\"routeDiscovery\"],\n  isSpaMode: boolean,\n) {\n  React.useEffect(() => {\n    // Don't prefetch if not enabled or if the user has `saveData` enabled\n    if (\n      !isFogOfWarEnabled(routeDiscovery, ssr) ||\n      // @ts-expect-error - TS doesn't know about this yet\n      window.navigator?.connection?.saveData === true\n    ) {\n      return;\n    }\n\n    // Register a link href for patching\n    function registerElement(el: Element) {\n      let path =\n        el.tagName === \"FORM\"\n          ? el.getAttribute(\"action\")\n          : el.getAttribute(\"href\");\n      if (!path) {\n        return;\n      }\n      // optimization: use the already-parsed pathname from links\n      let pathname =\n        el.tagName === \"A\"\n          ? (el as HTMLAnchorElement).pathname\n          : new URL(path, window.location.origin).pathname;\n      if (!discoveredPaths.has(pathname)) {\n        nextPaths.add(pathname);\n      }\n    }\n\n    // Register and fetch patches for all initially-rendered links/forms\n    async function fetchPatches() {\n      // re-check/update registered links\n      document\n        .querySelectorAll(\"a[data-discover], form[data-discover]\")\n        .forEach(registerElement);\n\n      let lazyPaths = Array.from(nextPaths.keys()).filter((path) => {\n        if (discoveredPaths.has(path)) {\n          nextPaths.delete(path);\n          return false;\n        }\n        return true;\n      });\n\n      if (lazyPaths.length === 0) {\n        return;\n      }\n\n      try {\n        await fetchAndApplyManifestPatches(\n          lazyPaths,\n          null,\n          manifest,\n          routeModules,\n          ssr,\n          isSpaMode,\n          router.basename,\n          routeDiscovery.manifestPath,\n          router.patchRoutes,\n        );\n      } catch (e) {\n        console.error(\"Failed to fetch manifest patches\", e);\n      }\n    }\n\n    let debouncedFetchPatches = debounce(fetchPatches, 100);\n\n    // scan and fetch initial links\n    fetchPatches();\n\n    // Setup a MutationObserver to fetch all subsequently rendered links/form\n    // It just schedules a full scan since that's faster than checking subtrees\n    let observer = new MutationObserver(() => debouncedFetchPatches());\n\n    observer.observe(document.documentElement, {\n      subtree: true,\n      childList: true,\n      attributes: true,\n      attributeFilter: [\"data-discover\", \"href\", \"action\"],\n    });\n\n    return () => observer.disconnect();\n  }, [ssr, isSpaMode, manifest, routeModules, router, routeDiscovery]);\n}\n\nexport function getManifestPath(\n  _manifestPath: string | undefined,\n  basename: string | undefined,\n) {\n  let manifestPath = _manifestPath || \"/__manifest\";\n\n  if (basename == null) {\n    return manifestPath;\n  }\n\n  return `${basename}${manifestPath}`.replace(/\\/+/g, \"/\");\n}\n\nconst MANIFEST_VERSION_STORAGE_KEY = \"react-router-manifest-version\";\n\nexport async function fetchAndApplyManifestPatches(\n  paths: string[],\n  errorReloadPath: string | null,\n  manifest: AssetsManifest,\n  routeModules: RouteModules,\n  ssr: boolean,\n  isSpaMode: boolean,\n  basename: string | undefined,\n  manifestPath: string,\n  patchRoutes: DataRouter[\"patchRoutes\"],\n  signal?: AbortSignal,\n): Promise<void> {\n  // NOTE: Intentionally using a standalone `URLSearchParams` instance\n  // instead of mutating `url.searchParams`, which is *significantly* slower:\n  // https://issues.chromium.org/issues/331406951\n  // https://github.com/nodejs/node/issues/51518\n  const searchParams = new URLSearchParams();\n  searchParams.set(\"paths\", paths.sort().join(\",\"));\n  searchParams.set(\"version\", manifest.version);\n  let url = new URL(\n    getManifestPath(manifestPath, basename),\n    window.location.origin,\n  );\n  url.search = searchParams.toString();\n\n  // If the URL is nearing the ~8k limit on GET requests, skip this optimization\n  // step and just let discovery happen on link click.  We also wipe out the\n  // nextPaths Set here so we can start filling it with fresh links\n  if (url.toString().length > URL_LIMIT) {\n    nextPaths.clear();\n    return;\n  }\n\n  let serverPatches: AssetsManifest[\"routes\"];\n  try {\n    let res = await fetch(url, { signal });\n\n    if (!res.ok) {\n      throw new Error(`${res.status} ${res.statusText}`);\n    } else if (\n      res.status === 204 &&\n      res.headers.has(\"X-Remix-Reload-Document\")\n    ) {\n      if (!errorReloadPath) {\n        // No-op during eager route discovery so we will trigger a hard reload\n        // of the destination during the next navigation instead of reloading\n        // while the user is sitting on the current page.  Slightly more\n        // disruptive on fetcher calls because we reload the current page, but\n        // it's better than the `React.useContext` error that occurs without\n        // this detection.\n        console.warn(\n          \"Detected a manifest version mismatch during eager route discovery. \" +\n            \"The next navigation/fetch to an undiscovered route will result in \" +\n            \"a new document navigation to sync up with the latest manifest.\",\n        );\n        return;\n      }\n\n      try {\n        // This will hard reload the destination path on navigations, or the\n        // current path on fetcher calls\n        if (\n          sessionStorage.getItem(MANIFEST_VERSION_STORAGE_KEY) ===\n          manifest.version\n        ) {\n          // We've already tried fixing for this version, don' try again to\n          // avoid loops - just let this navigation/fetch 404\n          console.error(\n            \"Unable to discover routes due to manifest version mismatch.\",\n          );\n          return;\n        }\n\n        sessionStorage.setItem(MANIFEST_VERSION_STORAGE_KEY, manifest.version);\n      } catch {\n        // Session storage unavailable\n      }\n\n      window.location.href = errorReloadPath;\n      console.warn(\"Detected manifest version mismatch, reloading...\");\n\n      // Stall here and let the browser reload and avoid triggering a flash of\n      // an ErrorBoundary if we threw (same thing we do in `loadRouteModule()`)\n      await new Promise(() => {\n        // check out of this hook cause the DJs never gonna re[s]olve this\n      });\n    } else if (res.status >= 400) {\n      throw new Error(await res.text());\n    }\n\n    // Reset loop-detection on a successful response\n    try {\n      sessionStorage.removeItem(MANIFEST_VERSION_STORAGE_KEY);\n    } catch {\n      // Session storage unavailable\n    }\n    serverPatches = (await res.json()) as AssetsManifest[\"routes\"];\n  } catch (e) {\n    if (signal?.aborted) return;\n    throw e;\n  }\n\n  // Patch routes we don't know about yet into the manifest\n  let knownRoutes = new Set(Object.keys(manifest.routes));\n  let patches = Object.values(serverPatches).reduce((acc, route) => {\n    if (route && !knownRoutes.has(route.id)) {\n      acc[route.id] = route;\n    }\n    return acc;\n  }, {} as RouteManifest<EntryRoute>);\n  Object.assign(manifest.routes, patches);\n\n  // Track discovered paths so we don't have to fetch them again\n  paths.forEach((p) => addToFifoQueue(p, discoveredPaths));\n\n  // Identify all parentIds for which we have new children to add and patch\n  // in their new children\n  let parentIds = new Set<string | undefined>();\n  Object.values(patches).forEach((patch) => {\n    if (patch && (!patch.parentId || !patches[patch.parentId])) {\n      parentIds.add(patch.parentId);\n    }\n  });\n  parentIds.forEach((parentId) =>\n    patchRoutes(\n      parentId || null,\n      createClientRoutes(patches, routeModules, null, ssr, isSpaMode, parentId),\n    ),\n  );\n}\n\nfunction addToFifoQueue(path: string, queue: Set<string>) {\n  if (queue.size >= discoveredPathsMaxSize) {\n    let first = queue.values().next().value;\n    queue.delete(first);\n  }\n  queue.add(path);\n}\n\n// Thanks Josh!\n// https://www.joshwcomeau.com/snippets/javascript/debounce/\nfunction debounce(callback: (...args: unknown[]) => unknown, wait: number) {\n  let timeoutId: number | undefined;\n  return (...args: unknown[]) => {\n    window.clearTimeout(timeoutId);\n    timeoutId = window.setTimeout(() => callback(...args), wait);\n  };\n}\n"
  },
  {
    "path": "packages/react-router/lib/dom/ssr/hydration.tsx",
    "content": "import type { DataRouteObject } from \"../../context\";\nimport type { Path } from \"../../router/history\";\nimport type { Router as DataRouter, HydrationState } from \"../../router/router\";\nimport { matchRoutes } from \"../../router/utils\";\nimport type { ClientLoaderFunction } from \"./routeModules\";\nimport { shouldHydrateRouteLoader } from \"./routes\";\n\nexport function getHydrationData({\n  state,\n  routes,\n  getRouteInfo,\n  location,\n  basename,\n  isSpaMode,\n}: {\n  state: {\n    loaderData?: DataRouter[\"state\"][\"loaderData\"];\n    actionData?: DataRouter[\"state\"][\"actionData\"];\n    errors?: DataRouter[\"state\"][\"errors\"];\n  };\n  routes: DataRouteObject[];\n  getRouteInfo: (routeId: string) => {\n    clientLoader: ClientLoaderFunction | undefined;\n    hasLoader: boolean;\n    hasHydrateFallback: boolean;\n  };\n  location: Path;\n  basename: string | undefined;\n  isSpaMode: boolean;\n}): HydrationState {\n  // Create a shallow clone of `loaderData` we can mutate for partial hydration.\n  // When a route exports a `clientLoader` and a `HydrateFallback`, the SSR will\n  // render the fallback so we need the client to do the same for hydration.\n  // The server loader data has already been exposed to these route `clientLoader`'s\n  // in `createClientRoutes` above, so we need to clear out the version we pass to\n  // `createBrowserRouter` so it initializes and runs the client loaders.\n  let hydrationData = {\n    ...state,\n    loaderData: { ...state.loaderData },\n  };\n  let initialMatches = matchRoutes(routes, location, basename);\n  if (initialMatches) {\n    for (let match of initialMatches) {\n      let routeId = match.route.id;\n      let routeInfo = getRouteInfo(routeId);\n      // Clear out the loaderData to avoid rendering the route component when the\n      // route opted into clientLoader hydration and either:\n      // * gave us a HydrateFallback\n      // * or doesn't have a server loader and we have no data to render\n      if (\n        shouldHydrateRouteLoader(\n          routeId,\n          routeInfo.clientLoader,\n          routeInfo.hasLoader,\n          isSpaMode,\n        ) &&\n        (routeInfo.hasHydrateFallback || !routeInfo.hasLoader)\n      ) {\n        delete hydrationData.loaderData![routeId];\n      } else if (!routeInfo.hasLoader) {\n        // Since every Remix route gets a `loader` on the client side to load\n        // the route JS module, we need to add a `null` value to `loaderData`\n        // for any routes that don't have server loaders so our partial\n        // hydration logic doesn't kick off the route module loaders during\n        // hydration\n        hydrationData.loaderData![routeId] = null;\n      }\n    }\n  }\n\n  return hydrationData;\n}\n"
  },
  {
    "path": "packages/react-router/lib/dom/ssr/invariant.ts",
    "content": "export default function invariant(\n  value: boolean,\n  message?: string,\n): asserts value;\nexport default function invariant<T>(\n  value: T | null | undefined,\n  message?: string,\n): asserts value is T;\nexport default function invariant(value: any, message?: string) {\n  if (value === false || value === null || typeof value === \"undefined\") {\n    throw new Error(message);\n  }\n}\n"
  },
  {
    "path": "packages/react-router/lib/dom/ssr/links.ts",
    "content": "import type { Location } from \"../../router/history\";\nimport type { AgnosticDataRouteMatch } from \"../../router/utils\";\n\nimport type { AssetsManifest } from \"./entry\";\nimport type { RouteModules, RouteModule } from \"./routeModules\";\nimport type { EntryRoute } from \"./routes\";\nimport { loadRouteModule } from \"./routeModules\";\nimport type {\n  HtmlLinkDescriptor,\n  LinkDescriptor,\n  PageLinkDescriptor,\n} from \"../../router/links\";\n\n/**\n * Gets all the links for a set of matches. The modules are assumed to have been\n * loaded already.\n */\nexport function getKeyedLinksForMatches(\n  matches: AgnosticDataRouteMatch[],\n  routeModules: RouteModules,\n  manifest: AssetsManifest,\n): KeyedLinkDescriptor[] {\n  let descriptors = matches\n    .map((match): LinkDescriptor[][] => {\n      let module = routeModules[match.route.id];\n      let route = manifest.routes[match.route.id];\n      return [\n        route && route.css\n          ? route.css.map((href) => ({ rel: \"stylesheet\", href }))\n          : [],\n        module?.links?.() || [],\n      ];\n    })\n    .flat(2);\n\n  let preloads = getModuleLinkHrefs(matches, manifest);\n  return dedupeLinkDescriptors(descriptors, preloads);\n}\n\nfunction getRouteCssDescriptors(route: EntryRoute): HtmlLinkDescriptor[] {\n  if (!route.css) return [];\n  return route.css.map((href) => ({ rel: \"stylesheet\", href }));\n}\n\nexport async function prefetchRouteCss(route: EntryRoute): Promise<void> {\n  if (!route.css) return;\n  let descriptors = getRouteCssDescriptors(route);\n  await Promise.all(descriptors.map(prefetchStyleLink));\n}\n\nexport async function prefetchStyleLinks(\n  route: EntryRoute,\n  routeModule: RouteModule,\n): Promise<void> {\n  if ((!route.css && !routeModule.links) || !isPreloadSupported()) return;\n\n  let descriptors: LinkDescriptor[] = [];\n  if (route.css) {\n    descriptors.push(...getRouteCssDescriptors(route));\n  }\n  if (routeModule.links) {\n    descriptors.push(...routeModule.links());\n  }\n  if (descriptors.length === 0) return;\n\n  let styleLinks: HtmlLinkDescriptor[] = [];\n  for (let descriptor of descriptors) {\n    if (!isPageLinkDescriptor(descriptor) && descriptor.rel === \"stylesheet\") {\n      styleLinks.push({\n        ...descriptor,\n        rel: \"preload\",\n        as: \"style\",\n      } as HtmlLinkDescriptor);\n    }\n  }\n\n  await Promise.all(styleLinks.map(prefetchStyleLink));\n}\n\nasync function prefetchStyleLink(\n  descriptor: HtmlLinkDescriptor,\n): Promise<void> {\n  return new Promise((resolve) => {\n    // don't prefetch non-matching media queries, or stylesheets that are\n    // already in the DOM (active route revalidations)\n    if (\n      (descriptor.media && !window.matchMedia(descriptor.media).matches) ||\n      document.querySelector(\n        `link[rel=\"stylesheet\"][href=\"${descriptor.href}\"]`,\n      )\n    ) {\n      return resolve();\n    }\n\n    let link = document.createElement(\"link\");\n    Object.assign(link, descriptor);\n\n    function removeLink() {\n      // if a navigation interrupts this prefetch React will update the <head>\n      // and remove the link we put in there manually, so we check if it's still\n      // there before trying to remove it\n      if (document.head.contains(link)) {\n        document.head.removeChild(link);\n      }\n    }\n\n    link.onload = () => {\n      removeLink();\n      resolve();\n    };\n\n    link.onerror = () => {\n      removeLink();\n      resolve();\n    };\n\n    document.head.appendChild(link);\n  });\n}\n\n////////////////////////////////////////////////////////////////////////////////\nexport function isPageLinkDescriptor(\n  object: any,\n): object is PageLinkDescriptor {\n  return object != null && typeof object.page === \"string\";\n}\n\nfunction isHtmlLinkDescriptor(object: any): object is HtmlLinkDescriptor {\n  if (object == null) {\n    return false;\n  }\n\n  // <link> may not have an href if <link rel=\"preload\"> is used with imageSrcSet + imageSizes\n  // https://github.com/remix-run/remix/issues/184\n  // https://html.spec.whatwg.org/commit-snapshots/cb4f5ff75de5f4cbd7013c4abad02f21c77d4d1c/#attr-link-imagesrcset\n  if (object.href == null) {\n    return (\n      object.rel === \"preload\" &&\n      typeof object.imageSrcSet === \"string\" &&\n      typeof object.imageSizes === \"string\"\n    );\n  }\n\n  return typeof object.rel === \"string\" && typeof object.href === \"string\";\n}\n\nexport type KeyedHtmlLinkDescriptor = { key: string; link: HtmlLinkDescriptor };\n\nexport async function getKeyedPrefetchLinks(\n  matches: AgnosticDataRouteMatch[],\n  manifest: AssetsManifest,\n  routeModules: RouteModules,\n): Promise<KeyedHtmlLinkDescriptor[]> {\n  let links = await Promise.all(\n    matches.map(async (match) => {\n      let route = manifest.routes[match.route.id];\n      if (route) {\n        let mod = await loadRouteModule(route, routeModules);\n        return mod.links ? mod.links() : [];\n      }\n      return [];\n    }),\n  );\n\n  return dedupeLinkDescriptors(\n    links\n      .flat(1)\n      .filter(isHtmlLinkDescriptor)\n      .filter((link) => link.rel === \"stylesheet\" || link.rel === \"preload\")\n      .map((link) =>\n        link.rel === \"stylesheet\"\n          ? ({ ...link, rel: \"prefetch\", as: \"style\" } as HtmlLinkDescriptor)\n          : ({ ...link, rel: \"prefetch\" } as HtmlLinkDescriptor),\n      ),\n  );\n}\n\n// This is ridiculously identical to transition.ts `filterMatchesToLoad`\nexport function getNewMatchesForLinks(\n  page: string,\n  nextMatches: AgnosticDataRouteMatch[],\n  currentMatches: AgnosticDataRouteMatch[],\n  manifest: AssetsManifest,\n  location: Location,\n  mode: \"data\" | \"assets\",\n): AgnosticDataRouteMatch[] {\n  let isNew = (match: AgnosticDataRouteMatch, index: number) => {\n    if (!currentMatches[index]) return true;\n    return match.route.id !== currentMatches[index].route.id;\n  };\n\n  let matchPathChanged = (match: AgnosticDataRouteMatch, index: number) => {\n    return (\n      // param change, /users/123 -> /users/456\n      currentMatches[index].pathname !== match.pathname ||\n      // splat param changed, which is not present in match.path\n      // e.g. /files/images/avatar.jpg -> files/finances.xls\n      (currentMatches[index].route.path?.endsWith(\"*\") &&\n        currentMatches[index].params[\"*\"] !== match.params[\"*\"])\n    );\n  };\n\n  if (mode === \"assets\") {\n    return nextMatches.filter(\n      (match, index) => isNew(match, index) || matchPathChanged(match, index),\n    );\n  }\n\n  // NOTE: keep this mostly up-to-date w/ the router data diff, but this\n  // version doesn't care about submissions\n  // TODO: this is really similar to stuff in router.ts, maybe somebody smarter\n  // than me (or in less of a hurry) can share some of it. You're the best.\n  if (mode === \"data\") {\n    return nextMatches.filter((match, index) => {\n      let manifestRoute = manifest.routes[match.route.id];\n      if (!manifestRoute || !manifestRoute.hasLoader) {\n        return false;\n      }\n\n      if (isNew(match, index) || matchPathChanged(match, index)) {\n        return true;\n      }\n\n      if (match.route.shouldRevalidate) {\n        let routeChoice = match.route.shouldRevalidate({\n          currentUrl: new URL(\n            location.pathname + location.search + location.hash,\n            window.origin,\n          ),\n          currentParams: currentMatches[0]?.params || {},\n          nextUrl: new URL(page, window.origin),\n          nextParams: match.params,\n          defaultShouldRevalidate: true,\n        });\n        if (typeof routeChoice === \"boolean\") {\n          return routeChoice;\n        }\n      }\n      return true;\n    });\n  }\n\n  return [];\n}\n\nexport function getModuleLinkHrefs(\n  matches: AgnosticDataRouteMatch[],\n  manifest: AssetsManifest,\n  { includeHydrateFallback }: { includeHydrateFallback?: boolean } = {},\n): string[] {\n  return dedupeHrefs(\n    matches\n      .map((match) => {\n        let route = manifest.routes[match.route.id];\n        if (!route) return [];\n        let hrefs = [route.module];\n        if (route.clientActionModule) {\n          hrefs = hrefs.concat(route.clientActionModule);\n        }\n        if (route.clientLoaderModule) {\n          hrefs = hrefs.concat(route.clientLoaderModule);\n        }\n        if (includeHydrateFallback && route.hydrateFallbackModule) {\n          hrefs = hrefs.concat(route.hydrateFallbackModule);\n        }\n        if (route.imports) {\n          hrefs = hrefs.concat(route.imports);\n        }\n        return hrefs;\n      })\n      .flat(1),\n  );\n}\n\nfunction dedupeHrefs(hrefs: string[]): string[] {\n  return [...new Set(hrefs)];\n}\n\nfunction sortKeys<Obj extends { [Key in keyof Obj]: Obj[Key] }>(obj: Obj): Obj {\n  let sorted = {} as Obj;\n  let keys = Object.keys(obj).sort();\n\n  for (let key of keys) {\n    sorted[key as keyof Obj] = obj[key as keyof Obj];\n  }\n\n  return sorted;\n}\n\ntype KeyedLinkDescriptor<Descriptor extends LinkDescriptor = LinkDescriptor> = {\n  key: string;\n  link: Descriptor;\n};\n\nfunction dedupeLinkDescriptors<Descriptor extends LinkDescriptor>(\n  descriptors: Descriptor[],\n  preloads?: string[],\n): KeyedLinkDescriptor<Descriptor>[] {\n  let set = new Set();\n  let preloadsSet = new Set(preloads);\n\n  return descriptors.reduce((deduped, descriptor) => {\n    let alreadyModulePreload =\n      preloads &&\n      !isPageLinkDescriptor(descriptor) &&\n      descriptor.as === \"script\" &&\n      descriptor.href &&\n      preloadsSet.has(descriptor.href);\n\n    if (alreadyModulePreload) {\n      return deduped;\n    }\n\n    let key = JSON.stringify(sortKeys(descriptor));\n    if (!set.has(key)) {\n      set.add(key);\n      deduped.push({ key, link: descriptor });\n    }\n\n    return deduped;\n  }, [] as KeyedLinkDescriptor<Descriptor>[]);\n}\n\n// Detect if this browser supports <link rel=\"preload\"> (or has it enabled).\n// Originally added to handle the firefox `network.preload` config:\n//   https://bugzilla.mozilla.org/show_bug.cgi?id=1847811\nlet _isPreloadSupported: boolean | undefined;\nfunction isPreloadSupported(): boolean {\n  if (_isPreloadSupported !== undefined) {\n    return _isPreloadSupported;\n  }\n  let el: HTMLLinkElement | null = document.createElement(\"link\");\n  _isPreloadSupported = el.relList.supports(\"preload\");\n  el = null;\n  return _isPreloadSupported;\n}\n"
  },
  {
    "path": "packages/react-router/lib/dom/ssr/markup.ts",
    "content": "// This escapeHtml utility is based on https://github.com/zertosh/htmlescape\n// License: https://github.com/zertosh/htmlescape/blob/0527ca7156a524d256101bb310a9f970f63078ad/LICENSE\n\n// We've chosen to inline the utility here to reduce the number of npm dependencies we have,\n// slightly decrease the code size compared the original package and make it esm compatible.\n\nconst ESCAPE_LOOKUP: { [match: string]: string } = {\n  \"&\": \"\\\\u0026\",\n  \">\": \"\\\\u003e\",\n  \"<\": \"\\\\u003c\",\n  \"\\u2028\": \"\\\\u2028\",\n  \"\\u2029\": \"\\\\u2029\",\n};\n\nconst ESCAPE_REGEX = /[&><\\u2028\\u2029]/g;\n\nexport function escapeHtml(html: string) {\n  return html.replace(ESCAPE_REGEX, (match) => ESCAPE_LOOKUP[match]);\n}\n"
  },
  {
    "path": "packages/react-router/lib/dom/ssr/routeModules.ts",
    "content": "import type { ComponentType, ReactElement } from \"react\";\nimport type { Location } from \"../../router/history\";\nimport type {\n  ActionFunction,\n  ActionFunctionArgs,\n  LoaderFunction,\n  LoaderFunctionArgs,\n  MiddlewareFunction,\n  Params,\n  ShouldRevalidateFunction,\n  DataStrategyResult,\n} from \"../../router/utils\";\n\nimport type { EntryRoute } from \"./routes\";\nimport type { DataRouteMatch } from \"../../context\";\nimport type { LinkDescriptor } from \"../../router/links\";\nimport type { SerializeFrom } from \"../../types/route-data\";\n\nexport interface RouteModules {\n  [routeId: string]: RouteModule | undefined;\n}\n\n/**\n * The shape of a route module shipped to the client\n */\nexport interface RouteModule {\n  clientAction?: ClientActionFunction;\n  clientLoader?: ClientLoaderFunction;\n  clientMiddleware?: MiddlewareFunction<Record<string, DataStrategyResult>>[];\n  ErrorBoundary?: ErrorBoundaryComponent;\n  HydrateFallback?: HydrateFallbackComponent;\n  Layout?: LayoutComponent;\n  default: RouteComponent;\n  handle?: RouteHandle;\n  links?: LinksFunction;\n  meta?: MetaFunction;\n  shouldRevalidate?: ShouldRevalidateFunction;\n}\n\n/**\n * The shape of a route module on the server\n */\nexport interface ServerRouteModule extends RouteModule {\n  action?: ActionFunction;\n  headers?: HeadersFunction | { [name: string]: string };\n  loader?: LoaderFunction;\n  middleware?: MiddlewareFunction<Response>[];\n}\n\n/**\n * A function that handles data mutations for a route on the client\n */\nexport type ClientActionFunction = (\n  args: ClientActionFunctionArgs,\n) => ReturnType<ActionFunction>;\n\n/**\n * Arguments passed to a route `clientAction` function\n */\nexport type ClientActionFunctionArgs = ActionFunctionArgs & {\n  serverAction: <T = unknown>() => Promise<SerializeFrom<T>>;\n};\n\n/**\n * A function that loads data for a route on the client\n */\nexport type ClientLoaderFunction = ((\n  args: ClientLoaderFunctionArgs,\n) => ReturnType<LoaderFunction>) & {\n  hydrate?: boolean;\n};\n\n/**\n * Arguments passed to a route `clientLoader` function\n */\nexport type ClientLoaderFunctionArgs = LoaderFunctionArgs & {\n  serverLoader: <T = unknown>() => Promise<SerializeFrom<T>>;\n};\n\n/**\n * ErrorBoundary to display for this route\n */\nexport type ErrorBoundaryComponent = ComponentType;\n\nexport type HeadersArgs = {\n  loaderHeaders: Headers;\n  parentHeaders: Headers;\n  actionHeaders: Headers;\n  errorHeaders: Headers | undefined;\n};\n\n/**\n * A function that returns HTTP headers to be used for a route. These headers\n * will be merged with (and take precedence over) headers from parent routes.\n */\nexport interface HeadersFunction {\n  (args: HeadersArgs): Headers | HeadersInit;\n}\n\n/**\n * `<Route HydrateFallback>` component to render on initial loads\n * when client loaders are present\n */\nexport type HydrateFallbackComponent = ComponentType;\n\n/**\n * Optional, root-only `<Route Layout>` component to wrap the root content in.\n * Useful for defining the <html>/<head>/<body> document shell shared by the\n * Component, HydrateFallback, and ErrorBoundary\n */\nexport type LayoutComponent = ComponentType<{\n  children: ReactElement<\n    unknown,\n    ErrorBoundaryComponent | HydrateFallbackComponent | RouteComponent\n  >;\n}>;\n\n/**\n * A function that defines `<link>` tags to be inserted into the `<head>` of\n * the document on route transitions.\n *\n * @see https://reactrouter.com/start/framework/route-module#meta\n */\nexport interface LinksFunction {\n  (): LinkDescriptor[];\n}\n\nexport interface MetaMatch<\n  RouteId extends string = string,\n  Loader extends LoaderFunction | ClientLoaderFunction | unknown = unknown,\n> {\n  id: RouteId;\n  pathname: DataRouteMatch[\"pathname\"];\n  /** @deprecated Use `MetaMatch.loaderData` instead */\n  data: Loader extends LoaderFunction | ClientLoaderFunction\n    ? SerializeFrom<Loader>\n    : unknown;\n  loaderData: Loader extends LoaderFunction | ClientLoaderFunction\n    ? SerializeFrom<Loader>\n    : unknown;\n  handle?: RouteHandle;\n  params: DataRouteMatch[\"params\"];\n  meta: MetaDescriptor[];\n  error?: unknown;\n}\n\nexport type MetaMatches<\n  MatchLoaders extends Record<\n    string,\n    LoaderFunction | ClientLoaderFunction | unknown\n  > = Record<string, unknown>,\n> = Array<\n  {\n    [K in keyof MatchLoaders]: MetaMatch<\n      Exclude<K, number | symbol>,\n      MatchLoaders[K]\n    >;\n  }[keyof MatchLoaders]\n>;\n\nexport interface MetaArgs<\n  Loader extends LoaderFunction | ClientLoaderFunction | unknown = unknown,\n  MatchLoaders extends Record<\n    string,\n    LoaderFunction | ClientLoaderFunction | unknown\n  > = Record<string, unknown>,\n> {\n  /** @deprecated Use `MetaArgs.loaderData` instead */\n  data:\n    | (Loader extends LoaderFunction | ClientLoaderFunction\n        ? SerializeFrom<Loader>\n        : unknown)\n    | undefined;\n  loaderData:\n    | (Loader extends LoaderFunction | ClientLoaderFunction\n        ? SerializeFrom<Loader>\n        : unknown)\n    | undefined;\n  params: Params;\n  location: Location;\n  matches: MetaMatches<MatchLoaders>;\n  error?: unknown;\n}\n\n/**\n * A function that returns an array of data objects to use for rendering\n * metadata HTML tags in a route. These tags are not rendered on descendant\n * routes in the route hierarchy. In other words, they will only be rendered on\n * the route in which they are exported.\n *\n * @param Loader - The type of the current route's loader function\n * @param MatchLoaders - Mapping from a parent route's filepath to its loader\n * function type\n *\n * Note that parent route filepaths are relative to the `app/` directory.\n *\n * For example, if this meta function is for `/sales/customers/$customerId`:\n *\n * ```ts\n * // app/root.tsx\n * const loader = () => ({ hello: \"world\" })\n * export type Loader = typeof loader\n *\n * // app/routes/sales.tsx\n * const loader = () => ({ salesCount: 1074 })\n * export type Loader = typeof loader\n *\n * // app/routes/sales/customers.tsx\n * const loader = () => ({ customerCount: 74 })\n * export type Loader = typeof loader\n *\n * // app/routes/sales/customers/$customersId.tsx\n * import type { Loader as RootLoader } from \"../../../root\"\n * import type { Loader as SalesLoader } from \"../../sales\"\n * import type { Loader as CustomersLoader } from \"../../sales/customers\"\n *\n * const loader = () => ({ name: \"Customer name\" })\n *\n * const meta: MetaFunction<typeof loader, {\n *  \"root\": RootLoader,\n *  \"routes/sales\": SalesLoader,\n *  \"routes/sales/customers\": CustomersLoader,\n * }> = ({ data, matches }) => {\n *   const { name } = data\n *   //      ^? string\n *   const { customerCount } = matches.find((match) => match.id === \"routes/sales/customers\").data\n *   //      ^? number\n *   const { salesCount } = matches.find((match) => match.id === \"routes/sales\").data\n *   //      ^? number\n *   const { hello } = matches.find((match) => match.id === \"root\").data\n *   //      ^? \"world\"\n * }\n * ```\n */\nexport interface MetaFunction<\n  Loader extends LoaderFunction | ClientLoaderFunction | unknown = unknown,\n  MatchLoaders extends Record<\n    string,\n    LoaderFunction | ClientLoaderFunction | unknown\n  > = Record<string, unknown>,\n> {\n  (args: MetaArgs<Loader, MatchLoaders>): MetaDescriptor[] | undefined;\n}\n\nexport type MetaDescriptor =\n  | { charSet: \"utf-8\" }\n  | { title: string }\n  | { name: string; content: string }\n  | { property: string; content: string }\n  | { httpEquiv: string; content: string }\n  | { \"script:ld+json\": LdJsonObject }\n  | { tagName: \"meta\" | \"link\"; [name: string]: string }\n  | { [name: string]: unknown };\n\ntype LdJsonObject = { [Key in string]: LdJsonValue } & {\n  [Key in string]?: LdJsonValue | undefined;\n};\ntype LdJsonArray = LdJsonValue[] | readonly LdJsonValue[];\ntype LdJsonPrimitive = string | number | boolean | null;\ntype LdJsonValue = LdJsonPrimitive | LdJsonObject | LdJsonArray;\n\n/**\n * A React component that is rendered for a route.\n */\nexport type RouteComponent = ComponentType<{}>;\n\n/**\n * An arbitrary object that is associated with a route.\n *\n * @see https://reactrouter.com/how-to/using-handle\n */\nexport type RouteHandle = unknown;\n\nexport async function loadRouteModule(\n  route: EntryRoute,\n  routeModulesCache: RouteModules,\n): Promise<RouteModule> {\n  if (route.id in routeModulesCache) {\n    return routeModulesCache[route.id] as RouteModule;\n  }\n\n  try {\n    let routeModule = await import(\n      /* @vite-ignore */\n      /* webpackIgnore: true */\n      route.module\n    );\n    routeModulesCache[route.id] = routeModule;\n    return routeModule;\n  } catch (error: unknown) {\n    // If we can't load the route it's likely one of 2 things:\n    // - User got caught in the middle of a deploy and the CDN no longer has the\n    //   asset we're trying to import! Reload from the server and the user\n    //   (should) get the new manifest--unless the developer purged the static\n    //   assets, the manifest path, but not the documents 😬\n    // - Or, the asset trying to be imported has an error (usually in vite dev\n    //   mode), so the best we can do here is log the error for visibility\n    //   (via `Preserve log`) and reload\n\n    // Log the error so it can be accessed via the `Preserve Log` setting\n    console.error(\n      `Error loading route module \\`${route.module}\\`, reloading page...`,\n    );\n    console.error(error);\n\n    if (\n      window.__reactRouterContext &&\n      window.__reactRouterContext.isSpaMode &&\n      // @ts-expect-error\n      import.meta.hot\n    ) {\n      // In SPA Mode (which implies vite) we don't want to perform a hard reload\n      // on dev-time errors since it's a vite compilation error and a reload is\n      // just going to fail with the same issue.  Let the UI bubble to the error\n      // boundary and let them see the error in the overlay or the dev server log\n      throw error;\n    }\n\n    window.location.reload();\n\n    return new Promise(() => {\n      // check out of this hook cause the DJs never gonna re[s]olve this\n    });\n  }\n}\n"
  },
  {
    "path": "packages/react-router/lib/dom/ssr/routes-test-stub.tsx",
    "content": "import * as React from \"react\";\nimport type {\n  ActionFunction,\n  ActionFunctionArgs,\n  LoaderFunction,\n  LoaderFunctionArgs,\n  MiddlewareFunction,\n} from \"../../router/utils\";\nimport type {\n  DataRouteObject,\n  IndexRouteObject,\n  NonIndexRouteObject,\n} from \"../../context\";\nimport type { LinksFunction, MetaFunction, RouteModules } from \"./routeModules\";\nimport type { InitialEntry } from \"../../router/history\";\nimport type { HydrationState } from \"../../router/router\";\nimport {\n  convertRoutesToDataRoutes,\n  RouterContextProvider,\n} from \"../../router/utils\";\nimport type { MiddlewareEnabled } from \"../../types/future\";\nimport type { AppLoadContext } from \"../../server-runtime/data\";\nimport type {\n  AssetsManifest,\n  FutureConfig,\n  FrameworkContextObject,\n} from \"./entry\";\nimport {\n  type RouteComponentType,\n  type HydrateFallbackType,\n  type ErrorBoundaryType,\n  Outlet,\n  RouterProvider,\n  createMemoryRouter,\n  withComponentProps,\n  withErrorBoundaryProps,\n  withHydrateFallbackProps,\n} from \"../../components\";\nimport type { EntryRoute } from \"./routes\";\nimport { FrameworkContext } from \"./components\";\n\ninterface StubRouteExtensions {\n  Component?: RouteComponentType;\n  HydrateFallback?: HydrateFallbackType;\n  ErrorBoundary?: ErrorBoundaryType;\n  loader?: LoaderFunction;\n  action?: ActionFunction;\n  children?: StubRouteObject[];\n  meta?: MetaFunction;\n  links?: LinksFunction;\n}\n\ninterface StubIndexRouteObject\n  extends Omit<\n      IndexRouteObject,\n      | \"Component\"\n      | \"HydrateFallback\"\n      | \"ErrorBoundary\"\n      | \"loader\"\n      | \"action\"\n      | \"element\"\n      | \"errorElement\"\n      | \"children\"\n    >,\n    StubRouteExtensions {}\n\ninterface StubNonIndexRouteObject\n  extends Omit<\n      NonIndexRouteObject,\n      | \"Component\"\n      | \"HydrateFallback\"\n      | \"ErrorBoundary\"\n      | \"loader\"\n      | \"action\"\n      | \"element\"\n      | \"errorElement\"\n      | \"children\"\n    >,\n    StubRouteExtensions {}\n\ntype StubRouteObject = StubIndexRouteObject | StubNonIndexRouteObject;\n\nexport interface RoutesTestStubProps {\n  /**\n   *  The initial entries in the history stack. This allows you to start a test with\n   *  multiple locations already in the history stack (for testing a back navigation, etc.)\n   *  The test will default to the last entry in initialEntries if no initialIndex is provided.\n   *  e.g. initialEntries={[\"/home\", \"/about\", \"/contact\"]}\n   */\n  initialEntries?: InitialEntry[];\n\n  /**\n   * The initial index in the history stack to render. This allows you to start a test at a specific entry.\n   * It defaults to the last entry in initialEntries.\n   * e.g.\n   *   initialEntries: [\"/\", \"/events/123\"]\n   *   initialIndex: 1 // start at \"/events/123\"\n   */\n  initialIndex?: number;\n\n  /**\n   *  Used to set the route's initial loader and action data.\n   *  e.g. hydrationData={{\n   *   loaderData: { \"/contact\": { locale: \"en-US\" } },\n   *   actionData: { \"/login\": { errors: { email: \"invalid email\" } }}\n   *  }}\n   */\n  hydrationData?: HydrationState;\n\n  /**\n   * Future flags mimicking the settings in react-router.config.ts\n   */\n  future?: Partial<FutureConfig>;\n}\n\n/**\n * @category Utils\n */\nexport function createRoutesStub(\n  routes: StubRouteObject[],\n  _context?: AppLoadContext | RouterContextProvider,\n) {\n  return function RoutesTestStub({\n    initialEntries,\n    initialIndex,\n    hydrationData,\n    future,\n  }: RoutesTestStubProps) {\n    let routerRef = React.useRef<ReturnType<typeof createMemoryRouter>>();\n    let frameworkContextRef = React.useRef<FrameworkContextObject>();\n\n    if (routerRef.current == null) {\n      frameworkContextRef.current = {\n        future: {\n          unstable_subResourceIntegrity:\n            future?.unstable_subResourceIntegrity === true,\n          v8_middleware: future?.v8_middleware === true,\n          unstable_trailingSlashAwareDataRequests:\n            future?.unstable_trailingSlashAwareDataRequests === true,\n        },\n        manifest: {\n          routes: {},\n          entry: { imports: [], module: \"\" },\n          url: \"\",\n          version: \"\",\n        },\n        routeModules: {},\n        ssr: false,\n        isSpaMode: false,\n        routeDiscovery: { mode: \"lazy\", manifestPath: \"/__manifest\" },\n      };\n\n      // Update the routes to include context in the loader/action and populate\n      // the manifest and routeModules during the walk\n      let patched = processRoutes(\n        // @ts-expect-error `StubRouteObject` is stricter about `loader`/`action`\n        // types compared to `AgnosticRouteObject`\n        convertRoutesToDataRoutes(routes, (r) => r),\n        _context !== undefined\n          ? _context\n          : future?.v8_middleware\n            ? new RouterContextProvider()\n            : {},\n        frameworkContextRef.current.manifest,\n        frameworkContextRef.current.routeModules,\n      );\n      routerRef.current = createMemoryRouter(patched, {\n        initialEntries,\n        initialIndex,\n        hydrationData,\n      });\n    }\n\n    return (\n      <FrameworkContext.Provider value={frameworkContextRef.current}>\n        <RouterProvider router={routerRef.current} />\n      </FrameworkContext.Provider>\n    );\n  };\n}\n\nfunction processRoutes(\n  routes: StubRouteObject[],\n  context: unknown,\n  manifest: AssetsManifest,\n  routeModules: RouteModules,\n  parentId?: string,\n): DataRouteObject[] {\n  return routes.map((route) => {\n    if (!route.id) {\n      throw new Error(\n        \"Expected a route.id in react-router processRoutes() function\",\n      );\n    }\n\n    let newRoute: DataRouteObject = {\n      id: route.id,\n      path: route.path,\n      index: route.index,\n      Component: route.Component\n        ? withComponentProps(route.Component)\n        : undefined,\n      HydrateFallback: route.HydrateFallback\n        ? withHydrateFallbackProps(route.HydrateFallback)\n        : undefined,\n      ErrorBoundary: route.ErrorBoundary\n        ? withErrorBoundaryProps(route.ErrorBoundary)\n        : undefined,\n      action: route.action\n        ? (args: ActionFunctionArgs) => route.action!({ ...args, context })\n        : undefined,\n      loader: route.loader\n        ? (args: LoaderFunctionArgs) => route.loader!({ ...args, context })\n        : undefined,\n      middleware: route.middleware\n        ? route.middleware.map(\n            (mw) =>\n              (...args: Parameters<MiddlewareFunction>) =>\n                mw(\n                  { ...args[0], context: context as RouterContextProvider },\n                  args[1],\n                ),\n          )\n        : undefined,\n      handle: route.handle,\n      shouldRevalidate: route.shouldRevalidate,\n    };\n\n    // Add the EntryRoute to the manifest\n    let entryRoute: EntryRoute = {\n      id: route.id,\n      path: route.path,\n      index: route.index,\n      parentId,\n      hasAction: route.action != null,\n      hasLoader: route.loader != null,\n      // When testing routes, you should be stubbing loader/action/middleware,\n      // not trying to re-implement the full loader/clientLoader/SSR/hydration\n      // flow. That is better tested via E2E tests.\n      hasClientAction: false,\n      hasClientLoader: false,\n      hasClientMiddleware: false,\n      hasErrorBoundary: route.ErrorBoundary != null,\n      // any need for these?\n      module: \"build/stub-path-to-module.js\",\n      clientActionModule: undefined,\n      clientLoaderModule: undefined,\n      clientMiddlewareModule: undefined,\n      hydrateFallbackModule: undefined,\n    };\n    manifest.routes[newRoute.id] = entryRoute;\n\n    // Add the route to routeModules\n    routeModules[route.id] = {\n      default: newRoute.Component || Outlet,\n      ErrorBoundary: newRoute.ErrorBoundary || undefined,\n      handle: route.handle,\n      links: route.links,\n      meta: route.meta,\n      shouldRevalidate: route.shouldRevalidate,\n    };\n\n    if (route.children) {\n      newRoute.children = processRoutes(\n        route.children,\n        context,\n        manifest,\n        routeModules,\n        newRoute.id,\n      );\n    }\n\n    return newRoute;\n  });\n}\n"
  },
  {
    "path": "packages/react-router/lib/dom/ssr/routes.tsx",
    "content": "import * as React from \"react\";\n\nimport type { HydrationState } from \"../../router/router\";\nimport type {\n  ActionFunctionArgs,\n  LoaderFunctionArgs,\n  RouteManifest,\n  ShouldRevalidateFunction,\n  ShouldRevalidateFunctionArgs,\n} from \"../../router/utils\";\nimport { ErrorResponseImpl, compilePath } from \"../../router/utils\";\nimport type {\n  ClientLoaderFunction,\n  RouteModule,\n  RouteModules,\n} from \"./routeModules\";\nimport { loadRouteModule } from \"./routeModules\";\nimport type { FutureConfig } from \"./entry\";\nimport { prefetchRouteCss, prefetchStyleLinks } from \"./links\";\nimport { RemixRootDefaultErrorBoundary } from \"./errorBoundaries\";\nimport { RemixRootDefaultHydrateFallback } from \"./fallback\";\nimport invariant from \"./invariant\";\nimport { useRouteError } from \"../../hooks\";\nimport type { DataRouteObject } from \"../../context\";\n\nexport interface Route {\n  index?: boolean;\n  caseSensitive?: boolean;\n  id: string;\n  parentId?: string;\n  path?: string;\n}\n\nexport interface EntryRoute extends Route {\n  hasAction: boolean;\n  hasLoader: boolean;\n  hasClientAction: boolean;\n  hasClientLoader: boolean;\n  hasClientMiddleware: boolean;\n  hasErrorBoundary: boolean;\n  imports?: string[];\n  css?: string[];\n  module: string;\n  clientActionModule: string | undefined;\n  clientLoaderModule: string | undefined;\n  clientMiddlewareModule: string | undefined;\n  hydrateFallbackModule: string | undefined;\n  parentId?: string;\n}\n\n// Create a map of routes by parentId to use recursively instead of\n// repeatedly filtering the manifest.\nfunction groupRoutesByParentId(manifest: RouteManifest<EntryRoute>) {\n  let routes: Record<string, Omit<EntryRoute, \"children\">[]> = {};\n\n  Object.values(manifest).forEach((route) => {\n    if (route) {\n      let parentId = route.parentId || \"\";\n      if (!routes[parentId]) {\n        routes[parentId] = [];\n      }\n      routes[parentId].push(route);\n    }\n  });\n\n  return routes;\n}\n\nfunction getRouteComponents(\n  route: EntryRoute,\n  routeModule: RouteModule,\n  isSpaMode: boolean,\n) {\n  let Component = getRouteModuleComponent(routeModule);\n  // HydrateFallback can only exist on the root route in SPA Mode\n  let HydrateFallback =\n    routeModule.HydrateFallback && (!isSpaMode || route.id === \"root\")\n      ? routeModule.HydrateFallback\n      : route.id === \"root\"\n        ? RemixRootDefaultHydrateFallback\n        : undefined;\n  let ErrorBoundary = routeModule.ErrorBoundary\n    ? routeModule.ErrorBoundary\n    : route.id === \"root\"\n      ? () => <RemixRootDefaultErrorBoundary error={useRouteError()} />\n      : undefined;\n\n  if (route.id === \"root\" && routeModule.Layout) {\n    return {\n      ...(Component\n        ? {\n            element: (\n              <routeModule.Layout>\n                <Component />\n              </routeModule.Layout>\n            ),\n          }\n        : { Component }),\n      ...(ErrorBoundary\n        ? {\n            errorElement: (\n              <routeModule.Layout>\n                <ErrorBoundary />\n              </routeModule.Layout>\n            ),\n          }\n        : { ErrorBoundary }),\n      ...(HydrateFallback\n        ? {\n            hydrateFallbackElement: (\n              <routeModule.Layout>\n                <HydrateFallback />\n              </routeModule.Layout>\n            ),\n          }\n        : { HydrateFallback }),\n    };\n  }\n\n  return { Component, ErrorBoundary, HydrateFallback };\n}\n\nexport function createServerRoutes(\n  manifest: RouteManifest<EntryRoute>,\n  routeModules: RouteModules,\n  future: FutureConfig,\n  isSpaMode: boolean,\n  parentId: string = \"\",\n  routesByParentId: Record<\n    string,\n    Omit<EntryRoute, \"children\">[]\n  > = groupRoutesByParentId(manifest),\n  spaModeLazyPromise = Promise.resolve({ Component: () => null }),\n): DataRouteObject[] {\n  return (routesByParentId[parentId] || []).map((route) => {\n    let routeModule = routeModules[route.id];\n    invariant(\n      routeModule,\n      \"No `routeModule` available to create server routes\",\n    );\n\n    let dataRoute: DataRouteObject = {\n      ...getRouteComponents(route, routeModule, isSpaMode),\n      caseSensitive: route.caseSensitive,\n      id: route.id,\n      index: route.index,\n      path: route.path,\n      handle: routeModule.handle,\n      // For SPA Mode, all routes are lazy except root.  However we tell the\n      // router root is also lazy here too since we don't need a full\n      // implementation - we just need a `lazy` prop to tell the RR rendering\n      // where to stop which is always at the root route in SPA mode\n      lazy: isSpaMode ? () => spaModeLazyPromise : undefined,\n      // For partial hydration rendering, we need to indicate when the route\n      // has a loader/clientLoader, but it won't ever be called during the static\n      // render, so just give it a no-op function so we can render down to the\n      // proper fallback\n      loader: route.hasLoader || route.hasClientLoader ? () => null : undefined,\n      // We don't need middleware/action/shouldRevalidate on these routes since\n      // they're for a static render\n    };\n\n    let children = createServerRoutes(\n      manifest,\n      routeModules,\n      future,\n      isSpaMode,\n      route.id,\n      routesByParentId,\n      spaModeLazyPromise,\n    );\n    if (children.length > 0) dataRoute.children = children;\n    return dataRoute;\n  });\n}\n\nexport function createClientRoutesWithHMRRevalidationOptOut(\n  needsRevalidation: Set<string>,\n  manifest: RouteManifest<EntryRoute>,\n  routeModulesCache: RouteModules,\n  initialState: HydrationState,\n  ssr: boolean,\n  isSpaMode: boolean,\n) {\n  return createClientRoutes(\n    manifest,\n    routeModulesCache,\n    initialState,\n    ssr,\n    isSpaMode,\n    \"\",\n    groupRoutesByParentId(manifest),\n    needsRevalidation,\n  );\n}\n\nfunction preventInvalidServerHandlerCall(\n  type: \"action\" | \"loader\",\n  route: Omit<EntryRoute, \"children\">,\n) {\n  if (\n    (type === \"loader\" && !route.hasLoader) ||\n    (type === \"action\" && !route.hasAction)\n  ) {\n    let fn = type === \"action\" ? \"serverAction()\" : \"serverLoader()\";\n    let msg =\n      `You are trying to call ${fn} on a route that does not have a server ` +\n      `${type} (routeId: \"${route.id}\")`;\n    console.error(msg);\n    throw new ErrorResponseImpl(400, \"Bad Request\", new Error(msg), true);\n  }\n}\n\nexport function noActionDefinedError(\n  type: \"action\" | \"clientAction\",\n  routeId: string,\n) {\n  let article = type === \"clientAction\" ? \"a\" : \"an\";\n  let msg =\n    `Route \"${routeId}\" does not have ${article} ${type}, but you are trying to ` +\n    `submit to it. To fix this, please add ${article} \\`${type}\\` function to the route`;\n  console.error(msg);\n  throw new ErrorResponseImpl(405, \"Method Not Allowed\", new Error(msg), true);\n}\n\nexport function createClientRoutes(\n  manifest: RouteManifest<EntryRoute>,\n  routeModulesCache: RouteModules,\n  initialState: HydrationState | null,\n  ssr: boolean,\n  isSpaMode: boolean,\n  parentId: string = \"\",\n  routesByParentId: Record<\n    string,\n    Omit<EntryRoute, \"children\">[]\n  > = groupRoutesByParentId(manifest),\n  needsRevalidation?: Set<string>,\n): DataRouteObject[] {\n  return (routesByParentId[parentId] || []).map((route) => {\n    let routeModule = routeModulesCache[route.id];\n\n    function fetchServerHandler(singleFetch: unknown) {\n      invariant(\n        typeof singleFetch === \"function\",\n        \"No single fetch function available for route handler\",\n      );\n      return singleFetch();\n    }\n\n    function fetchServerLoader(singleFetch: unknown) {\n      if (!route.hasLoader) return Promise.resolve(null);\n      return fetchServerHandler(singleFetch);\n    }\n\n    function fetchServerAction(singleFetch: unknown) {\n      if (!route.hasAction) {\n        throw noActionDefinedError(\"action\", route.id);\n      }\n      return fetchServerHandler(singleFetch);\n    }\n\n    function prefetchModule(modulePath: string) {\n      import(\n        /* @vite-ignore */\n        /* webpackIgnore: true */\n        modulePath\n      );\n    }\n\n    function prefetchRouteModuleChunks(route: EntryRoute) {\n      // We fetch the client action module first since the loader function we\n      // create internally already handles the client loader. This function is\n      // most useful in cases where only the client action is splittable, but is\n      // also useful for prefetching the client loader module if a client action\n      // is triggered from another route.\n      if (route.clientActionModule) {\n        prefetchModule(route.clientActionModule);\n      }\n      // Also prefetch the client loader module if it exists\n      // since it's called after the client action\n      if (route.clientLoaderModule) {\n        prefetchModule(route.clientLoaderModule);\n      }\n    }\n\n    async function prefetchStylesAndCallHandler(\n      handler: () => Promise<unknown>,\n    ) {\n      // Only prefetch links if we exist in the routeModulesCache (critical modules\n      // and navigating back to pages previously loaded via route.lazy).  Initial\n      // execution of route.lazy (when the module is not in the cache) will handle\n      // prefetching style links via loadRouteModuleWithBlockingLinks.\n      let cachedModule = routeModulesCache[route.id];\n      let linkPrefetchPromise = cachedModule\n        ? prefetchStyleLinks(route, cachedModule)\n        : Promise.resolve();\n      try {\n        return handler();\n      } finally {\n        await linkPrefetchPromise;\n      }\n    }\n\n    let dataRoute: DataRouteObject = {\n      id: route.id,\n      index: route.index,\n      path: route.path,\n    };\n\n    if (routeModule) {\n      // Use critical path modules directly\n      Object.assign(dataRoute, {\n        ...dataRoute,\n        ...getRouteComponents(route, routeModule, isSpaMode),\n        middleware: routeModule.clientMiddleware,\n        handle: routeModule.handle,\n        shouldRevalidate: getShouldRevalidateFunction(\n          dataRoute.path,\n          routeModule,\n          route,\n          ssr,\n          needsRevalidation,\n        ),\n      });\n\n      let hasInitialData =\n        initialState &&\n        initialState.loaderData &&\n        route.id in initialState.loaderData;\n      let initialData = hasInitialData\n        ? initialState?.loaderData?.[route.id]\n        : undefined;\n      let hasInitialError =\n        initialState && initialState.errors && route.id in initialState.errors;\n      let initialError = hasInitialError\n        ? initialState?.errors?.[route.id]\n        : undefined;\n      let isHydrationRequest =\n        needsRevalidation == null &&\n        (routeModule.clientLoader?.hydrate === true || !route.hasLoader);\n\n      dataRoute.loader = async (\n        { request, params, context, unstable_pattern }: LoaderFunctionArgs,\n        singleFetch?: unknown,\n      ) => {\n        try {\n          let result = await prefetchStylesAndCallHandler(async () => {\n            invariant(\n              routeModule,\n              \"No `routeModule` available for critical-route loader\",\n            );\n            if (!routeModule.clientLoader) {\n              // Call the server when no client loader exists\n              return fetchServerLoader(singleFetch);\n            }\n\n            return routeModule.clientLoader({\n              request,\n              params,\n              context,\n              unstable_pattern,\n              async serverLoader() {\n                preventInvalidServerHandlerCall(\"loader\", route);\n\n                // On the first call, resolve with the server result\n                if (isHydrationRequest) {\n                  if (hasInitialData) {\n                    return initialData;\n                  }\n                  if (hasInitialError) {\n                    throw initialError;\n                  }\n                }\n\n                // Call the server loader for client-side navigations\n                return fetchServerLoader(singleFetch);\n              },\n            });\n          });\n          return result;\n        } finally {\n          // Whether or not the user calls `serverLoader`, we only let this\n          // stick around as true for one loader call\n          isHydrationRequest = false;\n        }\n      };\n\n      // Let React Router know whether to run this on hydration\n      dataRoute.loader.hydrate = shouldHydrateRouteLoader(\n        route.id,\n        routeModule.clientLoader,\n        route.hasLoader,\n        isSpaMode,\n      );\n\n      dataRoute.action = (\n        { request, params, context, unstable_pattern }: ActionFunctionArgs,\n        singleFetch?: unknown,\n      ) => {\n        return prefetchStylesAndCallHandler(async () => {\n          invariant(\n            routeModule,\n            \"No `routeModule` available for critical-route action\",\n          );\n          if (!routeModule.clientAction) {\n            if (isSpaMode) {\n              throw noActionDefinedError(\"clientAction\", route.id);\n            }\n            return fetchServerAction(singleFetch);\n          }\n\n          return routeModule.clientAction({\n            request,\n            params,\n            context,\n            unstable_pattern,\n            async serverAction() {\n              preventInvalidServerHandlerCall(\"action\", route);\n              return fetchServerAction(singleFetch);\n            },\n          });\n        });\n      };\n    } else {\n      // If the lazy route does not have a client loader/action we want to call\n      // the server loader/action in parallel with the module load so we add\n      // loader/action as static props on the route\n      if (!route.hasClientLoader) {\n        dataRoute.loader = (_: LoaderFunctionArgs, singleFetch?: unknown) =>\n          prefetchStylesAndCallHandler(() => {\n            return fetchServerLoader(singleFetch);\n          });\n      }\n      if (!route.hasClientAction) {\n        dataRoute.action = (_: ActionFunctionArgs, singleFetch?: unknown) =>\n          prefetchStylesAndCallHandler(() => {\n            if (isSpaMode) {\n              throw noActionDefinedError(\"clientAction\", route.id);\n            }\n            return fetchServerAction(singleFetch);\n          });\n      }\n\n      let lazyRoutePromise:\n        | ReturnType<typeof loadRouteModuleWithBlockingLinks>\n        | undefined;\n      async function getLazyRoute() {\n        if (lazyRoutePromise) {\n          return await lazyRoutePromise;\n        }\n        lazyRoutePromise = (async () => {\n          if (route.clientLoaderModule || route.clientActionModule) {\n            // If a client loader/action chunk is present, we push the loading of\n            // the main route chunk to the next tick to ensure the downloading of\n            // loader/action chunks takes precedence. This can be seen via their\n            // order in the network tab. Also note that since this is happening\n            // within `route.lazy`, this imperceptible delay only happens on the\n            // first load of this route.\n            await new Promise((resolve) => setTimeout(resolve, 0));\n          }\n\n          let routeModulePromise = loadRouteModuleWithBlockingLinks(\n            route,\n            routeModulesCache,\n          );\n          prefetchRouteModuleChunks(route);\n          return await routeModulePromise;\n        })();\n        return await lazyRoutePromise;\n      }\n\n      dataRoute.lazy = {\n        loader: route.hasClientLoader\n          ? async () => {\n              let { clientLoader } = route.clientLoaderModule\n                ? await import(\n                    /* @vite-ignore */\n                    /* webpackIgnore: true */\n                    route.clientLoaderModule\n                  )\n                : await getLazyRoute();\n              invariant(clientLoader, \"No `clientLoader` export found\");\n              return (args: LoaderFunctionArgs, singleFetch?: unknown) =>\n                clientLoader({\n                  ...args,\n                  async serverLoader() {\n                    preventInvalidServerHandlerCall(\"loader\", route);\n                    return fetchServerLoader(singleFetch);\n                  },\n                });\n            }\n          : undefined,\n        action: route.hasClientAction\n          ? async () => {\n              let clientActionPromise = route.clientActionModule\n                ? import(\n                    /* @vite-ignore */\n                    /* webpackIgnore: true */\n                    route.clientActionModule\n                  )\n                : getLazyRoute();\n              prefetchRouteModuleChunks(route);\n              let { clientAction } = await clientActionPromise;\n              invariant(clientAction, \"No `clientAction` export found\");\n              return (args: ActionFunctionArgs, singleFetch?: unknown) =>\n                clientAction({\n                  ...args,\n                  async serverAction() {\n                    preventInvalidServerHandlerCall(\"action\", route);\n                    return fetchServerAction(singleFetch);\n                  },\n                });\n            }\n          : undefined,\n        middleware: route.hasClientMiddleware\n          ? async () => {\n              let { clientMiddleware } = route.clientMiddlewareModule\n                ? await import(\n                    /* @vite-ignore */\n                    /* webpackIgnore: true */\n                    route.clientMiddlewareModule\n                  )\n                : await getLazyRoute();\n              invariant(clientMiddleware, \"No `clientMiddleware` export found\");\n              return clientMiddleware;\n            }\n          : undefined,\n        shouldRevalidate: async () => {\n          let lazyRoute = await getLazyRoute();\n          return getShouldRevalidateFunction(\n            dataRoute.path,\n            lazyRoute,\n            route,\n            ssr,\n            needsRevalidation,\n          );\n        },\n        handle: async () => (await getLazyRoute()).handle,\n        // No need to wrap these in layout since the root route is never\n        // loaded via route.lazy()\n        Component: async () => (await getLazyRoute()).Component,\n        ErrorBoundary: route.hasErrorBoundary\n          ? async () => (await getLazyRoute()).ErrorBoundary\n          : undefined,\n      };\n    }\n\n    let children = createClientRoutes(\n      manifest,\n      routeModulesCache,\n      initialState,\n      ssr,\n      isSpaMode,\n      route.id,\n      routesByParentId,\n      needsRevalidation,\n    );\n    if (children.length > 0) dataRoute.children = children;\n    return dataRoute;\n  });\n}\n\nfunction getShouldRevalidateFunction(\n  path: string | undefined,\n  route: Partial<DataRouteObject>,\n  manifestRoute: Omit<EntryRoute, \"children\">,\n  ssr: boolean,\n  needsRevalidation: Set<string> | undefined,\n) {\n  // During HDR we force revalidation for updated routes\n  if (needsRevalidation) {\n    return wrapShouldRevalidateForHdr(\n      manifestRoute.id,\n      route.shouldRevalidate,\n      needsRevalidation,\n    );\n  }\n\n  // When prerendering is enabled with `ssr:false`, any `loader` data is\n  // statically generated at build time so if we have a `loader` but not a\n  // `clientLoader`, we only revalidate if the route's params changed since we\n  // can't be sure if a `.data` file was pre-rendered otherwise.\n  //\n  // I.e., If I have a parent and a child route and I only prerender `/parent`,\n  // we can't have parent revalidate when going from `/parent -> /parent/child`\n  // because `/parent/child.data` doesn't exist.\n  //\n  // If users are somehow re-generating updated versions of these on the backend\n  // they can still opt-into revalidation which will make the `.data` request\n  if (!ssr && manifestRoute.hasLoader && !manifestRoute.hasClientLoader) {\n    let myParams = path ? compilePath(path)[1].map((p) => p.paramName) : [];\n    const didParamsChange = (opts: ShouldRevalidateFunctionArgs) =>\n      myParams.some((p) => opts.currentParams[p] !== opts.nextParams[p]);\n\n    if (route.shouldRevalidate) {\n      let fn = route.shouldRevalidate;\n      return (opts: ShouldRevalidateFunctionArgs) =>\n        fn({\n          ...opts,\n          defaultShouldRevalidate: didParamsChange(opts),\n        });\n    } else {\n      return (opts: ShouldRevalidateFunctionArgs) => didParamsChange(opts);\n    }\n  }\n\n  return route.shouldRevalidate;\n}\n\n// When an HMR / HDR update happens we opt out of all user-defined\n// revalidation logic and force a revalidation on the first call\nfunction wrapShouldRevalidateForHdr(\n  routeId: string,\n  routeShouldRevalidate: ShouldRevalidateFunction | undefined,\n  needsRevalidation: Set<string>,\n): ShouldRevalidateFunction {\n  let handledRevalidation = false;\n  return (arg) => {\n    if (!handledRevalidation) {\n      handledRevalidation = true;\n      return needsRevalidation.has(routeId);\n    }\n\n    return routeShouldRevalidate\n      ? routeShouldRevalidate(arg)\n      : arg.defaultShouldRevalidate;\n  };\n}\n\nasync function loadRouteModuleWithBlockingLinks(\n  route: EntryRoute,\n  routeModules: RouteModules,\n) {\n  // Ensure the route module and its static CSS links are loaded in parallel as\n  // soon as possible before blocking on the route module\n  let routeModulePromise = loadRouteModule(route, routeModules);\n  let prefetchRouteCssPromise = prefetchRouteCss(route);\n\n  let routeModule = await routeModulePromise;\n  await Promise.all([\n    prefetchRouteCssPromise,\n    prefetchStyleLinks(route, routeModule),\n  ]);\n\n  // Include all `browserSafeRouteExports` fields, except `HydrateFallback`\n  // since those aren't used on lazily loaded routes\n  return {\n    Component: getRouteModuleComponent(routeModule),\n    ErrorBoundary: routeModule.ErrorBoundary,\n    clientMiddleware: routeModule.clientMiddleware,\n    clientAction: routeModule.clientAction,\n    clientLoader: routeModule.clientLoader,\n    handle: routeModule.handle,\n    links: routeModule.links,\n    meta: routeModule.meta,\n    shouldRevalidate: routeModule.shouldRevalidate,\n  };\n}\n\n// Our compiler generates the default export as `{}` when no default is provided,\n// which can lead us to trying to use that as a Component in RR and calling\n// createElement on it.  Patching here as a quick fix and hoping it's no longer\n// an issue in Vite.\nfunction getRouteModuleComponent(routeModule: RouteModule) {\n  if (routeModule.default == null) return undefined;\n  let isEmptyObject =\n    typeof routeModule.default === \"object\" &&\n    Object.keys(routeModule.default).length === 0;\n  if (!isEmptyObject) {\n    return routeModule.default;\n  }\n}\n\nexport function shouldHydrateRouteLoader(\n  routeId: string,\n  clientLoader: ClientLoaderFunction | undefined,\n  hasLoader: boolean,\n  isSpaMode: boolean,\n) {\n  return (\n    (isSpaMode && routeId !== \"root\") ||\n    (clientLoader != null &&\n      (clientLoader.hydrate === true || hasLoader !== true))\n  );\n}\n"
  },
  {
    "path": "packages/react-router/lib/dom/ssr/server.tsx",
    "content": "import type { ReactElement } from \"react\";\nimport * as React from \"react\";\n\nimport { createStaticRouter, StaticRouterProvider } from \"../server\";\nimport { FrameworkContext } from \"./components\";\nimport type { EntryContext } from \"./entry\";\nimport { RemixErrorBoundary } from \"./errorBoundaries\";\nimport { createServerRoutes, shouldHydrateRouteLoader } from \"./routes\";\nimport { StreamTransfer } from \"./single-fetch\";\n\n/**\n * @category Types\n */\nexport interface ServerRouterProps {\n  /**\n   * The entry context containing the manifest, route modules, and other data\n   * needed for rendering.\n   */\n  context: EntryContext;\n  /**\n   * The URL of the request being handled.\n   */\n  url: string | URL;\n  /**\n   * An optional `nonce` for [Content Security Policy (CSP)](https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/CSP)\n   * compliance, used to allow inline scripts to run safely.\n   */\n  nonce?: string;\n}\n\n/**\n * The server entry point for a React Router app in Framework Mode. This\n * component is used to generate the HTML in the response from the server. See\n * [`entry.server.tsx`](../framework-conventions/entry.server.tsx).\n *\n * @public\n * @category Framework Routers\n * @mode framework\n * @param props Props\n * @param {ServerRouterProps.context} props.context n/a\n * @param {ServerRouterProps.nonce} props.nonce n/a\n * @param {ServerRouterProps.url} props.url n/a\n * @returns A React element that represents the server-rendered application.\n */\nexport function ServerRouter({\n  context,\n  url,\n  nonce,\n}: ServerRouterProps): ReactElement {\n  if (typeof url === \"string\") {\n    url = new URL(url);\n  }\n\n  let { manifest, routeModules, criticalCss, serverHandoffString } = context;\n  let routes = createServerRoutes(\n    manifest.routes,\n    routeModules,\n    context.future,\n    context.isSpaMode,\n  );\n\n  // Create a shallow clone of `loaderData` we can mutate for partial hydration.\n  // When a route exports a `clientLoader` and a `HydrateFallback`, we want to\n  // render the fallback on the server so we clear our the `loaderData` during SSR.\n  // Is it important not to change the `context` reference here since we use it\n  // for context._deepestRenderedBoundaryId tracking\n  context.staticHandlerContext.loaderData = {\n    ...context.staticHandlerContext.loaderData,\n  };\n  for (let match of context.staticHandlerContext.matches) {\n    let routeId = match.route.id;\n    let route = routeModules[routeId];\n    let manifestRoute = context.manifest.routes[routeId];\n    // Clear out the loaderData to avoid rendering the route component when the\n    // route opted into clientLoader hydration and either:\n    // * gave us a HydrateFallback\n    // * or doesn't have a server loader and we have no data to render\n    if (\n      route &&\n      manifestRoute &&\n      shouldHydrateRouteLoader(\n        routeId,\n        route.clientLoader,\n        manifestRoute.hasLoader,\n        context.isSpaMode,\n      ) &&\n      (route.HydrateFallback || !manifestRoute.hasLoader)\n    ) {\n      delete context.staticHandlerContext.loaderData[routeId];\n    }\n  }\n\n  let router = createStaticRouter(routes, context.staticHandlerContext);\n\n  return (\n    <>\n      <FrameworkContext.Provider\n        value={{\n          manifest,\n          routeModules,\n          criticalCss,\n          serverHandoffString,\n          future: context.future,\n          ssr: context.ssr,\n          isSpaMode: context.isSpaMode,\n          routeDiscovery: context.routeDiscovery,\n          serializeError: context.serializeError,\n          renderMeta: context.renderMeta,\n        }}\n      >\n        <RemixErrorBoundary location={router.state.location}>\n          <StaticRouterProvider\n            router={router}\n            context={context.staticHandlerContext}\n            hydrate={false}\n          />\n        </RemixErrorBoundary>\n      </FrameworkContext.Provider>\n      {context.serverHandoffStream ? (\n        <React.Suspense>\n          <StreamTransfer\n            context={context}\n            identifier={0}\n            reader={context.serverHandoffStream.getReader()}\n            textDecoder={new TextDecoder()}\n            nonce={nonce}\n          />\n        </React.Suspense>\n      ) : null}\n    </>\n  );\n}\n"
  },
  {
    "path": "packages/react-router/lib/dom/ssr/single-fetch.tsx",
    "content": "import * as React from \"react\";\n\nimport { decode } from \"../../../vendor/turbo-stream-v2/turbo-stream\";\nimport type { Router as DataRouter } from \"../../router/router\";\nimport { isDataWithResponseInit, isResponse } from \"../../router/router\";\nimport type {\n  DataStrategyFunction,\n  DataStrategyFunctionArgs,\n  DataStrategyResult,\n} from \"../../router/utils\";\nimport {\n  ErrorResponseImpl,\n  isRouteErrorResponse,\n  redirect,\n  data,\n  stripBasename,\n} from \"../../router/utils\";\nimport { createRequestInit } from \"./data\";\nimport type { AssetsManifest, EntryContext } from \"./entry\";\nimport { escapeHtml } from \"./markup\";\nimport invariant from \"./invariant\";\nimport type { RouteModules } from \"./routeModules\";\nimport type { DataRouteMatch } from \"../../context\";\n\nexport const SingleFetchRedirectSymbol = Symbol(\"SingleFetchRedirect\");\n\nclass SingleFetchNoResultError extends Error {}\n\nexport type SingleFetchRedirectResult = {\n  redirect: string;\n  status: number;\n  revalidate: boolean;\n  reload: boolean;\n  replace: boolean;\n};\n\n// Shared/serializable type used by both turbo-stream and RSC implementations\nexport type DecodedSingleFetchResults =\n  | { routes: { [key: string]: SingleFetchResult } }\n  | { redirect: SingleFetchRedirectResult };\n\n// This and SingleFetchResults are only used over the wire, and are converted to\n// DecodedSingleFetchResults in `fetchAndDecode`.  This way turbo-stream/RSC\n// can use the same `unwrapSingleFetchResult` implementation.\nexport type SingleFetchResult =\n  | { data: unknown }\n  | { error: unknown }\n  | SingleFetchRedirectResult;\n\nexport type SingleFetchResults =\n  | { [key: string]: SingleFetchResult }\n  | { [SingleFetchRedirectSymbol]: SingleFetchRedirectResult };\n\ninterface StreamTransferProps {\n  context: EntryContext;\n  identifier: number;\n  reader: ReadableStreamDefaultReader<Uint8Array>;\n  textDecoder: TextDecoder;\n  nonce?: string;\n}\n\n// We can't use a 3xx status or else the `fetch()` would follow the redirect.\n// We need to communicate the redirect back as data so we can act on it in the\n// client side router.  We use a 202 to avoid any automatic caching we might\n// get from a 200 since a \"temporary\" redirect should not be cached.  This lets\n// the user control cache behavior via Cache-Control\nexport const SINGLE_FETCH_REDIRECT_STATUS = 202;\n\n// Some status codes are not permitted to have bodies, so we want to just\n// treat those as \"no data\" instead of throwing an exception:\n//   https://datatracker.ietf.org/doc/html/rfc9110#name-informational-1xx\n//   https://datatracker.ietf.org/doc/html/rfc9110#name-204-no-content\n//   https://datatracker.ietf.org/doc/html/rfc9110#name-205-reset-content\n//\n// Note: 304 is not included here because the browser should fill those responses\n// with the cached body content.\nexport const NO_BODY_STATUS_CODES = new Set([100, 101, 204, 205]);\n\n// StreamTransfer recursively renders down chunks of the `serverHandoffStream`\n// into the client-side `streamController`\nexport function StreamTransfer({\n  context,\n  identifier,\n  reader,\n  textDecoder,\n  nonce,\n}: StreamTransferProps) {\n  // If the user didn't render the <Scripts> component then we don't have to\n  // bother streaming anything in\n  if (!context.renderMeta || !context.renderMeta.didRenderScripts) {\n    return null;\n  }\n\n  if (!context.renderMeta.streamCache) {\n    context.renderMeta.streamCache = {};\n  }\n  let { streamCache } = context.renderMeta;\n  let promise = streamCache[identifier];\n  if (!promise) {\n    promise = streamCache[identifier] = reader\n      .read()\n      .then((result) => {\n        streamCache[identifier].result = {\n          done: result.done,\n          value: textDecoder.decode(result.value, { stream: true }),\n        };\n      })\n      .catch((e) => {\n        streamCache[identifier].error = e;\n      });\n  }\n\n  if (promise.error) {\n    throw promise.error;\n  }\n  if (promise.result === undefined) {\n    throw promise;\n  }\n\n  let { done, value } = promise.result;\n  let scriptTag = value ? (\n    <script\n      nonce={nonce}\n      dangerouslySetInnerHTML={{\n        __html: `window.__reactRouterContext.streamController.enqueue(${escapeHtml(\n          JSON.stringify(value),\n        )});`,\n      }}\n    />\n  ) : null;\n\n  if (done) {\n    return (\n      <>\n        {scriptTag}\n        <script\n          nonce={nonce}\n          dangerouslySetInnerHTML={{\n            __html: `window.__reactRouterContext.streamController.close();`,\n          }}\n        />\n      </>\n    );\n  } else {\n    return (\n      <>\n        {scriptTag}\n        <React.Suspense>\n          <StreamTransfer\n            context={context}\n            identifier={identifier + 1}\n            reader={reader}\n            textDecoder={textDecoder}\n            nonce={nonce}\n          />\n        </React.Suspense>\n      </>\n    );\n  }\n}\n\ntype GetRouteInfoFunction = (match: DataRouteMatch) => {\n  hasLoader: boolean;\n  hasClientLoader: boolean;\n  hasShouldRevalidate: boolean;\n};\n\ntype ShouldAllowOptOutFunction = (match: DataRouteMatch) => boolean;\n\nexport type FetchAndDecodeFunction = (\n  args: DataStrategyFunctionArgs,\n  basename: string | undefined,\n  trailingSlashAware: boolean,\n  targetRoutes?: string[],\n  shouldAllowOptOut?: ShouldAllowOptOutFunction,\n) => Promise<{ status: number; data: DecodedSingleFetchResults }>;\n\nexport function getTurboStreamSingleFetchDataStrategy(\n  getRouter: () => DataRouter,\n  manifest: AssetsManifest,\n  routeModules: RouteModules,\n  ssr: boolean,\n  basename: string | undefined,\n  trailingSlashAware: boolean,\n): DataStrategyFunction {\n  let dataStrategy = getSingleFetchDataStrategyImpl(\n    getRouter,\n    (match: DataRouteMatch) => {\n      let manifestRoute = manifest.routes[match.route.id];\n      invariant(manifestRoute, \"Route not found in manifest\");\n      let routeModule = routeModules[match.route.id];\n      return {\n        hasLoader: manifestRoute.hasLoader,\n        hasClientLoader: manifestRoute.hasClientLoader,\n        hasShouldRevalidate: Boolean(routeModule?.shouldRevalidate),\n      };\n    },\n    fetchAndDecodeViaTurboStream,\n    ssr,\n    basename,\n    trailingSlashAware,\n  );\n  return async (args) => args.runClientMiddleware(dataStrategy);\n}\n\nexport function getSingleFetchDataStrategyImpl(\n  getRouter: () => DataRouter,\n  getRouteInfo: GetRouteInfoFunction,\n  fetchAndDecode: FetchAndDecodeFunction,\n  ssr: boolean,\n  basename: string | undefined,\n  trailingSlashAware: boolean,\n  shouldAllowOptOut: ShouldAllowOptOutFunction = () => true,\n): DataStrategyFunction {\n  return async (args) => {\n    let { request, matches, fetcherKey } = args;\n    let router = getRouter();\n\n    // Actions are simple and behave the same for navigations and fetchers\n    if (request.method !== \"GET\") {\n      return singleFetchActionStrategy(\n        args,\n        fetchAndDecode,\n        basename,\n        trailingSlashAware,\n      );\n    }\n\n    let foundRevalidatingServerLoader = matches.some((m) => {\n      let { hasLoader, hasClientLoader } = getRouteInfo(m);\n      return m.shouldCallHandler() && hasLoader && !hasClientLoader;\n    });\n    if (!ssr && !foundRevalidatingServerLoader) {\n      // If this is SPA mode, there won't be any loaders below root and we'll\n      // disable single fetch.  We have to keep the `dataStrategy` defined for\n      // SPA mode because we may load a SPA fallback page but then navigate into\n      // a pre-rendered path and need to fetch the pre-rendered `.data` file.\n      //\n      // If this is `ssr:false` with a `prerender` config, we need to keep single\n      // fetch enabled because we can prerender the `.data` files at build time\n      // and load them from a static file server/CDN at runtime.\n      //\n      // However, with the SPA Fallback logic, we can have SPA routes operating\n      // alongside pre-rendered routes.  If any pre-rendered routes have a\n      // `loader` then the default behavior would be to make the single fetch\n      // `.data` request on navigation to get the updated root/parent route\n      // `loader` data.\n      //\n      // We need to detect these scenarios because if it's a non-pre-rendered\n      // route being handled by SPA mode, then the `.data` file won't have been\n      // pre-generated and it'll cause a 404.  Thankfully, we can do this\n      // without knowing the prerender'd paths and can just do loader detection\n      // from the manifest:\n      //\n      // - We only allow loaders on pre-rendered routes at build time\n      // - We opt out of revalidation automatically for routes with a `loader`\n      //   and no `clientLoader` because the data is static\n      // - So if no routes with a server `loader` need to revalidate we can just\n      //   call the normal resolve functions and short circuit any single fetch\n      //   behavior\n      // - If we find this a loader that needs to be called, we know the route must\n      //   have been pre-rendered at build time since the loader would have\n      //   errored otherwise\n      // - So it's safe to make the call knowing there will be a `.data` file on\n      //   the other end\n      return nonSsrStrategy(\n        args,\n        getRouteInfo,\n        fetchAndDecode,\n        basename,\n        trailingSlashAware,\n      );\n    }\n\n    // Fetcher loads are singular calls to one loader\n    if (fetcherKey) {\n      return singleFetchLoaderFetcherStrategy(\n        args,\n        fetchAndDecode,\n        basename,\n        trailingSlashAware,\n      );\n    }\n\n    // Navigational loads are more complex...\n    return singleFetchLoaderNavigationStrategy(\n      args,\n      router,\n      getRouteInfo,\n      fetchAndDecode,\n      ssr,\n      basename,\n      trailingSlashAware,\n      shouldAllowOptOut,\n    );\n  };\n}\n\n// Actions are simple since they're singular calls to the server for both\n// navigations and fetchers)\nasync function singleFetchActionStrategy(\n  args: DataStrategyFunctionArgs,\n  fetchAndDecode: FetchAndDecodeFunction,\n  basename: string | undefined,\n  trailingSlashAware: boolean,\n) {\n  let actionMatch = args.matches.find((m) => m.shouldCallHandler());\n  invariant(actionMatch, \"No action match found\");\n  let actionStatus: number | undefined = undefined;\n  let result = await actionMatch.resolve(async (handler) => {\n    let result = await handler(async () => {\n      let { data, status } = await fetchAndDecode(\n        args,\n        basename,\n        trailingSlashAware,\n        [actionMatch!.route.id],\n      );\n      actionStatus = status;\n      return unwrapSingleFetchResult(data, actionMatch!.route.id);\n    });\n    return result;\n  });\n\n  if (\n    isResponse(result.result) ||\n    isRouteErrorResponse(result.result) ||\n    isDataWithResponseInit(result.result)\n  ) {\n    return { [actionMatch.route.id]: result };\n  }\n\n  // For non-responses, proxy along the statusCode via data()\n  // (most notably for skipping action error revalidation)\n  return {\n    [actionMatch.route.id]: {\n      type: result.type,\n      result: data(result.result, actionStatus),\n    },\n  };\n}\n\n// We want to opt-out of Single Fetch when we aren't in SSR mode\nasync function nonSsrStrategy(\n  args: DataStrategyFunctionArgs,\n  getRouteInfo: GetRouteInfoFunction,\n  fetchAndDecode: FetchAndDecodeFunction,\n  basename: string | undefined,\n  trailingSlashAware: boolean,\n) {\n  let matchesToLoad = args.matches.filter((m) => m.shouldCallHandler());\n  let results: Record<string, DataStrategyResult> = {};\n  await Promise.all(\n    matchesToLoad.map((m) =>\n      m.resolve(async (handler) => {\n        try {\n          let { hasClientLoader } = getRouteInfo(m);\n          // Need to pass through a `singleFetch` override handler so\n          // clientLoader's can still call server loaders through `.data`\n          // requests\n          let routeId = m.route.id;\n          let result = hasClientLoader\n            ? await handler(async () => {\n                let { data } = await fetchAndDecode(\n                  args,\n                  basename,\n                  trailingSlashAware,\n                  [routeId],\n                );\n                return unwrapSingleFetchResult(data, routeId);\n              })\n            : await handler();\n          results[m.route.id] = { type: \"data\", result };\n        } catch (e) {\n          results[m.route.id] = { type: \"error\", result: e };\n        }\n      }),\n    ),\n  );\n  return results;\n}\n\n// Loaders are trickier since we only want to hit the server once, so we\n// create a singular promise for all server-loader routes to latch onto.\nasync function singleFetchLoaderNavigationStrategy(\n  args: DataStrategyFunctionArgs,\n  router: DataRouter,\n  getRouteInfo: GetRouteInfoFunction,\n  fetchAndDecode: FetchAndDecodeFunction,\n  ssr: boolean,\n  basename: string | undefined,\n  trailingSlashAware: boolean,\n  shouldAllowOptOut: (match: DataRouteMatch) => boolean = () => true,\n) {\n  // Track which routes need a server load for use in a `_routes` param\n  let routesParams = new Set<string>();\n\n  // Only add `_routes` when at least 1 route opts out via `shouldRevalidate`/`clientLoader`\n  let foundOptOutRoute = false;\n\n  // Deferreds per-route so we can be sure they've all loaded via `match.resolve()`\n  let routeDfds = args.matches.map(() => createDeferred<void>());\n\n  // Deferred we'll use for the singleular call to the server\n  let singleFetchDfd = createDeferred<DecodedSingleFetchResults>();\n\n  // We'll build up this results object as we loop through matches\n  let results: Record<string, DataStrategyResult> = {};\n\n  let resolvePromise = Promise.all(\n    args.matches.map(async (m, i) =>\n      m.resolve(async (handler) => {\n        routeDfds[i].resolve();\n        let routeId = m.route.id;\n        let { hasLoader, hasClientLoader, hasShouldRevalidate } =\n          getRouteInfo(m);\n\n        let defaultShouldRevalidate =\n          !m.shouldRevalidateArgs ||\n          m.shouldRevalidateArgs.actionStatus == null ||\n          m.shouldRevalidateArgs.actionStatus < 400;\n        let shouldCall = m.shouldCallHandler(defaultShouldRevalidate);\n\n        if (!shouldCall) {\n          // If this route opted out, don't include in the .data request\n          foundOptOutRoute ||=\n            m.shouldRevalidateArgs != null && // This is a revalidation,\n            hasLoader && // for a route with a server loader,\n            hasShouldRevalidate === true; // and a shouldRevalidate function\n          return;\n        }\n\n        // When a route has a client loader, it opts out of the singular call and\n        // calls it's server loader via `serverLoader()` using a `?_routes` param\n        if (shouldAllowOptOut(m) && hasClientLoader) {\n          if (hasLoader) {\n            foundOptOutRoute = true;\n          }\n          try {\n            let result = await handler(async () => {\n              let { data } = await fetchAndDecode(\n                args,\n                basename,\n                trailingSlashAware,\n                [routeId],\n              );\n              return unwrapSingleFetchResult(data, routeId);\n            });\n\n            results[routeId] = { type: \"data\", result };\n          } catch (e) {\n            results[routeId] = { type: \"error\", result: e };\n          }\n          return;\n        }\n\n        // Load this route on the server if it has a loader\n        if (hasLoader) {\n          routesParams.add(routeId);\n        }\n\n        // Lump this match in with the others on a singular promise\n        try {\n          let result = await handler(async () => {\n            let data = await singleFetchDfd.promise;\n            return unwrapSingleFetchResult(data, routeId);\n          });\n          results[routeId] = { type: \"data\", result };\n        } catch (e) {\n          results[routeId] = { type: \"error\", result: e };\n        }\n      }),\n    ),\n  );\n\n  // Wait for all routes to resolve above before we make the HTTP call\n  await Promise.all(routeDfds.map((d) => d.promise));\n\n  // We can skip the server call:\n  // - On initial hydration - only clientLoaders can pass through via\n  //   `clientLoader.hydrate`. We check the navigation state below as well\n  //   because if a clientLoader redirected we'll still be `initialized=false`\n  //   but we want to call loaders for the new location\n  // - If there are no routes to fetch from the server\n  //\n  // One exception - if we are performing an HDR revalidation we have to call\n  // the server in case a new loader has shown up that the manifest doesn't yet\n  // know about\n  let isInitialLoad =\n    !router.state.initialized && router.state.navigation.state === \"idle\";\n  if (\n    (isInitialLoad || routesParams.size === 0) &&\n    !window.__reactRouterHdrActive\n  ) {\n    singleFetchDfd.resolve({ routes: {} });\n  } else {\n    // When routes have opted out, add a `_routes` param to filter server loaders\n    // Skipped in `ssr:false` because we expect to be loading static `.data` files\n    let targetRoutes =\n      ssr && foundOptOutRoute && routesParams.size > 0\n        ? [...routesParams.keys()]\n        : undefined;\n    try {\n      let data = await fetchAndDecode(\n        args,\n        basename,\n        trailingSlashAware,\n        targetRoutes,\n      );\n      singleFetchDfd.resolve(data.data);\n    } catch (e) {\n      singleFetchDfd.reject(e);\n    }\n  }\n\n  await resolvePromise;\n\n  await bubbleMiddlewareErrors(\n    singleFetchDfd.promise,\n    args.matches,\n    routesParams,\n    results,\n  );\n\n  return results;\n}\n\n// If a middleware threw on the way down, we won't have data for our requested\n// loaders and they'll resolve to `SingleFetchNoResultError` results.  If this\n// happens, take the highest error we find in our results (which is a middleware\n// error if no loaders ever ran), and assign to these missing routes and let\n// the router bubble accordingly\nasync function bubbleMiddlewareErrors(\n  singleFetchPromise: Promise<DecodedSingleFetchResults>,\n  matches: DataStrategyFunctionArgs[\"matches\"],\n  routesParams: Set<string>,\n  results: Record<string, DataStrategyResult>,\n) {\n  try {\n    let middlewareError: unknown;\n    let fetchedData = await singleFetchPromise;\n\n    if (\"routes\" in fetchedData) {\n      for (let match of matches) {\n        if (match.route.id in fetchedData.routes) {\n          let routeResult = fetchedData.routes[match.route.id];\n          if (\"error\" in routeResult) {\n            middlewareError = routeResult.error;\n            // If we didn't have a loader to call for this route but it threw an\n            // error from middleware, assign the error and let the router bubble it\n            if (results[match.route.id]?.result == null) {\n              results[match.route.id] = {\n                type: \"error\",\n                result: middlewareError,\n              };\n            }\n            break;\n          }\n        }\n      }\n    }\n\n    if (middlewareError !== undefined) {\n      Array.from(routesParams.values()).forEach((routeId) => {\n        if (results[routeId].result instanceof SingleFetchNoResultError) {\n          results[routeId].result = middlewareError;\n        }\n      });\n    }\n  } catch (e) {\n    // No-op - this logic is only intended to process successful responses\n    // If the `.data` failed, the routes will handle those errors themselves\n  }\n}\n\n// Fetcher loader calls are much simpler than navigational loader calls\nasync function singleFetchLoaderFetcherStrategy(\n  args: DataStrategyFunctionArgs,\n  fetchAndDecode: FetchAndDecodeFunction,\n  basename: string | undefined,\n  trailingSlashAware: boolean,\n) {\n  let fetcherMatch = args.matches.find((m) => m.shouldCallHandler());\n  invariant(fetcherMatch, \"No fetcher match found\");\n  let routeId = fetcherMatch.route.id;\n  let result = await fetcherMatch.resolve(async (handler) =>\n    handler(async () => {\n      let { data } = await fetchAndDecode(args, basename, trailingSlashAware, [\n        routeId,\n      ]);\n      return unwrapSingleFetchResult(data, routeId);\n    }),\n  );\n  return { [fetcherMatch.route.id]: result };\n}\n\nexport function stripIndexParam(url: URL) {\n  let indexValues = url.searchParams.getAll(\"index\");\n  url.searchParams.delete(\"index\");\n  let indexValuesToKeep = [];\n  for (let indexValue of indexValues) {\n    if (indexValue) {\n      indexValuesToKeep.push(indexValue);\n    }\n  }\n  for (let toKeep of indexValuesToKeep) {\n    url.searchParams.append(\"index\", toKeep);\n  }\n\n  return url;\n}\n\nexport function singleFetchUrl(\n  reqUrl: URL | string,\n  basename: string | undefined,\n  trailingSlashAware: boolean,\n  extension: \"data\" | \"rsc\",\n) {\n  let url =\n    typeof reqUrl === \"string\"\n      ? new URL(\n          reqUrl,\n          // This can be called during the SSR flow via PrefetchPageLinksImpl so\n          // don't assume window is available\n          typeof window === \"undefined\"\n            ? \"server://singlefetch/\"\n            : window.location.origin,\n        )\n      : reqUrl;\n\n  if (trailingSlashAware) {\n    if (url.pathname.endsWith(\"/\")) {\n      // Preserve trailing slash by using /_.data pattern\n      // e.g., /about/ -> /about/_.data\n      url.pathname = `${url.pathname}_.${extension}`;\n    } else {\n      url.pathname = `${url.pathname}.${extension}`;\n    }\n  } else {\n    if (url.pathname === \"/\") {\n      url.pathname = `_root.${extension}`;\n    } else if (basename && stripBasename(url.pathname, basename) === \"/\") {\n      url.pathname = `${basename.replace(/\\/$/, \"\")}/_root.${extension}`;\n    } else {\n      url.pathname = `${url.pathname.replace(/\\/$/, \"\")}.${extension}`;\n    }\n  }\n\n  return url;\n}\n\nasync function fetchAndDecodeViaTurboStream(\n  args: DataStrategyFunctionArgs,\n  basename: string | undefined,\n  trailingSlashAware: boolean,\n  targetRoutes?: string[],\n): Promise<{ status: number; data: DecodedSingleFetchResults }> {\n  let { request } = args;\n  let url = singleFetchUrl(request.url, basename, trailingSlashAware, \"data\");\n  if (request.method === \"GET\") {\n    url = stripIndexParam(url);\n    if (targetRoutes) {\n      url.searchParams.set(\"_routes\", targetRoutes.join(\",\"));\n    }\n  }\n\n  let res = await fetch(url, await createRequestInit(request));\n\n  // If this error'd without hitting the running server, then bubble a normal\n  // `ErrorResponse` and don't try to decode the body with `turbo-stream`.\n  //\n  // This could be triggered by a few scenarios:\n  // - `.data` request 404 on a pre-rendered app using a CDN\n  // - 429 error returned from a CDN on a SSR app\n  if (res.status >= 400 && !res.headers.has(\"X-Remix-Response\")) {\n    throw new ErrorResponseImpl(res.status, res.statusText, await res.text());\n  }\n\n  // Handle non-RR redirects (i.e., from express middleware)\n  if (res.status === 204 && res.headers.has(\"X-Remix-Redirect\")) {\n    return {\n      status: SINGLE_FETCH_REDIRECT_STATUS,\n      data: {\n        redirect: {\n          redirect: res.headers.get(\"X-Remix-Redirect\")!,\n          status: Number(res.headers.get(\"X-Remix-Status\") || \"302\"),\n          revalidate: res.headers.get(\"X-Remix-Revalidate\") === \"true\",\n          reload: res.headers.get(\"X-Remix-Reload-Document\") === \"true\",\n          replace: res.headers.get(\"X-Remix-Replace\") === \"true\",\n        },\n      },\n    };\n  }\n\n  if (NO_BODY_STATUS_CODES.has(res.status)) {\n    let routes: { [key: string]: SingleFetchResult } = {};\n    // We get back just a single result for action requests - normalize that\n    // to a DecodedSingleFetchResults shape here\n    if (targetRoutes && request.method !== \"GET\") {\n      routes[targetRoutes[0]] = { data: undefined };\n    }\n    return {\n      status: res.status,\n      data: { routes },\n    };\n  }\n\n  invariant(res.body, \"No response body to decode\");\n\n  try {\n    let decoded = await decodeViaTurboStream(res.body, window);\n    let data: DecodedSingleFetchResults;\n    if (request.method === \"GET\") {\n      let typed = decoded.value as SingleFetchResults;\n      if (SingleFetchRedirectSymbol in typed) {\n        data = { redirect: typed[SingleFetchRedirectSymbol] };\n      } else {\n        data = { routes: typed };\n      }\n    } else {\n      let typed = decoded.value as SingleFetchResult;\n      let routeId = targetRoutes?.[0];\n      invariant(routeId, \"No routeId found for single fetch call decoding\");\n      if (\"redirect\" in typed) {\n        data = { redirect: typed };\n      } else {\n        data = { routes: { [routeId]: typed } };\n      }\n    }\n    return { status: res.status, data };\n  } catch (e) {\n    // Can't clone after consuming the body via turbo-stream so we can't\n    // include the body here.  In an ideal world we'd look for a turbo-stream\n    // content type here, or even X-Remix-Response but then folks can't\n    // statically deploy their prerendered .data files to a CDN unless they can\n    // tell that CDN to add special headers to those certain files - which is a\n    // bit restrictive.\n    throw new Error(\"Unable to decode turbo-stream response\");\n  }\n}\n\n// Note: If you change this function please change the corresponding\n// encodeViaTurboStream function in server-runtime\nexport function decodeViaTurboStream(\n  body: ReadableStream<Uint8Array>,\n  global: Window | typeof globalThis,\n) {\n  return decode(body, {\n    plugins: [\n      (type: string, ...rest: unknown[]) => {\n        // Decode Errors back into Error instances using the right type and with\n        // the right (potentially undefined) stacktrace\n        if (type === \"SanitizedError\") {\n          let [name, message, stack] = rest as [\n            string,\n            string,\n            string | undefined,\n          ];\n          let Constructor = Error;\n          // @ts-expect-error\n          if (name && name in global && typeof global[name] === \"function\") {\n            // @ts-expect-error\n            Constructor = global[name];\n          }\n          let error = new Constructor(message);\n          error.stack = stack;\n          return { value: error };\n        }\n\n        if (type === \"ErrorResponse\") {\n          let [data, status, statusText] = rest as [\n            unknown,\n            number,\n            string | undefined,\n          ];\n          return {\n            value: new ErrorResponseImpl(status, statusText, data),\n          };\n        }\n\n        if (type === \"SingleFetchRedirect\") {\n          return { value: { [SingleFetchRedirectSymbol]: rest[0] } };\n        }\n\n        if (type === \"SingleFetchClassInstance\") {\n          return { value: rest[0] };\n        }\n\n        if (type === \"SingleFetchFallback\") {\n          return { value: undefined };\n        }\n      },\n    ],\n  });\n}\n\nfunction unwrapSingleFetchResult(\n  result: DecodedSingleFetchResults,\n  routeId: string,\n) {\n  if (\"redirect\" in result) {\n    let {\n      redirect: location,\n      revalidate,\n      reload,\n      replace,\n      status,\n    } = result.redirect;\n    throw redirect(location, {\n      status,\n      headers: {\n        // Three R's of redirecting (lol Veep)\n        ...(revalidate ? { \"X-Remix-Revalidate\": \"yes\" } : null),\n        ...(reload ? { \"X-Remix-Reload-Document\": \"yes\" } : null),\n        ...(replace ? { \"X-Remix-Replace\": \"yes\" } : null),\n      },\n    });\n  }\n\n  let routeResult = result.routes[routeId];\n  if (routeResult == null) {\n    throw new SingleFetchNoResultError(\n      `No result found for routeId \"${routeId}\"`,\n    );\n  } else if (\"error\" in routeResult) {\n    throw routeResult.error;\n  } else if (\"data\" in routeResult) {\n    return routeResult.data;\n  } else {\n    throw new Error(`Invalid response found for routeId \"${routeId}\"`);\n  }\n}\n\nfunction createDeferred<T = unknown>() {\n  let resolve: (val: T) => Promise<void>;\n  let reject: (error: unknown) => Promise<void>;\n  let promise = new Promise<T>((res, rej) => {\n    resolve = async (val: T) => {\n      res(val);\n      try {\n        await promise;\n      } catch (e) {}\n    };\n    reject = async (error?: unknown) => {\n      rej(error);\n      try {\n        await promise;\n      } catch (e) {}\n    };\n  });\n  return {\n    promise,\n    //@ts-ignore\n    resolve,\n    //@ts-ignore\n    reject,\n  };\n}\n"
  },
  {
    "path": "packages/react-router/lib/dom-export/dom-router-provider.tsx",
    "content": "import * as React from \"react\";\nimport * as ReactDOM from \"react-dom\";\n\nimport type { RouterProviderProps as BaseRouterProviderProps } from \"react-router\";\nimport { RouterProvider as BaseRouterProvider } from \"react-router\";\n\nexport type RouterProviderProps = Omit<BaseRouterProviderProps, \"flushSync\">;\n\nexport function RouterProvider(props: Omit<RouterProviderProps, \"flushSync\">) {\n  return <BaseRouterProvider flushSync={ReactDOM.flushSync} {...props} />;\n}\n"
  },
  {
    "path": "packages/react-router/lib/dom-export/hydrated-router.tsx",
    "content": "import * as React from \"react\";\n\nimport type {\n  UNSAFE_AssetsManifest as AssetsManifest,\n  UNSAFE_RouteModules as RouteModules,\n  DataRouter,\n  HydrationState,\n  RouterInit,\n  ClientOnErrorFunction,\n} from \"react-router\";\nimport {\n  UNSAFE_getHydrationData as getHydrationData,\n  UNSAFE_invariant as invariant,\n  UNSAFE_FrameworkContext as FrameworkContext,\n  UNSAFE_decodeViaTurboStream as decodeViaTurboStream,\n  UNSAFE_RemixErrorBoundary as RemixErrorBoundary,\n  UNSAFE_createBrowserHistory as createBrowserHistory,\n  UNSAFE_createClientRoutes as createClientRoutes,\n  UNSAFE_createRouter as createRouter,\n  UNSAFE_deserializeErrors as deserializeErrors,\n  UNSAFE_getTurboStreamSingleFetchDataStrategy as getTurboStreamSingleFetchDataStrategy,\n  UNSAFE_getPatchRoutesOnNavigationFunction as getPatchRoutesOnNavigationFunction,\n  UNSAFE_useFogOFWarDiscovery as useFogOFWarDiscovery,\n  UNSAFE_mapRouteProperties as mapRouteProperties,\n  UNSAFE_hydrationRouteProperties as hydrationRouteProperties,\n  UNSAFE_createClientRoutesWithHMRRevalidationOptOut as createClientRoutesWithHMRRevalidationOptOut,\n} from \"react-router\";\nimport { CRITICAL_CSS_DATA_ATTRIBUTE } from \"../dom/ssr/components\";\nimport { RouterProvider } from \"./dom-router-provider\";\nimport type { unstable_ClientInstrumentation } from \"../router/instrumentation\";\n\ntype SSRInfo = {\n  context: NonNullable<(typeof window)[\"__reactRouterContext\"]>;\n  routeModules: RouteModules;\n  manifest: AssetsManifest;\n  stateDecodingPromise:\n    | (Promise<void> & {\n        value?: unknown;\n        error?: unknown;\n      })\n    | undefined;\n  router: DataRouter | undefined;\n  routerInitialized: boolean;\n};\n\nlet ssrInfo: SSRInfo | null = null;\nlet router: DataRouter | null = null;\n\nfunction initSsrInfo(): void {\n  if (\n    !ssrInfo &&\n    window.__reactRouterContext &&\n    window.__reactRouterManifest &&\n    window.__reactRouterRouteModules\n  ) {\n    if (window.__reactRouterManifest.sri === true) {\n      const importMap = document.querySelector(\"script[rr-importmap]\");\n      if (importMap?.textContent) {\n        try {\n          window.__reactRouterManifest.sri = JSON.parse(\n            importMap.textContent,\n          ).integrity;\n        } catch (err) {\n          console.error(\"Failed to parse import map\", err);\n        }\n      }\n    }\n\n    ssrInfo = {\n      context: window.__reactRouterContext,\n      manifest: window.__reactRouterManifest,\n      routeModules: window.__reactRouterRouteModules,\n      stateDecodingPromise: undefined,\n      router: undefined,\n      routerInitialized: false,\n    };\n  }\n}\n\nfunction createHydratedRouter({\n  getContext,\n  unstable_instrumentations,\n}: {\n  getContext?: RouterInit[\"getContext\"];\n  unstable_instrumentations?: unstable_ClientInstrumentation[];\n}): DataRouter {\n  initSsrInfo();\n\n  if (!ssrInfo) {\n    throw new Error(\n      \"You must be using the SSR features of React Router in order to skip \" +\n        \"passing a `router` prop to `<RouterProvider>`\",\n    );\n  }\n\n  // We need to suspend until the initial state snapshot is decoded into\n  // window.__reactRouterContext.state\n\n  let localSsrInfo = ssrInfo;\n  // Note: `stateDecodingPromise` is not coupled to `router` - we'll reach this\n  // code potentially many times waiting for our state to arrive, but we'll\n  // then only get past here and create the `router` one time\n  if (!ssrInfo.stateDecodingPromise) {\n    let stream = ssrInfo.context.stream;\n    invariant(stream, \"No stream found for single fetch decoding\");\n    ssrInfo.context.stream = undefined;\n    ssrInfo.stateDecodingPromise = decodeViaTurboStream(stream, window)\n      .then((value) => {\n        ssrInfo!.context.state =\n          value.value as typeof localSsrInfo.context.state;\n        localSsrInfo.stateDecodingPromise!.value = true;\n      })\n      .catch((e) => {\n        localSsrInfo.stateDecodingPromise!.error = e;\n      });\n  }\n  if (ssrInfo.stateDecodingPromise.error) {\n    throw ssrInfo.stateDecodingPromise.error;\n  }\n  if (!ssrInfo.stateDecodingPromise.value) {\n    throw ssrInfo.stateDecodingPromise;\n  }\n\n  let routes = createClientRoutes(\n    ssrInfo.manifest.routes,\n    ssrInfo.routeModules,\n    ssrInfo.context.state,\n    ssrInfo.context.ssr,\n    ssrInfo.context.isSpaMode,\n  );\n\n  let hydrationData: HydrationState | undefined = undefined;\n  // In SPA mode we only hydrate build-time root loader data\n  if (ssrInfo.context.isSpaMode) {\n    let { loaderData } = ssrInfo.context.state;\n    if (\n      ssrInfo.manifest.routes.root?.hasLoader &&\n      loaderData &&\n      \"root\" in loaderData\n    ) {\n      hydrationData = {\n        loaderData: {\n          root: loaderData.root,\n        },\n      };\n    }\n  } else {\n    hydrationData = getHydrationData({\n      state: ssrInfo.context.state,\n      routes,\n      getRouteInfo: (routeId) => ({\n        clientLoader: ssrInfo!.routeModules[routeId]?.clientLoader,\n        hasLoader: ssrInfo!.manifest.routes[routeId]?.hasLoader === true,\n        hasHydrateFallback:\n          ssrInfo!.routeModules[routeId]?.HydrateFallback != null,\n      }),\n      location: window.location,\n      basename: window.__reactRouterContext?.basename,\n      isSpaMode: ssrInfo.context.isSpaMode,\n    });\n\n    if (hydrationData && hydrationData.errors) {\n      // TODO: De-dup this or remove entirely in v7 where single fetch is the\n      // only approach and we have already serialized or deserialized on the server\n      hydrationData.errors = deserializeErrors(hydrationData.errors);\n    }\n  }\n\n  // We cannot support history-state-driven masking with SSR, so if a hard\n  // reload is performed we remove the mask and hydrate according to the\n  // browser URL\n  if (window.history.state && window.history.state.masked) {\n    window.history.replaceState(\n      { ...window.history.state, masked: undefined },\n      \"\",\n    );\n  }\n\n  // We don't use createBrowserRouter here because we need fine-grained control\n  // over initialization to support synchronous `clientLoader` flows.\n  let router = createRouter({\n    routes,\n    history: createBrowserHistory(),\n    basename: ssrInfo.context.basename,\n    getContext,\n    hydrationData,\n    hydrationRouteProperties,\n    unstable_instrumentations,\n    mapRouteProperties,\n    future: {\n      middleware: ssrInfo.context.future.v8_middleware,\n    },\n    dataStrategy: getTurboStreamSingleFetchDataStrategy(\n      () => router,\n      ssrInfo.manifest,\n      ssrInfo.routeModules,\n      ssrInfo.context.ssr,\n      ssrInfo.context.basename,\n      ssrInfo.context.future.unstable_trailingSlashAwareDataRequests,\n    ),\n    patchRoutesOnNavigation: getPatchRoutesOnNavigationFunction(\n      () => router,\n      ssrInfo.manifest,\n      ssrInfo.routeModules,\n      ssrInfo.context.ssr,\n      ssrInfo.context.routeDiscovery,\n      ssrInfo.context.isSpaMode,\n      ssrInfo.context.basename,\n    ),\n  });\n\n  ssrInfo.router = router;\n\n  // We can call initialize() immediately if the router doesn't have any\n  // loaders to run on hydration\n  if (router.state.initialized) {\n    ssrInfo.routerInitialized = true;\n    router.initialize();\n  }\n\n  // @ts-ignore\n  router.createRoutesForHMR =\n    /* spacer so ts-ignore does not affect the right hand of the assignment */\n    createClientRoutesWithHMRRevalidationOptOut;\n  window.__reactRouterDataRouter = router;\n\n  return router;\n}\n\n/**\n * Props for the {@link dom.HydratedRouter} component.\n *\n * @category Types\n */\nexport interface HydratedRouterProps {\n  /**\n   * Context factory function to be passed through to {@link createBrowserRouter}.\n   * This function will be called to create a fresh `context` instance on each\n   * navigation/fetch and made available to\n   * [`clientAction`](../../start/framework/route-module#clientAction)/[`clientLoader`](../../start/framework/route-module#clientLoader)\n   * functions.\n   */\n  getContext?: RouterInit[\"getContext\"];\n  /**\n   * Array of instrumentation objects allowing you to instrument the router and\n   * individual routes prior to router initialization (and on any subsequently\n   * added routes via `route.lazy` or `patchRoutesOnNavigation`).  This is\n   * mostly useful for observability such as wrapping navigations, fetches,\n   * as well as route loaders/actions/middlewares with logging and/or performance\n   * tracing. See the [docs](../../how-to/instrumentation) for more information.\n   *\n   * ```tsx\n   * const logging = {\n   *   router({ instrument }) {\n   *     instrument({\n   *       navigate: (impl, { to }) => logExecution(`navigate ${to}`, impl),\n   *       fetch: (impl, { to }) => logExecution(`fetch ${to}`, impl)\n   *     });\n   *   },\n   *   route({ instrument, id }) {\n   *     instrument({\n   *       middleware: (impl, { request }) => logExecution(\n   *         `middleware ${request.url} (route ${id})`,\n   *         impl\n   *       ),\n   *       loader: (impl, { request }) => logExecution(\n   *         `loader ${request.url} (route ${id})`,\n   *         impl\n   *       ),\n   *       action: (impl, { request }) => logExecution(\n   *         `action ${request.url} (route ${id})`,\n   *         impl\n   *       ),\n   *     })\n   *   }\n   * };\n   *\n   * async function logExecution(label: string, impl: () => Promise<void>) {\n   *   let start = performance.now();\n   *   console.log(`start ${label}`);\n   *   await impl();\n   *   let duration = Math.round(performance.now() - start);\n   *   console.log(`end ${label} (${duration}ms)`);\n   * }\n   *\n   * startTransition(() => {\n   *   hydrateRoot(\n   *     document,\n   *     <HydratedRouter unstable_instrumentations={[logging]} />\n   *   );\n   * });\n   * ```\n   */\n  unstable_instrumentations?: unstable_ClientInstrumentation[];\n  /**\n   * An error handler function that will be called for any middleware, loader, action,\n   * or render errors that are encountered in your application.  This is useful for\n   * logging or reporting errors instead of in the {@link ErrorBoundary} because it's not\n   * subject to re-rendering and will only run one time per error.\n   *\n   * The `errorInfo` parameter is passed along from\n   * [`componentDidCatch`](https://react.dev/reference/react/Component#componentdidcatch)\n   * and is only present for render errors.\n   *\n   * ```tsx\n   * <HydratedRouter onError=(error, info) => {\n   *   let { location, params, unstable_pattern, errorInfo } = info;\n   *   console.error(error, location, errorInfo);\n   *   reportToErrorService(error, location, errorInfo);\n   * }} />\n   * ```\n   */\n  onError?: ClientOnErrorFunction;\n  /**\n   * Control whether router state updates are internally wrapped in\n   * [`React.startTransition`](https://react.dev/reference/react/startTransition).\n   *\n   * - When left `undefined`, all state updates are wrapped in\n   *   `React.startTransition`\n   *   - This can lead to buggy behaviors if you are wrapping your own\n   *     navigations/fetchers in `startTransition`.\n   * - When set to `true`, {@link Link} and {@link Form} navigations will be wrapped\n   *   in `React.startTransition` and router state changes will be wrapped in\n   *   `React.startTransition` and also sent through\n   *   [`useOptimistic`](https://react.dev/reference/react/useOptimistic) to\n   *   surface mid-navigation router state changes to the UI.\n   * - When set to `false`, the router will not leverage `React.startTransition` or\n   *   `React.useOptimistic` on any navigations or state changes.\n   *\n   * For more information, please see the [docs](https://reactrouter.com/explanation/react-transitions).\n   */\n  unstable_useTransitions?: boolean;\n}\n\n/**\n * Framework-mode router component to be used to hydrate a router from a\n * {@link ServerRouter}. See [`entry.client.tsx`](../framework-conventions/entry.client.tsx).\n *\n * @public\n * @category Framework Routers\n * @mode framework\n * @param props Props\n * @param {dom.HydratedRouterProps.getContext} props.getContext n/a\n * @param {dom.HydratedRouterProps.onError} props.onError n/a\n * @returns A React element that represents the hydrated application.\n */\nexport function HydratedRouter(props: HydratedRouterProps) {\n  if (!router) {\n    router = createHydratedRouter({\n      getContext: props.getContext,\n      unstable_instrumentations: props.unstable_instrumentations,\n    });\n  }\n\n  // We only want to show critical CSS in dev for the initial server render to\n  // avoid a flash of unstyled content. Once the client-side JS kicks in, we can\n  // clear it to avoid duplicate styles.\n  let [criticalCss, setCriticalCss] = React.useState(\n    process.env.NODE_ENV === \"development\"\n      ? ssrInfo?.context.criticalCss\n      : undefined,\n  );\n  React.useEffect(() => {\n    if (process.env.NODE_ENV === \"development\") {\n      setCriticalCss(undefined);\n    }\n  }, []);\n  React.useEffect(() => {\n    if (process.env.NODE_ENV === \"development\" && criticalCss === undefined) {\n      // When there's a hydration mismatch, React 19 ignores the server HTML and\n      // re-renders from the root, but it doesn't remove any head tags that\n      // aren't present in the virtual DOM. This means that the original\n      // critical CSS elements are still in the document even though we cleared\n      // them in the effect above. To fix this, this effect is designed to clean\n      // up any leftover elements. If `criticalCss` is undefined in this effect,\n      // this means that React is no longer managing the critical CSS elements,\n      // so if there are any left in the document, these are stale elements from\n      // the original SSR pass and we can safely remove them.\n      document\n        .querySelectorAll(`[${CRITICAL_CSS_DATA_ATTRIBUTE}]`)\n        .forEach((element) => element.remove());\n    }\n  }, [criticalCss]);\n\n  let [location, setLocation] = React.useState(router.state.location);\n\n  React.useLayoutEffect(() => {\n    // If we had to run clientLoaders on hydration, we delay initialization until\n    // after we've hydrated to avoid hydration issues from synchronous client loaders\n    if (ssrInfo && ssrInfo.router && !ssrInfo.routerInitialized) {\n      ssrInfo.routerInitialized = true;\n      ssrInfo.router.initialize();\n    }\n  }, []);\n\n  React.useLayoutEffect(() => {\n    if (ssrInfo && ssrInfo.router) {\n      return ssrInfo.router.subscribe((newState) => {\n        if (newState.location !== location) {\n          setLocation(newState.location);\n        }\n      });\n    }\n  }, [location]);\n\n  invariant(ssrInfo, \"ssrInfo unavailable for HydratedRouter\");\n\n  useFogOFWarDiscovery(\n    router,\n    ssrInfo.manifest,\n    ssrInfo.routeModules,\n    ssrInfo.context.ssr,\n    ssrInfo.context.routeDiscovery,\n    ssrInfo.context.isSpaMode,\n  );\n\n  // We need to include a wrapper RemixErrorBoundary here in case the root error\n  // boundary also throws and we need to bubble up outside of the router entirely.\n  // Then we need a stateful location here so the user can back-button navigate\n  // out of there\n  return (\n    // This fragment is important to ensure we match the <ServerRouter> JSX\n    // structure so that useId values hydrate correctly\n    <>\n      <FrameworkContext.Provider\n        value={{\n          manifest: ssrInfo.manifest,\n          routeModules: ssrInfo.routeModules,\n          future: ssrInfo.context.future,\n          criticalCss,\n          ssr: ssrInfo.context.ssr,\n          isSpaMode: ssrInfo.context.isSpaMode,\n          routeDiscovery: ssrInfo.context.routeDiscovery,\n        }}\n      >\n        <RemixErrorBoundary location={location}>\n          <RouterProvider\n            router={router}\n            unstable_useTransitions={props.unstable_useTransitions}\n            onError={props.onError}\n          />\n        </RemixErrorBoundary>\n      </FrameworkContext.Provider>\n      {/*\n          This fragment is important to ensure we match the <ServerRouter> JSX\n          structure so that useId values hydrate correctly\n        */}\n      <></>\n    </>\n  );\n}\n"
  },
  {
    "path": "packages/react-router/lib/errors.ts",
    "content": "import { isDataWithResponseInit } from \"./router/router\";\nimport { ErrorResponseImpl } from \"./router/utils\";\nimport type { DataWithResponseInit } from \"./router/utils\";\n\nconst ERROR_DIGEST_BASE = \"REACT_ROUTER_ERROR\"; // 18\nconst ERROR_DIGEST_REDIRECT = \"REDIRECT\"; // 8\nconst ERROR_DIGEST_ROUTE_ERROR_RESPONSE = \"ROUTE_ERROR_RESPONSE\"; // 20\n\nexport function createRedirectErrorDigest(response: Response) {\n  return `${ERROR_DIGEST_BASE}:${ERROR_DIGEST_REDIRECT}:${JSON.stringify({\n    status: response.status,\n    statusText: response.statusText,\n    location: response.headers.get(\"Location\"),\n    reloadDocument: response.headers.get(\"X-Remix-Reload-Document\") === \"true\",\n    replace: response.headers.get(\"X-Remix-Replace\") === \"true\",\n  })}`;\n}\n\nexport function decodeRedirectErrorDigest(digest: string):\n  | undefined\n  | {\n      status: number;\n      statusText: string;\n      location: string;\n      reloadDocument: boolean;\n      replace: boolean;\n    } {\n  if (digest.startsWith(`${ERROR_DIGEST_BASE}:${ERROR_DIGEST_REDIRECT}:{`)) {\n    try {\n      let parsed = JSON.parse(digest.slice(28));\n      if (\n        typeof parsed === \"object\" &&\n        parsed &&\n        typeof parsed.status === \"number\" &&\n        typeof parsed.statusText === \"string\" &&\n        typeof parsed.location === \"string\" &&\n        typeof parsed.reloadDocument === \"boolean\" &&\n        typeof parsed.replace === \"boolean\"\n      ) {\n        return parsed;\n      }\n    } catch {}\n  }\n}\n\nexport function createRouteErrorResponseDigest(\n  response: DataWithResponseInit<unknown> | Response,\n) {\n  let status = 500;\n  let statusText = \"\";\n  let data: unknown;\n  if (isDataWithResponseInit(response)) {\n    status = response.init?.status ?? status;\n    statusText = response.init?.statusText ?? statusText;\n    data = response.data;\n  } else {\n    status = response.status;\n    statusText = response.statusText;\n    // We can't do async work here to read the response body.\n    data = undefined;\n  }\n\n  return `${ERROR_DIGEST_BASE}:${ERROR_DIGEST_ROUTE_ERROR_RESPONSE}:${JSON.stringify(\n    {\n      status,\n      statusText,\n      data,\n    },\n  )}`;\n}\n\nexport function decodeRouteErrorResponseDigest(\n  digest: string,\n): undefined | ErrorResponseImpl {\n  if (\n    digest.startsWith(\n      `${ERROR_DIGEST_BASE}:${ERROR_DIGEST_ROUTE_ERROR_RESPONSE}:{`,\n    )\n  ) {\n    try {\n      let parsed = JSON.parse(digest.slice(40));\n      if (\n        typeof parsed === \"object\" &&\n        parsed &&\n        typeof parsed.status === \"number\" &&\n        typeof parsed.statusText === \"string\"\n      ) {\n        return new ErrorResponseImpl(\n          parsed.status,\n          parsed.statusText,\n          parsed.data,\n        );\n      }\n    } catch {}\n  }\n}\n"
  },
  {
    "path": "packages/react-router/lib/hooks.tsx",
    "content": "import * as React from \"react\";\nimport type {\n  DataRouteMatch,\n  NavigateOptions,\n  RouteContextObject,\n  RouteMatch,\n  RouteObject,\n} from \"./context\";\nimport {\n  AwaitContext,\n  DataRouterContext,\n  DataRouterStateContext,\n  ENABLE_DEV_WARNINGS,\n  LocationContext,\n  NavigationContext,\n  RSCRouterContext,\n  RouteContext,\n  RouteErrorContext,\n} from \"./context\";\nimport type { Location, Path, To } from \"./router/history\";\nimport {\n  Action as NavigationType,\n  invariant,\n  parsePath,\n  warning,\n} from \"./router/history\";\nimport type {\n  Blocker,\n  BlockerFunction,\n  RelativeRoutingType,\n  Router as DataRouter,\n  RevalidationState,\n  Navigation,\n} from \"./router/router\";\nimport { IDLE_BLOCKER } from \"./router/router\";\nimport type {\n  ParamParseKey,\n  Params,\n  PathMatch,\n  PathPattern,\n  UIMatch,\n} from \"./router/utils\";\nimport {\n  convertRouteMatchToUiMatch,\n  decodePath,\n  getResolveToMatches,\n  getRoutePattern,\n  isBrowser,\n  isRouteErrorResponse,\n  joinPaths,\n  matchPath,\n  matchRoutes,\n  parseToInfo,\n  resolveTo,\n  stripBasename,\n} from \"./router/utils\";\nimport type {\n  GetActionData,\n  GetLoaderData,\n  SerializeFrom,\n} from \"./types/route-data\";\nimport type { ClientOnErrorFunction } from \"./components\";\nimport type { RouteModules } from \"./types/register\";\nimport {\n  decodeRedirectErrorDigest,\n  decodeRouteErrorResponseDigest,\n} from \"./errors\";\n\n/**\n * Resolves a URL against the current {@link Location}.\n *\n * @example\n * import { useHref } from \"react-router\";\n *\n * function SomeComponent() {\n *   let href = useHref(\"some/where\");\n *   // \"/resolved/some/where\"\n * }\n *\n * @public\n * @category Hooks\n * @param to The path to resolve\n * @param options Options\n * @param options.relative Defaults to `\"route\"` so routing is relative to the\n * route tree.\n * Set to `\"path\"` to make relative routing operate against path segments.\n * @returns The resolved href string\n */\nexport function useHref(\n  to: To,\n  { relative }: { relative?: RelativeRoutingType } = {},\n): string {\n  invariant(\n    useInRouterContext(),\n    // TODO: This error is probably because they somehow have 2 versions of the\n    // router loaded. We can help them understand how to avoid that.\n    `useHref() may be used only in the context of a <Router> component.`,\n  );\n\n  let { basename, navigator } = React.useContext(NavigationContext);\n  let { hash, pathname, search } = useResolvedPath(to, { relative });\n\n  let joinedPathname = pathname;\n\n  // If we're operating within a basename, prepend it to the pathname prior\n  // to creating the href.  If this is a root navigation, then just use the raw\n  // basename which allows the basename to have full control over the presence\n  // of a trailing slash on root links\n  if (basename !== \"/\") {\n    joinedPathname =\n      pathname === \"/\" ? basename : joinPaths([basename, pathname]);\n  }\n\n  return navigator.createHref({ pathname: joinedPathname, search, hash });\n}\n\n/**\n * Returns `true` if this component is a descendant of a {@link Router}, useful\n * to ensure a component is used within a {@link Router}.\n *\n * @public\n * @category Hooks\n * @mode framework\n * @mode data\n * @returns Whether the component is within a {@link Router} context\n */\nexport function useInRouterContext(): boolean {\n  return React.useContext(LocationContext) != null;\n}\n\n/**\n * Returns the current {@link Location}. This can be useful if you'd like to\n * perform some side effect whenever it changes.\n *\n * @example\n * import * as React from 'react'\n * import { useLocation } from 'react-router'\n *\n * function SomeComponent() {\n *   let location = useLocation()\n *\n *   React.useEffect(() => {\n *     // Google Analytics\n *     ga('send', 'pageview')\n *   }, [location]);\n *\n *   return (\n *     // ...\n *   );\n * }\n *\n * @public\n * @category Hooks\n * @returns The current {@link Location} object\n */\nexport function useLocation(): Location {\n  invariant(\n    useInRouterContext(),\n    // TODO: This error is probably because they somehow have 2 versions of the\n    // router loaded. We can help them understand how to avoid that.\n    `useLocation() may be used only in the context of a <Router> component.`,\n  );\n\n  return React.useContext(LocationContext).location;\n}\n\n/**\n * Returns the current {@link Navigation} action which describes how the router\n * came to the current {@link Location}, either by a pop, push, or replace on\n * the [`History`](https://developer.mozilla.org/en-US/docs/Web/API/History) stack.\n *\n * @public\n * @category Hooks\n * @returns The current {@link NavigationType} (`\"POP\"`, `\"PUSH\"`, or `\"REPLACE\"`)\n */\nexport function useNavigationType(): NavigationType {\n  return React.useContext(LocationContext).navigationType;\n}\n\n/**\n * Returns a {@link PathMatch} object if the given pattern matches the current URL.\n * This is useful for components that need to know \"active\" state, e.g.\n * {@link NavLink | `<NavLink>`}.\n *\n * @public\n * @category Hooks\n * @param pattern The pattern to match against the current {@link Location}\n * @returns The path match object if the pattern matches, `null` otherwise\n */\nexport function useMatch<\n  ParamKey extends ParamParseKey<Path>,\n  Path extends string,\n>(pattern: PathPattern<Path> | Path): PathMatch<ParamKey> | null {\n  invariant(\n    useInRouterContext(),\n    // TODO: This error is probably because they somehow have 2 versions of the\n    // router loaded. We can help them understand how to avoid that.\n    `useMatch() may be used only in the context of a <Router> component.`,\n  );\n\n  let { pathname } = useLocation();\n  return React.useMemo(\n    () => matchPath<ParamKey, Path>(pattern, decodePath(pathname)),\n    [pathname, pattern],\n  );\n}\n\n/**\n * The interface for the `navigate` function returned from {@link useNavigate}.\n */\nexport interface NavigateFunction {\n  (to: To, options?: NavigateOptions): void | Promise<void>;\n  (delta: number): void | Promise<void>;\n}\n\nconst navigateEffectWarning =\n  `You should call navigate() in a React.useEffect(), not when ` +\n  `your component is first rendered.`;\n\n// Mute warnings for calls to useNavigate in SSR environments\nfunction useIsomorphicLayoutEffect(\n  cb: Parameters<typeof React.useLayoutEffect>[0],\n) {\n  let isStatic = React.useContext(NavigationContext).static;\n  if (!isStatic) {\n    // We should be able to get rid of this once react 18.3 is released\n    // See: https://github.com/facebook/react/pull/26395\n    // eslint-disable-next-line react-hooks/rules-of-hooks\n    React.useLayoutEffect(cb);\n  }\n}\n\n/**\n * Returns a function that lets you navigate programmatically in the browser in\n * response to user interactions or effects.\n *\n * It's often better to use {@link redirect} in [`action`](../../start/framework/route-module#action)/[`loader`](../../start/framework/route-module#loader)\n * functions than this hook.\n *\n * The returned function signature is `navigate(to, options?)`/`navigate(delta)` where:\n *\n * * `to` can be a string path, a {@link To} object, or a number (delta)\n * * `options` contains options for modifying the navigation\n *   * These options work in all modes (Framework, Data, and Declarative):\n *     * `relative`: `\"route\"` or `\"path\"` to control relative routing logic\n *     * `replace`: Replace the current entry in the [`History`](https://developer.mozilla.org/en-US/docs/Web/API/History) stack\n *     * `state`: Optional [`history.state`](https://developer.mozilla.org/en-US/docs/Web/API/History/state) to include with the new {@link Location}\n *   * These options only work in Framework and Data modes:\n *     * `flushSync`: Wrap the DOM updates in [`ReactDom.flushSync`](https://react.dev/reference/react-dom/flushSync)\n *     * `preventScrollReset`: Do not scroll back to the top of the page after navigation\n *     * `viewTransition`: Enable [`document.startViewTransition`](https://developer.mozilla.org/en-US/docs/Web/API/Document/startViewTransition) for this navigation\n *\n * @example\n * import { useNavigate } from \"react-router\";\n *\n * function SomeComponent() {\n *   let navigate = useNavigate();\n *   return (\n *     <button onClick={() => navigate(-1)}>\n *       Go Back\n *     </button>\n *   );\n * }\n *\n * @additionalExamples\n * ### Navigate to another path\n *\n * ```tsx\n * navigate(\"/some/route\");\n * navigate(\"/some/route?search=param\");\n * ```\n *\n * ### Navigate with a {@link To} object\n *\n * All properties are optional.\n *\n * ```tsx\n * navigate({\n *   pathname: \"/some/route\",\n *   search: \"?search=param\",\n *   hash: \"#hash\",\n *   state: { some: \"state\" },\n * });\n * ```\n *\n * If you use `state`, that will be available on the {@link Location} object on\n * the next page. Access it with `useLocation().state` (see {@link useLocation}).\n *\n * ### Navigate back or forward in the history stack\n *\n * ```tsx\n * // back\n * // often used to close modals\n * navigate(-1);\n *\n * // forward\n * // often used in a multistep wizard workflows\n * navigate(1);\n * ```\n *\n * Be cautious with `navigate(number)`. If your application can load up to a\n * route that has a button that tries to navigate forward/back, there may not be\n * a `[`History`](https://developer.mozilla.org/en-US/docs/Web/API/History)\n * entry to go back or forward to, or it can go somewhere you don't expect\n * (like a different domain).\n *\n * Only use this if you're sure they will have an entry in the [`History`](https://developer.mozilla.org/en-US/docs/Web/API/History)\n * stack to navigate to.\n *\n * ### Replace the current entry in the history stack\n *\n * This will remove the current entry in the [`History`](https://developer.mozilla.org/en-US/docs/Web/API/History)\n * stack, replacing it with a new one, similar to a server side redirect.\n *\n * ```tsx\n * navigate(\"/some/route\", { replace: true });\n * ```\n *\n * ### Prevent Scroll Reset\n *\n * [MODES: framework, data]\n *\n * <br/>\n * <br/>\n *\n * To prevent {@link ScrollRestoration | `<ScrollRestoration>`} from resetting\n * the scroll position, use the `preventScrollReset` option.\n *\n * ```tsx\n * navigate(\"?some-tab=1\", { preventScrollReset: true });\n * ```\n *\n * For example, if you have a tab interface connected to search params in the\n * middle of a page, and you don't want it to scroll to the top when a tab is\n * clicked.\n *\n * ### Return Type Augmentation\n *\n * Internally, `useNavigate` uses a separate implementation when you are in\n * Declarative mode versus Data/Framework mode - the primary difference being\n * that the latter is able to return a stable reference that does not change\n * identity across navigations. The implementation in Data/Framework mode also\n * returns a [`Promise`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise)\n * that resolves when the navigation is completed. This means the return type of\n * `useNavigate` is `void | Promise<void>`. This is accurate, but can lead to\n * some red squigglies based on the union in the return value:\n *\n * - If you're using `typescript-eslint`, you may see errors from\n *   [`@typescript-eslint/no-floating-promises`](https://typescript-eslint.io/rules/no-floating-promises)\n * - In Framework/Data mode, `React.use(navigate())` will show a false-positive\n *   `Argument of type 'void | Promise<void>' is not assignable to parameter of\n *   type 'Usable<void>'` error\n *\n * The easiest way to work around these issues is to augment the type based on the\n * router you're using:\n *\n * ```ts\n * // If using <BrowserRouter>\n * declare module \"react-router\" {\n *   interface NavigateFunction {\n *     (to: To, options?: NavigateOptions): void;\n *     (delta: number): void;\n *   }\n * }\n *\n * // If using <RouterProvider> or Framework mode\n * declare module \"react-router\" {\n *   interface NavigateFunction {\n *     (to: To, options?: NavigateOptions): Promise<void>;\n *     (delta: number): Promise<void>;\n *   }\n * }\n * ```\n *\n * @public\n * @category Hooks\n * @returns A navigate function for programmatic navigation\n */\nexport function useNavigate(): NavigateFunction {\n  let { isDataRoute } = React.useContext(RouteContext);\n  // Conditional usage is OK here because the usage of a data router is static\n  // eslint-disable-next-line react-hooks/rules-of-hooks\n  return isDataRoute ? useNavigateStable() : useNavigateUnstable();\n}\n\nfunction useNavigateUnstable(): NavigateFunction {\n  invariant(\n    useInRouterContext(),\n    // TODO: This error is probably because they somehow have 2 versions of the\n    // router loaded. We can help them understand how to avoid that.\n    `useNavigate() may be used only in the context of a <Router> component.`,\n  );\n\n  let dataRouterContext = React.useContext(DataRouterContext);\n  let { basename, navigator } = React.useContext(NavigationContext);\n  let { matches } = React.useContext(RouteContext);\n  let { pathname: locationPathname } = useLocation();\n\n  let routePathnamesJson = JSON.stringify(getResolveToMatches(matches));\n\n  let activeRef = React.useRef(false);\n  useIsomorphicLayoutEffect(() => {\n    activeRef.current = true;\n  });\n\n  let navigate: NavigateFunction = React.useCallback(\n    (to: To | number, options: NavigateOptions = {}) => {\n      warning(activeRef.current, navigateEffectWarning);\n\n      // Short circuit here since if this happens on first render the navigate\n      // is useless because we haven't wired up our history listener yet\n      if (!activeRef.current) return;\n\n      if (typeof to === \"number\") {\n        navigator.go(to);\n        return;\n      }\n\n      let path = resolveTo(\n        to,\n        JSON.parse(routePathnamesJson),\n        locationPathname,\n        options.relative === \"path\",\n      );\n\n      // If we're operating within a basename, prepend it to the pathname prior\n      // to handing off to history (but only if we're not in a data router,\n      // otherwise it'll prepend the basename inside of the router).\n      // If this is a root navigation, then we navigate to the raw basename\n      // which allows the basename to have full control over the presence of a\n      // trailing slash on root links\n      if (dataRouterContext == null && basename !== \"/\") {\n        path.pathname =\n          path.pathname === \"/\"\n            ? basename\n            : joinPaths([basename, path.pathname]);\n      }\n\n      (!!options.replace ? navigator.replace : navigator.push)(\n        path,\n        options.state,\n        options,\n      );\n    },\n    [\n      basename,\n      navigator,\n      routePathnamesJson,\n      locationPathname,\n      dataRouterContext,\n    ],\n  );\n\n  return navigate;\n}\n\nconst OutletContext = React.createContext<unknown>(null);\n\n/**\n * Returns the parent route {@link Outlet | `<Outlet context>`}.\n *\n * Often parent routes manage state or other values you want shared with child\n * routes. You can create your own [context provider](https://react.dev/learn/passing-data-deeply-with-context)\n * if you like, but this is such a common situation that it's built-into\n * {@link Outlet | `<Outlet>`}.\n *\n * ```tsx\n * // Parent route\n * function Parent() {\n *   const [count, setCount] = React.useState(0);\n *   return <Outlet context={[count, setCount]} />;\n * }\n * ```\n *\n * ```tsx\n * // Child route\n * import { useOutletContext } from \"react-router\";\n *\n * function Child() {\n *   const [count, setCount] = useOutletContext();\n *   const increment = () => setCount((c) => c + 1);\n *   return <button onClick={increment}>{count}</button>;\n * }\n * ```\n *\n * If you're using TypeScript, we recommend the parent component provide a\n * custom hook for accessing the context value. This makes it easier for\n * consumers to get nice typings, control consumers, and know who's consuming\n * the context value.\n *\n * Here's a more realistic example:\n *\n * ```tsx filename=src/routes/dashboard.tsx lines=[14,20]\n * import { useState } from \"react\";\n * import { Outlet, useOutletContext } from \"react-router\";\n *\n * import type { User } from \"./types\";\n *\n * type ContextType = { user: User | null };\n *\n * export default function Dashboard() {\n *   const [user, setUser] = useState<User | null>(null);\n *\n *   return (\n *     <div>\n *       <h1>Dashboard</h1>\n *       <Outlet context={{ user } satisfies ContextType} />\n *     </div>\n *   );\n * }\n *\n * export function useUser() {\n *   return useOutletContext<ContextType>();\n * }\n * ```\n *\n * ```tsx filename=src/routes/dashboard/messages.tsx lines=[1,4]\n * import { useUser } from \"../dashboard\";\n *\n * export default function DashboardMessages() {\n *   const { user } = useUser();\n *   return (\n *     <div>\n *       <h2>Messages</h2>\n *       <p>Hello, {user.name}!</p>\n *     </div>\n *   );\n * }\n * ```\n *\n * @public\n * @category Hooks\n * @returns The context value passed to the parent {@link Outlet} component\n */\nexport function useOutletContext<Context = unknown>(): Context {\n  return React.useContext(OutletContext) as Context;\n}\n\n/**\n * Returns the element for the child route at this level of the route\n * hierarchy. Used internally by {@link Outlet | `<Outlet>`} to render child\n * routes.\n *\n * @public\n * @category Hooks\n * @param context The context to pass to the outlet\n * @returns The child route element or `null` if no child routes match\n */\nexport function useOutlet(context?: unknown): React.ReactElement | null {\n  let outlet = React.useContext(RouteContext).outlet;\n  return React.useMemo(\n    () =>\n      outlet && (\n        <OutletContext.Provider value={context}>\n          {outlet}\n        </OutletContext.Provider>\n      ),\n    [outlet, context],\n  );\n}\n\n/**\n * Returns an object of key/value-pairs of the dynamic params from the current\n * URL that were matched by the routes. Child routes inherit all params from\n * their parent routes.\n *\n * Assuming a route pattern like `/posts/:postId` is matched by `/posts/123`\n * then `params.postId` will be `\"123\"`.\n *\n * @example\n * import { useParams } from \"react-router\";\n *\n * function SomeComponent() {\n *   let params = useParams();\n *   params.postId;\n * }\n *\n * @additionalExamples\n * ### Basic Usage\n *\n * ```tsx\n * import { useParams } from \"react-router\";\n *\n * // given a route like:\n * <Route path=\"/posts/:postId\" element={<Post />} />;\n *\n * // or a data route like:\n * createBrowserRouter([\n *   {\n *     path: \"/posts/:postId\",\n *     component: Post,\n *   },\n * ]);\n *\n * // or in routes.ts\n * route(\"/posts/:postId\", \"routes/post.tsx\");\n * ```\n *\n * Access the params in a component:\n *\n * ```tsx\n * import { useParams } from \"react-router\";\n *\n * export default function Post() {\n *   let params = useParams();\n *   return <h1>Post: {params.postId}</h1>;\n * }\n * ```\n *\n * ### Multiple Params\n *\n * Patterns can have multiple params:\n *\n * ```tsx\n * \"/posts/:postId/comments/:commentId\";\n * ```\n *\n * All will be available in the params object:\n *\n * ```tsx\n * import { useParams } from \"react-router\";\n *\n * export default function Post() {\n *   let params = useParams();\n *   return (\n *     <h1>\n *       Post: {params.postId}, Comment: {params.commentId}\n *     </h1>\n *   );\n * }\n * ```\n *\n * ### Catchall Params\n *\n * Catchall params are defined with `*`:\n *\n * ```tsx\n * \"/files/*\";\n * ```\n *\n * The matched value will be available in the params object as follows:\n *\n * ```tsx\n * import { useParams } from \"react-router\";\n *\n * export default function File() {\n *   let params = useParams();\n *   let catchall = params[\"*\"];\n *   // ...\n * }\n * ```\n *\n * You can destructure the catchall param:\n *\n * ```tsx\n * export default function File() {\n *   let { \"*\": catchall } = useParams();\n *   console.log(catchall);\n * }\n * ```\n *\n * @public\n * @category Hooks\n * @returns An object containing the dynamic route parameters\n */\nexport function useParams<\n  ParamsOrKey extends string | Record<string, string | undefined> = string,\n>(): Readonly<\n  [ParamsOrKey] extends [string] ? Params<ParamsOrKey> : Partial<ParamsOrKey>\n> {\n  let { matches } = React.useContext(RouteContext);\n  let routeMatch = matches[matches.length - 1];\n  return routeMatch ? (routeMatch.params as any) : {};\n}\n\n/**\n * Resolves the pathname of the given `to` value against the current\n * {@link Location}. Similar to {@link useHref}, but returns a\n * {@link Path} instead of a string.\n *\n * @example\n * import { useResolvedPath } from \"react-router\";\n *\n * function SomeComponent() {\n *   // if the user is at /dashboard/profile\n *   let path = useResolvedPath(\"../accounts\");\n *   path.pathname; // \"/dashboard/accounts\"\n *   path.search; // \"\"\n *   path.hash; // \"\"\n * }\n *\n * @public\n * @category Hooks\n * @param to The path to resolve\n * @param options Options\n * @param options.relative Defaults to `\"route\"` so routing is relative to the route tree.\n *                         Set to `\"path\"` to make relative routing operate against path segments.\n * @returns The resolved {@link Path} object with `pathname`, `search`, and `hash`\n */\nexport function useResolvedPath(\n  to: To,\n  { relative }: { relative?: RelativeRoutingType } = {},\n): Path {\n  let { matches } = React.useContext(RouteContext);\n  let { pathname: locationPathname } = useLocation();\n  let routePathnamesJson = JSON.stringify(getResolveToMatches(matches));\n\n  return React.useMemo(\n    () =>\n      resolveTo(\n        to,\n        JSON.parse(routePathnamesJson),\n        locationPathname,\n        relative === \"path\",\n      ),\n    [to, routePathnamesJson, locationPathname, relative],\n  );\n}\n\n/**\n * Hook version of {@link Routes | `<Routes>`} that uses objects instead of\n * components. These objects have the same properties as the component props.\n * The return value of `useRoutes` is either a valid React element you can use\n * to render the route tree, or `null` if nothing matched.\n *\n * @example\n * import { useRoutes } from \"react-router\";\n *\n * function App() {\n *   let element = useRoutes([\n *     {\n *       path: \"/\",\n *       element: <Dashboard />,\n *       children: [\n *         {\n *           path: \"messages\",\n *           element: <DashboardMessages />,\n *         },\n *         { path: \"tasks\", element: <DashboardTasks /> },\n *       ],\n *     },\n *     { path: \"team\", element: <AboutPage /> },\n *   ]);\n *\n *   return element;\n * }\n *\n * @public\n * @category Hooks\n * @param routes An array of {@link RouteObject}s that define the route hierarchy\n * @param locationArg An optional {@link Location} object or pathname string to\n * use instead of the current {@link Location}\n * @returns A React element to render the matched route, or `null` if no routes matched\n */\nexport function useRoutes(\n  routes: RouteObject[],\n  locationArg?: Partial<Location> | string,\n): React.ReactElement | null {\n  return useRoutesImpl(routes, locationArg);\n}\n\n// Internal implementation with accept optional param for RouterProvider usage\nexport function useRoutesImpl(\n  routes: RouteObject[],\n  locationArg?: Partial<Location> | string,\n  dataRouterOpts?: {\n    state: DataRouter[\"state\"];\n    isStatic: boolean;\n    onError: ClientOnErrorFunction | undefined;\n    future: DataRouter[\"future\"];\n  },\n): React.ReactElement | null {\n  invariant(\n    useInRouterContext(),\n    // TODO: This error is probably because they somehow have 2 versions of the\n    // router loaded. We can help them understand how to avoid that.\n    `useRoutes() may be used only in the context of a <Router> component.`,\n  );\n\n  let { navigator } = React.useContext(NavigationContext);\n  let { matches: parentMatches } = React.useContext(RouteContext);\n  let routeMatch = parentMatches[parentMatches.length - 1];\n  let parentParams = routeMatch ? routeMatch.params : {};\n  let parentPathname = routeMatch ? routeMatch.pathname : \"/\";\n  let parentPathnameBase = routeMatch ? routeMatch.pathnameBase : \"/\";\n  let parentRoute = routeMatch && routeMatch.route;\n\n  if (ENABLE_DEV_WARNINGS) {\n    // You won't get a warning about 2 different <Routes> under a <Route>\n    // without a trailing *, but this is a best-effort warning anyway since we\n    // cannot even give the warning unless they land at the parent route.\n    //\n    // Example:\n    //\n    // <Routes>\n    //   {/* This route path MUST end with /* because otherwise\n    //       it will never match /blog/post/123 */}\n    //   <Route path=\"blog\" element={<Blog />} />\n    //   <Route path=\"blog/feed\" element={<BlogFeed />} />\n    // </Routes>\n    //\n    // function Blog() {\n    //   return (\n    //     <Routes>\n    //       <Route path=\"post/:id\" element={<Post />} />\n    //     </Routes>\n    //   );\n    // }\n    let parentPath = (parentRoute && parentRoute.path) || \"\";\n    warningOnce(\n      parentPathname,\n      !parentRoute || parentPath.endsWith(\"*\") || parentPath.endsWith(\"*?\"),\n      `You rendered descendant <Routes> (or called \\`useRoutes()\\`) at ` +\n        `\"${parentPathname}\" (under <Route path=\"${parentPath}\">) but the ` +\n        `parent route path has no trailing \"*\". This means if you navigate ` +\n        `deeper, the parent won't match anymore and therefore the child ` +\n        `routes will never render.\\n\\n` +\n        `Please change the parent <Route path=\"${parentPath}\"> to <Route ` +\n        `path=\"${parentPath === \"/\" ? \"*\" : `${parentPath}/*`}\">.`,\n    );\n  }\n\n  let locationFromContext = useLocation();\n\n  let location;\n  if (locationArg) {\n    let parsedLocationArg =\n      typeof locationArg === \"string\" ? parsePath(locationArg) : locationArg;\n\n    invariant(\n      parentPathnameBase === \"/\" ||\n        parsedLocationArg.pathname?.startsWith(parentPathnameBase),\n      `When overriding the location using \\`<Routes location>\\` or \\`useRoutes(routes, location)\\`, ` +\n        `the location pathname must begin with the portion of the URL pathname that was ` +\n        `matched by all parent routes. The current pathname base is \"${parentPathnameBase}\" ` +\n        `but pathname \"${parsedLocationArg.pathname}\" was given in the \\`location\\` prop.`,\n    );\n\n    location = parsedLocationArg;\n  } else {\n    location = locationFromContext;\n  }\n\n  let pathname = location.pathname || \"/\";\n\n  let remainingPathname = pathname;\n  if (parentPathnameBase !== \"/\") {\n    // Determine the remaining pathname by removing the # of URL segments the\n    // parentPathnameBase has, instead of removing based on character count.\n    // This is because we can't guarantee that incoming/outgoing encodings/\n    // decodings will match exactly.\n    // We decode paths before matching on a per-segment basis with\n    // decodeURIComponent(), but we re-encode pathnames via `new URL()` so they\n    // match what `window.location.pathname` would reflect.  Those don't 100%\n    // align when it comes to encoded URI characters such as % and &.\n    //\n    // So we may end up with:\n    //   pathname:           \"/descendant/a%25b/match\"\n    //   parentPathnameBase: \"/descendant/a%b\"\n    //\n    // And the direct substring removal approach won't work :/\n    let parentSegments = parentPathnameBase.replace(/^\\//, \"\").split(\"/\");\n    let segments = pathname.replace(/^\\//, \"\").split(\"/\");\n    remainingPathname = \"/\" + segments.slice(parentSegments.length).join(\"/\");\n  }\n\n  let matches = matchRoutes(routes, { pathname: remainingPathname });\n\n  if (ENABLE_DEV_WARNINGS) {\n    warning(\n      parentRoute || matches != null,\n      `No routes matched location \"${location.pathname}${location.search}${location.hash}\" `,\n    );\n\n    warning(\n      matches == null ||\n        matches[matches.length - 1].route.element !== undefined ||\n        matches[matches.length - 1].route.Component !== undefined ||\n        matches[matches.length - 1].route.lazy !== undefined,\n      `Matched leaf route at location \"${location.pathname}${location.search}${location.hash}\" ` +\n        `does not have an element or Component. This means it will render an <Outlet /> with a ` +\n        `null value by default resulting in an \"empty\" page.`,\n    );\n  }\n\n  let renderedMatches = _renderMatches(\n    matches &&\n      matches.map((match) =>\n        Object.assign({}, match, {\n          params: Object.assign({}, parentParams, match.params),\n          pathname: joinPaths([\n            parentPathnameBase,\n            // Re-encode pathnames that were decoded inside matchRoutes.\n            // Pre-encode `?` and `#` ahead of `encodeLocation` because it uses\n            // `new URL()` internally and we need to prevent it from treating\n            // them as separators\n            navigator.encodeLocation\n              ? navigator.encodeLocation(\n                  match.pathname.replace(/\\?/g, \"%3F\").replace(/#/g, \"%23\"),\n                ).pathname\n              : match.pathname,\n          ]),\n          pathnameBase:\n            match.pathnameBase === \"/\"\n              ? parentPathnameBase\n              : joinPaths([\n                  parentPathnameBase,\n                  // Re-encode pathnames that were decoded inside matchRoutes\n                  // Pre-encode `?` and `#` ahead of `encodeLocation` because it uses\n                  // `new URL()` internally and we need to prevent it from treating\n                  // them as separators\n                  navigator.encodeLocation\n                    ? navigator.encodeLocation(\n                        match.pathnameBase\n                          .replace(/\\?/g, \"%3F\")\n                          .replace(/#/g, \"%23\"),\n                      ).pathname\n                    : match.pathnameBase,\n                ]),\n        }),\n      ),\n    parentMatches,\n    dataRouterOpts,\n  );\n\n  // When a user passes in a `locationArg`, the associated routes need to\n  // be wrapped in a new `LocationContext.Provider` in order for `useLocation`\n  // to use the scoped location instead of the global location.\n  if (locationArg && renderedMatches) {\n    return (\n      <LocationContext.Provider\n        value={{\n          location: {\n            pathname: \"/\",\n            search: \"\",\n            hash: \"\",\n            state: null,\n            key: \"default\",\n            unstable_mask: undefined,\n            ...location,\n          },\n          navigationType: NavigationType.Pop,\n        }}\n      >\n        {renderedMatches}\n      </LocationContext.Provider>\n    );\n  }\n\n  return renderedMatches;\n}\n\nfunction DefaultErrorComponent() {\n  let error = useRouteError();\n  let message = isRouteErrorResponse(error)\n    ? `${error.status} ${error.statusText}`\n    : error instanceof Error\n      ? error.message\n      : JSON.stringify(error);\n  let stack = error instanceof Error ? error.stack : null;\n  let lightgrey = \"rgba(200,200,200, 0.5)\";\n  let preStyles = { padding: \"0.5rem\", backgroundColor: lightgrey };\n  let codeStyles = { padding: \"2px 4px\", backgroundColor: lightgrey };\n\n  let devInfo = null;\n  if (ENABLE_DEV_WARNINGS) {\n    console.error(\n      \"Error handled by React Router default ErrorBoundary:\",\n      error,\n    );\n\n    devInfo = (\n      <>\n        <p>💿 Hey developer 👋</p>\n        <p>\n          You can provide a way better UX than this when your app throws errors\n          by providing your own <code style={codeStyles}>ErrorBoundary</code> or{\" \"}\n          <code style={codeStyles}>errorElement</code> prop on your route.\n        </p>\n      </>\n    );\n  }\n\n  return (\n    <>\n      <h2>Unexpected Application Error!</h2>\n      <h3 style={{ fontStyle: \"italic\" }}>{message}</h3>\n      {stack ? <pre style={preStyles}>{stack}</pre> : null}\n      {devInfo}\n    </>\n  );\n}\n\nconst defaultErrorElement = <DefaultErrorComponent />;\n\ntype RenderErrorBoundaryProps = React.PropsWithChildren<{\n  location: Location;\n  revalidation: RevalidationState;\n  error: any;\n  component: React.ReactNode;\n  routeContext: RouteContextObject;\n  onError?: (error: unknown, errorInfo?: React.ErrorInfo) => void;\n}>;\n\ntype RenderErrorBoundaryState = {\n  location: Location;\n  revalidation: RevalidationState;\n  error: any;\n};\n\nexport class RenderErrorBoundary extends React.Component<\n  RenderErrorBoundaryProps,\n  RenderErrorBoundaryState\n> {\n  constructor(props: RenderErrorBoundaryProps) {\n    super(props);\n    this.state = {\n      location: props.location,\n      revalidation: props.revalidation,\n      error: props.error,\n    };\n  }\n\n  static contextType = RSCRouterContext;\n\n  static getDerivedStateFromError(error: any) {\n    return { error: error };\n  }\n\n  static getDerivedStateFromProps(\n    props: RenderErrorBoundaryProps,\n    state: RenderErrorBoundaryState,\n  ) {\n    // When we get into an error state, the user will likely click \"back\" to the\n    // previous page that didn't have an error. Because this wraps the entire\n    // application, that will have no effect--the error page continues to display.\n    // This gives us a mechanism to recover from the error when the location changes.\n    //\n    // Whether we're in an error state or not, we update the location in state\n    // so that when we are in an error state, it gets reset when a new location\n    // comes in and the user recovers from the error.\n    if (\n      state.location !== props.location ||\n      (state.revalidation !== \"idle\" && props.revalidation === \"idle\")\n    ) {\n      return {\n        error: props.error,\n        location: props.location,\n        revalidation: props.revalidation,\n      };\n    }\n\n    // If we're not changing locations, preserve the location but still surface\n    // any new errors that may come through. We retain the existing error, we do\n    // this because the error provided from the app state may be cleared without\n    // the location changing.\n    return {\n      error: props.error !== undefined ? props.error : state.error,\n      location: state.location,\n      revalidation: props.revalidation || state.revalidation,\n    };\n  }\n\n  componentDidCatch(error: any, errorInfo: React.ErrorInfo) {\n    if (this.props.onError) {\n      this.props.onError(error, errorInfo);\n    } else {\n      console.error(\n        \"React Router caught the following error during render\",\n        error,\n      );\n    }\n  }\n\n  render() {\n    let error = this.state.error;\n\n    if (\n      this.context &&\n      typeof error === \"object\" &&\n      error &&\n      \"digest\" in error &&\n      typeof error.digest === \"string\"\n    ) {\n      const decoded = decodeRouteErrorResponseDigest(error.digest);\n      if (decoded) error = decoded;\n    }\n\n    let result =\n      error !== undefined ? (\n        <RouteContext.Provider value={this.props.routeContext}>\n          <RouteErrorContext.Provider\n            value={error}\n            children={this.props.component}\n          />\n        </RouteContext.Provider>\n      ) : (\n        this.props.children\n      );\n\n    if (this.context) {\n      return <RSCErrorHandler error={error}>{result}</RSCErrorHandler>;\n    }\n\n    return result;\n  }\n}\n\nconst errorRedirectHandledMap = new WeakMap<any, Promise<void>>();\nfunction RSCErrorHandler({\n  children,\n  error,\n}: {\n  children: React.ReactNode;\n  error: unknown;\n}) {\n  let { basename } = React.useContext(NavigationContext);\n\n  if (\n    typeof error === \"object\" &&\n    error &&\n    \"digest\" in error &&\n    typeof error.digest === \"string\"\n  ) {\n    let redirect = decodeRedirectErrorDigest(error.digest);\n    if (redirect) {\n      let existingRedirect = errorRedirectHandledMap.get(error);\n      if (existingRedirect) throw existingRedirect;\n\n      let parsed = parseToInfo(redirect.location, basename);\n\n      if (isBrowser && !errorRedirectHandledMap.get(error)) {\n        if (parsed.isExternal || redirect.reloadDocument) {\n          window.location.href = parsed.absoluteURL || parsed.to;\n        } else {\n          const redirectPromise: Promise<void> = Promise.resolve().then(() =>\n            window.__reactRouterDataRouter!.navigate(parsed.to, {\n              replace: redirect.replace,\n            }),\n          );\n          errorRedirectHandledMap.set(error, redirectPromise);\n          throw redirectPromise;\n        }\n      }\n\n      return (\n        <meta\n          httpEquiv=\"refresh\"\n          content={`0;url=${parsed.absoluteURL || parsed.to}`}\n        />\n      );\n    }\n  }\n  return children;\n}\n\ninterface RenderedRouteProps {\n  routeContext: RouteContextObject;\n  match: RouteMatch<string, RouteObject>;\n  children: React.ReactNode | null;\n}\n\nfunction RenderedRoute({ routeContext, match, children }: RenderedRouteProps) {\n  let dataRouterContext = React.useContext(DataRouterContext);\n\n  // Track how deep we got in our render pass to emulate SSR componentDidCatch\n  // in a DataStaticRouter\n  if (\n    dataRouterContext &&\n    dataRouterContext.static &&\n    dataRouterContext.staticContext &&\n    (match.route.errorElement || match.route.ErrorBoundary)\n  ) {\n    dataRouterContext.staticContext._deepestRenderedBoundaryId = match.route.id;\n  }\n\n  return (\n    <RouteContext.Provider value={routeContext}>\n      {children}\n    </RouteContext.Provider>\n  );\n}\n\nexport function _renderMatches(\n  matches: RouteMatch[] | null,\n  parentMatches: RouteMatch[] = [],\n  dataRouterOpts?: {\n    state: DataRouter[\"state\"];\n    isStatic: boolean;\n    onError: ClientOnErrorFunction | undefined;\n    future: DataRouter[\"future\"];\n  },\n): React.ReactElement | null {\n  let dataRouterState = dataRouterOpts?.state;\n\n  if (matches == null) {\n    if (!dataRouterState) {\n      return null;\n    }\n\n    if (dataRouterState.errors) {\n      // Don't bail if we have data router errors so we can render them in the\n      // boundary.  Use the pre-matched (or shimmed) matches\n      matches = dataRouterState.matches as DataRouteMatch[];\n    } else if (\n      parentMatches.length === 0 &&\n      !dataRouterState.initialized &&\n      dataRouterState.matches.length > 0\n    ) {\n      // Don't bail if we're initializing with partial hydration and we have\n      // router matches.  That means we're actively running `patchRoutesOnNavigation`\n      // so we should render down the partial matches to the appropriate\n      // `HydrateFallback`.  We only do this if `parentMatches` is empty so it\n      // only impacts the root matches for `RouterProvider` and no descendant\n      // `<Routes>`\n      matches = dataRouterState.matches as DataRouteMatch[];\n    } else {\n      return null;\n    }\n  }\n\n  let renderedMatches = matches;\n\n  // If we have data errors, trim matches to the highest error boundary\n  let errors = dataRouterState?.errors;\n  if (errors != null) {\n    let errorIndex = renderedMatches.findIndex(\n      (m) => m.route.id && errors?.[m.route.id] !== undefined,\n    );\n    invariant(\n      errorIndex >= 0,\n      `Could not find a matching route for errors on route IDs: ${Object.keys(\n        errors,\n      ).join(\",\")}`,\n    );\n    renderedMatches = renderedMatches.slice(\n      0,\n      Math.min(renderedMatches.length, errorIndex + 1),\n    );\n  }\n\n  // If we're in a partial hydration mode, detect if we need to render down to\n  // a given HydrateFallback while we load the rest of the hydration data\n  let renderFallback = false;\n  let fallbackIndex = -1;\n  if (dataRouterOpts && dataRouterState) {\n    renderFallback = dataRouterState.renderFallback;\n    for (let i = 0; i < renderedMatches.length; i++) {\n      let match = renderedMatches[i];\n      // Track the deepest fallback up until the first route without data\n      if (match.route.HydrateFallback || match.route.hydrateFallbackElement) {\n        fallbackIndex = i;\n      }\n\n      if (match.route.id) {\n        let { loaderData, errors } = dataRouterState;\n        let needsToRunLoader =\n          match.route.loader &&\n          !loaderData.hasOwnProperty(match.route.id) &&\n          (!errors || errors[match.route.id] === undefined);\n        if (match.route.lazy || needsToRunLoader) {\n          // We found the first route that's not ready to render (waiting on\n          // lazy, or has a loader that hasn't run yet) - render up until the\n          // appropriate fallback\n          if (dataRouterOpts.isStatic) {\n            renderFallback = true;\n          }\n          if (fallbackIndex >= 0) {\n            renderedMatches = renderedMatches.slice(0, fallbackIndex + 1);\n          } else {\n            renderedMatches = [renderedMatches[0]];\n          }\n          break;\n        }\n      }\n    }\n  }\n\n  let onErrorHandler = dataRouterOpts?.onError;\n  let onError =\n    dataRouterState && onErrorHandler\n      ? (error: unknown, errorInfo?: React.ErrorInfo) => {\n          onErrorHandler(error, {\n            location: dataRouterState.location,\n            params: dataRouterState.matches?.[0]?.params ?? {},\n            unstable_pattern: getRoutePattern(dataRouterState.matches),\n            errorInfo,\n          });\n        }\n      : undefined;\n\n  return renderedMatches.reduceRight(\n    (outlet, match, index) => {\n      // Only data routers handle errors/fallbacks\n      let error: any;\n      let shouldRenderHydrateFallback = false;\n      let errorElement: React.ReactNode | null = null;\n      let hydrateFallbackElement: React.ReactNode | null = null;\n      if (dataRouterState) {\n        error = errors && match.route.id ? errors[match.route.id] : undefined;\n        errorElement = match.route.errorElement || defaultErrorElement;\n\n        if (renderFallback) {\n          if (fallbackIndex < 0 && index === 0) {\n            warningOnce(\n              \"route-fallback\",\n              false,\n              \"No `HydrateFallback` element provided to render during initial hydration\",\n            );\n            shouldRenderHydrateFallback = true;\n            hydrateFallbackElement = null;\n          } else if (fallbackIndex === index) {\n            shouldRenderHydrateFallback = true;\n            hydrateFallbackElement = match.route.hydrateFallbackElement || null;\n          }\n        }\n      }\n\n      let matches = parentMatches.concat(renderedMatches.slice(0, index + 1));\n      let getChildren = () => {\n        let children: React.ReactNode;\n        if (error) {\n          children = errorElement;\n        } else if (shouldRenderHydrateFallback) {\n          children = hydrateFallbackElement;\n        } else if (match.route.Component) {\n          // Note: This is a de-optimized path since React won't re-use the\n          // ReactElement since it's identity changes with each new\n          // React.createElement call.  We keep this so folks can use\n          // `<Route Component={...}>` in `<Routes>` but generally `Component`\n          // usage is only advised in `RouterProvider` when we can convert it to\n          // `element` ahead of time.\n          children = <match.route.Component />;\n        } else if (match.route.element) {\n          children = match.route.element;\n        } else {\n          children = outlet;\n        }\n\n        return (\n          <RenderedRoute\n            match={match}\n            routeContext={{\n              outlet,\n              matches,\n              isDataRoute: dataRouterState != null,\n            }}\n            children={children}\n          />\n        );\n      };\n      // Only wrap in an error boundary within data router usages when we have an\n      // ErrorBoundary/errorElement on this route.  Otherwise let it bubble up to\n      // an ancestor ErrorBoundary/errorElement\n      return dataRouterState &&\n        (match.route.ErrorBoundary ||\n          match.route.errorElement ||\n          index === 0) ? (\n        <RenderErrorBoundary\n          location={dataRouterState.location}\n          revalidation={dataRouterState.revalidation}\n          component={errorElement}\n          error={error}\n          children={getChildren()}\n          routeContext={{ outlet: null, matches, isDataRoute: true }}\n          onError={onError}\n        />\n      ) : (\n        getChildren()\n      );\n    },\n    null as React.ReactElement | null,\n  );\n}\n\nenum DataRouterHook {\n  UseBlocker = \"useBlocker\",\n  UseRevalidator = \"useRevalidator\",\n  UseNavigateStable = \"useNavigate\",\n}\n\nenum DataRouterStateHook {\n  UseBlocker = \"useBlocker\",\n  UseLoaderData = \"useLoaderData\",\n  UseActionData = \"useActionData\",\n  UseRouteError = \"useRouteError\",\n  UseNavigation = \"useNavigation\",\n  UseRouteLoaderData = \"useRouteLoaderData\",\n  UseMatches = \"useMatches\",\n  UseRevalidator = \"useRevalidator\",\n  UseNavigateStable = \"useNavigate\",\n  UseRouteId = \"useRouteId\",\n  UseRoute = \"useRoute\",\n}\n\nfunction getDataRouterConsoleError(\n  hookName: DataRouterHook | DataRouterStateHook,\n) {\n  return `${hookName} must be used within a data router.  See https://reactrouter.com/en/main/routers/picking-a-router.`;\n}\n\nfunction useDataRouterContext(hookName: DataRouterHook) {\n  let ctx = React.useContext(DataRouterContext);\n  invariant(ctx, getDataRouterConsoleError(hookName));\n  return ctx;\n}\n\nfunction useDataRouterState(hookName: DataRouterStateHook) {\n  let state = React.useContext(DataRouterStateContext);\n  invariant(state, getDataRouterConsoleError(hookName));\n  return state;\n}\n\nfunction useRouteContext(hookName: DataRouterStateHook) {\n  let route = React.useContext(RouteContext);\n  invariant(route, getDataRouterConsoleError(hookName));\n  return route;\n}\n\n// Internal version with hookName-aware debugging\nfunction useCurrentRouteId(hookName: DataRouterStateHook) {\n  let route = useRouteContext(hookName);\n  let thisRoute = route.matches[route.matches.length - 1];\n  invariant(\n    thisRoute.route.id,\n    `${hookName} can only be used on routes that contain a unique \"id\"`,\n  );\n  return thisRoute.route.id;\n}\n\n/**\n * Returns the ID for the nearest contextual route\n *\n * @category Hooks\n * @returns The ID of the nearest contextual route\n */\nexport function useRouteId() {\n  return useCurrentRouteId(DataRouterStateHook.UseRouteId);\n}\n\n/**\n * Returns the current {@link Navigation}, defaulting to an \"idle\" navigation\n * when no navigation is in progress. You can use this to render pending UI\n * (like a global spinner) or read [`FormData`](https://developer.mozilla.org/en-US/docs/Web/API/FormData)\n * from a form navigation.\n *\n * @example\n * import { useNavigation } from \"react-router\";\n *\n * function SomeComponent() {\n *   let navigation = useNavigation();\n *   navigation.state;\n *   navigation.formData;\n *   // etc.\n * }\n *\n * @public\n * @category Hooks\n * @mode framework\n * @mode data\n * @returns The current {@link Navigation} object\n */\nexport function useNavigation(): Navigation {\n  let state = useDataRouterState(DataRouterStateHook.UseNavigation);\n  return state.navigation;\n}\n\n/**\n * Revalidate the data on the page for reasons outside of normal data mutations\n * like [`Window` focus](https://developer.mozilla.org/en-US/docs/Web/API/Window/focus_event)\n * or polling on an interval.\n *\n * Note that page data is already revalidated automatically after actions.\n * If you find yourself using this for normal CRUD operations on your data in\n * response to user interactions, you're probably not taking advantage of the\n * other APIs like {@link useFetcher}, {@link Form}, {@link useSubmit} that do\n * this automatically.\n *\n * @example\n * import { useRevalidator } from \"react-router\";\n *\n * function WindowFocusRevalidator() {\n *   const revalidator = useRevalidator();\n *\n *   useFakeWindowFocus(() => {\n *     revalidator.revalidate();\n *   });\n *\n *   return (\n *     <div hidden={revalidator.state === \"idle\"}>\n *       Revalidating...\n *     </div>\n *   );\n * }\n *\n * @public\n * @category Hooks\n * @mode framework\n * @mode data\n * @returns An object with a `revalidate` function and the current revalidation\n * `state`\n */\nexport function useRevalidator(): {\n  revalidate: () => Promise<void>;\n  state: DataRouter[\"state\"][\"revalidation\"];\n} {\n  let dataRouterContext = useDataRouterContext(DataRouterHook.UseRevalidator);\n  let state = useDataRouterState(DataRouterStateHook.UseRevalidator);\n  let revalidate = React.useCallback(async () => {\n    await dataRouterContext.router.revalidate();\n  }, [dataRouterContext.router]);\n\n  return React.useMemo(\n    () => ({ revalidate, state: state.revalidation }),\n    [revalidate, state.revalidation],\n  );\n}\n\n/**\n * Returns the active route matches, useful for accessing `loaderData` for\n * parent/child routes or the route [`handle`](../../start/framework/route-module#handle)\n * property\n *\n * @public\n * @category Hooks\n * @mode framework\n * @mode data\n * @returns An array of {@link UIMatch | UI matches} for the current route hierarchy\n */\nexport function useMatches(): UIMatch[] {\n  let { matches, loaderData } = useDataRouterState(\n    DataRouterStateHook.UseMatches,\n  );\n  return React.useMemo(\n    () => matches.map((m) => convertRouteMatchToUiMatch(m, loaderData)),\n    [matches, loaderData],\n  );\n}\n\n/**\n * Returns the data from the closest route\n * [`loader`](../../start/framework/route-module#loader) or\n * [`clientLoader`](../../start/framework/route-module#clientloader).\n *\n * @example\n * import { useLoaderData } from \"react-router\";\n *\n * export async function loader() {\n *   return await fakeDb.invoices.findAll();\n * }\n *\n * export default function Invoices() {\n *   let invoices = useLoaderData<typeof loader>();\n *   // ...\n * }\n *\n * @public\n * @category Hooks\n * @mode framework\n * @mode data\n * @returns The data returned from the route's [`loader`](../../start/framework/route-module#loader) or [`clientLoader`](../../start/framework/route-module#clientloader) function\n */\nexport function useLoaderData<T = any>(): SerializeFrom<T> {\n  let state = useDataRouterState(DataRouterStateHook.UseLoaderData);\n  let routeId = useCurrentRouteId(DataRouterStateHook.UseLoaderData);\n  return state.loaderData[routeId] as SerializeFrom<T>;\n}\n\n/**\n * Returns the [`loader`](../../start/framework/route-module#loader) data for a\n * given route by route ID.\n *\n * Route IDs are created automatically. They are simply the path of the route file\n * relative to the app folder without the extension.\n *\n * | Route Filename               | Route ID               |\n * | ---------------------------- | ---------------------- |\n * | `app/root.tsx`               | `\"root\"`               |\n * | `app/routes/teams.tsx`       | `\"routes/teams\"`       |\n * | `app/whatever/teams.$id.tsx` | `\"whatever/teams.$id\"` |\n *\n * @example\n * import { useRouteLoaderData } from \"react-router\";\n *\n * function SomeComponent() {\n *   const { user } = useRouteLoaderData(\"root\");\n * }\n *\n * // You can also specify your own route ID's manually in your routes.ts file:\n * route(\"/\", \"containers/app.tsx\", { id: \"app\" })\n * useRouteLoaderData(\"app\");\n *\n * @public\n * @category Hooks\n * @mode framework\n * @mode data\n * @param routeId The ID of the route to return loader data from\n * @returns The data returned from the specified route's [`loader`](../../start/framework/route-module#loader)\n * function, or `undefined` if not found\n */\nexport function useRouteLoaderData<T = any>(\n  routeId: string,\n): SerializeFrom<T> | undefined {\n  let state = useDataRouterState(DataRouterStateHook.UseRouteLoaderData);\n  return state.loaderData[routeId] as SerializeFrom<T> | undefined;\n}\n\n/**\n * Returns the [`action`](../../start/framework/route-module#action) data from\n * the most recent `POST` navigation form submission or `undefined` if there\n * hasn't been one.\n *\n * @example\n * import { Form, useActionData } from \"react-router\";\n *\n * export async function action({ request }) {\n *   const body = await request.formData();\n *   const name = body.get(\"visitorsName\");\n *   return { message: `Hello, ${name}` };\n * }\n *\n * export default function Invoices() {\n *   const data = useActionData();\n *   return (\n *     <Form method=\"post\">\n *       <input type=\"text\" name=\"visitorsName\" />\n *       {data ? data.message : \"Waiting...\"}\n *     </Form>\n *   );\n * }\n *\n * @public\n * @category Hooks\n * @mode framework\n * @mode data\n * @returns The data returned from the route's [`action`](../../start/framework/route-module#action)\n * function, or `undefined` if no [`action`](../../start/framework/route-module#action)\n * has been called\n */\nexport function useActionData<T = any>(): SerializeFrom<T> | undefined {\n  let state = useDataRouterState(DataRouterStateHook.UseActionData);\n  let routeId = useCurrentRouteId(DataRouterStateHook.UseLoaderData);\n  return (state.actionData ? state.actionData[routeId] : undefined) as\n    | SerializeFrom<T>\n    | undefined;\n}\n\n/**\n * Accesses the error thrown during an\n * [`action`](../../start/framework/route-module#action),\n * [`loader`](../../start/framework/route-module#loader),\n * or component render to be used in a route module\n * [`ErrorBoundary`](../../start/framework/route-module#errorboundary).\n *\n * @example\n * export function ErrorBoundary() {\n *   const error = useRouteError();\n *   return <div>{error.message}</div>;\n * }\n *\n * @public\n * @category Hooks\n * @mode framework\n * @mode data\n * @returns The error that was thrown during route [loading](../../start/framework/route-module#loader),\n * [`action`](../../start/framework/route-module#action) execution, or rendering\n */\nexport function useRouteError(): unknown {\n  let error = React.useContext(RouteErrorContext);\n  let state = useDataRouterState(DataRouterStateHook.UseRouteError);\n  let routeId = useCurrentRouteId(DataRouterStateHook.UseRouteError);\n\n  // If this was a render error, we put it in a RouteError context inside\n  // of RenderErrorBoundary\n  if (error !== undefined) {\n    return error;\n  }\n\n  // Otherwise look for errors from our data router state\n  return state.errors?.[routeId];\n}\n\n/**\n * Returns the resolved promise value from the closest {@link Await | `<Await>`}.\n *\n * @example\n * function SomeDescendant() {\n *   const value = useAsyncValue();\n *   // ...\n * }\n *\n * // somewhere in your app\n * <Await resolve={somePromise}>\n *   <SomeDescendant />\n * </Await>;\n *\n * @public\n * @category Hooks\n * @mode framework\n * @mode data\n * @returns The resolved value from the nearest {@link Await} component\n */\nexport function useAsyncValue(): unknown {\n  let value = React.useContext(AwaitContext);\n  return value?._data;\n}\n\n/**\n * Returns the rejection value from the closest {@link Await | `<Await>`}.\n *\n * @example\n * import { Await, useAsyncError } from \"react-router\";\n *\n * function ErrorElement() {\n *   const error = useAsyncError();\n *   return (\n *     <p>Uh Oh, something went wrong! {error.message}</p>\n *   );\n * }\n *\n * // somewhere in your app\n * <Await\n *   resolve={promiseThatRejects}\n *   errorElement={<ErrorElement />}\n * />;\n *\n * @public\n * @category Hooks\n * @mode framework\n * @mode data\n * @returns The error that was thrown in the nearest {@link Await} component\n */\nexport function useAsyncError(): unknown {\n  let value = React.useContext(AwaitContext);\n  return value?._error;\n}\n\nlet blockerId = 0;\n\n/**\n * Allow the application to block navigations within the SPA and present the\n * user a confirmation dialog to confirm the navigation. Mostly used to avoid\n * using half-filled form data. This does not handle hard-reloads or\n * cross-origin navigations.\n *\n * The {@link Blocker} object returned by the hook has the following properties:\n *\n * - **`state`**\n *   - `unblocked` - the blocker is idle and has not prevented any navigation\n *   - `blocked` - the blocker has prevented a navigation\n *   - `proceeding` - the blocker is proceeding through from a blocked navigation\n * - **`location`**\n *   - When in a `blocked` state, this represents the {@link Location} to which\n *     we blocked a navigation. When in a `proceeding` state, this is the\n *     location being navigated to after a `blocker.proceed()` call.\n * - **`proceed()`**\n *   - When in a `blocked` state, you may call `blocker.proceed()` to proceed to\n *     the blocked location.\n * - **`reset()`**\n *   - When in a `blocked` state, you may call `blocker.reset()` to return the\n *     blocker to an `unblocked` state and leave the user at the current\n *     location.\n *\n * @example\n * // Boolean version\n * let blocker = useBlocker(value !== \"\");\n *\n * // Function version\n * let blocker = useBlocker(\n *   ({ currentLocation, nextLocation, historyAction }) =>\n *     value !== \"\" &&\n *     currentLocation.pathname !== nextLocation.pathname\n * );\n *\n * @additionalExamples\n * ```tsx\n * import { useCallback, useState } from \"react\";\n * import { BlockerFunction, useBlocker } from \"react-router\";\n *\n * export function ImportantForm() {\n *   const [value, setValue] = useState(\"\");\n *\n *   const shouldBlock = useCallback<BlockerFunction>(\n *     () => value !== \"\",\n *     [value]\n *   );\n *   const blocker = useBlocker(shouldBlock);\n *\n *   return (\n *     <form\n *       onSubmit={(e) => {\n *         e.preventDefault();\n *         setValue(\"\");\n *         if (blocker.state === \"blocked\") {\n *           blocker.proceed();\n *         }\n *       }}\n *     >\n *       <input\n *         name=\"data\"\n *         value={value}\n *         onChange={(e) => setValue(e.target.value)}\n *       />\n *\n *       <button type=\"submit\">Save</button>\n *\n *       {blocker.state === \"blocked\" ? (\n *         <>\n *           <p style={{ color: \"red\" }}>\n *             Blocked the last navigation to\n *           </p>\n *           <button\n *             type=\"button\"\n *             onClick={() => blocker.proceed()}\n *           >\n *             Let me through\n *           </button>\n *           <button\n *             type=\"button\"\n *             onClick={() => blocker.reset()}\n *           >\n *             Keep me here\n *           </button>\n *         </>\n *       ) : blocker.state === \"proceeding\" ? (\n *         <p style={{ color: \"orange\" }}>\n *           Proceeding through blocked navigation\n *         </p>\n *       ) : (\n *         <p style={{ color: \"green\" }}>\n *           Blocker is currently unblocked\n *         </p>\n *       )}\n *     </form>\n *   );\n * }\n * ```\n *\n * @public\n * @category Hooks\n * @mode framework\n * @mode data\n * @param shouldBlock Either a boolean or a function returning a boolean which\n * indicates whether the navigation should be blocked. The function format\n * receives a single object parameter containing the `currentLocation`,\n * `nextLocation`, and `historyAction` of the potential navigation.\n * @returns A {@link Blocker} object with state and reset functionality\n */\nexport function useBlocker(shouldBlock: boolean | BlockerFunction): Blocker {\n  let { router, basename } = useDataRouterContext(DataRouterHook.UseBlocker);\n  let state = useDataRouterState(DataRouterStateHook.UseBlocker);\n\n  let [blockerKey, setBlockerKey] = React.useState(\"\");\n  let blockerFunction = React.useCallback<BlockerFunction>(\n    (arg) => {\n      if (typeof shouldBlock !== \"function\") {\n        return !!shouldBlock;\n      }\n      if (basename === \"/\") {\n        return shouldBlock(arg);\n      }\n\n      // If they provided us a function and we've got an active basename, strip\n      // it from the locations we expose to the user to match the behavior of\n      // useLocation\n      let { currentLocation, nextLocation, historyAction } = arg;\n      return shouldBlock({\n        currentLocation: {\n          ...currentLocation,\n          pathname:\n            stripBasename(currentLocation.pathname, basename) ||\n            currentLocation.pathname,\n        },\n        nextLocation: {\n          ...nextLocation,\n          pathname:\n            stripBasename(nextLocation.pathname, basename) ||\n            nextLocation.pathname,\n        },\n        historyAction,\n      });\n    },\n    [basename, shouldBlock],\n  );\n\n  // This effect is in charge of blocker key assignment and deletion (which is\n  // tightly coupled to the key)\n  React.useEffect(() => {\n    let key = String(++blockerId);\n    setBlockerKey(key);\n    return () => router.deleteBlocker(key);\n  }, [router]);\n\n  // This effect handles assigning the blockerFunction.  This is to handle\n  // unstable blocker function identities, and happens only after the prior\n  // effect so we don't get an orphaned blockerFunction in the router with a\n  // key of \"\".  Until then we just have the IDLE_BLOCKER.\n  React.useEffect(() => {\n    if (blockerKey !== \"\") {\n      router.getBlocker(blockerKey, blockerFunction);\n    }\n  }, [router, blockerKey, blockerFunction]);\n\n  // Prefer the blocker from `state` not `router.state` since DataRouterContext\n  // is memoized so this ensures we update on blocker state updates\n  return blockerKey && state.blockers.has(blockerKey)\n    ? state.blockers.get(blockerKey)!\n    : IDLE_BLOCKER;\n}\n\n// Stable version of useNavigate that is used when we are in the context of\n// a RouterProvider.\nfunction useNavigateStable(): NavigateFunction {\n  let { router } = useDataRouterContext(DataRouterHook.UseNavigateStable);\n  let id = useCurrentRouteId(DataRouterStateHook.UseNavigateStable);\n\n  let activeRef = React.useRef(false);\n  useIsomorphicLayoutEffect(() => {\n    activeRef.current = true;\n  });\n\n  let navigate: NavigateFunction = React.useCallback(\n    async (to: To | number, options: NavigateOptions = {}) => {\n      warning(activeRef.current, navigateEffectWarning);\n\n      // Short circuit here since if this happens on first render the navigate\n      // is useless because we haven't wired up our router subscriber yet\n      if (!activeRef.current) return;\n\n      if (typeof to === \"number\") {\n        await router.navigate(to);\n      } else {\n        await router.navigate(to, { fromRouteId: id, ...options });\n      }\n    },\n    [router, id],\n  );\n\n  return navigate;\n}\n\nconst alreadyWarned: Record<string, boolean> = {};\n\nfunction warningOnce(key: string, cond: boolean, message: string) {\n  if (!cond && !alreadyWarned[key]) {\n    alreadyWarned[key] = true;\n    warning(false, message);\n  }\n}\n\ntype UseRouteArgs = [] | [routeId: keyof RouteModules];\n\n// prettier-ignore\ntype UseRouteResult<Args extends UseRouteArgs> =\n  Args extends [] ? UseRoute<unknown> :\n  Args extends [\"root\"] ? UseRoute<\"root\"> :\n  Args extends [infer RouteId extends keyof RouteModules] ? UseRoute<RouteId> | undefined :\n  never;\n\n// prettier-ignore\ntype UseRoute<RouteId extends keyof RouteModules | unknown> = {\n  handle:\n    RouteId extends keyof RouteModules ?\n      RouteModules[RouteId] extends { handle: infer handle } ? handle :\n      unknown\n    :\n    unknown;\n  loaderData:\n    RouteId extends keyof RouteModules ? GetLoaderData<RouteModules[RouteId]> | undefined :\n    unknown;\n  actionData:\n    RouteId extends keyof RouteModules ? GetActionData<RouteModules[RouteId]> | undefined :\n    unknown;\n};\n\nexport function useRoute<Args extends UseRouteArgs>(\n  ...args: Args\n): UseRouteResult<Args> {\n  const currentRouteId: keyof RouteModules = useCurrentRouteId(\n    DataRouterStateHook.UseRoute,\n  );\n  const id: keyof RouteModules = args[0] ?? currentRouteId;\n\n  const state = useDataRouterState(DataRouterStateHook.UseRoute);\n  const route = state.matches.find(({ route }) => route.id === id);\n\n  if (route === undefined) return undefined as UseRouteResult<Args>;\n  return {\n    handle: route.route.handle,\n    loaderData: state.loaderData[id],\n    actionData: state.actionData?.[id],\n  } as UseRouteResult<Args>;\n}\n"
  },
  {
    "path": "packages/react-router/lib/href.ts",
    "content": "import type { Pages } from \"./types/register\";\nimport type { Equal } from \"./types/utils\";\n\ntype Args = { [K in keyof Pages]: ToArgs<Pages[K][\"params\"]> };\n\n// prettier-ignore\ntype ToArgs<Params extends Record<string, string | undefined>> =\n  // path without params -> no `params` arg\n  Equal<Params, {}> extends true ? [] :\n  // path with only optional params -> optional `params` arg\n  Partial<Params> extends Params ? [Params] | [] :\n  // otherwise, require `params` arg\n  [Params];\n\n/**\n  Returns a resolved URL path for the specified route.\n\n  ```tsx\n  const h = href(\"/:lang?/about\", { lang: \"en\" })\n  // -> `/en/about`\n\n  <Link to={href(\"/products/:id\", { id: \"abc123\" })} />\n  ```\n */\nexport function href<Path extends keyof Args>(\n  path: Path,\n  ...args: Args[Path]\n): string {\n  let params = args[0];\n  let result = trimTrailingSplat(path) // Ignore trailing / and /*, we'll handle it below\n    .replace(\n      /\\/:([\\w-]+)(\\?)?/g, // same regex as in .\\router\\utils.ts: compilePath().\n      (_: string, param: string, questionMark: string | undefined) => {\n        const isRequired = questionMark === undefined;\n        const value = params?.[param];\n        if (isRequired && value === undefined) {\n          throw new Error(\n            `Path '${path}' requires param '${param}' but it was not provided`,\n          );\n        }\n        return value === undefined ? \"\" : \"/\" + value;\n      },\n    );\n\n  if (path.endsWith(\"*\")) {\n    // treat trailing splat the same way as compilePath, and force it to be as if it were `/*`.\n    // `react-router typegen` will not generate the params for a malformed splat, causing a type error, but we can still do the correct thing here.\n    const value = params?.[\"*\"];\n    if (value !== undefined) {\n      result += \"/\" + value;\n    }\n  }\n\n  return result || \"/\";\n}\n\n/**\n * Removes a trailing splat and any number of slashes from the end of the path.\n *\n * Benchmarked to be faster than `path.replace(/\\/*\\*?$/, \"\")`, which backtracks.\n */\nfunction trimTrailingSplat(path: string): string {\n  let i = path.length - 1;\n  let char = path[i];\n  if (char !== \"*\" && char !== \"/\") return path;\n\n  // for/break benchmarks faster than do/while\n  i--;\n  for (; i >= 0; i--) {\n    if (path[i] !== \"/\") break;\n  }\n\n  return path.slice(0, i + 1);\n}\n"
  },
  {
    "path": "packages/react-router/lib/router/history.ts",
    "content": "////////////////////////////////////////////////////////////////////////////////\n//#region Types and Constants\n////////////////////////////////////////////////////////////////////////////////\n\n/**\n * Actions represent the type of change to a location value.\n */\nexport enum Action {\n  /**\n   * A POP indicates a change to an arbitrary index in the history stack, such\n   * as a back or forward navigation. It does not describe the direction of the\n   * navigation, only that the current index changed.\n   *\n   * Note: This is the default action for newly created history objects.\n   */\n  Pop = \"POP\",\n\n  /**\n   * A PUSH indicates a new entry being added to the history stack, such as when\n   * a link is clicked and a new page loads. When this happens, all subsequent\n   * entries in the stack are lost.\n   */\n  Push = \"PUSH\",\n\n  /**\n   * A REPLACE indicates the entry at the current index in the history stack\n   * being replaced by a new one.\n   */\n  Replace = \"REPLACE\",\n}\n\n/**\n * The pathname, search, and hash values of a URL.\n */\nexport interface Path {\n  /**\n   * A URL pathname, beginning with a /.\n   */\n  pathname: string;\n\n  /**\n   * A URL search string, beginning with a ?.\n   */\n  search: string;\n\n  /**\n   * A URL fragment identifier, beginning with a #.\n   */\n  hash: string;\n}\n\n// TODO: (v7) Change the Location generic default from `any` to `unknown` and\n// remove Remix `useLocation` wrapper.\n\n/**\n * An entry in a history stack. A location contains information about the\n * URL path, as well as possibly some arbitrary state and a key.\n */\nexport interface Location<State = any> extends Path {\n  /**\n   * A value of arbitrary data associated with this location.\n   */\n  state: State;\n\n  /**\n   * A unique string associated with this location. May be used to safely store\n   * and retrieve data in some other storage API, like `localStorage`.\n   *\n   * Note: This value is always \"default\" on the initial location.\n   */\n  key: string;\n\n  /**\n   * The masked location displayed in the URL bar, which differs from the URL the\n   * router is operating on\n   */\n  unstable_mask: Path | undefined;\n}\n\n/**\n * A change to the current location.\n */\nexport interface Update {\n  /**\n   * The action that triggered the change.\n   */\n  action: Action;\n\n  /**\n   * The new location.\n   */\n  location: Location;\n\n  /**\n   * The delta between this location and the former location in the history stack\n   */\n  delta: number | null;\n}\n\n/**\n * A function that receives notifications about location changes.\n */\nexport interface Listener {\n  (update: Update): void;\n}\n\n/**\n * Describes a location that is the destination of some navigation used in\n * {@link Link}, {@link useNavigate}, etc.\n */\nexport type To = string | Partial<Path>;\n\n/**\n * A history is an interface to the navigation stack. The history serves as the\n * source of truth for the current location, as well as provides a set of\n * methods that may be used to change it.\n *\n * It is similar to the DOM's `window.history` object, but with a smaller, more\n * focused API.\n */\nexport interface History {\n  /**\n   * The last action that modified the current location. This will always be\n   * Action.Pop when a history instance is first created. This value is mutable.\n   */\n  readonly action: Action;\n\n  /**\n   * The current location. This value is mutable.\n   */\n  readonly location: Location;\n\n  /**\n   * Returns a valid href for the given `to` value that may be used as\n   * the value of an <a href> attribute.\n   *\n   * @param to - The destination URL\n   */\n  createHref(to: To): string;\n\n  /**\n   * Returns a URL for the given `to` value\n   *\n   * @param to - The destination URL\n   */\n  createURL(to: To): URL;\n\n  /**\n   * Encode a location the same way window.history would do (no-op for memory\n   * history) so we ensure our PUSH/REPLACE navigations for data routers\n   * behave the same as POP\n   *\n   * @param to Unencoded path\n   */\n  encodeLocation(to: To): Path;\n\n  /**\n   * Pushes a new location onto the history stack, increasing its length by one.\n   * If there were any entries in the stack after the current one, they are\n   * lost.\n   *\n   * @param to - The new URL\n   * @param state - Data to associate with the new location\n   */\n  push(to: To, state?: any): void;\n\n  /**\n   * Replaces the current location in the history stack with a new one.  The\n   * location that was replaced will no longer be available.\n   *\n   * @param to - The new URL\n   * @param state - Data to associate with the new location\n   */\n  replace(to: To, state?: any): void;\n\n  /**\n   * Navigates `n` entries backward/forward in the history stack relative to the\n   * current index. For example, a \"back\" navigation would use go(-1).\n   *\n   * @param delta - The delta in the stack index\n   */\n  go(delta: number): void;\n\n  /**\n   * Sets up a listener that will be called whenever the current location\n   * changes.\n   *\n   * @param listener - A function that will be called when the location changes\n   * @returns unlisten - A function that may be used to stop listening\n   */\n  listen(listener: Listener): () => void;\n}\n\ntype HistoryState = {\n  usr: any;\n  key?: string;\n  idx: number;\n  masked?: Path;\n};\n\nconst PopStateEventType = \"popstate\";\n\nfunction isLocation(obj: unknown): obj is Location {\n  return (\n    typeof obj === \"object\" &&\n    obj != null &&\n    \"pathname\" in obj &&\n    \"search\" in obj &&\n    \"hash\" in obj &&\n    \"state\" in obj &&\n    \"key\" in obj\n  );\n}\n\n//#endregion\n\n////////////////////////////////////////////////////////////////////////////////\n//#region Memory History\n////////////////////////////////////////////////////////////////////////////////\n\n/**\n * A user-supplied object that describes a location. Used when providing\n * entries to `createMemoryHistory` via its `initialEntries` option.\n */\nexport type InitialEntry = string | Partial<Location>;\n\nexport type MemoryHistoryOptions = {\n  initialEntries?: InitialEntry[];\n  initialIndex?: number;\n  v5Compat?: boolean;\n};\n\n/**\n * A memory history stores locations in memory. This is useful in stateful\n * environments where there is no web browser, such as node tests or React\n * Native.\n */\nexport interface MemoryHistory extends History {\n  /**\n   * The current index in the history stack.\n   */\n  readonly index: number;\n}\n\n/**\n * Memory history stores the current location in memory. It is designed for use\n * in stateful non-browser environments like tests and React Native.\n */\nexport function createMemoryHistory(\n  options: MemoryHistoryOptions = {},\n): MemoryHistory {\n  let { initialEntries = [\"/\"], initialIndex, v5Compat = false } = options;\n  let entries: Location[]; // Declare so we can access from createMemoryLocation\n  entries = initialEntries.map((entry, index) =>\n    createMemoryLocation(\n      entry,\n      typeof entry === \"string\" ? null : entry.state,\n      index === 0 ? \"default\" : undefined,\n      typeof entry === \"string\" ? undefined : entry.unstable_mask,\n    ),\n  );\n  let index = clampIndex(\n    initialIndex == null ? entries.length - 1 : initialIndex,\n  );\n  let action = Action.Pop;\n  let listener: Listener | null = null;\n\n  function clampIndex(n: number): number {\n    return Math.min(Math.max(n, 0), entries.length - 1);\n  }\n  function getCurrentLocation(): Location {\n    return entries[index];\n  }\n  function createMemoryLocation(\n    to: To,\n    state: any = null,\n    key?: string,\n    unstable_mask?: Path,\n  ): Location {\n    let location = createLocation(\n      entries ? getCurrentLocation().pathname : \"/\",\n      to,\n      state,\n      key,\n      unstable_mask,\n    );\n    warning(\n      location.pathname.charAt(0) === \"/\",\n      `relative pathnames are not supported in memory history: ${JSON.stringify(\n        to,\n      )}`,\n    );\n    return location;\n  }\n\n  function createHref(to: To) {\n    return typeof to === \"string\" ? to : createPath(to);\n  }\n\n  let history: MemoryHistory = {\n    get index() {\n      return index;\n    },\n    get action() {\n      return action;\n    },\n    get location() {\n      return getCurrentLocation();\n    },\n    createHref,\n    createURL(to) {\n      return new URL(createHref(to), \"http://localhost\");\n    },\n    encodeLocation(to: To) {\n      let path = typeof to === \"string\" ? parsePath(to) : to;\n      return {\n        pathname: path.pathname || \"\",\n        search: path.search || \"\",\n        hash: path.hash || \"\",\n      };\n    },\n    push(to, state) {\n      action = Action.Push;\n      let nextLocation = isLocation(to) ? to : createMemoryLocation(to, state);\n      index += 1;\n      entries.splice(index, entries.length, nextLocation);\n      if (v5Compat && listener) {\n        listener({ action, location: nextLocation, delta: 1 });\n      }\n    },\n    replace(to, state) {\n      action = Action.Replace;\n      let nextLocation = isLocation(to) ? to : createMemoryLocation(to, state);\n      entries[index] = nextLocation;\n      if (v5Compat && listener) {\n        listener({ action, location: nextLocation, delta: 0 });\n      }\n    },\n    go(delta) {\n      action = Action.Pop;\n      let nextIndex = clampIndex(index + delta);\n      let nextLocation = entries[nextIndex];\n      index = nextIndex;\n      if (listener) {\n        listener({ action, location: nextLocation, delta });\n      }\n    },\n    listen(fn: Listener) {\n      listener = fn;\n      return () => {\n        listener = null;\n      };\n    },\n  };\n\n  return history;\n}\n//#endregion\n\n////////////////////////////////////////////////////////////////////////////////\n//#region Browser History\n////////////////////////////////////////////////////////////////////////////////\n\n/**\n * A browser history stores the current location in regular URLs in a web\n * browser environment. This is the standard for most web apps and provides the\n * cleanest URLs the browser's address bar.\n *\n * @see https://github.com/remix-run/history/tree/main/docs/api-reference.md#browserhistory\n */\nexport interface BrowserHistory extends UrlHistory {}\n\nexport type BrowserHistoryOptions = UrlHistoryOptions;\n\n/**\n * Browser history stores the location in regular URLs. This is the standard for\n * most web apps, but it requires some configuration on the server to ensure you\n * serve the same app at multiple URLs.\n *\n * @see https://github.com/remix-run/history/tree/main/docs/api-reference.md#createbrowserhistory\n */\nexport function createBrowserHistory(\n  options: BrowserHistoryOptions = {},\n): BrowserHistory {\n  function createBrowserLocation(\n    window: Window,\n    globalHistory: Window[\"history\"],\n  ) {\n    let maskedLocation = (globalHistory.state as HistoryState)?.masked;\n    let { pathname, search, hash } = maskedLocation || window.location;\n    return createLocation(\n      \"\",\n      { pathname, search, hash },\n      // state defaults to `null` because `window.history.state` does\n      (globalHistory.state && globalHistory.state.usr) || null,\n      (globalHistory.state && globalHistory.state.key) || \"default\",\n      maskedLocation\n        ? {\n            pathname: window.location.pathname,\n            search: window.location.search,\n            hash: window.location.hash,\n          }\n        : undefined,\n    );\n  }\n\n  function createBrowserHref(window: Window, to: To) {\n    return typeof to === \"string\" ? to : createPath(to);\n  }\n\n  return getUrlBasedHistory(\n    createBrowserLocation,\n    createBrowserHref,\n    null,\n    options,\n  );\n}\n//#endregion\n\n////////////////////////////////////////////////////////////////////////////////\n//#region Hash History\n////////////////////////////////////////////////////////////////////////////////\n\n/**\n * A hash history stores the current location in the fragment identifier portion\n * of the URL in a web browser environment.\n *\n * This is ideal for apps that do not control the server for some reason\n * (because the fragment identifier is never sent to the server), including some\n * shared hosting environments that do not provide fine-grained controls over\n * which pages are served at which URLs.\n *\n * @see https://github.com/remix-run/history/tree/main/docs/api-reference.md#hashhistory\n */\nexport interface HashHistory extends UrlHistory {}\n\nexport type HashHistoryOptions = UrlHistoryOptions;\n\n/**\n * Hash history stores the location in window.location.hash. This makes it ideal\n * for situations where you don't want to send the location to the server for\n * some reason, either because you do cannot configure it or the URL space is\n * reserved for something else.\n *\n * @see https://github.com/remix-run/history/tree/main/docs/api-reference.md#createhashhistory\n */\nexport function createHashHistory(\n  options: HashHistoryOptions = {},\n): HashHistory {\n  function createHashLocation(\n    window: Window,\n    globalHistory: Window[\"history\"],\n  ) {\n    let {\n      pathname = \"/\",\n      search = \"\",\n      hash = \"\",\n    } = parsePath(window.location.hash.substring(1));\n\n    // Hash URL should always have a leading / just like window.location.pathname\n    // does, so if an app ends up at a route like /#something then we add a\n    // leading slash so all of our path-matching behaves the same as if it would\n    // in a browser router.  This is particularly important when there exists a\n    // root splat route (<Route path=\"*\">) since that matches internally against\n    // \"/*\" and we'd expect /#something to 404 in a hash router app.\n    if (!pathname.startsWith(\"/\") && !pathname.startsWith(\".\")) {\n      pathname = \"/\" + pathname;\n    }\n\n    return createLocation(\n      \"\",\n      { pathname, search, hash },\n      // state defaults to `null` because `window.history.state` does\n      (globalHistory.state && globalHistory.state.usr) || null,\n      (globalHistory.state && globalHistory.state.key) || \"default\",\n    );\n  }\n\n  function createHashHref(window: Window, to: To) {\n    let base = window.document.querySelector(\"base\");\n    let href = \"\";\n\n    if (base && base.getAttribute(\"href\")) {\n      let url = window.location.href;\n      let hashIndex = url.indexOf(\"#\");\n      href = hashIndex === -1 ? url : url.slice(0, hashIndex);\n    }\n\n    return href + \"#\" + (typeof to === \"string\" ? to : createPath(to));\n  }\n\n  function validateHashLocation(location: Location, to: To) {\n    warning(\n      location.pathname.charAt(0) === \"/\",\n      `relative pathnames are not supported in hash history.push(${JSON.stringify(\n        to,\n      )})`,\n    );\n  }\n\n  return getUrlBasedHistory(\n    createHashLocation,\n    createHashHref,\n    validateHashLocation,\n    options,\n  );\n}\n//#endregion\n\n////////////////////////////////////////////////////////////////////////////////\n//#region UTILS\n////////////////////////////////////////////////////////////////////////////////\n\n/**\n * @private\n */\nexport function invariant(value: boolean, message?: string): asserts value;\nexport function invariant<T>(\n  value: T | null | undefined,\n  message?: string,\n): asserts value is T;\nexport function invariant(value: any, message?: string) {\n  if (value === false || value === null || typeof value === \"undefined\") {\n    throw new Error(message);\n  }\n}\n\nexport function warning(cond: any, message: string) {\n  if (!cond) {\n    if (typeof console !== \"undefined\") console.warn(message);\n\n    try {\n      // Welcome to debugging history!\n      //\n      // This error is thrown as a convenience, so you can more easily\n      // find the source for a warning that appears in the console by\n      // enabling \"pause on exceptions\" in your JavaScript debugger.\n      throw new Error(message);\n    } catch (e) {}\n  }\n}\n\nfunction createKey() {\n  return Math.random().toString(36).substring(2, 10);\n}\n\n/**\n * For browser-based histories, we combine the state and key into an object\n */\nfunction getHistoryState(location: Location, index: number): HistoryState {\n  return {\n    usr: location.state,\n    key: location.key,\n    idx: index,\n    masked: location.unstable_mask\n      ? {\n          pathname: location.pathname,\n          search: location.search,\n          hash: location.hash,\n        }\n      : undefined,\n  };\n}\n\n/**\n * Creates a Location object with a unique key from the given Path\n */\nexport function createLocation(\n  current: string | Location,\n  to: To,\n  state: any = null,\n  key?: string,\n  unstable_mask?: Path,\n): Readonly<Location> {\n  let location: Readonly<Location> = {\n    pathname: typeof current === \"string\" ? current : current.pathname,\n    search: \"\",\n    hash: \"\",\n    ...(typeof to === \"string\" ? parsePath(to) : to),\n    state,\n    // TODO: This could be cleaned up.  push/replace should probably just take\n    // full Locations now and avoid the need to run through this flow at all\n    // But that's a pretty big refactor to the current test suite so going to\n    // keep as is for the time being and just let any incoming keys take precedence\n    key: (to && (to as Location).key) || key || createKey(),\n    unstable_mask,\n  };\n  return location;\n}\n\n/**\n * Creates a string URL path from the given pathname, search, and hash components.\n *\n * @category Utils\n */\nexport function createPath({\n  pathname = \"/\",\n  search = \"\",\n  hash = \"\",\n}: Partial<Path>) {\n  if (search && search !== \"?\")\n    pathname += search.charAt(0) === \"?\" ? search : \"?\" + search;\n  if (hash && hash !== \"#\")\n    pathname += hash.charAt(0) === \"#\" ? hash : \"#\" + hash;\n  return pathname;\n}\n\n/**\n * Parses a string URL path into its separate pathname, search, and hash components.\n *\n * @category Utils\n */\nexport function parsePath(path: string): Partial<Path> {\n  let parsedPath: Partial<Path> = {};\n\n  if (path) {\n    let hashIndex = path.indexOf(\"#\");\n    if (hashIndex >= 0) {\n      parsedPath.hash = path.substring(hashIndex);\n      path = path.substring(0, hashIndex);\n    }\n\n    let searchIndex = path.indexOf(\"?\");\n    if (searchIndex >= 0) {\n      parsedPath.search = path.substring(searchIndex);\n      path = path.substring(0, searchIndex);\n    }\n\n    if (path) {\n      parsedPath.pathname = path;\n    }\n  }\n\n  return parsedPath;\n}\n\nexport interface UrlHistory extends History {}\n\nexport type UrlHistoryOptions = {\n  window?: Window;\n  v5Compat?: boolean;\n};\n\nfunction getUrlBasedHistory(\n  getLocation: (window: Window, globalHistory: Window[\"history\"]) => Location,\n  createHref: (window: Window, to: To) => string,\n  validateLocation: ((location: Location, to: To) => void) | null,\n  options: UrlHistoryOptions = {},\n): UrlHistory {\n  let { window = document.defaultView!, v5Compat = false } = options;\n  let globalHistory = window.history;\n  let action = Action.Pop;\n  let listener: Listener | null = null;\n\n  let index = getIndex()!;\n  // Index should only be null when we initialize. If not, it's because the\n  // user called history.pushState or history.replaceState directly, in which\n  // case we should log a warning as it will result in bugs.\n  if (index == null) {\n    index = 0;\n    globalHistory.replaceState({ ...globalHistory.state, idx: index }, \"\");\n  }\n\n  function getIndex(): number {\n    let state = globalHistory.state || { idx: null };\n    return state.idx;\n  }\n\n  function handlePop() {\n    action = Action.Pop;\n    let nextIndex = getIndex();\n    let delta = nextIndex == null ? null : nextIndex - index;\n    index = nextIndex;\n    if (listener) {\n      listener({ action, location: history.location, delta });\n    }\n  }\n\n  function push(to: Location | To, state?: any) {\n    action = Action.Push;\n    let location = isLocation(to)\n      ? to\n      : createLocation(history.location, to, state);\n    if (validateLocation) validateLocation(location, to);\n\n    index = getIndex() + 1;\n    let historyState = getHistoryState(location, index);\n    let url = history.createHref(location.unstable_mask || location);\n\n    // try...catch because iOS limits us to 100 pushState calls :/\n    try {\n      globalHistory.pushState(historyState, \"\", url);\n    } catch (error) {\n      // If the exception is because `state` can't be serialized, let that throw\n      // outwards just like a replace call would so the dev knows the cause\n      // https://html.spec.whatwg.org/multipage/nav-history-apis.html#shared-history-push/replace-state-steps\n      // https://html.spec.whatwg.org/multipage/structured-data.html#structuredserializeinternal\n      if (error instanceof DOMException && error.name === \"DataCloneError\") {\n        throw error;\n      }\n      // They are going to lose state here, but there is no real\n      // way to warn them about it since the page will refresh...\n      window.location.assign(url);\n    }\n\n    if (v5Compat && listener) {\n      listener({ action, location: history.location, delta: 1 });\n    }\n  }\n\n  function replace(to: To, state?: any) {\n    action = Action.Replace;\n    let location = isLocation(to)\n      ? to\n      : createLocation(history.location, to, state);\n    if (validateLocation) validateLocation(location, to);\n\n    index = getIndex();\n    let historyState = getHistoryState(location, index);\n    let url = history.createHref(location.unstable_mask || location);\n    globalHistory.replaceState(historyState, \"\", url);\n\n    if (v5Compat && listener) {\n      listener({ action, location: history.location, delta: 0 });\n    }\n  }\n\n  function createURL(to: To): URL {\n    return createBrowserURLImpl(to);\n  }\n\n  let history: History = {\n    get action() {\n      return action;\n    },\n    get location() {\n      return getLocation(window, globalHistory);\n    },\n    listen(fn: Listener) {\n      if (listener) {\n        throw new Error(\"A history only accepts one active listener\");\n      }\n      window.addEventListener(PopStateEventType, handlePop);\n      listener = fn;\n\n      return () => {\n        window.removeEventListener(PopStateEventType, handlePop);\n        listener = null;\n      };\n    },\n    createHref(to) {\n      return createHref(window, to);\n    },\n    createURL,\n    encodeLocation(to) {\n      // Encode a Location the same way window.location would\n      let url = createURL(to);\n      return {\n        pathname: url.pathname,\n        search: url.search,\n        hash: url.hash,\n      };\n    },\n    push,\n    replace,\n    go(n) {\n      return globalHistory.go(n);\n    },\n  };\n\n  return history;\n}\n\nexport function createBrowserURLImpl(to: To, isAbsolute = false): URL {\n  let base = \"http://localhost\";\n  if (typeof window !== \"undefined\") {\n    // window.location.origin is \"null\" (the literal string value) in Firefox\n    // under certain conditions, notably when serving from a local HTML file\n    // See https://bugzilla.mozilla.org/show_bug.cgi?id=878297\n    base =\n      window.location.origin !== \"null\"\n        ? window.location.origin\n        : window.location.href;\n  }\n\n  invariant(base, \"No window.location.(origin|href) available to create URL\");\n\n  let href = typeof to === \"string\" ? to : createPath(to);\n\n  // Treating this as a full URL will strip any trailing spaces so we need to\n  // pre-encode them since they might be part of a matching splat param from\n  // an ancestor route\n  href = href.replace(/ $/, \"%20\");\n\n  // If this isn't a usage for absolute URLs (currently only for redirects),\n  // then we need to avoid the URL constructor treating a leading double slash\n  // as a protocol-less URL. By prepending the base, it forces the double slash\n  // to be parsed correctly as part of the pathname.\n  if (!isAbsolute && href.startsWith(\"//\")) {\n    // new URL('//', 'https://localhost') -> error!\n    // new URL('https://localhost//', 'https://localhost') -> no error!\n    href = base + href;\n  }\n\n  return new URL(href, base);\n}\n\n//#endregion\n"
  },
  {
    "path": "packages/react-router/lib/router/instrumentation.ts",
    "content": "import type { AppLoadContext } from \"../server-runtime/data\";\nimport type { RequestHandler } from \"../server-runtime/server\";\nimport type { MiddlewareEnabled } from \"../types/future\";\nimport { createPath, invariant } from \"./history\";\nimport type { Router } from \"./router\";\nimport type {\n  ActionFunctionArgs,\n  AgnosticDataRouteObject,\n  FormEncType,\n  HTMLFormMethod,\n  LazyRouteObject,\n  LoaderFunction,\n  LoaderFunctionArgs,\n  MaybePromise,\n  MiddlewareFunction,\n  RouterContext,\n  RouterContextProvider,\n} from \"./utils\";\n\n// Public APIs\nexport type unstable_ServerInstrumentation = {\n  handler?: unstable_InstrumentRequestHandlerFunction;\n  route?: unstable_InstrumentRouteFunction;\n};\n\nexport type unstable_ClientInstrumentation = {\n  router?: unstable_InstrumentRouterFunction;\n  route?: unstable_InstrumentRouteFunction;\n};\n\nexport type unstable_InstrumentRequestHandlerFunction = (\n  handler: InstrumentableRequestHandler,\n) => void;\n\nexport type unstable_InstrumentRouterFunction = (\n  router: InstrumentableRouter,\n) => void;\n\nexport type unstable_InstrumentRouteFunction = (\n  route: InstrumentableRoute,\n) => void;\n\nexport type unstable_InstrumentationHandlerResult =\n  | { status: \"success\"; error: undefined }\n  | { status: \"error\"; error: Error };\n\n// Shared\ntype InstrumentFunction<T> = (\n  handler: () => Promise<unstable_InstrumentationHandlerResult>,\n  info: T,\n) => Promise<void>;\n\ntype InstrumentationInfo =\n  | RouteLazyInstrumentationInfo\n  | RouteHandlerInstrumentationInfo\n  | RouterNavigationInstrumentationInfo\n  | RouterFetchInstrumentationInfo\n  | RequestHandlerInstrumentationInfo;\n\ntype ReadonlyRequest = {\n  method: string;\n  url: string;\n  headers: Pick<Headers, \"get\">;\n};\n\ntype ReadonlyContext = MiddlewareEnabled extends true\n  ? Pick<RouterContextProvider, \"get\">\n  : Readonly<AppLoadContext>;\n\n// Route Instrumentation\ntype InstrumentableRoute = {\n  id: string;\n  index: boolean | undefined;\n  path: string | undefined;\n  instrument(instrumentations: RouteInstrumentations): void;\n};\n\ntype RouteInstrumentations = {\n  lazy?: InstrumentFunction<RouteLazyInstrumentationInfo>;\n  \"lazy.loader\"?: InstrumentFunction<RouteLazyInstrumentationInfo>;\n  \"lazy.action\"?: InstrumentFunction<RouteLazyInstrumentationInfo>;\n  \"lazy.middleware\"?: InstrumentFunction<RouteLazyInstrumentationInfo>;\n  middleware?: InstrumentFunction<RouteHandlerInstrumentationInfo>;\n  loader?: InstrumentFunction<RouteHandlerInstrumentationInfo>;\n  action?: InstrumentFunction<RouteHandlerInstrumentationInfo>;\n};\n\ntype RouteLazyInstrumentationInfo = undefined;\n\ntype RouteHandlerInstrumentationInfo = Readonly<{\n  request: ReadonlyRequest;\n  params: LoaderFunctionArgs[\"params\"];\n  unstable_pattern: string;\n  context: ReadonlyContext;\n}>;\n\n// Router Instrumentation\ntype InstrumentableRouter = {\n  instrument(instrumentations: RouterInstrumentations): void;\n};\n\ntype RouterInstrumentations = {\n  navigate?: InstrumentFunction<RouterNavigationInstrumentationInfo>;\n  fetch?: InstrumentFunction<RouterFetchInstrumentationInfo>;\n};\n\ntype RouterNavigationInstrumentationInfo = Readonly<{\n  to: string | number;\n  currentUrl: string;\n  formMethod?: HTMLFormMethod;\n  formEncType?: FormEncType;\n  formData?: FormData;\n  body?: any;\n}>;\n\ntype RouterFetchInstrumentationInfo = Readonly<{\n  href: string;\n  currentUrl: string;\n  fetcherKey: string;\n  formMethod?: HTMLFormMethod;\n  formEncType?: FormEncType;\n  formData?: FormData;\n  body?: any;\n}>;\n\n// Request Handler Instrumentation\ntype InstrumentableRequestHandler = {\n  instrument(instrumentations: RequestHandlerInstrumentations): void;\n};\n\ntype RequestHandlerInstrumentations = {\n  request?: InstrumentFunction<RequestHandlerInstrumentationInfo>;\n};\n\ntype RequestHandlerInstrumentationInfo = Readonly<{\n  request: ReadonlyRequest;\n  context: ReadonlyContext | undefined;\n}>;\n\nconst UninstrumentedSymbol = Symbol(\"Uninstrumented\");\n\nexport function getRouteInstrumentationUpdates(\n  fns: unstable_InstrumentRouteFunction[],\n  route: Readonly<AgnosticDataRouteObject>,\n) {\n  let aggregated: {\n    lazy: InstrumentFunction<RouteLazyInstrumentationInfo>[];\n    \"lazy.loader\": InstrumentFunction<RouteLazyInstrumentationInfo>[];\n    \"lazy.action\": InstrumentFunction<RouteLazyInstrumentationInfo>[];\n    \"lazy.middleware\": InstrumentFunction<RouteLazyInstrumentationInfo>[];\n    middleware: InstrumentFunction<RouteHandlerInstrumentationInfo>[];\n    loader: InstrumentFunction<RouteHandlerInstrumentationInfo>[];\n    action: InstrumentFunction<RouteHandlerInstrumentationInfo>[];\n  } = {\n    lazy: [],\n    \"lazy.loader\": [],\n    \"lazy.action\": [],\n    \"lazy.middleware\": [],\n    middleware: [],\n    loader: [],\n    action: [],\n  };\n\n  fns.forEach((fn) =>\n    fn({\n      id: route.id,\n      index: route.index,\n      path: route.path,\n      instrument(i) {\n        let keys = Object.keys(aggregated) as Array<keyof typeof aggregated>;\n        for (let key of keys) {\n          if (i[key]) {\n            aggregated[key].push(i[key] as any);\n          }\n        }\n      },\n    }),\n  );\n\n  let updates: {\n    middleware?: AgnosticDataRouteObject[\"middleware\"];\n    loader?: AgnosticDataRouteObject[\"loader\"];\n    action?: AgnosticDataRouteObject[\"action\"];\n    lazy?: AgnosticDataRouteObject[\"lazy\"];\n  } = {};\n\n  // Instrument lazy functions\n  if (typeof route.lazy === \"function\" && aggregated.lazy.length > 0) {\n    let instrumented = wrapImpl(aggregated.lazy, route.lazy, () => undefined);\n    if (instrumented) {\n      updates.lazy = instrumented as AgnosticDataRouteObject[\"lazy\"];\n    }\n  }\n\n  // Instrument the lazy object format\n  if (typeof route.lazy === \"object\") {\n    let lazyObject: LazyRouteObject<AgnosticDataRouteObject> = route.lazy;\n    ([\"middleware\", \"loader\", \"action\"] as const).forEach((key) => {\n      let lazyFn = lazyObject[key];\n      let instrumentations = aggregated[`lazy.${key}`];\n      if (typeof lazyFn === \"function\" && instrumentations.length > 0) {\n        let instrumented = wrapImpl(instrumentations, lazyFn, () => undefined);\n        if (instrumented) {\n          updates.lazy = Object.assign(updates.lazy || {}, {\n            [key]: instrumented,\n          });\n        }\n      }\n    });\n  }\n\n  // Instrument loader/action functions\n  ([\"loader\", \"action\"] as const).forEach((key) => {\n    let handler = route[key];\n    if (typeof handler === \"function\" && aggregated[key].length > 0) {\n      // @ts-expect-error\n      let original = handler[UninstrumentedSymbol] ?? handler;\n      let instrumented = wrapImpl(aggregated[key], original, (...args) =>\n        getHandlerInfo(args[0] as LoaderFunctionArgs | ActionFunctionArgs),\n      );\n      if (instrumented) {\n        if (key === \"loader\" && original.hydrate === true) {\n          (instrumented as LoaderFunction).hydrate = true;\n        }\n        // @ts-expect-error\n        instrumented[UninstrumentedSymbol] = original;\n        updates[key] = instrumented;\n      }\n    }\n  });\n\n  // Instrument middleware functions\n  if (\n    route.middleware &&\n    route.middleware.length > 0 &&\n    aggregated.middleware.length > 0\n  ) {\n    updates.middleware = route.middleware.map((middleware) => {\n      // @ts-expect-error\n      let original = middleware[UninstrumentedSymbol] ?? middleware;\n      let instrumented = wrapImpl(aggregated.middleware, original, (...args) =>\n        getHandlerInfo(args[0] as Parameters<MiddlewareFunction>[0]),\n      );\n      if (instrumented) {\n        // @ts-expect-error\n        instrumented[UninstrumentedSymbol] = original;\n        return instrumented;\n      }\n      return middleware;\n    });\n  }\n\n  return updates;\n}\n\nexport function instrumentClientSideRouter(\n  router: Router,\n  fns: unstable_InstrumentRouterFunction[],\n): Router {\n  let aggregated: {\n    navigate: InstrumentFunction<RouterNavigationInstrumentationInfo>[];\n    fetch: InstrumentFunction<RouterFetchInstrumentationInfo>[];\n  } = {\n    navigate: [],\n    fetch: [],\n  };\n\n  fns.forEach((fn) =>\n    fn({\n      instrument(i) {\n        let keys = Object.keys(i) as Array<keyof RouterInstrumentations>;\n        for (let key of keys) {\n          if (i[key]) {\n            aggregated[key].push(i[key] as any);\n          }\n        }\n      },\n    }),\n  );\n\n  if (aggregated.navigate.length > 0) {\n    // @ts-expect-error\n    let navigate = router.navigate[UninstrumentedSymbol] ?? router.navigate;\n    let instrumentedNavigate = wrapImpl(\n      aggregated.navigate,\n      navigate,\n      (...args) => {\n        let [to, opts] = args as Parameters<Router[\"navigate\"]>;\n        return {\n          to:\n            typeof to === \"number\" || typeof to === \"string\"\n              ? to\n              : to\n                ? createPath(to)\n                : \".\",\n          ...getRouterInfo(router, opts ?? {}),\n        } satisfies RouterNavigationInstrumentationInfo;\n      },\n    ) as Router[\"navigate\"];\n    if (instrumentedNavigate) {\n      // @ts-expect-error\n      instrumentedNavigate[UninstrumentedSymbol] = navigate;\n      router.navigate = instrumentedNavigate;\n    }\n  }\n\n  if (aggregated.fetch.length > 0) {\n    // @ts-expect-error\n    let fetch = router.fetch[UninstrumentedSymbol] ?? router.fetch;\n    let instrumentedFetch = wrapImpl(aggregated.fetch, fetch, (...args) => {\n      let [key, , href, opts] = args as Parameters<Router[\"fetch\"]>;\n      return {\n        href: href ?? \".\",\n        fetcherKey: key,\n        ...getRouterInfo(router, opts ?? {}),\n      } satisfies RouterFetchInstrumentationInfo;\n    }) as Router[\"fetch\"];\n    if (instrumentedFetch) {\n      // @ts-expect-error\n      instrumentedFetch[UninstrumentedSymbol] = fetch;\n      router.fetch = instrumentedFetch;\n    }\n  }\n\n  return router;\n}\n\nexport function instrumentHandler(\n  handler: RequestHandler,\n  fns: unstable_InstrumentRequestHandlerFunction[],\n): RequestHandler {\n  let aggregated: {\n    request: InstrumentFunction<RequestHandlerInstrumentationInfo>[];\n  } = {\n    request: [],\n  };\n\n  fns.forEach((fn) =>\n    fn({\n      instrument(i) {\n        let keys = Object.keys(i) as Array<keyof typeof i>;\n        for (let key of keys) {\n          if (i[key]) {\n            aggregated[key].push(i[key] as any);\n          }\n        }\n      },\n    }),\n  );\n\n  let instrumentedHandler = handler;\n\n  if (aggregated.request.length > 0) {\n    instrumentedHandler = wrapImpl(aggregated.request, handler, (...args) => {\n      let [request, context] = args as Parameters<RequestHandler>;\n      return {\n        request: getReadonlyRequest(request),\n        context: context != null ? getReadonlyContext(context) : context,\n      } satisfies RequestHandlerInstrumentationInfo;\n    }) as RequestHandler;\n  }\n\n  return instrumentedHandler;\n}\n\nfunction wrapImpl<T extends InstrumentationInfo>(\n  impls: InstrumentFunction<T>[],\n  handler: (...args: any[]) => MaybePromise<any>,\n  getInfo: (...args: unknown[]) => T,\n) {\n  if (impls.length === 0) {\n    return null;\n  }\n  return async (...args: unknown[]) => {\n    let result = await recurseRight(\n      impls,\n      getInfo(...args),\n      () => handler(...args),\n      impls.length - 1,\n    );\n    if (result.type === \"error\") {\n      throw result.value;\n    }\n    return result.value;\n  };\n}\n\ntype RecurseResult = { type: \"success\" | \"error\"; value: unknown };\n\nasync function recurseRight<T extends InstrumentationInfo>(\n  impls: InstrumentFunction<T>[],\n  info: T,\n  handler: () => MaybePromise<void>,\n  index: number,\n): Promise<RecurseResult> {\n  let impl = impls[index];\n  let result: RecurseResult | undefined;\n  if (!impl) {\n    try {\n      let value = await handler();\n      result = { type: \"success\", value };\n    } catch (e) {\n      result = { type: \"error\", value: e };\n    }\n  } else {\n    // If they forget to call the handler, or if they throw before calling the\n    // handler, we need to ensure the handlers still gets called\n    let handlerPromise: ReturnType<typeof recurseRight> | undefined = undefined;\n    let callHandler =\n      async (): Promise<unstable_InstrumentationHandlerResult> => {\n        if (handlerPromise) {\n          console.error(\"You cannot call instrumented handlers more than once\");\n        } else {\n          handlerPromise = recurseRight(impls, info, handler, index - 1);\n        }\n        result = await handlerPromise;\n        invariant(result, \"Expected a result\");\n        if (result.type === \"error\" && result.value instanceof Error) {\n          return { status: \"error\", error: result.value };\n        }\n        return { status: \"success\", error: undefined };\n      };\n\n    try {\n      await impl(callHandler, info);\n    } catch (e) {\n      console.error(\"An instrumentation function threw an error:\", e);\n    }\n\n    if (!handlerPromise) {\n      await callHandler();\n    }\n\n    // If the user forgot to await the handler, we can wait for it to resolve here\n    await handlerPromise;\n  }\n\n  if (result) {\n    return result;\n  }\n\n  return {\n    type: \"error\",\n    value: new Error(\"No result assigned in instrumentation chain.\"),\n  };\n}\n\nfunction getHandlerInfo(\n  args:\n    | LoaderFunctionArgs\n    | ActionFunctionArgs\n    | Parameters<MiddlewareFunction>[0],\n): RouteHandlerInstrumentationInfo {\n  let { request, context, params, unstable_pattern } = args;\n  return {\n    request: getReadonlyRequest(request),\n    params: { ...params },\n    unstable_pattern,\n    context: getReadonlyContext(context),\n  };\n}\n\nfunction getRouterInfo(\n  router: Router,\n  opts: NonNullable<\n    Parameters<Router[\"navigate\"]>[1] | Parameters<Router[\"fetch\"]>[3]\n  >,\n) {\n  return {\n    currentUrl: createPath(router.state.location),\n    ...(\"formMethod\" in opts ? { formMethod: opts.formMethod } : {}),\n    ...(\"formEncType\" in opts ? { formEncType: opts.formEncType } : {}),\n    ...(\"formData\" in opts ? { formData: opts.formData } : {}),\n    ...(\"body\" in opts ? { body: opts.body } : {}),\n  };\n}\n// Return a shallow readonly \"clone\" of the Request with the info they may\n// want to read from during instrumentation\nfunction getReadonlyRequest(request: Request): {\n  method: string;\n  url: string;\n  headers: Pick<Headers, \"get\">;\n} {\n  return {\n    method: request.method,\n    url: request.url,\n    headers: {\n      get: (...args) => request.headers.get(...args),\n    },\n  };\n}\n\nfunction getReadonlyContext(\n  context: MiddlewareEnabled extends true\n    ? RouterContextProvider\n    : AppLoadContext,\n): MiddlewareEnabled extends true\n  ? Pick<RouterContextProvider, \"get\">\n  : Readonly<AppLoadContext> {\n  if (isPlainObject(context)) {\n    let frozen = { ...context };\n    Object.freeze(frozen);\n    return frozen;\n  } else {\n    return {\n      get: <T>(ctx: RouterContext<T>) =>\n        (context as unknown as RouterContextProvider).get(ctx),\n    };\n  }\n}\n\n// From turbo-stream-v2/flatten.ts\nconst objectProtoNames = Object.getOwnPropertyNames(Object.prototype)\n  .sort()\n  .join(\"\\0\");\n\nfunction isPlainObject(\n  thing: unknown,\n): thing is Record<string | number | symbol, unknown> {\n  if (thing === null || typeof thing !== \"object\") {\n    return false;\n  }\n  const proto = Object.getPrototypeOf(thing);\n  return (\n    proto === Object.prototype ||\n    proto === null ||\n    Object.getOwnPropertyNames(proto).sort().join(\"\\0\") === objectProtoNames\n  );\n}\n"
  },
  {
    "path": "packages/react-router/lib/router/links.ts",
    "content": "type Primitive = null | undefined | string | number | boolean | symbol | bigint;\n\ntype LiteralUnion<LiteralType, BaseType extends Primitive> =\n  | LiteralType\n  | (BaseType & Record<never, never>);\n\ninterface HtmlLinkProps {\n  /**\n   * Address of the hyperlink\n   */\n  href?: string;\n\n  /**\n   * How the element handles crossorigin requests\n   */\n  crossOrigin?: \"anonymous\" | \"use-credentials\";\n\n  /**\n   * Relationship between the document containing the hyperlink and the destination resource\n   */\n  rel: LiteralUnion<\n    | \"alternate\"\n    | \"dns-prefetch\"\n    | \"icon\"\n    | \"manifest\"\n    | \"modulepreload\"\n    | \"next\"\n    | \"pingback\"\n    | \"preconnect\"\n    | \"prefetch\"\n    | \"preload\"\n    | \"prerender\"\n    | \"search\"\n    | \"stylesheet\",\n    string\n  >;\n\n  /**\n   * Applicable media: \"screen\", \"print\", \"(max-width: 764px)\"\n   */\n  media?: string;\n\n  /**\n   * Integrity metadata used in Subresource Integrity checks\n   */\n  integrity?: string;\n\n  /**\n   * Language of the linked resource\n   */\n  hrefLang?: string;\n\n  /**\n   * Hint for the type of the referenced resource\n   */\n  type?: string;\n\n  /**\n   * Referrer policy for fetches initiated by the element\n   */\n  referrerPolicy?:\n    | \"\"\n    | \"no-referrer\"\n    | \"no-referrer-when-downgrade\"\n    | \"same-origin\"\n    | \"origin\"\n    | \"strict-origin\"\n    | \"origin-when-cross-origin\"\n    | \"strict-origin-when-cross-origin\"\n    | \"unsafe-url\";\n\n  /**\n   * Sizes of the icons (for rel=\"icon\")\n   */\n  sizes?: string;\n\n  /**\n   * Potential destination for a preload request (for rel=\"preload\" and rel=\"modulepreload\")\n   */\n  as?: LiteralUnion<\n    | \"audio\"\n    | \"audioworklet\"\n    | \"document\"\n    | \"embed\"\n    | \"fetch\"\n    | \"font\"\n    | \"frame\"\n    | \"iframe\"\n    | \"image\"\n    | \"manifest\"\n    | \"object\"\n    | \"paintworklet\"\n    | \"report\"\n    | \"script\"\n    | \"serviceworker\"\n    | \"sharedworker\"\n    | \"style\"\n    | \"track\"\n    | \"video\"\n    | \"worker\"\n    | \"xslt\",\n    string\n  >;\n\n  /**\n   * Color to use when customizing a site's icon (for rel=\"mask-icon\")\n   */\n  color?: string;\n\n  /**\n   * Whether the link is disabled\n   */\n  disabled?: boolean;\n\n  /**\n   * The title attribute has special semantics on this element: Title of the link; CSS style sheet set name.\n   */\n  title?: string;\n\n  /**\n   * Images to use in different situations, e.g., high-resolution displays,\n   * small monitors, etc. (for rel=\"preload\")\n   */\n  imageSrcSet?: string;\n\n  /**\n   * Image sizes for different page layouts (for rel=\"preload\")\n   */\n  imageSizes?: string;\n}\n\ninterface HtmlLinkPreloadImage extends HtmlLinkProps {\n  /**\n   * Relationship between the document containing the hyperlink and the destination resource\n   */\n  rel: \"preload\";\n\n  /**\n   * Potential destination for a preload request (for rel=\"preload\" and rel=\"modulepreload\")\n   */\n  as: \"image\";\n\n  /**\n   * Address of the hyperlink\n   */\n  href?: string;\n\n  /**\n   * Images to use in different situations, e.g., high-resolution displays,\n   * small monitors, etc. (for rel=\"preload\")\n   */\n  imageSrcSet: string;\n\n  /**\n   * Image sizes for different page layouts (for rel=\"preload\")\n   */\n  imageSizes?: string;\n}\n\n/**\n * Represents a `<link>` element.\n *\n * WHATWG Specification: https://html.spec.whatwg.org/multipage/semantics.html#the-link-element\n */\nexport type HtmlLinkDescriptor =\n  // Must have an href *unless* it's a `<link rel=\"preload\" as=\"image\">` with an\n  // `imageSrcSet` and `imageSizes` props\n  | (HtmlLinkProps & Pick<Required<HtmlLinkProps>, \"href\">)\n  | (HtmlLinkPreloadImage & Pick<Required<HtmlLinkPreloadImage>, \"imageSizes\">)\n  | (HtmlLinkPreloadImage &\n      Pick<Required<HtmlLinkPreloadImage>, \"href\"> & { imageSizes?: never });\n\nexport interface PageLinkDescriptor\n  extends Omit<\n    HtmlLinkDescriptor,\n    | \"href\"\n    | \"rel\"\n    | \"type\"\n    | \"sizes\"\n    | \"imageSrcSet\"\n    | \"imageSizes\"\n    | \"as\"\n    | \"color\"\n    | \"title\"\n  > {\n  /**\n   * A [`nonce`](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Global_attributes/nonce)\n   * attribute to render on the [`<link>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/link)\n   * element\n   */\n  nonce?: string | undefined;\n  /**\n   * The absolute path of the page to prefetch, e.g. `/absolute/path`.\n   */\n  page: string;\n}\n\nexport type LinkDescriptor = HtmlLinkDescriptor | PageLinkDescriptor;\n"
  },
  {
    "path": "packages/react-router/lib/router/router.ts",
    "content": "import type { DataRouteMatch, RouteObject } from \"../context\";\nimport type { History, Location, Path, To } from \"./history\";\nimport {\n  Action as NavigationType,\n  createLocation,\n  createPath,\n  createBrowserURLImpl,\n  invariant,\n  parsePath,\n  warning,\n} from \"./history\";\nimport type {\n  unstable_ClientInstrumentation,\n  unstable_InstrumentRouteFunction,\n  unstable_InstrumentRouterFunction,\n  unstable_ServerInstrumentation,\n} from \"./instrumentation\";\nimport {\n  getRouteInstrumentationUpdates,\n  instrumentClientSideRouter,\n} from \"./instrumentation\";\nimport type {\n  AgnosticDataRouteMatch,\n  AgnosticDataRouteObject,\n  DataStrategyMatch,\n  AgnosticRouteObject,\n  DataResult,\n  DataStrategyFunction,\n  DataStrategyFunctionArgs,\n  ErrorResult,\n  FormEncType,\n  FormMethod,\n  HTMLFormMethod,\n  DataStrategyResult,\n  MapRoutePropertiesFunction,\n  MaybePromise,\n  MutationFormMethod,\n  RedirectResult,\n  RouteData,\n  RouteManifest,\n  ShouldRevalidateFunctionArgs,\n  Submission,\n  SuccessResult,\n  UIMatch,\n  AgnosticPatchRoutesOnNavigationFunction,\n  DataWithResponseInit,\n  LoaderFunctionArgs,\n  ActionFunctionArgs,\n  LoaderFunction,\n  ActionFunction,\n  MiddlewareFunction,\n  MiddlewareNextFunction,\n} from \"./utils\";\nimport {\n  ErrorResponseImpl,\n  ResultType,\n  convertRouteMatchToUiMatch,\n  convertRoutesToDataRoutes,\n  getPathContributingMatches,\n  getResolveToMatches,\n  isAbsoluteUrl,\n  isUnsupportedLazyRouteObjectKey,\n  isUnsupportedLazyRouteFunctionKey,\n  isRouteErrorResponse,\n  matchRoutes,\n  matchRoutesImpl,\n  prependBasename,\n  resolveTo,\n  stripBasename,\n  RouterContextProvider,\n  getRoutePattern,\n} from \"./utils\";\n\n////////////////////////////////////////////////////////////////////////////////\n//#region Types and Constants\n////////////////////////////////////////////////////////////////////////////////\n\n/**\n * A Router instance manages all navigation and data loading/mutations\n */\nexport interface Router {\n  /**\n   * @private\n   * PRIVATE - DO NOT USE\n   *\n   * Return the basename for the router\n   */\n  get basename(): RouterInit[\"basename\"];\n\n  /**\n   * @private\n   * PRIVATE - DO NOT USE\n   *\n   * Return the future config for the router\n   */\n  get future(): FutureConfig;\n\n  /**\n   * @private\n   * PRIVATE - DO NOT USE\n   *\n   * Return the current state of the router\n   */\n  get state(): RouterState;\n\n  /**\n   * @private\n   * PRIVATE - DO NOT USE\n   *\n   * Return the routes for this router instance\n   */\n  get routes(): AgnosticDataRouteObject[];\n\n  /**\n   * @private\n   * PRIVATE - DO NOT USE\n   *\n   * Return the window associated with the router\n   */\n  get window(): RouterInit[\"window\"];\n\n  /**\n   * @private\n   * PRIVATE - DO NOT USE\n   *\n   * Initialize the router, including adding history listeners and kicking off\n   * initial data fetches.  Returns a function to cleanup listeners and abort\n   * any in-progress loads\n   */\n  initialize(): Router;\n\n  /**\n   * @private\n   * PRIVATE - DO NOT USE\n   *\n   * Subscribe to router.state updates\n   *\n   * @param fn function to call with the new state\n   */\n  subscribe(fn: RouterSubscriber): () => void;\n\n  /**\n   * @private\n   * PRIVATE - DO NOT USE\n   *\n   * Enable scroll restoration behavior in the router\n   *\n   * @param savedScrollPositions Object that will manage positions, in case\n   *                             it's being restored from sessionStorage\n   * @param getScrollPosition    Function to get the active Y scroll position\n   * @param getKey               Function to get the key to use for restoration\n   */\n  enableScrollRestoration(\n    savedScrollPositions: Record<string, number>,\n    getScrollPosition: GetScrollPositionFunction,\n    getKey?: GetScrollRestorationKeyFunction,\n  ): () => void;\n\n  /**\n   * @private\n   * PRIVATE - DO NOT USE\n   *\n   * Navigate forward/backward in the history stack\n   * @param to Delta to move in the history stack\n   */\n  navigate(to: number): Promise<void>;\n\n  /**\n   * Navigate to the given path\n   * @param to Path to navigate to\n   * @param opts Navigation options (method, submission, etc.)\n   */\n  navigate(to: To | null, opts?: RouterNavigateOptions): Promise<void>;\n\n  /**\n   * @private\n   * PRIVATE - DO NOT USE\n   *\n   * Trigger a fetcher load/submission\n   *\n   * @param key     Fetcher key\n   * @param routeId Route that owns the fetcher\n   * @param href    href to fetch\n   * @param opts    Fetcher options, (method, submission, etc.)\n   */\n  fetch(\n    key: string,\n    routeId: string,\n    href: string | null,\n    opts?: RouterFetchOptions,\n  ): Promise<void>;\n\n  /**\n   * @private\n   * PRIVATE - DO NOT USE\n   *\n   * Trigger a revalidation of all current route loaders and fetcher loads\n   */\n  revalidate(): Promise<void>;\n\n  /**\n   * @private\n   * PRIVATE - DO NOT USE\n   *\n   * Utility function to create an href for the given location\n   * @param location\n   */\n  createHref(location: Location | URL): string;\n\n  /**\n   * @private\n   * PRIVATE - DO NOT USE\n   *\n   * Utility function to URL encode a destination path according to the internal\n   * history implementation\n   * @param to\n   */\n  encodeLocation(to: To): Path;\n\n  /**\n   * @private\n   * PRIVATE - DO NOT USE\n   *\n   * Get/create a fetcher for the given key\n   * @param key\n   */\n  getFetcher<TData = any>(key: string): Fetcher<TData>;\n\n  /**\n   * @internal\n   * PRIVATE - DO NOT USE\n   *\n   * Reset the fetcher for a given key\n   * @param key\n   */\n  resetFetcher(key: string, opts?: { reason?: unknown }): void;\n\n  /**\n   * @private\n   * PRIVATE - DO NOT USE\n   *\n   * Delete the fetcher for a given key\n   * @param key\n   */\n  deleteFetcher(key: string): void;\n\n  /**\n   * @private\n   * PRIVATE - DO NOT USE\n   *\n   * Cleanup listeners and abort any in-progress loads\n   */\n  dispose(): void;\n\n  /**\n   * @private\n   * PRIVATE - DO NOT USE\n   *\n   * Get a navigation blocker\n   * @param key The identifier for the blocker\n   * @param fn The blocker function implementation\n   */\n  getBlocker(key: string, fn: BlockerFunction): Blocker;\n\n  /**\n   * @private\n   * PRIVATE - DO NOT USE\n   *\n   * Delete a navigation blocker\n   * @param key The identifier for the blocker\n   */\n  deleteBlocker(key: string): void;\n\n  /**\n   * @private\n   * PRIVATE DO NOT USE\n   *\n   * Patch additional children routes into an existing parent route\n   * @param routeId The parent route id or a callback function accepting `patch`\n   *                to perform batch patching\n   * @param children The additional children routes\n   * @param unstable_allowElementMutations Allow mutation or route elements on\n   *                                       existing routes. Intended for RSC-usage\n   *                                       only.\n   */\n  patchRoutes(\n    routeId: string | null,\n    children: AgnosticRouteObject[],\n    unstable_allowElementMutations?: boolean,\n  ): void;\n\n  /**\n   * @private\n   * PRIVATE - DO NOT USE\n   *\n   * HMR needs to pass in-flight route updates to React Router\n   * TODO: Replace this with granular route update APIs (addRoute, updateRoute, deleteRoute)\n   */\n  _internalSetRoutes(routes: AgnosticRouteObject[]): void;\n\n  /**\n   * @private\n   * PRIVATE - DO NOT USE\n   *\n   * Cause subscribers to re-render.  This is used to force a re-render.\n   */\n  _internalSetStateDoNotUseOrYouWillBreakYourApp(\n    state: Partial<RouterState>,\n  ): void;\n\n  /**\n   * @private\n   * PRIVATE - DO NOT USE\n   *\n   * Internal fetch AbortControllers accessed by unit tests\n   */\n  _internalFetchControllers: Map<string, AbortController>;\n}\n\n/**\n * State maintained internally by the router.  During a navigation, all states\n * reflect the \"old\" location unless otherwise noted.\n */\nexport interface RouterState {\n  // TODO: (v7) should we consider renaming this `navigationType` to align with\n  // `useNavigationType` at some point?\n  /**\n   * The action of the most recent navigation\n   */\n  historyAction: NavigationType;\n\n  /**\n   * The current location reflected by the router\n   */\n  location: Location;\n\n  /**\n   * The current set of route matches\n   */\n  matches: AgnosticDataRouteMatch[];\n\n  /**\n   * Tracks whether we've completed our initial data load\n   */\n  initialized: boolean;\n\n  /**\n   * Tracks whether we should be rendering a HydrateFallback during hydration\n   */\n  renderFallback: boolean;\n\n  /**\n   * Current scroll position we should start at for a new view\n   *  - number -> scroll position to restore to\n   *  - false -> do not restore scroll at all (used during submissions/revalidations)\n   *  - null -> don't have a saved position, scroll to hash or top of page\n   */\n  restoreScrollPosition: number | false | null;\n\n  /**\n   * Indicate whether this navigation should skip resetting the scroll position\n   * if we are unable to restore the scroll position\n   */\n  preventScrollReset: boolean;\n\n  /**\n   * Tracks the state of the current navigation\n   */\n  navigation: Navigation;\n\n  /**\n   * Tracks any in-progress revalidations\n   */\n  revalidation: RevalidationState;\n\n  /**\n   * Data from the loaders for the current matches\n   */\n  loaderData: RouteData;\n\n  /**\n   * Data from the action for the current matches\n   */\n  actionData: RouteData | null;\n\n  /**\n   * Errors caught from loaders for the current matches\n   */\n  errors: RouteData | null;\n\n  /**\n   * Map of current fetchers\n   */\n  fetchers: Map<string, Fetcher>;\n\n  /**\n   * Map of current blockers\n   */\n  blockers: Map<string, Blocker>;\n}\n\n/**\n * Data that can be passed into hydrate a Router from SSR\n */\nexport type HydrationState = Partial<\n  Pick<RouterState, \"loaderData\" | \"actionData\" | \"errors\">\n>;\n\n/**\n * Future flags to toggle new feature behavior\n */\nexport interface FutureConfig {}\n\n/**\n * Initialization options for createRouter\n */\nexport interface RouterInit {\n  routes: AgnosticRouteObject[];\n  history: History;\n  basename?: string;\n  getContext?: () => MaybePromise<RouterContextProvider>;\n  unstable_instrumentations?: unstable_ClientInstrumentation[];\n  mapRouteProperties?: MapRoutePropertiesFunction;\n  future?: Partial<FutureConfig>;\n  hydrationRouteProperties?: string[];\n  hydrationData?: HydrationState;\n  window?: Window;\n  dataStrategy?: DataStrategyFunction;\n  patchRoutesOnNavigation?: AgnosticPatchRoutesOnNavigationFunction;\n}\n\n/**\n * State returned from a server-side query() call\n */\nexport interface StaticHandlerContext {\n  basename: Router[\"basename\"];\n  location: RouterState[\"location\"];\n  matches: RouterState[\"matches\"];\n  loaderData: RouterState[\"loaderData\"];\n  actionData: RouterState[\"actionData\"];\n  errors: RouterState[\"errors\"];\n  statusCode: number;\n  loaderHeaders: Record<string, Headers>;\n  actionHeaders: Record<string, Headers>;\n  _deepestRenderedBoundaryId?: string | null;\n}\n\n/**\n * A StaticHandler instance manages a singular SSR navigation/fetch event\n */\nexport interface StaticHandler {\n  dataRoutes: AgnosticDataRouteObject[];\n  query(\n    request: Request,\n    opts?: {\n      requestContext?: unknown;\n      filterMatchesToLoad?: (match: AgnosticDataRouteMatch) => boolean;\n      skipLoaderErrorBubbling?: boolean;\n      skipRevalidation?: boolean;\n      dataStrategy?: DataStrategyFunction<unknown>;\n      generateMiddlewareResponse?: (\n        query: (\n          r: Request,\n          args?: {\n            filterMatchesToLoad?: (match: AgnosticDataRouteMatch) => boolean;\n          },\n        ) => Promise<StaticHandlerContext | Response>,\n      ) => MaybePromise<Response>;\n    },\n  ): Promise<StaticHandlerContext | Response>;\n  queryRoute(\n    request: Request,\n    opts?: {\n      routeId?: string;\n      requestContext?: unknown;\n      dataStrategy?: DataStrategyFunction<unknown>;\n      generateMiddlewareResponse?: (\n        queryRoute: (r: Request) => Promise<Response>,\n      ) => MaybePromise<Response>;\n    },\n  ): Promise<any>;\n}\n\ntype ViewTransitionOpts = {\n  currentLocation: Location;\n  nextLocation: Location;\n};\n\n/**\n * Subscriber function signature for changes to router state\n */\nexport interface RouterSubscriber {\n  (\n    state: RouterState,\n    opts: {\n      deletedFetchers: string[];\n      newErrors: RouteData | null;\n      viewTransitionOpts?: ViewTransitionOpts;\n      flushSync: boolean;\n    },\n  ): void;\n}\n\n/**\n * Function signature for determining the key to be used in scroll restoration\n * for a given location\n */\nexport interface GetScrollRestorationKeyFunction {\n  (location: Location, matches: UIMatch[]): string | null;\n}\n\n/**\n * Function signature for determining the current scroll position\n */\nexport interface GetScrollPositionFunction {\n  (): number;\n}\n\n/**\n * - \"route\": relative to the route hierarchy so `..` means remove all segments\n * of the current route even if it has many. For example, a `route(\"posts/:id\")`\n * would have both `:id` and `posts` removed from the url.\n * - \"path\": relative to the pathname so `..` means remove one segment of the\n * pathname. For example, a `route(\"posts/:id\")` would have only `:id` removed\n * from the url.\n */\nexport type RelativeRoutingType = \"route\" | \"path\";\n\n// Allowed for any navigation or fetch\ntype BaseNavigateOrFetchOptions = {\n  preventScrollReset?: boolean;\n  relative?: RelativeRoutingType;\n  flushSync?: boolean;\n  unstable_defaultShouldRevalidate?: boolean;\n};\n\n// Only allowed for navigations\ntype BaseNavigateOptions = BaseNavigateOrFetchOptions & {\n  replace?: boolean;\n  state?: any;\n  fromRouteId?: string;\n  viewTransition?: boolean;\n  unstable_mask?: To;\n};\n\n// Only allowed for submission navigations\ntype BaseSubmissionOptions = {\n  formMethod?: HTMLFormMethod;\n  formEncType?: FormEncType;\n} & (\n  | { formData: FormData; body?: undefined }\n  | { formData?: undefined; body: any }\n);\n\n/**\n * Options for a navigate() call for a normal (non-submission) navigation\n */\ntype LinkNavigateOptions = BaseNavigateOptions;\n\n/**\n * Options for a navigate() call for a submission navigation\n */\ntype SubmissionNavigateOptions = BaseNavigateOptions & BaseSubmissionOptions;\n\n/**\n * Options to pass to navigate() for a navigation\n */\nexport type RouterNavigateOptions =\n  | LinkNavigateOptions\n  | SubmissionNavigateOptions;\n\n/**\n * Options for a fetch() load\n */\ntype LoadFetchOptions = BaseNavigateOrFetchOptions;\n\n/**\n * Options for a fetch() submission\n */\ntype SubmitFetchOptions = BaseNavigateOrFetchOptions & BaseSubmissionOptions;\n\n/**\n * Options to pass to fetch()\n */\nexport type RouterFetchOptions = LoadFetchOptions | SubmitFetchOptions;\n\n/**\n * Potential states for state.navigation\n */\nexport type NavigationStates = {\n  Idle: {\n    state: \"idle\";\n    location: undefined;\n    formMethod: undefined;\n    formAction: undefined;\n    formEncType: undefined;\n    formData: undefined;\n    json: undefined;\n    text: undefined;\n  };\n  Loading: {\n    state: \"loading\";\n    location: Location;\n    formMethod: Submission[\"formMethod\"] | undefined;\n    formAction: Submission[\"formAction\"] | undefined;\n    formEncType: Submission[\"formEncType\"] | undefined;\n    formData: Submission[\"formData\"] | undefined;\n    json: Submission[\"json\"] | undefined;\n    text: Submission[\"text\"] | undefined;\n  };\n  Submitting: {\n    state: \"submitting\";\n    location: Location;\n    formMethod: Submission[\"formMethod\"];\n    formAction: Submission[\"formAction\"];\n    formEncType: Submission[\"formEncType\"];\n    formData: Submission[\"formData\"];\n    json: Submission[\"json\"];\n    text: Submission[\"text\"];\n  };\n};\n\nexport type Navigation = NavigationStates[keyof NavigationStates];\n\nexport type RevalidationState = \"idle\" | \"loading\";\n\n/**\n * Potential states for fetchers\n */\nexport type FetcherStates<TData = any> = {\n  /**\n   * The fetcher is not calling a loader or action\n   *\n   * ```tsx\n   * fetcher.state === \"idle\"\n   * ```\n   */\n  Idle: {\n    state: \"idle\";\n    formMethod: undefined;\n    formAction: undefined;\n    formEncType: undefined;\n    text: undefined;\n    formData: undefined;\n    json: undefined;\n    /**\n     * If the fetcher has never been called, this will be undefined.\n     */\n    data: TData | undefined;\n  };\n\n  /**\n   * The fetcher is loading data from a {@link LoaderFunction | loader} from a\n   * call to {@link FetcherWithComponents.load | `fetcher.load`}.\n   *\n   * ```tsx\n   * // somewhere\n   * <button onClick={() => fetcher.load(\"/some/route\") }>Load</button>\n   *\n   * // the state will update\n   * fetcher.state === \"loading\"\n   * ```\n   */\n  Loading: {\n    state: \"loading\";\n    formMethod: Submission[\"formMethod\"] | undefined;\n    formAction: Submission[\"formAction\"] | undefined;\n    formEncType: Submission[\"formEncType\"] | undefined;\n    text: Submission[\"text\"] | undefined;\n    formData: Submission[\"formData\"] | undefined;\n    json: Submission[\"json\"] | undefined;\n    data: TData | undefined;\n  };\n\n  /**\n    The fetcher is submitting to a {@link LoaderFunction} (GET) or {@link ActionFunction} (POST) from a {@link FetcherWithComponents.Form | `fetcher.Form`} or {@link FetcherWithComponents.submit | `fetcher.submit`}.\n\n    ```tsx\n    // somewhere\n    <input\n      onChange={e => {\n        fetcher.submit(event.currentTarget.form, { method: \"post\" });\n      }}\n    />\n\n    // the state will update\n    fetcher.state === \"submitting\"\n\n    // and formData will be available\n    fetcher.formData\n    ```\n   */\n  Submitting: {\n    state: \"submitting\";\n    formMethod: Submission[\"formMethod\"];\n    formAction: Submission[\"formAction\"];\n    formEncType: Submission[\"formEncType\"];\n    text: Submission[\"text\"];\n    formData: Submission[\"formData\"];\n    json: Submission[\"json\"];\n    data: TData | undefined;\n  };\n};\n\nexport type Fetcher<TData = any> =\n  FetcherStates<TData>[keyof FetcherStates<TData>];\n\ninterface BlockerBlocked {\n  state: \"blocked\";\n  reset: () => void;\n  proceed: () => void;\n  location: Location;\n}\n\ninterface BlockerUnblocked {\n  state: \"unblocked\";\n  reset: undefined;\n  proceed: undefined;\n  location: undefined;\n}\n\ninterface BlockerProceeding {\n  state: \"proceeding\";\n  reset: undefined;\n  proceed: undefined;\n  location: Location;\n}\n\nexport type Blocker = BlockerUnblocked | BlockerBlocked | BlockerProceeding;\n\nexport type BlockerFunction = (args: {\n  currentLocation: Location;\n  nextLocation: Location;\n  historyAction: NavigationType;\n}) => boolean;\n\ninterface ShortCircuitable {\n  /**\n   * startNavigation does not need to complete the navigation because we\n   * redirected or got interrupted\n   */\n  shortCircuited?: boolean;\n}\n\n// Track any pending errors from the action (or other pre-loader flows).\n// The format is [bubbledRouteId, result, actionRouteId?]\n// We should probably change this to an object now that we (optionally) track\n// the original action route id so we can clear out loaderData in the right in\n// processRouteLoaderData\n//\ntype PendingActionResult =\n  | [string, SuccessResult | ErrorResult]\n  | [string, SuccessResult | ErrorResult, string];\n\ninterface HandleActionResult extends ShortCircuitable {\n  /**\n   * Route matches which may have been updated from fog of war discovery\n   */\n  matches?: RouterState[\"matches\"];\n  /**\n   * Tuple for the returned or thrown value from the current action.  The routeId\n   * is the action route for success and the bubbled boundary route for errors.\n   */\n  pendingActionResult?: PendingActionResult;\n}\n\ninterface HandleLoadersResult extends ShortCircuitable {\n  /**\n   * Route matches which may have been updated from fog of war discovery\n   */\n  matches?: RouterState[\"matches\"];\n  /**\n   * loaderData returned from the current set of loaders\n   */\n  loaderData?: RouterState[\"loaderData\"];\n  /**\n   * errors thrown from the current set of loaders\n   */\n  errors?: RouterState[\"errors\"];\n}\n\n/**\n * Cached info for active fetcher.load() instances so they can participate\n * in revalidation\n */\ninterface FetchLoadMatch {\n  routeId: string;\n  path: string;\n}\n\n/**\n * Identified fetcher.load() calls that need to be revalidated\n */\ninterface RevalidatingFetcher extends FetchLoadMatch {\n  key: string;\n  match: AgnosticDataRouteMatch | null;\n  matches: DataStrategyMatch[] | null;\n  request: Request | null;\n  controller: AbortController | null;\n}\n\nconst validMutationMethodsArr: MutationFormMethod[] = [\n  \"POST\",\n  \"PUT\",\n  \"PATCH\",\n  \"DELETE\",\n];\nconst validMutationMethods = new Set<MutationFormMethod>(\n  validMutationMethodsArr,\n);\n\nconst validRequestMethodsArr: FormMethod[] = [\n  \"GET\",\n  ...validMutationMethodsArr,\n];\nconst validRequestMethods = new Set<FormMethod>(validRequestMethodsArr);\n\nexport const redirectStatusCodes = new Set([301, 302, 303, 307, 308]);\nconst redirectPreserveMethodStatusCodes = new Set([307, 308]);\n\nexport const IDLE_NAVIGATION: NavigationStates[\"Idle\"] = {\n  state: \"idle\",\n  location: undefined,\n  formMethod: undefined,\n  formAction: undefined,\n  formEncType: undefined,\n  formData: undefined,\n  json: undefined,\n  text: undefined,\n};\n\nexport const IDLE_FETCHER: FetcherStates[\"Idle\"] = {\n  state: \"idle\",\n  data: undefined,\n  formMethod: undefined,\n  formAction: undefined,\n  formEncType: undefined,\n  formData: undefined,\n  json: undefined,\n  text: undefined,\n};\n\nexport const IDLE_BLOCKER: BlockerUnblocked = {\n  state: \"unblocked\",\n  proceed: undefined,\n  reset: undefined,\n  location: undefined,\n};\n\nconst defaultMapRouteProperties: MapRoutePropertiesFunction = (route) => ({\n  hasErrorBoundary: Boolean(route.hasErrorBoundary),\n});\n\nconst TRANSITIONS_STORAGE_KEY = \"remix-router-transitions\";\n\n// Flag used on new `loaderData` to indicate that we do not want to preserve\n// any prior loader data from the throwing route in `mergeLoaderData`\nconst ResetLoaderDataSymbol = Symbol(\"ResetLoaderData\");\n\n//#endregion\n\n////////////////////////////////////////////////////////////////////////////////\n//#region createRouter\n////////////////////////////////////////////////////////////////////////////////\n\n/**\n * Create a router and listen to history POP navigations\n */\nexport function createRouter(init: RouterInit): Router {\n  const routerWindow = init.window\n    ? init.window\n    : typeof window !== \"undefined\"\n      ? window\n      : undefined;\n  const isBrowser =\n    typeof routerWindow !== \"undefined\" &&\n    typeof routerWindow.document !== \"undefined\" &&\n    typeof routerWindow.document.createElement !== \"undefined\";\n\n  invariant(\n    init.routes.length > 0,\n    \"You must provide a non-empty routes array to createRouter\",\n  );\n\n  let hydrationRouteProperties = init.hydrationRouteProperties || [];\n  let _mapRouteProperties =\n    init.mapRouteProperties || defaultMapRouteProperties;\n  let mapRouteProperties = _mapRouteProperties;\n\n  // Leverage the existing mapRouteProperties logic to execute instrumentRoute\n  // (if it exists) on all routes in the application\n  if (init.unstable_instrumentations) {\n    let instrumentations = init.unstable_instrumentations;\n\n    mapRouteProperties = (route: AgnosticDataRouteObject) => {\n      return {\n        ..._mapRouteProperties(route),\n        ...getRouteInstrumentationUpdates(\n          instrumentations\n            .map((i) => i.route)\n            .filter(Boolean) as unstable_InstrumentRouteFunction[],\n          route,\n        ),\n      };\n    };\n  }\n\n  // Routes keyed by ID\n  let manifest: RouteManifest = {};\n  // Routes in tree format for matching\n  let dataRoutes = convertRoutesToDataRoutes(\n    init.routes,\n    mapRouteProperties,\n    undefined,\n    manifest,\n  );\n  let inFlightDataRoutes: AgnosticDataRouteObject[] | undefined;\n  let basename = init.basename || \"/\";\n  if (!basename.startsWith(\"/\")) {\n    basename = `/${basename}`;\n  }\n  let dataStrategyImpl = init.dataStrategy || defaultDataStrategyWithMiddleware;\n\n  // Config driven behavior flags\n  let future: FutureConfig = {\n    ...init.future,\n  };\n  // Cleanup function for history\n  let unlistenHistory: (() => void) | null = null;\n  // Externally-provided functions to call on all state changes\n  let subscribers = new Set<RouterSubscriber>();\n  // Externally-provided object to hold scroll restoration locations during routing\n  let savedScrollPositions: Record<string, number> | null = null;\n  // Externally-provided function to get scroll restoration keys\n  let getScrollRestorationKey: GetScrollRestorationKeyFunction | null = null;\n  // Externally-provided function to get current scroll position\n  let getScrollPosition: GetScrollPositionFunction | null = null;\n  // One-time flag to control the initial hydration scroll restoration.  Because\n  // we don't get the saved positions from <ScrollRestoration /> until _after_\n  // the initial render, we need to manually trigger a separate updateState to\n  // send along the restoreScrollPosition\n  // Set to true if we have `hydrationData` since we assume we were SSR'd and that\n  // SSR did the initial scroll restoration.\n  let initialScrollRestored = init.hydrationData != null;\n\n  let initialMatches = matchRoutes(dataRoutes, init.history.location, basename);\n  let initialMatchesIsFOW = false;\n  let initialErrors: RouteData | null = null;\n  let initialized: boolean;\n  let renderFallback: boolean;\n\n  if (initialMatches == null && !init.patchRoutesOnNavigation) {\n    // If we do not match a user-provided-route, fall back to the root\n    // to allow the error boundary to take over\n    let error = getInternalRouterError(404, {\n      pathname: init.history.location.pathname,\n    });\n    let { matches, route } = getShortCircuitMatches(dataRoutes);\n    initialized = true;\n    renderFallback = !initialized;\n    initialMatches = matches;\n    initialErrors = { [route.id]: error };\n  } else {\n    // In SPA apps, if the user provided a patchRoutesOnNavigation implementation and\n    // our initial match is a splat route, clear them out so we run through lazy\n    // discovery on hydration in case there's a more accurate lazy route match.\n    // In SSR apps (with `hydrationData`), we expect that the server will send\n    // up the proper matched routes so we don't want to run lazy discovery on\n    // initial hydration and want to hydrate into the splat route.\n    if (initialMatches && !init.hydrationData) {\n      let fogOfWar = checkFogOfWar(\n        initialMatches,\n        dataRoutes,\n        init.history.location.pathname,\n      );\n      if (fogOfWar.active) {\n        initialMatches = null;\n      }\n    }\n\n    if (!initialMatches) {\n      initialized = false;\n      renderFallback = !initialized;\n      initialMatches = [];\n\n      // If partial hydration and fog of war is enabled, we will be running\n      // `patchRoutesOnNavigation` during hydration so include any partial matches as\n      // the initial matches so we can properly render `HydrateFallback`'s\n      let fogOfWar = checkFogOfWar(\n        null,\n        dataRoutes,\n        init.history.location.pathname,\n      );\n      if (fogOfWar.active && fogOfWar.matches) {\n        initialMatchesIsFOW = true;\n        initialMatches = fogOfWar.matches;\n      }\n    } else if (initialMatches.some((m) => m.route.lazy)) {\n      // All initialMatches need to be loaded before we're ready.  If we have lazy\n      // functions around still then we'll need to run them in initialize()\n      initialized = false;\n      renderFallback = !initialized;\n    } else if (\n      !initialMatches.some((m) => routeHasLoaderOrMiddleware(m.route))\n    ) {\n      // If we've got no loaders or middleware to run, then we're good to go\n      initialized = true;\n      renderFallback = !initialized;\n    } else {\n      // With \"partial hydration\", we're initialized so long as we were\n      // provided with hydrationData for every route with a loader, and no loaders\n      // were marked for explicit hydration\n      let loaderData = init.hydrationData\n        ? init.hydrationData.loaderData\n        : null;\n      let errors = init.hydrationData ? init.hydrationData.errors : null;\n      let relevantMatches = initialMatches;\n\n      // If errors exist, don't consider routes below the boundary\n      if (errors) {\n        let idx = initialMatches.findIndex(\n          (m) => errors![m.route.id] !== undefined,\n        );\n        relevantMatches = relevantMatches.slice(0, idx + 1);\n      }\n\n      // Toggle renderFallback based on per-route values\n      renderFallback = false;\n      initialized = relevantMatches.every((m) => {\n        let status = getRouteHydrationStatus(m.route, loaderData, errors);\n        renderFallback = renderFallback || status.renderFallback;\n        return !status.shouldLoad;\n      });\n    }\n  }\n\n  let router: Router;\n  let state: RouterState = {\n    historyAction: init.history.action,\n    location: init.history.location,\n    matches: initialMatches,\n    initialized,\n    renderFallback,\n    navigation: IDLE_NAVIGATION,\n    // Don't restore on initial updateState() if we were SSR'd\n    restoreScrollPosition: init.hydrationData != null ? false : null,\n    preventScrollReset: false,\n    revalidation: \"idle\",\n    loaderData: (init.hydrationData && init.hydrationData.loaderData) || {},\n    actionData: (init.hydrationData && init.hydrationData.actionData) || null,\n    errors: (init.hydrationData && init.hydrationData.errors) || initialErrors,\n    fetchers: new Map(),\n    blockers: new Map(),\n  };\n\n  // -- Stateful internal variables to manage navigations --\n  // Current navigation in progress (to be committed in completeNavigation)\n  let pendingAction: NavigationType = NavigationType.Pop;\n\n  // Deferred to use for tracking popstate navigations\n  let pendingPopstateNavigationDfd: ReturnType<\n    typeof createDeferred<void>\n  > | null = null;\n\n  // Should the current navigation prevent the scroll reset if scroll cannot\n  // be restored?\n  let pendingPreventScrollReset = false;\n\n  // AbortController for the active navigation\n  let pendingNavigationController: AbortController | null;\n\n  // Should the current navigation enable document.startViewTransition?\n  let pendingViewTransitionEnabled = false;\n\n  // Store applied view transitions so we can apply them on POP\n  let appliedViewTransitions: Map<string, Set<string>> = new Map<\n    string,\n    Set<string>\n  >();\n\n  // Cleanup function for persisting applied transitions to sessionStorage\n  let removePageHideEventListener: (() => void) | null = null;\n\n  // We use this to avoid touching history in completeNavigation if a\n  // revalidation is entirely uninterrupted\n  let isUninterruptedRevalidation = false;\n\n  // Use this internal flag to force revalidation of all loaders:\n  //  - submissions (completed or interrupted)\n  //  - useRevalidator()\n  //  - X-Remix-Revalidate (from redirect)\n  let isRevalidationRequired = false;\n\n  // Use this internal array to capture fetcher loads that were cancelled by an\n  // action navigation and require revalidation\n  let cancelledFetcherLoads: Set<string> = new Set();\n\n  // AbortControllers for any in-flight fetchers\n  let fetchControllers = new Map<string, AbortController>();\n\n  // Track loads based on the order in which they started\n  let incrementingLoadId = 0;\n\n  // Track the outstanding pending navigation data load to be compared against\n  // the globally incrementing load when a fetcher load lands after a completed\n  // navigation\n  let pendingNavigationLoadId = -1;\n\n  // Fetchers that triggered data reloads as a result of their actions\n  let fetchReloadIds = new Map<string, number>();\n\n  // Fetchers that triggered redirect navigations\n  let fetchRedirectIds = new Set<string>();\n\n  // Most recent href/match for fetcher.load calls for fetchers\n  let fetchLoadMatches = new Map<string, FetchLoadMatch>();\n\n  // Ref-count mounted fetchers so we know when it's ok to clean them up\n  let activeFetchers = new Map<string, number>();\n\n  // Fetchers queued for deletion because they've been removed from the UI.\n  // These will be officially deleted after they return to idle\n  let fetchersQueuedForDeletion = new Set<string>();\n\n  // Store blocker functions in a separate Map outside of router state since\n  // we don't need to update UI state if they change\n  let blockerFunctions = new Map<string, BlockerFunction>();\n\n  // Flag to ignore the next history update, so we can revert the URL change on\n  // a POP navigation that was blocked by the user without touching router state\n  let unblockBlockerHistoryUpdate: (() => void) | undefined = undefined;\n\n  // Revalidation deferred to be resolved next time we land through completeNavigation\n  let pendingRevalidationDfd: ReturnType<typeof createDeferred<void>> | null =\n    null;\n\n  // Initialize the router, all side effects should be kicked off from here.\n  // Implemented as a Fluent API for ease of:\n  //   let router = createRouter(init).initialize();\n  function initialize() {\n    // If history informs us of a POP navigation, start the navigation but do not update\n    // state.  We'll update our own state once the navigation completes\n    unlistenHistory = init.history.listen(\n      ({ action: historyAction, location, delta }) => {\n        // Ignore this event if it was just us resetting the URL from a\n        // blocked POP navigation\n        if (unblockBlockerHistoryUpdate) {\n          unblockBlockerHistoryUpdate();\n          unblockBlockerHistoryUpdate = undefined;\n          return;\n        }\n\n        warning(\n          blockerFunctions.size === 0 || delta != null,\n          \"You are trying to use a blocker on a POP navigation to a location \" +\n            \"that was not created by @remix-run/router. This will fail silently in \" +\n            \"production. This can happen if you are navigating outside the router \" +\n            \"via `window.history.pushState`/`window.location.hash` instead of using \" +\n            \"router navigation APIs.  This can also happen if you are using \" +\n            \"createHashRouter and the user manually changes the URL.\",\n        );\n\n        let blockerKey = shouldBlockNavigation({\n          currentLocation: state.location,\n          nextLocation: location,\n          historyAction,\n        });\n\n        if (blockerKey && delta != null) {\n          // Restore the URL to match the current UI, but don't update router state\n          let nextHistoryUpdatePromise = new Promise<void>((resolve) => {\n            unblockBlockerHistoryUpdate = resolve;\n          });\n          init.history.go(delta * -1);\n\n          // Put the blocker into a blocked state\n          updateBlocker(blockerKey, {\n            state: \"blocked\",\n            location,\n            proceed() {\n              updateBlocker(blockerKey!, {\n                state: \"proceeding\",\n                proceed: undefined,\n                reset: undefined,\n                location,\n              });\n              // Re-do the same POP navigation we just blocked, after the url\n              // restoration is also complete.  See:\n              // https://github.com/remix-run/react-router/issues/11613\n              nextHistoryUpdatePromise.then(() => init.history.go(delta));\n            },\n            reset() {\n              let blockers = new Map(state.blockers);\n              blockers.set(blockerKey!, IDLE_BLOCKER);\n              updateState({ blockers });\n            },\n          });\n\n          // Resolve the promise for the blocked popstate\n          pendingPopstateNavigationDfd?.resolve();\n          pendingPopstateNavigationDfd = null;\n\n          return;\n        }\n\n        return startNavigation(historyAction, location);\n      },\n    );\n\n    if (isBrowser) {\n      // FIXME: This feels gross.  How can we cleanup the lines between\n      // scrollRestoration/appliedTransitions persistance?\n      restoreAppliedTransitions(routerWindow, appliedViewTransitions);\n      let _saveAppliedTransitions = () =>\n        persistAppliedTransitions(routerWindow, appliedViewTransitions);\n      routerWindow.addEventListener(\"pagehide\", _saveAppliedTransitions);\n      removePageHideEventListener = () =>\n        routerWindow.removeEventListener(\"pagehide\", _saveAppliedTransitions);\n    }\n\n    // Kick off initial data load if needed.  Use Pop to avoid modifying history\n    // Note we don't do any handling of lazy here.  For SPA's it'll get handled\n    // in the normal navigation flow.  For SSR it's expected that lazy modules are\n    // resolved prior to router creation since we can't go into a fallback\n    // UI for SSR'd apps\n    if (!state.initialized) {\n      startNavigation(NavigationType.Pop, state.location, {\n        initialHydration: true,\n      });\n    }\n\n    return router;\n  }\n\n  // Clean up a router and it's side effects\n  function dispose() {\n    if (unlistenHistory) {\n      unlistenHistory();\n    }\n    if (removePageHideEventListener) {\n      removePageHideEventListener();\n    }\n    subscribers.clear();\n    pendingNavigationController && pendingNavigationController.abort();\n    state.fetchers.forEach((_, key) => deleteFetcher(key));\n    state.blockers.forEach((_, key) => deleteBlocker(key));\n  }\n\n  // Subscribe to state updates for the router\n  function subscribe(fn: RouterSubscriber) {\n    subscribers.add(fn);\n    return () => subscribers.delete(fn);\n  }\n\n  // Update our state and notify the calling context of the change\n  function updateState(\n    newState: Partial<RouterState>,\n    opts: {\n      flushSync?: boolean;\n      viewTransitionOpts?: ViewTransitionOpts;\n    } = {},\n  ): void {\n    // We may have patched routes during the navigation and if so we need to\n    // get references to the latest elements here\n    if (newState.matches) {\n      newState.matches = newState.matches.map((m) => {\n        let route = manifest[m.route.id]! as RouteObject;\n        let matchRoute = m.route as RouteObject;\n        if (\n          matchRoute.element !== route.element ||\n          matchRoute.errorElement !== route.errorElement ||\n          matchRoute.hydrateFallbackElement !== route.hydrateFallbackElement\n        ) {\n          return {\n            ...m,\n            route: route as AgnosticDataRouteObject,\n          };\n        }\n        return m;\n      });\n    }\n\n    state = {\n      ...state,\n      ...newState,\n    };\n\n    // Cleanup for all fetchers that have returned to idle since we only\n    // care about in-flight fetchers\n    // - If it's been unmounted then we can completely delete it\n    // - If it's still mounted we can remove it from `state.fetchers`, but we\n    //   need to keep it around in things like `fetchLoadMatches`, etc. since\n    //   it may be called again\n    let unmountedFetchers: string[] = [];\n    let mountedFetchers: string[] = [];\n\n    state.fetchers.forEach((fetcher, key) => {\n      if (fetcher.state === \"idle\") {\n        if (fetchersQueuedForDeletion.has(key)) {\n          unmountedFetchers.push(key);\n        } else {\n          mountedFetchers.push(key);\n        }\n      }\n    });\n\n    // Delete any other `idle` fetchers unmounted in the UI that were previously\n    // removed from state.fetchers.  Check `fetchControllers` in case this\n    // fetcher is actively revalidating and we want to let that finish\n    fetchersQueuedForDeletion.forEach((key) => {\n      if (!state.fetchers.has(key) && !fetchControllers.has(key)) {\n        unmountedFetchers.push(key);\n      }\n    });\n\n    // Iterate over a local copy so that if flushSync is used and we end up\n    // removing and adding a new subscriber due to the useCallback dependencies,\n    // we don't get ourselves into a loop calling the new subscriber immediately\n    [...subscribers].forEach((subscriber) =>\n      subscriber(state, {\n        deletedFetchers: unmountedFetchers,\n        newErrors: newState.errors ?? null,\n        viewTransitionOpts: opts.viewTransitionOpts,\n        flushSync: opts.flushSync === true,\n      }),\n    );\n\n    // Cleanup internally now that we've called our subscribers/updated state\n    unmountedFetchers.forEach((key) => deleteFetcher(key));\n    mountedFetchers.forEach((key) => state.fetchers.delete(key));\n  }\n\n  // Complete a navigation returning the state.navigation back to the IDLE_NAVIGATION\n  // and setting state.[historyAction/location/matches] to the new route.\n  // - Location is a required param\n  // - Navigation will always be set to IDLE_NAVIGATION\n  // - Can pass any other state in newState\n  function completeNavigation(\n    location: Location,\n    newState: Partial<Omit<RouterState, \"action\" | \"location\" | \"navigation\">>,\n    { flushSync }: { flushSync?: boolean } = {},\n  ): void {\n    // Deduce if we're in a loading/actionReload state:\n    // - We have committed actionData in the store\n    // - The current navigation was a mutation submission\n    // - We're past the submitting state and into the loading state\n    // - The location being loaded is not the result of a redirect\n    let isActionReload =\n      state.actionData != null &&\n      state.navigation.formMethod != null &&\n      isMutationMethod(state.navigation.formMethod) &&\n      state.navigation.state === \"loading\" &&\n      location.state?._isRedirect !== true;\n\n    let actionData: RouteData | null;\n    if (newState.actionData) {\n      if (Object.keys(newState.actionData).length > 0) {\n        actionData = newState.actionData;\n      } else {\n        // Empty actionData -> clear prior actionData due to an action error\n        actionData = null;\n      }\n    } else if (isActionReload) {\n      // Keep the current data if we're wrapping up the action reload\n      actionData = state.actionData;\n    } else {\n      // Clear actionData on any other completed navigations\n      actionData = null;\n    }\n\n    // Always preserve any existing loaderData from re-used routes\n    let loaderData = newState.loaderData\n      ? mergeLoaderData(\n          state.loaderData,\n          newState.loaderData,\n          newState.matches || [],\n          newState.errors,\n        )\n      : state.loaderData;\n\n    // On a successful navigation we can assume we got through all blockers\n    // so we can start fresh\n    let blockers = state.blockers;\n    if (blockers.size > 0) {\n      blockers = new Map(blockers);\n      blockers.forEach((_, k) => blockers.set(k, IDLE_BLOCKER));\n    }\n\n    // Don't restore on router.revalidate()\n    let restoreScrollPosition = isUninterruptedRevalidation\n      ? false\n      : getSavedScrollPosition(location, newState.matches || state.matches);\n\n    // Always respect the user flag.  Otherwise don't reset on mutation\n    // submission navigations unless they redirect\n    let preventScrollReset =\n      pendingPreventScrollReset === true ||\n      (state.navigation.formMethod != null &&\n        isMutationMethod(state.navigation.formMethod) &&\n        location.state?._isRedirect !== true);\n\n    // Commit any in-flight routes at the end of the HMR revalidation \"navigation\"\n    if (inFlightDataRoutes) {\n      dataRoutes = inFlightDataRoutes;\n      inFlightDataRoutes = undefined;\n    }\n\n    if (isUninterruptedRevalidation) {\n      // If this was an uninterrupted revalidation then do not touch history\n    } else if (pendingAction === NavigationType.Pop) {\n      // Do nothing for POP - URL has already been updated\n    } else if (pendingAction === NavigationType.Push) {\n      init.history.push(location, location.state);\n    } else if (pendingAction === NavigationType.Replace) {\n      init.history.replace(location, location.state);\n    }\n\n    let viewTransitionOpts: ViewTransitionOpts | undefined;\n\n    // On POP, enable transitions if they were enabled on the original navigation\n    if (pendingAction === NavigationType.Pop) {\n      // Forward takes precedence so they behave like the original navigation\n      let priorPaths = appliedViewTransitions.get(state.location.pathname);\n      if (priorPaths && priorPaths.has(location.pathname)) {\n        viewTransitionOpts = {\n          currentLocation: state.location,\n          nextLocation: location,\n        };\n      } else if (appliedViewTransitions.has(location.pathname)) {\n        // If we don't have a previous forward nav, assume we're popping back to\n        // the new location and enable if that location previously enabled\n        viewTransitionOpts = {\n          currentLocation: location,\n          nextLocation: state.location,\n        };\n      }\n    } else if (pendingViewTransitionEnabled) {\n      // Store the applied transition on PUSH/REPLACE\n      let toPaths = appliedViewTransitions.get(state.location.pathname);\n      if (toPaths) {\n        toPaths.add(location.pathname);\n      } else {\n        toPaths = new Set<string>([location.pathname]);\n        appliedViewTransitions.set(state.location.pathname, toPaths);\n      }\n      viewTransitionOpts = {\n        currentLocation: state.location,\n        nextLocation: location,\n      };\n    }\n\n    updateState(\n      {\n        ...newState, // matches, errors, fetchers go through as-is\n        actionData,\n        loaderData,\n        historyAction: pendingAction,\n        location,\n        initialized: true,\n        renderFallback: false,\n        navigation: IDLE_NAVIGATION,\n        revalidation: \"idle\",\n        restoreScrollPosition,\n        preventScrollReset,\n        blockers,\n      },\n      {\n        viewTransitionOpts,\n        flushSync: flushSync === true,\n      },\n    );\n\n    // Reset stateful navigation vars\n    pendingAction = NavigationType.Pop;\n    pendingPreventScrollReset = false;\n    pendingViewTransitionEnabled = false;\n    isUninterruptedRevalidation = false;\n    isRevalidationRequired = false;\n    pendingPopstateNavigationDfd?.resolve();\n    pendingPopstateNavigationDfd = null;\n    pendingRevalidationDfd?.resolve();\n    pendingRevalidationDfd = null;\n  }\n\n  // Trigger a navigation event, which can either be a numerical POP or a PUSH\n  // replace with an optional submission\n  async function navigate(\n    to: number | To | null,\n    opts?: RouterNavigateOptions,\n  ) {\n    // If another popstate is pending and we're about to interrupt it, resolve\n    // the promise since we'll never reach completeNavigation for *that* popstate\n    pendingPopstateNavigationDfd?.resolve();\n    pendingPopstateNavigationDfd = null;\n\n    if (typeof to === \"number\") {\n      // Track this popstate with a deferred so we can expose the promise back\n      // out through useNNavigate.  This will be resolved in one of the following\n      // places:\n      //  - completeNavigation if we are not interrupted\n      //  - history listener if this navigation is blocked\n      //  - this function if another navigation is triggered that interrupts us\n      //  - startRedirectNavigation if we are interrupted by a fetcher-driven\n      //    redirect\n      if (!pendingPopstateNavigationDfd) {\n        pendingPopstateNavigationDfd = createDeferred<void>();\n      }\n\n      let promise = pendingPopstateNavigationDfd.promise;\n      init.history.go(to);\n\n      // Use a local copy here because memory router popstates can be fully synchronous\n      // so it's possible completeNavigation has already cleared out the pending\n      // variable by now\n      return promise;\n    }\n\n    let normalizedPath = normalizeTo(\n      state.location,\n      state.matches,\n      basename,\n      to,\n      opts?.fromRouteId,\n      opts?.relative,\n    );\n    let { path, submission, error } = normalizeNavigateOptions(\n      false,\n      normalizedPath,\n      opts,\n    );\n\n    // If mask is provided, normalize and create a separate path for the router\n    let maskPath: Path | undefined;\n    if (opts?.unstable_mask) {\n      let partialPath =\n        typeof opts.unstable_mask === \"string\"\n          ? parsePath(opts.unstable_mask)\n          : {\n              ...state.location.unstable_mask,\n              ...opts.unstable_mask,\n            };\n      maskPath = {\n        pathname: \"\",\n        search: \"\",\n        hash: \"\",\n        ...partialPath,\n      };\n    }\n\n    let currentLocation = state.location;\n    let nextLocation = createLocation(\n      currentLocation,\n      path,\n      opts && opts.state,\n      undefined,\n      maskPath,\n    );\n\n    // When using navigate as a PUSH/REPLACE we aren't reading an already-encoded\n    // URL from window.location, so we need to encode it here so the behavior\n    // remains the same as POP and non-data-router usages.  new URL() does all\n    // the same encoding we'd get from a history.pushState/window.location read\n    // without having to touch history\n    nextLocation = {\n      ...nextLocation,\n      ...init.history.encodeLocation(nextLocation),\n    };\n\n    let userReplace = opts && opts.replace != null ? opts.replace : undefined;\n\n    let historyAction = NavigationType.Push;\n\n    if (userReplace === true) {\n      historyAction = NavigationType.Replace;\n    } else if (userReplace === false) {\n      // no-op\n    } else if (\n      submission != null &&\n      isMutationMethod(submission.formMethod) &&\n      submission.formAction === state.location.pathname + state.location.search\n    ) {\n      // By default on submissions to the current location we REPLACE so that\n      // users don't have to double-click the back button to get to the prior\n      // location.  If the user redirects to a different location from the\n      // action/loader this will be ignored and the redirect will be a PUSH\n      historyAction = NavigationType.Replace;\n    }\n\n    let preventScrollReset =\n      opts && \"preventScrollReset\" in opts\n        ? opts.preventScrollReset === true\n        : undefined;\n\n    let flushSync = (opts && opts.flushSync) === true;\n\n    let blockerKey = shouldBlockNavigation({\n      currentLocation,\n      nextLocation,\n      historyAction,\n    });\n\n    if (blockerKey) {\n      // Put the blocker into a blocked state\n      updateBlocker(blockerKey, {\n        state: \"blocked\",\n        location: nextLocation,\n        proceed() {\n          updateBlocker(blockerKey!, {\n            state: \"proceeding\",\n            proceed: undefined,\n            reset: undefined,\n            location: nextLocation,\n          });\n          // Send the same navigation through\n          navigate(to, opts);\n        },\n        reset() {\n          let blockers = new Map(state.blockers);\n          blockers.set(blockerKey!, IDLE_BLOCKER);\n          updateState({ blockers });\n        },\n      });\n      return;\n    }\n\n    await startNavigation(historyAction, nextLocation, {\n      submission,\n      // Send through the formData serialization error if we have one so we can\n      // render at the right error boundary after we match routes\n      pendingError: error,\n      preventScrollReset,\n      replace: opts && opts.replace,\n      enableViewTransition: opts && opts.viewTransition,\n      flushSync,\n      callSiteDefaultShouldRevalidate:\n        opts && opts.unstable_defaultShouldRevalidate,\n    });\n  }\n\n  // Revalidate all current loaders.  If a navigation is in progress or if this\n  // is interrupted by a navigation, allow this to \"succeed\" by calling all\n  // loaders during the next loader round\n  function revalidate() {\n    // We can't just return the promise from `startNavigation` because that\n    // navigation may be interrupted and our revalidation wouldn't be finished\n    // until the _next_ navigation completes.  Instead we just track via a\n    // deferred and resolve it the next time we run through `completeNavigation`\n    // This is different than navigations which will settle if interrupted\n    // because the navigation to a specific location is no longer relevant.\n    // Revalidations are location-independent and will settle whenever we land\n    // on our final location\n    if (!pendingRevalidationDfd) {\n      pendingRevalidationDfd = createDeferred<void>();\n    }\n\n    interruptActiveLoads();\n    updateState({ revalidation: \"loading\" });\n\n    // Capture this here for the edge-case that we have a fully synchronous\n    // startNavigation which would resolve and null out pendingRevalidationDfd\n    // before we return from this function\n    let promise = pendingRevalidationDfd.promise;\n\n    // If we're currently submitting an action, we don't need to start a new\n    // navigation, we'll just let the follow up loader execution call all loaders\n    if (state.navigation.state === \"submitting\") {\n      return promise;\n    }\n\n    // If we're currently in an idle state, start a new navigation for the current\n    // action/location and mark it as uninterrupted, which will skip the history\n    // update in completeNavigation\n    if (state.navigation.state === \"idle\") {\n      startNavigation(state.historyAction, state.location, {\n        startUninterruptedRevalidation: true,\n      });\n      return promise;\n    }\n\n    // Otherwise, if we're currently in a loading state, just start a new\n    // navigation to the navigation.location but do not trigger an uninterrupted\n    // revalidation so that history correctly updates once the navigation completes\n    startNavigation(\n      pendingAction || state.historyAction,\n      state.navigation.location,\n      {\n        overrideNavigation: state.navigation,\n        // Proxy through any rending view transition\n        enableViewTransition: pendingViewTransitionEnabled === true,\n      },\n    );\n    return promise;\n  }\n\n  // Start a navigation to the given action/location.  Can optionally provide a\n  // overrideNavigation which will override the normalLoad in the case of a redirect\n  // navigation\n  async function startNavigation(\n    historyAction: NavigationType,\n    location: Location,\n    opts?: {\n      initialHydration?: boolean;\n      submission?: Submission;\n      fetcherSubmission?: Submission;\n      overrideNavigation?: Navigation;\n      pendingError?: ErrorResponseImpl;\n      startUninterruptedRevalidation?: boolean;\n      preventScrollReset?: boolean;\n      replace?: boolean;\n      enableViewTransition?: boolean;\n      flushSync?: boolean;\n      callSiteDefaultShouldRevalidate?: boolean;\n    },\n  ): Promise<void> {\n    // Abort any in-progress navigations and start a new one. Unset any ongoing\n    // uninterrupted revalidations unless told otherwise, since we want this\n    // new navigation to update history normally\n    pendingNavigationController && pendingNavigationController.abort();\n    pendingNavigationController = null;\n    pendingAction = historyAction;\n    isUninterruptedRevalidation =\n      (opts && opts.startUninterruptedRevalidation) === true;\n\n    // Save the current scroll position every time we start a new navigation,\n    // and track whether we should reset scroll on completion\n    saveScrollPosition(state.location, state.matches);\n    pendingPreventScrollReset = (opts && opts.preventScrollReset) === true;\n\n    pendingViewTransitionEnabled = (opts && opts.enableViewTransition) === true;\n\n    let routesToUse = inFlightDataRoutes || dataRoutes;\n    let loadingNavigation = opts && opts.overrideNavigation;\n    let matches =\n      opts?.initialHydration &&\n      state.matches &&\n      state.matches.length > 0 &&\n      !initialMatchesIsFOW\n        ? // `matchRoutes()` has already been called if we're in here via `router.initialize()`\n          state.matches\n        : matchRoutes(routesToUse, location, basename);\n    let flushSync = (opts && opts.flushSync) === true;\n\n    // Short circuit if it's only a hash change and not a revalidation or\n    // mutation submission.\n    //\n    // Ignore on initial page loads because since the initial hydration will always\n    // be \"same hash\".  For example, on /page#hash and submit a <Form method=\"post\">\n    // which will default to a navigation to /page\n    if (\n      matches &&\n      state.initialized &&\n      !isRevalidationRequired &&\n      isHashChangeOnly(state.location, location) &&\n      !(opts && opts.submission && isMutationMethod(opts.submission.formMethod))\n    ) {\n      completeNavigation(location, { matches }, { flushSync });\n      return;\n    }\n\n    let fogOfWar = checkFogOfWar(matches, routesToUse, location.pathname);\n    if (fogOfWar.active && fogOfWar.matches) {\n      matches = fogOfWar.matches;\n    }\n\n    // Short circuit with a 404 on the root error boundary if we match nothing\n    if (!matches) {\n      let { error, notFoundMatches, route } = handleNavigational404(\n        location.pathname,\n      );\n      completeNavigation(\n        location,\n        {\n          matches: notFoundMatches,\n          loaderData: {},\n          errors: {\n            [route.id]: error,\n          },\n        },\n        { flushSync },\n      );\n      return;\n    }\n\n    // Create a controller/Request for this navigation\n    pendingNavigationController = new AbortController();\n    let request = createClientSideRequest(\n      init.history,\n      location,\n      pendingNavigationController.signal,\n      opts && opts.submission,\n    );\n    // Create a new context per navigation\n    let scopedContext = init.getContext\n      ? await init.getContext()\n      : new RouterContextProvider();\n    let pendingActionResult: PendingActionResult | undefined;\n\n    if (opts && opts.pendingError) {\n      // If we have a pendingError, it means the user attempted a GET submission\n      // with binary FormData so assign here and skip to handleLoaders.  That\n      // way we handle calling loaders above the boundary etc.  It's not really\n      // different from an actionError in that sense.\n      pendingActionResult = [\n        findNearestBoundary(matches).route.id,\n        { type: ResultType.error, error: opts.pendingError },\n      ];\n    } else if (\n      opts &&\n      opts.submission &&\n      isMutationMethod(opts.submission.formMethod)\n    ) {\n      // Call action if we received an action submission\n      let actionResult = await handleAction(\n        request,\n        location,\n        opts.submission,\n        matches,\n        scopedContext,\n        fogOfWar.active,\n        opts && opts.initialHydration === true,\n        { replace: opts.replace, flushSync },\n      );\n\n      if (actionResult.shortCircuited) {\n        return;\n      }\n\n      // If we received a 404 from handleAction, it's because we couldn't lazily\n      // discover the destination route so we don't want to call loaders\n      if (actionResult.pendingActionResult) {\n        let [routeId, result] = actionResult.pendingActionResult;\n        if (\n          isErrorResult(result) &&\n          isRouteErrorResponse(result.error) &&\n          result.error.status === 404\n        ) {\n          pendingNavigationController = null;\n\n          completeNavigation(location, {\n            matches: actionResult.matches,\n            loaderData: {},\n            errors: {\n              [routeId]: result.error,\n            },\n          });\n          return;\n        }\n      }\n\n      matches = actionResult.matches || matches;\n      pendingActionResult = actionResult.pendingActionResult;\n      loadingNavigation = getLoadingNavigation(location, opts.submission);\n      flushSync = false;\n      // No need to do fog of war matching again on loader execution\n      fogOfWar.active = false;\n\n      // Create a GET request for the loaders\n      request = createClientSideRequest(\n        init.history,\n        request.url,\n        request.signal,\n      );\n    }\n\n    // Call loaders\n    let {\n      shortCircuited,\n      matches: updatedMatches,\n      loaderData,\n      errors,\n    } = await handleLoaders(\n      request,\n      location,\n      matches,\n      scopedContext,\n      fogOfWar.active,\n      loadingNavigation,\n      opts && opts.submission,\n      opts && opts.fetcherSubmission,\n      opts && opts.replace,\n      opts && opts.initialHydration === true,\n      flushSync,\n      pendingActionResult,\n      opts && opts.callSiteDefaultShouldRevalidate,\n    );\n\n    if (shortCircuited) {\n      return;\n    }\n\n    // Clean up now that the action/loaders have completed.  Don't clean up if\n    // we short circuited because pendingNavigationController will have already\n    // been assigned to a new controller for the next navigation\n    pendingNavigationController = null;\n\n    completeNavigation(location, {\n      matches: updatedMatches || matches,\n      ...getActionDataForCommit(pendingActionResult),\n      loaderData,\n      errors,\n    });\n  }\n\n  // Call the action matched by the leaf route for this navigation and handle\n  // redirects/errors\n  async function handleAction(\n    request: Request,\n    location: Location,\n    submission: Submission,\n    matches: AgnosticDataRouteMatch[],\n    scopedContext: RouterContextProvider,\n    isFogOfWar: boolean,\n    initialHydration: boolean,\n    opts: { replace?: boolean; flushSync?: boolean } = {},\n  ): Promise<HandleActionResult> {\n    interruptActiveLoads();\n\n    // Put us in a submitting state\n    let navigation = getSubmittingNavigation(location, submission);\n    updateState({ navigation }, { flushSync: opts.flushSync === true });\n\n    if (isFogOfWar) {\n      let discoverResult = await discoverRoutes(\n        matches,\n        location.pathname,\n        request.signal,\n      );\n      if (discoverResult.type === \"aborted\") {\n        return { shortCircuited: true };\n      } else if (discoverResult.type === \"error\") {\n        if (discoverResult.partialMatches.length === 0) {\n          let { matches, route } = getShortCircuitMatches(dataRoutes);\n          return {\n            matches,\n            pendingActionResult: [\n              route.id,\n              {\n                type: ResultType.error,\n                error: discoverResult.error,\n              },\n            ],\n          };\n        }\n\n        let boundaryId = findNearestBoundary(discoverResult.partialMatches)\n          .route.id;\n        return {\n          matches: discoverResult.partialMatches,\n          pendingActionResult: [\n            boundaryId,\n            {\n              type: ResultType.error,\n              error: discoverResult.error,\n            },\n          ],\n        };\n      } else if (!discoverResult.matches) {\n        let { notFoundMatches, error, route } = handleNavigational404(\n          location.pathname,\n        );\n        return {\n          matches: notFoundMatches,\n          pendingActionResult: [\n            route.id,\n            {\n              type: ResultType.error,\n              error,\n            },\n          ],\n        };\n      } else {\n        matches = discoverResult.matches;\n      }\n    }\n\n    // Call our action and get the result\n    let result: DataResult;\n    let actionMatch = getTargetMatch(matches, location);\n\n    if (!actionMatch.route.action && !actionMatch.route.lazy) {\n      result = {\n        type: ResultType.error,\n        error: getInternalRouterError(405, {\n          method: request.method,\n          pathname: location.pathname,\n          routeId: actionMatch.route.id,\n        }),\n      };\n    } else {\n      let dsMatches = getTargetedDataStrategyMatches(\n        mapRouteProperties,\n        manifest,\n        request,\n        matches,\n        actionMatch,\n        initialHydration ? [] : hydrationRouteProperties,\n        scopedContext,\n      );\n      let results = await callDataStrategy(\n        request,\n        dsMatches,\n        scopedContext,\n        null,\n      );\n      result = results[actionMatch.route.id];\n\n      if (!result) {\n        // If this error came from a parent middleware before the action ran,\n        // then it won't be tied to the action route\n        for (let match of matches) {\n          if (results[match.route.id]) {\n            result = results[match.route.id];\n            break;\n          }\n        }\n      }\n\n      if (request.signal.aborted) {\n        return { shortCircuited: true };\n      }\n    }\n\n    if (isRedirectResult(result)) {\n      let replace: boolean;\n      if (opts && opts.replace != null) {\n        replace = opts.replace;\n      } else {\n        // If the user didn't explicitly indicate replace behavior, replace if\n        // we redirected to the exact same location we're currently at to avoid\n        // double back-buttons\n        let location = normalizeRedirectLocation(\n          result.response.headers.get(\"Location\")!,\n          new URL(request.url),\n          basename,\n          init.history,\n        );\n        replace = location === state.location.pathname + state.location.search;\n      }\n      await startRedirectNavigation(request, result, true, {\n        submission,\n        replace,\n      });\n      return { shortCircuited: true };\n    }\n\n    if (isErrorResult(result)) {\n      // Store off the pending error - we use it to determine which loaders\n      // to call and will commit it when we complete the navigation\n      let boundaryMatch = findNearestBoundary(matches, actionMatch.route.id);\n\n      // By default, all submissions to the current location are REPLACE\n      // navigations, but if the action threw an error that'll be rendered in\n      // an errorElement, we fall back to PUSH so that the user can use the\n      // back button to get back to the pre-submission form location to try\n      // again\n      if ((opts && opts.replace) !== true) {\n        pendingAction = NavigationType.Push;\n      }\n\n      return {\n        matches,\n        pendingActionResult: [\n          boundaryMatch.route.id,\n          result,\n          actionMatch.route.id,\n        ],\n      };\n    }\n\n    return {\n      matches,\n      pendingActionResult: [actionMatch.route.id, result],\n    };\n  }\n\n  // Call all applicable loaders for the given matches, handling redirects,\n  // errors, etc.\n  async function handleLoaders(\n    request: Request,\n    location: Location,\n    matches: AgnosticDataRouteMatch[],\n    scopedContext: RouterContextProvider,\n    isFogOfWar: boolean,\n    overrideNavigation?: Navigation,\n    submission?: Submission,\n    fetcherSubmission?: Submission,\n    replace?: boolean,\n    initialHydration?: boolean,\n    flushSync?: boolean,\n    pendingActionResult?: PendingActionResult,\n    callSiteDefaultShouldRevalidate?: boolean,\n  ): Promise<HandleLoadersResult> {\n    // Figure out the right navigation we want to use for data loading\n    let loadingNavigation =\n      overrideNavigation || getLoadingNavigation(location, submission);\n\n    // If this was a redirect from an action we don't have a \"submission\" but\n    // we have it on the loading navigation so use that if available\n    let activeSubmission =\n      submission ||\n      fetcherSubmission ||\n      getSubmissionFromNavigation(loadingNavigation);\n\n    // If this is an uninterrupted revalidation, we remain in our current idle\n    // state.  If not, we need to switch to our loading state and load data,\n    // preserving any new action data or existing action data (in the case of\n    // a revalidation interrupting an actionReload)\n    // Also (with \"partial hydration\"), don't update the state for the initial\n    // data load since it's not a \"navigation\"\n    let shouldUpdateNavigationState =\n      !isUninterruptedRevalidation && !initialHydration;\n\n    // When fog of war is enabled, we enter our `loading` state earlier so we\n    // can discover new routes during the `loading` state.  We skip this if\n    // we've already run actions since we would have done our matching already.\n    // If the children() function threw then, we want to proceed with the\n    // partial matches it discovered.\n    if (isFogOfWar) {\n      if (shouldUpdateNavigationState) {\n        let actionData = getUpdatedActionData(pendingActionResult);\n        updateState(\n          {\n            navigation: loadingNavigation,\n            ...(actionData !== undefined ? { actionData } : {}),\n          },\n          {\n            flushSync,\n          },\n        );\n      }\n\n      let discoverResult = await discoverRoutes(\n        matches,\n        location.pathname,\n        request.signal,\n      );\n\n      if (discoverResult.type === \"aborted\") {\n        return { shortCircuited: true };\n      } else if (discoverResult.type === \"error\") {\n        if (discoverResult.partialMatches.length === 0) {\n          let { matches, route } = getShortCircuitMatches(dataRoutes);\n          return {\n            matches,\n            loaderData: {},\n            errors: {\n              [route.id]: discoverResult.error,\n            },\n          };\n        }\n\n        let boundaryId = findNearestBoundary(discoverResult.partialMatches)\n          .route.id;\n        return {\n          matches: discoverResult.partialMatches,\n          loaderData: {},\n          errors: {\n            [boundaryId]: discoverResult.error,\n          },\n        };\n      } else if (!discoverResult.matches) {\n        let { error, notFoundMatches, route } = handleNavigational404(\n          location.pathname,\n        );\n        return {\n          matches: notFoundMatches,\n          loaderData: {},\n          errors: {\n            [route.id]: error,\n          },\n        };\n      } else {\n        matches = discoverResult.matches;\n      }\n    }\n\n    let routesToUse = inFlightDataRoutes || dataRoutes;\n    let { dsMatches, revalidatingFetchers } = getMatchesToLoad(\n      request,\n      scopedContext,\n      mapRouteProperties,\n      manifest,\n      init.history,\n      state,\n      matches,\n      activeSubmission,\n      location,\n      initialHydration ? [] : hydrationRouteProperties,\n      initialHydration === true,\n      isRevalidationRequired,\n      cancelledFetcherLoads,\n      fetchersQueuedForDeletion,\n      fetchLoadMatches,\n      fetchRedirectIds,\n      routesToUse,\n      basename,\n      init.patchRoutesOnNavigation != null,\n      pendingActionResult,\n      callSiteDefaultShouldRevalidate,\n    );\n\n    pendingNavigationLoadId = ++incrementingLoadId;\n\n    // Short circuit if we have no loaders/middlewares to run, unless there's a\n    // custom dataStrategy since they may have different revalidation rules\n    // (i.e., single fetch)\n    if (\n      !init.dataStrategy &&\n      !dsMatches.some((m) => m.shouldLoad) &&\n      !dsMatches.some(\n        (m) => m.route.middleware && m.route.middleware.length > 0,\n      ) &&\n      revalidatingFetchers.length === 0\n    ) {\n      let updatedFetchers = markFetchRedirectsDone();\n      completeNavigation(\n        location,\n        {\n          matches,\n          loaderData: {},\n          // Commit pending error if we're short circuiting\n          errors:\n            pendingActionResult && isErrorResult(pendingActionResult[1])\n              ? { [pendingActionResult[0]]: pendingActionResult[1].error }\n              : null,\n          ...getActionDataForCommit(pendingActionResult),\n          ...(updatedFetchers ? { fetchers: new Map(state.fetchers) } : {}),\n        },\n        { flushSync },\n      );\n      return { shortCircuited: true };\n    }\n\n    if (shouldUpdateNavigationState) {\n      let updates: Partial<RouterState> = {};\n      if (!isFogOfWar) {\n        // Only update navigation/actionNData if we didn't already do it above\n        updates.navigation = loadingNavigation;\n        let actionData = getUpdatedActionData(pendingActionResult);\n        if (actionData !== undefined) {\n          updates.actionData = actionData;\n        }\n      }\n      if (revalidatingFetchers.length > 0) {\n        updates.fetchers = getUpdatedRevalidatingFetchers(revalidatingFetchers);\n      }\n      updateState(updates, { flushSync });\n    }\n\n    revalidatingFetchers.forEach((rf) => {\n      abortFetcher(rf.key);\n      if (rf.controller) {\n        // Fetchers use an independent AbortController so that aborting a fetcher\n        // (via deleteFetcher) does not abort the triggering navigation that\n        // triggered the revalidation\n        fetchControllers.set(rf.key, rf.controller);\n      }\n    });\n\n    // Proxy navigation abort through to revalidation fetchers\n    let abortPendingFetchRevalidations = () =>\n      revalidatingFetchers.forEach((f) => abortFetcher(f.key));\n    if (pendingNavigationController) {\n      pendingNavigationController.signal.addEventListener(\n        \"abort\",\n        abortPendingFetchRevalidations,\n      );\n    }\n\n    let { loaderResults, fetcherResults } =\n      await callLoadersAndMaybeResolveData(\n        dsMatches,\n        revalidatingFetchers,\n        request,\n        scopedContext,\n      );\n\n    if (request.signal.aborted) {\n      return { shortCircuited: true };\n    }\n\n    // Clean up _after_ loaders have completed.  Don't clean up if we short\n    // circuited because fetchControllers would have been aborted and\n    // reassigned to new controllers for the next navigation\n    if (pendingNavigationController) {\n      pendingNavigationController.signal.removeEventListener(\n        \"abort\",\n        abortPendingFetchRevalidations,\n      );\n    }\n\n    revalidatingFetchers.forEach((rf) => fetchControllers.delete(rf.key));\n\n    // If any loaders returned a redirect Response, start a new REPLACE navigation\n    let redirect = findRedirect(loaderResults);\n    if (redirect) {\n      await startRedirectNavigation(request, redirect.result, true, {\n        replace,\n      });\n      return { shortCircuited: true };\n    }\n\n    redirect = findRedirect(fetcherResults);\n    if (redirect) {\n      // If this redirect came from a fetcher make sure we mark it in\n      // fetchRedirectIds so it doesn't get revalidated on the next set of\n      // loader executions\n      fetchRedirectIds.add(redirect.key);\n      await startRedirectNavigation(request, redirect.result, true, {\n        replace,\n      });\n      return { shortCircuited: true };\n    }\n\n    // Process and commit output from loaders\n    let { loaderData, errors } = processLoaderData(\n      state,\n      matches,\n      loaderResults,\n      pendingActionResult,\n      revalidatingFetchers,\n      fetcherResults,\n    );\n\n    // Preserve SSR errors during partial hydration\n    if (initialHydration && state.errors) {\n      errors = { ...state.errors, ...errors };\n    }\n\n    let updatedFetchers = markFetchRedirectsDone();\n    let didAbortFetchLoads = abortStaleFetchLoads(pendingNavigationLoadId);\n    let shouldUpdateFetchers =\n      updatedFetchers || didAbortFetchLoads || revalidatingFetchers.length > 0;\n\n    return {\n      matches,\n      loaderData,\n      errors,\n      ...(shouldUpdateFetchers ? { fetchers: new Map(state.fetchers) } : {}),\n    };\n  }\n\n  function getUpdatedActionData(\n    pendingActionResult: PendingActionResult | undefined,\n  ): Record<string, RouteData> | null | undefined {\n    if (pendingActionResult && !isErrorResult(pendingActionResult[1])) {\n      // This is cast to `any` currently because `RouteData`uses any and it\n      // would be a breaking change to use any.\n      // TODO: v7 - change `RouteData` to use `unknown` instead of `any`\n      return {\n        [pendingActionResult[0]]: pendingActionResult[1].data as any,\n      };\n    } else if (state.actionData) {\n      if (Object.keys(state.actionData).length === 0) {\n        return null;\n      } else {\n        return state.actionData;\n      }\n    }\n  }\n\n  function getUpdatedRevalidatingFetchers(\n    revalidatingFetchers: RevalidatingFetcher[],\n  ) {\n    revalidatingFetchers.forEach((rf) => {\n      let fetcher = state.fetchers.get(rf.key);\n      let revalidatingFetcher = getLoadingFetcher(\n        undefined,\n        fetcher ? fetcher.data : undefined,\n      );\n      state.fetchers.set(rf.key, revalidatingFetcher);\n    });\n    return new Map(state.fetchers);\n  }\n\n  // Trigger a fetcher load/submit for the given fetcher key\n  async function fetch(\n    key: string,\n    routeId: string,\n    href: string | null,\n    opts?: RouterFetchOptions,\n  ) {\n    abortFetcher(key);\n\n    let flushSync = (opts && opts.flushSync) === true;\n\n    let routesToUse = inFlightDataRoutes || dataRoutes;\n    let normalizedPath = normalizeTo(\n      state.location,\n      state.matches,\n      basename,\n      href,\n      routeId,\n      opts?.relative,\n    );\n    let matches = matchRoutes(routesToUse, normalizedPath, basename);\n\n    let fogOfWar = checkFogOfWar(matches, routesToUse, normalizedPath);\n    if (fogOfWar.active && fogOfWar.matches) {\n      matches = fogOfWar.matches;\n    }\n\n    if (!matches) {\n      setFetcherError(\n        key,\n        routeId,\n        getInternalRouterError(404, { pathname: normalizedPath }),\n        { flushSync },\n      );\n      return;\n    }\n\n    let { path, submission, error } = normalizeNavigateOptions(\n      true,\n      normalizedPath,\n      opts,\n    );\n\n    if (error) {\n      setFetcherError(key, routeId, error, { flushSync });\n      return;\n    }\n\n    // Create a new context per fetch\n    let scopedContext = init.getContext\n      ? await init.getContext()\n      : new RouterContextProvider();\n    let preventScrollReset = (opts && opts.preventScrollReset) === true;\n\n    if (submission && isMutationMethod(submission.formMethod)) {\n      await handleFetcherAction(\n        key,\n        routeId,\n        path,\n        matches,\n        scopedContext,\n        fogOfWar.active,\n        flushSync,\n        preventScrollReset,\n        submission,\n        opts && opts.unstable_defaultShouldRevalidate,\n      );\n      return;\n    }\n\n    // Store off the match so we can call it's shouldRevalidate on subsequent\n    // revalidations\n    fetchLoadMatches.set(key, { routeId, path });\n    await handleFetcherLoader(\n      key,\n      routeId,\n      path,\n      matches,\n      scopedContext,\n      fogOfWar.active,\n      flushSync,\n      preventScrollReset,\n      submission,\n    );\n  }\n\n  // Call the action for the matched fetcher.submit(), and then handle redirects,\n  // errors, and revalidation\n  async function handleFetcherAction(\n    key: string,\n    routeId: string,\n    path: string,\n    requestMatches: AgnosticDataRouteMatch[],\n    scopedContext: RouterContextProvider,\n    isFogOfWar: boolean,\n    flushSync: boolean,\n    preventScrollReset: boolean,\n    submission: Submission,\n    callSiteDefaultShouldRevalidate: boolean | undefined,\n  ) {\n    interruptActiveLoads();\n    fetchLoadMatches.delete(key);\n\n    // Put this fetcher into it's submitting state\n    let existingFetcher = state.fetchers.get(key);\n    updateFetcherState(key, getSubmittingFetcher(submission, existingFetcher), {\n      flushSync,\n    });\n\n    let abortController = new AbortController();\n    let fetchRequest = createClientSideRequest(\n      init.history,\n      path,\n      abortController.signal,\n      submission,\n    );\n\n    if (isFogOfWar) {\n      let discoverResult = await discoverRoutes(\n        requestMatches,\n        new URL(fetchRequest.url).pathname,\n        fetchRequest.signal,\n        key,\n      );\n\n      if (discoverResult.type === \"aborted\") {\n        return;\n      } else if (discoverResult.type === \"error\") {\n        setFetcherError(key, routeId, discoverResult.error, { flushSync });\n        return;\n      } else if (!discoverResult.matches) {\n        setFetcherError(\n          key,\n          routeId,\n          getInternalRouterError(404, { pathname: path }),\n          { flushSync },\n        );\n        return;\n      } else {\n        requestMatches = discoverResult.matches;\n      }\n    }\n\n    let match = getTargetMatch(requestMatches, path);\n\n    if (!match.route.action && !match.route.lazy) {\n      let error = getInternalRouterError(405, {\n        method: submission.formMethod,\n        pathname: path,\n        routeId: routeId,\n      });\n      setFetcherError(key, routeId, error, { flushSync });\n      return;\n    }\n\n    // Call the action for the fetcher\n    fetchControllers.set(key, abortController);\n\n    let originatingLoadId = incrementingLoadId;\n    let fetchMatches = getTargetedDataStrategyMatches(\n      mapRouteProperties,\n      manifest,\n      fetchRequest,\n      requestMatches,\n      match,\n      hydrationRouteProperties,\n      scopedContext,\n    );\n    let actionResults = await callDataStrategy(\n      fetchRequest,\n      fetchMatches,\n      scopedContext,\n      key,\n    );\n    let actionResult = actionResults[match.route.id];\n\n    if (!actionResult) {\n      // If this error came from a parent middleware before the action ran,\n      // then it won't be tied to the action route\n      for (let match of fetchMatches) {\n        if (actionResults[match.route.id]) {\n          actionResult = actionResults[match.route.id];\n          break;\n        }\n      }\n    }\n\n    if (fetchRequest.signal.aborted) {\n      // We can delete this so long as we weren't aborted by our own fetcher\n      // re-submit which would have put _new_ controller is in fetchControllers\n      if (fetchControllers.get(key) === abortController) {\n        fetchControllers.delete(key);\n      }\n      return;\n    }\n\n    // We don't want errors bubbling up to the UI or redirects processed for\n    // unmounted fetchers so we just revert them to idle\n    if (fetchersQueuedForDeletion.has(key)) {\n      if (isRedirectResult(actionResult) || isErrorResult(actionResult)) {\n        updateFetcherState(key, getDoneFetcher(undefined));\n        return;\n      }\n      // Let SuccessResult's fall through for revalidation\n    } else {\n      if (isRedirectResult(actionResult)) {\n        fetchControllers.delete(key);\n        if (pendingNavigationLoadId > originatingLoadId) {\n          // A new navigation was kicked off after our action started, so that\n          // should take precedence over this redirect navigation.  We already\n          // set isRevalidationRequired so all loaders for the new route should\n          // fire unless opted out via shouldRevalidate\n          updateFetcherState(key, getDoneFetcher(undefined));\n          return;\n        } else {\n          fetchRedirectIds.add(key);\n          updateFetcherState(key, getLoadingFetcher(submission));\n          return startRedirectNavigation(fetchRequest, actionResult, false, {\n            fetcherSubmission: submission,\n            preventScrollReset,\n          });\n        }\n      }\n\n      // Process any non-redirect errors thrown\n      if (isErrorResult(actionResult)) {\n        setFetcherError(key, routeId, actionResult.error);\n        return;\n      }\n    }\n\n    // Start the data load for current matches, or the next location if we're\n    // in the middle of a navigation\n    let nextLocation = state.navigation.location || state.location;\n    let revalidationRequest = createClientSideRequest(\n      init.history,\n      nextLocation,\n      abortController.signal,\n    );\n    let routesToUse = inFlightDataRoutes || dataRoutes;\n    let matches =\n      state.navigation.state !== \"idle\"\n        ? matchRoutes(routesToUse, state.navigation.location, basename)\n        : state.matches;\n\n    invariant(matches, \"Didn't find any matches after fetcher action\");\n\n    let loadId = ++incrementingLoadId;\n    fetchReloadIds.set(key, loadId);\n\n    let loadFetcher = getLoadingFetcher(submission, actionResult.data);\n    state.fetchers.set(key, loadFetcher);\n\n    let { dsMatches, revalidatingFetchers } = getMatchesToLoad(\n      revalidationRequest,\n      scopedContext,\n      mapRouteProperties,\n      manifest,\n      init.history,\n      state,\n      matches,\n      submission,\n      nextLocation,\n      hydrationRouteProperties,\n      false,\n      isRevalidationRequired,\n      cancelledFetcherLoads,\n      fetchersQueuedForDeletion,\n      fetchLoadMatches,\n      fetchRedirectIds,\n      routesToUse,\n      basename,\n      init.patchRoutesOnNavigation != null,\n      [match.route.id, actionResult],\n      callSiteDefaultShouldRevalidate,\n    );\n\n    // Put all revalidating fetchers into the loading state, except for the\n    // current fetcher which we want to keep in it's current loading state which\n    // contains it's action submission info + action data\n    revalidatingFetchers\n      .filter((rf) => rf.key !== key)\n      .forEach((rf) => {\n        let staleKey = rf.key;\n        let existingFetcher = state.fetchers.get(staleKey);\n        let revalidatingFetcher = getLoadingFetcher(\n          undefined,\n          existingFetcher ? existingFetcher.data : undefined,\n        );\n        state.fetchers.set(staleKey, revalidatingFetcher);\n        abortFetcher(staleKey);\n        if (rf.controller) {\n          fetchControllers.set(staleKey, rf.controller);\n        }\n      });\n\n    updateState({ fetchers: new Map(state.fetchers) });\n\n    let abortPendingFetchRevalidations = () =>\n      revalidatingFetchers.forEach((rf) => abortFetcher(rf.key));\n\n    abortController.signal.addEventListener(\n      \"abort\",\n      abortPendingFetchRevalidations,\n    );\n\n    let { loaderResults, fetcherResults } =\n      await callLoadersAndMaybeResolveData(\n        dsMatches,\n        revalidatingFetchers,\n        revalidationRequest,\n        scopedContext,\n      );\n\n    if (abortController.signal.aborted) {\n      return;\n    }\n\n    abortController.signal.removeEventListener(\n      \"abort\",\n      abortPendingFetchRevalidations,\n    );\n\n    fetchReloadIds.delete(key);\n    fetchControllers.delete(key);\n    revalidatingFetchers.forEach((r) => fetchControllers.delete(r.key));\n\n    // Since we let revalidations complete even if the submitting fetcher was\n    // deleted, only put it back to idle if it hasn't been deleted\n    if (state.fetchers.has(key)) {\n      let doneFetcher = getDoneFetcher(actionResult.data);\n      state.fetchers.set(key, doneFetcher);\n    }\n\n    let redirect = findRedirect(loaderResults);\n    if (redirect) {\n      return startRedirectNavigation(\n        revalidationRequest,\n        redirect.result,\n        false,\n        { preventScrollReset },\n      );\n    }\n\n    redirect = findRedirect(fetcherResults);\n    if (redirect) {\n      // If this redirect came from a fetcher make sure we mark it in\n      // fetchRedirectIds so it doesn't get revalidated on the next set of\n      // loader executions\n      fetchRedirectIds.add(redirect.key);\n      return startRedirectNavigation(\n        revalidationRequest,\n        redirect.result,\n        false,\n        { preventScrollReset },\n      );\n    }\n\n    // Process and commit output from loaders\n    let { loaderData, errors } = processLoaderData(\n      state,\n      matches,\n      loaderResults,\n      undefined,\n      revalidatingFetchers,\n      fetcherResults,\n    );\n\n    abortStaleFetchLoads(loadId);\n\n    // If we are currently in a navigation loading state and this fetcher is\n    // more recent than the navigation, we want the newer data so abort the\n    // navigation and complete it with the fetcher data\n    if (\n      state.navigation.state === \"loading\" &&\n      loadId > pendingNavigationLoadId\n    ) {\n      invariant(pendingAction, \"Expected pending action\");\n      pendingNavigationController && pendingNavigationController.abort();\n\n      completeNavigation(state.navigation.location, {\n        matches,\n        loaderData,\n        errors,\n        fetchers: new Map(state.fetchers),\n      });\n    } else {\n      // otherwise just update with the fetcher data, preserving any existing\n      // loaderData for loaders that did not need to reload.  We have to\n      // manually merge here since we aren't going through completeNavigation\n      updateState({\n        errors,\n        loaderData: mergeLoaderData(\n          state.loaderData,\n          loaderData,\n          matches,\n          errors,\n        ),\n        fetchers: new Map(state.fetchers),\n      });\n      isRevalidationRequired = false;\n    }\n  }\n\n  // Call the matched loader for fetcher.load(), handling redirects, errors, etc.\n  async function handleFetcherLoader(\n    key: string,\n    routeId: string,\n    path: string,\n    matches: AgnosticDataRouteMatch[],\n    scopedContext: RouterContextProvider,\n    isFogOfWar: boolean,\n    flushSync: boolean,\n    preventScrollReset: boolean,\n    submission?: Submission,\n  ) {\n    let existingFetcher = state.fetchers.get(key);\n    updateFetcherState(\n      key,\n      getLoadingFetcher(\n        submission,\n        existingFetcher ? existingFetcher.data : undefined,\n      ),\n      { flushSync },\n    );\n\n    let abortController = new AbortController();\n    let fetchRequest = createClientSideRequest(\n      init.history,\n      path,\n      abortController.signal,\n    );\n\n    if (isFogOfWar) {\n      let discoverResult = await discoverRoutes(\n        matches,\n        new URL(fetchRequest.url).pathname,\n        fetchRequest.signal,\n        key,\n      );\n\n      if (discoverResult.type === \"aborted\") {\n        return;\n      } else if (discoverResult.type === \"error\") {\n        setFetcherError(key, routeId, discoverResult.error, { flushSync });\n        return;\n      } else if (!discoverResult.matches) {\n        setFetcherError(\n          key,\n          routeId,\n          getInternalRouterError(404, { pathname: path }),\n          { flushSync },\n        );\n        return;\n      } else {\n        matches = discoverResult.matches;\n      }\n    }\n\n    let match = getTargetMatch(matches, path);\n\n    // Call the loader for this fetcher route match\n    fetchControllers.set(key, abortController);\n\n    let originatingLoadId = incrementingLoadId;\n    let dsMatches = getTargetedDataStrategyMatches(\n      mapRouteProperties,\n      manifest,\n      fetchRequest,\n      matches,\n      match,\n      hydrationRouteProperties,\n      scopedContext,\n    );\n    let results = await callDataStrategy(\n      fetchRequest,\n      dsMatches,\n      scopedContext,\n      key,\n    );\n    let result = results[match.route.id];\n\n    // We can delete this so long as we weren't aborted by our our own fetcher\n    // re-load which would have put _new_ controller is in fetchControllers\n    if (fetchControllers.get(key) === abortController) {\n      fetchControllers.delete(key);\n    }\n\n    if (fetchRequest.signal.aborted) {\n      return;\n    }\n\n    // We don't want errors bubbling up or redirects followed for unmounted\n    // fetchers, so short circuit here if it was removed from the UI\n    if (fetchersQueuedForDeletion.has(key)) {\n      updateFetcherState(key, getDoneFetcher(undefined));\n      return;\n    }\n\n    // If the loader threw a redirect Response, start a new REPLACE navigation\n    if (isRedirectResult(result)) {\n      if (pendingNavigationLoadId > originatingLoadId) {\n        // A new navigation was kicked off after our loader started, so that\n        // should take precedence over this redirect navigation\n        updateFetcherState(key, getDoneFetcher(undefined));\n        return;\n      } else {\n        fetchRedirectIds.add(key);\n        await startRedirectNavigation(fetchRequest, result, false, {\n          preventScrollReset,\n        });\n        return;\n      }\n    }\n\n    // Process any non-redirect errors thrown\n    if (isErrorResult(result)) {\n      setFetcherError(key, routeId, result.error);\n      return;\n    }\n\n    // Put the fetcher back into an idle state\n    updateFetcherState(key, getDoneFetcher(result.data));\n  }\n\n  /**\n   * Utility function to handle redirects returned from an action or loader.\n   * Normally, a redirect \"replaces\" the navigation that triggered it.  So, for\n   * example:\n   *\n   *  - user is on /a\n   *  - user clicks a link to /b\n   *  - loader for /b redirects to /c\n   *\n   * In a non-JS app the browser would track the in-flight navigation to /b and\n   * then replace it with /c when it encountered the redirect response.  In\n   * the end it would only ever update the URL bar with /c.\n   *\n   * In client-side routing using pushState/replaceState, we aim to emulate\n   * this behavior and we also do not update history until the end of the\n   * navigation (including processed redirects).  This means that we never\n   * actually touch history until we've processed redirects, so we just use\n   * the history action from the original navigation (PUSH or REPLACE).\n   */\n  async function startRedirectNavigation(\n    request: Request,\n    redirect: RedirectResult,\n    isNavigation: boolean,\n    {\n      submission,\n      fetcherSubmission,\n      preventScrollReset,\n      replace,\n    }: {\n      submission?: Submission;\n      fetcherSubmission?: Submission;\n      preventScrollReset?: boolean;\n      replace?: boolean;\n    } = {},\n  ) {\n    // If we are interrupting a popstate navigation from a fetcher call that\n    // redirected, resolve the pending promise since we won't reach completeNavigation\n    // for that popstate navigation\n    if (!isNavigation) {\n      pendingPopstateNavigationDfd?.resolve();\n      pendingPopstateNavigationDfd = null;\n    }\n\n    if (redirect.response.headers.has(\"X-Remix-Revalidate\")) {\n      isRevalidationRequired = true;\n    }\n\n    let location = redirect.response.headers.get(\"Location\");\n    invariant(location, \"Expected a Location header on the redirect Response\");\n    location = normalizeRedirectLocation(\n      location,\n      new URL(request.url),\n      basename,\n      init.history,\n    );\n    let redirectLocation = createLocation(state.location, location, {\n      _isRedirect: true,\n    });\n\n    if (isBrowser) {\n      let isDocumentReload = false;\n\n      if (redirect.response.headers.has(\"X-Remix-Reload-Document\")) {\n        // Hard reload if the response contained X-Remix-Reload-Document\n        isDocumentReload = true;\n      } else if (isAbsoluteUrl(location)) {\n        // We skip `history.createURL` here for absolute URLs because we don't\n        // want to inherit the current `window.location` base URL\n        const url = createBrowserURLImpl(location, true);\n        isDocumentReload =\n          // Hard reload if it's an absolute URL to a new origin\n          url.origin !== routerWindow.location.origin ||\n          // Hard reload if it's an absolute URL that does not match our basename\n          stripBasename(url.pathname, basename) == null;\n      }\n\n      if (isDocumentReload) {\n        if (replace) {\n          routerWindow.location.replace(location);\n        } else {\n          routerWindow.location.assign(location);\n        }\n        return;\n      }\n    }\n\n    // There's no need to abort on redirects, since we don't detect the\n    // redirect until the action/loaders have settled\n    pendingNavigationController = null;\n\n    let redirectNavigationType =\n      replace === true || redirect.response.headers.has(\"X-Remix-Replace\")\n        ? NavigationType.Replace\n        : NavigationType.Push;\n\n    // Use the incoming submission if provided, fallback on the active one in\n    // state.navigation\n    let { formMethod, formAction, formEncType } = state.navigation;\n    if (\n      !submission &&\n      !fetcherSubmission &&\n      formMethod &&\n      formAction &&\n      formEncType\n    ) {\n      submission = getSubmissionFromNavigation(state.navigation);\n    }\n\n    // If this was a 307/308 submission we want to preserve the HTTP method and\n    // re-submit the GET/POST/PUT/PATCH/DELETE as a submission navigation to the\n    // redirected location\n    let activeSubmission = submission || fetcherSubmission;\n    if (\n      redirectPreserveMethodStatusCodes.has(redirect.response.status) &&\n      activeSubmission &&\n      isMutationMethod(activeSubmission.formMethod)\n    ) {\n      await startNavigation(redirectNavigationType, redirectLocation, {\n        submission: {\n          ...activeSubmission,\n          formAction: location,\n        },\n        // Preserve these flags across redirects\n        preventScrollReset: preventScrollReset || pendingPreventScrollReset,\n        enableViewTransition: isNavigation\n          ? pendingViewTransitionEnabled\n          : undefined,\n      });\n    } else {\n      // If we have a navigation submission, we will preserve it through the\n      // redirect navigation\n      let overrideNavigation = getLoadingNavigation(\n        redirectLocation,\n        submission,\n      );\n      await startNavigation(redirectNavigationType, redirectLocation, {\n        overrideNavigation,\n        // Send fetcher submissions through for shouldRevalidate\n        fetcherSubmission,\n        // Preserve these flags across redirects\n        preventScrollReset: preventScrollReset || pendingPreventScrollReset,\n        enableViewTransition: isNavigation\n          ? pendingViewTransitionEnabled\n          : undefined,\n      });\n    }\n  }\n\n  // Utility wrapper for calling dataStrategy client-side without having to\n  // pass around the manifest, mapRouteProperties, etc.\n  async function callDataStrategy(\n    request: Request,\n    matches: DataStrategyMatch[],\n    scopedContext: RouterContextProvider,\n    fetcherKey: string | null,\n  ): Promise<Record<string, DataResult>> {\n    let results: Record<string, DataStrategyResult>;\n    let dataResults: Record<string, DataResult> = {};\n    try {\n      results = await callDataStrategyImpl(\n        dataStrategyImpl as DataStrategyFunction<unknown>,\n        request,\n        matches,\n        fetcherKey,\n        scopedContext,\n        false,\n      );\n    } catch (e) {\n      // If the outer dataStrategy method throws, just return the error for all\n      // matches - and it'll naturally bubble to the root\n      matches\n        .filter((m) => m.shouldLoad)\n        .forEach((m) => {\n          dataResults[m.route.id] = {\n            type: ResultType.error,\n            error: e,\n          };\n        });\n      return dataResults;\n    }\n\n    if (request.signal.aborted) {\n      return dataResults;\n    }\n\n    // Stub errors where needed if they skipped returning data for routes\n    // above a route which returned an error.  This is a problem if we don't\n    // already have data (or errors) for the ancestor route because we won't be\n    // able to render through that route in order to get down to the actual error\n    // returned on the descendant route\n    if (!isMutationMethod(request.method)) {\n      for (let match of matches) {\n        if (results[match.route.id]?.type === ResultType.error) {\n          break;\n        }\n        if (\n          !results.hasOwnProperty(match.route.id) &&\n          !state.loaderData.hasOwnProperty(match.route.id) &&\n          (!state.errors || !state.errors.hasOwnProperty(match.route.id)) &&\n          match.shouldCallHandler()\n        ) {\n          results[match.route.id] = {\n            type: ResultType.error,\n            result: new Error(\n              `No result returned from dataStrategy for route ${match.route.id}`,\n            ),\n          };\n        }\n      }\n    }\n\n    for (let [routeId, result] of Object.entries(results)) {\n      if (isRedirectDataStrategyResult(result)) {\n        let response = result.result as Response;\n        dataResults[routeId] = {\n          type: ResultType.redirect,\n          response: normalizeRelativeRoutingRedirectResponse(\n            response,\n            request,\n            routeId,\n            matches,\n            basename,\n          ),\n        };\n      } else {\n        dataResults[routeId] =\n          await convertDataStrategyResultToDataResult(result);\n      }\n    }\n\n    return dataResults;\n  }\n\n  async function callLoadersAndMaybeResolveData(\n    matches: DataStrategyMatch[],\n    fetchersToLoad: RevalidatingFetcher[],\n    request: Request,\n    scopedContext: RouterContextProvider,\n  ) {\n    // Kick off loaders and fetchers in parallel\n    let loaderResultsPromise = callDataStrategy(\n      request,\n      matches,\n      scopedContext,\n      null,\n    );\n\n    let fetcherResultsPromise = Promise.all(\n      fetchersToLoad.map(async (f) => {\n        if (f.matches && f.match && f.request && f.controller) {\n          let results = await callDataStrategy(\n            f.request,\n            f.matches,\n            scopedContext,\n            f.key,\n          );\n          let result = results[f.match.route.id];\n          // Fetcher results are keyed by fetcher key from here on out, not routeId\n          return { [f.key]: result };\n        } else {\n          return Promise.resolve({\n            [f.key]: {\n              type: ResultType.error,\n              error: getInternalRouterError(404, {\n                pathname: f.path,\n              }),\n            } as ErrorResult,\n          });\n        }\n      }),\n    );\n\n    let loaderResults = await loaderResultsPromise;\n    let fetcherResults = (await fetcherResultsPromise).reduce(\n      (acc, r) => Object.assign(acc, r),\n      {},\n    );\n\n    return {\n      loaderResults,\n      fetcherResults,\n    };\n  }\n\n  function interruptActiveLoads() {\n    // Every interruption triggers a revalidation\n    isRevalidationRequired = true;\n\n    // Abort in-flight fetcher loads\n    fetchLoadMatches.forEach((_, key) => {\n      if (fetchControllers.has(key)) {\n        cancelledFetcherLoads.add(key);\n      }\n      abortFetcher(key);\n    });\n  }\n\n  function updateFetcherState(\n    key: string,\n    fetcher: Fetcher,\n    opts: { flushSync?: boolean } = {},\n  ) {\n    state.fetchers.set(key, fetcher);\n    updateState(\n      { fetchers: new Map(state.fetchers) },\n      { flushSync: (opts && opts.flushSync) === true },\n    );\n  }\n\n  function setFetcherError(\n    key: string,\n    routeId: string,\n    error: any,\n    opts: { flushSync?: boolean } = {},\n  ) {\n    let boundaryMatch = findNearestBoundary(state.matches, routeId);\n    deleteFetcher(key);\n    updateState(\n      {\n        errors: {\n          [boundaryMatch.route.id]: error,\n        },\n        fetchers: new Map(state.fetchers),\n      },\n      { flushSync: (opts && opts.flushSync) === true },\n    );\n  }\n\n  function getFetcher<TData = any>(key: string): Fetcher<TData> {\n    activeFetchers.set(key, (activeFetchers.get(key) || 0) + 1);\n    // If this fetcher was previously marked for deletion, unmark it since we\n    // have a new instance\n    if (fetchersQueuedForDeletion.has(key)) {\n      fetchersQueuedForDeletion.delete(key);\n    }\n    return state.fetchers.get(key) || IDLE_FETCHER;\n  }\n\n  function resetFetcher(key: string, opts?: { reason?: unknown }) {\n    abortFetcher(key, opts?.reason);\n    updateFetcherState(key, getDoneFetcher(null));\n  }\n\n  function deleteFetcher(key: string): void {\n    let fetcher = state.fetchers.get(key);\n    // Don't abort the controller if this is a deletion of a fetcher.submit()\n    // in it's loading phase since - we don't want to abort the corresponding\n    // revalidation and want them to complete and land\n    if (\n      fetchControllers.has(key) &&\n      !(fetcher && fetcher.state === \"loading\" && fetchReloadIds.has(key))\n    ) {\n      abortFetcher(key);\n    }\n    fetchLoadMatches.delete(key);\n    fetchReloadIds.delete(key);\n    fetchRedirectIds.delete(key);\n    fetchersQueuedForDeletion.delete(key);\n    cancelledFetcherLoads.delete(key);\n    state.fetchers.delete(key);\n  }\n\n  function queueFetcherForDeletion(key: string): void {\n    let count = (activeFetchers.get(key) || 0) - 1;\n    if (count <= 0) {\n      activeFetchers.delete(key);\n      fetchersQueuedForDeletion.add(key);\n    } else {\n      activeFetchers.set(key, count);\n    }\n    updateState({ fetchers: new Map(state.fetchers) });\n  }\n\n  function abortFetcher(key: string, reason?: unknown) {\n    let controller = fetchControllers.get(key);\n    if (controller) {\n      controller.abort(reason);\n      fetchControllers.delete(key);\n    }\n  }\n\n  function markFetchersDone(keys: string[]) {\n    for (let key of keys) {\n      let fetcher = getFetcher(key);\n      let doneFetcher = getDoneFetcher(fetcher.data);\n      state.fetchers.set(key, doneFetcher);\n    }\n  }\n\n  function markFetchRedirectsDone(): boolean {\n    let doneKeys = [];\n    let updatedFetchers = false;\n    for (let key of fetchRedirectIds) {\n      let fetcher = state.fetchers.get(key);\n      invariant(fetcher, `Expected fetcher: ${key}`);\n      if (fetcher.state === \"loading\") {\n        fetchRedirectIds.delete(key);\n        doneKeys.push(key);\n        updatedFetchers = true;\n      }\n    }\n    markFetchersDone(doneKeys);\n    return updatedFetchers;\n  }\n\n  function abortStaleFetchLoads(landedId: number): boolean {\n    let yeetedKeys = [];\n    for (let [key, id] of fetchReloadIds) {\n      if (id < landedId) {\n        let fetcher = state.fetchers.get(key);\n        invariant(fetcher, `Expected fetcher: ${key}`);\n        if (fetcher.state === \"loading\") {\n          abortFetcher(key);\n          fetchReloadIds.delete(key);\n          yeetedKeys.push(key);\n        }\n      }\n    }\n    markFetchersDone(yeetedKeys);\n    return yeetedKeys.length > 0;\n  }\n\n  function getBlocker(key: string, fn: BlockerFunction) {\n    let blocker: Blocker = state.blockers.get(key) || IDLE_BLOCKER;\n\n    if (blockerFunctions.get(key) !== fn) {\n      blockerFunctions.set(key, fn);\n    }\n\n    return blocker;\n  }\n\n  function deleteBlocker(key: string) {\n    state.blockers.delete(key);\n    blockerFunctions.delete(key);\n  }\n\n  // Utility function to update blockers, ensuring valid state transitions\n  function updateBlocker(key: string, newBlocker: Blocker) {\n    let blocker = state.blockers.get(key) || IDLE_BLOCKER;\n\n    // Poor mans state machine :)\n    // https://mermaid.live/edit#pako:eNqVkc9OwzAMxl8l8nnjAYrEtDIOHEBIgwvKJTReGy3_lDpIqO27k6awMG0XcrLlnz87nwdonESogKXXBuE79rq75XZO3-yHds0RJVuv70YrPlUrCEe2HfrORS3rubqZfuhtpg5C9wk5tZ4VKcRUq88q9Z8RS0-48cE1iHJkL0ugbHuFLus9L6spZy8nX9MP2CNdomVaposqu3fGayT8T8-jJQwhepo_UtpgBQaDEUom04dZhAN1aJBDlUKJBxE1ceB2Smj0Mln-IBW5AFU2dwUiktt_2Qaq2dBfaKdEup85UV7Yd-dKjlnkabl2Pvr0DTkTreM\n    invariant(\n      (blocker.state === \"unblocked\" && newBlocker.state === \"blocked\") ||\n        (blocker.state === \"blocked\" && newBlocker.state === \"blocked\") ||\n        (blocker.state === \"blocked\" && newBlocker.state === \"proceeding\") ||\n        (blocker.state === \"blocked\" && newBlocker.state === \"unblocked\") ||\n        (blocker.state === \"proceeding\" && newBlocker.state === \"unblocked\"),\n      `Invalid blocker state transition: ${blocker.state} -> ${newBlocker.state}`,\n    );\n\n    let blockers = new Map(state.blockers);\n    blockers.set(key, newBlocker);\n    updateState({ blockers });\n  }\n\n  function shouldBlockNavigation({\n    currentLocation,\n    nextLocation,\n    historyAction,\n  }: {\n    currentLocation: Location;\n    nextLocation: Location;\n    historyAction: NavigationType;\n  }): string | undefined {\n    if (blockerFunctions.size === 0) {\n      return;\n    }\n\n    // We ony support a single active blocker at the moment since we don't have\n    // any compelling use cases for multi-blocker yet\n    if (blockerFunctions.size > 1) {\n      warning(false, \"A router only supports one blocker at a time\");\n    }\n\n    let entries = Array.from(blockerFunctions.entries());\n    let [blockerKey, blockerFunction] = entries[entries.length - 1];\n    let blocker = state.blockers.get(blockerKey);\n\n    if (blocker && blocker.state === \"proceeding\") {\n      // If the blocker is currently proceeding, we don't need to re-check\n      // it and can let this navigation continue\n      return;\n    }\n\n    // At this point, we know we're unblocked/blocked so we need to check the\n    // user-provided blocker function\n    if (blockerFunction({ currentLocation, nextLocation, historyAction })) {\n      return blockerKey;\n    }\n  }\n\n  function handleNavigational404(pathname: string) {\n    let error = getInternalRouterError(404, { pathname });\n    let routesToUse = inFlightDataRoutes || dataRoutes;\n    let { matches, route } = getShortCircuitMatches(routesToUse);\n\n    return { notFoundMatches: matches, route, error };\n  }\n\n  // Opt in to capturing and reporting scroll positions during navigations,\n  // used by the <ScrollRestoration> component\n  function enableScrollRestoration(\n    positions: Record<string, number>,\n    getPosition: GetScrollPositionFunction,\n    getKey?: GetScrollRestorationKeyFunction,\n  ) {\n    savedScrollPositions = positions;\n    getScrollPosition = getPosition;\n    getScrollRestorationKey = getKey || null;\n\n    // Perform initial hydration scroll restoration, since we miss the boat on\n    // the initial updateState() because we've not yet rendered <ScrollRestoration/>\n    // and therefore have no savedScrollPositions available\n    if (!initialScrollRestored && state.navigation === IDLE_NAVIGATION) {\n      initialScrollRestored = true;\n      let y = getSavedScrollPosition(state.location, state.matches);\n      if (y != null) {\n        updateState({ restoreScrollPosition: y });\n      }\n    }\n\n    return () => {\n      savedScrollPositions = null;\n      getScrollPosition = null;\n      getScrollRestorationKey = null;\n    };\n  }\n\n  function getScrollKey(location: Location, matches: AgnosticDataRouteMatch[]) {\n    if (getScrollRestorationKey) {\n      let key = getScrollRestorationKey(\n        location,\n        matches.map((m) => convertRouteMatchToUiMatch(m, state.loaderData)),\n      );\n      return key || location.key;\n    }\n    return location.key;\n  }\n\n  function saveScrollPosition(\n    location: Location,\n    matches: AgnosticDataRouteMatch[],\n  ): void {\n    if (savedScrollPositions && getScrollPosition) {\n      let key = getScrollKey(location, matches);\n      savedScrollPositions[key] = getScrollPosition();\n    }\n  }\n\n  function getSavedScrollPosition(\n    location: Location,\n    matches: AgnosticDataRouteMatch[],\n  ): number | null {\n    if (savedScrollPositions) {\n      let key = getScrollKey(location, matches);\n      let y = savedScrollPositions[key];\n      if (typeof y === \"number\") {\n        return y;\n      }\n    }\n    return null;\n  }\n\n  function checkFogOfWar(\n    matches: AgnosticDataRouteMatch[] | null,\n    routesToUse: AgnosticDataRouteObject[],\n    pathname: string,\n  ): { active: boolean; matches: AgnosticDataRouteMatch[] | null } {\n    if (init.patchRoutesOnNavigation) {\n      if (!matches) {\n        let fogMatches = matchRoutesImpl<AgnosticDataRouteObject>(\n          routesToUse,\n          pathname,\n          basename,\n          true,\n        );\n\n        return { active: true, matches: fogMatches || [] };\n      } else {\n        if (Object.keys(matches[0].params).length > 0) {\n          // If we matched a dynamic param or a splat, it might only be because\n          // we haven't yet discovered other routes that would match with a\n          // higher score.  Call patchRoutesOnNavigation just to be sure\n          let partialMatches = matchRoutesImpl<AgnosticDataRouteObject>(\n            routesToUse,\n            pathname,\n            basename,\n            true,\n          );\n          return { active: true, matches: partialMatches };\n        }\n      }\n    }\n\n    return { active: false, matches: null };\n  }\n\n  type DiscoverRoutesSuccessResult = {\n    type: \"success\";\n    matches: AgnosticDataRouteMatch[] | null;\n  };\n  type DiscoverRoutesErrorResult = {\n    type: \"error\";\n    error: any;\n    partialMatches: AgnosticDataRouteMatch[];\n  };\n  type DiscoverRoutesAbortedResult = { type: \"aborted\" };\n  type DiscoverRoutesResult =\n    | DiscoverRoutesSuccessResult\n    | DiscoverRoutesErrorResult\n    | DiscoverRoutesAbortedResult;\n\n  async function discoverRoutes(\n    matches: AgnosticDataRouteMatch[],\n    pathname: string,\n    signal: AbortSignal,\n    fetcherKey?: string,\n  ): Promise<DiscoverRoutesResult> {\n    if (!init.patchRoutesOnNavigation) {\n      return { type: \"success\", matches };\n    }\n\n    let partialMatches: AgnosticDataRouteMatch[] | null = matches;\n    while (true) {\n      let isNonHMR = inFlightDataRoutes == null;\n      let routesToUse = inFlightDataRoutes || dataRoutes;\n      let localManifest = manifest;\n      try {\n        await init.patchRoutesOnNavigation({\n          signal,\n          path: pathname,\n          matches: partialMatches,\n          fetcherKey,\n          patch: (routeId, children) => {\n            if (signal.aborted) return;\n            patchRoutesImpl(\n              routeId,\n              children,\n              routesToUse,\n              localManifest,\n              mapRouteProperties,\n              false,\n            );\n          },\n        });\n      } catch (e) {\n        return { type: \"error\", error: e, partialMatches };\n      } finally {\n        // If we are not in the middle of an HMR revalidation and we changed the\n        // routes, provide a new identity so when we `updateState` at the end of\n        // this navigation/fetch `router.routes` will be a new identity and\n        // trigger a re-run of memoized `router.routes` dependencies.\n        // HMR will already update the identity and reflow when it lands\n        // `inFlightDataRoutes` in `completeNavigation`\n        if (isNonHMR && !signal.aborted) {\n          dataRoutes = [...dataRoutes];\n        }\n      }\n\n      if (signal.aborted) {\n        return { type: \"aborted\" };\n      }\n\n      let newMatches = matchRoutes(routesToUse, pathname, basename);\n      let newPartialMatches: AgnosticDataRouteMatch[] | null = null;\n\n      if (newMatches) {\n        if (Object.keys(newMatches[0].params).length === 0) {\n          // Static match - use it\n          return { type: \"success\", matches: newMatches };\n        } else {\n          // Dynamic match - confirm this is the best match.\n          newPartialMatches = matchRoutesImpl(\n            routesToUse,\n            pathname,\n            basename,\n            true,\n          );\n\n          // If we matched deeper into the same branch of `partialMatches` we were already\n          // checking, we want to make another pass through `patchRoutesOnNavigation()`\n          let matchedDeeper =\n            newPartialMatches &&\n            partialMatches.length < newPartialMatches.length &&\n            compareMatches(\n              partialMatches,\n              newPartialMatches.slice(0, partialMatches.length),\n            );\n\n          if (!matchedDeeper) {\n            // Otherwise, use the dynamic matches\n            return { type: \"success\", matches: newMatches };\n          }\n        }\n      }\n\n      // Perform partial matching if we didn't already do it above\n      if (!newPartialMatches) {\n        newPartialMatches = matchRoutesImpl<AgnosticDataRouteObject>(\n          routesToUse,\n          pathname,\n          basename,\n          true,\n        );\n      }\n\n      // Avoid loops if the second pass results in the same partial matches\n      if (\n        !newPartialMatches ||\n        compareMatches(partialMatches, newPartialMatches)\n      ) {\n        return { type: \"success\", matches: null };\n      }\n\n      partialMatches = newPartialMatches;\n    }\n  }\n\n  function compareMatches(\n    a: AgnosticDataRouteMatch[],\n    b: AgnosticDataRouteMatch[],\n  ) {\n    return (\n      a.length === b.length && a.every((m, i) => m.route.id === b[i].route.id)\n    );\n  }\n\n  function _internalSetRoutes(newRoutes: AgnosticDataRouteObject[]) {\n    manifest = {};\n    inFlightDataRoutes = convertRoutesToDataRoutes(\n      newRoutes,\n      mapRouteProperties,\n      undefined,\n      manifest,\n    );\n  }\n\n  function patchRoutes(\n    routeId: string | null,\n    children: AgnosticRouteObject[],\n    unstable_allowElementMutations = false,\n  ): void {\n    let isNonHMR = inFlightDataRoutes == null;\n    let routesToUse = inFlightDataRoutes || dataRoutes;\n    patchRoutesImpl(\n      routeId,\n      children,\n      routesToUse,\n      manifest,\n      mapRouteProperties,\n      unstable_allowElementMutations,\n    );\n\n    // If we are not in the middle of an HMR revalidation and we changed the\n    // routes, provide a new identity and trigger a reflow via `updateState`\n    // to re-run memoized `router.routes` dependencies.\n    // HMR will already update the identity and reflow when it lands\n    // `inFlightDataRoutes` in `completeNavigation`\n    if (isNonHMR) {\n      dataRoutes = [...dataRoutes];\n      updateState({});\n    }\n  }\n\n  router = {\n    get basename() {\n      return basename;\n    },\n    get future() {\n      return future;\n    },\n    get state() {\n      return state;\n    },\n    get routes() {\n      return dataRoutes;\n    },\n    get window() {\n      return routerWindow;\n    },\n    initialize,\n    subscribe,\n    enableScrollRestoration,\n    navigate,\n    fetch,\n    revalidate,\n    // Passthrough to history-aware createHref used by useHref so we get proper\n    // hash-aware URLs in DOM paths\n    createHref: (to: To) => init.history.createHref(to),\n    encodeLocation: (to: To) => init.history.encodeLocation(to),\n    getFetcher,\n    resetFetcher,\n    deleteFetcher: queueFetcherForDeletion,\n    dispose,\n    getBlocker,\n    deleteBlocker,\n    patchRoutes,\n    _internalFetchControllers: fetchControllers,\n    // TODO: Remove setRoutes, it's temporary to avoid dealing with\n    // updating the tree while validating the update algorithm.\n    _internalSetRoutes,\n    _internalSetStateDoNotUseOrYouWillBreakYourApp(newState) {\n      updateState(newState);\n    },\n  };\n\n  if (init.unstable_instrumentations) {\n    router = instrumentClientSideRouter(\n      router,\n      init.unstable_instrumentations\n        .map((i) => i.router)\n        .filter(Boolean) as unstable_InstrumentRouterFunction[],\n    );\n  }\n\n  return router;\n}\n//#endregion\n\n////////////////////////////////////////////////////////////////////////////////\n//#region createStaticHandler\n////////////////////////////////////////////////////////////////////////////////\n\nexport interface CreateStaticHandlerOptions {\n  basename?: string;\n  mapRouteProperties?: MapRoutePropertiesFunction;\n  unstable_instrumentations?: Pick<unstable_ServerInstrumentation, \"route\">[];\n  future?: {};\n}\n\nexport function createStaticHandler(\n  routes: AgnosticRouteObject[],\n  opts?: CreateStaticHandlerOptions,\n): StaticHandler {\n  invariant(\n    routes.length > 0,\n    \"You must provide a non-empty routes array to createStaticHandler\",\n  );\n\n  let manifest: RouteManifest = {};\n  let basename = (opts ? opts.basename : null) || \"/\";\n  let _mapRouteProperties =\n    opts?.mapRouteProperties || defaultMapRouteProperties;\n  let mapRouteProperties = _mapRouteProperties;\n\n  // Leverage the existing mapRouteProperties logic to execute instrumentRoute\n  // (if it exists) on all routes in the application\n  if (opts?.unstable_instrumentations) {\n    let instrumentations = opts.unstable_instrumentations;\n\n    mapRouteProperties = (route: AgnosticDataRouteObject) => {\n      return {\n        ..._mapRouteProperties(route),\n        ...getRouteInstrumentationUpdates(\n          instrumentations\n            .map((i) => i.route)\n            .filter(Boolean) as unstable_InstrumentRouteFunction[],\n          route,\n        ),\n      };\n    };\n  }\n\n  let dataRoutes = convertRoutesToDataRoutes(\n    routes,\n    mapRouteProperties,\n    undefined,\n    manifest,\n  );\n\n  /**\n   * The query() method is intended for document requests, in which we want to\n   * call an optional action and potentially multiple loaders for all nested\n   * routes.  It returns a StaticHandlerContext object, which is very similar\n   * to the router state (location, loaderData, actionData, errors, etc.) and\n   * also adds SSR-specific information such as the statusCode and headers\n   * from action/loaders Responses.\n   *\n   * It _should_ never throw and should report all errors through the\n   * returned handlerContext.errors object, properly associating errors to\n   * their error boundary.  Additionally, it tracks _deepestRenderedBoundaryId\n   * which can be used to emulate React error boundaries during SSR by performing\n   * a second pass only down to the boundaryId.\n   *\n   * The one exception where we do not return a StaticHandlerContext is when a\n   * redirect response is returned or thrown from any action/loader.  We\n   * propagate that out and return the raw Response so the HTTP server can\n   * return it directly.\n   *\n   * - `opts.requestContext` is an optional server context that will be passed\n   *   to actions/loaders in the `context` parameter\n   * - `opts.skipLoaderErrorBubbling` is an optional parameter that will prevent\n   *   the bubbling of errors which allows single-fetch-type implementations\n   *   where the client will handle the bubbling and we may need to return data\n   *   for the handling route\n   */\n  async function query(\n    request: Parameters<StaticHandler[\"query\"]>[0],\n    {\n      requestContext,\n      filterMatchesToLoad,\n      skipLoaderErrorBubbling,\n      skipRevalidation,\n      dataStrategy,\n      generateMiddlewareResponse,\n    }: Parameters<StaticHandler[\"query\"]>[1] = {},\n  ): Promise<StaticHandlerContext | Response> {\n    let url = new URL(request.url);\n    let method = request.method;\n    let location = createLocation(\"\", createPath(url), null, \"default\");\n    let matches = matchRoutes(dataRoutes, location, basename);\n    requestContext =\n      requestContext != null ? requestContext : new RouterContextProvider();\n\n    // SSR supports HEAD requests while SPA doesn't\n    if (!isValidMethod(method) && method !== \"HEAD\") {\n      let error = getInternalRouterError(405, { method });\n      let { matches: methodNotAllowedMatches, route } =\n        getShortCircuitMatches(dataRoutes);\n      let staticContext: StaticHandlerContext = {\n        basename,\n        location,\n        matches: methodNotAllowedMatches,\n        loaderData: {},\n        actionData: null,\n        errors: {\n          [route.id]: error,\n        },\n        statusCode: error.status,\n        loaderHeaders: {},\n        actionHeaders: {},\n      };\n      return generateMiddlewareResponse\n        ? generateMiddlewareResponse(() => Promise.resolve(staticContext))\n        : staticContext;\n    } else if (!matches) {\n      let error = getInternalRouterError(404, { pathname: location.pathname });\n      let { matches: notFoundMatches, route } =\n        getShortCircuitMatches(dataRoutes);\n      let staticContext: StaticHandlerContext = {\n        basename,\n        location,\n        matches: notFoundMatches,\n        loaderData: {},\n        actionData: null,\n        errors: {\n          [route.id]: error,\n        },\n        statusCode: error.status,\n        loaderHeaders: {},\n        actionHeaders: {},\n      };\n      return generateMiddlewareResponse\n        ? generateMiddlewareResponse(() => Promise.resolve(staticContext))\n        : staticContext;\n    }\n\n    // The `stream` API is what enables middleware execution and makes 3 changes\n    // from the original `respond` API:\n    //  - It _always_ runs the middleware pipeline down to the `stream()`\n    //    function even if middleware doesn't exist\n    //  - We can now insert arbitrary logic at the end of the middleware\n    //    chain (inside the leaf `next()` call) such as running a server\n    //    action and let route middlewares apply to that action.\n    //  - We have control over the calling/awaiting of query() which\n    //    theoretically would allow us to send down the `actionResult`\n    //    immediately and then stream the entirety of the `query`/`staticContext`\n    //    info.  That has other implications though which we may not want,\n    //    such as not being able to leverage `headers()`, or respect\n    //    redirects from loaders after a server action, etc.\n    //    - If we decide this isn't warranted than the stream implementation\n    //      can simplify and potentially even collapse back into respond()\n    if (generateMiddlewareResponse) {\n      invariant(\n        requestContext instanceof RouterContextProvider,\n        \"When using middleware in `staticHandler.query()`, any provided \" +\n          \"`requestContext` must be an instance of `RouterContextProvider`\",\n      );\n      try {\n        await loadLazyMiddlewareForMatches(\n          matches,\n          manifest,\n          mapRouteProperties,\n        );\n        let renderedStaticContext: StaticHandlerContext | undefined;\n        let response = await runServerMiddlewarePipeline(\n          {\n            request,\n            unstable_pattern: getRoutePattern(matches),\n            matches,\n            params: matches[0].params,\n            // If we're calling middleware then it must be enabled so we can cast\n            // this to the proper type knowing it's not an `AppLoadContext`\n            context: requestContext as RouterContextProvider,\n          },\n          async () => {\n            let res = await generateMiddlewareResponse(\n              async (\n                revalidationRequest: Request,\n                opts: {\n                  filterMatchesToLoad?:\n                    | ((match: AgnosticDataRouteMatch) => boolean)\n                    | undefined;\n                } = {},\n              ) => {\n                let result = await queryImpl(\n                  revalidationRequest,\n                  location,\n                  matches!,\n                  requestContext,\n                  dataStrategy || null,\n                  skipLoaderErrorBubbling === true,\n                  null,\n                  \"filterMatchesToLoad\" in opts\n                    ? (opts.filterMatchesToLoad ?? null)\n                    : (filterMatchesToLoad ?? null),\n                  skipRevalidation === true,\n                );\n\n                if (isResponse(result)) {\n                  return result;\n                }\n                // When returning StaticHandlerContext, we patch back in the location here\n                // since we need it for React Context.  But this helps keep our submit and\n                // loadRouteData operating on a Request instead of a Location\n                renderedStaticContext = { location, basename, ...result };\n                return renderedStaticContext;\n              },\n            );\n            return res;\n          },\n          async (error, routeId) => {\n            // Redirects propagate verbatim\n            if (isRedirectResponse(error)) {\n              return error;\n            }\n\n            // All other thrown responses during document/data request bubble\n            // to the boundary\n            if (isResponse(error)) {\n              try {\n                error = new ErrorResponseImpl(\n                  error.status,\n                  error.statusText,\n                  await parseResponseBody(error),\n                );\n              } catch (e) {\n                error = e;\n              }\n            }\n\n            if (isDataWithResponseInit(error)) {\n              // Convert thrown data() values to ErrorResponses for the UI\n              error = dataWithResponseInitToErrorResponse(error);\n            }\n\n            if (renderedStaticContext) {\n              // We rendered an HTML response and caught an error going back up\n              // the middleware chain - render again with an updated context\n\n              // If we had loaderData for the route that threw, clear it out\n              // to align server/client behavior.  Client side middleware uses\n              // dataStrategy and a given route can only have one result, so the\n              // error overwrites any prior loader data.\n              if (routeId in renderedStaticContext.loaderData) {\n                renderedStaticContext.loaderData[routeId] = undefined;\n              }\n\n              let staticContext = getStaticContextFromError(\n                dataRoutes,\n                renderedStaticContext,\n                error,\n                skipLoaderErrorBubbling\n                  ? routeId\n                  : findNearestBoundary(matches, routeId).route.id,\n              );\n              return generateMiddlewareResponse(() =>\n                Promise.resolve(staticContext),\n              );\n            } else {\n              // We never even got to the handlers, so we've got no data -\n              // just create an empty context reflecting the error.\n\n              // Find the boundary at or above the source of the middleware\n              // error or the highest loader. We can't render any UI below\n              // the highest loader since we have no loader data available\n              let boundaryRouteId = skipLoaderErrorBubbling\n                ? routeId\n                : findNearestBoundary(\n                    matches,\n                    matches.find(\n                      (m) => m.route.id === routeId || m.route.loader,\n                    )?.route.id || routeId,\n                  ).route.id;\n\n              let staticContext: StaticHandlerContext = {\n                matches: matches!,\n                location,\n                basename,\n                loaderData: {},\n                actionData: null,\n                errors: {\n                  [boundaryRouteId]: error,\n                },\n                statusCode: isRouteErrorResponse(error) ? error.status : 500,\n                actionHeaders: {},\n                loaderHeaders: {},\n              };\n              return generateMiddlewareResponse(() =>\n                Promise.resolve(staticContext),\n              );\n            }\n          },\n        );\n\n        invariant(isResponse(response), \"Expected a response in query()\");\n        return response;\n      } catch (e) {\n        if (isResponse(e)) {\n          return e;\n        }\n        throw e;\n      }\n    }\n\n    let result = await queryImpl(\n      request,\n      location,\n      matches,\n      requestContext,\n      dataStrategy || null,\n      skipLoaderErrorBubbling === true,\n      null,\n      filterMatchesToLoad || null,\n      skipRevalidation === true,\n    );\n\n    if (isResponse(result)) {\n      return result;\n    }\n\n    // When returning StaticHandlerContext, we patch back in the location here\n    // since we need it for React Context.  But this helps keep our submit and\n    // loadRouteData operating on a Request instead of a Location\n    return { location, basename, ...result };\n  }\n\n  /**\n   * The queryRoute() method is intended for targeted route requests, either\n   * for fetch ?_data requests or resource route requests.  In this case, we\n   * are only ever calling a single action or loader, and we are returning the\n   * returned value directly.  In most cases, this will be a Response returned\n   * from the action/loader, but it may be a primitive or other value as well -\n   * and in such cases the calling context should handle that accordingly.\n   *\n   * We do respect the throw/return differentiation, so if an action/loader\n   * throws, then this method will throw the value.  This is important so we\n   * can do proper boundary identification in Remix where a thrown Response\n   * must go to the Catch Boundary but a returned Response is happy-path.\n   *\n   * One thing to note is that any Router-initiated Errors that make sense\n   * to associate with a status code will be thrown as an ErrorResponse\n   * instance which include the raw Error, such that the calling context can\n   * serialize the error as they see fit while including the proper response\n   * code.  Examples here are 404 and 405 errors that occur prior to reaching\n   * any user-defined loaders.\n   *\n   * - `opts.routeId` allows you to specify the specific route handler to call.\n   *   If not provided the handler will determine the proper route by matching\n   *   against `request.url`\n   * - `opts.requestContext` is an optional server context that will be passed\n   *    to actions/loaders in the `context` parameter\n   */\n  async function queryRoute(\n    request: Parameters<StaticHandler[\"queryRoute\"]>[0],\n    {\n      routeId,\n      requestContext,\n      dataStrategy,\n      generateMiddlewareResponse,\n    }: Parameters<StaticHandler[\"queryRoute\"]>[1] = {},\n  ): Promise<any> {\n    let url = new URL(request.url);\n    let method = request.method;\n    let location = createLocation(\"\", createPath(url), null, \"default\");\n    let matches = matchRoutes(dataRoutes, location, basename);\n    requestContext =\n      requestContext != null ? requestContext : new RouterContextProvider();\n\n    // SSR supports HEAD requests while SPA doesn't\n    if (!isValidMethod(method) && method !== \"HEAD\" && method !== \"OPTIONS\") {\n      throw getInternalRouterError(405, { method });\n    } else if (!matches) {\n      throw getInternalRouterError(404, { pathname: location.pathname });\n    }\n\n    let match = routeId\n      ? matches.find((m) => m.route.id === routeId)\n      : getTargetMatch(matches, location);\n\n    if (routeId && !match) {\n      throw getInternalRouterError(403, {\n        pathname: location.pathname,\n        routeId,\n      });\n    } else if (!match) {\n      // This should never hit I don't think?\n      throw getInternalRouterError(404, { pathname: location.pathname });\n    }\n\n    if (generateMiddlewareResponse) {\n      invariant(\n        requestContext instanceof RouterContextProvider,\n        \"When using middleware in `staticHandler.queryRoute()`, any provided \" +\n          \"`requestContext` must be an instance of `RouterContextProvider`\",\n      );\n      await loadLazyMiddlewareForMatches(matches, manifest, mapRouteProperties);\n      let response = await runServerMiddlewarePipeline(\n        {\n          request,\n          unstable_pattern: getRoutePattern(matches),\n          matches,\n          params: matches[0].params,\n          // If we're calling middleware then it must be enabled so we can cast\n          // this to the proper type knowing it's not an `AppLoadContext`\n          context: requestContext as RouterContextProvider,\n        },\n        async () => {\n          let res = await generateMiddlewareResponse(\n            async (innerRequest: Request) => {\n              let result = await queryImpl(\n                innerRequest,\n                location,\n                matches!,\n                requestContext,\n                dataStrategy || null,\n                false,\n                match!,\n                null,\n                false,\n              );\n\n              let processed = handleQueryResult(result);\n\n              return isResponse(processed)\n                ? processed\n                : typeof processed === \"string\"\n                  ? new Response(processed)\n                  : Response.json(processed);\n            },\n          );\n          return res;\n        },\n        (error) => {\n          if (isDataWithResponseInit(error)) {\n            // Convert thrown data() values to Responses for resource routes\n            return Promise.resolve(dataWithResponseInitToResponse(error));\n          }\n          if (isResponse(error)) {\n            return Promise.resolve(error);\n          }\n          throw error;\n        },\n      );\n      return response;\n    }\n\n    let result = await queryImpl(\n      request,\n      location,\n      matches,\n      requestContext,\n      dataStrategy || null,\n      false,\n      match,\n      null,\n      false,\n    );\n\n    return handleQueryResult(result);\n\n    function handleQueryResult(result: Awaited<ReturnType<typeof queryImpl>>) {\n      if (isResponse(result)) {\n        return result;\n      }\n\n      let error = result.errors ? Object.values(result.errors)[0] : undefined;\n      if (error !== undefined) {\n        // If we got back result.errors, that means the loader/action threw\n        // _something_ that wasn't a Response, but it's not guaranteed/required\n        // to be an `instanceof Error` either, so we have to use throw here to\n        // preserve the \"error\" state outside of queryImpl.\n        throw error;\n      }\n\n      // Pick off the right state value to return\n      if (result.actionData) {\n        return Object.values(result.actionData)[0];\n      }\n\n      if (result.loaderData) {\n        return Object.values(result.loaderData)[0];\n      }\n\n      return undefined;\n    }\n  }\n\n  async function queryImpl(\n    request: Request,\n    location: Location,\n    matches: AgnosticDataRouteMatch[],\n    requestContext: unknown,\n    dataStrategy: DataStrategyFunction<unknown> | null,\n    skipLoaderErrorBubbling: boolean,\n    routeMatch: AgnosticDataRouteMatch | null,\n    filterMatchesToLoad: ((m: AgnosticDataRouteMatch) => boolean) | null,\n    skipRevalidation: boolean,\n  ): Promise<Omit<StaticHandlerContext, \"location\" | \"basename\"> | Response> {\n    invariant(\n      request.signal,\n      \"query()/queryRoute() requests must contain an AbortController signal\",\n    );\n\n    try {\n      if (isMutationMethod(request.method)) {\n        let result = await submit(\n          request,\n          matches,\n          routeMatch || getTargetMatch(matches, location),\n          requestContext,\n          dataStrategy,\n          skipLoaderErrorBubbling,\n          routeMatch != null,\n          filterMatchesToLoad,\n          skipRevalidation,\n        );\n        return result;\n      }\n\n      let result = await loadRouteData(\n        request,\n        matches,\n        requestContext,\n        dataStrategy,\n        skipLoaderErrorBubbling,\n        routeMatch,\n        filterMatchesToLoad,\n      );\n      return isResponse(result)\n        ? result\n        : {\n            ...result,\n            actionData: null,\n            actionHeaders: {},\n          };\n    } catch (e) {\n      // If the user threw/returned a Response in callLoaderOrAction for a\n      // `queryRoute` call, we throw the `DataStrategyResult` to bail out early\n      // and then return or throw the raw Response here accordingly\n      if (isDataStrategyResult(e) && isResponse(e.result)) {\n        if (e.type === ResultType.error) {\n          throw e.result;\n        }\n        return e.result;\n      }\n      // Redirects are always returned since they don't propagate to catch\n      // boundaries\n      if (isRedirectResponse(e)) {\n        return e;\n      }\n      throw e;\n    }\n  }\n\n  async function submit(\n    request: Request,\n    matches: AgnosticDataRouteMatch[],\n    actionMatch: AgnosticDataRouteMatch,\n    requestContext: unknown,\n    dataStrategy: DataStrategyFunction<unknown> | null,\n    skipLoaderErrorBubbling: boolean,\n    isRouteRequest: boolean,\n    filterMatchesToLoad: ((m: AgnosticDataRouteMatch) => boolean) | null,\n    skipRevalidation: boolean,\n  ): Promise<Omit<StaticHandlerContext, \"location\" | \"basename\"> | Response> {\n    let result: DataResult;\n\n    if (!actionMatch.route.action && !actionMatch.route.lazy) {\n      let error = getInternalRouterError(405, {\n        method: request.method,\n        pathname: new URL(request.url).pathname,\n        routeId: actionMatch.route.id,\n      });\n      if (isRouteRequest) {\n        throw error;\n      }\n      result = {\n        type: ResultType.error,\n        error,\n      };\n    } else {\n      let dsMatches = getTargetedDataStrategyMatches(\n        mapRouteProperties,\n        manifest,\n        request,\n        matches,\n        actionMatch,\n        [],\n        requestContext,\n      );\n\n      let results = await callDataStrategy(\n        request,\n        dsMatches,\n        isRouteRequest,\n        requestContext,\n        dataStrategy,\n      );\n      result = results[actionMatch.route.id];\n\n      if (request.signal.aborted) {\n        throwStaticHandlerAbortedError(request, isRouteRequest);\n      }\n    }\n\n    if (isRedirectResult(result)) {\n      // Uhhhh - this should never happen, we should always throw these from\n      // callLoaderOrAction, but the type narrowing here keeps TS happy and we\n      // can get back on the \"throw all redirect responses\" train here should\n      // this ever happen :/\n      throw new Response(null, {\n        status: result.response.status,\n        headers: {\n          Location: result.response.headers.get(\"Location\")!,\n        },\n      });\n    }\n\n    if (isRouteRequest) {\n      // Note: This should only be non-Response values if we get here, since\n      // isRouteRequest should throw any Response received in callLoaderOrAction\n      if (isErrorResult(result)) {\n        throw result.error;\n      }\n\n      return {\n        matches: [actionMatch],\n        loaderData: {},\n        actionData: { [actionMatch.route.id]: result.data },\n        errors: null,\n        // Note: statusCode + headers are unused here since queryRoute will\n        // return the raw Response or value\n        statusCode: 200,\n        loaderHeaders: {},\n        actionHeaders: {},\n      };\n    }\n\n    if (skipRevalidation) {\n      if (isErrorResult(result)) {\n        let boundaryMatch = skipLoaderErrorBubbling\n          ? actionMatch\n          : findNearestBoundary(matches, actionMatch.route.id);\n\n        return {\n          statusCode: isRouteErrorResponse(result.error)\n            ? result.error.status\n            : result.statusCode != null\n              ? result.statusCode\n              : 500,\n          actionData: null,\n          actionHeaders: {\n            ...(result.headers\n              ? { [actionMatch.route.id]: result.headers }\n              : {}),\n          },\n          matches,\n          loaderData: {},\n          errors: {\n            [boundaryMatch.route.id]: result.error,\n          },\n          loaderHeaders: {},\n        };\n      } else {\n        return {\n          actionData: {\n            [actionMatch.route.id]: result.data,\n          },\n          actionHeaders: result.headers\n            ? { [actionMatch.route.id]: result.headers }\n            : {},\n          matches,\n          loaderData: {},\n          errors: null,\n          statusCode: result.statusCode || 200,\n          loaderHeaders: {},\n        };\n      }\n    }\n\n    // Create a GET request for the loaders\n    let loaderRequest = new Request(request.url, {\n      headers: request.headers,\n      redirect: request.redirect,\n      signal: request.signal,\n    });\n\n    if (isErrorResult(result)) {\n      // Store off the pending error - we use it to determine which loaders\n      // to call and will commit it when we complete the navigation\n      let boundaryMatch = skipLoaderErrorBubbling\n        ? actionMatch\n        : findNearestBoundary(matches, actionMatch.route.id);\n\n      let handlerContext = await loadRouteData(\n        loaderRequest,\n        matches,\n        requestContext,\n        dataStrategy,\n        skipLoaderErrorBubbling,\n        null,\n        filterMatchesToLoad,\n        [boundaryMatch.route.id, result],\n      );\n\n      // action status codes take precedence over loader status codes\n      return {\n        ...handlerContext,\n        statusCode: isRouteErrorResponse(result.error)\n          ? result.error.status\n          : result.statusCode != null\n            ? result.statusCode\n            : 500,\n        actionData: null,\n        actionHeaders: {\n          ...(result.headers ? { [actionMatch.route.id]: result.headers } : {}),\n        },\n      };\n    }\n\n    let handlerContext = await loadRouteData(\n      loaderRequest,\n      matches,\n      requestContext,\n      dataStrategy,\n      skipLoaderErrorBubbling,\n      null,\n      filterMatchesToLoad,\n    );\n\n    return {\n      ...handlerContext,\n      actionData: {\n        [actionMatch.route.id]: result.data,\n      },\n      // action status codes take precedence over loader status codes\n      ...(result.statusCode ? { statusCode: result.statusCode } : {}),\n      actionHeaders: result.headers\n        ? { [actionMatch.route.id]: result.headers }\n        : {},\n    };\n  }\n\n  async function loadRouteData(\n    request: Request,\n    matches: AgnosticDataRouteMatch[],\n    requestContext: unknown,\n    dataStrategy: DataStrategyFunction<unknown> | null,\n    skipLoaderErrorBubbling: boolean,\n    routeMatch: AgnosticDataRouteMatch | null,\n    filterMatchesToLoad: ((match: AgnosticDataRouteMatch) => boolean) | null,\n    pendingActionResult?: PendingActionResult,\n  ): Promise<\n    | Omit<\n        StaticHandlerContext,\n        \"location\" | \"basename\" | \"actionData\" | \"actionHeaders\"\n      >\n    | Response\n  > {\n    let isRouteRequest = routeMatch != null;\n\n    // Short circuit if we have no loaders to run (queryRoute())\n    if (\n      isRouteRequest &&\n      !routeMatch?.route.loader &&\n      !routeMatch?.route.lazy\n    ) {\n      throw getInternalRouterError(400, {\n        method: request.method,\n        pathname: new URL(request.url).pathname,\n        routeId: routeMatch?.route.id,\n      });\n    }\n\n    let dsMatches: DataStrategyMatch[];\n    if (routeMatch) {\n      dsMatches = getTargetedDataStrategyMatches(\n        mapRouteProperties,\n        manifest,\n        request,\n        matches,\n        routeMatch,\n        [],\n        requestContext,\n      );\n    } else {\n      let maxIdx =\n        pendingActionResult && isErrorResult(pendingActionResult[1])\n          ? // Up to but not including the boundary\n            matches.findIndex((m) => m.route.id === pendingActionResult[0]) - 1\n          : undefined;\n\n      let pattern = getRoutePattern(matches);\n      dsMatches = matches.map((match, index) => {\n        if (maxIdx != null && index > maxIdx) {\n          return getDataStrategyMatch(\n            mapRouteProperties,\n            manifest,\n            request,\n            pattern,\n            match,\n            [],\n            requestContext,\n            false,\n          );\n        }\n\n        return getDataStrategyMatch(\n          mapRouteProperties,\n          manifest,\n          request,\n          pattern,\n          match,\n          [],\n          requestContext,\n          (match.route.loader || match.route.lazy) != null &&\n            (!filterMatchesToLoad || filterMatchesToLoad(match)),\n        );\n      });\n    }\n\n    // Short circuit if we have no loaders to run, unless there's a custom dataStrategy\n    // since they may have different revalidation rules (i.e., single fetch)\n    if (!dataStrategy && !dsMatches.some((m) => m.shouldLoad)) {\n      return {\n        matches,\n        loaderData: {},\n        errors:\n          pendingActionResult && isErrorResult(pendingActionResult[1])\n            ? {\n                [pendingActionResult[0]]: pendingActionResult[1].error,\n              }\n            : null,\n        statusCode: 200,\n        loaderHeaders: {},\n      };\n    }\n\n    let results = await callDataStrategy(\n      request,\n      dsMatches,\n      isRouteRequest,\n      requestContext,\n      dataStrategy,\n    );\n\n    if (request.signal.aborted) {\n      throwStaticHandlerAbortedError(request, isRouteRequest);\n    }\n\n    // Process and commit output from loaders\n    let handlerContext = processRouteLoaderData(\n      matches,\n      results,\n      pendingActionResult,\n      true,\n      skipLoaderErrorBubbling,\n    );\n\n    return {\n      ...handlerContext,\n      matches,\n    };\n  }\n\n  // Utility wrapper for calling dataStrategy server-side without having to\n  // pass around the manifest, mapRouteProperties, etc.\n  async function callDataStrategy(\n    request: Request,\n    matches: DataStrategyMatch[],\n    isRouteRequest: boolean,\n    requestContext: unknown,\n    dataStrategy: DataStrategyFunction<unknown> | null,\n  ): Promise<Record<string, DataResult>> {\n    let results = await callDataStrategyImpl(\n      dataStrategy || defaultDataStrategy,\n      request,\n      matches,\n      null,\n      requestContext,\n      true,\n    );\n\n    let dataResults: Record<string, DataResult> = {};\n    await Promise.all(\n      matches.map(async (match) => {\n        if (!(match.route.id in results)) {\n          return;\n        }\n        let result = results[match.route.id];\n        if (isRedirectDataStrategyResult(result)) {\n          let response = result.result as Response;\n          // Throw redirects and let the server handle them with an HTTP redirect\n          throw normalizeRelativeRoutingRedirectResponse(\n            response,\n            request,\n            match.route.id,\n            matches,\n            basename,\n          );\n        }\n\n        // For SSR single-route requests, we want to hand Responses back\n        // directly, as well as upgrade data() calls to Response instances\n        // (this allows utilities using data() and be shared between normal and\n        // resource routes).\n        if (isRouteRequest) {\n          if (isResponse(result.result)) {\n            throw result;\n          } else if (isDataWithResponseInit(result.result)) {\n            // Upgrade `data()` to `Response` so utilities using `data()` can be\n            // shared between resource and non-resource routes\n            throw dataWithResponseInitToResponse(result.result);\n          }\n        }\n\n        dataResults[match.route.id] =\n          await convertDataStrategyResultToDataResult(result);\n      }),\n    );\n    return dataResults;\n  }\n\n  return {\n    dataRoutes,\n    query,\n    queryRoute,\n  };\n}\n\n//#endregion\n\n////////////////////////////////////////////////////////////////////////////////\n//#region Helpers\n////////////////////////////////////////////////////////////////////////////////\n\n/**\n * Given an existing StaticHandlerContext and an error thrown at render time,\n * provide an updated StaticHandlerContext suitable for a second SSR render\n *\n * @category Utils\n */\nexport function getStaticContextFromError(\n  routes: AgnosticDataRouteObject[],\n  handlerContext: StaticHandlerContext,\n  error: any,\n  boundaryId?: string,\n): StaticHandlerContext {\n  let errorBoundaryId =\n    boundaryId || handlerContext._deepestRenderedBoundaryId || routes[0].id;\n  return {\n    ...handlerContext,\n    statusCode: isRouteErrorResponse(error) ? error.status : 500,\n    errors: {\n      [errorBoundaryId]: error,\n    },\n  };\n}\n\nfunction throwStaticHandlerAbortedError(\n  request: Request,\n  isRouteRequest: boolean,\n) {\n  if (request.signal.reason !== undefined) {\n    throw request.signal.reason;\n  }\n\n  let method = isRouteRequest ? \"queryRoute\" : \"query\";\n  throw new Error(\n    `${method}() call aborted without an \\`AbortSignal.reason\\`: ${request.method} ${request.url}`,\n  );\n}\n\nfunction isSubmissionNavigation(\n  opts: BaseNavigateOrFetchOptions,\n): opts is SubmissionNavigateOptions {\n  return (\n    opts != null &&\n    ((\"formData\" in opts && opts.formData != null) ||\n      (\"body\" in opts && opts.body !== undefined))\n  );\n}\n\nfunction normalizeTo(\n  location: Path,\n  matches: AgnosticDataRouteMatch[],\n  basename: string,\n  to: To | null,\n  fromRouteId?: string,\n  relative?: RelativeRoutingType,\n) {\n  let contextualMatches: AgnosticDataRouteMatch[];\n  let activeRouteMatch: AgnosticDataRouteMatch | undefined;\n  if (fromRouteId) {\n    // Grab matches up to the calling route so our route-relative logic is\n    // relative to the correct source route\n    contextualMatches = [];\n    for (let match of matches) {\n      contextualMatches.push(match);\n      if (match.route.id === fromRouteId) {\n        activeRouteMatch = match;\n        break;\n      }\n    }\n  } else {\n    contextualMatches = matches;\n    activeRouteMatch = matches[matches.length - 1];\n  }\n\n  // Resolve the relative path\n  let path = resolveTo(\n    to ? to : \".\",\n    getResolveToMatches(contextualMatches),\n    stripBasename(location.pathname, basename) || location.pathname,\n    relative === \"path\",\n  );\n\n  // When `to` is not specified we inherit search/hash from the current\n  // location, unlike when to=\".\" and we just inherit the path.\n  // See https://github.com/remix-run/remix/issues/927\n  if (to == null) {\n    path.search = location.search;\n    path.hash = location.hash;\n  }\n\n  // Account for `?index` params when routing to the current location\n  if ((to == null || to === \"\" || to === \".\") && activeRouteMatch) {\n    let nakedIndex = hasNakedIndexQuery(path.search);\n    if (activeRouteMatch.route.index && !nakedIndex) {\n      // Add one when we're targeting an index route\n      path.search = path.search\n        ? path.search.replace(/^\\?/, \"?index&\")\n        : \"?index\";\n    } else if (!activeRouteMatch.route.index && nakedIndex) {\n      // Remove existing ones when we're not\n      let params = new URLSearchParams(path.search);\n      let indexValues = params.getAll(\"index\");\n      params.delete(\"index\");\n      indexValues.filter((v) => v).forEach((v) => params.append(\"index\", v));\n      let qs = params.toString();\n      path.search = qs ? `?${qs}` : \"\";\n    }\n  }\n\n  // If we're operating within a basename, prepend it to the pathname.\n  if (basename !== \"/\") {\n    path.pathname = prependBasename({ basename, pathname: path.pathname });\n  }\n\n  return createPath(path);\n}\n\n// Normalize navigation options by converting formMethod=GET formData objects to\n// URLSearchParams so they behave identically to links with query params\nfunction normalizeNavigateOptions(\n  isFetcher: boolean,\n  path: string,\n  opts?: BaseNavigateOrFetchOptions,\n): {\n  path: string;\n  submission?: Submission;\n  error?: ErrorResponseImpl;\n} {\n  // Return location verbatim on non-submission navigations\n  if (!opts || !isSubmissionNavigation(opts)) {\n    return { path };\n  }\n\n  if (opts.formMethod && !isValidMethod(opts.formMethod)) {\n    return {\n      path,\n      error: getInternalRouterError(405, { method: opts.formMethod }),\n    };\n  }\n\n  let getInvalidBodyError = () => ({\n    path,\n    error: getInternalRouterError(400, { type: \"invalid-body\" }),\n  });\n\n  // Create a Submission on non-GET navigations\n  let rawFormMethod = opts.formMethod || \"get\";\n  let formMethod = rawFormMethod.toUpperCase() as FormMethod;\n  let formAction = stripHashFromPath(path);\n\n  if (opts.body !== undefined) {\n    if (opts.formEncType === \"text/plain\") {\n      // text only support POST/PUT/PATCH/DELETE submissions\n      if (!isMutationMethod(formMethod)) {\n        return getInvalidBodyError();\n      }\n\n      let text =\n        typeof opts.body === \"string\"\n          ? opts.body\n          : opts.body instanceof FormData ||\n              opts.body instanceof URLSearchParams\n            ? // https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#plain-text-form-data\n              Array.from(opts.body.entries()).reduce(\n                (acc, [name, value]) => `${acc}${name}=${value}\\n`,\n                \"\",\n              )\n            : String(opts.body);\n\n      return {\n        path,\n        submission: {\n          formMethod,\n          formAction,\n          formEncType: opts.formEncType,\n          formData: undefined,\n          json: undefined,\n          text,\n        },\n      };\n    } else if (opts.formEncType === \"application/json\") {\n      // json only supports POST/PUT/PATCH/DELETE submissions\n      if (!isMutationMethod(formMethod)) {\n        return getInvalidBodyError();\n      }\n\n      try {\n        let json =\n          typeof opts.body === \"string\" ? JSON.parse(opts.body) : opts.body;\n\n        return {\n          path,\n          submission: {\n            formMethod,\n            formAction,\n            formEncType: opts.formEncType,\n            formData: undefined,\n            json,\n            text: undefined,\n          },\n        };\n      } catch (e) {\n        return getInvalidBodyError();\n      }\n    }\n  }\n\n  invariant(\n    typeof FormData === \"function\",\n    \"FormData is not available in this environment\",\n  );\n\n  let searchParams: URLSearchParams;\n  let formData: FormData;\n\n  if (opts.formData) {\n    searchParams = convertFormDataToSearchParams(opts.formData);\n    formData = opts.formData;\n  } else if (opts.body instanceof FormData) {\n    searchParams = convertFormDataToSearchParams(opts.body);\n    formData = opts.body;\n  } else if (opts.body instanceof URLSearchParams) {\n    searchParams = opts.body;\n    formData = convertSearchParamsToFormData(searchParams);\n  } else if (opts.body == null) {\n    searchParams = new URLSearchParams();\n    formData = new FormData();\n  } else {\n    try {\n      searchParams = new URLSearchParams(opts.body);\n      formData = convertSearchParamsToFormData(searchParams);\n    } catch (e) {\n      return getInvalidBodyError();\n    }\n  }\n\n  let submission: Submission = {\n    formMethod,\n    formAction,\n    formEncType:\n      (opts && opts.formEncType) || \"application/x-www-form-urlencoded\",\n    formData,\n    json: undefined,\n    text: undefined,\n  };\n\n  if (isMutationMethod(submission.formMethod)) {\n    return { path, submission };\n  }\n\n  // Flatten submission onto URLSearchParams for GET submissions\n  let parsedPath = parsePath(path);\n  // On GET navigation submissions we can drop the ?index param from the\n  // resulting location since all loaders will run.  But fetcher GET submissions\n  // only run a single loader so we need to preserve any incoming ?index params\n  if (isFetcher && parsedPath.search && hasNakedIndexQuery(parsedPath.search)) {\n    searchParams.append(\"index\", \"\");\n  }\n  parsedPath.search = `?${searchParams}`;\n\n  return { path: createPath(parsedPath), submission };\n}\n\nfunction getMatchesToLoad(\n  request: Request,\n  scopedContext: unknown,\n  mapRouteProperties: MapRoutePropertiesFunction,\n  manifest: RouteManifest,\n  history: History,\n  state: RouterState,\n  matches: AgnosticDataRouteMatch[],\n  submission: Submission | undefined,\n  location: Location,\n  lazyRoutePropertiesToSkip: string[],\n  initialHydration: boolean,\n  isRevalidationRequired: boolean,\n  cancelledFetcherLoads: Set<string>,\n  fetchersQueuedForDeletion: Set<string>,\n  fetchLoadMatches: Map<string, FetchLoadMatch>,\n  fetchRedirectIds: Set<string>,\n  routesToUse: AgnosticDataRouteObject[],\n  basename: string | undefined,\n  hasPatchRoutesOnNavigation: boolean,\n  pendingActionResult?: PendingActionResult,\n  callSiteDefaultShouldRevalidate?: boolean,\n): {\n  dsMatches: DataStrategyMatch[];\n  revalidatingFetchers: RevalidatingFetcher[];\n} {\n  let actionResult = pendingActionResult\n    ? isErrorResult(pendingActionResult[1])\n      ? pendingActionResult[1].error\n      : pendingActionResult[1].data\n    : undefined;\n  let currentUrl = history.createURL(state.location);\n  let nextUrl = history.createURL(location);\n\n  // Pick navigation matches that are net-new or qualify for revalidation\n  let maxIdx: number | undefined;\n  if (initialHydration && state.errors) {\n    // On initial hydration, only consider matches up to _and including_ the boundary.\n    // This is inclusive to handle cases where a server loader ran successfully,\n    // a child server loader bubbled up to this route, but this route has\n    // `clientLoader.hydrate` so we want to still run the `clientLoader` so that\n    // we have a complete version of `loaderData`\n    let boundaryId = Object.keys(state.errors)[0];\n    maxIdx = matches.findIndex((m) => m.route.id === boundaryId);\n  } else if (pendingActionResult && isErrorResult(pendingActionResult[1])) {\n    // If an action threw an error, we call loaders up to, but not including the\n    // boundary\n    let boundaryId = pendingActionResult[0];\n    maxIdx = matches.findIndex((m) => m.route.id === boundaryId) - 1;\n  }\n\n  // Don't revalidate loaders by default after action 4xx/5xx responses\n  // when the flag is enabled.  They can still opt-into revalidation via\n  // `shouldRevalidate` via `actionResult`\n  let actionStatus = pendingActionResult\n    ? pendingActionResult[1].statusCode\n    : undefined;\n  let shouldSkipRevalidation = actionStatus && actionStatus >= 400;\n\n  let baseShouldRevalidateArgs = {\n    currentUrl,\n    currentParams: state.matches[0]?.params || {},\n    nextUrl,\n    nextParams: matches[0].params,\n    ...submission,\n    actionResult,\n    actionStatus,\n  };\n\n  let pattern = getRoutePattern(matches);\n  let dsMatches: DataStrategyMatch[] = matches.map((match, index) => {\n    let { route } = match;\n\n    // For these cases we don't let the user have control via shouldRevalidate\n    // and we either force the loader to run or not run\n    let forceShouldLoad: boolean | null = null;\n\n    if (maxIdx != null && index > maxIdx) {\n      // Don't call loaders below the boundary\n      forceShouldLoad = false;\n    } else if (route.lazy) {\n      // We haven't loaded this route yet so we don't know if it's got a loader!\n      forceShouldLoad = true;\n    } else if (!routeHasLoaderOrMiddleware(route)) {\n      // Nothing to load!\n      forceShouldLoad = false;\n    } else if (initialHydration) {\n      // Only run on hydration if this is a hydrating `clientLoader`\n      let { shouldLoad } = getRouteHydrationStatus(\n        route,\n        state.loaderData,\n        state.errors,\n      );\n      forceShouldLoad = shouldLoad;\n    } else if (isNewLoader(state.loaderData, state.matches[index], match)) {\n      // Always call the loader on new route instances\n      forceShouldLoad = true;\n    }\n\n    if (forceShouldLoad !== null) {\n      return getDataStrategyMatch(\n        mapRouteProperties,\n        manifest,\n        request,\n        pattern,\n        match,\n        lazyRoutePropertiesToSkip,\n        scopedContext,\n        forceShouldLoad,\n      );\n    }\n\n    // This is the default implementation for when we revalidate.  If the route\n    // provides it's own implementation, then we give them full control but\n    // provide this value so they can leverage it if needed after they check\n    // their own specific use cases\n    let defaultShouldRevalidate = false;\n    if (typeof callSiteDefaultShouldRevalidate === \"boolean\") {\n      // Use call-site value verbatim if provided\n      defaultShouldRevalidate = callSiteDefaultShouldRevalidate;\n    } else if (shouldSkipRevalidation) {\n      // Skip due to 4xx/5xx action result\n      defaultShouldRevalidate = false;\n    } else if (isRevalidationRequired) {\n      // Forced revalidation due to submission, useRevalidator, or X-Remix-Revalidate\n      defaultShouldRevalidate = true;\n    } else if (\n      currentUrl.pathname + currentUrl.search ===\n      nextUrl.pathname + nextUrl.search\n    ) {\n      // Same URL - mimic a hard reload\n      defaultShouldRevalidate = true;\n    } else if (currentUrl.search !== nextUrl.search) {\n      // Search params affect all loaders\n      defaultShouldRevalidate = true;\n    } else if (isNewRouteInstance(state.matches[index], match)) {\n      defaultShouldRevalidate = true;\n    }\n\n    let shouldRevalidateArgs = {\n      ...baseShouldRevalidateArgs,\n      defaultShouldRevalidate,\n    };\n    let shouldLoad = shouldRevalidateLoader(match, shouldRevalidateArgs);\n    return getDataStrategyMatch(\n      mapRouteProperties,\n      manifest,\n      request,\n      pattern,\n      match,\n      lazyRoutePropertiesToSkip,\n      scopedContext,\n      shouldLoad,\n      shouldRevalidateArgs,\n      callSiteDefaultShouldRevalidate,\n    );\n  });\n\n  // Pick fetcher.loads that need to be revalidated\n  let revalidatingFetchers: RevalidatingFetcher[] = [];\n  fetchLoadMatches.forEach((f, key) => {\n    // Don't revalidate:\n    //  - on initial hydration (shouldn't be any fetchers then anyway)\n    //  - if fetcher won't be present in the subsequent render\n    //    - no longer matches the URL (v7_fetcherPersist=false)\n    //    - was unmounted but persisted due to v7_fetcherPersist=true\n    if (\n      initialHydration ||\n      !matches.some((m) => m.route.id === f.routeId) ||\n      fetchersQueuedForDeletion.has(key)\n    ) {\n      return;\n    }\n\n    let fetcher = state.fetchers.get(key);\n    let isMidInitialLoad =\n      fetcher && fetcher.state !== \"idle\" && fetcher.data === undefined;\n    let fetcherMatches = matchRoutes(routesToUse, f.path, basename);\n\n    // If the fetcher path no longer matches, push it in with null matches so\n    // we can trigger a 404 in callLoadersAndMaybeResolveData.  Note this is\n    // currently only a use-case for Remix HMR where the route tree can change\n    // at runtime and remove a route previously loaded via a fetcher\n    if (!fetcherMatches) {\n      // If this fetcher is still in it's initial loading state, then this is\n      // most likely not a 404 and the fetcher is still in the middle of lazy\n      // route discovery so we can just skip revalidation and let it finish\n      // it's initial load\n      if (hasPatchRoutesOnNavigation && isMidInitialLoad) {\n        return;\n      }\n      revalidatingFetchers.push({\n        key,\n        routeId: f.routeId,\n        path: f.path,\n        matches: null,\n        match: null,\n        request: null,\n        controller: null,\n      });\n      return;\n    }\n\n    if (fetchRedirectIds.has(key)) {\n      // Never trigger a revalidation of an actively redirecting fetcher\n      return;\n    }\n\n    // Revalidating fetchers are decoupled from the route matches since they\n    // load from a static href.  They revalidate based on explicit revalidation\n    // (submission, useRevalidator, or X-Remix-Revalidate)\n    let fetcherMatch = getTargetMatch(fetcherMatches, f.path);\n\n    let fetchController = new AbortController();\n    let fetchRequest = createClientSideRequest(\n      history,\n      f.path,\n      fetchController.signal,\n    );\n\n    let fetcherDsMatches: DataStrategyMatch[] | null = null;\n\n    if (cancelledFetcherLoads.has(key)) {\n      // Always mark for revalidation if the fetcher was cancelled\n      cancelledFetcherLoads.delete(key);\n      fetcherDsMatches = getTargetedDataStrategyMatches(\n        mapRouteProperties,\n        manifest,\n        fetchRequest,\n        fetcherMatches,\n        fetcherMatch,\n        lazyRoutePropertiesToSkip,\n        scopedContext,\n      );\n    } else if (isMidInitialLoad) {\n      if (isRevalidationRequired) {\n        // If the fetcher hasn't ever completed loading yet, then this isn't a\n        // revalidation, it would just be a brand new load if an explicit\n        // revalidation is required\n        fetcherDsMatches = getTargetedDataStrategyMatches(\n          mapRouteProperties,\n          manifest,\n          fetchRequest,\n          fetcherMatches,\n          fetcherMatch,\n          lazyRoutePropertiesToSkip,\n          scopedContext,\n        );\n      }\n    } else {\n      // Otherwise fall back on any user-defined shouldRevalidate, defaulting\n      // to explicit revalidations only\n      let defaultShouldRevalidate: boolean;\n      if (typeof callSiteDefaultShouldRevalidate === \"boolean\") {\n        // Use call-site value verbatim if provided\n        defaultShouldRevalidate = callSiteDefaultShouldRevalidate;\n      } else if (shouldSkipRevalidation) {\n        defaultShouldRevalidate = false;\n      } else {\n        defaultShouldRevalidate = isRevalidationRequired;\n      }\n\n      let shouldRevalidateArgs: ShouldRevalidateFunctionArgs = {\n        ...baseShouldRevalidateArgs,\n        defaultShouldRevalidate,\n      };\n      if (shouldRevalidateLoader(fetcherMatch, shouldRevalidateArgs)) {\n        fetcherDsMatches = getTargetedDataStrategyMatches(\n          mapRouteProperties,\n          manifest,\n          fetchRequest,\n          fetcherMatches,\n          fetcherMatch,\n          lazyRoutePropertiesToSkip,\n          scopedContext,\n          shouldRevalidateArgs,\n        );\n      }\n    }\n\n    if (fetcherDsMatches) {\n      revalidatingFetchers.push({\n        key,\n        routeId: f.routeId,\n        path: f.path,\n        matches: fetcherDsMatches,\n        match: fetcherMatch,\n        request: fetchRequest,\n        controller: fetchController,\n      });\n    }\n  });\n\n  return { dsMatches, revalidatingFetchers };\n}\n\nfunction routeHasLoaderOrMiddleware(route: RouteObject) {\n  return (\n    route.loader != null ||\n    (route.middleware != null && route.middleware.length > 0)\n  );\n}\n\n// Determine if a given route needs to be loaded during hydration and whether\n// or not re should render the HydrateFallback.  The are usually tightly coupled\n// except for when we are loading a route due to `loader.hydrate=true`, in which\n// case we don't want to render a fallback\nfunction getRouteHydrationStatus(\n  route: AgnosticDataRouteObject,\n  loaderData: RouteData | null | undefined,\n  errors: RouteData | null | undefined,\n): { shouldLoad: boolean; renderFallback: boolean } {\n  // We dunno if we have a loader - gotta find out!\n  if (route.lazy) {\n    return { shouldLoad: true, renderFallback: true };\n  }\n\n  // No loader or middleware, nothing to run\n  if (!routeHasLoaderOrMiddleware(route)) {\n    return { shouldLoad: false, renderFallback: false };\n  }\n\n  let hasData = loaderData != null && route.id in loaderData;\n  let hasError = errors != null && errors[route.id] !== undefined;\n\n  // Don't run if we error'd during SSR\n  if (!hasData && hasError) {\n    return { shouldLoad: false, renderFallback: false };\n  }\n\n  // Explicitly opting-in to running on hydration, only showing the fallback if\n  // we don't have data\n  if (typeof route.loader === \"function\" && route.loader.hydrate === true) {\n    return { shouldLoad: true, renderFallback: !hasData };\n  }\n\n  // Otherwise, run if we're not yet initialized with anything\n  let shouldLoad = !hasData && !hasError;\n  return { shouldLoad, renderFallback: shouldLoad };\n}\n\nfunction isNewLoader(\n  currentLoaderData: RouteData,\n  currentMatch: AgnosticDataRouteMatch,\n  match: AgnosticDataRouteMatch,\n) {\n  let isNew =\n    // [a] -> [a, b]\n    !currentMatch ||\n    // [a, b] -> [a, c]\n    match.route.id !== currentMatch.route.id;\n\n  // Handle the case that we don't have data for a re-used route, potentially\n  // from a prior error\n  let isMissingData = !currentLoaderData.hasOwnProperty(match.route.id);\n\n  // Always load if this is a net-new route or we don't yet have data\n  return isNew || isMissingData;\n}\n\nfunction isNewRouteInstance(\n  currentMatch: AgnosticDataRouteMatch,\n  match: AgnosticDataRouteMatch,\n) {\n  let currentPath = currentMatch.route.path;\n  return (\n    // param change for this match, /users/123 -> /users/456\n    currentMatch.pathname !== match.pathname ||\n    // splat param changed, which is not present in match.path\n    // e.g. /files/images/avatar.jpg -> files/finances.xls\n    (currentPath != null &&\n      currentPath.endsWith(\"*\") &&\n      currentMatch.params[\"*\"] !== match.params[\"*\"])\n  );\n}\n\nfunction shouldRevalidateLoader(\n  loaderMatch: AgnosticDataRouteMatch,\n  arg: ShouldRevalidateFunctionArgs,\n) {\n  if (loaderMatch.route.shouldRevalidate) {\n    let routeChoice = loaderMatch.route.shouldRevalidate(arg);\n    if (typeof routeChoice === \"boolean\") {\n      return routeChoice;\n    }\n  }\n\n  return arg.defaultShouldRevalidate;\n}\n\nfunction patchRoutesImpl(\n  routeId: string | null,\n  children: AgnosticRouteObject[],\n  routesToUse: AgnosticDataRouteObject[],\n  manifest: RouteManifest,\n  mapRouteProperties: MapRoutePropertiesFunction,\n  allowElementMutations: boolean,\n) {\n  let childrenToPatch: AgnosticDataRouteObject[];\n  if (routeId) {\n    let route = manifest[routeId];\n    invariant(\n      route,\n      `No route found to patch children into: routeId = ${routeId}`,\n    );\n    if (!route.children) {\n      route.children = [];\n    }\n    childrenToPatch = route.children;\n  } else {\n    childrenToPatch = routesToUse;\n  }\n\n  // Don't patch in routes we already know about so that `patch` is idempotent\n  // to simplify user-land code. This is useful because we re-call the\n  // `patchRoutesOnNavigation` function for matched routes with params.\n  let uniqueChildren: AgnosticRouteObject[] = [];\n  let existingChildren: {\n    existingRoute: AgnosticRouteObject;\n    newRoute: AgnosticRouteObject;\n  }[] = [];\n  children.forEach((newRoute) => {\n    let existingRoute = childrenToPatch.find((existingRoute) =>\n      isSameRoute(newRoute, existingRoute),\n    );\n    if (existingRoute) {\n      existingChildren.push({ existingRoute, newRoute });\n    } else {\n      uniqueChildren.push(newRoute);\n    }\n  });\n\n  if (uniqueChildren.length > 0) {\n    let newRoutes = convertRoutesToDataRoutes(\n      uniqueChildren,\n      mapRouteProperties,\n      [routeId || \"_\", \"patch\", String(childrenToPatch?.length || \"0\")],\n      manifest,\n    );\n    childrenToPatch.push(...newRoutes);\n  }\n\n  // When flag is enabled we allow mutations of elements on exiting routes\n  if (allowElementMutations && existingChildren.length > 0) {\n    for (let i = 0; i < existingChildren.length; i++) {\n      let { existingRoute, newRoute } = existingChildren[i];\n      let existingRouteTyped = existingRoute as RouteObject;\n      // All this will end up doing for these scenarios is adding `hasErrorBoundary`\n      // to the route.  There's no need for Component->element conversions since\n      // we're already dealing with elements here\n      let [newRouteTyped] = convertRoutesToDataRoutes(\n        [newRoute],\n        mapRouteProperties,\n        [], // Doesn't matter for mutated routes since they already have an id\n        {}, // Don't touch the manifest here since we're updating in place\n        true,\n      ) as RouteObject[];\n      Object.assign(existingRouteTyped, {\n        element: newRouteTyped.element\n          ? newRouteTyped.element\n          : existingRouteTyped.element,\n        errorElement: newRouteTyped.errorElement\n          ? newRouteTyped.errorElement\n          : existingRouteTyped.errorElement,\n        hydrateFallbackElement: newRouteTyped.hydrateFallbackElement\n          ? newRouteTyped.hydrateFallbackElement\n          : existingRouteTyped.hydrateFallbackElement,\n      });\n    }\n  }\n}\n\nfunction isSameRoute(\n  newRoute: AgnosticRouteObject,\n  existingRoute: AgnosticRouteObject,\n): boolean {\n  // Most optimal check is by id\n  if (\n    \"id\" in newRoute &&\n    \"id\" in existingRoute &&\n    newRoute.id === existingRoute.id\n  ) {\n    return true;\n  }\n\n  // Second is by pathing differences\n  if (\n    !(\n      newRoute.index === existingRoute.index &&\n      newRoute.path === existingRoute.path &&\n      newRoute.caseSensitive === existingRoute.caseSensitive\n    )\n  ) {\n    return false;\n  }\n\n  // Pathless layout routes are trickier since we need to check children.\n  // If they have no children then they're the same as far as we can tell\n  if (\n    (!newRoute.children || newRoute.children.length === 0) &&\n    (!existingRoute.children || existingRoute.children.length === 0)\n  ) {\n    return true;\n  }\n\n  // Otherwise, we look to see if every child in the new route is already\n  // represented in the existing route's children\n  return (\n    newRoute.children?.every((aChild, i) =>\n      existingRoute.children?.some((bChild) => isSameRoute(aChild, bChild)),\n    ) ?? false\n  );\n}\n\nconst lazyRoutePropertyCache = new WeakMap<\n  AgnosticDataRouteObject,\n  Partial<Record<keyof AgnosticDataRouteObject, Promise<void>>>\n>();\n\nconst loadLazyRouteProperty = ({\n  key,\n  route,\n  manifest,\n  mapRouteProperties,\n}: {\n  key: keyof AgnosticDataRouteObject;\n  route: AgnosticDataRouteObject;\n  manifest: RouteManifest;\n  mapRouteProperties: MapRoutePropertiesFunction;\n}): Promise<void> | undefined => {\n  let routeToUpdate = manifest[route.id];\n  invariant(routeToUpdate, \"No route found in manifest\");\n\n  if (!routeToUpdate.lazy || typeof routeToUpdate.lazy !== \"object\") {\n    return;\n  }\n\n  let lazyFn = routeToUpdate.lazy[key as keyof typeof routeToUpdate.lazy];\n\n  if (!lazyFn) {\n    return;\n  }\n\n  let cache = lazyRoutePropertyCache.get(routeToUpdate);\n  if (!cache) {\n    cache = {};\n    lazyRoutePropertyCache.set(routeToUpdate, cache);\n  }\n\n  let cachedPromise = cache[key];\n  if (cachedPromise) {\n    return cachedPromise;\n  }\n\n  let propertyPromise = (async () => {\n    let isUnsupported = isUnsupportedLazyRouteObjectKey(key);\n    let staticRouteValue = routeToUpdate[key as keyof typeof routeToUpdate];\n    let isStaticallyDefined =\n      staticRouteValue !== undefined && key !== \"hasErrorBoundary\";\n\n    if (isUnsupported) {\n      warning(\n        !isUnsupported,\n        \"Route property \" +\n          key +\n          \" is not a supported lazy route property. This property will be ignored.\",\n      );\n      cache[key] = Promise.resolve();\n    } else if (isStaticallyDefined) {\n      warning(\n        false,\n        `Route \"${routeToUpdate.id}\" has a static property \"${key}\" ` +\n          `defined. The lazy property will be ignored.`,\n      );\n    } else {\n      let value = await lazyFn();\n      if (value != null) {\n        Object.assign(routeToUpdate, { [key]: value });\n        Object.assign(routeToUpdate, mapRouteProperties(routeToUpdate));\n      }\n    }\n\n    // Clean up lazy property and clean up lazy object if it's now empty\n    if (typeof routeToUpdate.lazy === \"object\") {\n      routeToUpdate.lazy[key as keyof typeof routeToUpdate.lazy] = undefined;\n      if (\n        Object.values(routeToUpdate.lazy).every((value) => value === undefined)\n      ) {\n        routeToUpdate.lazy = undefined;\n      }\n    }\n  })();\n\n  cache[key] = propertyPromise;\n  return propertyPromise;\n};\n\nconst lazyRouteFunctionCache = new WeakMap<\n  AgnosticDataRouteObject,\n  Promise<void>\n>();\n\n/**\n * Execute route.lazy functions to lazily load route modules (loader, action,\n * shouldRevalidate) and update the routeManifest in place which shares objects\n * with dataRoutes so those get updated as well.\n */\nfunction loadLazyRoute(\n  route: AgnosticDataRouteObject,\n  type: \"loader\" | \"action\",\n  manifest: RouteManifest,\n  mapRouteProperties: MapRoutePropertiesFunction,\n  lazyRoutePropertiesToSkip?: string[],\n): {\n  lazyRoutePromise: Promise<void> | undefined;\n  lazyHandlerPromise: Promise<void> | undefined;\n} {\n  let routeToUpdate = manifest[route.id];\n  invariant(routeToUpdate, \"No route found in manifest\");\n\n  if (!route.lazy) {\n    return {\n      lazyRoutePromise: undefined,\n      lazyHandlerPromise: undefined,\n    };\n  }\n\n  if (typeof route.lazy === \"function\") {\n    // Check if we have a cached promise from a previous call\n    let cachedPromise = lazyRouteFunctionCache.get(routeToUpdate);\n    if (cachedPromise) {\n      return {\n        lazyRoutePromise: cachedPromise,\n        lazyHandlerPromise: cachedPromise,\n      };\n    }\n\n    // We use `.then` to chain additional logic to the lazy route promise so that\n    // the consumer's lazy route logic is coupled to our logic for updating the\n    // route in place in a single task. This ensures that the cached promise\n    // contains all logic for managing the lazy route. This chained promise is\n    // then awaited so that consumers of this function see the updated route.\n    let lazyRoutePromise = (async () => {\n      invariant(\n        typeof route.lazy === \"function\",\n        \"No lazy route function found\",\n      );\n      let lazyRoute = await route.lazy();\n      // Here we update the route in place.  This should be safe because there's\n      // no way we could yet be sitting on this route as we can't get there\n      // without resolving lazy() first.\n      //\n      // This is different than the HMR \"update\" use-case where we may actively be\n      // on the route being updated.  The main concern boils down to \"does this\n      // mutation affect any ongoing navigations or any current state.matches\n      // values?\".  If not, it should be safe to update in place.\n      let routeUpdates: Record<string, any> = {};\n      for (let lazyRouteProperty in lazyRoute) {\n        let lazyValue = lazyRoute[lazyRouteProperty as keyof typeof lazyRoute];\n\n        if (lazyValue === undefined) {\n          continue;\n        }\n\n        let isUnsupported =\n          isUnsupportedLazyRouteFunctionKey(lazyRouteProperty);\n        let staticRouteValue =\n          routeToUpdate[lazyRouteProperty as keyof typeof routeToUpdate];\n        let isStaticallyDefined =\n          staticRouteValue !== undefined &&\n          // This property isn't static since it should always be updated based\n          // on the route updates\n          lazyRouteProperty !== \"hasErrorBoundary\";\n\n        if (isUnsupported) {\n          warning(\n            !isUnsupported,\n            \"Route property \" +\n              lazyRouteProperty +\n              \" is not a supported property to be returned from a lazy route function. This property will be ignored.\",\n          );\n        } else if (isStaticallyDefined) {\n          warning(\n            !isStaticallyDefined,\n            `Route \"${routeToUpdate.id}\" has a static property \"${lazyRouteProperty}\" ` +\n              `defined but its lazy function is also returning a value for this property. ` +\n              `The lazy route property \"${lazyRouteProperty}\" will be ignored.`,\n          );\n        } else {\n          routeUpdates[lazyRouteProperty] = lazyValue;\n        }\n      }\n\n      // Mutate the route with the provided updates.  Do this first so we pass\n      // the updated version to mapRouteProperties\n      Object.assign(routeToUpdate, routeUpdates);\n\n      // Mutate the `hasErrorBoundary` property on the route based on the route\n      // updates and remove the `lazy` function so we don't resolve the lazy\n      // route again.\n      Object.assign(routeToUpdate, {\n        // To keep things framework agnostic, we use the provided `mapRouteProperties`\n        // function to set the framework-aware properties (`element`/`hasErrorBoundary`)\n        // since the logic will differ between frameworks.\n        ...mapRouteProperties(routeToUpdate),\n        lazy: undefined,\n      });\n    })();\n\n    lazyRouteFunctionCache.set(routeToUpdate, lazyRoutePromise);\n\n    // Prevent unhandled rejection errors - handled inside of `callLoadOrAction`\n    lazyRoutePromise.catch(() => {});\n\n    return {\n      lazyRoutePromise,\n      lazyHandlerPromise: lazyRoutePromise,\n    };\n  }\n\n  let lazyKeys = Object.keys(route.lazy) as Array<keyof typeof route.lazy>;\n  let lazyPropertyPromises: Array<Promise<void>> = [];\n  let lazyHandlerPromise: Promise<void> | undefined = undefined;\n\n  for (let key of lazyKeys) {\n    if (lazyRoutePropertiesToSkip && lazyRoutePropertiesToSkip.includes(key)) {\n      continue;\n    }\n\n    let promise = loadLazyRouteProperty({\n      key,\n      route,\n      manifest,\n      mapRouteProperties,\n    });\n    if (promise) {\n      lazyPropertyPromises.push(promise);\n      if (key === type) {\n        lazyHandlerPromise = promise;\n      }\n    }\n  }\n\n  let lazyRoutePromise =\n    lazyPropertyPromises.length > 0\n      ? Promise.all(lazyPropertyPromises)\n          // Ensure type is Promise<void>, not Promise<void[]>\n          .then(() => {})\n      : undefined;\n\n  // Prevent unhandled rejection errors - handled inside of `callLoadOrAction`\n  lazyRoutePromise?.catch(() => {});\n  lazyHandlerPromise?.catch(() => {});\n\n  return {\n    lazyRoutePromise,\n    lazyHandlerPromise,\n  };\n}\n\nfunction isNonNullable<T>(value: T): value is NonNullable<T> {\n  return value !== undefined;\n}\n\nfunction loadLazyMiddlewareForMatches(\n  matches: AgnosticDataRouteMatch[],\n  manifest: RouteManifest,\n  mapRouteProperties: MapRoutePropertiesFunction,\n): Promise<void[]> | void {\n  let promises: Promise<void>[] = matches\n    .map(({ route }) => {\n      if (typeof route.lazy !== \"object\" || !route.lazy.middleware) {\n        return undefined;\n      }\n\n      return loadLazyRouteProperty({\n        key: \"middleware\",\n        route,\n        manifest,\n        mapRouteProperties,\n      });\n    })\n    .filter(isNonNullable);\n\n  return promises.length > 0 ? Promise.all(promises) : undefined;\n}\n\n// Default implementation of `dataStrategy` which fetches all loaders in parallel\nasync function defaultDataStrategy(\n  args: DataStrategyFunctionArgs<unknown>,\n): ReturnType<DataStrategyFunction<unknown>> {\n  let matchesToLoad = args.matches.filter((m) => m.shouldLoad);\n  let keyedResults: Record<string, DataStrategyResult> = {};\n  let results = await Promise.all(matchesToLoad.map((m) => m.resolve()));\n  results.forEach((result, i) => {\n    keyedResults[matchesToLoad[i].route.id] = result;\n  });\n  return keyedResults;\n}\n\n// Middleware-enabled implementation of `dataStrategy` which calls middleware\n// and fetches all loaders in parallel\nasync function defaultDataStrategyWithMiddleware(\n  args: DataStrategyFunctionArgs<RouterContextProvider>,\n): ReturnType<DataStrategyFunction<unknown>> {\n  // Short circuit all the middleware logic if we have no middlewares\n  if (!args.matches.some((m) => m.route.middleware)) {\n    return defaultDataStrategy(args);\n  }\n\n  return runClientMiddlewarePipeline(args, () => defaultDataStrategy(args));\n}\n\nfunction runServerMiddlewarePipeline(\n  args: (\n    | LoaderFunctionArgs<RouterContextProvider>\n    | ActionFunctionArgs<RouterContextProvider>\n  ) & {\n    // Don't use `DataStrategyFunctionArgs` directly so we can we reduce these\n    // back from `DataStrategyMatch` to regular matches for use in the staticHandler\n    matches: AgnosticDataRouteMatch[];\n  },\n  handler: () => Promise<Response>,\n  errorHandler: (\n    error: unknown,\n    routeId: string,\n    nextResult: { value: Response } | undefined,\n  ) => Promise<Response>,\n): Promise<Response> {\n  return runMiddlewarePipeline(\n    args,\n    handler,\n    processResult,\n    isResponse,\n    errorHandler,\n  );\n\n  // Upgrade returned data() values to real Responses\n  function processResult(result: any) {\n    return isDataWithResponseInit(result)\n      ? dataWithResponseInitToResponse(result)\n      : result;\n  }\n}\n\nfunction runClientMiddlewarePipeline(\n  args: Omit<\n    DataStrategyFunctionArgs<RouterContextProvider>,\n    \"fetcherKey\" | \"runClientMiddleware\"\n  >,\n  handler: () => Promise<Record<string, DataStrategyResult>>,\n): Promise<Record<string, DataStrategyResult>> {\n  return runMiddlewarePipeline(\n    args,\n    handler,\n    (r) => {\n      // Throw any redirect responses to short circuit\n      if (isRedirectResponse(r)) {\n        throw r;\n      }\n      return r;\n    },\n    isDataStrategyResults,\n    errorHandler,\n  );\n\n  // Handle error bubbling on the client\n  function errorHandler(\n    error: unknown,\n    routeId: string,\n    nextResult: { value: Record<string, DataStrategyResult> } | undefined,\n  ): Promise<Record<string, DataStrategyResult>> {\n    if (nextResult) {\n      return Promise.resolve(\n        Object.assign(nextResult.value, {\n          [routeId]: { type: \"error\", result: error },\n        }),\n      );\n    } else {\n      // We never even got to the handlers, so we might not have data for new routes.\n      // Find the boundary at or above the source of the middleware error or the\n      // highest route that needs to load - we can't render any UI below that since\n      // we won't have valid loader data.\n      let { matches } = args;\n      let maxBoundaryIdx = Math.min(\n        // Throwing route\n        Math.max(\n          matches.findIndex((m) => m.route.id === routeId),\n          0,\n        ),\n        // or the shallowest route that needs to load data\n        Math.max(\n          matches.findIndex((m) => m.shouldCallHandler()),\n          0,\n        ),\n      );\n      let boundaryRouteId = findNearestBoundary(\n        matches,\n        matches[maxBoundaryIdx].route.id,\n      ).route.id;\n      return Promise.resolve({\n        [boundaryRouteId]: { type: \"error\", result: error },\n      });\n    }\n  }\n}\n\nasync function runMiddlewarePipeline<Result>(\n  args: (\n    | LoaderFunctionArgs<RouterContextProvider>\n    | ActionFunctionArgs<RouterContextProvider>\n  ) & {\n    // Don't use `DataStrategyFunctionArgs` directly so we can we reduce these\n    // back from `DataStrategyMatch` to regular matches for use in the staticHandler\n    matches: AgnosticDataRouteMatch[];\n  },\n  // Handler to generate a Result in the leaf next() function\n  handler: () => Promise<Result>,\n  // Post-process any results returned from middlewares, used to upgrade returned\n  // data() values to Responses on the server\n  processResult: (r: Result) => Result,\n  // A type predicate function to check if the values returned by the user without\n  // a next() call is of the proper type.  If so we use it directly, otherwise we\n  // call next() for them.\n  isResult: (v: unknown) => v is Result,\n  // Handle errors thrown by a middleware and return a Result to bubble up through\n  // the parent next() call\n  errorHandler: (\n    error: unknown,\n    routeId: string,\n    nextResult: { value: Result } | undefined,\n  ) => Promise<Result>,\n): Promise<Result> {\n  let { matches, request, params, context, unstable_pattern } = args;\n  let tuples = matches.flatMap((m) =>\n    m.route.middleware ? m.route.middleware.map((fn) => [m.route.id, fn]) : [],\n  ) as [string, MiddlewareFunction<Result>][];\n\n  let result = await callRouteMiddleware(\n    {\n      request,\n      params,\n      context,\n      unstable_pattern,\n    },\n    tuples,\n    handler,\n    processResult,\n    isResult,\n    errorHandler,\n  );\n  return result;\n}\n\nasync function callRouteMiddleware<Result>(\n  args:\n    | LoaderFunctionArgs<RouterContextProvider>\n    | ActionFunctionArgs<RouterContextProvider>,\n  middlewares: [string, MiddlewareFunction<Result>][],\n  handler: () => Promise<Result>,\n  processResult: (r: Result) => Result,\n  isResult: (v: unknown) => v is Result,\n  errorHandler: (\n    error: unknown,\n    routeId: string,\n    pendingResult: { value: Result } | undefined,\n  ) => Promise<Result>,\n  idx = 0,\n): Promise<Result> {\n  let { request } = args;\n  if (request.signal.aborted) {\n    throw (\n      request.signal.reason ??\n      new Error(`Request aborted: ${request.method} ${request.url}`)\n    );\n  }\n\n  let tuple = middlewares[idx];\n  if (!tuple) {\n    // We reached the end of our middlewares, call the handler\n    let result = await handler();\n    return result;\n  }\n\n  let [routeId, middleware] = tuple;\n  let nextResult: { value: Result } | undefined;\n  let next: MiddlewareNextFunction<Result> = async () => {\n    if (nextResult) {\n      throw new Error(\"You may only call `next()` once per middleware\");\n    }\n\n    try {\n      let result = await callRouteMiddleware(\n        args,\n        middlewares,\n        handler,\n        processResult,\n        isResult,\n        errorHandler,\n        idx + 1,\n      );\n\n      nextResult = { value: result };\n      return nextResult.value;\n    } catch (error) {\n      nextResult = { value: await errorHandler(error, routeId, nextResult) };\n      return nextResult.value;\n    }\n  };\n\n  try {\n    let value = await middleware(args, next);\n    let result = value != null ? processResult(value) : undefined;\n\n    if (isResult(result)) {\n      // Use short circuit values of the proper type without having called next()\n      return result;\n    } else if (nextResult) {\n      // If they called next() but didn't return the response, we can bubble\n      // it for them. This allows some minor syntactic sugar (or forgetfulness)\n      // where you can grab the response to add a header without re-returning it\n      return result ?? nextResult.value;\n    } else {\n      // Otherwise call next() for them\n      nextResult = { value: await next() };\n      return nextResult.value;\n    }\n  } catch (error) {\n    let response = await errorHandler(error, routeId, nextResult);\n    return response;\n  }\n}\n\nfunction getDataStrategyMatchLazyPromises(\n  mapRouteProperties: MapRoutePropertiesFunction,\n  manifest: RouteManifest,\n  request: Request,\n  match: DataRouteMatch,\n  lazyRoutePropertiesToSkip: string[],\n): DataStrategyMatch[\"_lazyPromises\"] {\n  let lazyMiddlewarePromise = loadLazyRouteProperty({\n    key: \"middleware\",\n    route: match.route,\n    manifest,\n    mapRouteProperties,\n  });\n\n  let lazyRoutePromises = loadLazyRoute(\n    match.route,\n    isMutationMethod(request.method) ? \"action\" : \"loader\",\n    manifest,\n    mapRouteProperties,\n    lazyRoutePropertiesToSkip,\n  );\n\n  return {\n    middleware: lazyMiddlewarePromise,\n    route: lazyRoutePromises.lazyRoutePromise,\n    handler: lazyRoutePromises.lazyHandlerPromise,\n  };\n}\n\nfunction getDataStrategyMatch(\n  mapRouteProperties: MapRoutePropertiesFunction,\n  manifest: RouteManifest,\n  request: Request,\n  unstable_pattern: string,\n  match: DataRouteMatch,\n  lazyRoutePropertiesToSkip: string[],\n  scopedContext: unknown,\n  shouldLoad: boolean,\n  shouldRevalidateArgs: DataStrategyMatch[\"shouldRevalidateArgs\"] = null,\n  callSiteDefaultShouldRevalidate?: boolean,\n): DataStrategyMatch {\n  // The hope here is to avoid a breaking change to the resolve behavior.\n  // Opt-ing into the `shouldCallHandler` API changes some nuanced behavior\n  // around when resolve calls through to the handler\n  let isUsingNewApi = false;\n\n  let _lazyPromises = getDataStrategyMatchLazyPromises(\n    mapRouteProperties,\n    manifest,\n    request,\n    match,\n    lazyRoutePropertiesToSkip,\n  );\n\n  return {\n    ...match,\n    _lazyPromises,\n    shouldLoad,\n    shouldRevalidateArgs,\n    shouldCallHandler(defaultShouldRevalidate) {\n      isUsingNewApi = true;\n      if (!shouldRevalidateArgs) {\n        return shouldLoad;\n      }\n\n      if (typeof callSiteDefaultShouldRevalidate === \"boolean\") {\n        return shouldRevalidateLoader(match, {\n          ...shouldRevalidateArgs,\n          defaultShouldRevalidate: callSiteDefaultShouldRevalidate,\n        });\n      }\n\n      if (typeof defaultShouldRevalidate === \"boolean\") {\n        return shouldRevalidateLoader(match, {\n          ...shouldRevalidateArgs,\n          defaultShouldRevalidate,\n        });\n      }\n\n      return shouldRevalidateLoader(match, shouldRevalidateArgs);\n    },\n    resolve(handlerOverride) {\n      let { lazy, loader, middleware } = match.route;\n\n      let callHandler =\n        isUsingNewApi ||\n        shouldLoad ||\n        (handlerOverride &&\n          !isMutationMethod(request.method) &&\n          (lazy || loader));\n\n      // For GET requests, if this match was marked `shouldLoad` due to a\n      // middleware and it doesn't have a `loader` to run and no `lazy` to add\n      // one, then we can just return undefined from the \"loader\" here\n      let isMiddlewareOnlyRoute =\n        middleware && middleware.length > 0 && !loader && !lazy;\n\n      if (\n        callHandler &&\n        (isMutationMethod(request.method) || !isMiddlewareOnlyRoute)\n      ) {\n        return callLoaderOrAction({\n          request,\n          unstable_pattern,\n          match,\n          lazyHandlerPromise: _lazyPromises?.handler,\n          lazyRoutePromise: _lazyPromises?.route,\n          handlerOverride,\n          scopedContext,\n        });\n      }\n      return Promise.resolve({ type: ResultType.data, result: undefined });\n    },\n  };\n}\n\nfunction getTargetedDataStrategyMatches(\n  mapRouteProperties: MapRoutePropertiesFunction,\n  manifest: RouteManifest,\n  request: Request,\n  matches: AgnosticDataRouteMatch[],\n  targetMatch: AgnosticDataRouteMatch,\n  lazyRoutePropertiesToSkip: string[],\n  scopedContext: unknown,\n  shouldRevalidateArgs: DataStrategyMatch[\"shouldRevalidateArgs\"] = null,\n): DataStrategyMatch[] {\n  return matches.map((match) => {\n    if (match.route.id !== targetMatch.route.id) {\n      // We don't use getDataStrategyMatch here because these are for actions/fetchers\n      // where we should _never_ call the handler for any matches other than the target\n      return {\n        ...match,\n        shouldLoad: false,\n        shouldRevalidateArgs: shouldRevalidateArgs,\n        shouldCallHandler: () => false,\n        _lazyPromises: getDataStrategyMatchLazyPromises(\n          mapRouteProperties,\n          manifest,\n          request,\n          match,\n          lazyRoutePropertiesToSkip,\n        ),\n        resolve: () => Promise.resolve({ type: \"data\", result: undefined }),\n      };\n    }\n\n    return getDataStrategyMatch(\n      mapRouteProperties,\n      manifest,\n      request,\n      getRoutePattern(matches),\n      match,\n      lazyRoutePropertiesToSkip,\n      scopedContext,\n      true,\n      shouldRevalidateArgs,\n    );\n  });\n}\n\nasync function callDataStrategyImpl(\n  dataStrategyImpl: DataStrategyFunction<unknown>,\n  request: Request,\n  matches: DataStrategyMatch[],\n  fetcherKey: string | null,\n  scopedContext: unknown,\n  isStaticHandler: boolean,\n): Promise<Record<string, DataStrategyResult>> {\n  // Ensure all middleware is loaded before we start executing routes\n  if (matches.some((m) => m._lazyPromises?.middleware)) {\n    await Promise.all(matches.map((m) => m._lazyPromises?.middleware));\n  }\n\n  // Send all matches here to allow for a middleware-type implementation.\n  // handler will be a no-op for unneeded routes and we filter those results\n  // back out below.\n  let dataStrategyArgs = {\n    request,\n    unstable_pattern: getRoutePattern(matches),\n    params: matches[0].params,\n    context: scopedContext,\n    matches,\n  };\n  let runClientMiddleware = isStaticHandler\n    ? () => {\n        throw new Error(\n          \"You cannot call `runClientMiddleware()` from a static handler \" +\n            \"`dataStrategy`. Middleware is run outside of `dataStrategy` during \" +\n            \"SSR in order to bubble up the Response.  You can enable middleware \" +\n            \"via the `respond` API in `query`/`queryRoute`\",\n        );\n      }\n    : (cb: DataStrategyFunction<RouterContextProvider>) => {\n        let typedDataStrategyArgs =\n          dataStrategyArgs as DataStrategyFunctionArgs<RouterContextProvider>;\n        return runClientMiddlewarePipeline(typedDataStrategyArgs, () => {\n          return cb({\n            ...typedDataStrategyArgs,\n            fetcherKey,\n            runClientMiddleware: () => {\n              throw new Error(\n                \"Cannot call `runClientMiddleware()` from within an \" +\n                  \"`runClientMiddleware` handler\",\n              );\n            },\n          });\n        });\n      };\n\n  let results = await dataStrategyImpl({\n    ...dataStrategyArgs,\n    fetcherKey,\n    runClientMiddleware,\n  });\n\n  // Wait for all routes to load here but swallow the error since we want\n  // it to bubble up from the `await loadRoutePromise` in `callLoaderOrAction` -\n  // called from `match.resolve()`. We also ensure that all promises are\n  // awaited so that we don't inadvertently leave any hanging promises.\n  try {\n    await Promise.all(\n      matches.flatMap((m) => [\n        m._lazyPromises?.handler,\n        m._lazyPromises?.route,\n      ]),\n    );\n  } catch (e) {\n    // No-op\n  }\n\n  return results;\n}\n\n// Default logic for calling a loader/action is the user has no specified a dataStrategy\nasync function callLoaderOrAction({\n  request,\n  unstable_pattern,\n  match,\n  lazyHandlerPromise,\n  lazyRoutePromise,\n  handlerOverride,\n  scopedContext,\n}: {\n  request: Request;\n  unstable_pattern: string;\n  match: AgnosticDataRouteMatch;\n  lazyHandlerPromise: Promise<void> | undefined;\n  lazyRoutePromise: Promise<void> | undefined;\n  handlerOverride: Parameters<DataStrategyMatch[\"resolve\"]>[0];\n  scopedContext: unknown;\n}): Promise<DataStrategyResult> {\n  let result: DataStrategyResult;\n  let onReject: (() => void) | undefined;\n  let isAction = isMutationMethod(request.method);\n  let type = isAction ? \"action\" : \"loader\";\n  let runHandler = (\n    handler: boolean | LoaderFunction<unknown> | ActionFunction<unknown>,\n  ): Promise<DataStrategyResult> => {\n    // Setup a promise we can race against so that abort signals short circuit\n    let reject: () => void;\n    // This will never resolve so safe to type it as Promise<DataStrategyResult> to\n    // satisfy the function return value\n    let abortPromise = new Promise<DataStrategyResult>((_, r) => (reject = r));\n    onReject = () => reject();\n    request.signal.addEventListener(\"abort\", onReject);\n\n    let actualHandler = (ctx?: unknown) => {\n      if (typeof handler !== \"function\") {\n        return Promise.reject(\n          new Error(\n            `You cannot call the handler for a route which defines a boolean ` +\n              `\"${type}\" [routeId: ${match.route.id}]`,\n          ),\n        );\n      }\n      return handler(\n        {\n          request,\n          unstable_pattern,\n          params: match.params,\n          context: scopedContext,\n        },\n        ...(ctx !== undefined ? [ctx] : []),\n      );\n    };\n\n    let handlerPromise: Promise<DataStrategyResult> = (async () => {\n      try {\n        let val = await (handlerOverride\n          ? handlerOverride((ctx: unknown) => actualHandler(ctx))\n          : actualHandler());\n        return { type: \"data\", result: val };\n      } catch (e) {\n        return { type: \"error\", result: e };\n      }\n    })();\n\n    return Promise.race([handlerPromise, abortPromise]);\n  };\n\n  try {\n    let handler = isAction ? match.route.action : match.route.loader;\n\n    // If we have a promise for a lazy route, await that first\n    if (lazyHandlerPromise || lazyRoutePromise) {\n      if (handler) {\n        // Run statically defined handler in parallel with lazy route loading\n        let handlerError;\n        let [value] = await Promise.all([\n          // If the handler throws, don't let it immediately bubble out,\n          // since we need to let the lazy() execution finish so we know if this\n          // route has a boundary that can handle the error\n          runHandler(handler).catch((e) => {\n            handlerError = e;\n          }),\n          // Ensure all lazy route promises are resolved before continuing\n          lazyHandlerPromise,\n          lazyRoutePromise,\n        ]);\n        if (handlerError !== undefined) {\n          throw handlerError;\n        }\n        result = value!;\n      } else {\n        // Load lazy loader/action before running it\n        await lazyHandlerPromise;\n\n        let handler = isAction ? match.route.action : match.route.loader;\n        if (handler) {\n          // Handler still runs even if we got interrupted to maintain consistency\n          // with un-abortable behavior of handler execution on non-lazy or\n          // previously-lazy-loaded routes. We also ensure all lazy route\n          // promises are resolved before continuing.\n          [result] = await Promise.all([runHandler(handler), lazyRoutePromise]);\n        } else if (type === \"action\") {\n          let url = new URL(request.url);\n          let pathname = url.pathname + url.search;\n          throw getInternalRouterError(405, {\n            method: request.method,\n            pathname,\n            routeId: match.route.id,\n          });\n        } else {\n          // lazy route has no loader to run.  Short circuit here so we don't\n          // hit the invariant below that errors on returning undefined.\n          return { type: ResultType.data, result: undefined };\n        }\n      }\n    } else if (!handler) {\n      let url = new URL(request.url);\n      let pathname = url.pathname + url.search;\n      throw getInternalRouterError(404, {\n        pathname,\n      });\n    } else {\n      result = await runHandler(handler);\n    }\n  } catch (e) {\n    // We should already be catching and converting normal handler executions to\n    // Record<string, DataStrategyResult> and returning them, so anything that throws here is an\n    // unexpected error we still need to wrap\n    return { type: ResultType.error, result: e };\n  } finally {\n    if (onReject) {\n      request.signal.removeEventListener(\"abort\", onReject);\n    }\n  }\n\n  return result;\n}\n\nasync function parseResponseBody(response: Response) {\n  let contentType = response.headers.get(\"Content-Type\");\n\n  // Check between word boundaries instead of startsWith() due to the last\n  // paragraph of https://httpwg.org/specs/rfc9110.html#field.content-type\n  if (contentType && /\\bapplication\\/json\\b/.test(contentType)) {\n    return response.body == null ? null : response.json();\n  }\n\n  return response.text();\n}\n\nasync function convertDataStrategyResultToDataResult(\n  dataStrategyResult: DataStrategyResult,\n): Promise<DataResult> {\n  let { result, type } = dataStrategyResult;\n\n  if (isResponse(result)) {\n    let data: any;\n\n    try {\n      data = await parseResponseBody(result);\n    } catch (e) {\n      return { type: ResultType.error, error: e };\n    }\n\n    if (type === ResultType.error) {\n      return {\n        type: ResultType.error,\n        error: new ErrorResponseImpl(result.status, result.statusText, data),\n        statusCode: result.status,\n        headers: result.headers,\n      };\n    }\n\n    return {\n      type: ResultType.data,\n      data,\n      statusCode: result.status,\n      headers: result.headers,\n    };\n  }\n\n  if (type === ResultType.error) {\n    if (isDataWithResponseInit(result)) {\n      if (result.data instanceof Error) {\n        return {\n          type: ResultType.error,\n          error: result.data,\n          statusCode: result.init?.status,\n          headers: result.init?.headers\n            ? new Headers(result.init.headers)\n            : undefined,\n        };\n      }\n\n      // Convert thrown data() to ErrorResponse instances\n      return {\n        type: ResultType.error,\n        error: dataWithResponseInitToErrorResponse(result),\n        statusCode: isRouteErrorResponse(result) ? result.status : undefined,\n        headers: result.init?.headers\n          ? new Headers(result.init.headers)\n          : undefined,\n      };\n    }\n\n    return {\n      type: ResultType.error,\n      error: result,\n      statusCode: isRouteErrorResponse(result) ? result.status : undefined,\n    };\n  }\n\n  if (isDataWithResponseInit(result)) {\n    return {\n      type: ResultType.data,\n      data: result.data,\n      statusCode: result.init?.status,\n      headers: result.init?.headers\n        ? new Headers(result.init.headers)\n        : undefined,\n    };\n  }\n\n  return { type: ResultType.data, data: result };\n}\n\n// Support relative routing in internal redirects\nfunction normalizeRelativeRoutingRedirectResponse(\n  response: Response,\n  request: Request,\n  routeId: string,\n  matches: AgnosticDataRouteMatch[],\n  basename: string,\n) {\n  let location = response.headers.get(\"Location\");\n  invariant(\n    location,\n    \"Redirects returned/thrown from loaders/actions must have a Location header\",\n  );\n\n  if (!isAbsoluteUrl(location)) {\n    let trimmedMatches = matches.slice(\n      0,\n      matches.findIndex((m) => m.route.id === routeId) + 1,\n    );\n    location = normalizeTo(\n      new URL(request.url),\n      trimmedMatches,\n      basename,\n      location,\n    );\n    response.headers.set(\"Location\", location);\n  }\n\n  return response;\n}\n\nfunction normalizeRedirectLocation(\n  location: string,\n  currentUrl: URL,\n  basename: string,\n  historyInstance: History,\n): string {\n  // Match Chrome's behavior:\n  // https://github.com/chromium/chromium/blob/216dbeb61db0c667e62082e5f5400a32d6983df3/content/public/common/url_utils.cc#L82\n  let invalidProtocols = [\n    \"about:\",\n    \"blob:\",\n    \"chrome:\",\n    \"chrome-untrusted:\",\n    \"content:\",\n    \"data:\",\n    \"devtools:\",\n    \"file:\",\n    \"filesystem:\",\n    // eslint-disable-next-line no-script-url\n    \"javascript:\",\n  ];\n\n  if (isAbsoluteUrl(location)) {\n    // Strip off the protocol+origin for same-origin + same-basename absolute redirects\n    let normalizedLocation = location;\n    let url = normalizedLocation.startsWith(\"//\")\n      ? new URL(currentUrl.protocol + normalizedLocation)\n      : new URL(normalizedLocation);\n    if (invalidProtocols.includes(url.protocol)) {\n      throw new Error(\"Invalid redirect location\");\n    }\n    let isSameBasename = stripBasename(url.pathname, basename) != null;\n    if (url.origin === currentUrl.origin && isSameBasename) {\n      return url.pathname + url.search + url.hash;\n    }\n  }\n\n  try {\n    let url = historyInstance.createURL(location);\n    if (invalidProtocols.includes(url.protocol)) {\n      throw new Error(\"Invalid redirect location\");\n    }\n  } catch (e) {}\n\n  return location;\n}\n\n// Utility method for creating the Request instances for loaders/actions during\n// client-side navigations and fetches.  During SSR we will always have a\n// Request instance from the static handler (query/queryRoute)\nfunction createClientSideRequest(\n  history: History,\n  location: string | Location,\n  signal: AbortSignal,\n  submission?: Submission,\n): Request {\n  let url = history.createURL(stripHashFromPath(location)).toString();\n  let init: RequestInit = { signal };\n\n  if (submission && isMutationMethod(submission.formMethod)) {\n    let { formMethod, formEncType } = submission;\n    // Didn't think we needed this but it turns out unlike other methods, patch\n    // won't be properly normalized to uppercase and results in a 405 error.\n    // See: https://fetch.spec.whatwg.org/#concept-method\n    init.method = formMethod.toUpperCase();\n\n    if (formEncType === \"application/json\") {\n      init.headers = new Headers({ \"Content-Type\": formEncType });\n      init.body = JSON.stringify(submission.json);\n    } else if (formEncType === \"text/plain\") {\n      // Content-Type is inferred (https://fetch.spec.whatwg.org/#dom-request)\n      init.body = submission.text;\n    } else if (\n      formEncType === \"application/x-www-form-urlencoded\" &&\n      submission.formData\n    ) {\n      // Content-Type is inferred (https://fetch.spec.whatwg.org/#dom-request)\n      init.body = convertFormDataToSearchParams(submission.formData);\n    } else {\n      // Content-Type is inferred (https://fetch.spec.whatwg.org/#dom-request)\n      init.body = submission.formData;\n    }\n  }\n\n  return new Request(url, init);\n}\n\nfunction convertFormDataToSearchParams(formData: FormData): URLSearchParams {\n  let searchParams = new URLSearchParams();\n\n  for (let [key, value] of formData.entries()) {\n    // https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#converting-an-entry-list-to-a-list-of-name-value-pairs\n    searchParams.append(key, typeof value === \"string\" ? value : value.name);\n  }\n\n  return searchParams;\n}\n\nfunction convertSearchParamsToFormData(\n  searchParams: URLSearchParams,\n): FormData {\n  let formData = new FormData();\n  for (let [key, value] of searchParams.entries()) {\n    formData.append(key, value);\n  }\n  return formData;\n}\n\nfunction processRouteLoaderData(\n  matches: AgnosticDataRouteMatch[],\n  results: Record<string, DataResult>,\n  pendingActionResult: PendingActionResult | undefined,\n  isStaticHandler = false,\n  skipLoaderErrorBubbling = false,\n): {\n  loaderData: RouterState[\"loaderData\"];\n  errors: RouterState[\"errors\"] | null;\n  statusCode: number;\n  loaderHeaders: Record<string, Headers>;\n} {\n  // Fill in loaderData/errors from our loaders\n  let loaderData: RouterState[\"loaderData\"] = {};\n  let errors: RouterState[\"errors\"] | null = null;\n  let statusCode: number | undefined;\n  let foundError = false;\n  let loaderHeaders: Record<string, Headers> = {};\n  let pendingError =\n    pendingActionResult && isErrorResult(pendingActionResult[1])\n      ? pendingActionResult[1].error\n      : undefined;\n\n  // Process loader results into state.loaderData/state.errors\n  matches.forEach((match) => {\n    if (!(match.route.id in results)) {\n      return;\n    }\n    let id = match.route.id;\n    let result = results[id];\n    invariant(\n      !isRedirectResult(result),\n      \"Cannot handle redirect results in processLoaderData\",\n    );\n    if (isErrorResult(result)) {\n      let error = result.error;\n      // If we have a pending action error, we report it at the highest-route\n      // that throws a loader error, and then clear it out to indicate that\n      // it was consumed\n      if (pendingError !== undefined) {\n        error = pendingError;\n        pendingError = undefined;\n      }\n\n      errors = errors || {};\n\n      if (skipLoaderErrorBubbling) {\n        errors[id] = error;\n      } else {\n        // Look upwards from the matched route for the closest ancestor error\n        // boundary, defaulting to the root match.  Prefer higher error values\n        // if lower errors bubble to the same boundary\n        let boundaryMatch = findNearestBoundary(matches, id);\n        if (errors[boundaryMatch.route.id] == null) {\n          errors[boundaryMatch.route.id] = error;\n        }\n      }\n\n      // Clear our any prior loaderData for the throwing route\n      if (!isStaticHandler) {\n        loaderData[id] = ResetLoaderDataSymbol;\n      }\n\n      // Once we find our first (highest) error, we set the status code and\n      // prevent deeper status codes from overriding\n      if (!foundError) {\n        foundError = true;\n        statusCode = isRouteErrorResponse(result.error)\n          ? result.error.status\n          : 500;\n      }\n      if (result.headers) {\n        loaderHeaders[id] = result.headers;\n      }\n    } else {\n      loaderData[id] = result.data;\n      // Error status codes always override success status codes, but if all\n      // loaders are successful we take the deepest status code.\n      if (result.statusCode && result.statusCode !== 200 && !foundError) {\n        statusCode = result.statusCode;\n      }\n      if (result.headers) {\n        loaderHeaders[id] = result.headers;\n      }\n    }\n  });\n\n  // If we didn't consume the pending action error (i.e., all loaders\n  // resolved), then consume it here\n  if (pendingError !== undefined && pendingActionResult) {\n    errors = { [pendingActionResult[0]]: pendingError };\n    // Clear out any loaderData for the throwing route\n    if (pendingActionResult[2]) {\n      loaderData[pendingActionResult[2]] = undefined;\n    }\n  }\n\n  return {\n    loaderData,\n    errors,\n    statusCode: statusCode || 200,\n    loaderHeaders,\n  };\n}\n\nfunction processLoaderData(\n  state: RouterState,\n  matches: AgnosticDataRouteMatch[],\n  results: Record<string, DataResult>,\n  pendingActionResult: PendingActionResult | undefined,\n  revalidatingFetchers: RevalidatingFetcher[],\n  fetcherResults: Record<string, DataResult>,\n): {\n  loaderData: RouterState[\"loaderData\"];\n  errors?: RouterState[\"errors\"];\n} {\n  let { loaderData, errors } = processRouteLoaderData(\n    matches,\n    results,\n    pendingActionResult,\n  );\n\n  // Process results from our revalidating fetchers\n  revalidatingFetchers\n    // Keep those with no matches so we can bubble their 404's, otherwise only\n    // process fetchers that needed to load\n    .filter((f) => !f.matches || f.matches.some((m) => m.shouldLoad))\n    .forEach((rf) => {\n      let { key, match, controller } = rf;\n      if (controller && controller.signal.aborted) {\n        // Nothing to do for aborted fetchers\n        return;\n      }\n\n      let result = fetcherResults[key];\n      invariant(result, \"Did not find corresponding fetcher result\");\n\n      // Process fetcher non-redirect errors\n      if (isErrorResult(result)) {\n        let boundaryMatch = findNearestBoundary(state.matches, match?.route.id);\n        if (!(errors && errors[boundaryMatch.route.id])) {\n          errors = {\n            ...errors,\n            [boundaryMatch.route.id]: result.error,\n          };\n        }\n        state.fetchers.delete(key);\n      } else if (isRedirectResult(result)) {\n        // Should never get here, redirects should get processed above, but we\n        // keep this to type narrow to a success result in the else\n        invariant(false, \"Unhandled fetcher revalidation redirect\");\n      } else {\n        let doneFetcher = getDoneFetcher(result.data);\n        state.fetchers.set(key, doneFetcher);\n      }\n    });\n\n  return { loaderData, errors };\n}\n\nfunction mergeLoaderData(\n  loaderData: RouteData,\n  newLoaderData: RouteData,\n  matches: AgnosticDataRouteMatch[],\n  errors: RouteData | null | undefined,\n): RouteData {\n  // Start with all new entries that are not being reset\n  let mergedLoaderData = Object.entries(newLoaderData)\n    .filter(([, v]) => v !== ResetLoaderDataSymbol)\n    .reduce((merged, [k, v]) => {\n      merged[k] = v;\n      return merged;\n    }, {} as RouteData);\n\n  // Preserve existing `loaderData` for routes not included in `newLoaderData` and\n  // where a loader wasn't removed by HMR\n  for (let match of matches) {\n    let id = match.route.id;\n    if (\n      !newLoaderData.hasOwnProperty(id) &&\n      loaderData.hasOwnProperty(id) &&\n      match.route.loader\n    ) {\n      mergedLoaderData[id] = loaderData[id];\n    }\n\n    if (errors && errors.hasOwnProperty(id)) {\n      // Don't keep any loader data below the boundary\n      break;\n    }\n  }\n  return mergedLoaderData;\n}\n\nfunction getActionDataForCommit(\n  pendingActionResult: PendingActionResult | undefined,\n) {\n  if (!pendingActionResult) {\n    return {};\n  }\n  return isErrorResult(pendingActionResult[1])\n    ? {\n        // Clear out prior actionData on errors\n        actionData: {},\n      }\n    : {\n        actionData: {\n          [pendingActionResult[0]]: pendingActionResult[1].data,\n        },\n      };\n}\n\n// Find the nearest error boundary, looking upwards from the leaf route (or the\n// route specified by routeId) for the closest ancestor error boundary,\n// defaulting to the root match\nfunction findNearestBoundary(\n  matches: AgnosticDataRouteMatch[],\n  routeId?: string,\n): AgnosticDataRouteMatch {\n  let eligibleMatches = routeId\n    ? matches.slice(0, matches.findIndex((m) => m.route.id === routeId) + 1)\n    : [...matches];\n  return (\n    eligibleMatches.reverse().find((m) => m.route.hasErrorBoundary === true) ||\n    matches[0]\n  );\n}\n\nfunction getShortCircuitMatches(routes: AgnosticDataRouteObject[]): {\n  matches: AgnosticDataRouteMatch[];\n  route: AgnosticDataRouteObject;\n} {\n  // Prefer a root layout route if present, otherwise shim in a route object\n  let route =\n    routes.length === 1\n      ? routes[0]\n      : routes.find((r) => r.index || !r.path || r.path === \"/\") || {\n          id: `__shim-error-route__`,\n        };\n\n  return {\n    matches: [\n      {\n        params: {},\n        pathname: \"\",\n        pathnameBase: \"\",\n        route,\n      },\n    ],\n    route,\n  };\n}\n\nfunction getInternalRouterError(\n  status: number,\n  {\n    pathname,\n    routeId,\n    method,\n    type,\n    message,\n  }: {\n    pathname?: string;\n    routeId?: string;\n    method?: string;\n    type?: \"invalid-body\";\n    message?: string;\n  } = {},\n) {\n  let statusText = \"Unknown Server Error\";\n  let errorMessage = \"Unknown @remix-run/router error\";\n\n  if (status === 400) {\n    statusText = \"Bad Request\";\n    if (method && pathname && routeId) {\n      errorMessage =\n        `You made a ${method} request to \"${pathname}\" but ` +\n        `did not provide a \\`loader\\` for route \"${routeId}\", ` +\n        `so there is no way to handle the request.`;\n    } else if (type === \"invalid-body\") {\n      errorMessage = \"Unable to encode submission body\";\n    }\n  } else if (status === 403) {\n    statusText = \"Forbidden\";\n    errorMessage = `Route \"${routeId}\" does not match URL \"${pathname}\"`;\n  } else if (status === 404) {\n    statusText = \"Not Found\";\n    errorMessage = `No route matches URL \"${pathname}\"`;\n  } else if (status === 405) {\n    statusText = \"Method Not Allowed\";\n    if (method && pathname && routeId) {\n      errorMessage =\n        `You made a ${method.toUpperCase()} request to \"${pathname}\" but ` +\n        `did not provide an \\`action\\` for route \"${routeId}\", ` +\n        `so there is no way to handle the request.`;\n    } else if (method) {\n      errorMessage = `Invalid request method \"${method.toUpperCase()}\"`;\n    }\n  }\n\n  return new ErrorResponseImpl(\n    status || 500,\n    statusText,\n    new Error(errorMessage),\n    true,\n  );\n}\n\n// Find any returned redirect errors, starting from the lowest match\nfunction findRedirect(\n  results: Record<string, DataResult>,\n): { key: string; result: RedirectResult } | undefined {\n  let entries = Object.entries(results);\n  for (let i = entries.length - 1; i >= 0; i--) {\n    let [key, result] = entries[i];\n    if (isRedirectResult(result)) {\n      return { key, result };\n    }\n  }\n}\n\nfunction stripHashFromPath(path: To) {\n  let parsedPath = typeof path === \"string\" ? parsePath(path) : path;\n  return createPath({ ...parsedPath, hash: \"\" });\n}\n\nfunction isHashChangeOnly(a: Location, b: Location): boolean {\n  if (a.pathname !== b.pathname || a.search !== b.search) {\n    return false;\n  }\n\n  if (a.hash === \"\") {\n    // /page -> /page#hash\n    return b.hash !== \"\";\n  } else if (a.hash === b.hash) {\n    // /page#hash -> /page#hash\n    return true;\n  } else if (b.hash !== \"\") {\n    // /page#hash -> /page#other\n    return true;\n  }\n\n  // If the hash is removed the browser will re-perform a request to the server\n  // /page#hash -> /page\n  return false;\n}\n\nfunction dataWithResponseInitToResponse<D>(\n  data: DataWithResponseInit<D>,\n): Response {\n  return Response.json(data.data, data.init ?? undefined);\n}\n\nfunction dataWithResponseInitToErrorResponse<D>(\n  data: DataWithResponseInit<D>,\n): ErrorResponseImpl {\n  return new ErrorResponseImpl(\n    data.init?.status ?? 500,\n    data.init?.statusText ?? \"Internal Server Error\",\n    data.data,\n  );\n}\n\nfunction isDataStrategyResults(\n  result: unknown,\n): result is Record<string, DataStrategyResult> {\n  return (\n    result != null &&\n    typeof result === \"object\" &&\n    Object.entries(result).every(\n      ([key, value]) => typeof key === \"string\" && isDataStrategyResult(value),\n    )\n  );\n}\n\nfunction isDataStrategyResult(result: unknown): result is DataStrategyResult {\n  return (\n    result != null &&\n    typeof result === \"object\" &&\n    \"type\" in result &&\n    \"result\" in result &&\n    (result.type === ResultType.data || result.type === ResultType.error)\n  );\n}\n\nfunction isRedirectDataStrategyResult(result: DataStrategyResult) {\n  return (\n    isResponse(result.result) && redirectStatusCodes.has(result.result.status)\n  );\n}\nfunction isErrorResult(result: DataResult): result is ErrorResult {\n  return result.type === ResultType.error;\n}\n\nfunction isRedirectResult(result?: DataResult): result is RedirectResult {\n  return (result && result.type) === ResultType.redirect;\n}\n\nexport function isDataWithResponseInit(\n  value: any,\n): value is DataWithResponseInit<unknown> {\n  return (\n    typeof value === \"object\" &&\n    value != null &&\n    \"type\" in value &&\n    \"data\" in value &&\n    \"init\" in value &&\n    value.type === \"DataWithResponseInit\"\n  );\n}\n\nexport function isResponse(value: any): value is Response {\n  return (\n    value != null &&\n    typeof value.status === \"number\" &&\n    typeof value.statusText === \"string\" &&\n    typeof value.headers === \"object\" &&\n    typeof value.body !== \"undefined\"\n  );\n}\n\nexport function isRedirectStatusCode(statusCode: number): boolean {\n  return redirectStatusCodes.has(statusCode);\n}\n\nexport function isRedirectResponse(result: any): result is Response {\n  return (\n    isResponse(result) &&\n    isRedirectStatusCode(result.status) &&\n    result.headers.has(\"Location\")\n  );\n}\n\nfunction isValidMethod(method: string): method is FormMethod {\n  return validRequestMethods.has(method.toUpperCase() as FormMethod);\n}\n\nexport function isMutationMethod(method: string): method is MutationFormMethod {\n  // TODO: This should probably check against GET and HEAD, and consider any other\n  // method, including non-standard methods as mutations. We should also consider\n  // allowing \"non-standard\" method through, right now we 405 on anything non-standard.\n  return validMutationMethods.has(method.toUpperCase() as MutationFormMethod);\n}\n\nfunction hasNakedIndexQuery(search: string): boolean {\n  return new URLSearchParams(search).getAll(\"index\").some((v) => v === \"\");\n}\n\nfunction getTargetMatch(\n  matches: AgnosticDataRouteMatch[],\n  location: Location | string,\n) {\n  let search =\n    typeof location === \"string\" ? parsePath(location).search : location.search;\n  if (\n    matches[matches.length - 1].route.index &&\n    hasNakedIndexQuery(search || \"\")\n  ) {\n    // Return the leaf index route when index is present\n    return matches[matches.length - 1];\n  }\n  // Otherwise grab the deepest \"path contributing\" match (ignoring index and\n  // pathless layout routes)\n  let pathMatches = getPathContributingMatches(matches);\n  return pathMatches[pathMatches.length - 1];\n}\n\nfunction getSubmissionFromNavigation(\n  navigation: Navigation,\n): Submission | undefined {\n  let { formMethod, formAction, formEncType, text, formData, json } =\n    navigation;\n  if (!formMethod || !formAction || !formEncType) {\n    return;\n  }\n\n  if (text != null) {\n    return {\n      formMethod,\n      formAction,\n      formEncType,\n      formData: undefined,\n      json: undefined,\n      text,\n    };\n  } else if (formData != null) {\n    return {\n      formMethod,\n      formAction,\n      formEncType,\n      formData,\n      json: undefined,\n      text: undefined,\n    };\n  } else if (json !== undefined) {\n    return {\n      formMethod,\n      formAction,\n      formEncType,\n      formData: undefined,\n      json,\n      text: undefined,\n    };\n  }\n}\n\nfunction getLoadingNavigation(\n  location: Location,\n  submission?: Submission,\n): NavigationStates[\"Loading\"] {\n  if (submission) {\n    let navigation: NavigationStates[\"Loading\"] = {\n      state: \"loading\",\n      location,\n      formMethod: submission.formMethod,\n      formAction: submission.formAction,\n      formEncType: submission.formEncType,\n      formData: submission.formData,\n      json: submission.json,\n      text: submission.text,\n    };\n    return navigation;\n  } else {\n    let navigation: NavigationStates[\"Loading\"] = {\n      state: \"loading\",\n      location,\n      formMethod: undefined,\n      formAction: undefined,\n      formEncType: undefined,\n      formData: undefined,\n      json: undefined,\n      text: undefined,\n    };\n    return navigation;\n  }\n}\n\nfunction getSubmittingNavigation(\n  location: Location,\n  submission: Submission,\n): NavigationStates[\"Submitting\"] {\n  let navigation: NavigationStates[\"Submitting\"] = {\n    state: \"submitting\",\n    location,\n    formMethod: submission.formMethod,\n    formAction: submission.formAction,\n    formEncType: submission.formEncType,\n    formData: submission.formData,\n    json: submission.json,\n    text: submission.text,\n  };\n  return navigation;\n}\n\nfunction getLoadingFetcher(\n  submission?: Submission,\n  data?: Fetcher[\"data\"],\n): FetcherStates[\"Loading\"] {\n  if (submission) {\n    let fetcher: FetcherStates[\"Loading\"] = {\n      state: \"loading\",\n      formMethod: submission.formMethod,\n      formAction: submission.formAction,\n      formEncType: submission.formEncType,\n      formData: submission.formData,\n      json: submission.json,\n      text: submission.text,\n      data,\n    };\n    return fetcher;\n  } else {\n    let fetcher: FetcherStates[\"Loading\"] = {\n      state: \"loading\",\n      formMethod: undefined,\n      formAction: undefined,\n      formEncType: undefined,\n      formData: undefined,\n      json: undefined,\n      text: undefined,\n      data,\n    };\n    return fetcher;\n  }\n}\n\nfunction getSubmittingFetcher(\n  submission: Submission,\n  existingFetcher?: Fetcher,\n): FetcherStates[\"Submitting\"] {\n  let fetcher: FetcherStates[\"Submitting\"] = {\n    state: \"submitting\",\n    formMethod: submission.formMethod,\n    formAction: submission.formAction,\n    formEncType: submission.formEncType,\n    formData: submission.formData,\n    json: submission.json,\n    text: submission.text,\n    data: existingFetcher ? existingFetcher.data : undefined,\n  };\n  return fetcher;\n}\n\nfunction getDoneFetcher(data: Fetcher[\"data\"]): FetcherStates[\"Idle\"] {\n  let fetcher: FetcherStates[\"Idle\"] = {\n    state: \"idle\",\n    formMethod: undefined,\n    formAction: undefined,\n    formEncType: undefined,\n    formData: undefined,\n    json: undefined,\n    text: undefined,\n    data,\n  };\n  return fetcher;\n}\n\nfunction restoreAppliedTransitions(\n  _window: Window,\n  transitions: Map<string, Set<string>>,\n) {\n  try {\n    let sessionPositions = _window.sessionStorage.getItem(\n      TRANSITIONS_STORAGE_KEY,\n    );\n    if (sessionPositions) {\n      let json = JSON.parse(sessionPositions);\n      for (let [k, v] of Object.entries(json || {})) {\n        if (v && Array.isArray(v)) {\n          transitions.set(k, new Set(v || []));\n        }\n      }\n    }\n  } catch (e) {\n    // no-op, use default empty object\n  }\n}\n\nfunction persistAppliedTransitions(\n  _window: Window,\n  transitions: Map<string, Set<string>>,\n) {\n  if (transitions.size > 0) {\n    let json: Record<string, string[]> = {};\n    for (let [k, v] of transitions) {\n      json[k] = [...v];\n    }\n    try {\n      _window.sessionStorage.setItem(\n        TRANSITIONS_STORAGE_KEY,\n        JSON.stringify(json),\n      );\n    } catch (error) {\n      warning(\n        false,\n        `Failed to save applied view transitions in sessionStorage (${error}).`,\n      );\n    }\n  }\n}\n\nfunction createDeferred<T = unknown>() {\n  let resolve: (val?: any) => Promise<void>;\n  let reject: (error?: Error) => Promise<void>;\n  let promise = new Promise<T>((res, rej) => {\n    resolve = async (val: T) => {\n      res(val);\n      try {\n        await promise;\n      } catch (e) {}\n    };\n    reject = async (error?: Error) => {\n      rej(error);\n      try {\n        await promise;\n      } catch (e) {}\n    };\n  });\n  return {\n    promise,\n    //@ts-ignore\n    resolve,\n    //@ts-ignore\n    reject,\n  };\n}\n//#endregion\n"
  },
  {
    "path": "packages/react-router/lib/router/utils.ts",
    "content": "import type { MiddlewareEnabled } from \"../types/future\";\nimport type { Equal, Expect } from \"../types/utils\";\nimport type { Location, Path, To } from \"./history\";\nimport { invariant, parsePath, warning } from \"./history\";\n\nexport type MaybePromise<T> = T | Promise<T>;\n\n/**\n * Map of routeId -> data returned from a loader/action/error\n */\nexport interface RouteData {\n  [routeId: string]: any;\n}\n\nexport enum ResultType {\n  data = \"data\",\n  redirect = \"redirect\",\n  error = \"error\",\n}\n\n/**\n * Successful result from a loader or action\n */\nexport interface SuccessResult {\n  type: ResultType.data;\n  data: unknown;\n  statusCode?: number;\n  headers?: Headers;\n}\n\n/**\n * Redirect result from a loader or action\n */\nexport interface RedirectResult {\n  type: ResultType.redirect;\n  // We keep the raw Response for redirects so we can return it verbatim\n  response: Response;\n}\n\n/**\n * Unsuccessful result from a loader or action\n */\nexport interface ErrorResult {\n  type: ResultType.error;\n  error: unknown;\n  statusCode?: number;\n  headers?: Headers;\n}\n\n/**\n * Result from a loader or action - potentially successful or unsuccessful\n */\nexport type DataResult = SuccessResult | RedirectResult | ErrorResult;\n\nexport type LowerCaseFormMethod = \"get\" | \"post\" | \"put\" | \"patch\" | \"delete\";\nexport type UpperCaseFormMethod = Uppercase<LowerCaseFormMethod>;\n\n/**\n * Users can specify either lowercase or uppercase form methods on `<Form>`,\n * useSubmit(), `<fetcher.Form>`, etc.\n */\nexport type HTMLFormMethod = LowerCaseFormMethod | UpperCaseFormMethod;\n\n/**\n * Active navigation/fetcher form methods are exposed in uppercase on the\n * RouterState. This is to align with the normalization done via fetch().\n */\nexport type FormMethod = UpperCaseFormMethod;\nexport type MutationFormMethod = Exclude<FormMethod, \"GET\">;\n\nexport type FormEncType =\n  | \"application/x-www-form-urlencoded\"\n  | \"multipart/form-data\"\n  | \"application/json\"\n  | \"text/plain\";\n\n// Thanks https://github.com/sindresorhus/type-fest!\ntype JsonObject = { [Key in string]: JsonValue } & {\n  [Key in string]?: JsonValue | undefined;\n};\ntype JsonArray = JsonValue[] | readonly JsonValue[];\ntype JsonPrimitive = string | number | boolean | null;\ntype JsonValue = JsonPrimitive | JsonObject | JsonArray;\n\n/**\n * @private\n * Internal interface to pass around for action submissions, not intended for\n * external consumption\n */\nexport type Submission =\n  | {\n      formMethod: FormMethod;\n      formAction: string;\n      formEncType: FormEncType;\n      formData: FormData;\n      json: undefined;\n      text: undefined;\n    }\n  | {\n      formMethod: FormMethod;\n      formAction: string;\n      formEncType: FormEncType;\n      formData: undefined;\n      json: JsonValue;\n      text: undefined;\n    }\n  | {\n      formMethod: FormMethod;\n      formAction: string;\n      formEncType: FormEncType;\n      formData: undefined;\n      json: undefined;\n      text: string;\n    };\n\n/**\n * A context instance used as the key for the `get`/`set` methods of a\n * {@link RouterContextProvider}. Accepts an optional default\n * value to be returned if no value has been set.\n */\nexport interface RouterContext<T = unknown> {\n  defaultValue?: T;\n}\n\n/**\n * Creates a type-safe {@link RouterContext} object that can be used to\n * store and retrieve arbitrary values in [`action`](../../start/framework/route-module#action)s,\n * [`loader`](../../start/framework/route-module#loader)s, and [middleware](../../how-to/middleware).\n * Similar to React's [`createContext`](https://react.dev/reference/react/createContext),\n * but specifically designed for React Router's request/response lifecycle.\n *\n * If a `defaultValue` is provided, it will be returned from `context.get()`\n * when no value has been set for the context. Otherwise, reading this context\n * when no value has been set will throw an error.\n *\n * ```tsx filename=app/context.ts\n * import { createContext } from \"react-router\";\n *\n * // Create a context for user data\n * export const userContext =\n *   createContext<User | null>(null);\n * ```\n *\n * ```tsx filename=app/middleware/auth.ts\n * import { getUserFromSession } from \"~/auth.server\";\n * import { userContext } from \"~/context\";\n *\n * export const authMiddleware = async ({\n *   context,\n *   request,\n * }) => {\n *   const user = await getUserFromSession(request);\n *   context.set(userContext, user);\n * };\n * ```\n *\n * ```tsx filename=app/routes/profile.tsx\n * import { userContext } from \"~/context\";\n *\n * export async function loader({\n *   context,\n * }: Route.LoaderArgs) {\n *   const user = context.get(userContext);\n *\n *   if (!user) {\n *     throw new Response(\"Unauthorized\", { status: 401 });\n *   }\n *\n *   return { user };\n * }\n * ```\n *\n * @public\n * @category Utils\n * @mode framework\n * @mode data\n * @param defaultValue An optional default value for the context. This value\n * will be returned if no value has been set for this context.\n * @returns A {@link RouterContext} object that can be used with\n * `context.get()` and `context.set()` in [`action`](../../start/framework/route-module#action)s,\n * [`loader`](../../start/framework/route-module#loader)s, and [middleware](../../how-to/middleware).\n */\nexport function createContext<T>(defaultValue?: T): RouterContext<T> {\n  return { defaultValue };\n}\n\n/**\n * Provides methods for writing/reading values in application context in a\n * type-safe way. Primarily for usage with [middleware](../../how-to/middleware).\n *\n * @example\n * import {\n *   createContext,\n *   RouterContextProvider\n * } from \"react-router\";\n *\n * const userContext = createContext<User | null>(null);\n * const contextProvider = new RouterContextProvider();\n * contextProvider.set(userContext, getUser());\n * //                               ^ Type-safe\n * const user = contextProvider.get(userContext);\n * //    ^ User\n *\n * @public\n * @category Utils\n * @mode framework\n * @mode data\n */\nexport class RouterContextProvider {\n  #map = new Map<RouterContext, unknown>();\n\n  /**\n   * Create a new `RouterContextProvider` instance\n   * @param init An optional initial context map to populate the provider with\n   */\n  constructor(init?: Map<RouterContext, unknown>) {\n    if (init) {\n      for (let [context, value] of init) {\n        this.set(context, value);\n      }\n    }\n  }\n\n  /**\n   * Access a value from the context. If no value has been set for the context,\n   * it will return the context's `defaultValue` if provided, or throw an error\n   * if no `defaultValue` was set.\n   * @param context The context to get the value for\n   * @returns The value for the context, or the context's `defaultValue` if no\n   * value was set\n   */\n  get<T>(context: RouterContext<T>): T {\n    if (this.#map.has(context)) {\n      return this.#map.get(context) as T;\n    }\n\n    if (context.defaultValue !== undefined) {\n      return context.defaultValue;\n    }\n\n    throw new Error(\"No value found for context\");\n  }\n\n  /**\n   * Set a value for the context. If the context already has a value set, this\n   * will overwrite it.\n   *\n   * @param context The context to set the value for\n   * @param value The value to set for the context\n   * @returns {void}\n   */\n  set<C extends RouterContext>(\n    context: C,\n    value: C extends RouterContext<infer T> ? T : never,\n  ): void {\n    this.#map.set(context, value);\n  }\n}\n\ntype DefaultContext = MiddlewareEnabled extends true\n  ? Readonly<RouterContextProvider>\n  : any;\n\n/**\n * @private\n * Arguments passed to route loader/action functions.  Same for now but we keep\n * this as a private implementation detail in case they diverge in the future.\n */\ninterface DataFunctionArgs<Context> {\n  /** A {@link https://developer.mozilla.org/en-US/docs/Web/API/Request Fetch Request instance} which you can use to read headers (like cookies, and {@link https://developer.mozilla.org/en-US/docs/Web/API/URLSearchParams URLSearchParams} from the request. */\n  request: Request;\n  /**\n   * Matched un-interpolated route pattern for the current path (i.e., /blog/:slug).\n   * Mostly useful as a identifier to aggregate on for logging/tracing/etc.\n   */\n  unstable_pattern: string;\n  /**\n   * {@link https://reactrouter.com/start/framework/routing#dynamic-segments Dynamic route params} for the current route.\n   * @example\n   * // app/routes.ts\n   * route(\"teams/:teamId\", \"./team.tsx\"),\n   *\n   * // app/team.tsx\n   * export function loader({\n   *   params,\n   * }: Route.LoaderArgs) {\n   *   params.teamId;\n   *   //        ^ string\n   * }\n   */\n  params: Params;\n  /**\n   * This is the context passed in to your server adapter's getLoadContext() function.\n   * It's a way to bridge the gap between the adapter's request/response API with your React Router app.\n   * It is only applicable if you are using a custom server adapter.\n   */\n  context: Context;\n}\n\n/**\n * Route middleware `next` function to call downstream handlers and then complete\n * middlewares from the bottom-up\n */\nexport interface MiddlewareNextFunction<Result = unknown> {\n  (): Promise<Result>;\n}\n\n/**\n * Route middleware function signature.  Receives the same \"data\" arguments as a\n * `loader`/`action` (`request`, `params`, `context`) as the first parameter and\n * a `next` function as the second parameter which will call downstream handlers\n * and then complete middlewares from the bottom-up\n */\nexport type MiddlewareFunction<Result = unknown> = (\n  args: DataFunctionArgs<Readonly<RouterContextProvider>>,\n  next: MiddlewareNextFunction<Result>,\n) => MaybePromise<Result | void>;\n\n/**\n * Arguments passed to loader functions\n */\nexport interface LoaderFunctionArgs<Context = DefaultContext>\n  extends DataFunctionArgs<Context> {}\n\n/**\n * Arguments passed to action functions\n */\nexport interface ActionFunctionArgs<Context = DefaultContext>\n  extends DataFunctionArgs<Context> {}\n\n/**\n * Loaders and actions can return anything\n */\ntype DataFunctionValue = unknown;\n\ntype DataFunctionReturnValue = MaybePromise<DataFunctionValue>;\n\n/**\n * Route loader function signature\n */\nexport type LoaderFunction<Context = DefaultContext> = {\n  (\n    args: LoaderFunctionArgs<Context>,\n    handlerCtx?: unknown,\n  ): DataFunctionReturnValue;\n} & { hydrate?: boolean };\n\n/**\n * Route action function signature\n */\nexport interface ActionFunction<Context = DefaultContext> {\n  (\n    args: ActionFunctionArgs<Context>,\n    handlerCtx?: unknown,\n  ): DataFunctionReturnValue;\n}\n\n/**\n * Arguments passed to shouldRevalidate function\n */\nexport interface ShouldRevalidateFunctionArgs {\n  /** This is the url the navigation started from. You can compare it with `nextUrl` to decide if you need to revalidate this route's data. */\n  currentUrl: URL;\n  /** These are the {@link https://reactrouter.com/start/framework/routing#dynamic-segments dynamic route params} from the URL that can be compared to the `nextParams` to decide if you need to reload or not. Perhaps you're using only a partial piece of the param for data loading, you don't need to revalidate if a superfluous part of the param changed. */\n  currentParams: AgnosticDataRouteMatch[\"params\"];\n  /** In the case of navigation, this the URL the user is requesting. Some revalidations are not navigation, so it will simply be the same as currentUrl. */\n  nextUrl: URL;\n  /** In the case of navigation, these are the {@link https://reactrouter.com/start/framework/routing#dynamic-segments dynamic route params}  from the next location the user is requesting. Some revalidations are not navigation, so it will simply be the same as currentParams. */\n  nextParams: AgnosticDataRouteMatch[\"params\"];\n  /** The method (probably `\"GET\"` or `\"POST\"`) used in the form submission that triggered the revalidation. */\n  formMethod?: Submission[\"formMethod\"];\n  /** The form action (`<Form action=\"/somewhere\">`) that triggered the revalidation. */\n  formAction?: Submission[\"formAction\"];\n  /** The form encType (`<Form encType=\"application/x-www-form-urlencoded\">) used in the form submission that triggered the revalidation*/\n  formEncType?: Submission[\"formEncType\"];\n  /** The form submission data when the form's encType is `text/plain` */\n  text?: Submission[\"text\"];\n  /** The form submission data when the form's encType is `application/x-www-form-urlencoded` or `multipart/form-data` */\n  formData?: Submission[\"formData\"];\n  /** The form submission data when the form's encType is `application/json` */\n  json?: Submission[\"json\"];\n  /** The status code of the action response */\n  actionStatus?: number;\n  /**\n   * When a submission causes the revalidation this will be the result of the action—either action data or an error if the action failed. It's common to include some information in the action result to instruct shouldRevalidate to revalidate or not.\n   *\n   * @example\n   * export async function action() {\n   *   await saveSomeStuff();\n   *   return { ok: true };\n   * }\n   *\n   * export function shouldRevalidate({\n   *   actionResult,\n   * }) {\n   *   if (actionResult?.ok) {\n   *     return false;\n   *   }\n   *   return true;\n   * }\n   */\n  actionResult?: any;\n  /**\n   * By default, React Router doesn't call every loader all the time. There are reliable optimizations it can make by default. For example, only loaders with changing params are called. Consider navigating from the following URL to the one below it:\n   *\n   * /projects/123/tasks/abc\n   * /projects/123/tasks/def\n   * React Router will only call the loader for tasks/def because the param for projects/123 didn't change.\n   *\n   * It's safest to always return defaultShouldRevalidate after you've done your specific optimizations that return false, otherwise your UI might get out of sync with your data on the server.\n   */\n  defaultShouldRevalidate: boolean;\n}\n\n/**\n * Route shouldRevalidate function signature.  This runs after any submission\n * (navigation or fetcher), so we flatten the navigation/fetcher submission\n * onto the arguments.  It shouldn't matter whether it came from a navigation\n * or a fetcher, what really matters is the URLs and the formData since loaders\n * have to re-run based on the data models that were potentially mutated.\n */\nexport interface ShouldRevalidateFunction {\n  (args: ShouldRevalidateFunctionArgs): boolean;\n}\n\nexport interface DataStrategyMatch\n  extends AgnosticRouteMatch<string, AgnosticDataRouteObject> {\n  /**\n   * @private\n   */\n  _lazyPromises?: {\n    middleware: Promise<void> | undefined;\n    handler: Promise<void> | undefined;\n    route: Promise<void> | undefined;\n  };\n  /**\n   * @deprecated Deprecated in favor of `shouldCallHandler`\n   *\n   * A boolean value indicating whether this route handler should be called in\n   * this pass.\n   *\n   * The `matches` array always includes _all_ matched routes even when only\n   * _some_ route handlers need to be called so that things like middleware can\n   * be implemented.\n   *\n   * `shouldLoad` is usually only interesting if you are skipping the route\n   * handler entirely and implementing custom handler logic - since it lets you\n   * determine if that custom logic should run for this route or not.\n   *\n   * For example:\n   *  - If you are on `/parent/child/a` and you navigate to `/parent/child/b` -\n   *    you'll get an array of three matches (`[parent, child, b]`), but only `b`\n   *    will have `shouldLoad=true` because the data for `parent` and `child` is\n   *    already loaded\n   *  - If you are on `/parent/child/a` and you submit to `a`'s [`action`](https://reactrouter.com/docs/start/data/route-object#action),\n   *    then only `a` will have `shouldLoad=true` for the action execution of\n   *    `dataStrategy`\n   *  - After the [`action`](https://reactrouter.com/docs/start/data/route-object#action),\n   *    `dataStrategy` will be called again for the [`loader`](https://reactrouter.com/docs/start/data/route-object#loader)\n   *    revalidation, and all matches will have `shouldLoad=true` (assuming no\n   *    custom `shouldRevalidate` implementations)\n   */\n  shouldLoad: boolean;\n  /**\n   * Arguments passed to the `shouldRevalidate` function for this `loader` execution.\n   * Will be `null` if this is not a revalidating loader {@link DataStrategyMatch}.\n   */\n  shouldRevalidateArgs: ShouldRevalidateFunctionArgs | null;\n  /**\n   * Determine if this route's handler should be called during this `dataStrategy`\n   * execution. Calling it with no arguments will leverage the default revalidation\n   * behavior. You can pass your own `defaultShouldRevalidate` value if you wish\n   * to change the default revalidation behavior with your `dataStrategy`.\n   *\n   * @param defaultShouldRevalidate `defaultShouldRevalidate` override value (optional)\n   */\n  shouldCallHandler(defaultShouldRevalidate?: boolean): boolean;\n  /**\n   * An async function that will resolve any `route.lazy` implementations and\n   * execute the route's handler (if necessary), returning a {@link DataStrategyResult}\n   *\n   * - Calling `match.resolve` does not mean you're calling the\n   *   [`action`](https://reactrouter.com/docs/start/data/route-object#action)/[`loader`](https://reactrouter.com/docs/start/data/route-object#loader)\n   *   (the \"handler\") - `resolve` will only call the `handler` internally if\n   *   needed _and_ if you don't pass your own `handlerOverride` function parameter\n   * - It is safe to call `match.resolve` for all matches, even if they have\n   *   `shouldLoad=false`, and it will no-op if no loading is required\n   * - You should generally always call `match.resolve()` for `shouldLoad:true`\n   *   routes to ensure that any `route.lazy` implementations are processed\n   * - See the examples below for how to implement custom handler execution via\n   *   `match.resolve`\n   */\n  resolve: (\n    handlerOverride?: (\n      handler: (ctx?: unknown) => DataFunctionReturnValue,\n    ) => DataFunctionReturnValue,\n  ) => Promise<DataStrategyResult>;\n}\n\nexport interface DataStrategyFunctionArgs<Context = DefaultContext>\n  extends DataFunctionArgs<Context> {\n  /**\n   * Matches for this route extended with Data strategy APIs\n   */\n  matches: DataStrategyMatch[];\n  runClientMiddleware: (\n    cb: DataStrategyFunction<Context>,\n  ) => Promise<Record<string, DataStrategyResult>>;\n  /**\n   * The key of the fetcher we are calling `dataStrategy` for, otherwise `null`\n   * for navigational executions\n   */\n  fetcherKey: string | null;\n}\n\n/**\n * Result from a loader or action called via dataStrategy\n */\nexport interface DataStrategyResult {\n  type: \"data\" | \"error\";\n  result: unknown; // data, Error, Response, DeferredData, DataWithResponseInit\n}\n\nexport interface DataStrategyFunction<Context = DefaultContext> {\n  (\n    args: DataStrategyFunctionArgs<Context>,\n  ): Promise<Record<string, DataStrategyResult>>;\n}\n\nexport type AgnosticPatchRoutesOnNavigationFunctionArgs<\n  O extends AgnosticRouteObject = AgnosticRouteObject,\n  M extends AgnosticRouteMatch = AgnosticRouteMatch,\n> = {\n  signal: AbortSignal;\n  path: string;\n  matches: M[];\n  fetcherKey: string | undefined;\n  patch: (routeId: string | null, children: O[]) => void;\n};\n\nexport type AgnosticPatchRoutesOnNavigationFunction<\n  O extends AgnosticRouteObject = AgnosticRouteObject,\n  M extends AgnosticRouteMatch = AgnosticRouteMatch,\n> = (\n  opts: AgnosticPatchRoutesOnNavigationFunctionArgs<O, M>,\n) => MaybePromise<void>;\n\n/**\n * Function provided by the framework-aware layers to set any framework-specific\n * properties from framework-agnostic properties\n */\nexport interface MapRoutePropertiesFunction {\n  (route: AgnosticDataRouteObject): {\n    hasErrorBoundary: boolean;\n  } & Record<string, any>;\n}\n\n/**\n * Keys we cannot change from within a lazy object. We spread all other keys\n * onto the route. Either they're meaningful to the router, or they'll get\n * ignored.\n */\ntype UnsupportedLazyRouteObjectKey =\n  | \"lazy\"\n  | \"caseSensitive\"\n  | \"path\"\n  | \"id\"\n  | \"index\"\n  | \"children\";\nconst unsupportedLazyRouteObjectKeys = new Set<UnsupportedLazyRouteObjectKey>([\n  \"lazy\",\n  \"caseSensitive\",\n  \"path\",\n  \"id\",\n  \"index\",\n  \"children\",\n]);\nexport function isUnsupportedLazyRouteObjectKey(\n  key: string,\n): key is UnsupportedLazyRouteObjectKey {\n  return unsupportedLazyRouteObjectKeys.has(\n    key as UnsupportedLazyRouteObjectKey,\n  );\n}\n\n/**\n * Keys we cannot change from within a lazy() function. We spread all other keys\n * onto the route. Either they're meaningful to the router, or they'll get\n * ignored.\n */\ntype UnsupportedLazyRouteFunctionKey =\n  | UnsupportedLazyRouteObjectKey\n  | \"middleware\";\nconst unsupportedLazyRouteFunctionKeys =\n  new Set<UnsupportedLazyRouteFunctionKey>([\n    \"lazy\",\n    \"caseSensitive\",\n    \"path\",\n    \"id\",\n    \"index\",\n    \"middleware\",\n    \"children\",\n  ]);\nexport function isUnsupportedLazyRouteFunctionKey(\n  key: string,\n): key is UnsupportedLazyRouteFunctionKey {\n  return unsupportedLazyRouteFunctionKeys.has(\n    key as UnsupportedLazyRouteFunctionKey,\n  );\n}\n\n/**\n * lazy object to load route properties, which can add non-matching\n * related properties to a route\n */\nexport type LazyRouteObject<R extends AgnosticRouteObject> = {\n  [K in keyof R as K extends UnsupportedLazyRouteObjectKey\n    ? never\n    : K]?: () => Promise<R[K] | null | undefined>;\n};\n\n/**\n * lazy() function to load a route definition, which can add non-matching\n * related properties to a route\n */\nexport interface LazyRouteFunction<R extends AgnosticRouteObject> {\n  (): Promise<\n    Omit<R, UnsupportedLazyRouteFunctionKey> &\n      Partial<Record<UnsupportedLazyRouteFunctionKey, never>>\n  >;\n}\n\nexport type LazyRouteDefinition<R extends AgnosticRouteObject> =\n  | LazyRouteObject<R>\n  | LazyRouteFunction<R>;\n\n/**\n * Base RouteObject with common props shared by all types of routes\n */\ntype AgnosticBaseRouteObject = {\n  caseSensitive?: boolean;\n  path?: string;\n  id?: string;\n  middleware?: MiddlewareFunction[];\n  loader?: LoaderFunction | boolean;\n  action?: ActionFunction | boolean;\n  hasErrorBoundary?: boolean;\n  shouldRevalidate?: ShouldRevalidateFunction;\n  handle?: any;\n  lazy?: LazyRouteDefinition<AgnosticBaseRouteObject>;\n};\n\n/**\n * Index routes must not have children\n */\nexport type AgnosticIndexRouteObject = AgnosticBaseRouteObject & {\n  children?: undefined;\n  index: true;\n};\n\n/**\n * Non-index routes may have children, but cannot have index\n */\nexport type AgnosticNonIndexRouteObject = AgnosticBaseRouteObject & {\n  children?: AgnosticRouteObject[];\n  index?: false;\n};\n\n/**\n * A route object represents a logical route, with (optionally) its child\n * routes organized in a tree-like structure.\n */\nexport type AgnosticRouteObject =\n  | AgnosticIndexRouteObject\n  | AgnosticNonIndexRouteObject;\n\nexport type AgnosticDataIndexRouteObject = AgnosticIndexRouteObject & {\n  id: string;\n};\n\nexport type AgnosticDataNonIndexRouteObject = AgnosticNonIndexRouteObject & {\n  children?: AgnosticDataRouteObject[];\n  id: string;\n};\n\n/**\n * A data route object, which is just a RouteObject with a required unique ID\n */\nexport type AgnosticDataRouteObject =\n  | AgnosticDataIndexRouteObject\n  | AgnosticDataNonIndexRouteObject;\n\nexport type RouteManifest<R = AgnosticDataRouteObject> = Record<\n  string,\n  R | undefined\n>;\n\n// prettier-ignore\ntype Regex_az = \"a\" | \"b\" | \"c\" | \"d\" | \"e\" | \"f\" | \"g\" | \"h\" | \"i\" | \"j\" | \"k\" | \"l\" | \"m\" | \"n\" | \"o\" | \"p\" | \"q\" | \"r\" | \"s\" | \"t\" | \"u\" | \"v\" | \"w\" | \"x\" | \"y\" | \"z\"\n// prettier-ignore\ntype Regez_AZ = \"A\" | \"B\" | \"C\" | \"D\" | \"E\" | \"F\" | \"G\" | \"H\" | \"I\" | \"J\" | \"K\" | \"L\" | \"M\" | \"N\" | \"O\" | \"P\" | \"Q\" | \"R\" | \"S\" | \"T\" | \"U\" | \"V\" | \"W\" | \"X\" | \"Y\" | \"Z\"\ntype Regex_09 = \"0\" | \"1\" | \"2\" | \"3\" | \"4\" | \"5\" | \"6\" | \"7\" | \"8\" | \"9\";\ntype Regex_w = Regex_az | Regez_AZ | Regex_09 | \"_\";\ntype ParamChar = Regex_w | \"-\";\n\n// Emulates regex `+`\ntype RegexMatchPlus<\n  CharPattern extends string,\n  T extends string,\n> = T extends `${infer First}${infer Rest}`\n  ? First extends CharPattern\n    ? RegexMatchPlus<CharPattern, Rest> extends never\n      ? First\n      : `${First}${RegexMatchPlus<CharPattern, Rest>}`\n    : never\n  : never;\n\n// Recursive helper for finding path parameters in the absence of wildcards\ntype _PathParam<Path extends string> =\n  // split path into individual path segments\n  Path extends `${infer L}/${infer R}`\n    ? _PathParam<L> | _PathParam<R>\n    : // find params after `:`\n      Path extends `:${infer Param}`\n      ? Param extends `${infer Optional}?${string}`\n        ? RegexMatchPlus<ParamChar, Optional>\n        : RegexMatchPlus<ParamChar, Param>\n      : // otherwise, there aren't any params present\n        never;\n\nexport type PathParam<Path extends string> =\n  // check if path is just a wildcard\n  Path extends \"*\" | \"/*\"\n    ? \"*\"\n    : // look for wildcard at the end of the path\n      Path extends `${infer Rest}/*`\n      ? \"*\" | _PathParam<Rest>\n      : // look for params in the absence of wildcards\n        _PathParam<Path>;\n\n// eslint-disable-next-line @typescript-eslint/no-unused-vars\ntype _tests = [\n  Expect<Equal<PathParam<\"/a/b/*\">, \"*\">>,\n  Expect<Equal<PathParam<\":a\">, \"a\">>,\n  Expect<Equal<PathParam<\"/a/:b\">, \"b\">>,\n  Expect<Equal<PathParam<\"/a/blahblahblah:b\">, never>>,\n  Expect<Equal<PathParam<\"/:a/:b\">, \"a\" | \"b\">>,\n  Expect<Equal<PathParam<\"/:a/b/:c/*\">, \"a\" | \"c\" | \"*\">>,\n  Expect<Equal<PathParam<\"/:lang.xml\">, \"lang\">>,\n  Expect<Equal<PathParam<\"/:lang?.xml\">, \"lang\">>,\n];\n\n// Attempt to parse the given string segment. If it fails, then just return the\n// plain string type as a default fallback. Otherwise, return the union of the\n// parsed string literals that were referenced as dynamic segments in the route.\nexport type ParamParseKey<Segment extends string> =\n  // if you could not find path params, fallback to `string`\n  [PathParam<Segment>] extends [never] ? string : PathParam<Segment>;\n\n/**\n * The parameters that were parsed from the URL path.\n */\nexport type Params<Key extends string = string> = {\n  readonly [key in Key]: string | undefined;\n};\n\n/**\n * A RouteMatch contains info about how a route matched a URL.\n */\nexport interface AgnosticRouteMatch<\n  ParamKey extends string = string,\n  RouteObjectType extends AgnosticRouteObject = AgnosticRouteObject,\n> {\n  /**\n   * The names and values of dynamic parameters in the URL.\n   */\n  params: Params<ParamKey>;\n  /**\n   * The portion of the URL pathname that was matched.\n   */\n  pathname: string;\n  /**\n   * The portion of the URL pathname that was matched before child routes.\n   */\n  pathnameBase: string;\n  /**\n   * The route object that was used to match.\n   */\n  route: RouteObjectType;\n}\n\nexport interface AgnosticDataRouteMatch\n  extends AgnosticRouteMatch<string, AgnosticDataRouteObject> {}\n\nfunction isIndexRoute(\n  route: AgnosticRouteObject,\n): route is AgnosticIndexRouteObject {\n  return route.index === true;\n}\n\n// Walk the route tree generating unique IDs where necessary, so we are working\n// solely with AgnosticDataRouteObject's within the Router\nexport function convertRoutesToDataRoutes(\n  routes: AgnosticRouteObject[],\n  mapRouteProperties: MapRoutePropertiesFunction,\n  parentPath: string[] = [],\n  manifest: RouteManifest = {},\n  allowInPlaceMutations = false,\n): AgnosticDataRouteObject[] {\n  return routes.map((route, index) => {\n    let treePath = [...parentPath, String(index)];\n    let id = typeof route.id === \"string\" ? route.id : treePath.join(\"-\");\n    invariant(\n      route.index !== true || !route.children,\n      `Cannot specify children on an index route`,\n    );\n    invariant(\n      allowInPlaceMutations || !manifest[id],\n      `Found a route id collision on id \"${id}\".  Route ` +\n        \"id's must be globally unique within Data Router usages\",\n    );\n\n    if (isIndexRoute(route)) {\n      let indexRoute: AgnosticDataIndexRouteObject = {\n        ...route,\n        id,\n      };\n      manifest[id] = mergeRouteUpdates(\n        indexRoute,\n        mapRouteProperties(indexRoute),\n      );\n      return indexRoute;\n    } else {\n      let pathOrLayoutRoute: AgnosticDataNonIndexRouteObject = {\n        ...route,\n        id,\n        children: undefined,\n      };\n      manifest[id] = mergeRouteUpdates(\n        pathOrLayoutRoute,\n        mapRouteProperties(pathOrLayoutRoute),\n      );\n\n      if (route.children) {\n        pathOrLayoutRoute.children = convertRoutesToDataRoutes(\n          route.children,\n          mapRouteProperties,\n          treePath,\n          manifest,\n          allowInPlaceMutations,\n        );\n      }\n\n      return pathOrLayoutRoute;\n    }\n  });\n}\n\nfunction mergeRouteUpdates<T extends AgnosticDataRouteObject>(\n  route: T,\n  updates: ReturnType<MapRoutePropertiesFunction>,\n): T {\n  return Object.assign(route, {\n    ...updates,\n    ...(typeof updates.lazy === \"object\" && updates.lazy != null\n      ? {\n          lazy: {\n            ...route.lazy,\n            ...updates.lazy,\n          },\n        }\n      : {}),\n  });\n}\n\n/**\n * Matches the given routes to a location and returns the match data.\n *\n * @example\n * import { matchRoutes } from \"react-router\";\n *\n * let routes = [{\n *   path: \"/\",\n *   Component: Root,\n *   children: [{\n *     path: \"dashboard\",\n *     Component: Dashboard,\n *   }]\n * }];\n *\n * matchRoutes(routes, \"/dashboard\"); // [rootMatch, dashboardMatch]\n *\n * @public\n * @category Utils\n * @param routes The array of route objects to match against.\n * @param locationArg The location to match against, either a string path or a\n * partial {@link Location} object\n * @param basename Optional base path to strip from the location before matching.\n * Defaults to `/`.\n * @returns An array of matched routes, or `null` if no matches were found.\n */\nexport function matchRoutes<\n  RouteObjectType extends AgnosticRouteObject = AgnosticRouteObject,\n>(\n  routes: RouteObjectType[],\n  locationArg: Partial<Location> | string,\n  basename = \"/\",\n): AgnosticRouteMatch<string, RouteObjectType>[] | null {\n  return matchRoutesImpl(routes, locationArg, basename, false);\n}\n\nexport function matchRoutesImpl<\n  RouteObjectType extends AgnosticRouteObject = AgnosticRouteObject,\n>(\n  routes: RouteObjectType[],\n  locationArg: Partial<Location> | string,\n  basename: string,\n  allowPartial: boolean,\n): AgnosticRouteMatch<string, RouteObjectType>[] | null {\n  let location =\n    typeof locationArg === \"string\" ? parsePath(locationArg) : locationArg;\n\n  let pathname = stripBasename(location.pathname || \"/\", basename);\n\n  if (pathname == null) {\n    return null;\n  }\n\n  let branches = flattenRoutes(routes);\n  rankRouteBranches(branches);\n\n  let matches = null;\n  for (let i = 0; matches == null && i < branches.length; ++i) {\n    // Incoming pathnames are generally encoded from either window.location\n    // or from router.navigate, but we want to match against the unencoded\n    // paths in the route definitions.  Memory router locations won't be\n    // encoded here but there also shouldn't be anything to decode so this\n    // should be a safe operation.  This avoids needing matchRoutes to be\n    // history-aware.\n    let decoded = decodePath(pathname);\n    matches = matchRouteBranch<string, RouteObjectType>(\n      branches[i],\n      decoded,\n      allowPartial,\n    );\n  }\n\n  return matches;\n}\n\nexport interface UIMatch<Data = unknown, Handle = unknown> {\n  id: string;\n  pathname: string;\n  /**\n   * {@link https://reactrouter.com/start/framework/routing#dynamic-segments Dynamic route params} for the matched route.\n   */\n  params: AgnosticRouteMatch[\"params\"];\n  /**\n   * The return value from the matched route's loader or clientLoader. This might\n   * be `undefined` if this route's `loader` (or a deeper route's `loader`) threw\n   * an error and we're currently displaying an `ErrorBoundary`.\n   *\n   * @deprecated Use `UIMatch.loaderData` instead\n   */\n  data: Data | undefined;\n  /**\n   * The return value from the matched route's loader or clientLoader. This might\n   * be `undefined` if this route's `loader` (or a deeper route's `loader`) threw\n   * an error and we're currently displaying an `ErrorBoundary`.\n   */\n  loaderData: Data | undefined;\n  /**\n   * The {@link https://reactrouter.com/start/framework/route-module#handle handle object}\n   * exported from the matched route module\n   */\n  handle: Handle;\n}\n\nexport function convertRouteMatchToUiMatch(\n  match: AgnosticDataRouteMatch,\n  loaderData: RouteData,\n): UIMatch {\n  let { route, pathname, params } = match;\n  return {\n    id: route.id,\n    pathname,\n    params,\n    data: loaderData[route.id],\n    loaderData: loaderData[route.id],\n    handle: route.handle,\n  };\n}\n\ninterface RouteMeta<\n  RouteObjectType extends AgnosticRouteObject = AgnosticRouteObject,\n> {\n  relativePath: string;\n  caseSensitive: boolean;\n  childrenIndex: number;\n  route: RouteObjectType;\n}\n\ninterface RouteBranch<\n  RouteObjectType extends AgnosticRouteObject = AgnosticRouteObject,\n> {\n  path: string;\n  score: number;\n  routesMeta: RouteMeta<RouteObjectType>[];\n}\n\nfunction flattenRoutes<\n  RouteObjectType extends AgnosticRouteObject = AgnosticRouteObject,\n>(\n  routes: RouteObjectType[],\n  branches: RouteBranch<RouteObjectType>[] = [],\n  parentsMeta: RouteMeta<RouteObjectType>[] = [],\n  parentPath = \"\",\n  _hasParentOptionalSegments = false,\n): RouteBranch<RouteObjectType>[] {\n  let flattenRoute = (\n    route: RouteObjectType,\n    index: number,\n    hasParentOptionalSegments = _hasParentOptionalSegments,\n    relativePath?: string,\n  ) => {\n    let meta: RouteMeta<RouteObjectType> = {\n      relativePath:\n        relativePath === undefined ? route.path || \"\" : relativePath,\n      caseSensitive: route.caseSensitive === true,\n      childrenIndex: index,\n      route,\n    };\n\n    if (meta.relativePath.startsWith(\"/\")) {\n      if (\n        !meta.relativePath.startsWith(parentPath) &&\n        hasParentOptionalSegments\n      ) {\n        // If we're inside of a parent route that has optional segments, we don't\n        // want to throw a hard error here because due to the route exploding\n        // approach, some of the routes won't match by design and we can just\n        // discard them instead.\n        // https://github.com/remix-run/react-router/issues/9925#issuecomment-1387252214\n        return;\n      }\n      invariant(\n        meta.relativePath.startsWith(parentPath),\n        `Absolute route path \"${meta.relativePath}\" nested under path ` +\n          `\"${parentPath}\" is not valid. An absolute child route path ` +\n          `must start with the combined path of all its parent routes.`,\n      );\n\n      meta.relativePath = meta.relativePath.slice(parentPath.length);\n    }\n\n    let path = joinPaths([parentPath, meta.relativePath]);\n    let routesMeta = parentsMeta.concat(meta);\n\n    // Add the children before adding this route to the array, so we traverse the\n    // route tree depth-first and child routes appear before their parents in\n    // the \"flattened\" version.\n    if (route.children && route.children.length > 0) {\n      invariant(\n        // Our types know better, but runtime JS may not!\n        // @ts-expect-error\n        route.index !== true,\n        `Index routes must not have child routes. Please remove ` +\n          `all child routes from route path \"${path}\".`,\n      );\n      flattenRoutes(\n        route.children,\n        branches,\n        routesMeta,\n        path,\n        hasParentOptionalSegments,\n      );\n    }\n\n    // Routes without a path shouldn't ever match by themselves unless they are\n    // index routes, so don't add them to the list of possible branches.\n    if (route.path == null && !route.index) {\n      return;\n    }\n\n    branches.push({\n      path,\n      score: computeScore(path, route.index),\n      routesMeta,\n    });\n  };\n  routes.forEach((route, index) => {\n    // coarse-grain check for optional params\n    if (route.path === \"\" || !route.path?.includes(\"?\")) {\n      flattenRoute(route, index);\n    } else {\n      for (let exploded of explodeOptionalSegments(route.path)) {\n        flattenRoute(route, index, true, exploded);\n      }\n    }\n  });\n\n  return branches;\n}\n\n/*\n * Computes all combinations of optional path segments for a given path,\n * excluding combinations that are ambiguous and of lower priority.\n *\n * For example, `/one/:two?/three/:four?/:five?` explodes to:\n * - `/one/three`\n * - `/one/:two/three`\n * - `/one/three/:four`\n * - `/one/three/:five`\n * - `/one/:two/three/:four`\n * - `/one/:two/three/:five`\n * - `/one/three/:four/:five`\n * - `/one/:two/three/:four/:five`\n */\nfunction explodeOptionalSegments(path: string): string[] {\n  let segments = path.split(\"/\");\n  if (segments.length === 0) return [];\n\n  let [first, ...rest] = segments;\n\n  // Optional path segments are denoted by a trailing `?`\n  let isOptional = first.endsWith(\"?\");\n  // Compute the corresponding required segment: `foo?` -> `foo`\n  let required = first.replace(/\\?$/, \"\");\n\n  if (rest.length === 0) {\n    // Interpret empty string as omitting an optional segment\n    // `[\"one\", \"\", \"three\"]` corresponds to omitting `:two` from `/one/:two?/three` -> `/one/three`\n    return isOptional ? [required, \"\"] : [required];\n  }\n\n  let restExploded = explodeOptionalSegments(rest.join(\"/\"));\n\n  let result: string[] = [];\n\n  // All child paths with the prefix.  Do this for all children before the\n  // optional version for all children, so we get consistent ordering where the\n  // parent optional aspect is preferred as required.  Otherwise, we can get\n  // child sections interspersed where deeper optional segments are higher than\n  // parent optional segments, where for example, /:two would explode _earlier_\n  // then /:one.  By always including the parent as required _for all children_\n  // first, we avoid this issue\n  result.push(\n    ...restExploded.map((subpath) =>\n      subpath === \"\" ? required : [required, subpath].join(\"/\"),\n    ),\n  );\n\n  // Then, if this is an optional value, add all child versions without\n  if (isOptional) {\n    result.push(...restExploded);\n  }\n\n  // for absolute paths, ensure `/` instead of empty segment\n  return result.map((exploded) =>\n    path.startsWith(\"/\") && exploded === \"\" ? \"/\" : exploded,\n  );\n}\n\nfunction rankRouteBranches(branches: RouteBranch[]): void {\n  branches.sort((a, b) =>\n    a.score !== b.score\n      ? b.score - a.score // Higher score first\n      : compareIndexes(\n          a.routesMeta.map((meta) => meta.childrenIndex),\n          b.routesMeta.map((meta) => meta.childrenIndex),\n        ),\n  );\n}\n\nconst paramRe = /^:[\\w-]+$/;\nconst dynamicSegmentValue = 3;\nconst indexRouteValue = 2;\nconst emptySegmentValue = 1;\nconst staticSegmentValue = 10;\nconst splatPenalty = -2;\nconst isSplat = (s: string) => s === \"*\";\n\nfunction computeScore(path: string, index: boolean | undefined): number {\n  let segments = path.split(\"/\");\n  let initialScore = segments.length;\n  if (segments.some(isSplat)) {\n    initialScore += splatPenalty;\n  }\n\n  if (index) {\n    initialScore += indexRouteValue;\n  }\n\n  return segments\n    .filter((s) => !isSplat(s))\n    .reduce(\n      (score, segment) =>\n        score +\n        (paramRe.test(segment)\n          ? dynamicSegmentValue\n          : segment === \"\"\n            ? emptySegmentValue\n            : staticSegmentValue),\n      initialScore,\n    );\n}\n\nfunction compareIndexes(a: number[], b: number[]): number {\n  let siblings =\n    a.length === b.length && a.slice(0, -1).every((n, i) => n === b[i]);\n\n  return siblings\n    ? // If two routes are siblings, we should try to match the earlier sibling\n      // first. This allows people to have fine-grained control over the matching\n      // behavior by simply putting routes with identical paths in the order they\n      // want them tried.\n      a[a.length - 1] - b[b.length - 1]\n    : // Otherwise, it doesn't really make sense to rank non-siblings by index,\n      // so they sort equally.\n      0;\n}\n\nfunction matchRouteBranch<\n  ParamKey extends string = string,\n  RouteObjectType extends AgnosticRouteObject = AgnosticRouteObject,\n>(\n  branch: RouteBranch<RouteObjectType>,\n  pathname: string,\n  allowPartial = false,\n): AgnosticRouteMatch<ParamKey, RouteObjectType>[] | null {\n  let { routesMeta } = branch;\n\n  let matchedParams = {};\n  let matchedPathname = \"/\";\n  let matches: AgnosticRouteMatch<ParamKey, RouteObjectType>[] = [];\n  for (let i = 0; i < routesMeta.length; ++i) {\n    let meta = routesMeta[i];\n    let end = i === routesMeta.length - 1;\n    let remainingPathname =\n      matchedPathname === \"/\"\n        ? pathname\n        : pathname.slice(matchedPathname.length) || \"/\";\n    let match = matchPath(\n      { path: meta.relativePath, caseSensitive: meta.caseSensitive, end },\n      remainingPathname,\n    );\n\n    let route = meta.route;\n\n    if (\n      !match &&\n      end &&\n      allowPartial &&\n      !routesMeta[routesMeta.length - 1].route.index\n    ) {\n      match = matchPath(\n        {\n          path: meta.relativePath,\n          caseSensitive: meta.caseSensitive,\n          end: false,\n        },\n        remainingPathname,\n      );\n    }\n\n    if (!match) {\n      return null;\n    }\n\n    Object.assign(matchedParams, match.params);\n\n    matches.push({\n      // TODO: Can this as be avoided?\n      params: matchedParams as Params<ParamKey>,\n      pathname: joinPaths([matchedPathname, match.pathname]),\n      pathnameBase: normalizePathname(\n        joinPaths([matchedPathname, match.pathnameBase]),\n      ),\n      route,\n    });\n\n    if (match.pathnameBase !== \"/\") {\n      matchedPathname = joinPaths([matchedPathname, match.pathnameBase]);\n    }\n  }\n\n  return matches;\n}\n\n/**\n * Returns a path with params interpolated.\n *\n * @example\n * import { generatePath } from \"react-router\";\n *\n * generatePath(\"/users/:id\", { id: \"123\" }); // \"/users/123\"\n *\n * @public\n * @category Utils\n * @param originalPath The original path to generate.\n * @param params The parameters to interpolate into the path.\n * @returns The generated path with parameters interpolated.\n */\nexport function generatePath<Path extends string>(\n  originalPath: Path,\n  params: {\n    [key in PathParam<Path>]: string | null;\n  } = {} as any,\n): string {\n  let path: string = originalPath;\n  if (path.endsWith(\"*\") && path !== \"*\" && !path.endsWith(\"/*\")) {\n    warning(\n      false,\n      `Route path \"${path}\" will be treated as if it were ` +\n        `\"${path.replace(/\\*$/, \"/*\")}\" because the \\`*\\` character must ` +\n        `always follow a \\`/\\` in the pattern. To get rid of this warning, ` +\n        `please change the route path to \"${path.replace(/\\*$/, \"/*\")}\".`,\n    );\n    path = path.replace(/\\*$/, \"/*\") as Path;\n  }\n\n  // ensure `/` is added at the beginning if the path is absolute\n  const prefix = path.startsWith(\"/\") ? \"/\" : \"\";\n\n  const stringify = (p: any) =>\n    p == null ? \"\" : typeof p === \"string\" ? p : String(p);\n\n  const segments = path\n    .split(/\\/+/)\n    .map((segment, index, array) => {\n      const isLastSegment = index === array.length - 1;\n\n      // only apply the splat if it's the last segment\n      if (isLastSegment && segment === \"*\") {\n        const star = \"*\" as PathParam<Path>;\n        // Apply the splat\n        return stringify(params[star]);\n      }\n\n      const keyMatch = segment.match(/^:([\\w-]+)(\\??)(.*)/);\n      if (keyMatch) {\n        const [, key, optional, suffix] = keyMatch;\n        let param = params[key as PathParam<Path>];\n        invariant(optional === \"?\" || param != null, `Missing \":${key}\" param`);\n        return encodeURIComponent(stringify(param)) + suffix;\n      }\n\n      // Remove any optional markers from optional static segments\n      return segment.replace(/\\?$/g, \"\");\n    })\n    // Remove empty segments\n    .filter((segment) => !!segment);\n\n  return prefix + segments.join(\"/\");\n}\n\n/**\n * Used to match on some portion of a URL pathname.\n */\nexport interface PathPattern<Path extends string = string> {\n  /**\n   * A string to match against a URL pathname. May contain `:id`-style segments\n   * to indicate placeholders for dynamic parameters. It May also end with `/*`\n   * to indicate matching the rest of the URL pathname.\n   */\n  path: Path;\n  /**\n   * Should be `true` if the static portions of the `path` should be matched in\n   * the same case.\n   */\n  caseSensitive?: boolean;\n  /**\n   * Should be `true` if this pattern should match the entire URL pathname.\n   */\n  end?: boolean;\n}\n\n/**\n * Contains info about how a {@link PathPattern} matched on a URL pathname.\n */\nexport interface PathMatch<ParamKey extends string = string> {\n  /**\n   * The names and values of dynamic parameters in the URL.\n   */\n  params: Params<ParamKey>;\n  /**\n   * The portion of the URL pathname that was matched.\n   */\n  pathname: string;\n  /**\n   * The portion of the URL pathname that was matched before child routes.\n   */\n  pathnameBase: string;\n  /**\n   * The pattern that was used to match.\n   */\n  pattern: PathPattern;\n}\n\ntype Mutable<T> = {\n  -readonly [P in keyof T]: T[P];\n};\n\n/**\n * Performs pattern matching on a URL pathname and returns information about\n * the match.\n *\n * @public\n * @category Utils\n * @param pattern The pattern to match against the URL pathname. This can be a\n * string or a {@link PathPattern} object. If a string is provided, it will be\n * treated as a pattern with `caseSensitive` set to `false` and `end` set to\n * `true`.\n * @param pathname The URL pathname to match against the pattern.\n * @returns A path match object if the pattern matches the pathname,\n * or `null` if it does not match.\n */\nexport function matchPath<\n  ParamKey extends ParamParseKey<Path>,\n  Path extends string,\n>(\n  pattern: PathPattern<Path> | Path,\n  pathname: string,\n): PathMatch<ParamKey> | null {\n  if (typeof pattern === \"string\") {\n    pattern = { path: pattern, caseSensitive: false, end: true };\n  }\n\n  let [matcher, compiledParams] = compilePath(\n    pattern.path,\n    pattern.caseSensitive,\n    pattern.end,\n  );\n\n  let match = pathname.match(matcher);\n  if (!match) return null;\n\n  let matchedPathname = match[0];\n  let pathnameBase = matchedPathname.replace(/(.)\\/+$/, \"$1\");\n  let captureGroups = match.slice(1);\n  let params: Params = compiledParams.reduce<Mutable<Params>>(\n    (memo, { paramName, isOptional }, index) => {\n      // We need to compute the pathnameBase here using the raw splat value\n      // instead of using params[\"*\"] later because it will be decoded then\n      if (paramName === \"*\") {\n        let splatValue = captureGroups[index] || \"\";\n        pathnameBase = matchedPathname\n          .slice(0, matchedPathname.length - splatValue.length)\n          .replace(/(.)\\/+$/, \"$1\");\n      }\n\n      const value = captureGroups[index];\n      if (isOptional && !value) {\n        memo[paramName] = undefined;\n      } else {\n        memo[paramName] = (value || \"\").replace(/%2F/g, \"/\");\n      }\n      return memo;\n    },\n    {},\n  );\n\n  return {\n    params,\n    pathname: matchedPathname,\n    pathnameBase,\n    pattern,\n  };\n}\n\ntype CompiledPathParam = { paramName: string; isOptional?: boolean };\n\nexport function compilePath(\n  path: string,\n  caseSensitive = false,\n  end = true,\n): [RegExp, CompiledPathParam[]] {\n  warning(\n    path === \"*\" || !path.endsWith(\"*\") || path.endsWith(\"/*\"),\n    `Route path \"${path}\" will be treated as if it were ` +\n      `\"${path.replace(/\\*$/, \"/*\")}\" because the \\`*\\` character must ` +\n      `always follow a \\`/\\` in the pattern. To get rid of this warning, ` +\n      `please change the route path to \"${path.replace(/\\*$/, \"/*\")}\".`,\n  );\n\n  let params: CompiledPathParam[] = [];\n  let regexpSource =\n    \"^\" +\n    path\n      .replace(/\\/*\\*?$/, \"\") // Ignore trailing / and /*, we'll handle it below\n      .replace(/^\\/*/, \"/\") // Make sure it has a leading /\n      .replace(/[\\\\.*+^${}|()[\\]]/g, \"\\\\$&\") // Escape special regex chars\n      .replace(\n        /\\/:([\\w-]+)(\\?)?/g,\n        (\n          match: string,\n          paramName: string,\n          isOptional: string | undefined,\n          index: number,\n          str: string,\n        ) => {\n          params.push({ paramName, isOptional: isOptional != null });\n\n          if (isOptional) {\n            let nextChar = str.charAt(index + match.length);\n            if (nextChar && nextChar !== \"/\") {\n              return \"/([^\\\\/]*)\";\n            }\n\n            return \"(?:/([^\\\\/]*))?\";\n          }\n\n          return \"/([^\\\\/]+)\";\n        },\n      ) // Dynamic segment\n      .replace(/\\/([\\w-]+)\\?(\\/|$)/g, \"(/$1)?$2\"); // Optional static segment\n\n  if (path.endsWith(\"*\")) {\n    params.push({ paramName: \"*\" });\n    regexpSource +=\n      path === \"*\" || path === \"/*\"\n        ? \"(.*)$\" // Already matched the initial /, just match the rest\n        : \"(?:\\\\/(.+)|\\\\/*)$\"; // Don't include the / in params[\"*\"]\n  } else if (end) {\n    // When matching to the end, ignore trailing slashes\n    regexpSource += \"\\\\/*$\";\n  } else if (path !== \"\" && path !== \"/\") {\n    // If our path is non-empty and contains anything beyond an initial slash,\n    // then we have _some_ form of path in our regex, so we should expect to\n    // match only if we find the end of this path segment.  Look for an optional\n    // non-captured trailing slash (to match a portion of the URL) or the end\n    // of the path (if we've matched to the end).  We used to do this with a\n    // word boundary but that gives false positives on routes like\n    // /user-preferences since `-` counts as a word boundary.\n    regexpSource += \"(?:(?=\\\\/|$))\";\n  } else {\n    // Nothing to match for \"\" or \"/\"\n  }\n\n  let matcher = new RegExp(regexpSource, caseSensitive ? undefined : \"i\");\n\n  return [matcher, params];\n}\n\nexport function decodePath(value: string) {\n  try {\n    return value\n      .split(\"/\")\n      .map((v) => decodeURIComponent(v).replace(/\\//g, \"%2F\"))\n      .join(\"/\");\n  } catch (error) {\n    warning(\n      false,\n      `The URL path \"${value}\" could not be decoded because it is a ` +\n        `malformed URL segment. This is probably due to a bad percent ` +\n        `encoding (${error}).`,\n    );\n\n    return value;\n  }\n}\n\nexport function stripBasename(\n  pathname: string,\n  basename: string,\n): string | null {\n  if (basename === \"/\") return pathname;\n\n  if (!pathname.toLowerCase().startsWith(basename.toLowerCase())) {\n    return null;\n  }\n\n  // We want to leave trailing slash behavior in the user's control, so if they\n  // specify a basename with a trailing slash, we should support it\n  let startIndex = basename.endsWith(\"/\")\n    ? basename.length - 1\n    : basename.length;\n  let nextChar = pathname.charAt(startIndex);\n  if (nextChar && nextChar !== \"/\") {\n    // pathname does not start with basename/\n    return null;\n  }\n\n  return pathname.slice(startIndex) || \"/\";\n}\n\nexport function prependBasename({\n  basename,\n  pathname,\n}: {\n  basename: string;\n  pathname: string;\n}): string {\n  // If this is a root navigation, then just use the raw basename which allows\n  // the basename to have full control over the presence of a trailing slash on\n  // root actions\n  return pathname === \"/\" ? basename : joinPaths([basename, pathname]);\n}\n\nconst ABSOLUTE_URL_REGEX = /^(?:[a-z][a-z0-9+.-]*:|\\/\\/)/i;\nexport const isAbsoluteUrl = (url: string) => ABSOLUTE_URL_REGEX.test(url);\n\n/**\n * Returns a resolved {@link Path} object relative to the given pathname.\n *\n * @public\n * @category Utils\n * @param to The path to resolve, either a string or a partial {@link Path}\n * object.\n * @param fromPathname The pathname to resolve the path from. Defaults to `/`.\n * @returns A {@link Path} object with the resolved pathname, search, and hash.\n */\nexport function resolvePath(to: To, fromPathname = \"/\"): Path {\n  let {\n    pathname: toPathname,\n    search = \"\",\n    hash = \"\",\n  } = typeof to === \"string\" ? parsePath(to) : to;\n\n  let pathname: string;\n  if (toPathname) {\n    toPathname = toPathname.replace(/\\/\\/+/g, \"/\");\n    if (toPathname.startsWith(\"/\")) {\n      pathname = resolvePathname(toPathname.substring(1), \"/\");\n    } else {\n      pathname = resolvePathname(toPathname, fromPathname);\n    }\n  } else {\n    pathname = fromPathname;\n  }\n\n  return {\n    pathname,\n    search: normalizeSearch(search),\n    hash: normalizeHash(hash),\n  };\n}\n\nfunction resolvePathname(relativePath: string, fromPathname: string): string {\n  let segments = fromPathname.replace(/\\/+$/, \"\").split(\"/\");\n  let relativeSegments = relativePath.split(\"/\");\n\n  relativeSegments.forEach((segment) => {\n    if (segment === \"..\") {\n      // Keep the root \"\" segment so the pathname starts at /\n      if (segments.length > 1) segments.pop();\n    } else if (segment !== \".\") {\n      segments.push(segment);\n    }\n  });\n\n  return segments.length > 1 ? segments.join(\"/\") : \"/\";\n}\n\nfunction getInvalidPathError(\n  char: string,\n  field: string,\n  dest: string,\n  path: Partial<Path>,\n) {\n  return (\n    `Cannot include a '${char}' character in a manually specified ` +\n    `\\`to.${field}\\` field [${JSON.stringify(\n      path,\n    )}].  Please separate it out to the ` +\n    `\\`to.${dest}\\` field. Alternatively you may provide the full path as ` +\n    `a string in <Link to=\"...\"> and the router will parse it for you.`\n  );\n}\n\n// When processing relative navigation we want to ignore ancestor routes that\n// do not contribute to the path, such that index/pathless layout routes don't\n// interfere.\n//\n// For example, when moving a route element into an index route and/or a\n// pathless layout route, relative link behavior contained within should stay\n// the same.  Both of the following examples should link back to the root:\n//\n// <Route path=\"/\">\n//   <Route path=\"accounts\" element={<Link to=\"..\"}>\n// </Route>\n//\n// <Route path=\"/\">\n//   <Route path=\"accounts\">\n//     <Route element={<AccountsLayout />}>       // <-- Does not contribute\n//       <Route index element={<Link to=\"..\"} />  // <-- Does not contribute\n//     </Route\n//   </Route>\n// </Route>\nexport function getPathContributingMatches<\n  T extends AgnosticRouteMatch = AgnosticRouteMatch,\n>(matches: T[]) {\n  return matches.filter(\n    (match, index) =>\n      index === 0 || (match.route.path && match.route.path.length > 0),\n  );\n}\n\n// Return the array of pathnames for the current route matches - used to\n// generate the routePathnames input for resolveTo()\nexport function getResolveToMatches<\n  T extends AgnosticRouteMatch = AgnosticRouteMatch,\n>(matches: T[]) {\n  let pathMatches = getPathContributingMatches(matches);\n\n  // Use the full pathname for the leaf match so we include splat values for \".\" links\n  // https://github.com/remix-run/react-router/issues/11052#issuecomment-1836589329\n  return pathMatches.map((match, idx) =>\n    idx === pathMatches.length - 1 ? match.pathname : match.pathnameBase,\n  );\n}\n\nexport function resolveTo(\n  toArg: To,\n  routePathnames: string[],\n  locationPathname: string,\n  isPathRelative = false,\n): Path {\n  let to: Partial<Path>;\n  if (typeof toArg === \"string\") {\n    to = parsePath(toArg);\n  } else {\n    to = { ...toArg };\n\n    invariant(\n      !to.pathname || !to.pathname.includes(\"?\"),\n      getInvalidPathError(\"?\", \"pathname\", \"search\", to),\n    );\n    invariant(\n      !to.pathname || !to.pathname.includes(\"#\"),\n      getInvalidPathError(\"#\", \"pathname\", \"hash\", to),\n    );\n    invariant(\n      !to.search || !to.search.includes(\"#\"),\n      getInvalidPathError(\"#\", \"search\", \"hash\", to),\n    );\n  }\n\n  let isEmptyPath = toArg === \"\" || to.pathname === \"\";\n  let toPathname = isEmptyPath ? \"/\" : to.pathname;\n\n  let from: string;\n\n  // Routing is relative to the current pathname if explicitly requested.\n  //\n  // If a pathname is explicitly provided in `to`, it should be relative to the\n  // route context. This is explained in `Note on `<Link to>` values` in our\n  // migration guide from v5 as a means of disambiguation between `to` values\n  // that begin with `/` and those that do not. However, this is problematic for\n  // `to` values that do not provide a pathname. `to` can simply be a search or\n  // hash string, in which case we should assume that the navigation is relative\n  // to the current location's pathname and *not* the route pathname.\n  if (toPathname == null) {\n    from = locationPathname;\n  } else {\n    let routePathnameIndex = routePathnames.length - 1;\n\n    // With relative=\"route\" (the default), each leading .. segment means\n    // \"go up one route\" instead of \"go up one URL segment\".  This is a key\n    // difference from how <a href> works and a major reason we call this a\n    // \"to\" value instead of a \"href\".\n    if (!isPathRelative && toPathname.startsWith(\"..\")) {\n      let toSegments = toPathname.split(\"/\");\n\n      while (toSegments[0] === \"..\") {\n        toSegments.shift();\n        routePathnameIndex -= 1;\n      }\n\n      to.pathname = toSegments.join(\"/\");\n    }\n\n    from = routePathnameIndex >= 0 ? routePathnames[routePathnameIndex] : \"/\";\n  }\n\n  let path = resolvePath(to, from);\n\n  // Ensure the pathname has a trailing slash if the original \"to\" had one\n  let hasExplicitTrailingSlash =\n    toPathname && toPathname !== \"/\" && toPathname.endsWith(\"/\");\n  // Or if this was a link to the current path which has a trailing slash\n  let hasCurrentTrailingSlash =\n    (isEmptyPath || toPathname === \".\") && locationPathname.endsWith(\"/\");\n  if (\n    !path.pathname.endsWith(\"/\") &&\n    (hasExplicitTrailingSlash || hasCurrentTrailingSlash)\n  ) {\n    path.pathname += \"/\";\n  }\n\n  return path;\n}\n\nexport const joinPaths = (paths: string[]): string =>\n  paths.join(\"/\").replace(/\\/\\/+/g, \"/\");\n\nexport const normalizePathname = (pathname: string): string =>\n  pathname.replace(/\\/+$/, \"\").replace(/^\\/*/, \"/\");\n\n/*\nlol - this comment is needed because the JSDoc parser for docs.ts gets confused\nby the star-slash in the normalizePathname regex and messes up the parsed comment\nfor data() below.  This comment seems to reset the parser.\n*/\n\nexport const normalizeSearch = (search: string): string =>\n  !search || search === \"?\"\n    ? \"\"\n    : search.startsWith(\"?\")\n      ? search\n      : \"?\" + search;\n\nexport const normalizeHash = (hash: string): string =>\n  !hash || hash === \"#\" ? \"\" : hash.startsWith(\"#\") ? hash : \"#\" + hash;\n\nexport class DataWithResponseInit<D> {\n  type: string = \"DataWithResponseInit\";\n  data: D;\n  init: ResponseInit | null;\n\n  constructor(data: D, init?: ResponseInit) {\n    this.data = data;\n    this.init = init || null;\n  }\n}\n\n/**\n * Create \"responses\" that contain `headers`/`status` without forcing\n * serialization into an actual [`Response`](https://developer.mozilla.org/en-US/docs/Web/API/Response)\n *\n * @example\n * import { data } from \"react-router\";\n *\n * export async function action({ request }: Route.ActionArgs) {\n *   let formData = await request.formData();\n *   let item = await createItem(formData);\n *   return data(item, {\n *     headers: { \"X-Custom-Header\": \"value\" }\n *     status: 201,\n *   });\n * }\n *\n * @public\n * @category Utils\n * @mode framework\n * @mode data\n * @param data The data to be included in the response.\n * @param init The status code or a `ResponseInit` object to be included in the\n * response.\n * @returns A {@link DataWithResponseInit} instance containing the data and\n * response init.\n */\nexport function data<D>(data: D, init?: number | ResponseInit) {\n  return new DataWithResponseInit(\n    data,\n    typeof init === \"number\" ? { status: init } : init,\n  );\n}\n\n// This is now only used by the Await component and will eventually probably\n// go away in favor of the format used by `React.use`\nexport interface TrackedPromise extends Promise<any> {\n  _tracked?: boolean;\n  _data?: any;\n  _error?: any;\n}\n\nexport type RedirectFunction = (\n  url: string,\n  init?: number | ResponseInit,\n) => Response;\n\n/**\n * A redirect [`Response`](https://developer.mozilla.org/en-US/docs/Web/API/Response).\n * Sets the status code and the [`Location`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Location)\n * header. Defaults to [`302 Found`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/302).\n *\n * @example\n * import { redirect } from \"react-router\";\n *\n * export async function loader({ request }: Route.LoaderArgs) {\n *   if (!isLoggedIn(request))\n *     throw redirect(\"/login\");\n *   }\n *\n *   // ...\n * }\n *\n * @public\n * @category Utils\n * @mode framework\n * @mode data\n * @param url The URL to redirect to.\n * @param init The status code or a `ResponseInit` object to be included in the\n * response.\n * @returns A [`Response`](https://developer.mozilla.org/en-US/docs/Web/API/Response)\n * object with the redirect status and [`Location`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Location)\n * header.\n */\nexport const redirect: RedirectFunction = (url, init = 302) => {\n  let responseInit = init;\n  if (typeof responseInit === \"number\") {\n    responseInit = { status: responseInit };\n  } else if (typeof responseInit.status === \"undefined\") {\n    responseInit.status = 302;\n  }\n\n  let headers = new Headers(responseInit.headers);\n  headers.set(\"Location\", url);\n\n  return new Response(null, { ...responseInit, headers });\n};\n\n/**\n * A redirect [`Response`](https://developer.mozilla.org/en-US/docs/Web/API/Response)\n * that will force a document reload to the new location. Sets the status code\n * and the [`Location`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Location)\n * header. Defaults to [`302 Found`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/302).\n *\n * ```tsx filename=routes/logout.tsx\n * import { redirectDocument } from \"react-router\";\n *\n * import { destroySession } from \"../sessions.server\";\n *\n * export async function action({ request }: Route.ActionArgs) {\n *   let session = await getSession(request.headers.get(\"Cookie\"));\n *   return redirectDocument(\"/\", {\n *     headers: { \"Set-Cookie\": await destroySession(session) }\n *   });\n * }\n * ```\n *\n * @public\n * @category Utils\n * @mode framework\n * @mode data\n * @param url The URL to redirect to.\n * @param init The status code or a `ResponseInit` object to be included in the\n * response.\n * @returns A [`Response`](https://developer.mozilla.org/en-US/docs/Web/API/Response)\n * object with the redirect status and [`Location`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Location)\n * header.\n */\nexport const redirectDocument: RedirectFunction = (url, init) => {\n  let response = redirect(url, init);\n  response.headers.set(\"X-Remix-Reload-Document\", \"true\");\n  return response;\n};\n\n/**\n * A redirect [`Response`](https://developer.mozilla.org/en-US/docs/Web/API/Response)\n * that will perform a [`history.replaceState`](https://developer.mozilla.org/en-US/docs/Web/API/History/replaceState)\n * instead of a [`history.pushState`](https://developer.mozilla.org/en-US/docs/Web/API/History/pushState)\n * for client-side navigation redirects. Sets the status code and the [`Location`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Location)\n * header. Defaults to [`302 Found`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/302).\n *\n * @example\n * import { replace } from \"react-router\";\n *\n * export async function loader() {\n *   return replace(\"/new-location\");\n * }\n *\n * @public\n * @category Utils\n * @mode framework\n * @mode data\n * @param url The URL to redirect to.\n * @param init The status code or a `ResponseInit` object to be included in the\n * response.\n * @returns A [`Response`](https://developer.mozilla.org/en-US/docs/Web/API/Response)\n * object with the redirect status and [`Location`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Location)\n * header.\n */\nexport const replace: RedirectFunction = (url, init) => {\n  let response = redirect(url, init);\n  response.headers.set(\"X-Remix-Replace\", \"true\");\n  return response;\n};\n\nexport type ErrorResponse = {\n  status: number;\n  statusText: string;\n  data: any;\n};\n\n/*\n * Utility class we use to hold auto-unwrapped 4xx/5xx Response bodies\n *\n * We don't export the class for public use since it's an implementation\n * detail, but we export the interface above so folks can build their own\n * abstractions around instances via isRouteErrorResponse()\n */\nexport class ErrorResponseImpl implements ErrorResponse {\n  status: number;\n  statusText: string;\n  data: any;\n  private error?: Error;\n  private internal: boolean;\n\n  constructor(\n    status: number,\n    statusText: string | undefined,\n    data: any,\n    internal = false,\n  ) {\n    this.status = status;\n    this.statusText = statusText || \"\";\n    this.internal = internal;\n    if (data instanceof Error) {\n      this.data = data.toString();\n      this.error = data;\n    } else {\n      this.data = data;\n    }\n  }\n}\n\n/**\n * Check if the given error is an {@link ErrorResponse} generated from a 4xx/5xx\n * [`Response`](https://developer.mozilla.org/en-US/docs/Web/API/Response)\n * thrown from an [`action`](../../start/framework/route-module#action) or\n * [`loader`](../../start/framework/route-module#loader) function.\n *\n * @example\n * import { isRouteErrorResponse } from \"react-router\";\n *\n * export function ErrorBoundary({ error }: Route.ErrorBoundaryProps) {\n *   if (isRouteErrorResponse(error)) {\n *     return (\n *       <>\n *         <p>Error: `${error.status}: ${error.statusText}`</p>\n *         <p>{error.data}</p>\n *       </>\n *     );\n *   }\n *\n *   return (\n *     <p>Error: {error instanceof Error ? error.message : \"Unknown Error\"}</p>\n *   );\n * }\n *\n * @public\n * @category Utils\n * @mode framework\n * @mode data\n * @param error The error to check.\n * @returns `true` if the error is an {@link ErrorResponse}, `false` otherwise.\n */\nexport function isRouteErrorResponse(error: any): error is ErrorResponse {\n  return (\n    error != null &&\n    typeof error.status === \"number\" &&\n    typeof error.statusText === \"string\" &&\n    typeof error.internal === \"boolean\" &&\n    \"data\" in error\n  );\n}\n\n/*\nlol - this comment is needed because the JSDoc parser for `docs.ts` gets confused\nby the star-slash in the `getRoutePattern` regex and messes up the parsed comment\nfor `isRouteErrorResponse` above.  This comment seems to reset the parser.\n*/\n\nexport function getRoutePattern(matches: AgnosticRouteMatch[]) {\n  return (\n    matches\n      .map((m) => m.route.path)\n      .filter(Boolean)\n      .join(\"/\")\n      .replace(/\\/\\/*/g, \"/\") || \"/\"\n  );\n}\n\nexport const isBrowser =\n  typeof window !== \"undefined\" &&\n  typeof window.document !== \"undefined\" &&\n  typeof window.document.createElement !== \"undefined\";\n\nexport type ParsedLocationInfo<T extends To> =\n  | {\n      absoluteURL: string;\n      isExternal: boolean;\n      to: string;\n    }\n  | {\n      absoluteURL: undefined;\n      isExternal: false;\n      to: T;\n    };\nexport function parseToInfo<T extends To | string>(\n  _to: T,\n  basename: string,\n): ParsedLocationInfo<T | string> {\n  let to = _to as string;\n  if (typeof to !== \"string\" || !ABSOLUTE_URL_REGEX.test(to)) {\n    return {\n      absoluteURL: undefined,\n      isExternal: false,\n      to,\n    };\n  }\n\n  let absoluteURL = to;\n  let isExternal = false;\n  if (isBrowser) {\n    try {\n      let currentUrl = new URL(window.location.href);\n      let targetUrl = to.startsWith(\"//\")\n        ? new URL(currentUrl.protocol + to)\n        : new URL(to);\n      let path = stripBasename(targetUrl.pathname, basename);\n\n      if (targetUrl.origin === currentUrl.origin && path != null) {\n        // Strip the protocol/origin/basename for same-origin absolute URLs\n        to = path + targetUrl.search + targetUrl.hash;\n      } else {\n        isExternal = true;\n      }\n    } catch (e) {\n      // We can't do external URL detection without a valid URL\n      warning(\n        false,\n        `<Link to=\"${to}\"> contains an invalid URL which will probably break ` +\n          `when clicked - please update to a valid URL path.`,\n      );\n    }\n  }\n\n  return {\n    absoluteURL,\n    isExternal,\n    to,\n  };\n}\n"
  },
  {
    "path": "packages/react-router/lib/rsc/browser.tsx",
    "content": "import * as React from \"react\";\nimport * as ReactDOM from \"react-dom\";\n\nimport { RouterProvider } from \"../components\";\nimport {\n  RSCRouterContext,\n  type DataRouteMatch,\n  type DataRouteObject,\n} from \"../context\";\nimport { FrameworkContext, setIsHydrated } from \"../dom/ssr/components\";\nimport type { FrameworkContextObject } from \"../dom/ssr/entry\";\nimport { createBrowserHistory, invariant } from \"../router/history\";\nimport type { Router as DataRouter, RouterInit } from \"../router/router\";\nimport { createRouter, isMutationMethod } from \"../router/router\";\nimport type {\n  RSCPayload,\n  RSCRouteManifest,\n  RSCRenderPayload,\n} from \"./server.rsc\";\nimport type {\n  AgnosticDataRouteObject,\n  DataStrategyFunction,\n  DataStrategyFunctionArgs,\n  RouterContextProvider,\n} from \"../router/utils\";\nimport { ErrorResponseImpl, createContext } from \"../router/utils\";\nimport type {\n  DecodedSingleFetchResults,\n  FetchAndDecodeFunction,\n} from \"../dom/ssr/single-fetch\";\nimport {\n  getSingleFetchDataStrategyImpl,\n  singleFetchUrl,\n  stripIndexParam,\n} from \"../dom/ssr/single-fetch\";\nimport { createRequestInit } from \"../dom/ssr/data\";\nimport { getHydrationData } from \"../dom/ssr/hydration\";\nimport {\n  noActionDefinedError,\n  shouldHydrateRouteLoader,\n} from \"../dom/ssr/routes\";\nimport { RSCRouterGlobalErrorBoundary } from \"./errorBoundaries\";\nimport type { RouteModules } from \"../dom/ssr/routeModules\";\nimport { populateRSCRouteModules } from \"./route-modules\";\n\nexport type BrowserCreateFromReadableStreamFunction = (\n  body: ReadableStream<Uint8Array>,\n  {\n    temporaryReferences,\n  }: {\n    temporaryReferences: unknown;\n  },\n) => Promise<unknown>;\n\nexport type EncodeReplyFunction = (\n  args: unknown[],\n  options: { temporaryReferences: unknown },\n) => Promise<BodyInit>;\n\ntype WindowWithRouterGlobals = Window &\n  typeof globalThis & {\n    __reactRouterDataRouter: DataRouter;\n    __routerInitialized: boolean;\n    __routerActionID: number;\n  };\n\n/**\n * Create a React `callServer` implementation for React Router.\n *\n * @example\n * import {\n *   createFromReadableStream,\n *   createTemporaryReferenceSet,\n *   encodeReply,\n *   setServerCallback,\n * } from \"@vitejs/plugin-rsc/browser\";\n * import { unstable_createCallServer as createCallServer } from \"react-router\";\n *\n * setServerCallback(\n *   createCallServer({\n *     createFromReadableStream,\n *     createTemporaryReferenceSet,\n *     encodeReply,\n *   })\n * );\n *\n * @name unstable_createCallServer\n * @public\n * @category RSC\n * @mode data\n * @param opts Options\n * @param opts.createFromReadableStream Your `react-server-dom-xyz/client`'s\n * `createFromReadableStream`. Used to decode payloads from the server.\n * @param opts.createTemporaryReferenceSet A function that creates a temporary\n * reference set for the [RSC](https://react.dev/reference/rsc/server-components)\n * payload.\n * @param opts.encodeReply Your `react-server-dom-xyz/client`'s `encodeReply`.\n * Used when sending payloads to the server.\n * @param opts.fetch Optional [Fetch API](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API)\n * implementation. Defaults to global [`fetch`](https://developer.mozilla.org/en-US/docs/Web/API/fetch).\n * @returns A function that can be used to call server actions.\n */\nexport function createCallServer({\n  createFromReadableStream,\n  createTemporaryReferenceSet,\n  encodeReply,\n  fetch: fetchImplementation = fetch,\n}: {\n  createFromReadableStream: BrowserCreateFromReadableStreamFunction;\n  createTemporaryReferenceSet: () => unknown;\n  encodeReply: EncodeReplyFunction;\n  fetch?: (request: Request) => Promise<Response>;\n}) {\n  const globalVar = window as WindowWithRouterGlobals;\n\n  let landedActionId = 0;\n  return async (id: string, args: unknown[]) => {\n    let actionId = (globalVar.__routerActionID =\n      (globalVar.__routerActionID ??= 0) + 1);\n\n    const temporaryReferences = createTemporaryReferenceSet();\n    const payloadPromise = fetchImplementation(\n      new Request(location.href, {\n        body: await encodeReply(args, { temporaryReferences }),\n        method: \"POST\",\n        headers: {\n          Accept: \"text/x-component\",\n          \"rsc-action-id\": id,\n        },\n      }),\n    ).then((response) => {\n      if (!response.body) {\n        throw new Error(\"No response body\");\n      }\n      return createFromReadableStream(response.body, {\n        temporaryReferences,\n      }) as Promise<RSCPayload>;\n    });\n\n    React.startTransition(() =>\n      // @ts-expect-error - Needs React 19 types\n      Promise.resolve(payloadPromise)\n        .then(async (payload) => {\n          if (payload.type === \"redirect\") {\n            if (payload.reload || isExternalLocation(payload.location)) {\n              window.location.href = payload.location;\n              return;\n            }\n\n            React.startTransition(() => {\n              globalVar.__reactRouterDataRouter.navigate(payload.location, {\n                replace: payload.replace,\n              });\n            });\n            return;\n          }\n\n          if (payload.type !== \"action\") {\n            throw new Error(\"Unexpected payload type\");\n          }\n\n          const rerender = await payload.rerender;\n          if (\n            rerender &&\n            landedActionId < actionId &&\n            globalVar.__routerActionID <= actionId\n          ) {\n            if (rerender.type === \"redirect\") {\n              if (rerender.reload || isExternalLocation(rerender.location)) {\n                window.location.href = rerender.location;\n                return;\n              }\n              React.startTransition(() => {\n                globalVar.__reactRouterDataRouter.navigate(rerender.location, {\n                  replace: rerender.replace,\n                });\n              });\n              return;\n            }\n\n            React.startTransition(() => {\n              let lastMatch: RSCRouteManifest | undefined;\n              for (const match of rerender.matches) {\n                globalVar.__reactRouterDataRouter.patchRoutes(\n                  lastMatch?.id ?? null,\n                  [createRouteFromServerManifest(match)],\n                  true,\n                );\n                lastMatch = match;\n              }\n\n              (\n                window as WindowWithRouterGlobals\n              ).__reactRouterDataRouter._internalSetStateDoNotUseOrYouWillBreakYourApp(\n                {\n                  loaderData: Object.assign(\n                    {},\n                    globalVar.__reactRouterDataRouter.state.loaderData,\n                    rerender.loaderData,\n                  ),\n                  errors: rerender.errors\n                    ? Object.assign(\n                        {},\n                        globalVar.__reactRouterDataRouter.state.errors,\n                        rerender.errors,\n                      )\n                    : null,\n                },\n              );\n            });\n          }\n        })\n        .catch(() => {}),\n    );\n\n    return payloadPromise.then((payload) => {\n      if (payload.type !== \"action\" && payload.type !== \"redirect\") {\n        throw new Error(\"Unexpected payload type\");\n      }\n\n      return payload.actionResult;\n    });\n  };\n}\n\nfunction createRouterFromPayload({\n  fetchImplementation,\n  createFromReadableStream,\n  getContext,\n  payload,\n}: {\n  payload: RSCPayload;\n  createFromReadableStream: BrowserCreateFromReadableStreamFunction;\n  fetchImplementation: (request: Request) => Promise<Response>;\n  getContext: RouterInit[\"getContext\"] | undefined;\n}): {\n  router: DataRouter;\n  routeModules: RouteModules;\n} {\n  const globalVar = window as WindowWithRouterGlobals;\n\n  if (globalVar.__reactRouterDataRouter && globalVar.__reactRouterRouteModules)\n    return {\n      router: globalVar.__reactRouterDataRouter,\n      routeModules: globalVar.__reactRouterRouteModules,\n    };\n\n  if (payload.type !== \"render\") throw new Error(\"Invalid payload type\");\n\n  globalVar.__reactRouterRouteModules =\n    globalVar.__reactRouterRouteModules ?? {};\n  populateRSCRouteModules(globalVar.__reactRouterRouteModules, payload.matches);\n\n  let patches = new Map<string, RSCRouteManifest[]>();\n  payload.patches?.forEach((patch) => {\n    invariant(patch.parentId, \"Invalid patch parentId\");\n    if (!patches.has(patch.parentId)) {\n      patches.set(patch.parentId, []);\n    }\n    patches.get(patch.parentId)?.push(patch);\n  });\n  let routes = payload.matches.reduceRight((previous, match) => {\n    const route: DataRouteObject = createRouteFromServerManifest(\n      match,\n      payload,\n    );\n    if (previous.length > 0) {\n      route.children = previous;\n      let childrenToPatch = patches.get(match.id);\n      if (childrenToPatch) {\n        route.children.push(\n          ...childrenToPatch.map((r) => createRouteFromServerManifest(r)),\n        );\n      }\n    }\n    return [route];\n  }, [] as DataRouteObject[]);\n\n  globalVar.__reactRouterDataRouter = createRouter({\n    routes,\n    getContext,\n    basename: payload.basename,\n    history: createBrowserHistory(),\n    hydrationData: getHydrationData({\n      state: {\n        loaderData: payload.loaderData,\n        actionData: payload.actionData,\n        errors: payload.errors,\n      },\n      routes,\n      getRouteInfo: (routeId) => {\n        let match = payload.matches.find((m) => m.id === routeId);\n        invariant(match, \"Route not found in payload\");\n        return {\n          clientLoader: match.clientLoader,\n          hasLoader: match.hasLoader,\n          hasHydrateFallback: match.hydrateFallbackElement != null,\n        };\n      },\n      location: payload.location,\n      basename: payload.basename,\n      isSpaMode: false,\n    }),\n    async patchRoutesOnNavigation({ path, signal }) {\n      if (discoveredPaths.has(path)) {\n        return;\n      }\n      await fetchAndApplyManifestPatches(\n        [path],\n        createFromReadableStream,\n        fetchImplementation,\n        signal,\n      );\n    },\n    // FIXME: Pass `build.ssr` into this function\n    dataStrategy: getRSCSingleFetchDataStrategy(\n      () => globalVar.__reactRouterDataRouter,\n      true,\n      payload.basename,\n      createFromReadableStream,\n      fetchImplementation,\n    ),\n  });\n\n  // We can call initialize() immediately if the router doesn't have any\n  // loaders to run on hydration\n  if (globalVar.__reactRouterDataRouter.state.initialized) {\n    globalVar.__routerInitialized = true;\n    globalVar.__reactRouterDataRouter.initialize();\n  } else {\n    globalVar.__routerInitialized = false;\n  }\n\n  let lastLoaderData: unknown = undefined;\n  globalVar.__reactRouterDataRouter.subscribe(({ loaderData, actionData }) => {\n    if (lastLoaderData !== loaderData) {\n      globalVar.__routerActionID = (globalVar.__routerActionID ??= 0) + 1;\n    }\n  });\n\n  // @ts-expect-error\n  globalVar.__reactRouterDataRouter._updateRoutesForHMR = (\n    routeUpdateByRouteId: Map<\n      string,\n      {\n        routeModule: any;\n        hasAction: boolean;\n        hasComponent: boolean;\n        hasErrorBoundary: boolean;\n        hasLoader: boolean;\n      }\n    >,\n  ) => {\n    const oldRoutes = (window as WindowWithRouterGlobals)\n      .__reactRouterDataRouter.routes;\n    const newRoutes: DataRouteObjectWithManifestInfo[] = [];\n\n    function walkRoutes(\n      routes: DataRouteObjectWithManifestInfo[],\n      parentId?: string,\n    ): DataRouteObjectWithManifestInfo[] {\n      return routes.map((route) => {\n        const routeUpdate = routeUpdateByRouteId.get(route.id);\n\n        if (routeUpdate) {\n          const {\n            routeModule,\n            hasAction,\n            hasComponent,\n            hasErrorBoundary,\n            hasLoader,\n          } = routeUpdate;\n          const newRoute = createRouteFromServerManifest({\n            clientAction: routeModule.clientAction,\n            clientLoader: routeModule.clientLoader,\n            element: route.element as React.ReactElement,\n            errorElement: route.errorElement as React.ReactElement,\n            handle: route.handle,\n            hasAction,\n            hasComponent,\n            hasErrorBoundary,\n            hasLoader,\n            hydrateFallbackElement:\n              route.hydrateFallbackElement as React.ReactElement,\n            id: route.id,\n            index: route.index,\n            links: routeModule.links,\n            meta: routeModule.meta,\n            parentId: parentId,\n            path: route.path,\n            shouldRevalidate: routeModule.shouldRevalidate,\n          });\n          if (route.children) {\n            newRoute.children = walkRoutes(route.children, route.id);\n          }\n          return newRoute;\n        }\n\n        const updatedRoute = { ...route };\n        if (route.children) {\n          updatedRoute.children = walkRoutes(route.children, route.id);\n        }\n        return updatedRoute;\n      });\n    }\n\n    newRoutes.push(\n      ...walkRoutes(oldRoutes as DataRouteObjectWithManifestInfo[], undefined),\n    );\n\n    (\n      window as WindowWithRouterGlobals\n    ).__reactRouterDataRouter._internalSetRoutes(newRoutes);\n  };\n\n  return {\n    router: globalVar.__reactRouterDataRouter,\n    routeModules: globalVar.__reactRouterRouteModules,\n  };\n}\n\nconst renderedRoutesContext = createContext<RSCRouteManifest[]>();\n\nexport function getRSCSingleFetchDataStrategy(\n  getRouter: () => DataRouter,\n  ssr: boolean,\n  basename: string | undefined,\n  createFromReadableStream: BrowserCreateFromReadableStreamFunction,\n  fetchImplementation: (request: Request) => Promise<Response>,\n): DataStrategyFunction {\n  // TODO: Clean this up with a shared type\n  type RSCDataRouteMatch = DataRouteMatch & {\n    route: DataRouteObject & {\n      hasLoader: boolean;\n      hasClientLoader: boolean;\n      hasComponent: boolean;\n      hasAction: boolean;\n      hasClientAction: boolean;\n      hasShouldRevalidate: boolean;\n    };\n  };\n\n  // create map\n  let dataStrategy = getSingleFetchDataStrategyImpl(\n    getRouter,\n    (match) => {\n      let M = match as RSCDataRouteMatch;\n      return {\n        hasLoader: M.route.hasLoader,\n        hasClientLoader: M.route.hasClientLoader,\n        hasComponent: M.route.hasComponent,\n        hasAction: M.route.hasAction,\n        hasClientAction: M.route.hasClientAction,\n        hasShouldRevalidate: M.route.hasShouldRevalidate,\n      };\n    },\n    // pass map into fetchAndDecode so it can add payloads\n    getFetchAndDecodeViaRSC(createFromReadableStream, fetchImplementation),\n    ssr,\n    basename,\n    // .rsc requests are always trailing slash aware\n    true,\n    // If the route has a component but we don't have an element, we need to hit\n    // the server loader flow regardless of whether the client loader calls\n    // `serverLoader` or not, otherwise we'll have nothing to render.\n    (match) => {\n      let M = match as RSCDataRouteMatch;\n      return M.route.hasComponent && !M.route.element;\n    },\n  );\n  return async (args) =>\n    args.runClientMiddleware(async () => {\n      // Before we run the dataStrategy, create a place to stick rendered routes\n      // from the payload so we can patch them into the router after all loaders\n      // have completed.  Need to do this since we may have multiple fetch\n      // requests returning multiple server payloads (due to clientLoaders, fine\n      // grained revalidation, etc.).  This lets us stitch them all together and\n      // patch them all at the end\n      // This cast should be fine since this is always run client side and\n      // `context` is always of this type on the client -- unlike on the server\n      // in framework mode when it could be `AppLoadContext`\n      let context = args.context as RouterContextProvider;\n      context.set(renderedRoutesContext, []);\n      let results = await dataStrategy(args);\n      // patch into router from all payloads in map\n      // TODO: Confirm that it's correct for us to have multiple rendered routes\n      // with the same ID. This is currently happening in `clientLoader` cases\n      // where we're calling `fetchAndDecode` multiple times. This may be a\n      // sign of a logical error in how we're handling client loader routes.\n      const renderedRoutesById = new Map<string, RSCRouteManifest[]>();\n      for (const route of context.get(renderedRoutesContext)) {\n        if (!renderedRoutesById.has(route.id)) {\n          renderedRoutesById.set(route.id, []);\n        }\n        renderedRoutesById.get(route.id)!.push(route);\n      }\n\n      React.startTransition(() => {\n        for (const match of args.matches) {\n          const renderedRoutes = renderedRoutesById.get(match.route.id);\n          if (renderedRoutes) {\n            for (const rendered of renderedRoutes) {\n              (\n                window as WindowWithRouterGlobals\n              ).__reactRouterDataRouter.patchRoutes(\n                rendered.parentId ?? null,\n                [createRouteFromServerManifest(rendered)],\n                true,\n              );\n            }\n          }\n        }\n      });\n      return results;\n    });\n}\n\nfunction getFetchAndDecodeViaRSC(\n  createFromReadableStream: BrowserCreateFromReadableStreamFunction,\n  fetchImplementation: (request: Request) => Promise<Response>,\n): FetchAndDecodeFunction {\n  return async (\n    args: DataStrategyFunctionArgs<unknown>,\n    basename: string | undefined,\n    trailingSlashAware: boolean,\n    targetRoutes?: string[],\n  ) => {\n    let { request, context } = args;\n    let url = singleFetchUrl(request.url, basename, trailingSlashAware, \"rsc\");\n    if (request.method === \"GET\") {\n      url = stripIndexParam(url);\n      if (targetRoutes) {\n        url.searchParams.set(\"_routes\", targetRoutes.join(\",\"));\n      }\n    }\n\n    let res = await fetchImplementation(\n      new Request(url, await createRequestInit(request)),\n    );\n\n    // If this error'd without hitting the running server, then bubble a normal\n    // `ErrorResponse` and don't try to decode the body with `turbo-stream`.\n    //\n    // This could be triggered by a few scenarios:\n    // - `.data` request 404 on a pre-rendered app using a CDN\n    // - 429 error returned from a CDN on a SSR app\n    if (res.status >= 400 && !res.headers.has(\"X-Remix-Response\")) {\n      throw new ErrorResponseImpl(res.status, res.statusText, await res.text());\n    }\n\n    invariant(res.body, \"No response body to decode\");\n\n    try {\n      const payload = (await createFromReadableStream(res.body, {\n        temporaryReferences: undefined,\n      })) as RSCPayload;\n      if (payload.type === \"redirect\") {\n        return {\n          status: res.status,\n          data: {\n            redirect: {\n              redirect: payload.location,\n              reload: payload.reload,\n              replace: payload.replace,\n              revalidate: false,\n              status: payload.status,\n            },\n          },\n        };\n      }\n\n      if (payload.type !== \"render\") {\n        throw new Error(\"Unexpected payload type\");\n      }\n\n      // Track routes rendered per-single-fetch call so we can gather them up\n      // and patch them in together at the end.  This cast should be fine since\n      // this is always run client side and `context` is always of this type on\n      // the client -- unlike on the server in framework mode when it could be\n      // `AppLoadContext`\n      (context as RouterContextProvider)\n        .get(renderedRoutesContext)\n        .push(...payload.matches);\n\n      let results: DecodedSingleFetchResults = { routes: {} };\n      const dataKey = isMutationMethod(request.method)\n        ? \"actionData\"\n        : \"loaderData\";\n      for (let [routeId, data] of Object.entries(payload[dataKey] || {})) {\n        results.routes[routeId] = { data };\n      }\n      if (payload.errors) {\n        for (let [routeId, error] of Object.entries(payload.errors)) {\n          results.routes[routeId] = { error };\n        }\n      }\n      return { status: res.status, data: results };\n    } catch (e) {\n      // Can't clone after consuming the body via decode so we can't include the\n      // body here.  In an ideal world we'd look for an RSC  content type here,\n      // or even X-Remix-Response but then folks can't statically deploy their\n      // prerendered .rsc files to a CDN unless they can tell that CDN to add\n      // special headers to those certain files - which is a bit restrictive.\n      throw new Error(\"Unable to decode RSC response\");\n    }\n  };\n}\n\n/**\n * Props for the {@link unstable_RSCHydratedRouter} component.\n *\n * @name unstable_RSCHydratedRouterProps\n * @category Types\n */\nexport interface RSCHydratedRouterProps {\n  /**\n   * Your `react-server-dom-xyz/client`'s `createFromReadableStream` function,\n   * used to decode payloads from the server.\n   */\n  createFromReadableStream: BrowserCreateFromReadableStreamFunction;\n  /**\n   * Optional fetch implementation. Defaults to global [`fetch`](https://developer.mozilla.org/en-US/docs/Web/API/fetch).\n   */\n  fetch?: (request: Request) => Promise<Response>;\n  /**\n   * The decoded {@link unstable_RSCPayload} to hydrate.\n   */\n  payload: RSCPayload;\n  /**\n   * `\"eager\"` or `\"lazy\"` - Determines if links are eagerly discovered, or\n   * delayed until clicked.\n   */\n  routeDiscovery?: \"eager\" | \"lazy\";\n  /**\n   * A function that returns an {@link RouterContextProvider} instance\n   * which is provided as the `context` argument to client [`action`](../../start/data/route-object#action)s,\n   * [`loader`](../../start/data/route-object#loader)s and [middleware](../../how-to/middleware).\n   * This function is called to generate a fresh `context` instance on each\n   * navigation or fetcher call.\n   */\n  getContext?: RouterInit[\"getContext\"];\n}\n\n/**\n * Hydrates a server rendered {@link unstable_RSCPayload} in the browser.\n *\n * @example\n * import { startTransition, StrictMode } from \"react\";\n * import { hydrateRoot } from \"react-dom/client\";\n * import {\n *   unstable_getRSCStream as getRSCStream,\n *   unstable_RSCHydratedRouter as RSCHydratedRouter,\n * } from \"react-router\";\n * import type { unstable_RSCPayload as RSCPayload } from \"react-router\";\n *\n * createFromReadableStream(getRSCStream()).then((payload) =>\n *   startTransition(async () => {\n *     hydrateRoot(\n *       document,\n *       <StrictMode>\n *         <RSCHydratedRouter\n *           createFromReadableStream={createFromReadableStream}\n *           payload={payload}\n *         />\n *       </StrictMode>,\n *       { formState: await getFormState(payload) },\n *     );\n *   }),\n * );\n *\n * @name unstable_RSCHydratedRouter\n * @public\n * @category RSC\n * @mode data\n * @param props Props\n * @param {unstable_RSCHydratedRouterProps.createFromReadableStream} props.createFromReadableStream n/a\n * @param {unstable_RSCHydratedRouterProps.fetch} props.fetch n/a\n * @param {unstable_RSCHydratedRouterProps.getContext} props.getContext n/a\n * @param {unstable_RSCHydratedRouterProps.payload} props.payload n/a\n * @param {unstable_RSCHydratedRouterProps.routeDiscovery} props.routeDiscovery n/a\n * @returns A hydrated {@link DataRouter} that can be used to navigate and\n * render routes.\n */\nexport function RSCHydratedRouter({\n  createFromReadableStream,\n  fetch: fetchImplementation = fetch,\n  payload,\n  routeDiscovery = \"eager\",\n  getContext,\n}: RSCHydratedRouterProps) {\n  if (payload.type !== \"render\") throw new Error(\"Invalid payload type\");\n\n  let { router, routeModules } = React.useMemo(\n    () =>\n      createRouterFromPayload({\n        payload,\n        fetchImplementation,\n        getContext,\n        createFromReadableStream,\n      }),\n    [createFromReadableStream, payload, fetchImplementation, getContext],\n  );\n\n  React.useEffect(() => {\n    setIsHydrated();\n  }, []);\n\n  React.useLayoutEffect(() => {\n    // If we had to run clientLoaders on hydration, we delay initialization until\n    // after we've hydrated to avoid hydration issues from synchronous client loaders\n    const globalVar = window as WindowWithRouterGlobals;\n    if (!globalVar.__routerInitialized) {\n      globalVar.__routerInitialized = true;\n      globalVar.__reactRouterDataRouter.initialize();\n    }\n  }, []);\n\n  let [{ routes, state }, setState] = React.useState(() => ({\n    routes: cloneRoutes(router.routes),\n    state: router.state,\n  }));\n\n  React.useLayoutEffect(\n    () =>\n      router.subscribe((newState) => {\n        if (diffRoutes(router.routes, routes))\n          React.startTransition(() => {\n            setState({\n              routes: cloneRoutes(router.routes),\n              state: newState,\n            });\n          });\n      }),\n    [router.subscribe, routes, router],\n  );\n\n  const transitionEnabledRouter = React.useMemo(\n    () =>\n      ({\n        ...router,\n        state,\n        routes,\n      }) as typeof router,\n    [router, routes, state],\n  );\n\n  React.useEffect(() => {\n    if (\n      routeDiscovery === \"lazy\" ||\n      // @ts-expect-error - TS doesn't know about this yet\n      window.navigator?.connection?.saveData === true\n    ) {\n      return;\n    }\n\n    // Register a link href for patching\n    function registerElement(el: Element) {\n      let path =\n        el.tagName === \"FORM\"\n          ? el.getAttribute(\"action\")\n          : el.getAttribute(\"href\");\n      if (!path) {\n        return;\n      }\n      // optimization: use the already-parsed pathname from links\n      let pathname =\n        el.tagName === \"A\"\n          ? (el as HTMLAnchorElement).pathname\n          : new URL(path, window.location.origin).pathname;\n      if (!discoveredPaths.has(pathname)) {\n        nextPaths.add(pathname);\n      }\n    }\n\n    // Register and fetch patches for all initially-rendered links/forms\n    async function fetchPatches() {\n      // re-check/update registered links\n      document\n        .querySelectorAll(\"a[data-discover], form[data-discover]\")\n        .forEach(registerElement);\n\n      let paths = Array.from(nextPaths.keys()).filter((path) => {\n        if (discoveredPaths.has(path)) {\n          nextPaths.delete(path);\n          return false;\n        }\n        return true;\n      });\n\n      if (paths.length === 0) {\n        return;\n      }\n\n      try {\n        await fetchAndApplyManifestPatches(\n          paths,\n          createFromReadableStream,\n          fetchImplementation,\n        );\n      } catch (e) {\n        console.error(\"Failed to fetch manifest patches\", e);\n      }\n    }\n\n    let debouncedFetchPatches = debounce(fetchPatches, 100);\n\n    // scan and fetch initial links\n    fetchPatches();\n\n    let observer = new MutationObserver(() => debouncedFetchPatches());\n\n    observer.observe(document.documentElement, {\n      subtree: true,\n      childList: true,\n      attributes: true,\n      attributeFilter: [\"data-discover\", \"href\", \"action\"],\n    });\n  }, [routeDiscovery, createFromReadableStream, fetchImplementation]);\n\n  const frameworkContext: FrameworkContextObject = {\n    future: {\n      // These flags have no runtime impact so can always be false.  If we add\n      // flags that drive runtime behavior they'll need to be proxied through.\n      v8_middleware: false,\n      unstable_subResourceIntegrity: false,\n      unstable_trailingSlashAwareDataRequests: true, // always on for RSC\n    },\n    isSpaMode: false,\n    ssr: true,\n    criticalCss: \"\",\n    manifest: {\n      routes: {},\n      version: \"1\",\n      url: \"\",\n      entry: {\n        module: \"\",\n        imports: [],\n      },\n    },\n    routeDiscovery: { mode: \"lazy\", manifestPath: \"/__manifest\" },\n    routeModules,\n  };\n\n  return (\n    <RSCRouterContext.Provider value={true}>\n      <RSCRouterGlobalErrorBoundary location={state.location}>\n        <FrameworkContext.Provider value={frameworkContext}>\n          <RouterProvider\n            router={transitionEnabledRouter}\n            flushSync={ReactDOM.flushSync}\n          />\n        </FrameworkContext.Provider>\n      </RSCRouterGlobalErrorBoundary>\n    </RSCRouterContext.Provider>\n  );\n}\n\ntype DataRouteObjectWithManifestInfo = DataRouteObject & {\n  children?: DataRouteObjectWithManifestInfo[];\n  hasLoader: boolean;\n  hasClientLoader: boolean;\n  hasAction: boolean;\n  hasClientAction: boolean;\n  hasShouldRevalidate: boolean;\n};\n\nfunction createRouteFromServerManifest(\n  match: RSCRouteManifest,\n  payload?: RSCRenderPayload,\n): DataRouteObjectWithManifestInfo {\n  let hasInitialData = payload && match.id in payload.loaderData;\n  let initialData = payload?.loaderData[match.id];\n  let hasInitialError = payload?.errors && match.id in payload.errors;\n  let initialError = payload?.errors?.[match.id];\n  let isHydrationRequest =\n    match.clientLoader?.hydrate === true ||\n    !match.hasLoader ||\n    // If the route has a component but we don't have an element, we need to hit\n    // the server loader flow regardless of whether the client loader calls\n    // `serverLoader` or not, otherwise we'll have nothing to render.\n    (match.hasComponent && !match.element);\n\n  invariant(window.__reactRouterRouteModules);\n  populateRSCRouteModules(window.__reactRouterRouteModules, match);\n\n  let dataRoute: DataRouteObjectWithManifestInfo = {\n    id: match.id,\n    element: match.element,\n    errorElement: match.errorElement,\n    handle: match.handle,\n    hasErrorBoundary: match.hasErrorBoundary,\n    hydrateFallbackElement: match.hydrateFallbackElement,\n    index: match.index,\n    loader: match.clientLoader\n      ? async (args, singleFetch) => {\n          try {\n            let result = await match.clientLoader!({\n              ...args,\n              serverLoader: () => {\n                preventInvalidServerHandlerCall(\n                  \"loader\",\n                  match.id,\n                  match.hasLoader,\n                );\n                // On the first call, resolve with the server result\n                if (isHydrationRequest) {\n                  if (hasInitialData) {\n                    return initialData;\n                  }\n                  if (hasInitialError) {\n                    throw initialError;\n                  }\n                }\n                return callSingleFetch(singleFetch);\n              },\n            });\n            return result;\n          } finally {\n            isHydrationRequest = false;\n          }\n        }\n      : // We always make the call in this RSC world since even if we don't\n        // have a `loader` we may need to get the `element` implementation\n        (_, singleFetch) => callSingleFetch(singleFetch),\n    action: match.clientAction\n      ? (args, singleFetch) =>\n          match.clientAction!({\n            ...args,\n            serverAction: async () => {\n              preventInvalidServerHandlerCall(\n                \"action\",\n                match.id,\n                match.hasLoader,\n              );\n              return await callSingleFetch(singleFetch);\n            },\n          })\n      : match.hasAction\n        ? (_, singleFetch) => callSingleFetch(singleFetch)\n        : () => {\n            throw noActionDefinedError(\"action\", match.id);\n          },\n    path: match.path,\n    shouldRevalidate: match.shouldRevalidate,\n    // We always have a \"loader\" in this RSC world since even if we don't\n    // have a `loader` we may need to get the `element` implementation\n    hasLoader: true,\n    hasClientLoader: match.clientLoader != null,\n    hasAction: match.hasAction,\n    hasClientAction: match.clientAction != null,\n    hasShouldRevalidate: match.shouldRevalidate != null,\n  };\n\n  if (typeof dataRoute.loader === \"function\") {\n    dataRoute.loader.hydrate = shouldHydrateRouteLoader(\n      match.id,\n      match.clientLoader,\n      match.hasLoader,\n      false,\n    );\n  }\n\n  return dataRoute;\n}\n\nfunction callSingleFetch(singleFetch: unknown) {\n  invariant(typeof singleFetch === \"function\", \"Invalid singleFetch parameter\");\n  return singleFetch();\n}\n\nfunction preventInvalidServerHandlerCall(\n  type: \"action\" | \"loader\",\n  routeId: string,\n  hasHandler: boolean,\n) {\n  if (!hasHandler) {\n    let fn = type === \"action\" ? \"serverAction()\" : \"serverLoader()\";\n    let msg =\n      `You are trying to call ${fn} on a route that does not have a server ` +\n      `${type} (routeId: \"${routeId}\")`;\n    console.error(msg);\n    throw new ErrorResponseImpl(400, \"Bad Request\", new Error(msg), true);\n  }\n}\n\n// Currently rendered links that may need prefetching\nconst nextPaths = new Set<string>();\n\n// FIFO queue of previously discovered routes to prevent re-calling on\n// subsequent navigations to the same path\nconst discoveredPathsMaxSize = 1000;\nconst discoveredPaths = new Set<string>();\n\n// 7.5k to come in under the ~8k limit for most browsers\n// https://stackoverflow.com/a/417184\nconst URL_LIMIT = 7680;\n\nfunction getManifestUrl(paths: string[]): URL | null {\n  if (paths.length === 0) {\n    return null;\n  }\n\n  if (paths.length === 1) {\n    return new URL(`${paths[0]}.manifest`, window.location.origin);\n  }\n\n  const globalVar = window as WindowWithRouterGlobals;\n  let basename = (globalVar.__reactRouterDataRouter.basename ?? \"\").replace(\n    /^\\/|\\/$/g,\n    \"\",\n  );\n  let url = new URL(`${basename}/.manifest`, window.location.origin);\n  url.searchParams.set(\"paths\", paths.sort().join(\",\"));\n\n  return url;\n}\n\nasync function fetchAndApplyManifestPatches(\n  paths: string[],\n  createFromReadableStream: BrowserCreateFromReadableStreamFunction,\n  fetchImplementation: (request: Request) => Promise<Response>,\n  signal?: AbortSignal,\n) {\n  let url = getManifestUrl(paths);\n  if (url == null) {\n    return;\n  }\n\n  // If the URL is nearing the ~8k limit on GET requests, skip this optimization\n  // step and just let discovery happen on link click.  We also wipe out the\n  // nextPaths Set here so we can start filling it with fresh links\n  if (url.toString().length > URL_LIMIT) {\n    nextPaths.clear();\n    return;\n  }\n\n  let response = await fetchImplementation(new Request(url, { signal }));\n  if (!response.body || response.status < 200 || response.status >= 300) {\n    throw new Error(\"Unable to fetch new route matches from the server\");\n  }\n\n  let payload = (await createFromReadableStream(response.body, {\n    temporaryReferences: undefined,\n  })) as RSCPayload;\n  if (payload.type !== \"manifest\") {\n    throw new Error(\"Failed to patch routes\");\n  }\n\n  // Track discovered paths so we don't have to fetch them again\n  paths.forEach((p) => addToFifoQueue(p, discoveredPaths));\n\n  // Without the `allowElementMutations` flag, this will no-op if the route\n  // already exists so we can just call it for all returned patches\n  React.startTransition(() => {\n    payload.patches.forEach((p) => {\n      (window as WindowWithRouterGlobals).__reactRouterDataRouter.patchRoutes(\n        p.parentId ?? null,\n        [createRouteFromServerManifest(p)],\n      );\n    });\n  });\n}\n\nfunction addToFifoQueue(path: string, queue: Set<string>) {\n  if (queue.size >= discoveredPathsMaxSize) {\n    let first = queue.values().next().value;\n    if (typeof first === \"string\") queue.delete(first);\n  }\n  queue.add(path);\n}\n\n// Thanks Josh!\n// https://www.joshwcomeau.com/snippets/javascript/debounce/\nfunction debounce(callback: (...args: unknown[]) => unknown, wait: number) {\n  let timeoutId: number | undefined;\n  return (...args: unknown[]) => {\n    window.clearTimeout(timeoutId);\n    timeoutId = window.setTimeout(() => callback(...args), wait);\n  };\n}\n\nfunction isExternalLocation(location: string) {\n  const newLocation = new URL(location, window.location.href);\n  return newLocation.origin !== window.location.origin;\n}\n\nfunction cloneRoutes(\n  routes: AgnosticDataRouteObject[] | undefined,\n): AgnosticDataRouteObject[] {\n  if (!routes) return undefined as any;\n  return routes.map((route) => ({\n    ...route,\n    children: cloneRoutes(route.children),\n  })) as any;\n}\n\nfunction diffRoutes(\n  a: AgnosticDataRouteObject[],\n  b: AgnosticDataRouteObject[],\n): boolean {\n  if (a.length !== b.length) return true;\n  return a.some((route, index) => {\n    if ((route as any).element !== (b[index] as any).element) return true;\n    if ((route as any).errorElement !== (b[index] as any).errorElement)\n      return true;\n    if (\n      (route as any).hydrateFallbackElement !==\n      (b[index] as any).hydrateFallbackElement\n    )\n      return true;\n    if ((route as any).hasErrorBoundary !== (b[index] as any).hasErrorBoundary)\n      return true;\n    if ((route as any).hasLoader !== (b[index] as any).hasLoader) return true;\n    if ((route as any).hasClientLoader !== (b[index] as any).hasClientLoader)\n      return true;\n    if ((route as any).hasAction !== (b[index] as any).hasAction) return true;\n    if ((route as any).hasClientAction !== (b[index] as any).hasClientAction)\n      return true;\n    return diffRoutes(route.children || [], b[index].children || []);\n  });\n}\n"
  },
  {
    "path": "packages/react-router/lib/rsc/errorBoundaries.tsx",
    "content": "import React from \"react\";\nimport { useRouteError } from \"../hooks\";\nimport type { Location } from \"../router/history\";\nimport { isRouteErrorResponse } from \"../router/utils\";\nimport { ENABLE_DEV_WARNINGS } from \"../context\";\n\ntype RSCRouterGlobalErrorBoundaryProps = React.PropsWithChildren<{\n  location: Location;\n}>;\n\ntype RSCRouterGlobalErrorBoundaryState = {\n  error: null | Error;\n  location: Location;\n};\n\nexport class RSCRouterGlobalErrorBoundary extends React.Component<\n  RSCRouterGlobalErrorBoundaryProps,\n  RSCRouterGlobalErrorBoundaryState\n> {\n  constructor(props: RSCRouterGlobalErrorBoundaryProps) {\n    super(props);\n    this.state = { error: null, location: props.location };\n  }\n\n  static getDerivedStateFromError(error: Error) {\n    return { error };\n  }\n\n  static getDerivedStateFromProps(\n    props: RSCRouterGlobalErrorBoundaryProps,\n    state: RSCRouterGlobalErrorBoundaryState,\n  ) {\n    // When we get into an error state, the user will likely click \"back\" to the\n    // previous page that didn't have an error. Because this wraps the entire\n    // application (even the HTML!) that will have no effect--the error page\n    // continues to display. This gives us a mechanism to recover from the error\n    // when the location changes.\n    //\n    // Whether we're in an error state or not, we update the location in state\n    // so that when we are in an error state, it gets reset when a new location\n    // comes in and the user recovers from the error.\n    if (state.location !== props.location) {\n      return { error: null, location: props.location };\n    }\n\n    // If we're not changing locations, preserve the location but still surface\n    // any new errors that may come through. We retain the existing error, we do\n    // this because the error provided from the app state may be cleared without\n    // the location changing.\n    return { error: state.error, location: state.location };\n  }\n\n  render() {\n    if (this.state.error) {\n      return (\n        <RSCDefaultRootErrorBoundaryImpl\n          error={this.state.error}\n          renderAppShell={true}\n        />\n      );\n    } else {\n      return this.props.children;\n    }\n  }\n}\n\nfunction ErrorWrapper({\n  renderAppShell,\n  title,\n  children,\n}: {\n  renderAppShell: boolean;\n  title: string;\n  children: React.ReactNode;\n}) {\n  if (!renderAppShell) {\n    return children;\n  }\n\n  return (\n    <html lang=\"en\">\n      <head>\n        <meta charSet=\"utf-8\" />\n        <meta\n          name=\"viewport\"\n          content=\"width=device-width,initial-scale=1,viewport-fit=cover\"\n        />\n        <title>{title}</title>\n      </head>\n      <body>\n        <main style={{ fontFamily: \"system-ui, sans-serif\", padding: \"2rem\" }}>\n          {children}\n        </main>\n      </body>\n    </html>\n  );\n}\n\nfunction RSCDefaultRootErrorBoundaryImpl({\n  error,\n  renderAppShell,\n}: {\n  error: unknown;\n  renderAppShell: boolean;\n}) {\n  console.error(error);\n\n  let heyDeveloper = (\n    <script\n      dangerouslySetInnerHTML={{\n        __html: `\n        console.log(\n          \"💿 Hey developer 👋. You can provide a way better UX than this when your app throws errors. Check out https://reactrouter.com/how-to/error-boundary for more information.\"\n        );\n      `,\n      }}\n    />\n  );\n\n  if (isRouteErrorResponse(error)) {\n    return (\n      <ErrorWrapper\n        renderAppShell={renderAppShell}\n        title=\"Unhandled Thrown Response!\"\n      >\n        <h1 style={{ fontSize: \"24px\" }}>\n          {error.status} {error.statusText}\n        </h1>\n        {ENABLE_DEV_WARNINGS ? heyDeveloper : null}\n      </ErrorWrapper>\n    );\n  }\n\n  let errorInstance: Error;\n  if (error instanceof Error) {\n    errorInstance = error;\n  } else {\n    let errorString =\n      error == null\n        ? \"Unknown Error\"\n        : typeof error === \"object\" && \"toString\" in error\n          ? error.toString()\n          : JSON.stringify(error);\n    errorInstance = new Error(errorString);\n  }\n\n  return (\n    <ErrorWrapper renderAppShell={renderAppShell} title=\"Application Error!\">\n      <h1 style={{ fontSize: \"24px\" }}>Application Error</h1>\n      <pre\n        style={{\n          padding: \"2rem\",\n          background: \"hsla(10, 50%, 50%, 0.1)\",\n          color: \"red\",\n          overflow: \"auto\",\n        }}\n      >\n        {errorInstance.stack}\n      </pre>\n      {heyDeveloper}\n    </ErrorWrapper>\n  );\n}\n\nexport function RSCDefaultRootErrorBoundary({\n  hasRootLayout,\n}: {\n  hasRootLayout: boolean;\n}) {\n  let error = useRouteError();\n\n  if (hasRootLayout === undefined) {\n    throw new Error(\"Missing 'hasRootLayout' prop\");\n  }\n  return (\n    <RSCDefaultRootErrorBoundaryImpl\n      renderAppShell={!hasRootLayout}\n      error={error}\n    />\n  );\n}\n"
  },
  {
    "path": "packages/react-router/lib/rsc/html-stream/browser.ts",
    "content": "// https://github.com/devongovett/rsc-html-stream/blob/main/client.js\n\ndeclare global {\n  interface Window {\n    __FLIGHT_DATA: any[];\n  }\n}\n\n/**\n * Get the prerendered [RSC](https://react.dev/reference/rsc/server-components)\n * stream for hydration. Usually passed directly to your\n * `react-server-dom-xyz/client`'s `createFromReadableStream`.\n *\n * @example\n * import { startTransition, StrictMode } from \"react\";\n * import { hydrateRoot } from \"react-dom/client\";\n * import {\n *   unstable_getRSCStream as getRSCStream,\n *   unstable_RSCHydratedRouter as RSCHydratedRouter,\n * } from \"react-router\";\n * import type { unstable_RSCPayload as RSCPayload } from \"react-router\";\n *\n * createFromReadableStream(getRSCStream()).then(\n *   (payload: RSCServerPayload) => {\n *     startTransition(async () => {\n *       hydrateRoot(\n *         document,\n *         <StrictMode>\n *           <RSCHydratedRouter {...props} />\n *         </StrictMode>,\n *         {\n *           // Options\n *         }\n *       );\n *     });\n *   }\n * );\n *\n * @name unstable_getRSCStream\n * @public\n * @category RSC\n * @mode data\n * @returns A [`ReadableStream`](https://developer.mozilla.org/en-US/docs/Web/API/ReadableStream)\n * that contains the [RSC](https://react.dev/reference/rsc/server-components)\n * data for hydration.\n */\nexport function getRSCStream(): ReadableStream {\n  let encoder = new TextEncoder();\n  let streamController: ReadableStreamDefaultController<Uint8Array> | null =\n    null;\n  let rscStream = new ReadableStream({\n    start(controller) {\n      if (typeof window === \"undefined\") {\n        return;\n      }\n      let handleChunk = (chunk: string) => {\n        if (typeof chunk === \"string\") {\n          controller.enqueue(encoder.encode(chunk));\n        } else {\n          controller.enqueue(chunk);\n        }\n      };\n      window.__FLIGHT_DATA ||= [];\n      window.__FLIGHT_DATA.forEach(handleChunk);\n      window.__FLIGHT_DATA.push = (chunk) => {\n        handleChunk(chunk);\n        return 0;\n      };\n      streamController = controller;\n    },\n  });\n\n  if (typeof document !== \"undefined\" && document.readyState === \"loading\") {\n    document.addEventListener(\"DOMContentLoaded\", () => {\n      streamController?.close();\n    });\n  } else {\n    (streamController as any)?.close();\n  }\n\n  return rscStream;\n}\n"
  },
  {
    "path": "packages/react-router/lib/rsc/html-stream/server.ts",
    "content": "// https://github.com/devongovett/rsc-html-stream/blob/main/server.js\n\nconst encoder = new TextEncoder();\nconst trailer = \"</body></html>\";\n\nexport function injectRSCPayload(rscStream: ReadableStream<Uint8Array>) {\n  let decoder = new TextDecoder();\n  let resolveFlightDataPromise: (value: void) => void;\n  let flightDataPromise = new Promise(\n    (resolve) => (resolveFlightDataPromise = resolve),\n  );\n  let startedRSC = false;\n\n  // Buffer all HTML chunks enqueued during the current tick of the event loop (roughly)\n  // and write them to the output stream all at once. This ensures that we don't generate\n  // invalid HTML by injecting RSC in between two partial chunks of HTML.\n  let buffered: Uint8Array[] = [];\n  let timeout: ReturnType<typeof setTimeout> | null = null;\n  function flushBufferedChunks(\n    controller: TransformStreamDefaultController<Uint8Array>,\n  ) {\n    for (let chunk of buffered) {\n      let buf = decoder.decode(chunk, { stream: true });\n      if (buf.endsWith(trailer)) {\n        buf = buf.slice(0, -trailer.length);\n      }\n      controller.enqueue(encoder.encode(buf));\n    }\n\n    buffered.length = 0;\n    timeout = null;\n  }\n\n  return new TransformStream({\n    transform(chunk, controller) {\n      buffered.push(chunk);\n      if (timeout) {\n        return;\n      }\n\n      timeout = setTimeout(async () => {\n        flushBufferedChunks(controller);\n        if (!startedRSC) {\n          startedRSC = true;\n          writeRSCStream(rscStream, controller)\n            .catch((err) => controller.error(err))\n            .then(resolveFlightDataPromise);\n        }\n      }, 0);\n    },\n    async flush(controller) {\n      await flightDataPromise;\n      if (timeout) {\n        clearTimeout(timeout);\n        flushBufferedChunks(controller);\n      }\n      controller.enqueue(encoder.encode(\"</body></html>\"));\n    },\n  });\n}\n\nasync function writeRSCStream(\n  rscStream: ReadableStream<Uint8Array>,\n  controller: TransformStreamDefaultController<Uint8Array>,\n) {\n  let decoder = new TextDecoder(\"utf-8\", { fatal: true });\n  const reader = rscStream.getReader();\n  try {\n    let read: ReadableStreamReadResult<Uint8Array>;\n    while ((read = await reader.read()) && !read.done) {\n      const chunk = read.value;\n      // Try decoding the chunk to send as a string.\n      // If that fails (e.g. binary data that is invalid unicode), write as base64.\n      try {\n        writeChunk(\n          JSON.stringify(decoder.decode(chunk, { stream: true })),\n          controller,\n        );\n      } catch (err) {\n        let base64 = JSON.stringify(btoa(String.fromCodePoint(...chunk)));\n        writeChunk(\n          `Uint8Array.from(atob(${base64}), m => m.codePointAt(0))`,\n          controller,\n        );\n      }\n    }\n  } finally {\n    reader.releaseLock();\n  }\n\n  let remaining = decoder.decode();\n  if (remaining.length) {\n    writeChunk(JSON.stringify(remaining), controller);\n  }\n}\n\nfunction writeChunk(\n  chunk: string,\n  controller: TransformStreamDefaultController<Uint8Array>,\n) {\n  controller.enqueue(\n    encoder.encode(\n      `<script>${escapeScript(\n        `(self.__FLIGHT_DATA||=[]).push(${chunk})`,\n      )}</script>`,\n    ),\n  );\n}\n\n// Escape closing script tags and HTML comments in JS content.\n// https://www.w3.org/TR/html52/semantics-scripting.html#restrictions-for-contents-of-script-elements\n// Avoid replacing </script with <\\/script as it would break the following valid JS: 0</script/ (i.e. regexp literal).\n// Instead, escape the s character.\nfunction escapeScript(script: string) {\n  return script.replace(/<!--/g, \"<\\\\!--\").replace(/<\\/(script)/gi, \"</\\\\$1\");\n}\n"
  },
  {
    "path": "packages/react-router/lib/rsc/route-modules.ts",
    "content": "import type { RouteModules } from \"../dom/ssr/routeModules\";\nimport type { RSCRenderPayload, RSCRouteManifest } from \"./server.rsc\";\n\nexport function createRSCRouteModules(payload: RSCRenderPayload): RouteModules {\n  const routeModules: RouteModules = {};\n  for (const match of payload.matches) {\n    populateRSCRouteModules(routeModules, match);\n  }\n  return routeModules;\n}\n\nexport function populateRSCRouteModules(\n  routeModules: RouteModules,\n  matches: RSCRouteManifest | RSCRouteManifest[],\n) {\n  matches = Array.isArray(matches) ? matches : [matches];\n  for (const match of matches) {\n    routeModules[match.id] = {\n      links: match.links,\n      meta: match.meta,\n      default: noopComponent,\n    };\n  }\n}\n\nconst noopComponent = () => null;\n"
  },
  {
    "path": "packages/react-router/lib/rsc/server.rsc.ts",
    "content": "// eslint-disable-next-line import/no-nodejs-modules\nimport { AsyncLocalStorage } from \"node:async_hooks\";\nimport * as React from \"react\";\n\nimport type {\n  ClientActionFunction,\n  ClientLoaderFunction,\n  HeadersFunction,\n  LinksFunction,\n  MetaFunction,\n} from \"../dom/ssr/routeModules\";\nimport { type Location } from \"../router/history\";\nimport {\n  createStaticHandler,\n  isDataWithResponseInit,\n  isMutationMethod,\n  isResponse,\n  isRedirectResponse,\n  type StaticHandlerContext,\n} from \"../router/router\";\nimport {\n  type ActionFunction,\n  type AgnosticDataRouteMatch,\n  type LoaderFunction,\n  type Params,\n  type ShouldRevalidateFunction,\n  type RouterContextProvider,\n  type TrackedPromise,\n  isAbsoluteUrl,\n  isRouteErrorResponse,\n  matchRoutes,\n  prependBasename,\n  convertRouteMatchToUiMatch,\n  redirect as baseRedirect,\n  redirectDocument as baseRedirectDocument,\n  replace as baseReplace,\n  stripBasename,\n} from \"../router/utils\";\nimport { getDocumentHeadersImpl } from \"../server-runtime/headers\";\nimport { SINGLE_FETCH_REDIRECT_STATUS } from \"../dom/ssr/single-fetch\";\nimport { throwIfPotentialCSRFAttack } from \"../actions\";\nimport type { RouteMatch, RouteObject } from \"../context\";\nimport invariant from \"../server-runtime/invariant\";\n\nimport {\n  Outlet as UNTYPED_Outlet,\n  UNSAFE_AwaitContextProvider,\n  UNSAFE_WithComponentProps,\n  UNSAFE_WithHydrateFallbackProps,\n  UNSAFE_WithErrorBoundaryProps,\n  // @ts-ignore There are no types before the tsup build when used internally, so\n  // we need to cast. If we add an alias for 'internal/react-server-client' to our\n  // TSConfig, it breaks the Parcel build.\n} from \"react-router/internal/react-server-client\";\nimport type {\n  Await as AwaitType,\n  Outlet as OutletType,\n  WithComponentProps as WithComponentPropsType,\n  WithErrorBoundaryProps as WithErrorBoundaryPropsType,\n  WithHydrateFallbackProps as WithHydrateFallbackPropsType,\n  RouteComponentProps,\n  ErrorBoundaryProps,\n  HydrateFallbackProps,\n} from \"../components\";\n\nimport {\n  createRedirectErrorDigest,\n  createRouteErrorResponseDigest,\n} from \"../errors\";\n\nconst Outlet: typeof OutletType = UNTYPED_Outlet;\nconst WithComponentProps: typeof WithComponentPropsType =\n  UNSAFE_WithComponentProps;\nconst WithErrorBoundaryProps: typeof WithErrorBoundaryPropsType =\n  UNSAFE_WithErrorBoundaryProps;\nconst WithHydrateFallbackProps: typeof WithHydrateFallbackPropsType =\n  UNSAFE_WithHydrateFallbackProps;\n\ntype ServerContext = {\n  redirect?: Response;\n  request: Request;\n  runningAction: boolean;\n};\n\nconst globalVar = (typeof globalThis !== \"undefined\" ? globalThis : global) as {\n  ___reactRouterServerStorage___?: AsyncLocalStorage<ServerContext>;\n};\n\nconst ServerStorage = (globalVar.___reactRouterServerStorage___ ??=\n  new AsyncLocalStorage<ServerContext>());\n\nexport function getRequest() {\n  const ctx = ServerStorage.getStore();\n\n  if (!ctx)\n    throw new Error(\n      \"getRequest must be called from within a React Server render context\",\n    );\n  return ctx.request;\n}\n\nexport const redirect: typeof baseRedirect = (...args) => {\n  const response = baseRedirect(...args);\n\n  const ctx = ServerStorage.getStore();\n  if (ctx && ctx.runningAction) {\n    ctx.redirect = response;\n  }\n\n  return response;\n};\n\nexport const redirectDocument: typeof baseRedirectDocument = (...args) => {\n  const response = baseRedirectDocument(...args);\n\n  const ctx = ServerStorage.getStore();\n  if (ctx && ctx.runningAction) {\n    ctx.redirect = response;\n  }\n\n  return response;\n};\n\nexport const replace: typeof baseReplace = (...args) => {\n  const response = baseReplace(...args);\n\n  const ctx = ServerStorage.getStore();\n  if (ctx && ctx.runningAction) {\n    ctx.redirect = response;\n  }\n\n  return response;\n};\n\nconst cachedResolvePromise: <T>(\n  resolve: T,\n) => Promise<PromiseSettledResult<Awaited<T>>> =\n  // @ts-expect-error - on 18 types, requires 19.\n  React.cache(async <T>(resolve: T) => {\n    return Promise.allSettled([resolve]).then((r) => r[0]);\n  });\n\nexport const Await: typeof AwaitType = (async ({\n  children,\n  resolve,\n  errorElement,\n}: React.ComponentProps<typeof AwaitType>) => {\n  let promise = cachedResolvePromise(resolve);\n  let resolved: Awaited<typeof promise> = await promise;\n\n  if (resolved.status === \"rejected\" && !errorElement) {\n    throw resolved.reason;\n  }\n  if (resolved.status === \"rejected\") {\n    return React.createElement(UNSAFE_AwaitContextProvider, {\n      children: React.createElement(React.Fragment, null, errorElement),\n      value: { _tracked: true, _error: resolved.reason } as TrackedPromise,\n    });\n  }\n\n  const toRender =\n    typeof children === \"function\" ? children(resolved.value) : children;\n\n  return React.createElement(UNSAFE_AwaitContextProvider, {\n    children: toRender,\n    value: { _tracked: true, _data: resolved.value } as TrackedPromise,\n  });\n}) as any;\n\ntype RSCRouteConfigEntryBase = {\n  action?: ActionFunction;\n  clientAction?: ClientActionFunction;\n  clientLoader?: ClientLoaderFunction;\n  ErrorBoundary?: React.ComponentType<any>;\n  handle?: any;\n  headers?: HeadersFunction;\n  HydrateFallback?: React.ComponentType<any>;\n  Layout?: React.ComponentType<any>;\n  links?: LinksFunction;\n  loader?: LoaderFunction;\n  meta?: MetaFunction;\n  shouldRevalidate?: ShouldRevalidateFunction;\n};\n\nexport type RSCRouteConfigEntry = RSCRouteConfigEntryBase & {\n  id: string;\n  path?: string;\n  Component?: React.ComponentType<any>;\n  lazy?: () => Promise<\n    RSCRouteConfigEntryBase &\n      (\n        | {\n            default?: React.ComponentType<any>;\n            Component?: never;\n          }\n        | {\n            default?: never;\n            Component?: React.ComponentType<any>;\n          }\n      )\n  >;\n} & (\n    | {\n        index: true;\n      }\n    | {\n        children?: RSCRouteConfigEntry[];\n      }\n  );\n\nexport type RSCRouteConfig = Array<RSCRouteConfigEntry>;\n\nexport type RSCRouteManifest = {\n  clientAction?: ClientActionFunction;\n  clientLoader?: ClientLoaderFunction;\n  element?: React.ReactElement | false;\n  errorElement?: React.ReactElement;\n  handle?: any;\n  hasAction: boolean;\n  hasComponent: boolean;\n  hasErrorBoundary: boolean;\n  hasLoader: boolean;\n  hydrateFallbackElement?: React.ReactElement;\n  id: string;\n  index?: boolean;\n  links?: LinksFunction;\n  meta?: MetaFunction;\n  parentId?: string;\n  path?: string;\n  shouldRevalidate?: ShouldRevalidateFunction;\n};\n\nexport type RSCRouteMatch = RSCRouteManifest & {\n  params: Params;\n  pathname: string;\n  pathnameBase: string;\n};\n\nexport type RSCRenderPayload = {\n  type: \"render\";\n  actionData: Record<string, any> | null;\n  basename: string | undefined;\n  errors: Record<string, any> | null;\n  loaderData: Record<string, any>;\n  location: Location;\n  matches: RSCRouteMatch[];\n  // Additional routes we should patch into the router for subsequent navigations.\n  // Mostly a collection of pathless/index routes that may be needed for complete\n  // matching on upward navigations.  Only needed on the initial document request,\n  // for SPA navigations the manifest call will handle these patches.\n  patches?: RSCRouteManifest[];\n  nonce?: string;\n  formState?: unknown;\n};\n\nexport type RSCManifestPayload = {\n  type: \"manifest\";\n  // Routes we should patch into the router for subsequent navigations.\n  patches: RSCRouteManifest[];\n};\n\nexport type RSCActionPayload = {\n  type: \"action\";\n  actionResult: Promise<unknown>;\n  rerender?: Promise<RSCRenderPayload | RSCRedirectPayload>;\n};\n\nexport type RSCRedirectPayload = {\n  type: \"redirect\";\n  status: number;\n  location: string;\n  replace: boolean;\n  reload: boolean;\n  actionResult?: Promise<unknown>;\n};\n\nexport type RSCPayload =\n  | RSCRenderPayload\n  | RSCManifestPayload\n  | RSCActionPayload\n  | RSCRedirectPayload;\n\nexport type RSCMatch = {\n  statusCode: number;\n  headers: Headers;\n  payload: RSCPayload;\n};\n\nexport type DecodeActionFunction = (\n  formData: FormData,\n) => Promise<() => Promise<unknown>>;\n\nexport type DecodeFormStateFunction = (\n  result: unknown,\n  formData: FormData,\n) => unknown;\n\nexport type DecodeReplyFunction = (\n  reply: FormData | string,\n  options: { temporaryReferences: unknown },\n) => Promise<unknown[]>;\n\nexport type LoadServerActionFunction = (id: string) => Promise<Function>;\n\n/**\n * Matches the given routes to a [`Request`](https://developer.mozilla.org/en-US/docs/Web/API/Request)\n * and returns an [RSC](https://react.dev/reference/rsc/server-components)\n * [`Response`](https://developer.mozilla.org/en-US/docs/Web/API/Response)\n * encoding an {@link unstable_RSCPayload} for consumption by an [RSC](https://react.dev/reference/rsc/server-components)\n * enabled client router.\n *\n * @example\n * import {\n *   createTemporaryReferenceSet,\n *   decodeAction,\n *   decodeReply,\n *   loadServerAction,\n *   renderToReadableStream,\n * } from \"@vitejs/plugin-rsc/rsc\";\n * import { unstable_matchRSCServerRequest as matchRSCServerRequest } from \"react-router\";\n *\n * matchRSCServerRequest({\n *   createTemporaryReferenceSet,\n *   decodeAction,\n *   decodeFormState,\n *   decodeReply,\n *   loadServerAction,\n *   request,\n *   routes: routes(),\n *   generateResponse(match) {\n *     return new Response(\n *       renderToReadableStream(match.payload),\n *       {\n *         status: match.statusCode,\n *         headers: match.headers,\n *       }\n *     );\n *   },\n * });\n *\n * @name unstable_matchRSCServerRequest\n * @public\n * @category RSC\n * @mode data\n * @param opts Options\n * @param opts.allowedActionOrigins Origin patterns that are allowed to execute actions.\n * @param opts.basename The basename to use when matching the request.\n * @param opts.createTemporaryReferenceSet A function that returns a temporary\n * reference set for the request, used to track temporary references in the [RSC](https://react.dev/reference/rsc/server-components)\n * stream.\n * @param opts.decodeAction Your `react-server-dom-xyz/server`'s `decodeAction`\n * function, responsible for loading a server action.\n * @param opts.decodeFormState A function responsible for decoding form state for\n * progressively enhanceable forms with React's [`useActionState`](https://react.dev/reference/react/useActionState)\n * using your `react-server-dom-xyz/server`'s `decodeFormState`.\n * @param opts.decodeReply Your `react-server-dom-xyz/server`'s `decodeReply`\n * function, used to decode the server function's arguments and bind them to the\n * implementation for invocation by the router.\n * @param opts.generateResponse A function responsible for using your\n * `renderToReadableStream` to generate a [`Response`](https://developer.mozilla.org/en-US/docs/Web/API/Response)\n * encoding the {@link unstable_RSCPayload}.\n * @param opts.loadServerAction Your `react-server-dom-xyz/server`'s\n * `loadServerAction` function, used to load a server action by ID.\n * @param opts.onError An optional error handler that will be called with any\n * errors that occur during the request processing.\n * @param opts.request The [`Request`](https://developer.mozilla.org/en-US/docs/Web/API/Request)\n * to match against.\n * @param opts.requestContext An instance of {@link RouterContextProvider}\n * that should be created per request, to be passed to [`action`](../../start/data/route-object#action)s,\n * [`loader`](../../start/data/route-object#loader)s and [middleware](../../how-to/middleware).\n * @param opts.routes Your {@link unstable_RSCRouteConfigEntry | route definitions}.\n * @returns A [`Response`](https://developer.mozilla.org/en-US/docs/Web/API/Response)\n * that contains the [RSC](https://react.dev/reference/rsc/server-components)\n * data for hydration.\n */\nexport async function matchRSCServerRequest({\n  allowedActionOrigins,\n  createTemporaryReferenceSet,\n  basename,\n  decodeReply,\n  requestContext,\n  loadServerAction,\n  decodeAction,\n  decodeFormState,\n  onError,\n  request,\n  routes,\n  generateResponse,\n}: {\n  allowedActionOrigins?: string[];\n  createTemporaryReferenceSet: () => unknown;\n  basename?: string;\n  decodeReply?: DecodeReplyFunction;\n  decodeAction?: DecodeActionFunction;\n  decodeFormState?: DecodeFormStateFunction;\n  requestContext?: RouterContextProvider;\n  loadServerAction?: LoadServerActionFunction;\n  onError?: (error: unknown) => void;\n  request: Request;\n  routes: RSCRouteConfigEntry[];\n  generateResponse: (\n    match: RSCMatch,\n    {\n      onError,\n      temporaryReferences,\n    }: {\n      onError(error: unknown): string | undefined;\n      temporaryReferences: unknown;\n    },\n  ) => Response;\n}): Promise<Response> {\n  let url = new URL(request.url);\n\n  basename = basename || \"/\";\n  let normalizedPath = url.pathname;\n  if (url.pathname.endsWith(\"/_.rsc\")) {\n    normalizedPath = url.pathname.replace(/_\\.rsc$/, \"\");\n  } else if (url.pathname.endsWith(\".rsc\")) {\n    normalizedPath = url.pathname.replace(/\\.rsc$/, \"\");\n  }\n\n  if (\n    stripBasename(normalizedPath, basename) !== \"/\" &&\n    normalizedPath.endsWith(\"/\")\n  ) {\n    normalizedPath = normalizedPath.slice(0, -1);\n  }\n  url.pathname = normalizedPath;\n  basename =\n    basename.length > normalizedPath.length ? normalizedPath : basename;\n\n  let routerRequest = new Request(url.toString(), {\n    method: request.method,\n    headers: request.headers,\n    body: request.body,\n    signal: request.signal,\n    duplex: request.body ? \"half\" : undefined,\n  } as RequestInit);\n\n  const temporaryReferences = createTemporaryReferenceSet();\n\n  const requestUrl = new URL(request.url);\n  if (isManifestRequest(requestUrl)) {\n    let response = await generateManifestResponse(\n      routes,\n      basename,\n      request,\n      generateResponse,\n      temporaryReferences,\n    );\n    return response;\n  }\n\n  let isDataRequest = isReactServerRequest(requestUrl);\n\n  // Explode lazy functions out the routes so we can use middleware\n  // TODO: This isn't ideal but we can't do it through `lazy()` in the router,\n  // and if we move to `lazy: {}` then we lose all the other things from the\n  // `RSCRouteConfigEntry` like `Layout` etc.\n  let matches = matchRoutes(routes, url.pathname, basename);\n  if (matches) {\n    await Promise.all(matches.map((m) => explodeLazyRoute(m.route)));\n  }\n\n  const leafMatch = matches?.[matches.length - 1];\n  if (\n    !isDataRequest &&\n    leafMatch &&\n    !leafMatch.route.Component &&\n    !leafMatch.route.ErrorBoundary\n  ) {\n    return generateResourceResponse(\n      routerRequest,\n      routes,\n      basename,\n      leafMatch.route.id,\n      requestContext,\n      onError,\n    );\n  }\n\n  let response = await generateRenderResponse(\n    routerRequest,\n    routes,\n    basename,\n    isDataRequest,\n    decodeReply,\n    requestContext,\n    loadServerAction,\n    decodeAction,\n    decodeFormState,\n    onError,\n    generateResponse,\n    temporaryReferences,\n    allowedActionOrigins,\n  );\n  // The front end uses this to know whether a 4xx/5xx status came from app code\n  // or never reached the origin server\n  response.headers.set(\"X-Remix-Response\", \"yes\");\n  return response;\n}\n\nasync function generateManifestResponse(\n  routes: RSCRouteConfigEntry[],\n  basename: string | undefined,\n  request: Request,\n  generateResponse: (\n    match: RSCMatch,\n    options: {\n      onError(error: unknown): string | undefined;\n      temporaryReferences: unknown;\n    },\n  ) => Response,\n  temporaryReferences: unknown,\n) {\n  let url = new URL(request.url);\n  let pathParam = url.searchParams.get(\"paths\");\n  let pathnames = pathParam\n    ? pathParam.split(\",\").filter(Boolean)\n    : [url.pathname.replace(/\\.manifest$/, \"\")];\n  let routeIds = new Set<string>();\n  let matchedRoutes = pathnames\n    .flatMap((pathname) => {\n      let pathnameMatches = matchRoutes(routes, pathname, basename);\n      return (\n        pathnameMatches?.map((m, i) => ({\n          ...m.route,\n          parentId: pathnameMatches[i - 1]?.route.id,\n        })) ?? []\n      );\n    })\n    .filter((route) => {\n      if (!routeIds.has(route.id)) {\n        routeIds.add(route.id);\n        return true;\n      }\n      return false;\n    });\n  let payload: RSCManifestPayload = {\n    type: \"manifest\",\n    patches: (\n      await Promise.all([\n        ...matchedRoutes.map((route) => getManifestRoute(route)),\n        getAdditionalRoutePatches(\n          pathnames,\n          routes,\n          basename,\n          Array.from(routeIds),\n        ),\n      ])\n    ).flat(1),\n  };\n\n  return generateResponse(\n    {\n      statusCode: 200,\n      headers: new Headers({\n        \"Content-Type\": \"text/x-component\",\n        Vary: \"Content-Type\",\n      }),\n      payload,\n    },\n    { temporaryReferences, onError: defaultOnError },\n  );\n}\n\nfunction prependBasenameToRedirectResponse(\n  response: Response,\n  basename: string | undefined = \"/\",\n): Response {\n  if (basename === \"/\") {\n    return response;\n  }\n\n  let redirect = response.headers.get(\"Location\");\n  if (!redirect || isAbsoluteUrl(redirect)) {\n    return response;\n  }\n\n  response.headers.set(\n    \"Location\",\n    prependBasename({ basename, pathname: redirect }),\n  );\n  return response;\n}\n\nasync function processServerAction(\n  request: Request,\n  basename: string | undefined,\n  decodeReply: DecodeReplyFunction | undefined,\n  loadServerAction: LoadServerActionFunction | undefined,\n  decodeAction: DecodeActionFunction | undefined,\n  decodeFormState: DecodeFormStateFunction | undefined,\n  onError: ((error: unknown) => void) | undefined,\n  temporaryReferences: unknown,\n): Promise<\n  | {\n      skipRevalidation: boolean;\n      revalidationRequest: Request;\n      actionResult?: Promise<unknown>;\n      formState?: unknown;\n    }\n  | Response\n  | undefined\n> {\n  const getRevalidationRequest = () =>\n    new Request(request.url, {\n      method: \"GET\",\n      headers: request.headers,\n      signal: request.signal,\n    });\n\n  const isFormRequest = canDecodeWithFormData(\n    request.headers.get(\"Content-Type\"),\n  );\n  const actionId = request.headers.get(\"rsc-action-id\");\n  if (actionId) {\n    if (!decodeReply || !loadServerAction) {\n      throw new Error(\n        \"Cannot handle enhanced server action without decodeReply and loadServerAction functions\",\n      );\n    }\n\n    const reply = isFormRequest\n      ? await request.formData()\n      : await request.text();\n\n    const actionArgs = await decodeReply(reply, { temporaryReferences });\n    const action = await loadServerAction(actionId);\n    const serverAction = action.bind(null, ...actionArgs);\n\n    let actionResult = Promise.resolve(serverAction());\n    try {\n      // Wait for actions to finish regardless of state\n      await actionResult;\n    } catch (error) {\n      if (isResponse(error)) {\n        return error;\n      }\n      // The error is propagated to the client through the result promise in the stream\n      onError?.(error);\n    }\n\n    let maybeFormData = actionArgs.length === 1 ? actionArgs[0] : actionArgs[1];\n    let formData =\n      maybeFormData &&\n      typeof maybeFormData === \"object\" &&\n      maybeFormData instanceof FormData\n        ? maybeFormData\n        : null;\n\n    let skipRevalidation = formData?.has(\"$SKIP_REVALIDATION\") ?? false;\n\n    return {\n      actionResult,\n      revalidationRequest: getRevalidationRequest(),\n      skipRevalidation,\n    };\n  } else if (isFormRequest) {\n    const formData = await request.clone().formData();\n    if (Array.from(formData.keys()).some((k) => k.startsWith(\"$ACTION_\"))) {\n      if (!decodeAction) {\n        throw new Error(\n          \"Cannot handle form actions without a decodeAction function\",\n        );\n      }\n      const action = await decodeAction(formData);\n      let formState = undefined;\n      try {\n        let result = await action();\n        if (isRedirectResponse(result)) {\n          result = prependBasenameToRedirectResponse(result, basename);\n        }\n        formState = decodeFormState?.(result, formData);\n      } catch (error) {\n        if (isRedirectResponse(error)) {\n          return prependBasenameToRedirectResponse(error, basename);\n        }\n        if (isResponse(error)) {\n          return error;\n        }\n        onError?.(error);\n      }\n      return {\n        formState,\n        revalidationRequest: getRevalidationRequest(),\n        skipRevalidation: false,\n      };\n    }\n  }\n}\n\nasync function generateResourceResponse(\n  request: Request,\n  routes: RSCRouteConfigEntry[],\n  basename: string | undefined,\n  routeId: string,\n  requestContext: RouterContextProvider | undefined,\n  onError: ((error: unknown) => void) | undefined,\n) {\n  try {\n    const staticHandler = createStaticHandler(routes, {\n      basename,\n    });\n\n    let response = await staticHandler.queryRoute(request, {\n      routeId,\n      requestContext,\n      async generateMiddlewareResponse(queryRoute) {\n        try {\n          let response = await queryRoute(request);\n          return generateResourceResponse(response);\n        } catch (error) {\n          return generateErrorResponse(error);\n        }\n      },\n    });\n    return response;\n  } catch (error) {\n    return generateErrorResponse(error);\n  }\n\n  function generateErrorResponse(error: unknown) {\n    let response: Response;\n    if (isResponse(error)) {\n      response = error;\n    } else if (isRouteErrorResponse(error)) {\n      onError?.(error);\n      const errorMessage =\n        typeof error.data === \"string\" ? error.data : error.statusText;\n      response = new Response(errorMessage, {\n        status: error.status,\n        statusText: error.statusText,\n      });\n    } else {\n      onError?.(error);\n      response = new Response(\"Internal Server Error\", { status: 500 });\n    }\n\n    return generateResourceResponse(response);\n  }\n\n  function generateResourceResponse(response: Response) {\n    const headers = new Headers(response.headers);\n    headers.set(\"React-Router-Resource\", \"true\");\n    return new Response(response.body, {\n      status: response.status,\n      statusText: response.statusText,\n      headers,\n    });\n  }\n}\n\nasync function generateRenderResponse(\n  request: Request,\n  routes: RSCRouteConfigEntry[],\n  basename: string | undefined,\n  isDataRequest: boolean,\n  decodeReply: DecodeReplyFunction | undefined,\n  requestContext: RouterContextProvider | undefined,\n  loadServerAction: LoadServerActionFunction | undefined,\n  decodeAction: DecodeActionFunction | undefined,\n  decodeFormState: DecodeFormStateFunction | undefined,\n  onError: ((error: unknown) => void) | undefined,\n  generateResponse: (\n    match: RSCMatch,\n    options: {\n      onError(error: unknown): string | undefined;\n      temporaryReferences: unknown;\n    },\n  ) => Response,\n  temporaryReferences: unknown,\n  allowedActionOrigins: string[] | undefined,\n): Promise<Response> {\n  // If this is a RR submission, we just want the `actionData` but don't want\n  // to call any loaders or render any components back in the response - that\n  // will happen in the subsequent revalidation request\n  let statusCode = 200;\n  let url = new URL(request.url);\n  let isSubmission = isMutationMethod(request.method);\n  let routeIdsToLoad =\n    !isSubmission && url.searchParams.has(\"_routes\")\n      ? url.searchParams.get(\"_routes\")!.split(\",\")\n      : null;\n\n  // Create the handler here with exploded routes\n  const staticHandler = createStaticHandler(routes, {\n    basename,\n    mapRouteProperties: (r) => ({\n      hasErrorBoundary: (r as RouteObject).ErrorBoundary != null,\n    }),\n  });\n\n  let actionResult: Promise<unknown> | undefined;\n  const ctx: ServerContext = {\n    request,\n    runningAction: false,\n  };\n\n  const result = await ServerStorage.run(ctx, () =>\n    staticHandler.query(request, {\n      requestContext,\n      skipLoaderErrorBubbling: isDataRequest,\n      skipRevalidation: isSubmission,\n      ...(routeIdsToLoad\n        ? { filterMatchesToLoad: (m) => routeIdsToLoad!.includes(m.route.id) }\n        : {}),\n      async generateMiddlewareResponse(query) {\n        // If this is an RSC server action, process that and then call query as a\n        // revalidation.  If this is a RR Form/Fetcher submission,\n        // `processServerAction` will fall through as a no-op and we'll pass the\n        // POST `request` to `query` and process our action there.\n        let formState: unknown;\n        let skipRevalidation = false;\n        let potentialCSRFAttackError: unknown | undefined;\n        if (request.method === \"POST\") {\n          try {\n            throwIfPotentialCSRFAttack(request.headers, allowedActionOrigins);\n\n            ctx.runningAction = true;\n            let result = await processServerAction(\n              request,\n              basename,\n              decodeReply,\n              loadServerAction,\n              decodeAction,\n              decodeFormState,\n              onError,\n              temporaryReferences,\n            ).finally(() => {\n              ctx.runningAction = false;\n            });\n\n            if (isResponse(result)) {\n              return generateRedirectResponse(\n                result,\n                actionResult,\n                basename,\n                isDataRequest,\n                generateResponse,\n                temporaryReferences,\n                (ctx.redirect as unknown as Response)?.headers,\n              );\n            }\n\n            skipRevalidation = result?.skipRevalidation ?? false;\n            actionResult = result?.actionResult;\n            formState = result?.formState;\n            request = result?.revalidationRequest ?? request;\n\n            if (ctx.redirect) {\n              return generateRedirectResponse(\n                ctx.redirect,\n                actionResult,\n                basename,\n                isDataRequest,\n                generateResponse,\n                temporaryReferences,\n                undefined,\n              );\n            }\n          } catch (error) {\n            potentialCSRFAttackError = error;\n          }\n        }\n\n        let staticContext = await query(\n          request,\n          skipRevalidation || !!potentialCSRFAttackError\n            ? {\n                filterMatchesToLoad: () => false,\n              }\n            : undefined,\n        );\n\n        if (isResponse(staticContext)) {\n          return generateRedirectResponse(\n            staticContext,\n            actionResult,\n            basename,\n            isDataRequest,\n            generateResponse,\n            temporaryReferences,\n            ctx.redirect?.headers,\n          );\n        }\n\n        if (potentialCSRFAttackError) {\n          staticContext.errors ??= {};\n          staticContext.errors[staticContext.matches[0].route.id] =\n            potentialCSRFAttackError;\n          staticContext.statusCode = 400;\n        }\n\n        return generateStaticContextResponse(\n          routes,\n          basename,\n          generateResponse,\n          statusCode,\n          routeIdsToLoad,\n          isDataRequest,\n          isSubmission,\n          actionResult,\n          formState,\n          staticContext,\n          temporaryReferences,\n          skipRevalidation,\n          ctx.redirect?.headers,\n        );\n      },\n    }),\n  );\n\n  if (isRedirectResponse(result)) {\n    return generateRedirectResponse(\n      result,\n      actionResult,\n      basename,\n      isDataRequest,\n      generateResponse,\n      temporaryReferences,\n      ctx.redirect?.headers,\n    );\n  }\n\n  invariant(isResponse(result), \"Expected a response from query\");\n  return result;\n}\n\nfunction generateRedirectResponse(\n  response: Response,\n  actionResult: Promise<unknown> | undefined,\n  basename: string | undefined,\n  isDataRequest: boolean,\n  generateResponse: (\n    match: RSCMatch,\n    options: {\n      onError(error: unknown): string | undefined;\n      temporaryReferences: unknown;\n    },\n  ) => Response,\n  temporaryReferences: unknown,\n  sideEffectRedirectHeaders: Headers | undefined,\n) {\n  let redirect = response.headers.get(\"Location\")!;\n\n  if (isDataRequest && basename) {\n    redirect = stripBasename(redirect, basename) || redirect;\n  }\n\n  let payload: RSCRedirectPayload = {\n    type: \"redirect\",\n    location: redirect,\n    reload: response.headers.get(\"X-Remix-Reload-Document\") === \"true\",\n    replace: response.headers.get(\"X-Remix-Replace\") === \"true\",\n    status: response.status,\n    actionResult,\n  };\n\n  // Preserve non-internal headers on the user-created redirect and merge in\n  // any side-effect redirect headers\n  let headers = new Headers(sideEffectRedirectHeaders);\n  for (const [key, value] of response.headers.entries()) {\n    headers.append(key, value);\n  }\n  headers.delete(\"Location\");\n  headers.delete(\"X-Remix-Reload-Document\");\n  headers.delete(\"X-Remix-Replace\");\n  // Remove Content-Length because node:http will truncate the response body\n  // to match the Content-Length header, which can result in incomplete data\n  // if the actual encoded body is longer.\n  // https://nodejs.org/api/http.html#class-httpclientrequest\n  headers.delete(\"Content-Length\");\n  headers.set(\"Content-Type\", \"text/x-component\");\n  headers.set(\"Vary\", \"Content-Type\");\n\n  return generateResponse(\n    {\n      statusCode: SINGLE_FETCH_REDIRECT_STATUS,\n      headers,\n      payload,\n    },\n    { temporaryReferences, onError: defaultOnError },\n  );\n}\n\nasync function generateStaticContextResponse(\n  routes: RSCRouteConfigEntry[],\n  basename: string | undefined,\n  generateResponse: (\n    match: RSCMatch,\n    options: {\n      onError(error: unknown): string | undefined;\n      temporaryReferences: unknown;\n    },\n  ) => Response,\n  statusCode: number,\n  routeIdsToLoad: string[] | null,\n  isDataRequest: boolean,\n  isSubmission: boolean,\n  actionResult: Promise<unknown> | undefined,\n  formState: unknown | undefined,\n  staticContext: StaticHandlerContext,\n  temporaryReferences: unknown,\n  skipRevalidation: boolean,\n  sideEffectRedirectHeaders: Headers | undefined,\n): Promise<Response> {\n  statusCode = staticContext.statusCode ?? statusCode;\n\n  if (staticContext.errors) {\n    staticContext.errors = Object.fromEntries(\n      Object.entries(staticContext.errors).map(([key, error]) => [\n        key,\n        isRouteErrorResponse(error)\n          ? Object.fromEntries(Object.entries(error))\n          : error,\n      ]),\n    );\n  }\n\n  // In the RSC world we set `hasLoader:true` eve if a route doesn't have a\n  // loader so that we always make the single fetch call to get the rendered\n  // `element`.  We add a `null` value for any of the routes that don't\n  // actually have a loader so the single fetch logic can find a result for\n  // the route.  This is a bit of a hack but allows us to re-use all the\n  // existing logic.  This can go away if we ever fork off and re-implement a\n  // standalone RSC `dataStrategy`. We also need to ensure that we don't set\n  // `loaderData` to `null` for routes that have an error, otherwise they won't\n  // be forced to revalidate on navigation.\n  staticContext.matches.forEach((m) => {\n    const routeHasNoLoaderData =\n      staticContext.loaderData[m.route.id] === undefined;\n    const routeHasError = Boolean(\n      staticContext.errors && m.route.id in staticContext.errors,\n    );\n    if (routeHasNoLoaderData && !routeHasError) {\n      staticContext.loaderData[m.route.id] = null;\n    }\n  });\n\n  let headers = getDocumentHeadersImpl(\n    staticContext,\n    (match) => (match as RouteMatch<string, RSCRouteConfigEntry>).route.headers,\n    sideEffectRedirectHeaders,\n  );\n\n  // Remove Content-Length because node:http will truncate the response body\n  // to match the Content-Length header, which can result in incomplete data\n  // if the actual encoded body is longer.\n  // https://nodejs.org/api/http.html#class-httpclientrequest\n  headers.delete(\"Content-Length\");\n\n  const baseRenderPayload: Omit<RSCRenderPayload, \"matches\" | \"patches\"> = {\n    type: \"render\",\n    basename: staticContext.basename,\n    actionData: staticContext.actionData,\n    errors: staticContext.errors,\n    loaderData: staticContext.loaderData,\n    location: staticContext.location,\n    formState,\n  };\n\n  const renderPayloadPromise = () =>\n    getRenderPayload(\n      baseRenderPayload,\n      routes,\n      basename,\n      routeIdsToLoad,\n      isDataRequest,\n      staticContext,\n    );\n\n  let payload: RSCRenderPayload | RSCActionPayload;\n\n  if (actionResult) {\n    // Don't await the payload so we can stream down the actionResult immediately\n    payload = {\n      type: \"action\",\n      actionResult,\n      rerender: skipRevalidation ? undefined : renderPayloadPromise(),\n    };\n  } else if (isSubmission && isDataRequest) {\n    // Short circuit without matches on non server-action submissions since\n    // we'll revalidate in a separate request\n    payload = {\n      ...baseRenderPayload,\n      matches: [],\n      patches: [],\n    };\n  } else {\n    // Await the full RSC render on all normal requests\n    payload = await renderPayloadPromise();\n  }\n\n  return generateResponse(\n    {\n      statusCode,\n      headers,\n      payload,\n    },\n    { temporaryReferences, onError: defaultOnError },\n  );\n}\n\nasync function getRenderPayload(\n  baseRenderPayload: Omit<RSCRenderPayload, \"matches\" | \"patches\">,\n  routes: RSCRouteConfigEntry[],\n  basename: string | undefined,\n  routeIdsToLoad: string[] | null,\n  isDataRequest: boolean,\n  staticContext: StaticHandlerContext,\n) {\n  // Figure out how deep we want to render server components based on any\n  // triggered error boundaries and/or `routeIdsToLoad`\n  let deepestRenderedRouteIdx = staticContext.matches.length - 1;\n  // Capture parentIds for assignment on the RSCRouteMatch later\n  let parentIds: Record<string, string | undefined> = {};\n\n  staticContext.matches.forEach((m, i) => {\n    if (i > 0) {\n      parentIds[m.route.id] = staticContext.matches[i - 1].route.id;\n    }\n    if (\n      staticContext.errors &&\n      m.route.id in staticContext.errors &&\n      deepestRenderedRouteIdx > i\n    ) {\n      deepestRenderedRouteIdx = i;\n    }\n  });\n\n  let matchesPromise = Promise.all(\n    staticContext.matches.map((match, i) => {\n      let isBelowErrorBoundary = i > deepestRenderedRouteIdx;\n      let parentId = parentIds[match.route.id];\n      return getRSCRouteMatch({\n        staticContext,\n        match,\n        routeIdsToLoad,\n        isBelowErrorBoundary,\n        parentId,\n      });\n    }),\n  );\n\n  let patchesPromise = getAdditionalRoutePatches(\n    [staticContext.location.pathname],\n    routes,\n    basename,\n    staticContext.matches.map((m) => m.route.id),\n  );\n\n  let [matches, patches] = await Promise.all([matchesPromise, patchesPromise]);\n\n  return {\n    ...baseRenderPayload,\n    matches,\n    patches,\n  };\n}\n\nasync function getRSCRouteMatch({\n  staticContext,\n  match,\n  isBelowErrorBoundary,\n  routeIdsToLoad,\n  parentId,\n}: {\n  staticContext: StaticHandlerContext;\n  match: AgnosticDataRouteMatch;\n  isBelowErrorBoundary: boolean;\n  routeIdsToLoad: string[] | null;\n  parentId: string | undefined;\n}) {\n  // @ts-expect-error - FIXME: Fix the types here\n  await explodeLazyRoute(match.route);\n  const Layout = (match.route as any).Layout || React.Fragment;\n  const Component = (match.route as any).Component;\n  const ErrorBoundary = (match.route as any).ErrorBoundary;\n  const HydrateFallback = (match.route as any).HydrateFallback;\n  const loaderData = staticContext.loaderData[match.route.id];\n  const actionData = staticContext.actionData?.[match.route.id];\n  const params = match.params;\n  // TODO: DRY this up once it's fully fleshed out\n  let element: React.ReactElement | undefined = undefined;\n  let shouldLoadRoute =\n    !routeIdsToLoad || routeIdsToLoad.includes(match.route.id);\n  // Only bother rendering Server Components for routes that we're surfacing,\n  // so nothing at/below an error boundary and prune routes if included in\n  // `routeIdsToLoad`.  This is specifically important when a middleware\n  // or loader throws and we don't have any `loaderData` to pass through as\n  // props leading to render-time errors of the server component\n  if (Component && shouldLoadRoute) {\n    element = !isBelowErrorBoundary\n      ? React.createElement(\n          Layout,\n          null,\n          isClientReference(Component)\n            ? React.createElement(WithComponentProps, {\n                children: React.createElement(Component),\n              })\n            : React.createElement(Component, {\n                loaderData,\n                actionData,\n                params,\n                matches: staticContext.matches.map((match) =>\n                  convertRouteMatchToUiMatch(match, staticContext.loaderData),\n                ),\n              } satisfies RouteComponentProps),\n        )\n      : React.createElement(Outlet);\n  }\n  let error: unknown = undefined;\n\n  if (ErrorBoundary && staticContext.errors) {\n    error = staticContext.errors[match.route.id];\n  }\n  const errorElement = ErrorBoundary\n    ? React.createElement(\n        Layout,\n        null,\n        isClientReference(ErrorBoundary)\n          ? React.createElement(WithErrorBoundaryProps, {\n              children: React.createElement(ErrorBoundary),\n            })\n          : React.createElement(ErrorBoundary, {\n              loaderData,\n              actionData,\n              params,\n              error,\n            } satisfies ErrorBoundaryProps),\n      )\n    : undefined;\n  const hydrateFallbackElement = HydrateFallback\n    ? React.createElement(\n        Layout,\n        null,\n        isClientReference(HydrateFallback)\n          ? React.createElement(WithHydrateFallbackProps, {\n              children: React.createElement(HydrateFallback),\n            })\n          : React.createElement(HydrateFallback, {\n              loaderData,\n              actionData,\n              params,\n            } satisfies HydrateFallbackProps),\n      )\n    : undefined;\n\n  return {\n    clientAction: (match.route as any).clientAction,\n    clientLoader: (match.route as any).clientLoader,\n    element,\n    errorElement,\n    handle: (match.route as any).handle,\n    hasAction: !!match.route.action,\n    hasComponent: !!Component,\n    hasErrorBoundary: !!ErrorBoundary,\n    hasLoader: !!match.route.loader,\n    hydrateFallbackElement,\n    id: match.route.id,\n    index: match.route.index,\n    links: (match.route as any).links,\n    meta: (match.route as any).meta,\n    params,\n    parentId,\n    path: match.route.path,\n    pathname: match.pathname,\n    pathnameBase: match.pathnameBase,\n    shouldRevalidate: (match.route as any).shouldRevalidate,\n    // Add an unused client-only export (if present) so HMR can support\n    // switching between server-first and client-only routes during development\n    ...((match.route as any).__ensureClientRouteModuleForHMR\n      ? {\n          __ensureClientRouteModuleForHMR: (match.route as any)\n            .__ensureClientRouteModuleForHMR,\n        }\n      : {}),\n  };\n}\n\nasync function getManifestRoute(\n  route: RSCRouteConfigEntry & { parentId: string | undefined },\n): Promise<RSCRouteManifest> {\n  await explodeLazyRoute(route);\n\n  const Layout = (route as any).Layout || React.Fragment;\n  // We send errorElement early in the manifest so we have it client\n  // side for any client-side errors thrown during dataStrategy\n  const errorElement = route.ErrorBoundary\n    ? React.createElement(\n        Layout,\n        null,\n        React.createElement(route.ErrorBoundary),\n      )\n    : undefined;\n\n  return {\n    clientAction: route.clientAction,\n    clientLoader: route.clientLoader,\n    handle: route.handle,\n    hasAction: !!route.action,\n    hasComponent: !!route.Component,\n    hasErrorBoundary: !!route.ErrorBoundary,\n    errorElement,\n    hasLoader: !!route.loader,\n    id: route.id,\n    parentId: route.parentId,\n    path: route.path,\n    index: \"index\" in route ? route.index : undefined,\n    links: route.links,\n    meta: route.meta,\n  };\n}\n\nasync function explodeLazyRoute(route: RSCRouteConfigEntry) {\n  if (\"lazy\" in route && route.lazy) {\n    let {\n      default: lazyDefaultExport,\n      Component: lazyComponentExport,\n      ...lazyProperties\n    } = (await route.lazy()) as any;\n    let Component = lazyComponentExport || lazyDefaultExport;\n    if (Component && !route.Component) {\n      route.Component = Component;\n    }\n    for (let [k, v] of Object.entries(lazyProperties)) {\n      if (\n        k !== \"id\" &&\n        k !== \"path\" &&\n        k !== \"index\" &&\n        k !== \"children\" &&\n        route[k as keyof RSCRouteConfigEntry] == null\n      ) {\n        route[k as keyof RSCRouteConfigEntry] = v;\n      }\n    }\n    route.lazy = undefined;\n  }\n}\n\nasync function getAdditionalRoutePatches(\n  pathnames: string[],\n  routes: RSCRouteConfigEntry[],\n  basename: string | undefined,\n  matchedRouteIds: string[],\n): Promise<RSCRouteManifest[]> {\n  let patchRouteMatches = new Map<\n    string,\n    RSCRouteConfigEntry & { parentId: string | undefined }\n  >();\n  let matchedPaths = new Set<string>();\n\n  for (const pathname of pathnames) {\n    let segments = pathname.split(\"/\").filter(Boolean);\n    let paths: string[] = [\"/\"];\n\n    // We've already matched to the last segment\n    segments.pop();\n\n    // Traverse each path for our parents and match in case they have pathless/index\n    // children we need to include in the initial manifest\n    while (segments.length > 0) {\n      paths.push(`/${segments.join(\"/\")}`);\n      segments.pop();\n    }\n\n    paths.forEach((path) => {\n      if (matchedPaths.has(path)) {\n        return;\n      }\n      matchedPaths.add(path);\n      let matches = matchRoutes(routes, path, basename) || [];\n      matches.forEach((m, i) => {\n        if (patchRouteMatches.get(m.route.id)) {\n          return;\n        }\n        patchRouteMatches.set(m.route.id, {\n          ...m.route,\n          parentId: matches[i - 1]?.route.id,\n        });\n      });\n    });\n  }\n\n  let patches = await Promise.all(\n    [...patchRouteMatches.values()]\n      .filter((route) => !matchedRouteIds.some((id) => id === route.id))\n      .map((route) => getManifestRoute(route)),\n  );\n  return patches;\n}\n\nexport function isReactServerRequest(url: URL) {\n  return url.pathname.endsWith(\".rsc\");\n}\n\nexport function isManifestRequest(url: URL) {\n  return url.pathname.endsWith(\".manifest\");\n}\n\nfunction defaultOnError(error: unknown) {\n  if (isRedirectResponse(error)) {\n    return createRedirectErrorDigest(error);\n  }\n  if (isResponse(error) || isDataWithResponseInit(error)) {\n    return createRouteErrorResponseDigest(error);\n  }\n}\n\nfunction isClientReference(x: any) {\n  try {\n    return x.$$typeof === Symbol.for(\"react.client.reference\");\n  } catch {\n    return false;\n  }\n}\n\nfunction canDecodeWithFormData(contentType: string | null) {\n  if (!contentType) return false;\n  return (\n    contentType.match(/\\bapplication\\/x-www-form-urlencoded\\b/) ||\n    contentType.match(/\\bmultipart\\/form-data\\b/)\n  );\n}\n"
  },
  {
    "path": "packages/react-router/lib/rsc/server.ssr.tsx",
    "content": "import * as React from \"react\";\nimport { RSCRouterContext, type DataRouteObject } from \"../context\";\nimport { FrameworkContext } from \"../dom/ssr/components\";\nimport type { FrameworkContextObject } from \"../dom/ssr/entry\";\nimport { SINGLE_FETCH_REDIRECT_STATUS } from \"../dom/ssr/single-fetch\";\nimport { createStaticRouter, StaticRouterProvider } from \"../dom/server\";\nimport { injectRSCPayload } from \"./html-stream/server\";\nimport { RSCRouterGlobalErrorBoundary } from \"./errorBoundaries\";\nimport { shouldHydrateRouteLoader } from \"../dom/ssr/routes\";\nimport type { RSCPayload } from \"./server.rsc\";\nimport { createRSCRouteModules } from \"./route-modules\";\nimport { isRouteErrorResponse } from \"../router/utils\";\nimport {\n  decodeRedirectErrorDigest,\n  decodeRouteErrorResponseDigest,\n} from \"../errors\";\nimport { escapeHtml } from \"../dom/ssr/markup\";\n\ntype DecodedPayload = Promise<RSCPayload> & {\n  _deepestRenderedBoundaryId?: string | null;\n  formState: Promise<any>;\n};\n\n// Safe version of React.use() that will not cause compilation errors against\n// React 18 and will result in a runtime error if used (you can't use RSC against\n// React 18).\nconst REACT_USE = \"use\";\nconst useImpl = (React as any)[REACT_USE];\n\nfunction useSafe<T>(promise: Promise<T> | React.Context<T>): T {\n  if (useImpl) {\n    return useImpl(promise);\n  }\n  throw new Error(\"React Router v7 requires React 19+ for RSC features.\");\n}\n\nexport type SSRCreateFromReadableStreamFunction = (\n  body: ReadableStream<Uint8Array>,\n) => Promise<unknown>;\n\n/**\n * Routes the incoming [`Request`](https://developer.mozilla.org/en-US/docs/Web/API/Request)\n * to the [RSC](https://react.dev/reference/rsc/server-components) server and\n * appropriately proxies the server response for data / resource requests, or\n * renders to HTML for a document request.\n *\n * @example\n * import { createFromReadableStream } from \"@vitejs/plugin-rsc/ssr\";\n * import * as ReactDomServer from \"react-dom/server.edge\";\n * import {\n *   unstable_RSCStaticRouter as RSCStaticRouter,\n *   unstable_routeRSCServerRequest as routeRSCServerRequest,\n * } from \"react-router\";\n *\n * routeRSCServerRequest({\n *   request,\n *   serverResponse,\n *   createFromReadableStream,\n *   async renderHTML(getPayload) {\n *     const payload = getPayload();\n *\n *     return await renderHTMLToReadableStream(\n *       <RSCStaticRouter getPayload={getPayload} />,\n *       {\n *         bootstrapScriptContent,\n *         formState: await payload.formState,\n *       }\n *     );\n *   },\n * });\n *\n * @name unstable_routeRSCServerRequest\n * @public\n * @category RSC\n * @mode data\n * @param opts Options\n * @param opts.createFromReadableStream Your `react-server-dom-xyz/client`'s\n * `createFromReadableStream` function, used to decode payloads from the server.\n * @param opts.serverResponse A Response or partial response generated by the [RSC](https://react.dev/reference/rsc/server-components) handler containing a serialized {@link unstable_RSCPayload}.\n * @param opts.hydrate Whether to hydrate the server response with the RSC payload.\n * Defaults to `true`.\n * @param opts.renderHTML A function that renders the {@link unstable_RSCPayload} to\n * HTML, usually using a {@link unstable_RSCStaticRouter | `<RSCStaticRouter>`}.\n * @param opts.request The request to route.\n * @returns A [`Response`](https://developer.mozilla.org/en-US/docs/Web/API/Response)\n * that either contains the [RSC](https://react.dev/reference/rsc/server-components)\n * payload for data requests, or renders the HTML for document requests.\n */\nexport async function routeRSCServerRequest({\n  request,\n  serverResponse,\n  createFromReadableStream,\n  renderHTML,\n  hydrate = true,\n}: {\n  request: Request;\n  serverResponse: Response;\n  createFromReadableStream: SSRCreateFromReadableStreamFunction;\n  renderHTML: (\n    getPayload: () => DecodedPayload,\n    options: {\n      onError(error: unknown): string | undefined;\n      onHeaders(headers: Headers): void;\n    },\n  ) => ReadableStream<Uint8Array> | Promise<ReadableStream<Uint8Array>>;\n  hydrate?: boolean;\n}): Promise<Response> {\n  const url = new URL(request.url);\n  const isDataRequest = isReactServerRequest(url);\n  const respondWithRSCPayload =\n    isDataRequest ||\n    isManifestRequest(url) ||\n    request.headers.has(\"rsc-action-id\");\n\n  if (\n    respondWithRSCPayload ||\n    serverResponse.headers.get(\"React-Router-Resource\") === \"true\"\n  ) {\n    return serverResponse;\n  }\n\n  if (!serverResponse.body) {\n    throw new Error(\"Missing body in server response\");\n  }\n\n  const detectRedirectResponse = serverResponse.clone();\n\n  let serverResponseB: Response | null = null;\n  if (hydrate) {\n    serverResponseB = serverResponse.clone();\n  }\n\n  const body = serverResponse.body;\n\n  let buffer: Uint8Array[] | undefined;\n  let streamControllers: ReadableStreamDefaultController<Uint8Array>[] = [];\n\n  const createStream = () => {\n    if (!buffer) {\n      buffer = [];\n      return body.pipeThrough(\n        new TransformStream({\n          transform(chunk, controller) {\n            buffer!.push(chunk);\n            controller.enqueue(chunk);\n            streamControllers.forEach((c) => c.enqueue(chunk));\n          },\n          flush() {\n            streamControllers.forEach((c) => c.close());\n            streamControllers = [];\n          },\n        }),\n      );\n    }\n\n    return new ReadableStream({\n      start(controller) {\n        buffer!.forEach((chunk) => controller.enqueue(chunk));\n        streamControllers.push(controller);\n      },\n    });\n  };\n\n  let deepestRenderedBoundaryId: string | null = null;\n  const getPayload = (): DecodedPayload => {\n    const payloadPromise = Promise.resolve(\n      createFromReadableStream(createStream()),\n    ) as Promise<RSCPayload>;\n\n    return Object.defineProperties(payloadPromise, {\n      _deepestRenderedBoundaryId: {\n        get() {\n          return deepestRenderedBoundaryId;\n        },\n        set(boundaryId: string | null) {\n          deepestRenderedBoundaryId = boundaryId;\n        },\n      },\n      formState: {\n        get() {\n          return payloadPromise.then((payload) =>\n            payload.type === \"render\" ? payload.formState : undefined,\n          );\n        },\n      },\n    }) as DecodedPayload;\n  };\n\n  let renderRedirect: { status: number; location: string } | undefined;\n  let renderError: unknown;\n\n  try {\n    if (!detectRedirectResponse.body) {\n      throw new Error(\"Failed to clone server response\");\n    }\n    const payload = (await createFromReadableStream(\n      detectRedirectResponse.body,\n    )) as RSCPayload;\n    if (\n      serverResponse.status === SINGLE_FETCH_REDIRECT_STATUS &&\n      payload.type === \"redirect\"\n    ) {\n      const headers = new Headers(serverResponse.headers);\n      headers.delete(\"Content-Encoding\");\n      headers.delete(\"Content-Length\");\n      headers.delete(\"Content-Type\");\n      headers.delete(\"X-Remix-Response\");\n      headers.set(\"Location\", payload.location);\n\n      return new Response(serverResponseB?.body || \"\", {\n        headers,\n        status: payload.status,\n        statusText: serverResponse.statusText,\n      });\n    }\n\n    let reactHeaders = new Headers();\n    let status = serverResponse.status;\n    let statusText = serverResponse.statusText;\n\n    let html = await renderHTML(getPayload, {\n      onError(error: unknown) {\n        if (\n          typeof error === \"object\" &&\n          error &&\n          \"digest\" in error &&\n          typeof error.digest === \"string\"\n        ) {\n          renderRedirect = decodeRedirectErrorDigest(error.digest);\n          if (renderRedirect) {\n            return error.digest;\n          }\n          let routeErrorResponse = decodeRouteErrorResponseDigest(error.digest);\n          if (routeErrorResponse) {\n            renderError = routeErrorResponse;\n            status = routeErrorResponse.status;\n            statusText = routeErrorResponse.statusText;\n            return error.digest;\n          }\n        }\n      },\n      onHeaders(headers) {\n        for (const [key, value] of headers) {\n          reactHeaders.append(key, value);\n        }\n      },\n    });\n\n    const headers = new Headers(reactHeaders);\n    for (const [key, value] of serverResponse.headers) {\n      headers.append(key, value);\n    }\n    headers.set(\"Content-Type\", \"text/html; charset=utf-8\");\n\n    if (renderRedirect) {\n      headers.set(\"Location\", renderRedirect.location);\n      return new Response(html, {\n        status: renderRedirect.status,\n        headers,\n      });\n    }\n\n    const redirectTransform = new TransformStream({\n      flush(controller) {\n        if (renderRedirect) {\n          controller.enqueue(\n            new TextEncoder().encode(\n              `<meta http-equiv=\"refresh\" content=\"0;url=${escapeHtml(renderRedirect.location)}\"/>`,\n            ),\n          );\n        }\n      },\n    });\n\n    if (!hydrate) {\n      return new Response(html.pipeThrough(redirectTransform), {\n        status,\n        statusText,\n        headers,\n      });\n    }\n\n    if (!serverResponseB?.body) {\n      throw new Error(\"Failed to clone server response\");\n    }\n\n    const body = html\n      .pipeThrough(injectRSCPayload(serverResponseB.body))\n      .pipeThrough(redirectTransform);\n    return new Response(body, {\n      status,\n      statusText,\n      headers,\n    });\n  } catch (reason) {\n    if (reason instanceof Response) {\n      return reason;\n    }\n\n    if (renderRedirect) {\n      return new Response(`Redirect: ${renderRedirect.location}`, {\n        status: renderRedirect.status,\n        headers: {\n          Location: renderRedirect.location,\n        },\n      });\n    }\n\n    try {\n      reason = renderError ?? reason;\n      let [status, statusText] = isRouteErrorResponse(reason)\n        ? [reason.status, reason.statusText]\n        : [500, \"\"];\n\n      let retryRedirect: { status: number; location: string } | undefined;\n      let reactHeaders = new Headers();\n      const html = await renderHTML(\n        () => {\n          const decoded = Promise.resolve(\n            createFromReadableStream(createStream()),\n          ) as Promise<RSCPayload>;\n\n          const payloadPromise = decoded.then((payload) =>\n            Object.assign(payload, {\n              status,\n              errors: deepestRenderedBoundaryId\n                ? {\n                    [deepestRenderedBoundaryId]: reason,\n                  }\n                : {},\n            }),\n          );\n\n          return Object.defineProperties(payloadPromise, {\n            _deepestRenderedBoundaryId: {\n              get() {\n                return deepestRenderedBoundaryId;\n              },\n              set(boundaryId: string | null) {\n                deepestRenderedBoundaryId = boundaryId;\n              },\n            },\n            formState: {\n              get() {\n                return payloadPromise.then((payload) =>\n                  payload.type === \"render\" ? payload.formState : undefined,\n                );\n              },\n            },\n          }) as unknown as DecodedPayload;\n        },\n        {\n          onError(error: unknown) {\n            if (\n              typeof error === \"object\" &&\n              error &&\n              \"digest\" in error &&\n              typeof error.digest === \"string\"\n            ) {\n              retryRedirect = decodeRedirectErrorDigest(error.digest);\n              if (retryRedirect) {\n                return error.digest;\n              }\n              let routeErrorResponse = decodeRouteErrorResponseDigest(\n                error.digest,\n              );\n              if (routeErrorResponse) {\n                status = routeErrorResponse.status;\n                statusText = routeErrorResponse.statusText;\n                return error.digest;\n              }\n            }\n          },\n          onHeaders(headers) {\n            for (const [key, value] of headers) {\n              reactHeaders.append(key, value);\n            }\n          },\n        },\n      );\n\n      const headers = new Headers(reactHeaders);\n      for (const [key, value] of serverResponse.headers) {\n        headers.append(key, value);\n      }\n      headers.set(\"Content-Type\", \"text/html; charset=utf-8\");\n\n      if (retryRedirect) {\n        headers.set(\"Location\", retryRedirect.location);\n        return new Response(html, {\n          status: retryRedirect.status,\n          headers,\n        });\n      }\n\n      const retryRedirectTransform = new TransformStream({\n        flush(controller) {\n          if (retryRedirect) {\n            controller.enqueue(\n              new TextEncoder().encode(\n                `<meta http-equiv=\"refresh\" content=\"0;url=${escapeHtml(retryRedirect.location)}\"/>`,\n              ),\n            );\n          }\n        },\n      });\n\n      if (!hydrate) {\n        return new Response(html.pipeThrough(retryRedirectTransform), {\n          status,\n          statusText,\n          headers,\n        });\n      }\n\n      if (!serverResponseB?.body) {\n        throw new Error(\"Failed to clone server response\");\n      }\n\n      const body = html\n        .pipeThrough(injectRSCPayload(serverResponseB.body))\n        .pipeThrough(retryRedirectTransform);\n      return new Response(body, {\n        status,\n        statusText,\n        headers,\n      });\n    } catch {\n      // Throw the original error below\n    }\n\n    throw reason;\n  }\n}\n\n/**\n * Props for the {@link unstable_RSCStaticRouter} component.\n *\n * @name unstable_RSCStaticRouterProps\n * @category Types\n */\nexport interface RSCStaticRouterProps {\n  /**\n   * A function that starts decoding of the {@link unstable_RSCPayload}. Usually passed\n   * through from {@link unstable_routeRSCServerRequest}'s `renderHTML`.\n   */\n  getPayload: () => DecodedPayload;\n}\n\n/**\n * Pre-renders an {@link unstable_RSCPayload} to HTML. Usually used in\n * {@link unstable_routeRSCServerRequest}'s `renderHTML` callback.\n *\n * @example\n * import { createFromReadableStream } from \"@vitejs/plugin-rsc/ssr\";\n * import * as ReactDomServer from \"react-dom/server.edge\";\n * import {\n *   unstable_RSCStaticRouter as RSCStaticRouter,\n *   unstable_routeRSCServerRequest as routeRSCServerRequest,\n * } from \"react-router\";\n *\n * routeRSCServerRequest({\n *   request,\n *   serverResponse,\n *   createFromReadableStream,\n *   async renderHTML(getPayload) {\n *     const payload = getPayload();\n *\n *     return await renderHTMLToReadableStream(\n *       <RSCStaticRouter getPayload={getPayload} />,\n *       {\n *         bootstrapScriptContent,\n *         formState: await payload.formState,\n *       }\n *     );\n *   },\n * });\n *\n * @name unstable_RSCStaticRouter\n * @public\n * @category RSC\n * @mode data\n * @param props Props\n * @param {unstable_RSCStaticRouterProps.getPayload} props.getPayload n/a\n * @returns A React component that renders the {@link unstable_RSCPayload} as HTML.\n */\nexport function RSCStaticRouter({ getPayload }: RSCStaticRouterProps) {\n  const decoded = getPayload();\n  // Can be replaced with React.use when v18 compatibility is no longer required.\n  const payload = useSafe(decoded);\n\n  if (payload.type === \"redirect\") {\n    throw new Response(null, {\n      status: payload.status,\n      headers: {\n        Location: payload.location,\n      },\n    });\n  }\n\n  if (payload.type !== \"render\") return null;\n\n  let patchedLoaderData = { ...payload.loaderData };\n  for (const match of payload.matches) {\n    // Clear out the loaderData to avoid rendering the route component when the\n    // route opted into clientLoader hydration and either:\n    // * gave us a HydrateFallback\n    // * or doesn't have a server loader and we have no data to render\n    if (\n      shouldHydrateRouteLoader(\n        match.id,\n        match.clientLoader,\n        match.hasLoader,\n        false,\n      ) &&\n      (match.hydrateFallbackElement || !match.hasLoader)\n    ) {\n      delete patchedLoaderData[match.id];\n    }\n  }\n\n  const context = {\n    get _deepestRenderedBoundaryId() {\n      return decoded._deepestRenderedBoundaryId ?? null;\n    },\n    set _deepestRenderedBoundaryId(boundaryId: string | null) {\n      decoded._deepestRenderedBoundaryId = boundaryId;\n    },\n    actionData: payload.actionData,\n    actionHeaders: {},\n    basename: payload.basename,\n    errors: payload.errors,\n    loaderData: patchedLoaderData,\n    loaderHeaders: {},\n    location: payload.location,\n    statusCode: 200,\n    matches: payload.matches.map((match) => ({\n      params: match.params,\n      pathname: match.pathname,\n      pathnameBase: match.pathnameBase,\n      route: {\n        id: match.id,\n        action: match.hasAction || !!match.clientAction,\n        handle: match.handle,\n        hasErrorBoundary: match.hasErrorBoundary,\n        loader: match.hasLoader || !!match.clientLoader,\n        index: match.index,\n        path: match.path,\n        shouldRevalidate: match.shouldRevalidate,\n      },\n    })),\n  };\n\n  const router = createStaticRouter(\n    payload.matches.reduceRight((previous, match) => {\n      const route: DataRouteObject = {\n        id: match.id,\n        action: match.hasAction || !!match.clientAction,\n        element: match.element,\n        errorElement: match.errorElement,\n        handle: match.handle,\n        hasErrorBoundary: !!match.errorElement,\n        hydrateFallbackElement: match.hydrateFallbackElement,\n        index: match.index,\n        loader: match.hasLoader || !!match.clientLoader,\n        path: match.path,\n        shouldRevalidate: match.shouldRevalidate,\n      };\n      if (previous.length > 0) {\n        route.children = previous;\n      }\n      return [route];\n    }, [] as DataRouteObject[]),\n    context,\n  );\n\n  const frameworkContext: FrameworkContextObject = {\n    future: {\n      // These flags have no runtime impact so can always be false.  If we add\n      // flags that drive runtime behavior they'll need to be proxied through.\n      v8_middleware: false,\n      unstable_subResourceIntegrity: false,\n      unstable_trailingSlashAwareDataRequests: true, // always on for RSC\n    },\n    isSpaMode: false,\n    ssr: true,\n    criticalCss: \"\",\n    manifest: {\n      routes: {},\n      version: \"1\",\n      url: \"\",\n      entry: {\n        module: \"\",\n        imports: [],\n      },\n    },\n    routeDiscovery: { mode: \"lazy\", manifestPath: \"/__manifest\" },\n    routeModules: createRSCRouteModules(payload),\n  };\n\n  return (\n    <RSCRouterContext.Provider value={true}>\n      <RSCRouterGlobalErrorBoundary location={payload.location}>\n        <FrameworkContext.Provider value={frameworkContext}>\n          <StaticRouterProvider\n            context={context}\n            router={router}\n            hydrate={false}\n            nonce={payload.nonce}\n          />\n        </FrameworkContext.Provider>\n      </RSCRouterGlobalErrorBoundary>\n    </RSCRouterContext.Provider>\n  );\n}\n\nexport function isReactServerRequest(url: URL) {\n  return url.pathname.endsWith(\".rsc\");\n}\n\nexport function isManifestRequest(url: URL) {\n  return url.pathname.endsWith(\".manifest\");\n}\n"
  },
  {
    "path": "packages/react-router/lib/server-runtime/.eslintrc.js",
    "content": "module.exports = {\n  rules: {\n    \"no-restricted-syntax\": \"off\",\n  },\n};\n"
  },
  {
    "path": "packages/react-router/lib/server-runtime/build.ts",
    "content": "import type {\n  ActionFunctionArgs,\n  LoaderFunctionArgs,\n  RouterContextProvider,\n} from \"../router/utils\";\nimport type {\n  AssetsManifest,\n  CriticalCss,\n  EntryContext,\n  FutureConfig,\n} from \"../dom/ssr/entry\";\nimport type { ServerRouteManifest } from \"./routes\";\nimport type { AppLoadContext } from \"./data\";\nimport type { MiddlewareEnabled } from \"../types/future\";\nimport type { unstable_ServerInstrumentation } from \"../router/instrumentation\";\n\ntype OptionalCriticalCss = CriticalCss | undefined;\n\n/**\n * The output of the compiler for the server build.\n */\nexport interface ServerBuild {\n  entry: {\n    module: ServerEntryModule;\n  };\n  routes: ServerRouteManifest;\n  assets: AssetsManifest;\n  basename?: string;\n  publicPath: string;\n  assetsBuildDirectory: string;\n  future: FutureConfig;\n  ssr: boolean;\n  unstable_getCriticalCss?: (args: {\n    pathname: string;\n  }) => OptionalCriticalCss | Promise<OptionalCriticalCss>;\n  /**\n   * @deprecated This is now done via a custom header during prerendering\n   */\n  isSpaMode: boolean;\n  prerender: string[];\n  routeDiscovery: {\n    mode: \"lazy\" | \"initial\";\n    manifestPath: string;\n  };\n  allowedActionOrigins?: string[] | false;\n}\n\nexport interface HandleDocumentRequestFunction {\n  (\n    request: Request,\n    responseStatusCode: number,\n    responseHeaders: Headers,\n    context: EntryContext,\n    loadContext: MiddlewareEnabled extends true\n      ? RouterContextProvider\n      : AppLoadContext,\n  ): Promise<Response> | Response;\n}\n\nexport interface HandleDataRequestFunction {\n  (\n    response: Response,\n    args: {\n      request: LoaderFunctionArgs[\"request\"] | ActionFunctionArgs[\"request\"];\n      context: LoaderFunctionArgs[\"context\"] | ActionFunctionArgs[\"context\"];\n      params: LoaderFunctionArgs[\"params\"] | ActionFunctionArgs[\"params\"];\n    },\n  ): Promise<Response> | Response;\n}\n\nexport interface HandleErrorFunction {\n  (\n    error: unknown,\n    args: {\n      request: LoaderFunctionArgs[\"request\"] | ActionFunctionArgs[\"request\"];\n      context: LoaderFunctionArgs[\"context\"] | ActionFunctionArgs[\"context\"];\n      params: LoaderFunctionArgs[\"params\"] | ActionFunctionArgs[\"params\"];\n    },\n  ): void;\n}\n\n/**\n * A module that serves as the entry point for a Remix app during server\n * rendering.\n */\nexport interface ServerEntryModule {\n  default: HandleDocumentRequestFunction;\n  handleDataRequest?: HandleDataRequestFunction;\n  handleError?: HandleErrorFunction;\n  unstable_instrumentations?: unstable_ServerInstrumentation[];\n  streamTimeout?: number;\n}\n"
  },
  {
    "path": "packages/react-router/lib/server-runtime/cookies.ts",
    "content": "import type { ParseOptions, SerializeOptions } from \"cookie\";\nimport { parse, serialize } from \"cookie\";\n\nimport { sign, unsign } from \"./crypto\";\nimport { warnOnce } from \"./warnings\";\n\nexport type {\n  ParseOptions as CookieParseOptions,\n  SerializeOptions as CookieSerializeOptions,\n};\n\nexport interface CookieSignatureOptions {\n  /**\n   * An array of secrets that may be used to sign/unsign the value of a cookie.\n   *\n   * The array makes it easy to rotate secrets. New secrets should be added to\n   * the beginning of the array. `cookie.serialize()` will always use the first\n   * value in the array, but `cookie.parse()` may use any of them so that\n   * cookies that were signed with older secrets still work.\n   */\n  secrets?: string[];\n}\n\nexport type CookieOptions = ParseOptions &\n  SerializeOptions &\n  CookieSignatureOptions;\n\n/**\n * A HTTP cookie.\n *\n * A Cookie is a logical container for metadata about a HTTP cookie; its name\n * and options. But it doesn't contain a value. Instead, it has `parse()` and\n * `serialize()` methods that allow a single instance to be reused for\n * parsing/encoding multiple different values.\n *\n * @see https://remix.run/utils/cookies#cookie-api\n */\nexport interface Cookie {\n  /**\n   * The name of the cookie, used in the `Cookie` and `Set-Cookie` headers.\n   */\n  readonly name: string;\n\n  /**\n   * True if this cookie uses one or more secrets for verification.\n   */\n  readonly isSigned: boolean;\n\n  /**\n   * The Date this cookie expires.\n   *\n   * Note: This is calculated at access time using `maxAge` when no `expires`\n   * option is provided to `createCookie()`.\n   */\n  readonly expires?: Date;\n\n  /**\n   * Parses a raw `Cookie` header and returns the value of this cookie or\n   * `null` if it's not present.\n   */\n  parse(cookieHeader: string | null, options?: ParseOptions): Promise<any>;\n\n  /**\n   * Serializes the given value to a string and returns the `Set-Cookie`\n   * header.\n   */\n  serialize(value: any, options?: SerializeOptions): Promise<string>;\n}\n\n/**\n * Creates a logical container for managing a browser cookie from the server.\n */\nexport const createCookie = (\n  name: string,\n  cookieOptions: CookieOptions = {},\n): Cookie => {\n  let { secrets = [], ...options } = {\n    path: \"/\",\n    sameSite: \"lax\" as const,\n    ...cookieOptions,\n  };\n\n  warnOnceAboutExpiresCookie(name, options.expires);\n\n  return {\n    get name() {\n      return name;\n    },\n    get isSigned() {\n      return secrets.length > 0;\n    },\n    get expires() {\n      // Max-Age takes precedence over Expires\n      return typeof options.maxAge !== \"undefined\"\n        ? new Date(Date.now() + options.maxAge * 1000)\n        : options.expires;\n    },\n    async parse(cookieHeader, parseOptions) {\n      if (!cookieHeader) return null;\n      let cookies = parse(cookieHeader, { ...options, ...parseOptions });\n      if (name in cookies) {\n        let value = cookies[name];\n        if (typeof value === \"string\" && value !== \"\") {\n          let decoded = await decodeCookieValue(value, secrets);\n          return decoded;\n        } else {\n          return \"\";\n        }\n      } else {\n        return null;\n      }\n    },\n    async serialize(value, serializeOptions) {\n      return serialize(\n        name,\n        value === \"\" ? \"\" : await encodeCookieValue(value, secrets),\n        {\n          ...options,\n          ...serializeOptions,\n        },\n      );\n    },\n  };\n};\n\nexport type IsCookieFunction = (object: any) => object is Cookie;\n\n/**\n * Returns true if an object is a Remix cookie container.\n *\n * @see https://remix.run/utils/cookies#iscookie\n */\nexport const isCookie: IsCookieFunction = (object): object is Cookie => {\n  return (\n    object != null &&\n    typeof object.name === \"string\" &&\n    typeof object.isSigned === \"boolean\" &&\n    typeof object.parse === \"function\" &&\n    typeof object.serialize === \"function\"\n  );\n};\n\nasync function encodeCookieValue(\n  value: any,\n  secrets: string[],\n): Promise<string> {\n  let encoded = encodeData(value);\n\n  if (secrets.length > 0) {\n    encoded = await sign(encoded, secrets[0]);\n  }\n\n  return encoded;\n}\n\nasync function decodeCookieValue(\n  value: string,\n  secrets: string[],\n): Promise<any> {\n  if (secrets.length > 0) {\n    for (let secret of secrets) {\n      let unsignedValue = await unsign(value, secret);\n      if (unsignedValue !== false) {\n        return decodeData(unsignedValue);\n      }\n    }\n\n    return null;\n  }\n\n  return decodeData(value);\n}\n\nfunction encodeData(value: any): string {\n  return btoa(myUnescape(encodeURIComponent(JSON.stringify(value))));\n}\n\nfunction decodeData(value: string): any {\n  try {\n    return JSON.parse(decodeURIComponent(myEscape(atob(value))));\n  } catch (error: unknown) {\n    return {};\n  }\n}\n\n// See: https://github.com/zloirock/core-js/blob/master/packages/core-js/modules/es.escape.js\nfunction myEscape(value: string): string {\n  let str = value.toString();\n  let result = \"\";\n  let index = 0;\n  let chr, code;\n  while (index < str.length) {\n    chr = str.charAt(index++);\n    if (/[\\w*+\\-./@]/.exec(chr)) {\n      result += chr;\n    } else {\n      code = chr.charCodeAt(0);\n      if (code < 256) {\n        result += \"%\" + hex(code, 2);\n      } else {\n        result += \"%u\" + hex(code, 4).toUpperCase();\n      }\n    }\n  }\n  return result;\n}\n\nfunction hex(code: number, length: number): string {\n  let result = code.toString(16);\n  while (result.length < length) result = \"0\" + result;\n  return result;\n}\n\n// See: https://github.com/zloirock/core-js/blob/master/packages/core-js/modules/es.unescape.js\nfunction myUnescape(value: string): string {\n  let str = value.toString();\n  let result = \"\";\n  let index = 0;\n  let chr, part;\n  while (index < str.length) {\n    chr = str.charAt(index++);\n    if (chr === \"%\") {\n      if (str.charAt(index) === \"u\") {\n        part = str.slice(index + 1, index + 5);\n        if (/^[\\da-f]{4}$/i.exec(part)) {\n          result += String.fromCharCode(parseInt(part, 16));\n          index += 5;\n          continue;\n        }\n      } else {\n        part = str.slice(index, index + 2);\n        if (/^[\\da-f]{2}$/i.exec(part)) {\n          result += String.fromCharCode(parseInt(part, 16));\n          index += 2;\n          continue;\n        }\n      }\n    }\n    result += chr;\n  }\n  return result;\n}\n\nfunction warnOnceAboutExpiresCookie(name: string, expires?: Date) {\n  warnOnce(\n    !expires,\n    `The \"${name}\" cookie has an \"expires\" property set. ` +\n      `This will cause the expires value to not be updated when the session is committed. ` +\n      `Instead, you should set the expires value when serializing the cookie. ` +\n      `You can use \\`commitSession(session, { expires })\\` if using a session storage object, ` +\n      `or \\`cookie.serialize(\"value\", { expires })\\` if you're using the cookie directly.`,\n  );\n}\n"
  },
  {
    "path": "packages/react-router/lib/server-runtime/crypto.ts",
    "content": "const encoder = /* @__PURE__ */ new TextEncoder();\n\nexport const sign = async (value: string, secret: string): Promise<string> => {\n  let data = encoder.encode(value);\n  let key = await createKey(secret, [\"sign\"]);\n  let signature = await crypto.subtle.sign(\"HMAC\", key, data);\n  let hash = btoa(String.fromCharCode(...new Uint8Array(signature))).replace(\n    /=+$/,\n    \"\",\n  );\n\n  return value + \".\" + hash;\n};\n\nexport const unsign = async (\n  cookie: string,\n  secret: string,\n): Promise<string | false> => {\n  let index = cookie.lastIndexOf(\".\");\n  let value = cookie.slice(0, index);\n  let hash = cookie.slice(index + 1);\n\n  let data = encoder.encode(value);\n\n  let key = await createKey(secret, [\"verify\"]);\n  try {\n    let signature = byteStringToUint8Array(atob(hash));\n    let valid = await crypto.subtle.verify(\"HMAC\", key, signature, data);\n\n    return valid ? value : false;\n  } catch (error: unknown) {\n    // atob will throw a DOMException with name === 'InvalidCharacterError'\n    // if the signature contains a non-base64 character, which should just\n    // be treated as an invalid signature.\n    return false;\n  }\n};\n\nconst createKey = async (\n  secret: string,\n  usages: CryptoKey[\"usages\"],\n): Promise<CryptoKey> =>\n  crypto.subtle.importKey(\n    \"raw\",\n    encoder.encode(secret),\n    { name: \"HMAC\", hash: \"SHA-256\" },\n    false,\n    usages,\n  );\n\nfunction byteStringToUint8Array(byteString: string): Uint8Array {\n  let array = new Uint8Array(byteString.length);\n\n  for (let i = 0; i < byteString.length; i++) {\n    array[i] = byteString.charCodeAt(i);\n  }\n\n  return array;\n}\n"
  },
  {
    "path": "packages/react-router/lib/server-runtime/data.ts",
    "content": "import type {\n  LoaderFunction,\n  ActionFunction,\n  LoaderFunctionArgs,\n  ActionFunctionArgs,\n} from \"../router/utils\";\nimport { isDataWithResponseInit, isRedirectStatusCode } from \"../router/router\";\n\n/**\n * An object of unknown type for route loaders and actions provided by the\n * server's `getLoadContext()` function.  This is defined as an empty interface\n * specifically so apps can leverage declaration merging to augment this type\n * globally: https://www.typescriptlang.org/docs/handbook/declaration-merging.html\n */\nexport interface AppLoadContext {\n  [key: string]: unknown;\n}\n\n// Need to use RR's version here to permit the optional context even\n// though we know it'll always be provided in remix\nexport async function callRouteHandler(\n  handler: LoaderFunction | ActionFunction,\n  args: LoaderFunctionArgs | ActionFunctionArgs,\n) {\n  let result = await handler({\n    request: stripRoutesParam(stripIndexParam(args.request)),\n    params: args.params,\n    context: args.context,\n    unstable_pattern: args.unstable_pattern,\n  });\n\n  // If they returned a redirect via data(), re-throw it as a Response\n  if (\n    isDataWithResponseInit(result) &&\n    result.init &&\n    result.init.status &&\n    isRedirectStatusCode(result.init.status)\n  ) {\n    throw new Response(null, result.init);\n  }\n\n  return result;\n}\n\n// TODO: Document these search params better\n// and stop stripping these in V2. These break\n// support for running in a SW and also expose\n// valuable info to data funcs that is being asked\n// for such as \"is this a data request?\".\nfunction stripIndexParam(request: Request) {\n  let url = new URL(request.url);\n  let indexValues = url.searchParams.getAll(\"index\");\n  url.searchParams.delete(\"index\");\n  let indexValuesToKeep = [];\n  for (let indexValue of indexValues) {\n    if (indexValue) {\n      indexValuesToKeep.push(indexValue);\n    }\n  }\n  for (let toKeep of indexValuesToKeep) {\n    url.searchParams.append(\"index\", toKeep);\n  }\n\n  let init: RequestInit = {\n    method: request.method,\n    body: request.body,\n    headers: request.headers,\n    signal: request.signal,\n  };\n\n  if (init.body) {\n    (init as { duplex: \"half\" }).duplex = \"half\";\n  }\n\n  return new Request(url.href, init);\n}\n\nfunction stripRoutesParam(request: Request) {\n  let url = new URL(request.url);\n  url.searchParams.delete(\"_routes\");\n  let init: RequestInit = {\n    method: request.method,\n    body: request.body,\n    headers: request.headers,\n    signal: request.signal,\n  };\n\n  if (init.body) {\n    (init as { duplex: \"half\" }).duplex = \"half\";\n  }\n\n  return new Request(url.href, init);\n}\n"
  },
  {
    "path": "packages/react-router/lib/server-runtime/dev.ts",
    "content": "type DevServerHooks = {\n  getCriticalCss?: (pathname: string) => Promise<string | undefined>;\n  processRequestError?: (error: unknown) => void;\n};\n\nconst globalDevServerHooksKey = \"__reactRouterDevServerHooks\";\n\nexport function setDevServerHooks(devServerHooks: DevServerHooks) {\n  // @ts-expect-error\n  globalThis[globalDevServerHooksKey] = devServerHooks;\n}\n\nexport function getDevServerHooks(): DevServerHooks | undefined {\n  // @ts-expect-error\n  return globalThis[globalDevServerHooksKey];\n}\n\n// Guarded access to build-time-only headers\nexport function getBuildTimeHeader(request: Request, headerName: string) {\n  if (typeof process !== \"undefined\") {\n    try {\n      if (process.env?.IS_RR_BUILD_REQUEST === \"yes\") {\n        return request.headers.get(headerName);\n      }\n    } catch (e) {}\n  }\n  return null;\n}\n"
  },
  {
    "path": "packages/react-router/lib/server-runtime/entry.ts",
    "content": "import type { RouteModules } from \"../dom/ssr/routeModules\";\nimport type { ServerRouteManifest } from \"./routes\";\n\nexport function createEntryRouteModules(\n  manifest: ServerRouteManifest,\n): RouteModules {\n  return Object.keys(manifest).reduce((memo, routeId) => {\n    let route = manifest[routeId];\n    if (route) {\n      memo[routeId] = route.module;\n    }\n    return memo;\n  }, {} as RouteModules);\n}\n"
  },
  {
    "path": "packages/react-router/lib/server-runtime/errors.ts",
    "content": "import type { StaticHandlerContext } from \"../router/router\";\nimport { isRouteErrorResponse } from \"../router/utils\";\n\nimport { ServerMode } from \"./mode\";\n\n/**\n * This thing probably warrants some explanation.\n *\n * The whole point here is to emulate componentDidCatch for server rendering and\n * data loading. It can get tricky. React can do this on component boundaries\n * but doesn't support it for server rendering or data loading. We know enough\n * with nested routes to be able to emulate the behavior (because we know them\n * statically before rendering.)\n *\n * Each route can export an `ErrorBoundary`.\n *\n * - When rendering throws an error, the nearest error boundary will render\n *   (normal react componentDidCatch). This will be the route's own boundary, but\n *   if none is provided, it will bubble up to the parents.\n * - When data loading throws an error, the nearest error boundary will render\n * - When performing an action, the nearest error boundary for the action's\n *   route tree will render (no redirect happens)\n *\n * During normal react rendering, we do nothing special, just normal\n * componentDidCatch.\n *\n * For server rendering, we mutate `renderBoundaryRouteId` to know the last\n * layout that has an error boundary that tried to render. This emulates which\n * layout would catch a thrown error. If the rendering fails, we catch the error\n * on the server, and go again a second time with the emulator holding on to the\n * information it needs to render the same error boundary as a dynamically\n * thrown render error.\n *\n * When data loading, server or client side, we use the emulator to likewise\n * hang on to the error and re-render at the appropriate layout (where a thrown\n * error would have been caught by cDC).\n *\n * When actions throw, it all works the same. There's an edge case to be aware\n * of though. Actions normally are required to redirect, but in the case of\n * errors, we render the action's route with the emulator holding on to the\n * error. If during this render a parent route/loader throws we ignore that new\n * error and render the action's original error as deeply as possible. In other\n * words, we simply ignore the new error and use the action's error in place\n * because it came first, and that just wouldn't be fair to let errors cut in\n * line.\n */\n\nexport function sanitizeError<T = unknown>(error: T, serverMode: ServerMode) {\n  if (error instanceof Error && serverMode !== ServerMode.Development) {\n    let sanitized = new Error(\"Unexpected Server Error\");\n    sanitized.stack = undefined;\n    return sanitized;\n  }\n  return error;\n}\n\nexport function sanitizeErrors(\n  errors: NonNullable<StaticHandlerContext[\"errors\"]>,\n  serverMode: ServerMode,\n) {\n  return Object.entries(errors).reduce((acc, [routeId, error]) => {\n    return Object.assign(acc, { [routeId]: sanitizeError(error, serverMode) });\n  }, {});\n}\n\n// must be type alias due to inference issues on interfaces\n// https://github.com/microsoft/TypeScript/issues/15300\nexport type SerializedError = {\n  message: string;\n  stack?: string;\n};\n\nexport function serializeError(\n  error: Error,\n  serverMode: ServerMode,\n): SerializedError {\n  let sanitized = sanitizeError(error, serverMode);\n  return {\n    message: sanitized.message,\n    stack: sanitized.stack,\n  };\n}\n\nexport function serializeErrors(\n  errors: StaticHandlerContext[\"errors\"],\n  serverMode: ServerMode,\n): StaticHandlerContext[\"errors\"] {\n  if (!errors) return null;\n  let entries = Object.entries(errors);\n  let serialized: StaticHandlerContext[\"errors\"] = {};\n  for (let [key, val] of entries) {\n    // Hey you!  If you change this, please change the corresponding logic in\n    // deserializeErrors in remix-react/errors.ts :)\n    if (isRouteErrorResponse(val)) {\n      serialized[key] = { ...val, __type: \"RouteErrorResponse\" };\n    } else if (val instanceof Error) {\n      let sanitized = sanitizeError(val, serverMode);\n      serialized[key] = {\n        message: sanitized.message,\n        stack: sanitized.stack,\n        __type: \"Error\",\n        // If this is a subclass (i.e., ReferenceError), send up the type so we\n        // can re-create the same type during hydration.  This will only apply\n        // in dev mode since all production errors are sanitized to normal\n        // Error instances\n        ...(sanitized.name !== \"Error\"\n          ? {\n              __subType: sanitized.name,\n            }\n          : {}),\n      };\n    } else {\n      serialized[key] = val;\n    }\n  }\n  return serialized;\n}\n"
  },
  {
    "path": "packages/react-router/lib/server-runtime/headers.ts",
    "content": "import { splitCookiesString } from \"set-cookie-parser\";\n\nimport type { DataRouteMatch } from \"../context\";\nimport type { StaticHandlerContext } from \"../router/router\";\nimport type { ServerRouteModule } from \"../dom/ssr/routeModules\";\nimport type { ServerBuild } from \"./build\";\nimport invariant from \"./invariant\";\n\n// Version used by v7 framework mode\nexport function getDocumentHeaders(\n  context: StaticHandlerContext,\n  build: ServerBuild,\n): Headers {\n  return getDocumentHeadersImpl(context, (m) => {\n    let route = build.routes[m.route.id];\n    invariant(route, `Route with id \"${m.route.id}\" not found in build`);\n    return route.module.headers;\n  });\n}\n\nexport function getDocumentHeadersImpl(\n  context: StaticHandlerContext,\n  getRouteHeadersFn: (match: DataRouteMatch) => ServerRouteModule[\"headers\"],\n  _defaultHeaders?: Headers,\n): Headers {\n  let boundaryIdx = context.errors\n    ? context.matches.findIndex((m) => context.errors![m.route.id])\n    : -1;\n  let matches =\n    boundaryIdx >= 0\n      ? context.matches.slice(0, boundaryIdx + 1)\n      : context.matches;\n\n  let errorHeaders: Headers | undefined;\n\n  if (boundaryIdx >= 0) {\n    // Look for any errorHeaders from the boundary route down, which can be\n    // identified by the presence of headers but no data\n    let { actionHeaders, actionData, loaderHeaders, loaderData } = context;\n    context.matches.slice(boundaryIdx).some((match) => {\n      let id = match.route.id;\n      if (\n        actionHeaders[id] &&\n        (!actionData || !actionData.hasOwnProperty(id))\n      ) {\n        errorHeaders = actionHeaders[id];\n      } else if (loaderHeaders[id] && !loaderData.hasOwnProperty(id)) {\n        errorHeaders = loaderHeaders[id];\n      }\n      return errorHeaders != null;\n    });\n  }\n\n  const defaultHeaders = new Headers(_defaultHeaders);\n\n  return matches.reduce((parentHeaders, match, idx) => {\n    let { id } = match.route;\n    let loaderHeaders = context.loaderHeaders[id] || new Headers();\n    let actionHeaders = context.actionHeaders[id] || new Headers();\n\n    // Only expose errorHeaders to the leaf headers() function to\n    // avoid duplication via parentHeaders\n    let includeErrorHeaders =\n      errorHeaders != null && idx === matches.length - 1;\n    // Only prepend cookies from errorHeaders at the leaf renderable route\n    // when it's not the same as loaderHeaders/actionHeaders to avoid\n    // duplicate cookies\n    let includeErrorCookies =\n      includeErrorHeaders &&\n      errorHeaders !== loaderHeaders &&\n      errorHeaders !== actionHeaders;\n\n    let headersFn = getRouteHeadersFn(match);\n\n    // Use the parent headers for any route without a `headers` export\n    if (headersFn == null) {\n      let headers = new Headers(parentHeaders);\n      if (includeErrorCookies) {\n        prependCookies(errorHeaders!, headers);\n      }\n      prependCookies(actionHeaders, headers);\n      prependCookies(loaderHeaders, headers);\n      return headers;\n    }\n\n    let headers = new Headers(\n      typeof headersFn === \"function\"\n        ? headersFn({\n            loaderHeaders,\n            parentHeaders,\n            actionHeaders,\n            errorHeaders: includeErrorHeaders ? errorHeaders : undefined,\n          })\n        : headersFn,\n    );\n\n    // Automatically preserve Set-Cookie headers from bubbled responses,\n    // loaders, errors, and parent routes\n    if (includeErrorCookies) {\n      prependCookies(errorHeaders!, headers);\n    }\n    prependCookies(actionHeaders, headers);\n    prependCookies(loaderHeaders, headers);\n    prependCookies(parentHeaders, headers);\n\n    return headers;\n  }, new Headers(defaultHeaders));\n}\n\nfunction prependCookies(parentHeaders: Headers, childHeaders: Headers): void {\n  let parentSetCookieString = parentHeaders.get(\"Set-Cookie\");\n\n  if (parentSetCookieString) {\n    let cookies = splitCookiesString(parentSetCookieString);\n    let childCookies = new Set(childHeaders.getSetCookie());\n    cookies.forEach((cookie) => {\n      if (!childCookies.has(cookie)) {\n        childHeaders.append(\"Set-Cookie\", cookie);\n      }\n    });\n  }\n}\n"
  },
  {
    "path": "packages/react-router/lib/server-runtime/invariant.ts",
    "content": "export default function invariant(\n  value: boolean,\n  message?: string,\n): asserts value;\nexport default function invariant<T>(\n  value: T | null | undefined,\n  message?: string,\n): asserts value is T;\nexport default function invariant(value: any, message?: string) {\n  if (value === false || value === null || typeof value === \"undefined\") {\n    console.error(\n      \"The following error is a bug in React Router; please open an issue! https://github.com/remix-run/react-router/issues/new/choose\",\n    );\n    throw new Error(message);\n  }\n}\n"
  },
  {
    "path": "packages/react-router/lib/server-runtime/mode.ts",
    "content": "/**\n * The mode to use when running the server.\n */\nexport enum ServerMode {\n  Development = \"development\",\n  Production = \"production\",\n  Test = \"test\",\n}\n\nexport function isServerMode(value: any): value is ServerMode {\n  return (\n    value === ServerMode.Development ||\n    value === ServerMode.Production ||\n    value === ServerMode.Test\n  );\n}\n"
  },
  {
    "path": "packages/react-router/lib/server-runtime/routeMatching.ts",
    "content": "import type { Params, AgnosticRouteObject } from \"../router/utils\";\nimport { matchRoutes } from \"../router/utils\";\nimport type { ServerRoute } from \"./routes\";\n\nexport interface RouteMatch<Route> {\n  params: Params;\n  pathname: string;\n  route: Route;\n}\n\nexport function matchServerRoutes(\n  routes: ServerRoute[],\n  pathname: string,\n  basename?: string,\n): RouteMatch<ServerRoute>[] | null {\n  let matches = matchRoutes(\n    routes as unknown as AgnosticRouteObject[],\n    pathname,\n    basename,\n  );\n  if (!matches) return null;\n\n  return matches.map((match) => ({\n    params: match.params,\n    pathname: match.pathname,\n    route: match.route as unknown as ServerRoute,\n  }));\n}\n"
  },
  {
    "path": "packages/react-router/lib/server-runtime/routes.ts",
    "content": "import type {\n  AgnosticDataRouteObject,\n  LoaderFunctionArgs as RRLoaderFunctionArgs,\n  ActionFunctionArgs as RRActionFunctionArgs,\n  RouteManifest,\n  MiddlewareFunction,\n} from \"../router/utils\";\nimport { redirectDocument, replace, redirect } from \"../router/utils\";\nimport { callRouteHandler } from \"./data\";\nimport type { FutureConfig } from \"../dom/ssr/entry\";\nimport type { Route } from \"../dom/ssr/routes\";\nimport type {\n  SingleFetchResult,\n  SingleFetchResults,\n} from \"../dom/ssr/single-fetch\";\nimport {\n  SingleFetchRedirectSymbol,\n  decodeViaTurboStream,\n} from \"../dom/ssr/single-fetch\";\nimport invariant from \"./invariant\";\nimport type { ServerRouteModule } from \"../dom/ssr/routeModules\";\nimport { getBuildTimeHeader } from \"./dev\";\n\nexport type ServerRouteManifest = RouteManifest<Omit<ServerRoute, \"children\">>;\n\nexport interface ServerRoute extends Route {\n  children: ServerRoute[];\n  module: ServerRouteModule;\n}\n\nfunction groupRoutesByParentId(manifest: ServerRouteManifest) {\n  let routes: Record<string, Omit<ServerRoute, \"children\">[]> = {};\n\n  Object.values(manifest).forEach((route) => {\n    if (route) {\n      let parentId = route.parentId || \"\";\n      if (!routes[parentId]) {\n        routes[parentId] = [];\n      }\n      routes[parentId].push(route);\n    }\n  });\n\n  return routes;\n}\n\n// Create a map of routes by parentId to use recursively instead of\n// repeatedly filtering the manifest.\nexport function createRoutes(\n  manifest: ServerRouteManifest,\n  parentId: string = \"\",\n  routesByParentId: Record<\n    string,\n    Omit<ServerRoute, \"children\">[]\n  > = groupRoutesByParentId(manifest),\n): ServerRoute[] {\n  return (routesByParentId[parentId] || []).map((route) => ({\n    ...route,\n    children: createRoutes(manifest, route.id, routesByParentId),\n  }));\n}\n\n// Convert the Remix ServerManifest into DataRouteObject's for use with\n// createStaticHandler\nexport function createStaticHandlerDataRoutes(\n  manifest: ServerRouteManifest,\n  future: FutureConfig,\n  parentId: string = \"\",\n  routesByParentId: Record<\n    string,\n    Omit<ServerRoute, \"children\">[]\n  > = groupRoutesByParentId(manifest),\n): AgnosticDataRouteObject[] {\n  return (routesByParentId[parentId] || []).map((route) => {\n    let commonRoute = {\n      // Always include root due to default boundaries\n      hasErrorBoundary:\n        route.id === \"root\" || route.module.ErrorBoundary != null,\n      id: route.id,\n      path: route.path,\n      middleware: route.module.middleware as unknown as\n        | MiddlewareFunction[]\n        | undefined,\n      // Need to use RR's version in the param typed here to permit the optional\n      // context even though we know it'll always be provided in remix\n      loader: route.module.loader\n        ? async (args: RRLoaderFunctionArgs) => {\n            // If we're prerendering, use the data passed in from prerendering\n            // the .data route so we don't call loaders twice\n            let preRenderedData = getBuildTimeHeader(\n              args.request,\n              \"X-React-Router-Prerender-Data\",\n            );\n            if (preRenderedData != null) {\n              let encoded = preRenderedData\n                ? decodeURI(preRenderedData)\n                : preRenderedData;\n              invariant(encoded, \"Missing prerendered data for route\");\n              let uint8array = new TextEncoder().encode(encoded);\n              let stream = new ReadableStream({\n                start(controller) {\n                  controller.enqueue(uint8array);\n                  controller.close();\n                },\n              });\n              let decoded = await decodeViaTurboStream(stream, global);\n              let data = decoded.value as SingleFetchResults;\n\n              // If the loader returned a `.data` redirect, re-throw a normal\n              // Response here to trigger a document level SSG redirect\n              if (data && SingleFetchRedirectSymbol in data) {\n                let result = data[SingleFetchRedirectSymbol]!;\n                let init = { status: result.status };\n                if (result.reload) {\n                  throw redirectDocument(result.redirect, init);\n                } else if (result.replace) {\n                  throw replace(result.redirect, init);\n                } else {\n                  throw redirect(result.redirect, init);\n                }\n              } else {\n                invariant(\n                  data && route.id in data,\n                  \"Unable to decode prerendered data\",\n                );\n                let result = data[route.id] as SingleFetchResult;\n                invariant(\n                  \"data\" in result,\n                  \"Unable to process prerendered data\",\n                );\n                return result.data;\n              }\n            }\n            let val = await callRouteHandler(route.module.loader!, args);\n            return val;\n          }\n        : undefined,\n      action: route.module.action\n        ? (args: RRActionFunctionArgs) =>\n            callRouteHandler(route.module.action!, args)\n        : undefined,\n      handle: route.module.handle,\n    };\n\n    return route.index\n      ? {\n          index: true,\n          ...commonRoute,\n        }\n      : {\n          caseSensitive: route.caseSensitive,\n          children: createStaticHandlerDataRoutes(\n            manifest,\n            future,\n            route.id,\n            routesByParentId,\n          ),\n          ...commonRoute,\n        };\n  });\n}\n"
  },
  {
    "path": "packages/react-router/lib/server-runtime/server.ts",
    "content": "import type { StaticHandler, StaticHandlerContext } from \"../router/router\";\nimport type { ErrorResponse } from \"../router/utils\";\nimport { RouterContextProvider } from \"../router/utils\";\nimport {\n  isRouteErrorResponse,\n  ErrorResponseImpl,\n  stripBasename,\n} from \"../router/utils\";\nimport {\n  getStaticContextFromError,\n  createStaticHandler,\n  isRedirectResponse,\n  isResponse,\n} from \"../router/router\";\nimport type { AppLoadContext } from \"./data\";\nimport type { HandleErrorFunction, ServerBuild } from \"./build\";\nimport type { CriticalCss, EntryContext } from \"../dom/ssr/entry\";\nimport { createEntryRouteModules } from \"./entry\";\nimport { sanitizeErrors, serializeError, serializeErrors } from \"./errors\";\nimport { ServerMode, isServerMode } from \"./mode\";\nimport type { RouteMatch } from \"./routeMatching\";\nimport { matchServerRoutes } from \"./routeMatching\";\nimport type { ServerRoute } from \"./routes\";\nimport { createStaticHandlerDataRoutes, createRoutes } from \"./routes\";\nimport type { ServerHandoff } from \"./serverHandoff\";\nimport { createServerHandoffString } from \"./serverHandoff\";\nimport { getBuildTimeHeader, getDevServerHooks } from \"./dev\";\nimport {\n  encodeViaTurboStream,\n  singleFetchAction,\n  singleFetchLoaders,\n  SERVER_NO_BODY_STATUS_CODES,\n  generateSingleFetchRedirectResponse,\n} from \"./single-fetch\";\nimport { getDocumentHeaders } from \"./headers\";\nimport type { EntryRoute } from \"../dom/ssr/routes\";\nimport type { MiddlewareEnabled } from \"../types/future\";\nimport { getManifestPath } from \"../dom/ssr/fog-of-war\";\nimport type { unstable_InstrumentRequestHandlerFunction } from \"../router/instrumentation\";\nimport { instrumentHandler } from \"../router/instrumentation\";\nimport { throwIfPotentialCSRFAttack } from \"../actions\";\n\nexport type RequestHandler = (\n  request: Request,\n  loadContext?: MiddlewareEnabled extends true\n    ? RouterContextProvider\n    : AppLoadContext,\n) => Promise<Response>;\n\nexport type CreateRequestHandlerFunction = (\n  build: ServerBuild | (() => ServerBuild | Promise<ServerBuild>),\n  mode?: string,\n) => RequestHandler;\n\nfunction derive(build: ServerBuild, mode?: string) {\n  let routes = createRoutes(build.routes);\n  let dataRoutes = createStaticHandlerDataRoutes(build.routes, build.future);\n  let serverMode = isServerMode(mode) ? mode : ServerMode.Production;\n  let staticHandler = createStaticHandler(dataRoutes, {\n    basename: build.basename,\n    unstable_instrumentations: build.entry.module.unstable_instrumentations,\n  });\n\n  let errorHandler =\n    build.entry.module.handleError ||\n    ((error, { request }) => {\n      if (serverMode !== ServerMode.Test && !request.signal.aborted) {\n        console.error(\n          // @ts-expect-error This is \"private\" from users but intended for internal use\n          isRouteErrorResponse(error) && error.error ? error.error : error,\n        );\n      }\n    });\n\n  let requestHandler: RequestHandler = async (request, initialContext) => {\n    let params: RouteMatch<ServerRoute>[\"params\"] = {};\n    let loadContext: AppLoadContext | RouterContextProvider;\n\n    let handleError = (error: unknown) => {\n      if (mode === ServerMode.Development) {\n        getDevServerHooks()?.processRequestError?.(error);\n      }\n\n      errorHandler(error, {\n        context: loadContext,\n        params,\n        request,\n      });\n    };\n\n    if (build.future.v8_middleware) {\n      if (\n        initialContext &&\n        !(initialContext instanceof RouterContextProvider)\n      ) {\n        let error = new Error(\n          \"Invalid `context` value provided to `handleRequest`. When middleware \" +\n            \"is enabled you must return an instance of `RouterContextProvider` \" +\n            \"from your `getLoadContext` function.\",\n        );\n        handleError(error);\n        return returnLastResortErrorResponse(error, serverMode);\n      }\n      loadContext = initialContext || new RouterContextProvider();\n    } else {\n      loadContext = initialContext || {};\n    }\n\n    let url = new URL(request.url);\n\n    let normalizedBasename = build.basename || \"/\";\n    let normalizedPath = url.pathname;\n    if (build.future.unstable_trailingSlashAwareDataRequests) {\n      if (normalizedPath.endsWith(\"/_.data\")) {\n        // Handle trailing slash URLs: /about/_.data -> /about/\n        normalizedPath = normalizedPath.replace(/_.data$/, \"\");\n      } else {\n        normalizedPath = normalizedPath.replace(/\\.data$/, \"\");\n      }\n    } else {\n      if (stripBasename(normalizedPath, normalizedBasename) === \"/_root.data\") {\n        normalizedPath = normalizedBasename;\n      } else if (normalizedPath.endsWith(\".data\")) {\n        normalizedPath = normalizedPath.replace(/\\.data$/, \"\");\n      }\n\n      if (\n        stripBasename(normalizedPath, normalizedBasename) !== \"/\" &&\n        normalizedPath.endsWith(\"/\")\n      ) {\n        normalizedPath = normalizedPath.slice(0, -1);\n      }\n    }\n\n    let isSpaMode =\n      getBuildTimeHeader(request, \"X-React-Router-SPA-Mode\") === \"yes\";\n\n    // When runtime SSR is disabled, make our dev server behave like the deployed\n    // pre-rendered site would\n    if (!build.ssr) {\n      // Decode the URL path before checking against the prerender config\n      let decodedPath = decodeURI(normalizedPath);\n\n      if (normalizedBasename !== \"/\") {\n        let strippedPath = stripBasename(decodedPath, normalizedBasename);\n        if (strippedPath == null) {\n          errorHandler(\n            new ErrorResponseImpl(\n              404,\n              \"Not Found\",\n              `Refusing to prerender the \\`${decodedPath}\\` path because it does ` +\n                `not start with the basename \\`${normalizedBasename}\\``,\n            ),\n            {\n              context: loadContext,\n              params,\n              request,\n            },\n          );\n          return new Response(\"Not Found\", {\n            status: 404,\n            statusText: \"Not Found\",\n          });\n        }\n        decodedPath = strippedPath;\n      }\n\n      // When SSR is disabled this, file can only ever run during dev because we\n      // delete the server build at the end of the build\n      if (build.prerender.length === 0) {\n        // ssr:false and no prerender config indicates \"SPA Mode\"\n        isSpaMode = true;\n      } else if (\n        !build.prerender.includes(decodedPath) &&\n        !build.prerender.includes(decodedPath + \"/\")\n      ) {\n        if (url.pathname.endsWith(\".data\")) {\n          // 404 on non-pre-rendered `.data` requests\n          errorHandler(\n            new ErrorResponseImpl(\n              404,\n              \"Not Found\",\n              `Refusing to SSR the path \\`${decodedPath}\\` because \\`ssr:false\\` is set and the path is not included in the \\`prerender\\` config, so in production the path will be a 404.`,\n            ),\n            {\n              context: loadContext,\n              params,\n              request,\n            },\n          );\n          return new Response(\"Not Found\", {\n            status: 404,\n            statusText: \"Not Found\",\n          });\n        } else {\n          // Serve a SPA fallback for non-pre-rendered document requests\n          isSpaMode = true;\n        }\n      }\n    }\n\n    // Manifest request for fog of war\n    let manifestUrl = getManifestPath(\n      build.routeDiscovery.manifestPath,\n      normalizedBasename,\n    );\n    if (url.pathname === manifestUrl) {\n      try {\n        let res = await handleManifestRequest(build, routes, url);\n        return res;\n      } catch (e) {\n        handleError(e);\n        return new Response(\"Unknown Server Error\", { status: 500 });\n      }\n    }\n\n    let matches = matchServerRoutes(routes, normalizedPath, build.basename);\n    if (matches && matches.length > 0) {\n      Object.assign(params, matches[0].params);\n    }\n\n    let response: Response;\n    if (url.pathname.endsWith(\".data\")) {\n      let handlerUrl = new URL(request.url);\n      handlerUrl.pathname = normalizedPath;\n\n      let singleFetchMatches = matchServerRoutes(\n        routes,\n        handlerUrl.pathname,\n        build.basename,\n      );\n\n      response = await handleSingleFetchRequest(\n        serverMode,\n        build,\n        staticHandler,\n        request,\n        handlerUrl,\n        loadContext,\n        handleError,\n      );\n\n      if (isRedirectResponse(response)) {\n        response = generateSingleFetchRedirectResponse(\n          response,\n          request,\n          build,\n          serverMode,\n        );\n      }\n\n      if (build.entry.module.handleDataRequest) {\n        response = await build.entry.module.handleDataRequest(response, {\n          context: loadContext,\n          params: singleFetchMatches ? singleFetchMatches[0].params : {},\n          request,\n        });\n\n        if (isRedirectResponse(response)) {\n          response = generateSingleFetchRedirectResponse(\n            response,\n            request,\n            build,\n            serverMode,\n          );\n        }\n      }\n    } else if (\n      !isSpaMode &&\n      matches &&\n      matches[matches.length - 1].route.module.default == null &&\n      matches[matches.length - 1].route.module.ErrorBoundary == null\n    ) {\n      response = await handleResourceRequest(\n        serverMode,\n        build,\n        staticHandler,\n        matches.slice(-1)[0].route.id,\n        request,\n        loadContext,\n        handleError,\n      );\n    } else {\n      let { pathname } = url;\n\n      let criticalCss: CriticalCss | undefined = undefined;\n      if (build.unstable_getCriticalCss) {\n        criticalCss = await build.unstable_getCriticalCss({ pathname });\n      } else if (\n        mode === ServerMode.Development &&\n        getDevServerHooks()?.getCriticalCss\n      ) {\n        criticalCss = await getDevServerHooks()?.getCriticalCss?.(pathname);\n      }\n\n      response = await handleDocumentRequest(\n        serverMode,\n        build,\n        staticHandler,\n        request,\n        loadContext,\n        handleError,\n        isSpaMode,\n        criticalCss,\n      );\n    }\n\n    if (request.method === \"HEAD\") {\n      return new Response(null, {\n        headers: response.headers,\n        status: response.status,\n        statusText: response.statusText,\n      });\n    }\n\n    return response;\n  };\n\n  if (build.entry.module.unstable_instrumentations) {\n    requestHandler = instrumentHandler(\n      requestHandler,\n      build.entry.module.unstable_instrumentations\n        .map((i) => i.handler)\n        .filter(Boolean) as unstable_InstrumentRequestHandlerFunction[],\n    );\n  }\n\n  return {\n    routes,\n    dataRoutes,\n    serverMode,\n    staticHandler,\n    errorHandler,\n    requestHandler,\n  };\n}\n\nexport const createRequestHandler: CreateRequestHandlerFunction = (\n  build,\n  mode,\n) => {\n  let _build: ServerBuild;\n  let routes: ServerRoute[];\n  let serverMode: ServerMode;\n  let staticHandler: StaticHandler;\n  let errorHandler: HandleErrorFunction;\n  let _requestHandler: RequestHandler;\n\n  return async function requestHandler(request, initialContext) {\n    _build = typeof build === \"function\" ? await build() : build;\n\n    if (typeof build === \"function\") {\n      let derived = derive(_build, mode);\n      routes = derived.routes;\n      serverMode = derived.serverMode;\n      staticHandler = derived.staticHandler;\n      errorHandler = derived.errorHandler;\n      _requestHandler = derived.requestHandler;\n    } else if (\n      !routes ||\n      !serverMode ||\n      !staticHandler ||\n      !errorHandler ||\n      !_requestHandler\n    ) {\n      let derived = derive(_build, mode);\n      routes = derived.routes;\n      serverMode = derived.serverMode;\n      staticHandler = derived.staticHandler;\n      errorHandler = derived.errorHandler;\n      _requestHandler = derived.requestHandler;\n    }\n\n    return _requestHandler(request, initialContext);\n  };\n};\n\nasync function handleManifestRequest(\n  build: ServerBuild,\n  routes: ServerRoute[],\n  url: URL,\n) {\n  if (build.assets.version !== url.searchParams.get(\"version\")) {\n    return new Response(null, {\n      status: 204,\n      headers: {\n        \"X-Remix-Reload-Document\": \"true\",\n      },\n    });\n  }\n\n  let patches: Record<string, EntryRoute> = {};\n\n  if (url.searchParams.has(\"paths\")) {\n    let paths = new Set<string>();\n\n    // In addition to responding with the patches for the requested paths, we\n    // need to include patches for each partial path so that we pick up any\n    // pathless/index routes below ancestor segments.  So if we\n    // get a request for `/parent/child`, we need to look for a match on `/parent`\n    // so that if a `parent._index` route exists we return it so it's available\n    // for client side matching if the user routes back up to `/parent`.\n    // This is the same thing we do on initial load in <Scripts> via\n    // `getPartialManifest()`\n    let pathParam = url.searchParams.get(\"paths\") || \"\";\n    let requestedPaths = pathParam.split(\",\").filter(Boolean);\n    requestedPaths.forEach((path) => {\n      if (!path.startsWith(\"/\")) {\n        path = `/${path}`;\n      }\n      let segments = path.split(\"/\").slice(1);\n      segments.forEach((_, i) => {\n        let partialPath = segments.slice(0, i + 1).join(\"/\");\n        paths.add(`/${partialPath}`);\n      });\n    });\n\n    for (let path of paths) {\n      let matches = matchServerRoutes(routes, path, build.basename);\n      if (matches) {\n        for (let match of matches) {\n          let routeId = match.route.id;\n          let route = build.assets.routes[routeId];\n          if (route) {\n            patches[routeId] = route;\n          }\n        }\n      }\n    }\n\n    return Response.json(patches, {\n      headers: {\n        \"Cache-Control\": \"public, max-age=31536000, immutable\",\n      },\n    });\n  }\n\n  return new Response(\"Invalid Request\", { status: 400 });\n}\n\nasync function handleSingleFetchRequest(\n  serverMode: ServerMode,\n  build: ServerBuild,\n  staticHandler: StaticHandler,\n  request: Request,\n  handlerUrl: URL,\n  loadContext: AppLoadContext | RouterContextProvider,\n  handleError: (err: unknown) => void,\n): Promise<Response> {\n  let response =\n    request.method !== \"GET\"\n      ? await singleFetchAction(\n          build,\n          serverMode,\n          staticHandler,\n          request,\n          handlerUrl,\n          loadContext,\n          handleError,\n        )\n      : await singleFetchLoaders(\n          build,\n          serverMode,\n          staticHandler,\n          request,\n          handlerUrl,\n          loadContext,\n          handleError,\n        );\n\n  return response;\n}\n\nasync function handleDocumentRequest(\n  serverMode: ServerMode,\n  build: ServerBuild,\n  staticHandler: StaticHandler,\n  request: Request,\n  loadContext: AppLoadContext | RouterContextProvider,\n  handleError: (err: unknown) => void,\n  isSpaMode: boolean,\n  criticalCss?: CriticalCss,\n) {\n  try {\n    if (request.method === \"POST\") {\n      try {\n        throwIfPotentialCSRFAttack(\n          request.headers,\n          Array.isArray(build.allowedActionOrigins)\n            ? build.allowedActionOrigins\n            : [],\n        );\n      } catch (e) {\n        handleError(e);\n        return new Response(\"Bad Request\", { status: 400 });\n      }\n    }\n    let result = await staticHandler.query(request, {\n      requestContext: loadContext,\n      generateMiddlewareResponse: build.future.v8_middleware\n        ? async (query) => {\n            try {\n              let innerResult = await query(request);\n              if (!isResponse(innerResult)) {\n                innerResult = await renderHtml(innerResult, isSpaMode);\n              }\n              return innerResult;\n            } catch (error: unknown) {\n              handleError(error);\n              return new Response(null, { status: 500 });\n            }\n          }\n        : undefined,\n    });\n\n    if (!isResponse(result)) {\n      result = await renderHtml(result, isSpaMode);\n    }\n    return result;\n  } catch (error: unknown) {\n    handleError(error);\n    return new Response(null, { status: 500 });\n  }\n\n  async function renderHtml(context: StaticHandlerContext, isSpaMode: boolean) {\n    let headers = getDocumentHeaders(context, build);\n\n    // Skip response body for unsupported status codes\n    if (SERVER_NO_BODY_STATUS_CODES.has(context.statusCode)) {\n      return new Response(null, { status: context.statusCode, headers });\n    }\n\n    // Sanitize errors outside of development environments\n    if (context.errors) {\n      Object.values(context.errors).forEach((err) => {\n        // @ts-expect-error This is \"private\" from users but intended for internal use\n        if (!isRouteErrorResponse(err) || err.error) {\n          handleError(err);\n        }\n      });\n      context.errors = sanitizeErrors(context.errors, serverMode);\n    }\n\n    // Server UI state to send to the client.\n    // - When single fetch is enabled, this is streamed down via `serverHandoffStream`\n    // - Otherwise it's stringified into `serverHandoffString`\n    let state = {\n      loaderData: context.loaderData,\n      actionData: context.actionData,\n      errors: serializeErrors(context.errors, serverMode),\n    };\n    let baseServerHandoff: ServerHandoff = {\n      basename: build.basename,\n      future: build.future,\n      routeDiscovery: build.routeDiscovery,\n      ssr: build.ssr,\n      isSpaMode,\n    };\n    let entryContext: EntryContext = {\n      manifest: build.assets,\n      routeModules: createEntryRouteModules(build.routes),\n      staticHandlerContext: context,\n      criticalCss,\n      serverHandoffString: createServerHandoffString({\n        ...baseServerHandoff,\n        criticalCss,\n      }),\n      serverHandoffStream: encodeViaTurboStream(\n        state,\n        request.signal,\n        build.entry.module.streamTimeout,\n        serverMode,\n      ),\n      renderMeta: {},\n      future: build.future,\n      ssr: build.ssr,\n      routeDiscovery: build.routeDiscovery,\n      isSpaMode,\n      serializeError: (err) => serializeError(err, serverMode),\n    };\n\n    let handleDocumentRequestFunction = build.entry.module.default;\n    try {\n      return await handleDocumentRequestFunction(\n        request,\n        context.statusCode,\n        headers,\n        entryContext,\n        loadContext as MiddlewareEnabled extends true\n          ? RouterContextProvider\n          : AppLoadContext,\n      );\n    } catch (error: unknown) {\n      handleError(error);\n\n      let errorForSecondRender = error;\n\n      // If they threw a response, unwrap it into an ErrorResponse like we would\n      // have for a loader/action\n      if (isResponse(error)) {\n        try {\n          let data = await unwrapResponse(error);\n          errorForSecondRender = new ErrorResponseImpl(\n            error.status,\n            error.statusText,\n            data,\n          );\n        } catch (e) {\n          // If we can't unwrap the response - just leave it as-is\n        }\n      }\n\n      // Get a new StaticHandlerContext that contains the error at the right boundary\n      context = getStaticContextFromError(\n        staticHandler.dataRoutes,\n        context,\n        errorForSecondRender,\n      );\n\n      // Sanitize errors outside of development environments\n      if (context.errors) {\n        context.errors = sanitizeErrors(context.errors, serverMode);\n      }\n\n      // Get a new entryContext for the second render pass\n      // Server UI state to send to the client.\n      // - When single fetch is enabled, this is streamed down via `serverHandoffStream`\n      // - Otherwise it's stringified into `serverHandoffString`\n      let state = {\n        loaderData: context.loaderData,\n        actionData: context.actionData,\n        errors: serializeErrors(context.errors, serverMode),\n      };\n      entryContext = {\n        ...entryContext,\n        staticHandlerContext: context,\n        serverHandoffString: createServerHandoffString(baseServerHandoff),\n        serverHandoffStream: encodeViaTurboStream(\n          state,\n          request.signal,\n          build.entry.module.streamTimeout,\n          serverMode,\n        ),\n        renderMeta: {},\n      };\n\n      try {\n        return await handleDocumentRequestFunction(\n          request,\n          context.statusCode,\n          headers,\n          entryContext,\n          loadContext as MiddlewareEnabled extends true\n            ? RouterContextProvider\n            : AppLoadContext,\n        );\n      } catch (error: any) {\n        handleError(error);\n        return returnLastResortErrorResponse(error, serverMode);\n      }\n    }\n  }\n}\n\nasync function handleResourceRequest(\n  serverMode: ServerMode,\n  build: ServerBuild,\n  staticHandler: StaticHandler,\n  routeId: string,\n  request: Request,\n  loadContext: AppLoadContext | RouterContextProvider,\n  handleError: (err: unknown) => void,\n) {\n  try {\n    // Note we keep the routeId here to align with the Remix handling of\n    // resource routes which doesn't take ?index into account and just takes\n    // the leaf match\n    let result = await staticHandler.queryRoute(request, {\n      routeId,\n      requestContext: loadContext,\n      generateMiddlewareResponse: build.future.v8_middleware\n        ? async (queryRoute) => {\n            try {\n              let innerResult = await queryRoute(request);\n              return handleQueryRouteResult(innerResult);\n            } catch (error) {\n              return handleQueryRouteError(error);\n            }\n          }\n        : undefined,\n    });\n\n    return handleQueryRouteResult(result);\n  } catch (error: unknown) {\n    return handleQueryRouteError(error);\n  }\n\n  function handleQueryRouteResult(\n    result: Awaited<ReturnType<StaticHandler[\"queryRoute\"]>>,\n  ) {\n    if (isResponse(result)) {\n      return result;\n    }\n\n    if (typeof result === \"string\") {\n      return new Response(result);\n    }\n\n    return Response.json(result);\n  }\n\n  function handleQueryRouteError(error: unknown) {\n    if (isResponse(error)) {\n      return error;\n    }\n\n    if (isRouteErrorResponse(error)) {\n      handleError(error);\n      return errorResponseToJson(error, serverMode);\n    }\n\n    if (\n      error instanceof Error &&\n      error.message === \"Expected a response from queryRoute\"\n    ) {\n      let newError = new Error(\n        \"Expected a Response to be returned from resource route handler\",\n      );\n      handleError(newError);\n      return returnLastResortErrorResponse(newError, serverMode);\n    }\n\n    handleError(error);\n    return returnLastResortErrorResponse(error, serverMode);\n  }\n}\n\nfunction errorResponseToJson(\n  errorResponse: ErrorResponse,\n  serverMode: ServerMode,\n): Response {\n  return Response.json(\n    serializeError(\n      // @ts-expect-error This is \"private\" from users but intended for internal use\n      errorResponse.error || new Error(\"Unexpected Server Error\"),\n      serverMode,\n    ),\n    {\n      status: errorResponse.status,\n      statusText: errorResponse.statusText,\n    },\n  );\n}\n\nfunction returnLastResortErrorResponse(error: any, serverMode?: ServerMode) {\n  let message = \"Unexpected Server Error\";\n\n  if (serverMode !== ServerMode.Production) {\n    message += `\\n\\n${String(error)}`;\n  }\n\n  // Good grief folks, get your act together 😂!\n  return new Response(message, {\n    status: 500,\n    headers: {\n      \"Content-Type\": \"text/plain\",\n    },\n  });\n}\n\nfunction unwrapResponse(response: Response) {\n  let contentType = response.headers.get(\"Content-Type\");\n  // Check between word boundaries instead of startsWith() due to the last\n  // paragraph of https://httpwg.org/specs/rfc9110.html#field.content-type\n  return contentType && /\\bapplication\\/json\\b/.test(contentType)\n    ? response.body == null\n      ? null\n      : response.json()\n    : response.text();\n}\n"
  },
  {
    "path": "packages/react-router/lib/server-runtime/serverHandoff.ts",
    "content": "import type { CriticalCss, FutureConfig } from \"../dom/ssr/entry\";\nimport { escapeHtml } from \"../dom/ssr/markup\";\nimport type { ServerBuild } from \"./build\";\n\nexport type ServerHandoff = {\n  criticalCss?: CriticalCss;\n  basename: string | undefined;\n  future: FutureConfig;\n  ssr: boolean;\n  isSpaMode: boolean;\n  routeDiscovery: ServerBuild[\"routeDiscovery\"];\n};\n\nexport function createServerHandoffString(\n  serverHandoff: ServerHandoff,\n): string {\n  // Uses faster alternative of jsesc to escape data returned from the loaders.\n  // This string is inserted directly into the HTML in the `<Scripts>` element.\n  return escapeHtml(JSON.stringify(serverHandoff));\n}\n"
  },
  {
    "path": "packages/react-router/lib/server-runtime/sessions/cookieStorage.ts",
    "content": "import { createCookie, isCookie } from \"../cookies\";\nimport type {\n  SessionStorage,\n  SessionIdStorageStrategy,\n  SessionData,\n} from \"../sessions\";\nimport { warnOnceAboutSigningSessionCookie, createSession } from \"../sessions\";\n\ninterface CookieSessionStorageOptions {\n  /**\n   * The Cookie used to store the session data on the client, or options used\n   * to automatically create one.\n   */\n  cookie?: SessionIdStorageStrategy[\"cookie\"];\n}\n\n/**\n * Creates and returns a SessionStorage object that stores all session data\n * directly in the session cookie itself.\n *\n * This has the advantage that no database or other backend services are\n * needed, and can help to simplify some load-balanced scenarios. However, it\n * also has the limitation that serialized session data may not exceed the\n * browser's maximum cookie size. Trade-offs!\n */\nexport function createCookieSessionStorage<\n  Data = SessionData,\n  FlashData = Data,\n>({ cookie: cookieArg }: CookieSessionStorageOptions = {}): SessionStorage<\n  Data,\n  FlashData\n> {\n  let cookie = isCookie(cookieArg)\n    ? cookieArg\n    : createCookie(cookieArg?.name || \"__session\", cookieArg);\n\n  warnOnceAboutSigningSessionCookie(cookie);\n\n  return {\n    async getSession(cookieHeader, options) {\n      return createSession(\n        (cookieHeader && (await cookie.parse(cookieHeader, options))) || {},\n      );\n    },\n    async commitSession(session, options) {\n      let serializedCookie = await cookie.serialize(session.data, options);\n      if (serializedCookie.length > 4096) {\n        throw new Error(\n          \"Cookie length will exceed browser maximum. Length: \" +\n            serializedCookie.length,\n        );\n      }\n      return serializedCookie;\n    },\n    async destroySession(_session, options) {\n      return cookie.serialize(\"\", {\n        ...options,\n        maxAge: undefined,\n        expires: new Date(0),\n      });\n    },\n  };\n}\n"
  },
  {
    "path": "packages/react-router/lib/server-runtime/sessions/memoryStorage.ts",
    "content": "import type {\n  SessionData,\n  SessionStorage,\n  SessionIdStorageStrategy,\n  FlashSessionData,\n} from \"../sessions\";\nimport { createSessionStorage } from \"../sessions\";\n\ninterface MemorySessionStorageOptions {\n  /**\n   * The Cookie used to store the session id on the client, or options used\n   * to automatically create one.\n   */\n  cookie?: SessionIdStorageStrategy[\"cookie\"];\n}\n\n/**\n * Creates and returns a simple in-memory SessionStorage object, mostly useful\n * for testing and as a reference implementation.\n *\n * Note: This storage does not scale beyond a single process, so it is not\n * suitable for most production scenarios.\n */\nexport function createMemorySessionStorage<\n  Data = SessionData,\n  FlashData = Data,\n>({ cookie }: MemorySessionStorageOptions = {}): SessionStorage<\n  Data,\n  FlashData\n> {\n  let map = new Map<\n    string,\n    { data: FlashSessionData<Data, FlashData>; expires?: Date }\n  >();\n\n  return createSessionStorage({\n    cookie,\n    async createData(data, expires) {\n      let id = Math.random().toString(36).substring(2, 10);\n      map.set(id, { data, expires });\n      return id;\n    },\n    async readData(id) {\n      if (map.has(id)) {\n        let { data, expires } = map.get(id)!;\n\n        if (!expires || expires > new Date()) {\n          return data;\n        }\n\n        // Remove expired session data.\n        if (expires) map.delete(id);\n      }\n\n      return null;\n    },\n    async updateData(id, data, expires) {\n      map.set(id, { data, expires });\n    },\n    async deleteData(id) {\n      map.delete(id);\n    },\n  });\n}\n"
  },
  {
    "path": "packages/react-router/lib/server-runtime/sessions.ts",
    "content": "import type { ParseOptions, SerializeOptions } from \"cookie\";\n\nimport type { Cookie, CookieOptions } from \"./cookies\";\nimport { createCookie, isCookie } from \"./cookies\";\nimport { warnOnce } from \"./warnings\";\n\n/**\n * An object of name/value pairs to be used in the session.\n */\nexport interface SessionData {\n  [name: string]: any;\n}\n\n/**\n * Session persists data across HTTP requests.\n *\n * @see https://reactrouter.com/explanation/sessions-and-cookies#sessions\n */\nexport interface Session<Data = SessionData, FlashData = Data> {\n  /**\n   * A unique identifier for this session.\n   *\n   * Note: This will be the empty string for newly created sessions and\n   * sessions that are not backed by a database (i.e. cookie-based sessions).\n   */\n  readonly id: string;\n\n  /**\n   * The raw data contained in this session.\n   *\n   * This is useful mostly for SessionStorage internally to access the raw\n   * session data to persist.\n   */\n  readonly data: FlashSessionData<Data, FlashData>;\n\n  /**\n   * Returns `true` if the session has a value for the given `name`, `false`\n   * otherwise.\n   */\n  has(name: (keyof Data | keyof FlashData) & string): boolean;\n\n  /**\n   * Returns the value for the given `name` in this session.\n   */\n  get<Key extends (keyof Data | keyof FlashData) & string>(\n    name: Key,\n  ):\n    | (Key extends keyof Data ? Data[Key] : undefined)\n    | (Key extends keyof FlashData ? FlashData[Key] : undefined)\n    | undefined;\n\n  /**\n   * Sets a value in the session for the given `name`.\n   */\n  set<Key extends keyof Data & string>(name: Key, value: Data[Key]): void;\n\n  /**\n   * Sets a value in the session that is only valid until the next `get()`.\n   * This can be useful for temporary values, like error messages.\n   */\n  flash<Key extends keyof FlashData & string>(\n    name: Key,\n    value: FlashData[Key],\n  ): void;\n\n  /**\n   * Removes a value from the session.\n   */\n  unset(name: keyof Data & string): void;\n}\n\nexport type FlashSessionData<Data, FlashData> = Partial<\n  Data & {\n    [Key in keyof FlashData as FlashDataKey<Key & string>]: FlashData[Key];\n  }\n>;\ntype FlashDataKey<Key extends string> = `__flash_${Key}__`;\nfunction flash<Key extends string>(name: Key): FlashDataKey<Key> {\n  return `__flash_${name}__`;\n}\n\nexport type CreateSessionFunction = <Data = SessionData, FlashData = Data>(\n  initialData?: Data,\n  id?: string,\n) => Session<Data, FlashData>;\n\n/**\n * Creates a new Session object.\n *\n * Note: This function is typically not invoked directly by application code.\n * Instead, use a `SessionStorage` object's `getSession` method.\n */\nexport const createSession: CreateSessionFunction = <\n  Data = SessionData,\n  FlashData = Data,\n>(\n  initialData: Partial<Data> = {},\n  id = \"\",\n): Session<Data, FlashData> => {\n  let map = new Map(Object.entries(initialData)) as Map<\n    keyof Data | FlashDataKey<keyof FlashData & string>,\n    any\n  >;\n\n  return {\n    get id() {\n      return id;\n    },\n    get data() {\n      return Object.fromEntries(map) as FlashSessionData<Data, FlashData>;\n    },\n    has(name) {\n      return (\n        map.has(name as keyof Data) ||\n        map.has(flash(name as keyof FlashData & string))\n      );\n    },\n    get(name) {\n      if (map.has(name as keyof Data)) return map.get(name as keyof Data);\n\n      let flashName = flash(name as keyof FlashData & string);\n      if (map.has(flashName)) {\n        let value = map.get(flashName);\n        map.delete(flashName);\n        return value;\n      }\n\n      return undefined;\n    },\n    set(name, value) {\n      map.set(name, value);\n    },\n    flash(name, value) {\n      map.set(flash(name), value);\n    },\n    unset(name) {\n      map.delete(name);\n    },\n  };\n};\n\nexport type IsSessionFunction = (object: any) => object is Session;\n\n/**\n * Returns true if an object is a React Router session.\n *\n * @see https://reactrouter.com/api/utils/isSession\n */\nexport const isSession: IsSessionFunction = (object): object is Session => {\n  return (\n    object != null &&\n    typeof object.id === \"string\" &&\n    typeof object.data !== \"undefined\" &&\n    typeof object.has === \"function\" &&\n    typeof object.get === \"function\" &&\n    typeof object.set === \"function\" &&\n    typeof object.flash === \"function\" &&\n    typeof object.unset === \"function\"\n  );\n};\n\n/**\n * SessionStorage stores session data between HTTP requests and knows how to\n * parse and create cookies.\n *\n * A SessionStorage creates Session objects using a `Cookie` header as input.\n * Then, later it generates the `Set-Cookie` header to be used in the response.\n */\nexport interface SessionStorage<Data = SessionData, FlashData = Data> {\n  /**\n   * Parses a Cookie header from a HTTP request and returns the associated\n   * Session. If there is no session associated with the cookie, this will\n   * return a new Session with no data.\n   */\n  getSession: (\n    cookieHeader?: string | null,\n    options?: ParseOptions,\n  ) => Promise<Session<Data, FlashData>>;\n\n  /**\n   * Stores all data in the Session and returns the Set-Cookie header to be\n   * used in the HTTP response.\n   */\n  commitSession: (\n    session: Session<Data, FlashData>,\n    options?: SerializeOptions,\n  ) => Promise<string>;\n\n  /**\n   * Deletes all data associated with the Session and returns the Set-Cookie\n   * header to be used in the HTTP response.\n   */\n  destroySession: (\n    session: Session<Data, FlashData>,\n    options?: SerializeOptions,\n  ) => Promise<string>;\n}\n\n/**\n * SessionIdStorageStrategy is designed to allow anyone to easily build their\n * own SessionStorage using `createSessionStorage(strategy)`.\n *\n * This strategy describes a common scenario where the session id is stored in\n * a cookie but the actual session data is stored elsewhere, usually in a\n * database or on disk. A set of create, read, update, and delete operations\n * are provided for managing the session data.\n */\nexport interface SessionIdStorageStrategy<\n  Data = SessionData,\n  FlashData = Data,\n> {\n  /**\n   * The Cookie used to store the session id, or options used to automatically\n   * create one.\n   */\n  cookie?: Cookie | (CookieOptions & { name?: string });\n\n  /**\n   * Creates a new record with the given data and returns the session id.\n   */\n  createData: (\n    data: FlashSessionData<Data, FlashData>,\n    expires?: Date,\n  ) => Promise<string>;\n\n  /**\n   * Returns data for a given session id, or `null` if there isn't any.\n   */\n  readData: (id: string) => Promise<FlashSessionData<Data, FlashData> | null>;\n\n  /**\n   * Updates data for the given session id.\n   */\n  updateData: (\n    id: string,\n    data: FlashSessionData<Data, FlashData>,\n    expires?: Date,\n  ) => Promise<void>;\n\n  /**\n   * Deletes data for a given session id from the data store.\n   */\n  deleteData: (id: string) => Promise<void>;\n}\n\n/**\n * Creates a SessionStorage object using a SessionIdStorageStrategy.\n *\n * Note: This is a low-level API that should only be used if none of the\n * existing session storage options meet your requirements.\n */\nexport function createSessionStorage<Data = SessionData, FlashData = Data>({\n  cookie: cookieArg,\n  createData,\n  readData,\n  updateData,\n  deleteData,\n}: SessionIdStorageStrategy<Data, FlashData>): SessionStorage<Data, FlashData> {\n  let cookie = isCookie(cookieArg)\n    ? cookieArg\n    : createCookie(cookieArg?.name || \"__session\", cookieArg);\n\n  warnOnceAboutSigningSessionCookie(cookie);\n\n  return {\n    async getSession(cookieHeader, options) {\n      let id = cookieHeader && (await cookie.parse(cookieHeader, options));\n      let data = id && (await readData(id));\n      return createSession(data || {}, id || \"\");\n    },\n    async commitSession(session, options) {\n      let { id, data } = session;\n      let expires =\n        options?.maxAge != null\n          ? new Date(Date.now() + options.maxAge * 1000)\n          : options?.expires != null\n            ? options.expires\n            : cookie.expires;\n\n      if (id) {\n        await updateData(id, data, expires);\n      } else {\n        id = await createData(data, expires);\n      }\n\n      return cookie.serialize(id, options);\n    },\n    async destroySession(session, options) {\n      await deleteData(session.id);\n      return cookie.serialize(\"\", {\n        ...options,\n        maxAge: undefined,\n        expires: new Date(0),\n      });\n    },\n  };\n}\n\nexport function warnOnceAboutSigningSessionCookie(cookie: Cookie) {\n  warnOnce(\n    cookie.isSigned,\n    `The \"${cookie.name}\" cookie is not signed, but session cookies should be ` +\n      `signed to prevent tampering on the client before they are sent back to the ` +\n      `server. See https://reactrouter.com/explanation/sessions-and-cookies#signing-cookies ` +\n      `for more information.`,\n  );\n}\n"
  },
  {
    "path": "packages/react-router/lib/server-runtime/single-fetch.ts",
    "content": "import { encode } from \"../../vendor/turbo-stream-v2/turbo-stream\";\nimport type { StaticHandler, StaticHandlerContext } from \"../router/router\";\nimport { isRedirectStatusCode, isResponse } from \"../router/router\";\nimport type { RouterContextProvider } from \"../router/utils\";\nimport {\n  isRouteErrorResponse,\n  ErrorResponseImpl,\n  data as routerData,\n  stripBasename,\n} from \"../router/utils\";\nimport type {\n  SingleFetchRedirectResult,\n  SingleFetchResult,\n  SingleFetchResults,\n} from \"../dom/ssr/single-fetch\";\nimport {\n  NO_BODY_STATUS_CODES,\n  SINGLE_FETCH_REDIRECT_STATUS,\n  SingleFetchRedirectSymbol,\n} from \"../dom/ssr/single-fetch\";\nimport type { AppLoadContext } from \"./data\";\nimport { sanitizeError, sanitizeErrors } from \"./errors\";\nimport { ServerMode } from \"./mode\";\nimport { getDocumentHeaders } from \"./headers\";\nimport type { ServerBuild } from \"./build\";\nimport { throwIfPotentialCSRFAttack } from \"../actions\";\n\n// Add 304 for server side - that is not included in the client side logic\n// because the browser should fill those responses with the cached data\n// https://datatracker.ietf.org/doc/html/rfc9110#name-304-not-modified\nexport const SERVER_NO_BODY_STATUS_CODES = new Set([\n  ...NO_BODY_STATUS_CODES,\n  304,\n]);\n\nexport async function singleFetchAction(\n  build: ServerBuild,\n  serverMode: ServerMode,\n  staticHandler: StaticHandler,\n  request: Request,\n  handlerUrl: URL,\n  loadContext: AppLoadContext | RouterContextProvider,\n  handleError: (err: unknown) => void,\n): Promise<Response> {\n  try {\n    try {\n      throwIfPotentialCSRFAttack(\n        request.headers,\n        Array.isArray(build.allowedActionOrigins)\n          ? build.allowedActionOrigins\n          : [],\n      );\n    } catch (e) {\n      return handleQueryError(new Error(\"Bad Request\"), 400);\n    }\n\n    let handlerRequest = new Request(handlerUrl, {\n      method: request.method,\n      body: request.body,\n      headers: request.headers,\n      signal: request.signal,\n      ...(request.body ? { duplex: \"half\" } : undefined),\n    });\n\n    let result = await staticHandler.query(handlerRequest, {\n      requestContext: loadContext,\n      skipLoaderErrorBubbling: true,\n      skipRevalidation: true,\n      generateMiddlewareResponse: build.future.v8_middleware\n        ? async (query) => {\n            try {\n              let innerResult = await query(handlerRequest);\n              return handleQueryResult(innerResult);\n            } catch (error) {\n              return handleQueryError(error);\n            }\n          }\n        : undefined,\n    });\n\n    return handleQueryResult(result);\n  } catch (error) {\n    return handleQueryError(error);\n  }\n\n  function handleQueryResult(\n    result: Awaited<ReturnType<StaticHandler[\"query\"]>>,\n  ) {\n    return isResponse(result) ? result : staticContextToResponse(result);\n  }\n\n  function handleQueryError(error: unknown, status = 500) {\n    handleError(error);\n    // These should only be internal remix errors, no need to deal with responseStubs\n    return generateSingleFetchResponse(request, build, serverMode, {\n      result: { error },\n      headers: new Headers(),\n      status,\n    });\n  }\n\n  function staticContextToResponse(context: StaticHandlerContext) {\n    let headers = getDocumentHeaders(context, build);\n\n    if (isRedirectStatusCode(context.statusCode) && headers.has(\"Location\")) {\n      return new Response(null, { status: context.statusCode, headers });\n    }\n\n    // Sanitize errors outside of development environments\n    if (context.errors) {\n      Object.values(context.errors).forEach((err) => {\n        // @ts-expect-error This is \"private\" from users but intended for internal use\n        if (!isRouteErrorResponse(err) || err.error) {\n          handleError(err);\n        }\n      });\n      context.errors = sanitizeErrors(context.errors, serverMode);\n    }\n\n    let singleFetchResult: SingleFetchResult;\n    if (context.errors) {\n      singleFetchResult = { error: Object.values(context.errors)[0] };\n    } else {\n      singleFetchResult = {\n        data: Object.values(context.actionData || {})[0],\n      };\n    }\n\n    return generateSingleFetchResponse(request, build, serverMode, {\n      result: singleFetchResult,\n      headers,\n      status: context.statusCode,\n    });\n  }\n}\n\nexport async function singleFetchLoaders(\n  build: ServerBuild,\n  serverMode: ServerMode,\n  staticHandler: StaticHandler,\n  request: Request,\n  handlerUrl: URL,\n  loadContext: AppLoadContext | RouterContextProvider,\n  handleError: (err: unknown) => void,\n): Promise<Response> {\n  let routesParam = new URL(request.url).searchParams.get(\"_routes\");\n  let loadRouteIds = routesParam ? new Set(routesParam.split(\",\")) : null;\n\n  try {\n    let handlerRequest = new Request(handlerUrl, {\n      headers: request.headers,\n      signal: request.signal,\n    });\n\n    let result = await staticHandler.query(handlerRequest, {\n      requestContext: loadContext,\n      filterMatchesToLoad: (m) => !loadRouteIds || loadRouteIds.has(m.route.id),\n      skipLoaderErrorBubbling: true,\n      generateMiddlewareResponse: build.future.v8_middleware\n        ? async (query) => {\n            try {\n              let innerResult = await query(handlerRequest);\n              return handleQueryResult(innerResult);\n            } catch (error) {\n              return handleQueryError(error);\n            }\n          }\n        : undefined,\n    });\n\n    return handleQueryResult(result);\n  } catch (error: unknown) {\n    return handleQueryError(error);\n  }\n\n  // Handle the query() result - either inside stream() with middleware enabled\n  // or after query() without\n  function handleQueryResult(result: StaticHandlerContext | Response) {\n    return isResponse(result) ? result : staticContextToResponse(result);\n  }\n\n  // Handle any thrown errors from query() result - either inside stream() with\n  // middleware enabled or after query() without\n  function handleQueryError(error: unknown) {\n    handleError(error);\n    // These should only be internal remix errors, no need to deal with responseStubs\n    return generateSingleFetchResponse(request, build, serverMode, {\n      result: { error },\n      headers: new Headers(),\n      status: 500,\n    });\n  }\n\n  function staticContextToResponse(context: StaticHandlerContext) {\n    let headers = getDocumentHeaders(context, build);\n\n    if (isRedirectStatusCode(context.statusCode) && headers.has(\"Location\")) {\n      return new Response(null, { status: context.statusCode, headers });\n    }\n\n    // Sanitize errors outside of development environments\n    if (context.errors) {\n      Object.values(context.errors).forEach((err) => {\n        // @ts-expect-error This is \"private\" from users but intended for internal use\n        if (!isRouteErrorResponse(err) || err.error) {\n          handleError(err);\n        }\n      });\n      context.errors = sanitizeErrors(context.errors, serverMode);\n    }\n\n    // Aggregate results based on the matches we intended to load since we get\n    // `null` values back in `context.loaderData` for routes we didn't load\n    let results: SingleFetchResults = {};\n    let loadedMatches = new Set(\n      context.matches\n        .filter((m) =>\n          loadRouteIds ? loadRouteIds.has(m.route.id) : m.route.loader != null,\n        )\n        .map((m) => m.route.id),\n    );\n\n    if (context.errors) {\n      for (let [id, error] of Object.entries(context.errors)) {\n        results[id] = { error };\n      }\n    }\n    for (let [id, data] of Object.entries(context.loaderData)) {\n      if (!(id in results) && loadedMatches.has(id)) {\n        results[id] = { data };\n      }\n    }\n\n    return generateSingleFetchResponse(request, build, serverMode, {\n      result: results,\n      headers,\n      status: context.statusCode,\n    });\n  }\n}\n\nfunction generateSingleFetchResponse(\n  request: Request,\n  build: ServerBuild,\n  serverMode: ServerMode,\n  {\n    result,\n    headers,\n    status,\n  }: {\n    result: SingleFetchResult | SingleFetchResults;\n    headers: Headers;\n    status: number;\n  },\n) {\n  // Mark all successful responses with a header so we can identify in-flight\n  // network errors that are missing this header\n  let resultHeaders = new Headers(headers);\n  resultHeaders.set(\"X-Remix-Response\", \"yes\");\n\n  // Skip response body for unsupported status codes\n  if (SERVER_NO_BODY_STATUS_CODES.has(status)) {\n    return new Response(null, { status, headers: resultHeaders });\n  }\n\n  // We use a less-descriptive `text/x-script` here instead of something like\n  // `text/x-turbo` to enable compression when deployed via Cloudflare.  See:\n  //  - https://github.com/remix-run/remix/issues/9884\n  //  - https://developers.cloudflare.com/speed/optimization/content/brotli/content-compression/\n  resultHeaders.set(\"Content-Type\", \"text/x-script\");\n\n  // Remove Content-Length because node:http will truncate the response body\n  // to match the Content-Length header, which can result in incomplete data\n  // if the actual encoded body is longer.\n  // https://nodejs.org/api/http.html#class-httpclientrequest\n  resultHeaders.delete(\"Content-Length\");\n\n  return new Response(\n    encodeViaTurboStream(\n      result,\n      request.signal,\n      build.entry.module.streamTimeout,\n      serverMode,\n    ),\n    {\n      status: status || 200,\n      headers: resultHeaders,\n    },\n  );\n}\n\nexport function generateSingleFetchRedirectResponse(\n  redirectResponse: Response,\n  request: Request,\n  build: ServerBuild,\n  serverMode: ServerMode,\n) {\n  let redirect = getSingleFetchRedirect(\n    redirectResponse.status,\n    redirectResponse.headers,\n    build.basename,\n  );\n\n  let headers = new Headers(redirectResponse.headers);\n  headers.delete(\"Location\");\n  headers.set(\"Content-Type\", \"text/x-script\");\n\n  return generateSingleFetchResponse(request, build, serverMode, {\n    result:\n      request.method === \"GET\"\n        ? { [SingleFetchRedirectSymbol]: redirect }\n        : redirect,\n    headers,\n    status: SINGLE_FETCH_REDIRECT_STATUS,\n  });\n}\n\nexport function getSingleFetchRedirect(\n  status: number,\n  headers: Headers,\n  basename: string | undefined,\n): SingleFetchRedirectResult {\n  let redirect = headers.get(\"Location\")!;\n\n  if (basename) {\n    redirect = stripBasename(redirect, basename) || redirect;\n  }\n\n  return {\n    redirect,\n    status,\n    revalidate:\n      // Technically X-Remix-Revalidate isn't needed here - that was an implementation\n      // detail of ?_data requests as our way to tell the front end to revalidate when\n      // we didn't have a response body to include that information in.\n      // With single fetch, we tell the front end via this revalidate boolean field.\n      // However, we're respecting it for now because it may be something folks have\n      // used in their own responses\n      // TODO(v3): Consider removing or making this official public API\n      headers.has(\"X-Remix-Revalidate\") || headers.has(\"Set-Cookie\"),\n    reload: headers.has(\"X-Remix-Reload-Document\"),\n    replace: headers.has(\"X-Remix-Replace\"),\n  };\n}\n\nexport type Serializable =\n  | undefined\n  | null\n  | boolean\n  | string\n  | symbol\n  | number\n  | Array<Serializable>\n  | { [key: PropertyKey]: Serializable }\n  | bigint\n  | Date\n  | URL\n  | RegExp\n  | Error\n  | Map<Serializable, Serializable>\n  | Set<Serializable>\n  | Promise<Serializable>;\n\nexport function data(value: Serializable, init?: number | ResponseInit) {\n  return routerData(value, init);\n}\n\n// Note: If you change this function please change the corresponding\n// decodeViaTurboStream function in server-runtime\nexport function encodeViaTurboStream(\n  data: any,\n  requestSignal: AbortSignal,\n  streamTimeout: number | undefined,\n  serverMode: ServerMode,\n) {\n  let controller = new AbortController();\n  // How long are we willing to wait for all of the promises in `data` to resolve\n  // before timing out?  We default this to 50ms shorter than the default value\n  // of 5000ms we had in `ABORT_DELAY` in Remix v2 that folks may still be using\n  // in RR v7 so that once we reject we have time to flush the rejections down\n  // through React's rendering stream before we call `abort()` on that.  If the\n  // user provides their own it's up to them to decouple the aborting of the\n  // stream from the aborting of React's `renderToPipeableStream`\n  let timeoutId = setTimeout(\n    () => controller.abort(new Error(\"Server Timeout\")),\n    typeof streamTimeout === \"number\" ? streamTimeout : 4950,\n  );\n\n  let clearStreamTimeout = () => clearTimeout(timeoutId);\n\n  requestSignal.addEventListener(\"abort\", clearStreamTimeout);\n\n  return encode(data, {\n    signal: controller.signal,\n    onComplete: clearStreamTimeout,\n    plugins: [\n      (value) => {\n        // Even though we sanitized errors on context.errors prior to responding,\n        // we still need to handle this for any deferred data that rejects with an\n        // Error - as those will not be sanitized yet\n        if (value instanceof Error) {\n          let { name, message, stack } =\n            serverMode === ServerMode.Production\n              ? sanitizeError(value, serverMode)\n              : value;\n          return [\"SanitizedError\", name, message, stack];\n        }\n\n        if (value instanceof ErrorResponseImpl) {\n          let { data, status, statusText } = value;\n          return [\"ErrorResponse\", data, status, statusText];\n        }\n\n        if (\n          value &&\n          typeof value === \"object\" &&\n          SingleFetchRedirectSymbol in value\n        ) {\n          return [\"SingleFetchRedirect\", value[SingleFetchRedirectSymbol]];\n        }\n      },\n    ],\n    postPlugins: [\n      (value) => {\n        if (!value) return;\n        if (typeof value !== \"object\") return;\n\n        return [\n          \"SingleFetchClassInstance\",\n          Object.fromEntries(Object.entries(value)),\n        ];\n      },\n      () => [\"SingleFetchFallback\"],\n    ],\n  });\n}\n"
  },
  {
    "path": "packages/react-router/lib/server-runtime/warnings.ts",
    "content": "const alreadyWarned: { [message: string]: boolean } = {};\n\nexport function warnOnce(condition: boolean, message: string): void {\n  if (!condition && !alreadyWarned[message]) {\n    alreadyWarned[message] = true;\n    console.warn(message);\n  }\n}\n"
  },
  {
    "path": "packages/react-router/lib/types/future.ts",
    "content": "/**\n * An augmentable interface users can modify in their app-code to opt into\n * future-flag-specific types\n */\nexport interface Future {\n  // We list the potential fields here in comments strictly for clarity.\n  // They will be generated by the react-router/dev/typegen/generate.ts module\n  //\n  // v8_middleware: boolean\n}\n\n// prettier-ignore\nexport type MiddlewareEnabled =\n  Future extends { v8_middleware: infer T extends boolean; } ? T : false\n"
  },
  {
    "path": "packages/react-router/lib/types/internal.ts",
    "content": "export type { GetAnnotations } from \"./route-module-annotations\";\n\nimport type { Params } from \"./params\";\nimport type { RouteFiles } from \"./register\";\nimport type { GetLoaderData, GetActionData } from \"./route-data\";\nimport type { RouteModule } from \"./route-module.ts\";\n\nexport type GetInfo<T extends { file: keyof RouteFiles; module: RouteModule }> =\n  {\n    params: Params<T[\"file\"]>;\n    loaderData: GetLoaderData<T[\"module\"]>;\n    actionData: GetActionData<T[\"module\"]>;\n  };\n"
  },
  {
    "path": "packages/react-router/lib/types/params.ts",
    "content": "import type { Pages, RouteFiles } from \"./register\";\nimport type { Normalize } from \"./utils\";\n\nexport type Params<RouteFile extends keyof RouteFiles> = Normalize<\n  Pages[RouteFiles[RouteFile][\"page\"]][\"params\"]\n>;\n"
  },
  {
    "path": "packages/react-router/lib/types/register.ts",
    "content": "import type { RouteModule } from \"./route-module\";\n\n/**\n * Apps can use this interface to \"register\" app-wide types for React Router via interface declaration merging and module augmentation.\n * React Router should handle this for you via type generation.\n *\n * For more on declaration merging and module augmentation, see https://www.typescriptlang.org/docs/handbook/declaration-merging.html#module-augmentation .\n */\nexport interface Register {\n  // pages\n  // routeFiles\n  // routeModules\n}\n\n// pages\ntype AnyParams = Record<string, string | undefined>;\ntype AnyPages = Record<string, { params: AnyParams }>;\nexport type Pages = Register extends {\n  pages: infer Registered extends AnyPages;\n}\n  ? Registered\n  : AnyPages;\n\n// route files\ntype AnyRouteFiles = Record<string, { id: string; page: string }>;\nexport type RouteFiles = Register extends {\n  routeFiles: infer Registered extends AnyRouteFiles;\n}\n  ? Registered\n  : AnyRouteFiles;\n\ntype AnyRouteModules = Record<string, RouteModule>;\nexport type RouteModules = Register extends {\n  routeModules: infer Registered extends AnyRouteModules;\n}\n  ? Registered\n  : AnyRouteModules;\n"
  },
  {
    "path": "packages/react-router/lib/types/route-data.ts",
    "content": "import type {\n  ClientLoaderFunctionArgs,\n  ClientActionFunctionArgs,\n} from \"../dom/ssr/routeModules\";\nimport type {\n  DataWithResponseInit,\n  RouterContextProvider,\n} from \"../router/utils\";\nimport type { Serializable } from \"../server-runtime/single-fetch\";\nimport type { AppLoadContext } from \"../server-runtime/data\";\n\nimport type { MiddlewareEnabled } from \"./future\";\nimport type { RouteModule } from \"./route-module\";\nimport type { unstable_SerializesTo } from \"./serializes-to\";\nimport type { Equal, Expect, Func, IsAny, Pretty } from \"./utils\";\n\n// prettier-ignore\ntype Serialize<T> =\n  // If type has a `SerializesTo` brand, use that type\n  T extends unstable_SerializesTo<infer To> ? To :\n\n  // Then, let type stay as-is if its already serializable...\n  T extends Serializable ? T :\n\n  // ...then don't allow functions to be serialized...\n  T extends (...args: any[]) => unknown ? undefined :\n\n  // ...lastly handle inner types for all container types allowed by `turbo-stream`\n\n  // Promise\n  T extends Promise<infer U> ? Promise<Serialize<U>> :\n\n  // Map & Set\n  T extends Map<infer K, infer V> ? Map<Serialize<K>, Serialize<V>> :\n  T extends ReadonlyMap<infer K, infer V> ? ReadonlyMap<Serialize<K>, Serialize<V>> :\n  T extends Set<infer U> ? Set<Serialize<U>> :\n  T extends ReadonlySet<infer U> ? ReadonlySet<Serialize<U>> :\n\n  // Array\n  T extends [] ? [] :\n  T extends readonly [infer F, ...infer R] ? [Serialize<F>, ...Serialize<R>] :\n  T extends Array<infer U> ? Array<Serialize<U>> :\n  T extends readonly unknown[] ? readonly Serialize<T[number]>[] :\n\n  // Record\n  T extends Record<any, any> ? {[K in keyof T]: Serialize<T[K]>} :\n\n  undefined\n\ntype VoidToUndefined<T> = Equal<T, void> extends true ? undefined : T;\n\n// prettier-ignore\ntype DataFrom<T> =\n  IsAny<T> extends true ? undefined :\n  T extends Func ? VoidToUndefined<Awaited<ReturnType<T>>> :\n  undefined\n\n// prettier-ignore\ntype ClientData<T> =\n  T extends Response ? never :\n  T extends DataWithResponseInit<infer U> ? U :\n  T\n\n// prettier-ignore\ntype ServerData<T> =\n  T extends Response ? never :\n  T extends DataWithResponseInit<infer U> ? Serialize<U> :\n  Serialize<T>\n\nexport type ServerDataFrom<T> = ServerData<DataFrom<T>>;\nexport type ClientDataFrom<T> = ClientData<DataFrom<T>>;\n\nexport type ClientDataFunctionArgs<Params> = {\n  /**\n   * A {@link https://developer.mozilla.org/en-US/docs/Web/API/Request Fetch Request instance} which you can use to read the URL, the method, the \"content-type\" header, and the request body from the request.\n   *\n   * @note Because client data functions are called before a network request is made, the Request object does not include the headers which the browser automatically adds. React Router infers the \"content-type\" header from the enc-type of the form that performed the submission.\n   **/\n  request: Request;\n  /**\n   * {@link https://reactrouter.com/start/framework/routing#dynamic-segments Dynamic route params} for the current route.\n   * @example\n   * // app/routes.ts\n   * route(\"teams/:teamId\", \"./team.tsx\"),\n   *\n   * // app/team.tsx\n   * export function clientLoader({\n   *   params,\n   * }: Route.ClientLoaderArgs) {\n   *   params.teamId;\n   *   //        ^ string\n   * }\n   **/\n  params: Params;\n  /**\n   * Matched un-interpolated route pattern for the current path (i.e., /blog/:slug).\n   * Mostly useful as a identifier to aggregate on for logging/tracing/etc.\n   */\n  unstable_pattern: string;\n  /**\n   * When `future.v8_middleware` is not enabled, this is undefined.\n   *\n   * When `future.v8_middleware` is enabled, this is an instance of\n   * `RouterContextProvider` and can be used to access context values\n   * from your route middlewares.  You may pass in initial context values in your\n   * `<HydratedRouter getContext>` prop\n   */\n  context: Readonly<RouterContextProvider>;\n};\n\nexport type ServerDataFunctionArgs<Params> = {\n  /** A {@link https://developer.mozilla.org/en-US/docs/Web/API/Request Fetch Request instance} which you can use to read the url, method, headers (such as cookies), and request body from the request. */\n  request: Request;\n  /**\n   * {@link https://reactrouter.com/start/framework/routing#dynamic-segments Dynamic route params} for the current route.\n   * @example\n   * // app/routes.ts\n   * route(\"teams/:teamId\", \"./team.tsx\"),\n   *\n   * // app/team.tsx\n   * export function loader({\n   *   params,\n   * }: Route.LoaderArgs) {\n   *   params.teamId;\n   *   //        ^ string\n   * }\n   **/\n  params: Params;\n  /**\n   * Matched un-interpolated route pattern for the current path (i.e., /blog/:slug).\n   * Mostly useful as a identifier to aggregate on for logging/tracing/etc.\n   */\n  unstable_pattern: string;\n  /**\n   * Without `future.v8_middleware` enabled, this is the context passed in\n   * to your server adapter's `getLoadContext` function. It's a way to bridge the\n   * gap between the adapter's request/response API with your React Router app.\n   * It is only applicable if you are using a custom server adapter.\n   *\n   * With `future.v8_middleware` enabled, this is an instance of\n   * `RouterContextProvider` and can be used for type-safe access to\n   * context value set in your route middlewares.  If you are using a custom\n   * server adapter, you may provide an initial set of context values from your\n   * `getLoadContext` function.\n   */\n  context: MiddlewareEnabled extends true\n    ? Readonly<RouterContextProvider>\n    : AppLoadContext;\n};\n\nexport type SerializeFrom<T> = T extends (...args: infer Args) => unknown\n  ? Args extends [\n      | ClientLoaderFunctionArgs\n      | ClientActionFunctionArgs\n      | ClientDataFunctionArgs<unknown>,\n    ]\n    ? ClientDataFrom<T>\n    : ServerDataFrom<T>\n  : T;\n\ntype IsDefined<T> = Equal<T, undefined> extends true ? false : true;\n\n// prettier-ignore\ntype IsHydrate<ClientLoader> =\n  ClientLoader extends { hydrate: true } ? true :\n  ClientLoader extends { hydrate: false } ? false :\n  false\n\nexport type GetLoaderData<T extends RouteModule> = _DataLoaderData<\n  ServerDataFrom<T[\"loader\"]>,\n  ClientDataFrom<T[\"clientLoader\"]>,\n  IsHydrate<T[\"clientLoader\"]>,\n  T extends { HydrateFallback: Func } ? true : false\n>;\n\n// prettier-ignore\ntype _DataLoaderData<\n  ServerLoaderData,\n  ClientLoaderData,\n  ClientLoaderHydrate extends boolean,\n  HasHydrateFallback\n> =\n  [HasHydrateFallback, ClientLoaderHydrate] extends [true, true] ?\n    IsDefined<ClientLoaderData> extends true ? ClientLoaderData :\n    undefined\n  :\n  [IsDefined<ClientLoaderData>, IsDefined<ServerLoaderData>] extends [true, true] ? ServerLoaderData | ClientLoaderData :\n  IsDefined<ClientLoaderData> extends true ? ClientLoaderData :\n  IsDefined<ServerLoaderData> extends true ? ServerLoaderData :\n  undefined\n\nexport type GetActionData<T extends RouteModule> = _DataActionData<\n  ServerDataFrom<T[\"action\"]>,\n  ClientDataFrom<T[\"clientAction\"]>\n>;\n\n// prettier-ignore\ntype _DataActionData<ServerActionData, ClientActionData> = Awaited<\n  [IsDefined<ServerActionData>, IsDefined<ClientActionData>] extends [true, true] ? ServerActionData | ClientActionData :\n  IsDefined<ClientActionData> extends true ? ClientActionData :\n  IsDefined<ServerActionData> extends true ? ServerActionData :\n  undefined\n>\n\ntype __tests = [\n  // ServerDataFrom\n  Expect<Equal<ServerDataFrom<any>, undefined>>,\n  Expect<\n    Equal<\n      ServerDataFrom<\n        () => {\n          a: string;\n          b: Date;\n          c: () => boolean;\n          d: unstable_SerializesTo<number>;\n        }\n      >,\n      { a: string; b: Date; c: undefined; d: number }\n    >\n  >,\n  Expect<\n    Equal<\n      Pretty<\n        ServerDataFrom<\n          () =>\n            | {\n                json: string;\n                b: Date;\n                c: () => boolean;\n                d: unstable_SerializesTo<number>;\n              }\n            | DataWithResponseInit<{\n                data: string;\n                b: Date;\n                c: () => boolean;\n                d: unstable_SerializesTo<number>;\n              }>\n        >\n      >,\n      | { json: string; b: Date; c: undefined; d: number }\n      | { data: string; b: Date; c: undefined; d: number }\n    >\n  >,\n  Expect<Equal<ServerDataFrom<() => { a: string } | Response>, { a: string }>>,\n  Expect<\n    Equal<\n      ServerDataFrom<\n        () => {\n          map: Map<string, number>;\n          readonlyMap: ReadonlyMap<string, number>;\n        }\n      >,\n      { map: Map<string, number>; readonlyMap: ReadonlyMap<string, number> }\n    >\n  >,\n  Expect<\n    Equal<\n      ServerDataFrom<\n        () => { set: Set<string>; readonlySet: ReadonlySet<string> }\n      >,\n      { set: Set<string>; readonlySet: ReadonlySet<string> }\n    >\n  >,\n\n  // ClientDataFrom\n  Expect<Equal<ClientDataFrom<any>, undefined>>,\n  Expect<\n    Equal<\n      ClientDataFrom<() => { a: string; b: Date; c: () => boolean }>,\n      { a: string; b: Date; c: () => boolean }\n    >\n  >,\n  Expect<\n    Equal<\n      Pretty<\n        ClientDataFrom<\n          () =>\n            | { json: string; b: Date; c: () => boolean }\n            | DataWithResponseInit<{ data: string; b: Date; c: () => boolean }>\n        >\n      >,\n      | { json: string; b: Date; c: () => boolean }\n      | { data: string; b: Date; c: () => boolean }\n    >\n  >,\n  Expect<Equal<ClientDataFrom<() => { a: string } | Response>, { a: string }>>,\n\n  // GetLoaderData\n  Expect<Equal<GetLoaderData<{}>, undefined>>,\n  Expect<\n    Equal<\n      GetLoaderData<{\n        loader: () => { a: string; b: Date; c: () => boolean };\n      }>,\n      { a: string; b: Date; c: undefined }\n    >\n  >,\n  Expect<\n    Equal<\n      GetLoaderData<{\n        clientLoader: () => { a: string; b: Date; c: () => boolean };\n      }>,\n      { a: string; b: Date; c: () => boolean }\n    >\n  >,\n  Expect<\n    Equal<\n      GetLoaderData<{\n        loader: () => { a: string; b: Date; c: () => boolean };\n        clientLoader: () => { d: string; e: Date; f: () => boolean };\n      }>,\n      | { a: string; b: Date; c: undefined }\n      | { d: string; e: Date; f: () => boolean }\n    >\n  >,\n  Expect<\n    Equal<\n      GetLoaderData<{\n        loader: () => { a: string; b: Date; c: () => boolean };\n        clientLoader: () => { d: string; e: Date; f: () => boolean };\n        HydrateFallback: () => unknown;\n      }>,\n      | { a: string; b: Date; c: undefined }\n      | { d: string; e: Date; f: () => boolean }\n    >\n  >,\n  Expect<\n    Equal<\n      GetLoaderData<{\n        loader: () => { a: string; b: Date; c: () => boolean };\n        clientLoader: (() => { d: string; e: Date; f: () => boolean }) & {\n          hydrate: true;\n        };\n      }>,\n      | { a: string; b: Date; c: undefined }\n      | { d: string; e: Date; f: () => boolean }\n    >\n  >,\n  Expect<\n    Equal<\n      GetLoaderData<{\n        loader: () => { a: string; b: Date; c: () => boolean };\n        clientLoader: (() => { d: string; e: Date; f: () => boolean }) & {\n          hydrate: true;\n        };\n        HydrateFallback: () => unknown;\n      }>,\n      { d: string; e: Date; f: () => boolean }\n    >\n  >,\n\n  // ActionData\n  Expect<Equal<GetActionData<{}>, undefined>>,\n  Expect<\n    Equal<\n      GetActionData<{\n        action: () => { a: string; b: Date; c: () => boolean };\n      }>,\n      { a: string; b: Date; c: undefined }\n    >\n  >,\n  Expect<\n    Equal<\n      GetActionData<{\n        clientAction: () => { a: string; b: Date; c: () => boolean };\n      }>,\n      { a: string; b: Date; c: () => boolean }\n    >\n  >,\n  Expect<\n    Equal<\n      GetActionData<{\n        action: () => { a: string; b: Date; c: () => boolean };\n        clientAction: () => { d: string; e: Date; f: () => boolean };\n      }>,\n      | { a: string; b: Date; c: undefined }\n      | { d: string; e: Date; f: () => boolean }\n    >\n  >,\n];\n"
  },
  {
    "path": "packages/react-router/lib/types/route-module-annotations.ts",
    "content": "import type { MetaDescriptor } from \"../dom/ssr/routeModules\";\nimport type { Location } from \"../router/history\";\nimport type { LinkDescriptor } from \"../router/links\";\nimport type {\n  DataStrategyResult,\n  MiddlewareNextFunction,\n} from \"../router/utils\";\n\nimport type {\n  ClientDataFunctionArgs,\n  GetLoaderData,\n  ServerDataFrom,\n  ServerDataFunctionArgs,\n} from \"./route-data\";\nimport type { RouteModule } from \"./route-module\";\nimport type { Pretty, Func, Expect, Equal } from \"./utils\";\n\ntype MaybePromise<T> = T | Promise<T>;\n\ntype Props = {\n  params: unknown;\n  loaderData: unknown;\n  actionData: unknown;\n};\n\ntype RouteInfo = Props & {\n  module: RouteModule;\n  matches: Array<MatchInfo>;\n};\n\ntype MatchInfo = {\n  id: string;\n  module: RouteModule;\n};\n\ntype MetaMatch<T extends MatchInfo> = Pretty<{\n  id: T[\"id\"];\n  params: Record<string, string | undefined>;\n  pathname: string;\n  meta: MetaDescriptor[];\n  /** @deprecated Use `MetaMatch.loaderData` instead */\n  data: GetLoaderData<T[\"module\"]>;\n  loaderData: GetLoaderData<T[\"module\"]>;\n  handle?: unknown;\n  error?: unknown;\n}>;\n\n// prettier-ignore\ntype MetaMatches<T extends Array<MatchInfo>> =\n  T extends [infer F extends MatchInfo, ...infer R extends Array<MatchInfo>]\n    ? [MetaMatch<F>, ...MetaMatches<R>]\n    : Array<MetaMatch<MatchInfo> | undefined>;\n\ntype HasErrorBoundary<T extends RouteInfo> = T[\"module\"] extends {\n  ErrorBoundary: Func;\n}\n  ? true\n  : false;\n\ntype CreateMetaArgs<T extends RouteInfo> = {\n  /** This is the current router `Location` object. This is useful for generating tags for routes at specific paths or query parameters. */\n  location: Location;\n  /** {@link https://reactrouter.com/start/framework/routing#dynamic-segments Dynamic route params} for the current route. */\n  params: T[\"params\"];\n  /**\n   * The return value for this route's server loader function\n   *\n   * @deprecated Use `Route.MetaArgs.loaderData` instead\n   */\n  data:\n    | T[\"loaderData\"]\n    | (HasErrorBoundary<T> extends true ? undefined : never);\n  /** The return value for this route's server loader function */\n  loaderData:\n    | T[\"loaderData\"]\n    | (HasErrorBoundary<T> extends true ? undefined : never);\n  /** Thrown errors that trigger error boundaries will be passed to the meta function. This is useful for generating metadata for error pages. */\n  error?: unknown;\n  /** An array of the current {@link https://api.reactrouter.com/v7/interfaces/react-router.UIMatch.html route matches}, including parent route matches. */\n  matches: MetaMatches<T[\"matches\"]>;\n};\ntype MetaDescriptors = MetaDescriptor[];\n\ntype HeadersArgs = {\n  loaderHeaders: Headers;\n  parentHeaders: Headers;\n  actionHeaders: Headers;\n  errorHeaders: Headers | undefined;\n};\n\ntype CreateServerMiddlewareFunction<T extends RouteInfo> = (\n  args: ServerDataFunctionArgs<T[\"params\"]>,\n  next: MiddlewareNextFunction<Response>,\n) => MaybePromise<Response | void>;\n\ntype CreateClientMiddlewareFunction<T extends RouteInfo> = (\n  args: ClientDataFunctionArgs<T[\"params\"]>,\n  next: MiddlewareNextFunction<Record<string, DataStrategyResult>>,\n) => MaybePromise<Record<string, DataStrategyResult> | void>;\n\ntype CreateServerLoaderArgs<T extends RouteInfo> = ServerDataFunctionArgs<\n  T[\"params\"]\n>;\n\ntype CreateClientLoaderArgs<T extends RouteInfo> = ClientDataFunctionArgs<\n  T[\"params\"]\n> & {\n  /** This is an asynchronous function to get the data from the server loader for this route. On client-side navigations, this will make a {@link https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API fetch} call to the React Router server loader. If you opt-into running your clientLoader on hydration, then this function will return the data that was already loaded on the server (via Promise.resolve). */\n  serverLoader: () => Promise<ServerDataFrom<T[\"module\"][\"loader\"]>>;\n};\n\ntype CreateServerActionArgs<T extends RouteInfo> = ServerDataFunctionArgs<\n  T[\"params\"]\n>;\n\ntype CreateClientActionArgs<T extends RouteInfo> = ClientDataFunctionArgs<\n  T[\"params\"]\n> & {\n  /** This is an asynchronous function that makes the {@link https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API fetch} call to the React Router server action for this route. */\n  serverAction: () => Promise<ServerDataFrom<T[\"module\"][\"action\"]>>;\n};\n\ntype IsServerFirstRoute<\n  T extends RouteInfo,\n  RSCEnabled extends boolean,\n> = RSCEnabled extends true\n  ? T[\"module\"] extends { ServerComponent: Func }\n    ? true\n    : false\n  : false;\n\ntype CreateHydrateFallbackProps<\n  T extends RouteInfo,\n  RSCEnabled extends boolean,\n> = {\n  params: T[\"params\"];\n} & (IsServerFirstRoute<T, RSCEnabled> extends true\n  ? {\n      /** The data returned from the `loader` */\n      loaderData?: ServerDataFrom<T[\"module\"][\"loader\"]>;\n      /** The data returned from the `action` following an action submission. */\n      actionData?: ServerDataFrom<T[\"module\"][\"action\"]>;\n    }\n  : {\n      /** The data returned from the `loader` or `clientLoader` */\n      loaderData?: T[\"loaderData\"];\n      /** The data returned from the `action` or `clientAction` following an action submission. */\n      actionData?: T[\"actionData\"];\n    });\n\ntype Match<T extends MatchInfo> = Pretty<{\n  id: T[\"id\"];\n  params: Record<string, string | undefined>;\n  pathname: string;\n  /** @deprecated Use `Match.loaderData` instead */\n  data: GetLoaderData<T[\"module\"]>;\n  loaderData: GetLoaderData<T[\"module\"]>;\n  handle: unknown;\n}>;\n\n// prettier-ignore\ntype Matches<T extends Array<MatchInfo>> =\n  T extends [infer F extends MatchInfo, ...infer R extends Array<MatchInfo>]\n    ? [Match<F>, ...Matches<R>]\n    : Array<Match<MatchInfo> | undefined>;\n\ntype CreateComponentProps<T extends RouteInfo, RSCEnabled extends boolean> = {\n  /**\n   * {@link https://reactrouter.com/start/framework/routing#dynamic-segments Dynamic route params} for the current route.\n   * @example\n   * // app/routes.ts\n   * route(\"teams/:teamId\", \"./team.tsx\"),\n   *\n   * // app/team.tsx\n   * export default function Component({\n   *   params,\n   * }: Route.ComponentProps) {\n   *   params.teamId;\n   *   //        ^ string\n   * }\n   **/\n  params: T[\"params\"];\n  /** An array of the current {@link https://api.reactrouter.com/v7/interfaces/react-router.UIMatch.html route matches}, including parent route matches. */\n  matches: Matches<T[\"matches\"]>;\n} & (IsServerFirstRoute<T, RSCEnabled> extends true\n  ? {\n      /** The data returned from the `loader` */\n      loaderData: ServerDataFrom<T[\"module\"][\"loader\"]>;\n      /** The data returned from the `action` following an action submission. */\n      actionData?: ServerDataFrom<T[\"module\"][\"action\"]>;\n    }\n  : {\n      /** The data returned from the `loader` or `clientLoader` */\n      loaderData: T[\"loaderData\"];\n      /** The data returned from the `action` or `clientAction` following an action submission. */\n      actionData?: T[\"actionData\"];\n    });\n\ntype CreateErrorBoundaryProps<\n  T extends RouteInfo,\n  RSCEnabled extends boolean,\n> = {\n  /**\n   * {@link https://reactrouter.com/start/framework/routing#dynamic-segments Dynamic route params} for the current route.\n   * @example\n   * // app/routes.ts\n   * route(\"teams/:teamId\", \"./team.tsx\"),\n   *\n   * // app/team.tsx\n   * export function ErrorBoundary({\n   *   params,\n   * }: Route.ErrorBoundaryProps) {\n   *   params.teamId;\n   *   //        ^ string\n   * }\n   **/\n  params: T[\"params\"];\n  error: unknown;\n} & (IsServerFirstRoute<T, RSCEnabled> extends true\n  ? {\n      /** The data returned from the `loader` */\n      loaderData?: ServerDataFrom<T[\"module\"][\"loader\"]>;\n      /** The data returned from the `action` following an action submission. */\n      actionData?: ServerDataFrom<T[\"module\"][\"action\"]>;\n    }\n  : {\n      /** The data returned from the `loader` or `clientLoader` */\n      loaderData?: T[\"loaderData\"];\n      /** The data returned from the `action` or `clientAction` following an action submission. */\n      actionData?: T[\"actionData\"];\n    });\n\nexport type GetAnnotations<\n  Info extends RouteInfo,\n  RSCEnabled extends boolean,\n> = {\n  // links\n  LinkDescriptors: LinkDescriptor[];\n  LinksFunction: () => LinkDescriptor[];\n\n  // meta\n  MetaArgs: CreateMetaArgs<Info>;\n  MetaDescriptors: MetaDescriptors;\n  MetaFunction: (args: CreateMetaArgs<Info>) => MetaDescriptors;\n\n  // headers\n  HeadersArgs: HeadersArgs;\n  HeadersFunction: (args: HeadersArgs) => Headers | HeadersInit;\n\n  // middleware\n  MiddlewareFunction: CreateServerMiddlewareFunction<Info>;\n\n  // clientMiddleware\n  ClientMiddlewareFunction: CreateClientMiddlewareFunction<Info>;\n\n  // loader\n  LoaderArgs: CreateServerLoaderArgs<Info>;\n\n  // clientLoader\n  ClientLoaderArgs: CreateClientLoaderArgs<Info>;\n\n  // action\n  ActionArgs: CreateServerActionArgs<Info>;\n\n  // clientAction\n  ClientActionArgs: CreateClientActionArgs<Info>;\n\n  // HydrateFallback\n  HydrateFallbackProps: CreateHydrateFallbackProps<Info, RSCEnabled>;\n\n  // default (Component)\n  ComponentProps: CreateComponentProps<Info, RSCEnabled>;\n\n  // ErrorBoundary\n  ErrorBoundaryProps: CreateErrorBoundaryProps<Info, RSCEnabled>;\n};\n\n// eslint-disable-next-line @typescript-eslint/no-unused-vars\ntype __tests = [\n  // Test that MetaArgs.loaderData is only potentially undefined when ErrorBoundary is present\n  Expect<\n    Equal<\n      CreateMetaArgs<{\n        module: {\n          loader: () => { test: string };\n        };\n        loaderData: { test: string };\n        params: unknown;\n        actionData: unknown;\n        matches: [];\n      }>[\"loaderData\"],\n      { test: string }\n    >\n  >,\n  Expect<\n    Equal<\n      CreateMetaArgs<{\n        module: {\n          loader: () => { test: string };\n          ErrorBoundary: Func;\n        };\n        loaderData: { test: string };\n        params: unknown;\n        actionData: unknown;\n        matches: [];\n      }>[\"loaderData\"],\n      { test: string } | undefined\n    >\n  >,\n  Expect<\n    Equal<\n      CreateMetaArgs<{\n        module: {};\n        loaderData: never;\n        params: unknown;\n        actionData: unknown;\n        matches: [];\n      }>[\"loaderData\"],\n      never\n    >\n  >,\n  Expect<\n    Equal<\n      CreateMetaArgs<{\n        module: {\n          ErrorBoundary: Func;\n        };\n        loaderData: never;\n        params: unknown;\n        actionData: unknown;\n        matches: [];\n      }>[\"loaderData\"],\n      undefined\n    >\n  >,\n  // Test that MetaArgs.data (deprecated) also follows the same pattern\n  Expect<\n    Equal<\n      CreateMetaArgs<{\n        module: {\n          loader: () => { test: string };\n        };\n        loaderData: { test: string };\n        params: unknown;\n        actionData: unknown;\n        matches: [];\n      }>[\"data\"],\n      { test: string }\n    >\n  >,\n  Expect<\n    Equal<\n      CreateMetaArgs<{\n        module: {\n          loader: () => { test: string };\n          ErrorBoundary: Func;\n        };\n        loaderData: { test: string };\n        params: unknown;\n        actionData: unknown;\n        matches: [];\n      }>[\"data\"],\n      { test: string } | undefined\n    >\n  >,\n];\n"
  },
  {
    "path": "packages/react-router/lib/types/route-module.ts",
    "content": "import type { Func } from \"./utils\";\n\nexport type RouteModule = {\n  meta?: Func;\n  links?: Func;\n  headers?: Func;\n  loader?: Func;\n  clientLoader?: Func;\n  action?: Func;\n  clientAction?: Func;\n  HydrateFallback?: Func;\n  default?: Func;\n  ErrorBoundary?: Func;\n  [key: string]: unknown; // allow user-defined exports\n};\n"
  },
  {
    "path": "packages/react-router/lib/types/serializes-to.ts",
    "content": "import type { Equal, Expect } from \"./utils\";\n\n/**\n * A brand that can be applied to a type to indicate that it will serialize\n * to a specific type when transported to the client from a loader.\n * Only use this if you have additional serialization/deserialization logic\n * in your application.\n */\nexport type unstable_SerializesTo<T> = {\n  unstable__ReactRouter_SerializesTo: [T];\n};\n\n// eslint-disable-next-line @typescript-eslint/no-unused-vars\ntype __tests = [\n  Expect<\n    Equal<\n      Record<string, any> extends unstable_SerializesTo<any> ? true : false,\n      false\n    >\n  >,\n];\n"
  },
  {
    "path": "packages/react-router/lib/types/utils.ts",
    "content": "export type Expect<T extends true> = T;\n\n// prettier-ignore\nexport type Equal<X, Y> =\n  (<T>() => T extends X ? 1 : 2) extends\n  (<T>() => T extends Y ? 1 : 2) ? true : false\n\nexport type IsAny<T> = 0 extends 1 & T ? true : false;\n\nexport type Func = (...args: any[]) => unknown;\n\nexport type Pretty<T> = { [K in keyof T]: T[K] } & {};\n\n// Emulates https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-7.html#improved-type-inference-for-object-literals\nexport type Normalize<T> = _Normalize<UnionKeys<T>, T>;\n// prettier-ignore\ntype _Normalize<Key extends keyof any, T> =\n  T extends infer U ?\n    Pretty<\n      & { [K in Key as K extends keyof U ? undefined extends U[K] ? never : K : never]: K extends keyof U ? U[K] : never }\n      & { [K in Key as K extends keyof U ? undefined extends U[K] ? K : never : never]?: K extends keyof U ? U[K] : never }\n      & { [K in Key as K extends keyof U ? never : K]?: undefined}\n    >\n  :\n  never\ntype UnionKeys<T> = T extends any ? keyof T : never;\n\n// prettier-ignore\ntype __tests = [\n  Expect<Equal<Normalize<{}>, {}>>,\n  Expect<Equal<Normalize<{a: string}>, {a: string}>>,\n  Expect<Equal<Normalize<{a: string} | {b: string}>, {a: string, b?: undefined} | {a?: undefined , b: string}>>,\n  Expect<Equal<Normalize<{a?: string} | {b?: string}>, {a?: string, b?: undefined} | {a?: undefined , b?: string}>>,\n]\n"
  },
  {
    "path": "packages/react-router/node-main-dom-export.js",
    "content": "/* eslint-env node */\n\nif (process.env.NODE_ENV === \"production\") {\n  module.exports = require(\"./umd/react-router-dom.production.min.js\");\n} else {\n  module.exports = require(\"./umd/react-router-dom.development.js\");\n}\n"
  },
  {
    "path": "packages/react-router/node-main.js",
    "content": "/* eslint-env node */\n\nif (process.env.NODE_ENV === \"production\") {\n  module.exports = require(\"./umd/react-router.production.min.js\");\n} else {\n  module.exports = require(\"./umd/react-router.development.js\");\n}\n"
  },
  {
    "path": "packages/react-router/package.json",
    "content": "{\n  \"name\": \"react-router\",\n  \"version\": \"7.13.1\",\n  \"description\": \"Declarative routing for React\",\n  \"keywords\": [\n    \"react\",\n    \"router\",\n    \"route\",\n    \"routing\",\n    \"history\",\n    \"link\"\n  ],\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"https://github.com/remix-run/react-router\",\n    \"directory\": \"packages/react-router\"\n  },\n  \"license\": \"MIT\",\n  \"author\": \"Remix Software <hello@remix.run>\",\n  \"sideEffects\": false,\n  \"types\": \"./dist/development/index.d.ts\",\n  \"main\": \"./dist/development/index.js\",\n  \"module\": \"./dist/development/index.mjs\",\n  \"exports\": {\n    \".\": {\n      \"react-server\": {\n        \"module\": \"./dist/development/index-react-server.mjs\",\n        \"default\": \"./dist/development/index-react-server.js\"\n      },\n      \"node\": {\n        \"types\": \"./dist/development/index.d.ts\",\n        \"module\": \"./dist/development/index.mjs\",\n        \"module-sync\": \"./dist/development/index.mjs\",\n        \"default\": \"./dist/development/index.js\"\n      },\n      \"module\": {\n        \"types\": \"./dist/development/index.d.mts\",\n        \"default\": \"./dist/development/index.mjs\"\n      },\n      \"import\": {\n        \"types\": \"./dist/development/index.d.mts\",\n        \"default\": \"./dist/development/index.mjs\"\n      },\n      \"default\": {\n        \"types\": \"./dist/development/index.d.ts\",\n        \"default\": \"./dist/development/index.js\"\n      }\n    },\n    \"./dom\": {\n      \"node\": {\n        \"types\": \"./dist/development/dom-export.d.ts\",\n        \"module\": \"./dist/development/dom-export.mjs\",\n        \"module-sync\": \"./dist/development/dom-export.mjs\",\n        \"default\": \"./dist/development/dom-export.js\"\n      },\n      \"module\": {\n        \"types\": \"./dist/development/dom-export.d.mts\",\n        \"default\": \"./dist/development/dom-export.mjs\"\n      },\n      \"import\": {\n        \"types\": \"./dist/development/dom-export.d.mts\",\n        \"default\": \"./dist/development/dom-export.mjs\"\n      },\n      \"default\": {\n        \"types\": \"./dist/development/dom-export.d.ts\",\n        \"default\": \"./dist/development/dom-export.js\"\n      }\n    },\n    \"./internal\": {\n      \"node\": {\n        \"types\": \"./dist/development/lib/types/internal.d.ts\"\n      },\n      \"import\": {\n        \"types\": \"./dist/development/lib/types/internal.d.mts\"\n      },\n      \"default\": {\n        \"types\": \"./dist/development/lib/types/index.d.ts\"\n      }\n    },\n    \"./internal/react-server-client\": {\n      \"react-server\": {\n        \"module\": \"./dist/development/index-react-server-client.mjs\",\n        \"default\": \"./dist/development/index-react-server-client.js\"\n      },\n      \"node\": {\n        \"types\": \"./dist/development/index.d.ts\",\n        \"module\": \"./dist/development/index.mjs\",\n        \"module-sync\": \"./dist/development/index.mjs\",\n        \"default\": \"./dist/development/index.js\"\n      },\n      \"module\": {\n        \"types\": \"./dist/development/index.d.mts\",\n        \"default\": \"./dist/development/index.mjs\"\n      },\n      \"import\": {\n        \"types\": \"./dist/development/index.d.mts\",\n        \"default\": \"./dist/development/index.mjs\"\n      },\n      \"default\": {\n        \"types\": \"./dist/development/index.d.ts\",\n        \"default\": \"./dist/development/index.js\"\n      }\n    },\n    \"./package.json\": \"./package.json\"\n  },\n  \"scripts\": {\n    \"build\": \"wireit\",\n    \"watch\": \"tsup --watch & tsup --config tsup.config.rsc.ts --watch\",\n    \"typecheck\": \"tsc\"\n  },\n  \"wireit\": {\n    \"build\": {\n      \"command\": \"premove dist && tsup && tsup --config tsup.config.rsc.ts\",\n      \"files\": [\n        \"../../pnpm-workspace.yaml\",\n        \"lib/**\",\n        \"*.ts\",\n        \"tsconfig.json\",\n        \"package.json\"\n      ],\n      \"output\": [\n        \"dist/**\"\n      ]\n    }\n  },\n  \"dependencies\": {\n    \"cookie\": \"^1.0.1\",\n    \"set-cookie-parser\": \"^2.6.0\"\n  },\n  \"devDependencies\": {\n    \"@testing-library/jest-dom\": \"^6.6.3\",\n    \"@testing-library/react\": \"^16.3.0\",\n    \"@testing-library/user-event\": \"^14.6.1\",\n    \"@types/set-cookie-parser\": \"^2.4.1\",\n    \"jest-environment-jsdom\": \"^29.6.2\",\n    \"premove\": \"^4.0.0\",\n    \"react\": \"catalog:\",\n    \"react-dom\": \"catalog:\",\n    \"react-test-renderer\": \"^19.1.0\",\n    \"tsup\": \"catalog:\",\n    \"typescript\": \"catalog:\",\n    \"undici\": \"^6.19.2\",\n    \"wireit\": \"catalog:\"\n  },\n  \"peerDependencies\": {\n    \"react\": \">=18\",\n    \"react-dom\": \">=18\"\n  },\n  \"peerDependenciesMeta\": {\n    \"react-dom\": {\n      \"optional\": true\n    }\n  },\n  \"files\": [\n    \"dist/\",\n    \"CHANGELOG.md\",\n    \"LICENSE.md\",\n    \"README.md\"\n  ],\n  \"engines\": {\n    \"node\": \">=20.0.0\"\n  }\n}\n"
  },
  {
    "path": "packages/react-router/tsconfig.dom.json",
    "content": "{\n  \"extends\": \"./tsconfig.json\",\n  \"include\": [\"./dom-export.tsx\"]\n}\n"
  },
  {
    "path": "packages/react-router/tsconfig.json",
    "content": "{\n  \"include\": [\"**/*.ts\"],\n  \"exclude\": [\"dist\", \"__tests__\", \"node_modules\"],\n  \"compilerOptions\": {\n    \"lib\": [\"ES2020\", \"DOM\", \"DOM.Iterable\"],\n    \"target\": \"ES2020\",\n    \"module\": \"Node16\",\n    \"moduleResolution\": \"Node16\",\n\n    \"strict\": true,\n    \"jsx\": \"react\",\n\n    \"declaration\": true,\n\n    \"skipLibCheck\": true,\n\n    \"outDir\": \"dist\",\n    \"rootDir\": \".\",\n    \"noEmit\": true,\n    \"resolveJsonModule\": true,\n    \"paths\": {\n      \"react-router\": [\"./index.ts\"]\n    }\n  }\n}\n"
  },
  {
    "path": "packages/react-router/tsup.config.rsc.ts",
    "content": "import { defineConfig, type Options } from \"tsup\";\n\n// @ts-ignore - out of scope\nimport { createBanner } from \"../../build.utils.js\";\n\nimport pkg from \"./package.json\";\n\nconst entry = [\"index-react-server.ts\"];\nconst external = [\"react-router\", \"react-router/internal/react-server-client\"];\n\nconst config = (enableDevWarnings: boolean) =>\n  defineConfig([\n    {\n      clean: false,\n      entry,\n      external,\n      format: [\"cjs\"],\n      removeNodeProtocol: false,\n      splitting: true,\n      outDir: enableDevWarnings ? \"dist/development\" : \"dist/production\",\n      dts: true,\n      banner: {\n        js: createBanner(pkg.name, pkg.version),\n      },\n      define: {\n        \"import.meta.hot\": \"undefined\",\n        REACT_ROUTER_VERSION: JSON.stringify(pkg.version),\n        __DEV__: JSON.stringify(enableDevWarnings),\n      },\n      treeshake: true,\n    },\n    {\n      clean: false,\n      entry,\n      external,\n      format: [\"esm\"],\n      removeNodeProtocol: false,\n      splitting: true,\n      outDir: enableDevWarnings ? \"dist/development\" : \"dist/production\",\n      dts: true,\n      banner: {\n        js: createBanner(pkg.name, pkg.version),\n      },\n      define: {\n        REACT_ROUTER_VERSION: JSON.stringify(pkg.version),\n        __DEV__: JSON.stringify(enableDevWarnings),\n      },\n      treeshake: true,\n    },\n  ]) as Options[];\n\nexport default defineConfig([...config(false), ...config(true)]);\n"
  },
  {
    "path": "packages/react-router/tsup.config.ts",
    "content": "import { defineConfig } from \"tsup\";\n\n// @ts-ignore - out of scope\nimport { createBanner } from \"../../build.utils.js\";\n\nimport pkg from \"./package.json\";\n\nconst entry = [\n  \"index.ts\",\n  \"index-react-server-client.ts\",\n  \"dom-export.ts\",\n  \"lib/types/internal.ts\",\n];\n\nconst config = (enableDevWarnings: boolean) =>\n  defineConfig([\n    {\n      clean: false,\n      entry,\n      format: [\"cjs\"],\n      splitting: true,\n      // Don't bundle `react-router` in sub-exports (i.e., `react-router/dom`)\n      external: [\"react-router\"],\n      outDir: enableDevWarnings ? \"dist/development\" : \"dist/production\",\n      dts: true,\n      banner: {\n        js: createBanner(pkg.name, pkg.version),\n      },\n      define: {\n        \"import.meta.hot\": \"undefined\",\n        REACT_ROUTER_VERSION: JSON.stringify(pkg.version),\n        __DEV__: JSON.stringify(enableDevWarnings),\n      },\n    },\n    {\n      clean: false,\n      entry,\n      format: [\"esm\"],\n      splitting: true,\n      // We don't do the external thing for `react-router` here because it\n      // doesn't get bundled by default in the ESM build, and when we tried it\n      // in https://github.com/remix-run/react-router/pull/13497 it changed up\n      // some chunk creation that we didn't want to risk having any side effects\n      outDir: enableDevWarnings ? \"dist/development\" : \"dist/production\",\n      dts: true,\n      banner: {\n        js: createBanner(pkg.name, pkg.version),\n      },\n      define: {\n        REACT_ROUTER_VERSION: JSON.stringify(pkg.version),\n        __DEV__: JSON.stringify(enableDevWarnings),\n      },\n    },\n  ]);\n\nexport default defineConfig([\n  // @ts-expect-error\n  ...config(false),\n  ...config(true),\n]);\n"
  },
  {
    "path": "packages/react-router/typedoc.mjs",
    "content": "import { blockTags } from \"../../typedoc.mjs\";\n\n/** @type {Partial<import('typedoc').TypeDocOptions>} */\nconst config = {\n  entryPoints: [\"./index.ts\", \"./dom-export.ts\"],\n  categoryOrder: [\n    \"Components\",\n    \"Hooks\",\n    \"Data Routers\",\n    \"Component Routers\",\n    \"Utils\",\n    \"Types\",\n    \"*\",\n  ],\n  blockTags,\n};\n\nexport default config;\n"
  },
  {
    "path": "packages/react-router/vendor/turbo-stream-v2/flatten.ts",
    "content": "import {\n  HOLE,\n  NAN,\n  NEGATIVE_INFINITY,\n  NEGATIVE_ZERO,\n  NULL,\n  POSITIVE_INFINITY,\n  UNDEFINED,\n  TYPE_BIGINT,\n  TYPE_DATE,\n  TYPE_ERROR,\n  TYPE_MAP,\n  TYPE_NULL_OBJECT,\n  TYPE_PROMISE,\n  TYPE_REGEXP,\n  TYPE_SET,\n  TYPE_SYMBOL,\n  TYPE_URL,\n  type ThisEncode,\n} from \"./utils\";\n\nexport function flatten(this: ThisEncode, input: unknown): number | [number] {\n  const { indices } = this;\n  const existing = indices.get(input);\n  if (existing) return [existing];\n\n  if (input === undefined) return UNDEFINED;\n  if (input === null) return NULL;\n  if (Number.isNaN(input)) return NAN;\n  if (input === Number.POSITIVE_INFINITY) return POSITIVE_INFINITY;\n  if (input === Number.NEGATIVE_INFINITY) return NEGATIVE_INFINITY;\n  if (input === 0 && 1 / input < 0) return NEGATIVE_ZERO;\n\n  const index = this.index++;\n  indices.set(input, index);\n  stringify.call(this, input, index);\n  return index;\n}\n\nfunction stringify(this: ThisEncode, input: unknown, index: number) {\n  const { deferred, plugins, postPlugins } = this;\n  const str = this.stringified;\n\n  const stack: [unknown, number][] = [[input, index]];\n  while (stack.length > 0) {\n    const [input, index] = stack.pop()!;\n\n    const partsForObj = (obj: any) =>\n      Object.keys(obj)\n        .map((k) => `\"_${flatten.call(this, k)}\":${flatten.call(this, obj[k])}`)\n        .join(\",\");\n    let error: Error | null = null;\n\n    switch (typeof input) {\n      case \"boolean\":\n      case \"number\":\n      case \"string\":\n        str[index] = JSON.stringify(input);\n        break;\n      case \"bigint\":\n        str[index] = `[\"${TYPE_BIGINT}\",\"${input}\"]`;\n        break;\n      case \"symbol\": {\n        const keyFor = Symbol.keyFor(input);\n        if (!keyFor) {\n          error = new Error(\n            \"Cannot encode symbol unless created with Symbol.for()\",\n          );\n        } else {\n          str[index] = `[\"${TYPE_SYMBOL}\",${JSON.stringify(keyFor)}]`;\n        }\n        break;\n      }\n      case \"object\": {\n        if (!input) {\n          str[index] = `${NULL}`;\n          break;\n        }\n\n        const isArray = Array.isArray(input);\n        let pluginHandled = false;\n        if (!isArray && plugins) {\n          for (const plugin of plugins) {\n            const pluginResult = plugin(input);\n            if (Array.isArray(pluginResult)) {\n              pluginHandled = true;\n              const [pluginIdentifier, ...rest] = pluginResult;\n              str[index] = `[${JSON.stringify(pluginIdentifier)}`;\n              if (rest.length > 0) {\n                str[index] += `,${rest\n                  .map((v) => flatten.call(this, v))\n                  .join(\",\")}`;\n              }\n              str[index] += \"]\";\n              break;\n            }\n          }\n        }\n\n        if (!pluginHandled) {\n          let result = isArray ? \"[\" : \"{\";\n          if (isArray) {\n            for (let i = 0; i < input.length; i++)\n              result +=\n                (i ? \",\" : \"\") +\n                (i in input ? flatten.call(this, input[i]) : HOLE);\n            str[index] = `${result}]`;\n          } else if (input instanceof Date) {\n            const dateTime = input.getTime();\n            str[index] = `[\"${TYPE_DATE}\",${\n              Number.isNaN(dateTime) ? JSON.stringify(\"invalid\") : dateTime\n            }]`;\n          } else if (input instanceof URL) {\n            str[index] = `[\"${TYPE_URL}\",${JSON.stringify(input.href)}]`;\n          } else if (input instanceof RegExp) {\n            str[index] = `[\"${TYPE_REGEXP}\",${JSON.stringify(\n              input.source,\n            )},${JSON.stringify(input.flags)}]`;\n          } else if (input instanceof Set) {\n            if (input.size > 0) {\n              str[index] = `[\"${TYPE_SET}\",${[...input]\n                .map((val) => flatten.call(this, val))\n                .join(\",\")}]`;\n            } else {\n              str[index] = `[\"${TYPE_SET}\"]`;\n            }\n          } else if (input instanceof Map) {\n            if (input.size > 0) {\n              str[index] = `[\"${TYPE_MAP}\",${[...input]\n                .flatMap(([k, v]) => [\n                  flatten.call(this, k),\n                  flatten.call(this, v),\n                ])\n                .join(\",\")}]`;\n            } else {\n              str[index] = `[\"${TYPE_MAP}\"]`;\n            }\n          } else if (input instanceof Promise) {\n            str[index] = `[\"${TYPE_PROMISE}\",${index}]`;\n            deferred[index] = input;\n          } else if (input instanceof Error) {\n            str[index] = `[\"${TYPE_ERROR}\",${JSON.stringify(input.message)}`;\n            if (input.name !== \"Error\") {\n              str[index] += `,${JSON.stringify(input.name)}`;\n            }\n            str[index] += \"]\";\n          } else if (Object.getPrototypeOf(input) === null) {\n            str[index] = `[\"${TYPE_NULL_OBJECT}\",{${partsForObj(input)}}]`;\n          } else if (isPlainObject(input)) {\n            str[index] = `{${partsForObj(input)}}`;\n          } else {\n            error = new Error(\"Cannot encode object with prototype\");\n          }\n        }\n        break;\n      }\n      default: {\n        const isArray = Array.isArray(input);\n        let pluginHandled = false;\n        if (!isArray && plugins) {\n          for (const plugin of plugins) {\n            const pluginResult = plugin(input);\n            if (Array.isArray(pluginResult)) {\n              pluginHandled = true;\n              const [pluginIdentifier, ...rest] = pluginResult;\n              str[index] = `[${JSON.stringify(pluginIdentifier)}`;\n              if (rest.length > 0) {\n                str[index] += `,${rest\n                  .map((v) => flatten.call(this, v))\n                  .join(\",\")}`;\n              }\n              str[index] += \"]\";\n              break;\n            }\n          }\n        }\n\n        if (!pluginHandled) {\n          error = new Error(\"Cannot encode function or unexpected type\");\n        }\n      }\n    }\n\n    if (error) {\n      let pluginHandled = false;\n\n      if (postPlugins) {\n        for (const plugin of postPlugins) {\n          const pluginResult = plugin(input);\n          if (Array.isArray(pluginResult)) {\n            pluginHandled = true;\n            const [pluginIdentifier, ...rest] = pluginResult;\n            str[index] = `[${JSON.stringify(pluginIdentifier)}`;\n            if (rest.length > 0) {\n              str[index] += `,${rest\n                .map((v) => flatten.call(this, v))\n                .join(\",\")}`;\n            }\n            str[index] += \"]\";\n            break;\n          }\n        }\n      }\n\n      if (!pluginHandled) {\n        throw error;\n      }\n    }\n  }\n}\n\nconst objectProtoNames = Object.getOwnPropertyNames(Object.prototype)\n  .sort()\n  .join(\"\\0\");\n\nfunction isPlainObject(\n  thing: unknown,\n): thing is Record<string | number | symbol, unknown> {\n  const proto = Object.getPrototypeOf(thing);\n  return (\n    proto === Object.prototype ||\n    proto === null ||\n    Object.getOwnPropertyNames(proto).sort().join(\"\\0\") === objectProtoNames\n  );\n}\n"
  },
  {
    "path": "packages/react-router/vendor/turbo-stream-v2/turbo-stream.ts",
    "content": "import { flatten } from \"./flatten\";\nimport { unflatten } from \"./unflatten\";\nimport {\n  Deferred,\n  TYPE_ERROR,\n  TYPE_PREVIOUS_RESOLVED,\n  TYPE_PROMISE,\n  createLineSplittingTransform,\n  type DecodePlugin,\n  type EncodePlugin,\n  type ThisDecode,\n  type ThisEncode,\n} from \"./utils\";\n\nexport type { DecodePlugin, EncodePlugin };\n\nexport async function decode(\n  readable: ReadableStream<Uint8Array>,\n  options?: { plugins?: DecodePlugin[] },\n) {\n  const { plugins } = options ?? {};\n\n  const done = new Deferred<void>();\n  const reader = readable\n    .pipeThrough(createLineSplittingTransform())\n    .getReader();\n\n  const decoder: ThisDecode = {\n    values: [],\n    hydrated: [],\n    deferred: {},\n    plugins,\n  };\n\n  const decoded = await decodeInitial.call(decoder, reader);\n\n  let donePromise = done.promise;\n  if (decoded.done) {\n    done.resolve();\n  } else {\n    donePromise = decodeDeferred\n      .call(decoder, reader)\n      .then(done.resolve)\n      .catch((reason) => {\n        for (const deferred of Object.values(decoder.deferred)) {\n          deferred.reject(reason);\n        }\n\n        done.reject(reason);\n      });\n  }\n\n  return {\n    done: donePromise.then(() => reader.closed),\n    value: decoded.value,\n  };\n}\n\nasync function decodeInitial(\n  this: ThisDecode,\n  reader: ReadableStreamDefaultReader<string>,\n) {\n  const read = await reader.read();\n  if (!read.value) {\n    throw new SyntaxError();\n  }\n\n  let line: unknown;\n  try {\n    line = JSON.parse(read.value);\n  } catch (reason) {\n    throw new SyntaxError();\n  }\n\n  return {\n    done: read.done,\n    value: unflatten.call(this, line),\n  };\n}\n\nasync function decodeDeferred(\n  this: ThisDecode,\n  reader: ReadableStreamDefaultReader<string>,\n) {\n  let read = await reader.read();\n  while (!read.done) {\n    if (!read.value) continue;\n    const line = read.value;\n    switch (line[0]) {\n      case TYPE_PROMISE: {\n        const colonIndex = line.indexOf(\":\");\n        const deferredId = Number(line.slice(1, colonIndex));\n        const deferred = this.deferred[deferredId];\n        if (!deferred) {\n          throw new Error(`Deferred ID ${deferredId} not found in stream`);\n        }\n        const lineData = line.slice(colonIndex + 1);\n        let jsonLine: unknown;\n        try {\n          jsonLine = JSON.parse(lineData);\n        } catch (reason) {\n          throw new SyntaxError();\n        }\n\n        const value = unflatten.call(this, jsonLine);\n        deferred.resolve(value);\n\n        break;\n      }\n      case TYPE_ERROR: {\n        const colonIndex = line.indexOf(\":\");\n        const deferredId = Number(line.slice(1, colonIndex));\n        const deferred = this.deferred[deferredId];\n        if (!deferred) {\n          throw new Error(`Deferred ID ${deferredId} not found in stream`);\n        }\n        const lineData = line.slice(colonIndex + 1);\n        let jsonLine: unknown;\n        try {\n          jsonLine = JSON.parse(lineData);\n        } catch (reason) {\n          throw new SyntaxError();\n        }\n        const value = unflatten.call(this, jsonLine);\n        deferred.reject(value);\n        break;\n      }\n      default:\n        throw new SyntaxError();\n    }\n    read = await reader.read();\n  }\n}\n\nexport function encode(\n  input: unknown,\n  options?: {\n    plugins?: EncodePlugin[];\n    postPlugins?: EncodePlugin[];\n    signal?: AbortSignal;\n    onComplete?: () => void;\n  },\n) {\n  const { plugins, postPlugins, signal, onComplete } = options ?? {};\n\n  const encoder: ThisEncode = {\n    deferred: {},\n    index: 0,\n    indices: new Map(),\n    stringified: [],\n    plugins,\n    postPlugins,\n    signal,\n  };\n  const textEncoder = new TextEncoder();\n  let lastSentIndex = 0;\n  const readable = new ReadableStream<Uint8Array>({\n    async start(controller) {\n      const id = flatten.call(encoder, input);\n      if (Array.isArray(id)) {\n        throw new Error(\"This should never happen\");\n      }\n      if (id < 0) {\n        controller.enqueue(textEncoder.encode(`${id}\\n`));\n      } else {\n        controller.enqueue(\n          textEncoder.encode(`[${encoder.stringified.join(\",\")}]\\n`),\n        );\n        lastSentIndex = encoder.stringified.length - 1;\n      }\n\n      const seenPromises = new WeakSet<Promise<unknown>>();\n      if (Object.keys(encoder.deferred).length) {\n        let raceDone!: () => void;\n        const racePromise = new Promise<never>((resolve, reject) => {\n          raceDone = resolve as () => void;\n          if (signal) {\n            const rejectPromise = () =>\n              reject(signal.reason || new Error(\"Signal was aborted.\"));\n            if (signal.aborted) {\n              rejectPromise();\n            } else {\n              signal.addEventListener(\"abort\", (event) => {\n                rejectPromise();\n              });\n            }\n          }\n        });\n        while (Object.keys(encoder.deferred).length > 0) {\n          for (const [deferredId, deferred] of Object.entries(\n            encoder.deferred,\n          )) {\n            if (seenPromises.has(deferred)) continue;\n            seenPromises.add(\n              // biome-ignore lint/suspicious/noAssignInExpressions: <explanation>\n              (encoder.deferred[Number(deferredId)] = Promise.race([\n                racePromise,\n                deferred,\n              ])\n                .then(\n                  (resolved) => {\n                    const id = flatten.call(encoder, resolved);\n                    if (Array.isArray(id)) {\n                      controller.enqueue(\n                        textEncoder.encode(\n                          `${TYPE_PROMISE}${deferredId}:[[\"${TYPE_PREVIOUS_RESOLVED}\",${id[0]}]]\\n`,\n                        ),\n                      );\n                      encoder.index++;\n                      lastSentIndex++;\n                    } else if (id < 0) {\n                      controller.enqueue(\n                        textEncoder.encode(\n                          `${TYPE_PROMISE}${deferredId}:${id}\\n`,\n                        ),\n                      );\n                    } else {\n                      const values = encoder.stringified\n                        .slice(lastSentIndex + 1)\n                        .join(\",\");\n                      controller.enqueue(\n                        textEncoder.encode(\n                          `${TYPE_PROMISE}${deferredId}:[${values}]\\n`,\n                        ),\n                      );\n                      lastSentIndex = encoder.stringified.length - 1;\n                    }\n                  },\n                  (reason) => {\n                    if (\n                      !reason ||\n                      typeof reason !== \"object\" ||\n                      !(reason instanceof Error)\n                    ) {\n                      reason = new Error(\"An unknown error occurred\");\n                    }\n\n                    const id = flatten.call(encoder, reason);\n                    if (Array.isArray(id)) {\n                      controller.enqueue(\n                        textEncoder.encode(\n                          `${TYPE_ERROR}${deferredId}:[[\"${TYPE_PREVIOUS_RESOLVED}\",${id[0]}]]\\n`,\n                        ),\n                      );\n                      encoder.index++;\n                      lastSentIndex++;\n                    } else if (id < 0) {\n                      controller.enqueue(\n                        textEncoder.encode(\n                          `${TYPE_ERROR}${deferredId}:${id}\\n`,\n                        ),\n                      );\n                    } else {\n                      const values = encoder.stringified\n                        .slice(lastSentIndex + 1)\n                        .join(\",\");\n                      controller.enqueue(\n                        textEncoder.encode(\n                          `${TYPE_ERROR}${deferredId}:[${values}]\\n`,\n                        ),\n                      );\n                      lastSentIndex = encoder.stringified.length - 1;\n                    }\n                  },\n                )\n                .finally(() => {\n                  delete encoder.deferred[Number(deferredId)];\n                })),\n            );\n          }\n          await Promise.race(Object.values(encoder.deferred));\n        }\n\n        raceDone();\n      }\n      await Promise.all(Object.values(encoder.deferred));\n\n      onComplete?.();\n      controller.close();\n    },\n  });\n\n  return readable;\n}\n"
  },
  {
    "path": "packages/react-router/vendor/turbo-stream-v2/unflatten.ts",
    "content": "import {\n  Deferred,\n  HOLE,\n  NAN,\n  NEGATIVE_INFINITY,\n  NEGATIVE_ZERO,\n  NULL,\n  POSITIVE_INFINITY,\n  UNDEFINED,\n  TYPE_BIGINT,\n  TYPE_DATE,\n  TYPE_ERROR,\n  TYPE_MAP,\n  TYPE_NULL_OBJECT,\n  TYPE_PREVIOUS_RESOLVED,\n  TYPE_PROMISE,\n  TYPE_REGEXP,\n  TYPE_SET,\n  TYPE_SYMBOL,\n  TYPE_URL,\n  type ThisDecode,\n} from \"./utils\";\n\nconst globalObj = (\n  typeof window !== \"undefined\"\n    ? window\n    : typeof globalThis !== \"undefined\"\n      ? globalThis\n      : undefined\n) as Record<string, typeof Error> | undefined;\n\nexport function unflatten(this: ThisDecode, parsed: unknown): unknown {\n  const { hydrated, values } = this;\n  if (typeof parsed === \"number\") return hydrate.call(this, parsed);\n\n  if (!Array.isArray(parsed) || !parsed.length) throw new SyntaxError();\n\n  const startIndex = values.length;\n  for (const value of parsed) {\n    values.push(value);\n  }\n  hydrated.length = values.length;\n\n  return hydrate.call(this, startIndex);\n}\n\nfunction hydrate(this: ThisDecode, index: number): any {\n  const { hydrated, values, deferred, plugins } = this;\n\n  let result: unknown;\n  const stack = [\n    [\n      index,\n      (v: unknown) => {\n        result = v;\n      },\n    ] as const,\n  ];\n\n  let postRun: Array<() => void> = [];\n\n  while (stack.length > 0) {\n    const [index, set] = stack.pop()!;\n\n    switch (index) {\n      case UNDEFINED:\n        set(undefined);\n        continue;\n      case NULL:\n        set(null);\n        continue;\n      case NAN:\n        set(NaN);\n        continue;\n      case POSITIVE_INFINITY:\n        set(Infinity);\n        continue;\n      case NEGATIVE_INFINITY:\n        set(-Infinity);\n        continue;\n      case NEGATIVE_ZERO:\n        set(-0);\n        continue;\n    }\n\n    if (hydrated[index]) {\n      set(hydrated[index]);\n      continue;\n    }\n\n    const value = values[index];\n    if (!value || typeof value !== \"object\") {\n      hydrated[index] = value;\n      set(value);\n      continue;\n    }\n\n    if (Array.isArray(value)) {\n      if (typeof value[0] === \"string\") {\n        const [type, b, c] = value;\n        switch (type) {\n          case TYPE_DATE:\n            set((hydrated[index] = new Date(b)));\n            continue;\n          case TYPE_URL:\n            set((hydrated[index] = new URL(b)));\n            continue;\n          case TYPE_BIGINT:\n            set((hydrated[index] = BigInt(b)));\n            continue;\n          case TYPE_REGEXP:\n            set((hydrated[index] = new RegExp(b, c)));\n            continue;\n          case TYPE_SYMBOL:\n            set((hydrated[index] = Symbol.for(b)));\n            continue;\n          case TYPE_SET:\n            const newSet = new Set();\n            hydrated[index] = newSet;\n            for (let i = value.length - 1; i > 0; i--)\n              stack.push([\n                value[i],\n                (v) => {\n                  newSet.add(v);\n                },\n              ]);\n            set(newSet);\n            continue;\n          case TYPE_MAP:\n            const map = new Map();\n            hydrated[index] = map;\n            for (let i = value.length - 2; i > 0; i -= 2) {\n              const r: any[] = [];\n              stack.push([\n                value[i + 1],\n                (v) => {\n                  r[1] = v;\n                },\n              ]);\n              stack.push([\n                value[i],\n                (k) => {\n                  r[0] = k;\n                },\n              ]);\n              postRun.push(() => {\n                map.set(r[0], r[1]);\n              });\n            }\n            set(map);\n            continue;\n          case TYPE_NULL_OBJECT:\n            const obj = Object.create(null);\n            hydrated[index] = obj;\n            for (const key of Object.keys(b).reverse()) {\n              const r: any[] = [];\n              stack.push([\n                b[key],\n                (v) => {\n                  r[1] = v;\n                },\n              ]);\n              stack.push([\n                Number(key.slice(1)),\n                (k) => {\n                  r[0] = k;\n                },\n              ]);\n              postRun.push(() => {\n                obj[r[0]] = r[1];\n              });\n            }\n            set(obj);\n            continue;\n          case TYPE_PROMISE:\n            if (hydrated[b]) {\n              set((hydrated[index] = hydrated[b]));\n            } else {\n              const d = new Deferred();\n              deferred[b] = d;\n              set((hydrated[index] = d.promise));\n            }\n            continue;\n          case TYPE_ERROR:\n            const [, message, errorType] = value;\n            let error =\n              errorType && globalObj && globalObj[errorType]\n                ? new globalObj[errorType](message)\n                : new Error(message);\n            hydrated[index] = error;\n            set(error);\n            continue;\n          case TYPE_PREVIOUS_RESOLVED:\n            set((hydrated[index] = hydrated[b]));\n            continue;\n          default:\n            // Run plugins at the end so we have a chance to resolve primitives\n            // without running into a loop\n            if (Array.isArray(plugins)) {\n              const r: unknown[] = [];\n              const vals = value.slice(1);\n              for (let i = 0; i < vals.length; i++) {\n                const v = vals[i];\n                stack.push([\n                  v,\n                  (v) => {\n                    r[i] = v;\n                  },\n                ]);\n              }\n              postRun.push(() => {\n                for (const plugin of plugins) {\n                  const result = plugin(value[0], ...r);\n                  if (result) {\n                    set((hydrated[index] = result.value));\n                    return;\n                  }\n                }\n                throw new SyntaxError();\n              });\n              continue;\n            }\n            throw new SyntaxError();\n        }\n      } else {\n        const array: unknown[] = [];\n        hydrated[index] = array;\n\n        for (let i = 0; i < value.length; i++) {\n          const n = value[i];\n          if (n !== HOLE) {\n            stack.push([\n              n,\n              (v) => {\n                array[i] = v;\n              },\n            ]);\n          }\n        }\n        set(array);\n        continue;\n      }\n    } else {\n      const object: Record<string, unknown> = {};\n      hydrated[index] = object;\n\n      for (const key of Object.keys(value).reverse()) {\n        const r: any[] = [];\n        stack.push([\n          (value as Record<string, number>)[key],\n          (v) => {\n            r[1] = v;\n          },\n        ]);\n        stack.push([\n          Number(key.slice(1)),\n          (k) => {\n            r[0] = k;\n          },\n        ]);\n        postRun.push(() => {\n          object[r[0]] = r[1];\n        });\n      }\n      set(object);\n      continue;\n    }\n  }\n\n  while (postRun.length > 0) {\n    postRun.pop()!();\n  }\n\n  return result;\n}\n"
  },
  {
    "path": "packages/react-router/vendor/turbo-stream-v2/utils.ts",
    "content": "export const HOLE = -1;\nexport const NAN = -2;\nexport const NEGATIVE_INFINITY = -3;\nexport const NEGATIVE_ZERO = -4;\nexport const NULL = -5;\nexport const POSITIVE_INFINITY = -6;\nexport const UNDEFINED = -7;\n\nexport const TYPE_BIGINT = \"B\";\nexport const TYPE_DATE = \"D\";\nexport const TYPE_ERROR = \"E\";\nexport const TYPE_MAP = \"M\";\nexport const TYPE_NULL_OBJECT = \"N\";\nexport const TYPE_PROMISE = \"P\";\nexport const TYPE_REGEXP = \"R\";\nexport const TYPE_SET = \"S\";\nexport const TYPE_SYMBOL = \"Y\";\nexport const TYPE_URL = \"U\";\nexport const TYPE_PREVIOUS_RESOLVED = \"Z\";\n\nexport type DecodePlugin = (\n  type: string,\n  ...data: unknown[]\n) => { value: unknown } | false | null | undefined;\n\nexport type EncodePlugin = (\n  value: unknown,\n) => [string, ...unknown[]] | false | null | undefined;\n\nexport interface ThisDecode {\n  values: unknown[];\n  hydrated: unknown[];\n  deferred: Record<number, Deferred<unknown>>;\n  plugins?: DecodePlugin[];\n}\n\nexport interface ThisEncode {\n  index: number;\n  indices: Map<unknown, number>;\n  stringified: string[];\n  deferred: Record<number, Promise<unknown>>;\n  plugins?: EncodePlugin[];\n  postPlugins?: EncodePlugin[];\n  signal?: AbortSignal;\n}\n\nexport class Deferred<T = unknown> {\n  promise: Promise<T>;\n  resolve!: (value: T) => void;\n  reject!: (reason: unknown) => void;\n\n  constructor() {\n    this.promise = new Promise<T>((resolve, reject) => {\n      this.resolve = resolve;\n      this.reject = reject;\n    });\n  }\n}\n\nexport function createLineSplittingTransform() {\n  const decoder = new TextDecoder();\n  let leftover = \"\";\n\n  return new TransformStream({\n    transform(chunk, controller) {\n      const str = decoder.decode(chunk, { stream: true });\n      const parts = (leftover + str).split(\"\\n\");\n\n      // The last part might be a partial line, so keep it for the next chunk.\n      leftover = parts.pop() || \"\";\n\n      for (const part of parts) {\n        controller.enqueue(part);\n      }\n    },\n\n    flush(controller) {\n      // If there's any leftover data, enqueue it before closing.\n      if (leftover) {\n        controller.enqueue(leftover);\n      }\n    },\n  });\n}\n"
  },
  {
    "path": "packages/react-router-architect/CHANGELOG.md",
    "content": "# `@react-router/architect`\n\n## 7.13.1\n\n### Patch Changes\n\n- Updated dependencies:\n  - `react-router@7.13.1`\n  - `@react-router/node@7.13.1`\n\n## 7.13.0\n\n### Patch Changes\n\n- Updated dependencies:\n  - `react-router@7.13.0`\n  - `@react-router/node@7.13.0`\n\n## 7.12.0\n\n### Patch Changes\n\n- Updated dependencies:\n  - `react-router@7.12.0`\n  - `@react-router/node@7.12.0`\n\n## 7.11.0\n\n### Patch Changes\n\n- Updated dependencies:\n  - `react-router@7.11.0`\n  - `@react-router/node@7.11.0`\n\n## 7.10.1\n\n### Patch Changes\n\n- Updated dependencies:\n  - `react-router@7.10.1`\n  - `@react-router/node@7.10.1`\n\n## 7.10.0\n\n### Patch Changes\n\n- Updated dependencies:\n  - `react-router@7.10.0`\n  - `@react-router/node@7.10.0`\n\n## 7.9.6\n\n### Patch Changes\n\n- Updated dependencies:\n  - `react-router@7.9.6`\n  - `@react-router/node@7.9.6`\n\n## 7.9.5\n\n### Patch Changes\n\n- Updated dependencies:\n  - `react-router@7.9.5`\n  - `@react-router/node@7.9.5`\n\n## 7.9.4\n\n### Patch Changes\n\n- Updated dependencies:\n  - `react-router@7.9.4`\n  - `@react-router/node@7.9.4`\n\n## 7.9.3\n\n### Patch Changes\n\n- Updated dependencies:\n  - `react-router@7.9.3`\n  - `@react-router/node@7.9.3`\n\n## 7.9.2\n\n### Patch Changes\n\n- Updated dependencies:\n  - `react-router@7.9.2`\n  - `@react-router/node@7.9.2`\n\n## 7.9.1\n\n### Patch Changes\n\n- Updated dependencies:\n  - `react-router@7.9.1`\n  - `@react-router/node@7.9.1`\n\n## 7.9.0\n\n### Minor Changes\n\n- Stabilize middleware and context APIs. ([#14215](https://github.com/remix-run/react-router/pull/14215))\n\n  We have removed the `unstable_` prefix from the following APIs and they are now considered stable and ready for production use:\n  - [`RouterContextProvider`](https://reactrouter.com/api/utils/RouterContextProvider)\n  - [`createContext`](https://reactrouter.com/api/utils/createContext)\n  - `createBrowserRouter` [`getContext`](https://reactrouter.com/api/data-routers/createBrowserRouter#optsgetcontext) option\n  - `<HydratedRouter>` [`getContext`](https://reactrouter.com/api/framework-routers/HydratedRouter#getcontext) prop\n\n  Please see the [Middleware Docs](https://reactrouter.com/how-to/middleware), the [Middleware RFC](https://github.com/remix-run/remix/discussions/7642), and the [Client-side Context RFC](https://github.com/remix-run/react-router/discussions/9856) for more information.\n\n### Patch Changes\n\n- Updated dependencies:\n  - `react-router@7.9.0`\n  - `@react-router/node@7.9.0`\n\n## 7.8.2\n\n### Patch Changes\n\n- Updated dependencies:\n  - `react-router@7.8.2`\n  - `@react-router/node@7.8.2`\n\n## 7.8.1\n\n### Patch Changes\n\n- Updated dependencies:\n  - `react-router@7.8.1`\n  - `@react-router/node@7.8.1`\n\n## 7.8.0\n\n### Patch Changes\n\n- \\[UNSTABLE] Change `getLoadContext` signature (`type GetLoadContextFunction`) when `future.unstable_middleware` is enabled so that it returns an `unstable_RouterContextProvider` instance instead of a `Map` used to contruct the instance internally ([#14097](https://github.com/remix-run/react-router/pull/14097))\n  - This also removes the `type unstable_InitialContext` export\n  - ⚠️ This is a breaking change if you have adopted middleware and are using a custom server with a `getLoadContext` function\n\n- Updated dependencies:\n  - `react-router@7.8.0`\n  - `@react-router/node@7.8.0`\n\n## 7.7.1\n\n### Patch Changes\n\n- Updated dependencies:\n  - `react-router@7.7.1`\n  - `@react-router/node@7.7.1`\n\n## 7.7.0\n\n### Patch Changes\n\n- Updated dependencies:\n  - `react-router@7.7.0`\n  - `@react-router/node@7.7.0`\n\n## 7.6.3\n\n### Patch Changes\n\n- Updated dependencies:\n  - `@react-router/node@7.6.3`\n  - `react-router@7.6.3`\n\n## 7.6.2\n\n### Patch Changes\n\n- Updated dependencies:\n  - `react-router@7.6.2`\n  - `@react-router/node@7.6.2`\n\n## 7.6.1\n\n### Patch Changes\n\n- Update `@architect/functions` from `^5.2.0` to `^7.0.0` ([#13556](https://github.com/remix-run/react-router/pull/13556))\n- Updated dependencies:\n  - `react-router@7.6.1`\n  - `@react-router/node@7.6.1`\n\n## 7.6.0\n\n### Patch Changes\n\n- Updated dependencies:\n  - `react-router@7.6.0`\n  - `@react-router/node@7.6.0`\n\n## 7.5.3\n\n### Patch Changes\n\n- Updated dependencies:\n  - `react-router@7.5.3`\n  - `@react-router/node@7.5.3`\n\n## 7.5.2\n\n### Patch Changes\n\n- Updated dependencies:\n  - `react-router@7.5.2`\n  - `@react-router/node@7.5.2`\n\n## 7.5.1\n\n### Patch Changes\n\n- Updated dependencies:\n  - `react-router@7.5.1`\n  - `@react-router/node@7.5.1`\n\n## 7.5.0\n\n### Patch Changes\n\n- Updated dependencies:\n  - `react-router@7.5.0`\n  - `@react-router/node@7.5.0`\n\n## 7.4.1\n\n### Patch Changes\n\n- Updated dependencies:\n  - `react-router@7.4.1`\n  - `@react-router/node@7.4.1`\n\n## 7.4.0\n\n### Patch Changes\n\n- Updated dependencies:\n  - `react-router@7.4.0`\n  - `@react-router/node@7.4.0`\n\n## 7.3.0\n\n### Patch Changes\n\n- Updated dependencies:\n  - `react-router@7.3.0`\n  - `@react-router/node@7.3.0`\n\n## 7.2.0\n\n### Patch Changes\n\n- Updated dependencies:\n  - `react-router@7.2.0`\n  - `@react-router/node@7.2.0`\n\n## 7.1.5\n\n### Patch Changes\n\n- Updated dependencies:\n  - `react-router@7.1.5`\n  - `@react-router/node@7.1.5`\n\n## 7.1.4\n\n### Patch Changes\n\n- Updated dependencies:\n  - `react-router@7.1.4`\n  - `@react-router/node@7.1.4`\n\n## 7.1.3\n\n### Patch Changes\n\n- Updated dependencies:\n  - `react-router@7.1.3`\n  - `@react-router/node@7.1.3`\n\n## 7.1.2\n\n### Patch Changes\n\n- Updated dependencies:\n  - `react-router@7.1.2`\n  - `@react-router/node@7.1.2`\n\n## 7.1.1\n\n### Patch Changes\n\n- Updated dependencies:\n  - `react-router@7.1.1`\n  - `@react-router/node@7.1.1`\n\n## 7.1.0\n\n### Patch Changes\n\n- Updated dependencies:\n  - `react-router@7.1.0`\n  - `@react-router/node@7.1.0`\n\n## 7.0.2\n\n### Patch Changes\n\n- Updated dependencies:\n  - `react-router@7.0.2`\n  - `@react-router/node@7.0.2`\n\n## 7.0.1\n\n### Patch Changes\n\n- Updated dependencies:\n  - `react-router@7.0.1`\n  - `@react-router/node@7.0.1`\n\n## 7.0.0\n\n### Major Changes\n\n- For Remix consumers migrating to React Router, the `crypto` global from the [Web Crypto API](https://developer.mozilla.org/en-US/docs/Web/API/Web_Crypto_API) is now required when using cookie and session APIs. This means that the following APIs are provided from `react-router` rather than platform-specific packages: ([#11837](https://github.com/remix-run/react-router/pull/11837))\n  - `createCookie`\n  - `createCookieSessionStorage`\n  - `createMemorySessionStorage`\n  - `createSessionStorage`\n\n  For consumers running older versions of Node, the `installGlobals` function from `@remix-run/node` has been updated to define `globalThis.crypto`, using [Node's `require('node:crypto').webcrypto` implementation.](https://nodejs.org/api/webcrypto.html)\n\n  Since platform-specific packages no longer need to implement this API, the following low-level APIs have been removed:\n  - `createCookieFactory`\n  - `createSessionStorageFactory`\n  - `createCookieSessionStorageFactory`\n  - `createMemorySessionStorageFactory`\n\n### Patch Changes\n\n- Updated dependencies:\n  - `react-router@7.0.0`\n  - `@react-router/node@7.0.0`\n"
  },
  {
    "path": "packages/react-router-architect/README.md",
    "content": "[Architect](https://arc.codes) server request handler for React Router.\n\n```bash\nnpm install @react-router/architect\n```\n"
  },
  {
    "path": "packages/react-router-architect/__tests__/binaryTypes-test.ts",
    "content": "import { isBinaryType } from \"../binaryTypes\";\n\ndescribe(\"architect isBinaryType\", () => {\n  it(\"should detect binary contentType correctly\", () => {\n    expect(isBinaryType(undefined)).toBe(false);\n    expect(isBinaryType(null)).toBe(false);\n    expect(isBinaryType(\"text/html; charset=utf-8\")).toBe(false);\n    expect(isBinaryType(\"application/octet-stream\")).toBe(true);\n    expect(isBinaryType(\"application/octet-stream; charset=test\")).toBe(true);\n  });\n});\n"
  },
  {
    "path": "packages/react-router-architect/__tests__/server-test.ts",
    "content": "import fsp from \"node:fs/promises\";\nimport path from \"node:path\";\nimport { createRequestHandler as createReactRequestHandler } from \"react-router\";\nimport type {\n  APIGatewayProxyEventV2,\n  APIGatewayProxyStructuredResultV2,\n} from \"aws-lambda\";\nimport lambdaTester from \"lambda-tester\";\n\nimport {\n  createRequestHandler,\n  createReactRouterHeaders,\n  createReactRouterRequest,\n  sendReactRouterResponse,\n} from \"../server\";\n\n// We don't want to test that the React Router server works here,\n// we just want to test the architect adapter\njest.mock(\"react-router\", () => {\n  let original = jest.requireActual(\"react-router\");\n  return {\n    ...original,\n    createRequestHandler: jest.fn(),\n  };\n});\nlet mockedCreateRequestHandler =\n  createReactRequestHandler as jest.MockedFunction<\n    typeof createReactRequestHandler\n  >;\n\nfunction createMockEvent(event: Partial<APIGatewayProxyEventV2> = {}) {\n  let now = new Date();\n  return {\n    headers: {\n      host: \"localhost:3333\",\n      accept: \"text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8\",\n      \"upgrade-insecure-requests\": \"1\",\n      \"user-agent\":\n        \"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.0 Safari/605.1.15\",\n      \"accept-language\": \"en-US,en;q=0.9\",\n      \"accept-encoding\": \"gzip, deflate\",\n      ...event.headers,\n    },\n    isBase64Encoded: false,\n    rawPath: \"/\",\n    rawQueryString: \"\",\n    requestContext: {\n      http: {\n        method: \"GET\",\n        path: \"/\",\n        protocol: \"HTTP/1.1\",\n        userAgent:\n          \"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.0 Safari/605.1.15\",\n        sourceIp: \"127.0.0.1\",\n        ...event.requestContext?.http,\n      },\n      routeKey: \"ANY /{proxy+}\",\n      accountId: \"accountId\",\n      requestId: \"requestId\",\n      apiId: \"apiId\",\n      domainName: \"id.execute-api.us-east-1.amazonaws.com\",\n      domainPrefix: \"id\",\n      stage: \"test\",\n      time: now.toISOString(),\n      timeEpoch: now.getTime(),\n      ...event.requestContext,\n    },\n    routeKey: \"foo\",\n    version: \"2.0\",\n    ...event,\n  };\n}\n\ndescribe(\"architect createRequestHandler\", () => {\n  describe(\"basic requests\", () => {\n    afterEach(() => {\n      mockedCreateRequestHandler.mockReset();\n    });\n\n    afterAll(() => {\n      jest.restoreAllMocks();\n    });\n\n    it(\"handles requests\", async () => {\n      mockedCreateRequestHandler.mockImplementation(() => async (req) => {\n        return new Response(`URL: ${new URL(req.url).pathname}`);\n      });\n\n      // We don't have a real app to test, but it doesn't matter. We won't ever\n      // call through to the real createRequestHandler\n      // @ts-expect-error\n      await lambdaTester(createRequestHandler({ build: undefined }))\n        .event(createMockEvent({ rawPath: \"/foo/bar\" }))\n        .expectResolve((res: any) => {\n          expect(res.statusCode).toBe(200);\n          expect(res.body).toBe(\"URL: /foo/bar\");\n        });\n    });\n\n    it(\"handles root // requests\", async () => {\n      mockedCreateRequestHandler.mockImplementation(() => async (req) => {\n        return new Response(`URL: ${new URL(req.url).pathname}`);\n      });\n\n      // We don't have a real app to test, but it doesn't matter. We won't ever\n      // call through to the real createRequestHandler\n      // @ts-expect-error\n      await lambdaTester(createRequestHandler({ build: undefined }))\n        .event(createMockEvent({ rawPath: \"//\" }))\n        .expectResolve((res: any) => {\n          expect(res.statusCode).toBe(200);\n          expect(res.body).toBe(\"URL: //\");\n        });\n    });\n\n    it(\"handles nested // requests\", async () => {\n      mockedCreateRequestHandler.mockImplementation(() => async (req) => {\n        return new Response(`URL: ${new URL(req.url).pathname}`);\n      });\n\n      // We don't have a real app to test, but it doesn't matter. We won't ever\n      // call through to the real createRequestHandler\n      // @ts-expect-error\n      await lambdaTester(createRequestHandler({ build: undefined }))\n        .event(createMockEvent({ rawPath: \"//foo//bar\" }))\n        .expectResolve((res: APIGatewayProxyStructuredResultV2) => {\n          expect(res.statusCode).toBe(200);\n          expect(res.body).toBe(\"URL: //foo//bar\");\n        });\n    });\n\n    it(\"handles null body\", async () => {\n      mockedCreateRequestHandler.mockImplementation(() => async () => {\n        return new Response(null, { status: 200 });\n      });\n\n      // We don't have a real app to test, but it doesn't matter. We won't ever\n      // call through to the real createRequestHandler\n      // @ts-expect-error\n      await lambdaTester(createRequestHandler({ build: undefined }))\n        .event(createMockEvent({ rawPath: \"/foo/bar\" }))\n        .expectResolve((res: APIGatewayProxyStructuredResultV2) => {\n          expect(res.statusCode).toBe(200);\n        });\n    });\n\n    it(\"handles status codes\", async () => {\n      mockedCreateRequestHandler.mockImplementation(() => async () => {\n        return new Response(null, { status: 204 });\n      });\n\n      // We don't have a real app to test, but it doesn't matter. We won't ever\n      // call through to the real createRequestHandler\n      // @ts-expect-error\n      await lambdaTester(createRequestHandler({ build: undefined }))\n        .event(createMockEvent({ rawPath: \"/foo/bar\" }))\n        .expectResolve((res: APIGatewayProxyStructuredResultV2) => {\n          expect(res.statusCode).toBe(204);\n        });\n    });\n\n    it(\"sets headers\", async () => {\n      mockedCreateRequestHandler.mockImplementation(() => async () => {\n        let headers = new Headers();\n        headers.append(\"X-Time-Of-Year\", \"most wonderful\");\n        headers.append(\n          \"Set-Cookie\",\n          \"first=one; Expires=0; Path=/; HttpOnly; Secure; SameSite=Lax\",\n        );\n        headers.append(\n          \"Set-Cookie\",\n          \"second=two; MaxAge=1209600; Path=/; HttpOnly; Secure; SameSite=Lax\",\n        );\n        headers.append(\n          \"Set-Cookie\",\n          \"third=three; Expires=Wed, 21 Oct 2015 07:28:00 GMT; Path=/; HttpOnly; Secure; SameSite=Lax\",\n        );\n\n        return new Response(null, { headers });\n      });\n\n      // We don't have a real app to test, but it doesn't matter. We won't ever\n      // call through to the real createRequestHandler\n      // @ts-expect-error\n      await lambdaTester(createRequestHandler({ build: undefined }))\n        .event(createMockEvent({ rawPath: \"/\" }))\n        .expectResolve((res: APIGatewayProxyStructuredResultV2) => {\n          expect(res.statusCode).toBe(200);\n          expect(res.headers?.[\"x-time-of-year\"]).toBe(\"most wonderful\");\n          expect(res.cookies).toEqual([\n            \"first=one; Expires=0; Path=/; HttpOnly; Secure; SameSite=Lax\",\n            \"second=two; MaxAge=1209600; Path=/; HttpOnly; Secure; SameSite=Lax\",\n            \"third=three; Expires=Wed, 21 Oct 2015 07:28:00 GMT; Path=/; HttpOnly; Secure; SameSite=Lax\",\n          ]);\n        });\n    });\n  });\n});\n\ndescribe(\"architect createReactRouterHeaders\", () => {\n  describe(\"creates fetch headers from architect headers\", () => {\n    it(\"handles empty headers\", () => {\n      let headers = createReactRouterHeaders({});\n      expect(Object.fromEntries(headers.entries())).toMatchInlineSnapshot(`{}`);\n    });\n\n    it(\"handles simple headers\", () => {\n      let headers = createReactRouterHeaders({ \"x-foo\": \"bar\" });\n      expect(headers.get(\"x-foo\")).toBe(\"bar\");\n    });\n\n    it(\"handles multiple headers\", () => {\n      let headers = createReactRouterHeaders({\n        \"x-foo\": \"bar\",\n        \"x-bar\": \"baz\",\n      });\n      expect(headers.get(\"x-foo\")).toBe(\"bar\");\n      expect(headers.get(\"x-bar\")).toBe(\"baz\");\n    });\n\n    it(\"handles headers with multiple values\", () => {\n      let headers = createReactRouterHeaders({\n        \"x-foo\": \"bar, baz\",\n        \"x-bar\": \"baz\",\n      });\n      expect(headers.get(\"x-foo\")).toEqual(\"bar, baz\");\n      expect(headers.get(\"x-bar\")).toBe(\"baz\");\n    });\n\n    it(\"handles multiple request cookies\", () => {\n      let headers = createReactRouterHeaders({}, [\n        \"__session=some_value\",\n        \"__other=some_other_value\",\n      ]);\n      expect(headers.get(\"cookie\")).toEqual(\n        \"__session=some_value; __other=some_other_value\",\n      );\n    });\n  });\n});\n\ndescribe(\"architect createReactRouterRequest\", () => {\n  it(\"creates a request with the correct headers\", () => {\n    let request = createReactRouterRequest(\n      createMockEvent({ cookies: [\"__session=value\"] }),\n    );\n\n    expect(request.method).toBe(\"GET\");\n    expect(request.headers.get(\"cookie\")).toBe(\"__session=value\");\n  });\n});\n\ndescribe(\"sendReactRouterResponse\", () => {\n  it(\"handles regular responses\", async () => {\n    let response = new Response(\"anything\");\n    let result = await sendReactRouterResponse(response);\n    expect(result.body).toBe(\"anything\");\n  });\n\n  it(\"handles resource routes with regular data\", async () => {\n    let json = JSON.stringify({ foo: \"bar\" });\n    let response = new Response(json, {\n      headers: {\n        \"Content-Type\": \"application/json\",\n        \"content-length\": json.length.toString(),\n      },\n    });\n\n    let result = await sendReactRouterResponse(response);\n\n    expect(result.body).toMatch(json);\n  });\n\n  it(\"handles resource routes with binary data\", async () => {\n    let image = await fsp.readFile(path.join(__dirname, \"554828.jpeg\"));\n\n    let response = new Response(image, {\n      headers: {\n        \"content-type\": \"image/jpeg\",\n        \"content-length\": image.length.toString(),\n      },\n    });\n\n    let result = await sendReactRouterResponse(response);\n\n    expect(result.body).toMatch(image.toString(\"base64\"));\n  });\n});\n"
  },
  {
    "path": "packages/react-router-architect/binaryTypes.ts",
    "content": "/**\n * Common binary MIME types\n * @see https://github.com/architect/functions/blob/45254fc1936a1794c185aac07e9889b241a2e5c6/src/http/helpers/binary-types.js\n */\nconst binaryTypes = [\n  \"application/octet-stream\",\n  // Docs\n  \"application/epub+zip\",\n  \"application/msword\",\n  \"application/pdf\",\n  \"application/rtf\",\n  \"application/vnd.amazon.ebook\",\n  \"application/vnd.ms-excel\",\n  \"application/vnd.ms-powerpoint\",\n  \"application/vnd.openxmlformats-officedocument.presentationml.presentation\",\n  \"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet\",\n  \"application/vnd.openxmlformats-officedocument.wordprocessingml.document\",\n  // Fonts\n  \"font/otf\",\n  \"font/woff\",\n  \"font/woff2\",\n  // Images\n  \"image/avif\",\n  \"image/bmp\",\n  \"image/gif\",\n  \"image/jpeg\",\n  \"image/png\",\n  \"image/tiff\",\n  \"image/vnd.microsoft.icon\",\n  \"image/webp\",\n  // Audio\n  \"audio/3gpp\",\n  \"audio/aac\",\n  \"audio/basic\",\n  \"audio/mpeg\",\n  \"audio/ogg\",\n  \"audio/wav\",\n  \"audio/webm\",\n  \"audio/x-aiff\",\n  \"audio/x-midi\",\n  \"audio/x-wav\",\n  // Video\n  \"video/3gpp\",\n  \"video/mp2t\",\n  \"video/mpeg\",\n  \"video/ogg\",\n  \"video/quicktime\",\n  \"video/webm\",\n  \"video/x-msvideo\",\n  // Archives\n  \"application/java-archive\",\n  \"application/vnd.apple.installer+xml\",\n  \"application/x-7z-compressed\",\n  \"application/x-apple-diskimage\",\n  \"application/x-bzip\",\n  \"application/x-bzip2\",\n  \"application/x-gzip\",\n  \"application/x-java-archive\",\n  \"application/x-rar-compressed\",\n  \"application/x-tar\",\n  \"application/x-zip\",\n  \"application/zip\",\n];\n\nexport function isBinaryType(contentType: string | null | undefined) {\n  if (!contentType) return false;\n  let [test] = contentType.split(\";\");\n  return binaryTypes.includes(test);\n}\n"
  },
  {
    "path": "packages/react-router-architect/index.ts",
    "content": "export { createArcTableSessionStorage } from \"./sessions/arcTableSessionStorage\";\n\nexport type { GetLoadContextFunction, RequestHandler } from \"./server\";\nexport { createRequestHandler } from \"./server\";\n"
  },
  {
    "path": "packages/react-router-architect/jest.config.js",
    "content": "/** @type {import('jest').Config} */\nmodule.exports = {\n  ...require(\"../../jest/jest.config.shared\"),\n  displayName: \"architect\",\n};\n"
  },
  {
    "path": "packages/react-router-architect/package.json",
    "content": "{\n  \"name\": \"@react-router/architect\",\n  \"version\": \"7.13.1\",\n  \"description\": \"Architect server request handler for React Router\",\n  \"bugs\": {\n    \"url\": \"https://github.com/remix-run/react-router/issues\"\n  },\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"https://github.com/remix-run/react-router\",\n    \"directory\": \"packages/react-router-architect\"\n  },\n  \"license\": \"MIT\",\n  \"main\": \"dist/index.js\",\n  \"typings\": \"dist/index.d.ts\",\n  \"exports\": {\n    \".\": {\n      \"node\": {\n        \"types\": \"./dist/index.d.ts\",\n        \"module-sync\": \"./dist/index.mjs\",\n        \"default\": \"./dist/index.js\"\n      },\n      \"import\": {\n        \"types\": \"./dist/index.d.mts\",\n        \"default\": \"./dist/index.mjs\"\n      },\n      \"default\": {\n        \"types\": \"./dist/index.d.ts\",\n        \"default\": \"./dist/index.js\"\n      }\n    },\n    \"./package.json\": \"./package.json\"\n  },\n  \"scripts\": {\n    \"build\": \"wireit\",\n    \"typecheck\": \"tsc\"\n  },\n  \"wireit\": {\n    \"build\": {\n      \"command\": \"tsup\",\n      \"files\": [\n        \"../../pnpm-workspace.yaml\",\n        \"sessions/**\",\n        \"*.ts\",\n        \"tsconfig.json\",\n        \"package.json\"\n      ],\n      \"output\": [\n        \"dist/**\"\n      ]\n    }\n  },\n  \"dependencies\": {\n    \"@architect/functions\": \"^7.0.0\",\n    \"@types/aws-lambda\": \"^8.10.82\"\n  },\n  \"devDependencies\": {\n    \"@react-router/node\": \"workspace:*\",\n    \"@types/lambda-tester\": \"^3.6.1\",\n    \"@types/node\": \"^20.0.0\",\n    \"lambda-tester\": \"^4.0.1\",\n    \"react\": \"catalog:\",\n    \"react-dom\": \"catalog:\",\n    \"react-router\": \"workspace:*\",\n    \"tsup\": \"catalog:\",\n    \"typescript\": \"catalog:\",\n    \"wireit\": \"catalog:\"\n  },\n  \"peerDependencies\": {\n    \"@react-router/node\": \"workspace:^\",\n    \"react-router\": \"workspace:^\",\n    \"typescript\": \"^5.1.0\"\n  },\n  \"peerDependenciesMeta\": {\n    \"typescript\": {\n      \"optional\": true\n    }\n  },\n  \"engines\": {\n    \"node\": \">=20.0.0\"\n  },\n  \"files\": [\n    \"dist/\",\n    \"CHANGELOG.md\",\n    \"LICENSE.md\",\n    \"README.md\"\n  ]\n}\n"
  },
  {
    "path": "packages/react-router-architect/server.ts",
    "content": "import type {\n  AppLoadContext,\n  UNSAFE_MiddlewareEnabled as MiddlewareEnabled,\n  ServerBuild,\n  RouterContextProvider,\n} from \"react-router\";\nimport { createRequestHandler as createReactRouterRequestHandler } from \"react-router\";\nimport { readableStreamToString } from \"@react-router/node\";\nimport type {\n  APIGatewayProxyEventHeaders,\n  APIGatewayProxyEventV2,\n  APIGatewayProxyHandlerV2,\n  APIGatewayProxyStructuredResultV2,\n} from \"aws-lambda\";\n\nimport { isBinaryType } from \"./binaryTypes\";\n\ntype MaybePromise<T> = T | Promise<T>;\n\n/**\n * A function that returns the value to use as `context` in route `loader` and\n * `action` functions.\n *\n * You can think of this as an escape hatch that allows you to pass\n * environment/platform-specific values through to your loader/action.\n */\nexport type GetLoadContextFunction = (\n  event: APIGatewayProxyEventV2,\n) => MiddlewareEnabled extends true\n  ? MaybePromise<RouterContextProvider>\n  : MaybePromise<AppLoadContext>;\n\nexport type RequestHandler = APIGatewayProxyHandlerV2;\n\n/**\n * Returns a request handler for Architect that serves the response using\n * React Router.\n */\nexport function createRequestHandler({\n  build,\n  getLoadContext,\n  mode = process.env.NODE_ENV,\n}: {\n  build: ServerBuild;\n  getLoadContext?: GetLoadContextFunction;\n  mode?: string;\n}): RequestHandler {\n  let handleRequest = createReactRouterRequestHandler(build, mode);\n\n  return async (event) => {\n    let request = createReactRouterRequest(event);\n    let loadContext = await getLoadContext?.(event);\n\n    let response = await handleRequest(request, loadContext);\n\n    return sendReactRouterResponse(response);\n  };\n}\n\nexport function createReactRouterRequest(\n  event: APIGatewayProxyEventV2,\n): Request {\n  let host = event.headers[\"x-forwarded-host\"] || event.headers.host;\n  let search = event.rawQueryString.length ? `?${event.rawQueryString}` : \"\";\n  let scheme = process.env.ARC_SANDBOX ? \"http\" : \"https\";\n  let url = new URL(`${scheme}://${host}${event.rawPath}${search}`);\n  let isFormData = event.headers[\"content-type\"]?.includes(\n    \"multipart/form-data\",\n  );\n  // Note: No current way to abort these for Architect, but our router expects\n  // requests to contain a signal, so it can detect aborted requests\n  let controller = new AbortController();\n\n  return new Request(url.href, {\n    method: event.requestContext.http.method,\n    headers: createReactRouterHeaders(event.headers, event.cookies),\n    signal: controller.signal,\n    body:\n      event.body && event.isBase64Encoded\n        ? isFormData\n          ? Buffer.from(event.body, \"base64\")\n          : Buffer.from(event.body, \"base64\").toString()\n        : event.body,\n  });\n}\n\nexport function createReactRouterHeaders(\n  requestHeaders: APIGatewayProxyEventHeaders,\n  requestCookies?: string[],\n): Headers {\n  let headers = new Headers();\n\n  for (let [header, value] of Object.entries(requestHeaders)) {\n    if (value) {\n      headers.append(header, value);\n    }\n  }\n\n  if (requestCookies) {\n    headers.append(\"Cookie\", requestCookies.join(\"; \"));\n  }\n\n  return headers;\n}\n\nexport async function sendReactRouterResponse(\n  nodeResponse: Response,\n): Promise<APIGatewayProxyStructuredResultV2> {\n  let cookies: string[] = [];\n\n  // Arc/AWS API Gateway will send back set-cookies outside of response headers.\n  for (let [key, value] of nodeResponse.headers.entries()) {\n    if (key.toLowerCase() === \"set-cookie\") {\n      cookies.push(value);\n    }\n  }\n\n  if (cookies.length) {\n    nodeResponse.headers.delete(\"Set-Cookie\");\n  }\n\n  let contentType = nodeResponse.headers.get(\"Content-Type\");\n  let isBase64Encoded = isBinaryType(contentType);\n  let body: string | undefined;\n\n  if (nodeResponse.body) {\n    if (isBase64Encoded) {\n      body = await readableStreamToString(nodeResponse.body, \"base64\");\n    } else {\n      body = await nodeResponse.text();\n    }\n  }\n\n  return {\n    statusCode: nodeResponse.status,\n    headers: Object.fromEntries(nodeResponse.headers.entries()),\n    cookies,\n    body,\n    isBase64Encoded,\n  };\n}\n"
  },
  {
    "path": "packages/react-router-architect/sessions/arcTableSessionStorage.ts",
    "content": "import type {\n  SessionData,\n  SessionStorage,\n  SessionIdStorageStrategy,\n} from \"react-router\";\nimport { createSessionStorage } from \"react-router\";\nimport arc from \"@architect/functions\";\nimport type { ArcTable } from \"@architect/functions/types/tables\";\n\ninterface ArcTableSessionStorageOptions {\n  /**\n   * The Cookie used to store the session id on the client, or options used\n   * to automatically create one.\n   */\n  cookie?: SessionIdStorageStrategy[\"cookie\"];\n\n  /**\n   * The table used to store sessions, or its name as it appears in your\n   * project's app.arc file.\n   */\n  table: ArcTable<SessionData> | string;\n\n  /**\n   * The name of the DynamoDB attribute used to store the session ID.\n   * This should be the table's partition key.\n   */\n  idx: string;\n\n  /**\n   * The name of the DynamoDB attribute used to store the expiration time.\n   * If absent, then no TTL will be stored and session records will not expire.\n   */\n  ttl?: string;\n}\n\n/**\n * Session storage using a DynamoDB table managed by Architect.\n *\n * Add the following lines to your project's `app.arc` file:\n *\n *   @tables\n *   arc-sessions\n *     _idx *String\n *     _ttl TTL\n */\nexport function createArcTableSessionStorage<\n  Data = SessionData,\n  FlashData = Data,\n>({\n  cookie,\n  ...props\n}: ArcTableSessionStorageOptions): SessionStorage<Data, FlashData> {\n  async function getTable() {\n    if (typeof props.table === \"string\") {\n      let tables = await arc.tables();\n      return tables[props.table];\n    } else {\n      return props.table;\n    }\n  }\n  return createSessionStorage({\n    cookie,\n    async createData(data, expires) {\n      let table = await getTable();\n      while (true) {\n        let randomBytes = crypto.getRandomValues(new Uint8Array(8));\n        // This storage manages an id space of 2^64 ids, which is far greater\n        // than the maximum number of files allowed on an NTFS or ext4 volume\n        // (2^32). However, the larger id space should help to avoid collisions\n        // with existing ids when creating new sessions, which speeds things up.\n        let id = [...randomBytes]\n          .map((x) => x.toString(16).padStart(2, \"0\"))\n          .join(\"\");\n\n        if (await table.get({ [props.idx]: id })) {\n          continue;\n        }\n\n        let params: Record<string, unknown> = {\n          [props.idx]: id,\n          ...data,\n        };\n        if (props.ttl) {\n          params[props.ttl] = expires\n            ? Math.round(expires.getTime() / 1000)\n            : undefined;\n        }\n        await table.put(params);\n\n        return id;\n      }\n    },\n    async readData(id) {\n      let table = await getTable();\n      let data = await table.get({ [props.idx]: id });\n      if (data) {\n        delete data[props.idx];\n        if (props.ttl) delete data[props.ttl];\n      }\n      return data;\n    },\n    async updateData(id, data, expires) {\n      let table = await getTable();\n      let params: Record<string, unknown> = {\n        [props.idx]: id,\n        ...data,\n      };\n      if (props.ttl) {\n        params[props.ttl] = expires\n          ? Math.round(expires.getTime() / 1000)\n          : undefined;\n      }\n      await table.put(params);\n    },\n    async deleteData(id) {\n      let table = await getTable();\n      await table.delete({ [props.idx]: id });\n    },\n  });\n}\n"
  },
  {
    "path": "packages/react-router-architect/tsconfig.json",
    "content": "{\n  \"include\": [\"**/*.ts\"],\n  \"exclude\": [\"dist\", \"node_modules\"],\n  \"compilerOptions\": {\n    \"lib\": [\"DOM\", \"DOM.Iterable\", \"ES2022\"],\n    \"target\": \"ES2022\",\n\n    \"moduleResolution\": \"Bundler\",\n    \"allowSyntheticDefaultImports\": true,\n    \"strict\": true,\n    \"declaration\": true,\n    \"emitDeclarationOnly\": true,\n    \"rootDir\": \".\",\n    \"outDir\": \"./dist\",\n\n    // Avoid naming conflicts between history and react-router-dom relying on\n    // lib.dom.d.ts Window and this being a WebWorker env.\n    \"skipLibCheck\": true\n  }\n}\n"
  },
  {
    "path": "packages/react-router-architect/tsup.config.ts",
    "content": "import { defineConfig } from \"tsup\";\n\n// @ts-ignore - out of scope\nimport { createBanner } from \"../../build.utils.js\";\n\nimport pkg from \"./package.json\";\n\nconst entry = [\"index.ts\"];\n\nexport default defineConfig([\n  {\n    clean: true,\n    entry,\n    format: [\"cjs\", \"esm\"],\n    outDir: \"dist\",\n    dts: true,\n    banner: {\n      js: createBanner(pkg.name, pkg.version),\n    },\n  },\n]);\n"
  },
  {
    "path": "packages/react-router-architect/typedoc.mjs",
    "content": "import { blockTags } from \"../../typedoc.mjs\";\n\n/** @type {Partial<import('typedoc').TypeDocOptions>} */\nconst config = {\n  entryPoints: [\"./index.ts\"],\n  blockTags,\n};\n\nexport default config;\n"
  },
  {
    "path": "packages/react-router-cloudflare/CHANGELOG.md",
    "content": "# `@react-router/cloudflare`\n\n## 7.13.1\n\n### Patch Changes\n\n- Updated dependencies:\n  - `react-router@7.13.1`\n\n## 7.13.0\n\n### Patch Changes\n\n- Updated dependencies:\n  - `react-router@7.13.0`\n\n## 7.12.0\n\n### Patch Changes\n\n- Updated dependencies:\n  - `react-router@7.12.0`\n\n## 7.11.0\n\n### Patch Changes\n\n- Updated dependencies:\n  - `react-router@7.11.0`\n\n## 7.10.1\n\n### Patch Changes\n\n- Updated dependencies:\n  - `react-router@7.10.1`\n\n## 7.10.0\n\n### Patch Changes\n\n- Updated dependencies:\n  - `react-router@7.10.0`\n\n## 7.9.6\n\n### Patch Changes\n\n- Updated dependencies:\n  - `react-router@7.9.6`\n\n## 7.9.5\n\n### Patch Changes\n\n- Updated dependencies:\n  - `react-router@7.9.5`\n\n## 7.9.4\n\n### Patch Changes\n\n- Updated dependencies:\n  - `react-router@7.9.4`\n\n## 7.9.3\n\n### Patch Changes\n\n- Updated dependencies:\n  - `react-router@7.9.3`\n\n## 7.9.2\n\n### Patch Changes\n\n- Updated dependencies:\n  - `react-router@7.9.2`\n\n## 7.9.1\n\n### Patch Changes\n\n- Updated dependencies:\n  - `react-router@7.9.1`\n\n## 7.9.0\n\n### Minor Changes\n\n- Stabilize middleware and context APIs. ([#14215](https://github.com/remix-run/react-router/pull/14215))\n\n  We have removed the `unstable_` prefix from the following APIs and they are now considered stable and ready for production use:\n  - [`RouterContextProvider`](https://reactrouter.com/api/utils/RouterContextProvider)\n  - [`createContext`](https://reactrouter.com/api/utils/createContext)\n  - `createBrowserRouter` [`getContext`](https://reactrouter.com/api/data-routers/createBrowserRouter#optsgetcontext) option\n  - `<HydratedRouter>` [`getContext`](https://reactrouter.com/api/framework-routers/HydratedRouter#getcontext) prop\n\n  Please see the [Middleware Docs](https://reactrouter.com/how-to/middleware), the [Middleware RFC](https://github.com/remix-run/remix/discussions/7642), and the [Client-side Context RFC](https://github.com/remix-run/react-router/discussions/9856) for more information.\n\n### Patch Changes\n\n- Updated dependencies:\n  - `react-router@7.9.0`\n\n## 7.8.2\n\n### Patch Changes\n\n- Updated dependencies:\n  - `react-router@7.8.2`\n\n## 7.8.1\n\n### Patch Changes\n\n- Updated dependencies:\n  - `react-router@7.8.1`\n\n## 7.8.0\n\n### Patch Changes\n\n- \\[UNSTABLE] Change `getLoadContext` signature (`type GetLoadContextFunction`) when `future.unstable_middleware` is enabled so that it returns an `unstable_RouterContextProvider` instance instead of a `Map` used to contruct the instance internally ([#14097](https://github.com/remix-run/react-router/pull/14097))\n  - This also removes the `type unstable_InitialContext` export\n  - ⚠️ This is a breaking change if you have adopted middleware and are using a custom server with a `getLoadContext` function\n\n- Updated dependencies:\n  - `react-router@7.8.0`\n\n## 7.7.1\n\n### Patch Changes\n\n- Updated dependencies:\n  - `react-router@7.7.1`\n\n## 7.7.0\n\n### Patch Changes\n\n- Updated dependencies:\n  - `react-router@7.7.0`\n\n## 7.6.3\n\n### Patch Changes\n\n- Remove `tsup` from `peerDependencies` ([#13757](https://github.com/remix-run/react-router/pull/13757))\n- Updated dependencies:\n  - `react-router@7.6.3`\n\n## 7.6.2\n\n### Patch Changes\n\n- Updated dependencies:\n  - `react-router@7.6.2`\n\n## 7.6.1\n\n### Patch Changes\n\n- Updated dependencies:\n  - `react-router@7.6.1`\n\n## 7.6.0\n\n### Patch Changes\n\n- Updated dependencies:\n  - `react-router@7.6.0`\n\n## 7.5.3\n\n### Patch Changes\n\n- Updated dependencies:\n  - `react-router@7.5.3`\n\n## 7.5.2\n\n### Patch Changes\n\n- Updated dependencies:\n  - `react-router@7.5.2`\n\n## 7.5.1\n\n### Patch Changes\n\n- Updated dependencies:\n  - `react-router@7.5.1`\n\n## 7.5.0\n\n### Patch Changes\n\n- Updated dependencies:\n  - `react-router@7.5.0`\n\n## 7.4.1\n\n### Patch Changes\n\n- Updated dependencies:\n  - `react-router@7.4.1`\n\n## 7.4.0\n\n### Patch Changes\n\n- Updated dependencies:\n  - `react-router@7.4.0`\n\n## 7.3.0\n\n### Patch Changes\n\n- Updated dependencies:\n  - `react-router@7.3.0`\n\n## 7.2.0\n\n### Patch Changes\n\n- Updated dependencies:\n  - `react-router@7.2.0`\n\n## 7.1.5\n\n### Patch Changes\n\n- Updated dependencies:\n  - `react-router@7.1.5`\n\n## 7.1.4\n\n### Patch Changes\n\n- Updated dependencies:\n  - `react-router@7.1.4`\n\n## 7.1.3\n\n### Patch Changes\n\n- Updated dependencies:\n  - `react-router@7.1.3`\n\n## 7.1.2\n\n### Patch Changes\n\n- Updated dependencies:\n  - `react-router@7.1.2`\n\n## 7.1.1\n\n### Patch Changes\n\n- Updated dependencies:\n  - `react-router@7.1.1`\n\n## 7.1.0\n\n### Patch Changes\n\n- Updated dependencies:\n  - `react-router@7.1.0`\n\n## 7.0.2\n\n### Patch Changes\n\n- Updated dependencies:\n  - `react-router@7.0.2`\n\n## 7.0.1\n\n### Patch Changes\n\n- Updated dependencies:\n  - `react-router@7.0.1`\n\n## 7.0.0\n\n### Major Changes\n\n- For Remix consumers migrating to React Router, all exports from `@remix-run/cloudflare-pages` are now provided for React Router consumers in the `@react-router/cloudflare` package. There is no longer a separate package for Cloudflare Pages. ([#11801](https://github.com/remix-run/react-router/pull/11801))\n- For Remix consumers migrating to React Router, the `crypto` global from the [Web Crypto API](https://developer.mozilla.org/en-US/docs/Web/API/Web_Crypto_API) is now required when using cookie and session APIs. This means that the following APIs are provided from `react-router` rather than platform-specific packages: ([#11837](https://github.com/remix-run/react-router/pull/11837))\n  - `createCookie`\n  - `createCookieSessionStorage`\n  - `createMemorySessionStorage`\n  - `createSessionStorage`\n\n  For consumers running older versions of Node, the `installGlobals` function from `@remix-run/node` has been updated to define `globalThis.crypto`, using [Node's `require('node:crypto').webcrypto` implementation.](https://nodejs.org/api/webcrypto.html)\n\n  Since platform-specific packages no longer need to implement this API, the following low-level APIs have been removed:\n  - `createCookieFactory`\n  - `createSessionStorageFactory`\n  - `createCookieSessionStorageFactory`\n  - `createMemorySessionStorageFactory`\n\n### Minor Changes\n\n- The `@remix-run/cloudflare-workers` package has been deprecated. Remix consumers migrating to React Router should use the `@react-router/cloudflare` package directly. For guidance on how to use `@react-router/cloudflare` within a Cloudflare Workers context, refer to the Cloudflare Workers template. ([#11801](https://github.com/remix-run/react-router/pull/11801))\n\n### Patch Changes\n\n- Updated dependencies:\n  - `react-router@7.0.0`\n"
  },
  {
    "path": "packages/react-router-cloudflare/README.md",
    "content": "Cloudflare platform abstractions for React Router\n\n```bash\nnpm install @react-router/cloudflare @cloudflare/workers-types\n```\n"
  },
  {
    "path": "packages/react-router-cloudflare/index.ts",
    "content": "export { createWorkersKVSessionStorage } from \"./sessions/workersKVStorage\";\n\nexport type {\n  createPagesFunctionHandlerParams,\n  GetLoadContextFunction,\n  RequestHandler,\n} from \"./worker\";\nexport { createPagesFunctionHandler, createRequestHandler } from \"./worker\";\n"
  },
  {
    "path": "packages/react-router-cloudflare/package.json",
    "content": "{\n  \"name\": \"@react-router/cloudflare\",\n  \"version\": \"7.13.1\",\n  \"description\": \"Cloudflare platform abstractions for React Router\",\n  \"bugs\": {\n    \"url\": \"https://github.com/remix-run/react-router/issues\"\n  },\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"https://github.com/remix-run/react-router\",\n    \"directory\": \"packages/react-router-cloudflare\"\n  },\n  \"license\": \"MIT\",\n  \"main\": \"dist/index.js\",\n  \"typings\": \"dist/index.d.ts\",\n  \"exports\": {\n    \".\": {\n      \"import\": {\n        \"types\": \"./dist/index.d.mts\",\n        \"default\": \"./dist/index.mjs\"\n      },\n      \"default\": {\n        \"types\": \"./dist/index.d.ts\",\n        \"default\": \"./dist/index.js\"\n      }\n    },\n    \"./package.json\": \"./package.json\"\n  },\n  \"scripts\": {\n    \"build\": \"wireit\",\n    \"typecheck\": \"tsc\"\n  },\n  \"wireit\": {\n    \"build\": {\n      \"command\": \"tsup\",\n      \"files\": [\n        \"../../pnpm-workspace.yaml\",\n        \"sessions/**\",\n        \"*.ts\",\n        \"tsconfig.json\",\n        \"package.json\"\n      ],\n      \"output\": [\n        \"dist/**\"\n      ]\n    }\n  },\n  \"devDependencies\": {\n    \"@cloudflare/workers-types\": \"^4.20250803.0\",\n    \"react-router\": \"workspace:*\",\n    \"tsup\": \"catalog:\",\n    \"typescript\": \"catalog:\",\n    \"wireit\": \"catalog:\"\n  },\n  \"peerDependencies\": {\n    \"@cloudflare/workers-types\": \"^4.0.0\",\n    \"react-router\": \"workspace:^\",\n    \"typescript\": \"^5.1.0\"\n  },\n  \"peerDependenciesMeta\": {\n    \"typescript\": {\n      \"optional\": true\n    }\n  },\n  \"engines\": {\n    \"node\": \">=20.0.0\"\n  },\n  \"files\": [\n    \"dist/\",\n    \"CHANGELOG.md\",\n    \"LICENSE.md\",\n    \"README.md\"\n  ]\n}\n"
  },
  {
    "path": "packages/react-router-cloudflare/sessions/workersKVStorage.ts",
    "content": "import type {\n  SessionStorage,\n  SessionIdStorageStrategy,\n  SessionData,\n} from \"react-router\";\nimport { createSessionStorage } from \"react-router\";\n\ninterface WorkersKVSessionStorageOptions {\n  /**\n   * The Cookie used to store the session id on the client, or options used\n   * to automatically create one.\n   */\n  cookie?: SessionIdStorageStrategy[\"cookie\"];\n\n  /**\n   * The KVNamespace used to store the sessions.\n   */\n  kv: KVNamespace;\n}\n\n/**\n * Creates a SessionStorage that stores session data in the Clouldflare KV Store.\n *\n * The advantage of using this instead of cookie session storage is that\n * KV Store may contain much more data than cookies.\n */\nexport function createWorkersKVSessionStorage<\n  Data = SessionData,\n  FlashData = Data,\n>({\n  cookie,\n  kv,\n}: WorkersKVSessionStorageOptions): SessionStorage<Data, FlashData> {\n  return createSessionStorage({\n    cookie,\n    async createData(data, expires) {\n      while (true) {\n        let randomBytes = crypto.getRandomValues(new Uint8Array(8));\n        // This storage manages an id space of 2^64 ids, which is far greater\n        // than the maximum number of files allowed on an NTFS or ext4 volume\n        // (2^32). However, the larger id space should help to avoid collisions\n        // with existing ids when creating new sessions, which speeds things up.\n        let id = [...randomBytes]\n          .map((x) => x.toString(16).padStart(2, \"0\"))\n          .join(\"\");\n\n        if (await kv.get(id, \"json\")) {\n          continue;\n        }\n\n        await kv.put(id, JSON.stringify(data), {\n          expiration: expires\n            ? Math.round(expires.getTime() / 1000)\n            : undefined,\n        });\n\n        return id;\n      }\n    },\n    async readData(id) {\n      let session = await kv.get(id);\n\n      if (!session) {\n        return null;\n      }\n\n      return JSON.parse(session);\n    },\n    async updateData(id, data, expires) {\n      await kv.put(id, JSON.stringify(data), {\n        expiration: expires ? Math.round(expires.getTime() / 1000) : undefined,\n      });\n    },\n    async deleteData(id) {\n      await kv.delete(id);\n    },\n  });\n}\n"
  },
  {
    "path": "packages/react-router-cloudflare/tsconfig.json",
    "content": "{\n  \"include\": [\"**/*.ts\"],\n  \"exclude\": [\"dist\", \"__tests__\", \"node_modules\"],\n  \"compilerOptions\": {\n    \"lib\": [\"ES2022\"],\n    \"target\": \"ES2022\",\n    \"types\": [\"@cloudflare/workers-types\"],\n\n    \"moduleResolution\": \"Bundler\",\n    \"allowSyntheticDefaultImports\": true,\n    \"strict\": true,\n    \"declaration\": true,\n    \"emitDeclarationOnly\": true,\n    \"rootDir\": \".\",\n    \"outDir\": \"./dist\",\n\n    // Avoid naming conflicts between history and react-router-dom relying on\n    // lib.dom.d.ts Window and this being a WebWorker env.\n    \"skipLibCheck\": true\n  }\n}\n"
  },
  {
    "path": "packages/react-router-cloudflare/tsup.config.ts",
    "content": "import { defineConfig } from \"tsup\";\n\n// @ts-ignore - out of scope\nimport { createBanner } from \"../../build.utils.js\";\n\nimport pkg from \"./package.json\";\n\nconst entry = [\"index.ts\"];\n\nexport default defineConfig([\n  {\n    clean: true,\n    entry,\n    format: [\"cjs\", \"esm\"],\n    outDir: \"dist\",\n    dts: true,\n    banner: {\n      js: createBanner(pkg.name, pkg.version),\n    },\n  },\n]);\n"
  },
  {
    "path": "packages/react-router-cloudflare/typedoc.mjs",
    "content": "import { blockTags } from \"../../typedoc.mjs\";\n\n/** @type {Partial<import('typedoc').TypeDocOptions>} */\nconst config = {\n  entryPoints: [\"./index.ts\"],\n  blockTags,\n};\n\nexport default config;\n"
  },
  {
    "path": "packages/react-router-cloudflare/worker.ts",
    "content": "import type {\n  AppLoadContext,\n  UNSAFE_MiddlewareEnabled as MiddlewareEnabled,\n  ServerBuild,\n  RouterContextProvider,\n} from \"react-router\";\nimport { createRequestHandler as createReactRouterRequestHandler } from \"react-router\";\nimport { type CacheStorage } from \"@cloudflare/workers-types\";\n\ntype MaybePromise<T> = T | Promise<T>;\n\n/**\n * A function that returns the value to use as `context` in route `loader` and\n * `action` functions.\n *\n * You can think of this as an escape hatch that allows you to pass\n * environment/platform-specific values through to your loader/action.\n */\nexport type GetLoadContextFunction<\n  Env = unknown,\n  Params extends string = any,\n  Data extends Record<string, unknown> = Record<string, unknown>,\n> = (args: {\n  request: Request;\n  context: {\n    cloudflare: EventContext<Env, Params, Data> & {\n      cf: EventContext<Env, Params, Data>[\"request\"][\"cf\"];\n      ctx: {\n        waitUntil: EventContext<Env, Params, Data>[\"waitUntil\"];\n        passThroughOnException: EventContext<\n          Env,\n          Params,\n          Data\n        >[\"passThroughOnException\"];\n      };\n      caches: CacheStorage;\n    };\n  };\n}) => MiddlewareEnabled extends true\n  ? MaybePromise<RouterContextProvider>\n  : MaybePromise<AppLoadContext>;\n\nexport type RequestHandler<Env = any> = PagesFunction<Env>;\n\nexport interface createPagesFunctionHandlerParams<Env = any> {\n  build: ServerBuild | (() => ServerBuild | Promise<ServerBuild>);\n  getLoadContext?: GetLoadContextFunction<Env>;\n  mode?: string;\n}\n\nexport function createRequestHandler<Env = any>({\n  build,\n  mode,\n  getLoadContext = ({ context }) => ({\n    ...context,\n    cloudflare: {\n      ...context.cloudflare,\n      cf: context.cloudflare.request.cf,\n    },\n  }),\n}: createPagesFunctionHandlerParams<Env>): RequestHandler<Env> {\n  let handleRequest = createReactRouterRequestHandler(build, mode);\n\n  return async (cloudflare) => {\n    let loadContext = await getLoadContext({\n      request: cloudflare.request,\n      context: {\n        cloudflare: {\n          ...cloudflare,\n          cf: cloudflare.request.cf!,\n          ctx: {\n            waitUntil: cloudflare.waitUntil.bind(cloudflare),\n            passThroughOnException:\n              cloudflare.passThroughOnException.bind(cloudflare),\n          },\n          caches,\n        },\n      },\n    });\n\n    return handleRequest(cloudflare.request, loadContext);\n  };\n}\n\ndeclare const process: any;\n\nexport function createPagesFunctionHandler<Env = any>({\n  build,\n  getLoadContext,\n  mode,\n}: createPagesFunctionHandlerParams<Env>) {\n  let handleRequest = createRequestHandler<Env>({\n    build,\n    getLoadContext,\n    mode,\n  });\n\n  let handleFetch = async (context: EventContext<Env, any, any>) => {\n    let response: Response | undefined;\n\n    // https://github.com/cloudflare/wrangler2/issues/117\n    context.request.headers.delete(\"if-none-match\");\n\n    try {\n      response = await context.env.ASSETS.fetch(\n        context.request.url,\n        context.request.clone(),\n      );\n      response =\n        response && response.status >= 200 && response.status < 400\n          ? new Response(response.body, response)\n          : undefined;\n    } catch {}\n\n    if (!response) {\n      response = await handleRequest(context);\n    }\n\n    return response;\n  };\n\n  return async (context: EventContext<Env, any, any>) => {\n    try {\n      return await handleFetch(context);\n    } catch (error: unknown) {\n      if (process.env.NODE_ENV === \"development\" && error instanceof Error) {\n        console.error(error);\n        return new Response(error.message || error.toString(), {\n          status: 500,\n        });\n      }\n\n      return new Response(\"Internal Error\", {\n        status: 500,\n      });\n    }\n  };\n}\n"
  },
  {
    "path": "packages/react-router-dev/.gitignore",
    "content": "server-build.js\nserver-build.d.ts\n"
  },
  {
    "path": "packages/react-router-dev/CHANGELOG.md",
    "content": "# `@react-router/dev`\n\n## 7.13.1\n\n### Patch Changes\n\n- Updated dependencies:\n  - `react-router@7.13.1`\n  - `@react-router/node@7.13.1`\n  - `@react-router/serve@7.13.1`\n\n## 7.13.0\n\n### Patch Changes\n\n- Bump @remix-run/node-fetch-server dep ([#14704](https://github.com/remix-run/react-router/pull/14704))\n- Updated dependencies:\n  - `react-router@7.13.0`\n  - `@react-router/node@7.13.0`\n  - `@react-router/serve@7.13.0`\n\n## 7.12.0\n\n### Minor Changes\n\n- Add additional layer of CSRF protection by rejecting submissions to UI routes from external origins. If you need to permit access to specific external origins, you can specify them in the `react-router.config.ts` config `allowedActionOrigins` field. ([#14708](https://github.com/remix-run/react-router/pull/14708))\n\n### Patch Changes\n\n- Fix `Maximum call stack size exceeded` errors when HMR is triggered against code with cyclic imports ([#14522](https://github.com/remix-run/react-router/pull/14522))\n\n- fix(vite): Skip SSR middleware in preview server for SPA mode ([#14673](https://github.com/remix-run/react-router/pull/14673))\n\n- \\[UNSTABLE] Add a new `future.unstable_trailingSlashAwareDataRequests` flag to provide consistent behavior of `request.pathname` inside `middleware`, `loader`, and `action` functions on document and data requests when a trailing slash is present in the browser URL. ([#14644](https://github.com/remix-run/react-router/pull/14644))\n\n  Currently, your HTTP and `request` pathnames would be as follows for `/a/b/c` and `/a/b/c/`\n\n  | URL `/a/b/c` | **HTTP pathname** | **`request` pathname\\`** |\n  | ------------ | ----------------- | ------------------------ |\n  | **Document** | `/a/b/c`          | `/a/b/c` ✅              |\n  | **Data**     | `/a/b/c.data`     | `/a/b/c` ✅              |\n\n  | URL `/a/b/c/` | **HTTP pathname** | **`request` pathname\\`** |\n  | ------------- | ----------------- | ------------------------ |\n  | **Document**  | `/a/b/c/`         | `/a/b/c/` ✅             |\n  | **Data**      | `/a/b/c.data`     | `/a/b/c` ⚠️              |\n\n  With this flag enabled, these pathnames will be made consistent though a new `_.data` format for client-side `.data` requests:\n\n  | URL `/a/b/c` | **HTTP pathname** | **`request` pathname\\`** |\n  | ------------ | ----------------- | ------------------------ |\n  | **Document** | `/a/b/c`          | `/a/b/c` ✅              |\n  | **Data**     | `/a/b/c.data`     | `/a/b/c` ✅              |\n\n  | URL `/a/b/c/` | **HTTP pathname**  | **`request` pathname\\`** |\n  | ------------- | ------------------ | ------------------------ |\n  | **Document**  | `/a/b/c/`          | `/a/b/c/` ✅             |\n  | **Data**      | `/a/b/c/_.data` ⬅️ | `/a/b/c/` ✅             |\n\n  This a bug fix but we are putting it behind an opt-in flag because it has the potential to be a \"breaking bug fix\" if you are relying on the URL format for any other application or caching logic.\n\n  Enabling this flag also changes the format of client side `.data` requests from `/_root.data` to `/_.data` when navigating to `/` to align with the new format. This does not impact the `request` pathname which is still `/` in all cases.\n\n- Updated dependencies:\n  - `react-router@7.12.0`\n  - `@react-router/node@7.12.0`\n  - `@react-router/serve@7.12.0`\n\n## 7.11.0\n\n### Minor Changes\n\n- feat: add `vite preview` support ([#14507](https://github.com/remix-run/react-router/pull/14507))\n\n### Patch Changes\n\n- rsc framework mode manual chunking for react and react-router deps ([#14655](https://github.com/remix-run/react-router/pull/14655))\n- add support for throwing redirect Response's at RSC render time ([#14596](https://github.com/remix-run/react-router/pull/14596))\n- support custom entrypoints for RSC framework mode ([#14643](https://github.com/remix-run/react-router/pull/14643))\n- `routeRSCServerRequest` replace `fetchServer` with `serverResponse` ([#14597](https://github.com/remix-run/react-router/pull/14597))\n- rsc framewlrk mode - optimize react-server-dom-webpack if in project package.json ([#14656](https://github.com/remix-run/react-router/pull/14656))\n- Updated dependencies:\n  - `react-router@7.11.0`\n  - `@react-router/serve@7.11.0`\n  - `@react-router/node@7.11.0`\n\n## 7.10.1\n\n### Patch Changes\n\n- Import ESM package `pkg-types` with a dynamic `import()` to fix issues on Node 20.18 ([#14624](https://github.com/remix-run/react-router/pull/14624))\n- Update `valibot` dependency to `^1.2.0` to address [GHSA-vqpr-j7v3-hqw9](https://github.com/advisories/GHSA-vqpr-j7v3-hqw9) ([#14608](https://github.com/remix-run/react-router/pull/14608))\n- Updated dependencies:\n  - `react-router@7.10.1`\n  - `@react-router/node@7.10.1`\n  - `@react-router/serve@7.10.1`\n\n## 7.10.0\n\n### Minor Changes\n\n- Stabilize `future.v8_splitRouteModules`, replacing `future.unstable_splitRouteModules` ([#14595](https://github.com/remix-run/react-router/pull/14595))\n  - ⚠️ This is a breaking change if you have begun using `future.unstable_splitRouteModules`. Please update your `react-router.config.ts`.\n\n- Stabilize `future.v8_viteEnvironmentApi`, replacing `future.unstable_viteEnvironmentApi` ([#14595](https://github.com/remix-run/react-router/pull/14595))\n  - ⚠️ This is a breaking change if you have begun using `future.unstable_viteEnvironmentApi`. Please update your `react-router.config.ts`.\n\n### Patch Changes\n\n- Load environment variables before evaluating `routes.ts` ([#14446](https://github.com/remix-run/react-router/pull/14446))\n\n  For example, you can now compute your routes based on [`VITE_`-prefixed environment variables](https://vite.dev/guide/env-and-mode#env-variables):\n\n  ```txt\n  # .env\n  VITE_ENV_ROUTE=my-route\n  ```\n\n  ```ts\n  // app/routes.ts\n  import { type RouteConfig, route } from \"@react-router/dev/routes\";\n\n  const routes: RouteConfig = [];\n  if (import.meta.env.VITE_ENV_ROUTE === \"my-route\") {\n    routes.push(route(\"my-route\", \"routes/my-route.tsx\"));\n  }\n\n  export default routes;\n  ```\n\n- Updated dependencies:\n  - `react-router@7.10.0`\n  - `@react-router/node@7.10.0`\n  - `@react-router/serve@7.10.0`\n\n## 7.9.6\n\n### Patch Changes\n\n- Use a dynamic `import()` to load ESM-only `p-map` dependency to avoid issues on Node 20.18 and below ([#14492](https://github.com/remix-run/react-router/pull/14492))\n- Short circuit `HEAD` document requests before calling `renderToPipeableStream` in the default `entry.server.tsx` to more closely align with the [spec](https://httpwg.org/specs/rfc9110.html#HEAD) ([#14488](https://github.com/remix-run/react-router/pull/14488))\n- Updated dependencies:\n  - `react-router@7.9.6`\n  - `@react-router/node@7.9.6`\n  - `@react-router/serve@7.9.6`\n\n## 7.9.5\n\n### Patch Changes\n\n- Introduce a `prerender.unstable_concurrency` option, to support running the prerendering concurrently, potentially speeding up the build. ([#14380](https://github.com/remix-run/react-router/pull/14380))\n- Move RSCHydratedRouter and utils to `/dom` export. ([#14457](https://github.com/remix-run/react-router/pull/14457))\n- Ensure route navigation doesn't remove CSS `link` elements used by dynamic imports ([#14463](https://github.com/remix-run/react-router/pull/14463))\n- Typegen: only register route module types for routes within the app directory ([#14439](https://github.com/remix-run/react-router/pull/14439))\n- Updated dependencies:\n  - `react-router@7.9.5`\n  - `@react-router/node@7.9.5`\n  - `@react-router/serve@7.9.5`\n\n## 7.9.4\n\n### Patch Changes\n\n- Update `valibot` dependency to `^1.1.0` ([#14379](https://github.com/remix-run/react-router/pull/14379))\n\n- New (unstable) `useRoute` hook for accessing data from specific routes ([#14407](https://github.com/remix-run/react-router/pull/14407))\n\n  For example, let's say you have an `admin` route somewhere in your app and you want any child routes of `admin` to all have access to the `loaderData` and `actionData` from `admin.`\n\n  ```tsx\n  // app/routes/admin.tsx\n  import { Outlet } from \"react-router\";\n\n  export const loader = () => ({ message: \"Hello, loader!\" });\n\n  export const action = () => ({ count: 1 });\n\n  export default function Component() {\n    return (\n      <div>\n        {/* ... */}\n        <Outlet />\n        {/* ... */}\n      </div>\n    );\n  }\n  ```\n\n  You might even want to create a reusable widget that all of the routes nested under `admin` could use:\n\n  ```tsx\n  import { unstable_useRoute as useRoute } from \"react-router\";\n\n  export function AdminWidget() {\n    // How to get `message` and `count` from `admin` route?\n  }\n  ```\n\n  In framework mode, `useRoute` knows all your app's routes and gives you TS errors when invalid route IDs are passed in:\n\n  ```tsx\n  export function AdminWidget() {\n    const admin = useRoute(\"routes/dmin\");\n    //                      ^^^^^^^^^^^\n  }\n  ```\n\n  `useRoute` returns `undefined` if the route is not part of the current page:\n\n  ```tsx\n  export function AdminWidget() {\n    const admin = useRoute(\"routes/admin\");\n    if (!admin) {\n      throw new Error(`AdminWidget used outside of \"routes/admin\"`);\n    }\n  }\n  ```\n\n  Note: the `root` route is the exception since it is guaranteed to be part of the current page.\n  As a result, `useRoute` never returns `undefined` for `root`.\n\n  `loaderData` and `actionData` are marked as optional since they could be accessed before the `action` is triggered or after the `loader` threw an error:\n\n  ```tsx\n  export function AdminWidget() {\n    const admin = useRoute(\"routes/admin\");\n    if (!admin) {\n      throw new Error(`AdminWidget used outside of \"routes/admin\"`);\n    }\n    const { loaderData, actionData } = admin;\n    console.log(loaderData);\n    //          ^? { message: string } | undefined\n    console.log(actionData);\n    //          ^? { count: number } | undefined\n  }\n  ```\n\n  If instead of a specific route, you wanted access to the _current_ route's `loaderData` and `actionData`, you can call `useRoute` without arguments:\n\n  ```tsx\n  export function AdminWidget() {\n    const currentRoute = useRoute();\n    currentRoute.loaderData;\n    currentRoute.actionData;\n  }\n  ```\n\n  This usage is equivalent to calling `useLoaderData` and `useActionData`, but consolidates all route data access into one hook: `useRoute`.\n\n  Note: when calling `useRoute()` (without a route ID), TS has no way to know which route is the current route.\n  As a result, `loaderData` and `actionData` are typed as `unknown`.\n  If you want more type-safety, you can either narrow the type yourself with something like `zod` or you can refactor your app to pass down typed props to your `AdminWidget`:\n\n  ```tsx\n  export function AdminWidget({\n    message,\n    count,\n  }: {\n    message: string;\n    count: number;\n  }) {\n    /* ... */\n  }\n  ```\n\n- Updated dependencies:\n  - `react-router@7.9.4`\n  - `@react-router/node@7.9.4`\n  - `@react-router/serve@7.9.4`\n\n## 7.9.3\n\n### Patch Changes\n\n- Updated dependencies:\n  - `react-router@7.9.3`\n  - `@react-router/node@7.9.3`\n  - `@react-router/serve@7.9.3`\n\n## 7.9.2\n\n### Patch Changes\n\n- Fix preset future flags being ignored during config resolution ([#14369](https://github.com/remix-run/react-router/pull/14369))\n\n  Fixes a bug where future flags defined by presets were completely ignored. The config resolution was incorrectly reading from `reactRouterUserConfig.future` instead of the merged `userAndPresetConfigs.future`, causing all preset-defined future flags to be lost.\n\n  This fix ensures presets can properly enable experimental features as intended by the preset system design.\n\n- Add unstable support for RSC Framework Mode ([#14336](https://github.com/remix-run/react-router/pull/14336))\n\n- Switch internal vite plugin Response logic to use `@remix-run/node-fetch-server` ([#13927](https://github.com/remix-run/react-router/pull/13927))\n\n- Updated dependencies:\n  - `react-router@7.9.2`\n  - `@react-router/serve@7.9.2`\n  - `@react-router/node@7.9.2`\n\n## 7.9.1\n\n### Patch Changes\n\n- Fix internal `Future` interface naming from `middleware` -> `v8_middleware` ([#14327](https://github.com/remix-run/react-router/pull/14327))\n- Updated dependencies:\n  - `react-router@7.9.1`\n  - `@react-router/node@7.9.1`\n  - `@react-router/serve@7.9.1`\n\n## 7.9.0\n\n### Minor Changes\n\n- Stabilize middleware and context APIs. ([#14215](https://github.com/remix-run/react-router/pull/14215))\n\n  We have removed the `unstable_` prefix from the following APIs and they are now considered stable and ready for production use:\n  - [`RouterContextProvider`](https://reactrouter.com/api/utils/RouterContextProvider)\n  - [`createContext`](https://reactrouter.com/api/utils/createContext)\n  - `createBrowserRouter` [`getContext`](https://reactrouter.com/api/data-routers/createBrowserRouter#optsgetcontext) option\n  - `<HydratedRouter>` [`getContext`](https://reactrouter.com/api/framework-routers/HydratedRouter#getcontext) prop\n\n  Please see the [Middleware Docs](https://reactrouter.com/how-to/middleware), the [Middleware RFC](https://github.com/remix-run/remix/discussions/7642), and the [Client-side Context RFC](https://github.com/remix-run/react-router/discussions/9856) for more information.\n\n### Patch Changes\n\n- Updated dependencies:\n  - `react-router@7.9.0`\n  - `@react-router/node@7.9.0`\n  - `@react-router/serve@7.9.0`\n\n## 7.8.2\n\n### Patch Changes\n\n- fix: memory leak in default entry.server ([#14200](https://github.com/remix-run/react-router/pull/14200))\n- Updated dependencies:\n  - `react-router@7.8.2`\n  - `@react-router/node@7.8.2`\n  - `@react-router/serve@7.8.2`\n\n## 7.8.1\n\n### Patch Changes\n\n- Update generated `Route.MetaArgs` type so `loaderData` is only potentially undefined when an `ErrorBoundary` export is present ([#14173](https://github.com/remix-run/react-router/pull/14173))\n- Updated dependencies:\n  - `react-router@7.8.1`\n  - `@react-router/node@7.8.1`\n  - `@react-router/serve@7.8.1`\n\n## 7.8.0\n\n### Patch Changes\n\n- Fix rename without mkdir in Vite plugin ([#14105](https://github.com/remix-run/react-router/pull/14105))\n- Updated dependencies:\n  - `react-router@7.8.0`\n  - `@react-router/node@7.8.0`\n  - `@react-router/serve@7.8.0`\n\n## 7.7.1\n\n### Patch Changes\n\n- Update to Prettier v3 for formatting when running `react-router reveal --no-typescript` ([#14049](https://github.com/remix-run/react-router/pull/14049))\n- Updated dependencies:\n  - `react-router@7.7.1`\n  - `@react-router/node@7.7.1`\n  - `@react-router/serve@7.7.1`\n\n## 7.7.0\n\n### Patch Changes\n\n- Update `vite-node` to `^3.2.2` to support Vite 7 ([#13781](https://github.com/remix-run/react-router/pull/13781))\n- Properly handle `https` protocol in dev mode ([#13746](https://github.com/remix-run/react-router/pull/13746))\n- Fix missing styles when Vite's `build.cssCodeSplit` option is disabled ([#13943](https://github.com/remix-run/react-router/pull/13943))\n- Allow `.mts` and `.mjs` extensions for route config file ([#13931](https://github.com/remix-run/react-router/pull/13931))\n- Fix prerender file locations when `cwd` differs from project root ([#13824](https://github.com/remix-run/react-router/pull/13824))\n- Improve chunk error logging when a chunk cannot be found during the build ([#13799](https://github.com/remix-run/react-router/pull/13799))\n- Fix incorrectly configured `externalConditions` which had enabled `module` condition for externals and broke builds with certain packages, like Emotion. ([#13871](https://github.com/remix-run/react-router/pull/13871))\n- Updated dependencies:\n  - `react-router@7.7.0`\n  - `@react-router/node@7.7.0`\n  - `@react-router/serve@7.7.0`\n\n## 7.6.3\n\n### Patch Changes\n\n- Add Vite 7 support ([#13748](https://github.com/remix-run/react-router/pull/13748))\n- Skip `package.json` resolution checks when a custom `entry.server.(j|t)sx` file is provided. ([#13744](https://github.com/remix-run/react-router/pull/13744))\n- Add validation for a route's id not being 'root' ([#13792](https://github.com/remix-run/react-router/pull/13792))\n- Updated dependencies:\n  - `@react-router/node@7.6.3`\n  - `react-router@7.6.3`\n  - `@react-router/serve@7.6.3`\n\n## 7.6.2\n\n### Patch Changes\n\n- Avoid additional `with-props` chunk in Framework Mode by moving route module component prop logic from the Vite plugin to `react-router` ([#13650](https://github.com/remix-run/react-router/pull/13650))\n\n- When `future.unstable_viteEnvironmentApi` is enabled and an absolute Vite `base` has been configured, ensure critical CSS is handled correctly during development ([#13598](https://github.com/remix-run/react-router/pull/13598))\n\n- Update `vite-node` ([#13673](https://github.com/remix-run/react-router/pull/13673))\n\n- Fix typegen for non-{.js,.jsx,.ts,.tsx} routes like .mdx ([#12453](https://github.com/remix-run/react-router/pull/12453))\n\n- Fix href types for optional dynamic params ([#13725](https://github.com/remix-run/react-router/pull/13725))\n\n  7.6.1 introduced fixes for `href` when using optional static segments,\n  but those fixes caused regressions with how optional dynamic params worked in 7.6.0:\n\n  ```ts\n  // 7.6.0\n  href(\"/users/:id?\"); // ✅\n  href(\"/users/:id?\", { id: 1 }); // ✅\n\n  // 7.6.1\n  href(\"/users/:id?\"); // ❌\n  href(\"/users/:id?\", { id: 1 }); // ❌\n  ```\n\n  Now, optional static segments are expanded into different paths for `href`, but optional dynamic params are not.\n  This way `href` can unambiguously refer to an exact URL path, all while keeping the number of path options to a minimum.\n\n  ```ts\n  // 7.6.2\n\n  // path: /users/:id?/edit?\n  href(\"\n  //    ^ suggestions when cursor is here:\n  //\n  //    /users/:id?\n  //    /users/:id?/edit\n  ```\n\n  Additionally, you can pass `params` from component props without needing to narrow them manually:\n\n  ```ts\n  declare const params: { id?: number };\n\n  // 7.6.0\n  href(\"/users/:id?\", params);\n\n  // 7.6.1\n  href(\"/users/:id?\", params); // ❌\n  \"id\" in params ? href(\"/users/:id\", params) : href(\"/users\"); // works... but is annoying\n\n  // 7.6.2\n  href(\"/users/:id?\", params); // restores behavior of 7.6.0\n  ```\n\n- Updated dependencies:\n  - `react-router@7.6.2`\n  - `@react-router/node@7.6.2`\n  - `@react-router/serve@7.6.2`\n\n## 7.6.1\n\n### Patch Changes\n\n- Prevent typegen with route files are outside the app directory ([#12996](https://github.com/remix-run/react-router/pull/12996))\n\n- Fix typegen when same route is used at multiple paths ([#13574](https://github.com/remix-run/react-router/pull/13574))\n\n  For example, `routes/route.tsx` is used at 4 different paths here:\n\n  ```ts\n  import { type RouteConfig, route } from \"@react-router/dev/routes\";\n  export default [\n    route(\"base/:base\", \"routes/base.tsx\", [\n      route(\"home/:home\", \"routes/route.tsx\", { id: \"home\" }),\n      route(\"changelog/:changelog\", \"routes/route.tsx\", { id: \"changelog\" }),\n      route(\"splat/*\", \"routes/route.tsx\", { id: \"splat\" }),\n    ]),\n    route(\"other/:other\", \"routes/route.tsx\", { id: \"other\" }),\n  ] satisfies RouteConfig;\n  ```\n\n  Previously, typegen would arbitrarily pick one of these paths to be the \"winner\" and generate types for the route module based on that path.\n  Now, typegen creates unions as necessary for alternate paths for the same route file.\n\n- Add additional logging to `build` command output when cleaning assets from server build ([#13547](https://github.com/remix-run/react-router/pull/13547))\n\n- Better types for `params` ([#13543](https://github.com/remix-run/react-router/pull/13543))\n\n  For example:\n\n  ```ts\n  // routes.ts\n  import { type RouteConfig, route } from \"@react-router/dev/routes\";\n\n  export default [\n    route(\"parent/:p\", \"routes/parent.tsx\", [\n      route(\"layout/:l\", \"routes/layout.tsx\", [\n        route(\"child1/:c1a/:c1b\", \"routes/child1.tsx\"),\n        route(\"child2/:c2a/:c2b\", \"routes/child2.tsx\"),\n      ]),\n    ]),\n  ] satisfies RouteConfig;\n  ```\n\n  Previously, `params` for the `routes/layout.tsx` route were calculated as `{ p: string, l: string }`.\n  This incorrectly ignores params that could come from child routes.\n  If visiting `/parent/1/layout/2/child1/3/4`, the actual params passed to `routes/layout.tsx` will have a type of `{ p: string, l: string, c1a: string, c1b: string }`.\n\n  Now, `params` are aware of child routes and autocompletion will include child params as optionals:\n\n  ```ts\n  params.|\n  //     ^ cursor is here and you ask for autocompletion\n  // p: string\n  // l: string\n  // c1a?: string\n  // c1b?: string\n  // c2a?: string\n  // c2b?: string\n  ```\n\n  You can also narrow the types for `params` as it is implemented as a normalized union of params for each page that includes `routes/layout.tsx`:\n\n  ```ts\n  if (typeof params.c1a === 'string') {\n    params.|\n    //     ^ cursor is here and you ask for autocompletion\n    // p: string\n    // l: string\n    // c1a: string\n    // c1b: string\n  }\n  ```\n\n  ***\n\n  UNSTABLE: renamed internal `react-router/route-module` export to `react-router/internal`\n  UNSTABLE: removed `Info` export from generated `+types/*` files\n\n- \\[UNSTABLE] Normalize dirent entry path across node versions when generating SRI manifest ([#13591](https://github.com/remix-run/react-router/pull/13591))\n\n- Don't clean assets from server build when `build.ssrEmitAssets` has been enabled in Vite config ([#13547](https://github.com/remix-run/react-router/pull/13547))\n\n- Fix `href` for optional segments ([#13595](https://github.com/remix-run/react-router/pull/13595))\n\n  Type generation now expands paths with optionals into their corresponding non-optional paths.\n  For example, the path `/user/:id?` gets expanded into `/user` and `/user/:id` to more closely model visitable URLs.\n  `href` then uses these expanded (non-optional) paths to construct type-safe paths for your app:\n\n  ```ts\n  // original: /user/:id?\n  // expanded: /user & /user/:id\n  href(\"/user\"); // ✅\n  href(\"/user/:id\", { id: 1 }); // ✅\n  ```\n\n  This becomes even more important for static optional paths where there wasn't a good way to indicate whether the optional should be included in the resulting path:\n\n  ```ts\n  // original: /products/:id/detail?\n\n  // before\n  href(\"/products/:id/detail?\"); // ❌ How can we tell `href` to include or omit `detail?` segment with a complex API?\n\n  // now\n  // expanded: /products/:id & /products/:id/detail\n  href(\"/product/:id\"); // ✅\n  href(\"/product/:id/detail\"); // ✅\n  ```\n\n- Updated dependencies:\n  - `react-router@7.6.1`\n  - `@react-router/node@7.6.1`\n  - `@react-router/serve@7.6.1`\n\n## 7.6.0\n\n### Minor Changes\n\n- Added a new `react-router.config.ts` `routeDiscovery` option to configure Lazy Route Discovery behavior. ([#13451](https://github.com/remix-run/react-router/pull/13451))\n  - By default, Lazy Route Discovery is enabled and makes manifest requests to the `/__manifest` path:\n    - `routeDiscovery: { mode: \"lazy\", manifestPath: \"/__manifest\" }`\n  - You can modify the manifest path used:\n    - `routeDiscovery: { mode: \"lazy\", manifestPath: \"/custom-manifest\" }`\n  - Or you can disable this feature entirely and include all routes in the manifest on initial document load:\n    - `routeDiscovery: { mode: \"initial\" }`\n\n- Automatic types for future flags ([#13506](https://github.com/remix-run/react-router/pull/13506))\n\n  Some future flags alter the way types should work in React Router.\n  Previously, you had to remember to manually opt-in to the new types.\n\n  For example, for `unstable_middleware`:\n\n  ```ts\n  // react-router.config.ts\n\n  // Step 1: Enable middleware\n  export default {\n    future: {\n      unstable_middleware: true,\n    },\n  };\n\n  // Step 2: Enable middleware types\n  declare module \"react-router\" {\n    interface Future {\n      unstable_middleware: true; // 👈 Enable middleware types\n    }\n  }\n  ```\n\n  It was up to you to keep the runtime future flags synced with the types for those future flags.\n  This was confusing and error-prone.\n\n  Now, React Router will automatically enable types for future flags.\n  That means you only need to specify the runtime future flag:\n\n  ```ts\n  // react-router.config.ts\n\n  // Step 1: Enable middleware\n  export default {\n    future: {\n      unstable_middleware: true,\n    },\n  };\n\n  // No step 2! That's it!\n  ```\n\n  Behind the scenes, React Router will generate the corresponding `declare module` into `.react-router/types`.\n  Currently this is done in `.react-router/types/+register.ts` but this is an implementation detail that may change in the future.\n\n### Patch Changes\n\n- Support project root directories without a `package.json` if it exists in a parent directory ([#13472](https://github.com/remix-run/react-router/pull/13472))\n\n- When providing a custom Vite config path via the CLI `--config`/`-c` flag, default the project root directory to the directory containing the Vite config when not explicitly provided ([#13472](https://github.com/remix-run/react-router/pull/13472))\n\n- In a `routes.ts` context, ensure the `--mode` flag is respected for `import.meta.env.MODE` ([#13485](https://github.com/remix-run/react-router/pull/13485))\n\n  Previously, `import.meta.env.MODE` within a `routes.ts` context was always `\"development\"` for the `dev` and `typegen --watch` commands, but otherwise resolved to `\"production\"`. These defaults are still in place, but if a `--mode` flag is provided, this will now take precedence.\n\n- Ensure consistent project root directory resolution logic in CLI commands ([#13472](https://github.com/remix-run/react-router/pull/13472))\n\n- When executing `react-router.config.ts` and `routes.ts` with `vite-node`, ensure that PostCSS config files are ignored ([#13489](https://github.com/remix-run/react-router/pull/13489))\n\n- When extracting critical CSS during development, ensure it's loaded from the client environment to avoid issues with plugins that handle the SSR environment differently ([#13503](https://github.com/remix-run/react-router/pull/13503))\n\n- When `future.unstable_viteEnvironmentApi` is enabled, ensure that `build.assetsDir` in Vite config is respected when `environments.client.build.assetsDir` is not configured ([#13491](https://github.com/remix-run/react-router/pull/13491))\n\n- Fix \"Status message is not supported by HTTP/2\" error during dev when using HTTPS ([#13460](https://github.com/remix-run/react-router/pull/13460))\n\n- Update config when `react-router.config.ts` is created or deleted during development. ([#12319](https://github.com/remix-run/react-router/pull/12319))\n\n- Skip unnecessary `routes.ts` evaluation before Vite build is started ([#13513](https://github.com/remix-run/react-router/pull/13513))\n\n- Fix `TS2300: Duplicate identifier` errors caused by generated types ([#13499](https://github.com/remix-run/react-router/pull/13499))\n\n  Previously, routes that had the same full path would cause duplicate entries in the generated types for `href` (`.react-router/types/+register.ts`), causing type checking errors.\n\n- Updated dependencies:\n  - `react-router@7.6.0`\n  - `@react-router/node@7.6.0`\n  - `@react-router/serve@7.6.0`\n\n## 7.5.3\n\n### Patch Changes\n\n- Updated dependencies:\n  - `react-router@7.5.3`\n  - `@react-router/node@7.5.3`\n  - `@react-router/serve@7.5.3`\n\n## 7.5.2\n\n### Patch Changes\n\n- Adjust approach for Prerendering/SPA Mode via headers ([#13453](https://github.com/remix-run/react-router/pull/13453))\n- Updated dependencies:\n  - `react-router@7.5.2`\n  - `@react-router/node@7.5.2`\n  - `@react-router/serve@7.5.2`\n\n## 7.5.1\n\n### Patch Changes\n\n- Fix prerendering when a loader returns a redirect ([#13365](https://github.com/remix-run/react-router/pull/13365))\n- Updated dependencies:\n  - `react-router@7.5.1`\n  - `@react-router/node@7.5.1`\n  - `@react-router/serve@7.5.1`\n\n## 7.5.0\n\n### Patch Changes\n\n- Introduce `unstable_subResourceIntegrity` future flag that enables generation of an importmap with integrity for the scripts that will be loaded by the browser. ([#13163](https://github.com/remix-run/react-router/pull/13163))\n- Update optional Wrangler peer dependency range to support Wrangler v4 ([#13258](https://github.com/remix-run/react-router/pull/13258))\n- When `future.unstable_viteEnvironmentApi` is enabled, ensure critical CSS in development works when using a custom Vite `base` has been configured ([#13305](https://github.com/remix-run/react-router/pull/13305))\n- Reinstate dependency optimization in the child compiler to fix `depsOptimizer is required in dev mode` errors when using `vite-plugin-cloudflare` and importing Node.js builtins ([#13317](https://github.com/remix-run/react-router/pull/13317))\n- Updated dependencies:\n  - `react-router@7.5.0`\n  - `@react-router/node@7.5.0`\n  - `@react-router/serve@7.5.0`\n\n## 7.4.1\n\n### Patch Changes\n\n- Fix path in prerender error messages ([#13257](https://github.com/remix-run/react-router/pull/13257))\n- Fix typegen for virtual modules when `moduleDetection` is set to `force` ([#13267](https://github.com/remix-run/react-router/pull/13267))\n- When both `future.unstable_middleware` and `future.unstable_splitRouteModules` are enabled, split `unstable_clientMiddleware` route exports into separate chunks when possible ([#13210](https://github.com/remix-run/react-router/pull/13210))\n- Improve performance of `future.unstable_middleware` by ensuring that route modules are only blocking during the middleware phase when the `unstable_clientMiddleware` has been defined ([#13210](https://github.com/remix-run/react-router/pull/13210))\n- Updated dependencies:\n  - `react-router@7.4.1`\n  - `@react-router/node@7.4.1`\n  - `@react-router/serve@7.4.1`\n\n## 7.4.0\n\n### Minor Changes\n\n- Generate types for `virtual:react-router/server-build` module ([#13152](https://github.com/remix-run/react-router/pull/13152))\n\n### Patch Changes\n\n- When `future.unstable_splitRouteModules` is set to `\"enforce\"`, allow both splittable and unsplittable root route exports since it's always in a single chunk. ([#13238](https://github.com/remix-run/react-router/pull/13238))\n- When `future.unstable_viteEnvironmentApi` is enabled, allow plugins that override the default SSR environment (such as `@cloudflare/vite-plugin`) to be placed before or after the React Router plugin. ([#13183](https://github.com/remix-run/react-router/pull/13183))\n- Fix conflicts with other Vite plugins that use the `configureServer` and/or `configurePreviewServer` hooks ([#13184](https://github.com/remix-run/react-router/pull/13184))\n- Updated dependencies:\n  - `react-router@7.4.0`\n  - `@react-router/node@7.4.0`\n  - `@react-router/serve@7.4.0`\n\n## 7.3.0\n\n### Patch Changes\n\n- Fix support for custom client `build.rollupOptions.output.entryFileNames` ([#13098](https://github.com/remix-run/react-router/pull/13098))\n\n- Fix usage of `prerender` option when `serverBundles` option has been configured or provided by a preset, e.g. `vercelPreset` from `@vercel/react-router` ([#13082](https://github.com/remix-run/react-router/pull/13082))\n\n- Fix support for custom `build.assetsDir` ([#13077](https://github.com/remix-run/react-router/pull/13077))\n\n- Remove unused dependencies ([#13134](https://github.com/remix-run/react-router/pull/13134))\n\n- Stub all routes except root in \"SPA Mode\" server builds to avoid issues when route modules or their dependencies import non-SSR-friendly modules ([#13023](https://github.com/remix-run/react-router/pull/13023))\n\n- Fix errors with `future.unstable_viteEnvironmentApi` when the `ssr` environment has been configured by another plugin to be a custom `Vite.DevEnvironment` rather than the default `Vite.RunnableDevEnvironment` ([#13008](https://github.com/remix-run/react-router/pull/13008))\n\n- Remove unused Vite file system watcher ([#13133](https://github.com/remix-run/react-router/pull/13133))\n\n- Fix support for custom SSR build input when `serverBundles` option has been configured ([#13107](https://github.com/remix-run/react-router/pull/13107))\n\n  Note that for consumers using the `future.unstable_viteEnvironmentApi` and `serverBundles` options together, hyphens are no longer supported in server bundle IDs since they also need to be valid Vite environment names.\n\n- Fix dev server when using HTTPS by stripping HTTP/2 pseudo headers from dev server requests ([#12830](https://github.com/remix-run/react-router/pull/12830))\n\n- Lazy load Cloudflare platform proxy on first dev server request when using the `cloudflareDevProxy` Vite plugin to avoid creating unnecessary workerd processes ([#13016](https://github.com/remix-run/react-router/pull/13016))\n\n- When `future.unstable_viteEnvironmentApi` is enabled and the `ssr` environment has `optimizeDeps.noDiscovery` disabled, define `optimizeDeps.entries` and `optimizeDeps.include` ([#13007](https://github.com/remix-run/react-router/pull/13007))\n\n- Fix duplicated entries in typegen for layout routes and their corresponding index route ([#13140](https://github.com/remix-run/react-router/pull/13140))\n\n- Updated dependencies:\n  - `react-router@7.3.0`\n  - `@react-router/node@7.3.0`\n  - `@react-router/serve@7.3.0`\n\n## 7.2.0\n\n### Minor Changes\n\n- Generate a \"SPA fallback\" HTML file for scenarios where applications are prerendering the `/` route with `ssr:false` ([#12948](https://github.com/remix-run/react-router/pull/12948))\n  - If you specify `ssr:false` without a `prerender` config, this is considered \"SPA Mode\" and the generated `index.html` file will only render down to the root route and will be able to hydrate for any valid application path\n  - If you specify `ssr:false` with a `prerender` config but _do not_ include the `/` path (i.e., `prerender: ['/blog/post']`), then we still generate a \"SPA Mode\" `index.html` file that can hydrate for any path in the application\n  - However, previously if you specified `ssr:false` and included the `/` path in your `prerender` config, we would prerender the `/` route into `index.html` as a non-SPA page\n    - The generated HTML would include the root index route which prevented hydration for any other paths\n    - With this change, we now generate a \"SPA Mode\" file in `__spa-fallback.html` that will allow you to hydrate for any non-prerendered paths\n    - You can serve this file from your static file server for any paths that would otherwise 404 if you only want to pre-render _some_ routes in your `ssr:false` app and serve the others as a SPA\n    - `npx sirv-cli build/client --single __spa-fallback.html`\n\n- Allow a `loader` in the root route in SPA mode because it can be called/server-rendered at build time ([#12948](https://github.com/remix-run/react-router/pull/12948))\n  - `Route.HydrateFallbackProps` now also receives `loaderData`\n    - This will be defined so long as the `HydrateFallback` is rendering while _children_ routes are loading\n    - This will be `undefined` if the `HydrateFallback` is rendering because the route has it's own hydrating `clientLoader`\n    - In SPA mode, this will allow you to render loader root data into the SPA `index.html`\n\n- New type-safe `href` utility that guarantees links point to actual paths in your app ([#13012](https://github.com/remix-run/react-router/pull/13012))\n\n  ```tsx\n  import { href } from \"react-router\";\n\n  export default function Component() {\n    const link = href(\"/blog/:slug\", { slug: \"my-first-post\" });\n    return (\n      <main>\n        <Link to={href(\"/products/:id\", { id: \"asdf\" })} />\n        <NavLink to={href(\"/:lang?/about\", { lang: \"en\" })} />\n      </main>\n    );\n  }\n  ```\n\n### Patch Changes\n\n- Handle custom `envDir` in Vite config ([#12969](https://github.com/remix-run/react-router/pull/12969))\n\n- Fix typegen for repeated params ([#13012](https://github.com/remix-run/react-router/pull/13012))\n\n  In React Router, path parameters are keyed by their name.\n  So for a path pattern like `/a/:id/b/:id?/c/:id`, the last `:id` will set the value for `id` in `useParams` and the `params` prop.\n  For example, `/a/1/b/2/c/3` will result in the value `{ id: 3 }` at runtime.\n\n  Previously, generated types for params incorrectly modeled repeated params with an array.\n  So `/a/1/b/2/c/3` generated a type like `{ id: [1,2,3] }`.\n\n  To be consistent with runtime behavior, the generated types now correctly model the \"last one wins\" semantics of path parameters.\n  So `/a/1/b/2/c/3` now generates a type like `{ id: 3 }`.\n\n- Fix CLI parsing to allow argumentless `npx react-router` usage ([#12925](https://github.com/remix-run/react-router/pull/12925))\n\n- Fix `ArgError: unknown or unexpected option: --version` when running `react-router --version` ([#13012](https://github.com/remix-run/react-router/pull/13012))\n\n- Skip action-only resource routes when using `prerender:true` ([#13004](https://github.com/remix-run/react-router/pull/13004))\n\n- Enhance invalid export detection when using `ssr:false` ([#12948](https://github.com/remix-run/react-router/pull/12948))\n  - `headers`/`action` are prohibited in all routes with `ssr:false` because there will be no runtime server on which to run them\n  - `loader` functions are more nuanced and depend on whether a given route is prerendered\n    - When using `ssr:false` without a `prerender` config, only the `root` route can have a `loader`\n      - This is \"SPA mode\" which generates a single `index.html` file with the root route `HydrateFallback` so it is capable of hydrating for any path in your application - therefore we can only call a root route `loader` at build time\n    - When using `ssr:false` with a `prerender` config, you can export a `loader` from routes matched by one of the `prerender` paths because those routes will be server rendered at build time\n      - Exporting a `loader` from a route that is never matched by a `prerender` path will throw a build time error because there will be no runtime server to ever run the loader\n\n- Limit prerendered resource route `.data` files to only the target route ([#13004](https://github.com/remix-run/react-router/pull/13004))\n\n- Add unstable support for splitting route modules in framework mode via `future.unstable_splitRouteModules` ([#11871](https://github.com/remix-run/react-router/pull/11871))\n\n- Fix prerendering of binary files ([#13039](https://github.com/remix-run/react-router/pull/13039))\n\n- Add `future.unstable_viteEnvironmentApi` flag to enable experimental Vite Environment API support ([#12936](https://github.com/remix-run/react-router/pull/12936))\n\n- Disable Lazy Route Discovery for all `ssr:false` apps and not just \"SPA Mode\" because there is no runtime server to serve the search-param-configured `__manifest` requests ([#12894](https://github.com/remix-run/react-router/pull/12894))\n  - We previously only disabled this for \"SPA Mode\" which is `ssr:false` and no `prerender` config but we realized it should apply to all `ssr:false` apps, including those prerendering multiple pages\n  - In those `prerender` scenarios we would prerender the `/__manifest` file assuming the static file server would serve it but that makes some unneccesary assumptions about the static file server behaviors\n\n- Updated dependencies:\n  - `react-router@7.2.0`\n  - `@react-router/node@7.2.0`\n  - `@react-router/serve@7.2.0`\n\n## 7.1.5\n\n### Patch Changes\n\n- Updated dependencies:\n  - `react-router@7.1.5`\n  - `@react-router/node@7.1.5`\n  - `@react-router/serve@7.1.5`\n\n## 7.1.4\n\n### Patch Changes\n\n- Properly resolve Windows file paths to scan for Vite's dependency optimization when using the `unstable_optimizeDeps` future flag. ([#12637](https://github.com/remix-run/react-router/pull/12637))\n- Fix prerendering when using a custom server - previously we ended up trying to import the users custom server when we actually want to import the virtual server build module ([#12759](https://github.com/remix-run/react-router/pull/12759))\n- Updated dependencies:\n  - `react-router@7.1.4`\n  - `@react-router/node@7.1.4`\n  - `@react-router/serve@7.1.4`\n\n## 7.1.3\n\n### Patch Changes\n\n- Fix `reveal` and `routes` CLI commands ([#12745](https://github.com/remix-run/react-router/pull/12745))\n- Updated dependencies:\n  - `react-router@7.1.3`\n  - `@react-router/node@7.1.3`\n  - `@react-router/serve@7.1.3`\n\n## 7.1.2\n\n### Patch Changes\n\n- Fix default external conditions in Vite v6. This fixes resolution issues with certain npm packages. ([#12644](https://github.com/remix-run/react-router/pull/12644))\n- Fix mismatch in prerendering html/data files when path is missing a leading slash ([#12684](https://github.com/remix-run/react-router/pull/12684))\n- Use `module-sync` server condition when enabled in the runtime. This fixes React context mismatches (e.g. `useHref() may be used only in the context of a <Router> component.`) during development on Node 22.10.0+ when using libraries that have a peer dependency on React Router. ([#12729](https://github.com/remix-run/react-router/pull/12729))\n- Fix react-refresh source maps ([#12686](https://github.com/remix-run/react-router/pull/12686))\n- Updated dependencies:\n  - `react-router@7.1.2`\n  - `@react-router/node@7.1.2`\n  - `@react-router/serve@7.1.2`\n\n## 7.1.1\n\n### Patch Changes\n\n- Fix for a crash when optional args are passed to the CLI ([`5b1ca202f`](https://github.com/remix-run/react-router/commit/5b1ca202f77ef342db0109c6b791d33188077cd0))\n- Updated dependencies:\n  - `react-router@7.1.1`\n  - `@react-router/node@7.1.1`\n  - `@react-router/serve@7.1.1`\n\n## 7.1.0\n\n### Minor Changes\n\n- Add support for Vite v6 ([#12469](https://github.com/remix-run/react-router/pull/12469))\n\n### Patch Changes\n\n- Properly initialize `NODE_ENV` if not already set for compatibility with React 19 ([#12578](https://github.com/remix-run/react-router/pull/12578))\n\n- Remove the leftover/unused `abortDelay` prop from `ServerRouter` and update the default `entry.server.tsx` to use the new `streamTimeout` value for Single Fetch ([#12478](https://github.com/remix-run/react-router/pull/12478))\n  - The `abortDelay` functionality was removed in v7 as it was coupled to the `defer` implementation from Remix v2, but this removal of this prop was missed\n  - If you were still using this prop in your `entry.server` file, it's likely your app is not aborting streams as you would expect and you will need to adopt the new [`streamTimeout`](https://reactrouter.com/explanation/special-files#streamtimeout) value introduced with Single Fetch\n\n- Updated dependencies:\n  - `react-router@7.1.0`\n  - `@react-router/node@7.1.0`\n  - `@react-router/serve@7.1.0`\n\n## 7.0.2\n\n### Patch Changes\n\n- Support `moduleResolution` `Node16` and `NodeNext` ([#12440](https://github.com/remix-run/react-router/pull/12440))\n\n- Generate wide `matches` and `params` types for current route and child routes ([#12397](https://github.com/remix-run/react-router/pull/12397))\n\n  At runtime, `matches` includes child route matches and `params` include child route path parameters.\n  But previously, we only generated types for parent routes in `matches`; for `params`, we only considered the parent routes and the current route.\n  To align our generated types more closely to the runtime behavior, we now generate more permissive, wider types when accessing child route information.\n\n- Updated dependencies:\n  - `react-router@7.0.2`\n  - `@react-router/node@7.0.2`\n  - `@react-router/serve@7.0.2`\n\n## 7.0.1\n\n### Patch Changes\n\n- Pass route error to ErrorBoundary as a prop ([#12338](https://github.com/remix-run/react-router/pull/12338))\n- Ensure typegen file watcher is cleaned up when Vite dev server restarts ([#12331](https://github.com/remix-run/react-router/pull/12331))\n- Updated dependencies:\n  - `react-router@7.0.1`\n  - `@react-router/node@7.0.1`\n  - `@react-router/serve@7.0.1`\n\n## 7.0.0\n\n### Major Changes\n\n- For Remix consumers migrating to React Router, the `vitePlugin` and `cloudflareDevProxyVitePlugin` exports have been renamed and moved. ([#11904](https://github.com/remix-run/react-router/pull/11904))\n\n  ```diff\n  -import {\n  -  vitePlugin as remix,\n  -  cloudflareDevProxyVitePlugin,\n  -} from \"@remix/dev\";\n\n  +import { reactRouter } from \"@react-router/dev/vite\";\n  +import { cloudflareDevProxy } from \"@react-router/dev/vite/cloudflare\";\n  ```\n\n- Remove single fetch future flag. ([#11522](https://github.com/remix-run/react-router/pull/11522))\n\n- update minimum node version to 18 ([#11690](https://github.com/remix-run/react-router/pull/11690))\n\n- Add `exports` field to all packages ([#11675](https://github.com/remix-run/react-router/pull/11675))\n\n- node package no longer re-exports from react-router ([#11702](https://github.com/remix-run/react-router/pull/11702))\n\n- For Remix consumers migrating to React Router who used the Vite plugin's `buildEnd` hook, the resolved `reactRouterConfig` object no longer contains a `publicPath` property since this belongs to Vite, not React Router. ([#11575](https://github.com/remix-run/react-router/pull/11575))\n\n- For Remix consumers migrating to React Router, the Vite plugin's `manifest` option has been removed. ([#11573](https://github.com/remix-run/react-router/pull/11573))\n\n  The `manifest` option been superseded by the more powerful `buildEnd` hook since it's passed the `buildManifest` argument. You can still write the build manifest to disk if needed, but you'll most likely find it more convenient to write any logic depending on the build manifest within the `buildEnd` hook itself.\n\n  If you were using the `manifest` option, you can replace it with a `buildEnd` hook that writes the manifest to disk like this:\n\n  ```ts\n  // react-router.config.ts\n  import type { Config } from \"@react-router/dev/config\";\n  import { writeFile } from \"node:fs/promises\";\n\n  export default {\n    async buildEnd({ buildManifest }) {\n      await writeFile(\n        \"build/manifest.json\",\n        JSON.stringify(buildManifest, null, 2),\n        \"utf-8\",\n      );\n    },\n  } satisfies Config;\n  ```\n\n- Consolidate types previously duplicated across `@remix-run/router`, `@remix-run/server-runtime`, and `@remix-run/react` now that they all live in `react-router` ([#12177](https://github.com/remix-run/react-router/pull/12177))\n  - Examples: `LoaderFunction`, `LoaderFunctionArgs`, `ActionFunction`, `ActionFunctionArgs`, `DataFunctionArgs`, `RouteManifest`, `LinksFunction`, `Route`, `EntryRoute`\n  - The `RouteManifest` type used by the \"remix\" code is now slightly stricter because it is using the former `@remix-run/router` `RouteManifest`\n    - `Record<string, Route> -> Record<string, Route | undefined>`\n  - Removed `AppData` type in favor of inlining `unknown` in the few locations it was used\n  - Removed `ServerRuntimeMeta*` types in favor of the `Meta*` types they were duplicated from\n\n- Update default `isbot` version to v5 and drop support for `isbot@3` ([#11770](https://github.com/remix-run/react-router/pull/11770))\n  - If you have `isbot@4` or `isbot@5` in your `package.json`:\n    - You do not need to make any changes\n  - If you have `isbot@3` in your `package.json` and you have your own `entry.server.tsx` file in your repo\n    - You do not need to make any changes\n    - You can upgrade to `isbot@5` independent of the React Router v7 upgrade\n  - If you have `isbot@3` in your `package.json` and you do not have your own `entry.server.tsx` file in your repo\n    - You are using the internal default entry provided by React Router v7 and you will need to upgrade to `isbot@5` in your `package.json`\n\n- Drop support for Node 18, update minimum Node vestion to 20 ([#12171](https://github.com/remix-run/react-router/pull/12171))\n  - Remove `installGlobals()` as this should no longer be necessary\n\n- For Remix consumers migrating to React Router, Vite manifests (i.e. `.vite/manifest.json`) are now written within each build subdirectory, e.g. `build/client/.vite/manifest.json` and `build/server/.vite/manifest.json` instead of `build/.vite/client-manifest.json` and `build/.vite/server-manifest.json`. This means that the build output is now much closer to what you'd expect from a typical Vite project. ([#11573](https://github.com/remix-run/react-router/pull/11573))\n\n  Originally the Remix Vite plugin moved all Vite manifests to a root-level `build/.vite` directory to avoid accidentally serving them in production, particularly from the client build. This was later improved with additional logic that deleted these Vite manifest files at the end of the build process unless Vite's `build.manifest` had been enabled within the app's Vite config. This greatly reduced the risk of accidentally serving the Vite manifests in production since they're only present when explicitly asked for. As a result, we can now assume that consumers will know that they need to manage these additional files themselves, and React Router can safely generate a more standard Vite build output.\n\n### Minor Changes\n\n- Params, loader data, and action data as props for route component exports ([#11961](https://github.com/remix-run/react-router/pull/11961))\n\n  ```tsx\n  export default function Component({ params, loaderData, actionData }) {}\n\n  export function HydrateFallback({ params }) {}\n  export function ErrorBoundary({ params, loaderData, actionData }) {}\n  ```\n\n- Remove internal entry.server.spa.tsx implementation ([#11681](https://github.com/remix-run/react-router/pull/11681))\n\n- Add `prefix` route config helper to `@react-router/dev/routes` ([#12094](https://github.com/remix-run/react-router/pull/12094))\n\n- ### Typesafety improvements ([#12019](https://github.com/remix-run/react-router/pull/12019))\n\n  React Router now generates types for each of your route modules.\n  You can access those types by importing them from `./+types.<route filename without extension>`.\n  For example:\n\n  ```ts\n  // app/routes/product.tsx\n  import type * as Route from \"./+types.product\";\n\n  export function loader({ params }: Route.LoaderArgs) {}\n\n  export default function Component({ loaderData }: Route.ComponentProps) {}\n  ```\n\n  This initial implementation targets type inference for:\n  - `Params` : Path parameters from your routing config in `routes.ts` including file-based routing\n  - `LoaderData` : Loader data from `loader` and/or `clientLoader` within your route module\n  - `ActionData` : Action data from `action` and/or `clientAction` within your route module\n\n  In the future, we plan to add types for the rest of the route module exports: `meta`, `links`, `headers`, `shouldRevalidate`, etc.\n  We also plan to generate types for typesafe `Link`s:\n\n  ```tsx\n  <Link to=\"/products/:id\" params={{ id: 1 }} />\n  //        ^^^^^^^^^^^^^          ^^^^^^^^^\n  // typesafe `to` and `params` based on the available routes in your app\n  ```\n\n  Check out our docs for more:\n  - [_Explanations > Type Safety_](https://reactrouter.com/dev/guides/explanation/type-safety)\n  - [_How-To > Setting up type safety_](https://reactrouter.com/dev/guides/how-to/setting-up-type-safety)\n\n### Patch Changes\n\n- Enable prerendering for resource routes ([#12200](https://github.com/remix-run/react-router/pull/12200))\n- chore: warn instead of error for min node version in CLI ([#12270](https://github.com/remix-run/react-router/pull/12270))\n- chore: re-enable development warnings through a `development` exports condition. ([#12269](https://github.com/remix-run/react-router/pull/12269))\n- include root \"react-dom\" module for optimization ([#12060](https://github.com/remix-run/react-router/pull/12060))\n- resolve config directory relative to flat output file structure ([#12187](https://github.com/remix-run/react-router/pull/12187))\n- if we are in SAP mode, always render the `index.html` for hydration ([#12268](https://github.com/remix-run/react-router/pull/12268))\n- fix(react-router): (v7) fix static prerender of non-ascii characters ([#12161](https://github.com/remix-run/react-router/pull/12161))\n- Updated dependencies:\n  - `react-router@7.0.0`\n  - `@react-router/serve@7.0.0`\n  - `@react-router/node@7.0.0`\n\n## 2.9.0\n\n### Minor Changes\n\n- New `future.unstable_singleFetch` flag ([#8773](https://github.com/remix-run/remix/pull/8773))\n  - Naked objects returned from loaders/actions are no longer automatically converted to JSON responses. They'll be streamed as-is via `turbo-stream` so `Date`'s will become `Date` through `useLoaderData()`\n  - You can return naked objects with `Promise`'s without needing to use `defer()` - including nested `Promise`'s\n    - If you need to return a custom status code or custom response headers, you can still use the `defer` utility\n  - `<RemixServer abortDelay>` is no longer used. Instead, you should `export const streamTimeout` from `entry.server.tsx` and the remix server runtime will use that as the delay to abort the streamed response\n    - If you export your own streamTimeout, you should decouple that from aborting the react `renderToPipeableStream`. You should always ensure that react is aborted _afer_ the stream is aborted so that abort rejections can be flushed down\n  - Actions no longer automatically revalidate on 4xx/5xx responses (via RR `future.unstable_skipActionErrorRevalidation` flag) - you can return a 2xx to opt-into revalidation or use `shouldRevalidate`\n\n### Patch Changes\n\n- Improve `getDependenciesToBundle` resolution in monorepos ([#8848](https://github.com/remix-run/remix/pull/8848))\n- Fix SPA mode when single fetch is enabled by using streaming entry.server ([#9063](https://github.com/remix-run/remix/pull/9063))\n- Vite: added sourcemap support for transformed routes ([#8970](https://github.com/remix-run/remix/pull/8970))\n- Update links printed to the console by the Remix CLI/Dev Server to point to updated docs locations ([#9176](https://github.com/remix-run/remix/pull/9176))\n- Updated dependencies:\n  - `@remix-run/node@2.9.0`\n  - `@remix-run/server-runtime@2.9.0`\n\n## 2.8.1\n\n### Patch Changes\n\n- Support reading from Vite config when running `remix reveal` and `remix routes` CLI commands ([#8916](https://github.com/remix-run/remix/pull/8916))\n- Add Vite commands to Remix CLI `--help` output ([#8939](https://github.com/remix-run/remix/pull/8939))\n- Vite: Fix support for `build.sourcemap` option in Vite config ([#8965](https://github.com/remix-run/remix/pull/8965))\n- Clean up redundant client route query strings on route JavaScript files in production builds ([#8969](https://github.com/remix-run/remix/pull/8969))\n- Vite: Fix error when using Vite's `server.fs.allow` option without a client entry file ([#8966](https://github.com/remix-run/remix/pull/8966))\n- Updated dependencies:\n  - `@remix-run/node@2.8.1`\n  - `@remix-run/server-runtime@2.8.1`\n\n## 2.8.0\n\n### Minor Changes\n\n- Pass resolved `viteConfig` to Remix Vite plugin's `buildEnd` hook ([#8885](https://github.com/remix-run/remix/pull/8885))\n\n### Patch Changes\n\n- Mark `Layout` as browser safe route export in `esbuild` compiler ([#8842](https://github.com/remix-run/remix/pull/8842))\n- Vite: Silence build warnings when dependencies include \"use client\" directives ([#8897](https://github.com/remix-run/remix/pull/8897))\n- Vite: Fix `serverBundles` issue where multiple browser manifests are generated ([#8864](https://github.com/remix-run/remix/pull/8864))\n- Support custom Vite `build.assetsDir` option ([#8843](https://github.com/remix-run/remix/pull/8843))\n- Updated dependencies:\n  - `@remix-run/node@2.8.0`\n  - `@remix-run/server-runtime@2.8.0`\n\n## 2.7.2\n\n### Patch Changes\n\n- Vite: Fix error when building projects with `.css?url` imports ([#8829](https://github.com/remix-run/remix/pull/8829))\n- Updated dependencies:\n  - `@remix-run/node@2.7.2`\n  - `@remix-run/server-runtime@2.7.2`\n\n## 2.7.1\n\n### Patch Changes\n\n- Updated dependencies:\n  - `@remix-run/node@2.7.1`\n  - `@remix-run/server-runtime@2.7.1`\n\n## 2.7.0\n\n### Minor Changes\n\n- Allow an optional `Layout` export from the root route ([#8709](https://github.com/remix-run/remix/pull/8709))\n\n- Vite: Cloudflare Proxy as a Vite plugin ([#8749](https://github.com/remix-run/remix/pull/8749))\n\n  **This is a breaking change for projects relying on Cloudflare support from the unstable Vite plugin**\n\n  The Cloudflare preset (`unstable_cloudflarePreset`) as been removed and replaced with a new Vite plugin:\n\n  ```diff\n   import {\n      unstable_vitePlugin as remix,\n  -   unstable_cloudflarePreset as cloudflare,\n  +   cloudflareDevProxyVitePlugin as remixCloudflareDevProxy,\n    } from \"@remix-run/dev\";\n    import { defineConfig } from \"vite\";\n\n    export default defineConfig({\n      plugins: [\n  +     remixCloudflareDevProxy(),\n  +     remix(),\n  -     remix({\n  -       presets: [cloudflare()],\n  -     }),\n      ],\n  -   ssr: {\n  -     resolve: {\n  -       externalConditions: [\"workerd\", \"worker\"],\n  -     },\n  -   },\n    });\n  ```\n\n  `remixCloudflareDevProxy` must come _before_ the `remix` plugin so that it can override Vite's dev server middleware to be compatible with Cloudflare's proxied environment.\n\n  Because it is a Vite plugin, `remixCloudflareDevProxy` can set `ssr.resolve.externalConditions` to be `workerd`-compatible for you.\n\n  `remixCloudflareDevProxy` accepts a `getLoadContext` function that replaces the old `getRemixDevLoadContext`.\n  If you were using a `nightly` version that required `getBindingsProxy` or `getPlatformProxy`, that is no longer required.\n  Any options you were passing to `getBindingsProxy` or `getPlatformProxy` should now be passed to `remixCloudflareDevProxy` instead.\n\n  This API also better aligns with future plans to support Cloudflare with a framework-agnostic Vite plugin that makes use of Vite's (experimental) Runtime API.\n\n- Vite: Stabilize the Remix Vite plugin, Cloudflare preset, and all related types by removing all `unstable_` / `Unstable_` prefixes. ([#8713](https://github.com/remix-run/remix/pull/8713))\n\n  While this is a breaking change for existing Remix Vite plugin consumers, now that the plugin has stabilized, there will no longer be any breaking changes outside of a major release. Thank you to all of our early adopters and community contributors for helping us get here! 🙏\n\n- Vite: Stabilize \"SPA Mode\" by renaming the Remix vite plugin config from `unstable_ssr -> ssr` ([#8692](https://github.com/remix-run/remix/pull/8692))\n\n- Vite: Add a new `basename` option to the Vite plugin, allowing users to set the internal React Router [`basename`](https://reactrouter.com/en/main/routers/create-browser-router#basename) in order to to serve their applications underneath a subpath ([#8145](https://github.com/remix-run/remix/pull/8145))\n\n### Patch Changes\n\n- Vite: fix server exports dead-code elimination for routes outside of app directory ([#8795](https://github.com/remix-run/remix/pull/8795))\n\n- Always prepend DOCTYPE in SPA mode entry.server.tsx, can opt out via remix reveal ([#8725](https://github.com/remix-run/remix/pull/8725))\n\n- Fix build issue in SPA mode when using a `basename` ([#8720](https://github.com/remix-run/remix/pull/8720))\n\n- Vite: Validate that the MDX Rollup plugin, if present, is placed before Remix in Vite config ([#8690](https://github.com/remix-run/remix/pull/8690))\n\n- Vite: reliably detect non-root routes in Windows ([#8806](https://github.com/remix-run/remix/pull/8806))\n\n  Sometimes route `file` will be unnormalized Windows path with `\\` instead of `/`.\n\n- Vite: Pass `remixUserConfig` to preset `remixConfig` hook ([#8797](https://github.com/remix-run/remix/pull/8797))\n\n- Vite: Fix issue resolving critical CSS during development when the current working directory differs from the project root ([#8752](https://github.com/remix-run/remix/pull/8752))\n\n- Vite: Ensure CSS file URLs that are only referenced in the server build are available on the client ([#8796](https://github.com/remix-run/remix/pull/8796))\n\n- Vite: Require version 5.1.0 to support `.css?url` imports ([#8723](https://github.com/remix-run/remix/pull/8723))\n\n- Fix type error in Remix config for synchronous `routes` function ([#8745](https://github.com/remix-run/remix/pull/8745))\n\n- Vite: Support Vite v5.1.0's `.css?url` imports ([#8684](https://github.com/remix-run/remix/pull/8684))\n\n- Always ignore route files starting with `.` ([#8801](https://github.com/remix-run/remix/pull/8801))\n\n- Vite: Enable use of [`vite preview`](https://main.vitejs.dev/guide/static-deploy.html#deploying-a-static-site) to preview Remix SPA applications ([#8624](https://github.com/remix-run/remix/pull/8624))\n  - In the SPA template, `npm run start` has been renamed to `npm run preview` which uses `vite preview` instead of a standalone HTTP server such as `http-server` or `serv-cli`\n\n- Vite: Remove the ability to pass `publicPath` as an option to the Remix vite plugin ([#8145](https://github.com/remix-run/remix/pull/8145))\n  - ⚠️ **This is a breaking change for projects using the unstable Vite plugin with a `publicPath`**\n  - This is already handled in Vite via the [`base`](https://vitejs.dev/guide/build.html#public-base-path) config so we now set the Remix `publicPath` from the Vite `base` config\n\n- Vite: Fix issue where client route file requests fail if search params have been parsed and serialized before reaching the Remix Vite plugin ([#8740](https://github.com/remix-run/remix/pull/8740))\n\n- Vite: Enable HMR for .md and .mdx files ([#8711](https://github.com/remix-run/remix/pull/8711))\n\n- Updated dependencies:\n  - `@remix-run/server-runtime@2.7.0`\n  - `@remix-run/node@2.7.0`\n\n## 2.6.0\n\n### Minor Changes\n\n- Add `future.v3_throwAbortReason` flag to throw `request.signal.reason` when a request is aborted instead of an `Error` such as `new Error(\"query() call aborted: GET /path\")` ([#8251](https://github.com/remix-run/remix/pull/8251))\n\n### Patch Changes\n\n- Vite: Add `manifest` option to Vite plugin to enable writing a `.remix/manifest.json` file to the build directory ([#8575](https://github.com/remix-run/remix/pull/8575))\n\n  **This is a breaking change for consumers of the Vite plugin's \"server bundles\" feature.**\n\n  The `build/server/bundles.json` file has been superseded by the more general `build/.remix/manifest.json`. While the old server bundles manifest was always written to disk when generating server bundles, the build manifest file must be explicitly enabled via the `manifest` option.\n\n- Vite: Provide `Unstable_ServerBundlesFunction` and `Unstable_VitePluginConfig` types ([#8654](https://github.com/remix-run/remix/pull/8654))\n\n- Vite: add `--sourcemapClient` and `--sourcemapServer` flags to `remix vite:build` ([#8613](https://github.com/remix-run/remix/pull/8613))\n  - `--sourcemapClient`\n\n  - `--sourcemapClient=inline`\n\n  - `--sourcemapClient=hidden`\n\n  - `--sourcemapServer`\n\n  - `--sourcemapServer=inline`\n\n  - `--sourcemapServer=hidden`\n\n  See <https://vitejs.dev/config/build-options.html#build-sourcemap>\n\n- Vite: Validate IDs returned from the `serverBundles` function to ensure they only contain alphanumeric characters, hyphens and underscores ([#8598](https://github.com/remix-run/remix/pull/8598))\n\n- Vite: fix \"could not fast refresh\" false alarm ([#8580](https://github.com/remix-run/remix/pull/8580))\n\n  HMR is already functioning correctly but was incorrectly logging that it \"could not fast refresh\" on internal client routes.\n  Now internal client routes correctly register Remix exports like `meta` for fast refresh,\n  which removes the false alarm.\n\n- Vite: Cloudflare Pages support ([#8531](https://github.com/remix-run/remix/pull/8531))\n\n  To get started with Cloudflare, you can use the \\[`unstable-vite-cloudflare`]\\[template-vite-cloudflare] template:\n\n  ```shellscript nonumber\n  npx create-remix@latest --template remix-run/remix/templates/unstable-vite-cloudflare\n  ```\n\n  Or read the new docs at [Future > Vite > Cloudflare](https://remix.run/docs/en/main/future/vite#cloudflare) and\n  [Future > Vite > Migrating > Migrating Cloudflare Functions](https://remix.run/docs/en/main/future/vite#migrating-cloudflare-functions).\n\n- Vite: Remove undocumented backwards compatibility layer for Vite v4 ([#8581](https://github.com/remix-run/remix/pull/8581))\n\n- Vite: rely on Vite plugin ordering ([#8627](https://github.com/remix-run/remix/pull/8627))\n\n  **This is a breaking change for projects using the unstable Vite plugin.**\n\n  The Remix plugin expects to process JavaScript or TypeScript files, so any transpilation from other languages must be done first.\n  For example, that means putting the MDX plugin _before_ the Remix plugin:\n\n  ```diff\n    import mdx from \"@mdx-js/rollup\";\n    import { unstable_vitePlugin as remix } from \"@remix-run/dev\";\n    import { defineConfig } from \"vite\";\n\n    export default defineConfig({\n      plugins: [\n  +     mdx(),\n        remix()\n  -     mdx(),\n      ],\n    });\n  ```\n\n  Previously, the Remix plugin misused `enforce: \"post\"` from Vite's plugin API to ensure that it ran last.\n  However, this caused other unforeseen issues.\n  Instead, we now rely on standard Vite semantics for plugin ordering.\n\n  The official [Vite React SWC plugin](https://github.com/vitejs/vite-plugin-react-swc/blob/main/src/index.ts#L97-L116) also relies on plugin ordering for MDX.\n\n- Vite: Add `presets` option to ease integration with different platforms and tools. ([#8514](https://github.com/remix-run/remix/pull/8514))\n\n- Vite: Remove interop with `<LiveReload />`, rely on `<Scripts />` instead ([#8636](https://github.com/remix-run/remix/pull/8636))\n\n  **This is a breaking change for projects using the unstable Vite plugin.**\n\n  Vite provides a robust client-side runtime for development features like HMR,\n  making the `<LiveReload />` component obsolete.\n\n  In fact, having a separate dev scripts component was causing issues with script execution order.\n  To work around this, the Remix Vite plugin used to override `<LiveReload />` into a bespoke\n  implementation that was compatible with Vite.\n\n  Instead of all this indirection, now the Remix Vite plugin instructs the `<Scripts />` component\n  to automatically include Vite's client-side runtime and other dev-only scripts.\n\n  ```diff\n    import {\n  -   LiveReload,\n      Outlet,\n      Scripts,\n    }\n\n    export default function App() {\n      return (\n        <html>\n          <head>\n          </head>\n          <body>\n            <Outlet />\n            <Scripts />\n  -         <LiveReload />\n          </body>\n        </html>\n      )\n    }\n  ```\n\n- Vite: Add `buildEnd` hook ([#8620](https://github.com/remix-run/remix/pull/8620))\n\n- Vite: add dev load context option to Cloudflare preset ([#8649](https://github.com/remix-run/remix/pull/8649))\n\n- Vite: Add `mode` field into generated server build ([#8539](https://github.com/remix-run/remix/pull/8539))\n\n- Vite: Only write Vite manifest files if `build.manifest` is enabled within the Vite config ([#8599](https://github.com/remix-run/remix/pull/8599))\n\n  **This is a breaking change for consumers of Vite's `manifest.json` files.**\n\n  To explicitly enable generation of Vite manifest files, you must set `build.manifest` to `true` in your Vite config.\n\n  ```ts\n  export default defineConfig({\n    build: { manifest: true },\n    // ...\n  });\n  ```\n\n- Vite: reduce network calls for route modules during HMR ([#8591](https://github.com/remix-run/remix/pull/8591))\n\n- Vite: Add new `buildDirectory` option with a default value of `\"build\"`. This replaces the old `assetsBuildDirectory` and `serverBuildDirectory` options which defaulted to `\"build/client\"` and `\"build/server\"` respectively. ([#8575](https://github.com/remix-run/remix/pull/8575))\n\n  **This is a breaking change for consumers of the Vite plugin that were using the `assetsBuildDirectory` and `serverBuildDirectory` options.**\n\n  The Remix Vite plugin now builds into a single directory containing `client` and `server` directories. If you've customized your build output directories, you'll need to migrate to the new `buildDirectory` option, e.g.\n\n  ```diff\n  import { unstable_vitePlugin as remix } from \"@remix-run/dev\";\n  import { defineConfig } from \"vite\";\n\n  export default defineConfig({\n    plugins: [\n      remix({\n  -      serverBuildDirectory: \"dist/server\",\n  -      assetsBuildDirectory: \"dist/client\",\n  +      buildDirectory: \"dist\",\n      })\n    ],\n  });\n  ```\n\n- Vite: Remove `unstable` prefix from `serverBundles` option. ([#8596](https://github.com/remix-run/remix/pull/8596))\n\n- Vite: Write Vite manifest files to `build/.vite` directory rather than being nested within `build/client` and `build/server` directories. ([#8599](https://github.com/remix-run/remix/pull/8599))\n\n  **This is a breaking change for consumers of Vite's `manifest.json` files.**\n\n  Vite manifest files are now written to the Remix build directory. Since all Vite manifests are now in the same directory, they're no longer named `manifest.json`. Instead, they're named `build/.vite/client-manifest.json` and `build/.vite/server-manifest.json`, or `build/.vite/server-{BUNDLE_ID}-manifest.json` when using server bundles.\n\n- Updated dependencies:\n  - `@remix-run/server-runtime@2.6.0`\n  - `@remix-run/node@2.6.0`\n\n## 2.5.1\n\n### Patch Changes\n\n- Add `isSpaMode` to `@remix-run/dev/server-build` virtual module ([#8492](https://github.com/remix-run/remix/pull/8492))\n- Automatically prepend `<!DOCTYPE html>` if not present to fix quirks mode warnings for SPA template ([#8495](https://github.com/remix-run/remix/pull/8495))\n- Vite: Errors for server-only code point to new docs ([#8488](https://github.com/remix-run/remix/pull/8488))\n- Vite: Fix HMR race condition when reading changed file contents ([#8479](https://github.com/remix-run/remix/pull/8479))\n- Vite: Tree-shake unused route exports in the client build ([#8468](https://github.com/remix-run/remix/pull/8468))\n- Vite: Performance profiling ([#8493](https://github.com/remix-run/remix/pull/8493))\n  - Run `remix vite:build --profile` to generate a `.cpuprofile` that can be shared or uploaded to speedscope.app\n  - In dev, press `p + enter` to start a new profiling session or stop the current session\n  - If you need to profile dev server startup, run `remix vite:dev --profile` to initialize the dev server with a running profiling session\n  - For more, see the new docs: Vite > Performance\n- Vite: Improve performance of dev server requests by invalidating Remix's virtual modules on relevant file changes rather than on every request ([#8164](https://github.com/remix-run/remix/pull/8164))\n- Updated dependencies:\n  - `@remix-run/node@2.5.1`\n  - `@remix-run/server-runtime@2.5.1`\n\n## 2.5.0\n\n### Minor Changes\n\n- Add unstable support for \"SPA Mode\" ([#8457](https://github.com/remix-run/remix/pull/8457))\n\n  You can opt into SPA Mode by setting `unstable_ssr: false` in your Remix Vite plugin config:\n\n  ```js\n  // vite.config.ts\n  import { unstable_vitePlugin as remix } from \"@remix-run/dev\";\n  import { defineConfig } from \"vite\";\n\n  export default defineConfig({\n    plugins: [remix({ unstable_ssr: false })],\n  });\n  ```\n\n  Development in SPA Mode is just like a normal Remix app, and still uses the Remix dev server for HMR/HDR:\n\n  ```sh\n  remix vite:dev\n  ```\n\n  Building in SPA Mode will generate an `index.html` file in your client assets directory:\n\n  ```sh\n  remix vite:build\n  ```\n\n  To run your SPA, you serve your client assets directory via an HTTP server:\n\n  ```sh\n  npx http-server build/client\n  ```\n\n  For more information, please refer to the [SPA Mode docs](https://remix.run/future/spa-mode).\n\n- Add `unstable_serverBundles` option to Vite plugin to support splitting server code into multiple request handlers. ([#8332](https://github.com/remix-run/remix/pull/8332))\n\n  This is an advanced feature designed for hosting provider integrations. When compiling your app into multiple server bundles, there will need to be a custom routing layer in front of your app directing requests to the correct bundle. This feature is currently unstable and only designed to gather early feedback.\n\n  **Example usage:**\n\n  ```ts\n  import { unstable_vitePlugin as remix } from \"@remix-run/dev\";\n  import { defineConfig } from \"vite\";\n\n  export default defineConfig({\n    plugins: [\n      remix({\n        unstable_serverBundles: ({ branch }) => {\n          const isAuthenticatedRoute = branch.some(\n            (route) => route.id === \"routes/_authenticated\",\n          );\n\n          return isAuthenticatedRoute ? \"authenticated\" : \"unauthenticated\";\n        },\n      }),\n    ],\n  });\n  ```\n\n### Patch Changes\n\n- Fix issue with `isbot` v4 released on 1/1/2024 ([#8415](https://github.com/remix-run/remix/pull/8415))\n  - `remix dev` will now add `\"isbot\": \"^4\"` to `package.json` instead of using `latest`\n  - Update built-in `entry.server` files to work with both `isbot@3` and `isbot@4` for backwards-compatibility with Remix apps that have pinned `isbot` to v3\n  - Templates are updated to use `isbot@4` moving forward via `create-remix`\n\n- Vite: Fix HMR issues when altering exports for non-rendered routes ([#8157](https://github.com/remix-run/remix/pull/8157))\n\n- Vite: Default `NODE_ENV` to `\"production\"` when running `remix vite:build` command ([#8405](https://github.com/remix-run/remix/pull/8405))\n\n- Vite: Remove Vite plugin config option `serverBuildPath` in favor of separate `serverBuildDirectory` and `serverBuildFile` options ([#8332](https://github.com/remix-run/remix/pull/8332))\n\n- Vite: Loosen strict route exports restriction, reinstating support for non-Remix route exports ([#8420](https://github.com/remix-run/remix/pull/8420))\n\n- Updated dependencies:\n  - `@remix-run/server-runtime@2.5.0`\n  - `@remix-run/node@2.5.0`\n\n## 2.4.1\n\n### Patch Changes\n\n- Vite: Error messages when `.server` files are referenced by client ([#8267](https://github.com/remix-run/remix/pull/8267))\n  - Previously, referencing a `.server` module from client code resulted in an error message like:\n    - `The requested module '/app/models/answer.server.ts' does not provide an export named 'isDateType'`\n  - This was confusing because `answer.server.ts` _does_ provide the `isDateType` export, but Remix was replacing `.server` modules with empty modules (`export {}`) for the client build\n  - Now, Remix explicitly fails at compile time when a `.server` module is referenced from client code and includes dedicated error messages depending on whether the import occurs in a route or a non-route module\n  - The error messages also include links to relevant documentation\n\n- Remove `unstable_viteServerBuildModuleId` in favor of manually referencing virtual module name `\"virtual:remix/server-build\"`. ([#8264](https://github.com/remix-run/remix/pull/8264))\n\n  **This is a breaking change for projects using the unstable Vite plugin with a custom server.**\n\n  This change was made to avoid issues where `@remix-run/dev` could be inadvertently required in your server's production dependencies.\n\n  Instead, you should manually write the virtual module name `\"virtual:remix/server-build\"` when calling `ssrLoadModule` in development.\n\n  ```diff\n  -import { unstable_viteServerBuildModuleId } from \"@remix-run/dev\";\n\n  // ...\n\n  app.all(\n    \"*\",\n    createRequestHandler({\n      build: vite\n  -      ? () => vite.ssrLoadModule(unstable_viteServerBuildModuleId)\n  +      ? () => vite.ssrLoadModule(\"virtual:remix/server-build\")\n        : await import(\"./build/server/index.js\"),\n    })\n  );\n  ```\n\n- Vite: Fix errors for non-existent `index.html` importer ([#8353](https://github.com/remix-run/remix/pull/8353))\n\n- Add `vite:dev` and `vite:build` commands to the Remix CLI. ([#8211](https://github.com/remix-run/remix/pull/8211))\n\n  In order to handle upcoming Remix features where your plugin options can impact the number of Vite builds required, you should now run your Vite `dev` and `build` processes via the Remix CLI.\n\n  ```diff\n  {\n    \"scripts\": {\n  -    \"dev\": \"vite dev\",\n  -    \"build\": \"vite build && vite build --ssr\"\n  +    \"dev\": \"remix vite:dev\",\n  +    \"build\": \"remix vite:build\"\n    }\n  }\n  ```\n\n- Vite: Preserve names for exports from `.client` modules ([#8200](https://github.com/remix-run/remix/pull/8200))\n\n  Unlike `.server` modules, the main idea is not to prevent code from leaking into the server build\n  since the client build is already public. Rather, the goal is to isolate the SSR render from client-only code.\n  Routes need to import code from `.client` modules without compilation failing and then rely on runtime checks\n  or otherwise ensure that execution only happens within a client-only context (e.g. event handlers, `useEffect`).\n\n  Replacing `.client` modules with empty modules would cause the build to fail as ESM named imports are statically analyzed.\n  So instead, we preserve the named export but replace each exported value with `undefined`.\n  That way, the import is valid at build time and standard runtime checks can be used to determine if the\n  code is running on the server or client.\n\n- Disable watch mode in Vite child compiler during build ([#8342](https://github.com/remix-run/remix/pull/8342))\n\n- Vite: Show warning when source maps are enabled in production build ([#8222](https://github.com/remix-run/remix/pull/8222))\n\n- Updated dependencies:\n  - `@remix-run/server-runtime@2.4.1`\n  - `@remix-run/node@2.4.1`\n\n## 2.4.0\n\n### Minor Changes\n\n- Vite: exclude modules within `.server` directories from client build ([#8154](https://github.com/remix-run/remix/pull/8154))\n\n- Add support for `clientLoader`/`clientAction`/`HydrateFallback` route exports ([RFC](https://github.com/remix-run/remix/discussions/7634)) ([#8173](https://github.com/remix-run/remix/pull/8173))\n\n  Remix now supports loaders/actions that run on the client (in addition to, or instead of the loader/action that runs on the server). While we still recommend server loaders/actions for the majority of your data needs in a Remix app - these provide some levers you can pull for more advanced use-cases such as:\n  - Leveraging a data source local to the browser (i.e., `localStorage`)\n  - Managing a client-side cache of server data (like `IndexedDB`)\n  - Bypassing the Remix server in a BFF setup and hitting your API directly from the browser\n  - Migrating a React Router SPA to a Remix application\n\n  By default, `clientLoader` will not run on hydration, and will only run on subsequent client side navigations.\n\n  If you wish to run your client loader on hydration, you can set `clientLoader.hydrate=true` to force Remix to execute it on initial page load. Keep in mind that Remix will still SSR your route component so you should ensure that there is no new _required_ data being added by your `clientLoader`.\n\n  If your `clientLoader` needs to run on hydration and adds data you require to render the route component, you can export a `HydrateFallback` component that will render during SSR, and then your route component will not render until the `clientLoader` has executed on hydration.\n\n  `clientAction` is simpler than `clientLoader` because it has no hydration use-cases. `clientAction` will only run on client-side navigations.\n\n  For more information, please refer to the [`clientLoader`](https://remix.run/route/client-loader) and [`clientAction`](https://remix.run/route/client-action) documentation.\n\n- Vite: Strict route exports ([#8171](https://github.com/remix-run/remix/pull/8171))\n\n  With Vite, Remix gets stricter about which exports are allowed from your route modules.\n  Previously, the Remix compiler would allow any export from routes.\n  While this was convenient, it was also a common source of bugs that were hard to track down because they only surfaced at runtime.\n\n  For more, see <https://remix.run/docs/en/main/future/vite#strict-route-exports>\n\n- Add a new `future.v3_relativeSplatPath` flag to implement a breaking bug fix to relative routing when inside a splat route. For more information, please see the React Router [`6.21.0` Release Notes](https://github.com/remix-run/react-router/blob/release-next/CHANGELOG.md#futurev7_relativesplatpath) and the [`useResolvedPath` docs](https://remix.run/hooks/use-resolved-path#splat-paths). ([#8216](https://github.com/remix-run/remix/pull/8216))\n\n### Patch Changes\n\n- Upgrade Vite peer dependency range to v5 ([#8172](https://github.com/remix-run/remix/pull/8172))\n\n- Support HMR for routes with `handle` export in Vite dev ([#8022](https://github.com/remix-run/remix/pull/8022))\n\n- Fix flash of unstyled content for non-Express custom servers in Vite dev ([#8076](https://github.com/remix-run/remix/pull/8076))\n\n- Bundle CSS imported in client entry file in Vite plugin ([#8143](https://github.com/remix-run/remix/pull/8143))\n\n- Change Vite build output paths to fix a conflict between how Vite and the Remix compiler each manage the `public` directory. ([#8077](https://github.com/remix-run/remix/pull/8077))\n\n  **This is a breaking change for projects using the unstable Vite plugin.**\n\n  The server is now compiled into `build/server` rather than `build`, and the client is now compiled into `build/client` rather than `public`.\n\n  For more information on the changes and guidance on how to migrate your project, refer to the updated [Remix Vite documentation](https://remix.run/docs/en/main/future/vite).\n\n- Remove undocumented `legacyCssImports` option from Vite plugin due to issues with `?url` imports of CSS files not being processed correctly in Vite ([#8096](https://github.com/remix-run/remix/pull/8096))\n\n- Vite: fix access to default `entry.{client,server}.tsx` within pnpm workspace on Windows ([#8057](https://github.com/remix-run/remix/pull/8057))\n\n- Remove `unstable_createViteServer` and `unstable_loadViteServerBuild` which were only minimal wrappers around Vite's `createServer` and `ssrLoadModule` functions when using a custom server. ([#8120](https://github.com/remix-run/remix/pull/8120))\n\n  **This is a breaking change for projects using the unstable Vite plugin with a custom server.**\n\n  Instead, we now provide `unstable_viteServerBuildModuleId` so that custom servers interact with Vite directly rather than via Remix APIs, for example:\n\n  ```diff\n  -import {\n  -  unstable_createViteServer,\n  -  unstable_loadViteServerBuild,\n  -} from \"@remix-run/dev\";\n  +import { unstable_viteServerBuildModuleId } from \"@remix-run/dev\";\n  ```\n\n  Creating the Vite server in middleware mode:\n\n  ```diff\n  const vite =\n    process.env.NODE_ENV === \"production\"\n      ? undefined\n  -    : await unstable_createViteServer();\n  +    : await import(\"vite\").then(({ createServer }) =>\n  +        createServer({\n  +          server: {\n  +            middlewareMode: true,\n  +          },\n  +        })\n  +      );\n  ```\n\n  Loading the Vite server build in the request handler:\n\n  ```diff\n  app.all(\n    \"*\",\n    createRequestHandler({\n      build: vite\n  -      ? () => unstable_loadViteServerBuild(vite)\n  +      ? () => vite.ssrLoadModule(unstable_viteServerBuildModuleId)\n        : await import(\"./build/server/index.js\"),\n    })\n  );\n  ```\n\n- Pass request handler errors to `vite.ssrFixStacktrace` in Vite dev to ensure stack traces correctly map to the original source code ([#8066](https://github.com/remix-run/remix/pull/8066))\n\n- Vite: Preserve names for exports from .client imports ([#8200](https://github.com/remix-run/remix/pull/8200))\n\n  Unlike `.server` modules, the main idea is not to prevent code from leaking into the server build\n  since the client build is already public. Rather, the goal is to isolate the SSR render from client-only code.\n  Routes need to import code from `.client` modules without compilation failing and then rely on runtime checks\n  to determine if the code is running on the server or client.\n\n  Replacing `.client` modules with empty modules would cause the build to fail as ESM named imports are statically analyzed.\n  So instead, we preserve the named export but replace each exported value with an empty object.\n  That way, the import is valid at build time and the standard runtime checks can be used to determine if then\n  code is running on the server or client.\n\n- Add `@remix-run/node` to Vite's `optimizeDeps.include` array ([#8177](https://github.com/remix-run/remix/pull/8177))\n\n- Improve Vite plugin performance ([#8121](https://github.com/remix-run/remix/pull/8121))\n  - Parallelize detection of route module exports\n  - Disable `server.preTransformRequests` in Vite child compiler since it's only used to process route modules\n\n- Remove automatic global Node polyfill installation from the built-in Vite dev server and instead allow explicit opt-in. ([#8119](https://github.com/remix-run/remix/pull/8119))\n\n  **This is a breaking change for projects using the unstable Vite plugin without a custom server.**\n\n  If you're not using a custom server, you should call `installGlobals` in your Vite config instead.\n\n  ```diff\n  import { unstable_vitePlugin as remix } from \"@remix-run/dev\";\n  +import { installGlobals } from \"@remix-run/node\";\n  import { defineConfig } from \"vite\";\n\n  +installGlobals();\n\n  export default defineConfig({\n    plugins: [remix()],\n  });\n  ```\n\n- Vite: Errors at build-time when client imports .server default export ([#8184](https://github.com/remix-run/remix/pull/8184))\n\n  Remix already stripped .server file code before ensuring that server code never makes it into the client.\n  That results in errors when client code tries to import server code, which is exactly what we want!\n  But those errors were happening at runtime for default imports.\n  A better experience is to have those errors happen at build-time so that you guarantee that your users won't hit them.\n\n- Fix `request instanceof Request` checks when using Vite dev server ([#8062](https://github.com/remix-run/remix/pull/8062))\n\n- Updated dependencies:\n  - `@remix-run/server-runtime@2.4.0`\n  - `@remix-run/node@2.4.0`\n\n## 2.3.1\n\n### Patch Changes\n\n- Support `nonce` prop on `LiveReload` component in Vite dev ([#8014](https://github.com/remix-run/remix/pull/8014))\n- Ensure code-split JS files in the server build's assets directory aren't cleaned up after Vite build ([#8042](https://github.com/remix-run/remix/pull/8042))\n- Fix redundant copying of assets from `public` directory in Vite build ([#8039](https://github.com/remix-run/remix/pull/8039))\n  - This ensures that static assets aren't duplicated in the server build directory\n  - This also fixes an issue where the build would break if `assetsBuildDirectory` was deeply nested within the `public` directory\n- Updated dependencies:\n  - `@remix-run/node@2.3.1`\n  - `@remix-run/server-runtime@2.3.1`\n\n## 2.3.0\n\n### Patch Changes\n\n- Support rendering of `LiveReload` component after `Scripts` in Vite dev ([#7919](https://github.com/remix-run/remix/pull/7919))\n- fix(vite): fix \"react-refresh/babel\" resolution for custom server with pnpm ([#7904](https://github.com/remix-run/remix/pull/7904))\n- Support JSX usage in `.jsx` files without manual `React` import in Vite ([#7888](https://github.com/remix-run/remix/pull/7888))\n- Support optional rendering of `LiveReload` component in Vite dev ([#7919](https://github.com/remix-run/remix/pull/7919))\n- Fix Vite production builds when plugins that have different local state between `development` and `production` modes are present, e.g. `@mdx-js/rollup`. ([#7911](https://github.com/remix-run/remix/pull/7911))\n- Cache resolution of Remix Vite plugin options ([#7908](https://github.com/remix-run/remix/pull/7908))\n- Support Vite 5 ([#7846](https://github.com/remix-run/remix/pull/7846))\n- Allow `process.env.NODE_ENV` values other than `\"development\"` in Vite dev ([#7980](https://github.com/remix-run/remix/pull/7980))\n- Attach CSS from shared chunks to routes in Vite build ([#7952](https://github.com/remix-run/remix/pull/7952))\n- fix(vite): Let Vite handle serving files outside of project root via `/@fs` ([#7913](https://github.com/remix-run/remix/pull/7913))\n  - This fixes errors when using default client entry or server entry in a pnpm project where those files may be outside of the project root, but within the workspace root.\n  - By default, Vite prevents access to files outside the workspace root (when using workspaces) or outside of the project root (when not using workspaces) unless user explicitly opts into it via Vite's `server.fs.allow`.\n- Improve performance of LiveReload proxy in Vite dev ([#7883](https://github.com/remix-run/remix/pull/7883))\n- fix(vite): deduplicate `@remix-run/react` ([#7926](https://github.com/remix-run/remix/pull/7926))\n  - Pre-bundle Remix dependencies to avoid Remix router duplicates.\n  - Our remix-react-proxy plugin does not process default client and\n  - server entry files since those come from within `node_modules`.\n  - That means that before Vite pre-bundles dependencies (e.g. first time dev server is run) mismatching Remix routers cause `Error: You must render this element inside a <Remix> element`.\n- Fix React Fast Refresh error on load when using `defer` in Vite dev server ([#7842](https://github.com/remix-run/remix/pull/7842))\n- Handle multiple \"Set-Cookie\" headers in Vite dev server ([#7843](https://github.com/remix-run/remix/pull/7843))\n- Fix flash of unstyled content on initial page load in Vite dev when using a custom Express server ([#7937](https://github.com/remix-run/remix/pull/7937))\n- Emit assets that were only referenced in the server build into the client assets directory in Vite build ([#7892](https://github.com/remix-run/remix/pull/7892), cherry-picked in [`8cd31d65`](https://github.com/remix-run/remix/commit/8cd31d6543ef4c765220fc64dca9bcc9c61ee9eb))\n- Populate `process.env` from `.env` files on the server in Vite dev ([#7958](https://github.com/remix-run/remix/pull/7958))\n- Fix `FutureConfig` type ([#7895](https://github.com/remix-run/remix/pull/7895))\n- Updated dependencies:\n  - `@remix-run/server-runtime@2.3.0`\n  - `@remix-run/node@2.3.0`\n\n## 2.2.0\n\n### Minor Changes\n\n- Unstable Vite support for Node-based Remix apps ([#7590](https://github.com/remix-run/remix/pull/7590))\n  - `remix build` 👉 `vite build && vite build --ssr`\n  - `remix dev` 👉 `vite dev`\n  - Other runtimes (e.g. Deno, Cloudflare) not yet supported.\n  - See \"Future > Vite\" in the Remix Docs for details\n- Add a new `future.v3_fetcherPersist` flag to change the persistence behavior of fetchers. Instead of being immediately cleaned up when unmounted in the UI, fetchers will persist until they return to an `idle` state ([RFC](https://github.com/remix-run/remix/discussions/7698)) ([#7704](https://github.com/remix-run/remix/pull/7704))\n  - For more details, please refer to the [React Router 6.18.0](https://github.com/remix-run/react-router/releases/tag/react-router%406.18.0) release notes\n\n### Patch Changes\n\n- Updated dependencies:\n  - `@remix-run/server-runtime@2.2.0`\n  - `@remix-run/node@2.2.0`\n\n## 2.1.0\n\n### Patch Changes\n\n- Sourcemap takes into account special chars in output file ([#7574](https://github.com/remix-run/remix/pull/7574))\n- Updated dependencies:\n  - `@remix-run/server-runtime@2.1.0`\n\n## 2.0.1\n\n### Patch Changes\n\n- Fix types for MDX files when using pnpm ([#7491](https://github.com/remix-run/remix/pull/7491))\n- Update `getDependenciesToBundle` to handle ESM packages without main exports ([#7272](https://github.com/remix-run/remix/pull/7272))\n  - Note that these packages must expose `package.json` in their `exports` field so that their path can be resolved\n- Fix server builds where `serverBuildPath` extension is `.cjs` ([#7180](https://github.com/remix-run/remix/pull/7180))\n- Updated dependencies:\n  - `@remix-run/server-runtime@2.0.1`\n\n## 2.0.0\n\n### Major Changes\n\n- The `create-remix` CLI has been rewritten to feature a cleaner interface, Git repo initialization and optional `remix.init` script execution. The interactive template prompt and official Remix stack/template shorthands have also been removed so that community/third-party templates are now on a more equal footing. ([#6887](https://github.com/remix-run/remix/pull/6887))\n  - The code for `create-remix` has been moved out of the Remix CLI since it's not intended for use within an existing Remix application\n  - This means that the `remix create` command is no longer available.\n- Enable built-in PostCSS and Tailwind support by default. ([#6909](https://github.com/remix-run/remix/pull/6909))\n  - These tools are now automatically used within the Remix compiler if PostCSS and/or Tailwind configuration files are present in your project.\n  - If you have a custom PostCSS and/or Tailwind setup outside of Remix, you can disable these features in your `remix.config.js` via the `postcss:false` and/or `tailwind:false` flags\n- Drop React 17 support ([#7121](https://github.com/remix-run/remix/pull/7121))\n- Require Node >=18.0.0 ([#6939](https://github.com/remix-run/remix/pull/6939))\n- Compile server build to Node 18 ([#7292](https://github.com/remix-run/remix/pull/7292))\n  - This allows features like top-level `await` to be used within a Remix app\n- Remove default Node.js polyfills - you must now opt-into polyfills via the [`serverNodeBuiltinsPolyfill`](https://remix.run/docs/en/2.0.0/start/v2#servernodebuiltinspolyfill) and [`browserNodeBuiltinsPolyfill`](https://remix.run/docs/en/2.0.0/start/v2#browsernodebuiltinspolyfill) configs ([#7269](https://github.com/remix-run/remix/pull/7269))\n- Remove `v2_errorBoundary` flag and `CatchBoundary` implementation ([#6906](https://github.com/remix-run/remix/pull/6906))\n- Remove `v2_normalizeFormMethod` future flag - all `formMethod` values will be normalized in v2 ([#6875](https://github.com/remix-run/remix/pull/6875))\n- Remove `v2_routeConvention` flag - the flat route file convention is now standard ([#6969](https://github.com/remix-run/remix/pull/6969))\n- Remove `v2_headers` flag - it is now the default behavior to use the deepest `headers` function in the route tree ([#6979](https://github.com/remix-run/remix/pull/6979))\n- The route `meta` API now defaults to the new \"V2 Meta\" API ([#6958](https://github.com/remix-run/remix/pull/6958))\n  - Please refer to the ([docs](https://remix.run/docs/en/2.0.0/route/meta) and [Preparing for V2](https://remix.run/docs/en/2.0.0/start/v2#route-meta) guide for more information.\n- Default to `serverModuleFormat: \"esm\"` and update `remix-serve` to use dynamic import to support ESM and CJS build outputs ([#6949](https://github.com/remix-run/remix/pull/6949))\n- Remove `serverBuildTarget` config option ([#6896](https://github.com/remix-run/remix/pull/6896))\n- Remove deprecated `REMIX_DEV_HTTP_ORIGIN` env var - use `REMIX_DEV_ORIGIN` instead ([#6963](https://github.com/remix-run/remix/pull/6963))\n- Remove `devServerBroadcastDelay` config option ([#7063](https://github.com/remix-run/remix/pull/7063))\n- Remove deprecated `devServerPort` option - use `--port` / `dev.port` instead ([#7078](https://github.com/remix-run/remix/pull/7078))\n- Remove deprecated `REMIX_DEV_SERVER_WS_PORT` env var - use `remix dev`'s '`--port` / `port` option instead ([#6965](https://github.com/remix-run/remix/pull/6965))\n- Stop passing `isTypeScript` to `remix.init` script ([#7099](https://github.com/remix-run/remix/pull/7099))\n- Remove `replace-remix-magic-imports` codemod ([#6899](https://github.com/remix-run/remix/pull/6899))\n- Remove deprecated `--no-restart`/`restart` cli args/flags - use `--manual`/`manual` instead ([#6962](https://github.com/remix-run/remix/pull/6962))\n- Remove deprecated `--scheme`/`scheme` and `--host`/`host` cli args/flags - use `REMIX_DEV_ORIGIN` instead ([#6962](https://github.com/remix-run/remix/pull/6962))\n- Promote the `future.v2_dev` flag in `remix.config.js` to a root level `dev` config ([#7002](https://github.com/remix-run/remix/pull/7002))\n- Remove `browserBuildDirectory` config option ([#6900](https://github.com/remix-run/remix/pull/6900))\n- Remove `serverBuildDirectory` config option (\\[#6897]\\(<https://github.com/remix-run/remix/pull/-> Remove `codemod` command ([#6918](https://github.com/remix-run/remix/pull/6918))\n  6897\\))\n- Removed support for \"magic exports\" from the `remix` package. This package can be removed from your `package.json` and you should update all imports to use the source `@remix-run/*` packages: ([#6895](https://github.com/remix-run/remix/pull/6895))\n\n  ```diff\n  - import type { ActionArgs } from \"remix\";\n  - import { json, useLoaderData } from \"remix\";\n  + import type { ActionArgs } from \"@remix-run/node\";\n  + import { json } from \"@remix-run/node\";\n  + import { useLoaderData } from \"@remix-run/react\";\n  ```\n\n### Minor Changes\n\n- Warn users about obsolete future flags in `remix.config.js` ([#7048](https://github.com/remix-run/remix/pull/7048))\n- Detect built mode via `build.mode` ([#6964](https://github.com/remix-run/remix/pull/6964))\n  - Prevents mode mismatch between built Remix server entry and user-land server\n  - Additionally, all runtimes (including non-Node runtimes) can use `build.mode` to determine if HMR should be performed\n- Support `bun` package manager ([#7074](https://github.com/remix-run/remix/pull/7074))\n- The `serverNodeBuiltinsPolyfill` option (along with the newly added `browserNodeBuiltinsPolyfill`) now supports defining global polyfills in addition to module polyfills ([#7269](https://github.com/remix-run/remix/pull/7269))\n  - For example, to polyfill Node's `Buffer` global:\n\n    ```js\n    module.exports = {\n      serverNodeBuiltinsPolyfill: {\n        globals: {\n          Buffer: true,\n        },\n        // You'll probably need to polyfill the \"buffer\" module\n        // too since the global polyfill imports this:\n        modules: {\n          buffer: true,\n        },\n      },\n    };\n    ```\n\n### Patch Changes\n\n- Fix importing of PNGs, SVGs, and other assets from packages in `node_modules` ([#6813](https://github.com/remix-run/remix/pull/6813), [#7182](https://github.com/remix-run/remix/pull/7182))\n\n- Decouple the `@remix-run/dev` package from the contents of the `@remix-run/css-bundle` package. ([#6982](https://github.com/remix-run/remix/pull/6982))\n  - The contents of the `@remix-run/css-bundle` package are now entirely managed by the Remix compiler\n  - Even though it's still recommended that your Remix dependencies all share the same version, this change ensures that there are no runtime errors when upgrading `@remix-run/dev` without upgrading `@remix-run/css-bundle`\n\n- Allow non-development modes for `remix watch` ([#7117](https://github.com/remix-run/remix/pull/7117))\n\n- Stop `remix dev` when `esbuild` is not running ([#7158](https://github.com/remix-run/remix/pull/7158))\n\n- Do not interpret JSX in `.ts` files ([#7306](https://github.com/remix-run/remix/pull/7306))\n  - While JSX is supported in `.js` files for compatibility with existing apps and libraries,\n    `.ts` files should not contain JSX. By not interpreting `.ts` files as JSX, `.ts` files\n    can contain single-argument type generics without needing a comma to disambiguate from JSX:\n\n    ```ts\n    // this works in .ts files\n    const id = <T>(x: T) => x;\n    //          ^ single-argument type generic\n    ```\n\n    ```tsx\n    // this doesn't work in .tsx files\n    const id = <T,>(x: T) => x;\n    //          ^ is this a JSX element? or a single-argument type generic?\n    ```\n\n    ```tsx\n    // this works in .tsx files\n    const id = <T,>(x: T) => x;\n    //           ^ comma: this is a generic, not a JSX element\n    const component = <h1>hello</h1>;\n    //                   ^ no comma: this is a JSX element\n    ```\n\n- Enhance obsolete flag warning for `future.v2_dev` if it was an object, and prompt users to lift it to the root `dev` config ([#7427](https://github.com/remix-run/remix/pull/7427))\n\n- Allow decorators in app code ([#7176](https://github.com/remix-run/remix/pull/7176))\n\n- Allow JSX in `.js` files during HMR ([#7112](https://github.com/remix-run/remix/pull/7112))\n\n- Kill app server when remix dev terminates ([#7280](https://github.com/remix-run/remix/pull/7280))\n\n- Support dependencies that import polyfill packages for Node built-ins via a trailing slash (e.g. importing the `buffer` package with `var Buffer = require('buffer/').Buffer` as recommended in their README) ([#7198](https://github.com/remix-run/remix/pull/7198))\n  - These imports were previously marked as external\n  - This meant that they were left as dynamic imports in the client bundle and would throw a runtime error in the browser (e.g. `Dynamic require of \"buffer/\" is not supported`)\n\n- Surface errors when PostCSS config is invalid ([#7391](https://github.com/remix-run/remix/pull/7391))\n\n- Restart dev server when Remix config changes ([#7269](https://github.com/remix-run/remix/pull/7269))\n\n- Remove outdated ESM import warnings ([#6916](https://github.com/remix-run/remix/pull/6916))\n  - Most of the time these warnings were false positives.\n  - Instead, we now rely on built-in Node warnings for ESM imports.\n\n- Do not trigger rebuilds when `.DS_Store` changes ([#7172](https://github.com/remix-run/remix/pull/7172))\n\n- Remove warnings for stabilized flags: ([#6905](https://github.com/remix-run/remix/pull/6905))\n  - `unstable_cssSideEffectImports`\n  - `unstable_cssModules`\n  - `unstable_vanillaExtract`\n\n- Allow any mode (`NODE_ENV`) ([#7113](https://github.com/remix-run/remix/pull/7113))\n\n- Replace the deprecated [`xdm`](https://github.com/wooorm/xdm) package with [`@mdx-js/mdx`](https://github.com/mdx-js/mdx) ([#4054](https://github.com/remix-run/remix/pull/4054))\n\n- Write a `version.txt` sentinel file _after_ server build is completely written ([#7299](https://github.com/remix-run/remix/pull/7299))\n\n- Updated dependencies:\n  - `@remix-run/server-runtime@2.0.0`\n\n## 1.19.3\n\n### Patch Changes\n\n- Show deprecation warning when using `devServerBroadcastDelay` and `devServerPort` config options ([#7064](https://github.com/remix-run/remix/pull/7064))\n- Updated dependencies:\n  - `@remix-run/server-runtime@1.19.3`\n\n## 1.19.2\n\n### Patch Changes\n\n- Update `proxy-agent` to resolve npm audit security vulnerability ([#7027](https://github.com/remix-run/remix/pull/7027))\n- Updated dependencies:\n  - `@remix-run/server-runtime@1.19.2`\n\n## 1.19.1\n\n### Patch Changes\n\n- Add a heartbeat ping to prevent the WebSocket connection from being closed due to inactivity when using a proxy like Cloudflare ([#6904](https://github.com/remix-run/remix/pull/6904), [#6927](https://github.com/remix-run/remix/pull/6927))\n- Treeshake out HMR code from production builds ([#6894](https://github.com/remix-run/remix/pull/6894))\n- Updated dependencies:\n  - `@remix-run/server-runtime@1.19.1`\n\n## 1.19.0\n\n### Minor Changes\n\n- improved networking options for `v2_dev` ([#6724](https://github.com/remix-run/remix/pull/6724))\n\n  deprecate the `--scheme` and `--host` options and replace them with the `REMIX_DEV_ORIGIN` environment variable\n\n- Output esbuild metafiles for bundle analysis ([#6772](https://github.com/remix-run/remix/pull/6772))\n\n  Written to server build directory (`build/` by default):\n  - `metafile.css.json`\n  - `metafile.js.json` (browser JS)\n  - `metafile.server.json` (server JS)\n\n  Metafiles can be uploaded to <https://esbuild.github.io/analyze/> for analysis.\n\n- Add `serverNodeBuiltinsPolyfill` config option. In `remix.config.js` you can now disable polyfills of Node.js built-in modules for non-Node.js server platforms, or opt into a subset of polyfills. ([#6814](https://github.com/remix-run/remix/pull/6814), [#6859](https://github.com/remix-run/remix/pull/6859), [#6877](https://github.com/remix-run/remix/pull/6877))\n\n  ```js\n  // Disable all polyfills\n  exports.serverNodeBuiltinsPolyfill = { modules: {} };\n\n  // Enable specific polyfills\n  exports.serverNodeBuiltinsPolyfill = {\n    modules: {\n      crypto: true, // Provide a JSPM polyfill\n      fs: \"empty\", // Provide an empty polyfill\n    },\n  };\n  ```\n\n### Patch Changes\n\n- ignore missing react-dom/client for react 17 ([#6725](https://github.com/remix-run/remix/pull/6725))\n\n- Warn if not using `v2_dev` ([#6818](https://github.com/remix-run/remix/pull/6818))\n\n  Also, rename `--no-restart` to `--manual` to match intention and documentation.\n  `--no-restart` remains an alias for `--manual` in v1 for backwards compatibility.\n\n- ignore errors when killing already dead processes ([#6773](https://github.com/remix-run/remix/pull/6773))\n\n- Always rewrite css-derived assets during builds ([#6837](https://github.com/remix-run/remix/pull/6837))\n\n- fix sourcemaps for `v2_dev` ([#6762](https://github.com/remix-run/remix/pull/6762))\n\n- Do not clear screen when dev server starts ([#6719](https://github.com/remix-run/remix/pull/6719))\n\n  On some terminal emulators, \"clearing\" only scrolls the next line to the\n  top. on others, it erases the scrollback.\n\n  Instead, let users call `clear` themselves (`clear && remix dev`) if\n  they want to clear.\n\n- Updated dependencies:\n  - `@remix-run/server-runtime@1.19.0`\n\n## 1.18.1\n\n### Patch Changes\n\n- Ignore missing `react-dom/client` for React 17 ([#6725](https://github.com/remix-run/remix/pull/6725))\n- Updated dependencies:\n  - `@remix-run/server-runtime@1.18.1`\n\n## 1.18.0\n\n### Minor Changes\n\n- stabilize v2 dev server ([#6615](https://github.com/remix-run/remix/pull/6615))\n- improved logging for `remix build` and `remix dev` ([#6596](https://github.com/remix-run/remix/pull/6596))\n\n### Patch Changes\n\n- fix docs links for msw and mkcert ([#6672](https://github.com/remix-run/remix/pull/6672))\n- fix `remix dev -c`: kill all descendant processes of specified command when restarting ([#6663](https://github.com/remix-run/remix/pull/6663))\n- Add caching to regular stylesheet compilation ([#6638](https://github.com/remix-run/remix/pull/6638))\n- Rename `Architect (AWS Lambda)` -> `Architect` in the `create-remix` CLI to avoid confusion for other methods of deploying to AWS (i.e., SST) ([#6484](https://github.com/remix-run/remix/pull/6484))\n- Improve CSS bundle build performance by skipping unused Node polyfills ([#6639](https://github.com/remix-run/remix/pull/6639))\n- Improve performance of CSS bundle build by skipping compilation of Remix/React packages that are known not to contain CSS imports ([#6654](https://github.com/remix-run/remix/pull/6654))\n- Cache CSS side-effect imports transform when using HMR ([#6622](https://github.com/remix-run/remix/pull/6622))\n- Fix bug with pathless layout routes beneath nested path segments ([#6649](https://github.com/remix-run/remix/pull/6649))\n- Add caching to PostCSS for CSS Modules ([#6604](https://github.com/remix-run/remix/pull/6604))\n- Add caching to PostCSS for side-effect imports ([#6554](https://github.com/remix-run/remix/pull/6554))\n- cache getRouteModuleExports calls to significantly speed up build and HMR rebuild times ([#6629](https://github.com/remix-run/remix/pull/6629))\n- group rebuild logs with surrounding whitespace ([#6607](https://github.com/remix-run/remix/pull/6607))\n- instructions for integrating with msw ([#6669](https://github.com/remix-run/remix/pull/6669))\n- Update minimum version of `esbuild-plugins-node-modules-polyfill` to 1.0.16 to ensure that the plugin is cached ([#6652](https://github.com/remix-run/remix/pull/6652))\n- Updated dependencies:\n  - `@remix-run/server-runtime@1.18.0`\n\n## 1.17.1\n\n### Patch Changes\n\n- Replace `esbuild-plugin-polyfill-node` with `esbuild-plugins-node-modules-polyfill` ([#6562](https://github.com/remix-run/remix/pull/6562))\n- Lazily generate CSS bundle when import of `@remix-run/css-bundle` is detected ([#6535](https://github.com/remix-run/remix/pull/6535))\n- Updated dependencies:\n  - `@remix-run/server-runtime@1.17.1`\n\n## 1.17.0\n\n### Minor Changes\n\n- built-in tls support ([#6483](https://github.com/remix-run/remix/pull/6483))\n\n  New options:\n  - `--tls-key` / `tlsKey`: TLS key\n  - `--tls-cert` / `tlsCert`: TLS Certificate\n\n  If both TLS options are set, `scheme` defaults to `https`\n\n  ## Example\n\n  Install [mkcert](https://github.com/FiloSottile/mkcert) and create a local CA:\n\n  ```sh\n  brew install mkcert\n  mkcert -install\n  ```\n\n  Then make sure you inform `node` about your CA certs:\n\n  ```sh\n  export NODE_EXTRA_CA_CERTS=\"$(mkcert -CAROOT)/rootCA.pem\"\n  ```\n\n  👆 You'll probably want to put that env var in your scripts or `.bashrc`/`.zshrc`\n\n  Now create `key.pem` and `cert.pem`:\n\n  ```sh\n  mkcert -key-file key.pem -cert-file cert.pem localhost\n  ```\n\n  See `mkcert` docs for more details.\n\n  Finally, pass in the paths to the key and cert via flags:\n\n  ```sh\n  remix dev --tls-key=key.pem --tls-cert=cert.pem\n  ```\n\n  or via config:\n\n  ```js\n  module.exports = {\n    future: {\n      unstable_dev: {\n        tlsKey: \"key.pem\",\n        tlsCert: \"cert.pem\",\n      },\n    },\n  };\n  ```\n\n  That's all that's needed to set up the Remix Dev Server with TLS.\n\n  🚨 Make sure to update your app server for TLS as well.\n\n  For example, with `express`:\n\n  ```ts\n  import fs from \"node:fs\";\n  import https from \"node:https\";\n\n  import express from \"express\";\n\n  const app = express();\n\n  // ...code setting up your express app...\n\n  const appServer = https.createServer(\n    {\n      key: fs.readFileSync(\"key.pem\"),\n      cert: fs.readFileSync(\"cert.pem\"),\n    },\n    app,\n  );\n\n  appServer.listen(3000, () => {\n    console.log(\"Ready on https://localhost:3000\");\n  });\n  ```\n\n  ## Known limitations\n\n  `remix-serve` does not yet support TLS.\n  That means this only works for custom app server using the `-c` flag for now.\n\n- Reuse dev server port for WebSocket (Live Reload,HMR,HDR) ([#6476](https://github.com/remix-run/remix/pull/6476))\n\n  As a result the `webSocketPort`/`--websocket-port` option has been obsoleted.\n  Additionally, scheme/host/port options for the dev server have been renamed.\n\n  Available options are:\n\n  | Option     | flag               | config           | default                           |\n  | ---------- | ------------------ | ---------------- | --------------------------------- |\n  | Command    | `-c` / `--command` | `command`        | `remix-serve <server build path>` |\n  | Scheme     | `--scheme`         | `scheme`         | `http`                            |\n  | Host       | `--host`           | `host`           | `localhost`                       |\n  | Port       | `--port`           | `port`           | Dynamically chosen open port      |\n  | No restart | `--no-restart`     | `restart: false` | `restart: true`                   |\n\n  Note that scheme/host/port options are for the _dev server_, not your app server.\n  You probably don't need to use scheme/host/port option if you aren't configuring networking (e.g. for Docker or SSL).\n\n### Patch Changes\n\n- Add caching to PostCSS for regular stylesheets ([#6505](https://github.com/remix-run/remix/pull/6505))\n\n- Fix warnings when importing CSS files with `future.unstable_dev` enabled ([#6506](https://github.com/remix-run/remix/pull/6506))\n\n- Fix Tailwind performance issue when `postcss.config.js` contains `plugins: { tailwindcss: {} }` and `remix.config.js` contains both `tailwind: true` and `postcss: true`. ([#6468](https://github.com/remix-run/remix/pull/6468))\n\n  Note that this was _not_ an issue when the plugin function had been explicitly called, i.e. `plugins: [tailwindcss()]`. Remix avoids adding the Tailwind plugin to PostCSS if it's already present but we were failing to detect when the plugin function hadn't been called — either because the plugin function itself had been passed, i.e. `plugins: [require('tailwindcss')]`, or the plugin config object syntax had been used, i.e. `plugins: { tailwindcss: {} }`.\n\n- Faster server export removal for routes when `unstable_dev` is enabled. ([#6455](https://github.com/remix-run/remix/pull/6455))\n\n  Also, only render modulepreloads on SSR.\n  Do not render modulepreloads when hydrated.\n\n- Add `HeadersArgs` type to be consistent with loaders/actions/meta and allows for using a `function` declaration in addition to an arrow function expression ([#6247](https://github.com/remix-run/remix/pull/6247))\n\n  ```tsx\n  import type { HeadersArgs } from \"@remix-run/node\"; // or cloudflare/deno\n\n  export function headers({ loaderHeaders }: HeadersArgs) {\n    return {\n      \"x-my-custom-thing\": loaderHeaders.get(\"x-my-custom-thing\") || \"fallback\",\n    };\n  }\n  ```\n\n- better error message when `remix-serve` is not found ([#6477](https://github.com/remix-run/remix/pull/6477))\n\n- restore color for app server output ([#6485](https://github.com/remix-run/remix/pull/6485))\n\n- Fix route ranking bug with pathless layout route next to a sibling index route ([#4421](https://github.com/remix-run/remix/pull/4421))\n  - Under the hood this is done by removing the trailing slash from all generated `path` values since the number of slash-delimited segments counts towards route ranking so the trailing slash incorrectly increases the score for routes\n\n- Support sibling pathless layout routes by removing pathless layout routes from the unique route path checks in conventional route generation since they inherently trigger duplicate paths ([#4421](https://github.com/remix-run/remix/pull/4421))\n\n- fix dev server crashes caused by ungraceful hdr error handling ([#6467](https://github.com/remix-run/remix/pull/6467))\n\n- Updated dependencies:\n  - `@remix-run/server-runtime@1.17.0`\n\n## 1.16.1\n\n### Patch Changes\n\n- Cross-module `loader` change detection for HDR ([#6299](https://github.com/remix-run/remix/pull/6299))\n- Normalize path for dev server `PATH` envvar so that it works cross-platform (e.g. Windows) ([#6310](https://github.com/remix-run/remix/pull/6310))\n- Fix CSS imports in JS files that use JSX ([#6309](https://github.com/remix-run/remix/pull/6309))\n- Kill app server when dev server exits ([#6395](https://github.com/remix-run/remix/pull/6395))\n- Wait until app server is killed before starting a new app server ([#6289](https://github.com/remix-run/remix/pull/6289))\n- Ensure CSS bundle changes result in a new manifest hash ([#6374](https://github.com/remix-run/remix/pull/6374))\n- Normalize file paths before testing if a changed file is a route entry ([#6293](https://github.com/remix-run/remix/pull/6293))\n- Fix race where app server responds with updated manifest version _before_ dev server is listening for it ([#6294](https://github.com/remix-run/remix/pull/6294))\n  - dev server now listens for updated versions _before_ writing the server changes, guaranteeing that it is listening before the app server gets a chance to send its 'ready' message\n- Only process `.css.ts`/`.css.js` files with Vanilla Extract if `@vanilla-extract/css` is installed ([#6345](https://github.com/remix-run/remix/pull/6345))\n- Stop modifying a user's `tsconfig.json` when running using `getConfig` (`remix dev`, `remix routes`, `remix build`, etc) ([#6156](https://github.com/remix-run/remix/pull/6156))\n- Cancel previous build when rebuild is kicked off to prevent rebuilds from hanging ([#6295](https://github.com/remix-run/remix/pull/6295))\n- Update minimum version of Babel dependencies to avoid errors parsing decorators ([#6390](https://github.com/remix-run/remix/pull/6390))\n- Support asset imports when detecting loader changes for HDR ([#6396](https://github.com/remix-run/remix/pull/6396))\n- Updated dependencies:\n  - `@remix-run/server-runtime@1.16.1`\n\n## 1.16.0\n\n### Minor Changes\n\n- Enable support for [CSS Modules](https://github.com/css-modules/css-modules), [Vanilla Extract](http://vanilla-extract.style) and CSS side-effect imports ([#6046](https://github.com/remix-run/remix/pull/6046))\n\n  These CSS bundling features were previously only available via `future.unstable_cssModules`, `future.unstable_vanillaExtract` and `future.unstable_cssSideEffectImports` options in `remix.config.js`, but they have now been stabilized.\n\n  In order to use these features, check out our guide to [CSS bundling](https://remix.run/docs/en/1.16.0/guides/styling#css-bundling) in your project.\n\n- Stabilize built-in PostCSS support via the new `postcss` option in `remix.config.js`. As a result, the `future.unstable_postcss` option has also been deprecated. ([#5960](https://github.com/remix-run/remix/pull/5960))\n\n  The `postcss` option is `false` by default, but when set to `true` will enable processing of all CSS files using PostCSS if `postcss.config.js` is present.\n\n  If you followed the original PostCSS setup guide for Remix, you may have a folder structure that looks like this, separating your source files from its processed output:\n\n      .\n      ├── app\n      │   └── styles (processed files)\n      │       ├── app.css\n      │       └── routes\n      │           └── index.css\n      └── styles (source files)\n          ├── app.css\n          └── routes\n              └── index.css\n\n  After you've enabled the new `postcss` option, you can delete the processed files from `app/styles` folder and move your source files from `styles` to `app/styles`:\n\n      .\n      ├── app\n      │   └── styles (source files)\n      │       ├── app.css\n      │       └── routes\n      │           └── index.css\n\n  You should then remove `app/styles` from your `.gitignore` file since it now contains source files rather than processed output.\n\n  You can then update your `package.json` scripts to remove any usage of `postcss` since Remix handles this automatically. For example, if you had followed the original setup guide:\n\n  ```diff\n  {\n    \"scripts\": {\n  -    \"dev:css\": \"postcss styles --base styles --dir app/styles -w\",\n  -    \"build:css\": \"postcss styles --base styles --dir app/styles --env production\",\n  -    \"dev\": \"concurrently \\\"npm run dev:css\\\" \\\"remix dev\\\"\"\n  +    \"dev\": \"remix dev\"\n    }\n  }\n  ```\n\n- Stabilize built-in Tailwind support via the new `tailwind` option in `remix.config.js`. As a result, the `future.unstable_tailwind` option has also been deprecated. ([#5960](https://github.com/remix-run/remix/pull/5960))\n\n  The `tailwind` option is `false` by default, but when set to `true` will enable built-in support for Tailwind functions and directives in your CSS files if `tailwindcss` is installed.\n\n  If you followed the original Tailwind setup guide for Remix and want to make use of this feature, you should first delete the generated `app/tailwind.css`.\n\n  Then, if you have a `styles/tailwind.css` file, you should move it to `app/tailwind.css`.\n\n  ```sh\n  rm app/tailwind.css\n  mv styles/tailwind.css app/tailwind.css\n  ```\n\n  Otherwise, if you don't already have an `app/tailwind.css` file, you should create one with the following contents:\n\n  ```css\n  @tailwind base;\n  @tailwind components;\n  @tailwind utilities;\n  ```\n\n  You should then remove `/app/tailwind.css` from your `.gitignore` file since it now contains source code rather than processed output.\n\n  You can then update your `package.json` scripts to remove any usage of `tailwindcss` since Remix handles this automatically. For example, if you had followed the original setup guide:\n\n  ```diff\n  {\n    // ...\n    \"scripts\": {\n  -    \"build\": \"run-s \\\"build:*\\\"\",\n  +    \"build\": \"remix build\",\n  -    \"build:css\": \"npm run generate:css -- --minify\",\n  -    \"build:remix\": \"remix build\",\n  -    \"dev\": \"run-p \\\"dev:*\\\"\",\n  +    \"dev\": \"remix dev\",\n  -    \"dev:css\": \"npm run generate:css -- --watch\",\n  -    \"dev:remix\": \"remix dev\",\n  -    \"generate:css\": \"npx tailwindcss -o ./app/tailwind.css\",\n      \"start\": \"remix-serve build\"\n    }\n    // ...\n  }\n  ```\n\n- The Remix dev server spins up your app server as a managed subprocess. ([#6133](https://github.com/remix-run/remix/pull/6133))\n  This keeps your development environment as close to production as possible.\n  It also means that the Remix dev server is compatible with _any_ app server.\n\n  By default, the dev server will use the Remix App Server, but you opt to use your own app server by specifying the command to run it via the `-c`/`--command` flag:\n\n  ```sh\n  remix dev # uses `remix-serve <serve build path>` as the app server\n  remix dev -c \"node ./server.js\" # uses your custom app server at `./server.js`\n  ```\n\n  The dev server will:\n  - force `NODE_ENV=development` and warn you if it was previously set to something else\n  - rebuild your app whenever your Remix app code changes\n  - restart your app server whenever rebuilds succeed\n  - handle live reload and HMR + Hot Data Revalidation\n\n  ### App server coordination\n\n  In order to manage your app server, the dev server needs to be told what server build is currently being used by your app server.\n  This works by having the app server send a \"I'm ready!\" message with the Remix server build hash as the payload.\n\n  This is handled automatically in Remix App Server and is set up for you via calls to `broadcastDevReady` or `logDevReady` in the official Remix templates.\n\n  If you are not using Remix App Server and your server doesn't call `broadcastDevReady`, you'll need to call it in your app server _after_ it is up and running.\n  For example, in an Express server:\n\n  ```js\n  // server.js\n  // <other imports>\n  import { broadcastDevReady } from \"@remix-run/node\";\n\n  // Path to Remix's server build directory ('build/' by default)\n  const BUILD_DIR = path.join(process.cwd(), \"build\");\n\n  // <code setting up your express server>\n\n  app.listen(3000, () => {\n    const build = require(BUILD_DIR);\n    console.log(\"Ready: http://localhost:\" + port);\n\n    // in development, call `broadcastDevReady` _after_ your server is up and running\n    if (process.env.NODE_ENV === \"development\") {\n      broadcastDevReady(build);\n    }\n  });\n  ```\n\n  ### Options\n\n  Options priority order is: 1. flags, 2. config, 3. defaults.\n\n  | Option         | flag               | config           | default                           |\n  | -------------- | ------------------ | ---------------- | --------------------------------- |\n  | Command        | `-c` / `--command` | `command`        | `remix-serve <server build path>` |\n  | HTTP(S) scheme | `--http-scheme`    | `httpScheme`     | `http`                            |\n  | HTTP(S) host   | `--http-host`      | `httpHost`       | `localhost`                       |\n  | HTTP(S) port   | `--http-port`      | `httpPort`       | Dynamically chosen open port      |\n  | Websocket port | `--websocket-port` | `websocketPort`  | Dynamically chosen open port      |\n  | No restart     | `--no-restart`     | `restart: false` | `restart: true`                   |\n\n  🚨 The `--http-*` flags are only used for internal dev server <-> app server communication.\n  Your app will run on your app server's normal URL.\n\n  To set `unstable_dev` configuration, replace `unstable_dev: true` with `unstable_dev: { <options> }`.\n  For example, to set the HTTP(S) port statically:\n\n  ```js\n  // remix.config.js\n  module.exports = {\n    future: {\n      unstable_dev: {\n        httpPort: 8001,\n      },\n    },\n  };\n  ```\n\n  #### SSL and custom hosts\n\n  You should only need to use the `--http-*` flags and `--websocket-port` flag if you need fine-grain control of what scheme/host/port for the dev server.\n  If you are setting up SSL or Docker networking, these are the flags you'll want to use.\n\n  🚨 Remix **will not** set up SSL and custom host for you.\n  The `--http-scheme` and `--http-host` flag are for you to tell Remix how you've set things up.\n  It is your task to set up SSL certificates and host files if you want those features.\n\n  #### `--no-restart` and `require` cache purging\n\n  If you want to manage server changes yourself, you can use the `--no-restart` flag to tell the dev server to refrain from restarting your app server when builds succeed:\n\n  ```sh\n  remix dev -c \"node ./server.js\" --no-restart\n  ```\n\n  For example, you could purge the `require` cache of your app server to keep it running while picking up server changes.\n  If you do so, you should watch the server build path (`build/` by default) for changes and only purge the `require` cache when changes are detected.\n\n  🚨 If you use `--no-restart`, it is your responsibility to call `broadcastDevReady` when your app server has picked up server changes.\n  For example, with `chokidar`:\n\n  ```js\n  // server.dev.js\n  const BUILD_PATH = path.resolve(__dirname, \"build\");\n\n  const watcher = chokidar.watch(BUILD_PATH);\n\n  watcher.on(\"change\", () => {\n    // 1. purge require cache\n    purgeRequireCache();\n    // 2. load updated server build\n    const build = require(BUILD_PATH);\n    // 3. tell dev server that this app server is now ready\n    broadcastDevReady(build);\n  });\n  ```\n\n### Patch Changes\n\n- Fix absolute paths in CSS `url()` rules when using CSS Modules, Vanilla Extract and CSS side-effect imports ([#5788](https://github.com/remix-run/remix/pull/5788))\n- look for @remix-run/serve in `devDependencies` when running remix dev ([#6228](https://github.com/remix-run/remix/pull/6228))\n- add warning for v2 \"cjs\"->\"esm\" `serverModuleFormat` default change ([#6154](https://github.com/remix-run/remix/pull/6154))\n- write mjs server output files ([#6225](https://github.com/remix-run/remix/pull/6225))\n- fix(react,dev): dev chunking and refresh race condition ([#6201](https://github.com/remix-run/remix/pull/6201))\n- Use correct require context in `bareImports` plugin. ([#6181](https://github.com/remix-run/remix/pull/6181))\n- use minimatch for regex instead of glob-to-regexp ([#6017](https://github.com/remix-run/remix/pull/6017))\n- add `logDevReady` as replacement for platforms that can't initialize async I/O outside of the request response lifecycle. ([#6204](https://github.com/remix-run/remix/pull/6204))\n- Use the \"automatic\" JSX runtime when processing MDX files. ([#6098](https://github.com/remix-run/remix/pull/6098))\n- forcibly kill app server during dev ([#6197](https://github.com/remix-run/remix/pull/6197))\n- show first compilation error instead of cancelation errors ([#6202](https://github.com/remix-run/remix/pull/6202))\n- Resolve imports from route modules across the graph back to the virtual module created by the v2 routes plugin. This fixes issues where we would duplicate portions of route modules that were imported. ([#6098](https://github.com/remix-run/remix/pull/6098))\n- Updated dependencies:\n  - `@remix-run/server-runtime@1.16.0`\n\n## 1.15.0\n\n### Minor Changes\n\n- Added deprecation warning for `v2_normalizeFormMethod` ([#5863](https://github.com/remix-run/remix/pull/5863))\n\n- Added a new `future.v2_normalizeFormMethod` flag to normalize the exposed `useNavigation().formMethod` as an uppercase HTTP method to align with the previous `useTransition` behavior as well as the `fetch()` behavior of normalizing to uppercase HTTP methods. ([#5815](https://github.com/remix-run/remix/pull/5815))\n  - When `future.v2_normalizeFormMethod === false`,\n    - `useNavigation().formMethod` is lowercase\n    - `useFetcher().formMethod` is uppercase\n  - When `future.v2_normalizeFormMethod === true`:\n    - `useNavigation().formMethod` is uppercase\n    - `useFetcher().formMethod` is uppercase\n\n- Added deprecation warning for `browserBuildDirectory` in `remix.config` ([#5702](https://github.com/remix-run/remix/pull/5702))\n\n- Added deprecation warning for `CatchBoundary` in favor of `future.v2_errorBoundary` ([#5718](https://github.com/remix-run/remix/pull/5718))\n\n- Added experimental support for Vanilla Extract caching, which can be enabled by setting `future.unstable_vanillaExtract: { cache: true }` in `remix.config`. This is considered experimental due to the use of a brand new Vanilla Extract compiler under the hood. In order to use this feature, you must be using at least `v1.10.0` of `@vanilla-extract/css`. ([#5735](https://github.com/remix-run/remix/pull/5735))\n\n- Added deprecation warning for `serverBuildDirectory` in `remix.config` ([#5704](https://github.com/remix-run/remix/pull/5704))\n\n### Patch Changes\n\n- Fixed issue to ensure changes to CSS inserted via `@remix-run/css-bundle` are picked up during HMR ([#5823](https://github.com/remix-run/remix/pull/5823))\n- We now use `path.resolve` when re-exporting `entry.client` ([#5707](https://github.com/remix-run/remix/pull/5707))\n- Added support for `.mjs` and `.cjs` extensions when detecting CSS side-effect imports ([#5564](https://github.com/remix-run/remix/pull/5564))\n- Fixed resolution issues for pnpm users installing `react-refresh` ([#5637](https://github.com/remix-run/remix/pull/5637))\n- Added deprecation warning for `future.v2_meta` ([#5878](https://github.com/remix-run/remix/pull/5878))\n- Added optional entry file support for React 17 ([#5681](https://github.com/remix-run/remix/pull/5681))\n- Updated dependencies:\n  - `@remix-run/server-runtime@1.15.0`\n\n## 1.14.3\n\n### Patch Changes\n\n- dev server is resilient to build failures ([#5795](https://github.com/remix-run/remix/pull/5795))\n- Updated dependencies:\n  - `@remix-run/server-runtime@1.14.3`\n\n## 1.14.2\n\n### Patch Changes\n\n- remove premature deprecation warnings ([#5790](https://github.com/remix-run/remix/pull/5790))\n- Updated dependencies:\n  - `@remix-run/server-runtime@1.14.2`\n\n## 1.14.1\n\n### Patch Changes\n\n- Add types for importing `*.ico` files ([#5430](https://github.com/remix-run/remix/pull/5430))\n- Allow `moduleResolution: \"bundler\"` in tsconfig.json ([#5576](https://github.com/remix-run/remix/pull/5576))\n- Fix issue with x-route imports creating multiple entries in the module graph ([#5721](https://github.com/remix-run/remix/pull/5721))\n- Add `serverBuildTarget` deprecation warning ([#5624](https://github.com/remix-run/remix/pull/5624))\n- Updated dependencies:\n  - `@remix-run/server-runtime@1.14.1`\n\n## 1.14.0\n\n### Minor Changes\n\n- Hot Module Replacement and Hot Data Revalidation ([#5259](https://github.com/remix-run/remix/pull/5259))\n  - Requires `unstable_dev` future flag to be enabled\n  - HMR provided through React Refresh\n  - Features:\n    - HMR for component and style changes\n    - HDR when loaders for current route change\n  - Known limitations for MVP:\n    - Only implemented for React via React Refresh\n    - No `import.meta.hot` API exposed yet\n    - Revalidates _all_ loaders on route when loader changes are detected\n    - Loader changes do not account for imported dependencies changing\n- Make `entry.client` and `entry.server` files optional ([#4600](https://github.com/remix-run/remix/pull/4600))\n  - we'll use a bundled version of each unless you provide your own\n\n### Patch Changes\n\n- Fixes flat route inconsistencies where `route.{ext}` wasn't always being treated like `index.{ext}` when used in a folder ([#5459](https://github.com/remix-run/remix/pull/5459))\n  - Route conflict no longer throw errors and instead display a helpful warning that we're using the first one we found.\n\n    ```log\n    ⚠️ Route Path Collision: \"/dashboard\"\n\n    The following routes all define the same URL, only the first one will be used\n\n    🟢️️ routes/dashboard/route.tsx\n    ⭕️️ routes/dashboard.tsx\n    ```\n\n    ```log\n    ⚠️ Route Path Collision: \"/\"\n\n    The following routes all define the same URL, only the first one will be used\n\n    🟢️️ routes/_landing._index.tsx\n    ⭕️️ routes/_dashboard._index.tsx\n    ⭕️ routes/_index.tsx\n    ```\n\n- Log errors thrown during initial build in development. ([#5441](https://github.com/remix-run/remix/pull/5441))\n\n- Sync `FutureConfig` interface between packages ([#5398](https://github.com/remix-run/remix/pull/5398))\n\n- Add file loader for importing `.csv` files ([#3920](https://github.com/remix-run/remix/pull/3920))\n\n- Updated dependencies:\n  - `@remix-run/server-runtime@1.14.0`\n\n## 1.13.0\n\n### Minor Changes\n\n- We are deprecating `serverBuildTarget` in `remix.config`. See the [release notes for v1.13.0](https://github.com/remix-run/remix/releases/tag/remix%401.13.0) for more information. ([#5354](https://github.com/remix-run/remix/pull/5354))\n- Add built-in support for PostCSS via the `future.unstable_postcss` feature flag ([#5229](https://github.com/remix-run/remix/pull/5229))\n- Add built-in support for Tailwind via the `future.unstable_tailwind` feature flag ([#5229](https://github.com/remix-run/remix/pull/5229))\n\n### Patch Changes\n\n- Mark Vanilla Extract files as side effects to ensure that files only containing global styles aren't tree-shaken ([#5246](https://github.com/remix-run/remix/pull/5246))\n- Support decorators in files using CSS side-effect imports ([#5305](https://github.com/remix-run/remix/pull/5305))\n- We made several Flat route fixes and enhancements. See the [release notes for v1.13.0](https://github.com/remix-run/remix/releases/tag/remix%401.13.0) for more information. ([#5228](https://github.com/remix-run/remix/pull/5228))\n- Updated dependencies:\n  - `@remix-run/server-runtime@1.13.0`\n\n## 1.12.0\n\n### Minor Changes\n\n- Added a new development server available in the Remix config under the `unstable_dev` flag. [See the release notes](https://github.com/remix-run/remix/releases/tag/remix%401.12.0) for a full description. ([#5133](https://github.com/remix-run/remix/pull/5133))\n\n### Patch Changes\n\n- Fixed issues with `v2_routeConvention` on Windows so that new and renamed files are properly included ([#5266](https://github.com/remix-run/remix/pull/5266))\n- Server build should not be removed in `remix watch` and `remix dev` ([#5228](https://github.com/remix-run/remix/pull/5228))\n- The dev server will now clean up build directories whenever a rebuild starts ([#5223](https://github.com/remix-run/remix/pull/5223))\n- Updated dependencies:\n  - `@remix-run/server-runtime@1.12.0`\n\n## 1.11.1\n\n### Patch Changes\n\n- Fixed a bug with `v2_routeConvention` that prevented `index` modules from being recognized for route paths ([`195291a3d`](https://github.com/remix-run/remix/commit/195291a3d8c0e098931199bcc26277a45cee0eb9))\n- Updated dependencies:\n  - `@remix-run/server-runtime@1.11.1`\n\n## 1.11.0\n\n### Minor Changes\n\n- Specify file loader for `.fbx`, `.glb`, `.gltf`, `.hdr`, and `.mov` files ([#5030](https://github.com/remix-run/remix/pull/5030))\n- Added support for [Vanilla Extract](https://vanilla-extract.style) via the `unstable_vanillaExtract` future flag. **IMPORTANT:** Features marked with `unstable` are … unstable. While we're confident in the use cases they solve, the API and implementation may change without a major version bump. ([#5040](https://github.com/remix-run/remix/pull/5040))\n- Add support for CSS side-effect imports via the `unstable_cssSideEffectImports` future flag. **IMPORTANT:** Features marked with `unstable` are … unstable. While we're confident in the use cases they solve, the API and implementation may change without a major version bump. ([#4919](https://github.com/remix-run/remix/pull/4919))\n- Add support for CSS Modules via the `unstable_cssModules` future flag. **IMPORTANT:** Features marked with `unstable` are … unstable. While we're confident in the use cases they solve, the API and implementation may change without a major version bump. ([#4852](https://github.com/remix-run/remix/pull/4852))\n\n### Patch Changes\n\n- Add new \"flat\" routing conventions. This convention will be the default in v2 but is available now under the `v2_routeConvention` future flag. ([#4880](https://github.com/remix-run/remix/pull/4880))\n- Added support for `handle` in MDX frontmatter ([#4865](https://github.com/remix-run/remix/pull/4865))\n- Updated dependencies:\n  - `@remix-run/server-runtime@1.11.0`\n\n## 1.10.1\n\n### Patch Changes\n\n- Update babel config to transpile down to node 14 ([#5047](https://github.com/remix-run/remix/pull/5047))\n- Updated dependencies:\n  - `@remix-run/server-runtime@1.10.1`\n\n## 1.10.0\n\n### Patch Changes\n\n- Fixed several issues with TypeScript to JavaScript conversion when running `create-remix` ([#4891](https://github.com/remix-run/remix/pull/4891))\n- Resolve asset entry full path to support monorepo import of styles ([#4855](https://github.com/remix-run/remix/pull/4855))\n- Updated dependencies:\n  - `@remix-run/server-runtime@1.10.0`\n\n## 1.9.0\n\n### Minor Changes\n\n- Allow defining multiple routes for the same route module file ([#3970](https://github.com/remix-run/remix/pull/3970))\n- Added support and conventions for optional route segments ([#4706](https://github.com/remix-run/remix/pull/4706))\n\n### Patch Changes\n\n- The Remix compiler now supports new Typescript 4.9 syntax (like the `satisfies` keyword) ([#4754](https://github.com/remix-run/remix/pull/4754))\n- Optimize `parentRouteId` lookup in `defineConventionalRoutes`. ([#4800](https://github.com/remix-run/remix/pull/4800))\n- Fixed a bug in `.ts` -> `.js` conversion on Windows by using a relative unix-style path ([#4718](https://github.com/remix-run/remix/pull/4718))\n- Updated dependencies:\n  - `@remix-run/server-runtime@1.9.0`\n\n## 1.8.2\n\n### Patch Changes\n\n- Updated dependencies:\n  - `@remix-run/server-runtime@1.8.2`\n  - `@remix-run/serve@1.8.2`\n\n## 1.8.1\n\n### Patch Changes\n\n- Added a missing type definition for the Remix config `future` option to the `@remix-run/dev/server-build` virtual module ([#4771](https://github.com/remix-run/remix/pull/4771))\n- Updated dependencies:\n  - `@remix-run/serve@1.8.1`\n  - `@remix-run/server-runtime@1.8.1`\n\n## 1.8.0\n\n### Minor Changes\n\n- Added support for a new route `meta` API to handle arrays of tags instead of an object. For details, check out the [RFC](https://github.com/remix-run/remix/discussions/4462). ([#4610](https://github.com/remix-run/remix/pull/4610))\n\n### Patch Changes\n\n- Importing functions and types from the `remix` package is deprecated, and all exported modules will be removed in the next major release. For more details,[see the release notes for 1.4.0](https://github.com/remix-run/remix/releases/tag/v1.4.0) where these changes were first announced. ([#4661](https://github.com/remix-run/remix/pull/4661))\n- Updated dependencies:\n  - `@remix-run/server-runtime@1.8.0`\n  - `@remix-run/serve@1.8.0`\n\n## 1.7.6\n\n### Patch Changes\n\n- Updated dependencies:\n  - `@remix-run/serve@1.7.6`\n  - `@remix-run/server-runtime@1.7.6`\n\n### Patch Changes\n\n- Updated dependencies:\n  - `@remix-run/serve@1.7.6-pre.0`\n  - `@remix-run/server-runtime@1.7.6-pre.0`\n\n## 1.7.5\n\n### Patch Changes\n\n- Updated dependencies:\n  - `@remix-run/serve@1.7.5`\n  - `@remix-run/server-runtime@1.7.5`\n\n## 1.7.4\n\n### Patch Changes\n\n- Updated dependencies:\n  - `@remix-run/server-runtime@1.7.4`\n  - `@remix-run/serve@1.7.4`\n\n## 1.7.3\n\n### Patch Changes\n\n- Update `create-remix` to use the new examples repository when using `--template example/<name>` ([#4208](https://github.com/remix-run/remix/pull/4208))\n- Add support for setting `moduleResolution` to `node`, `node16` or `nodenext` in `tsconfig.json`. ([#4034](https://github.com/remix-run/remix/pull/4034))\n- Add resources imported only by resource routes to `assetsBuildDirectory` ([#3841](https://github.com/remix-run/remix/pull/3841))\n- Ensure that any assets referenced in CSS files are hashed and copied to the `assetsBuildDirectory`. ([#4130](https://github.com/remix-run/remix/pull/4130))\n- Updated dependencies:\n  - `@remix-run/serve@1.7.3`\n  - `@remix-run/server-runtime@1.7.3`\n\n## 1.7.2\n\n### Patch Changes\n\n- Updated dependencies:\n  - `@remix-run/server-runtime@1.7.2`\n  - `@remix-run/serve@1.7.2`\n\n## 1.7.1\n\n### Patch Changes\n\n- Updated dependencies:\n  - `@remix-run/server-runtime@1.7.1`\n  - `@remix-run/serve@1.7.1`\n\n## 1.7.0\n\n### Minor Changes\n\n- Added support for importing `.gql` and `.graphql` files as plain text ([#3923](https://github.com/remix-run/remix/pull/3923))\n- Added support for importing `.zip` and `.avif` files as resource URLs ([#3985](https://github.com/remix-run/remix/pull/3985))\n\n### Patch Changes\n\n- Removed our compiler's React shim in favor of esbuild's new automatic JSX transform ([#3860](https://github.com/remix-run/remix/pull/3860))\n- Updated dependencies:\n  - `@remix-run/server-runtime@1.7.0`\n  - `@remix-run/serve@1.7.0`\n\n## 1.6.8\n\n### Patch Changes\n\n- Added support for `.mjs` and `.cjs` file extensions for `remix.config` ([#3675](https://github.com/remix-run/remix/pull/3675))\n- Added support for importing `.sql` files as text content ([#3190](https://github.com/remix-run/remix/pull/3190))\n- Updated the compiler to make MDX builds deterministic (and a little faster!) ([#3966](https://github.com/remix-run/remix/pull/3966))\n- Updated dependencies:\n  - `@remix-run/server-runtime@1.6.8`\n  - `@remix-run/serve@1.6.8`\n\n## 1.6.7\n\n### Patch Changes\n\n- Remove logical nullish assignment, which is incompatible with Node v14. ([#3880](https://github.com/remix-run/remix/pull/3880))\n- Don't show ESM warnings when consumed via dynamic import. ([#3872](https://github.com/remix-run/remix/pull/3872))\n- Updated dependencies:\n  - `@remix-run/serve@1.6.7`\n  - `@remix-run/server-runtime@1.6.7`\n\n## 1.6.6\n\n### Patch Changes\n\n- Write server build output files so that only assets imported from resource routes are written to disk ([#3817](https://github.com/remix-run/remix/pull/3817))\n- Add support for exporting links in `.mdx` files ([#3801](https://github.com/remix-run/remix/pull/3801))\n- Ensure that build hashing is deterministic ([#2027](https://github.com/remix-run/remix/pull/2027))\n- Fix types for `@remix-run/dev/server-build` virtual module ([#3743](https://github.com/remix-run/remix/pull/3743))\n- Updated dependencies:\n  - `@remix-run/serve@1.6.6`\n  - `@remix-run/server-runtime@1.6.6`\n\n## 1.6.5\n\n### Patch Changes\n\n- Update `serverBareModulesPlugin` warning to use full import path ([#3656](https://github.com/remix-run/remix/pull/3656))\n- Fix broken `--port` flag in `create-remix` ([#3694](https://github.com/remix-run/remix/pull/3694))\n- Updated dependencies\n  - `@remix-run/server-runtime`\n  - `@remix-run/serve`\n"
  },
  {
    "path": "packages/react-router-dev/README.md",
    "content": "Dev tools and CLI for React Router that enables framework features through bundler integration like server rendering, code splitting, HMR, etc.\n\n```sh\nnpm install @react-router/dev --save-dev\n```\n"
  },
  {
    "path": "packages/react-router-dev/__tests__/fixtures/basic/.gitignore",
    "content": "node_modules\n\n/build\n.env\n\n.react-router/\n"
  },
  {
    "path": "packages/react-router-dev/__tests__/fixtures/basic/app/root.tsx",
    "content": "import { Links, Meta, Outlet, Scripts, ScrollRestoration } from \"react-router\";\n\nexport default function App() {\n  return (\n    <html lang=\"en\">\n      <head>\n        <meta charSet=\"utf-8\" />\n        <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />\n        <Meta />\n        <Links />\n      </head>\n      <body>\n        <Outlet />\n        <ScrollRestoration />\n        <Scripts />\n      </body>\n    </html>\n  );\n}\n"
  },
  {
    "path": "packages/react-router-dev/__tests__/fixtures/basic/app/routes/_index.tsx",
    "content": "import type { MetaFunction } from \"react-router\";\n\nexport const meta: MetaFunction = () => {\n  return [\n    { title: \"New React Router App\" },\n    { name: \"description\", content: \"Welcome to React Router!\" },\n  ];\n};\n\nexport default function Index() {\n  return (\n    <div style={{ fontFamily: \"system-ui, sans-serif\", lineHeight: \"1.8\" }}>\n      <h1>Welcome to React Router</h1>\n    </div>\n  );\n}\n"
  },
  {
    "path": "packages/react-router-dev/__tests__/fixtures/basic/app/routes.ts",
    "content": "// Note that since this is used in a unit test context, we don't have access to\n// the `dev` build yet, so we can't import from `@react-router/dev/routes`.\nconst routes = [{ file: \"routes/_index.tsx\", index: true }];\n\nexport default routes;\n"
  },
  {
    "path": "packages/react-router-dev/__tests__/fixtures/basic/package.json",
    "content": "{\n  \"private\": true,\n  \"sideEffects\": false,\n  \"type\": \"module\",\n  \"scripts\": {\n    \"build\": \"react-router build\",\n    \"dev\": \"react-router dev\",\n    \"start\": \"react-router-serve build/index.js\",\n    \"typecheck\": \"tsc\"\n  },\n  \"dependencies\": {\n    \"react-router\": \"*\",\n    \"@react-router/node\": \"*\",\n    \"@react-router/serve\": \"*\",\n    \"isbot\": \"^5.1.11\",\n    \"react\": \"catalog:\",\n    \"react-dom\": \"catalog:\"\n  },\n  \"devDependencies\": {\n    \"@react-router/dev\": \"*\",\n    \"@types/react\": \"catalog:\",\n    \"@types/react-dom\": \"catalog:\",\n    \"eslint\": \"^8.38.0\",\n    \"typescript\": \"catalog:\"\n  },\n  \"engines\": {\n    \"node\": \">=20.0.0\"\n  }\n}\n"
  },
  {
    "path": "packages/react-router-dev/__tests__/fixtures/basic/tsconfig.json",
    "content": "{\n  \"include\": [\"**/*.ts\", \"**/*.tsx\"],\n  \"compilerOptions\": {\n    \"lib\": [\"DOM\", \"DOM.Iterable\", \"ES2022\"],\n    \"types\": [\"@react-router/node\", \"vite/client\"],\n    \"verbatimModuleSyntax\": true,\n    \"esModuleInterop\": true,\n    \"jsx\": \"react-jsx\",\n    \"moduleResolution\": \"Bundler\",\n    \"resolveJsonModule\": true,\n    \"target\": \"ES2022\",\n    \"strict\": true,\n    \"allowJs\": true,\n    \"baseUrl\": \".\",\n    \"paths\": {\n      \"~/*\": [\"./app/*\"]\n    },\n\n    // Remix takes care of building everything in `remix build`.\n    \"noEmit\": true\n  }\n}\n"
  },
  {
    "path": "packages/react-router-dev/__tests__/route-config-test.ts",
    "content": "import path from \"node:path\";\nimport { normalize as normalizePath } from \"pathe\";\n\nimport {\n  validateRouteConfig,\n  route,\n  layout,\n  index,\n  prefix,\n  relative,\n} from \"../config/routes\";\n\nconst cleanPathsForSnapshot = (obj: any): any =>\n  JSON.parse(\n    JSON.stringify(obj, (_key, value) =>\n      typeof value === \"string\" && path.isAbsolute(value)\n        ? normalizePath(value.replace(process.cwd(), \"{{CWD}}\"))\n        : value,\n    ),\n  );\n\ndescribe(\"route config\", () => {\n  describe(\"validateRouteConfig\", () => {\n    it(\"validates a route config\", () => {\n      const routeConfig = prefix(\"prefix\", [\n        route(\"parent\", \"parent.tsx\", [route(\"child\", \"child.tsx\")]),\n      ]);\n      expect(\n        validateRouteConfig({\n          routeConfigFile: \"routes.ts\",\n          routeConfig,\n        }),\n      ).toEqual({\n        valid: true,\n        routeConfig,\n      });\n    });\n\n    it(\"is invalid when not an array\", () => {\n      let result = validateRouteConfig({\n        routeConfigFile: \"routes.ts\",\n        routeConfig: route(\"path\", \"file.tsx\"),\n      });\n\n      expect(result.valid).toBe(false);\n      expect(!result.valid && result.message).toMatchInlineSnapshot(\n        `\"Route config in \"routes.ts\" must be an array.\"`,\n      );\n    });\n\n    it(\"is invalid when route is a promise\", () => {\n      let result = validateRouteConfig({\n        routeConfigFile: \"routes.ts\",\n        /* @ts-expect-error */\n        routeConfig: [route(\"parent\", \"parent.tsx\", [Promise.resolve({})])],\n      });\n\n      expect(result.valid).toBe(false);\n      expect(!result.valid && result.message).toMatchInlineSnapshot(`\n        \"Route config in \"routes.ts\" is invalid.\n\n        Path: routes.0.children.0\n        Invalid type: Expected object but received a promise. Did you forget to await?\"\n      `);\n    });\n\n    it(\"is invalid when file is missing\", () => {\n      let result = validateRouteConfig({\n        routeConfigFile: \"routes.ts\",\n        /* @ts-expect-error */\n        routeConfig: [route(\"parent\", \"parent.tsx\", [{ id: \"child\" }])],\n      });\n\n      expect(result.valid).toBe(false);\n      expect(!result.valid && result.message).toMatchInlineSnapshot(`\n        \"Route config in \"routes.ts\" is invalid.\n\n        Path: routes.0.children.0.file\n        Invalid key: Expected \"file\" but received undefined\"\n      `);\n    });\n\n    it(\"is invalid it uses the 'root' id\", () => {\n      let result = validateRouteConfig({\n        routeConfigFile: \"routes.ts\",\n        routeConfig: [route(\"/\", \"root.tsx\", { id: \"root\" })],\n      });\n\n      expect(result.valid).toBe(false);\n      expect(!result.valid && result.message).toMatchInlineSnapshot(`\n        \"Route config in \"routes.ts\" is invalid.\n\n        Path: routes.0.id\n        A route cannot use the reserved id 'root'.\"\n      `);\n    });\n\n    it(\"is invalid when property is wrong type\", () => {\n      let result = validateRouteConfig({\n        routeConfigFile: \"routes.ts\",\n        /* @ts-expect-error */\n        routeConfig: [route(\"parent\", \"parent.tsx\", [{ file: 123 }])],\n      });\n\n      expect(result.valid).toBe(false);\n      expect(!result.valid && result.message).toMatchInlineSnapshot(`\n        \"Route config in \"routes.ts\" is invalid.\n\n        Path: routes.0.children.0.file\n        Invalid type: Expected string but received 123\"\n      `);\n    });\n\n    it(\"shows multiple error messages\", () => {\n      let result = validateRouteConfig({\n        routeConfigFile: \"routes.ts\",\n        routeConfig: [\n          /* @ts-expect-error */\n          route(\"parent\", \"parent.tsx\", [\n            { id: \"child\" },\n            { file: 123 },\n            Promise.resolve(),\n          ]),\n        ],\n      });\n\n      expect(result.valid).toBe(false);\n      expect(!result.valid && result.message).toMatchInlineSnapshot(`\n        \"Route config in \"routes.ts\" is invalid.\n\n        Path: routes.0.children.0.file\n        Invalid key: Expected \"file\" but received undefined\n\n        Path: routes.0.children.1.file\n        Invalid type: Expected string but received 123\n\n        Path: routes.0.children.2\n        Invalid type: Expected object but received a promise. Did you forget to await?\"\n      `);\n    });\n  });\n\n  describe(\"route helpers\", () => {\n    describe(\"route\", () => {\n      it(\"supports basic routes\", () => {\n        expect(route(\"path\", \"file.tsx\")).toMatchInlineSnapshot(`\n          {\n            \"children\": undefined,\n            \"file\": \"file.tsx\",\n            \"path\": \"path\",\n          }\n        `);\n      });\n\n      it(\"supports children\", () => {\n        expect(route(\"parent\", \"parent.tsx\", [route(\"child\", \"child.tsx\")]))\n          .toMatchInlineSnapshot(`\n          {\n            \"children\": [\n              {\n                \"children\": undefined,\n                \"file\": \"child.tsx\",\n                \"path\": \"child\",\n              },\n            ],\n            \"file\": \"parent.tsx\",\n            \"path\": \"parent\",\n          }\n        `);\n      });\n\n      it(\"supports custom IDs\", () => {\n        expect(route(\"path\", \"file.tsx\", { id: \"custom-id\" }))\n          .toMatchInlineSnapshot(`\n          {\n            \"children\": undefined,\n            \"file\": \"file.tsx\",\n            \"id\": \"custom-id\",\n            \"path\": \"path\",\n          }\n        `);\n      });\n\n      it(\"supports custom IDs with children\", () => {\n        expect(\n          route(\"parent\", \"parent.tsx\", { id: \"custom-id\" }, [\n            route(\"child\", \"child.tsx\"),\n          ]),\n        ).toMatchInlineSnapshot(`\n          {\n            \"children\": [\n              {\n                \"children\": undefined,\n                \"file\": \"child.tsx\",\n                \"path\": \"child\",\n              },\n            ],\n            \"file\": \"parent.tsx\",\n            \"id\": \"custom-id\",\n            \"path\": \"parent\",\n          }\n        `);\n      });\n\n      it(\"supports case sensitive routes\", () => {\n        expect(route(\"path\", \"file.tsx\", { caseSensitive: true }))\n          .toMatchInlineSnapshot(`\n          {\n            \"caseSensitive\": true,\n            \"children\": undefined,\n            \"file\": \"file.tsx\",\n            \"path\": \"path\",\n          }\n        `);\n      });\n\n      it(\"supports pathless index\", () => {\n        expect(route(null, \"file.tsx\", { index: true })).toMatchInlineSnapshot(`\n          {\n            \"children\": undefined,\n            \"file\": \"file.tsx\",\n            \"index\": true,\n            \"path\": undefined,\n          }\n        `);\n      });\n\n      it(\"ignores unsupported options\", () => {\n        expect(\n          // @ts-expect-error unsupportedOption\n          route(null, \"file.tsx\", {\n            index: true,\n            unsupportedOption: 123,\n          }),\n        ).toMatchInlineSnapshot(`\n          {\n            \"children\": undefined,\n            \"file\": \"file.tsx\",\n            \"index\": true,\n            \"path\": undefined,\n          }\n        `);\n      });\n    });\n\n    describe(\"index\", () => {\n      it(\"supports basic routes\", () => {\n        expect(index(\"file.tsx\")).toMatchInlineSnapshot(`\n          {\n            \"file\": \"file.tsx\",\n            \"index\": true,\n          }\n        `);\n      });\n\n      it(\"supports custom IDs\", () => {\n        expect(index(\"file.tsx\", { id: \"custom-id\" })).toMatchInlineSnapshot(`\n          {\n            \"file\": \"file.tsx\",\n            \"id\": \"custom-id\",\n            \"index\": true,\n          }\n        `);\n      });\n\n      it(\"ignores unsupported options\", () => {\n        expect(\n          index(\"file.tsx\", {\n            id: \"custom-id\",\n            // @ts-expect-error\n            unsupportedOption: 123,\n          }),\n        ).toMatchInlineSnapshot(`\n          {\n            \"file\": \"file.tsx\",\n            \"id\": \"custom-id\",\n            \"index\": true,\n          }\n        `);\n      });\n    });\n\n    describe(\"layout\", () => {\n      it(\"supports basic routes\", () => {\n        expect(layout(\"layout.tsx\")).toMatchInlineSnapshot(`\n          {\n            \"children\": undefined,\n            \"file\": \"layout.tsx\",\n          }\n        `);\n      });\n\n      it(\"supports children\", () => {\n        expect(layout(\"layout.tsx\", [route(\"child\", \"child.tsx\")]))\n          .toMatchInlineSnapshot(`\n          {\n            \"children\": [\n              {\n                \"children\": undefined,\n                \"file\": \"child.tsx\",\n                \"path\": \"child\",\n              },\n            ],\n            \"file\": \"layout.tsx\",\n          }\n        `);\n      });\n\n      it(\"supports custom IDs\", () => {\n        expect(layout(\"layout.tsx\", { id: \"custom-id\" }))\n          .toMatchInlineSnapshot(`\n          {\n            \"children\": undefined,\n            \"file\": \"layout.tsx\",\n            \"id\": \"custom-id\",\n          }\n        `);\n      });\n\n      it(\"supports custom IDs with children\", () => {\n        expect(\n          layout(\"layout.tsx\", { id: \"custom-id\" }, [\n            route(\"child\", \"child.tsx\"),\n          ]),\n        ).toMatchInlineSnapshot(`\n          {\n            \"children\": [\n              {\n                \"children\": undefined,\n                \"file\": \"child.tsx\",\n                \"path\": \"child\",\n              },\n            ],\n            \"file\": \"layout.tsx\",\n            \"id\": \"custom-id\",\n          }\n        `);\n      });\n    });\n\n    describe(\"prefix\", () => {\n      it(\"adds a prefix to routes\", () => {\n        expect(prefix(\"prefix\", [route(\"route\", \"routes/route.tsx\")]))\n          .toMatchInlineSnapshot(`\n          [\n            {\n              \"children\": undefined,\n              \"file\": \"routes/route.tsx\",\n              \"path\": \"prefix/route\",\n            },\n          ]\n        `);\n      });\n\n      it(\"adds a prefix to routes with a blank path\", () => {\n        expect(prefix(\"prefix\", [route(\"\", \"routes/route.tsx\")]))\n          .toMatchInlineSnapshot(`\n          [\n            {\n              \"children\": undefined,\n              \"file\": \"routes/route.tsx\",\n              \"path\": \"prefix\",\n            },\n          ]\n        `);\n      });\n\n      it(\"adds a prefix with a trailing slash to routes\", () => {\n        expect(prefix(\"prefix/\", [route(\"route\", \"routes/route.tsx\")]))\n          .toMatchInlineSnapshot(`\n          [\n            {\n              \"children\": undefined,\n              \"file\": \"routes/route.tsx\",\n              \"path\": \"prefix/route\",\n            },\n          ]\n        `);\n      });\n\n      it(\"adds a prefix to routes with leading slash\", () => {\n        expect(prefix(\"prefix\", [route(\"/route\", \"routes/route.tsx\")]))\n          .toMatchInlineSnapshot(`\n          [\n            {\n              \"children\": undefined,\n              \"file\": \"routes/route.tsx\",\n              \"path\": \"prefix/route\",\n            },\n          ]\n        `);\n      });\n\n      it(\"adds a prefix with a trailing slash to routes with leading slash\", () => {\n        expect(prefix(\"prefix/\", [route(\"/route\", \"routes/route.tsx\")]))\n          .toMatchInlineSnapshot(`\n          [\n            {\n              \"children\": undefined,\n              \"file\": \"routes/route.tsx\",\n              \"path\": \"prefix/route\",\n            },\n          ]\n        `);\n      });\n\n      it(\"adds a prefix to index routes\", () => {\n        expect(prefix(\"prefix\", [index(\"routes/index.tsx\")]))\n          .toMatchInlineSnapshot(`\n          [\n            {\n              \"children\": undefined,\n              \"file\": \"routes/index.tsx\",\n              \"index\": true,\n              \"path\": \"prefix\",\n            },\n          ]\n        `);\n      });\n\n      it(\"adds a prefix to children of layout routes\", () => {\n        expect(\n          prefix(\"prefix\", [\n            layout(\"routes/layout.tsx\", [route(\"route\", \"routes/route.tsx\")]),\n          ]),\n        ).toMatchInlineSnapshot(`\n          [\n            {\n              \"children\": [\n                {\n                  \"children\": undefined,\n                  \"file\": \"routes/route.tsx\",\n                  \"path\": \"prefix/route\",\n                },\n              ],\n              \"file\": \"routes/layout.tsx\",\n            },\n          ]\n        `);\n      });\n\n      it(\"adds a prefix to children of nested layout routes\", () => {\n        expect(\n          prefix(\"prefix\", [\n            layout(\"routes/layout-1.tsx\", [\n              route(\"layout-1-child\", \"routes/layout-1-child.tsx\"),\n              layout(\"routes/layout-2.tsx\", [\n                route(\"layout-2-child\", \"routes/layout-2-child.tsx\"),\n                layout(\"routes/layout-3.tsx\", [\n                  route(\"layout-3-child\", \"routes/layout-3-child.tsx\"),\n                ]),\n              ]),\n            ]),\n          ]),\n        ).toMatchInlineSnapshot(`\n          [\n            {\n              \"children\": [\n                {\n                  \"children\": undefined,\n                  \"file\": \"routes/layout-1-child.tsx\",\n                  \"path\": \"prefix/layout-1-child\",\n                },\n                {\n                  \"children\": [\n                    {\n                      \"children\": undefined,\n                      \"file\": \"routes/layout-2-child.tsx\",\n                      \"path\": \"prefix/layout-2-child\",\n                    },\n                    {\n                      \"children\": [\n                        {\n                          \"children\": undefined,\n                          \"file\": \"routes/layout-3-child.tsx\",\n                          \"path\": \"prefix/layout-3-child\",\n                        },\n                      ],\n                      \"file\": \"routes/layout-3.tsx\",\n                    },\n                  ],\n                  \"file\": \"routes/layout-2.tsx\",\n                },\n              ],\n              \"file\": \"routes/layout-1.tsx\",\n            },\n          ]\n        `);\n      });\n    });\n\n    describe(\"relative\", () => {\n      it(\"supports relative routes\", () => {\n        let { route } = relative(path.join(process.cwd(), \"/path/to/dirname\"));\n        expect(\n          cleanPathsForSnapshot(\n            route(\"parent\", \"nested/parent.tsx\", [\n              route(\"child\", \"nested/child.tsx\", { id: \"child\" }),\n            ]),\n          ),\n        ).toMatchInlineSnapshot(`\n          {\n            \"children\": [\n              {\n                \"file\": \"{{CWD}}/path/to/dirname/nested/child.tsx\",\n                \"id\": \"child\",\n                \"path\": \"child\",\n              },\n            ],\n            \"file\": \"{{CWD}}/path/to/dirname/nested/parent.tsx\",\n            \"path\": \"parent\",\n          }\n        `);\n      });\n\n      it(\"supports relative index routes\", () => {\n        let { index } = relative(path.join(process.cwd(), \"/path/to/dirname\"));\n        expect(\n          cleanPathsForSnapshot([\n            index(\"nested/without-options.tsx\"),\n            index(\"nested/with-options.tsx\", { id: \"with-options\" }),\n          ]),\n        ).toMatchInlineSnapshot(`\n          [\n            {\n              \"file\": \"{{CWD}}/path/to/dirname/nested/without-options.tsx\",\n              \"index\": true,\n            },\n            {\n              \"file\": \"{{CWD}}/path/to/dirname/nested/with-options.tsx\",\n              \"id\": \"with-options\",\n              \"index\": true,\n            },\n          ]\n        `);\n      });\n\n      it(\"supports relative layout routes\", () => {\n        let { layout } = relative(path.join(process.cwd(), \"/path/to/dirname\"));\n        expect(\n          cleanPathsForSnapshot(\n            layout(\"nested/parent.tsx\", [\n              layout(\"nested/child.tsx\", { id: \"child\" }),\n            ]),\n          ),\n        ).toMatchInlineSnapshot(`\n          {\n            \"children\": [\n              {\n                \"file\": \"{{CWD}}/path/to/dirname/nested/child.tsx\",\n                \"id\": \"child\",\n              },\n            ],\n            \"file\": \"{{CWD}}/path/to/dirname/nested/parent.tsx\",\n          }\n        `);\n      });\n\n      it(\"provides passthrough for non-relative APIs\", () => {\n        let { prefix: relativePrefix } = relative(\"/path/to/dirname\");\n        expect(relativePrefix).toBe(prefix);\n      });\n    });\n  });\n});\n"
  },
  {
    "path": "packages/react-router-dev/__tests__/setupAfterEnv.ts",
    "content": "export let jestTimeout = process.platform === \"win32\" ? 20_000 : 10_000;\n\njest.setTimeout(jestTimeout);\n"
  },
  {
    "path": "packages/react-router-dev/__tests__/styles-test.ts",
    "content": "import { isCssUrlWithoutSideEffects } from \"../vite/styles\";\n\ndescribe(\"isCssUrlWithoutSideEffects\", () => {\n  it(\"returns true for query parameters that result in an exported value with no side effects\", () => {\n    let urls = [\n      \"my/file.css?inline\",\n      \"my/file.css?inline-css\",\n      \"my/file.css?inline&raw\",\n      \"my/file.css?raw\",\n      \"my/file.css?raw&url\",\n      \"my/file.css?url\",\n      \"my/file.css?url&something=else\",\n      \"my/file.css?something=else&url\",\n      \"my/file.css?url&raw\",\n\n      // other parameters mixed in\n      \"my/file.css?inline&something=else\",\n      \"my/file.css?something=else&inline\",\n      \"my/file.css?inline&raw&something=else\",\n      \"my/file.css?something=else&inline&raw\",\n      \"my/file.css?raw&something=else&url\",\n      \"my/file.css?something=else&raw&url\",\n      \"my/file.css?url&something=else&raw\",\n      \"my/file.css?url&raw&something=else\",\n      \"my/file.css?something=else&url&raw\",\n    ];\n\n    for (let url of urls) {\n      expect(isCssUrlWithoutSideEffects(url)).toBe(true);\n    }\n  });\n\n  it(\"returns false for other query parameters or no parameters\", () => {\n    let urls = [\n      \"my/file.css\",\n      \"my/file.css?foo\",\n      \"my/file.css?foo=bar\",\n      \"my/file.css?foo&bar\",\n      \"my/file.css?inlinex\",\n      \"my/file.css?rawx\",\n      \"my/file.css?urlx\",\n\n      // values other than blank since Vite doesn't match these\n      \"my/file.css?inline=foo\",\n      \"my/file.css?inline-css=foo\",\n      \"my/file.css?raw=foo\",\n      \"my/file.css?url=foo\",\n\n      // explicitly blank values since Vite doesn't match these\n      \"my/file.css?inline=\",\n      \"my/file.css?inline-css=\",\n      \"my/file.css?raw=\",\n      \"my/file.css?url=\",\n    ];\n\n    for (let url of urls) {\n      expect(isCssUrlWithoutSideEffects(url)).toBe(false);\n    }\n  });\n});\n"
  },
  {
    "path": "packages/react-router-dev/__tests__/utils/captureError.ts",
    "content": "class NoErrorThrownError extends Error {}\n\nexport default async function captureError(\n  erroring: Promise<unknown> | (() => Promise<unknown>),\n) {\n  try {\n    let promise = typeof erroring === \"function\" ? erroring() : erroring;\n    await promise;\n    throw new NoErrorThrownError();\n  } catch (error: unknown) {\n    if (error instanceof NoErrorThrownError) throw error;\n    return error;\n  }\n}\n"
  },
  {
    "path": "packages/react-router-dev/__tests__/utils/cli.ts",
    "content": "import type { Stats } from \"node:fs\";\nimport { statSync } from \"node:fs\";\nimport path from \"node:path\";\nimport execa from \"execa\";\nimport glob from \"fast-glob\";\n\nimport captureError from \"./captureError\";\n\nexport const isExecaError = (error: unknown): error is execa.ExecaError => {\n  if (!(error instanceof Error)) return false;\n  return \"exitCode\" in error;\n};\n\n/**\n * Read the details (`stat`) for a file or directory,\n * or return `undefined` if the file or directory does not exist.\n */\nconst safeStat = (fileOrDir: string): Stats | undefined => {\n  try {\n    return statSync(fileOrDir);\n  } catch (error: unknown) {\n    let systemError = error as { code?: string };\n    if (!systemError.code) throw error;\n    if (systemError.code !== \"ENOENT\") throw error;\n    throw error;\n  }\n};\n\n/**\n * Find the latest modified time (`mtime`) across all files (recursively) in a directory.\n */\nconst mtimeDir = async (dir: string): Promise<Date> => {\n  let files = await glob(`**/*.{js,jsx,ts,tsx}`, {\n    cwd: dir,\n    ignore: [\n      \"**/node_modules/**\",\n      \"**/dist/**\",\n      \"**/.git/**\",\n      \"**/__tests__/**\",\n    ],\n  });\n\n  let maxMtime: Date = new Date(0);\n  if (files.length === 0) return maxMtime;\n  for (let file of files) {\n    let filepath = path.resolve(dir, file);\n    let stat = safeStat(filepath);\n    if (stat === undefined) continue;\n    if (stat.mtime > maxMtime) {\n      maxMtime = stat.mtime;\n    }\n  }\n  return maxMtime;\n};\n\nexport const run = async (args: string[], options: execa.Options = {}) => {\n  // // Running build `.js` is ~8x faster than running source `.ts` via `esbuild-register`,\n  // // so unless source code changes are not yet reflected in the build, prefer running the built `.js`.\n  // // To get speed ups in dev, make sure you build before running tests or are running `pnpm watch`\n  let sourceDir = path.resolve(__dirname, \"../..\");\n  let sourceTS = path.resolve(sourceDir, \"cli/index.ts\");\n  // // when the most recent change happened _anywhere_ within `packages/react-router-dev/`\n\n  let sourceModified = await mtimeDir(sourceDir);\n\n  let buildDir = path.resolve(\n    __dirname,\n    \"../../../../build/node_modules/@react-router/dev\",\n  );\n  let builtJS = path.resolve(buildDir, \"dist/cli/index.js\");\n  let buildModified = await mtimeDir(buildDir);\n\n  // sometimes `pnpm watch` is so fast that the build mtime is reported\n  // to be _before_ the mtime for the change in source that _caused_ the build\n  // so we only use source if changes there are at least 5ms newer than latest build change\n  let thresholdMs = 5;\n  let isBuildUpToDate =\n    buildModified.valueOf() + thresholdMs >= sourceModified.valueOf();\n\n  let result = await execa(\n    \"node\",\n    [\n      ...(isBuildUpToDate\n        ? [builtJS]\n        : [\"--require\", require.resolve(\"esbuild-register\"), sourceTS]),\n      ...args,\n    ],\n    {\n      ...options,\n      env: { ...process.env, NO_COLOR: \"1\", ...(options?.env ?? {}) },\n    },\n  );\n  return result;\n};\n\nexport const shouldError = async (args: string[]) => {\n  let error = await captureError(async () => {\n    await run(args);\n  });\n  if (!isExecaError(error)) throw error;\n  return error;\n};\n"
  },
  {
    "path": "packages/react-router-dev/__tests__/utils/eol.ts",
    "content": "export const normalize = (text: string, normalized = \"\\n\") =>\n  text.replace(/\\r?\\n/g, normalized);\n"
  },
  {
    "path": "packages/react-router-dev/__tests__/utils/git.ts",
    "content": "import execa from \"execa\";\n\nexport const initialCommit = async (projectDir: string) => {\n  let run = (cmd: string, args: string[]) =>\n    execa(cmd, args, { cwd: projectDir });\n  let commands = [\n    [\"init\"],\n\n    [\"config\", \"user.name\", '\"github-actions[bot]\"'],\n    [\"config\", \"user.email\", '\"github-actions[bot]@users.noreply.github.com\"'],\n\n    [\"add\", \".\"],\n    [\"commit\", \"--message\", '\"initial commit\"'],\n  ];\n  for (let command of commands) {\n    await run(\"git\", command);\n  }\n};\n"
  },
  {
    "path": "packages/react-router-dev/__tests__/utils/withApp.ts",
    "content": "import { cpSync, realpathSync } from \"node:fs\";\nimport { mkdir, rm } from \"node:fs/promises\";\nimport os from \"node:os\";\nimport path from \"node:path\";\n\nconst retry = async (\n  callback: () => Promise<void>,\n  times: number,\n  delayMs: number = 0,\n) => {\n  try {\n    await callback();\n  } catch (error: unknown) {\n    if (times === 0) throw error;\n    setTimeout(() => retry(callback, times - 1), delayMs);\n  }\n};\n\nexport default async function withApp<Result>(\n  fixture: string,\n  callback: (projectDir: string) => Promise<Result>,\n): Promise<Result> {\n  let TEMP_DIR = path.join(\n    realpathSync(os.tmpdir()),\n    `remix-tests-${Math.random().toString(32).slice(2)}`,\n  );\n\n  let projectDir = path.join(TEMP_DIR);\n  await rm(TEMP_DIR, { force: true, recursive: true });\n  await mkdir(TEMP_DIR, { recursive: true });\n  cpSync(fixture, projectDir, { recursive: true });\n  try {\n    let result = await callback(projectDir);\n    return result;\n  } finally {\n    // Windows sometimes throws `EBUSY: resource busy or locked, rmdir`\n    // errors when attempting to remove the temporary directory.\n    // Retrying a couple of times seems to get it to succeed.\n    // See https://github.com/jprichardson/node-fs-extra/issues?q=EBUSY%3A+resource+busy+or+locked%2C+rmdir\n    await retry(\n      async () => await rm(TEMP_DIR, { force: true, recursive: true }),\n      3,\n      200,\n    );\n  }\n}\n"
  },
  {
    "path": "packages/react-router-dev/bin.js",
    "content": "#!/usr/bin/env node\nlet arg = require(\"arg\");\n\n// Minimal replication of our actual parsing in `run.ts`.  If not already set,\n// default `NODE_ENV` so React loads the proper version in it's CJS entry script.\n// We have to do this before importing `run.ts` since that is what imports\n// `react` (indirectly via `react-router`)\nlet args = arg({}, { argv: process.argv.slice(2), permissive: true });\nif (args._.length === 0 || args._[0] === \"dev\") {\n  process.env.NODE_ENV = process.env.NODE_ENV ?? \"development\";\n} else {\n  process.env.NODE_ENV = process.env.NODE_ENV ?? \"production\";\n}\n\nrequire(\"./dist/cli/index\");\n"
  },
  {
    "path": "packages/react-router-dev/cli/commands.ts",
    "content": "import { existsSync } from \"node:fs\";\nimport { readFile, writeFile } from \"node:fs/promises\";\nimport * as path from \"node:path\";\nimport exitHook from \"exit-hook\";\nimport colors from \"picocolors\";\n// Workaround for \"ERR_REQUIRE_CYCLE_MODULE\" in Node 22.10.0+\nimport \"react-router\";\n\nimport type { ViteDevOptions } from \"../vite/dev\";\nimport type { ViteBuildOptions } from \"../vite/build\";\nimport { loadConfig } from \"../config/config\";\nimport { formatRoutes } from \"../config/format\";\nimport type { RoutesFormat } from \"../config/format\";\nimport { transpile as convertFileToJS } from \"./useJavascript\";\nimport * as profiler from \"../vite/profiler\";\nimport * as Typegen from \"../typegen\";\nimport { preloadVite, getVite } from \"../vite/vite\";\nimport { hasReactRouterRscPlugin } from \"../vite/has-rsc-plugin\";\n\nexport async function routes(\n  rootDirectory?: string,\n  flags: {\n    config?: string;\n    json?: boolean;\n    mode?: string;\n  } = {},\n): Promise<void> {\n  rootDirectory = resolveRootDirectory(rootDirectory, flags);\n  let configResult = await loadConfig({\n    rootDirectory,\n    mode: flags.mode ?? \"production\",\n  });\n\n  if (!configResult.ok) {\n    console.error(colors.red(configResult.error));\n    process.exit(1);\n  }\n\n  let format: RoutesFormat = flags.json ? \"json\" : \"jsx\";\n  console.log(formatRoutes(configResult.value.routes, format));\n}\n\nexport async function build(\n  root?: string,\n  options: ViteBuildOptions = {},\n): Promise<void> {\n  root = resolveRootDirectory(root, options);\n\n  let { build } = await import(\"../vite/build\");\n  if (options.profile) {\n    await profiler.start();\n  }\n  try {\n    await build(root, options);\n  } finally {\n    await profiler.stop(console.info);\n  }\n}\n\nexport async function dev(root?: string, options: ViteDevOptions = {}) {\n  let { dev } = await import(\"../vite/dev\");\n  if (options.profile) {\n    await profiler.start();\n  }\n  exitHook(() => profiler.stop(console.info));\n\n  root = resolveRootDirectory(root, options);\n  await dev(root, options);\n\n  // keep `react-router dev` alive by waiting indefinitely\n  await new Promise(() => {});\n}\n\nlet clientEntries = [\"entry.client.tsx\", \"entry.client.js\", \"entry.client.jsx\"];\nlet serverEntries = [\"entry.server.tsx\", \"entry.server.js\", \"entry.server.jsx\"];\nlet entries = [\"entry.client\", \"entry.server\"];\n\nlet conjunctionListFormat = new Intl.ListFormat(\"en\", {\n  style: \"long\",\n  type: \"conjunction\",\n});\n\nexport async function generateEntry(\n  entry?: string,\n  rootDirectory?: string,\n  flags: {\n    typescript?: boolean;\n    config?: string;\n    mode?: string;\n  } = {},\n) {\n  rootDirectory = resolveRootDirectory(rootDirectory, flags);\n\n  if (\n    await hasReactRouterRscPlugin({\n      root: rootDirectory,\n      viteBuildOptions: {\n        config: flags.config,\n        mode: flags.mode,\n      },\n    })\n  ) {\n    console.error(\n      colors.red(\n        `The reveal command is currently not supported in RSC Framework Mode.`,\n      ),\n    );\n    process.exit(1);\n  }\n\n  // if no entry passed, attempt to create both\n  if (!entry) {\n    await generateEntry(\"entry.client\", rootDirectory, flags);\n    await generateEntry(\"entry.server\", rootDirectory, flags);\n    return;\n  }\n\n  let configResult = await loadConfig({\n    rootDirectory,\n    mode: flags.mode ?? \"production\",\n  });\n\n  if (!configResult.ok) {\n    console.error(colors.red(configResult.error));\n    return;\n  }\n\n  let appDirectory = configResult.value.appDirectory;\n\n  if (!entries.includes(entry)) {\n    let entriesArray = Array.from(entries);\n    let list = conjunctionListFormat.format(entriesArray);\n\n    console.error(\n      colors.red(`Invalid entry file. Valid entry files are ${list}`),\n    );\n    return;\n  }\n\n  // TODO(v8): Remove - only required for Node 20.18 and below\n  let { readPackageJSON } = await import(\"pkg-types\");\n  let pkgJson = await readPackageJSON(rootDirectory);\n  let deps = pkgJson.dependencies ?? {};\n\n  if (!deps[\"@react-router/node\"]) {\n    console.error(colors.red(`No default server entry detected.`));\n    return;\n  }\n\n  let defaultsDirectory = path.resolve(\n    path.dirname(require.resolve(\"@react-router/dev/package.json\")),\n    \"dist\",\n    \"config\",\n    \"defaults\",\n  );\n  let defaultEntryClient = path.resolve(defaultsDirectory, \"entry.client.tsx\");\n\n  let defaultEntryServer = path.resolve(\n    defaultsDirectory,\n    `entry.server.node.tsx`,\n  );\n\n  let isServerEntry = entry === \"entry.server\";\n\n  let contents = isServerEntry\n    ? await createServerEntry(rootDirectory, appDirectory, defaultEntryServer)\n    : await createClientEntry(rootDirectory, appDirectory, defaultEntryClient);\n\n  let useTypeScript = flags.typescript ?? true;\n  let outputExtension = useTypeScript ? \"tsx\" : \"jsx\";\n  let outputEntry = `${entry}.${outputExtension}`;\n  let outputFile = path.resolve(appDirectory, outputEntry);\n\n  if (!useTypeScript) {\n    let javascript = await convertFileToJS(contents, {\n      cwd: rootDirectory,\n      filename: isServerEntry ? defaultEntryServer : defaultEntryClient,\n    });\n    await writeFile(outputFile, javascript, \"utf-8\");\n  } else {\n    await writeFile(outputFile, contents, \"utf-8\");\n  }\n\n  console.log(\n    colors.blue(\n      `Entry file ${entry} created at ${path.relative(\n        rootDirectory,\n        outputFile,\n      )}.`,\n    ),\n  );\n}\n\nfunction resolveRootDirectory(root?: string, flags?: { config?: string }) {\n  if (root) {\n    return path.resolve(root);\n  }\n\n  return (\n    process.env.REACT_ROUTER_ROOT ||\n    (flags?.config ? path.dirname(path.resolve(flags.config)) : process.cwd())\n  );\n}\n\nasync function checkForEntry(\n  rootDirectory: string,\n  appDirectory: string,\n  entries: string[],\n) {\n  for (let entry of entries) {\n    let entryPath = path.resolve(appDirectory, entry);\n    let exists = existsSync(entryPath);\n    if (exists) {\n      let relative = path.relative(rootDirectory, entryPath);\n      console.error(colors.red(`Entry file ${relative} already exists.`));\n      return process.exit(1);\n    }\n  }\n}\n\nasync function createServerEntry(\n  rootDirectory: string,\n  appDirectory: string,\n  inputFile: string,\n) {\n  await checkForEntry(rootDirectory, appDirectory, serverEntries);\n  let contents = await readFile(inputFile, \"utf-8\");\n  return contents;\n}\n\nasync function createClientEntry(\n  rootDirectory: string,\n  appDirectory: string,\n  inputFile: string,\n) {\n  await checkForEntry(rootDirectory, appDirectory, clientEntries);\n  let contents = await readFile(inputFile, \"utf-8\");\n  return contents;\n}\n\nexport async function typegen(\n  root: string,\n  flags: {\n    watch: boolean;\n    mode?: string;\n    config?: string;\n  },\n) {\n  root = resolveRootDirectory(root, flags);\n\n  const rsc = await hasReactRouterRscPlugin({\n    root,\n    viteBuildOptions: {\n      config: flags.config,\n      mode: flags.mode,\n    },\n  });\n\n  if (flags.watch) {\n    await preloadVite();\n    const vite = getVite();\n    const logger = vite.createLogger(\"info\", { prefix: \"[react-router]\" });\n\n    await Typegen.watch(root, {\n      mode: flags.mode ?? \"development\",\n      rsc,\n      logger,\n    });\n    await new Promise(() => {}); // keep alive\n    return;\n  }\n\n  await Typegen.run(root, {\n    mode: flags.mode ?? \"production\",\n    rsc,\n  });\n}\n"
  },
  {
    "path": "packages/react-router-dev/cli/detectPackageManager.ts",
    "content": "type PackageManager = \"npm\" | \"pnpm\" | \"yarn\" | \"bun\";\n\n/**\n * Determine which package manager the user prefers.\n *\n * npm, pnpm and Yarn set the user agent environment variable\n * that can be used to determine which package manager ran\n * the command.\n */\nexport const detectPackageManager = (): PackageManager | undefined => {\n  let { npm_config_user_agent } = process.env;\n  if (!npm_config_user_agent) return undefined;\n  try {\n    let pkgManager = npm_config_user_agent.split(\"/\")[0];\n    if (pkgManager === \"npm\") return \"npm\";\n    if (pkgManager === \"pnpm\") return \"pnpm\";\n    if (pkgManager === \"yarn\") return \"yarn\";\n    if (pkgManager === \"bun\") return \"bun\";\n    return undefined;\n  } catch {\n    return undefined;\n  }\n};\n"
  },
  {
    "path": "packages/react-router-dev/cli/index.ts",
    "content": "#!/usr/bin/env node\nimport { run } from \"./run\";\n\nrun().then(\n  () => {\n    process.exit(0);\n  },\n  (error: unknown) => {\n    if (error) console.error(error);\n    process.exit(1);\n  },\n);\n"
  },
  {
    "path": "packages/react-router-dev/cli/run.ts",
    "content": "import arg from \"arg\";\nimport semver from \"semver\";\nimport colors from \"picocolors\";\n\nimport * as commands from \"./commands\";\n\nconst helpText = `\n${colors.blueBright(\"react-router\")}\n\n  ${colors.underline(\"Usage\")}:\n    $ react-router build [${colors.yellowBright(\"projectDir\")}]\n    $ react-router dev [${colors.yellowBright(\"projectDir\")}]\n    $ react-router routes [${colors.yellowBright(\"projectDir\")}]\n\n  ${colors.underline(\"Options\")}:\n    --help, -h          Print this help message and exit\n    --version, -v       Print the CLI version and exit\n    --no-color          Disable ANSI colors in console output\n  \\`build\\` Options:\n    --assetsInlineLimit Static asset base64 inline threshold in bytes (default: 4096) (number)\n    --clearScreen       Allow/disable clear screen when logging (boolean)\n    --config, -c        Use specified config file (string)\n    --emptyOutDir       Force empty outDir when it's outside of root (boolean)\n    --logLevel, -l      Info | warn | error | silent (string)\n    --minify            Enable/disable minification, or specify minifier to use (default: \"esbuild\") (boolean | \"terser\" | \"esbuild\")\n    --mode, -m          Set env mode (string)\n    --profile           Start built-in Node.js inspector\n    --sourcemapClient   Output source maps for client build (default: false) (boolean | \"inline\" | \"hidden\")\n    --sourcemapServer   Output source maps for server build (default: false) (boolean | \"inline\" | \"hidden\")\n  \\`dev\\` Options:\n    --clearScreen       Allow/disable clear screen when logging (boolean)\n    --config, -c        Use specified config file (string)\n    --cors              Enable CORS (boolean)\n    --force             Force the optimizer to ignore the cache and re-bundle (boolean)\n    --host              Specify hostname (string)\n    --logLevel, -l      Info | warn | error | silent (string)\n    --mode, -m          Set env mode (string)\n    --open              Open browser on startup (boolean | string)\n    --port              Specify port (number)\n    --profile           Start built-in Node.js inspector\n    --strictPort        Exit if specified port is already in use (boolean)\n  \\`routes\\` Options:\n    --config, -c        Use specified Vite config file (string)\n    --json              Print the routes as JSON\n  \\`reveal\\` Options:\n    --config, -c        Use specified Vite config file (string)\n    --no-typescript     Generate plain JavaScript files\n  \\`typegen\\` Options:\n    --watch             Automatically regenerate types whenever route config (\\`routes.ts\\`) or route modules change\n\n  ${colors.underline(\"Build your project\")}:\n\n    $ react-router build\n\n  ${colors.underline(\"Run your project locally in development\")}:\n\n    $ react-router dev\n\n  ${colors.underline(\"Show all routes in your app\")}:\n\n    $ react-router routes\n    $ react-router routes my-app\n    $ react-router routes --json\n    $ react-router routes --config vite.react-router.config.ts\n\n  ${colors.underline(\"Reveal the used entry point\")}:\n\n    $ react-router reveal entry.client\n    $ react-router reveal entry.server\n    $ react-router reveal entry.client --no-typescript\n    $ react-router reveal entry.server --no-typescript\n    $ react-router reveal entry.server --config vite.react-router.config.ts\n\n  ${colors.underline(\"Generate types for route modules\")}:\n\n   $ react-router typegen\n   $ react-router typegen --watch\n`;\n\n/**\n * Programmatic interface for running the react-router CLI with the given command line\n * arguments.\n */\nexport async function run(argv: string[] = process.argv.slice(2)) {\n  // Check the node version\n  let versions = process.versions;\n  let MINIMUM_NODE_VERSION = 20;\n  if (\n    versions &&\n    versions.node &&\n    semver.major(versions.node) < MINIMUM_NODE_VERSION\n  ) {\n    console.warn(\n      `️⚠️ Oops, Node v${versions.node} detected. react-router requires ` +\n        `a Node version greater than ${MINIMUM_NODE_VERSION}.`,\n    );\n  }\n\n  let isBooleanFlag = (arg: string) => {\n    let index = argv.indexOf(arg);\n    let nextArg = argv[index + 1];\n    return !nextArg || nextArg.startsWith(\"-\");\n  };\n\n  let args = arg(\n    {\n      \"--force\": Boolean,\n      \"--help\": Boolean,\n      \"-h\": \"--help\",\n      \"--json\": Boolean,\n      \"--token\": String,\n      \"--typescript\": Boolean,\n      \"--no-typescript\": Boolean,\n      \"--version\": Boolean,\n      \"-v\": \"--version\",\n      \"--port\": Number,\n      \"-p\": \"--port\",\n      \"--config\": String,\n      \"-c\": \"--config\",\n      \"--assetsInlineLimit\": Number,\n      \"--clearScreen\": Boolean,\n      \"--cors\": Boolean,\n      \"--emptyOutDir\": Boolean,\n      \"--host\": isBooleanFlag(\"--host\") ? Boolean : String,\n      \"--logLevel\": String,\n      \"-l\": \"--logLevel\",\n      \"--minify\": String,\n      \"--mode\": String,\n      \"-m\": \"--mode\",\n      \"--open\": isBooleanFlag(\"--open\") ? Boolean : String,\n      \"--strictPort\": Boolean,\n      \"--profile\": Boolean,\n      \"--sourcemapClient\": isBooleanFlag(\"--sourcemapClient\")\n        ? Boolean\n        : String,\n      \"--sourcemapServer\": isBooleanFlag(\"--sourcemapServer\")\n        ? Boolean\n        : String,\n      \"--watch\": Boolean,\n    },\n    {\n      argv,\n    },\n  );\n\n  let input = args._;\n\n  let flags: any = Object.entries(args).reduce((acc, [key, value]) => {\n    key = key.replace(/^--/, \"\");\n    acc[key] = value;\n    return acc;\n  }, {} as any);\n\n  if (flags.help) {\n    console.log(helpText);\n    return;\n  }\n  if (flags.version) {\n    let version = require(\"../../package.json\").version;\n    console.log(version);\n    return;\n  }\n\n  flags.interactive = flags.interactive ?? require.main === module;\n  if (args[\"--no-typescript\"]) {\n    flags.typescript = false;\n  }\n\n  let command = input[0];\n\n  // Note: Keep each case in this switch statement small.\n  switch (command) {\n    case \"routes\":\n      await commands.routes(input[1], flags);\n      break;\n    case \"build\":\n      await commands.build(input[1], flags);\n      break;\n    case \"reveal\": {\n      // TODO: simplify getting started guide\n      await commands.generateEntry(input[1], input[2], flags);\n      break;\n    }\n    case \"dev\":\n      await commands.dev(input[1], flags);\n      break;\n    case \"typegen\":\n      await commands.typegen(input[1], flags);\n      break;\n    default:\n      // `react-router ./my-project` is shorthand for `react-router dev ./my-project`\n      await commands.dev(input[0], flags);\n  }\n}\n"
  },
  {
    "path": "packages/react-router-dev/cli/useJavascript.ts",
    "content": "import * as babel from \"@babel/core\";\n// @ts-expect-error These modules don't have types\nimport babelPluginSyntaxJSX from \"@babel/plugin-syntax-jsx\";\n// @ts-expect-error These modules don't have types\nimport babelPresetTypeScript from \"@babel/preset-typescript\";\nimport prettier from \"prettier\";\n\nexport async function transpile(\n  tsx: string,\n  options: {\n    cwd?: string;\n    filename?: string;\n  } = {},\n): Promise<string> {\n  let mjs = babel.transformSync(tsx, {\n    compact: false,\n    cwd: options.cwd,\n    filename: options.filename,\n    plugins: [babelPluginSyntaxJSX],\n    presets: [[babelPresetTypeScript, { jsx: \"preserve\" }]],\n    retainLines: true,\n  });\n  if (!mjs || !mjs.code) throw new Error(\"Could not parse TypeScript\");\n\n  /**\n   * Babel's `compact` and `retainLines` options are both bad at formatting code.\n   * Use Prettier for nicer formatting.\n   */\n  return await prettier.format(mjs.code, { parser: \"babel\" });\n}\n"
  },
  {
    "path": "packages/react-router-dev/config/config.ts",
    "content": "import fs from \"node:fs\";\nimport { execSync } from \"node:child_process\";\nimport * as ViteNode from \"../vite/vite-node\";\nimport type * as Vite from \"vite\";\nimport Path from \"pathe\";\nimport chokidar, {\n  type FSWatcher,\n  type EmitArgs as ChokidarEmitArgs,\n} from \"chokidar\";\nimport colors from \"picocolors\";\nimport pick from \"lodash/pick\";\nimport omit from \"lodash/omit\";\nimport cloneDeep from \"lodash/cloneDeep\";\nimport isEqual from \"lodash/isEqual\";\n\nimport {\n  type RouteManifest,\n  type RouteManifestEntry,\n  type RouteConfigEntry,\n  setAppDirectory,\n  validateRouteConfig,\n  configRoutesToRouteManifest,\n} from \"./routes\";\nimport { detectPackageManager } from \"../cli/detectPackageManager\";\n\nconst excludedConfigPresetKeys = [\"presets\"] as const satisfies ReadonlyArray<\n  keyof ReactRouterConfig\n>;\n\ntype ExcludedConfigPresetKey = (typeof excludedConfigPresetKeys)[number];\n\ntype ConfigPreset = Omit<ReactRouterConfig, ExcludedConfigPresetKey>;\n\nexport type Preset = {\n  name: string;\n  reactRouterConfig?: (args: {\n    reactRouterUserConfig: ReactRouterConfig;\n  }) => ConfigPreset | Promise<ConfigPreset>;\n  reactRouterConfigResolved?: (args: {\n    reactRouterConfig: ResolvedReactRouterConfig;\n  }) => void | Promise<void>;\n};\n\n// Only expose a subset of route properties to the \"serverBundles\" function\nconst branchRouteProperties = [\n  \"id\",\n  \"path\",\n  \"file\",\n  \"index\",\n] as const satisfies ReadonlyArray<keyof RouteManifestEntry>;\ntype BranchRoute = Pick<\n  RouteManifestEntry,\n  (typeof branchRouteProperties)[number]\n>;\n\nexport const configRouteToBranchRoute = (\n  configRoute: RouteManifestEntry,\n): BranchRoute => pick(configRoute, branchRouteProperties);\n\nexport type ServerBundlesFunction = (args: {\n  branch: BranchRoute[];\n}) => string | Promise<string>;\n\ntype BaseBuildManifest = {\n  routes: RouteManifest;\n};\n\ntype DefaultBuildManifest = BaseBuildManifest & {\n  serverBundles?: never;\n  routeIdToServerBundleId?: never;\n};\n\ntype ServerBundlesBuildManifest = BaseBuildManifest & {\n  serverBundles: {\n    [serverBundleId: string]: {\n      id: string;\n      file: string;\n    };\n  };\n  routeIdToServerBundleId: Record<string, string>;\n};\n\ntype ServerModuleFormat = \"esm\" | \"cjs\";\n\ntype ValidateConfigFunction = (config: ReactRouterConfig) => string | void;\n\ninterface FutureConfig {\n  unstable_optimizeDeps: boolean;\n  unstable_subResourceIntegrity: boolean;\n  unstable_trailingSlashAwareDataRequests: boolean;\n  /**\n   * Prerender with Vite Preview server\n   */\n  unstable_previewServerPrerendering?: boolean;\n  /**\n   * Enable route middleware\n   */\n  v8_middleware: boolean;\n  /**\n   * Automatically split route modules into multiple chunks when possible.\n   */\n  v8_splitRouteModules: boolean | \"enforce\";\n  /**\n   * Use Vite Environment API\n   */\n  v8_viteEnvironmentApi: boolean;\n}\n\nexport type BuildManifest = DefaultBuildManifest | ServerBundlesBuildManifest;\n\ntype BuildEndHook = (args: {\n  buildManifest: BuildManifest | undefined;\n  reactRouterConfig: ResolvedReactRouterConfig;\n  viteConfig: Vite.ResolvedConfig;\n}) => void | Promise<void>;\n\nexport type PrerenderPaths =\n  | boolean\n  | Array<string>\n  | ((args: {\n      getStaticPaths: () => string[];\n    }) => Array<string> | Promise<Array<string>>);\n\n/**\n * Config to be exported via the default export from `react-router.config.ts`.\n */\nexport type ReactRouterConfig = {\n  /**\n   * The path to the `app` directory, relative to the root directory. Defaults\n   * to `\"app\"`.\n   */\n  appDirectory?: string;\n\n  /**\n   * The output format of the server build. Defaults to \"esm\".\n   */\n  serverModuleFormat?: ServerModuleFormat;\n\n  /**\n   * Enabled future flags\n   */\n  future?: [keyof FutureConfig] extends [never]\n    ? // Partial<FutureConfig> doesn't work when it's empty so just prevent any keys\n      { [key: string]: never }\n    : Partial<FutureConfig>;\n\n  /**\n   * The React Router app basename.  Defaults to `\"/\"`.\n   */\n  basename?: string;\n  /**\n   * The path to the build directory, relative to the project. Defaults to\n   * `\"build\"`.\n   */\n  buildDirectory?: string;\n  /**\n   * A function that is called after the full React Router build is complete.\n   */\n  buildEnd?: BuildEndHook;\n  /**\n   * An array of URLs to prerender to HTML files at build time.  Can also be a\n   * function returning an array to dynamically generate URLs.\n   *\n   * `unstable_concurrency` defaults to 1, which means \"no concurrency\" - fully serial execution.\n   * Setting it to a value more than 1 enables concurrent prerendering.\n   * Setting it to a value higher than one can increase the speed of the build,\n   * but may consume more resources, and send more concurrent requests to the\n   * server/CMS.\n   */\n  prerender?:\n    | PrerenderPaths\n    | {\n        paths: PrerenderPaths;\n        unstable_concurrency?: number;\n      };\n  /**\n   * An array of React Router plugin config presets to ease integration with\n   * other platforms and tools.\n   */\n  presets?: Array<Preset>;\n  /**\n   * Control the \"Lazy Route Discovery\" behavior\n   *\n   * - `routeDiscovery.mode`: By default, this resolves to `lazy` which will\n   *   lazily discover routes as the user navigates around your application.\n   *   You can set this to `initial` to opt-out of this behavior and load all\n   *   routes with the initial HTML document load.\n   * - `routeDiscovery.manifestPath`: The path to serve the manifest file from.\n   *    Only applies to `mode: \"lazy\"` and defaults to `/__manifest`.\n   */\n  routeDiscovery?:\n    | {\n        mode: \"lazy\";\n        manifestPath?: string;\n      }\n    | {\n        mode: \"initial\";\n      };\n  /**\n   * The file name of the server build output. This file\n   * should end in a `.js` extension and should be deployed to your server.\n   * Defaults to `\"index.js\"`.\n   */\n  serverBuildFile?: string;\n  /**\n   * A function for assigning routes to different server bundles. This\n   * function should return a server bundle ID which will be used as the\n   * bundle's directory name within the server build directory.\n   */\n  serverBundles?: ServerBundlesFunction;\n  /**\n   * Enable server-side rendering for your application. Disable to use \"SPA\n   * Mode\", which will request the `/` path at build-time and save it as an\n   * `index.html` file with your assets so your application can be deployed as a\n   * SPA without server-rendering. Default's to `true`.\n   */\n  ssr?: boolean;\n\n  /**\n   * An array of allowed origin hosts for action submissions to UI routes (does not apply\n   * to resource routes). Supports micromatch glob patterns (`*` to match one segment,\n   * `**` to match multiple).\n   *\n   * ```tsx\n   * export default {\n   *   allowedActionOrigins: [\n   *     \"example.com\",\n   *     \"*.example.com\", // sub.example.com\n   *     \"**.example.com\", // sub.domain.example.com\n   *   ],\n   * } satisfies Config;\n   * ```\n   *\n   * If you need to set this value at runtime, you can do in by setting the value\n   * on the server build in your custom server. For example, when using `express`:\n   *\n   * ```ts\n   * import express from \"express\";\n   * import { createRequestHandler } from \"@react-router/express\";\n   * import type { ServerBuild } from \"react-router\";\n   *\n   * export const app = express();\n   *\n   * async function getBuild() {\n   *   let build: ServerBuild = await import(\n   *     \"virtual:react-router/server-build\"\n   *   );\n   *   return {\n   *     ...build,\n   *     allowedActionOrigins:\n   *       process.env.NODE_ENV === \"development\"\n   *         ? undefined\n   *         : [\"staging.example.com\", \"www.example.com\"],\n   *   };\n   * }\n   *\n   * app.use(createRequestHandler({ build: getBuild }));\n   */\n  allowedActionOrigins?: string[];\n};\n\nexport type ResolvedReactRouterConfig = Readonly<{\n  /**\n   * The absolute path to the application source directory.\n   */\n  appDirectory: string;\n  /**\n   * The React Router app basename.  Defaults to `\"/\"`.\n   */\n  basename: string;\n  /**\n   * The absolute path to the build directory.\n   */\n  buildDirectory: string;\n  /**\n   * A function that is called after the full React Router build is complete.\n   */\n  buildEnd?: BuildEndHook;\n  /**\n   * Enabled future flags\n   */\n  future: FutureConfig;\n  /**\n   * An array of URLs to prerender to HTML files at build time.  Can also be a\n   * function returning an array to dynamically generate URLs.\n   */\n  prerender: ReactRouterConfig[\"prerender\"];\n  /**\n   * Control the \"Lazy Route Discovery\" behavior\n   *\n   * - `routeDiscovery.mode`: By default, this resolves to `lazy` which will\n   *   lazily discover routes as the user navigates around your application.\n   *   You can set this to `initial` to opt-out of this behavior and load all\n   *   routes with the initial HTML document load.\n   * - `routeDiscovery.manifestPath`: The path to serve the manifest file from.\n   *    Only applies to `mode: \"lazy\"` and defaults to `/__manifest`.\n   */\n  routeDiscovery: ReactRouterConfig[\"routeDiscovery\"];\n  /**\n   * An object of all available routes, keyed by route id.\n   */\n  routes: RouteManifest;\n  /**\n   * The file name of the server build output. This file\n   * should end in a `.js` extension and should be deployed to your server.\n   * Defaults to `\"index.js\"`.\n   */\n  serverBuildFile: string;\n  /**\n   * A function for assigning routes to different server bundles. This\n   * function should return a server bundle ID which will be used as the\n   * bundle's directory name within the server build directory.\n   */\n  serverBundles?: ServerBundlesFunction;\n  /**\n   * The output format of the server build. Defaults to \"esm\".\n   */\n  serverModuleFormat: ServerModuleFormat;\n  /**\n   * Enable server-side rendering for your application. Disable to use \"SPA\n   * Mode\", which will request the `/` path at build-time and save it as an\n   * `index.html` file with your assets so your application can be deployed as a\n   * SPA without server-rendering. Default's to `true`.\n   */\n  ssr: boolean;\n  /**\n   * The allowed origins for actions / mutations. Does not apply to routes\n   * without a component. micromatch glob patterns are supported.\n   */\n  allowedActionOrigins: string[] | false;\n  /**\n   * The resolved array of route config entries exported from `routes.ts`\n   */\n  unstable_routeConfig: RouteConfigEntry[];\n}>;\n\nlet mergeReactRouterConfig = (\n  ...configs: ReactRouterConfig[]\n): ReactRouterConfig => {\n  let reducer = (\n    configA: ReactRouterConfig,\n    configB: ReactRouterConfig,\n  ): ReactRouterConfig => {\n    let mergeRequired = (key: keyof ReactRouterConfig) =>\n      configA[key] !== undefined && configB[key] !== undefined;\n\n    return {\n      ...configA,\n      ...configB,\n      ...(mergeRequired(\"buildEnd\")\n        ? {\n            buildEnd: async (...args) => {\n              await Promise.all([\n                configA.buildEnd?.(...args),\n                configB.buildEnd?.(...args),\n              ]);\n            },\n          }\n        : {}),\n      ...(mergeRequired(\"future\")\n        ? {\n            future: {\n              ...configA.future,\n              ...configB.future,\n            },\n          }\n        : {}),\n      ...(mergeRequired(\"presets\")\n        ? {\n            presets: [...(configA.presets ?? []), ...(configB.presets ?? [])],\n          }\n        : {}),\n    };\n  };\n\n  return configs.reduce(reducer, {});\n};\n\n// Inlined from https://github.com/jsdf/deep-freeze\nlet deepFreeze = (o: any) => {\n  Object.freeze(o);\n  let oIsFunction = typeof o === \"function\";\n  let hasOwnProp = Object.prototype.hasOwnProperty;\n  Object.getOwnPropertyNames(o).forEach(function (prop) {\n    if (\n      hasOwnProp.call(o, prop) &&\n      (oIsFunction\n        ? prop !== \"caller\" && prop !== \"callee\" && prop !== \"arguments\"\n        : true) &&\n      o[prop] !== null &&\n      (typeof o[prop] === \"object\" || typeof o[prop] === \"function\") &&\n      !Object.isFrozen(o[prop])\n    ) {\n      deepFreeze(o[prop]);\n    }\n  });\n  return o;\n};\n\ntype Result<T> =\n  | {\n      ok: true;\n      value: T;\n      error?: undefined;\n    }\n  | {\n      ok: false;\n      value?: undefined;\n      error: string;\n    };\n\ntype ConfigResult = Result<ResolvedReactRouterConfig>;\n\nfunction ok<T>(value: T): Result<T> {\n  return { ok: true, value };\n}\n\nfunction err<T>(error: string): Result<T> {\n  return { ok: false, error };\n}\n\nasync function resolveConfig({\n  root,\n  viteNodeContext,\n  reactRouterConfigFile,\n  skipRoutes,\n  validateConfig,\n}: {\n  root: string;\n  viteNodeContext: ViteNode.Context;\n  reactRouterConfigFile?: string;\n  skipRoutes?: boolean;\n  validateConfig?: ValidateConfigFunction;\n}): Promise<ConfigResult> {\n  let reactRouterUserConfig: ReactRouterConfig = {};\n\n  if (reactRouterConfigFile) {\n    try {\n      if (!fs.existsSync(reactRouterConfigFile)) {\n        return err(`${reactRouterConfigFile} no longer exists`);\n      }\n\n      let configModule = await viteNodeContext.runner.executeFile(\n        reactRouterConfigFile,\n      );\n\n      if (configModule.default === undefined) {\n        return err(`${reactRouterConfigFile} must provide a default export`);\n      }\n\n      if (typeof configModule.default !== \"object\") {\n        return err(`${reactRouterConfigFile} must export a config`);\n      }\n\n      reactRouterUserConfig = configModule.default;\n\n      if (validateConfig) {\n        const error = validateConfig(reactRouterUserConfig);\n        if (error) {\n          return err(error);\n        }\n      }\n    } catch (error) {\n      return err(`Error loading ${reactRouterConfigFile}: ${error}`);\n    }\n  }\n\n  // Prevent mutations to the user config\n  reactRouterUserConfig = deepFreeze(cloneDeep(reactRouterUserConfig));\n\n  let presets: ReactRouterConfig[] = (\n    await Promise.all(\n      (reactRouterUserConfig.presets ?? []).map(async (preset) => {\n        if (!preset.name) {\n          throw new Error(\n            \"React Router presets must have a `name` property defined.\",\n          );\n        }\n\n        if (!preset.reactRouterConfig) {\n          return null;\n        }\n\n        let configPreset: ReactRouterConfig = omit(\n          await preset.reactRouterConfig({ reactRouterUserConfig }),\n          excludedConfigPresetKeys,\n        );\n\n        return configPreset;\n      }),\n    )\n  ).filter(function isNotNull<T>(value: T | null): value is T {\n    return value !== null;\n  });\n\n  let defaults = {\n    basename: \"/\",\n    buildDirectory: \"build\",\n    serverBuildFile: \"index.js\",\n    serverModuleFormat: \"esm\",\n    ssr: true,\n  } as const satisfies Partial<ReactRouterConfig>;\n\n  let userAndPresetConfigs = mergeReactRouterConfig(\n    ...presets,\n    reactRouterUserConfig,\n  );\n\n  let {\n    appDirectory: userAppDirectory,\n    basename,\n    buildDirectory: userBuildDirectory,\n    buildEnd,\n    prerender,\n    routeDiscovery: userRouteDiscovery,\n    serverBuildFile,\n    serverBundles,\n    serverModuleFormat,\n    ssr,\n  } = {\n    ...defaults, // Default values should be completely overridden by user/preset config, not merged\n    ...userAndPresetConfigs,\n  };\n\n  if (!ssr && serverBundles) {\n    serverBundles = undefined;\n  }\n\n  if (prerender) {\n    let isValidPrerenderPathsConfig = (p: unknown) =>\n      typeof p === \"boolean\" || typeof p === \"function\" || Array.isArray(p);\n\n    let isValidPrerenderConfig =\n      isValidPrerenderPathsConfig(prerender) ||\n      (typeof prerender === \"object\" &&\n        \"paths\" in prerender &&\n        isValidPrerenderPathsConfig(prerender.paths));\n\n    if (!isValidPrerenderConfig) {\n      return err(\n        \"The `prerender`/`prerender.paths` config must be a boolean, an array \" +\n          \"of string paths, or a function returning a boolean or array of string paths.\",\n      );\n    }\n\n    let isValidConcurrencyConfig =\n      typeof prerender != \"object\" ||\n      !(\"unstable_concurrency\" in prerender) ||\n      (typeof prerender.unstable_concurrency === \"number\" &&\n        Number.isInteger(prerender.unstable_concurrency) &&\n        prerender.unstable_concurrency > 0);\n\n    if (!isValidConcurrencyConfig) {\n      return err(\n        \"The `prerender.unstable_concurrency` config must be a positive integer if specified.\",\n      );\n    }\n  }\n\n  let routeDiscovery: ResolvedReactRouterConfig[\"routeDiscovery\"];\n  if (userRouteDiscovery == null) {\n    if (ssr) {\n      routeDiscovery = {\n        mode: \"lazy\",\n        manifestPath: \"/__manifest\",\n      };\n    } else {\n      routeDiscovery = { mode: \"initial\" };\n    }\n  } else if (userRouteDiscovery.mode === \"initial\") {\n    routeDiscovery = userRouteDiscovery;\n  } else if (userRouteDiscovery.mode === \"lazy\") {\n    if (!ssr) {\n      return err(\n        'The `routeDiscovery.mode` config cannot be set to \"lazy\" when setting `ssr:false`',\n      );\n    }\n\n    let { manifestPath } = userRouteDiscovery;\n    if (manifestPath != null && !manifestPath.startsWith(\"/\")) {\n      return err(\n        \"The `routeDiscovery.manifestPath` config must be a root-relative \" +\n          'pathname beginning with a slash (i.e., \"/__manifest\")',\n      );\n    }\n\n    routeDiscovery = userRouteDiscovery;\n  }\n\n  let appDirectory = Path.resolve(root, userAppDirectory || \"app\");\n  let buildDirectory = Path.resolve(root, userBuildDirectory);\n\n  let rootRouteFile = findEntry(appDirectory, \"root\", { absolute: true });\n  if (!rootRouteFile) {\n    let rootRouteDisplayPath = Path.relative(\n      root,\n      Path.join(appDirectory, \"root.tsx\"),\n    );\n    return err(\n      `Could not find a root route module in the app directory as \"${rootRouteDisplayPath}\"`,\n    );\n  }\n\n  let routes: RouteManifest;\n  let routeConfig: RouteConfigEntry[] = [];\n\n  if (skipRoutes) {\n    routes = {};\n  } else {\n    let routeConfigFile = findEntry(appDirectory, \"routes\");\n\n    try {\n      if (!routeConfigFile) {\n        let routeConfigDisplayPath = Path.relative(\n          root,\n          Path.join(appDirectory, \"routes.ts\"),\n        );\n        return err(\n          `Route config file not found at \"${routeConfigDisplayPath}\".`,\n        );\n      }\n\n      setAppDirectory(appDirectory);\n      let routeConfigExport = (\n        await viteNodeContext.runner.executeFile(\n          Path.join(appDirectory, routeConfigFile),\n        )\n      ).default;\n      let result = validateRouteConfig({\n        routeConfigFile,\n        routeConfig: await routeConfigExport,\n      });\n\n      if (!result.valid) {\n        return err(result.message);\n      }\n\n      // Nest the route config under the resolved root route\n      routeConfig = [\n        {\n          id: \"root\",\n          path: \"\",\n          file: Path.relative(appDirectory, rootRouteFile),\n          children: result.routeConfig,\n        },\n      ];\n\n      routes = configRoutesToRouteManifest(appDirectory, routeConfig);\n    } catch (error: any) {\n      return err(\n        [\n          colors.red(`Route config in \"${routeConfigFile}\" is invalid.`),\n          \"\",\n          error.loc?.file && error.loc?.column && error.frame\n            ? [\n                Path.relative(appDirectory, error.loc.file) +\n                  \":\" +\n                  error.loc.line +\n                  \":\" +\n                  error.loc.column,\n                error.frame.trim?.(),\n              ]\n            : error.stack,\n        ]\n          .flat()\n          .join(\"\\n\"),\n      );\n    }\n  }\n\n  // Check for renamed flags and provide helpful error messages\n  let futureConfig = userAndPresetConfigs.future as any;\n  if (futureConfig?.unstable_splitRouteModules !== undefined) {\n    return err(\n      'The \"future.unstable_splitRouteModules\" flag has been stabilized as \"future.v8_splitRouteModules\"',\n    );\n  }\n  if (futureConfig?.unstable_viteEnvironmentApi !== undefined) {\n    return err(\n      'The \"future.unstable_viteEnvironmentApi\" flag has been stabilized as \"future.v8_viteEnvironmentApi\"',\n    );\n  }\n\n  let future: FutureConfig = {\n    unstable_optimizeDeps:\n      userAndPresetConfigs.future?.unstable_optimizeDeps ?? false,\n    unstable_subResourceIntegrity:\n      userAndPresetConfigs.future?.unstable_subResourceIntegrity ?? false,\n    unstable_trailingSlashAwareDataRequests:\n      userAndPresetConfigs.future?.unstable_trailingSlashAwareDataRequests ??\n      false,\n    unstable_previewServerPrerendering:\n      userAndPresetConfigs.future?.unstable_previewServerPrerendering ?? false,\n    v8_middleware: userAndPresetConfigs.future?.v8_middleware ?? false,\n    v8_splitRouteModules:\n      userAndPresetConfigs.future?.v8_splitRouteModules ?? false,\n    v8_viteEnvironmentApi:\n      userAndPresetConfigs.future?.v8_viteEnvironmentApi ?? false,\n  };\n\n  let allowedActionOrigins = userAndPresetConfigs.allowedActionOrigins ?? false;\n\n  let reactRouterConfig: ResolvedReactRouterConfig = deepFreeze({\n    appDirectory,\n    basename,\n    buildDirectory,\n    buildEnd,\n    future,\n    prerender,\n    routes,\n    routeDiscovery,\n    serverBuildFile,\n    serverBundles,\n    serverModuleFormat,\n    ssr,\n    allowedActionOrigins,\n    unstable_routeConfig: routeConfig,\n  } satisfies ResolvedReactRouterConfig);\n\n  for (let preset of reactRouterUserConfig.presets ?? []) {\n    await preset.reactRouterConfigResolved?.({ reactRouterConfig });\n  }\n\n  return ok(reactRouterConfig);\n}\n\ntype ChokidarEventName = ChokidarEmitArgs[0];\n\ntype ChangeHandler = (args: {\n  result: ConfigResult;\n  configCodeChanged: boolean;\n  routeConfigCodeChanged: boolean;\n  configChanged: boolean;\n  routeConfigChanged: boolean;\n  path: string;\n  event: ChokidarEventName;\n}) => void;\n\nexport type ConfigLoader = {\n  getConfig: () => Promise<ConfigResult>;\n  onChange: (handler: ChangeHandler) => () => void;\n  close: () => Promise<void>;\n};\n\nexport async function createConfigLoader({\n  rootDirectory: root,\n  watch,\n  mode,\n  skipRoutes,\n  validateConfig,\n}: {\n  watch: boolean;\n  rootDirectory?: string;\n  mode: string;\n  skipRoutes?: boolean;\n  validateConfig?: ValidateConfigFunction;\n}): Promise<ConfigLoader> {\n  root = Path.normalize(root ?? process.env.REACT_ROUTER_ROOT ?? process.cwd());\n\n  let vite = await import(\"vite\");\n  let viteNodeContext = await ViteNode.createContext({\n    root,\n    mode,\n    // Filter out any info level logs from vite-node\n    customLogger: vite.createLogger(\"warn\", {\n      prefix: \"[react-router]\",\n    }),\n  });\n\n  let reactRouterConfigFile: string | undefined;\n\n  let updateReactRouterConfigFile = () => {\n    reactRouterConfigFile = findEntry(root, \"react-router.config\", {\n      absolute: true,\n    });\n  };\n\n  updateReactRouterConfigFile();\n\n  let getConfig = () =>\n    resolveConfig({\n      root,\n      viteNodeContext,\n      reactRouterConfigFile,\n      skipRoutes,\n      validateConfig,\n    });\n\n  let appDirectory: string;\n\n  let initialConfigResult = await getConfig();\n\n  if (!initialConfigResult.ok) {\n    throw new Error(initialConfigResult.error);\n  }\n\n  appDirectory = Path.normalize(initialConfigResult.value.appDirectory);\n\n  let currentConfig = initialConfigResult.value;\n\n  let fsWatcher: FSWatcher | undefined;\n  let changeHandlers: ChangeHandler[] = [];\n\n  return {\n    getConfig,\n    onChange: (handler: ChangeHandler) => {\n      if (!watch) {\n        throw new Error(\n          \"onChange is not supported when watch mode is disabled\",\n        );\n      }\n\n      changeHandlers.push(handler);\n\n      if (!fsWatcher) {\n        fsWatcher = chokidar.watch([root, appDirectory], {\n          ignoreInitial: true,\n          ignored: (path) => {\n            let dirname = Path.dirname(path);\n\n            return (\n              !dirname.startsWith(appDirectory) &&\n              // Ensure we're only watching files outside of the app directory\n              // that are at the root level, not nested in subdirectories\n              path !== root && // Watch the root directory itself\n              dirname !== root // Watch files at the root level\n            );\n          },\n        });\n\n        fsWatcher.on(\"all\", async (...args) => {\n          let [event, rawFilepath] = args;\n          let filepath = Path.normalize(rawFilepath);\n\n          let fileAddedOrRemoved = event === \"add\" || event === \"unlink\";\n\n          let appFileAddedOrRemoved =\n            fileAddedOrRemoved &&\n            filepath.startsWith(Path.normalize(appDirectory));\n\n          let rootRelativeFilepath = Path.relative(root, filepath);\n\n          let configFileAddedOrRemoved =\n            fileAddedOrRemoved &&\n            isEntryFile(\"react-router.config\", rootRelativeFilepath);\n\n          if (configFileAddedOrRemoved) {\n            updateReactRouterConfigFile();\n          }\n\n          let moduleGraphChanged =\n            configFileAddedOrRemoved ||\n            Boolean(\n              viteNodeContext.devServer?.moduleGraph.getModuleById(filepath),\n            );\n\n          // Bail out if no relevant changes detected\n          if (!moduleGraphChanged && !appFileAddedOrRemoved) {\n            return;\n          }\n\n          viteNodeContext.devServer?.moduleGraph.invalidateAll();\n          viteNodeContext.runner?.moduleCache.clear();\n\n          let result = await getConfig();\n\n          let prevAppDirectory = appDirectory;\n          appDirectory = Path.normalize(\n            (result.value ?? currentConfig).appDirectory,\n          );\n\n          if (appDirectory !== prevAppDirectory) {\n            fsWatcher!.unwatch(prevAppDirectory);\n            fsWatcher!.add(appDirectory);\n          }\n\n          let configCodeChanged =\n            configFileAddedOrRemoved ||\n            (reactRouterConfigFile !== undefined &&\n              isEntryFileDependency(\n                viteNodeContext.devServer.moduleGraph,\n                reactRouterConfigFile,\n                filepath,\n              ));\n\n          let routeConfigFile = !skipRoutes\n            ? findEntry(appDirectory, \"routes\", {\n                absolute: true,\n              })\n            : undefined;\n          let routeConfigCodeChanged =\n            routeConfigFile !== undefined &&\n            isEntryFileDependency(\n              viteNodeContext.devServer.moduleGraph,\n              routeConfigFile,\n              filepath,\n            );\n\n          let configChanged =\n            result.ok &&\n            !isEqual(omitRoutes(currentConfig), omitRoutes(result.value));\n\n          let routeConfigChanged =\n            result.ok && !isEqual(currentConfig?.routes, result.value.routes);\n\n          for (let handler of changeHandlers) {\n            handler({\n              result,\n              configCodeChanged,\n              routeConfigCodeChanged,\n              configChanged,\n              routeConfigChanged,\n              path: filepath,\n              event,\n            });\n          }\n\n          if (result.ok) {\n            currentConfig = result.value;\n          }\n        });\n      }\n\n      return () => {\n        changeHandlers = changeHandlers.filter(\n          (changeHandler) => changeHandler !== handler,\n        );\n      };\n    },\n    close: async () => {\n      changeHandlers = [];\n      await viteNodeContext.devServer.close();\n      await fsWatcher?.close();\n    },\n  };\n}\n\nexport async function loadConfig({\n  rootDirectory,\n  mode,\n  skipRoutes,\n}: {\n  rootDirectory: string;\n  mode: string;\n  skipRoutes?: boolean;\n}) {\n  let configLoader = await createConfigLoader({\n    rootDirectory,\n    mode,\n    skipRoutes,\n    watch: false,\n  });\n  let config = await configLoader.getConfig();\n  await configLoader.close();\n  return config;\n}\n\nexport async function resolveEntryFiles({\n  rootDirectory,\n  reactRouterConfig,\n}: {\n  rootDirectory: string;\n  reactRouterConfig: ResolvedReactRouterConfig;\n}) {\n  let { appDirectory } = reactRouterConfig;\n\n  let defaultsDirectory = Path.resolve(\n    Path.dirname(require.resolve(\"@react-router/dev/package.json\")),\n    \"dist\",\n    \"config\",\n    \"defaults\",\n  );\n\n  let userEntryClientFile = findEntry(appDirectory, \"entry.client\");\n  let userEntryServerFile = findEntry(appDirectory, \"entry.server\");\n\n  let entryServerFile: string;\n  let entryClientFile = userEntryClientFile || \"entry.client.tsx\";\n\n  if (userEntryServerFile) {\n    entryServerFile = userEntryServerFile;\n  } else {\n    let packageJsonPath = findEntry(rootDirectory, \"package\", {\n      extensions: [\".json\"],\n      absolute: true,\n      walkParents: true,\n    });\n\n    if (!packageJsonPath) {\n      throw new Error(\n        `Could not find package.json in ${rootDirectory} or any of its parent directories. Please add a package.json, or provide a custom entry.server.tsx/jsx file in your app directory.`,\n      );\n    }\n\n    // TODO(v8): Remove - only required for Node 20.18 and below\n    let { readPackageJSON, sortPackage, updatePackage } = await import(\n      \"pkg-types\"\n    );\n    let packageJsonDirectory = Path.dirname(packageJsonPath);\n    let pkgJson = await readPackageJSON(packageJsonDirectory);\n    let deps = pkgJson.dependencies ?? {};\n\n    if (!deps[\"@react-router/node\"]) {\n      throw new Error(\n        `Could not determine server runtime. Please install @react-router/node, or provide a custom entry.server.tsx/jsx file in your app directory.`,\n      );\n    }\n\n    if (!deps[\"isbot\"]) {\n      console.log(\n        \"adding `isbot@5` to your package.json, you should commit this change\",\n      );\n\n      await updatePackage(packageJsonPath, (pkg) => {\n        pkg.dependencies ??= {};\n        pkg.dependencies.isbot = \"^5\";\n        sortPackage(pkg);\n      });\n\n      let packageManager = detectPackageManager() ?? \"npm\";\n\n      execSync(`${packageManager} install`, {\n        cwd: packageJsonDirectory,\n        stdio: \"inherit\",\n      });\n    }\n\n    entryServerFile = `entry.server.node.tsx`;\n  }\n\n  let entryClientFilePath = userEntryClientFile\n    ? Path.resolve(reactRouterConfig.appDirectory, userEntryClientFile)\n    : Path.resolve(defaultsDirectory, entryClientFile);\n\n  let entryServerFilePath = userEntryServerFile\n    ? Path.resolve(reactRouterConfig.appDirectory, userEntryServerFile)\n    : Path.resolve(defaultsDirectory, entryServerFile);\n\n  return { entryClientFilePath, entryServerFilePath };\n}\n\nexport async function resolveRSCEntryFiles({\n  reactRouterConfig,\n}: {\n  reactRouterConfig: ResolvedReactRouterConfig;\n}) {\n  let { appDirectory } = reactRouterConfig;\n\n  let defaultsDirectory = Path.resolve(\n    Path.dirname(require.resolve(\"@react-router/dev/package.json\")),\n    \"dist\",\n    \"config\",\n    \"default-rsc-entries\",\n  );\n\n  let userEntryClientFile = findEntry(appDirectory, \"entry.client\", {\n    absolute: true,\n  });\n  let userEntryRSCFile = findEntry(appDirectory, \"entry.rsc\", {\n    absolute: true,\n  });\n  let userEntrySSRFile = findEntry(appDirectory, \"entry.ssr\", {\n    absolute: true,\n  });\n\n  let client =\n    userEntryClientFile ?? Path.join(defaultsDirectory, \"entry.client.tsx\");\n  let rsc = userEntryRSCFile ?? Path.join(defaultsDirectory, \"entry.rsc.tsx\");\n  let ssr = userEntrySSRFile ?? Path.join(defaultsDirectory, \"entry.ssr.tsx\");\n\n  return { client, rsc, ssr };\n}\n\nfunction omitRoutes(\n  config: ResolvedReactRouterConfig,\n): ResolvedReactRouterConfig {\n  return {\n    ...config,\n    routes: {},\n  };\n}\n\nconst entryExts = [\".js\", \".jsx\", \".ts\", \".tsx\", \".mjs\", \".mts\"];\n\nfunction isEntryFile(entryBasename: string, filename: string) {\n  return entryExts.some((ext) => filename === `${entryBasename}${ext}`);\n}\n\nfunction findEntry(\n  dir: string,\n  basename: string,\n  options?: {\n    absolute?: boolean;\n    extensions?: string[];\n    walkParents?: boolean;\n  },\n): string | undefined {\n  let currentDir = Path.resolve(dir);\n  let { root } = Path.parse(currentDir);\n\n  while (true) {\n    for (let ext of options?.extensions ?? entryExts) {\n      let file = Path.resolve(currentDir, basename + ext);\n      if (fs.existsSync(file)) {\n        return (options?.absolute ?? false) ? file : Path.relative(dir, file);\n      }\n    }\n\n    if (!options?.walkParents) {\n      return undefined;\n    }\n\n    let parentDir = Path.dirname(currentDir);\n    // Break out when we've reached the root directory or we're about to get\n    // stuck in a loop where `path.dirname` keeps returning \"/\"\n    if (currentDir === root || parentDir === currentDir) {\n      return undefined;\n    }\n\n    currentDir = parentDir;\n  }\n}\n\nfunction isEntryFileDependency(\n  moduleGraph: Vite.ModuleGraph,\n  entryFilepath: string,\n  filepath: string,\n  visited = new Set<string>(),\n): boolean {\n  // Ensure normalized paths\n  entryFilepath = Path.normalize(entryFilepath);\n  filepath = Path.normalize(filepath);\n\n  if (visited.has(filepath)) {\n    return false;\n  }\n\n  visited.add(filepath);\n\n  if (filepath === entryFilepath) {\n    return true;\n  }\n\n  let mod = moduleGraph.getModuleById(filepath);\n\n  if (!mod) {\n    return false;\n  }\n\n  // Recursively check all importers to see if any of them are the entry file\n  for (let importer of mod.importers) {\n    if (!importer.id) {\n      continue;\n    }\n\n    if (\n      importer.id === entryFilepath ||\n      isEntryFileDependency(moduleGraph, entryFilepath, importer.id, visited)\n    ) {\n      return true;\n    }\n  }\n\n  return false;\n}\n"
  },
  {
    "path": "packages/react-router-dev/config/default-rsc-entries/entry.client.tsx",
    "content": "import \"virtual:react-router/unstable_rsc/inject-hmr-runtime\";\n\nimport { startTransition, StrictMode } from \"react\";\nimport { hydrateRoot } from \"react-dom/client\";\nimport {\n  createFromReadableStream,\n  createTemporaryReferenceSet,\n  encodeReply,\n  setServerCallback,\n} from \"@vitejs/plugin-rsc/browser\";\nimport {\n  unstable_createCallServer as createCallServer,\n  unstable_getRSCStream as getRSCStream,\n  unstable_RSCHydratedRouter as RSCHydratedRouter,\n  type unstable_RSCPayload as RSCPayload,\n} from \"react-router/dom\";\n\nsetServerCallback(\n  createCallServer({\n    createFromReadableStream,\n    createTemporaryReferenceSet,\n    encodeReply,\n  }),\n);\n\ncreateFromReadableStream<RSCPayload>(getRSCStream()).then((payload) => {\n  // @ts-expect-error - on 18 types, requires 19.\n  startTransition(async () => {\n    const formState =\n      payload.type === \"render\" ? await payload.formState : undefined;\n\n    hydrateRoot(\n      document,\n      <StrictMode>\n        <RSCHydratedRouter\n          payload={payload}\n          createFromReadableStream={createFromReadableStream}\n        />\n      </StrictMode>,\n      {\n        // @ts-expect-error - no types for this yet\n        formState,\n      },\n    );\n  });\n});\n"
  },
  {
    "path": "packages/react-router-dev/config/default-rsc-entries/entry.rsc.tsx",
    "content": "import {\n  createTemporaryReferenceSet,\n  decodeAction,\n  decodeFormState,\n  decodeReply,\n  loadServerAction,\n  renderToReadableStream,\n} from \"@vitejs/plugin-rsc/rsc\";\nimport {\n  RouterContextProvider,\n  unstable_matchRSCServerRequest as matchRSCServerRequest,\n} from \"react-router\";\n\n// Import the routes generated by routes.ts\nimport routes from \"virtual:react-router/unstable_rsc/routes\";\nimport basename from \"virtual:react-router/unstable_rsc/basename\";\nimport unstable_reactRouterServeConfig from \"virtual:react-router/unstable_rsc/react-router-serve-config\";\n\nexport { unstable_reactRouterServeConfig };\n\nexport function fetchServer(\n  request: Request,\n  requestContext?: RouterContextProvider,\n) {\n  return matchRSCServerRequest({\n    basename,\n    // Provide the React Server touchpoints.\n    createTemporaryReferenceSet,\n    decodeAction,\n    decodeFormState,\n    decodeReply,\n    loadServerAction,\n    // The incoming request.\n    request,\n    requestContext,\n    // The app routes.\n    routes,\n    // Encode the match with the React Server implementation.\n    generateResponse(match, options) {\n      return new Response(renderToReadableStream(match.payload, options), {\n        status: match.statusCode,\n        headers: match.headers,\n      });\n    },\n  });\n}\n\nexport default {\n  async fetch(request: Request, requestContext?: RouterContextProvider) {\n    if (requestContext && !(requestContext instanceof RouterContextProvider)) {\n      requestContext = undefined;\n    }\n\n    const ssr = await import.meta.viteRsc.loadModule<\n      typeof import(\"./entry.ssr.tsx\")\n    >(\"ssr\", \"index\");\n\n    return await ssr.generateHTML(\n      request,\n      await fetchServer(request, requestContext),\n    );\n  },\n};\n\nif (import.meta.hot) {\n  import.meta.hot.accept();\n}\n"
  },
  {
    "path": "packages/react-router-dev/config/default-rsc-entries/entry.ssr.tsx",
    "content": "import { createFromReadableStream } from \"@vitejs/plugin-rsc/ssr\";\n// @ts-expect-error - no types for this, can import from root once on latest 19\nimport { renderToReadableStream } from \"react-dom/server.edge\";\nimport {\n  unstable_routeRSCServerRequest as routeRSCServerRequest,\n  unstable_RSCStaticRouter as RSCStaticRouter,\n} from \"react-router\";\n\nexport async function generateHTML(\n  request: Request,\n  serverResponse: Response,\n): Promise<Response> {\n  return await routeRSCServerRequest({\n    // The incoming request.\n    request,\n    // The response from the RSC server.\n    serverResponse,\n    // Provide the React Server touchpoints.\n    createFromReadableStream,\n    // Render the router to HTML.\n    async renderHTML(getPayload, options) {\n      const payload = await getPayload();\n      const formState =\n        payload.type === \"render\" ? await payload.formState : undefined;\n\n      const bootstrapScriptContent =\n        await import.meta.viteRsc.loadBootstrapScriptContent(\"index\");\n\n      return await renderToReadableStream(\n        <RSCStaticRouter getPayload={getPayload} />,\n        {\n          ...options,\n          bootstrapScriptContent,\n          formState,\n          signal: request.signal,\n        },\n      );\n    },\n  });\n}\n"
  },
  {
    "path": "packages/react-router-dev/config/defaults/entry.client.tsx",
    "content": "import { startTransition, StrictMode } from \"react\";\nimport { hydrateRoot } from \"react-dom/client\";\nimport { HydratedRouter } from \"react-router/dom\";\n\nstartTransition(() => {\n  hydrateRoot(\n    document,\n    <StrictMode>\n      <HydratedRouter />\n    </StrictMode>,\n  );\n});\n"
  },
  {
    "path": "packages/react-router-dev/config/defaults/entry.server.node.tsx",
    "content": "import { PassThrough } from \"node:stream\";\n\nimport type { AppLoadContext, EntryContext } from \"react-router\";\nimport { createReadableStreamFromReadable } from \"@react-router/node\";\nimport { ServerRouter } from \"react-router\";\nimport { isbot } from \"isbot\";\nimport type { RenderToPipeableStreamOptions } from \"react-dom/server\";\nimport { renderToPipeableStream } from \"react-dom/server\";\n\nexport const streamTimeout = 5_000;\n\nexport default function handleRequest(\n  request: Request,\n  responseStatusCode: number,\n  responseHeaders: Headers,\n  routerContext: EntryContext,\n  loadContext: AppLoadContext,\n  // If you have middleware enabled:\n  // loadContext: RouterContextProvider\n) {\n  // https://httpwg.org/specs/rfc9110.html#HEAD\n  if (request.method.toUpperCase() === \"HEAD\") {\n    return new Response(null, {\n      status: responseStatusCode,\n      headers: responseHeaders,\n    });\n  }\n\n  return new Promise((resolve, reject) => {\n    let shellRendered = false;\n    let userAgent = request.headers.get(\"user-agent\");\n\n    // Ensure requests from bots and SPA Mode renders wait for all content to load before responding\n    // https://react.dev/reference/react-dom/server/renderToPipeableStream#waiting-for-all-content-to-load-for-crawlers-and-static-generation\n    let readyOption: keyof RenderToPipeableStreamOptions =\n      (userAgent && isbot(userAgent)) || routerContext.isSpaMode\n        ? \"onAllReady\"\n        : \"onShellReady\";\n\n    // Abort the rendering stream after the `streamTimeout` so it has time to\n    // flush down the rejected boundaries\n    let timeoutId: ReturnType<typeof setTimeout> | undefined = setTimeout(\n      () => abort(),\n      streamTimeout + 1000,\n    );\n\n    const { pipe, abort } = renderToPipeableStream(\n      <ServerRouter context={routerContext} url={request.url} />,\n      {\n        [readyOption]() {\n          shellRendered = true;\n          const body = new PassThrough({\n            final(callback) {\n              // Clear the timeout to prevent retaining the closure and memory leak\n              clearTimeout(timeoutId);\n              timeoutId = undefined;\n              callback();\n            },\n          });\n          const stream = createReadableStreamFromReadable(body);\n\n          responseHeaders.set(\"Content-Type\", \"text/html\");\n\n          pipe(body);\n\n          resolve(\n            new Response(stream, {\n              headers: responseHeaders,\n              status: responseStatusCode,\n            }),\n          );\n        },\n        onShellError(error: unknown) {\n          reject(error);\n        },\n        onError(error: unknown) {\n          responseStatusCode = 500;\n          // Log streaming rendering errors from inside the shell.  Don't log\n          // errors encountered during initial shell rendering since they'll\n          // reject and get logged in handleDocumentRequest.\n          if (shellRendered) {\n            console.error(error);\n          }\n        },\n      },\n    );\n  });\n}\n"
  },
  {
    "path": "packages/react-router-dev/config/format.ts",
    "content": "import type { RouteManifest } from \"./routes\";\n\nexport type RoutesFormat = \"json\" | \"jsx\";\n\nexport function formatRoutes(\n  routeManifest: RouteManifest,\n  format: RoutesFormat,\n) {\n  switch (format) {\n    case \"json\":\n      return formatRoutesAsJson(routeManifest);\n    case \"jsx\":\n      return formatRoutesAsJsx(routeManifest);\n  }\n}\n\ntype JsonFormattedRoute = {\n  id: string;\n  index?: boolean;\n  path?: string;\n  caseSensitive?: boolean;\n  file: string;\n  children?: JsonFormattedRoute[];\n};\n\nexport function formatRoutesAsJson(routeManifest: RouteManifest): string {\n  function handleRoutesRecursive(\n    parentId?: string,\n  ): JsonFormattedRoute[] | undefined {\n    let routes = Object.values(routeManifest).filter(\n      (route) => route.parentId === parentId,\n    );\n\n    let children = [];\n\n    for (let route of routes) {\n      children.push({\n        id: route.id,\n        index: route.index,\n        path: route.path,\n        caseSensitive: route.caseSensitive,\n        file: route.file,\n        children: handleRoutesRecursive(route.id),\n      });\n    }\n\n    if (children.length > 0) {\n      return children;\n    }\n    return undefined;\n  }\n\n  return JSON.stringify(handleRoutesRecursive() || null, null, 2);\n}\n\nexport function formatRoutesAsJsx(routeManifest: RouteManifest) {\n  let output = \"<Routes>\";\n\n  function handleRoutesRecursive(parentId?: string, level = 1): boolean {\n    let routes = Object.values(routeManifest).filter(\n      (route) => route.parentId === parentId,\n    );\n\n    let indent = Array(level * 2)\n      .fill(\" \")\n      .join(\"\");\n\n    for (let route of routes) {\n      output += \"\\n\" + indent;\n      output += `<Route${\n        route.path ? ` path=${JSON.stringify(route.path)}` : \"\"\n      }${route.index ? \" index\" : \"\"}${\n        route.file ? ` file=${JSON.stringify(route.file)}` : \"\"\n      }>`;\n      if (handleRoutesRecursive(route.id, level + 1)) {\n        output += \"\\n\" + indent;\n        output += \"</Route>\";\n      } else {\n        output = output.slice(0, -1) + \" />\";\n      }\n    }\n\n    return routes.length > 0;\n  }\n\n  handleRoutesRecursive();\n\n  output += \"\\n</Routes>\";\n\n  return output;\n}\n"
  },
  {
    "path": "packages/react-router-dev/config/is-react-router-repo.ts",
    "content": "import path from \"pathe\";\n\nexport function isReactRouterRepo() {\n  // We use '@react-router/node' for this check since it's a\n  // dependency of this package and guaranteed to be in node_modules\n  let serverRuntimePath = path.dirname(\n    require.resolve(\"@react-router/node/package.json\"),\n  );\n  let serverRuntimeParentDir = path.basename(\n    path.resolve(serverRuntimePath, \"..\"),\n  );\n  return serverRuntimeParentDir === \"packages\";\n}\n"
  },
  {
    "path": "packages/react-router-dev/config/routes.ts",
    "content": "import * as Path from \"pathe\";\nimport * as v from \"valibot\";\nimport pick from \"lodash/pick\";\n\nimport invariant from \"../invariant\";\n\ndeclare global {\n  var __reactRouterAppDirectory: string;\n}\n\nexport function setAppDirectory(directory: string) {\n  globalThis.__reactRouterAppDirectory = directory;\n}\n\n/**\n * Provides the absolute path to the app directory, for use within `routes.ts`.\n * This is designed to support resolving file system routes.\n */\nexport function getAppDirectory() {\n  invariant(globalThis.__reactRouterAppDirectory);\n  return globalThis.__reactRouterAppDirectory;\n}\n\nexport interface RouteManifestEntry {\n  /**\n   * The path this route uses to match on the URL pathname.\n   */\n  path?: string;\n\n  /**\n   * Should be `true` if it is an index route. This disallows child routes.\n   */\n  index?: boolean;\n\n  /**\n   * Should be `true` if the `path` is case-sensitive. Defaults to `false`.\n   */\n  caseSensitive?: boolean;\n\n  /**\n   * The unique id for this route, named like its `file` but without the\n   * extension. So `app/routes/gists/$username.tsx` will have an `id` of\n   * `routes/gists/$username`.\n   */\n  id: string;\n\n  /**\n   * The unique `id` for this route's parent route, if there is one.\n   */\n  parentId?: string;\n\n  /**\n   * The path to the entry point for this route, relative to\n   * `config.appDirectory`.\n   */\n  file: string;\n}\n\nexport interface RouteManifest {\n  [routeId: string]: RouteManifestEntry;\n}\n\n/**\n * Configuration for an individual route, for use within `routes.ts`. As a\n * convenience, route config entries can be created with the {@link route},\n * {@link index} and {@link layout} helper functions.\n */\nexport interface RouteConfigEntry {\n  /**\n   * The unique id for this route.\n   */\n  id?: string;\n\n  /**\n   * The path this route uses to match on the URL pathname.\n   */\n  path?: string;\n\n  /**\n   * Should be `true` if it is an index route. This disallows child routes.\n   */\n  index?: boolean;\n\n  /**\n   * Should be `true` if the `path` is case-sensitive. Defaults to `false`.\n   */\n  caseSensitive?: boolean;\n\n  /**\n   * The path to the entry point for this route, relative to\n   * `config.appDirectory`.\n   */\n  file: string;\n\n  /**\n   * The child routes.\n   */\n  children?: RouteConfigEntry[];\n}\n\nexport const routeConfigEntrySchema: v.BaseSchema<\n  RouteConfigEntry,\n  any,\n  v.BaseIssue<unknown>\n> = v.pipe(\n  v.custom<RouteConfigEntry>((value) => {\n    return !(\n      typeof value === \"object\" &&\n      value !== null &&\n      \"then\" in value &&\n      \"catch\" in value\n    );\n  }, \"Invalid type: Expected object but received a promise. Did you forget to await?\"),\n  v.object({\n    id: v.optional(\n      v.pipe(\n        v.string(),\n        v.notValue(\"root\", \"A route cannot use the reserved id 'root'.\"),\n      ),\n    ),\n    path: v.optional(v.string()),\n    index: v.optional(v.boolean()),\n    caseSensitive: v.optional(v.boolean()),\n    file: v.string(),\n    children: v.optional(v.array(v.lazy(() => routeConfigEntrySchema))),\n  }),\n);\n\nexport const resolvedRouteConfigSchema = v.array(routeConfigEntrySchema);\ntype ResolvedRouteConfig = v.InferInput<typeof resolvedRouteConfigSchema>;\n\n/**\n * Route config to be exported via the default export from `app/routes.ts`.\n */\nexport type RouteConfig = ResolvedRouteConfig | Promise<ResolvedRouteConfig>;\n\nexport function validateRouteConfig({\n  routeConfigFile,\n  routeConfig,\n}: {\n  routeConfigFile: string;\n  routeConfig: unknown;\n}):\n  | { valid: false; message: string }\n  | { valid: true; routeConfig: RouteConfigEntry[] } {\n  if (!routeConfig) {\n    return {\n      valid: false,\n      message: `Route config must be the default export in \"${routeConfigFile}\".`,\n    };\n  }\n\n  if (!Array.isArray(routeConfig)) {\n    return {\n      valid: false,\n      message: `Route config in \"${routeConfigFile}\" must be an array.`,\n    };\n  }\n\n  let { issues } = v.safeParse(resolvedRouteConfigSchema, routeConfig);\n\n  if (issues?.length) {\n    let { root, nested } = v.flatten(issues);\n    return {\n      valid: false,\n      message: [\n        `Route config in \"${routeConfigFile}\" is invalid.`,\n        root ? `${root}` : [],\n        nested\n          ? Object.entries(nested).map(\n              ([path, message]) => `Path: routes.${path}\\n${message}`,\n            )\n          : [],\n      ]\n        .flat()\n        .join(\"\\n\\n\"),\n    };\n  }\n\n  return {\n    valid: true,\n    routeConfig: routeConfig as RouteConfigEntry[],\n  };\n}\n\nconst createConfigRouteOptionKeys = [\n  \"id\",\n  \"index\",\n  \"caseSensitive\",\n] as const satisfies Array<keyof RouteConfigEntry>;\ntype CreateRouteOptions = Pick<\n  RouteConfigEntry,\n  (typeof createConfigRouteOptionKeys)[number]\n>;\n/**\n * Helper function for creating a route config entry, for use within\n * `routes.ts`.\n */\nfunction route(\n  path: string | null | undefined,\n  file: string,\n  children?: RouteConfigEntry[],\n): RouteConfigEntry;\nfunction route(\n  path: string | null | undefined,\n  file: string,\n  options: CreateRouteOptions,\n  children?: RouteConfigEntry[],\n): RouteConfigEntry;\nfunction route(\n  path: string | null | undefined,\n  file: string,\n  optionsOrChildren: CreateRouteOptions | RouteConfigEntry[] | undefined,\n  children?: RouteConfigEntry[],\n): RouteConfigEntry {\n  let options: CreateRouteOptions = {};\n\n  if (Array.isArray(optionsOrChildren) || !optionsOrChildren) {\n    children = optionsOrChildren;\n  } else {\n    options = optionsOrChildren;\n  }\n\n  return {\n    file,\n    children,\n    path: path ?? undefined,\n    ...pick(options, createConfigRouteOptionKeys),\n  };\n}\n\nconst createIndexOptionKeys = [\"id\"] as const satisfies Array<\n  keyof RouteConfigEntry\n>;\ntype CreateIndexOptions = Pick<\n  RouteConfigEntry,\n  (typeof createIndexOptionKeys)[number]\n>;\n/**\n * Helper function for creating a route config entry for an index route, for use\n * within `routes.ts`.\n */\nfunction index(file: string, options?: CreateIndexOptions): RouteConfigEntry {\n  return {\n    file,\n    index: true,\n    ...pick(options, createIndexOptionKeys),\n  };\n}\n\nconst createLayoutOptionKeys = [\"id\"] as const satisfies Array<\n  keyof RouteConfigEntry\n>;\ntype CreateLayoutOptions = Pick<\n  RouteConfigEntry,\n  (typeof createLayoutOptionKeys)[number]\n>;\n/**\n * Helper function for creating a route config entry for a layout route, for use\n * within `routes.ts`.\n */\nfunction layout(file: string, children?: RouteConfigEntry[]): RouteConfigEntry;\nfunction layout(\n  file: string,\n  options: CreateLayoutOptions,\n  children?: RouteConfigEntry[],\n): RouteConfigEntry;\nfunction layout(\n  file: string,\n  optionsOrChildren: CreateLayoutOptions | RouteConfigEntry[] | undefined,\n  children?: RouteConfigEntry[],\n): RouteConfigEntry {\n  let options: CreateLayoutOptions = {};\n\n  if (Array.isArray(optionsOrChildren) || !optionsOrChildren) {\n    children = optionsOrChildren;\n  } else {\n    options = optionsOrChildren;\n  }\n\n  return {\n    file,\n    children,\n    ...pick(options, createLayoutOptionKeys),\n  };\n}\n\n/**\n * Helper function for adding a path prefix to a set of routes without needing\n * to introduce a parent route file, for use within `routes.ts`.\n */\nfunction prefix(\n  prefixPath: string,\n  routes: RouteConfigEntry[],\n): RouteConfigEntry[] {\n  return routes.map((route) => {\n    if (route.index || typeof route.path === \"string\") {\n      return {\n        ...route,\n        path: route.path ? joinRoutePaths(prefixPath, route.path) : prefixPath,\n        children: route.children,\n      };\n    } else if (route.children) {\n      return {\n        ...route,\n        children: prefix(prefixPath, route.children),\n      };\n    }\n    return route;\n  });\n}\n\nconst helpers = { route, index, layout, prefix };\nexport { route, index, layout, prefix };\n/**\n * Creates a set of route config helpers that resolve file paths relative to the\n * given directory, for use within `routes.ts`. This is designed to support\n * splitting route config into multiple files within different directories.\n */\nexport function relative(directory: string): typeof helpers {\n  return {\n    /**\n     * Helper function for creating a route config entry, for use within\n     * `routes.ts`. Note that this helper has been scoped, meaning that file\n     * path will be resolved relative to the directory provided to the\n     * `relative` call that created this helper.\n     */\n    route: (path, file, ...rest) => {\n      return route(path, Path.resolve(directory, file), ...(rest as any));\n    },\n    /**\n     * Helper function for creating a route config entry for an index route, for\n     * use within `routes.ts`. Note that this helper has been scoped, meaning\n     * that file path will be resolved relative to the directory provided to the\n     * `relative` call that created this helper.\n     */\n    index: (file, ...rest) => {\n      return index(Path.resolve(directory, file), ...(rest as any));\n    },\n    /**\n     * Helper function for creating a route config entry for a layout route, for\n     * use within `routes.ts`. Note that this helper has been scoped, meaning\n     * that file path will be resolved relative to the directory provided to the\n     * `relative` call that created this helper.\n     */\n    layout: (file, ...rest) => {\n      return layout(Path.resolve(directory, file), ...(rest as any));\n    },\n\n    // Passthrough of helper functions that don't need relative scoping so that\n    // a complete API is still provided.\n    prefix,\n  };\n}\n\nexport function configRoutesToRouteManifest(\n  appDirectory: string,\n  routes: RouteConfigEntry[],\n): RouteManifest {\n  let routeManifest: RouteManifest = {};\n\n  function walk(route: RouteConfigEntry, parentId?: string) {\n    let id = route.id || createRouteId(route.file);\n    let manifestItem: RouteManifestEntry = {\n      id,\n      parentId,\n      file: Path.isAbsolute(route.file)\n        ? Path.relative(appDirectory, route.file)\n        : route.file,\n      path: route.path,\n      index: route.index,\n      caseSensitive: route.caseSensitive,\n    };\n\n    if (routeManifest.hasOwnProperty(id)) {\n      throw new Error(\n        `Unable to define routes with duplicate route id: \"${id}\"`,\n      );\n    }\n    routeManifest[id] = manifestItem;\n\n    if (route.children) {\n      for (let child of route.children) {\n        walk(child, id);\n      }\n    }\n  }\n\n  for (let route of routes) {\n    walk(route);\n  }\n\n  return routeManifest;\n}\n\nfunction createRouteId(file: string) {\n  return Path.normalize(stripFileExtension(file));\n}\n\nfunction stripFileExtension(file: string) {\n  return file.replace(/\\.[a-z0-9]+$/i, \"\");\n}\n\nfunction joinRoutePaths(path1: string, path2: string): string {\n  return [\n    path1.replace(/\\/+$/, \"\"), // Remove trailing slashes\n    path2.replace(/^\\/+/, \"\"), // Remove leading slashes\n  ].join(\"/\");\n}\n"
  },
  {
    "path": "packages/react-router-dev/config.ts",
    "content": "export type {\n  ReactRouterConfig as Config,\n  BuildManifest,\n  Preset,\n  ServerBundlesFunction,\n} from \"./config/config\";\n"
  },
  {
    "path": "packages/react-router-dev/invariant.ts",
    "content": "export default function invariant(\n  value: boolean,\n  message?: string,\n): asserts value;\n\nexport default function invariant<T>(\n  value: T | null | undefined,\n  message?: string,\n): asserts value is T;\n\nexport default function invariant(value: any, message?: string) {\n  if (value === false || value === null || typeof value === \"undefined\") {\n    console.error(\n      \"The following error is a bug in React Router; please open an issue! https://github.com/remix-run/react-router/issues/new/choose\",\n    );\n    throw new Error(message);\n  }\n}\n"
  },
  {
    "path": "packages/react-router-dev/jest.config.js",
    "content": "/** @type {import('jest').Config} */\nmodule.exports = {\n  ...require(\"../../jest/jest.config.shared\"),\n  displayName: \"dev\",\n  setupFilesAfterEnv: [\"<rootDir>/__tests__/setupAfterEnv.ts\"],\n  setupFiles: [],\n};\n"
  },
  {
    "path": "packages/react-router-dev/manifest.ts",
    "content": "export type ManifestRoute = {\n  id: string;\n  parentId?: string;\n  path?: string;\n  index?: boolean;\n  caseSensitive?: boolean;\n  module: string;\n  clientLoaderModule: string | undefined;\n  clientActionModule: string | undefined;\n  clientMiddlewareModule: string | undefined;\n  hydrateFallbackModule: string | undefined;\n  imports?: string[];\n  hasAction: boolean;\n  hasLoader: boolean;\n  hasClientAction: boolean;\n  hasClientLoader: boolean;\n  hasClientMiddleware: boolean;\n  hasErrorBoundary: boolean;\n  hasDefaultExport: boolean;\n};\n\nexport type Manifest = {\n  version: string;\n  url?: string;\n  entry: {\n    module: string;\n    imports: string[];\n  };\n  routes: {\n    [routeId: string]: ManifestRoute;\n  };\n  sri: Record<string, string> | undefined;\n  hmr?: {\n    timestamp?: number;\n    runtime: string;\n  };\n};\n"
  },
  {
    "path": "packages/react-router-dev/module-sync-enabled/false.cjs",
    "content": "exports.default = false;\n"
  },
  {
    "path": "packages/react-router-dev/module-sync-enabled/index.d.mts",
    "content": "declare const moduleSyncEnabled: boolean;\nexport { moduleSyncEnabled };\n"
  },
  {
    "path": "packages/react-router-dev/module-sync-enabled/index.mjs",
    "content": "import { createRequire } from \"node:module\";\nconst require = createRequire(import.meta.url);\nconst moduleSyncEnabled = require(\"#module-sync-enabled\").default;\nexport { moduleSyncEnabled };\n"
  },
  {
    "path": "packages/react-router-dev/module-sync-enabled/true.mjs",
    "content": "const moduleSyncEnabled = true;\nexport default moduleSyncEnabled;\n"
  },
  {
    "path": "packages/react-router-dev/package.json",
    "content": "{\n  \"name\": \"@react-router/dev\",\n  \"version\": \"7.13.1\",\n  \"description\": \"Dev tools and CLI for React Router\",\n  \"homepage\": \"https://reactrouter.com\",\n  \"bugs\": {\n    \"url\": \"https://github.com/remix-run/react-router/issues\"\n  },\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"https://github.com/remix-run/react-router\",\n    \"directory\": \"packages/react-router-dev\"\n  },\n  \"license\": \"MIT\",\n  \"exports\": {\n    \"./config/default-rsc-entries/entry.client\": \"./dist/config/default-rsc-entries/entry.client.tsx\",\n    \"./config/default-rsc-entries/entry.rsc\": \"./dist/config/default-rsc-entries/entry.rsc.tsx\",\n    \"./config/default-rsc-entries/entry.ssr\": \"./dist/config/default-rsc-entries/entry.ssr.tsx\",\n    \"./config\": {\n      \"types\": \"./dist/config.d.ts\",\n      \"default\": \"./dist/config.js\"\n    },\n    \"./routes\": {\n      \"types\": \"./dist/routes.d.ts\",\n      \"default\": \"./dist/routes.js\"\n    },\n    \"./rsc-types\": \"./rsc-types.d.ts\",\n    \"./vite\": {\n      \"types\": \"./dist/vite.d.ts\",\n      \"default\": \"./dist/vite.js\"\n    },\n    \"./vite/cloudflare\": {\n      \"types\": \"./dist/vite/cloudflare.d.ts\",\n      \"default\": \"./dist/vite/cloudflare.js\"\n    },\n    \"./package.json\": \"./package.json\"\n  },\n  \"imports\": {\n    \"#module-sync-enabled\": {\n      \"module-sync\": \"./module-sync-enabled/true.mjs\",\n      \"default\": \"./module-sync-enabled/false.cjs\"\n    }\n  },\n  \"bin\": {\n    \"react-router\": \"bin.js\"\n  },\n  \"scripts\": {\n    \"build\": \"wireit\",\n    \"typecheck\": \"tsc\"\n  },\n  \"wireit\": {\n    \"build\": {\n      \"command\": \"tsup\",\n      \"files\": [\n        \"../../pnpm-workspace.yaml\",\n        \"cli/**\",\n        \"config/**\",\n        \"module-sync-enabled/**\",\n        \"typegen/**\",\n        \"vite/**\",\n        \"*.ts\",\n        \"bin.js\",\n        \"tsconfig.json\",\n        \"package.json\"\n      ],\n      \"output\": [\n        \"dist/**\"\n      ]\n    }\n  },\n  \"dependencies\": {\n    \"@babel/core\": \"^7.27.7\",\n    \"@babel/generator\": \"^7.27.5\",\n    \"@babel/parser\": \"^7.27.7\",\n    \"@babel/plugin-syntax-jsx\": \"^7.27.1\",\n    \"@babel/preset-typescript\": \"^7.27.1\",\n    \"@babel/traverse\": \"^7.27.7\",\n    \"@babel/types\": \"^7.27.7\",\n    \"@react-router/node\": \"workspace:*\",\n    \"@remix-run/node-fetch-server\": \"^0.13.0\",\n    \"arg\": \"^5.0.1\",\n    \"babel-dead-code-elimination\": \"^1.0.6\",\n    \"chokidar\": \"^4.0.0\",\n    \"dedent\": \"^1.5.3\",\n    \"es-module-lexer\": \"^1.3.1\",\n    \"exit-hook\": \"2.2.1\",\n    \"isbot\": \"^5.1.11\",\n    \"jsesc\": \"3.0.2\",\n    \"lodash\": \"^4.17.21\",\n    \"p-map\": \"^7.0.3\",\n    \"pathe\": \"^1.1.2\",\n    \"picocolors\": \"^1.1.1\",\n    \"pkg-types\": \"^2.3.0\",\n    \"prettier\": \"^3.6.2\",\n    \"react-refresh\": \"^0.14.0\",\n    \"semver\": \"^7.3.7\",\n    \"tinyglobby\": \"^0.2.14\",\n    \"valibot\": \"^1.2.0\",\n    \"vite-node\": \"^3.2.2\"\n  },\n  \"devDependencies\": {\n    \"@react-router/serve\": \"workspace:*\",\n    \"@types/babel__core\": \"^7.20.5\",\n    \"@types/babel__generator\": \"^7.27.0\",\n    \"@types/babel__traverse\": \"^7.20.7\",\n    \"@types/dedent\": \"^0.7.0\",\n    \"@types/express\": \"^4.17.9\",\n    \"@types/jest\": \"^29.5.4\",\n    \"@types/jsesc\": \"^3.0.1\",\n    \"@types/lodash\": \"^4.14.182\",\n    \"@types/node\": \"^20.0.0\",\n    \"@types/npmcli__package-json\": \"^4.0.0\",\n    \"@types/semver\": \"^7.7.0\",\n    \"@vitejs/plugin-rsc\": \"catalog:\",\n    \"esbuild-register\": \"^3.6.0\",\n    \"execa\": \"5.1.1\",\n    \"express\": \"^4.19.2\",\n    \"fast-glob\": \"3.2.11\",\n    \"react-router\": \"workspace:^\",\n    \"tsup\": \"catalog:\",\n    \"typescript\": \"catalog:\",\n    \"vite\": \"^6.3.0\",\n    \"wireit\": \"catalog:\",\n    \"wrangler\": \"^4.23.0\"\n  },\n  \"peerDependencies\": {\n    \"@react-router/serve\": \"workspace:^\",\n    \"@vitejs/plugin-rsc\": \"catalog:\",\n    \"react-router\": \"workspace:^\",\n    \"react-server-dom-webpack\": \"catalog:\",\n    \"typescript\": \"^5.1.0\",\n    \"vite\": \"^5.1.0 || ^6.0.0 || ^7.0.0\",\n    \"wrangler\": \"^3.28.2 || ^4.0.0\"\n  },\n  \"peerDependenciesMeta\": {\n    \"@vitejs/plugin-rsc\": {\n      \"optional\": true\n    },\n    \"@react-router/serve\": {\n      \"optional\": true\n    },\n    \"typescript\": {\n      \"optional\": true\n    },\n    \"react-server-dom-webpack\": {\n      \"optional\": true\n    },\n    \"wrangler\": {\n      \"optional\": true\n    }\n  },\n  \"engines\": {\n    \"node\": \">=20.0.0\"\n  },\n  \"files\": [\n    \"dist/\",\n    \"module-sync-enabled/\",\n    \"bin.js\",\n    \"rsc-types.d.ts\",\n    \"CHANGELOG.md\",\n    \"LICENSE.md\",\n    \"README.md\"\n  ]\n}\n"
  },
  {
    "path": "packages/react-router-dev/routes.ts",
    "content": "export type { RouteConfig, RouteConfigEntry } from \"./config/routes\";\n\nexport {\n  route,\n  index,\n  layout,\n  prefix,\n  relative,\n  getAppDirectory,\n} from \"./config/routes\";\n"
  },
  {
    "path": "packages/react-router-dev/rsc-types.d.ts",
    "content": "declare module \"virtual:react-router/unstable_rsc/routes\" {\n  import type { unstable_RSCRouteConfig as RSCRouteConfig } from \"react-router\";\n\n  const routes: RSCRouteConfig;\n  export default routes;\n}\n\ndeclare module \"virtual:react-router/unstable_rsc/basename\" {\n  const basename: string;\n  export default basename;\n}\n\ndeclare module \"virtual:react-router/unstable_rsc/react-router-serve-config\" {\n  const unstable_reactRouterServeConfig: {\n    publicPath: string;\n    assetsBuildDirectory: string;\n  };\n  export default unstable_reactRouterServeConfig;\n}\n\ndeclare module \"virtual:react-router/unstable_rsc/inject-hmr-runtime\" {}\n"
  },
  {
    "path": "packages/react-router-dev/tsconfig.json",
    "content": "{\n  \"include\": [\"**/*.ts\", \"./rsc-types.d.ts\", \"package.json\"],\n  \"exclude\": [\"dist\", \"__tests__\", \"node_modules\", \"**/*-test.ts\"],\n  \"compilerOptions\": {\n    \"lib\": [\"DOM\", \"DOM.Iterable\", \"ES2022\"],\n    \"types\": [\"vite/client\"],\n    \"target\": \"ES2022\",\n    \"module\": \"ES2022\",\n    \"moduleResolution\": \"Bundler\",\n\n    \"strict\": true,\n    \"jsx\": \"react\",\n\n    \"skipLibCheck\": true,\n\n    \"outDir\": \"dist\",\n    \"rootDir\": \".\",\n    \"noEmit\": true,\n    \"resolveJsonModule\": true,\n    \"allowSyntheticDefaultImports\": true\n  }\n}\n"
  },
  {
    "path": "packages/react-router-dev/tsup.config.ts",
    "content": "import * as fsp from \"fs/promises\";\n\nimport { defineConfig } from \"tsup\";\n\n// @ts-ignore - out of scope\nimport { createBanner } from \"../../build.utils.js\";\n\nimport pkg from \"./package.json\";\n\nconst entry = [\n  \"cli/index.ts\",\n  \"config.ts\",\n  \"internal.ts\",\n  \"routes.ts\",\n  \"vite.ts\",\n  \"vite/cloudflare.ts\",\n];\n\nconst external = [\n  \"./static/refresh-utils.mjs\",\n  \"./static/rsc-refresh-utils.mjs\",\n  /\\.json$/,\n];\n\nexport default defineConfig([\n  {\n    clean: true,\n    entry,\n    format: [\"cjs\"],\n    outDir: \"dist\",\n    dts: true,\n    external,\n    banner: {\n      js: createBanner(pkg.name, pkg.version),\n    },\n    plugins: [\n      {\n        name: \"copy\",\n        async buildEnd() {\n          await fsp.mkdir(\"dist/static\", { recursive: true });\n          await fsp.copyFile(\n            \"vite/static/refresh-utils.mjs\",\n            \"dist/static/refresh-utils.mjs\",\n          );\n\n          await fsp.mkdir(\"dist/static\", { recursive: true });\n          await fsp.copyFile(\n            \"vite/static/refresh-utils.mjs\",\n            \"dist/static/refresh-utils.mjs\",\n          );\n          await fsp.copyFile(\n            \"vite/static/rsc-refresh-utils.mjs\",\n            \"dist/static/rsc-refresh-utils.mjs\",\n          );\n\n          await fsp.mkdir(\"dist/config/defaults\", { recursive: true });\n          const files = await fsp.readdir(\"config/defaults\");\n          for (const file of files) {\n            await fsp.copyFile(\n              `config/defaults/${file}`,\n              `dist/config/defaults/${file}`,\n            );\n          }\n\n          await fsp.mkdir(\"dist/config/default-rsc-entries\", {\n            recursive: true,\n          });\n          const rscFiles = await fsp.readdir(\"config/default-rsc-entries\");\n          for (const file of rscFiles) {\n            await fsp.copyFile(\n              `config/default-rsc-entries/${file}`,\n              `dist/config/default-rsc-entries/${file}`,\n            );\n          }\n        },\n      },\n    ],\n  },\n]);\n"
  },
  {
    "path": "packages/react-router-dev/typedoc.mjs",
    "content": "import { blockTags } from \"../../typedoc.mjs\";\n\n/** @type {Partial<import('typedoc').TypeDocOptions>} */\nconst config = {\n  entryPoints: [\n    \"./index.ts\",\n    \"./config.ts\",\n    \"./routes.ts\",\n    \"./vite.ts\",\n    \"./vite/cloudflare.ts\",\n  ],\n  blockTags,\n};\n\nexport default config;\n"
  },
  {
    "path": "packages/react-router-dev/typegen/context.ts",
    "content": "import {\n  createConfigLoader,\n  type ConfigLoader,\n  type ResolvedReactRouterConfig,\n} from \"../config/config\";\n\nexport type Context = {\n  rootDirectory: string;\n  configLoader: ConfigLoader;\n  config: ResolvedReactRouterConfig;\n  rsc: boolean;\n};\n\nexport async function createContext({\n  rootDirectory,\n  watch,\n  mode,\n  rsc,\n}: {\n  rootDirectory: string;\n  watch: boolean;\n  mode: string;\n  rsc: boolean;\n}): Promise<Context> {\n  const configLoader = await createConfigLoader({ rootDirectory, mode, watch });\n  const configResult = await configLoader.getConfig();\n\n  if (!configResult.ok) {\n    throw new Error(configResult.error);\n  }\n\n  const config = configResult.value;\n\n  return {\n    configLoader,\n    rootDirectory,\n    config,\n    rsc,\n  };\n}\n"
  },
  {
    "path": "packages/react-router-dev/typegen/generate.ts",
    "content": "import ts from \"dedent\";\nimport * as Path from \"pathe\";\nimport * as Pathe from \"pathe/utils\";\n\nimport * as Babel from \"../vite/babel\";\nimport type { Context } from \"./context\";\nimport * as Params from \"./params\";\nimport * as Route from \"./route\";\nimport type { RouteManifestEntry } from \"../config/routes\";\n\nexport type VirtualFile = { filename: string; content: string };\n\nexport function typesDirectory(ctx: Context) {\n  return Path.join(ctx.rootDirectory, \".react-router/types\");\n}\n\nexport function generateFuture(ctx: Context): VirtualFile {\n  const filename = Path.join(typesDirectory(ctx), \"+future.ts\");\n  const content = ts`\n    // Generated by React Router\n\n    import \"react-router\";\n\n    declare module \"react-router\" {\n      interface Future {\n        v8_middleware: ${ctx.config.future.v8_middleware}\n      }\n    }\n  `;\n  return { filename, content };\n}\n\nexport function generateServerBuild(ctx: Context): VirtualFile {\n  const filename = Path.join(typesDirectory(ctx), \"+server-build.d.ts\");\n  const content = ts`\n    // Generated by React Router\n\n    declare module \"virtual:react-router/server-build\" {\n      import { ServerBuild } from \"react-router\";\n      export const assets: ServerBuild[\"assets\"];\n      export const assetsBuildDirectory: ServerBuild[\"assetsBuildDirectory\"];\n      export const basename: ServerBuild[\"basename\"];\n      export const entry: ServerBuild[\"entry\"];\n      export const future: ServerBuild[\"future\"];\n      export const isSpaMode: ServerBuild[\"isSpaMode\"];\n      export const prerender: ServerBuild[\"prerender\"];\n      export const publicPath: ServerBuild[\"publicPath\"];\n      export const routeDiscovery: ServerBuild[\"routeDiscovery\"];\n      export const routes: ServerBuild[\"routes\"];\n      export const ssr: ServerBuild[\"ssr\"];\n      export const allowedActionOrigins: ServerBuild[\"allowedActionOrigins\"];\n      export const unstable_getCriticalCss: ServerBuild[\"unstable_getCriticalCss\"];\n    }\n  `;\n  return { filename, content };\n}\n\nconst { t } = Babel;\nexport function generateRoutes(ctx: Context): Array<VirtualFile> {\n  // precompute\n  const fileToRoutes = new Map<string, Set<string>>();\n  const lineages = new Map<string, Array<RouteManifestEntry>>();\n  const allPages = new Set<string>();\n  const routeToPages = new Map<string, Set<string>>();\n  for (const route of Object.values(ctx.config.routes)) {\n    // fileToRoutes\n    let routeIds = fileToRoutes.get(route.file);\n    if (!routeIds) {\n      routeIds = new Set();\n      fileToRoutes.set(route.file, routeIds);\n    }\n    routeIds.add(route.id);\n\n    // lineages\n    const lineage = Route.lineage(ctx.config.routes, route);\n    lineages.set(route.id, lineage);\n\n    // pages\n    const fullpath = Route.fullpath(lineage);\n    if (!fullpath) continue;\n\n    const pages = expand(fullpath);\n    pages.forEach((page) => allPages.add(page));\n\n    // routePages\n    lineage.forEach(({ id }) => {\n      let routePages = routeToPages.get(id);\n      if (!routePages) {\n        routePages = new Set<string>();\n        routeToPages.set(id, routePages);\n      }\n      pages.forEach((page) => routePages.add(page));\n    });\n  }\n\n  // +routes.ts\n  const routesTs: VirtualFile = {\n    filename: Path.join(typesDirectory(ctx), \"+routes.ts\"),\n    content:\n      ts`\n        // Generated by React Router\n\n        import \"react-router\"\n\n        declare module \"react-router\" {\n          interface Register {\n            pages: Pages\n            routeFiles: RouteFiles\n            routeModules: RouteModules\n          }\n        }\n      ` +\n      \"\\n\\n\" +\n      Babel.generate(pagesType(allPages)).code +\n      \"\\n\\n\" +\n      Babel.generate(routeFilesType({ fileToRoutes, routeToPages })).code +\n      \"\\n\\n\" +\n      Babel.generate(routeModulesType(ctx)).code,\n  };\n\n  // **/+types/*.ts\n  const allAnnotations: Array<VirtualFile> = Array.from(fileToRoutes.entries())\n    .filter(([file]) => isInAppDirectory(ctx, file))\n    .map(([file, routeIds]) =>\n      getRouteAnnotations({ ctx, file, routeIds, lineages }),\n    );\n\n  return [routesTs, ...allAnnotations];\n}\n\nfunction pagesType(pages: Set<string>) {\n  return t.tsTypeAliasDeclaration(\n    t.identifier(\"Pages\"),\n    null,\n    t.tsTypeLiteral(\n      Array.from(pages).map((page) => {\n        return t.tsPropertySignature(\n          t.stringLiteral(page),\n          t.tsTypeAnnotation(\n            t.tsTypeLiteral([\n              t.tsPropertySignature(\n                t.identifier(\"params\"),\n                t.tsTypeAnnotation(paramsType(page)),\n              ),\n            ]),\n          ),\n        );\n      }),\n    ),\n  );\n}\n\nfunction routeFilesType({\n  fileToRoutes,\n  routeToPages,\n}: {\n  fileToRoutes: Map<string, Set<string>>;\n  routeToPages: Map<string, Set<string>>;\n}) {\n  return t.tsTypeAliasDeclaration(\n    t.identifier(\"RouteFiles\"),\n    null,\n    t.tsTypeLiteral(\n      Array.from(fileToRoutes).map(([file, routeIds]) =>\n        t.tsPropertySignature(\n          t.stringLiteral(file),\n          t.tsTypeAnnotation(\n            t.tsUnionType(\n              Array.from(routeIds).map((routeId) => {\n                const pages = routeToPages.get(routeId) ?? new Set();\n                return t.tsTypeLiteral([\n                  t.tsPropertySignature(\n                    t.identifier(\"id\"),\n                    t.tsTypeAnnotation(\n                      t.tsLiteralType(t.stringLiteral(routeId)),\n                    ),\n                  ),\n                  t.tsPropertySignature(\n                    t.identifier(\"page\"),\n                    t.tsTypeAnnotation(\n                      pages\n                        ? t.tsUnionType(\n                            Array.from(pages).map((page) =>\n                              t.tsLiteralType(t.stringLiteral(page)),\n                            ),\n                          )\n                        : t.tsNeverKeyword(),\n                    ),\n                  ),\n                ]);\n              }),\n            ),\n          ),\n        ),\n      ),\n    ),\n  );\n}\n\nfunction routeModulesType(ctx: Context) {\n  return t.tsTypeAliasDeclaration(\n    t.identifier(\"RouteModules\"),\n    null,\n    t.tsTypeLiteral(\n      Object.values(ctx.config.routes).map((route) =>\n        t.tsPropertySignature(\n          t.stringLiteral(route.id),\n          t.tsTypeAnnotation(\n            isInAppDirectory(ctx, route.file)\n              ? t.tsTypeQuery(\n                  t.tsImportType(\n                    t.stringLiteral(\n                      `./${Path.relative(ctx.rootDirectory, ctx.config.appDirectory)}/${route.file}`,\n                    ),\n                  ),\n                )\n              : t.tsUnknownKeyword(),\n          ),\n        ),\n      ),\n    ),\n  );\n}\n\nfunction isInAppDirectory(ctx: Context, routeFile: string): boolean {\n  const path = Path.resolve(ctx.config.appDirectory, routeFile);\n  return path.startsWith(ctx.config.appDirectory);\n}\n\nfunction getRouteAnnotations({\n  ctx,\n  file,\n  routeIds,\n  lineages,\n}: {\n  ctx: Context;\n  file: string;\n  routeIds: Set<string>;\n  lineages: Map<string, Array<RouteManifestEntry>>;\n}) {\n  const filename = Path.join(\n    typesDirectory(ctx),\n    Path.relative(ctx.rootDirectory, ctx.config.appDirectory),\n    Path.dirname(file),\n    \"+types\",\n    Pathe.filename(file) + \".ts\",\n  );\n\n  const matchesType = t.tsTypeAliasDeclaration(\n    t.identifier(\"Matches\"),\n    null,\n    t.tsUnionType(\n      Array.from(routeIds).map((routeId) => {\n        const lineage = lineages.get(routeId)!;\n        return t.tsTupleType(\n          lineage.map((route) =>\n            t.tsTypeLiteral([\n              t.tsPropertySignature(\n                t.identifier(\"id\"),\n                t.tsTypeAnnotation(t.tsLiteralType(t.stringLiteral(route.id))),\n              ),\n              t.tsPropertySignature(\n                t.identifier(\"module\"),\n                t.tsTypeAnnotation(\n                  t.tsTypeQuery(\n                    t.tsImportType(\n                      t.stringLiteral(\n                        relativeImportSource(\n                          rootDirsPath(ctx, filename),\n                          Path.resolve(ctx.config.appDirectory, route.file),\n                        ),\n                      ),\n                    ),\n                  ),\n                ),\n              ),\n            ]),\n          ),\n        );\n      }),\n    ),\n  );\n\n  const routeImportSource = relativeImportSource(\n    rootDirsPath(ctx, filename),\n    Path.resolve(ctx.config.appDirectory, file),\n  );\n\n  const content =\n    ts`\n      // Generated by React Router\n\n      import type { GetInfo, GetAnnotations } from \"react-router/internal\";\n\n      type Module = typeof import(\"${routeImportSource}\")\n\n      type Info = GetInfo<{\n        file: \"${file}\",\n        module: Module\n      }>\n    ` +\n    \"\\n\\n\" +\n    Babel.generate(matchesType).code +\n    \"\\n\\n\" +\n    ts`\n      type Annotations = GetAnnotations<Info & { module: Module, matches: Matches }, ${ctx.rsc}>;\n\n      export namespace Route {\n        // links\n        export type LinkDescriptors = Annotations[\"LinkDescriptors\"];\n        export type LinksFunction = Annotations[\"LinksFunction\"];\n\n        // meta\n        export type MetaArgs = Annotations[\"MetaArgs\"];\n        export type MetaDescriptors = Annotations[\"MetaDescriptors\"];\n        export type MetaFunction = Annotations[\"MetaFunction\"];\n\n        // headers\n        export type HeadersArgs = Annotations[\"HeadersArgs\"];\n        export type HeadersFunction = Annotations[\"HeadersFunction\"];\n\n        // middleware\n        export type MiddlewareFunction = Annotations[\"MiddlewareFunction\"];\n\n        // clientMiddleware\n        export type ClientMiddlewareFunction = Annotations[\"ClientMiddlewareFunction\"];\n\n        // loader\n        export type LoaderArgs = Annotations[\"LoaderArgs\"];\n\n        // clientLoader\n        export type ClientLoaderArgs = Annotations[\"ClientLoaderArgs\"];\n\n        // action\n        export type ActionArgs = Annotations[\"ActionArgs\"];\n\n        // clientAction\n        export type ClientActionArgs = Annotations[\"ClientActionArgs\"];\n\n        // HydrateFallback\n        export type HydrateFallbackProps = Annotations[\"HydrateFallbackProps\"];\n\n        // Component\n        export type ComponentProps = Annotations[\"ComponentProps\"];\n\n        // ErrorBoundary\n        export type ErrorBoundaryProps = Annotations[\"ErrorBoundaryProps\"];\n      }\n    `;\n  return { filename, content };\n}\n\nfunction relativeImportSource(from: string, to: string) {\n  let path = Path.relative(Path.dirname(from), to);\n\n  let extension = Path.extname(path);\n\n  // no extension\n  path = Path.join(Path.dirname(path), Pathe.filename(path));\n  if (!path.startsWith(\"../\")) path = \"./\" + path;\n\n  // In typescript, we want to support \"moduleResolution\": \"nodenext\" as well as not having \"allowImportingTsExtensions\": true,\n  // so we normalize all JS like files to `.js`, but allow other extensions such as `.mdx` and others that might be used as routes.\n  if (!extension || /\\.(js|ts)x?$/.test(extension)) {\n    extension = \".js\";\n  }\n\n  return path + extension;\n}\n\nfunction rootDirsPath(ctx: Context, typesPath: string): string {\n  const rel = Path.relative(typesDirectory(ctx), typesPath);\n  return Path.join(ctx.rootDirectory, rel);\n}\n\nfunction paramsType(path: string) {\n  const params = Params.parse(path);\n  return t.tsTypeLiteral(\n    Object.entries(params).map(([param, isRequired]) => {\n      const property = t.tsPropertySignature(\n        t.stringLiteral(param),\n        t.tsTypeAnnotation(t.tsStringKeyword()),\n      );\n      property.optional = !isRequired;\n      return property;\n    }),\n  );\n}\n\nfunction expand(fullpath: string): Set<string> {\n  function recurse(segments: Array<string>, index: number): Array<string> {\n    if (index === segments.length) return [\"\"];\n    const segment = segments[index];\n\n    const isOptional = segment.endsWith(\"?\");\n    const isDynamic = segment.startsWith(\":\");\n    const required = segment.replace(/\\?$/, \"\");\n\n    const keep = !isOptional || isDynamic;\n    const kept = isDynamic ? segment : required;\n\n    const withoutSegment = recurse(segments, index + 1);\n    const withSegment = withoutSegment.map((rest) => [kept, rest].join(\"/\"));\n\n    if (keep) return withSegment;\n    return [...withoutSegment, ...withSegment];\n  }\n\n  const segments = fullpath.split(\"/\");\n  const expanded = new Set<string>();\n  for (let result of recurse(segments, 0)) {\n    if (result !== \"/\") result = result.replace(/\\/$/, \"\");\n    expanded.add(result);\n  }\n  return expanded;\n}\n"
  },
  {
    "path": "packages/react-router-dev/typegen/index.ts",
    "content": "import fs from \"node:fs/promises\";\n\nimport * as Path from \"pathe\";\nimport { green, red } from \"picocolors\";\nimport type vite from \"vite\";\n\nimport { type Context, createContext } from \"./context\";\nimport {\n  type VirtualFile,\n  typesDirectory,\n  generateFuture,\n  generateRoutes,\n  generateServerBuild,\n} from \"./generate\";\n\nasync function clearRouteModuleAnnotations(ctx: Context) {\n  await fs.rm(\n    Path.join(typesDirectory(ctx), Path.basename(ctx.config.appDirectory)),\n    { recursive: true, force: true },\n  );\n}\n\nasync function write(...files: Array<VirtualFile>) {\n  return Promise.all(\n    files.map(async ({ filename, content }) => {\n      await fs.mkdir(Path.dirname(filename), { recursive: true });\n      await fs.writeFile(filename, content);\n    }),\n  );\n}\n\nexport async function run(\n  rootDirectory: string,\n  { mode, rsc }: { mode: string; rsc: boolean },\n) {\n  const ctx = await createContext({ rootDirectory, mode, rsc, watch: false });\n  await fs.rm(typesDirectory(ctx), { recursive: true, force: true });\n  await write(\n    generateFuture(ctx),\n    generateServerBuild(ctx),\n    ...generateRoutes(ctx),\n  );\n}\n\nexport type Watcher = {\n  close: () => Promise<void>;\n};\n\nexport async function watch(\n  rootDirectory: string,\n  { mode, logger, rsc }: { mode: string; logger?: vite.Logger; rsc: boolean },\n): Promise<Watcher> {\n  const ctx = await createContext({ rootDirectory, mode, rsc, watch: true });\n  await fs.rm(typesDirectory(ctx), { recursive: true, force: true });\n  await write(\n    generateFuture(ctx),\n    generateServerBuild(ctx),\n    ...generateRoutes(ctx),\n  );\n  logger?.info(green(\"generated types\"), { timestamp: true, clear: true });\n\n  ctx.configLoader.onChange(\n    async ({ result, configChanged, routeConfigChanged }) => {\n      if (!result.ok) {\n        logger?.error(red(result.error), { timestamp: true, clear: true });\n        return;\n      }\n      ctx.config = result.value;\n\n      if (configChanged) {\n        await write(generateFuture(ctx));\n        logger?.info(green(\"regenerated types\"), {\n          timestamp: true,\n          clear: true,\n        });\n      }\n\n      if (routeConfigChanged) {\n        await clearRouteModuleAnnotations(ctx);\n        await write(...generateRoutes(ctx));\n        logger?.info(green(\"regenerated types\"), {\n          timestamp: true,\n          clear: true,\n        });\n      }\n    },\n  );\n\n  return {\n    close: async () => await ctx.configLoader.close(),\n  };\n}\n"
  },
  {
    "path": "packages/react-router-dev/typegen/params.ts",
    "content": "export function parse(fullpath: string) {\n  const result: Record<string, boolean> = {};\n\n  let segments = fullpath.split(\"/\");\n  segments.forEach((segment) => {\n    const match = segment.match(/^:([\\w-]+)(\\?)?/);\n    if (!match) return;\n    const param = match[1];\n    const isRequired = match[2] === undefined;\n\n    result[param] ||= isRequired;\n    return;\n  });\n\n  const hasSplat = segments.at(-1) === \"*\";\n  if (hasSplat) result[\"*\"] = true;\n  return result;\n}\n"
  },
  {
    "path": "packages/react-router-dev/typegen/route.ts",
    "content": "import type { RouteManifest, RouteManifestEntry } from \"../config/routes\";\n\nexport function lineage(\n  routes: RouteManifest,\n  route: RouteManifestEntry,\n): RouteManifestEntry[] {\n  const result: RouteManifestEntry[] = [];\n  while (route) {\n    result.push(route);\n    if (!route.parentId) break;\n    route = routes[route.parentId];\n  }\n  result.reverse();\n  return result;\n}\n\nexport function fullpath(lineage: RouteManifestEntry[]) {\n  const route = lineage.at(-1);\n\n  // root\n  if (lineage.length === 1 && route?.id === \"root\") return \"/\";\n\n  // layout\n  const isLayout = route && route.index !== true && route.path === undefined;\n  if (isLayout) return undefined;\n\n  return (\n    \"/\" +\n    lineage\n      .map((route) => route.path?.replace(/^\\//, \"\")?.replace(/\\/$/, \"\"))\n      .filter((path) => path !== undefined && path !== \"\")\n      .join(\"/\")\n  );\n}\n"
  },
  {
    "path": "packages/react-router-dev/vite/babel.ts",
    "content": "/* eslint-disable @typescript-eslint/consistent-type-imports */\nimport type { NodePath } from \"@babel/traverse\";\nimport type { types as Babel } from \"@babel/core\";\nimport { parse, type ParseResult } from \"@babel/parser\";\nimport * as t from \"@babel/types\";\n\n// These `require`s were needed to support building within vite-ecosystem-ci,\n// otherwise we get errors that `traverse` and `generate` are not functions.\nconst traverse = require(\"@babel/traverse\")\n  .default as typeof import(\"@babel/traverse\").default;\nconst generate = require(\"@babel/generator\")\n  .default as typeof import(\"@babel/generator\").default;\n\nexport { traverse, generate, parse, t };\nexport type { Babel, NodePath, ParseResult };\n"
  },
  {
    "path": "packages/react-router-dev/vite/build.ts",
    "content": "import type * as Vite from \"vite\";\nimport colors from \"picocolors\";\n\nimport { loadConfig } from \"../config/config\";\nimport {\n  type EnvironmentName,\n  type EnvironmentBuildContext,\n  resolveViteConfig,\n  extractPluginContext,\n  cleanBuildDirectory,\n  cleanViteManifests,\n  getEnvironmentOptionsResolvers,\n  resolveEnvironmentsOptions,\n  getServerEnvironmentKeys,\n} from \"./plugin\";\nimport invariant from \"../invariant\";\nimport { preloadVite, getVite } from \"./vite\";\nimport { hasReactRouterRscPlugin } from \"./has-rsc-plugin\";\nexport interface ViteBuildOptions {\n  assetsInlineLimit?: number;\n  clearScreen?: boolean;\n  config?: string;\n  emptyOutDir?: boolean;\n  force?: boolean;\n  logLevel?: Vite.LogLevel;\n  minify?: Vite.BuildOptions[\"minify\"];\n  mode?: string;\n  profile?: boolean;\n  sourcemapClient?: boolean | \"inline\" | \"hidden\";\n  sourcemapServer?: boolean | \"inline\" | \"hidden\";\n}\n\nexport async function build(root: string, viteBuildOptions: ViteBuildOptions) {\n  // Ensure Vite's ESM build is preloaded at the start of the process\n  // so it can be accessed synchronously via `getVite`\n  await preloadVite();\n  let vite = getVite();\n\n  let configResult = await loadConfig({\n    rootDirectory: root,\n    mode: viteBuildOptions.mode ?? \"production\",\n    // In this scope we only need future flags, so we can skip evaluating\n    // routes.ts until we're within the Vite build context\n    skipRoutes: true,\n  });\n\n  if (!configResult.ok) {\n    throw new Error(configResult.error);\n  }\n\n  let config = configResult.value;\n\n  let viteMajor = parseInt(vite.version.split(\".\")[0], 10);\n  if (config.future.v8_viteEnvironmentApi && viteMajor === 5) {\n    throw new Error(\n      \"The future.v8_viteEnvironmentApi option is not supported in Vite 5\",\n    );\n  }\n\n  const useViteEnvironmentApi =\n    config.future.v8_viteEnvironmentApi ||\n    (await hasReactRouterRscPlugin({ root, viteBuildOptions }));\n\n  return await (useViteEnvironmentApi\n    ? viteAppBuild(root, viteBuildOptions)\n    : viteBuild(root, viteBuildOptions));\n}\n\nasync function viteAppBuild(\n  root: string,\n  {\n    assetsInlineLimit,\n    clearScreen,\n    config: configFile,\n    emptyOutDir,\n    force,\n    logLevel,\n    minify,\n    mode,\n    sourcemapClient,\n    sourcemapServer,\n  }: ViteBuildOptions,\n) {\n  let vite = getVite();\n  let builder = await vite.createBuilder({\n    root,\n    mode,\n    configFile,\n    build: {\n      assetsInlineLimit,\n      emptyOutDir,\n      minify,\n    },\n    optimizeDeps: { force },\n    clearScreen,\n    logLevel,\n    plugins: [\n      {\n        name: \"react-router:cli-config\",\n        configEnvironment(name) {\n          if (sourcemapClient && name === \"client\") {\n            return {\n              build: {\n                sourcemap: sourcemapClient,\n              },\n            };\n          }\n          if (sourcemapServer && name !== \"client\") {\n            return {\n              build: {\n                sourcemap: sourcemapServer,\n              },\n            };\n          }\n        },\n        configResolved(config) {\n          let hasReactRouterPlugin = config.plugins.find(\n            (plugin) =>\n              plugin.name === \"react-router\" ||\n              plugin.name === \"react-router/rsc\",\n          );\n          if (!hasReactRouterPlugin) {\n            throw new Error(\n              \"React Router Vite plugin not found in Vite config\",\n            );\n          }\n        },\n      },\n    ],\n  });\n  await builder.buildApp();\n}\n\nasync function viteBuild(\n  root: string,\n  {\n    assetsInlineLimit,\n    clearScreen,\n    config: configFile,\n    emptyOutDir,\n    force,\n    logLevel,\n    minify,\n    mode,\n    sourcemapClient,\n    sourcemapServer,\n  }: ViteBuildOptions,\n) {\n  let viteUserConfig: Vite.UserConfig = {};\n  let viteConfig = await resolveViteConfig({\n    configFile,\n    mode,\n    root,\n    plugins: [\n      {\n        name: \"react-router:extract-vite-user-config\",\n        config(config) {\n          viteUserConfig = config;\n        },\n      },\n    ],\n  });\n  let ctx = extractPluginContext(viteConfig);\n\n  if (!ctx) {\n    console.error(\n      colors.red(\"React Router Vite plugin not found in Vite config\"),\n    );\n    process.exit(1);\n  }\n\n  async function buildEnvironment(environmentName: EnvironmentName) {\n    let vite = getVite();\n    let ssr = environmentName !== \"client\";\n\n    let resolveOptions = environmentOptionsResolvers[environmentName];\n    invariant(resolveOptions);\n\n    let environmentBuildContext: EnvironmentBuildContext = {\n      name: environmentName,\n      resolveOptions,\n    };\n\n    await vite.build({\n      root,\n      mode,\n      configFile,\n      build: {\n        assetsInlineLimit,\n        emptyOutDir,\n        minify,\n        ssr,\n        sourcemap: ssr ? sourcemapServer : sourcemapClient,\n      },\n      optimizeDeps: { force },\n      clearScreen,\n      logLevel,\n      ...{\n        __reactRouterPluginContext: ctx,\n        __reactRouterEnvironmentBuildContext: environmentBuildContext,\n      },\n    });\n  }\n\n  let { reactRouterConfig, buildManifest } = ctx;\n  invariant(buildManifest, \"Expected build manifest to be present\");\n\n  let environmentOptionsResolvers = await getEnvironmentOptionsResolvers(\n    ctx,\n    \"build\",\n  );\n  let environmentsOptions = resolveEnvironmentsOptions(\n    environmentOptionsResolvers,\n    { viteUserConfig },\n  );\n\n  await cleanBuildDirectory(viteConfig, ctx);\n\n  // Run the Vite client build first\n  await buildEnvironment(\"client\");\n\n  // Then run Vite SSR builds in parallel\n  let serverEnvironmentNames = getServerEnvironmentKeys(\n    ctx,\n    environmentOptionsResolvers,\n  );\n\n  await Promise.all(serverEnvironmentNames.map(buildEnvironment));\n\n  await cleanViteManifests(environmentsOptions, ctx);\n\n  await reactRouterConfig.buildEnd?.({\n    buildManifest,\n    reactRouterConfig,\n    viteConfig,\n  });\n}\n"
  },
  {
    "path": "packages/react-router-dev/vite/cache.ts",
    "content": "type CacheEntry<T> = { value: T; version: string };\n\nexport type Cache = Map<string, CacheEntry<any>>;\n\nexport function getOrSetFromCache<T>(\n  cache: Cache,\n  key: string,\n  version: string,\n  getValue: () => T,\n): T {\n  if (!cache) {\n    return getValue();\n  }\n\n  let entry = cache.get(key) as CacheEntry<T> | undefined;\n\n  if (entry?.version === version) {\n    return entry.value as T;\n  }\n\n  let value = getValue();\n  let newEntry: CacheEntry<T> = { value, version };\n  cache.set(key, newEntry);\n  return value;\n}\n"
  },
  {
    "path": "packages/react-router-dev/vite/cloudflare-dev-proxy.ts",
    "content": "import { createRequestHandler } from \"react-router\";\nimport {\n  type AppLoadContext,\n  type ServerBuild,\n  type UNSAFE_MiddlewareEnabled,\n  type RouterContextProvider,\n} from \"react-router\";\nimport { type Plugin } from \"vite\";\nimport { type GetPlatformProxyOptions, type PlatformProxy } from \"wrangler\";\n\nimport { fromNodeRequest } from \"./node-adapter\";\nimport { preloadVite } from \"./vite\";\nimport { type ResolvedReactRouterConfig, loadConfig } from \"../config/config\";\n\nlet serverBuildId = \"virtual:react-router/server-build\";\n\ntype MaybePromise<T> = T | Promise<T>;\n\ntype CfProperties = Record<string, unknown>;\n\ntype LoadContext<Env, Cf extends CfProperties> = {\n  cloudflare: Omit<PlatformProxy<Env, Cf>, \"dispose\">;\n};\n\ntype GetLoadContext<Env, Cf extends CfProperties> = (args: {\n  request: Request;\n  context: LoadContext<Env, Cf>;\n}) => UNSAFE_MiddlewareEnabled extends true\n  ? MaybePromise<RouterContextProvider>\n  : MaybePromise<AppLoadContext>;\n\nfunction importWrangler() {\n  try {\n    return import(\"wrangler\");\n  } catch (_) {\n    throw Error(\"Could not import `wrangler`. Do you have it installed?\");\n  }\n}\n\nconst PLUGIN_NAME = \"react-router-cloudflare-vite-dev-proxy\";\n\n/**\n * Vite plugin that provides [Node proxies to local workerd\n * bindings](https://developers.cloudflare.com/workers/wrangler/api/#getplatformproxy)\n * to `context.cloudflare` in your server loaders and server actions during\n * development.\n */\nexport const cloudflareDevProxyVitePlugin = <Env, Cf extends CfProperties>(\n  options: {\n    getLoadContext?: GetLoadContext<Env, Cf>;\n  } & GetPlatformProxyOptions = {},\n): Plugin => {\n  let { getLoadContext, ...restOptions } = options;\n  const workerdConditions = [\"workerd\", \"worker\"];\n\n  let future: ResolvedReactRouterConfig[\"future\"];\n\n  return {\n    name: PLUGIN_NAME,\n    config: async (config, configEnv) => {\n      await preloadVite();\n      // This is a compatibility layer for Vite 5. Default conditions were\n      // automatically added to any custom conditions in Vite 5, but Vite 6\n      // removed this behavior. Instead, the default conditions are overridden\n      // by any custom conditions. If we wish to retain the default\n      // conditions, we need to manually merge them using the provided default\n      // conditions arrays exported from Vite. In Vite 5, these default\n      // conditions arrays do not exist.\n      // https://vite.dev/guide/migration.html#default-value-for-resolve-conditions\n      //\n      // In addition to that, these are external conditions (do not confuse them\n      // with server conditions) and there is no helpful export with the default\n      // external conditions (see https://github.com/vitejs/vite/pull/20279 for\n      // more details). So, for now, we are hardcording the default here.\n      const externalConditions: string[] = [\"node\"];\n\n      let configResult = await loadConfig({\n        rootDirectory: config.root ?? process.cwd(),\n        mode: configEnv.mode,\n      });\n\n      if (!configResult.ok) {\n        throw new Error(configResult.error);\n      }\n\n      future = configResult.value.future;\n\n      return {\n        ssr: {\n          resolve: {\n            externalConditions: [...workerdConditions, ...externalConditions],\n          },\n        },\n      };\n    },\n    configEnvironment: async (name, options) => {\n      if (!future.v8_viteEnvironmentApi) {\n        return;\n      }\n\n      if (name !== \"client\") {\n        options.resolve = options.resolve ?? {};\n        options.resolve.externalConditions = [\n          ...workerdConditions,\n          ...(options.resolve?.externalConditions ?? []),\n        ];\n      }\n    },\n    configResolved: (viteConfig) => {\n      let pluginIndex = (name: string) =>\n        viteConfig.plugins.findIndex((plugin) => plugin.name === name);\n      let reactRouterPluginIndex = pluginIndex(\"react-router\");\n      if (\n        reactRouterPluginIndex >= 0 &&\n        reactRouterPluginIndex < pluginIndex(PLUGIN_NAME)\n      ) {\n        throw new Error(\n          `The \"${PLUGIN_NAME}\" plugin should be placed before the React Router plugin in your Vite config file`,\n        );\n      }\n    },\n    configureServer: async (viteDevServer) => {\n      // Async import here to allow ESM only module on Node 20.18.\n      // TODO(v8): Can move to a normal import when Node 20 support\n      const { sendResponse } = await import(\"@remix-run/node-fetch-server\");\n      let context: Awaited<ReturnType<typeof getContext>>;\n      let getContext = async () => {\n        let { getPlatformProxy } = await importWrangler();\n        // Do not include `dispose` in Cloudflare context\n        let { dispose, ...cloudflare } = await getPlatformProxy<Env, Cf>(\n          restOptions,\n        );\n        return { cloudflare };\n      };\n      return () => {\n        if (!viteDevServer.config.server.middlewareMode) {\n          viteDevServer.middlewares.use(async (nodeReq, nodeRes, next) => {\n            try {\n              let build = (await viteDevServer.ssrLoadModule(\n                serverBuildId,\n              )) as ServerBuild;\n\n              let handler = createRequestHandler(build, \"development\");\n              let req = await fromNodeRequest(nodeReq, nodeRes);\n              context ??= await getContext();\n              let loadContext = getLoadContext\n                ? await getLoadContext({ request: req, context })\n                : context;\n              let res = await handler(req, loadContext);\n              await sendResponse(nodeRes, res);\n            } catch (error) {\n              next(error);\n            }\n          });\n        }\n      };\n    },\n  };\n};\n"
  },
  {
    "path": "packages/react-router-dev/vite/cloudflare.ts",
    "content": "export { cloudflareDevProxyVitePlugin as cloudflareDevProxy } from \"./cloudflare-dev-proxy\";\n"
  },
  {
    "path": "packages/react-router-dev/vite/combine-urls-test.ts",
    "content": "import { combineURLs } from \"./combine-urls\";\n\ndescribe(\"combineURLs\", () => {\n  test(\"URLs without slashes\", () => {\n    expect(combineURLs(\"http://example.com\", \"path\")).toBe(\n      \"http://example.com/path\",\n    );\n  });\n\n  test(\"URLs with slashes\", () => {\n    expect(combineURLs(\"http://example.com/\", \"/path\")).toBe(\n      \"http://example.com/path\",\n    );\n  });\n\n  test(\"base URL with trailing slash and relative URL with no leading slash\", () => {\n    expect(combineURLs(\"http://example.com/\", \"path\")).toBe(\n      \"http://example.com/path\",\n    );\n  });\n\n  test(\"base URL with no trailing slash and relative URL with leading slash\", () => {\n    expect(combineURLs(\"http://example.com\", \"/path\")).toBe(\n      \"http://example.com/path\",\n    );\n  });\n\n  test(\"multiple trailing slashes on base URL and leading slashes on relative URL\", () => {\n    expect(combineURLs(\"http://example.com///\", \"///path\")).toBe(\n      \"http://example.com/path\",\n    );\n  });\n\n  test(\"URLs with multiple slashes\", () => {\n    expect(combineURLs(\"http://example.com///\", \"///path\")).toBe(\n      \"http://example.com/path\",\n    );\n  });\n\n  test(\"both URLs with nested paths\", () => {\n    expect(combineURLs(\"http://example.com/dir/\", \"/subdir/file\")).toBe(\n      \"http://example.com/dir/subdir/file\",\n    );\n  });\n\n  test(\"empty relative URL\", () => {\n    expect(combineURLs(\"http://example.com/\", \"\")).toBe(\"http://example.com/\");\n  });\n\n  test(\"empty base URL\", () => {\n    expect(combineURLs(\"\", \"path\")).toBe(\"/path\");\n  });\n\n  test(\"both URLs empty\", () => {\n    expect(combineURLs(\"\", \"\")).toBe(\"\");\n  });\n});\n"
  },
  {
    "path": "packages/react-router-dev/vite/combine-urls.ts",
    "content": "// Borrowed from axios: https://github.com/axios/axios/blob/0e4f9fa29077ebee4499facea6be1492b42e8a26/lib/helpers/combineURLs.js#L11-L15\nexport function combineURLs(baseURL: string, relativeURL: string) {\n  return relativeURL\n    ? baseURL.replace(/\\/+$/, \"\") + \"/\" + relativeURL.replace(/^\\/+/, \"\")\n    : baseURL;\n}\n"
  },
  {
    "path": "packages/react-router-dev/vite/dev.ts",
    "content": "import type * as Vite from \"vite\";\nimport colors from \"picocolors\";\n\nimport { preloadVite, getVite } from \"./vite\";\nimport * as profiler from \"./profiler\";\n\nexport interface ViteDevOptions {\n  clearScreen?: boolean;\n  config?: string;\n  cors?: boolean;\n  force?: boolean;\n  host?: boolean | string;\n  logLevel?: Vite.LogLevel;\n  mode?: string;\n  open?: boolean | string;\n  port?: number;\n  strictPort?: boolean;\n  profile?: boolean;\n}\n\nexport async function dev(\n  root: string,\n  {\n    clearScreen,\n    config: configFile,\n    cors,\n    force,\n    host,\n    logLevel,\n    mode,\n    open,\n    port,\n    strictPort,\n  }: ViteDevOptions,\n) {\n  // Ensure Vite's ESM build is preloaded at the start of the process\n  // so it can be accessed synchronously via `getVite`\n  await preloadVite();\n  let vite = getVite();\n\n  let server = await vite.createServer({\n    root,\n    mode,\n    configFile,\n    server: { open, cors, host, port, strictPort },\n    optimizeDeps: { force },\n    clearScreen,\n    logLevel,\n  });\n\n  if (\n    !server.config.plugins.find(\n      (plugin) =>\n        plugin.name === \"react-router\" || plugin.name === \"react-router/rsc\",\n    )\n  ) {\n    console.error(\n      colors.red(\"React Router Vite plugin not found in Vite config\"),\n    );\n    process.exit(1);\n  }\n\n  await server.listen();\n  server.printUrls();\n\n  let customShortcuts: Vite.CLIShortcut<typeof server>[] = [\n    {\n      key: \"p\",\n      description: \"start/stop the profiler\",\n      async action(server) {\n        if (profiler.getSession()) {\n          await profiler.stop(server.config.logger.info);\n        } else {\n          await profiler.start(() => {\n            server.config.logger.info(\"Profiler started\");\n          });\n        }\n      },\n    },\n  ];\n\n  server.bindCLIShortcuts({ print: true, customShortcuts });\n}\n"
  },
  {
    "path": "packages/react-router-dev/vite/has-dependency.ts",
    "content": "export function hasDependency({\n  name,\n  rootDirectory,\n}: {\n  name: string;\n  rootDirectory: string;\n}) {\n  try {\n    return Boolean(require.resolve(name, { paths: [rootDirectory] }));\n  } catch (err) {\n    return false;\n  }\n}\n"
  },
  {
    "path": "packages/react-router-dev/vite/has-rsc-plugin.ts",
    "content": "import type * as Vite from \"vite\";\n\nexport async function hasReactRouterRscPlugin({\n  root,\n  viteBuildOptions: { config, logLevel, mode },\n}: {\n  root: string;\n  viteBuildOptions: {\n    config?: string;\n    logLevel?: Vite.LogLevel;\n    mode?: string;\n  };\n}): Promise<boolean> {\n  const vite = await import(\"vite\");\n  const viteConfig = await vite.resolveConfig(\n    {\n      configFile: config,\n      logLevel,\n      mode: mode ?? \"production\",\n      root,\n    },\n    \"build\", // command\n    \"production\", // default mode\n    \"production\", // default NODE_ENV\n  );\n  return viteConfig.plugins.some(\n    (plugin) => plugin?.name === \"react-router/rsc\",\n  );\n}\n"
  },
  {
    "path": "packages/react-router-dev/vite/load-dotenv.ts",
    "content": "import type * as Vite from \"vite\";\n\nexport async function loadDotenv({\n  rootDirectory,\n  viteUserConfig,\n  mode,\n}: {\n  rootDirectory: string;\n  viteUserConfig: Vite.UserConfig;\n  mode: string;\n}) {\n  const vite = await import(\"vite\");\n  Object.assign(\n    process.env,\n    vite.loadEnv(\n      mode,\n      viteUserConfig.envDir ?? rootDirectory,\n      // We override the default prefix of \"VITE_\" with a blank string since\n      // we're targeting the server, so we want to load all environment\n      // variables, not just those explicitly marked for the client\n      \"\",\n    ),\n  );\n}\n"
  },
  {
    "path": "packages/react-router-dev/vite/node-adapter.ts",
    "content": "import type { ServerResponse } from \"node:http\";\n\nimport type * as Vite from \"vite\";\nimport invariant from \"../invariant\";\n\nexport type NodeRequestHandler = (\n  req: Vite.Connect.IncomingMessage,\n  res: ServerResponse,\n) => Promise<void>;\n\nexport async function fromNodeRequest(\n  nodeReq: Vite.Connect.IncomingMessage,\n  nodeRes: ServerResponse<Vite.Connect.IncomingMessage>,\n): Promise<Request> {\n  // Use `req.originalUrl` so React Router is aware of the full path\n  invariant(\n    nodeReq.originalUrl,\n    \"Expected `nodeReq.originalUrl` to be defined\",\n  );\n  nodeReq.url = nodeReq.originalUrl;\n\n  // Async import here to allow ESM only module on Node 20.18.\n  // TODO(v8): Can move to a normal import when Node 20 support\n  const { createRequest } = await import(\"@remix-run/node-fetch-server\");\n  return createRequest(nodeReq, nodeRes);\n}\n"
  },
  {
    "path": "packages/react-router-dev/vite/optimize-deps-entries.ts",
    "content": "import { escapePath as escapePathAsGlob } from \"tinyglobby\";\nimport type { ResolvedReactRouterConfig } from \"../config/config\";\nimport { resolveRelativeRouteFilePath } from \"./resolve-relative-route-file-path\";\nimport { getVite } from \"./vite\";\n\nexport function getOptimizeDepsEntries({\n  entryClientFilePath,\n  reactRouterConfig,\n}: {\n  entryClientFilePath: string;\n  reactRouterConfig: ResolvedReactRouterConfig;\n}) {\n  if (!reactRouterConfig.future.unstable_optimizeDeps) {\n    return [];\n  }\n\n  const vite = getVite();\n  const viteMajorVersion = parseInt(vite.version.split(\".\")[0], 10);\n\n  return [\n    vite.normalizePath(entryClientFilePath),\n    ...Object.values(reactRouterConfig.routes).map((route) =>\n      resolveRelativeRouteFilePath(route, reactRouterConfig),\n    ),\n  ].map((entry) =>\n    // In Vite 7, the `optimizeDeps.entries` option only accepts glob patterns.\n    // In prior versions, absolute file paths were treated differently.\n    viteMajorVersion >= 7 ? escapePathAsGlob(entry) : entry,\n  );\n}\n"
  },
  {
    "path": "packages/react-router-dev/vite/plugin.ts",
    "content": "// We can only import types from Vite at the top level since we're in a CJS\n// context but want to use Vite's ESM build since Vite 7+ is ESM only\nimport type * as Vite from \"vite\";\nimport { type BinaryLike, createHash } from \"node:crypto\";\nimport { existsSync, readFileSync, readdirSync, rmSync } from \"node:fs\";\nimport {\n  cp,\n  mkdir,\n  readdir,\n  readFile,\n  rename,\n  rm,\n  writeFile,\n} from \"node:fs/promises\";\nimport * as path from \"node:path\";\nimport * as url from \"node:url\";\nimport * as babel from \"@babel/core\";\nimport {\n  unstable_setDevServerHooks as setDevServerHooks,\n  createRequestHandler,\n  matchRoutes,\n} from \"react-router\";\nimport type {\n  RequestHandler,\n  ServerBuild,\n  DataRouteObject,\n  UNSAFE_MiddlewareEnabled as MiddlewareEnabled,\n  RouterContextProvider,\n} from \"react-router\";\nimport {\n  init as initEsModuleLexer,\n  parse as esModuleLexer,\n} from \"es-module-lexer\";\nimport pick from \"lodash/pick\";\nimport jsesc from \"jsesc\";\nimport colors from \"picocolors\";\nimport kebabCase from \"lodash/kebabCase\";\n\nimport * as Typegen from \"../typegen\";\nimport type { RouteManifestEntry, RouteManifest } from \"../config/routes\";\nimport type {\n  ManifestRoute,\n  Manifest as ReactRouterManifest,\n} from \"../manifest\";\nimport invariant from \"../invariant\";\nimport type { Cache } from \"./cache\";\nimport { generate, parse } from \"./babel\";\nimport type { NodeRequestHandler } from \"./node-adapter\";\nimport { fromNodeRequest } from \"./node-adapter\";\nimport {\n  getCssStringFromViteDevModuleCode,\n  getStylesForPathname,\n  isCssModulesFile,\n} from \"./styles\";\nimport * as VirtualModule from \"./virtual-module\";\nimport { resolveFileUrl } from \"./resolve-file-url\";\nimport { resolveRelativeRouteFilePath } from \"./resolve-relative-route-file-path\";\nimport { combineURLs } from \"./combine-urls\";\nimport { removeExports } from \"./remove-exports\";\nimport { ssrExternals } from \"./ssr-externals\";\nimport { hasDependency } from \"./has-dependency\";\nimport {\n  type RouteChunkName,\n  type RouteChunkExportName,\n  routeChunkNames,\n  routeChunkExportNames,\n  detectRouteChunks,\n  getRouteChunkCode,\n  isRouteChunkModuleId,\n  getRouteChunkModuleId,\n  getRouteChunkNameFromModuleId,\n} from \"./route-chunks\";\nimport { preloadVite, getVite } from \"./vite\";\nimport {\n  type ResolvedReactRouterConfig,\n  type BuildManifest,\n  type ConfigLoader,\n  createConfigLoader,\n  resolveEntryFiles,\n  configRouteToBranchRoute,\n  type PrerenderPaths,\n} from \"../config/config\";\nimport { getOptimizeDepsEntries } from \"./optimize-deps-entries\";\nimport { decorateComponentExportsWithProps } from \"./with-props\";\nimport { loadDotenv } from \"./load-dotenv\";\nimport { validatePluginOrder } from \"./plugins/validate-plugin-order\";\nimport { warnOnClientSourceMaps } from \"./plugins/warn-on-client-source-maps\";\nimport type { PrerenderRequest } from \"./plugins/prerender\";\nimport { prerender } from \"./plugins/prerender\";\n\nexport type LoadCssContents = (\n  viteDevServer: Vite.ViteDevServer,\n  mod: Vite.ModuleNode,\n) => Promise<string>;\n\nexport async function resolveViteConfig({\n  configFile,\n  mode,\n  root,\n  plugins,\n}: {\n  configFile?: string;\n  mode?: string;\n  plugins?: Vite.Plugin[];\n  root: string;\n}) {\n  let vite = getVite();\n\n  let viteConfig = await vite.resolveConfig(\n    { mode, configFile, root, plugins },\n    \"build\", // command\n    \"production\", // default mode\n    \"production\", // default NODE_ENV\n  );\n\n  if (typeof viteConfig.build.manifest === \"string\") {\n    throw new Error(\"Custom Vite manifest paths are not supported\");\n  }\n\n  return viteConfig;\n}\n\nexport function extractPluginContext(\n  viteConfig: Vite.ResolvedConfig | Vite.UserConfig,\n) {\n  return viteConfig[\"__reactRouterPluginContext\" as keyof typeof viteConfig] as\n    | ReactRouterPluginContext\n    | undefined;\n}\n\nconst SERVER_ONLY_ROUTE_EXPORTS = [\"loader\", \"action\", \"middleware\", \"headers\"];\nconst CLIENT_NON_COMPONENT_EXPORTS = [\n  \"clientAction\",\n  \"clientLoader\",\n  \"clientMiddleware\",\n  \"handle\",\n  \"meta\",\n  \"links\",\n  \"shouldRevalidate\",\n];\nconst CLIENT_ROUTE_EXPORTS = [\n  ...CLIENT_NON_COMPONENT_EXPORTS,\n  \"default\",\n  \"ErrorBoundary\",\n  \"HydrateFallback\",\n  \"Layout\",\n];\n\n/** This is used to manage a build optimization to remove unused route exports\nfrom the client build output. This is important in cases where custom route\nexports are only ever used on the server. Without this optimization, we can't\ntree-shake any unused custom exports because routes are entry points. */\nconst BUILD_CLIENT_ROUTE_QUERY_STRING = \"?__react-router-build-client-route\";\n\nexport type EnvironmentName = \"client\" | SsrEnvironmentName;\n\nconst SSR_BUNDLE_PREFIX = \"ssrBundle_\";\ntype SsrBundleEnvironmentName = `${typeof SSR_BUNDLE_PREFIX}${string}`;\ntype SsrEnvironmentName = \"ssr\" | SsrBundleEnvironmentName;\n\nfunction isSsrBundleEnvironmentName(\n  name: string,\n): name is SsrBundleEnvironmentName {\n  return name.startsWith(SSR_BUNDLE_PREFIX);\n}\n\ntype EnvironmentOptions = Pick<Vite.EnvironmentOptions, \"build\" | \"resolve\">;\n\ntype EnvironmentOptionsResolver = (options: {\n  viteUserConfig: Vite.UserConfig;\n}) => EnvironmentOptions;\n\ntype EnvironmentOptionsResolvers = Partial<\n  Record<EnvironmentName, EnvironmentOptionsResolver>\n>;\n\nexport type EnvironmentBuildContext = {\n  name: EnvironmentName;\n  resolveOptions: EnvironmentOptionsResolver;\n};\n\nfunction getServerEnvironmentEntries<T>(\n  ctx: ReactRouterPluginContext,\n  record: Record<string, T>,\n): [SsrEnvironmentName, T][] {\n  return Object.entries(record).filter(([name]) =>\n    ctx.buildManifest?.serverBundles\n      ? isSsrBundleEnvironmentName(name)\n      : name === \"ssr\",\n  ) as [SsrEnvironmentName, T][];\n}\n\nexport function getServerEnvironmentKeys(\n  ctx: ReactRouterPluginContext,\n  record: Record<string, unknown>,\n): SsrEnvironmentName[] {\n  return getServerEnvironmentEntries(ctx, record).map(([key]) => key);\n}\n\nexport function getServerEnvironmentValues<T>(\n  ctx: ReactRouterPluginContext,\n  record: Record<string, T>,\n): T[] {\n  return getServerEnvironmentEntries(ctx, record).map(([, value]) => value);\n}\n\nconst isRouteEntryModuleId = (id: string): boolean => {\n  return id.endsWith(BUILD_CLIENT_ROUTE_QUERY_STRING);\n};\n\nconst isRouteVirtualModule = (id: string): boolean => {\n  return isRouteEntryModuleId(id) || isRouteChunkModuleId(id);\n};\n\nconst isServerBuildVirtualModuleId = (id: string): boolean => {\n  return id.split(\"?\")[0] === virtual.serverBuild.id;\n};\n\nconst getServerBuildFile = (viteManifest: Vite.Manifest): string => {\n  let serverBuildIds = Object.keys(viteManifest).filter(\n    isServerBuildVirtualModuleId,\n  );\n\n  invariant(\n    serverBuildIds.length <= 1,\n    \"Multiple server build files found in manifest\",\n  );\n\n  invariant(\n    serverBuildIds.length === 1,\n    \"Server build file not found in manifest\",\n  );\n\n  return viteManifest[serverBuildIds[0]].file;\n};\n\nexport type ServerBundleBuildConfig = {\n  routes: RouteManifest;\n  serverBundleId: string;\n};\n\ntype ResolvedEnvironmentBuildContext = {\n  name: EnvironmentName;\n  options: EnvironmentOptions;\n};\n\ntype ReactRouterPluginContext = {\n  environmentBuildContext: ResolvedEnvironmentBuildContext | null;\n  buildManifest: BuildManifest | null;\n  rootDirectory: string;\n  entryClientFilePath: string;\n  entryServerFilePath: string;\n  publicPath: string;\n  reactRouterConfig: ResolvedReactRouterConfig;\n  viteManifestEnabled: boolean;\n  reactRouterManifest: ReactRouterManifest | null;\n  prerenderPaths: Set<string> | null;\n};\n\nlet virtualHmrRuntime = VirtualModule.create(\"hmr-runtime\");\nlet virtualInjectHmrRuntime = VirtualModule.create(\"inject-hmr-runtime\");\n\nconst normalizeRelativeFilePath = (\n  file: string,\n  reactRouterConfig: ResolvedReactRouterConfig,\n) => {\n  let vite = getVite();\n  let fullPath = path.resolve(reactRouterConfig.appDirectory, file);\n  let relativePath = path.relative(reactRouterConfig.appDirectory, fullPath);\n\n  return vite.normalizePath(relativePath).split(\"?\")[0];\n};\n\nlet virtual = {\n  serverBuild: VirtualModule.create(\"server-build\"),\n  serverManifest: VirtualModule.create(\"server-manifest\"),\n  browserManifest: VirtualModule.create(\"browser-manifest\"),\n};\n\nlet invalidateVirtualModules = (viteDevServer: Vite.ViteDevServer) => {\n  Object.values(virtual).forEach((vmod) => {\n    let mod = viteDevServer.moduleGraph.getModuleById(vmod.resolvedId);\n    if (mod) {\n      viteDevServer.moduleGraph.invalidateModule(mod);\n    }\n  });\n};\n\nconst getHash = (source: BinaryLike, maxLength?: number): string => {\n  let hash = createHash(\"sha256\").update(source).digest(\"hex\");\n  return typeof maxLength === \"number\" ? hash.slice(0, maxLength) : hash;\n};\n\nconst resolveChunk = (\n  ctx: ReactRouterPluginContext,\n  viteManifest: Vite.Manifest,\n  absoluteFilePath: string,\n) => {\n  let vite = getVite();\n  let rootRelativeFilePath = vite.normalizePath(\n    path.relative(ctx.rootDirectory, absoluteFilePath),\n  );\n  let entryChunk = viteManifest[rootRelativeFilePath];\n\n  if (!entryChunk) {\n    return undefined;\n  }\n\n  return entryChunk;\n};\n\nconst getPublicModulePathForEntry = (\n  ctx: ReactRouterPluginContext,\n  viteManifest: Vite.Manifest,\n  entryFilePath: string,\n): string | undefined => {\n  let entryChunk = resolveChunk(ctx, viteManifest, entryFilePath);\n  return entryChunk ? `${ctx.publicPath}${entryChunk.file}` : undefined;\n};\n\nconst getCssCodeSplitDisabledFile = (\n  ctx: ReactRouterPluginContext,\n  viteConfig: Vite.ResolvedConfig,\n  viteManifest: Vite.Manifest,\n) => {\n  if (viteConfig.build.cssCodeSplit) {\n    return null;\n  }\n\n  let cssFile = viteManifest[\"style.css\"]?.file;\n  invariant(\n    cssFile,\n    \"Expected `style.css` to be present in Vite manifest when `build.cssCodeSplit` is disabled\",\n  );\n\n  return `${ctx.publicPath}${cssFile}`;\n};\n\nconst getClientEntryChunk = (\n  ctx: ReactRouterPluginContext,\n  viteManifest: Vite.Manifest,\n) => {\n  let filePath = ctx.entryClientFilePath;\n  let chunk = resolveChunk(ctx, viteManifest, filePath);\n  invariant(chunk, `Chunk not found: ${filePath}`);\n  return chunk;\n};\n\nconst getReactRouterManifestBuildAssets = (\n  ctx: ReactRouterPluginContext,\n  viteConfig: Vite.ResolvedConfig,\n  viteManifest: Vite.Manifest,\n  allDynamicCssFiles: Set<string>,\n  entryFilePath: string,\n  route: RouteManifestEntry | null,\n): ReactRouterManifest[\"entry\"] & { css: string[] } => {\n  let entryChunk = resolveChunk(ctx, viteManifest, entryFilePath);\n  invariant(entryChunk, `Chunk not found: ${entryFilePath}`);\n\n  let isRootRoute = Boolean(route && route.parentId === undefined);\n\n  let routeModuleChunks = routeChunkNames\n    .map((routeChunkName) =>\n      resolveChunk(\n        ctx,\n        viteManifest,\n        getRouteChunkModuleId(entryFilePath.split(\"?\")[0], routeChunkName),\n      ),\n    )\n    .filter(isNonNullable);\n\n  let chunks = resolveDependantChunks(\n    viteManifest,\n    [\n      // If this is the root route, we also need to include assets from the\n      // client entry file as this is a common way for consumers to import\n      // global reset styles, etc.\n      isRootRoute ? getClientEntryChunk(ctx, viteManifest) : null,\n      entryChunk,\n      routeModuleChunks,\n    ]\n      .flat(1)\n      .filter(isNonNullable),\n  );\n\n  return {\n    module: `${ctx.publicPath}${entryChunk.file}`,\n    imports:\n      dedupe(chunks.flatMap((e) => e.imports ?? [])).map((imported) => {\n        return `${ctx.publicPath}${viteManifest[imported].file}`;\n      }) ?? [],\n    css: dedupe(\n      [\n        // If CSS code splitting is disabled, Vite includes a singular 'style.css' asset\n        // in the manifest that isn't tied to any route file. If we want to render these\n        // styles correctly, we need to include them in the root route.\n        isRootRoute\n          ? getCssCodeSplitDisabledFile(ctx, viteConfig, viteManifest)\n          : null,\n        chunks\n          .flatMap((e) => e.css ?? [])\n          .map((href) => {\n            let publicHref = `${ctx.publicPath}${href}`;\n            // If this CSS file is also dynamically imported anywhere in the\n            // application, we append a hash to the href so Vite ignores it when\n            // managing dynamic CSS injection. If we don't do this, Vite's\n            // dynamic import logic might hold off on inserting a new `link`\n            // element because it's already in the page, only for React Router\n            // to remove it when navigating to a new route, resulting in missing\n            // styles. By appending a hash, Vite doesn't detect that the CSS is\n            // already in the page and always manages its own `link` element.\n            // This means that Vite's CSS stays in the page even if the\n            // route-level CSS is removed from the document. We use a hash here\n            // because it's a unique `href` value but isn't a unique network\n            // request and only adds a single character.\n            return allDynamicCssFiles.has(href) ? `${publicHref}#` : publicHref;\n          }),\n      ]\n        .flat(1)\n        .filter(isNonNullable),\n    ),\n  };\n};\n\nfunction resolveDependantChunks(\n  viteManifest: Vite.Manifest,\n  entryChunks: Vite.ManifestChunk[],\n): Vite.ManifestChunk[] {\n  let chunks = new Set<Vite.ManifestChunk>();\n\n  function walk(chunk: Vite.ManifestChunk) {\n    if (chunks.has(chunk)) {\n      return;\n    }\n\n    chunks.add(chunk);\n\n    if (chunk.imports) {\n      for (let importKey of chunk.imports) {\n        walk(viteManifest[importKey]);\n      }\n    }\n  }\n\n  for (let entryChunk of entryChunks) {\n    walk(entryChunk);\n  }\n\n  return Array.from(chunks);\n}\n\nfunction getAllDynamicCssFiles(\n  ctx: ReactRouterPluginContext,\n  viteManifest: Vite.Manifest,\n): Set<string> {\n  let allDynamicCssFiles = new Set<string>();\n\n  for (let route of Object.values(ctx.reactRouterConfig.routes)) {\n    let routeFile = path.join(ctx.reactRouterConfig.appDirectory, route.file);\n    let entryChunk = resolveChunk(\n      ctx,\n      viteManifest,\n      `${routeFile}${BUILD_CLIENT_ROUTE_QUERY_STRING}`,\n    );\n\n    if (entryChunk) {\n      let visitedChunks = new Set<Vite.ManifestChunk>();\n\n      function walk(\n        chunk: Vite.ManifestChunk,\n        isDynamicImportContext: boolean,\n      ) {\n        if (visitedChunks.has(chunk)) {\n          return;\n        }\n\n        visitedChunks.add(chunk);\n\n        if (isDynamicImportContext && chunk.css) {\n          for (let cssFile of chunk.css) {\n            allDynamicCssFiles.add(cssFile);\n          }\n        }\n\n        if (chunk.dynamicImports) {\n          for (let dynamicImportKey of chunk.dynamicImports) {\n            walk(viteManifest[dynamicImportKey], true);\n          }\n        }\n\n        if (chunk.imports) {\n          for (let importKey of chunk.imports) {\n            walk(viteManifest[importKey], isDynamicImportContext);\n          }\n        }\n      }\n\n      walk(entryChunk, false);\n    }\n  }\n\n  return allDynamicCssFiles;\n}\n\nfunction dedupe<T>(array: T[]): T[] {\n  return [...new Set(array)];\n}\n\nconst writeFileSafe = async (file: string, contents: string): Promise<void> => {\n  await mkdir(path.dirname(file), { recursive: true });\n  await writeFile(file, contents);\n};\n\nconst getExportNames = (code: string): string[] => {\n  let [, exportSpecifiers] = esModuleLexer(code);\n  return exportSpecifiers.map(({ n: name }) => name);\n};\n\nconst getRouteManifestModuleExports = async (\n  viteChildCompiler: Vite.ViteDevServer | null,\n  ctx: ReactRouterPluginContext,\n): Promise<Record<string, string[]>> => {\n  let entries = await Promise.all(\n    Object.entries(ctx.reactRouterConfig.routes).map(async ([key, route]) => {\n      let sourceExports = await getRouteModuleExports(\n        viteChildCompiler,\n        ctx,\n        route.file,\n      );\n      return [key, sourceExports] as const;\n    }),\n  );\n  return Object.fromEntries(entries);\n};\n\nconst compileRouteFile = async (\n  viteChildCompiler: Vite.ViteDevServer | null,\n  ctx: ReactRouterPluginContext,\n  routeFile: string,\n  readRouteFile?: () => string | Promise<string>,\n): Promise<string> => {\n  if (!viteChildCompiler) {\n    throw new Error(\"Vite child compiler not found\");\n  }\n\n  // We transform the route module code with the Vite child compiler so that we\n  // can parse the exports from non-JS files like MDX. This ensures that we can\n  // understand the exports from anything that Vite can compile to JS, not just\n  // the route file formats that the Remix compiler historically supported.\n\n  let ssr = true;\n  let { pluginContainer, moduleGraph } = viteChildCompiler;\n\n  let routePath = path.resolve(ctx.reactRouterConfig.appDirectory, routeFile);\n  let url = resolveFileUrl(ctx, routePath);\n\n  let resolveId = async () => {\n    let result = await pluginContainer.resolveId(url, undefined, { ssr });\n    if (!result) throw new Error(`Could not resolve module ID for ${url}`);\n    return result.id;\n  };\n\n  let [id, code] = await Promise.all([\n    resolveId(),\n    readRouteFile?.() ?? readFile(routePath, \"utf-8\"),\n    // pluginContainer.transform(...) fails if we don't do this first:\n    moduleGraph.ensureEntryFromUrl(url, ssr),\n  ]);\n\n  let transformed = await pluginContainer.transform(code, id, { ssr });\n  return transformed.code;\n};\n\nconst getRouteModuleExports = async (\n  viteChildCompiler: Vite.ViteDevServer | null,\n  ctx: ReactRouterPluginContext,\n  routeFile: string,\n  readRouteFile?: () => string | Promise<string>,\n): Promise<string[]> => {\n  if (!viteChildCompiler) {\n    throw new Error(\"Vite child compiler not found\");\n  }\n\n  let code = await compileRouteFile(\n    viteChildCompiler,\n    ctx,\n    routeFile,\n    readRouteFile,\n  );\n\n  return getExportNames(code);\n};\n\nconst resolveEnvironmentBuildContext = ({\n  viteCommand,\n  viteUserConfig,\n}: {\n  viteCommand: Vite.ResolvedConfig[\"command\"];\n  viteUserConfig: Vite.UserConfig;\n}): ResolvedEnvironmentBuildContext | null => {\n  if (\n    !(\"__reactRouterEnvironmentBuildContext\" in viteUserConfig) ||\n    !viteUserConfig.__reactRouterEnvironmentBuildContext\n  ) {\n    return null;\n  }\n\n  let buildContext =\n    viteUserConfig.__reactRouterEnvironmentBuildContext as EnvironmentBuildContext;\n\n  let resolvedBuildContext: ResolvedEnvironmentBuildContext = {\n    name: buildContext.name,\n    options: buildContext.resolveOptions({ viteUserConfig }),\n  };\n\n  return resolvedBuildContext;\n};\n\nlet getServerBuildDirectory = (\n  reactRouterConfig: ResolvedReactRouterConfig,\n  { serverBundleId }: { serverBundleId?: string } = {},\n) =>\n  path.join(\n    reactRouterConfig.buildDirectory,\n    \"server\",\n    ...(serverBundleId ? [serverBundleId] : []),\n  );\n\nlet getClientBuildDirectory = (reactRouterConfig: ResolvedReactRouterConfig) =>\n  path.join(reactRouterConfig.buildDirectory, \"client\");\n\nlet getServerBundleRouteIds = (\n  vitePluginContext: Vite.Rollup.PluginContext,\n  ctx: ReactRouterPluginContext,\n): string[] | undefined => {\n  if (!ctx.buildManifest) {\n    return undefined;\n  }\n\n  let environmentName = ctx.reactRouterConfig.future.v8_viteEnvironmentApi\n    ? vitePluginContext.environment.name\n    : ctx.environmentBuildContext?.name;\n\n  if (!environmentName || !isSsrBundleEnvironmentName(environmentName)) {\n    return undefined;\n  }\n\n  let serverBundleId = environmentName.replace(SSR_BUNDLE_PREFIX, \"\");\n  let routesByServerBundleId = getRoutesByServerBundleId(ctx.buildManifest);\n  let serverBundleRoutes = routesByServerBundleId[serverBundleId];\n\n  invariant(\n    serverBundleRoutes,\n    `Routes not found for server bundle \"${serverBundleId}\"`,\n  );\n\n  return Object.keys(serverBundleRoutes);\n};\n\nconst injectQuery = (url: string, query: string) =>\n  url.includes(\"?\") ? url.replace(\"?\", `?${query}&`) : `${url}?${query}`;\n\nlet defaultEntriesDir = path.resolve(\n  path.dirname(require.resolve(\"@react-router/dev/package.json\")),\n  \"dist\",\n  \"config\",\n  \"defaults\",\n);\nlet defaultEntries = readdirSync(defaultEntriesDir).map((filename) =>\n  path.join(defaultEntriesDir, filename),\n);\ninvariant(defaultEntries.length > 0, \"No default entries found\");\n\ntype MaybePromise<T> = T | Promise<T>;\n\nlet reactRouterDevLoadContext: (\n  request: Request,\n) => MaybePromise<\n  MiddlewareEnabled extends true\n    ? MaybePromise<RouterContextProvider | undefined>\n    : MaybePromise<Record<string, unknown> | undefined>\n> = () => undefined;\n\nexport let setReactRouterDevLoadContext = (\n  loadContext: (request: Request) => MaybePromise<Record<string, unknown>>,\n) => {\n  reactRouterDevLoadContext = loadContext;\n};\n\ntype ReactRouterVitePlugin = () => Vite.Plugin[];\n/**\n * React Router [Vite plugin.](https://vitejs.dev/guide/using-plugins.html)\n */\nexport const reactRouterVitePlugin: ReactRouterVitePlugin = () => {\n  let rootDirectory: string;\n  let viteCommand: Vite.ResolvedConfig[\"command\"];\n  let viteUserConfig: Vite.UserConfig;\n  let viteConfigEnv: Vite.ConfigEnv;\n  let viteConfig: Vite.ResolvedConfig | undefined;\n  let cssModulesManifest: Record<string, string> = {};\n  let viteChildCompiler: Vite.ViteDevServer | null = null;\n  let cache: Cache = new Map();\n\n  let reactRouterConfigLoader: ConfigLoader;\n  let typegenWatcherPromise: Promise<Typegen.Watcher> | undefined;\n  let logger: Vite.Logger;\n  let firstLoad = true;\n\n  // This is initialized by `updatePluginContext` during Vite's `config`\n  // hook, so most of the code can assume this defined without null check.\n  // During dev, `updatePluginContext` is called again on every config file\n  // change or route file addition/removal.\n  let ctx: ReactRouterPluginContext;\n\n  /** Mutates `ctx` as a side effect */\n  let updatePluginContext = async (): Promise<void> => {\n    let reactRouterConfig: ResolvedReactRouterConfig;\n    let reactRouterConfigResult = await reactRouterConfigLoader.getConfig();\n\n    if (reactRouterConfigResult.ok) {\n      reactRouterConfig = reactRouterConfigResult.value;\n    } else {\n      logger.error(reactRouterConfigResult.error);\n      if (firstLoad) {\n        process.exit(1);\n      }\n      return;\n    }\n\n    // This `injectedPluginContext` logic is so we can support injecting an\n    // already-resolved plugin context into the build in case we want to re-use\n    // any resolved values. Currently, this is used so that we can re-use the\n    // `buildManifest` object across multiple builds without having to\n    // re-execute the `serverBundles` function.\n    let injectedPluginContext =\n      !reactRouterConfig.future.v8_viteEnvironmentApi && viteCommand === \"build\"\n        ? extractPluginContext(viteUserConfig)\n        : undefined;\n\n    let { entryClientFilePath, entryServerFilePath } = await resolveEntryFiles({\n      rootDirectory,\n      reactRouterConfig,\n    });\n\n    let publicPath = viteUserConfig.base ?? \"/\";\n\n    if (\n      reactRouterConfig.basename !== \"/\" &&\n      viteCommand === \"serve\" &&\n      !viteUserConfig.server?.middlewareMode &&\n      !reactRouterConfig.basename.startsWith(publicPath)\n    ) {\n      logger.error(\n        colors.red(\n          \"When using the React Router `basename` and the Vite `base` config, \" +\n            \"the `basename` config must begin with `base` for the default \" +\n            \"Vite dev server.\",\n        ),\n      );\n      process.exit(1);\n    }\n\n    let viteManifestEnabled = viteUserConfig.build?.manifest === true;\n\n    let buildManifest =\n      viteCommand === \"build\"\n        ? (injectedPluginContext?.buildManifest ??\n          (await getBuildManifest({ reactRouterConfig, rootDirectory })))\n        : null;\n\n    let environmentBuildContext: ResolvedEnvironmentBuildContext | null =\n      viteCommand === \"build\"\n        ? resolveEnvironmentBuildContext({ viteCommand, viteUserConfig })\n        : null;\n\n    firstLoad = false;\n\n    ctx = {\n      environmentBuildContext,\n      reactRouterManifest: null,\n      prerenderPaths: null,\n      reactRouterConfig,\n      rootDirectory,\n      entryClientFilePath,\n      entryServerFilePath,\n      publicPath,\n      viteManifestEnabled,\n      buildManifest,\n    };\n  };\n\n  let getServerEntry = async ({ routeIds }: { routeIds?: Array<string> }) => {\n    invariant(viteConfig, \"viteconfig required to generate the server entry\");\n\n    let routes = routeIds\n      ? // For server bundle builds, the server build should only import the\n        // routes for this bundle rather than importing all routes\n        pick(ctx.reactRouterConfig.routes, routeIds)\n      : // Otherwise, all routes are imported as usual\n        ctx.reactRouterConfig.routes;\n\n    let prerenderPaths = await getPrerenderPaths(\n      ctx.reactRouterConfig.prerender,\n      ctx.reactRouterConfig.ssr,\n      routes,\n    );\n\n    if (!ctx.prerenderPaths) {\n      ctx.prerenderPaths = new Set<string>();\n    }\n\n    // Accumulate prerender paths from all bundles\n    for (let path of prerenderPaths) {\n      ctx.prerenderPaths.add(path);\n    }\n\n    let isSpaMode = isSpaModeEnabled(ctx.reactRouterConfig);\n\n    return `\n    import * as entryServer from ${JSON.stringify(\n      resolveFileUrl(ctx, ctx.entryServerFilePath),\n    )};\n    ${Object.keys(routes)\n      .map((key, index) => {\n        let route = routes[key]!;\n        if (isSpaMode && key !== \"root\") {\n          // In SPA mode, we only pre-render the root route and its `HydrateFallback`.\n          // Therefore, we can stub all other routes with an empty module as they\n          // (and their deps) may not be compatible with server-side rendering.\n          // This also helps keep the build fast.\n          return `const route${index} = { default: () => null };`;\n        } else {\n          return `import * as route${index} from ${JSON.stringify(\n            resolveFileUrl(\n              ctx,\n              resolveRelativeRouteFilePath(route, ctx.reactRouterConfig),\n            ),\n          )};`;\n        }\n      })\n      .join(\"\\n\")}\n      export { default as assets } from ${JSON.stringify(\n        virtual.serverManifest.id,\n      )};\n      export const assetsBuildDirectory = ${JSON.stringify(\n        path.relative(\n          ctx.rootDirectory,\n          getClientBuildDirectory(ctx.reactRouterConfig),\n        ),\n      )};\n      export const basename = ${JSON.stringify(ctx.reactRouterConfig.basename)};\n      export const future = ${JSON.stringify(ctx.reactRouterConfig.future)};\n      export const ssr = ${ctx.reactRouterConfig.ssr};\n      export const isSpaMode = ${isSpaMode};\n      export const prerender = ${JSON.stringify(prerenderPaths)};\n      export const routeDiscovery = ${JSON.stringify(\n        ctx.reactRouterConfig.routeDiscovery,\n      )};\n      export const publicPath = ${JSON.stringify(ctx.publicPath)};\n      export const entry = { module: entryServer };\n      export const routes = {\n        ${Object.keys(routes)\n          .map((key, index) => {\n            let route = routes[key]!;\n            return `${JSON.stringify(key)}: {\n          id: ${JSON.stringify(route.id)},\n          parentId: ${JSON.stringify(route.parentId)},\n          path: ${JSON.stringify(route.path)},\n          index: ${JSON.stringify(route.index)},\n          caseSensitive: ${JSON.stringify(route.caseSensitive)},\n          module: route${index}\n        }`;\n          })\n          .join(\",\\n  \")}\n      };\n      ${\n        ctx.reactRouterConfig.future.v8_viteEnvironmentApi &&\n        viteCommand === \"serve\"\n          ? `\n              export const unstable_getCriticalCss = ({ pathname }) => {\n                return {\n                  rel: \"stylesheet\",\n                  href: \"${ctx.publicPath}@react-router/critical.css?pathname=\" + pathname,\n                };\n              }\n            `\n          : \"\"\n      }\n      export const allowedActionOrigins = ${JSON.stringify(ctx.reactRouterConfig.allowedActionOrigins)};\n    `;\n  };\n\n  let loadViteManifest = async (directory: string) => {\n    let manifestContents = await readFile(\n      path.resolve(directory, \".vite\", \"manifest.json\"),\n      \"utf-8\",\n    );\n    return JSON.parse(manifestContents) as Vite.Manifest;\n  };\n\n  let getViteManifestAssetPaths = (\n    viteManifest: Vite.Manifest,\n  ): Set<string> => {\n    // Get .css?url imports and CSS entry points\n    let cssUrlPaths = Object.values(viteManifest)\n      .filter((chunk) => chunk.file.endsWith(\".css\"))\n      .map((chunk) => chunk.file);\n\n    // Get bundled CSS files and generic asset types\n    let chunkAssetPaths = Object.values(viteManifest).flatMap(\n      (chunk) => chunk.assets ?? [],\n    );\n\n    return new Set([...cssUrlPaths, ...chunkAssetPaths]);\n  };\n\n  let generateSriManifest = async (ctx: ReactRouterPluginContext) => {\n    let clientBuildDirectory = getClientBuildDirectory(ctx.reactRouterConfig);\n    // walk the client build directory and generate SRI hashes for all .js files\n    let entries = readdirSync(clientBuildDirectory, {\n      withFileTypes: true,\n      recursive: true,\n    });\n    let sriManifest: ReactRouterManifest[\"sri\"] = {};\n    for (const entry of entries) {\n      if (entry.isFile() && entry.name.endsWith(\".js\")) {\n        const entryNormalizedPath =\n          \"parentPath\" in entry && typeof entry.parentPath === \"string\"\n            ? entry.parentPath\n            : entry.path;\n\n        let contents;\n        try {\n          contents = await readFile(\n            path.join(entryNormalizedPath, entry.name),\n            \"utf-8\",\n          );\n        } catch (e) {\n          logger.error(`Failed to read file for SRI generation: ${entry.name}`);\n          throw e;\n        }\n        let hash = createHash(\"sha384\")\n          .update(contents)\n          .digest()\n          .toString(\"base64\");\n        let filepath = getVite().normalizePath(\n          path.relative(\n            clientBuildDirectory,\n            path.join(entryNormalizedPath, entry.name),\n          ),\n        );\n        sriManifest[`${ctx.publicPath}${filepath}`] = `sha384-${hash}`;\n      }\n    }\n    return sriManifest;\n  };\n\n  let generateReactRouterManifestsForBuild = async ({\n    viteConfig,\n    routeIds,\n  }: {\n    viteConfig: Vite.ResolvedConfig;\n    routeIds?: Array<string>;\n  }): Promise<{\n    reactRouterBrowserManifest: ReactRouterManifest;\n    reactRouterServerManifest: ReactRouterManifest;\n  }> => {\n    invariant(viteConfig);\n\n    let viteManifest = await loadViteManifest(\n      getClientBuildDirectory(ctx.reactRouterConfig),\n    );\n\n    let allDynamicCssFiles = getAllDynamicCssFiles(ctx, viteManifest);\n\n    let entry = getReactRouterManifestBuildAssets(\n      ctx,\n      viteConfig,\n      viteManifest,\n      allDynamicCssFiles,\n      ctx.entryClientFilePath,\n      null,\n    );\n\n    let browserRoutes: ReactRouterManifest[\"routes\"] = {};\n    let serverRoutes: ReactRouterManifest[\"routes\"] = {};\n\n    let routeManifestExports = await getRouteManifestModuleExports(\n      viteChildCompiler,\n      ctx,\n    );\n\n    let enforceSplitRouteModules =\n      ctx.reactRouterConfig.future.v8_splitRouteModules === \"enforce\";\n    for (let route of Object.values(ctx.reactRouterConfig.routes)) {\n      let routeFile = path.join(ctx.reactRouterConfig.appDirectory, route.file);\n      let sourceExports = routeManifestExports[route.id];\n      let hasClientAction = sourceExports.includes(\"clientAction\");\n      let hasClientLoader = sourceExports.includes(\"clientLoader\");\n      let hasClientMiddleware = sourceExports.includes(\"clientMiddleware\");\n      let hasHydrateFallback = sourceExports.includes(\"HydrateFallback\");\n\n      let { hasRouteChunkByExportName } = await detectRouteChunksIfEnabled(\n        cache,\n        ctx,\n        routeFile,\n        { routeFile, viteChildCompiler },\n      );\n\n      if (enforceSplitRouteModules) {\n        validateRouteChunks({\n          ctx,\n          id: route.file,\n          valid: {\n            clientAction:\n              !hasClientAction || hasRouteChunkByExportName.clientAction,\n            clientLoader:\n              !hasClientLoader || hasRouteChunkByExportName.clientLoader,\n            clientMiddleware:\n              !hasClientMiddleware ||\n              hasRouteChunkByExportName.clientMiddleware,\n            HydrateFallback:\n              !hasHydrateFallback || hasRouteChunkByExportName.HydrateFallback,\n          },\n        });\n      }\n\n      let routeManifestEntry: ReactRouterManifest[\"routes\"][string] = {\n        id: route.id,\n        parentId: route.parentId,\n        path: route.path,\n        index: route.index,\n        caseSensitive: route.caseSensitive,\n        hasAction: sourceExports.includes(\"action\"),\n        hasLoader: sourceExports.includes(\"loader\"),\n        hasClientAction,\n        hasClientLoader,\n        hasClientMiddleware,\n        hasDefaultExport: sourceExports.includes(\"default\"),\n        hasErrorBoundary: sourceExports.includes(\"ErrorBoundary\"),\n        ...getReactRouterManifestBuildAssets(\n          ctx,\n          viteConfig,\n          viteManifest,\n          allDynamicCssFiles,\n          `${routeFile}${BUILD_CLIENT_ROUTE_QUERY_STRING}`,\n          route,\n        ),\n        clientActionModule: hasRouteChunkByExportName.clientAction\n          ? getPublicModulePathForEntry(\n              ctx,\n              viteManifest,\n              getRouteChunkModuleId(routeFile, \"clientAction\"),\n            )\n          : undefined,\n        clientLoaderModule: hasRouteChunkByExportName.clientLoader\n          ? getPublicModulePathForEntry(\n              ctx,\n              viteManifest,\n              getRouteChunkModuleId(routeFile, \"clientLoader\"),\n            )\n          : undefined,\n        clientMiddlewareModule: hasRouteChunkByExportName.clientMiddleware\n          ? getPublicModulePathForEntry(\n              ctx,\n              viteManifest,\n              getRouteChunkModuleId(routeFile, \"clientMiddleware\"),\n            )\n          : undefined,\n        hydrateFallbackModule: hasRouteChunkByExportName.HydrateFallback\n          ? getPublicModulePathForEntry(\n              ctx,\n              viteManifest,\n              getRouteChunkModuleId(routeFile, \"HydrateFallback\"),\n            )\n          : undefined,\n      };\n\n      browserRoutes[route.id] = routeManifestEntry;\n\n      if (!routeIds || routeIds.includes(route.id)) {\n        serverRoutes[route.id] = routeManifestEntry;\n      }\n    }\n\n    let fingerprintedValues = { entry, routes: browserRoutes };\n    let version = getHash(JSON.stringify(fingerprintedValues), 8);\n    let manifestPath = path.posix.join(\n      viteConfig.build.assetsDir,\n      `manifest-${version}.js`,\n    );\n    let url = `${ctx.publicPath}${manifestPath}`;\n    let nonFingerprintedValues = { url, version };\n\n    let reactRouterBrowserManifest: ReactRouterManifest = {\n      ...fingerprintedValues,\n      ...nonFingerprintedValues,\n      sri: undefined,\n    };\n\n    // Write the browser manifest to disk as part of the build process\n    await writeFileSafe(\n      path.join(getClientBuildDirectory(ctx.reactRouterConfig), manifestPath),\n      `window.__reactRouterManifest=${JSON.stringify(\n        reactRouterBrowserManifest,\n      )};`,\n    );\n\n    let sri: ReactRouterManifest[\"sri\"] = undefined;\n    if (ctx.reactRouterConfig.future.unstable_subResourceIntegrity) {\n      sri = await generateSriManifest(ctx);\n    }\n\n    // The server manifest is the same as the browser manifest, except for\n    // server bundle builds which only includes routes for the current bundle,\n    // otherwise the server and client have the same routes\n    let reactRouterServerManifest: ReactRouterManifest = {\n      ...reactRouterBrowserManifest,\n      routes: serverRoutes,\n      sri,\n    };\n\n    return {\n      reactRouterBrowserManifest,\n      reactRouterServerManifest,\n    };\n  };\n\n  // In dev, the server and browser manifests are the same\n  let currentReactRouterManifestForDev: ReactRouterManifest | null = null;\n  let getReactRouterManifestForDev = async (): Promise<ReactRouterManifest> => {\n    let routes: ReactRouterManifest[\"routes\"] = {};\n\n    let routeManifestExports = await getRouteManifestModuleExports(\n      viteChildCompiler,\n      ctx,\n    );\n\n    let enforceSplitRouteModules =\n      ctx.reactRouterConfig.future.v8_splitRouteModules === \"enforce\";\n\n    for (let [key, route] of Object.entries(ctx.reactRouterConfig.routes)) {\n      let routeFile = route.file;\n      let sourceExports = routeManifestExports[key];\n      let hasClientAction = sourceExports.includes(\"clientAction\");\n      let hasClientLoader = sourceExports.includes(\"clientLoader\");\n      let hasClientMiddleware = sourceExports.includes(\"clientMiddleware\");\n      let hasHydrateFallback = sourceExports.includes(\"HydrateFallback\");\n      let routeModulePath = combineURLs(\n        ctx.publicPath,\n        `${resolveFileUrl(\n          ctx,\n          resolveRelativeRouteFilePath(route, ctx.reactRouterConfig),\n        )}`,\n      );\n\n      if (enforceSplitRouteModules) {\n        let { hasRouteChunkByExportName } = await detectRouteChunksIfEnabled(\n          cache,\n          ctx,\n          routeFile,\n          { routeFile, viteChildCompiler },\n        );\n\n        validateRouteChunks({\n          ctx,\n          id: route.file,\n          valid: {\n            clientAction:\n              !hasClientAction || hasRouteChunkByExportName.clientAction,\n            clientLoader:\n              !hasClientLoader || hasRouteChunkByExportName.clientLoader,\n            clientMiddleware:\n              !hasClientMiddleware ||\n              hasRouteChunkByExportName.clientMiddleware,\n            HydrateFallback:\n              !hasHydrateFallback || hasRouteChunkByExportName.HydrateFallback,\n          },\n        });\n      }\n\n      routes[key] = {\n        id: route.id,\n        parentId: route.parentId,\n        path: route.path,\n        index: route.index,\n        caseSensitive: route.caseSensitive,\n        module: routeModulePath,\n        // Split route modules are a build-time optimization\n        clientActionModule: undefined,\n        clientLoaderModule: undefined,\n        clientMiddlewareModule: undefined,\n        hydrateFallbackModule: undefined,\n        hasAction: sourceExports.includes(\"action\"),\n        hasLoader: sourceExports.includes(\"loader\"),\n        hasClientAction,\n        hasClientLoader,\n        hasClientMiddleware,\n        hasDefaultExport: sourceExports.includes(\"default\"),\n        hasErrorBoundary: sourceExports.includes(\"ErrorBoundary\"),\n        imports: [],\n      };\n    }\n\n    let sri: ReactRouterManifest[\"sri\"] = undefined;\n\n    let reactRouterManifestForDev = {\n      version: String(Math.random()),\n      url: combineURLs(ctx.publicPath, virtual.browserManifest.url),\n      hmr: {\n        runtime: combineURLs(ctx.publicPath, virtualInjectHmrRuntime.url),\n      },\n      entry: {\n        module: combineURLs(\n          ctx.publicPath,\n          resolveFileUrl(ctx, ctx.entryClientFilePath),\n        ),\n        imports: [],\n      },\n      sri,\n      routes,\n    };\n\n    currentReactRouterManifestForDev = reactRouterManifestForDev;\n\n    return reactRouterManifestForDev;\n  };\n\n  const loadCssContents: LoadCssContents = async (viteDevServer, dep) => {\n    invariant(\n      viteCommand === \"serve\",\n      \"loadCssContents is only available in dev mode\",\n    );\n\n    if (dep.file && isCssModulesFile(dep.file)) {\n      return cssModulesManifest[dep.file];\n    }\n\n    let transformedCssCode = (await viteDevServer.transformRequest(dep.url))\n      ?.code;\n    invariant(\n      transformedCssCode,\n      `Failed to load CSS for ${dep.file ?? dep.url}`,\n    );\n\n    let cssString = getCssStringFromViteDevModuleCode(transformedCssCode);\n    invariant(\n      typeof cssString === \"string\",\n      `Failed to extract CSS for ${dep.file ?? dep.url}`,\n    );\n\n    return cssString;\n  };\n\n  return [\n    {\n      name: \"react-router\",\n      config: async (_viteUserConfig, _viteConfigEnv) => {\n        // Preload Vite's ESM build up-front as soon as we're in an async context\n        await preloadVite();\n\n        // Ensure sync import of Vite works after async preload\n        let vite = getVite();\n\n        viteUserConfig = _viteUserConfig;\n        viteConfigEnv = _viteConfigEnv;\n        viteCommand = viteConfigEnv.command;\n\n        // This is a compatibility layer for Vite 5. Default conditions were\n        // automatically added to any custom conditions in Vite 5, but Vite 6\n        // removed this behavior. Instead, the default conditions are overridden\n        // by any custom conditions. If we wish to retain the default\n        // conditions, we need to manually merge them using the provided default\n        // conditions arrays exported from Vite. In Vite 5, these default\n        // conditions arrays do not exist.\n        // https://vite.dev/guide/migration.html#default-value-for-resolve-conditions\n        let viteClientConditions: string[] = [\n          ...(vite.defaultClientConditions ?? []),\n        ];\n\n        logger = vite.createLogger(viteUserConfig.logLevel, {\n          prefix: \"[react-router]\",\n        });\n\n        rootDirectory =\n          viteUserConfig.root ?? process.env.REACT_ROUTER_ROOT ?? process.cwd();\n\n        let mode = viteConfigEnv.mode;\n\n        if (viteCommand === \"serve\") {\n          typegenWatcherPromise = Typegen.watch(rootDirectory, {\n            mode,\n            rsc: false,\n            // ignore `info` logs from typegen since they are redundant when Vite plugin logs are active\n            logger: vite.createLogger(\"warn\", { prefix: \"[react-router]\" }),\n          });\n        }\n\n        await loadDotenv({\n          rootDirectory,\n          viteUserConfig,\n          mode,\n        });\n\n        reactRouterConfigLoader = await createConfigLoader({\n          rootDirectory,\n          mode,\n          watch: viteCommand === \"serve\",\n        });\n\n        await updatePluginContext();\n\n        let environments = await getEnvironmentsOptions(ctx, viteCommand, {\n          viteUserConfig,\n        });\n\n        let serverEnvironment = getServerEnvironmentValues(\n          ctx,\n          environments,\n        )[0];\n        invariant(serverEnvironment);\n\n        let clientEnvironment = environments.client;\n        invariant(clientEnvironment);\n\n        return {\n          __reactRouterPluginContext: ctx,\n          appType:\n            viteCommand === \"serve\" &&\n            viteConfigEnv.mode === \"production\" &&\n            ctx.reactRouterConfig.ssr === false\n              ? \"spa\"\n              : \"custom\",\n\n          ssr: {\n            external: serverEnvironment.resolve?.external,\n            resolve: serverEnvironment.resolve,\n          },\n          optimizeDeps: {\n            entries: getOptimizeDepsEntries({\n              entryClientFilePath: ctx.entryClientFilePath,\n              reactRouterConfig: ctx.reactRouterConfig,\n            }),\n            include: [\n              // Pre-bundle React dependencies to avoid React duplicates,\n              // even if React dependencies are not direct dependencies.\n              // https://react.dev/warnings/invalid-hook-call-warning#duplicate-react\n              \"react\",\n              \"react/jsx-runtime\",\n              \"react/jsx-dev-runtime\",\n              \"react-dom\",\n              \"react-dom/client\",\n\n              // Pre-bundle router dependencies to avoid router duplicates.\n              // Mismatching routers cause `Error: You must render this element inside a <Remix> element`.\n              \"react-router\",\n              \"react-router/dom\",\n              // Check to avoid \"Failed to resolve dependency: react-router-dom, present in 'optimizeDeps.include'\"\n              ...(hasDependency({\n                name: \"react-router-dom\",\n                rootDirectory: ctx.rootDirectory,\n              })\n                ? [\"react-router-dom\"]\n                : []),\n            ],\n          },\n          esbuild: {\n            jsx: \"automatic\",\n            jsxDev: viteCommand !== \"build\",\n          },\n          resolve: {\n            dedupe: [\n              // https://react.dev/warnings/invalid-hook-call-warning#duplicate-react\n              \"react\",\n              \"react-dom\",\n\n              // see description for `optimizeDeps.include`\n              \"react-router\",\n              \"react-router/dom\",\n              \"react-router-dom\",\n            ],\n            conditions:\n              viteCommand === \"build\"\n                ? viteClientConditions\n                : [\"development\", ...viteClientConditions],\n          },\n          base: viteUserConfig.base,\n\n          // When consumer provides an allowlist for files that can be read by\n          // the server, ensure that the default entry files are included.\n          // If we don't do this and a default entry file is used, the server\n          // will throw an error that the file is not allowed to be read.\n          // https://vitejs.dev/config/server-options#server-fs-allow\n          server: viteUserConfig.server?.fs?.allow\n            ? { fs: { allow: defaultEntries } }\n            : undefined,\n\n          ...(ctx.reactRouterConfig.future.v8_viteEnvironmentApi\n            ? {\n                environments,\n                build: {\n                  // This isn't honored by the SSR environment config (which seems\n                  // to be a Vite bug?) so we set it here too.\n                  ssrEmitAssets: true,\n                },\n                builder: {\n                  sharedConfigBuild: true,\n                  sharedPlugins: true,\n                  async buildApp(builder) {\n                    invariant(viteConfig);\n                    viteConfig.logger.info(\n                      \"Using Vite Environment API (experimental)\",\n                    );\n\n                    let { reactRouterConfig } = ctx;\n\n                    await cleanBuildDirectory(viteConfig, ctx);\n\n                    await builder.build(builder.environments.client);\n\n                    let serverEnvironments = getServerEnvironmentValues(\n                      ctx,\n                      builder.environments,\n                    );\n\n                    await Promise.all(serverEnvironments.map(builder.build));\n\n                    await cleanViteManifests(environments, ctx);\n\n                    let { buildManifest } = ctx;\n                    invariant(buildManifest, \"Expected build manifest\");\n\n                    await reactRouterConfig.buildEnd?.({\n                      buildManifest,\n                      reactRouterConfig,\n                      viteConfig,\n                    });\n                  },\n                },\n              }\n            : {\n                build:\n                  ctx.environmentBuildContext?.options.build ??\n                  (viteConfigEnv.isSsrBuild\n                    ? serverEnvironment.build\n                    : clientEnvironment.build),\n              }),\n        };\n      },\n      configEnvironment(name, options) {\n        if (\n          ctx.reactRouterConfig.future.v8_viteEnvironmentApi &&\n          (ctx.buildManifest?.serverBundles\n            ? isSsrBundleEnvironmentName(name)\n            : name === \"ssr\")\n        ) {\n          const vite = getVite();\n\n          return {\n            resolve: {\n              external:\n                // This check is required to honor the \"noExternal: true\" config\n                // provided by vite-plugin-cloudflare within this repo. When compiling\n                // for Cloudflare, all server dependencies are pre-bundled, but our\n                // `ssrExternals` config inadvertently overrides this. This doesn't\n                // impact consumers because for them `ssrExternals` is undefined and\n                // Cloudflare's \"noExternal: true\" config remains intact.\n                options.resolve?.noExternal === true ? undefined : ssrExternals,\n            },\n            optimizeDeps:\n              options.optimizeDeps?.noDiscovery === false\n                ? {\n                    entries: [\n                      vite.normalizePath(ctx.entryServerFilePath),\n                      ...Object.values(ctx.reactRouterConfig.routes).map(\n                        (route) =>\n                          resolveRelativeRouteFilePath(\n                            route,\n                            ctx.reactRouterConfig,\n                          ),\n                      ),\n                    ],\n                    include: [\n                      \"react\",\n                      \"react/jsx-dev-runtime\",\n                      \"react-dom/server\",\n                      \"react-router\",\n                    ],\n                  }\n                : undefined,\n          };\n        }\n      },\n      async configResolved(resolvedViteConfig) {\n        await initEsModuleLexer;\n\n        viteConfig = resolvedViteConfig;\n        invariant(viteConfig);\n\n        // We load the same Vite config file again for the child compiler so\n        // that both parent and child compiler's plugins have independent state.\n        // If we re-used the `viteUserConfig.plugins` array for the child\n        // compiler, it could lead to mutating shared state between plugin\n        // instances in unexpected ways, e.g. during `vite build` the\n        // `configResolved` plugin hook would be called with `command = \"build\"`\n        // by parent and then `command = \"serve\"` by child, which some plugins\n        // may respond to by updating state referenced by the parent.\n        if (!viteConfig.configFile) {\n          throw new Error(\n            \"The React Router Vite plugin requires the use of a Vite config file\",\n          );\n        }\n\n        let vite = getVite();\n\n        let childCompilerConfigFile = await vite.loadConfigFromFile(\n          {\n            command: viteConfig.command,\n            mode: viteConfig.mode,\n          },\n          viteConfig.configFile,\n        );\n\n        invariant(\n          childCompilerConfigFile,\n          \"Vite config file was unable to be resolved for React Router child compiler\",\n        );\n\n        const childCompilerPlugins = await asyncFlatten(\n          childCompilerConfigFile.config.plugins ?? [],\n        );\n\n        viteChildCompiler = await vite.createServer({\n          ...viteUserConfig,\n          // Ensure child compiler cannot overwrite the default cache directory\n          cacheDir: \"node_modules/.vite-child-compiler\",\n          mode: viteConfig.mode,\n          server: {\n            watch: viteConfig.command === \"build\" ? null : undefined,\n            preTransformRequests: false,\n            hmr: false,\n          },\n          configFile: false,\n          envFile: false,\n          plugins: [\n            childCompilerPlugins\n              // Exclude this plugin from the child compiler to prevent an\n              // infinite loop (plugin creates a child compiler with the same\n              // plugin that creates another child compiler, repeat ad\n              // infinitum), and to prevent the manifest from being written to\n              // disk from the child compiler. This is important in the\n              // production build because the child compiler is a Vite dev\n              // server and will generate incorrect manifests.\n              .filter(\n                (plugin): plugin is Vite.Plugin =>\n                  typeof plugin === \"object\" &&\n                  plugin !== null &&\n                  \"name\" in plugin &&\n                  plugin.name !== \"react-router\" &&\n                  plugin.name !== \"react-router:route-exports\" &&\n                  plugin.name !== \"react-router:hmr-updates\" &&\n                  plugin.name !== \"react-router:validate-plugin-order\",\n              )\n              // Remove server hooks to avoid conflicts with the main dev server\n              .map((plugin) => ({\n                ...plugin,\n                configureServer: undefined,\n                configurePreviewServer: undefined,\n              })),\n          ],\n        });\n        await viteChildCompiler.pluginContainer.buildStart({});\n      },\n      async transform(code, id) {\n        if (isCssModulesFile(id)) {\n          cssModulesManifest[id] = code;\n        }\n      },\n      async configureServer(viteDevServer) {\n        setDevServerHooks({\n          // Give the request handler access to the critical CSS in dev to avoid a\n          // flash of unstyled content since Vite injects CSS file contents via JS\n          getCriticalCss: async (pathname) => {\n            return getStylesForPathname({\n              rootDirectory: ctx.rootDirectory,\n              entryClientFilePath: ctx.entryClientFilePath,\n              reactRouterConfig: ctx.reactRouterConfig,\n              viteDevServer,\n              loadCssContents,\n              pathname,\n            });\n          },\n          // If an error is caught within the request handler, let Vite fix the\n          // stack trace so it maps back to the actual source code\n          processRequestError: (error) => {\n            if (error instanceof Error) {\n              viteDevServer.ssrFixStacktrace(error);\n            }\n          },\n        });\n\n        reactRouterConfigLoader.onChange(\n          async ({\n            result,\n            configCodeChanged,\n            routeConfigCodeChanged,\n            configChanged,\n            routeConfigChanged,\n          }) => {\n            if (!result.ok) {\n              invalidateVirtualModules(viteDevServer);\n              logger.error(result.error, {\n                clear: true,\n                timestamp: true,\n              });\n              return;\n            }\n\n            // prettier-ignore\n            let message =\n              configChanged ? \"Config changed.\" :\n              routeConfigChanged ? \"Route config changed.\" :\n              configCodeChanged ? \"Config saved.\" :\n              routeConfigCodeChanged ? \" Route config saved.\" :\n              \"Config saved\";\n\n            logger.info(colors.green(message), {\n              clear: true,\n              timestamp: true,\n            });\n\n            await updatePluginContext();\n\n            if (configChanged || routeConfigChanged) {\n              invalidateVirtualModules(viteDevServer);\n            }\n          },\n        );\n\n        if (ctx.reactRouterConfig.future.v8_viteEnvironmentApi) {\n          viteDevServer.middlewares.use(async (req, res, next) => {\n            let [reqPathname, reqSearch] = (req.url ?? \"\").split(\"?\");\n            if (reqPathname.endsWith(\"/@react-router/critical.css\")) {\n              let pathname = new URLSearchParams(reqSearch).get(\"pathname\");\n              if (!pathname) {\n                return next(\"No pathname provided\");\n              }\n              let css = await getStylesForPathname({\n                rootDirectory: ctx.rootDirectory,\n                entryClientFilePath: ctx.entryClientFilePath,\n                reactRouterConfig: ctx.reactRouterConfig,\n                viteDevServer,\n                loadCssContents,\n                pathname,\n              });\n              res.setHeader(\"Content-Type\", \"text/css\");\n              res.end(css);\n            } else {\n              next();\n            }\n          });\n        }\n\n        return () => {\n          // Let user servers handle SSR requests in middleware mode,\n          // otherwise the Vite plugin will handle the request\n          if (!viteDevServer.config.server.middlewareMode) {\n            viteDevServer.middlewares.use(async (req, res, next) => {\n              try {\n                let build: ServerBuild;\n                if (ctx.reactRouterConfig.future.v8_viteEnvironmentApi) {\n                  let vite = getVite();\n                  let ssrEnvironment = viteDevServer.environments.ssr;\n                  if (!vite.isRunnableDevEnvironment(ssrEnvironment)) {\n                    next();\n                    return;\n                  }\n                  build = (await ssrEnvironment.runner.import(\n                    virtual.serverBuild.id,\n                  )) as ServerBuild;\n                } else {\n                  build = (await viteDevServer.ssrLoadModule(\n                    virtual.serverBuild.id,\n                  )) as ServerBuild;\n                }\n\n                let handler = createRequestHandler(build, \"development\");\n                let nodeHandler: NodeRequestHandler = async (\n                  nodeReq,\n                  nodeRes,\n                ) => {\n                  let req = await fromNodeRequest(nodeReq, nodeRes);\n                  let res = await handler(\n                    req,\n                    await reactRouterDevLoadContext(req),\n                  );\n                  // Async import here to allow ESM only module on Node 20.18.\n                  // TODO(v8): Can move to a normal import when Node 20 support\n                  const { sendResponse } = await import(\n                    \"@remix-run/node-fetch-server\"\n                  );\n                  await sendResponse(nodeRes, res);\n                };\n                await nodeHandler(req, res);\n              } catch (error) {\n                next(error);\n              }\n            });\n          }\n        };\n      },\n      configurePreviewServer(previewServer) {\n        // Cache combined handler for all server bundles\n        let cachedHandler: RequestHandler | null = null;\n\n        async function getHandler(): Promise<RequestHandler> {\n          if (cachedHandler) return cachedHandler;\n\n          let serverBuildFiles: string[] = [];\n\n          // Get build manifest to find server bundles\n          let buildManifest =\n            ctx.buildManifest ??\n            (ctx.reactRouterConfig.serverBundles\n              ? await getBuildManifest({\n                  reactRouterConfig: ctx.reactRouterConfig,\n                  rootDirectory: ctx.rootDirectory,\n                })\n              : null);\n\n          if (buildManifest?.serverBundles) {\n            // Load all server bundle files\n            for (let bundle of Object.values(buildManifest.serverBundles)) {\n              serverBuildFiles.push(\n                path.resolve(ctx.rootDirectory, bundle.file),\n              );\n            }\n          } else {\n            let serverEntryPath = path.resolve(\n              getServerBuildDirectory(ctx.reactRouterConfig),\n              \"index.js\",\n            );\n\n            // Single server build\n            serverBuildFiles.push(serverEntryPath);\n          }\n\n          // Import all bundles and create handlers\n          let handlers: RequestHandler[] = [];\n          for (let file of serverBuildFiles) {\n            let build: ServerBuild = await import(url.pathToFileURL(file).href);\n            handlers.push(createRequestHandler(build, \"production\"));\n          }\n\n          // Return a combined handler that tries each bundle until one handles the request.\n          // A 404 response means \"not my route\", so we try the next bundle.\n          cachedHandler = async (request, loadContext) => {\n            let response: Response | undefined;\n\n            for (let handler of handlers) {\n              response = await handler(request, loadContext);\n\n              if (response.status !== 404) {\n                return response;\n              }\n            }\n\n            if (response) {\n              return response;\n            }\n\n            throw new Error(\"No handlers were found for the request.\");\n          };\n\n          return cachedHandler;\n        }\n\n        return () => {\n          // Skip SSR handling in SPA mode\n          if (!ctx.reactRouterConfig.ssr) {\n            return;\n          }\n\n          // Handle SSR requests in preview mode using the built server bundle\n          previewServer.middlewares.use(async (req, res, next) => {\n            try {\n              let handler = await getHandler();\n              let request = await fromNodeRequest(req, res);\n              let response = await handler(\n                request,\n                await reactRouterDevLoadContext(request),\n              );\n\n              // Async import here to allow ESM only module on Node 20.18.\n              // TODO(v8): Can move to a normal import when Node 20 support\n              const { sendResponse } = await import(\n                \"@remix-run/node-fetch-server\"\n              );\n              await sendResponse(res, response);\n            } catch (error) {\n              next(error);\n            }\n          });\n        };\n      },\n      writeBundle: {\n        // After the SSR build is finished, we inspect the Vite manifest for\n        // the SSR build and move server-only assets to client assets directory\n        async handler() {\n          let { future } = ctx.reactRouterConfig;\n\n          if (\n            future.v8_viteEnvironmentApi\n              ? this.environment.name === \"client\"\n              : !viteConfigEnv.isSsrBuild\n          ) {\n            return;\n          }\n          invariant(viteConfig);\n\n          let clientBuildDirectory = getClientBuildDirectory(\n            ctx.reactRouterConfig,\n          );\n\n          let serverBuildDirectory = future.v8_viteEnvironmentApi\n            ? this.environment.config?.build?.outDir\n            : (ctx.environmentBuildContext?.options.build?.outDir ??\n              getServerBuildDirectory(ctx.reactRouterConfig));\n\n          let ssrViteManifest = await loadViteManifest(serverBuildDirectory);\n          let ssrAssetPaths = getViteManifestAssetPaths(ssrViteManifest);\n\n          // If the consumer has explicitly opted in to keeping the SSR build\n          // assets, we don't remove them. We only copy missing assets from the\n          // SSR to the client build.\n          let userSsrEmitAssets =\n            (ctx.reactRouterConfig.future.v8_viteEnvironmentApi\n              ? (viteUserConfig.environments?.ssr?.build?.ssrEmitAssets ??\n                viteUserConfig.environments?.ssr?.build?.emitAssets)\n              : null) ??\n            viteUserConfig.build?.ssrEmitAssets ??\n            false;\n\n          // We only move/copy assets that aren't in the client build, otherwise\n          // we remove them if the consumer hasn't explicitly enabled\n          // `ssrEmitAssets` in their Vite config. These assets only exist\n          // because we internally enable `ssrEmitAssets` within our plugin.\n          // These assets typically wouldn't exist by default, which is why we\n          // assume it's safe to remove them.\n          let movedAssetPaths: string[] = [];\n          let removedAssetPaths: string[] = [];\n          let copiedAssetPaths: string[] = [];\n          for (let ssrAssetPath of ssrAssetPaths) {\n            let src = path.join(serverBuildDirectory, ssrAssetPath);\n            let dest = path.join(clientBuildDirectory, ssrAssetPath);\n\n            if (!userSsrEmitAssets) {\n              if (!existsSync(dest)) {\n                await mkdir(path.dirname(dest), { recursive: true });\n                await rename(src, dest);\n                movedAssetPaths.push(dest);\n              } else {\n                await rm(src, { force: true, recursive: true });\n                removedAssetPaths.push(dest);\n              }\n            } else if (!existsSync(dest)) {\n              await cp(src, dest, { recursive: true });\n              copiedAssetPaths.push(dest);\n            }\n          }\n\n          if (!userSsrEmitAssets) {\n            // We assume CSS assets from the SSR build are unnecessary and\n            // remove them for the same reasons as above.\n            let ssrCssPaths = Object.values(ssrViteManifest).flatMap(\n              (chunk) => chunk.css ?? [],\n            );\n            await Promise.all(\n              ssrCssPaths.map(async (cssPath) => {\n                let src = path.join(serverBuildDirectory, cssPath);\n                await rm(src, { force: true, recursive: true });\n                removedAssetPaths.push(src);\n              }),\n            );\n          }\n\n          let cleanedAssetPaths = [...removedAssetPaths, ...movedAssetPaths];\n          let handledAssetPaths = [...cleanedAssetPaths, ...copiedAssetPaths];\n\n          // Clean empty asset directories\n          let cleanedAssetDirs = new Set(cleanedAssetPaths.map(path.dirname));\n          await Promise.all(\n            Array.from(cleanedAssetDirs).map(async (dir) => {\n              try {\n                const files = await readdir(dir, { recursive: true });\n                if (files.length === 0) {\n                  await rm(dir, { force: true, recursive: true });\n                }\n              } catch {}\n            }),\n          );\n\n          // If we handled any assets, add some leading whitespace to\n          // our logs to make them more prominent\n          if (handledAssetPaths.length) {\n            viteConfig.logger.info(\"\");\n          }\n\n          function logHandledAssets(paths: string[], message: string) {\n            invariant(viteConfig);\n            if (paths.length) {\n              viteConfig.logger.info(\n                [\n                  `${colors.green(\"✓\")} ${message}`,\n                  ...paths.map((assetPath) =>\n                    colors.dim(path.relative(ctx.rootDirectory, assetPath)),\n                  ),\n                ].join(\"\\n\"),\n              );\n            }\n          }\n\n          logHandledAssets(\n            removedAssetPaths,\n            `${removedAssetPaths.length} asset${\n              removedAssetPaths.length > 1 ? \"s\" : \"\"\n            } cleaned from React Router server build.`,\n          );\n\n          logHandledAssets(\n            movedAssetPaths,\n            `${movedAssetPaths.length} asset${\n              movedAssetPaths.length > 1 ? \"s\" : \"\"\n            } moved from React Router server build to client assets.`,\n          );\n\n          logHandledAssets(\n            copiedAssetPaths,\n            `${copiedAssetPaths.length} asset${\n              copiedAssetPaths.length > 1 ? \"s\" : \"\"\n            } copied from React Router server build to client assets.`,\n          );\n\n          // If we handled any assets, add some leading whitespace to our logs\n          // to make them more prominent\n          if (handledAssetPaths.length) {\n            viteConfig.logger.info(\"\");\n          }\n\n          if (future.unstable_previewServerPrerendering) {\n            // Prerendering is handled by the prerender plugin\n            return;\n          }\n\n          // Set an environment variable we can look for in the handler to\n          // enable some build-time-only logic\n          process.env.IS_RR_BUILD_REQUEST = \"yes\";\n\n          if (isPrerenderingEnabled(ctx.reactRouterConfig)) {\n            // If we have prerender routes, that takes precedence over SPA mode\n            // which is ssr:false and only the root route being rendered\n            await handlePrerender(\n              viteConfig,\n              ctx.reactRouterConfig,\n              serverBuildDirectory,\n              getServerBuildFile(ssrViteManifest),\n              clientBuildDirectory,\n            );\n          }\n\n          // When `ssr:false` is set, we always want a SPA HTML they can use\n          // to serve non-prerendered routes.  This file will only SSR the root\n          // route and can hydrate for any path.\n          if (!ctx.reactRouterConfig.ssr) {\n            await handleSpaMode(\n              viteConfig,\n              ctx.reactRouterConfig,\n              serverBuildDirectory,\n              getServerBuildFile(ssrViteManifest),\n              clientBuildDirectory,\n            );\n          }\n\n          // For both SPA mode and prerendering, we can remove the server builds\n          // if ssr:false is set\n          if (!ctx.reactRouterConfig.ssr) {\n            // Cleanup - we no longer need the server build assets\n            viteConfig.logger.info(\n              [\n                \"Removing the server build in\",\n                colors.green(serverBuildDirectory),\n                \"due to ssr:false\",\n              ].join(\" \"),\n            );\n            rmSync(serverBuildDirectory, { force: true, recursive: true });\n          }\n        },\n      },\n      async buildEnd() {\n        await viteChildCompiler?.close();\n        await reactRouterConfigLoader.close();\n\n        let typegenWatcher = await typegenWatcherPromise;\n        await typegenWatcher?.close();\n      },\n    },\n    {\n      name: \"react-router:route-chunks-index\",\n      // This plugin provides the route module \"index\" since route modules can\n      // be chunked and may be made up of multiple smaller modules. This plugin\n      // primarily ensures code is never duplicated across a route module and\n      // its chunks. If we didn't have this plugin, any app that explicitly\n      // imports a route module would result in duplicate code since the app\n      // would contain code for both the unprocessed route module and its\n      // individual chunks. This is because, since they have different module\n      // IDs, they are treated as completely separate modules even though they\n      // all reference the same underlying file. This plugin addresses this by\n      // ensuring that any explicit imports of a route module resolve to a\n      // module that simply re-exports from its underlying chunks, if present.\n      async transform(code, id, options) {\n        // Routes are only chunked in build mode\n        if (viteCommand !== \"build\") return;\n\n        // Routes aren't chunked on the server\n        if (options?.ssr) {\n          return;\n        }\n\n        // Ensure we're only operating on routes\n        if (!isRoute(ctx.reactRouterConfig, id)) {\n          return;\n        }\n\n        // Ensure we're only operating on raw route module imports\n        if (isRouteVirtualModule(id)) {\n          return;\n        }\n\n        let { hasRouteChunks, chunkedExports } =\n          await detectRouteChunksIfEnabled(cache, ctx, id, code);\n\n        // If there are no chunks, we can let this resolve to the raw route\n        // module since there's no risk of duplication\n        if (!hasRouteChunks) {\n          return;\n        }\n\n        let sourceExports = await getRouteModuleExports(\n          viteChildCompiler,\n          ctx,\n          id,\n        );\n\n        let isMainChunkExport = (name: string) =>\n          !chunkedExports.includes(name as string & RouteChunkExportName);\n\n        let mainChunkReexports = sourceExports\n          .filter(isMainChunkExport)\n          .join(\", \");\n\n        let chunkBasePath = `./${path.basename(id)}`;\n\n        return [\n          `export { ${mainChunkReexports} } from \"${getRouteChunkModuleId(\n            chunkBasePath,\n            \"main\",\n          )}\";`,\n          ...chunkedExports.map(\n            (exportName) =>\n              `export { ${exportName} } from \"${getRouteChunkModuleId(\n                chunkBasePath,\n                exportName,\n              )}\";`,\n          ),\n        ]\n          .filter(Boolean)\n          .join(\"\\n\");\n      },\n    },\n    {\n      name: \"react-router:build-client-route\",\n      async transform(code, id, options) {\n        if (!id.endsWith(BUILD_CLIENT_ROUTE_QUERY_STRING)) return;\n        let routeModuleId = id.replace(BUILD_CLIENT_ROUTE_QUERY_STRING, \"\");\n\n        let routeFileName = path.basename(routeModuleId);\n\n        let sourceExports = await getRouteModuleExports(\n          viteChildCompiler,\n          ctx,\n          routeModuleId,\n        );\n\n        let { chunkedExports = [] } = options?.ssr\n          ? {}\n          : await detectRouteChunksIfEnabled(cache, ctx, id, code);\n\n        let reexports = sourceExports\n          .filter((exportName) => {\n            let isRouteEntryExport =\n              (options?.ssr &&\n                SERVER_ONLY_ROUTE_EXPORTS.includes(exportName)) ||\n              CLIENT_ROUTE_EXPORTS.includes(exportName);\n\n            let isChunkedExport = chunkedExports.includes(\n              exportName as string & RouteChunkExportName,\n            );\n\n            return isRouteEntryExport && !isChunkedExport;\n          })\n          .join(\", \");\n\n        return `export { ${reexports} } from \"./${routeFileName}\";`;\n      },\n    },\n    {\n      name: \"react-router:split-route-modules\",\n      async transform(code, id, options) {\n        // Routes aren't chunked on the server\n        if (options?.ssr) return;\n\n        // Ignore anything not marked as a route chunk\n        if (!isRouteChunkModuleId(id)) return;\n\n        invariant(\n          viteCommand === \"build\",\n          \"Route modules are only split in build mode\",\n        );\n\n        let chunkName = getRouteChunkNameFromModuleId(id);\n\n        if (!chunkName) {\n          throw new Error(`Invalid route chunk name \"${chunkName}\" in \"${id}\"`);\n        }\n\n        let chunk = await getRouteChunkIfEnabled(\n          cache,\n          ctx,\n          id,\n          chunkName,\n          code,\n        );\n\n        let preventEmptyChunkSnippet = ({ reason }: { reason: string }) =>\n          `Math.random()<0&&console.log(${JSON.stringify(reason)});`;\n\n        if (chunk === null) {\n          return preventEmptyChunkSnippet({\n            reason: \"Split round modules disabled\",\n          });\n        }\n\n        let enforceSplitRouteModules =\n          ctx.reactRouterConfig.future.v8_splitRouteModules === \"enforce\";\n\n        if (enforceSplitRouteModules && chunkName === \"main\" && chunk) {\n          let exportNames = getExportNames(chunk.code);\n\n          validateRouteChunks({\n            ctx,\n            id,\n            valid: {\n              clientAction: !exportNames.includes(\"clientAction\"),\n              clientLoader: !exportNames.includes(\"clientLoader\"),\n              clientMiddleware: !exportNames.includes(\"clientMiddleware\"),\n              HydrateFallback: !exportNames.includes(\"HydrateFallback\"),\n            },\n          });\n        }\n\n        return (\n          chunk ?? preventEmptyChunkSnippet({ reason: `No ${chunkName} chunk` })\n        );\n      },\n    },\n    {\n      name: \"react-router:virtual-modules\",\n      enforce: \"pre\",\n      resolveId(id) {\n        const vmod = Object.values(virtual).find((vmod) => vmod.id === id);\n        if (vmod) return vmod.resolvedId;\n      },\n      async load(id) {\n        switch (id) {\n          case virtual.serverBuild.resolvedId: {\n            let routeIds = getServerBundleRouteIds(this, ctx);\n            return await getServerEntry({ routeIds });\n          }\n          case virtual.serverManifest.resolvedId: {\n            let routeIds = getServerBundleRouteIds(this, ctx);\n            invariant(viteConfig);\n            let reactRouterManifest =\n              viteCommand === \"build\"\n                ? (\n                    await generateReactRouterManifestsForBuild({\n                      viteConfig,\n                      routeIds,\n                    })\n                  ).reactRouterServerManifest\n                : await getReactRouterManifestForDev();\n\n            // Cache manifest on context for prerendering later\n            ctx.reactRouterManifest = reactRouterManifest;\n\n            // Check for invalid APIs when SSR is disabled\n            if (!ctx.reactRouterConfig.ssr) {\n              invariant(viteConfig);\n              validateSsrFalsePrerenderExports(\n                viteConfig,\n                ctx,\n                reactRouterManifest,\n                viteChildCompiler,\n              );\n            }\n\n            return `export default ${jsesc(reactRouterManifest, {\n              es6: true,\n            })};`;\n          }\n          case virtual.browserManifest.resolvedId: {\n            if (viteCommand === \"build\") {\n              throw new Error(\"This module only exists in development\");\n            }\n\n            let reactRouterManifest = await getReactRouterManifestForDev();\n            let reactRouterManifestString = jsesc(reactRouterManifest, {\n              es6: true,\n            });\n\n            return `window.__reactRouterManifest=${reactRouterManifestString};`;\n          }\n        }\n      },\n    },\n    {\n      name: \"react-router:dot-server\",\n      enforce: \"pre\",\n      async resolveId(id, importer, options) {\n        // https://vitejs.dev/config/dep-optimization-options\n        let isOptimizeDeps =\n          viteCommand === \"serve\" &&\n          (options as { scan?: boolean })?.scan === true;\n\n        if (isOptimizeDeps || options?.ssr) return;\n\n        let isResolving = options?.custom?.[\"react-router:dot-server\"] ?? false;\n        if (isResolving) return;\n        options.custom = { ...options.custom, \"react-router:dot-server\": true };\n        let resolved = await this.resolve(id, importer, options);\n        if (!resolved) return;\n\n        let serverFileRE = /\\.server(\\.[cm]?[jt]sx?)?$/;\n        let serverDirRE = /\\/\\.server\\//;\n        let isDotServer =\n          serverFileRE.test(resolved!.id) || serverDirRE.test(resolved!.id);\n        if (!isDotServer) return;\n\n        if (!importer) return;\n        if (viteCommand !== \"build\" && importer.endsWith(\".html\")) {\n          // Vite has a special `index.html` importer for `resolveId` within `transformRequest`\n          // https://github.com/vitejs/vite/blob/5684fcd8d27110d098b3e1c19d851f44251588f1/packages/vite/src/node/server/transformRequest.ts#L158\n          // https://github.com/vitejs/vite/blob/5684fcd8d27110d098b3e1c19d851f44251588f1/packages/vite/src/node/server/pluginContainer.ts#L668\n          return;\n        }\n\n        let vite = getVite();\n        let importerShort = vite.normalizePath(\n          path.relative(ctx.rootDirectory, importer),\n        );\n        if (isRoute(ctx.reactRouterConfig, importer)) {\n          let serverOnlyExports = SERVER_ONLY_ROUTE_EXPORTS.map(\n            (xport) => `\\`${xport}\\``,\n          ).join(\", \");\n          throw Error(\n            [\n              colors.red(`Server-only module referenced by client`),\n              \"\",\n              `    '${id}' imported by route '${importerShort}'`,\n              \"\",\n              `  React Router automatically removes server-code from these exports:`,\n              `    ${serverOnlyExports}`,\n              \"\",\n              `  But other route exports in '${importerShort}' depend on '${id}'.`,\n              \"\",\n              \"  See https://reactrouter.com/explanation/code-splitting#removal-of-server-code\",\n              \"\",\n            ].join(\"\\n\"),\n          );\n        }\n\n        throw Error(\n          [\n            colors.red(`Server-only module referenced by client`),\n            \"\",\n            `    '${id}' imported by '${importerShort}'`,\n            \"\",\n            \"  See https://reactrouter.com/explanation/code-splitting#removal-of-server-code\",\n            \"\",\n          ].join(\"\\n\"),\n        );\n      },\n    },\n    {\n      name: \"react-router:dot-client\",\n      async transform(code, id, options) {\n        if (!options?.ssr) return;\n        let clientFileRE = /\\.client(\\.[cm]?[jt]sx?)?$/;\n        let clientDirRE = /\\/\\.client\\//;\n        if (clientFileRE.test(id) || clientDirRE.test(id)) {\n          let exports = getExportNames(code);\n          return {\n            code: exports\n              .map((name) =>\n                name === \"default\"\n                  ? \"export default undefined;\"\n                  : `export const ${name} = undefined;`,\n              )\n              .join(\"\\n\"),\n            map: null,\n          };\n        }\n      },\n    },\n    {\n      name: \"react-router:route-exports\",\n      async transform(code, id, options) {\n        // Ensure we perform this transform on all route module chunks\n        if (isRouteChunkModuleId(id)) {\n          id = id.split(\"?\")[0];\n        }\n\n        let route = getRoute(ctx.reactRouterConfig, id);\n        if (!route) return;\n\n        if (!options?.ssr && isSpaModeEnabled(ctx.reactRouterConfig)) {\n          let exportNames = getExportNames(code);\n          let serverOnlyExports = exportNames.filter((exp) => {\n            // Root route can have a loader in SPA mode\n            if (route.id === \"root\" && exp === \"loader\") {\n              return false;\n            }\n            return SERVER_ONLY_ROUTE_EXPORTS.includes(exp);\n          });\n\n          if (serverOnlyExports.length > 0) {\n            let str = serverOnlyExports.map((e) => `\\`${e}\\``).join(\", \");\n            let message =\n              `SPA Mode: ${serverOnlyExports.length} invalid route export(s) in ` +\n              `\\`${route.file}\\`: ${str}. See https://reactrouter.com/how-to/spa ` +\n              `for more information.`;\n            throw Error(message);\n          }\n\n          if (route.id !== \"root\") {\n            let hasHydrateFallback = exportNames.some(\n              (exp) => exp === \"HydrateFallback\",\n            );\n            if (hasHydrateFallback) {\n              let message =\n                `SPA Mode: Invalid \\`HydrateFallback\\` export found in ` +\n                `\\`${route.file}\\`. \\`HydrateFallback\\` is only permitted on ` +\n                `the root route in SPA Mode. See https://reactrouter.com/how-to/spa ` +\n                `for more information.`;\n              throw Error(message);\n            }\n          }\n        }\n\n        let [filepath] = id.split(\"?\");\n\n        let ast = parse(code, { sourceType: \"module\" });\n        if (!options?.ssr) {\n          removeExports(ast, SERVER_ONLY_ROUTE_EXPORTS);\n        }\n        decorateComponentExportsWithProps(ast);\n        return generate(ast, {\n          sourceMaps: true,\n          filename: id,\n          sourceFileName: filepath,\n        });\n      },\n    },\n    {\n      name: \"react-router:inject-hmr-runtime\",\n      enforce: \"pre\",\n      resolveId(id) {\n        if (id === virtualInjectHmrRuntime.id) {\n          return virtualInjectHmrRuntime.resolvedId;\n        }\n      },\n      async load(id) {\n        if (id !== virtualInjectHmrRuntime.resolvedId) return;\n\n        return [\n          `import RefreshRuntime from \"${virtualHmrRuntime.id}\"`,\n          \"RefreshRuntime.injectIntoGlobalHook(window)\",\n          \"window.$RefreshReg$ = () => {}\",\n          \"window.$RefreshSig$ = () => (type) => type\",\n          \"window.__vite_plugin_react_preamble_installed__ = true\",\n        ].join(\"\\n\");\n      },\n    },\n    {\n      name: \"react-router:hmr-runtime\",\n      enforce: \"pre\",\n      resolveId(id) {\n        if (id === virtualHmrRuntime.id) return virtualHmrRuntime.resolvedId;\n      },\n      async load(id) {\n        if (id !== virtualHmrRuntime.resolvedId) return;\n\n        let reactRefreshDir = path.dirname(\n          require.resolve(\"react-refresh/package.json\"),\n        );\n        let reactRefreshRuntimePath = path.join(\n          reactRefreshDir,\n          \"cjs/react-refresh-runtime.development.js\",\n        );\n\n        return [\n          \"const exports = {}\",\n          await readFile(reactRefreshRuntimePath, \"utf8\"),\n          await readFile(require.resolve(\"./static/refresh-utils.mjs\"), \"utf8\"),\n          \"export default exports\",\n        ].join(\"\\n\");\n      },\n    },\n    {\n      name: \"react-router:react-refresh-babel\",\n      async transform(code, id, options) {\n        if (viteCommand !== \"serve\") return;\n        if (id.includes(\"/node_modules/\")) return;\n\n        let [filepath] = id.split(\"?\");\n        let extensionsRE = /\\.(jsx?|tsx?|mdx?)$/;\n        if (!extensionsRE.test(filepath)) return;\n\n        let devRuntime = \"react/jsx-dev-runtime\";\n        let ssr = options?.ssr === true;\n        let isJSX = filepath.endsWith(\"x\");\n        let useFastRefresh = !ssr && (isJSX || code.includes(devRuntime));\n        if (!useFastRefresh) return;\n\n        if (isRouteVirtualModule(id)) {\n          return { code: addRefreshWrapper(ctx.reactRouterConfig, code, id) };\n        }\n\n        let result = await babel.transformAsync(code, {\n          babelrc: false,\n          configFile: false,\n          filename: id,\n          sourceFileName: filepath,\n          parserOpts: {\n            sourceType: \"module\",\n            allowAwaitOutsideFunction: true,\n          },\n          plugins: [[require(\"react-refresh/babel\"), { skipEnvCheck: true }]],\n          sourceMaps: true,\n        });\n        if (result === null) return;\n\n        code = result.code!;\n        let refreshContentRE = /\\$Refresh(?:Reg|Sig)\\$\\(/;\n        if (refreshContentRE.test(code)) {\n          code = addRefreshWrapper(ctx.reactRouterConfig, code, id);\n        }\n        return { code, map: result.map };\n      },\n    },\n    {\n      name: \"react-router:hmr-updates\",\n      async handleHotUpdate({ server, file, modules, read }) {\n        let route = getRoute(ctx.reactRouterConfig, file);\n\n        type HmrEventData = { route: ManifestRoute | null };\n        let hmrEventData: HmrEventData = { route: null };\n\n        if (route) {\n          // invalidate manifest on route exports change\n          let oldRouteMetadata =\n            currentReactRouterManifestForDev?.routes[route.id];\n          let newRouteMetadata = await getRouteMetadata(\n            cache,\n            ctx,\n            viteChildCompiler,\n            route,\n            read,\n          );\n\n          hmrEventData.route = newRouteMetadata;\n\n          if (\n            !oldRouteMetadata ||\n            (\n              [\n                \"hasLoader\",\n                \"hasClientLoader\",\n                \"clientLoaderModule\",\n                \"hasAction\",\n                \"hasClientAction\",\n                \"clientActionModule\",\n                \"hasClientMiddleware\",\n                \"clientMiddlewareModule\",\n                \"hasErrorBoundary\",\n                \"hydrateFallbackModule\",\n              ] as const\n            ).some((key) => oldRouteMetadata[key] !== newRouteMetadata[key])\n          ) {\n            invalidateVirtualModules(server);\n          }\n        }\n\n        server.hot.send({\n          type: \"custom\",\n          event: \"react-router:hmr\",\n          data: hmrEventData,\n        });\n\n        return modules;\n      },\n    },\n    {\n      name: \"react-router-server-change-trigger-client-hmr\",\n      // This hook is only available in Vite v6+ so this is a no-op in v5.\n      // Previously, the server and client modules were shared in a single module\n      // graph. This meant that changes to server code automatically resulted in\n      // client HMR updates. In Vite v6+, these module graphs are separate from\n      // each other, so we need to manually trigger client HMR updates if server\n      // code has changed.\n      hotUpdate(this, { server, modules }) {\n        if (this.environment.name !== \"ssr\" && modules.length <= 0) {\n          return;\n        }\n\n        let clientModules = modules.flatMap((mod) =>\n          getParentClientNodes(server.environments.client.moduleGraph, mod),\n        );\n\n        for (let clientModule of clientModules) {\n          server.environments.client.reloadModule(clientModule);\n        }\n      },\n    },\n    prerender({\n      config() {\n        process.env.IS_RR_BUILD_REQUEST = \"yes\";\n        return {\n          // Required as viteConfig.environments.client.build.outDir is only available in Vite v6+\n          buildDirectory: getClientBuildDirectory(ctx.reactRouterConfig),\n          concurrency: getPrerenderConcurrencyConfig(ctx.reactRouterConfig),\n        };\n      },\n      async requests() {\n        invariant(viteConfig);\n\n        let { future } = ctx.reactRouterConfig;\n\n        // Prerender during SSR build only\n        if (\n          future.v8_viteEnvironmentApi\n            ? this.environment.name === \"client\"\n            : !viteConfigEnv.isSsrBuild\n        ) {\n          return [];\n        }\n\n        // Skip prerendering if the future flag is disabled\n        if (!future.unstable_previewServerPrerendering) {\n          return [];\n        }\n\n        let requests: PrerenderRequest<PrerenderMetadata>[] = [];\n\n        if (isPrerenderingEnabled(ctx.reactRouterConfig)) {\n          invariant(ctx.prerenderPaths !== null, \"Prerender paths missing\");\n          invariant(\n            ctx.reactRouterManifest !== null,\n            \"Prerender manifest missing\",\n          );\n\n          let { reactRouterConfig, reactRouterManifest, prerenderPaths } = ctx;\n\n          assertPrerenderPathsMatchRoutes(\n            reactRouterConfig,\n            Array.from(prerenderPaths),\n          );\n\n          let buildRoutes = createPrerenderRoutes(reactRouterManifest.routes);\n\n          for (let prerenderPath of prerenderPaths) {\n            let matches = matchRoutes(\n              buildRoutes,\n              `/${prerenderPath}/`.replace(/^\\/\\/+/, \"/\"),\n            );\n\n            if (!matches) {\n              continue;\n            }\n\n            // When prerendering a resource route, we don't want to pass along the\n            // `.data` file since we want to prerender the raw Response returned from\n            // the loader.  Presumably this is for routes where a file extension is\n            // already included, such as `app/routes/items[.json].tsx` that will\n            // render into `/items.json`\n            let leafRoute = matches[matches.length - 1].route;\n            let manifestRoute = reactRouterManifest.routes[leafRoute.id];\n            let isResourceRoute =\n              manifestRoute &&\n              !manifestRoute.hasDefaultExport &&\n              !manifestRoute.hasErrorBoundary;\n\n            if (isResourceRoute) {\n              if (manifestRoute?.hasLoader) {\n                requests.push(\n                  // Prerender a .data file for turbo-stream consumption\n                  createDataRequest(\n                    prerenderPath,\n                    reactRouterConfig,\n                    [leafRoute.id],\n                    true,\n                  ),\n                  // Prerender a raw file for external consumption\n                  createResourceRouteRequest(prerenderPath, reactRouterConfig),\n                );\n              } else {\n                viteConfig.logger.warn(\n                  `⚠️ Skipping prerendering for resource route without a loader: ${leafRoute.id}`,\n                );\n              }\n            } else {\n              let hasLoaders = matches.some(\n                (m) => reactRouterManifest.routes[m.route.id]?.hasLoader,\n              );\n\n              if (hasLoaders) {\n                requests.push(\n                  createDataRequest(prerenderPath, reactRouterConfig, null),\n                );\n              } else {\n                requests.push(\n                  createRouteRequest(prerenderPath, reactRouterConfig),\n                );\n              }\n            }\n          }\n        }\n\n        // When `ssr:false` is set, we always want a SPA HTML they can use\n        // to serve non-prerendered routes.  This file will only SSR the root\n        // route and can hydrate for any path.\n        if (!ctx.reactRouterConfig.ssr) {\n          requests.push(createSpaModeRequest(ctx.reactRouterConfig));\n        }\n\n        return requests;\n      },\n      async postProcess(request, response, metadata) {\n        invariant(metadata);\n\n        // Handle loader data responses\n        if (metadata.type === \"data\") {\n          let pathname = new URL(request.url).pathname;\n\n          if (response.status !== 200 && response.status !== 202) {\n            throw new Error(\n              `Prerender (data): Received a ${response.status} status code from ` +\n                `\\`entry.server.tsx\\` while prerendering the \\`${metadata.path}\\` ` +\n                `path.\\n${pathname}`,\n              { cause: response },\n            );\n          }\n\n          let data = await response.text();\n\n          return {\n            files: [\n              {\n                path: pathname,\n                contents: data,\n              },\n            ],\n            // After saving the .data file, request the HTML page.\n            // The data is passed along to be embedded in the response header.\n            requests: !metadata.isResourceRoute\n              ? [createRouteRequest(metadata.path, ctx.reactRouterConfig, data)]\n              : [],\n          };\n        }\n\n        // Handle resource route responses\n        if (metadata.type === \"resource\") {\n          let pathname = new URL(request.url).pathname;\n          let contents = new Uint8Array(await response.arrayBuffer());\n\n          if (response.status !== 200) {\n            throw new Error(\n              `Prerender (resource): Received a ${response.status} status code from ` +\n                `\\`entry.server.tsx\\` while prerendering the \\`${pathname}\\` ` +\n                `path.\\n${new TextDecoder().decode(contents)}`,\n            );\n          }\n\n          return [\n            {\n              path: pathname,\n              contents,\n            },\n          ];\n        }\n\n        // Handle document responses (html or spa)\n        let html = await response.text();\n\n        if (metadata.type === \"spa\") {\n          if (response.status !== 200) {\n            throw new Error(\n              `SPA Mode: Received a ${response.status} status code from ` +\n                `\\`entry.server.tsx\\` while prerendering your SPA Fallback HTML file.\\n` +\n                html,\n            );\n          }\n\n          if (\n            !html.includes(\"window.__reactRouterContext =\") ||\n            !html.includes(\"window.__reactRouterRouteModules =\")\n          ) {\n            throw new Error(\n              \"SPA Mode: Did you forget to include `<Scripts/>` in your root route? \" +\n                \"Your pre-rendered HTML cannot hydrate without `<Scripts />`.\",\n            );\n          }\n\n          // SPA fallback is written to root regardless of basename\n          return [\n            {\n              path: \"/__spa-fallback.html\",\n              contents: html,\n            },\n          ];\n        }\n\n        // Handle html responses\n        let pathname = new URL(request.url).pathname;\n\n        if (redirectStatusCodes.has(response.status)) {\n          // This isn't ideal but gets the job done as a fallback if the user can't\n          // implement proper redirects via .htaccess or something else.  This is the\n          // approach used by Astro as well, so there's some precedent.\n          // https://github.com/withastro/roadmap/issues/466\n          // https://github.com/withastro/astro/blob/main/packages/astro/src/core/routing/3xx.ts\n          let location = response.headers.get(\"Location\");\n          // A short delay causes Google to interpret the redirect as temporary.\n          // https://developers.google.com/search/docs/crawling-indexing/301-redirects#metarefresh\n          let delay = response.status === 302 ? 2 : 0;\n          html = `<!doctype html>\n<head>\n<title>Redirecting to: ${location}</title>\n<meta http-equiv=\"refresh\" content=\"${delay};url=${location}\">\n<meta name=\"robots\" content=\"noindex\">\n</head>\n<body>\n\t<a href=\"${location}\">\n    Redirecting from <code>${pathname}</code> to <code>${location}</code>\n  </a>\n</body>\n</html>`;\n        } else if (response.status !== 200) {\n          throw new Error(\n            `Prerender (html): Received a ${response.status} status code from ` +\n              `\\`entry.server.tsx\\` while prerendering the \\`${pathname}\\` ` +\n              `path.\\n${html}`,\n          );\n        }\n\n        return [\n          {\n            path: `${pathname}/index.html`,\n            contents: html,\n          },\n        ];\n      },\n      logFile(outputPath, metadata) {\n        invariant(viteConfig);\n        invariant(metadata);\n        // SPA fallback logging is handled in finalize after we know the final filename\n        if (metadata.type === \"spa\") {\n          return;\n        }\n        viteConfig.logger.info(\n          `Prerender (${metadata.type}): ${metadata.path} -> ${colors.bold(outputPath)}`,\n        );\n      },\n      async finalize(buildDirectory) {\n        invariant(viteConfig);\n\n        let { ssr, future } = ctx.reactRouterConfig;\n\n        // if ssr:false is set\n        if (!ssr) {\n          let spaFallback = path.join(buildDirectory, \"__spa-fallback.html\");\n          let index = path.join(buildDirectory, \"index.html\");\n\n          // If the user didn't prerendered `/`, uses the SPA fallback as the main entry point.\n          let finalSpaPath: string | undefined;\n          if (existsSync(spaFallback) && !existsSync(index)) {\n            await rename(spaFallback, index);\n            finalSpaPath = index;\n          } else if (existsSync(spaFallback)) {\n            finalSpaPath = spaFallback;\n          }\n\n          // Log SPA fallback with the final filename\n          if (finalSpaPath) {\n            let prettyPath = path.relative(viteConfig.root, finalSpaPath);\n            if (ctx.prerenderPaths && ctx.prerenderPaths.size > 0) {\n              viteConfig.logger.info(\n                `Prerender (html): SPA Fallback -> ${colors.bold(prettyPath)}`,\n              );\n            } else {\n              viteConfig.logger.info(\n                `SPA Mode: Generated ${colors.bold(prettyPath)}`,\n              );\n            }\n          }\n\n          let serverBuildDirectory = future.v8_viteEnvironmentApi\n            ? this.environment.config?.build?.outDir\n            : (ctx.environmentBuildContext?.options.build?.outDir ??\n              getServerBuildDirectory(ctx.reactRouterConfig));\n\n          // Cleanup - we no longer need the server build assets\n          viteConfig.logger.info(\n            [\n              \"Removing the server build in\",\n              colors.green(serverBuildDirectory),\n              \"due to ssr:false\",\n            ].join(\" \"),\n          );\n\n          // For both SPA mode and prerendering, we can remove the server builds\n          rmSync(serverBuildDirectory, { force: true, recursive: true });\n        }\n      },\n    }),\n    validatePluginOrder(),\n    warnOnClientSourceMaps(),\n  ];\n};\n\nfunction getParentClientNodes(\n  clientModuleGraph: Vite.EnvironmentModuleGraph,\n  module: Vite.EnvironmentModuleNode,\n  seenNodes: Set<string> = new Set(),\n): Vite.EnvironmentModuleNode[] {\n  if (!module.id) {\n    return [];\n  }\n  if (seenNodes.has(module.url)) {\n    return [];\n  }\n  seenNodes.add(module.url);\n\n  let clientModule = clientModuleGraph.getModuleById(module.id);\n  if (clientModule) {\n    return [clientModule];\n  }\n\n  return [...module.importers].flatMap((importer) =>\n    getParentClientNodes(clientModuleGraph, importer, seenNodes),\n  );\n}\n\nfunction addRefreshWrapper(\n  reactRouterConfig: ResolvedReactRouterConfig,\n  code: string,\n  id: string,\n): string {\n  let route = getRoute(reactRouterConfig, id);\n  let acceptExports = route ? CLIENT_NON_COMPONENT_EXPORTS : [];\n  return (\n    REACT_REFRESH_HEADER.replaceAll(\"__SOURCE__\", JSON.stringify(id)) +\n    code +\n    REACT_REFRESH_FOOTER.replaceAll(\"__SOURCE__\", JSON.stringify(id))\n      .replaceAll(\"__ACCEPT_EXPORTS__\", JSON.stringify(acceptExports))\n      .replaceAll(\"__ROUTE_ID__\", JSON.stringify(route?.id))\n  );\n}\n\nconst REACT_REFRESH_HEADER = `\nimport RefreshRuntime from \"${virtualHmrRuntime.id}\";\n\nconst inWebWorker = typeof WorkerGlobalScope !== 'undefined' && self instanceof WorkerGlobalScope;\nlet prevRefreshReg;\nlet prevRefreshSig;\n\nif (import.meta.hot && !inWebWorker) {\n  if (!window.__vite_plugin_react_preamble_installed__) {\n    throw new Error(\n      \"React Router Vite plugin can't detect preamble. Something is wrong.\"\n    );\n  }\n\n  prevRefreshReg = window.$RefreshReg$;\n  prevRefreshSig = window.$RefreshSig$;\n  window.$RefreshReg$ = (type, id) => {\n    RefreshRuntime.register(type, __SOURCE__ + \" \" + id)\n  };\n  window.$RefreshSig$ = RefreshRuntime.createSignatureFunctionForTransform;\n}`.replaceAll(\"\\n\", \"\"); // Header is all on one line so source maps aren't affected\n\nconst REACT_REFRESH_FOOTER = `\nif (import.meta.hot && !inWebWorker) {\n  window.$RefreshReg$ = prevRefreshReg;\n  window.$RefreshSig$ = prevRefreshSig;\n  RefreshRuntime.__hmr_import(import.meta.url).then((currentExports) => {\n    RefreshRuntime.registerExportsForReactRefresh(__SOURCE__, currentExports);\n    import.meta.hot.accept((nextExports) => {\n      if (!nextExports) return;\n      __ROUTE_ID__ && window.__reactRouterRouteModuleUpdates.set(__ROUTE_ID__, nextExports);\n      const invalidateMessage = RefreshRuntime.validateRefreshBoundaryAndEnqueueUpdate(currentExports, nextExports, __ACCEPT_EXPORTS__);\n      if (invalidateMessage) import.meta.hot.invalidate(invalidateMessage);\n    });\n  });\n}`;\n\nfunction getRoute(\n  pluginConfig: ResolvedReactRouterConfig,\n  file: string,\n): RouteManifestEntry | undefined {\n  let vite = getVite();\n  let routePath = vite.normalizePath(\n    path.relative(pluginConfig.appDirectory, file),\n  );\n  let route = Object.values(pluginConfig.routes).find(\n    (r) => vite.normalizePath(r.file) === routePath,\n  );\n  return route;\n}\n\nfunction isRoute(\n  pluginConfig: ResolvedReactRouterConfig,\n  file: string,\n): boolean {\n  return Boolean(getRoute(pluginConfig, file));\n}\n\nasync function getRouteMetadata(\n  cache: Cache,\n  ctx: ReactRouterPluginContext,\n  viteChildCompiler: Vite.ViteDevServer | null,\n  route: RouteManifestEntry,\n  readRouteFile?: () => string | Promise<string>,\n): Promise<ManifestRoute & { url: string }> {\n  let routeFile = route.file;\n  let sourceExports = await getRouteModuleExports(\n    viteChildCompiler,\n    ctx,\n    route.file,\n    readRouteFile,\n  );\n\n  let { hasRouteChunkByExportName } = await detectRouteChunksIfEnabled(\n    cache,\n    ctx,\n    routeFile,\n    { routeFile, readRouteFile, viteChildCompiler },\n  );\n\n  let moduleUrl = combineURLs(\n    ctx.publicPath,\n    `${resolveFileUrl(\n      ctx,\n      resolveRelativeRouteFilePath(route, ctx.reactRouterConfig),\n    )}`,\n  );\n\n  let info: ManifestRoute & { url: string } = {\n    id: route.id,\n    parentId: route.parentId,\n    path: route.path,\n    index: route.index,\n    caseSensitive: route.caseSensitive,\n    url: combineURLs(\n      ctx.publicPath,\n      \"/\" +\n        path.relative(\n          ctx.rootDirectory,\n          resolveRelativeRouteFilePath(route, ctx.reactRouterConfig),\n        ),\n    ),\n    module: `${moduleUrl}?import`, // Ensure the Vite dev server responds with a JS module\n    clientActionModule: hasRouteChunkByExportName.clientAction\n      ? `${getRouteChunkModuleId(moduleUrl, \"clientAction\")}`\n      : undefined,\n    clientLoaderModule: hasRouteChunkByExportName.clientLoader\n      ? `${getRouteChunkModuleId(moduleUrl, \"clientLoader\")}`\n      : undefined,\n    clientMiddlewareModule: hasRouteChunkByExportName.clientMiddleware\n      ? `${getRouteChunkModuleId(moduleUrl, \"clientMiddleware\")}`\n      : undefined,\n    hydrateFallbackModule: hasRouteChunkByExportName.HydrateFallback\n      ? `${getRouteChunkModuleId(moduleUrl, \"HydrateFallback\")}`\n      : undefined,\n    hasAction: sourceExports.includes(\"action\"),\n    hasClientAction: sourceExports.includes(\"clientAction\"),\n    hasLoader: sourceExports.includes(\"loader\"),\n    hasClientLoader: sourceExports.includes(\"clientLoader\"),\n    hasClientMiddleware: sourceExports.includes(\"clientMiddleware\"),\n    hasDefaultExport: sourceExports.includes(\"default\"),\n    hasErrorBoundary: sourceExports.includes(\"ErrorBoundary\"),\n    imports: [],\n  };\n  return info;\n}\n\nfunction isPrerenderingEnabled(\n  reactRouterConfig: ReactRouterPluginContext[\"reactRouterConfig\"],\n) {\n  return (\n    reactRouterConfig.prerender != null && reactRouterConfig.prerender !== false\n  );\n}\n\nfunction isSpaModeEnabled(\n  reactRouterConfig: ReactRouterPluginContext[\"reactRouterConfig\"],\n) {\n  // \"SPA Mode\" is possible in 2 ways:\n  //  - `ssr:false` and no `prerender` config (undefined or null)\n  //  - `ssr:false` and `prerender: false`\n  //    - not an expected config but since we support `prerender:true` we allow it\n  //\n  // \"SPA Mode\" means we will only prerender a *single* `index.html` file which\n  // prerenders only to the root route and thus can hydrate for _any_ path and\n  // the proper routes below the root will be loaded via `route.lazy` during\n  // hydration.\n  //\n  // If `ssr:false` is specified and the user provided a `prerender` config -\n  // then it's no longer a \"SPA\" because we are generating multiple HTML pages.\n  // It's now a MPA and we can prerender down past the root, which unlocks the\n  // ability to use loaders on any routes and prerender the UI with build-time\n  // loaderData\n  return (\n    reactRouterConfig.ssr === false && !isPrerenderingEnabled(reactRouterConfig)\n  );\n}\n\nasync function getPrerenderBuildAndHandler(\n  viteConfig: Vite.ResolvedConfig,\n  serverBuildDirectory: string,\n  serverBuildFile: string,\n) {\n  let serverBuildPath = path.join(serverBuildDirectory, serverBuildFile);\n  let build = await import(url.pathToFileURL(serverBuildPath).toString());\n  let { createRequestHandler: createHandler } = await import(\"react-router\");\n  return {\n    build: build as ServerBuild,\n    handler: createHandler(build, viteConfig.mode),\n  };\n}\n\nasync function handleSpaMode(\n  viteConfig: Vite.ResolvedConfig,\n  reactRouterConfig: ResolvedReactRouterConfig,\n  serverBuildDirectory: string,\n  serverBuildFile: string,\n  clientBuildDirectory: string,\n) {\n  let { build, handler } = await getPrerenderBuildAndHandler(\n    viteConfig,\n    serverBuildDirectory,\n    serverBuildFile,\n  );\n  let request = new Request(`http://localhost${reactRouterConfig.basename}`, {\n    headers: {\n      // Enable SPA mode in the server runtime and only render down to the root\n      \"X-React-Router-SPA-Mode\": \"yes\",\n    },\n  });\n  let response = await handler(request);\n  let html = await response.text();\n\n  // If the user prerendered `/`, then we write this out to a separate file\n  // they can serve. Otherwise, it can be the main entry point.\n  let isPrerenderSpaFallback = build.prerender.includes(\"/\");\n  let filename = isPrerenderSpaFallback ? \"__spa-fallback.html\" : \"index.html\";\n  if (response.status !== 200) {\n    if (isPrerenderSpaFallback) {\n      throw new Error(\n        `Prerender: Received a ${response.status} status code from ` +\n          `\\`entry.server.tsx\\` while prerendering your \\`${filename}\\` file.\\n` +\n          html,\n      );\n    } else {\n      throw new Error(\n        `SPA Mode: Received a ${response.status} status code from ` +\n          `\\`entry.server.tsx\\` while prerendering your \\`${filename}\\` file.\\n` +\n          html,\n      );\n    }\n  }\n\n  if (\n    !html.includes(\"window.__reactRouterContext =\") ||\n    !html.includes(\"window.__reactRouterRouteModules =\")\n  ) {\n    throw new Error(\n      \"SPA Mode: Did you forget to include `<Scripts/>` in your root route? \" +\n        \"Your pre-rendered HTML cannot hydrate without `<Scripts />`.\",\n    );\n  }\n\n  // Write out the HTML file for the SPA\n  await writeFile(path.join(clientBuildDirectory, filename), html);\n  let prettyDir = path.relative(viteConfig.root, clientBuildDirectory);\n  let prettyPath = path.join(prettyDir, filename);\n  if (build.prerender.length > 0) {\n    viteConfig.logger.info(\n      `Prerender (html): SPA Fallback -> ${colors.bold(prettyPath)}`,\n    );\n  } else {\n    viteConfig.logger.info(`SPA Mode: Generated ${colors.bold(prettyPath)}`);\n  }\n}\n\nasync function handlePrerender(\n  viteConfig: Vite.ResolvedConfig,\n  reactRouterConfig: ResolvedReactRouterConfig,\n  serverBuildDirectory: string,\n  serverBuildPath: string,\n  clientBuildDirectory: string,\n) {\n  let { build, handler } = await getPrerenderBuildAndHandler(\n    viteConfig,\n    serverBuildDirectory,\n    serverBuildPath,\n  );\n\n  let routes = createPrerenderRoutes(reactRouterConfig.routes);\n  for (let path of build.prerender) {\n    let matches = matchRoutes(routes, `/${path}/`.replace(/^\\/\\/+/, \"/\"));\n    if (!matches) {\n      throw new Error(\n        `Unable to prerender path because it does not match any routes: ${path}`,\n      );\n    }\n  }\n\n  let buildRoutes = createPrerenderRoutes(build.routes);\n\n  let prerenderSinglePath = async (path: string) => {\n    // Ensure we have a leading slash for matching\n    let matches = matchRoutes(buildRoutes, `/${path}/`.replace(/^\\/\\/+/, \"/\"));\n    if (!matches) {\n      return;\n    }\n    // When prerendering a resource route, we don't want to pass along the\n    // `.data` file since we want to prerender the raw Response returned from\n    // the loader.  Presumably this is for routes where a file extension is\n    // already included, such as `app/routes/items[.json].tsx` that will\n    // render into `/items.json`\n    let leafRoute = matches ? matches[matches.length - 1].route : null;\n    let manifestRoute = leafRoute ? build.routes[leafRoute.id]?.module : null;\n    let isResourceRoute =\n      manifestRoute && !manifestRoute.default && !manifestRoute.ErrorBoundary;\n\n    if (isResourceRoute) {\n      invariant(leafRoute);\n      invariant(manifestRoute);\n      if (manifestRoute.loader) {\n        // Prerender a .data file for turbo-stream consumption\n        await prerenderData(\n          handler,\n          path,\n          [leafRoute.id],\n          clientBuildDirectory,\n          reactRouterConfig,\n          viteConfig,\n        );\n        // Prerender a raw file for external consumption\n        await prerenderResourceRoute(\n          handler,\n          path,\n          clientBuildDirectory,\n          reactRouterConfig,\n          viteConfig,\n        );\n      } else {\n        viteConfig.logger.warn(\n          `⚠️ Skipping prerendering for resource route without a loader: ${leafRoute?.id}`,\n        );\n      }\n    } else {\n      let hasLoaders = matches.some(\n        (m) => build.assets.routes[m.route.id]?.hasLoader,\n      );\n      let data: string | undefined;\n      if (!isResourceRoute && hasLoaders) {\n        data = await prerenderData(\n          handler,\n          path,\n          null,\n          clientBuildDirectory,\n          reactRouterConfig,\n          viteConfig,\n        );\n      }\n\n      await prerenderRoute(\n        handler,\n        path,\n        clientBuildDirectory,\n        reactRouterConfig,\n        viteConfig,\n        data\n          ? {\n              headers: {\n                \"X-React-Router-Prerender-Data\": encodeURI(data),\n              },\n            }\n          : undefined,\n      );\n    }\n  };\n\n  let concurrency = 1;\n  let { prerender } = reactRouterConfig;\n  if (typeof prerender === \"object\" && \"unstable_concurrency\" in prerender) {\n    concurrency = prerender.unstable_concurrency ?? 1;\n  }\n\n  const pMap = await import(\"p-map\");\n  await pMap.default(build.prerender, prerenderSinglePath, { concurrency });\n}\n\nfunction getStaticPrerenderPaths(routes: DataRouteObject[]) {\n  // Always start with the root/index route included\n  let paths: string[] = [\"/\"];\n  let paramRoutes: string[] = [];\n\n  // Then recursively add any new path defined by the tree\n  function recurse(subtree: typeof routes, prefix = \"\") {\n    for (let route of subtree) {\n      let newPath = [prefix, route.path].join(\"/\").replace(/\\/\\/+/g, \"/\");\n      if (route.path) {\n        let segments = route.path.split(\"/\");\n        if (segments.some((s) => s.startsWith(\":\") || s === \"*\")) {\n          paramRoutes.push(route.path);\n        } else {\n          paths.push(newPath);\n        }\n      }\n      if (route.children) {\n        recurse(route.children, newPath);\n      }\n    }\n  }\n  recurse(routes);\n\n  // Clean double slashes and remove trailing slashes\n  return {\n    paths: paths.map((p) => p.replace(/\\/\\/+/g, \"/\").replace(/(.+)\\/$/, \"$1\")),\n    paramRoutes,\n  };\n}\n\nasync function prerenderData(\n  handler: RequestHandler,\n  prerenderPath: string,\n  onlyRoutes: string[] | null,\n  clientBuildDirectory: string,\n  reactRouterConfig: ResolvedReactRouterConfig,\n  viteConfig: Vite.ResolvedConfig,\n  requestInit?: RequestInit,\n) {\n  let dataRequestPath: string;\n  if (reactRouterConfig.future.unstable_trailingSlashAwareDataRequests) {\n    if (prerenderPath.endsWith(\"/\")) {\n      dataRequestPath = `${prerenderPath}_.data`;\n    } else {\n      dataRequestPath = `${prerenderPath}.data`;\n    }\n  } else {\n    if (prerenderPath === \"/\") {\n      dataRequestPath = \"/_root.data\";\n    } else {\n      dataRequestPath = `${prerenderPath.replace(/\\/$/, \"\")}.data`;\n    }\n  }\n  let normalizedPath =\n    `${reactRouterConfig.basename}${dataRequestPath}`.replace(/\\/\\/+/g, \"/\");\n  let url = new URL(`http://localhost${normalizedPath}`);\n  if (onlyRoutes?.length) {\n    url.searchParams.set(\"_routes\", onlyRoutes.join(\",\"));\n  }\n  let request = new Request(url, requestInit);\n  let response = await handler(request);\n  let data = await response.text();\n\n  // 202 is used for `.data` redirects\n  if (response.status !== 200 && response.status !== 202) {\n    throw new Error(\n      `Prerender (data): Received a ${response.status} status code from ` +\n        `\\`entry.server.tsx\\` while prerendering the \\`${prerenderPath}\\` ` +\n        `path.\\n${normalizedPath}`,\n    );\n  }\n\n  // Write out the .data file\n  let outfile = path.join(clientBuildDirectory, ...normalizedPath.split(\"/\"));\n  await mkdir(path.dirname(outfile), { recursive: true });\n  await writeFile(outfile, data);\n  viteConfig.logger.info(\n    `Prerender (data): ${prerenderPath} -> ${colors.bold(\n      path.relative(viteConfig.root, outfile),\n    )}`,\n  );\n  return data;\n}\n\nlet redirectStatusCodes = new Set([301, 302, 303, 307, 308]);\n\nasync function prerenderRoute(\n  handler: RequestHandler,\n  prerenderPath: string,\n  clientBuildDirectory: string,\n  reactRouterConfig: ResolvedReactRouterConfig,\n  viteConfig: Vite.ResolvedConfig,\n  requestInit?: RequestInit,\n) {\n  let normalizedPath = `${reactRouterConfig.basename}${prerenderPath}/`.replace(\n    /\\/\\/+/g,\n    \"/\",\n  );\n  let request = new Request(`http://localhost${normalizedPath}`, requestInit);\n  let response = await handler(request);\n  let html = await response.text();\n\n  if (redirectStatusCodes.has(response.status)) {\n    // This isn't ideal but gets the job done as a fallback if the user can't\n    // implement proper redirects via .htaccess or something else.  This is the\n    // approach used by Astro as well, so there's some precedent.\n    // https://github.com/withastro/roadmap/issues/466\n    // https://github.com/withastro/astro/blob/main/packages/astro/src/core/routing/3xx.ts\n    let location = response.headers.get(\"Location\");\n    // A short delay causes Google to interpret the redirect as temporary.\n    // https://developers.google.com/search/docs/crawling-indexing/301-redirects#metarefresh\n    let delay = response.status === 302 ? 2 : 0;\n    html = `<!doctype html>\n<head>\n<title>Redirecting to: ${location}</title>\n<meta http-equiv=\"refresh\" content=\"${delay};url=${location}\">\n<meta name=\"robots\" content=\"noindex\">\n</head>\n<body>\n\t<a href=\"${location}\">\n    Redirecting from <code>${normalizedPath}</code> to <code>${location}</code>\n  </a>\n</body>\n</html>`;\n  } else if (response.status !== 200) {\n    throw new Error(\n      `Prerender (html): Received a ${response.status} status code from ` +\n        `\\`entry.server.tsx\\` while prerendering the \\`${normalizedPath}\\` ` +\n        `path.\\n${html}`,\n    );\n  }\n\n  // Write out the HTML file\n  let outfile = path.join(\n    clientBuildDirectory,\n    ...normalizedPath.split(\"/\"),\n    \"index.html\",\n  );\n  await mkdir(path.dirname(outfile), { recursive: true });\n  await writeFile(outfile, html);\n  viteConfig.logger.info(\n    `Prerender (html): ${prerenderPath} -> ${colors.bold(\n      path.relative(viteConfig.root, outfile),\n    )}`,\n  );\n}\n\nasync function prerenderResourceRoute(\n  handler: RequestHandler,\n  prerenderPath: string,\n  clientBuildDirectory: string,\n  reactRouterConfig: ResolvedReactRouterConfig,\n  viteConfig: Vite.ResolvedConfig,\n  requestInit?: RequestInit,\n) {\n  let normalizedPath = `${reactRouterConfig.basename}${prerenderPath}/`\n    .replace(/\\/\\/+/g, \"/\")\n    .replace(/\\/$/g, \"\");\n  let request = new Request(`http://localhost${normalizedPath}`, requestInit);\n  let response = await handler(request);\n  let content = Buffer.from(await response.arrayBuffer());\n\n  if (response.status !== 200) {\n    throw new Error(\n      `Prerender (resource): Received a ${response.status} status code from ` +\n        `\\`entry.server.tsx\\` while prerendering the \\`${normalizedPath}\\` ` +\n        `path.\\n${content.toString(\"utf8\")}`,\n    );\n  }\n\n  // Write out the resource route file\n  let outfile = path.join(clientBuildDirectory, ...normalizedPath.split(\"/\"));\n  await mkdir(path.dirname(outfile), { recursive: true });\n  await writeFile(outfile, content);\n  viteConfig.logger.info(\n    `Prerender (resource): ${prerenderPath} -> ${colors.bold(\n      path.relative(viteConfig.root, outfile),\n    )}`,\n  );\n}\n\n// Allows us to use both the RouteManifest and the ServerRouteManifest from the build\nexport interface GenericRouteManifest {\n  [routeId: string]: Omit<RouteManifestEntry, \"file\"> | undefined;\n}\n\nexport async function getPrerenderPaths(\n  prerender: ResolvedReactRouterConfig[\"prerender\"],\n  ssr: ResolvedReactRouterConfig[\"ssr\"],\n  routes: GenericRouteManifest,\n  logWarning = false,\n): Promise<string[]> {\n  if (prerender == null || prerender === false) {\n    return [];\n  }\n\n  let pathsConfig: PrerenderPaths;\n\n  if (typeof prerender === \"object\" && \"paths\" in prerender) {\n    pathsConfig = prerender.paths;\n  } else {\n    pathsConfig = prerender;\n  }\n\n  if (pathsConfig === false) {\n    return [];\n  }\n\n  let prerenderRoutes = createPrerenderRoutes(routes);\n\n  if (pathsConfig === true) {\n    let { paths, paramRoutes } = getStaticPrerenderPaths(prerenderRoutes);\n    if (logWarning && !ssr && paramRoutes.length > 0) {\n      console.warn(\n        colors.yellow(\n          [\n            \"⚠️ Paths with dynamic/splat params cannot be prerendered when \" +\n              \"using `prerender: true`. You may want to use the `prerender()` \" +\n              \"API to prerender the following paths:\",\n            ...paramRoutes.map((p) => \"  - \" + p),\n          ].join(\"\\n\"),\n        ),\n      );\n    }\n    return paths;\n  }\n\n  if (typeof pathsConfig === \"function\") {\n    let paths = await pathsConfig({\n      getStaticPaths: () => getStaticPrerenderPaths(prerenderRoutes).paths,\n    });\n    return paths;\n  }\n\n  return pathsConfig;\n}\n\n// Note: Duplicated from react-router/lib/server-runtime\nfunction groupRoutesByParentId(manifest: GenericRouteManifest) {\n  let routes: Record<string, Omit<RouteManifestEntry, \"file\">[]> = {};\n\n  Object.values(manifest).forEach((route) => {\n    if (route) {\n      let parentId = route.parentId || \"\";\n      if (!routes[parentId]) {\n        routes[parentId] = [];\n      }\n      routes[parentId].push(route);\n    }\n  });\n\n  return routes;\n}\n\n// Create a skeleton route tree of paths\nfunction createPrerenderRoutes(\n  manifest: GenericRouteManifest,\n  parentId: string = \"\",\n  routesByParentId = groupRoutesByParentId(manifest),\n): DataRouteObject[] {\n  return (routesByParentId[parentId] || []).map((route) => {\n    let commonRoute = {\n      id: route.id,\n      path: route.path,\n    };\n\n    if (route.index) {\n      return {\n        index: true,\n        ...commonRoute,\n      };\n    }\n\n    return {\n      children: createPrerenderRoutes(manifest, route.id, routesByParentId),\n      ...commonRoute,\n    };\n  });\n}\n\nasync function validateSsrFalsePrerenderExports(\n  viteConfig: Vite.ResolvedConfig,\n  ctx: ReactRouterPluginContext,\n  manifest: ReactRouterManifest,\n  viteChildCompiler: Vite.ViteDevServer | null,\n) {\n  let prerenderPaths = await getPrerenderPaths(\n    ctx.reactRouterConfig.prerender,\n    ctx.reactRouterConfig.ssr,\n    manifest.routes,\n    true,\n  );\n\n  if (prerenderPaths.length === 0) {\n    return;\n  }\n\n  // Identify all routes used by a prerender path\n  let prerenderRoutes = createPrerenderRoutes(manifest.routes);\n  let prerenderedRoutes = new Set<string>();\n  for (let path of prerenderPaths) {\n    // Ensure we have a leading slash for matching\n    let matches = matchRoutes(\n      prerenderRoutes,\n      `/${path}/`.replace(/^\\/\\/+/, \"/\"),\n    );\n    invariant(\n      matches,\n      `Unable to prerender path because it does not match any routes: ${path}`,\n    );\n    matches.forEach((m) => prerenderedRoutes.add(m.route.id));\n  }\n\n  // Identify invalid exports\n  let errors: string[] = [];\n  let routeExports = await getRouteManifestModuleExports(\n    viteChildCompiler,\n    ctx,\n  );\n  for (let [routeId, route] of Object.entries(manifest.routes)) {\n    let invalidApis: string[] = [];\n    invariant(route, \"Expected a route object in validateSsrFalseExports\");\n    let exports = routeExports[route.id];\n\n    // `headers`/`action` are never valid without SSR\n    if (exports.includes(\"headers\")) invalidApis.push(\"headers\");\n    if (exports.includes(\"action\")) invalidApis.push(\"action\");\n    if (invalidApis.length > 0) {\n      errors.push(\n        `Prerender: ${invalidApis.length} invalid route export(s) in \\`${route.id}\\` ` +\n          \"when pre-rendering with `ssr:false`: \" +\n          `${invalidApis.map((a) => `\\`${a}\\``).join(\", \")}.  ` +\n          \"See https://reactrouter.com/how-to/pre-rendering#invalid-exports for more information.\",\n      );\n    }\n\n    // `loader` is only valid if the route is matched by a `prerender` path\n    if (!prerenderedRoutes.has(routeId)) {\n      if (exports.includes(\"loader\")) {\n        errors.push(\n          `Prerender: 1 invalid route export in \\`${route.id}\\` ` +\n            \"when pre-rendering with `ssr:false`: `loader`. \" +\n            \"See https://reactrouter.com/how-to/pre-rendering#invalid-exports for more information.\",\n        );\n      }\n\n      let parentRoute = route.parentId ? manifest.routes[route.parentId] : null;\n      while (parentRoute && parentRoute.id !== \"root\") {\n        if (parentRoute.hasLoader && !parentRoute.hasClientLoader) {\n          errors.push(\n            `Prerender: 1 invalid route export in \\`${parentRoute.id}\\` when ` +\n              \"pre-rendering with `ssr:false`: `loader`. \" +\n              \"See https://reactrouter.com/how-to/pre-rendering#invalid-exports for more information.\",\n          );\n        }\n        parentRoute =\n          parentRoute.parentId && parentRoute.parentId !== \"root\"\n            ? manifest.routes[parentRoute.parentId]\n            : null;\n      }\n    }\n  }\n\n  if (errors.length > 0) {\n    viteConfig.logger.error(colors.red(errors.join(\"\\n\")));\n    throw new Error(\n      \"Invalid route exports found when prerendering with `ssr:false`\",\n    );\n  }\n}\n\nfunction getAddressableRoutes(routes: RouteManifest): RouteManifestEntry[] {\n  let nonAddressableIds = new Set<string>();\n\n  for (let id in routes) {\n    let route = routes[id];\n\n    // We omit the parent route of index routes since the index route takes ownership of its parent's path\n    if (route.index) {\n      invariant(\n        route.parentId,\n        `Expected index route \"${route.id}\" to have \"parentId\" set`,\n      );\n      nonAddressableIds.add(route.parentId);\n    }\n\n    // We omit pathless routes since they can only be addressed via descendant routes\n    if (typeof route.path !== \"string\" && !route.index) {\n      nonAddressableIds.add(id);\n    }\n  }\n\n  return Object.values(routes).filter(\n    (route) => !nonAddressableIds.has(route.id),\n  );\n}\n\nfunction getRouteBranch(routes: RouteManifest, routeId: string) {\n  let branch: RouteManifestEntry[] = [];\n  let currentRouteId: string | undefined = routeId;\n\n  while (currentRouteId) {\n    let route: RouteManifestEntry = routes[currentRouteId];\n    invariant(route, `Missing route for ${currentRouteId}`);\n    branch.push(route);\n    currentRouteId = route.parentId;\n  }\n\n  return branch.reverse();\n}\n\nfunction getServerBundleIds(ctx: ReactRouterPluginContext) {\n  return ctx.buildManifest?.serverBundles\n    ? Object.keys(ctx.buildManifest.serverBundles)\n    : undefined;\n}\n\nfunction getRoutesByServerBundleId(\n  buildManifest: BuildManifest,\n): Record<string, RouteManifest> {\n  if (!buildManifest.routeIdToServerBundleId) {\n    return {};\n  }\n\n  let routesByServerBundleId: Record<string, RouteManifest> = {};\n\n  for (let [routeId, serverBundleId] of Object.entries(\n    buildManifest.routeIdToServerBundleId,\n  )) {\n    routesByServerBundleId[serverBundleId] ??= {};\n    let branch = getRouteBranch(buildManifest.routes, routeId);\n    for (let route of branch) {\n      routesByServerBundleId[serverBundleId][route.id] = route;\n    }\n  }\n\n  return routesByServerBundleId;\n}\n\ntype ResolveRouteFileCodeInput =\n  | string\n  | {\n      routeFile: string;\n      readRouteFile?: () => string | Promise<string>;\n      viteChildCompiler: Vite.ViteDevServer | null;\n    };\nconst resolveRouteFileCode = async (\n  ctx: ReactRouterPluginContext,\n  input: ResolveRouteFileCodeInput,\n): Promise<string> => {\n  if (typeof input === \"string\") return input;\n  invariant(input.viteChildCompiler);\n  return await compileRouteFile(\n    input.viteChildCompiler,\n    ctx,\n    input.routeFile,\n    input.readRouteFile,\n  );\n};\n\nfunction isRootRouteModuleId(ctx: ReactRouterPluginContext, id: string) {\n  return (\n    normalizeRelativeFilePath(id, ctx.reactRouterConfig) ===\n    ctx.reactRouterConfig.routes.root.file\n  );\n}\n\nasync function detectRouteChunksIfEnabled(\n  cache: Cache,\n  ctx: ReactRouterPluginContext,\n  id: string,\n  input: ResolveRouteFileCodeInput,\n): Promise<ReturnType<typeof detectRouteChunks>> {\n  function noRouteChunks(): ReturnType<typeof detectRouteChunks> {\n    return {\n      chunkedExports: [],\n      hasRouteChunks: false,\n      hasRouteChunkByExportName: {\n        clientAction: false,\n        clientLoader: false,\n        clientMiddleware: false,\n        HydrateFallback: false,\n      },\n    };\n  }\n  if (!ctx.reactRouterConfig.future.v8_splitRouteModules) {\n    return noRouteChunks();\n  }\n\n  // If this is the root route, we disable chunking since the chunks would never\n  // be loaded on demand during navigation. Because the root route is matched\n  // for all requests, all of its chunks would always be loaded up front during\n  // the initial page load. Instead of firing off multiple requests to resolve\n  // the root route code, we want it to be downloaded in a single request.\n  if (isRootRouteModuleId(ctx, id)) {\n    return noRouteChunks();\n  }\n\n  let code = await resolveRouteFileCode(ctx, input);\n  if (!routeChunkExportNames.some((exportName) => code.includes(exportName))) {\n    return noRouteChunks();\n  }\n\n  let cacheKey =\n    normalizeRelativeFilePath(id, ctx.reactRouterConfig) +\n    (typeof input === \"string\" ? \"\" : \"?read\");\n\n  return detectRouteChunks(code, cache, cacheKey);\n}\n\nasync function getRouteChunkIfEnabled(\n  cache: Cache,\n  ctx: ReactRouterPluginContext,\n  id: string,\n  chunkName: RouteChunkName,\n  input: ResolveRouteFileCodeInput,\n): Promise<ReturnType<typeof getRouteChunkCode> | null> {\n  if (!ctx.reactRouterConfig.future.v8_splitRouteModules) {\n    return null;\n  }\n\n  let code = await resolveRouteFileCode(ctx, input);\n\n  let cacheKey =\n    normalizeRelativeFilePath(id, ctx.reactRouterConfig) +\n    (typeof input === \"string\" ? \"\" : \"?read\");\n\n  return getRouteChunkCode(code, chunkName, cache, cacheKey);\n}\n\nfunction validateRouteChunks({\n  ctx,\n  id,\n  valid,\n}: {\n  ctx: ReactRouterPluginContext;\n  id: string;\n  valid: Record<Exclude<RouteChunkName, \"main\">, boolean>;\n}): void {\n  if (isRootRouteModuleId(ctx, id)) {\n    return;\n  }\n\n  let invalidChunks = Object.entries(valid)\n    .filter(([_, isValid]) => !isValid)\n    .map(([chunkName]) => chunkName);\n\n  if (invalidChunks.length === 0) {\n    return;\n  }\n\n  let plural = invalidChunks.length > 1;\n\n  throw new Error(\n    [\n      `Error splitting route module: ${normalizeRelativeFilePath(\n        id,\n        ctx.reactRouterConfig,\n      )}`,\n\n      invalidChunks.map((name) => `- ${name}`).join(\"\\n\"),\n\n      `${plural ? \"These exports\" : \"This export\"} could not be split into ${\n        plural ? \"their own chunks\" : \"its own chunk\"\n      } because ${\n        plural ? \"they share\" : \"it shares\"\n      } code with other exports. You should extract any shared code into its own module and then import it within the route module.`,\n    ].join(\"\\n\\n\"),\n  );\n}\n\nexport async function cleanBuildDirectory(\n  viteConfig: Vite.ResolvedConfig,\n  ctx: ReactRouterPluginContext,\n) {\n  let buildDirectory = ctx.reactRouterConfig.buildDirectory;\n  let isWithinRoot = () => {\n    let relativePath = path.relative(ctx.rootDirectory, buildDirectory);\n    return !relativePath.startsWith(\"..\") && !path.isAbsolute(relativePath);\n  };\n\n  if (viteConfig.build.emptyOutDir ?? isWithinRoot()) {\n    await rm(buildDirectory, { force: true, recursive: true });\n  }\n}\n\nexport async function cleanViteManifests(\n  environmentsOptions: Record<string, EnvironmentOptions>,\n  ctx: ReactRouterPluginContext,\n) {\n  let viteManifestPaths = Object.entries(environmentsOptions).map(\n    ([environmentName, options]) => {\n      let outDir = options.build?.outDir;\n      invariant(outDir, `Expected build.outDir for ${environmentName}`);\n      return path.join(outDir, \".vite/manifest.json\");\n    },\n  );\n  await Promise.all(\n    viteManifestPaths.map(async (viteManifestPath) => {\n      let manifestExists = existsSync(viteManifestPath);\n      if (!manifestExists) return;\n\n      // Delete the original Vite manifest file if consumer doesn't want it\n      if (!ctx.viteManifestEnabled) {\n        await rm(viteManifestPath, { force: true, recursive: true });\n      }\n\n      // Remove .vite dir if it's now empty\n      let viteDir = path.dirname(viteManifestPath);\n      let viteDirFiles = await readdir(viteDir, { recursive: true });\n      if (viteDirFiles.length === 0) {\n        await rm(viteDir, { force: true, recursive: true });\n      }\n    }),\n  );\n}\n\nexport async function getBuildManifest({\n  reactRouterConfig,\n  rootDirectory,\n}: {\n  reactRouterConfig: ResolvedReactRouterConfig;\n  rootDirectory: string;\n}): Promise<BuildManifest> {\n  let { routes, serverBundles, appDirectory } = reactRouterConfig;\n\n  if (!serverBundles) {\n    return { routes };\n  }\n\n  let { normalizePath } = await import(\"vite\");\n  let serverBuildDirectory = getServerBuildDirectory(reactRouterConfig);\n  let resolvedAppDirectory = path.resolve(rootDirectory, appDirectory);\n  let rootRelativeRoutes = Object.fromEntries(\n    Object.entries(routes).map(([id, route]) => {\n      let filePath = path.join(resolvedAppDirectory, route.file);\n      let rootRelativeFilePath = normalizePath(\n        path.relative(rootDirectory, filePath),\n      );\n      return [id, { ...route, file: rootRelativeFilePath }];\n    }),\n  );\n\n  let buildManifest: BuildManifest = {\n    serverBundles: {},\n    routeIdToServerBundleId: {},\n    routes: rootRelativeRoutes,\n  };\n\n  await Promise.all(\n    getAddressableRoutes(routes).map(async (route) => {\n      let branch = getRouteBranch(routes, route.id);\n      let serverBundleId = await serverBundles({\n        branch: branch.map((route) =>\n          configRouteToBranchRoute({\n            ...route,\n            // Ensure absolute paths are passed to the serverBundles function\n            file: path.join(resolvedAppDirectory, route.file),\n          }),\n        ),\n      });\n      if (typeof serverBundleId !== \"string\") {\n        throw new Error(`The \"serverBundles\" function must return a string`);\n      }\n      if (reactRouterConfig.future.v8_viteEnvironmentApi) {\n        // Server bundle IDs must be valid Vite environment names, so hyphens are not allowed\n        if (!/^[a-zA-Z0-9_]+$/.test(serverBundleId)) {\n          throw new Error(\n            `The \"serverBundles\" function must only return strings containing alphanumeric characters and underscores.`,\n          );\n        }\n      } else {\n        if (!/^[a-zA-Z0-9-_]+$/.test(serverBundleId)) {\n          throw new Error(\n            `The \"serverBundles\" function must only return strings containing alphanumeric characters, hyphens and underscores.`,\n          );\n        }\n      }\n      buildManifest.routeIdToServerBundleId[route.id] = serverBundleId;\n\n      buildManifest.serverBundles[serverBundleId] ??= {\n        id: serverBundleId,\n        file: normalizePath(\n          path.join(\n            path.relative(\n              rootDirectory,\n              path.join(serverBuildDirectory, serverBundleId),\n            ),\n            reactRouterConfig.serverBuildFile,\n          ),\n        ),\n      };\n    }),\n  );\n\n  return buildManifest;\n}\n\nfunction mergeEnvironmentOptions(\n  base: EnvironmentOptions,\n  ...overrides: EnvironmentOptions[]\n): EnvironmentOptions {\n  let vite = getVite();\n\n  return overrides.reduce(\n    (merged, override) => vite.mergeConfig(merged, override, false),\n    base,\n  );\n}\n\nexport async function getEnvironmentOptionsResolvers(\n  ctx: ReactRouterPluginContext,\n  viteCommand: Vite.ResolvedConfig[\"command\"],\n): Promise<EnvironmentOptionsResolvers> {\n  let { serverBuildFile, serverModuleFormat } = ctx.reactRouterConfig;\n\n  let packageRoot = path.dirname(\n    require.resolve(\"@react-router/dev/package.json\"),\n  );\n  let { moduleSyncEnabled } = await import(\n    `file:///${path.join(packageRoot, \"module-sync-enabled/index.mjs\")}`\n  );\n  let vite = getVite();\n\n  function getBaseOptions({\n    viteUserConfig,\n  }: {\n    viteUserConfig: Vite.UserConfig;\n  }): EnvironmentOptions {\n    // This is a workaround for type errors when running in vite-ecosystem-ci\n    // against rolldown-vite since \"preserveEntrySignatures\" is not yet\n    // supported. We're doing this instead of using `ts-ignore` so we're still\n    // type checking against regular Vite build options. Once it's supported,\n    // this custom type can be removed and the build config can be inlined.\n    type RollupOptionsWithPreserveEntrySignatures =\n      Vite.BuildOptions[\"rollupOptions\"] extends {\n        preserveEntrySignatures: any;\n      }\n        ? Vite.BuildOptions[\"rollupOptions\"]\n        : Vite.BuildOptions[\"rollupOptions\"] & {\n            // We hard-code the one value we're using. If it's not valid in\n            // Rollup, the build will fail.\n            preserveEntrySignatures: \"exports-only\";\n          };\n    const rollupOptions: RollupOptionsWithPreserveEntrySignatures = {\n      preserveEntrySignatures: \"exports-only\",\n      // Silence Rollup \"use client\" warnings\n      // Adapted from https://github.com/vitejs/vite-plugin-react/pull/144\n      onwarn(warning, defaultHandler) {\n        if (\n          warning.code === \"MODULE_LEVEL_DIRECTIVE\" &&\n          warning.message.includes(\"use client\")\n        ) {\n          return;\n        }\n        let userHandler = viteUserConfig.build?.rollupOptions?.onwarn;\n        if (userHandler) {\n          userHandler(warning, defaultHandler);\n        } else {\n          defaultHandler(warning);\n        }\n      },\n    };\n\n    return {\n      build: {\n        cssMinify: viteUserConfig.build?.cssMinify ?? true,\n        manifest: true, // The manifest is enabled for all builds to detect SSR-only assets\n        rollupOptions,\n      },\n    };\n  }\n\n  function getBaseServerOptions({\n    viteUserConfig,\n  }: {\n    viteUserConfig: Vite.UserConfig;\n  }): EnvironmentOptions {\n    // We're using the module-sync condition, but Vite\n    // doesn't support it by default.\n    // See https://github.com/vitest-dev/vitest/issues/7692\n    let maybeModuleSyncConditions: string[] = [\n      ...(moduleSyncEnabled ? [\"module-sync\"] : []),\n    ];\n\n    let maybeDevelopmentConditions =\n      viteCommand === \"build\" ? [] : [\"development\"];\n\n    // This is a compatibility layer for Vite 5. Default conditions were\n    // automatically added to any custom conditions in Vite 5, but Vite 6\n    // removed this behavior. Instead, the default conditions are overridden\n    // by any custom conditions. If we wish to retain the default\n    // conditions, we need to manually merge them using the provided default\n    // conditions arrays exported from Vite. In Vite 5, these default\n    // conditions arrays do not exist.\n    // https://vite.dev/guide/migration.html#default-value-for-resolve-conditions\n    let maybeDefaultServerConditions = vite.defaultServerConditions || [];\n\n    // There is no helpful export with the default external conditions (see\n    // https://github.com/vitejs/vite/pull/20279 for more details). So, for now,\n    // we are hardcording the default here.\n    let defaultExternalConditions = [\"node\"];\n\n    let baseConditions = [\n      ...maybeDevelopmentConditions,\n      ...maybeModuleSyncConditions,\n    ];\n\n    return mergeEnvironmentOptions(getBaseOptions({ viteUserConfig }), {\n      resolve: {\n        external:\n          // If `v8_viteEnvironmentApi` is `true`, `resolve.external` is set in the `configEnvironment` hook\n          ctx.reactRouterConfig.future.v8_viteEnvironmentApi\n            ? undefined\n            : ssrExternals,\n        conditions: [...baseConditions, ...maybeDefaultServerConditions],\n        externalConditions: [...baseConditions, ...defaultExternalConditions],\n      },\n      build: {\n        // We move SSR-only assets to client assets. Note that the\n        // SSR build can also emit code-split JS files (e.g., by\n        // dynamic import) under the same assets directory\n        // regardless of \"ssrEmitAssets\" option, so we also need to\n        // keep these JS files to be kept as-is.\n        ssrEmitAssets: true,\n        copyPublicDir: false, // The client only uses assets in the public directory\n        rollupOptions: {\n          input:\n            (ctx.reactRouterConfig.future.v8_viteEnvironmentApi\n              ? viteUserConfig.environments?.ssr?.build?.rollupOptions?.input\n              : viteUserConfig.build?.rollupOptions?.input) ??\n            virtual.serverBuild.id,\n          output: {\n            entryFileNames: serverBuildFile,\n            format: serverModuleFormat,\n          },\n        },\n      },\n    });\n  }\n\n  let environmentOptionsResolvers: EnvironmentOptionsResolvers = {\n    client: ({ viteUserConfig }) =>\n      mergeEnvironmentOptions(getBaseOptions({ viteUserConfig }), {\n        build: {\n          rollupOptions: {\n            input: [\n              ctx.entryClientFilePath,\n              ...Object.values(ctx.reactRouterConfig.routes).flatMap(\n                (route) => {\n                  let routeFilePath = path.resolve(\n                    ctx.reactRouterConfig.appDirectory,\n                    route.file,\n                  );\n\n                  let isRootRoute =\n                    route.file === ctx.reactRouterConfig.routes.root.file;\n\n                  let code = readFileSync(routeFilePath, \"utf-8\");\n\n                  return [\n                    `${routeFilePath}${BUILD_CLIENT_ROUTE_QUERY_STRING}`,\n                    ...(ctx.reactRouterConfig.future.v8_splitRouteModules &&\n                    !isRootRoute\n                      ? routeChunkExportNames.map((exportName) =>\n                          code.includes(exportName)\n                            ? getRouteChunkModuleId(routeFilePath, exportName)\n                            : null,\n                        )\n                      : []),\n                  ].filter(isNonNullable);\n                },\n              ),\n            ],\n            output: (ctx.reactRouterConfig.future.v8_viteEnvironmentApi\n              ? viteUserConfig?.environments?.client?.build?.rollupOptions\n                  ?.output\n              : viteUserConfig?.build?.rollupOptions?.output) ?? {\n              entryFileNames: ({ moduleIds }) => {\n                let routeChunkModuleId = moduleIds.find(isRouteChunkModuleId);\n                let routeChunkName = routeChunkModuleId\n                  ? getRouteChunkNameFromModuleId(routeChunkModuleId)?.replace(\n                      \"unstable_\",\n                      \"\",\n                    )\n                  : null;\n                let routeChunkSuffix = routeChunkName\n                  ? `-${kebabCase(routeChunkName)}`\n                  : \"\";\n                let assetsDir =\n                  (ctx.reactRouterConfig.future.v8_viteEnvironmentApi\n                    ? viteUserConfig?.environments?.client?.build?.assetsDir\n                    : null) ??\n                  viteUserConfig?.build?.assetsDir ??\n                  \"assets\";\n                return path.posix.join(\n                  assetsDir,\n                  `[name]${routeChunkSuffix}-[hash].js`,\n                );\n              },\n            },\n          },\n          outDir: getClientBuildDirectory(ctx.reactRouterConfig),\n        },\n      }),\n  };\n\n  let serverBundleIds = getServerBundleIds(ctx);\n  if (serverBundleIds) {\n    for (let serverBundleId of serverBundleIds) {\n      const environmentName = `${SSR_BUNDLE_PREFIX}${serverBundleId}` as const;\n      environmentOptionsResolvers[environmentName] = ({ viteUserConfig }) =>\n        mergeEnvironmentOptions(\n          getBaseServerOptions({ viteUserConfig }),\n          {\n            build: {\n              outDir: getServerBuildDirectory(ctx.reactRouterConfig, {\n                serverBundleId,\n              }),\n            },\n          },\n          // Ensure server bundle environments extend the user's SSR\n          // environment config if it exists\n          viteUserConfig.environments?.ssr ?? {},\n        );\n    }\n  } else {\n    environmentOptionsResolvers.ssr = ({ viteUserConfig }) =>\n      mergeEnvironmentOptions(getBaseServerOptions({ viteUserConfig }), {\n        build: {\n          outDir: getServerBuildDirectory(ctx.reactRouterConfig),\n        },\n      });\n  }\n\n  return environmentOptionsResolvers;\n}\n\nexport function resolveEnvironmentsOptions(\n  environmentResolvers: EnvironmentOptionsResolvers,\n  resolverOptions: Parameters<EnvironmentOptionsResolver>[0],\n): Record<string, EnvironmentOptions> {\n  let environmentOptions: Record<string, EnvironmentOptions> = {};\n  for (let [environmentName, resolver] of Object.entries(\n    environmentResolvers,\n  ) as [EnvironmentName, EnvironmentOptionsResolver][]) {\n    environmentOptions[environmentName] = resolver(resolverOptions);\n  }\n  return environmentOptions;\n}\n\nasync function getEnvironmentsOptions(\n  ctx: ReactRouterPluginContext,\n  viteCommand: Vite.ResolvedConfig[\"command\"],\n  resolverOptions: Parameters<EnvironmentOptionsResolver>[0],\n): Promise<Record<string, EnvironmentOptions>> {\n  let environmentOptionsResolvers = await getEnvironmentOptionsResolvers(\n    ctx,\n    viteCommand,\n  );\n  return resolveEnvironmentsOptions(\n    environmentOptionsResolvers,\n    resolverOptions,\n  );\n}\n\nfunction isNonNullable<T>(x: T): x is NonNullable<T> {\n  return x != null;\n}\n\n// Type and function copied from Vite\ntype AsyncFlatten<T extends unknown[]> = T extends (infer U)[]\n  ? Exclude<Awaited<U>, U[]>[]\n  : never;\n\nasync function asyncFlatten<T extends unknown[]>(\n  arr: T,\n): Promise<AsyncFlatten<T>> {\n  do {\n    arr = (await Promise.all(arr)).flat(Infinity) as any;\n  } while (arr.some((v: any) => v?.then));\n  return arr as unknown[] as AsyncFlatten<T>;\n}\n\ntype PrerenderMetadata = {\n  type: \"data\" | \"resource\" | \"html\" | \"spa\";\n  path: string;\n  isResourceRoute?: boolean;\n};\n\nfunction assertPrerenderPathsMatchRoutes(\n  config: ResolvedReactRouterConfig,\n  prerenderPaths: string[],\n): void {\n  let routes = createPrerenderRoutes(config.routes);\n\n  for (let path of prerenderPaths) {\n    let matches = matchRoutes(routes, `/${path}/`.replace(/^\\/\\/+/, \"/\"));\n    if (!matches) {\n      throw new Error(\n        `Unable to prerender path because it does not match any routes: ${path}`,\n      );\n    }\n  }\n}\n\nfunction getPrerenderConcurrencyConfig(\n  reactRouterConfig: ResolvedReactRouterConfig,\n): number {\n  let concurrency = 1;\n  let { prerender } = reactRouterConfig;\n  if (typeof prerender === \"object\" && \"unstable_concurrency\" in prerender) {\n    concurrency = prerender.unstable_concurrency ?? 1;\n  }\n  return concurrency;\n}\n\nfunction createDataRequest(\n  prerenderPath: string,\n  reactRouterConfig: ResolvedReactRouterConfig,\n  onlyRoutes: string[] | null,\n  isResourceRoute?: boolean,\n): PrerenderRequest<PrerenderMetadata> {\n  let normalizedPath = `${reactRouterConfig.basename}${\n    prerenderPath === \"/\"\n      ? \"/_root.data\"\n      : `${prerenderPath.replace(/\\/$/, \"\")}.data`\n  }`.replace(/\\/\\/+/g, \"/\");\n  let url = new URL(`http://localhost${normalizedPath}`);\n  if (onlyRoutes?.length) {\n    url.searchParams.set(\"_routes\", onlyRoutes.join(\",\"));\n  }\n\n  return {\n    request: new Request(url),\n    metadata: { type: \"data\", path: prerenderPath, isResourceRoute },\n  };\n}\n\nfunction createRouteRequest(\n  prerenderPath: string,\n  reactRouterConfig: ResolvedReactRouterConfig,\n  data?: string,\n): PrerenderRequest<PrerenderMetadata> {\n  let normalizedPath = `${reactRouterConfig.basename}${prerenderPath}/`.replace(\n    /\\/\\/+/g,\n    \"/\",\n  );\n\n  let headers = new Headers();\n\n  if (data) {\n    let encodedData = encodeURI(data);\n\n    // Check if encoded data would exceed HTTP header limits (~8KB threshold)\n    // Skip header for large data, loaders will run again\n    if (encodedData.length < 8 * 1024) {\n      headers.set(\"X-React-Router-Prerender-Data\", encodedData);\n    }\n  }\n\n  return {\n    request: new Request(`http://localhost${normalizedPath}`, { headers }),\n    metadata: { type: \"html\", path: prerenderPath },\n  };\n}\n\nfunction createResourceRouteRequest(\n  prerenderPath: string,\n  reactRouterConfig: ResolvedReactRouterConfig,\n  requestInit?: RequestInit,\n): PrerenderRequest<PrerenderMetadata> {\n  let normalizedPath = `${reactRouterConfig.basename}${prerenderPath}/`\n    .replace(/\\/\\/+/g, \"/\")\n    .replace(/\\/$/g, \"\");\n\n  return {\n    request: new Request(`http://localhost${normalizedPath}`, requestInit),\n    metadata: { type: \"resource\", path: prerenderPath },\n  };\n}\n\nfunction createSpaModeRequest(\n  reactRouterConfig: ResolvedReactRouterConfig,\n): PrerenderRequest<PrerenderMetadata> {\n  return {\n    request: new Request(`http://localhost${reactRouterConfig.basename}`, {\n      headers: {\n        // Enable SPA mode in the server runtime and only render down to the root\n        \"X-React-Router-SPA-Mode\": \"yes\",\n      },\n    }),\n    metadata: { type: \"spa\", path: \"/\" },\n  };\n}\n"
  },
  {
    "path": "packages/react-router-dev/vite/plugins/prerender.ts",
    "content": "import type * as Vite from \"vite\";\nimport { mkdir, writeFile } from \"node:fs/promises\";\nimport path from \"node:path\";\n\nexport interface PrerenderFile {\n  /**\n   * The filename relative to the build output directory\n   * e.g., \"about.html\", \"api/data.json\", \"about/index.html\"\n   *\n   * Leading slash will be removed if present (\"/about.html\" -> \"about.html\")\n   */\n  path: string;\n  /**\n   * The file contents to write\n   */\n  contents: string | Uint8Array;\n}\n\n/**\n * Request input: string, Request, or object with metadata\n */\nexport type PrerenderRequest<Metadata extends Record<string, unknown>> =\n  | string\n  | Request\n  | {\n      request: string | Request;\n      metadata?: Metadata;\n    };\n\n/**\n * Result of the `postProcess` option. Return files array, or an object with files and follow-up requests\n */\nexport type PostProcessResult<Metadata extends Record<string, unknown>> =\n  | PrerenderFile[]\n  | {\n      files: PrerenderFile[];\n      requests?: PrerenderRequest<Metadata>[];\n    };\n\nexport interface PrerenderConfig {\n  /**\n   * Output directory for prerendered files\n   * @default viteConfig.environments.client.build.outDir\n   */\n  buildDirectory?: string;\n  /**\n   * Number of concurrent requests to prerender\n   * @default 1\n   */\n  concurrency?: number;\n  /**\n   * Number of times to retry failed requests\n   *\n   * Retries 5xx errors and timeout errors. Does not retry 4xx client errors.\n   *\n   * @default 0\n   */\n  retryCount?: number;\n  /**\n   * Delay in milliseconds between retry attempts\n   *\n   * @default 500\n   */\n  retryDelay?: number;\n  /**\n   * Maximum number of redirects to follow\n   *\n   * @default 0\n   */\n  maxRedirects?: number;\n  /**\n   * Request timeout in milliseconds\n   *\n   * @default 10000\n   */\n  timeout?: number;\n}\n\nexport interface PrerenderPluginOptions<\n  Metadata extends Record<string, unknown>,\n> {\n  /**\n   * Prerender configuration\n   */\n  config?:\n    | PrerenderConfig\n    | ((\n        this: Vite.Rollup.PluginContext,\n      ) => PrerenderConfig | Promise<PrerenderConfig>);\n\n  /**\n   * Requests to prerender\n   *\n   * Can return simple strings/Requests or objects with metadata.\n   * Metadata flows through to postProcess and logFile hooks.\n   *\n   * If no requests are returned, prerendering is skipped.\n   *\n   * @example\n   * ```ts\n   * requests() {\n   *   return [\n   *     \"/\",\n   *     \"/about\",\n   *     { request: \"/api/data\", metadata: { type: \"api\" } },\n   *   ];\n   * }\n   * ```\n   */\n  requests:\n    | PrerenderRequest<Metadata>[]\n    | ((\n        this: Vite.Rollup.PluginContext,\n      ) =>\n        | PrerenderRequest<Metadata>[]\n        | Promise<PrerenderRequest<Metadata>[]>);\n\n  /**\n   * Post-process server responses to generate output files\n   *\n   * Can return just files, or files with additional requests to process.\n   * Follow-up requests go through the same pipeline (retry, redirect, timeout).\n   *\n   * @example\n   * ```ts\n   * // Simple: just return files\n   * postProcess(request, response) {\n   *   return [{ path: \"/index.html\", contents: await response.text() }];\n   * }\n   *\n   * // With follow-up requests\n   * postProcess(request, response, metadata) {\n   *   let data = await response.text();\n   *   return {\n   *     files: [{ path: \"/data.json\", contents: data }],\n   *     requests: [{\n   *       request: new Request(htmlUrl, { headers: { \"X-Data\": data } }),\n   *       metadata: { type: \"html\" }\n   *     }]\n   *   };\n   * }\n   * ```\n   */\n  postProcess?: (\n    this: Vite.Rollup.PluginContext,\n    request: Request,\n    response: Response,\n    metadata: Metadata | undefined,\n  ) => Promise<NoInfer<PostProcessResult<Metadata>>>;\n\n  /**\n   * Handle errors during prerendering\n   *\n   * If this function does not throw, prerendering continues.\n   * If it throws, the build fails.\n   */\n  handleError?: (\n    this: Vite.Rollup.PluginContext,\n    request: Request,\n    error: Error,\n    metadata: Metadata | undefined,\n  ) => void;\n\n  /**\n   * Log when a file is written\n   *\n   * Use for custom logging with access to request metadata.\n   * If not provided, uses default logging.\n   */\n  logFile?: (\n    this: Vite.Rollup.PluginContext,\n    outputPath: string,\n    metadata: Metadata | undefined,\n  ) => void;\n\n  /**\n   * Called after all prerendering is complete\n   *\n   * Use for cleanup or post-processing of output files.\n   */\n  finalize?: (\n    this: Vite.Rollup.PluginContext,\n    buildDirectory: string,\n  ) => void | Promise<void>;\n}\n\nfunction normalizePrerenderRequest<Metadata extends Record<string, unknown>>(\n  input: PrerenderRequest<Metadata>,\n): {\n  request: string | Request;\n  metadata: Metadata | undefined;\n} {\n  if (typeof input === \"string\" || input instanceof Request) {\n    return { request: input, metadata: undefined };\n  }\n\n  return { request: input.request, metadata: input.metadata };\n}\n\nfunction normalizePostProcessResult<Metadata extends Record<string, unknown>>(\n  result: PostProcessResult<Metadata>,\n): {\n  files: PrerenderFile[];\n  requests: PrerenderRequest<Metadata>[];\n} {\n  if (Array.isArray(result)) {\n    return { files: result, requests: [] };\n  }\n\n  return { files: result.files, requests: result.requests ?? [] };\n}\n\n/**\n * Vite plugin for prerendering using the preview server\n */\nexport function prerender<Metadata extends Record<string, unknown>>(\n  options: PrerenderPluginOptions<Metadata>,\n): Vite.Plugin {\n  const {\n    config,\n    requests,\n    postProcess = defaultPostProcess,\n    handleError = defaultHandleError,\n    logFile,\n    finalize,\n  } = options;\n\n  let viteConfig: Vite.ResolvedConfig;\n\n  return {\n    name: \"prerender\",\n    configResolved(resolvedConfig) {\n      viteConfig = resolvedConfig;\n    },\n    writeBundle: {\n      async handler() {\n        const pluginContext = this;\n        const rawRequests =\n          typeof requests === \"function\"\n            ? await requests.call(pluginContext)\n            : requests;\n\n        const prerenderRequests = rawRequests.map(normalizePrerenderRequest);\n\n        if (prerenderRequests.length === 0) {\n          return;\n        }\n\n        const prerenderConfig =\n          typeof config === \"function\"\n            ? await config.call(pluginContext)\n            : config;\n        const {\n          buildDirectory = viteConfig.environments.client.build.outDir,\n          concurrency = 1,\n          retryCount = 0,\n          retryDelay = 500,\n          maxRedirects = 0,\n          timeout = 10000,\n        } = prerenderConfig ?? {};\n\n        const previewServer = await startPreviewServer(viteConfig);\n\n        try {\n          const baseUrl = getResolvedUrl(previewServer);\n\n          async function prerenderRequest(\n            input: string | Request,\n            metadata: Metadata | undefined,\n          ): Promise<PostProcessResult<Metadata>> {\n            let attemptCount = 0;\n            let redirectCount = 0;\n\n            const request = new Request(input);\n            const url = new URL(request.url);\n\n            if (url.origin !== baseUrl.origin) {\n              url.hostname = baseUrl.hostname;\n              url.protocol = baseUrl.protocol;\n              url.port = baseUrl.port;\n            }\n\n            async function attempt(\n              url: URL,\n            ): Promise<PostProcessResult<Metadata>> {\n              try {\n                const signal = AbortSignal.timeout(timeout);\n                const prerenderReq = new Request(url, request);\n                const response = await fetch(prerenderReq, {\n                  redirect: \"manual\",\n                  signal,\n                });\n\n                if (\n                  response.status >= 300 &&\n                  response.status < 400 &&\n                  response.headers.has(\"location\") &&\n                  ++redirectCount <= maxRedirects\n                ) {\n                  const location = response.headers.get(\"location\")!;\n                  const responseURL = new URL(response.url);\n                  const locationUrl = new URL(location, response.url);\n\n                  // External redirect: pass to postProcess\n                  if (responseURL.origin !== locationUrl.origin) {\n                    return await postProcess.call(\n                      pluginContext,\n                      request,\n                      response,\n                      metadata,\n                    );\n                  }\n\n                  // Internal redirect within limit: follow it\n                  const redirectUrl = new URL(location, url);\n                  return await attempt(redirectUrl);\n                }\n\n                if (response.status >= 500 && ++attemptCount <= retryCount) {\n                  await new Promise((resolve) =>\n                    setTimeout(resolve, retryDelay),\n                  );\n                  return attempt(url);\n                }\n\n                return await postProcess.call(\n                  pluginContext,\n                  request,\n                  response,\n                  metadata,\n                );\n              } catch (error) {\n                if (++attemptCount <= retryCount) {\n                  await new Promise((resolve) =>\n                    setTimeout(resolve, retryDelay),\n                  );\n                  return attempt(url);\n                }\n\n                // If handleError does not throw, return empty array and continue\n                handleError.call(\n                  pluginContext,\n                  request,\n                  error instanceof Error\n                    ? error\n                    : new Error(error?.toString() ?? \"Unknown error\"),\n                  metadata,\n                );\n\n                return [];\n              }\n            }\n\n            return attempt(url);\n          }\n\n          async function prerender(\n            input: string | Request,\n            metadata: Metadata | undefined,\n          ): Promise<void> {\n            const result = await prerenderRequest(input, metadata);\n            const { files, requests } = normalizePostProcessResult(result);\n\n            for (const file of files) {\n              await writePrerenderFile(file, metadata);\n            }\n\n            for (const followUp of requests) {\n              const normalized = normalizePrerenderRequest(followUp);\n              await prerender(normalized.request, normalized.metadata);\n            }\n          }\n\n          async function writePrerenderFile(\n            file: PrerenderFile,\n            metadata: Metadata | undefined,\n          ) {\n            // Removes leading slash if present (e.g. pathname \"/about\" -> \"about\")\n            const normalizedPath = file.path.startsWith(\"/\")\n              ? file.path.slice(1)\n              : file.path;\n            const outputPath = path.join(\n              buildDirectory,\n              ...normalizedPath.split(\"/\"),\n            );\n\n            await mkdir(path.dirname(outputPath), { recursive: true });\n            await writeFile(outputPath, file.contents);\n\n            const relativePath = path.relative(viteConfig.root, outputPath);\n\n            if (logFile) {\n              logFile.call(pluginContext, relativePath, metadata);\n            }\n\n            return relativePath;\n          }\n\n          const pMap = await import(\"p-map\");\n          await pMap.default(\n            prerenderRequests,\n            async ({ request, metadata }) => {\n              await prerender(request, metadata);\n            },\n            { concurrency },\n          );\n\n          if (finalize) {\n            await finalize.call(pluginContext, buildDirectory);\n          }\n        } finally {\n          await new Promise<void>((resolve, reject) => {\n            previewServer.httpServer.close((err) => {\n              if (err) {\n                reject(err);\n              } else {\n                resolve();\n              }\n            });\n          });\n        }\n      },\n    },\n  };\n}\n\nasync function defaultPostProcess(\n  request: Request,\n  response: Response,\n): Promise<PrerenderFile[]> {\n  const prerenderPath = new URL(request.url).pathname;\n\n  if (!response.ok) {\n    throw new Error(\n      `Prerender: Request failed for ${prerenderPath}: ${response.status} ${response.statusText}`,\n    );\n  }\n\n  return [\n    {\n      path: `${prerenderPath}/index.html`,\n      contents: await response.text(),\n    },\n  ];\n}\n\nfunction defaultHandleError(request: Request, error: Error): void {\n  const prerenderPath = new URL(request.url).pathname;\n\n  if (request.signal?.aborted) {\n    throw new Error(\n      `Prerender: Request timed out for ${prerenderPath}: ${error.message}`,\n    );\n  }\n\n  throw new Error(\n    `Prerender: Request failed for ${prerenderPath}: ${error.message}`,\n  );\n}\n\nasync function startPreviewServer(\n  viteConfig: Vite.ResolvedConfig,\n): Promise<Vite.PreviewServer> {\n  const vite = await import(\"vite\");\n\n  try {\n    return await vite.preview({\n      configFile: viteConfig.configFile,\n      logLevel: \"silent\",\n      preview: {\n        port: 0,\n        open: false,\n      },\n    });\n  } catch (error) {\n    throw new Error(\"Prerender: Failed to start Vite preview server\", {\n      cause: error,\n    });\n  }\n}\n\nfunction getResolvedUrl(previewServer: Vite.PreviewServer): URL {\n  const baseUrl = previewServer.resolvedUrls?.local[0];\n\n  if (!baseUrl) {\n    throw new Error(\n      \"Prerender: No resolved URL is available from the Vite preview server\",\n    );\n  }\n\n  return new URL(baseUrl);\n}\n"
  },
  {
    "path": "packages/react-router-dev/vite/plugins/validate-plugin-order.ts",
    "content": "import type * as Vite from \"vite\";\n\nexport function validatePluginOrder(): Vite.Plugin {\n  return {\n    name: \"react-router:validate-plugin-order\",\n    configResolved(viteConfig) {\n      let pluginIndex = (pluginName: string | string[]) => {\n        pluginName = Array.isArray(pluginName) ? pluginName : [pluginName];\n        return viteConfig.plugins.findIndex((plugin) =>\n          pluginName.includes(plugin.name),\n        );\n      };\n\n      let reactRouterRscPluginIndex = pluginIndex(\"react-router/rsc\");\n      let viteRscPluginIndex = pluginIndex(\"rsc\");\n      if (\n        reactRouterRscPluginIndex >= 0 &&\n        viteRscPluginIndex >= 0 &&\n        reactRouterRscPluginIndex > viteRscPluginIndex\n      ) {\n        throw new Error(\n          `The \"@vitejs/plugin-rsc\" plugin should be placed after the React Router RSC plugin in your Vite config`,\n        );\n      }\n\n      let reactRouterPluginIndex = pluginIndex([\n        \"react-router\",\n        \"react-router/rsc\",\n      ]);\n      let mdxPluginIndex = pluginIndex(\"@mdx-js/rollup\");\n      if (mdxPluginIndex >= 0 && mdxPluginIndex > reactRouterPluginIndex) {\n        throw new Error(\n          `The \"@mdx-js/rollup\" plugin should be placed before the React Router plugin in your Vite config`,\n        );\n      }\n    },\n  };\n}\n"
  },
  {
    "path": "packages/react-router-dev/vite/plugins/warn-on-client-source-maps.ts",
    "content": "import type * as Vite from \"vite\";\nimport colors from \"picocolors\";\nimport invariant from \"../../invariant\";\n\nexport function warnOnClientSourceMaps(): Vite.Plugin {\n  let viteConfig: Vite.ResolvedConfig;\n  let viteCommand: Vite.ConfigEnv[\"command\"];\n  let logged = false;\n\n  return {\n    name: \"react-router:warn-on-client-source-maps\",\n    config(_, configEnv) {\n      viteCommand = configEnv.command;\n    },\n    configResolved(config) {\n      viteConfig = config;\n    },\n    buildStart() {\n      invariant(viteConfig);\n\n      if (\n        !logged &&\n        viteCommand === \"build\" &&\n        viteConfig.mode === \"production\" &&\n        !viteConfig.build.ssr &&\n        (viteConfig.build.sourcemap ||\n          viteConfig.environments?.client?.build.sourcemap)\n      ) {\n        viteConfig.logger.warn(\n          colors.yellow(\n            \"\\n\" +\n              colors.bold(\"  ⚠️  Source maps are enabled in production\\n\") +\n              [\n                \"This makes your server code publicly\",\n                \"visible in the browser. This is highly\",\n                \"discouraged! If you insist, ensure that\",\n                \"you are using environment variables for\",\n                \"secrets and not hard-coding them in\",\n                \"your source code.\",\n              ]\n                .map((line) => \"     \" + line)\n                .join(\"\\n\") +\n              \"\\n\",\n          ),\n        );\n        logged = true;\n      }\n    },\n  };\n}\n"
  },
  {
    "path": "packages/react-router-dev/vite/profiler.ts",
    "content": "// Adapted from:\n// - https://github.com/vitejs/vite/blob/9fc5d9cb3a1b9df067e00959faa9da43ae03f776/packages/vite/bin/vite.js\n// - https://github.com/vitejs/vite/blob/9fc5d9cb3a1b9df067e00959faa9da43ae03f776/packages/vite/src/node/cli.ts\n\nimport fs from \"node:fs\";\nimport type { Session } from \"node:inspector\";\nimport path from \"node:path\";\nimport colors from \"picocolors\";\n\ndeclare namespace global {\n  let __reactRouter_profile_session: Session | undefined;\n}\n\nexport const getSession = () => global.__reactRouter_profile_session;\n\nexport const start = async (callback?: () => void | Promise<void>) => {\n  let inspector = await import(\"node:inspector\").then((r) => r.default);\n  let session = (global.__reactRouter_profile_session =\n    new inspector.Session());\n  session.connect();\n  session.post(\"Profiler.enable\", () => {\n    session.post(\"Profiler.start\", callback);\n  });\n};\n\nlet profileCount = 0;\n\nexport const stop = (log: (message: string) => void): void | Promise<void> => {\n  let session = getSession();\n  if (!session) return;\n  return new Promise((res, rej) => {\n    session!.post(\"Profiler.stop\", (err, { profile }) => {\n      if (err) return rej(err);\n      let outPath = path.resolve(`./react-router-${profileCount++}.cpuprofile`);\n      fs.writeFileSync(outPath, JSON.stringify(profile));\n      log(\n        colors.yellow(\n          `CPU profile written to ${colors.white(colors.dim(outPath))}`,\n        ),\n      );\n      global.__reactRouter_profile_session = undefined;\n      res();\n    });\n  });\n};\n"
  },
  {
    "path": "packages/react-router-dev/vite/remove-exports-test.ts",
    "content": "import { generate, parse } from \"./babel\";\nimport { removeExports } from \"./remove-exports\";\n\nfunction transform(code: string, exportsToRemove: string[]) {\n  let ast = parse(code, { sourceType: \"module\" });\n  removeExports(ast, exportsToRemove);\n  return generate(ast);\n}\n\ndescribe(\"transform\", () => {\n  test(\"arrow function\", () => {\n    let result = transform(\n      `\n      export const removedExport_1 = () => {}\n      export const removedExport_2 = () => {}\n\n      export const keptExport_1 = () => {}\n      export const keptExport_2 = () => {}\n    `,\n      [\"removedExport_1\", \"removedExport_2\"],\n    );\n    expect(result.code).toMatchInlineSnapshot(`\n      \"export const keptExport_1 = () => {};\n      export const keptExport_2 = () => {};\"\n    `);\n    expect(result.code).not.toMatch(/removed/i);\n  });\n\n  test(\"arrow function with dependencies\", () => {\n    let result = transform(\n      `\n      import { removedLib } from 'removed-lib'\n      import { keptLib } from 'kept-lib'\n      import { sharedLib } from 'shared-lib'\n\n      const REMOVED_STRING = 'REMOVED_STRING'\n\n      const sharedUtil = () => sharedLib()\n      const removedUtil = () => sharedUtil(removedLib(REMOVED_STRING))\n      const keptUtil = () => sharedUtil(keptLib())\n\n      export const removedExport_1 = () => removedUtil()\n      export const removedExport_2 = () => removedUtil()\n\n      export const keptExport_1 = () => keptUtil()\n      export const keptExport_2 = () => keptUtil()\n    `,\n      [\"removedExport_1\", \"removedExport_2\"],\n    );\n    expect(result.code).toMatchInlineSnapshot(`\n      \"import { keptLib } from 'kept-lib';\n      import { sharedLib } from 'shared-lib';\n      const sharedUtil = () => sharedLib();\n      const keptUtil = () => sharedUtil(keptLib());\n      export const keptExport_1 = () => keptUtil();\n      export const keptExport_2 = () => keptUtil();\"\n    `);\n    expect(result.code).not.toMatch(/removed/i);\n  });\n\n  test(\"arrow function with property assignment\", () => {\n    let result = transform(\n      `\n      export const removedExport_1 = () => {}\n      removedExport_1.removedProperty = true\n      export const removedExport_2 = () => {}\n      removedExport_2.removedProperty = true\n      \n      export const keptExport_1 = () => {}\n      keptExport_1.keptProperty = true\n      export const keptExport_2 = () => {}\n      keptExport_2.keptProperty = true\n    `,\n      [\"removedExport_1\", \"removedExport_2\"],\n    );\n    expect(result.code).toMatchInlineSnapshot(`\n      \"export const keptExport_1 = () => {};\n      keptExport_1.keptProperty = true;\n      export const keptExport_2 = () => {};\n      keptExport_2.keptProperty = true;\"\n    `);\n    expect(result.code).not.toMatch(/removed/i);\n  });\n\n  test(\"function statement\", () => {\n    let result = transform(\n      `\n      export function removedExport_1(){}\n      export function removedExport_2(){}\n\n      export function keptExport_1(){}\n      export function keptExport_2(){}\n    `,\n      [\"removedExport_1\", \"removedExport_2\"],\n    );\n    expect(result.code).toMatchInlineSnapshot(`\n      \"export function keptExport_1() {}\n      export function keptExport_2() {}\"\n    `);\n    expect(result.code).not.toMatch(/removed/i);\n  });\n\n  test(\"function statement with dependencies\", () => {\n    let result = transform(\n      `\n      import { removedLib } from 'removed-lib'\n      import { keptLib } from 'kept-lib'\n      import { sharedLib } from 'shared-lib'\n\n      const REMOVED_STRING = 'REMOVED_STRING'\n\n      function sharedUtil() { return sharedLib() }\n      function removedUtil() { return sharedUtil(removedLib(REMOVED_STRING)) }\n      function keptUtil() { return sharedUtil(keptLib()) }\n\n      export function removedExport_1() { return removedUtil() }\n      export function removedExport_2() { return removedUtil() }\n\n      export function keptExport_1() { return keptUtil() }\n      export function keptExport_2() { return keptUtil() }\n    `,\n      [\"removedExport_1\", \"removedExport_2\"],\n    );\n    expect(result.code).toMatchInlineSnapshot(`\n      \"import { keptLib } from 'kept-lib';\n      import { sharedLib } from 'shared-lib';\n      function sharedUtil() {\n        return sharedLib();\n      }\n      function keptUtil() {\n        return sharedUtil(keptLib());\n      }\n      export function keptExport_1() {\n        return keptUtil();\n      }\n      export function keptExport_2() {\n        return keptUtil();\n      }\"\n    `);\n    expect(result.code).not.toMatch(/removed/i);\n  });\n\n  test(\"function statement with property assignment\", () => {\n    let result = transform(\n      `\n      export function removedExport_1(){}\n      removedExport_1.removedProperty = true\n      export function removedExport_2(){}\n      removedExport_2.removedProperty = true\n\n      export function keptExport_1(){}\n      keptExport_1.keptProperty = true\n      export function keptExport_2(){}\n      keptExport_2.keptProperty = true\n    `,\n      [\"removedExport_1\", \"removedExport_2\"],\n    );\n    expect(result.code).toMatchInlineSnapshot(`\n      \"export function keptExport_1() {}\n      keptExport_1.keptProperty = true;\n      export function keptExport_2() {}\n      keptExport_2.keptProperty = true;\"\n    `);\n    expect(result.code).not.toMatch(/removed/i);\n  });\n\n  test(\"object\", () => {\n    let result = transform(\n      `\n      export const removedExport_1 = {}\n      export const removedExport_2 = {}\n\n      export const keptExport_1 = {}\n      export const keptExport_2 = {}\n    `,\n      [\"removedExport_1\", \"removedExport_2\"],\n    );\n    expect(result.code).toMatchInlineSnapshot(`\n      \"export const keptExport_1 = {};\n      export const keptExport_2 = {};\"\n    `);\n    expect(result.code).not.toMatch(/removed/i);\n  });\n\n  test(\"object with dependencies\", () => {\n    let result = transform(\n      `\n      import { removedLib } from 'removed-lib'\n      import { keptLib } from 'kept-lib'\n      import { sharedLib } from 'shared-lib'\n\n      const REMOVED_STRING = 'REMOVED_STRING'\n\n      const sharedUtil = () => sharedLib()\n      const removedUtil = () => sharedUtil(removedLib(REMOVED_STRING))\n      const keptUtil = () => sharedUtil(keptLib())\n\n      export const removedExport_1 = { value: removedUtil() }\n      export const removedExport_2 = { value: removedUtil() }\n\n      export const keptExport_1 = { value: keptUtil() }\n      export const keptExport_2 = { value: keptUtil() }\n    `,\n      [\"removedExport_1\", \"removedExport_2\"],\n    );\n    expect(result.code).toMatchInlineSnapshot(`\n      \"import { keptLib } from 'kept-lib';\n      import { sharedLib } from 'shared-lib';\n      const sharedUtil = () => sharedLib();\n      const keptUtil = () => sharedUtil(keptLib());\n      export const keptExport_1 = {\n        value: keptUtil()\n      };\n      export const keptExport_2 = {\n        value: keptUtil()\n      };\"\n    `);\n    expect(result.code).not.toMatch(/removed/i);\n  });\n\n  test(\"class\", () => {\n    let result = transform(\n      `\n      export class removedExport_1 {}\n      export class removedExport_2 {}\n\n      export class keptExport_1 {}\n      export class keptExport_2 {}\n    `,\n      [\"removedExport_1\", \"removedExport_2\"],\n    );\n    expect(result.code).toMatchInlineSnapshot(`\n      \"export class keptExport_1 {}\n      export class keptExport_2 {}\"\n    `);\n    expect(result.code).not.toMatch(/removed/i);\n  });\n\n  test(\"class with dependencies\", () => {\n    let result = transform(\n      `\n      import { removedLib } from 'removed-lib'\n      import { keptLib } from 'kept-lib'\n      import { sharedLib } from 'shared-lib'\n\n      const REMOVED_STRING = 'REMOVED_STRING'\n\n      const sharedUtil = () => sharedLib()\n      const removedUtil = () => sharedUtil(removedLib(REMOVED_STRING))\n      const keptUtil = () => sharedUtil(keptLib())\n\n      export class removedExport_1{\n        static util = removedUtil()\n      }\n      export class removedExport_2{\n        static util = removedUtil()\n      }\n\n      export class keptExport_1{\n        static util = keptUtil()\n      }\n      export class keptExport_2{\n        static util = keptUtil()\n      }\n    `,\n      [\"removedExport_1\", \"removedExport_2\"],\n    );\n    expect(result.code).toMatchInlineSnapshot(`\n      \"import { keptLib } from 'kept-lib';\n      import { sharedLib } from 'shared-lib';\n      const sharedUtil = () => sharedLib();\n      const keptUtil = () => sharedUtil(keptLib());\n      export class keptExport_1 {\n        static util = keptUtil();\n      }\n      export class keptExport_2 {\n        static util = keptUtil();\n      }\"\n    `);\n    expect(result.code).not.toMatch(/removed/i);\n  });\n\n  test(\"function call\", () => {\n    let result = transform(\n      `\n      export const removedExport_1 = globalFunction()\n      export const removedExport_2 = globalFunction()\n\n      export const keptExport_1 = globalFunction()\n      export const keptExport_2 = globalFunction()\n    `,\n      [\"removedExport_1\", \"removedExport_2\"],\n    );\n    expect(result.code).toMatchInlineSnapshot(`\n      \"export const keptExport_1 = globalFunction();\n      export const keptExport_2 = globalFunction();\"\n    `);\n    expect(result.code).not.toMatch(/removed/i);\n  });\n\n  test(\"function call with dependencies\", () => {\n    let result = transform(\n      `\n      import { removedLib } from 'removed-lib'\n      import { keptLib } from 'kept-lib'\n      import { sharedLib } from 'shared-lib'\n\n      const REMOVED_STRING = 'REMOVED_STRING'\n\n      const sharedUtil = () => sharedLib()\n      const removedUtil = () => sharedUtil(removedLib(REMOVED_STRING))\n      const keptUtil = () => sharedUtil(keptLib())\n\n      export const removedExport_1 = removedUtil()\n      export const removedExport_2 = removedUtil()\n\n      export const keptExport_1 = keptUtil()\n      export const keptExport_2 = keptUtil()\n    `,\n      [\"removedExport_1\", \"removedExport_2\"],\n    );\n    expect(result.code).toMatchInlineSnapshot(`\n      \"import { keptLib } from 'kept-lib';\n      import { sharedLib } from 'shared-lib';\n      const sharedUtil = () => sharedLib();\n      const keptUtil = () => sharedUtil(keptLib());\n      export const keptExport_1 = keptUtil();\n      export const keptExport_2 = keptUtil();\"\n    `);\n    expect(result.code).not.toMatch(/removed/i);\n  });\n\n  test(\"iife\", () => {\n    let result = transform(\n      `\n      export const removedExport_1 = (() => {})()\n      export const removedExport_2 = (() => {})()\n\n      export const keptExport_1 = (() => {})()\n      export const keptExport_2 = (() => {})()\n    `,\n      [\"removedExport_1\", \"removedExport_2\"],\n    );\n    expect(result.code).toMatchInlineSnapshot(`\n      \"export const keptExport_1 = (() => {})();\n      export const keptExport_2 = (() => {})();\"\n    `);\n    expect(result.code).not.toMatch(/removed/i);\n  });\n\n  test(\"iife with dependencies\", () => {\n    let result = transform(\n      `\n      import { removedLib } from 'removed-lib'\n      import { keptLib } from 'kept-lib'\n      import { sharedLib } from 'shared-lib'\n\n      const REMOVED_STRING = 'REMOVED_STRING'\n\n      const sharedUtil = () => sharedLib()\n      const removedUtil = () => sharedUtil(removedLib(REMOVED_STRING))\n      const keptUtil = () => sharedUtil(keptLib())\n\n      export const removedExport_1 = (() => removedUtil())()\n      export const removedExport_2 = (() => removedUtil())()\n\n      export const keptExport_1 = (() => keptUtil())()\n      export const keptExport_2 = (() => keptUtil())()\n    `,\n      [\"removedExport_1\", \"removedExport_2\"],\n    );\n    expect(result.code).toMatchInlineSnapshot(`\n      \"import { keptLib } from 'kept-lib';\n      import { sharedLib } from 'shared-lib';\n      const sharedUtil = () => sharedLib();\n      const keptUtil = () => sharedUtil(keptLib());\n      export const keptExport_1 = (() => keptUtil())();\n      export const keptExport_2 = (() => keptUtil())();\"\n    `);\n    expect(result.code).not.toMatch(/removed/i);\n  });\n\n  test(\"aggregated export\", () => {\n    let result = transform(\n      `\n      const removedExport_1 = () => {}\n      removedExport_1.removedProperty = true\n      const removedExport_2 = () => {}\n      removedExport_2.removedProperty = true\n\n      const keptExport_1 = () => {}\n      keptExport_1.keptProperty = true\n      const keptExport_2 = () => {}\n      keptExport_2.keptProperty = true\n\n      export { removedExport_1 }\n      export { removedExport_2 }\n\n      export { keptExport_1 }\n      export { keptExport_2 }\n    `,\n      [\"removedExport_1\", \"removedExport_2\"],\n    );\n    expect(result.code).toMatchInlineSnapshot(`\n      \"const keptExport_1 = () => {};\n      keptExport_1.keptProperty = true;\n      const keptExport_2 = () => {};\n      keptExport_2.keptProperty = true;\n      export { keptExport_1 };\n      export { keptExport_2 };\"\n    `);\n    expect(result.code).not.toMatch(/removed/i);\n  });\n\n  test(\"aggregated export multiple\", () => {\n    let result = transform(\n      `\n      const removedExport_1 = () => {}\n      removedExport_1.removedProperty = true\n      const removedExport_2 = () => {}\n      removedExport_2.removedProperty = true\n\n      const keptExport_1 = () => {}\n      keptExport_1.keptProperty = true\n      const keptExport_2 = () => {}\n      keptExport_2.keptProperty = true\n\n      export { removedExport_1, removedExport_2 }\n      export { keptExport_1, keptExport_2 }\n    `,\n      [\"removedExport_1\", \"removedExport_2\"],\n    );\n    expect(result.code).toMatchInlineSnapshot(`\n      \"const keptExport_1 = () => {};\n      keptExport_1.keptProperty = true;\n      const keptExport_2 = () => {};\n      keptExport_2.keptProperty = true;\n      export { keptExport_1, keptExport_2 };\"\n    `);\n    expect(result.code).not.toMatch(/removed/i);\n  });\n\n  test(\"aggregated export multiple mixed\", () => {\n    let result = transform(\n      `\n      const removedExport_1 = () => {}\n      removedExport_1.removedProperty = true\n      const removedExport_2 = () => {}\n      removedExport_2.removedProperty = true\n\n      const keptExport_1 = () => {}\n      keptExport_1.keptProperty = true\n      const keptExport_2 = () => {}\n      keptExport_2.keptProperty = true\n\n      export { removedExport_1, keptExport_1 }\n      export { removedExport_2, keptExport_2 }\n    `,\n      [\"removedExport_1\", \"removedExport_2\"],\n    );\n    expect(result.code).toMatchInlineSnapshot(`\n      \"const keptExport_1 = () => {};\n      keptExport_1.keptProperty = true;\n      const keptExport_2 = () => {};\n      keptExport_2.keptProperty = true;\n      export { keptExport_1 };\n      export { keptExport_2 };\"\n    `);\n    expect(result.code).not.toMatch(/removeMe/i);\n  });\n\n  test(\"aggregated export multiple mixed and renamed\", () => {\n    let result = transform(\n      `\n      const removedExport_1 = () => {}\n      removedExport_1.removedProperty = true\n      const removedExport_2 = () => {}\n      removedExport_2.removedProperty = true\n\n      const keptExport_1 = () => {}\n      keptExport_1.keptProperty = true\n      const keptExport_2 = () => {}\n      keptExport_2.keptProperty = true\n\n      export {\n        removedExport_1 as removedExport_1_renamed,\n        keptExport_1 as keptExport_1_renamed\n      }\n      export {\n        removedExport_2 as removedExport_2_renamed,\n        keptExport_2 as keptExport_2_renamed\n      }\n    `,\n      [\"removedExport_1_renamed\", \"removedExport_2_renamed\"],\n    );\n    expect(result.code).toMatchInlineSnapshot(`\n      \"const keptExport_1 = () => {};\n      keptExport_1.keptProperty = true;\n      const keptExport_2 = () => {};\n      keptExport_2.keptProperty = true;\n      export { keptExport_1 as keptExport_1_renamed };\n      export { keptExport_2 as keptExport_2_renamed };\"\n    `);\n    expect(result.code).not.toMatch(/removeMe/i);\n  });\n\n  test(\"re-export\", () => {\n    let result = transform(\n      `\n      export { removedExport_1 } from './removed/1'\n      export { removedExport_2 } from './removed/2'\n\n      export { keptExport_1 } from './kept/1'\n      export { keptExport_2 } from './kept/2'\n    `,\n      [\"removedExport_1\", \"removedExport_2\"],\n    );\n    expect(result.code).toMatchInlineSnapshot(`\n      \"export { keptExport_1 } from './kept/1';\n      export { keptExport_2 } from './kept/2';\"\n    `);\n    expect(result.code).not.toMatch(/removed/i);\n  });\n\n  test(\"re-export multiple\", () => {\n    let result = transform(\n      `\n      export { removedExport_1, removedExport_2 } from './removed'\n      export { keptExport_1, keptExport_2 } from './kept'\n    `,\n      [\"removedExport_1\", \"removedExport_2\"],\n    );\n    expect(result.code).toMatchInlineSnapshot(\n      \"\\\"export { keptExport_1, keptExport_2 } from './kept';\\\"\",\n    );\n    expect(result.code).not.toMatch(/removed/i);\n  });\n\n  test(\"re-export multiple mixed\", () => {\n    let result = transform(\n      `\n      export { removeMe_1, keepMe_1 } from './1'\n      export { removeMe_2, keepMe_2 } from './2'\n    `,\n      [\"removeMe_1\", \"removeMe_2\"],\n    );\n    expect(result.code).toMatchInlineSnapshot(`\n      \"export { keepMe_1 } from './1';\n      export { keepMe_2 } from './2';\"\n    `);\n    expect(result.code).not.toMatch(/removeMe/i);\n  });\n\n  test(\"re-export manual\", () => {\n    let result = transform(\n      `\n      import { removedExport_1 } from './removed/1'\n      import { removedExport_2 } from './removed/2'\n      import { keptExport_1 } from './kept/1'\n      import { keptExport_2 } from './kept/2'\n\n      export { removedExport_1 }\n      export { removedExport_2 }\n\n      export { keptExport_1 }\n      export { keptExport_2 }\n    `,\n      [\"removedExport_1\", \"removedExport_2\"],\n    );\n    expect(result.code).toMatchInlineSnapshot(`\n      \"import { keptExport_1 } from './kept/1';\n      import { keptExport_2 } from './kept/2';\n      export { keptExport_1 };\n      export { keptExport_2 };\"\n    `);\n    expect(result.code).not.toMatch(/removed/i);\n  });\n\n  test(\"re-export manual multiple\", () => {\n    let result = transform(\n      `\n      import { removedExport_1, removedExport_2 } from './removed'\n      import { keptExport_1, keptExport_2 } from './kept'\n\n      export { removedExport_1, removedExport_2 }\n      export { keptExport_1, keptExport_2 }\n    `,\n      [\"removedExport_1\", \"removedExport_2\"],\n    );\n    expect(result.code).toMatchInlineSnapshot(`\n      \"import { keptExport_1, keptExport_2 } from './kept';\n      export { keptExport_1, keptExport_2 };\"\n    `);\n    expect(result.code).not.toMatch(/removed/i);\n  });\n\n  test(\"re-export manual multiple mixed\", () => {\n    let result = transform(\n      `\n      import { removeMe_1, keepMe_1 } from './1'\n      import { removeMe_2, keepMe_2 } from './2'\n\n      export { removeMe_1, keepMe_1 }\n      export { removeMe_2, keepMe_2 }\n    `,\n      [\"removeMe_1\", \"removeMe_2\"],\n    );\n    expect(result.code).toMatchInlineSnapshot(`\n      \"import { keepMe_1 } from './1';\n      import { keepMe_2 } from './2';\n      export { keepMe_1 };\n      export { keepMe_2 };\"\n    `);\n    expect(result.code).not.toMatch(/removeMe/i);\n  });\n\n  test(\"number\", () => {\n    let result = transform(\n      `\n      export const removedExport_1 = 123\n      export const removedExport_2 = 123\n\n      export const keptExport_1 = 123\n      export const keptExport_2 = 123\n    `,\n      [\"removedExport_1\", \"removedExport_2\"],\n    );\n    expect(result.code).toMatchInlineSnapshot(`\n      \"export const keptExport_1 = 123;\n      export const keptExport_2 = 123;\"\n    `);\n    expect(result.code).not.toMatch(/removed/i);\n  });\n\n  test(\"string\", () => {\n    let result = transform(\n      `\n      export const removedExport_1 = 'string'\n      export const removedExport_2 = 'string'\n\n      export const keptExport_1 = 'string'\n      export const keptExport_2 = 'string'\n    `,\n      [\"removedExport_1\", \"removedExport_2\"],\n    );\n    expect(result.code).toMatchInlineSnapshot(`\n      \"export const keptExport_1 = 'string';\n      export const keptExport_2 = 'string';\"\n    `);\n    expect(result.code).not.toMatch(/removed/i);\n  });\n\n  test(\"string reference\", () => {\n    let result = transform(\n      `\n      const REMOVED_STRING = 'REMOVED_STRING';\n      const KEPT_STRING = 'KEPT_STRING';\n\n      export const removedExport_1 = REMOVED_STRING\n      export const removedExport_2 = REMOVED_STRING\n\n      export const keptExport_1 = KEPT_STRING\n      export const keptExport_2 = KEPT_STRING\n    `,\n      [\"removedExport_1\", \"removedExport_2\"],\n    );\n    expect(result.code).toMatchInlineSnapshot(`\n      \"const KEPT_STRING = 'KEPT_STRING';\n      export const keptExport_1 = KEPT_STRING;\n      export const keptExport_2 = KEPT_STRING;\"\n    `);\n    expect(result.code).not.toMatch(/removed/i);\n  });\n\n  test(\"null\", () => {\n    let result = transform(\n      `\n      export const removedExport_1 = null\n      export const removedExport_2 = null\n\n      export const keptExport_1 = null\n      export const keptExport_2 = null\n    `,\n      [\"removedExport_1\", \"removedExport_2\"],\n    );\n    expect(result.code).toMatchInlineSnapshot(`\n      \"export const keptExport_1 = null;\n      export const keptExport_2 = null;\"\n    `);\n    expect(result.code).not.toMatch(/removed/i);\n  });\n\n  test(\"multiple variable declarators\", () => {\n    let result = transform(\n      `\n      export const removedExport_1 = null,\n        removedExport_2 = null\n\n      export const keptExport_1 = null,\n        keptExport_2 = null\n    `,\n      [\"removedExport_1\", \"removedExport_2\"],\n    );\n    expect(result.code).toMatchInlineSnapshot(`\n      \"export const keptExport_1 = null,\n        keptExport_2 = null;\"\n    `);\n    expect(result.code).not.toMatch(/removed/i);\n  });\n\n  test(\"multiple variable declarators mixed\", () => {\n    let result = transform(\n      `\n      export const removedExport_1 = null,\n        keptExport_1 = null\n\n      export const keptExport_2 = null,\n        removedExport_2 = null\n    `,\n      [\"removedExport_1\", \"removedExport_2\"],\n    );\n    expect(result.code).toMatchInlineSnapshot(`\n      \"export const keptExport_1 = null;\n      export const keptExport_2 = null;\"\n    `);\n    expect(result.code).not.toMatch(/removed/i);\n  });\n\n  test(\"array destructuring throws on removed export\", () => {\n    expect(() =>\n      transform(\n        `\n        export const [removedExport_1, removedExport_2] = [null, null]\n\n        export const [keptExport_1, keptExport_2] = [null, null]\n      `,\n        [\"removedExport_1\", \"removedExport_2\"],\n      ),\n    ).toThrow(`Cannot remove destructured export \"removedExport_1\"`);\n  });\n\n  test(\"array rest destructuring throws on removed export\", () => {\n    expect(() =>\n      transform(\n        `\n        export const [...removedExport_1] = [null, null]\n\n        export const [keptExport_1, keptExport_2] = [null, null]\n      `,\n        [\"removedExport_1\", \"removedExport_2\"],\n      ),\n    ).toThrow('Cannot remove destructured export \"removedExport_1\"');\n  });\n\n  test(\"nested array destructuring throws on removed export\", () => {\n    expect(() =>\n      transform(\n        `\n        export const [keepMe_1, [{ nested: [ { nested: [removedExport_2] } ] }] ] = nested;\n      `,\n        [\"removedExport_1\", \"removedExport_2\"],\n      ),\n    ).toThrow('Cannot remove destructured export \"removedExport_2\"');\n  });\n\n  test(\"array destructuring works when nothing is removed\", () => {\n    let result = transform(\n      `\n      export const [keptExport_1, keptExport_2] = [null, null]\n    `,\n      [\"removedExport_1\", \"removedExport_2\"],\n    );\n    expect(result.code).toMatchInlineSnapshot(\n      `\"export const [keptExport_1, keptExport_2] = [null, null];\"`,\n    );\n    expect(result.code).not.toMatch(/removed/i);\n  });\n\n  test(\"object destructuring throws on removed export\", () => {\n    expect(() =>\n      transform(\n        `\n        export const { removedExport_1, removedExport_2 } = {}\n\n        export const { keptExport_1, keptExport_2 } = {}\n      `,\n        [\"removedExport_1\", \"removedExport_2\"],\n      ),\n    ).toThrow('Cannot remove destructured export \"removedExport_1\"');\n  });\n\n  test(\"object rest destructuring throws on removed export\", () => {\n    expect(() =>\n      transform(\n        `\n        export const { ...removedExport_1 } = {}\n\n        export const { ...keptExport_1 } = {}\n      `,\n        [\"removedExport_1\", \"removedExport_2\"],\n      ),\n    ).toThrow('Cannot remove destructured export \"removedExport_1\"');\n  });\n\n  test(\"nested object destructuring throws on removed export\", () => {\n    expect(() =>\n      transform(\n        `\n        export const [keepMe_1, [{ nested: [ { nested: { removedExport_2 } } ] }]] = nested;\n      `,\n        [\"removedExport_1\", \"removedExport_2\"],\n      ),\n    ).toThrow('Cannot remove destructured export \"removedExport_2\"');\n  });\n\n  test(\"object destructuring works when nothing is removed\", () => {\n    let result = transform(\n      `\n      export const { keptExport_1, keptExport_2 } = {}\n    `,\n      [\"removedExport_1\", \"removedExport_2\"],\n    );\n    expect(result.code).toMatchInlineSnapshot(`\n      \"export const {\n        keptExport_1,\n        keptExport_2\n      } = {};\"\n    `);\n    expect(result.code).not.toMatch(/removed/i);\n  });\n\n  test(\"default export\", () => {\n    let result = transform(\n      `\n      export const keepMe = () => {};\n      keepMe.keptProperty = true;\n\n      const removeMe = () => {};\n      removeMe.removedProperty = true;\n\n      export default removeMe;\n    `,\n      [\"default\"],\n    );\n    expect(result.code).toMatchInlineSnapshot(`\n      \"export const keepMe = () => {};\n      keepMe.keptProperty = true;\"\n    `);\n    expect(result.code).not.toMatch(/default/i);\n  });\n\n  test(\"default export aggregated\", () => {\n    let result = transform(\n      `\n      export const keepMe = () => {};\n      keepMe.keptProperty = true;\n\n      const removeMe = () => {};\n      removeMe.removedProperty = true;\n\n      export { removeMe as default };\n    `,\n      [\"default\"],\n    );\n    expect(result.code).toMatchInlineSnapshot(`\n      \"export const keepMe = () => {};\n      keepMe.keptProperty = true;\"\n    `);\n    expect(result.code).not.toMatch(/default/i);\n  });\n\n  test(\"default export aggregated mixed\", () => {\n    let result = transform(\n      `\n      const keepMe = () => {};\n      keepMe.keptProperty = true;\n\n      const removeMe = () => {};\n      removeMe.removedProperty = true;\n\n      export { removeMe as default, keepMe };\n    `,\n      [\"default\"],\n    );\n    expect(result.code).toMatchInlineSnapshot(`\n      \"const keepMe = () => {};\n      keepMe.keptProperty = true;\n      export { keepMe };\"\n    `);\n    expect(result.code).not.toMatch(/default/i);\n  });\n\n  test(\"default re-export\", () => {\n    let result = transform(\n      `\n      export const keepMe = null;\n\n      export { default } from \"./module\";\n    `,\n      [\"default\"],\n    );\n    expect(result.code).toMatchInlineSnapshot(`\"export const keepMe = null;\"`);\n    expect(result.code).not.toMatch(/default/i);\n  });\n\n  test(\"default re-export mixed\", () => {\n    let result = transform(\n      `\n      export { default, keepMe } from \"./module\";\n    `,\n      [\"default\"],\n    );\n    expect(result.code).toMatchInlineSnapshot(\n      `\"export { keepMe } from \"./module\";\"`,\n    );\n    expect(result.code).not.toMatch(/default/i);\n  });\n\n  test(\"nothing removed\", () => {\n    let result = transform(\n      `\n      export const keptExport_1 = () => {}\n      export const keptExport_2 = () => {}\n    `,\n      [\"removedExport_1\", \"removedExport_2\"],\n    );\n    expect(result.code).toMatchInlineSnapshot(`\n      \"export const keptExport_1 = () => {};\n      export const keptExport_2 = () => {};\"\n    `);\n    expect(result.code).not.toMatch(/removed/i);\n  });\n});\n"
  },
  {
    "path": "packages/react-router-dev/vite/remove-exports.ts",
    "content": "import {\n  findReferencedIdentifiers,\n  deadCodeElimination,\n} from \"babel-dead-code-elimination\";\n\nimport type { Babel, NodePath, ParseResult } from \"./babel\";\nimport { traverse } from \"./babel\";\n\nexport const removeExports = (\n  ast: ParseResult<Babel.File>,\n  exportsToRemove: readonly string[],\n) => {\n  let previouslyReferencedIdentifiers = findReferencedIdentifiers(ast);\n  let exportsFiltered = false;\n  let markedForRemoval = new Set<NodePath<Babel.Node>>();\n  // Keep track of identifiers referenced by removed exports,\n  // e.g. export { localName as exportName }, export default function localName\n  let removedExportLocalNames = new Set<string>();\n\n  traverse(ast, {\n    ExportDeclaration(path) {\n      // export { foo };\n      // export { bar } from \"./module\";\n      if (path.node.type === \"ExportNamedDeclaration\") {\n        if (path.node.specifiers.length) {\n          path.node.specifiers = path.node.specifiers.filter((specifier) => {\n            // Filter out individual specifiers\n            if (\n              specifier.type === \"ExportSpecifier\" &&\n              specifier.exported.type === \"Identifier\"\n            ) {\n              if (exportsToRemove.includes(specifier.exported.name)) {\n                exportsFiltered = true;\n                // Track the local identifier if it's different from the exported name\n                if (\n                  specifier.local &&\n                  specifier.local.name !== specifier.exported.name\n                ) {\n                  removedExportLocalNames.add(specifier.local.name);\n                }\n                return false;\n              }\n            }\n            return true;\n          });\n          // Remove the entire export statement if all specifiers were removed\n          if (path.node.specifiers.length === 0) {\n            markedForRemoval.add(path);\n          }\n        }\n\n        // export const foo = ...;\n        // export const [ foo ] = ...;\n        if (path.node.declaration?.type === \"VariableDeclaration\") {\n          let declaration = path.node.declaration;\n          declaration.declarations = declaration.declarations.filter(\n            (declaration) => {\n              // export const foo = ...;\n              // export const foo = ..., bar = ...;\n              if (\n                declaration.id.type === \"Identifier\" &&\n                exportsToRemove.includes(declaration.id.name)\n              ) {\n                // Filter out individual variables\n                exportsFiltered = true;\n                return false;\n              }\n\n              // export const [ foo ] = ...;\n              // export const { foo } = ...;\n              if (\n                declaration.id.type === \"ArrayPattern\" ||\n                declaration.id.type === \"ObjectPattern\"\n              ) {\n                // NOTE: These exports cannot be safely removed, so instead we\n                // validate them to ensure that any exports that are intended to\n                // be removed are not present\n                validateDestructuredExports(declaration.id, exportsToRemove);\n              }\n\n              return true;\n            },\n          );\n          // Remove the entire export statement if all variables were removed\n          if (declaration.declarations.length === 0) {\n            markedForRemoval.add(path);\n          }\n        }\n\n        // export function foo() {}\n        if (path.node.declaration?.type === \"FunctionDeclaration\") {\n          let id = path.node.declaration.id;\n          if (id && exportsToRemove.includes(id.name)) {\n            markedForRemoval.add(path);\n          }\n        }\n\n        // export class Foo() {}\n        if (path.node.declaration?.type === \"ClassDeclaration\") {\n          let id = path.node.declaration.id;\n          if (id && exportsToRemove.includes(id.name)) {\n            markedForRemoval.add(path);\n          }\n        }\n      }\n\n      // export default ...;\n      if (path.node.type === \"ExportDefaultDeclaration\") {\n        if (exportsToRemove.includes(\"default\")) {\n          markedForRemoval.add(path);\n          // Track the identifier being exported as default\n          if (path.node.declaration) {\n            if (path.node.declaration.type === \"Identifier\") {\n              removedExportLocalNames.add(path.node.declaration.name);\n            } else if (\n              (path.node.declaration.type === \"FunctionDeclaration\" ||\n                path.node.declaration.type === \"ClassDeclaration\") &&\n              path.node.declaration.id\n            ) {\n              removedExportLocalNames.add(path.node.declaration.id.name);\n            }\n          }\n        }\n      }\n    },\n  });\n\n  // Remove top-level property assignments to removed exports. Handles\n  // `clientLoader.hydrate = true`, `Component.displayName = \"...\"`, etc.\n  traverse(ast, {\n    ExpressionStatement(path) {\n      // Only handle top-level statements\n      if (!path.parentPath.isProgram()) {\n        return;\n      }\n\n      if (path.node.expression.type === \"AssignmentExpression\") {\n        const left = path.node.expression.left;\n        if (\n          left.type === \"MemberExpression\" &&\n          left.object.type === \"Identifier\" &&\n          (exportsToRemove.includes(left.object.name) ||\n            removedExportLocalNames.has(left.object.name))\n        ) {\n          markedForRemoval.add(path);\n        }\n      }\n    },\n  });\n\n  if (markedForRemoval.size > 0 || exportsFiltered) {\n    for (let path of markedForRemoval) {\n      path.remove();\n    }\n\n    // Run dead code elimination on any newly unreferenced identifiers\n    deadCodeElimination(ast, previouslyReferencedIdentifiers);\n  }\n};\n\nfunction validateDestructuredExports(\n  id: Babel.ArrayPattern | Babel.ObjectPattern,\n  exportsToRemove: readonly string[],\n) {\n  if (id.type === \"ArrayPattern\") {\n    for (let element of id.elements) {\n      if (!element) {\n        continue;\n      }\n\n      // [ foo ]\n      if (\n        element.type === \"Identifier\" &&\n        exportsToRemove.includes(element.name)\n      ) {\n        throw invalidDestructureError(element.name);\n      }\n\n      // [ ...foo ]\n      if (\n        element.type === \"RestElement\" &&\n        element.argument.type === \"Identifier\" &&\n        exportsToRemove.includes(element.argument.name)\n      ) {\n        throw invalidDestructureError(element.argument.name);\n      }\n\n      // [ [...] ]\n      // [ {...} ]\n      if (element.type === \"ArrayPattern\" || element.type === \"ObjectPattern\") {\n        validateDestructuredExports(element, exportsToRemove);\n      }\n    }\n  }\n\n  if (id.type === \"ObjectPattern\") {\n    for (let property of id.properties) {\n      if (!property) {\n        continue;\n      }\n\n      if (\n        property.type === \"ObjectProperty\" &&\n        property.key.type === \"Identifier\"\n      ) {\n        // { foo }\n        if (\n          property.value.type === \"Identifier\" &&\n          exportsToRemove.includes(property.value.name)\n        ) {\n          throw invalidDestructureError(property.value.name);\n        }\n\n        // { foo: [...] }\n        // { foo: {...} }\n        if (\n          property.value.type === \"ArrayPattern\" ||\n          property.value.type === \"ObjectPattern\"\n        ) {\n          validateDestructuredExports(property.value, exportsToRemove);\n        }\n      }\n\n      // { ...foo }\n      if (\n        property.type === \"RestElement\" &&\n        property.argument.type === \"Identifier\" &&\n        exportsToRemove.includes(property.argument.name)\n      ) {\n        throw invalidDestructureError(property.argument.name);\n      }\n    }\n  }\n}\n\nfunction invalidDestructureError(name: string) {\n  return new Error(`Cannot remove destructured export \"${name}\"`);\n}\n"
  },
  {
    "path": "packages/react-router-dev/vite/resolve-file-url.ts",
    "content": "import * as path from \"node:path\";\n\nimport { getVite } from \"./vite\";\n\nexport const resolveFileUrl = (\n  { rootDirectory }: { rootDirectory: string },\n  filePath: string,\n) => {\n  let vite = getVite();\n  let relativePath = path.relative(rootDirectory, filePath);\n  let isWithinRoot =\n    !relativePath.startsWith(\"..\") && !path.isAbsolute(relativePath);\n\n  if (!isWithinRoot) {\n    // Vite will prevent serving files outside of the workspace\n    // unless user explicitly opts in with `server.fs.allow`\n    // https://vitejs.dev/config/server-options.html#server-fs-allow\n    return path.posix.join(\"/@fs\", vite.normalizePath(filePath));\n  }\n\n  return \"/\" + vite.normalizePath(relativePath);\n};\n"
  },
  {
    "path": "packages/react-router-dev/vite/resolve-relative-route-file-path.ts",
    "content": "import path from \"pathe\";\nimport type { ResolvedReactRouterConfig } from \"../config/config\";\nimport type { RouteManifestEntry } from \"../config/routes\";\nimport { getVite } from \"./vite\";\n\nexport function resolveRelativeRouteFilePath(\n  route: RouteManifestEntry,\n  reactRouterConfig: ResolvedReactRouterConfig,\n) {\n  let vite = getVite();\n  let file = route.file;\n  let fullPath = path.resolve(reactRouterConfig.appDirectory, file);\n  return vite.normalizePath(fullPath);\n}\n"
  },
  {
    "path": "packages/react-router-dev/vite/route-chunks-test.ts",
    "content": "import dedent from \"dedent\";\n\nimport type { Cache } from \"./cache\";\nimport {\n  hasChunkableExport,\n  getChunkedExport,\n  omitChunkedExports,\n} from \"./route-chunks\";\n\nlet cache: [Cache, string] = [new Map(), \"cacheKey\"];\n\ndescribe(\"route chunks\", () => {\n  describe(\"chunkable\", () => {\n    test(\"functions with no identifiers\", () => {\n      const code = dedent`\n        export default function () { return null; }\n        export function target1() { return null; }\n        export function other1() { return null; }\n        export const target2 = () => null;\n        export const other2 = () => null;\n      `;\n      expect(hasChunkableExport(code, \"default\", ...cache)).toBe(true);\n      expect(hasChunkableExport(code, \"target1\", ...cache)).toBe(true);\n      expect(hasChunkableExport(code, \"target2\", ...cache)).toBe(true);\n      expect(getChunkedExport(code, \"default\", {}, ...cache)?.code)\n        .toMatchInlineSnapshot(`\n        \"export default function () {\n          return null;\n        }\"\n      `);\n      expect(getChunkedExport(code, \"target1\", {}, ...cache)?.code)\n        .toMatchInlineSnapshot(`\n        \"export function target1() {\n          return null;\n        }\"\n      `);\n      expect(\n        getChunkedExport(code, \"target2\", {}, ...cache)?.code,\n      ).toMatchInlineSnapshot(`\"export const target2 = () => null;\"`);\n      expect(\n        omitChunkedExports(\n          code,\n          [\"default\", \"target1\", \"target2\"],\n          {},\n          ...cache,\n        )?.code,\n      ).toMatchInlineSnapshot(`\n        \"export function other1() {\n          return null;\n        }\n        export const other2 = () => null;\"\n      `);\n    });\n\n    test(\"functions referencing their own identifiers\", () => {\n      const code = dedent`\n        import defaultMessage, {\n          targetMessage1,\n          targetMessage2,\n          otherMessage1,\n          otherMessage2,\n        } from \"./messages\";\n        import * as messages from \"./messages\"; \n\n        const getDefaultMessage = () => defaultMessage;\n        const getTargetMessage1 = () => targetMessage1;\n        const getOtherMessage1 = () => otherMessage1;\n        function getTargetMessage2() { return targetMessage2; }\n        function getOtherMessage2() { return otherMessage2; }\n        function getNamespacedMessage() { return messages.namespacedMessage; }\n\n        export default function() { return getDefaultMessage(); }\n        export function namespaced() { getNamespacedMessage(); }\n        export function target1() { return getTargetMessage1(); }\n        export function other1() { return getOtherMessage1(); }\n        export const target2 = () => getTargetMessage2();\n        export const other2 = () => getOtherMessage2();\n      `;\n      expect(hasChunkableExport(code, \"default\", ...cache)).toBe(true);\n      expect(hasChunkableExport(code, \"namespaced\", ...cache)).toBe(true);\n      expect(hasChunkableExport(code, \"target1\", ...cache)).toBe(true);\n      expect(hasChunkableExport(code, \"target2\", ...cache)).toBe(true);\n      expect(getChunkedExport(code, \"default\", {}, ...cache)?.code)\n        .toMatchInlineSnapshot(`\n        \"import defaultMessage from \"./messages\";\n        const getDefaultMessage = () => defaultMessage;\n        export default function () {\n          return getDefaultMessage();\n        }\"\n      `);\n      expect(getChunkedExport(code, \"namespaced\", {}, ...cache)?.code)\n        .toMatchInlineSnapshot(`\n        \"import * as messages from \"./messages\";\n        function getNamespacedMessage() {\n          return messages.namespacedMessage;\n        }\n        export function namespaced() {\n          getNamespacedMessage();\n        }\"\n      `);\n      expect(getChunkedExport(code, \"target1\", {}, ...cache)?.code)\n        .toMatchInlineSnapshot(`\n        \"import { targetMessage1 } from \"./messages\";\n        const getTargetMessage1 = () => targetMessage1;\n        export function target1() {\n          return getTargetMessage1();\n        }\"\n      `);\n      expect(getChunkedExport(code, \"target2\", {}, ...cache)?.code)\n        .toMatchInlineSnapshot(`\n        \"import { targetMessage2 } from \"./messages\";\n        function getTargetMessage2() {\n          return targetMessage2;\n        }\n        export const target2 = () => getTargetMessage2();\"\n      `);\n      expect(\n        omitChunkedExports(\n          code,\n          [\"default\", \"target1\", \"target2\", \"namespaced\"],\n          {},\n          ...cache,\n        )?.code,\n      ).toMatchInlineSnapshot(`\n        \"import { otherMessage1, otherMessage2 } from \"./messages\";\n        const getOtherMessage1 = () => otherMessage1;\n        function getOtherMessage2() {\n          return otherMessage2;\n        }\n        export function other1() {\n          return getOtherMessage1();\n        }\n        export const other2 = () => getOtherMessage2();\"\n      `);\n    });\n\n    test(\"imports and exports using shared statements\", () => {\n      const code = dedent`\n        import { chunk1Import, chunk2Import, mainImport } from \"./shared\";\n        const chunk1 = () => chunk1Import;\n        const chunk2 = () => chunk2Import;\n        const main = () => mainImport;\n        export { chunk1, chunk2, main };\n      `;\n      expect(hasChunkableExport(code, \"chunk1\", ...cache)).toBe(true);\n      expect(hasChunkableExport(code, \"chunk2\", ...cache)).toBe(true);\n      expect(hasChunkableExport(code, \"main\", ...cache)).toBe(true);\n      expect(getChunkedExport(code, \"chunk1\", {}, ...cache)?.code)\n        .toMatchInlineSnapshot(`\n        \"import { chunk1Import } from \"./shared\";\n        const chunk1 = () => chunk1Import;\n        export { chunk1 };\"\n      `);\n      expect(getChunkedExport(code, \"chunk2\", {}, ...cache)?.code)\n        .toMatchInlineSnapshot(`\n        \"import { chunk2Import } from \"./shared\";\n        const chunk2 = () => chunk2Import;\n        export { chunk2 };\"\n      `);\n      expect(omitChunkedExports(code, [\"chunk1\", \"chunk2\"], {}, ...cache)?.code)\n        .toMatchInlineSnapshot(`\n        \"import { mainImport } from \"./shared\";\n        const main = () => mainImport;\n        export { main };\"\n      `);\n    });\n\n    test(\"shared imports\", () => {\n      const code = dedent`\n        import { shared } from \"./shared\";\n        export const chunk = shared(\"chunk\");\n        export const main = shared(\"main\");\n      `;\n      expect(hasChunkableExport(code, \"chunk\", ...cache)).toBe(true);\n      expect(hasChunkableExport(code, \"main\", ...cache)).toBe(true);\n      expect(getChunkedExport(code, \"chunk\", {}, ...cache)?.code)\n        .toMatchInlineSnapshot(`\n        \"import { shared } from \"./shared\";\n        export const chunk = shared(\"chunk\");\"\n      `);\n      expect(omitChunkedExports(code, [\"chunk\"], {}, ...cache)?.code)\n        .toMatchInlineSnapshot(`\n        \"import { shared } from \"./shared\";\n        export const main = shared(\"main\");\"\n      `);\n    });\n\n    test(\"shared imports across chunks but not main chunk\", () => {\n      const code = dedent`\n        import { shared } from \"./shared\";\n        export const chunk1 = shared(\"chunk1\");\n        export const chunk2 = shared(\"chunk2\");\n        export const main = \"main\";\n      `;\n      expect(hasChunkableExport(code, \"chunk1\", ...cache)).toBe(true);\n      expect(hasChunkableExport(code, \"chunk2\", ...cache)).toBe(true);\n      expect(hasChunkableExport(code, \"main\", ...cache)).toBe(true);\n      expect(getChunkedExport(code, \"chunk1\", {}, ...cache)?.code)\n        .toMatchInlineSnapshot(`\n        \"import { shared } from \"./shared\";\n        export const chunk1 = shared(\"chunk1\");\"\n      `);\n      expect(getChunkedExport(code, \"chunk2\", {}, ...cache)?.code)\n        .toMatchInlineSnapshot(`\n        \"import { shared } from \"./shared\";\n        export const chunk2 = shared(\"chunk2\");\"\n      `);\n      expect(\n        omitChunkedExports(code, [\"chunk1\", \"chunk2\"], {}, ...cache)?.code,\n      ).toMatchInlineSnapshot(`\"export const main = \"main\";\"`);\n    });\n\n    test(\"import with side effect usage\", () => {\n      const code = dedent`\n        import { sideEffect } from \"./side-effect\";\n        sideEffect();\n        export const chunk = \"chunk\";\n        export const main = \"main\";\n      `;\n      expect(hasChunkableExport(code, \"chunk\", ...cache)).toBe(true);\n      expect(hasChunkableExport(code, \"main\", ...cache)).toBe(true);\n      expect(\n        getChunkedExport(code, \"chunk\", {}, ...cache)?.code,\n      ).toMatchInlineSnapshot(`\"export const chunk = \"chunk\";\"`);\n      expect(omitChunkedExports(code, [\"chunk\"], {}, ...cache)?.code)\n        .toMatchInlineSnapshot(`\n        \"import { sideEffect } from \"./side-effect\";\n        sideEffect();\n        export const main = \"main\";\"\n      `);\n    });\n\n    test(\"re-exports using shared statement\", () => {\n      const code = dedent`\n        export { chunk1, chunk2, main } from \"./shared\";\n      `;\n      expect(hasChunkableExport(code, \"chunk1\", ...cache)).toBe(true);\n      expect(hasChunkableExport(code, \"chunk2\", ...cache)).toBe(true);\n      expect(hasChunkableExport(code, \"main\", ...cache)).toBe(true);\n      expect(\n        getChunkedExport(code, \"chunk1\", {}, ...cache)?.code,\n      ).toMatchInlineSnapshot(`\"export { chunk1 } from \"./shared\";\"`);\n      expect(\n        getChunkedExport(code, \"chunk2\", {}, ...cache)?.code,\n      ).toMatchInlineSnapshot(`\"export { chunk2 } from \"./shared\";\"`);\n      expect(\n        omitChunkedExports(code, [\"chunk1\", \"chunk2\"], {}, ...cache)?.code,\n      ).toMatchInlineSnapshot(`\"export { main } from \"./shared\";\"`);\n    });\n\n    test(\"aliased re-exports using shared statement\", () => {\n      const code = dedent`\n        export {\n          foo as chunk1,\n          bar as chunk2,\n          baz as main,\n        } from \"./shared\";\n      `;\n      expect(hasChunkableExport(code, \"chunk1\", ...cache)).toBe(true);\n      expect(hasChunkableExport(code, \"chunk2\", ...cache)).toBe(true);\n      expect(hasChunkableExport(code, \"main\", ...cache)).toBe(true);\n      expect(\n        getChunkedExport(code, \"chunk1\", {}, ...cache)?.code,\n      ).toMatchInlineSnapshot(`\"export { foo as chunk1 } from \"./shared\";\"`);\n      expect(\n        getChunkedExport(code, \"chunk2\", {}, ...cache)?.code,\n      ).toMatchInlineSnapshot(`\"export { bar as chunk2 } from \"./shared\";\"`);\n      expect(\n        omitChunkedExports(code, [\"chunk1\", \"chunk2\"], {}, ...cache)?.code,\n      ).toMatchInlineSnapshot(`\"export { baz as main } from \"./shared\";\"`);\n    });\n\n    test(\"isolated exported variable declarations sharing an export statement\", () => {\n      const code = dedent`\n        import { chunkMessage, mainMessage } from \"./messages\";\n        export const chunk = chunkMessage,\n          main = mainMessage;\n      `;\n\n      expect(hasChunkableExport(code, \"chunk\", ...cache)).toBe(true);\n      expect(hasChunkableExport(code, \"main\", ...cache)).toBe(true);\n      expect(getChunkedExport(code, \"chunk\", {}, ...cache)?.code)\n        .toMatchInlineSnapshot(`\n        \"import { chunkMessage } from \"./messages\";\n        export const chunk = chunkMessage;\"\n      `);\n      expect(omitChunkedExports(code, [\"chunk\"], {}, ...cache)?.code)\n        .toMatchInlineSnapshot(`\n        \"import { mainMessage } from \"./messages\";\n        export const main = mainMessage;\"\n      `);\n    });\n\n    test(\"isolated exported destructured array variable declarations sharing an export statement\", () => {\n      const code = dedent`\n        import { chunkMessage, mainMessage } from \"./messages\";\n        export const [chunk] = [chunkMessage],\n          [main] = [mainMessage];\n      `;\n\n      expect(hasChunkableExport(code, \"chunk\", ...cache)).toBe(true);\n      expect(hasChunkableExport(code, \"main\", ...cache)).toBe(true);\n      expect(getChunkedExport(code, \"chunk\", {}, ...cache)?.code)\n        .toMatchInlineSnapshot(`\n        \"import { chunkMessage } from \"./messages\";\n        export const [chunk] = [chunkMessage];\"\n      `);\n      expect(omitChunkedExports(code, [\"chunk\"], {}, ...cache)?.code)\n        .toMatchInlineSnapshot(`\n        \"import { mainMessage } from \"./messages\";\n        export const [main] = [mainMessage];\"\n      `);\n    });\n\n    test(\"isolated exported destructured array spread variable declarations sharing an export statement\", () => {\n      const code = dedent`\n        import { chunkMessage, mainMessage } from \"./messages\";\n        export const [...chunk] = [...chunkMessage],\n          [...main] = [...mainMessage];\n      `;\n\n      expect(hasChunkableExport(code, \"chunk\", ...cache)).toBe(true);\n      expect(hasChunkableExport(code, \"main\", ...cache)).toBe(true);\n      expect(getChunkedExport(code, \"chunk\", {}, ...cache)?.code)\n        .toMatchInlineSnapshot(`\n        \"import { chunkMessage } from \"./messages\";\n        export const [...chunk] = [...chunkMessage];\"\n      `);\n      expect(omitChunkedExports(code, [\"chunk\"], {}, ...cache)?.code)\n        .toMatchInlineSnapshot(`\n        \"import { mainMessage } from \"./messages\";\n        export const [...main] = [...mainMessage];\"\n      `);\n    });\n\n    test(\"isolated exported destructured object variable declarations sharing an export statement\", () => {\n      const code = dedent`\n        import { chunkMessage, mainMessage } from \"./messages\";\n        export const { chunkMessage: chunk } = { chunkMessage },\n          { mainMessage: main } = { mainMessage };\n      `;\n\n      expect(hasChunkableExport(code, \"chunk\", ...cache)).toBe(true);\n      expect(hasChunkableExport(code, \"main\", ...cache)).toBe(true);\n      expect(getChunkedExport(code, \"chunk\", {}, ...cache)?.code)\n        .toMatchInlineSnapshot(`\n        \"import { chunkMessage } from \"./messages\";\n        export const {\n          chunkMessage: chunk\n        } = {\n          chunkMessage\n        };\"\n      `);\n      expect(omitChunkedExports(code, [\"chunk\"], {}, ...cache)?.code)\n        .toMatchInlineSnapshot(`\n        \"import { mainMessage } from \"./messages\";\n        export const {\n          mainMessage: main\n        } = {\n          mainMessage\n        };\"\n      `);\n    });\n\n    test(\"isolated exported destructured object spread variable declarations sharing an export statement\", () => {\n      const code = dedent`\n        import { chunkMessage, mainMessage } from \"./messages\";\n        export const { ...chunk } = { ...chunkMessage },\n          { ...main } = { ...mainMessage };\n      `;\n\n      expect(hasChunkableExport(code, \"chunk\", ...cache)).toBe(true);\n      expect(hasChunkableExport(code, \"main\", ...cache)).toBe(true);\n      expect(getChunkedExport(code, \"chunk\", {}, ...cache)?.code)\n        .toMatchInlineSnapshot(`\n        \"import { chunkMessage } from \"./messages\";\n        export const {\n          ...chunk\n        } = {\n          ...chunkMessage\n        };\"\n      `);\n      expect(omitChunkedExports(code, [\"chunk\"], {}, ...cache)?.code)\n        .toMatchInlineSnapshot(`\n        \"import { mainMessage } from \"./messages\";\n        export const {\n          ...main\n        } = {\n          ...mainMessage\n        };\"\n      `);\n    });\n\n    test(\"isolated exported nested destructured variable declarations sharing an export statement\", () => {\n      const code = dedent`\n        import { chunkMessage, mainMessage } from \"./messages\";\n        export const [, { nested: { ...chunk } }] = [null, { nested: { ...chunkMessage } }],\n          [, { nested: { ...main } }] = [null, { nested: { ...mainMessage } }];\n      `;\n\n      expect(hasChunkableExport(code, \"chunk\", ...cache)).toBe(true);\n      expect(hasChunkableExport(code, \"main\", ...cache)).toBe(true);\n      expect(getChunkedExport(code, \"chunk\", {}, ...cache)?.code)\n        .toMatchInlineSnapshot(`\n        \"import { chunkMessage } from \"./messages\";\n        export const [, {\n          nested: {\n            ...chunk\n          }\n        }] = [null, {\n          nested: {\n            ...chunkMessage\n          }\n        }];\"\n      `);\n      expect(omitChunkedExports(code, [\"chunk\"], {}, ...cache)?.code)\n        .toMatchInlineSnapshot(`\n        \"import { mainMessage } from \"./messages\";\n        export const [, {\n          nested: {\n            ...main\n          }\n        }] = [null, {\n          nested: {\n            ...mainMessage\n          }\n        }];\"\n      `);\n    });\n\n    test(\"shared browser global usage\", () => {\n      const code = dedent`\n        export const chunk1 = () => localStorage.getItem(\"chunk1\");\n        export const chunk2 = () => localStorage.getItem(\"chunk2\");\n        export const main = () => localStorage.getItem(\"main\");\n      `;\n\n      expect(hasChunkableExport(code, \"chunk1\", ...cache)).toBe(true);\n      expect(hasChunkableExport(code, \"chunk2\", ...cache)).toBe(true);\n      expect(hasChunkableExport(code, \"main\", ...cache)).toBe(true);\n      expect(\n        getChunkedExport(code, \"chunk1\", {}, ...cache)?.code,\n      ).toMatchInlineSnapshot(\n        `\"export const chunk1 = () => localStorage.getItem(\"chunk1\");\"`,\n      );\n      expect(\n        getChunkedExport(code, \"chunk2\", {}, ...cache)?.code,\n      ).toMatchInlineSnapshot(\n        `\"export const chunk2 = () => localStorage.getItem(\"chunk2\");\"`,\n      );\n      expect(\n        omitChunkedExports(code, [\"chunk1\", \"chunk2\"], {}, ...cache)?.code,\n      ).toMatchInlineSnapshot(\n        `\"export const main = () => localStorage.getItem(\"main\");\"`,\n      );\n    });\n\n    test(\"empty exports are placed in main chunk\", () => {\n      const code = dedent`\n        export const chunk = \"chunk\";\n        export {};\n      `;\n\n      expect(hasChunkableExport(code, \"chunk\", ...cache)).toBe(true);\n      expect(\n        getChunkedExport(code, \"chunk\", {}, ...cache)?.code,\n      ).toMatchInlineSnapshot(`\"export const chunk = \"chunk\";\"`);\n      expect(\n        omitChunkedExports(code, [\"chunk\"], {}, ...cache)?.code,\n      ).toMatchInlineSnapshot(`\"export {};\"`);\n    });\n\n    test(\"side effect imports are placed in main chunk\", () => {\n      const code = dedent`\n        import \"./side-effect\";\n        export const chunk = \"chunk\";\n        export const main = \"main\";\n      `;\n\n      expect(hasChunkableExport(code, \"chunk\", ...cache)).toBe(true);\n      expect(\n        getChunkedExport(code, \"chunk\", {}, ...cache)?.code,\n      ).toMatchInlineSnapshot(`\"export const chunk = \"chunk\";\"`);\n      expect(omitChunkedExports(code, [\"chunk\"], {}, ...cache)?.code)\n        .toMatchInlineSnapshot(`\n        \"import \"./side-effect\";\n        export const main = \"main\";\"\n      `);\n    });\n\n    test(\"unused imports are placed in main chunk\", () => {\n      const code = dedent`\n        import { unused } from \"./unused\";\n        export const chunk = \"chunk\";\n        export const main = \"main\";\n      `;\n\n      expect(hasChunkableExport(code, \"chunk\", ...cache)).toBe(true);\n      expect(\n        getChunkedExport(code, \"chunk\", {}, ...cache)?.code,\n      ).toMatchInlineSnapshot(`\"export const chunk = \"chunk\";\"`);\n      expect(omitChunkedExports(code, [\"chunk\"], {}, ...cache)?.code)\n        .toMatchInlineSnapshot(`\n        \"import { unused } from \"./unused\";\n        export const main = \"main\";\"\n      `);\n    });\n  });\n\n  describe(\"partially chunkable\", () => {\n    test(\"function with no identifiers, one with shared identifiers\", () => {\n      const code = dedent`\n      import defaultMessage, { target1Message } from \"./messages\";\n      import { sharedMessage } from \"./sharedMessage\";\n\n      const getDefaultMessage = () => defaultMessage;\n      const getTarget1Message = () => target1Message;\n      const getSharedMessage = () => sharedMessage;\n\n      export default function () { return getDefaultMessage(); }\n      export function target1() { return getTarget1Message(); }\n      export const target2 = () => getSharedMessage();\n      export const other1 = () => getSharedMessage();\n      export const other2 = () => getSharedMessage();\n    `;\n      expect(hasChunkableExport(code, \"default\", ...cache)).toBe(true);\n      expect(hasChunkableExport(code, \"target1\", ...cache)).toBe(true);\n      expect(hasChunkableExport(code, \"target2\", ...cache)).toBe(false);\n      expect(getChunkedExport(code, \"default\", {}, ...cache)?.code)\n        .toMatchInlineSnapshot(`\n        \"import defaultMessage from \"./messages\";\n        const getDefaultMessage = () => defaultMessage;\n        export default function () {\n          return getDefaultMessage();\n        }\"\n      `);\n      expect(getChunkedExport(code, \"target1\", {}, ...cache)?.code)\n        .toMatchInlineSnapshot(`\n        \"import { target1Message } from \"./messages\";\n        const getTarget1Message = () => target1Message;\n        export function target1() {\n          return getTarget1Message();\n        }\"\n      `);\n      expect(getChunkedExport(code, \"target2\", {}, ...cache)).toBeUndefined();\n      expect(\n        omitChunkedExports(\n          code,\n          [\"default\", \"target1\", \"target2\"],\n          {},\n          ...cache,\n        )?.code,\n      ).toMatchInlineSnapshot(`\n        \"import { sharedMessage } from \"./sharedMessage\";\n        const getSharedMessage = () => sharedMessage;\n        export const target2 = () => getSharedMessage();\n        export const other1 = () => getSharedMessage();\n        export const other2 = () => getSharedMessage();\"\n      `);\n    });\n\n    test(\"function referencing its own identifiers, another one sharing an identifier\", () => {\n      const code = dedent`\n        import { targetMessage1 } from \"./targetMessage1\";\n        import { sharedMessage } from \"./sharedMessage\";\n\n        const getTargetMessage1 = () => targetMessage1;\n        const getOtherMessage1 = () => sharedMessage;\n        function getTargetMessage2() { return sharedMessage; }\n        function getOtherMessage2() { return sharedMessage; }\n\n        export function target1() { return getTargetMessage1(); }\n        export function other1() { return getOtherMessage1(); }\n        export const target2 = () => getTargetMessage2();\n        export const other2 = () => getOtherMessage2();\n      `;\n      expect(hasChunkableExport(code, \"target1\", ...cache)).toBe(true);\n      expect(hasChunkableExport(code, \"target2\", ...cache)).toBe(false);\n      expect(getChunkedExport(code, \"target1\", {}, ...cache)?.code)\n        .toMatchInlineSnapshot(`\n        \"import { targetMessage1 } from \"./targetMessage1\";\n        const getTargetMessage1 = () => targetMessage1;\n        export function target1() {\n          return getTargetMessage1();\n        }\"\n      `);\n      expect(getChunkedExport(code, \"target2\", {}, ...cache)).toBeUndefined();\n      expect(\n        omitChunkedExports(code, [\"target1\", \"target2\"], {}, ...cache)?.code,\n      ).toMatchInlineSnapshot(`\n        \"import { sharedMessage } from \"./sharedMessage\";\n        const getOtherMessage1 = () => sharedMessage;\n        function getTargetMessage2() {\n          return sharedMessage;\n        }\n        function getOtherMessage2() {\n          return sharedMessage;\n        }\n        export function other1() {\n          return getOtherMessage1();\n        }\n        export const target2 = () => getTargetMessage2();\n        export const other2 = () => getOtherMessage2();\"\n      `);\n    });\n\n    test(\"isolated exported variable declarations sharing an export statement, another one with shared variable declaration\", () => {\n      const code = dedent`\n      import { chunkableMessage, unchunkableMessage } from \"./messages\";\n        export const chunkable = chunkableMessage,\n          unchunkable = unchunkableMessage,\n          main = unchunkable;\n      `;\n\n      expect(hasChunkableExport(code, \"chunkable\", ...cache)).toBe(true);\n      expect(hasChunkableExport(code, \"unchunkable\", ...cache)).toBe(false);\n      expect(hasChunkableExport(code, \"main\", ...cache)).toBe(false);\n      expect(getChunkedExport(code, \"chunkable\", {}, ...cache)?.code)\n        .toMatchInlineSnapshot(`\n        \"import { chunkableMessage } from \"./messages\";\n        export const chunkable = chunkableMessage;\"\n      `);\n      expect(\n        getChunkedExport(code, \"unchunkable\", {}, ...cache),\n      ).toBeUndefined();\n      expect(\n        omitChunkedExports(code, [\"chunkable\", \"unchunkable\"], {}, ...cache)\n          ?.code,\n      ).toMatchInlineSnapshot(`\n        \"import { unchunkableMessage } from \"./messages\";\n        export const unchunkable = unchunkableMessage,\n          main = unchunkable;\"\n      `);\n    });\n\n    test(\"isolated exported destructured variable declarations sharing an export statement, another one with shared variable declaration\", () => {\n      const code = dedent`\n      import { chunkableMessage, unchunkableMessage } from \"./messages\";\n        export const { chunkableMessage: chunkable } = { chunkableMessage },\n          [unchunkable] = [unchunkableMessage],\n          { unchunkable: main } = { unchunkable };\n      `;\n\n      expect(hasChunkableExport(code, \"chunkable\", ...cache)).toBe(true);\n      expect(hasChunkableExport(code, \"unchunkable\", ...cache)).toBe(false);\n      expect(hasChunkableExport(code, \"main\", ...cache)).toBe(false);\n      expect(getChunkedExport(code, \"chunkable\", {}, ...cache)?.code)\n        .toMatchInlineSnapshot(`\n        \"import { chunkableMessage } from \"./messages\";\n        export const {\n          chunkableMessage: chunkable\n        } = {\n          chunkableMessage\n        };\"\n      `);\n      expect(\n        getChunkedExport(code, \"unchunkable\", {}, ...cache),\n      ).toBeUndefined();\n      expect(\n        omitChunkedExports(code, [\"chunkable\", \"unchunkable\"], {}, ...cache)\n          ?.code,\n      ).toMatchInlineSnapshot(`\n        \"import { unchunkableMessage } from \"./messages\";\n        export const [unchunkable] = [unchunkableMessage],\n          {\n            unchunkable: main\n          } = {\n            unchunkable\n          };\"\n      `);\n    });\n  });\n\n  describe(\"not chunkable\", () => {\n    test(\"exports not present\", () => {\n      const code = dedent`\n        export default function () {}\n      `;\n      expect(hasChunkableExport(code, \"target1\", ...cache)).toBe(false);\n      expect(getChunkedExport(code, \"target1\", {}, ...cache)).toBeUndefined();\n      expect(hasChunkableExport(code, \"target2\", ...cache)).toBe(false);\n      expect(getChunkedExport(code, \"target2\", {}, ...cache)).toBeUndefined();\n      expect(\n        omitChunkedExports(code, [\"target1\", \"target2\"], {}, ...cache)?.code,\n      ).toMatchInlineSnapshot(`\"export default function () {}\"`);\n    });\n\n    test(\"functions sharing a variable\", () => {\n      const code = dedent`\n        const sharedMessage = \"shared\";\n        const getTargetMessage1 = () => sharedMessage;\n        const getTargetMessage2 = () => sharedMessage;\n        const getOtherMessage1 = () => sharedMessage;\n        const getOtherMessage2 = () => sharedMessage;\n        export const target1 = () => getTargetMessage1();\n        export const target2 = () => getTargetMessage2();\n        export const other1 = () => getOtherMessage1();\n        export const other2 = () => getOtherMessage2();\n      `;\n      expect(hasChunkableExport(code, \"target1\", ...cache)).toBe(false);\n      expect(getChunkedExport(code, \"target1\", {}, ...cache)).toBeUndefined();\n      expect(hasChunkableExport(code, \"target2\", ...cache)).toBe(false);\n      expect(getChunkedExport(code, \"target2\", {}, ...cache)).toBeUndefined();\n      expect(\n        omitChunkedExports(code, [\"target1\", \"target2\"], {}, ...cache)?.code,\n      ).toMatchInlineSnapshot(`\n        \"const sharedMessage = \"shared\";\n        const getTargetMessage1 = () => sharedMessage;\n        const getTargetMessage2 = () => sharedMessage;\n        const getOtherMessage1 = () => sharedMessage;\n        const getOtherMessage2 = () => sharedMessage;\n        export const target1 = () => getTargetMessage1();\n        export const target2 = () => getTargetMessage2();\n        export const other1 = () => getOtherMessage1();\n        export const other2 = () => getOtherMessage2();\"\n      `);\n    });\n\n    test(\"functions sharing an imported identifier\", () => {\n      const code = dedent`\n        import { sharedMessage } from \"./messages\";\n        const getTargetMessage1 = () => sharedMessage;\n        const getTargetMessage2 = () => sharedMessage;\n        const getOtherMessage1 = () => sharedMessage;\n        const getOtherMessage2 = () => sharedMessage;\n        export const target1 = () => getTargetMessage1();\n        export const target2 = () => getTargetMessage2();\n        export const other1 = () => getOtherMessage1();\n        export const other2 = () => getOtherMessage2();\n      `;\n      expect(hasChunkableExport(code, \"target1\", ...cache)).toBe(false);\n      expect(getChunkedExport(code, \"target1\", {}, ...cache)).toBeUndefined();\n      expect(hasChunkableExport(code, \"target2\", ...cache)).toBe(false);\n      expect(getChunkedExport(code, \"target2\", {}, ...cache)).toBeUndefined();\n      expect(\n        omitChunkedExports(code, [\"target1\", \"target2\"], {}, ...cache)?.code,\n      ).toMatchInlineSnapshot(`\n        \"import { sharedMessage } from \"./messages\";\n        const getTargetMessage1 = () => sharedMessage;\n        const getTargetMessage2 = () => sharedMessage;\n        const getOtherMessage1 = () => sharedMessage;\n        const getOtherMessage2 = () => sharedMessage;\n        export const target1 = () => getTargetMessage1();\n        export const target2 = () => getTargetMessage2();\n        export const other1 = () => getOtherMessage1();\n        export const other2 = () => getOtherMessage2();\"\n      `);\n    });\n\n    test(\"functions sharing a default import\", () => {\n      const code = dedent`\n        import sharedMessage from \"./messages\";\n        const getTargetMessage1 = () => sharedMessage;\n        const getTargetMessage2 = () => sharedMessage;\n        const getOtherMessage1 = () => sharedMessage;\n        const getOtherMessage2 = () => sharedMessage;\n        export const target1 = () => getTargetMessage1();\n        export const target2 = () => getTargetMessage2();\n        export const other1 = () => getOtherMessage1();\n        export const other2 = () => getOtherMessage2();\n      `;\n      expect(hasChunkableExport(code, \"target1\", ...cache)).toBe(false);\n      expect(getChunkedExport(code, \"target1\", {}, ...cache)).toBeUndefined();\n      expect(hasChunkableExport(code, \"target2\", ...cache)).toBe(false);\n      expect(getChunkedExport(code, \"target2\", {}, ...cache)).toBeUndefined();\n      expect(\n        omitChunkedExports(code, [\"target1\", \"target2\"], {}, ...cache)?.code,\n      ).toMatchInlineSnapshot(`\n        \"import sharedMessage from \"./messages\";\n        const getTargetMessage1 = () => sharedMessage;\n        const getTargetMessage2 = () => sharedMessage;\n        const getOtherMessage1 = () => sharedMessage;\n        const getOtherMessage2 = () => sharedMessage;\n        export const target1 = () => getTargetMessage1();\n        export const target2 = () => getTargetMessage2();\n        export const other1 = () => getOtherMessage1();\n        export const other2 = () => getOtherMessage2();\"\n      `);\n    });\n\n    test(\"functions sharing a namespace import\", () => {\n      const code = dedent`\n        import * as messages from \"./messages\";\n        const getTargetMessage1 = () => messages.targetMessage1;\n        const getTargetMessage2 = () => messages.targetMessage2;\n        const getOtherMessage1 = () => messages.otherMessage1;\n        const getOtherMessage2 = () => messages.otherMessage2;\n        export const target1 = () => getTargetMessage1();\n        export const target2 = () => getTargetMessage2();\n        export const other1 = () => getOtherMessage1();\n        export const other2 = () => getOtherMessage2();\n      `;\n      expect(hasChunkableExport(code, \"target1\", ...cache)).toBe(false);\n      expect(getChunkedExport(code, \"target1\", {}, ...cache)).toBeUndefined();\n      expect(hasChunkableExport(code, \"target2\", ...cache)).toBe(false);\n      expect(getChunkedExport(code, \"target2\", {}, ...cache)).toBeUndefined();\n      expect(\n        omitChunkedExports(code, [\"target1\", \"target2\"], {}, ...cache)?.code,\n      ).toMatchInlineSnapshot(`\n        \"import * as messages from \"./messages\";\n        const getTargetMessage1 = () => messages.targetMessage1;\n        const getTargetMessage2 = () => messages.targetMessage2;\n        const getOtherMessage1 = () => messages.otherMessage1;\n        const getOtherMessage2 = () => messages.otherMessage2;\n        export const target1 = () => getTargetMessage1();\n        export const target2 = () => getTargetMessage2();\n        export const other1 = () => getOtherMessage1();\n        export const other2 = () => getOtherMessage2();\"\n      `);\n    });\n\n    test(\"exported variable declarations sharing an export statement\", () => {\n      const code = dedent`\n        import { sharedMessage } from \"./messages\";\n        export const chunk = sharedMessage,\n          main = chunk;\n      `;\n\n      expect(hasChunkableExport(code, \"chunk\", ...cache)).toBe(false);\n      expect(hasChunkableExport(code, \"main\", ...cache)).toBe(false);\n      expect(getChunkedExport(code, \"chunk\", {}, ...cache)).toBeUndefined();\n      expect(omitChunkedExports(code, [\"chunk\"], {}, ...cache)?.code)\n        .toMatchInlineSnapshot(`\n        \"import { sharedMessage } from \"./messages\";\n        export const chunk = sharedMessage,\n          main = chunk;\"\n      `);\n    });\n\n    test(\"exported destructured array variable declarations sharing an export statement\", () => {\n      const code = dedent`\n        import { sharedMessage } from \"./messages\";\n        export const [chunk] = [sharedMessage],\n          [main] = [chunk];\n      `;\n\n      expect(hasChunkableExport(code, \"chunk\", ...cache)).toBe(false);\n      expect(hasChunkableExport(code, \"main\", ...cache)).toBe(false);\n      expect(getChunkedExport(code, \"chunk\", {}, ...cache)).toBeUndefined();\n      expect(omitChunkedExports(code, [\"chunk\"], {}, ...cache)?.code)\n        .toMatchInlineSnapshot(`\n        \"import { sharedMessage } from \"./messages\";\n        export const [chunk] = [sharedMessage],\n          [main] = [chunk];\"\n      `);\n    });\n\n    test(\"exported destructured array variable declarations sharing an assignment\", () => {\n      const code = dedent`\n        import { chunkMessage, mainMessage } from \"./messages\";\n        export const [chunk, main] = [chunkMessage, mainMessage];\n      `;\n\n      expect(hasChunkableExport(code, \"chunk\", ...cache)).toBe(false);\n      expect(hasChunkableExport(code, \"main\", ...cache)).toBe(false);\n      expect(getChunkedExport(code, \"chunk\", {}, ...cache)).toBeUndefined();\n      expect(omitChunkedExports(code, [\"chunk\"], {}, ...cache)?.code)\n        .toMatchInlineSnapshot(`\n        \"import { chunkMessage, mainMessage } from \"./messages\";\n        export const [chunk, main] = [chunkMessage, mainMessage];\"\n      `);\n    });\n\n    test(\"exported destructured object variable declarations sharing an export statement\", () => {\n      const code = dedent`\n        import { sharedMessage } from \"./messages\";\n        export const { sharedMessage: chunk } = { sharedMessage },\n          { chunk: main } = { chunk };\n      `;\n\n      expect(hasChunkableExport(code, \"chunk\", ...cache)).toBe(false);\n      expect(hasChunkableExport(code, \"main\", ...cache)).toBe(false);\n      expect(getChunkedExport(code, \"chunk\", {}, ...cache)).toBeUndefined();\n      expect(omitChunkedExports(code, [\"chunk\"], {}, ...cache)?.code)\n        .toMatchInlineSnapshot(`\n        \"import { sharedMessage } from \"./messages\";\n        export const {\n            sharedMessage: chunk\n          } = {\n            sharedMessage\n          },\n          {\n            chunk: main\n          } = {\n            chunk\n          };\"\n      `);\n    });\n\n    test(\"exported destructured object variable declarations sharing an assignment\", () => {\n      const code = dedent`\n        import { chunkMessage, mainMessage } from \"./messages\";\n        export const { chunkMessage: chunk, mainMessage: main } = { chunkMessage, mainMessage };\n      `;\n\n      expect(hasChunkableExport(code, \"chunk\", ...cache)).toBe(false);\n      expect(hasChunkableExport(code, \"main\", ...cache)).toBe(false);\n      expect(getChunkedExport(code, \"chunk\", {}, ...cache)).toBeUndefined();\n      expect(omitChunkedExports(code, [\"chunk\"], {}, ...cache)?.code)\n        .toMatchInlineSnapshot(`\n        \"import { chunkMessage, mainMessage } from \"./messages\";\n        export const {\n          chunkMessage: chunk,\n          mainMessage: main\n        } = {\n          chunkMessage,\n          mainMessage\n        };\"\n      `);\n    });\n\n    test(\"exported destructured object spread variable declarations sharing an export statement\", () => {\n      const code = dedent`\n        import { sharedMessage } from \"./messages\";\n        export const { ...chunk } = { ...sharedMessage },\n          { ...main } = { ...chunk };\n      `;\n\n      expect(hasChunkableExport(code, \"chunk\", ...cache)).toBe(false);\n      expect(hasChunkableExport(code, \"main\", ...cache)).toBe(false);\n      expect(getChunkedExport(code, \"chunk\", {}, ...cache)).toBeUndefined();\n      expect(omitChunkedExports(code, [\"chunk\"], {}, ...cache)?.code)\n        .toMatchInlineSnapshot(`\n        \"import { sharedMessage } from \"./messages\";\n        export const {\n            ...chunk\n          } = {\n            ...sharedMessage\n          },\n          {\n            ...main\n          } = {\n            ...chunk\n          };\"\n      `);\n    });\n\n    test(\"exported destructured object spread variable declarations sharing an assignment\", () => {\n      const code = dedent`\n        import { chunkMessage, mainMessage } from \"./messages\";\n        export const {\n          chunkMessage: { ...chunk },\n          mainMessage: { ...main }\n        } = {\n          chunkMessage: { ...chunkMessage },\n          mainMessage: { ...mainMessage }\n        };\n      `;\n\n      expect(hasChunkableExport(code, \"chunk\", ...cache)).toBe(false);\n      expect(hasChunkableExport(code, \"main\", ...cache)).toBe(false);\n      expect(getChunkedExport(code, \"chunk\", {}, ...cache)).toBeUndefined();\n      expect(omitChunkedExports(code, [\"chunk\"], {}, ...cache)?.code)\n        .toMatchInlineSnapshot(`\n        \"import { chunkMessage, mainMessage } from \"./messages\";\n        export const {\n          chunkMessage: {\n            ...chunk\n          },\n          mainMessage: {\n            ...main\n          }\n        } = {\n          chunkMessage: {\n            ...chunkMessage\n          },\n          mainMessage: {\n            ...mainMessage\n          }\n        };\"\n      `);\n    });\n\n    test(\"circular dependencies between exports\", () => {\n      const code = dedent`\n        export const getChunkMessage = (recurse = true) => {\n          return \"chunk \" + (recurse ? getMainMessage(false) : \"\");\n        };\n        export const getMainMessage = (recurse = true) => {\n          return \"main \" + (recurse ? getChunkMessage(false) : \"\");\n        };\n        export const chunk = getChunkMessage();\n        export const main = getMainMessage();\n      `;\n\n      expect(hasChunkableExport(code, \"chunk\", ...cache)).toBe(false);\n      expect(hasChunkableExport(code, \"main\", ...cache)).toBe(false);\n      expect(getChunkedExport(code, \"chunk\", {}, ...cache)).toBeUndefined();\n      expect(getChunkedExport(code, \"main\", {}, ...cache)).toBeUndefined();\n      expect(omitChunkedExports(code, [\"chunk\", \"main\"], {}, ...cache)?.code)\n        .toMatchInlineSnapshot(`\n        \"export const getChunkMessage = (recurse = true) => {\n          return \"chunk \" + (recurse ? getMainMessage(false) : \"\");\n        };\n        export const getMainMessage = (recurse = true) => {\n          return \"main \" + (recurse ? getChunkMessage(false) : \"\");\n        };\n        export const chunk = getChunkMessage();\n        export const main = getMainMessage();\"\n      `);\n    });\n\n    test(\"shared imports across chunks but not main chunk with shared side effect usage\", () => {\n      const code = dedent`\n        import { shared } from \"./shared\";\n        shared(\"side-effect\");\n        export const chunk1 = shared(\"chunk1\");\n        export const chunk2 = shared(\"chunk2\");\n        export const main = \"main\";\n      `;\n      expect(hasChunkableExport(code, \"chunk1\", ...cache)).toBe(false);\n      expect(hasChunkableExport(code, \"chunk2\", ...cache)).toBe(false);\n      expect(hasChunkableExport(code, \"main\", ...cache)).toBe(true);\n      expect(getChunkedExport(code, \"chunk1\", {}, ...cache)).toBeUndefined();\n      expect(getChunkedExport(code, \"chunk2\", {}, ...cache)).toBeUndefined();\n      expect(omitChunkedExports(code, [\"chunk1\", \"chunk2\"], {}, ...cache)?.code)\n        .toMatchInlineSnapshot(`\n        \"import { shared } from \"./shared\";\n        shared(\"side-effect\");\n        export const chunk1 = shared(\"chunk1\");\n        export const chunk2 = shared(\"chunk2\");\n        export const main = \"main\";\"\n      `);\n    });\n  });\n\n  describe(\"export dependency analysis\", () => {\n    test(\"function variables\", () => {\n      const code = dedent`\n        export const chunk = () => {\n          let chunkMessage = \"chunk\";\n          return chunkMessage;\n        }\n        export const main = \"main\";\n      `;\n\n      expect(hasChunkableExport(code, \"chunk\", ...cache)).toBe(true);\n      expect(getChunkedExport(code, \"chunk\", {}, ...cache)?.code)\n        .toMatchInlineSnapshot(`\n        \"export const chunk = () => {\n          let chunkMessage = \"chunk\";\n          return chunkMessage;\n        };\"\n      `);\n      expect(\n        omitChunkedExports(code, [\"chunk\"], {}, ...cache)?.code,\n      ).toMatchInlineSnapshot(`\"export const main = \"main\";\"`);\n    });\n\n    test(\"top level await\", () => {\n      const code = dedent`\n        import { getMessage } from \"./messages\";\n        let messages = [];\n        await getMessage().then((message) => {\n          messages.push(message);\n        });\n        export const chunk = messages;\n        export const main = \"main\";\n      `;\n\n      expect(hasChunkableExport(code, \"chunk\", ...cache)).toBe(true);\n      expect(getChunkedExport(code, \"chunk\", {}, ...cache)?.code)\n        .toMatchInlineSnapshot(`\n        \"import { getMessage } from \"./messages\";\n        let messages = [];\n        await getMessage().then(message => {\n          messages.push(message);\n        });\n        export const chunk = messages;\"\n      `);\n      expect(\n        omitChunkedExports(code, [\"chunk\"], {}, ...cache)?.code,\n      ).toMatchInlineSnapshot(`\"export const main = \"main\";\"`);\n    });\n\n    test(\"if else\", () => {\n      const code = dedent`\n        import { check } from \"./check\";\n        import { chunkMessage1, chunkMessage2 } from \"./messages\";\n        let messages = [];\n        export const chunk = () => {\n          if (check()) {\n            messages.push(chunkMessage1);\n          } else {\n            messages.push(chunkMessage2);\n          }\n          return messages;\n        };\n        export const main = \"main\";\n      `;\n\n      expect(hasChunkableExport(code, \"chunk\", ...cache)).toBe(true);\n      expect(getChunkedExport(code, \"chunk\", {}, ...cache)?.code)\n        .toMatchInlineSnapshot(`\n        \"import { check } from \"./check\";\n        import { chunkMessage1, chunkMessage2 } from \"./messages\";\n        let messages = [];\n        export const chunk = () => {\n          if (check()) {\n            messages.push(chunkMessage1);\n          } else {\n            messages.push(chunkMessage2);\n          }\n          return messages;\n        };\"\n      `);\n      expect(\n        omitChunkedExports(code, [\"chunk\"], {}, ...cache)?.code,\n      ).toMatchInlineSnapshot(`\"export const main = \"main\";\"`);\n    });\n\n    test(\"try catch\", () => {\n      const code = dedent`\n        import { chunkMessage1, errorMessage } from \"./messages\";\n        let messages = [];\n        export const chunk = () => {\n          try {\n            messages.push(chunkMessage1);\n          } catch (error) {\n            messages.push(errorMessage(error));\n          }\n          return messages;\n        };\n        export const main = \"main\";\n      `;\n\n      expect(hasChunkableExport(code, \"chunk\", ...cache)).toBe(true);\n      expect(getChunkedExport(code, \"chunk\", {}, ...cache)?.code)\n        .toMatchInlineSnapshot(`\n        \"import { chunkMessage1, errorMessage } from \"./messages\";\n        let messages = [];\n        export const chunk = () => {\n          try {\n            messages.push(chunkMessage1);\n          } catch (error) {\n            messages.push(errorMessage(error));\n          }\n          return messages;\n        };\"\n      `);\n      expect(\n        omitChunkedExports(code, [\"chunk\"], {}, ...cache)?.code,\n      ).toMatchInlineSnapshot(`\"export const main = \"main\";\"`);\n    });\n\n    test(\"for...of\", () => {\n      const code = dedent`\n        import { messages } from \"./messages\";\n        export const chunk = () => {\n          let chunkMessages = [];\n          for (let message of messages) {\n            chunkMessages.push(message);\n          }\n          return chunkMessages;\n        };\n        export const main = \"main\";\n      `;\n\n      expect(hasChunkableExport(code, \"chunk\", ...cache)).toBe(true);\n      expect(getChunkedExport(code, \"chunk\", {}, ...cache)?.code)\n        .toMatchInlineSnapshot(`\n        \"import { messages } from \"./messages\";\n        export const chunk = () => {\n          let chunkMessages = [];\n          for (let message of messages) {\n            chunkMessages.push(message);\n          }\n          return chunkMessages;\n        };\"\n      `);\n      expect(\n        omitChunkedExports(code, [\"chunk\"], {}, ...cache)?.code,\n      ).toMatchInlineSnapshot(`\"export const main = \"main\";\"`);\n    });\n\n    test(\"for...of with destructuring and default value\", () => {\n      const code = dedent`\n        import { messages, defaultMessage } from \"./messages\";\n        export const chunk = () => {\n          let chunkMessages = [];\n          for (let { key, value = defaultMessage } of messages) {\n            chunkMessages.push([key, value]);\n          }\n          return chunkMessages;\n        };\n        export const main = \"main\";\n      `;\n\n      expect(hasChunkableExport(code, \"chunk\", ...cache)).toBe(true);\n      expect(getChunkedExport(code, \"chunk\", {}, ...cache)?.code)\n        .toMatchInlineSnapshot(`\n        \"import { messages, defaultMessage } from \"./messages\";\n        export const chunk = () => {\n          let chunkMessages = [];\n          for (let {\n            key,\n            value = defaultMessage\n          } of messages) {\n            chunkMessages.push([key, value]);\n          }\n          return chunkMessages;\n        };\"\n      `);\n      expect(\n        omitChunkedExports(code, [\"chunk\"], {}, ...cache)?.code,\n      ).toMatchInlineSnapshot(`\"export const main = \"main\";\"`);\n    });\n\n    test(\"block\", () => {\n      const code = dedent`\n        import { chunkMessage } from \"./messages\";\n        export const chunk = () => {\n          let messages = [];\n          {\n            messages.push(chunkMessage);\n          }\n          return messages;\n        };\n        export const main = \"main\";\n      `;\n\n      expect(hasChunkableExport(code, \"chunk\", ...cache)).toBe(true);\n      expect(getChunkedExport(code, \"chunk\", {}, ...cache)?.code)\n        .toMatchInlineSnapshot(`\n        \"import { chunkMessage } from \"./messages\";\n        export const chunk = () => {\n          let messages = [];\n          {\n            messages.push(chunkMessage);\n          }\n          return messages;\n        };\"\n      `);\n      expect(\n        omitChunkedExports(code, [\"chunk\"], {}, ...cache)?.code,\n      ).toMatchInlineSnapshot(`\"export const main = \"main\";\"`);\n    });\n\n    test(\"default argument\", () => {\n      const code = dedent`\n        import { defaultMessage } from \"./defaultMessage\";\n        const getChunkMessage = (message = defaultMessage) => message.toUpperCase();\n        export const chunk = () => {\n          let message = \"chunk\";\n          return getChunkMessage(message);\n        };\n        export const main = \"main\";\n      `;\n\n      expect(hasChunkableExport(code, \"chunk\", ...cache)).toBe(true);\n      expect(getChunkedExport(code, \"chunk\", {}, ...cache)?.code)\n        .toMatchInlineSnapshot(`\n        \"import { defaultMessage } from \"./defaultMessage\";\n        const getChunkMessage = (message = defaultMessage) => message.toUpperCase();\n        export const chunk = () => {\n          let message = \"chunk\";\n          return getChunkMessage(message);\n        };\"\n      `);\n      expect(\n        omitChunkedExports(code, [\"chunk\"], {}, ...cache)?.code,\n      ).toMatchInlineSnapshot(`\"export const main = \"main\";\"`);\n    });\n\n    test(\"destructured argument with default value\", () => {\n      const code = dedent`\n        import { defaultMessage } from \"./defaultMessage\";\n        const getChunkMessage = ([{ defaultMessage: message }] = [{ defaultMessage }]) => message.toUpperCase();\n        export const chunk = () => {\n          return getChunkMessage();\n        };\n        export const main = \"main\";\n      `;\n\n      expect(hasChunkableExport(code, \"chunk\", ...cache)).toBe(true);\n      expect(getChunkedExport(code, \"chunk\", {}, ...cache)?.code)\n        .toMatchInlineSnapshot(`\n        \"import { defaultMessage } from \"./defaultMessage\";\n        const getChunkMessage = ([{\n          defaultMessage: message\n        }] = [{\n          defaultMessage\n        }]) => message.toUpperCase();\n        export const chunk = () => {\n          return getChunkMessage();\n        };\"\n      `);\n      expect(\n        omitChunkedExports(code, [\"chunk\"], {}, ...cache)?.code,\n      ).toMatchInlineSnapshot(`\"export const main = \"main\";\"`);\n    });\n\n    test(\"reassignment\", () => {\n      const code = dedent`\n        import { reassignedMessage } from \"./messages\";  \n        let chunkMessage = \"chunk\";\n        export const chunk = () => {\n          chunkMessage = reassignedMessage;\n          return chunkMessage;\n        };\n        export const main = \"main\";\n      `;\n\n      expect(hasChunkableExport(code, \"chunk\", ...cache)).toBe(true);\n      expect(getChunkedExport(code, \"chunk\", {}, ...cache)?.code)\n        .toMatchInlineSnapshot(`\n        \"import { reassignedMessage } from \"./messages\";\n        let chunkMessage = \"chunk\";\n        export const chunk = () => {\n          chunkMessage = reassignedMessage;\n          return chunkMessage;\n        };\"\n      `);\n      expect(\n        omitChunkedExports(code, [\"chunk\"], {}, ...cache)?.code,\n      ).toMatchInlineSnapshot(`\"export const main = \"main\";\"`);\n    });\n\n    test(\"constant reassignment\", () => {\n      const code = dedent`\n        const chunkMessage = () => \"chunk\";\n        chunkMessage = () => reassignedMessage;\n        export const chunk = () => chunkMessage();\n        export const main = \"main\";\n      `;\n\n      expect(hasChunkableExport(code, \"chunk\", ...cache)).toBe(true);\n      expect(getChunkedExport(code, \"chunk\", {}, ...cache)?.code)\n        .toMatchInlineSnapshot(`\n        \"const chunkMessage = () => \"chunk\";\n        chunkMessage = () => reassignedMessage;\n        export const chunk = () => chunkMessage();\"\n      `);\n      expect(\n        omitChunkedExports(code, [\"chunk\"], {}, ...cache)?.code,\n      ).toMatchInlineSnapshot(`\"export const main = \"main\";\"`);\n    });\n\n    test(\"reassignment with nullish coalescing\", () => {\n      const code = dedent`\n        import { reassignedMessage } from \"./messages\";  \n        let chunkMessage = () => \"chunk\";\n        chunkMessage ??= () => reassignedMessage;\n        export const chunk = () => chunkMessage();\n        export const main = \"main\";\n      `;\n\n      expect(hasChunkableExport(code, \"chunk\", ...cache)).toBe(true);\n      expect(getChunkedExport(code, \"chunk\", {}, ...cache)?.code)\n        .toMatchInlineSnapshot(`\n        \"import { reassignedMessage } from \"./messages\";\n        let chunkMessage = () => \"chunk\";\n        chunkMessage ??= () => reassignedMessage;\n        export const chunk = () => chunkMessage();\"\n      `);\n      expect(\n        omitChunkedExports(code, [\"chunk\"], {}, ...cache)?.code,\n      ).toMatchInlineSnapshot(`\"export const main = \"main\";\"`);\n    });\n\n    test(\"destructured reassignment\", () => {\n      const code = dedent`\n        import { reassignedMessage } from \"./messages\";\n        let chunkMessage = () => \"chunk\";\n        [chunkMessage] = [() => reassignedMessage];\n        export const chunk = () => chunkMessage();\n        export const main = \"main\";\n      `;\n\n      expect(hasChunkableExport(code, \"chunk\", ...cache)).toBe(true);\n      expect(getChunkedExport(code, \"chunk\", {}, ...cache)?.code)\n        .toMatchInlineSnapshot(`\n        \"import { reassignedMessage } from \"./messages\";\n        let chunkMessage = () => \"chunk\";\n        [chunkMessage] = [() => reassignedMessage];\n        export const chunk = () => chunkMessage();\"\n      `);\n      expect(\n        omitChunkedExports(code, [\"chunk\"], {}, ...cache)?.code,\n      ).toMatchInlineSnapshot(`\"export const main = \"main\";\"`);\n    });\n\n    test(\"function argument reassignment\", () => {\n      const code = dedent`\n        const reassignedMessage = \"reassigned\";\n        const getChunkMessage = (message) => {\n          message = reassignedMessage;\n          return message;\n        };\n        export const chunk = () => getChunkMessage(\"chunk\");\n        export const main = \"main\";\n      `;\n\n      expect(hasChunkableExport(code, \"chunk\", ...cache)).toBe(true);\n      expect(getChunkedExport(code, \"chunk\", {}, ...cache)?.code)\n        .toMatchInlineSnapshot(`\n        \"const reassignedMessage = \"reassigned\";\n        const getChunkMessage = message => {\n          message = reassignedMessage;\n          return message;\n        };\n        export const chunk = () => getChunkMessage(\"chunk\");\"\n      `);\n      expect(\n        omitChunkedExports(code, [\"chunk\"], {}, ...cache)?.code,\n      ).toMatchInlineSnapshot(`\"export const main = \"main\";\"`);\n    });\n\n    test(\"destructuring assignment\", () => {\n      const code = dedent`\n        import { getChunkMessage } from \"./messages\";\n        let [chunkMessage] = [() => getChunkMessage()];\n        export const chunk = () => chunkMessage();\n        export const main = \"main\";\n      `;\n\n      expect(hasChunkableExport(code, \"chunk\", ...cache)).toBe(true);\n      expect(getChunkedExport(code, \"chunk\", {}, ...cache)?.code)\n        .toMatchInlineSnapshot(`\n        \"import { getChunkMessage } from \"./messages\";\n        let [chunkMessage] = [() => getChunkMessage()];\n        export const chunk = () => chunkMessage();\"\n      `);\n      expect(\n        omitChunkedExports(code, [\"chunk\"], {}, ...cache)?.code,\n      ).toMatchInlineSnapshot(`\"export const main = \"main\";\"`);\n    });\n\n    test(\"object property usage\", () => {\n      const code = dedent`\n        let messages = { chunkMessage: \"chunk\" };\n        export const chunk = () => messages.chunkMessage;\n        export const main = \"main\";\n      `;\n\n      expect(hasChunkableExport(code, \"chunk\", ...cache)).toBe(true);\n      expect(getChunkedExport(code, \"chunk\", {}, ...cache)?.code)\n        .toMatchInlineSnapshot(`\n        \"let messages = {\n          chunkMessage: \"chunk\"\n        };\n        export const chunk = () => messages.chunkMessage;\"\n      `);\n      expect(\n        omitChunkedExports(code, [\"chunk\"], {}, ...cache)?.code,\n      ).toMatchInlineSnapshot(`\"export const main = \"main\";\"`);\n    });\n\n    test(\"object property mutation\", () => {\n      const code = dedent`\n        import { mutatedMessage } from \"./messages\";\n        let messages = { chunkMessage: \"chunk\" };\n        messages.chunkMessage = mutatedMessage;\n        export const chunk = () => messages.chunkMessage;\n        export const main = \"main\";\n      `;\n\n      expect(hasChunkableExport(code, \"chunk\", ...cache)).toBe(true);\n      expect(getChunkedExport(code, \"chunk\", {}, ...cache)?.code)\n        .toMatchInlineSnapshot(`\n        \"import { mutatedMessage } from \"./messages\";\n        let messages = {\n          chunkMessage: \"chunk\"\n        };\n        messages.chunkMessage = mutatedMessage;\n        export const chunk = () => messages.chunkMessage;\"\n      `);\n      expect(\n        omitChunkedExports(code, [\"chunk\"], {}, ...cache)?.code,\n      ).toMatchInlineSnapshot(`\"export const main = \"main\";\"`);\n    });\n\n    test(\"computed object property\", () => {\n      const code = dedent`\n        import { keyName } from \"./messages\";\n        let getKey = () => keyName;\n        let messages = { [getKey()]: \"chunk\" };\n        export const chunk = () => messages;\n        export const main = \"main\";\n      `;\n\n      expect(hasChunkableExport(code, \"chunk\", ...cache)).toBe(true);\n      expect(getChunkedExport(code, \"chunk\", {}, ...cache)?.code)\n        .toMatchInlineSnapshot(`\n        \"import { keyName } from \"./messages\";\n        let getKey = () => keyName;\n        let messages = {\n          [getKey()]: \"chunk\"\n        };\n        export const chunk = () => messages;\"\n      `);\n      expect(\n        omitChunkedExports(code, [\"chunk\"], {}, ...cache)?.code,\n      ).toMatchInlineSnapshot(`\"export const main = \"main\";\"`);\n    });\n\n    test(\"generator function\", () => {\n      const code = dedent`\n        import { chunkMessage1, chunkMessage2 } from \"./messages\";\n        function* chunkGenerator() {\n          yield chunkMessage1;\n          yield chunkMessage2;\n        }\n        export const chunk = () => [...chunkGenerator()].join(\" \");\n        export const main = \"main\";\n      `;\n\n      expect(hasChunkableExport(code, \"chunk\", ...cache)).toBe(true);\n      expect(getChunkedExport(code, \"chunk\", {}, ...cache)?.code)\n        .toMatchInlineSnapshot(`\n        \"import { chunkMessage1, chunkMessage2 } from \"./messages\";\n        function* chunkGenerator() {\n          yield chunkMessage1;\n          yield chunkMessage2;\n        }\n        export const chunk = () => [...chunkGenerator()].join(\" \");\"\n      `);\n      expect(\n        omitChunkedExports(code, [\"chunk\"], {}, ...cache)?.code,\n      ).toMatchInlineSnapshot(`\"export const main = \"main\";\"`);\n    });\n\n    test(\"class static property usage\", () => {\n      const code = dedent`\n        import { chunkMessage } from \"./messages\";\n        class Chunk {\n          static message = chunkMessage;\n        }\n        export const chunk = () => new Chunk();\n        export const main = \"main\";\n      `;\n\n      expect(hasChunkableExport(code, \"chunk\", ...cache)).toBe(true);\n      expect(getChunkedExport(code, \"chunk\", {}, ...cache)?.code)\n        .toMatchInlineSnapshot(`\n        \"import { chunkMessage } from \"./messages\";\n        class Chunk {\n          static message = chunkMessage;\n        }\n        export const chunk = () => new Chunk();\"\n      `);\n      expect(\n        omitChunkedExports(code, [\"chunk\"], {}, ...cache)?.code,\n      ).toMatchInlineSnapshot(`\"export const main = \"main\";\"`);\n    });\n\n    test(\"class static property mutation\", () => {\n      const code = dedent`\n        import { mutatedMessage } from \"./messages\";\n        class Chunk {\n          static message = \"chunk\";\n        }\n        let chunkInstance = new Chunk();\n        chunkInstance.message = mutatedMessage;\n        export const chunk = () => chunkInstance;\n        export const main = \"main\";\n      `;\n\n      expect(hasChunkableExport(code, \"chunk\", ...cache)).toBe(true);\n      expect(getChunkedExport(code, \"chunk\", {}, ...cache)?.code)\n        .toMatchInlineSnapshot(`\n        \"import { mutatedMessage } from \"./messages\";\n        class Chunk {\n          static message = \"chunk\";\n        }\n        let chunkInstance = new Chunk();\n        chunkInstance.message = mutatedMessage;\n        export const chunk = () => chunkInstance;\"\n      `);\n      expect(\n        omitChunkedExports(code, [\"chunk\"], {}, ...cache)?.code,\n      ).toMatchInlineSnapshot(`\"export const main = \"main\";\"`);\n    });\n\n    test(\"class method usage\", () => {\n      const code = dedent`\n        import { chunkMessage } from \"./messages\";\n        class Chunk {\n          message() {\n            return chunkMessage;\n          }\n        }\n        export const chunk = () => new Chunk();\n        export const main = \"main\";\n      `;\n\n      expect(hasChunkableExport(code, \"chunk\", ...cache)).toBe(true);\n      expect(getChunkedExport(code, \"chunk\", {}, ...cache)?.code)\n        .toMatchInlineSnapshot(`\n        \"import { chunkMessage } from \"./messages\";\n        class Chunk {\n          message() {\n            return chunkMessage;\n          }\n        }\n        export const chunk = () => new Chunk();\"\n      `);\n      expect(\n        omitChunkedExports(code, [\"chunk\"], {}, ...cache)?.code,\n      ).toMatchInlineSnapshot(`\"export const main = \"main\";\"`);\n    });\n\n    test(\"class method mutation\", () => {\n      const code = dedent`\n        import { chunkMessage, mutatedMessage } from \"./messages\";\n        class Chunk {\n          message() {\n            return chunkMessage;\n          }\n        }\n        let chunkInstance = new Chunk();\n        chunkInstance.message = () => mutatedMessage;\n        export const chunk = () => chunkInstance;\n        export const main = \"main\";\n      `;\n\n      expect(hasChunkableExport(code, \"chunk\", ...cache)).toBe(true);\n      expect(getChunkedExport(code, \"chunk\", {}, ...cache)?.code)\n        .toMatchInlineSnapshot(`\n        \"import { chunkMessage, mutatedMessage } from \"./messages\";\n        class Chunk {\n          message() {\n            return chunkMessage;\n          }\n        }\n        let chunkInstance = new Chunk();\n        chunkInstance.message = () => mutatedMessage;\n        export const chunk = () => chunkInstance;\"\n      `);\n      expect(\n        omitChunkedExports(code, [\"chunk\"], {}, ...cache)?.code,\n      ).toMatchInlineSnapshot(`\"export const main = \"main\";\"`);\n    });\n  });\n});\n"
  },
  {
    "path": "packages/react-router-dev/vite/route-chunks.ts",
    "content": "import type { GeneratorOptions, GeneratorResult } from \"@babel/generator\";\nimport invariant from \"../invariant\";\nimport { type Cache, getOrSetFromCache } from \"./cache\";\nimport {\n  type Babel,\n  type NodePath,\n  parse,\n  traverse,\n  generate,\n  t,\n} from \"./babel\";\n\ntype ExportDeclaration = Babel.ExportDeclaration;\ntype Identifier = Babel.Identifier;\ntype Pattern = Babel.Pattern;\ntype Statement = Babel.Statement;\ntype VariableDeclarator = Babel.VariableDeclarator;\n\ntype ExportDependencies = Map<string, Dependencies>;\n\ntype Dependencies = {\n  topLevelStatements: Set<Statement>;\n  topLevelNonModuleStatements: Set<Statement>;\n  importedIdentifierNames: Set<string>;\n  exportedVariableDeclarators: Set<VariableDeclarator>;\n};\n\nfunction codeToAst(code: string, cache: Cache, cacheKey: string): Babel.File {\n  // We use structuredClone to allow AST mutation without modifying the cache.\n  return structuredClone(\n    getOrSetFromCache(cache, `${cacheKey}::codeToAst`, code, () =>\n      parse(code, { sourceType: \"module\" }),\n    ),\n  );\n}\n\nfunction assertNodePath(\n  path:\n    | NodePath<Babel.Node | null>\n    | NodePath<Babel.Node | null>[]\n    | null\n    | undefined,\n): asserts path is NodePath {\n  invariant(\n    path && !Array.isArray(path),\n    `Expected a Path, but got ${Array.isArray(path) ? \"an array\" : path}`,\n  );\n}\n\nfunction assertNodePathIsStatement(\n  path: NodePath | NodePath[] | null | undefined,\n): asserts path is NodePath<Statement> {\n  invariant(\n    path && !Array.isArray(path) && t.isStatement(path.node),\n    `Expected a Statement path, but got ${\n      Array.isArray(path) ? \"an array\" : path?.node?.type\n    }`,\n  );\n}\n\nfunction assertNodePathIsVariableDeclarator(\n  path: NodePath | NodePath[] | null | undefined,\n): asserts path is NodePath<VariableDeclarator> {\n  invariant(\n    path && !Array.isArray(path) && t.isVariableDeclarator(path.node),\n    `Expected an Identifier path, but got ${\n      Array.isArray(path) ? \"an array\" : path?.node?.type\n    }`,\n  );\n}\n\nfunction assertNodePathIsPattern(\n  path: NodePath | NodePath[] | null | undefined,\n): asserts path is NodePath<Pattern> {\n  invariant(\n    path && !Array.isArray(path) && t.isPattern(path.node),\n    `Expected a Pattern path, but got ${\n      Array.isArray(path) ? \"an array\" : path?.node?.type\n    }`,\n  );\n}\n\nfunction getExportDependencies(\n  code: string,\n  cache: Cache,\n  cacheKey: string,\n): ExportDependencies {\n  return getOrSetFromCache(\n    cache,\n    `${cacheKey}::getExportDependencies`,\n    code,\n    () => {\n      let exportDependencies: ExportDependencies = new Map();\n      let ast = codeToAst(code, cache, cacheKey);\n\n      function handleExport(\n        exportName: string,\n        exportPath: NodePath<ExportDeclaration>,\n        identifiersPath: NodePath = exportPath,\n      ) {\n        let identifiers = getDependentIdentifiersForPath(identifiersPath);\n\n        let topLevelStatements = new Set([\n          exportPath.node,\n          ...getTopLevelStatementsForPaths(identifiers),\n        ]);\n\n        // We also keep track of non-import statements since import statements\n        // get more fine-grained filtering, meaning that we often need to\n        // exclude import statements in our chunking logic.\n        let topLevelNonModuleStatements = new Set(\n          Array.from(topLevelStatements).filter(\n            (statement) =>\n              !t.isImportDeclaration(statement) &&\n              !t.isExportDeclaration(statement),\n          ),\n        );\n\n        // We keep track of imported identifiers for each export since we\n        // perform more fine-grained filtering on import statements.\n        let importedIdentifierNames = new Set<string>();\n        for (let identifier of identifiers) {\n          if (identifier.parentPath.parentPath?.isImportDeclaration()) {\n            importedIdentifierNames.add(identifier.node.name);\n          }\n        }\n\n        // We keep track of variable declarators for each export since we\n        // perform more fine-grained filtering on export statements.\n        let exportedVariableDeclarators = new Set<VariableDeclarator>();\n        for (let identifier of identifiers) {\n          // export const foo = ...;\n          if (\n            identifier.parentPath.isVariableDeclarator() &&\n            identifier.parentPath.parentPath.parentPath?.isExportNamedDeclaration()\n          ) {\n            exportedVariableDeclarators.add(identifier.parentPath.node);\n            continue;\n          }\n\n          // export const { foo } = ...;\n          let isWithinExportDestructuring = Boolean(\n            identifier.findParent((path) =>\n              Boolean(\n                path.isPattern() &&\n                  path.parentPath?.isVariableDeclarator() &&\n                  path.parentPath.parentPath?.parentPath?.isExportNamedDeclaration(),\n              ),\n            ),\n          );\n          if (isWithinExportDestructuring) {\n            let currentPath: NodePath | null = identifier;\n            while (currentPath) {\n              if (\n                // Check the identifier is within a variable declaration, and if\n                // so, ensure we're on the left-hand side of the expression\n                // since these identifiers are what make up the export names,\n                // e.g. export const { foo } = { foo: bar }; should pick up\n                // `foo` but not `bar`.\n                currentPath.parentPath?.isVariableDeclarator() &&\n                currentPath.parentKey === \"id\"\n              ) {\n                exportedVariableDeclarators.add(currentPath.parentPath.node);\n                break;\n              }\n              currentPath = currentPath.parentPath;\n            }\n          }\n        }\n\n        let dependencies: Dependencies = {\n          topLevelStatements,\n          topLevelNonModuleStatements,\n          importedIdentifierNames,\n          exportedVariableDeclarators,\n        };\n\n        exportDependencies.set(exportName, dependencies);\n      }\n\n      traverse(ast, {\n        ExportDeclaration(exportPath) {\n          let { node } = exportPath;\n\n          // export * from \"./module\"\n          if (t.isExportAllDeclaration(node)) {\n            return;\n          }\n\n          // export default ...;\n          if (t.isExportDefaultDeclaration(node)) {\n            handleExport(\"default\", exportPath);\n            return;\n          }\n\n          let { declaration } = node;\n\n          // export const foo = ..., { bar } = ...;\n          if (t.isVariableDeclaration(declaration)) {\n            let { declarations } = declaration;\n            for (let i = 0; i < declarations.length; i++) {\n              let declarator = declarations[i];\n\n              // export const foo = ...;\n              if (t.isIdentifier(declarator.id)) {\n                let declaratorPath = exportPath.get(\n                  `declaration.declarations.${i}`,\n                );\n\n                assertNodePathIsVariableDeclarator(declaratorPath);\n\n                handleExport(declarator.id.name, exportPath, declaratorPath);\n                continue;\n              }\n\n              // export const { foo } = ...;\n              if (t.isPattern(declarator.id)) {\n                let exportedPatternPath = exportPath.get(\n                  `declaration.declarations.${i}.id`,\n                );\n\n                assertNodePathIsPattern(exportedPatternPath);\n\n                let identifiers =\n                  getIdentifiersForPatternPath(exportedPatternPath);\n\n                for (let identifier of identifiers) {\n                  handleExport(identifier.node.name, exportPath, identifier);\n                }\n              }\n            }\n\n            return;\n          }\n\n          // export function foo() {}\n          // export class Foo {}\n          if (\n            t.isFunctionDeclaration(declaration) ||\n            t.isClassDeclaration(declaration)\n          ) {\n            invariant(\n              declaration.id,\n              \"Expected exported function or class declaration to have a name when not the default export\",\n            );\n            handleExport(declaration.id.name, exportPath);\n            return;\n          }\n\n          // export { foo, bar }\n          if (t.isExportNamedDeclaration(node)) {\n            for (let specifier of node.specifiers) {\n              if (t.isIdentifier(specifier.exported)) {\n                let name = specifier.exported.name;\n                let specifierPath = exportPath\n                  .get(\"specifiers\")\n                  .find((path) => path.node === specifier);\n\n                invariant(\n                  specifierPath,\n                  `Expected to find specifier path for ${name}`,\n                );\n\n                handleExport(name, exportPath, specifierPath);\n              }\n            }\n            return;\n          }\n\n          // This should never happen:\n          // @ts-expect-error: We've handled all the export types\n          throw new Error(`Unknown export node type: ${node.type}`);\n        },\n      });\n\n      return exportDependencies;\n    },\n  );\n}\n\nfunction getDependentIdentifiersForPath(\n  path: NodePath,\n  state?: { visited: Set<NodePath>; identifiers: Set<NodePath<Identifier>> },\n): Set<NodePath<Identifier>> {\n  let { visited, identifiers } = state ?? {\n    visited: new Set(),\n    identifiers: new Set(),\n  };\n\n  // Ensure we don't recurse indefinitely\n  if (visited.has(path)) {\n    return identifiers;\n  }\n\n  visited.add(path);\n\n  // Recursively traverse the AST to find all identifiers the path depends on.\n  path.traverse({\n    Identifier(path) {\n      // We can skip all of this work if we've already processed this identifier.\n      if (identifiers.has(path)) {\n        return;\n      }\n\n      identifiers.add(path);\n\n      let binding = path.scope.getBinding(path.node.name);\n\n      if (!binding) {\n        return;\n      }\n\n      getDependentIdentifiersForPath(binding.path, { visited, identifiers });\n\n      // Trace all references to the identifier\n      for (let reference of binding.referencePaths) {\n        // Each export declaration is handled separately in our chunking logic\n        // so we don't want to trace the entire export statement, otherwise all\n        // identifiers in the export statement will be marked as dependencies\n        // and we won't be able to split chunks sharing this export statement.\n        if (reference.isExportNamedDeclaration()) {\n          continue;\n        }\n\n        getDependentIdentifiersForPath(reference, {\n          visited,\n          identifiers,\n        });\n      }\n\n      // For completeness we also want to trace constant violations since, even\n      // though the code results in a runtime error, it still compiles.\n      for (let constantViolation of binding.constantViolations) {\n        getDependentIdentifiersForPath(constantViolation, {\n          visited,\n          identifiers,\n        });\n      }\n    },\n  });\n\n  let topLevelStatement = getTopLevelStatementPathForPath(path);\n  let withinImportStatement = topLevelStatement.isImportDeclaration();\n  let withinExportStatement = topLevelStatement.isExportDeclaration();\n\n  // Include all identifiers in the top-level statement as dependencies, except\n  // for import/export statements since they have more fine-grained filtering.\n  if (!withinImportStatement && !withinExportStatement) {\n    getDependentIdentifiersForPath(topLevelStatement, {\n      visited,\n      identifiers,\n    });\n  }\n\n  // Destructuring assignments in export statements have more fine-grained\n  // filtering, so we include all identifiers in the expression as dependencies.\n  if (\n    withinExportStatement &&\n    path.isIdentifier() &&\n    (t.isPattern(path.parentPath.node) || // [foo]\n      t.isPattern(path.parentPath.parentPath?.node)) // {nested: foo}\n  ) {\n    // Find the root `const foo = ...` within `export const foo = ...`.\n    let variableDeclarator = path.findParent((p) => p.isVariableDeclarator());\n    assertNodePath(variableDeclarator);\n\n    getDependentIdentifiersForPath(variableDeclarator, {\n      visited,\n      identifiers,\n    });\n  }\n\n  return identifiers;\n}\n\nfunction getTopLevelStatementPathForPath(path: NodePath): NodePath<Statement> {\n  let ancestry = path.getAncestry();\n\n  // The last node is the Program node so we want the ancestor before that.\n  let topLevelStatement = ancestry[ancestry.length - 2];\n  assertNodePathIsStatement(topLevelStatement);\n\n  return topLevelStatement;\n}\n\nfunction getTopLevelStatementsForPaths(paths: Set<NodePath>): Set<Statement> {\n  let topLevelStatements = new Set<Statement>();\n\n  for (let path of paths) {\n    let topLevelStatement = getTopLevelStatementPathForPath(path);\n    topLevelStatements.add(topLevelStatement.node);\n  }\n\n  return topLevelStatements;\n}\n\nfunction getIdentifiersForPatternPath(\n  patternPath: NodePath<Pattern>,\n  identifiers: Set<NodePath<Identifier>> = new Set(),\n): Set<NodePath<Identifier>> {\n  function walk(currentPath: NodePath) {\n    if (currentPath.isIdentifier()) {\n      identifiers.add(currentPath);\n      return;\n    }\n\n    if (currentPath.isObjectPattern()) {\n      let { properties } = currentPath.node;\n      for (let i = 0; i < properties.length; i++) {\n        const property = properties[i];\n        if (t.isObjectProperty(property)) {\n          let valuePath = currentPath.get(`properties.${i}.value`);\n          assertNodePath(valuePath);\n          walk(valuePath);\n        } else if (t.isRestElement(property)) {\n          let argumentPath = currentPath.get(`properties.${i}.argument`);\n          assertNodePath(argumentPath);\n          walk(argumentPath);\n        }\n      }\n    } else if (currentPath.isArrayPattern()) {\n      let { elements } = currentPath.node;\n      for (let i = 0; i < elements.length; i++) {\n        const element = elements[i];\n        if (element) {\n          let elementPath = currentPath.get(`elements.${i}`);\n          assertNodePath(elementPath);\n          walk(elementPath);\n        }\n      }\n    } else if (currentPath.isRestElement()) {\n      let argumentPath = currentPath.get(\"argument\");\n      assertNodePath(argumentPath);\n      walk(argumentPath);\n    }\n  }\n\n  walk(patternPath);\n  return identifiers;\n}\n\nconst getExportedName = (exported: t.Identifier | t.StringLiteral): string => {\n  return t.isIdentifier(exported) ? exported.name : exported.value;\n};\n\nfunction setsIntersect(set1: Set<any>, set2: Set<any>): boolean {\n  // To optimize the check, we always iterate over the smaller set.\n  let smallerSet = set1;\n  let largerSet = set2;\n  if (set1.size > set2.size) {\n    smallerSet = set2;\n    largerSet = set1;\n  }\n\n  for (let element of smallerSet) {\n    if (largerSet.has(element)) {\n      return true;\n    }\n  }\n\n  return false;\n}\n\nexport function hasChunkableExport(\n  code: string,\n  exportName: string,\n  cache: Cache,\n  cacheKey: string,\n): boolean {\n  return getOrSetFromCache(\n    cache,\n    `${cacheKey}::hasChunkableExport::${exportName}`,\n    code,\n    () => {\n      let exportDependencies = getExportDependencies(code, cache, cacheKey);\n      let dependencies = exportDependencies.get(exportName);\n\n      // If there are no dependencies, the export wasn't found in the file.\n      if (!dependencies) {\n        return false;\n      }\n\n      // Loop through all other exports to see if they have top level non-import\n      // statements in common with the export we're trying to chunk.\n      for (let [currentExportName, currentDependencies] of exportDependencies) {\n        if (currentExportName === exportName) {\n          continue;\n        }\n\n        // As soon as we find any top level non-import statements in common with\n        // another export, we know this export cannot be placed in its own\n        // chunk. The reason import statements aren't factored into this check\n        // is because we perform more fine-grained optimizations on them,\n        // filtering out all unused imports within each chunk, meaning that it's\n        // okay for multiple exports to share an import statement. We perform a\n        // deeper check on imported identifiers in the step after this.\n        if (\n          setsIntersect(\n            currentDependencies.topLevelNonModuleStatements,\n            dependencies.topLevelNonModuleStatements,\n          )\n        ) {\n          return false;\n        }\n      }\n\n      // If the export we're trying to chunk depends on more than one exported\n      // variable declarator (where one of them might be the chunked export\n      // itself), it means it must depend on other exports and can't be chunked,\n      // so we can bail out early before comparing against other exports.\n      if (dependencies.exportedVariableDeclarators.size > 1) {\n        return false;\n      }\n\n      // Loop through all other exports to see if they depend on the export\n      // we're trying to chunk.\n      if (dependencies.exportedVariableDeclarators.size > 0) {\n        for (let [\n          currentExportName,\n          currentDependencies,\n        ] of exportDependencies) {\n          if (currentExportName === exportName) {\n            continue;\n          }\n\n          // As soon as we find any exported variable declarators in common with\n          // another export, we know this export cannot be placed in its own\n          // chunk. Note that the chunk can still share top level export\n          // statements with other exports because we filter out all unused\n          // exports, so we can treat each exported variable name as a separate\n          // entity in this check.\n          if (\n            setsIntersect(\n              currentDependencies.exportedVariableDeclarators,\n              dependencies.exportedVariableDeclarators,\n            )\n          ) {\n            return false;\n          }\n        }\n      }\n\n      return true;\n    },\n  );\n}\n\nexport function getChunkedExport(\n  code: string,\n  exportName: string,\n  generateOptions: GeneratorOptions = {},\n  cache: Cache,\n  cacheKey: string,\n): GeneratorResult | undefined {\n  return getOrSetFromCache(\n    cache,\n    `${cacheKey}::getChunkedExport::${exportName}::${JSON.stringify(\n      generateOptions,\n    )}`,\n    code,\n    () => {\n      // If we already know the export isn't chunkable, we can bail out early.\n      if (!hasChunkableExport(code, exportName, cache, cacheKey)) {\n        return undefined;\n      }\n\n      let exportDependencies = getExportDependencies(code, cache, cacheKey);\n      let dependencies = exportDependencies.get(exportName);\n      invariant(dependencies, \"Expected export to have dependencies\");\n\n      let topLevelStatementsArray = Array.from(dependencies.topLevelStatements);\n      let exportedVariableDeclaratorsArray = Array.from(\n        dependencies.exportedVariableDeclarators,\n      );\n\n      let ast = codeToAst(code, cache, cacheKey);\n\n      // Filter the AST body to only include statements that are part of the\n      // chunked export's dependencies. Note that since we bailed out early if\n      // the export isn't chunkable, we can now simply remove any unused imports\n      // and top-level statements.\n      ast.program.body = ast.program.body\n        .filter((node) =>\n          topLevelStatementsArray.some((statement) =>\n            t.isNodesEquivalent(node, statement),\n          ),\n        )\n        // Remove unused imports\n        .map((node) => {\n          // Skip non-import nodes for this step, return node as-is\n          if (!t.isImportDeclaration(node)) {\n            return node;\n          }\n\n          // If the chunked export doesn't depend on any imported identifiers,\n          // we know it can't contain any imports statements, so we remove it.\n          if (dependencies.importedIdentifierNames.size === 0) {\n            return null;\n          }\n\n          // Filter out unused import specifiers. Note that this handles\n          // default imports, named imports, and namespace imports.\n          node.specifiers = node.specifiers.filter((specifier) =>\n            dependencies.importedIdentifierNames.has(specifier.local.name),\n          );\n\n          // Ensure we haven't removed all specifiers. If we have, it means\n          // our dependency analysis is incorrect.\n          invariant(\n            node.specifiers.length > 0,\n            \"Expected import statement to have used specifiers\",\n          );\n\n          // Keep the modified AST node\n          return node;\n        })\n        // Filter export statements\n        .map((node) => {\n          // Skip non-export nodes for this step, return node as-is\n          if (!t.isExportDeclaration(node)) {\n            return node;\n          }\n\n          // `export * from \"./module\";\n          // Not chunkable, always remove within chunks.\n          if (t.isExportAllDeclaration(node)) {\n            return null;\n          }\n\n          // export default ...;\n          // If we're chunking the default export, keep it,\n          // otherwise remove it.\n          if (t.isExportDefaultDeclaration(node)) {\n            return exportName === \"default\" ? node : null;\n          }\n\n          let { declaration } = node;\n\n          // export const foo = ..., { bar } = ...;\n          if (t.isVariableDeclaration(declaration)) {\n            // Only keep variable declarators for the chunked export\n            declaration.declarations = declaration.declarations.filter((node) =>\n              exportedVariableDeclaratorsArray.some((declarator) =>\n                t.isNodesEquivalent(node, declarator),\n              ),\n            );\n\n            // If the export statement is now empty, remove it\n            if (declaration.declarations.length === 0) {\n              return null;\n            }\n\n            // Keep the modified AST node\n            return node;\n          }\n\n          // export function foo() {}\n          // export class Foo {}\n          if (\n            t.isFunctionDeclaration(node.declaration) ||\n            t.isClassDeclaration(node.declaration)\n          ) {\n            // If the function/class name matches the export name, keep the\n            // node, otherwise remove it.\n            return node.declaration.id?.name === exportName ? node : null;\n          }\n\n          // export { foo, bar }\n          if (t.isExportNamedDeclaration(node)) {\n            // export {}\n            // Remove empty export statements within chunks\n            if (node.specifiers.length === 0) {\n              return null;\n            }\n\n            // Only keep specifiers for the chunked export\n            node.specifiers = node.specifiers.filter(\n              (specifier) => getExportedName(specifier.exported) === exportName,\n            );\n\n            // If the export statement is now empty, remove it\n            if (node.specifiers.length === 0) {\n              return null;\n            }\n\n            // Keep the modified AST node\n            return node;\n          }\n\n          // This should never happen:\n          // @ts-expect-error: We've handled all the export types\n          throw new Error(`Unknown export node type: ${node.type}`);\n        })\n        .filter((node): node is NonNullable<typeof node> => node !== null);\n\n      return generate(ast, generateOptions);\n    },\n  );\n}\n\nexport function omitChunkedExports(\n  code: string,\n  exportNames: readonly string[],\n  generateOptions: GeneratorOptions = {},\n  cache: Cache,\n  cacheKey: string,\n): GeneratorResult | undefined {\n  return getOrSetFromCache(\n    cache,\n    `${cacheKey}::omitChunkedExports::${exportNames.join(\n      \",\",\n    )}::${JSON.stringify(generateOptions)}`,\n    code,\n    () => {\n      const isChunkable = (exportName: string): boolean =>\n        hasChunkableExport(code, exportName, cache, cacheKey);\n\n      const isOmitted = (exportName: string): boolean =>\n        exportNames.includes(exportName) && isChunkable(exportName);\n\n      const isRetained = (exportName: string): boolean =>\n        !isOmitted(exportName);\n\n      let exportDependencies = getExportDependencies(code, cache, cacheKey);\n\n      let allExportNames = Array.from(exportDependencies.keys());\n      let omittedExportNames = allExportNames.filter(isOmitted);\n      let retainedExportNames = allExportNames.filter(isRetained);\n\n      let omittedStatements = new Set<Statement>();\n      let omittedExportedVariableDeclarators = new Set<VariableDeclarator>();\n\n      for (let omittedExportName of omittedExportNames) {\n        let dependencies = exportDependencies.get(omittedExportName);\n\n        invariant(\n          dependencies,\n          `Expected dependencies for ${omittedExportName}`,\n        );\n\n        // Now that we know the export is chunkable, add all of its top level\n        // non-module statements to the set of statements to be omitted from the\n        // main chunk. Note that we don't include top level module statements in\n        // this step because we perform more fine-grained filtering of module\n        // statements below.\n        for (let statement of dependencies.topLevelNonModuleStatements) {\n          omittedStatements.add(statement);\n        }\n\n        // We also want to omit any exported variable declarators that belong to\n        // the chunked export.\n        for (let declarator of dependencies.exportedVariableDeclarators) {\n          omittedExportedVariableDeclarators.add(declarator);\n        }\n      }\n\n      let ast = codeToAst(code, cache, cacheKey);\n\n      let omittedStatementsArray = Array.from(omittedStatements);\n      let omittedExportedVariableDeclaratorsArray = Array.from(\n        omittedExportedVariableDeclarators,\n      );\n\n      ast.program.body = ast.program.body\n        // Remove top level statements that belong solely to the chunked\n        // exports that are being omitted.\n        .filter((node) =>\n          omittedStatementsArray.every(\n            (statement) => !t.isNodesEquivalent(node, statement),\n          ),\n        )\n        // Remove unused imports.\n        .map((node): Statement | null => {\n          // Skip non-import nodes for this step, return node as-is\n          if (!t.isImportDeclaration(node)) {\n            return node;\n          }\n\n          // If there are no specifiers, this is a side effect import. Side\n          // effects implicitly belong to the main chunk, so we leave them.\n          if (node.specifiers.length === 0) {\n            return node;\n          }\n\n          // Remove import specifiers that are only used by the omitted chunks.\n          // This ensures only the necessary imports remain in the main chunk.\n          node.specifiers = node.specifiers.filter((specifier) => {\n            let importedName = specifier.local.name;\n\n            // Keep the import specifier if it's depended on by any of the\n            // retained exports.\n            for (let retainedExportName of retainedExportNames) {\n              let dependencies = exportDependencies.get(retainedExportName);\n              if (dependencies?.importedIdentifierNames?.has(importedName)) {\n                return true;\n              }\n            }\n\n            // Now that we've bailed out early and kept the import specifier if\n            // any retained exports depend on it, remove the import specifier if\n            // it's depended on by any of the omitted exports.\n            for (let omittedExportName of omittedExportNames) {\n              let dependencies = exportDependencies.get(omittedExportName);\n              if (dependencies?.importedIdentifierNames?.has(importedName)) {\n                return false;\n              }\n            }\n\n            // Keep the import specifier if it isn't depended on by any export.\n            return true;\n          });\n\n          // If the import statement is now empty, remove it\n          if (node.specifiers.length === 0) {\n            return null;\n          }\n\n          // Keep the modified AST node\n          return node;\n        })\n        // Filter out omitted exports and remove unused identifiers\n        .map((node): Statement | null => {\n          // Skip non-export nodes for this step, return node as-is\n          if (!t.isExportDeclaration(node)) {\n            return node;\n          }\n\n          // The main chunk should include all \"export *\" declarations\n          if (t.isExportAllDeclaration(node)) {\n            return node;\n          }\n\n          // export default ...;\n          if (t.isExportDefaultDeclaration(node)) {\n            return isOmitted(\"default\") ? null : node;\n          }\n\n          // export const foo = ..., { bar } = ...;\n          if (t.isVariableDeclaration(node.declaration)) {\n            // Remove any omitted exported variable declarators\n            node.declaration.declarations =\n              node.declaration.declarations.filter((node) =>\n                omittedExportedVariableDeclaratorsArray.every(\n                  (declarator) => !t.isNodesEquivalent(node, declarator),\n                ),\n              );\n\n            // If the export statement is now empty, remove it\n            if (node.declaration.declarations.length === 0) {\n              return null;\n            }\n\n            // Keep the modified AST node\n            return node;\n          }\n\n          // export function foo() {}\n          // export class foo {}\n          if (\n            t.isFunctionDeclaration(node.declaration) ||\n            t.isClassDeclaration(node.declaration)\n          ) {\n            invariant(\n              node.declaration.id,\n              \"Expected exported function or class declaration to have a name when not the default export\",\n            );\n            return isOmitted(node.declaration.id.name) ? null : node;\n          }\n\n          // export { foo, bar }\n          if (t.isExportNamedDeclaration(node)) {\n            // export {}\n            // Keep empty export statements in main chunk\n            if (node.specifiers.length === 0) {\n              return node;\n            }\n\n            // Remove omitted export specifiers\n            node.specifiers = node.specifiers.filter((specifier) => {\n              const exportedName = getExportedName(specifier.exported);\n              return !isOmitted(exportedName);\n            });\n\n            // If the export statement is now empty, remove it\n            if (node.specifiers.length === 0) {\n              return null;\n            }\n\n            // Keep the modified AST node\n            return node;\n          }\n\n          // This should never happen:\n          // @ts-expect-error: We've handled all the export types\n          throw new Error(`Unknown node type: ${node.type}`);\n        })\n        // Filter out statements that were entirely omitted above.\n        .filter((node): node is NonNullable<typeof node> => node !== null);\n\n      if (ast.program.body.length === 0) {\n        return undefined;\n      }\n\n      return generate(ast, generateOptions);\n    },\n  );\n}\n\nexport function detectRouteChunks(\n  code: string,\n  cache: Cache,\n  cacheKey: string,\n): {\n  hasRouteChunks: boolean;\n  hasRouteChunkByExportName: Record<RouteChunkExportName, boolean>;\n  chunkedExports: RouteChunkExportName[];\n} {\n  const hasRouteChunkByExportName = Object.fromEntries(\n    routeChunkExportNames.map((exportName) => [\n      exportName,\n      hasChunkableExport(code, exportName, cache, cacheKey),\n    ]),\n  ) as Record<RouteChunkExportName, boolean>;\n\n  const chunkedExports = Object.entries(hasRouteChunkByExportName)\n    .filter(([, isChunked]) => isChunked)\n    .map(([exportName]) => exportName as RouteChunkExportName);\n\n  const hasRouteChunks = chunkedExports.length > 0;\n\n  return {\n    hasRouteChunks,\n    hasRouteChunkByExportName,\n    chunkedExports,\n  };\n}\n\nexport const routeChunkExportNames = [\n  \"clientAction\",\n  \"clientLoader\",\n  \"clientMiddleware\",\n  \"HydrateFallback\",\n] as const;\nexport type RouteChunkExportName = (typeof routeChunkExportNames)[number];\n\nconst mainChunkName = \"main\" as const;\nexport const routeChunkNames = [\"main\", ...routeChunkExportNames] as const;\nexport type RouteChunkName = (typeof routeChunkNames)[number];\n\nexport function getRouteChunkCode(\n  code: string,\n  chunkName: RouteChunkName,\n  cache: Cache,\n  cacheKey: string,\n): GeneratorResult | undefined {\n  if (chunkName === mainChunkName) {\n    return omitChunkedExports(code, routeChunkExportNames, {}, cache, cacheKey);\n  }\n\n  return getChunkedExport(code, chunkName, {}, cache, cacheKey);\n}\n\nconst routeChunkQueryStringPrefix = \"?route-chunk=\";\ntype RouteChunkQueryString =\n  `${typeof routeChunkQueryStringPrefix}${RouteChunkName}`;\n\nconst routeChunkQueryStrings: Record<RouteChunkName, RouteChunkQueryString> = {\n  main: `${routeChunkQueryStringPrefix}main`,\n  clientAction: `${routeChunkQueryStringPrefix}clientAction`,\n  clientLoader: `${routeChunkQueryStringPrefix}clientLoader`,\n  clientMiddleware: `${routeChunkQueryStringPrefix}clientMiddleware`,\n  HydrateFallback: `${routeChunkQueryStringPrefix}HydrateFallback`,\n};\n\nexport function getRouteChunkModuleId(\n  filePath: string,\n  chunkName: RouteChunkName,\n): string {\n  return `${filePath}${routeChunkQueryStrings[chunkName]}`;\n}\n\nexport function isRouteChunkModuleId(id: string): boolean {\n  return Object.values(routeChunkQueryStrings).some((queryString) =>\n    id.endsWith(queryString),\n  );\n}\n\nfunction isRouteChunkName(name: string): name is RouteChunkName {\n  return name === mainChunkName || routeChunkExportNames.includes(name as any);\n}\n\nexport function getRouteChunkNameFromModuleId(\n  id: string,\n): RouteChunkName | null {\n  if (!isRouteChunkModuleId(id)) {\n    return null;\n  }\n\n  let chunkName = id.split(routeChunkQueryStringPrefix)[1].split(\"&\")[0];\n\n  if (!isRouteChunkName(chunkName)) {\n    return null;\n  }\n\n  return chunkName;\n}\n"
  },
  {
    "path": "packages/react-router-dev/vite/rsc/plugin.ts",
    "content": "import type * as Vite from \"vite\";\nimport { init as initEsModuleLexer } from \"es-module-lexer\";\nimport * as Path from \"pathe\";\nimport * as babel from \"@babel/core\";\nimport colors from \"picocolors\";\n\nimport { create } from \"../virtual-module\";\nimport * as Typegen from \"../../typegen\";\nimport { readFile } from \"fs/promises\";\nimport path, { join } from \"pathe\";\nimport invariant from \"../../invariant\";\nimport {\n  type ConfigLoader,\n  type ResolvedReactRouterConfig,\n  createConfigLoader,\n  resolveRSCEntryFiles,\n} from \"../../config/config\";\nimport { preloadVite } from \"../vite\";\nimport { hasDependency } from \"../has-dependency\";\nimport { getOptimizeDepsEntries } from \"../optimize-deps-entries\";\nimport { createVirtualRouteConfig } from \"./virtual-route-config\";\nimport {\n  transformVirtualRouteModules,\n  parseRouteExports,\n  isVirtualClientRouteModuleId,\n  CLIENT_NON_COMPONENT_EXPORTS,\n} from \"./virtual-route-modules\";\nimport { loadDotenv } from \"../load-dotenv\";\nimport { validatePluginOrder } from \"../plugins/validate-plugin-order\";\nimport { warnOnClientSourceMaps } from \"../plugins/warn-on-client-source-maps\";\n\nexport function reactRouterRSCVitePlugin(): Vite.PluginOption[] {\n  let runningWithinTheReactRouterMonoRepo = Boolean(\n    arguments &&\n      arguments.length === 1 &&\n      typeof arguments[0] === \"object\" &&\n      arguments[0] &&\n      \"__runningWithinTheReactRouterMonoRepo\" in arguments[0] &&\n      arguments[0].__runningWithinTheReactRouterMonoRepo === true,\n  );\n  let configLoader: ConfigLoader;\n  let typegenWatcherPromise: Promise<Typegen.Watcher> | undefined;\n  let viteCommand: Vite.ConfigEnv[\"command\"];\n  let resolvedViteConfig: Vite.ResolvedConfig;\n  let routeIdByFile: Map<string, string> | undefined;\n  let logger: Vite.Logger;\n  let entries: { client: string; rsc: string; ssr: string };\n\n  let config: ResolvedReactRouterConfig;\n  let rootRouteFile: string;\n  function updateConfig(newConfig: ResolvedReactRouterConfig) {\n    config = newConfig;\n    rootRouteFile = Path.resolve(\n      newConfig.appDirectory,\n      newConfig.routes.root.file,\n    );\n  }\n\n  return [\n    {\n      name: \"react-router/rsc\",\n      async config(viteUserConfig, { command, mode }) {\n        await initEsModuleLexer;\n        await preloadVite();\n\n        viteCommand = command;\n        const rootDirectory = getRootDirectory(viteUserConfig);\n        const watch = command === \"serve\";\n\n        await loadDotenv({\n          rootDirectory,\n          viteUserConfig,\n          mode,\n        });\n\n        configLoader = await createConfigLoader({\n          rootDirectory,\n          mode,\n          watch,\n          validateConfig: (userConfig) => {\n            let errors: string[] = [];\n            if (userConfig.buildEnd) errors.push(\"buildEnd\");\n            if (userConfig.prerender) errors.push(\"prerender\");\n            if (userConfig.presets?.length) errors.push(\"presets\");\n            if (userConfig.routeDiscovery) errors.push(\"routeDiscovery\");\n            if (userConfig.serverBundles) errors.push(\"serverBundles\");\n            if (userConfig.ssr === false) errors.push(\"ssr: false\");\n            if (userConfig.future?.v8_middleware === false)\n              errors.push(\"future.v8_middleware: false\");\n            if (userConfig.future?.v8_splitRouteModules)\n              errors.push(\"future.v8_splitRouteModules\");\n            if (userConfig.future?.v8_viteEnvironmentApi === false)\n              errors.push(\"future.v8_viteEnvironmentApi: false\");\n            if (userConfig.future?.unstable_subResourceIntegrity)\n              errors.push(\"future.unstable_subResourceIntegrity\");\n            if (errors.length) {\n              return `RSC Framework Mode does not currently support the following React Router config:\\n${errors.map((x) => ` - ${x}`).join(\"\\n\")}\\n`;\n            }\n          },\n        });\n\n        const configResult = await configLoader.getConfig();\n        if (!configResult.ok) throw new Error(configResult.error);\n        updateConfig(configResult.value);\n\n        if (\n          viteUserConfig.base &&\n          config.basename !== \"/\" &&\n          viteCommand === \"serve\" &&\n          !viteUserConfig.server?.middlewareMode &&\n          !config.basename.startsWith(viteUserConfig.base)\n        ) {\n          throw new Error(\n            \"When using the React Router `basename` and the Vite `base` config, \" +\n              \"the `basename` config must begin with `base` for the default \" +\n              \"Vite dev server.\",\n          );\n        }\n\n        const vite = await import(\"vite\");\n        logger = vite.createLogger(viteUserConfig.logLevel, {\n          prefix: \"[react-router]\",\n        });\n\n        entries = await resolveRSCEntryFiles({\n          reactRouterConfig: config,\n        });\n\n        // Async import here to avoid CJS warnings on the console\n        let viteNormalizePath = (await import(\"vite\")).normalizePath;\n\n        return {\n          resolve: {\n            dedupe: [\n              // https://react.dev/warnings/invalid-hook-call-warning#duplicate-react\n              \"react\",\n              \"react/jsx-runtime\",\n              \"react/jsx-dev-runtime\",\n              \"react-dom\",\n              \"react-dom/client\",\n              // Avoid router duplicates since mismatching routers cause `Error:\n              // You must render this element inside a <Remix> element`.\n              \"react-router\",\n              \"react-router/dom\",\n              \"react-router/internal/react-server-client\",\n              ...(hasDependency({ name: \"react-router-dom\", rootDirectory })\n                ? [\"react-router-dom\"]\n                : []),\n              ...(hasDependency({\n                name: \"react-server-dom-webpack\",\n                rootDirectory,\n              })\n                ? [\"react-server-dom-webpack\"]\n                : []),\n            ],\n          },\n          optimizeDeps: {\n            entries: getOptimizeDepsEntries({\n              entryClientFilePath: entries.client,\n              reactRouterConfig: config,\n            }),\n            esbuildOptions: {\n              jsx: \"automatic\",\n            },\n            include: [\n              // Pre-bundle React dependencies to avoid React duplicates,\n              // even if React dependencies are not direct dependencies.\n              // https://react.dev/warnings/invalid-hook-call-warning#duplicate-react\n              \"react\",\n              \"react/jsx-runtime\",\n              \"react/jsx-dev-runtime\",\n              \"react-dom\",\n              ...(hasDependency({\n                name: \"react-server-dom-webpack\",\n                rootDirectory,\n              })\n                ? [\"react-server-dom-webpack\"]\n                : []),\n              ...(runningWithinTheReactRouterMonoRepo\n                ? []\n                : [\n                    \"react-router\",\n                    \"react-router/dom\",\n                    \"react-router/internal/react-server-client\",\n                  ]),\n              \"react-router > cookie\",\n              \"react-router > set-cookie-parser\",\n            ],\n          },\n          esbuild: {\n            jsx: \"automatic\",\n            jsxDev: viteCommand !== \"build\",\n          },\n          environments: {\n            client: {\n              build: {\n                rollupOptions: {\n                  input: {\n                    index: entries.client,\n                  },\n                  output: {\n                    manualChunks(id) {\n                      const normalized = viteNormalizePath(id);\n                      if (\n                        normalized.includes(\"node_modules/react/\") ||\n                        normalized.includes(\"node_modules/react-dom/\") ||\n                        normalized.includes(\n                          \"node_modules/react-server-dom-webpack/\",\n                        ) ||\n                        normalized.includes(\"node_modules/@vitejs/plugin-rsc/\")\n                      ) {\n                        return \"react\";\n                      }\n                      if (normalized.includes(\"node_modules/react-router/\")) {\n                        return \"router\";\n                      }\n                    },\n                  },\n                },\n                outDir: join(config.buildDirectory, \"client\"),\n              },\n            },\n            rsc: {\n              build: {\n                rollupOptions: {\n                  input: {\n                    index: entries.rsc,\n                  },\n                  output: {\n                    entryFileNames: config.serverBuildFile,\n                    format: config.serverModuleFormat,\n                  },\n                },\n                outDir: join(config.buildDirectory, \"server\"),\n              },\n              resolve: {\n                noExternal: [\n                  \"@react-router/dev/config/default-rsc-entries/entry.ssr\",\n                ],\n              },\n            },\n            ssr: {\n              build: {\n                rollupOptions: {\n                  input: {\n                    index: entries.ssr,\n                  },\n                  output: {\n                    // Note: We don't set `entryFileNames` here because it's\n                    // considered private to the RSC environment build, and\n                    // @vitejs/plugin-rsc currently breaks if it's set to\n                    // something other than `index.js`.\n                    format: config.serverModuleFormat,\n                  },\n                },\n                outDir: join(config.buildDirectory, \"server/__ssr_build\"),\n              },\n              resolve: {\n                noExternal: [\n                  \"@react-router/dev/config/default-rsc-entries/entry.rsc\",\n                ],\n              },\n            },\n          },\n          build: {\n            rollupOptions: {\n              // Copied from https://github.com/vitejs/vite-plugin-react/blob/c602225271d4acf462ba00f8d6d8a2e42492c5cd/packages/common/warning.ts\n              onwarn(warning, defaultHandler) {\n                if (\n                  warning.code === \"MODULE_LEVEL_DIRECTIVE\" &&\n                  (warning.message.includes(\"use client\") ||\n                    warning.message.includes(\"use server\"))\n                ) {\n                  return;\n                }\n                // https://github.com/vitejs/vite/issues/15012\n                if (\n                  warning.code === \"SOURCEMAP_ERROR\" &&\n                  warning.message.includes(\"resolve original location\") &&\n                  warning.pos === 0\n                ) {\n                  return;\n                }\n                if (viteUserConfig.build?.rollupOptions?.onwarn) {\n                  viteUserConfig.build.rollupOptions.onwarn(\n                    warning,\n                    defaultHandler,\n                  );\n                } else {\n                  defaultHandler(warning);\n                }\n              },\n            },\n          },\n        };\n      },\n      configResolved(viteConfig) {\n        resolvedViteConfig = viteConfig;\n      },\n      async configureServer(viteDevServer) {\n        configLoader.onChange(\n          async ({\n            result,\n            configCodeChanged,\n            routeConfigCodeChanged,\n            configChanged,\n            routeConfigChanged,\n          }) => {\n            if (!result.ok) {\n              invalidateVirtualModules(viteDevServer);\n              logger.error(result.error, {\n                clear: true,\n                timestamp: true,\n              });\n              return;\n            }\n\n            // prettier-ignore\n            let message =\n              configChanged ? \"Config changed.\" :\n              routeConfigChanged ? \"Route config changed.\" :\n              configCodeChanged ? \"Config saved.\" :\n              routeConfigCodeChanged ? \" Route config saved.\" :\n              \"Config saved\";\n\n            logger.info(colors.green(message), {\n              clear: true,\n              timestamp: true,\n            });\n\n            // Update shared plugin config reference\n            updateConfig(result.value);\n\n            if (configChanged || routeConfigChanged) {\n              invalidateVirtualModules(viteDevServer);\n            }\n          },\n        );\n      },\n      async buildEnd() {\n        await configLoader.close();\n      },\n    },\n    (() => {\n      let logged = false;\n      function logExperimentalNotice() {\n        if (logged) return;\n        logged = true;\n        logger.info(\n          colors.yellow(\n            `${viteCommand === \"serve\" ? \"  \" : \"\"}🧪 Using React Router's RSC Framework Mode (experimental)`,\n          ),\n        );\n      }\n      return {\n        name: \"react-router/rsc/log-experimental-notice\",\n        sharedDuringBuild: true,\n        buildStart: logExperimentalNotice,\n        configureServer: logExperimentalNotice,\n      };\n    })(),\n    {\n      name: \"react-router/rsc/typegen\",\n      async config(viteUserConfig, { command, mode }) {\n        if (command === \"serve\") {\n          const vite = await import(\"vite\");\n          typegenWatcherPromise = Typegen.watch(\n            getRootDirectory(viteUserConfig),\n            {\n              mode,\n              rsc: true,\n              // ignore `info` logs from typegen since they are\n              // redundant when Vite plugin logs are active\n              logger: vite.createLogger(\"warn\", {\n                prefix: \"[react-router]\",\n              }),\n            },\n          );\n        }\n      },\n      async buildEnd() {\n        (await typegenWatcherPromise)?.close();\n      },\n    },\n\n    {\n      name: \"react-router/rsc/virtual-route-config\",\n      resolveId(id) {\n        if (id === virtual.routeConfig.id) {\n          return virtual.routeConfig.resolvedId;\n        }\n      },\n      load(id) {\n        if (id === virtual.routeConfig.resolvedId) {\n          const result = createVirtualRouteConfig({\n            appDirectory: config.appDirectory,\n            routeConfig: config.unstable_routeConfig,\n          });\n          routeIdByFile = result.routeIdByFile;\n          return result.code;\n        }\n      },\n    },\n    {\n      name: \"react-router/rsc/virtual-route-modules\",\n      transform(code, id) {\n        if (!routeIdByFile) return;\n        return transformVirtualRouteModules({\n          code,\n          id,\n          viteCommand,\n          routeIdByFile,\n          rootRouteFile,\n          viteEnvironment: this.environment,\n        });\n      },\n    },\n    {\n      name: \"react-router/rsc/virtual-basename\",\n      resolveId(id) {\n        if (id === virtual.basename.id) {\n          return virtual.basename.resolvedId;\n        }\n      },\n      load(id) {\n        if (id === virtual.basename.resolvedId) {\n          return `export default ${JSON.stringify(config.basename)};`;\n        }\n      },\n    },\n    {\n      name: \"react-router/rsc/hmr/inject-runtime\",\n      enforce: \"pre\",\n      resolveId(id) {\n        if (id === virtual.injectHmrRuntime.id) {\n          return virtual.injectHmrRuntime.resolvedId;\n        }\n      },\n      async load(id) {\n        if (id !== virtual.injectHmrRuntime.resolvedId) return;\n\n        return viteCommand === \"serve\"\n          ? [\n              `import RefreshRuntime from \"${virtual.hmrRuntime.id}\"`,\n              \"RefreshRuntime.injectIntoGlobalHook(window)\",\n              \"window.$RefreshReg$ = () => {}\",\n              \"window.$RefreshSig$ = () => (type) => type\",\n              \"window.__vite_plugin_react_preamble_installed__ = true\",\n            ].join(\"\\n\")\n          : \"\";\n      },\n    },\n    {\n      name: \"react-router/rsc/hmr/runtime\",\n      enforce: \"pre\",\n      resolveId(id) {\n        if (id === virtual.hmrRuntime.id) return virtual.hmrRuntime.resolvedId;\n      },\n      async load(id) {\n        if (id !== virtual.hmrRuntime.resolvedId) return;\n\n        const reactRefreshDir = path.dirname(\n          require.resolve(\"react-refresh/package.json\"),\n        );\n        const reactRefreshRuntimePath = join(\n          reactRefreshDir,\n          \"cjs/react-refresh-runtime.development.js\",\n        );\n\n        return [\n          \"const exports = {}\",\n          await readFile(reactRefreshRuntimePath, \"utf8\"),\n          await readFile(\n            require.resolve(\"./static/rsc-refresh-utils.mjs\"),\n            \"utf8\",\n          ),\n          \"export default exports\",\n        ].join(\"\\n\");\n      },\n    },\n    {\n      name: \"react-router/rsc/hmr/react-refresh\",\n      async transform(code, id, options) {\n        if (viteCommand !== \"serve\") return;\n        if (id.includes(\"/node_modules/\")) return;\n\n        const filepath = id.split(\"?\")[0];\n        const extensionsRE = /\\.(jsx?|tsx?|mdx?)$/;\n        if (!extensionsRE.test(filepath)) return;\n\n        const devRuntime = \"react/jsx-dev-runtime\";\n        const ssr = options?.ssr === true;\n        const isJSX = filepath.endsWith(\"x\");\n        const useFastRefresh = !ssr && (isJSX || code.includes(devRuntime));\n        if (!useFastRefresh) return;\n\n        if (isVirtualClientRouteModuleId(id)) {\n          const routeId = routeIdByFile?.get(filepath);\n          return { code: addRefreshWrapper({ routeId, code, id }) };\n        }\n\n        const result = await babel.transformAsync(code, {\n          babelrc: false,\n          configFile: false,\n          filename: id,\n          sourceFileName: filepath,\n          parserOpts: {\n            sourceType: \"module\",\n            allowAwaitOutsideFunction: true,\n          },\n          plugins: [[require(\"react-refresh/babel\"), { skipEnvCheck: true }]],\n          sourceMaps: true,\n        });\n        if (result === null) return;\n\n        code = result.code!;\n        const refreshContentRE = /\\$Refresh(?:Reg|Sig)\\$\\(/;\n        if (refreshContentRE.test(code)) {\n          code = addRefreshWrapper({ code, id });\n        }\n        return { code, map: result.map };\n      },\n    },\n    {\n      name: \"react-router/rsc/hmr/updates\",\n      async hotUpdate(this, { server, file, modules }) {\n        if (this.environment.name !== \"rsc\") return;\n\n        const clientModules =\n          server.environments.client.moduleGraph.getModulesByFile(file);\n\n        const vite = await import(\"vite\");\n        const isServerOnlyChange =\n          !clientModules ||\n          clientModules.size === 0 ||\n          // Handle CSS injected from server-first routes (with ?direct query\n          // string) since the client graph has a reference to the CSS\n          (vite.isCSSRequest(file) &&\n            Array.from(clientModules).some((mod) =>\n              mod.id?.includes(\"?direct\"),\n            ));\n\n        for (const mod of getModulesWithImporters(modules)) {\n          if (!mod.file) continue;\n\n          const normalizedPath = path.normalize(mod.file);\n          const routeId = routeIdByFile?.get(normalizedPath);\n          if (routeId !== undefined) {\n            const routeSource = await readFile(normalizedPath, \"utf8\");\n            const virtualRouteModuleCode = (\n              await server.environments.rsc.pluginContainer.transform(\n                routeSource,\n                `${normalizedPath}?route-module`,\n              )\n            ).code;\n            const { staticExports } = parseRouteExports(virtualRouteModuleCode);\n            const hasAction = staticExports.includes(\"action\");\n            const hasComponent = staticExports.includes(\"default\");\n            const hasErrorBoundary = staticExports.includes(\"ErrorBoundary\");\n            const hasLoader = staticExports.includes(\"loader\");\n\n            server.hot.send({\n              type: \"custom\",\n              event: \"react-router:hmr\",\n              data: {\n                routeId,\n                isServerOnlyChange,\n                hasAction,\n                hasComponent,\n                hasErrorBoundary,\n                hasLoader,\n              },\n            });\n          }\n        }\n\n        return modules;\n      },\n    },\n    {\n      name: \"react-router/rsc/virtual-react-router-serve-config\",\n      resolveId(id) {\n        if (id === virtual.reactRouterServeConfig.id) {\n          return virtual.reactRouterServeConfig.resolvedId;\n        }\n      },\n      load(id) {\n        if (id === virtual.reactRouterServeConfig.resolvedId) {\n          const rscOutDir = resolvedViteConfig.environments.rsc?.build?.outDir;\n          invariant(rscOutDir, \"RSC build directory config not found\");\n          const clientOutDir =\n            resolvedViteConfig.environments.client?.build?.outDir;\n          invariant(clientOutDir, \"Client build directory config not found\");\n          const assetsBuildDirectory = Path.relative(rscOutDir, clientOutDir);\n          const publicPath = resolvedViteConfig.base;\n\n          return `export default ${JSON.stringify({\n            assetsBuildDirectory,\n            publicPath,\n          })};`;\n        }\n      },\n    },\n    validatePluginOrder(),\n    warnOnClientSourceMaps(),\n  ];\n}\n\nconst virtual = {\n  routeConfig: create(\"unstable_rsc/routes\"),\n  injectHmrRuntime: create(\"unstable_rsc/inject-hmr-runtime\"),\n  hmrRuntime: create(\"unstable_rsc/runtime\"),\n  basename: create(\"unstable_rsc/basename\"),\n  reactRouterServeConfig: create(\"unstable_rsc/react-router-serve-config\"),\n};\n\nfunction invalidateVirtualModules(viteDevServer: Vite.ViteDevServer) {\n  for (const vmod of Object.values(virtual)) {\n    for (const env of Object.values(viteDevServer.environments)) {\n      const mod = env.moduleGraph.getModuleById(vmod.resolvedId);\n      if (mod) {\n        env.moduleGraph.invalidateModule(mod);\n      }\n    }\n  }\n}\n\nfunction getRootDirectory(viteUserConfig: Vite.UserConfig) {\n  return viteUserConfig.root ?? process.env.REACT_ROUTER_ROOT ?? process.cwd();\n}\n\nfunction getModulesWithImporters(\n  modules: Vite.EnvironmentModuleNode[],\n): Set<Vite.EnvironmentModuleNode> {\n  const visited = new Set<Vite.EnvironmentModuleNode>();\n  const result = new Set<Vite.EnvironmentModuleNode>();\n\n  function walk(module: Vite.EnvironmentModuleNode) {\n    if (visited.has(module)) return;\n\n    visited.add(module);\n    result.add(module);\n\n    for (const importer of module.importers) {\n      walk(importer);\n    }\n  }\n\n  for (const module of modules) {\n    walk(module);\n  }\n\n  return result;\n}\n\nfunction addRefreshWrapper({\n  routeId,\n  code,\n  id,\n}: {\n  routeId?: string;\n  code: string;\n  id: string;\n}): string {\n  const acceptExports =\n    routeId !== undefined ? CLIENT_NON_COMPONENT_EXPORTS : [];\n  return (\n    REACT_REFRESH_HEADER.replaceAll(\"__SOURCE__\", JSON.stringify(id)) +\n    code +\n    REACT_REFRESH_FOOTER.replaceAll(\"__SOURCE__\", JSON.stringify(id))\n      .replaceAll(\"__ACCEPT_EXPORTS__\", JSON.stringify(acceptExports))\n      .replaceAll(\"__ROUTE_ID__\", JSON.stringify(routeId))\n  );\n}\n\nconst REACT_REFRESH_HEADER = `\nimport RefreshRuntime from \"${virtual.hmrRuntime.id}\";\n\nconst inWebWorker = typeof WorkerGlobalScope !== 'undefined' && self instanceof WorkerGlobalScope;\nlet prevRefreshReg;\nlet prevRefreshSig;\n\nif (import.meta.hot && !inWebWorker) {\n  if (!window.__vite_plugin_react_preamble_installed__) {\n    throw new Error(\n      \"React Router Vite plugin can't detect preamble. Something is wrong.\"\n    );\n  }\n\n  prevRefreshReg = window.$RefreshReg$;\n  prevRefreshSig = window.$RefreshSig$;\n  window.$RefreshReg$ = (type, id) => {\n    RefreshRuntime.register(type, __SOURCE__ + \" \" + id)\n  };\n  window.$RefreshSig$ = RefreshRuntime.createSignatureFunctionForTransform;\n}`.replaceAll(\"\\n\", \"\"); // Header is all on one line so source maps aren't affected\n\nconst REACT_REFRESH_FOOTER = `\nif (import.meta.hot && !inWebWorker) {\n  window.$RefreshReg$ = prevRefreshReg;\n  window.$RefreshSig$ = prevRefreshSig;\n  RefreshRuntime.__hmr_import(import.meta.url).then((currentExports) => {\n    RefreshRuntime.registerExportsForReactRefresh(__SOURCE__, currentExports);\n    import.meta.hot.accept((nextExports) => {\n      if (!nextExports) return;\n      __ROUTE_ID__ && window.__reactRouterRouteModuleUpdates.set(__ROUTE_ID__, nextExports);\n      const invalidateMessage = RefreshRuntime.validateRefreshBoundaryAndEnqueueUpdate(currentExports, nextExports, __ACCEPT_EXPORTS__);\n      if (invalidateMessage) import.meta.hot.invalidate(invalidateMessage);\n    });\n  });\n}`;\n"
  },
  {
    "path": "packages/react-router-dev/vite/rsc/virtual-route-config.ts",
    "content": "import path from \"pathe\";\nimport type { RouteConfigEntry } from \"../../routes\";\n\nexport function createVirtualRouteConfig({\n  appDirectory,\n  routeConfig,\n}: {\n  appDirectory: string;\n  routeConfig: RouteConfigEntry[];\n}): { code: string; routeIdByFile: Map<string, string> } {\n  let routeIdByFile = new Map<string, string>();\n  let code = \"export default [\";\n\n  const closeRouteSymbol = Symbol(\"CLOSE_ROUTE\");\n  let stack: Array<typeof closeRouteSymbol | RouteConfigEntry> = [\n    ...routeConfig,\n  ];\n  while (stack.length > 0) {\n    const route = stack.pop();\n    if (!route) break;\n    if (route === closeRouteSymbol) {\n      code += \"]},\";\n      continue;\n    }\n\n    code += \"{\";\n    const routeFile = path.resolve(appDirectory, route.file);\n    const routeId = route.id || createRouteId(route.file, appDirectory);\n    routeIdByFile.set(routeFile, routeId);\n    code += `lazy: () => import(${JSON.stringify(\n      `${routeFile}?route-module`,\n    )}),`;\n\n    code += `id: ${JSON.stringify(routeId)},`;\n    if (typeof route.path === \"string\") {\n      code += `path: ${JSON.stringify(route.path)},`;\n    }\n    if (route.index) {\n      code += `index: true,`;\n    }\n    if (route.caseSensitive) {\n      code += `caseSensitive: true,`;\n    }\n    if (route.children) {\n      code += [\"children:[\"];\n      stack.push(closeRouteSymbol);\n      stack.push(...[...route.children].reverse());\n    } else {\n      code += \"},\";\n    }\n  }\n\n  code += \"];\\n\";\n\n  return { code, routeIdByFile };\n}\n\nfunction createRouteId(file: string, appDirectory: string) {\n  return path\n    .relative(appDirectory, file)\n    .replace(/\\\\+/, \"/\")\n    .slice(0, -path.extname(file).length);\n}\n"
  },
  {
    "path": "packages/react-router-dev/vite/rsc/virtual-route-modules.ts",
    "content": "import type * as Vite from \"vite\";\nimport * as babel from \"../babel\";\nimport { parse as esModuleLexer } from \"es-module-lexer\";\nimport { removeExports } from \"../remove-exports\";\n\nconst SERVER_ONLY_COMPONENT_EXPORTS = [\"ServerComponent\"] as const;\n\nconst SERVER_ONLY_ROUTE_EXPORTS = [\n  ...SERVER_ONLY_COMPONENT_EXPORTS,\n  \"loader\",\n  \"action\",\n  \"middleware\",\n  \"headers\",\n] as const;\ntype ServerOnlyRouteExport = (typeof SERVER_ONLY_ROUTE_EXPORTS)[number];\nconst SERVER_ONLY_ROUTE_EXPORTS_SET = new Set(SERVER_ONLY_ROUTE_EXPORTS);\nfunction isServerOnlyRouteExport(name: string): name is ServerOnlyRouteExport {\n  return SERVER_ONLY_ROUTE_EXPORTS_SET.has(name as ServerOnlyRouteExport);\n}\n\nconst COMMON_COMPONENT_EXPORTS = [\n  \"ErrorBoundary\",\n  \"HydrateFallback\",\n  \"Layout\",\n] as const;\n\nconst SERVER_FIRST_COMPONENT_EXPORTS = [\n  ...COMMON_COMPONENT_EXPORTS,\n  ...SERVER_ONLY_COMPONENT_EXPORTS,\n] as const;\ntype ServerFirstComponentExport =\n  (typeof SERVER_FIRST_COMPONENT_EXPORTS)[number];\nconst SERVER_FIRST_COMPONENT_EXPORTS_SET = new Set(\n  SERVER_FIRST_COMPONENT_EXPORTS,\n);\nfunction isServerFirstComponentExport(\n  name: string,\n): name is ServerFirstComponentExport {\n  return SERVER_FIRST_COMPONENT_EXPORTS_SET.has(\n    name as ServerFirstComponentExport,\n  );\n}\n\nconst CLIENT_COMPONENT_EXPORTS = [\n  ...COMMON_COMPONENT_EXPORTS,\n  \"default\",\n] as const;\n\nexport const CLIENT_NON_COMPONENT_EXPORTS = [\n  \"clientAction\",\n  \"clientLoader\",\n  \"clientMiddleware\",\n  \"handle\",\n  \"meta\",\n  \"links\",\n  \"shouldRevalidate\",\n] as const;\ntype ClientNonComponentExport = (typeof CLIENT_NON_COMPONENT_EXPORTS)[number];\nconst CLIENT_NON_COMPONENT_EXPORTS_SET = new Set(CLIENT_NON_COMPONENT_EXPORTS);\nfunction isClientNonComponentExport(\n  name: string,\n): name is ClientNonComponentExport {\n  return CLIENT_NON_COMPONENT_EXPORTS_SET.has(name as ClientNonComponentExport);\n}\n\nconst CLIENT_ROUTE_EXPORTS = [\n  ...CLIENT_NON_COMPONENT_EXPORTS,\n  ...CLIENT_COMPONENT_EXPORTS,\n] as const;\ntype ClientRouteExport = (typeof CLIENT_ROUTE_EXPORTS)[number];\nconst CLIENT_ROUTE_EXPORTS_SET = new Set(CLIENT_ROUTE_EXPORTS);\nfunction isClientRouteExport(name: string): name is ClientRouteExport {\n  return CLIENT_ROUTE_EXPORTS_SET.has(name as ClientRouteExport);\n}\n\nconst ROUTE_EXPORTS = [\n  ...SERVER_ONLY_ROUTE_EXPORTS,\n  ...CLIENT_ROUTE_EXPORTS,\n] as const;\ntype RouteExport = (typeof ROUTE_EXPORTS)[number];\nconst ROUTE_EXPORTS_SET = new Set(ROUTE_EXPORTS);\nfunction isRouteExport(name: string): name is RouteExport {\n  return ROUTE_EXPORTS_SET.has(name as RouteExport);\n}\nfunction isCustomRouteExport(name: string) {\n  return !isRouteExport(name);\n}\n\nfunction hasReactServerCondition(viteEnvironment: Vite.Environment) {\n  return viteEnvironment.config.resolve.conditions.includes(\"react-server\");\n}\n\ntype ViteCommand = Vite.ConfigEnv[\"command\"];\n\nexport function transformVirtualRouteModules({\n  id,\n  code,\n  viteCommand,\n  routeIdByFile,\n  rootRouteFile,\n  viteEnvironment,\n}: {\n  id: string;\n  code: string;\n  viteCommand: ViteCommand;\n  routeIdByFile: Map<string, string>;\n  rootRouteFile: string;\n  viteEnvironment: Vite.Environment;\n}) {\n  if (isVirtualRouteModuleId(id) || routeIdByFile.has(id)) {\n    return createVirtualRouteModuleCode({\n      id,\n      code,\n      rootRouteFile,\n      viteCommand,\n      viteEnvironment,\n    });\n  }\n\n  if (isVirtualServerRouteModuleId(id)) {\n    return createVirtualServerRouteModuleCode({\n      id,\n      code,\n      viteEnvironment,\n    });\n  }\n\n  if (isVirtualClientRouteModuleId(id)) {\n    return createVirtualClientRouteModuleCode({\n      id,\n      code,\n      rootRouteFile,\n      viteCommand,\n    });\n  }\n}\n\nasync function createVirtualRouteModuleCode({\n  id,\n  code: routeSource,\n  rootRouteFile,\n  viteCommand,\n  viteEnvironment,\n}: {\n  id: string;\n  code: string;\n  rootRouteFile: string;\n  viteCommand: ViteCommand;\n  viteEnvironment: Vite.Environment;\n}) {\n  const isReactServer = hasReactServerCondition(viteEnvironment);\n  const { staticExports, isServerFirstRoute, hasClientExports } =\n    parseRouteExports(routeSource);\n\n  const clientModuleId = getVirtualClientModuleId(id);\n  const serverModuleId = getVirtualServerModuleId(id);\n\n  let code = \"\";\n  if (isServerFirstRoute) {\n    if (staticExports.some(isServerFirstComponentExport)) {\n      code += `import React from \"react\";\\n`;\n    }\n    for (const staticExport of staticExports) {\n      if (isClientNonComponentExport(staticExport)) {\n        code += `export { ${staticExport} } from \"${clientModuleId}\";\\n`;\n      } else if (\n        isReactServer &&\n        isServerFirstComponentExport(staticExport) &&\n        // Layout wraps all other component exports so doesn't need CSS injected\n        staticExport !== \"Layout\"\n      ) {\n        code += `import { ${staticExport} as ${staticExport}WithoutCss } from \"${serverModuleId}\";\\n`;\n        code += `export ${staticExport === \"ServerComponent\" ? \"default \" : \" \"}function ${staticExport}(props) {\\n`;\n        code += `  return React.createElement(React.Fragment, null,\\n`;\n        code += `    import.meta.viteRsc.loadCss(),\\n`;\n        code += `    React.createElement(${staticExport}WithoutCss, props),\\n`;\n        code += `  );\\n`;\n        code += `}\\n`;\n      } else if (isReactServer && isRouteExport(staticExport)) {\n        code += `export { ${staticExport} } from \"${serverModuleId}\";\\n`;\n      } else if (isCustomRouteExport(staticExport)) {\n        code += `export { ${staticExport} } from \"${isReactServer ? serverModuleId : clientModuleId}\";\\n`;\n      }\n    }\n    if (viteCommand === \"serve\" && !hasClientExports) {\n      code += `export { __ensureClientRouteModuleForHMR } from \"${clientModuleId}\";\\n`;\n    }\n  } else {\n    for (const staticExport of staticExports) {\n      if (isClientRouteExport(staticExport)) {\n        code += `export { ${staticExport} } from \"${clientModuleId}\";\\n`;\n      } else if (isReactServer && isServerOnlyRouteExport(staticExport)) {\n        code += `export { ${staticExport} } from \"${serverModuleId}\";\\n`;\n      } else if (isCustomRouteExport(staticExport)) {\n        code += `export { ${staticExport} } from \"${isReactServer ? serverModuleId : clientModuleId}\";\\n`;\n      }\n    }\n  }\n\n  if (\n    isRootRouteFile({ id, rootRouteFile }) &&\n    !staticExports.includes(\"ErrorBoundary\")\n  ) {\n    code += `export { ErrorBoundary } from \"${clientModuleId}\";\\n`;\n  }\n\n  return code;\n}\n\nfunction createVirtualServerRouteModuleCode({\n  id,\n  code: routeSource,\n  viteEnvironment,\n}: {\n  id: string;\n  code: string;\n  viteEnvironment: Vite.Environment;\n}) {\n  if (!hasReactServerCondition(viteEnvironment)) {\n    throw new Error(\n      [\n        \"Virtual server route module was loaded outside of the RSC environment.\",\n        `Environment Name: ${viteEnvironment.name}`,\n        `Module ID: ${id}`,\n      ].join(\"\\n\"),\n    );\n  }\n\n  const { staticExports, isServerFirstRoute } = parseRouteExports(routeSource);\n  const clientModuleId = getVirtualClientModuleId(id);\n  const serverRouteModuleAst = babel.parse(routeSource, {\n    sourceType: \"module\",\n  });\n  removeExports(\n    serverRouteModuleAst,\n    isServerFirstRoute ? CLIENT_NON_COMPONENT_EXPORTS : CLIENT_ROUTE_EXPORTS,\n  );\n\n  const generatorResult = babel.generate(serverRouteModuleAst);\n\n  if (!isServerFirstRoute) {\n    for (const staticExport of staticExports) {\n      if (isClientRouteExport(staticExport)) {\n        generatorResult.code += \"\\n\";\n        generatorResult.code += `export { ${staticExport} } from \"${clientModuleId}\";\\n`;\n      }\n    }\n  }\n\n  return generatorResult;\n}\n\nfunction createVirtualClientRouteModuleCode({\n  id,\n  code: routeSource,\n  rootRouteFile,\n  viteCommand,\n}: {\n  id: string;\n  code: string;\n  rootRouteFile: string;\n  viteCommand: ViteCommand;\n}) {\n  const { staticExports, isServerFirstRoute, hasClientExports } =\n    parseRouteExports(routeSource);\n  const exportsToRemove = isServerFirstRoute\n    ? [...SERVER_ONLY_ROUTE_EXPORTS, ...CLIENT_COMPONENT_EXPORTS]\n    : SERVER_ONLY_ROUTE_EXPORTS;\n\n  const clientRouteModuleAst = babel.parse(routeSource, {\n    sourceType: \"module\",\n  });\n  removeExports(clientRouteModuleAst, exportsToRemove);\n\n  const generatorResult = babel.generate(clientRouteModuleAst);\n  generatorResult.code = '\"use client\";' + generatorResult.code;\n\n  if (\n    isRootRouteFile({ id, rootRouteFile }) &&\n    !staticExports.includes(\"ErrorBoundary\")\n  ) {\n    const hasRootLayout = staticExports.includes(\"Layout\");\n    generatorResult.code += `\\nimport { createElement as __rr_createElement } from \"react\";\\n`;\n    generatorResult.code += `import { UNSAFE_RSCDefaultRootErrorBoundary } from \"react-router\";\\n`;\n    generatorResult.code += `export function ErrorBoundary() {\\n`;\n    generatorResult.code += `  return __rr_createElement(UNSAFE_RSCDefaultRootErrorBoundary, { hasRootLayout: ${hasRootLayout} });\\n`;\n    generatorResult.code += `}\\n`;\n  }\n\n  if (viteCommand === \"serve\" && isServerFirstRoute && !hasClientExports) {\n    generatorResult.code += `\\nexport const __ensureClientRouteModuleForHMR = true;`;\n  }\n\n  return generatorResult;\n}\n\nexport function parseRouteExports(code: string) {\n  const [, exportSpecifiers] = esModuleLexer(code);\n  const staticExports = exportSpecifiers.map(({ n: name }) => name);\n  const isServerFirstRoute = staticExports.some(\n    (staticExport) => staticExport === \"ServerComponent\",\n  );\n  return {\n    staticExports,\n    isServerFirstRoute,\n    hasClientExports: staticExports.some(\n      isServerFirstRoute ? isClientNonComponentExport : isClientRouteExport,\n    ),\n  };\n}\n\nfunction getVirtualClientModuleId(id: string): string {\n  return `${id.split(\"?\")[0]}?client-route-module`;\n}\n\nfunction getVirtualServerModuleId(id: string): string {\n  return `${id.split(\"?\")[0]}?server-route-module`;\n}\n\nfunction isVirtualRouteModuleId(id: string): boolean {\n  return /(\\?|&)route-module(&|$)/.test(id);\n}\n\nexport function isVirtualClientRouteModuleId(id: string): boolean {\n  return /(\\?|&)client-route-module(&|$)/.test(id);\n}\n\nfunction isVirtualServerRouteModuleId(id: string): boolean {\n  return /(\\?|&)server-route-module(&|$)/.test(id);\n}\n\nfunction isRootRouteFile({\n  id,\n  rootRouteFile,\n}: {\n  id: string;\n  rootRouteFile: string;\n}): boolean {\n  const filePath = id.split(\"?\")[0];\n  return filePath === rootRouteFile;\n}\n"
  },
  {
    "path": "packages/react-router-dev/vite/ssr-externals.ts",
    "content": "import { isReactRouterRepo } from \"../config/is-react-router-repo\";\n\nexport const ssrExternals = isReactRouterRepo()\n  ? [\n      // This is only needed within this repo because these packages\n      // are linked to a directory outside of node_modules so Vite\n      // treats them as internal code by default.\n      \"react-router\",\n      \"react-router-dom\",\n      \"@react-router/architect\",\n      \"@react-router/cloudflare\",\n      \"@react-router/dev\",\n      \"@react-router/express\",\n      \"@react-router/node\",\n      \"@react-router/serve\",\n    ]\n  : undefined;\n"
  },
  {
    "path": "packages/react-router-dev/vite/static/refresh-utils.mjs",
    "content": "// adapted from https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/src/refreshUtils.js\n// This file gets injected into the browser as a part of the HMR runtime\n\nfunction debounce(fn, delay) {\n  let handle;\n  return () => {\n    clearTimeout(handle);\n    handle = setTimeout(fn, delay);\n  };\n}\n\n/* eslint-disable no-undef */\nconst enqueueUpdate = debounce(async () => {\n  let manifest;\n  if (routeUpdates.size > 0) {\n    manifest = JSON.parse(JSON.stringify(__reactRouterManifest));\n\n    for (let route of routeUpdates.values()) {\n      manifest.routes[route.id] = route;\n      let imported = window.__reactRouterRouteModuleUpdates.get(route.id);\n      if (!imported) {\n        throw Error(\n          `[react-router:hmr] No module update found for route ${route.id}`,\n        );\n      }\n      let routeModule = {\n        ...imported,\n        // react-refresh takes care of updating these in-place,\n        // if we don't preserve existing values we'll loose state.\n        default: imported.default\n          ? (window.__reactRouterRouteModules[route.id]?.default ??\n            imported.default)\n          : imported.default,\n        ErrorBoundary: imported.ErrorBoundary\n          ? (window.__reactRouterRouteModules[route.id]?.ErrorBoundary ??\n            imported.ErrorBoundary)\n          : imported.ErrorBoundary,\n        HydrateFallback: imported.HydrateFallback\n          ? (window.__reactRouterRouteModules[route.id]?.HydrateFallback ??\n            imported.HydrateFallback)\n          : imported.HydrateFallback,\n      };\n      window.__reactRouterRouteModules[route.id] = routeModule;\n    }\n\n    let needsRevalidation = new Set(\n      Array.from(routeUpdates.values())\n        .filter(\n          (route) =>\n            route.hasLoader ||\n            route.hasClientLoader ||\n            route.hasClientMiddleware,\n        )\n        .map((route) => route.id),\n    );\n\n    let routes = __reactRouterDataRouter.createRoutesForHMR(\n      needsRevalidation,\n      manifest.routes,\n      window.__reactRouterRouteModules,\n      window.__reactRouterContext.ssr,\n      window.__reactRouterContext.isSpaMode,\n    );\n    __reactRouterDataRouter._internalSetRoutes(routes);\n    routeUpdates.clear();\n    window.__reactRouterRouteModuleUpdates.clear();\n  }\n\n  try {\n    window.__reactRouterHdrActive = true;\n    await __reactRouterDataRouter.revalidate();\n  } finally {\n    window.__reactRouterHdrActive = false;\n  }\n\n  if (manifest) {\n    Object.assign(window.__reactRouterManifest, manifest);\n  }\n  exports.performReactRefresh();\n}, 16);\n\n// Taken from https://github.com/pmmmwh/react-refresh-webpack-plugin/blob/main/lib/runtime/RefreshUtils.js#L141\n// This allows to resister components not detected by SWC like styled component\nfunction registerExportsForReactRefresh(filename, moduleExports) {\n  for (let key in moduleExports) {\n    if (key === \"__esModule\") continue;\n    let exportValue = moduleExports[key];\n    if (exports.isLikelyComponentType(exportValue)) {\n      // 'export' is required to avoid key collision when renamed exports that\n      // shadow a local component name: https://github.com/vitejs/vite-plugin-react/issues/116\n      // The register function has an identity check to not register twice the same component,\n      // so this is safe to not used the same key here.\n      exports.register(exportValue, filename + \" export \" + key);\n    }\n  }\n}\n\nfunction validateRefreshBoundaryAndEnqueueUpdate(\n  prevExports,\n  nextExports,\n  // non-component exports that are handled by the framework (e.g. `meta` and `links` for route modules)\n  acceptExports = [],\n) {\n  if (\n    !predicateOnExport(\n      prevExports,\n      (key) => key in nextExports || acceptExports.includes(key),\n    )\n  ) {\n    return \"Could not Fast Refresh (export removed)\";\n  }\n  if (\n    !predicateOnExport(\n      nextExports,\n      (key) => key in prevExports || acceptExports.includes(key),\n    )\n  ) {\n    return \"Could not Fast Refresh (new export)\";\n  }\n\n  let hasExports = false;\n  let allExportsAreHandledOrUnchanged = predicateOnExport(\n    nextExports,\n    (key, value) => {\n      hasExports = true;\n      // Remix can handle Remix-specific exports (e.g. `meta` and `links`)\n      if (acceptExports.includes(key)) return true;\n      // React Fast Refresh can handle component exports\n      if (exports.isLikelyComponentType(value)) return true;\n      // Unchanged exports are implicitly handled\n      return prevExports[key] === nextExports[key];\n    },\n  );\n  if (hasExports && allExportsAreHandledOrUnchanged) {\n    enqueueUpdate();\n  } else {\n    return \"Could not Fast Refresh. Learn more at https://github.com/vitejs/vite-plugin-react/tree/main/packages/plugin-react#consistent-components-exports\";\n  }\n}\n\nfunction predicateOnExport(moduleExports, predicate) {\n  for (let key in moduleExports) {\n    if (key === \"__esModule\") continue;\n    let desc = Object.getOwnPropertyDescriptor(moduleExports, key);\n    if (desc && desc.get) return false;\n    if (!predicate(key, moduleExports[key])) return false;\n  }\n  return true;\n}\n\n// Hides vite-ignored dynamic import so that Vite can skip analysis if no other\n// dynamic import is present (https://github.com/vitejs/vite/pull/12732)\nfunction __hmr_import(module) {\n  return import(/* @vite-ignore */ module);\n}\n\nconst routeUpdates = new Map();\nwindow.__reactRouterRouteModuleUpdates = new Map();\n\nimport.meta.hot.on(\"react-router:hmr\", async ({ route }) => {\n  if (route) {\n    routeUpdates.set(route.id, route);\n  }\n});\n\nexports.__hmr_import = __hmr_import;\nexports.registerExportsForReactRefresh = registerExportsForReactRefresh;\nexports.validateRefreshBoundaryAndEnqueueUpdate =\n  validateRefreshBoundaryAndEnqueueUpdate;\nexports.enqueueUpdate = enqueueUpdate;\n"
  },
  {
    "path": "packages/react-router-dev/vite/static/rsc-refresh-utils.mjs",
    "content": "// adapted from https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/src/refreshUtils.js\n// This file gets injected into the browser as a part of the HMR runtime\n\nfunction debounce(fn, delay) {\n  let handle;\n  return () => {\n    clearTimeout(handle);\n    handle = setTimeout(fn, delay);\n  };\n}\n\n/* eslint-disable no-undef */\nconst enqueueUpdate = debounce(async () => {\n  if (routeUpdates.size > 0) {\n    const routeUpdateByRouteId = new Map();\n    for (const routeUpdate of routeUpdates) {\n      const routeId = routeUpdate.routeId;\n      const routeModule = window.__reactRouterRouteModuleUpdates.get(routeId);\n      if (routeModule) {\n        routeUpdateByRouteId.set(routeId, { routeModule, ...routeUpdate });\n      }\n    }\n    routeUpdates.clear();\n    __reactRouterDataRouter._updateRoutesForHMR(routeUpdateByRouteId);\n  }\n\n  try {\n    window.__reactRouterHdrActive = true;\n    await __reactRouterDataRouter.revalidate();\n  } finally {\n    window.__reactRouterHdrActive = false;\n  }\n\n  exports.performReactRefresh();\n}, 16);\n\n// Taken from https://github.com/pmmmwh/react-refresh-webpack-plugin/blob/main/lib/runtime/RefreshUtils.js#L141\n// This allows to resister components not detected by SWC like styled component\nfunction registerExportsForReactRefresh(filename, moduleExports) {\n  for (const key in moduleExports) {\n    if (key === \"__esModule\") continue;\n    const exportValue = moduleExports[key];\n    if (exports.isLikelyComponentType(exportValue)) {\n      // 'export' is required to avoid key collision when renamed exports that\n      // shadow a local component name: https://github.com/vitejs/vite-plugin-react/issues/116\n      // The register function has an identity check to not register twice the same component,\n      // so this is safe to not used the same key here.\n      exports.register(exportValue, filename + \" export \" + key);\n    }\n  }\n}\n\nfunction validateRefreshBoundaryAndEnqueueUpdate(\n  prevExports,\n  nextExports,\n  // non-component exports that are handled by the framework (e.g. `meta` and `links` for route modules)\n  acceptExports = [],\n) {\n  if (\n    !predicateOnExport(\n      prevExports,\n      (key) => key in nextExports || acceptExports.includes(key),\n    )\n  ) {\n    return \"Could not Fast Refresh (export removed)\";\n  }\n  if (\n    !predicateOnExport(\n      nextExports,\n      (key) => key in prevExports || acceptExports.includes(key),\n    )\n  ) {\n    return \"Could not Fast Refresh (new export)\";\n  }\n\n  let hasExports = false;\n  const allExportsAreHandledOrUnchanged = predicateOnExport(\n    nextExports,\n    (key, value) => {\n      hasExports = true;\n      // React Router can handle additional exports (e.g. `meta` and `links`)\n      if (acceptExports.includes(key)) return true;\n      // React Fast Refresh can handle component exports\n      if (exports.isLikelyComponentType(value)) return true;\n      // Unchanged exports are implicitly handled\n      return prevExports[key] === nextExports[key];\n    },\n  );\n  if (hasExports && allExportsAreHandledOrUnchanged) {\n    enqueueUpdate();\n  } else {\n    return \"Could not Fast Refresh. Learn more at https://github.com/vitejs/vite-plugin-react/tree/main/packages/plugin-react#consistent-components-exports\";\n  }\n}\n\nfunction predicateOnExport(moduleExports, predicate) {\n  for (const key in moduleExports) {\n    if (key === \"__esModule\") continue;\n    const desc = Object.getOwnPropertyDescriptor(moduleExports, key);\n    if (desc && desc.get) return false;\n    if (!predicate(key, moduleExports[key])) return false;\n  }\n  return true;\n}\n\n// Hides vite-ignored dynamic import so that Vite can skip analysis if no other\n// dynamic import is present (https://github.com/vitejs/vite/pull/12732)\nfunction __hmr_import(module) {\n  return import(/* @vite-ignore */ module);\n}\n\nconst routeUpdates = new Set();\nwindow.__reactRouterRouteModuleUpdates = new Map();\n\nimport.meta.hot.on(\"react-router:hmr\", async (routeUpdate) => {\n  routeUpdates.add(routeUpdate);\n  if (routeUpdate.isServerOnlyChange) {\n    enqueueUpdate();\n  }\n});\n\nexports.__hmr_import = __hmr_import;\nexports.registerExportsForReactRefresh = registerExportsForReactRefresh;\nexports.validateRefreshBoundaryAndEnqueueUpdate =\n  validateRefreshBoundaryAndEnqueueUpdate;\nexports.enqueueUpdate = enqueueUpdate;\n"
  },
  {
    "path": "packages/react-router-dev/vite/styles.ts",
    "content": "import * as path from \"node:path\";\nimport { matchRoutes } from \"react-router\";\nimport type { ModuleNode, ViteDevServer } from \"vite\";\n\nimport type { ResolvedReactRouterConfig } from \"../config/config\";\nimport type { RouteManifest, RouteManifestEntry } from \"../config/routes\";\nimport type { LoadCssContents } from \"./plugin\";\nimport { resolveFileUrl } from \"./resolve-file-url\";\nimport * as babel from \"./babel\";\n\n// Style collection logic adapted from solid-start: https://github.com/solidjs/solid-start\n\n// Vite doesn't expose these so we just copy the list for now\n// https://github.com/vitejs/vite/blob/d6bde8b03d433778aaed62afc2be0630c8131908/packages/vite/src/node/constants.ts#L49C23-L50\nconst cssFileRegExp =\n  /\\.(css|less|sass|scss|styl|stylus|pcss|postcss|sss)(?:$|\\?)/;\n// https://github.com/vitejs/vite/blob/d6bde8b03d433778aaed62afc2be0630c8131908/packages/vite/src/node/plugins/css.ts#L160\nconst cssModulesRegExp = new RegExp(`\\\\.module${cssFileRegExp.source}`);\n\nconst isCssFile = (file: string) => cssFileRegExp.test(file);\nexport const isCssModulesFile = (file: string) => cssModulesRegExp.test(file);\n\n// https://vitejs.dev/guide/features#disabling-css-injection-into-the-page\n// https://github.com/vitejs/vite/blob/561b940f6f963fbb78058a6e23b4adad53a2edb9/packages/vite/src/node/plugins/css.ts#L194\n// https://vitejs.dev/guide/features#static-assets\n// https://github.com/vitejs/vite/blob/561b940f6f963fbb78058a6e23b4adad53a2edb9/packages/vite/src/node/utils.ts#L309-L310\nconst cssUrlParamsWithoutSideEffects = [\"url\", \"inline\", \"raw\", \"inline-css\"];\nexport const isCssUrlWithoutSideEffects = (url: string) => {\n  let queryString = url.split(\"?\")[1];\n\n  if (!queryString) {\n    return false;\n  }\n\n  let params = new URLSearchParams(queryString);\n  for (let paramWithoutSideEffects of cssUrlParamsWithoutSideEffects) {\n    if (\n      // Parameter is blank and not explicitly set, i.e. \"?url\", not \"?url=\"\n      params.get(paramWithoutSideEffects) === \"\" &&\n      !url.includes(`?${paramWithoutSideEffects}=`) &&\n      !url.includes(`&${paramWithoutSideEffects}=`)\n    ) {\n      return true;\n    }\n  }\n\n  return false;\n};\n\nconst getStylesForFiles = async ({\n  viteDevServer,\n  rootDirectory,\n  loadCssContents,\n  files,\n}: {\n  viteDevServer: ViteDevServer;\n  rootDirectory: string;\n  loadCssContents: LoadCssContents;\n  files: string[];\n}): Promise<string | undefined> => {\n  let styles: Record<string, string> = {};\n  let deps = new Set<ModuleNode>();\n\n  try {\n    for (let file of files) {\n      let normalizedPath = path\n        .resolve(rootDirectory, file)\n        .replace(/\\\\/g, \"/\");\n      let node = await viteDevServer.moduleGraph.getModuleById(normalizedPath);\n\n      // If the module is only present in the client module graph, the module\n      // won't have been found on the first request to the server. If so, we\n      // request the module so it's in the module graph, then try again.\n      if (!node) {\n        try {\n          await viteDevServer.transformRequest(\n            resolveFileUrl({ rootDirectory }, normalizedPath),\n          );\n        } catch (err) {\n          console.error(err);\n        }\n        node = await viteDevServer.moduleGraph.getModuleById(normalizedPath);\n      }\n\n      if (!node) {\n        console.log(`Could not resolve module for file: ${file}`);\n        continue;\n      }\n\n      await findDeps(viteDevServer, node, deps);\n    }\n  } catch (err) {\n    console.error(err);\n  }\n\n  for (let dep of deps) {\n    if (\n      dep.file &&\n      isCssFile(dep.file) &&\n      !isCssUrlWithoutSideEffects(dep.url) // Ignore styles that resolved as URLs, inline or raw. These shouldn't get injected.\n    ) {\n      try {\n        styles[dep.url] = await loadCssContents(viteDevServer, dep);\n      } catch {\n        console.warn(`Failed to load CSS for ${dep.file}`);\n        // this can happen with dynamically imported modules, I think\n        // because the Vite module graph doesn't distinguish between\n        // static and dynamic imports? TODO investigate, submit fix\n      }\n    }\n  }\n\n  return (\n    Object.entries(styles)\n      .map(([fileName, css], i) => [\n        `\\n/* ${fileName\n          // Escape comment syntax in file paths\n          .replace(/\\/\\*/g, \"/\\\\*\")\n          .replace(/\\*\\//g, \"*\\\\/\")} */`,\n        css,\n      ])\n      .flat()\n      .join(\"\\n\") || undefined\n  );\n};\n\nconst findDeps = async (\n  vite: ViteDevServer,\n  node: ModuleNode,\n  deps: Set<ModuleNode>,\n) => {\n  // since `ssrTransformResult.deps` contains URLs instead of `ModuleNode`s, this process is asynchronous.\n  // instead of using `await`, we resolve all branches in parallel.\n  let branches: Promise<void>[] = [];\n\n  async function addFromNode(node: ModuleNode) {\n    if (!deps.has(node)) {\n      deps.add(node);\n      await findDeps(vite, node, deps);\n    }\n  }\n\n  async function addFromUrl(url: string) {\n    let node = await vite.moduleGraph.getModuleByUrl(url);\n\n    if (node) {\n      await addFromNode(node);\n    }\n  }\n\n  if (node.ssrTransformResult) {\n    if (node.ssrTransformResult.deps) {\n      node.ssrTransformResult.deps.forEach((url) =>\n        branches.push(addFromUrl(url)),\n      );\n    }\n  } else {\n    node.importedModules.forEach((node) => branches.push(addFromNode(node)));\n  }\n\n  await Promise.all(branches);\n};\n\nconst groupRoutesByParentId = (manifest: RouteManifest) => {\n  let routes: Record<string, Array<RouteManifestEntry>> = {};\n\n  Object.values(manifest).forEach((route) => {\n    if (route) {\n      let parentId = route.parentId || \"\";\n      if (!routes[parentId]) {\n        routes[parentId] = [];\n      }\n      routes[parentId].push(route);\n    }\n  });\n\n  return routes;\n};\n\ntype RouteManifestEntryWithChildren = Omit<RouteManifestEntry, \"index\"> &\n  (\n    | { index?: false | undefined; children: RouteManifestEntryWithChildren[] }\n    | { index: true; children?: never }\n  );\n\nconst createRoutesWithChildren = (\n  manifest: RouteManifest,\n  parentId: string = \"\",\n  routesByParentId = groupRoutesByParentId(manifest),\n): RouteManifestEntryWithChildren[] => {\n  return (routesByParentId[parentId] || []).map((route) => ({\n    ...route,\n    ...(route.index\n      ? {\n          index: true,\n        }\n      : {\n          index: false,\n          children: createRoutesWithChildren(\n            manifest,\n            route.id,\n            routesByParentId,\n          ),\n        }),\n  }));\n};\n\nexport const getStylesForPathname = async ({\n  viteDevServer,\n  rootDirectory,\n  reactRouterConfig,\n  entryClientFilePath,\n  loadCssContents,\n  pathname,\n}: {\n  viteDevServer: ViteDevServer;\n  rootDirectory: string;\n  reactRouterConfig: Pick<\n    ResolvedReactRouterConfig,\n    \"appDirectory\" | \"routes\" | \"basename\"\n  >;\n  entryClientFilePath: string;\n  loadCssContents: LoadCssContents;\n  pathname: string | undefined;\n}): Promise<string | undefined> => {\n  if (pathname === undefined || pathname.includes(\"?_data=\")) {\n    return undefined;\n  }\n\n  let routesWithChildren = createRoutesWithChildren(reactRouterConfig.routes);\n  let appPath = path.relative(process.cwd(), reactRouterConfig.appDirectory);\n  let documentRouteFiles =\n    matchRoutes(routesWithChildren, pathname, reactRouterConfig.basename)?.map(\n      (match) =>\n        path.resolve(appPath, reactRouterConfig.routes[match.route.id].file),\n    ) ?? [];\n\n  let styles = await getStylesForFiles({\n    viteDevServer,\n    rootDirectory,\n    loadCssContents,\n    files: [\n      // Always include the client entry file when crawling the module graph for CSS\n      path.relative(rootDirectory, entryClientFilePath),\n      // Then include any styles from the matched routes\n      ...documentRouteFiles,\n    ],\n  });\n\n  return styles;\n};\n\nexport const getCssStringFromViteDevModuleCode = (\n  code: string,\n): string | undefined => {\n  let cssContent = undefined;\n\n  const ast = babel.parse(code, { sourceType: \"module\" });\n  babel.traverse(ast, {\n    VariableDeclaration(path) {\n      const declaration = path.node.declarations[0];\n      if (\n        declaration?.id?.type === \"Identifier\" &&\n        declaration.id.name === \"__vite__css\" &&\n        declaration.init?.type === \"StringLiteral\"\n      ) {\n        cssContent = declaration.init.value;\n        path.stop();\n      }\n    },\n  });\n\n  return cssContent;\n};\n"
  },
  {
    "path": "packages/react-router-dev/vite/virtual-module.ts",
    "content": "export function create(name: string) {\n  let id = `virtual:react-router/${name}`;\n  return {\n    id,\n    resolvedId: `\\0${id}`,\n    url: `/@id/__x00__${id}`,\n  };\n}\n"
  },
  {
    "path": "packages/react-router-dev/vite/vite-node.ts",
    "content": "// We can only import types from vite-node at the top level since we're in a CJS\n// context but want to use vite-node's ESM build since Vite 7+ is ESM only\nimport type { ViteNodeServer as ViteNodeServerType } from \"vite-node/server\";\nimport type { ViteNodeRunner as ViteNodeRunnerType } from \"vite-node/client\";\nimport type * as Vite from \"vite\";\n\nimport { preloadVite, getVite } from \"./vite\";\nimport { ssrExternals } from \"./ssr-externals\";\n\nexport type Context = {\n  devServer: Vite.ViteDevServer;\n  server: ViteNodeServerType;\n  runner: ViteNodeRunnerType;\n};\n\nexport async function createContext({\n  root,\n  mode,\n  customLogger,\n}: {\n  root: Vite.UserConfig[\"root\"];\n  mode: Vite.ConfigEnv[\"mode\"];\n  customLogger: Vite.UserConfig[\"customLogger\"];\n}): Promise<Context> {\n  await preloadVite();\n  const vite = getVite();\n\n  // Ensure we're using the ESM build of vite-node since Vite 7+ is ESM only\n  const [{ ViteNodeServer }, { ViteNodeRunner }, { installSourcemapsSupport }] =\n    await Promise.all([\n      import(\"vite-node/server\"),\n      import(\"vite-node/client\"),\n      import(\"vite-node/source-map\"),\n    ]);\n\n  const devServer = await vite.createServer({\n    root,\n    mode,\n    customLogger,\n    server: {\n      preTransformRequests: false,\n      hmr: false,\n      watch: null,\n    },\n    ssr: {\n      external: ssrExternals,\n    },\n    optimizeDeps: {\n      noDiscovery: true,\n    },\n    css: {\n      // This empty PostCSS config object prevents the PostCSS config file from\n      // being loaded. We don't need it in a React Router config context, and\n      // there's also an issue in Vite 5 when using a .ts PostCSS config file in\n      // an ESM project: https://github.com/vitejs/vite/issues/15869. Consumers\n      // can work around this in their own Vite config file, but they can't\n      // configure this internal usage of vite-node.\n      postcss: {},\n    },\n    configFile: false,\n    envFile: false,\n    plugins: [],\n  });\n  await devServer.pluginContainer.buildStart({});\n\n  const server = new ViteNodeServer(devServer);\n\n  installSourcemapsSupport({\n    getSourceMap: (source) => server.getSourceMap(source),\n  });\n\n  const runner = new ViteNodeRunner({\n    root: devServer.config.root,\n    base: devServer.config.base,\n    fetchModule(id) {\n      return server.fetchModule(id);\n    },\n    resolveId(id, importer) {\n      return server.resolveId(id, importer);\n    },\n  });\n\n  return { devServer, server, runner };\n}\n"
  },
  {
    "path": "packages/react-router-dev/vite/vite.ts",
    "content": "import path from \"pathe\";\n\nimport invariant from \"../invariant\";\nimport { isReactRouterRepo } from \"../config/is-react-router-repo\";\n\n// eslint-disable-next-line @typescript-eslint/consistent-type-imports\ntype Vite = typeof import(\"vite\");\nlet vite: Vite | undefined;\n\nconst viteImportSpecifier = isReactRouterRepo()\n  ? // Support testing against different versions of Vite by ensuring that Vite\n    // is resolved from the current working directory when running within this\n    // repo. If we don't do this, Vite will always be imported relative to this\n    // file, which means that it will always resolve to Vite 6.\n    `file:///${path\n      .normalize(\n        require.resolve(\"vite/package.json\", { paths: [process.cwd()] }),\n      )\n      .replace(\"package.json\", \"dist/node/index.js\")}`\n  : \"vite\";\n\nexport async function preloadVite(): Promise<void> {\n  // Use a dynamic import to force Vite to use the ESM build. If we don't do\n  // this, Vite logs CJS deprecation warnings.\n  vite = await import(viteImportSpecifier);\n}\n\nexport function getVite(): Vite {\n  invariant(vite, \"getVite() called before preloadVite()\");\n  return vite;\n}\n"
  },
  {
    "path": "packages/react-router-dev/vite/with-props.ts",
    "content": "import type { Babel, NodePath, ParseResult } from \"./babel\";\nimport { traverse, t } from \"./babel\";\n\nconst namedComponentExports = [\"HydrateFallback\", \"ErrorBoundary\"] as const;\ntype NamedComponentExport = (typeof namedComponentExports)[number];\nfunction isNamedComponentExport(name: string): name is NamedComponentExport {\n  return namedComponentExports.includes(name as NamedComponentExport);\n}\n\ntype HocName =\n  | \"UNSAFE_withComponentProps\"\n  | \"UNSAFE_withHydrateFallbackProps\"\n  | \"UNSAFE_withErrorBoundaryProps\";\n\nexport const decorateComponentExportsWithProps = (\n  ast: ParseResult<Babel.File>,\n) => {\n  const hocs: Array<[string, Babel.Identifier]> = [];\n  function getHocUid(path: NodePath, hocName: HocName) {\n    const uid = path.scope.generateUidIdentifier(hocName);\n    hocs.push([hocName, uid]);\n    return uid;\n  }\n\n  traverse(ast, {\n    ExportDeclaration(path) {\n      if (path.isExportDefaultDeclaration()) {\n        const declaration = path.get(\"declaration\");\n        // prettier-ignore\n        const expr =\n          declaration.isExpression() ? declaration.node :\n          declaration.isFunctionDeclaration() ? toFunctionExpression(declaration.node) :\n          undefined\n        if (expr) {\n          const uid = getHocUid(path, \"UNSAFE_withComponentProps\");\n          declaration.replaceWith(t.callExpression(uid, [expr]));\n        }\n        return;\n      }\n\n      if (path.isExportNamedDeclaration()) {\n        const decl = path.get(\"declaration\");\n\n        if (decl.isVariableDeclaration()) {\n          decl.get(\"declarations\").forEach((varDeclarator) => {\n            const id = varDeclarator.get(\"id\");\n            const init = varDeclarator.get(\"init\");\n            const expr = init.node;\n            if (!expr) return;\n            if (!id.isIdentifier()) return;\n            const { name } = id.node;\n            if (!isNamedComponentExport(name)) return;\n            const uid = getHocUid(path, `UNSAFE_with${name}Props`);\n            init.replaceWith(t.callExpression(uid, [expr]));\n          });\n          return;\n        }\n\n        if (decl.isFunctionDeclaration()) {\n          const { id } = decl.node;\n          if (!id) return;\n          const { name } = id;\n          if (!isNamedComponentExport(name)) return;\n\n          const uid = getHocUid(path, `UNSAFE_with${name}Props`);\n          decl.replaceWith(\n            t.variableDeclaration(\"const\", [\n              t.variableDeclarator(\n                t.identifier(name),\n                t.callExpression(uid, [toFunctionExpression(decl.node)]),\n              ),\n            ]),\n          );\n        }\n      }\n    },\n  });\n\n  if (hocs.length > 0) {\n    ast.program.body.unshift(\n      t.importDeclaration(\n        hocs.map(([name, identifier]) =>\n          t.importSpecifier(identifier, t.identifier(name)),\n        ),\n        t.stringLiteral(\"react-router\"),\n      ),\n    );\n  }\n};\n\nfunction toFunctionExpression(decl: Babel.FunctionDeclaration) {\n  return t.functionExpression(\n    decl.id,\n    decl.params,\n    decl.body,\n    decl.generator,\n    decl.async,\n  );\n}\n"
  },
  {
    "path": "packages/react-router-dev/vite.ts",
    "content": "export { reactRouterVitePlugin as reactRouter } from \"./vite/plugin\";\nexport { reactRouterRSCVitePlugin as unstable_reactRouterRSC } from \"./vite/rsc/plugin\";\n"
  },
  {
    "path": "packages/react-router-dom/CHANGELOG.md",
    "content": "# react-router-dom\n\n## 7.13.1\n\n### Patch Changes\n\n- Updated dependencies:\n  - `react-router@7.13.1`\n\n## 7.13.0\n\n### Patch Changes\n\n- Updated dependencies:\n  - `react-router@7.13.0`\n\n## 7.12.0\n\n### Patch Changes\n\n- Updated dependencies:\n  - `react-router@7.12.0`\n\n## 7.11.0\n\n### Patch Changes\n\n- Updated dependencies:\n  - `react-router@7.11.0`\n\n## 7.10.1\n\n### Patch Changes\n\n- Updated dependencies:\n  - `react-router@7.10.1`\n\n## 7.10.0\n\n### Patch Changes\n\n- Updated dependencies:\n  - `react-router@7.10.0`\n\n## 7.9.6\n\n### Patch Changes\n\n- Updated dependencies:\n  - `react-router@7.9.6`\n\n## 7.9.5\n\n### Patch Changes\n\n- Updated dependencies:\n  - `react-router@7.9.5`\n\n## 7.9.4\n\n### Patch Changes\n\n- Updated dependencies:\n  - `react-router@7.9.4`\n\n## 7.9.3\n\n### Patch Changes\n\n- Updated dependencies:\n  - `react-router@7.9.3`\n\n## 7.9.2\n\n### Patch Changes\n\n- Updated dependencies:\n  - `react-router@7.9.2`\n\n## 7.9.1\n\n### Patch Changes\n\n- Updated dependencies:\n  - `react-router@7.9.1`\n\n## 7.9.0\n\n### Patch Changes\n\n- Updated dependencies:\n  - `react-router@7.9.0`\n\n## 7.8.2\n\n### Patch Changes\n\n- Updated dependencies:\n  - `react-router@7.8.2`\n\n## 7.8.1\n\n### Patch Changes\n\n- Updated dependencies:\n  - `react-router@7.8.1`\n\n## 7.8.0\n\n### Patch Changes\n\n- Updated dependencies:\n  - `react-router@7.8.0`\n\n## 7.7.1\n\n### Patch Changes\n\n- Updated dependencies:\n  - `react-router@7.7.1`\n\n## 7.7.0\n\n### Patch Changes\n\n- Updated dependencies:\n  - `react-router@7.7.0`\n\n## 7.6.3\n\n### Patch Changes\n\n- Updated dependencies:\n  - `react-router@7.6.3`\n\n## 7.6.2\n\n### Patch Changes\n\n- Updated dependencies:\n  - `react-router@7.6.2`\n\n## 7.6.1\n\n### Patch Changes\n\n- Updated dependencies:\n  - `react-router@7.6.1`\n\n## 7.6.0\n\n### Patch Changes\n\n- Updated dependencies:\n  - `react-router@7.6.0`\n\n## 7.5.3\n\n### Patch Changes\n\n- Updated dependencies:\n  - `react-router@7.5.3`\n\n## 7.5.2\n\n### Patch Changes\n\n- Updated dependencies:\n  - `react-router@7.5.2`\n\n## 7.5.1\n\n### Patch Changes\n\n- Updated dependencies:\n  - `react-router@7.5.1`\n\n## 7.5.0\n\n### Patch Changes\n\n- Updated dependencies:\n  - `react-router@7.5.0`\n\n## 7.4.1\n\n### Patch Changes\n\n- Updated dependencies:\n  - `react-router@7.4.1`\n\n## 7.4.0\n\n### Patch Changes\n\n- Updated dependencies:\n  - `react-router@7.4.0`\n\n## 7.3.0\n\n### Patch Changes\n\n- Updated dependencies:\n  - `react-router@7.3.0`\n\n## 7.2.0\n\n### Patch Changes\n\n- Updated dependencies:\n  - `react-router@7.2.0`\n\n## 7.1.5\n\n### Patch Changes\n\n- Updated dependencies:\n  - `react-router@7.1.5`\n\n## 7.1.4\n\n### Patch Changes\n\n- Updated dependencies:\n  - `react-router@7.1.4`\n\n## 7.1.3\n\n### Patch Changes\n\n- Updated dependencies:\n  - `react-router@7.1.3`\n\n## 7.1.2\n\n### Patch Changes\n\n- Updated dependencies:\n  - `react-router@7.1.2`\n\n## 7.1.1\n\n### Patch Changes\n\n- Updated dependencies:\n  - `react-router@7.1.1`\n\n## 7.1.0\n\n### Patch Changes\n\n- Updated dependencies:\n  - `react-router@7.1.0`\n\n## 7.0.2\n\n### Patch Changes\n\n- Updated dependencies:\n  - `react-router@7.0.2`\n\n## 7.0.1\n\n### Patch Changes\n\n- Updated dependencies:\n  - `react-router@7.0.1`\n\n## 7.0.0\n\n### Major Changes\n\n- Remove the original `defer` implementation in favor of using raw promises via single fetch and `turbo-stream`. This removes these exports from React Router: ([#11744](https://github.com/remix-run/react-router/pull/11744))\n  - `defer`\n  - `AbortedDeferredError`\n  - `type TypedDeferredData`\n  - `UNSAFE_DeferredData`\n  - `UNSAFE_DEFERRED_SYMBOL`,\n\n- Use `createRemixRouter`/`RouterProvider` in `entry.client` instead of `RemixBrowser` ([#11469](https://github.com/remix-run/react-router/pull/11469))\n\n- Remove single fetch future flag. ([#11522](https://github.com/remix-run/react-router/pull/11522))\n\n- Remove `future.v7_startTransition` flag ([#11696](https://github.com/remix-run/react-router/pull/11696))\n\n- Remove `future.v7_normalizeFormMethod` future flag ([#11697](https://github.com/remix-run/react-router/pull/11697))\n\n- Allow returning `undefined` from actions and loaders ([#11680](https://github.com/remix-run/react-router/pull/11680))\n\n- update minimum node version to 18 ([#11690](https://github.com/remix-run/react-router/pull/11690))\n\n- Remove `future.v7_prependBasename` from the ionternalized `@remix-run/router` package ([#11726](https://github.com/remix-run/react-router/pull/11726))\n\n- Remove `future.v7_throwAbortReason` from internalized `@remix-run/router` package ([#11728](https://github.com/remix-run/react-router/pull/11728))\n\n- Add `exports` field to all packages ([#11675](https://github.com/remix-run/react-router/pull/11675))\n\n- node package no longer re-exports from react-router ([#11702](https://github.com/remix-run/react-router/pull/11702))\n\n- updates the minimum React version to 18 ([#11689](https://github.com/remix-run/react-router/pull/11689))\n\n- - Remove the `future.v7_partialHydration` flag ([#11725](https://github.com/remix-run/react-router/pull/11725))\n    - This also removes the `<RouterProvider fallbackElement>` prop\n      - To migrate, move the `fallbackElement` to a `hydrateFallbackElement`/`HydrateFallback` on your root route\n    - Also worth nothing there is a related breaking changer with this future flag:\n      - Without `future.v7_partialHydration` (when using `fallbackElement`), `state.navigation` was populated during the initial load\n      - With `future.v7_partialHydration`, `state.navigation` remains in an `\"idle\"` state during the initial load\n\n- Remove `future.v7_fetcherPersist` flag ([#11731](https://github.com/remix-run/react-router/pull/11731))\n\n### Minor Changes\n\n- Add prefetching support to `Link`/`NavLink` when using Remix SSR ([#11402](https://github.com/remix-run/react-router/pull/11402))\n- Enhance `ScrollRestoration` so it can restore properly on an SSR'd document load ([#11401](https://github.com/remix-run/react-router/pull/11401))\n- Add built-in Remix-style hydration support to `RouterProvider`. When running from a Remix-SSR'd HTML payload with the proper `window` variables (`__remixContext`, `__remixManifest`, `__remixRouteModules`), you don't need to pass a `router` prop and `RouterProvider` will create the `router` for you internally. ([#11396](https://github.com/remix-run/react-router/pull/11396)) ([#11400](https://github.com/remix-run/react-router/pull/11400))\n\n### Patch Changes\n\n- Memoize some `RouterProvider` internals to reduce uneccesary re-renders ([#11817](https://github.com/remix-run/react-router/pull/11817))\n- Updated dependencies:\n  - `react-router@7.0.0`\n"
  },
  {
    "path": "packages/react-router-dom/README.md",
    "content": "This package simply re-exports everything from `react-router` to smooth the upgrade path for v6 applications. Once upgraded you can change all of your imports and remove it from your dependencies:\n\n```diff\n-import { Routes } from \"react-router-dom\"\n+import { Routes } from \"react-router\"\n```\n"
  },
  {
    "path": "packages/react-router-dom/index.ts",
    "content": "import type { RouterProviderProps } from \"react-router/dom\";\nimport { HydratedRouter, RouterProvider } from \"react-router/dom\";\n\n// TODO: Confirm if this causes tree-shaking issues and if so, convert to named exports\nexport type * from \"react-router\";\nexport * from \"react-router\";\n\nexport type { RouterProviderProps };\nexport { HydratedRouter, RouterProvider };\n"
  },
  {
    "path": "packages/react-router-dom/package.json",
    "content": "{\n  \"name\": \"react-router-dom\",\n  \"version\": \"7.13.1\",\n  \"description\": \"Declarative routing for React web applications\",\n  \"keywords\": [\n    \"react\",\n    \"router\",\n    \"route\",\n    \"routing\",\n    \"history\",\n    \"link\"\n  ],\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"https://github.com/remix-run/react-router\",\n    \"directory\": \"packages/react-router-dom\"\n  },\n  \"license\": \"MIT\",\n  \"author\": \"Remix Software <hello@remix.run>\",\n  \"sideEffects\": false,\n  \"main\": \"./dist/main.js\",\n  \"unpkg\": \"./dist/umd/react-router-dom.production.min.js\",\n  \"module\": \"./dist/index.mjs\",\n  \"types\": \"./dist/index.d.ts\",\n  \"exports\": {\n    \".\": {\n      \"node\": {\n        \"types\": \"./dist/index.d.ts\",\n        \"module-sync\": \"./dist/index.mjs\",\n        \"default\": \"./dist/index.js\"\n      },\n      \"import\": {\n        \"types\": \"./dist/index.d.mts\",\n        \"default\": \"./dist/index.mjs\"\n      },\n      \"default\": {\n        \"types\": \"./dist/index.d.ts\",\n        \"default\": \"./dist/index.js\"\n      }\n    },\n    \"./package.json\": \"./package.json\"\n  },\n  \"scripts\": {\n    \"build\": \"wireit\",\n    \"typecheck\": \"tsc\"\n  },\n  \"wireit\": {\n    \"build\": {\n      \"command\": \"tsup\",\n      \"files\": [\n        \"../../pnpm-workspace.yaml\",\n        \"*.ts\",\n        \"tsconfig.json\",\n        \"package.json\"\n      ],\n      \"output\": [\n        \"dist/**\"\n      ]\n    }\n  },\n  \"dependencies\": {\n    \"react-router\": \"workspace:*\"\n  },\n  \"devDependencies\": {\n    \"react\": \"catalog:\",\n    \"react-dom\": \"catalog:\",\n    \"tsup\": \"catalog:\",\n    \"typescript\": \"catalog:\",\n    \"wireit\": \"catalog:\"\n  },\n  \"peerDependencies\": {\n    \"react\": \">=18\",\n    \"react-dom\": \">=18\"\n  },\n  \"files\": [\n    \"dist/\",\n    \"LICENSE.md\",\n    \"README.md\"\n  ],\n  \"engines\": {\n    \"node\": \">=20.0.0\"\n  }\n}\n"
  },
  {
    "path": "packages/react-router-dom/tsconfig.json",
    "content": "{\n  \"include\": [\"index.ts\"],\n  \"compilerOptions\": {\n    \"lib\": [\"ES2020\", \"DOM\", \"DOM.Iterable\"],\n    \"target\": \"ES2020\",\n    \"module\": \"Node16\",\n    \"moduleResolution\": \"Node16\",\n\n    \"strict\": true,\n    \"jsx\": \"react\",\n\n    \"declaration\": true,\n    \"emitDeclarationOnly\": true,\n\n    \"skipLibCheck\": true,\n\n    \"outDir\": \"dist\",\n    \"rootDir\": \".\"\n  }\n}\n"
  },
  {
    "path": "packages/react-router-dom/tsup.config.ts",
    "content": "import { defineConfig } from \"tsup\";\n\n// @ts-ignore - out of scope\nimport { createBanner } from \"../../build.utils.js\";\n\nimport pkg from \"./package.json\";\n\nconst entry = [\"index.ts\"];\n\nexport default defineConfig([\n  {\n    clean: true,\n    entry,\n    format: [\"cjs\", \"esm\"],\n    outDir: \"dist\",\n    dts: true,\n    banner: {\n      js: createBanner(pkg.name, pkg.version),\n    },\n  },\n]);\n"
  },
  {
    "path": "packages/react-router-express/CHANGELOG.md",
    "content": "# `@react-router/express`\n\n## 7.13.1\n\n### Patch Changes\n\n- Updated dependencies:\n  - `react-router@7.13.1`\n  - `@react-router/node@7.13.1`\n\n## 7.13.0\n\n### Patch Changes\n\n- Updated dependencies:\n  - `react-router@7.13.0`\n  - `@react-router/node@7.13.0`\n\n## 7.12.0\n\n### Patch Changes\n\n- Updated dependencies:\n  - `react-router@7.12.0`\n  - `@react-router/node@7.12.0`\n\n## 7.11.0\n\n### Patch Changes\n\n- Updated dependencies:\n  - `react-router@7.11.0`\n  - `@react-router/node@7.11.0`\n\n## 7.10.1\n\n### Patch Changes\n\n- Updated dependencies:\n  - `react-router@7.10.1`\n  - `@react-router/node@7.10.1`\n\n## 7.10.0\n\n### Patch Changes\n\n- Updated dependencies:\n  - `react-router@7.10.0`\n  - `@react-router/node@7.10.0`\n\n## 7.9.6\n\n### Patch Changes\n\n- Updated dependencies:\n  - `react-router@7.9.6`\n  - `@react-router/node@7.9.6`\n\n## 7.9.5\n\n### Patch Changes\n\n- Updated dependencies:\n  - `react-router@7.9.5`\n  - `@react-router/node@7.9.5`\n\n## 7.9.4\n\n### Patch Changes\n\n- Updated dependencies:\n  - `react-router@7.9.4`\n  - `@react-router/node@7.9.4`\n\n## 7.9.3\n\n### Patch Changes\n\n- Updated dependencies:\n  - `react-router@7.9.3`\n  - `@react-router/node@7.9.3`\n\n## 7.9.2\n\n### Patch Changes\n\n- Updated dependencies:\n  - `react-router@7.9.2`\n  - `@react-router/node@7.9.2`\n\n## 7.9.1\n\n### Patch Changes\n\n- Updated dependencies:\n  - `react-router@7.9.1`\n  - `@react-router/node@7.9.1`\n\n## 7.9.0\n\n### Minor Changes\n\n- Stabilize middleware and context APIs. ([#14215](https://github.com/remix-run/react-router/pull/14215))\n\n  We have removed the `unstable_` prefix from the following APIs and they are now considered stable and ready for production use:\n  - [`RouterContextProvider`](https://reactrouter.com/api/utils/RouterContextProvider)\n  - [`createContext`](https://reactrouter.com/api/utils/createContext)\n  - `createBrowserRouter` [`getContext`](https://reactrouter.com/api/data-routers/createBrowserRouter#optsgetcontext) option\n  - `<HydratedRouter>` [`getContext`](https://reactrouter.com/api/framework-routers/HydratedRouter#getcontext) prop\n\n  Please see the [Middleware Docs](https://reactrouter.com/how-to/middleware), the [Middleware RFC](https://github.com/remix-run/remix/discussions/7642), and the [Client-side Context RFC](https://github.com/remix-run/react-router/discussions/9856) for more information.\n\n### Patch Changes\n\n- Updated dependencies:\n  - `react-router@7.9.0`\n  - `@react-router/node@7.9.0`\n\n## 7.8.2\n\n### Patch Changes\n\n- Updated dependencies:\n  - `react-router@7.8.2`\n  - `@react-router/node@7.8.2`\n\n## 7.8.1\n\n### Patch Changes\n\n- Updated dependencies:\n  - `react-router@7.8.1`\n  - `@react-router/node@7.8.1`\n\n## 7.8.0\n\n### Patch Changes\n\n- \\[UNSTABLE] Change `getLoadContext` signature (`type GetLoadContextFunction`) when `future.unstable_middleware` is enabled so that it returns an `unstable_RouterContextProvider` instance instead of a `Map` used to contruct the instance internally ([#14097](https://github.com/remix-run/react-router/pull/14097))\n  - This also removes the `type unstable_InitialContext` export\n  - ⚠️ This is a breaking change if you have adopted middleware and are using a custom server with a `getLoadContext` function\n\n- Updated dependencies:\n  - `react-router@7.8.0`\n  - `@react-router/node@7.8.0`\n\n## 7.7.1\n\n### Patch Changes\n\n- Updated dependencies:\n  - `react-router@7.7.1`\n  - `@react-router/node@7.7.1`\n\n## 7.7.0\n\n### Patch Changes\n\n- Updated dependencies:\n  - `react-router@7.7.0`\n  - `@react-router/node@7.7.0`\n\n## 7.6.3\n\n### Patch Changes\n\n- Updated dependencies:\n  - `@react-router/node@7.6.3`\n  - `react-router@7.6.3`\n\n## 7.6.2\n\n### Patch Changes\n\n- Updated dependencies:\n  - `react-router@7.6.2`\n  - `@react-router/node@7.6.2`\n\n## 7.6.1\n\n### Patch Changes\n\n- Updated dependencies:\n  - `react-router@7.6.1`\n  - `@react-router/node@7.6.1`\n\n## 7.6.0\n\n### Patch Changes\n\n- Updated dependencies:\n  - `react-router@7.6.0`\n  - `@react-router/node@7.6.0`\n\n## 7.5.3\n\n### Patch Changes\n\n- Updated dependencies:\n  - `react-router@7.5.3`\n  - `@react-router/node@7.5.3`\n\n## 7.5.2\n\n### Patch Changes\n\n- Updated dependencies:\n  - `react-router@7.5.2`\n  - `@react-router/node@7.5.2`\n\n## 7.5.1\n\n### Patch Changes\n\n- Updated dependencies:\n  - `react-router@7.5.1`\n  - `@react-router/node@7.5.1`\n\n## 7.5.0\n\n### Patch Changes\n\n- Updated dependencies:\n  - `react-router@7.5.0`\n  - `@react-router/node@7.5.0`\n\n## 7.4.1\n\n### Patch Changes\n\n- Better validation of `x-forwarded-host` header to preent potential security issues. ([#13309](https://github.com/remix-run/react-router/pull/13309))\n- Updated dependencies:\n  - `react-router@7.4.1`\n  - `@react-router/node@7.4.1`\n\n## 7.4.0\n\n### Patch Changes\n\n- Updated dependencies:\n  - `react-router@7.4.0`\n  - `@react-router/node@7.4.0`\n\n## 7.3.0\n\n### Patch Changes\n\n- Update `express` `peerDependency` to include v5 (<https://github.com/remix-run/react-router/pull/13064>) ([#12961](https://github.com/remix-run/react-router/pull/12961))\n- Updated dependencies:\n  - `react-router@7.3.0`\n  - `@react-router/node@7.3.0`\n\n## 7.2.0\n\n### Patch Changes\n\n- Updated dependencies:\n  - `react-router@7.2.0`\n  - `@react-router/node@7.2.0`\n\n## 7.1.5\n\n### Patch Changes\n\n- Updated dependencies:\n  - `react-router@7.1.5`\n  - `@react-router/node@7.1.5`\n\n## 7.1.4\n\n### Patch Changes\n\n- Updated dependencies:\n  - `react-router@7.1.4`\n  - `@react-router/node@7.1.4`\n\n## 7.1.3\n\n### Patch Changes\n\n- Updated dependencies:\n  - `react-router@7.1.3`\n  - `@react-router/node@7.1.3`\n\n## 7.1.2\n\n### Patch Changes\n\n- Updated dependencies:\n  - `react-router@7.1.2`\n  - `@react-router/node@7.1.2`\n\n## 7.1.1\n\n### Patch Changes\n\n- Updated dependencies:\n  - `react-router@7.1.1`\n  - `@react-router/node@7.1.1`\n\n## 7.1.0\n\n### Patch Changes\n\n- Updated dependencies:\n  - `react-router@7.1.0`\n  - `@react-router/node@7.1.0`\n\n## 7.0.2\n\n### Patch Changes\n\n- Updated dependencies:\n  - `react-router@7.0.2`\n  - `@react-router/node@7.0.2`\n\n## 7.0.1\n\n### Patch Changes\n\n- Updated dependencies:\n  - `react-router@7.0.1`\n  - `@react-router/node@7.0.1`\n\n## 7.0.0\n\n### Major Changes\n\n- Remove single fetch future flag. ([#11522](https://github.com/remix-run/react-router/pull/11522))\n- update minimum node version to 18 ([#11690](https://github.com/remix-run/react-router/pull/11690))\n- Add `exports` field to all packages ([#11675](https://github.com/remix-run/react-router/pull/11675))\n- node package no longer re-exports from react-router ([#11702](https://github.com/remix-run/react-router/pull/11702))\n- Drop support for Node 18, update minimum Node vestion to 20 ([#12171](https://github.com/remix-run/react-router/pull/12171))\n  - Remove `installGlobals()` as this should no longer be necessary\n\n### Patch Changes\n\n- Updated dependencies:\n  - `react-router@7.0.0`\n  - `@react-router/node@7.0.0`\n\n## 2.9.0\n\n### Patch Changes\n\n- Updated dependencies:\n  - `@remix-run/node@2.9.0`\n\n## 2.8.1\n\n### Patch Changes\n\n- Updated dependencies:\n  - `@remix-run/node@2.8.1`\n\n## 2.8.0\n\n### Patch Changes\n\n- Updated dependencies:\n  - `@remix-run/node@2.8.0`\n\n## 2.7.2\n\n### Patch Changes\n\n- Updated dependencies:\n  - `@remix-run/node@2.7.2`\n\n## 2.7.1\n\n### Patch Changes\n\n- Updated dependencies:\n  - `@remix-run/node@2.7.1`\n\n## 2.7.0\n\n### Minor Changes\n\n- Vite: Add a new `basename` option to the Vite plugin, allowing users to set the internal React Router [`basename`](https://reactrouter.com/en/main/routers/create-browser-router#basename) in order to to serve their applications underneath a subpath ([#8145](https://github.com/remix-run/remix/pull/8145))\n\n### Patch Changes\n\n- Use `req.originalUrl` instead of `req.url` so that Remix sees the full URL ([#8145](https://github.com/remix-run/remix/pull/8145))\n  - Remix relies on the knowing the full URL to ensure that server and client code can function together, and does not support URL rewriting prior to the Remix handler\n\n- Updated dependencies:\n  - `@remix-run/node@2.7.0`\n\n## 2.6.0\n\n### Patch Changes\n\n- Updated dependencies:\n  - `@remix-run/node@2.6.0`\n\n## 2.5.1\n\n### Patch Changes\n\n- Updated dependencies:\n  - `@remix-run/node@2.5.1`\n\n## 2.5.0\n\n### Patch Changes\n\n- Updated dependencies:\n  - `@remix-run/node@2.5.0`\n\n## 2.4.1\n\n### Patch Changes\n\n- Updated dependencies:\n  - `@remix-run/node@2.4.1`\n\n## 2.4.0\n\n### Patch Changes\n\n- Updated dependencies:\n  - `@remix-run/node@2.4.0`\n\n## 2.3.1\n\n### Patch Changes\n\n- Updated dependencies:\n  - `@remix-run/node@2.3.1`\n\n## 2.3.0\n\n### Patch Changes\n\n- Fix flash of unstyled content on initial page load in Vite dev when using a custom Express server ([#7937](https://github.com/remix-run/remix/pull/7937))\n- Updated dependencies:\n  - `@remix-run/node@2.3.0`\n\n## 2.2.0\n\n### Patch Changes\n\n- Allow the `@remix-run/express` adapter to work behind a proxy when using `app.enable('trust proxy')` ([#7323](https://github.com/remix-run/remix/pull/7323))\n  - Previously, this used `req.get('host')` to construct the Remix `Request`, but that does not respect `X-Forwarded-Host`\n  - This now uses `req.hostname` which will respect `X-Forwarded-Host`\n- Updated dependencies:\n  - `@remix-run/node@2.2.0`\n\n## 2.1.0\n\n### Patch Changes\n\n- Flush headers for `text/event-stream` responses ([#7619](https://github.com/remix-run/remix/pull/7619))\n- Updated dependencies:\n  - `@remix-run/node@2.1.0`\n\n## 2.0.1\n\n### Patch Changes\n\n- Updated dependencies:\n  - `@remix-run/node@2.0.1`\n\n## 2.0.0\n\n### Major Changes\n\n- Require Node >=18.0.0 ([#6939](https://github.com/remix-run/remix/pull/6939))\n\n- For preparation of using Node's built in fetch implementation, installing the fetch globals is now a responsibility of the app server ([#7009](https://github.com/remix-run/remix/pull/7009))\n  - If you are using `remix-serve`, nothing is required\n  - If you are using your own app server, you will need to install the globals yourself\n\n    ```js filename=server.js\n    import { installGlobals } from \"@remix-run/node\";\n\n    installGlobals();\n    ```\n\n- `source-map-support` is now a responsibility of the app server ([#7009](https://github.com/remix-run/remix/pull/7009))\n  - If you are using `remix-serve`, nothing is required\n  - If you are using your own app server, you will need to install [`source-map-support`](https://www.npmjs.com/package/source-map-support) yourself.\n\n    ```sh\n    npm i source-map-support\n    ```\n\n    ```js filename=server.js\n    import sourceMapSupport from \"source-map-support\";\n    sourceMapSupport.install();\n    ```\n\n### Patch Changes\n\n- Switch to `headers.entries()` instead of non-spec-compliant `headers.raw()` in `sendRemixResponse` ([#7150](https://github.com/remix-run/remix/pull/7150))\n- Remove references to fetch polyfills in node and arc adapters ([#7230](https://github.com/remix-run/remix/pull/7230))\n- Updated dependencies:\n  - `@remix-run/node@2.0.0`\n  - `@remix-run/web-fetch@4.4.0`\n  - `@remix-run/web-file@3.1.0`\n  - `@remix-run/web-stream@1.1.0`\n\n## 1.19.3\n\n### Patch Changes\n\n- Updated dependencies:\n  - `@remix-run/node@1.19.3`\n\n## 1.19.2\n\n### Patch Changes\n\n- Updated dependencies:\n  - `@remix-run/node@1.19.2`\n\n## 1.19.1\n\n### Patch Changes\n\n- Updated dependencies:\n  - `@remix-run/node@1.19.1`\n\n## 1.19.0\n\n### Patch Changes\n\n- Updated dependencies:\n  - `@remix-run/node@1.19.0`\n\n## 1.18.1\n\n### Patch Changes\n\n- Updated dependencies:\n  - `@remix-run/node@1.18.1`\n\n## 1.18.0\n\n### Patch Changes\n\n- Updated dependencies:\n  - `@remix-run/node@1.18.0`\n\n## 1.17.1\n\n### Patch Changes\n\n- Updated dependencies:\n  - `@remix-run/node@1.17.1`\n\n## 1.17.0\n\n### Patch Changes\n\n- Updated dependencies:\n  - `@remix-run/node@1.17.0`\n\n## 1.16.1\n\n### Patch Changes\n\n- Updated dependencies:\n  - `@remix-run/node@1.16.1`\n\n## 1.16.0\n\n### Patch Changes\n\n- feat: support async `getLoadContext` in all adapters ([#6170](https://github.com/remix-run/remix/pull/6170))\n- Updated dependencies:\n  - `@remix-run/node@1.16.0`\n\n## 1.15.0\n\n### Patch Changes\n\n- Updated dependencies:\n  - `@remix-run/node@1.15.0`\n\n## 1.14.3\n\n### Patch Changes\n\n- Updated dependencies:\n  - `@remix-run/node@1.14.3`\n\n## 1.14.2\n\n### Patch Changes\n\n- Updated dependencies:\n  - `@remix-run/node@1.14.2`\n\n## 1.14.1\n\n### Patch Changes\n\n- Updated dependencies:\n  - `@remix-run/node@1.14.1`\n\n## 1.14.0\n\n### Patch Changes\n\n- Updated dependencies:\n  - `@remix-run/node@1.14.0`\n\n## 1.13.0\n\n### Patch Changes\n\n- Fix fetch `Request` creation for incoming URLs with double slashes ([#5336](https://github.com/remix-run/remix/pull/5336))\n- Updated dependencies:\n  - `@remix-run/node@1.13.0`\n\n## 1.12.0\n\n### Patch Changes\n\n- Updated dependencies:\n  - `@remix-run/node@1.12.0`\n\n## 1.11.1\n\n### Patch Changes\n\n- Updated dependencies:\n  - `@remix-run/node@1.11.1`\n\n## 1.11.0\n\n### Patch Changes\n\n- Updated dependencies:\n  - `@remix-run/node@1.11.0`\n\n## 1.10.1\n\n### Patch Changes\n\n- Updated dependencies:\n  - `@remix-run/node@1.10.1`\n\n## 1.10.0\n\n### Patch Changes\n\n- Updated dependencies:\n  - `@remix-run/node@1.10.0`\n\n## 1.9.0\n\n### Patch Changes\n\n- Updated dependencies:\n  - `@remix-run/node@1.9.0`\n\n## 1.8.2\n\n### Patch Changes\n\n- Updated dependencies:\n  - `@remix-run/node@1.8.2`\n\n## 1.8.1\n\n### Patch Changes\n\n- Updated dependencies:\n  - `@remix-run/node@1.8.1`\n\n## 1.8.0\n\n### Patch Changes\n\n- Updated dependencies:\n  - `@remix-run/node@1.8.0`\n\n## 1.7.6\n\n### Patch Changes\n\n- Updated dependencies:\n  - `@remix-run/node@1.7.6`\n\n## 1.7.5\n\n### Patch Changes\n\n- Updated dependencies:\n  - `@remix-run/node@1.7.5`\n\n## 1.7.4\n\n### Patch Changes\n\n- Updated dependencies:\n  - `@remix-run/node@1.7.4`\n\n## 1.7.3\n\n### Patch Changes\n\n- Updated dependencies:\n  - `@remix-run/node@1.7.3`\n\n## 1.7.2\n\n### Patch Changes\n\n- Updated dependencies:\n  - `@remix-run/node@1.7.2`\n\n## 1.7.1\n\n### Patch Changes\n\n- Ensured that requests are properly aborted on closing of a `Response` instead of `Request` ([#3626](https://github.com/remix-run/remix/pull/3626))\n- Updated dependencies:\n  - `@remix-run/node@1.7.1`\n\n## 1.7.0\n\n### Patch Changes\n\n- Updated dependencies:\n  - `@remix-run/node@1.7.0`\n\n## 1.6.8\n\n### Patch Changes\n\n- Updated dependencies:\n  - `@remix-run/node@1.6.8`\n\n## 1.6.7\n\n### Patch Changes\n\n- Updated dependencies:\n  - `@remix-run/node@1.6.7`\n\n## 1.6.6\n\n### Patch Changes\n\n- Updated dependencies:\n  - `@remix-run/node@1.6.6`\n\n## 1.6.5\n\n### Patch Changes\n\n- Updated dependencies\n  - `@remix-run/node@1.6.5`\n"
  },
  {
    "path": "packages/react-router-express/README.md",
    "content": "[Express](https://expressjs.com) server request handler for React Router.\n\n```sh\nnpm install @react-router/express\n```\n"
  },
  {
    "path": "packages/react-router-express/__tests__/server-test.ts",
    "content": "import { Readable } from \"node:stream\";\nimport { createRequestHandler as createRemixRequestHandler } from \"react-router\";\nimport { createReadableStreamFromReadable } from \"@react-router/node\";\nimport express from \"express\";\nimport { createRequest, createResponse } from \"node-mocks-http\";\nimport supertest from \"supertest\";\n\nimport {\n  createRemixHeaders,\n  createRemixRequest,\n  createRequestHandler,\n} from \"../server\";\n\n// We don't want to test that the remix server works here (that's what the\n// playwright tests do), we just want to test the express adapter\njest.mock(\"react-router\", () => {\n  let original = jest.requireActual(\"react-router\");\n  return {\n    ...original,\n    createRequestHandler: jest.fn(),\n  };\n});\nlet mockedCreateRequestHandler =\n  createRemixRequestHandler as jest.MockedFunction<\n    typeof createRemixRequestHandler\n  >;\n\nfunction createApp() {\n  let app = express();\n\n  app.all(\n    \"*\",\n    // We don't have a real app to test, but it doesn't matter. We won't ever\n    // call through to the real createRequestHandler\n    // @ts-expect-error\n    createRequestHandler({ build: {} }),\n  );\n\n  return app;\n}\n\ndescribe(\"express createRequestHandler\", () => {\n  describe(\"basic requests\", () => {\n    afterEach(() => {\n      mockedCreateRequestHandler.mockReset();\n    });\n\n    afterAll(() => {\n      jest.restoreAllMocks();\n    });\n\n    it(\"handles requests\", async () => {\n      mockedCreateRequestHandler.mockImplementation(() => async (req) => {\n        return new Response(`URL: ${new URL(req.url).pathname}`);\n      });\n\n      let request = supertest(createApp());\n      let res = await request.get(\"/foo/bar\");\n\n      expect(res.status).toBe(200);\n      expect(res.text).toBe(\"URL: /foo/bar\");\n      expect(res.headers[\"x-powered-by\"]).toBe(\"Express\");\n    });\n\n    it(\"handles root // URLs\", async () => {\n      mockedCreateRequestHandler.mockImplementation(() => async (req) => {\n        return new Response(\"URL: \" + new URL(req.url).pathname);\n      });\n\n      let request = supertest(createApp());\n      let res = await request.get(\"//\");\n\n      expect(res.status).toBe(200);\n      expect(res.text).toBe(\"URL: //\");\n    });\n\n    it(\"handles nested // URLs\", async () => {\n      mockedCreateRequestHandler.mockImplementation(() => async (req) => {\n        return new Response(\"URL: \" + new URL(req.url).pathname);\n      });\n\n      let request = supertest(createApp());\n      let res = await request.get(\"//foo//bar\");\n\n      expect(res.status).toBe(200);\n      expect(res.text).toBe(\"URL: //foo//bar\");\n    });\n\n    it(\"handles null body\", async () => {\n      mockedCreateRequestHandler.mockImplementation(() => async () => {\n        return new Response(null, { status: 200 });\n      });\n\n      let request = supertest(createApp());\n      let res = await request.get(\"/\");\n\n      expect(res.status).toBe(200);\n    });\n\n    // https://github.com/node-fetch/node-fetch/blob/4ae35388b078bddda238277142bf091898ce6fda/test/response.js#L142-L148\n    it(\"handles body as stream\", async () => {\n      mockedCreateRequestHandler.mockImplementation(() => async () => {\n        let readable = Readable.from(\"hello world\");\n        let stream = createReadableStreamFromReadable(readable);\n        return new Response(stream, { status: 200 });\n      });\n\n      let request = supertest(createApp());\n      let res = await request.get(\"/\");\n      expect(res.status).toBe(200);\n      expect(res.text).toBe(\"hello world\");\n    });\n\n    it(\"handles status codes\", async () => {\n      mockedCreateRequestHandler.mockImplementation(() => async () => {\n        return new Response(null, { status: 204 });\n      });\n\n      let request = supertest(createApp());\n      let res = await request.get(\"/\");\n\n      expect(res.status).toBe(204);\n    });\n\n    it(\"sets headers\", async () => {\n      mockedCreateRequestHandler.mockImplementation(() => async () => {\n        let headers = new Headers({ \"X-Time-Of-Year\": \"most wonderful\" });\n        headers.append(\n          \"Set-Cookie\",\n          \"first=one; Expires=0; Path=/; HttpOnly; Secure; SameSite=Lax\",\n        );\n        headers.append(\n          \"Set-Cookie\",\n          \"second=two; MaxAge=1209600; Path=/; HttpOnly; Secure; SameSite=Lax\",\n        );\n        headers.append(\n          \"Set-Cookie\",\n          \"third=three; Expires=Wed, 21 Oct 2015 07:28:00 GMT; Path=/; HttpOnly; Secure; SameSite=Lax\",\n        );\n        return new Response(null, { headers });\n      });\n\n      let request = supertest(createApp());\n      let res = await request.get(\"/\");\n\n      expect(res.headers[\"x-time-of-year\"]).toBe(\"most wonderful\");\n      expect(res.headers[\"set-cookie\"]).toEqual([\n        \"first=one; Expires=0; Path=/; HttpOnly; Secure; SameSite=Lax\",\n        \"second=two; MaxAge=1209600; Path=/; HttpOnly; Secure; SameSite=Lax\",\n        \"third=three; Expires=Wed, 21 Oct 2015 07:28:00 GMT; Path=/; HttpOnly; Secure; SameSite=Lax\",\n      ]);\n    });\n  });\n});\n\ndescribe(\"express createRemixHeaders\", () => {\n  describe(\"creates fetch headers from express headers\", () => {\n    it(\"handles empty headers\", () => {\n      let headers = createRemixHeaders({});\n      expect(Object.fromEntries(headers.entries())).toMatchInlineSnapshot(`{}`);\n    });\n\n    it(\"handles simple headers\", () => {\n      let headers = createRemixHeaders({ \"x-foo\": \"bar\" });\n      expect(headers.get(\"x-foo\")).toBe(\"bar\");\n    });\n\n    it(\"handles multiple headers\", () => {\n      let headers = createRemixHeaders({ \"x-foo\": \"bar\", \"x-bar\": \"baz\" });\n      expect(headers.get(\"x-foo\")).toBe(\"bar\");\n      expect(headers.get(\"x-bar\")).toBe(\"baz\");\n    });\n\n    it(\"handles headers with multiple values\", () => {\n      let headers = createRemixHeaders({\n        \"x-foo\": [\"bar\", \"baz\"],\n        \"x-bar\": \"baz\",\n      });\n      expect(headers.get(\"x-foo\")).toEqual(\"bar, baz\");\n      expect(headers.get(\"x-bar\")).toBe(\"baz\");\n    });\n\n    it(\"handles multiple set-cookie headers\", () => {\n      let headers = createRemixHeaders({\n        \"set-cookie\": [\n          \"__session=some_value; Path=/; Secure; HttpOnly; MaxAge=7200; SameSite=Lax\",\n          \"__other=some_other_value; Path=/; Secure; HttpOnly; Expires=Wed, 21 Oct 2015 07:28:00 GMT; SameSite=Lax\",\n        ],\n      });\n      expect(headers.getSetCookie()).toEqual([\n        \"__session=some_value; Path=/; Secure; HttpOnly; MaxAge=7200; SameSite=Lax\",\n        \"__other=some_other_value; Path=/; Secure; HttpOnly; Expires=Wed, 21 Oct 2015 07:28:00 GMT; SameSite=Lax\",\n      ]);\n    });\n  });\n});\n\ndescribe(\"express createRemixRequest\", () => {\n  it(\"creates a request with the correct headers\", async () => {\n    let expressRequest = createRequest({\n      url: \"/foo/bar\",\n      method: \"GET\",\n      protocol: \"http\",\n      hostname: \"localhost\",\n      headers: {\n        \"Cache-Control\": \"max-age=300, s-maxage=3600\",\n        Host: \"localhost:3000\",\n      },\n    });\n    let expressResponse = createResponse();\n\n    let remixRequest = createRemixRequest(expressRequest, expressResponse);\n\n    expect(remixRequest.method).toBe(\"GET\");\n    expect(remixRequest.headers.get(\"cache-control\")).toBe(\n      \"max-age=300, s-maxage=3600\",\n    );\n    expect(remixRequest.headers.get(\"host\")).toBe(\"localhost:3000\");\n  });\n\n  it(\"validates parsed port\", async () => {\n    let expressRequest = createRequest({\n      url: \"/foo/bar\",\n      method: \"GET\",\n      protocol: \"http\",\n      hostname: \"localhost\",\n      headers: {\n        \"Cache-Control\": \"max-age=300, s-maxage=3600\",\n        Host: \"localhost:3000\",\n        \"x-forwarded-host\": \":/spoofed\",\n      },\n    });\n    let expressResponse = createResponse();\n\n    let remixRequest = createRemixRequest(expressRequest, expressResponse);\n\n    expect(remixRequest.method).toBe(\"GET\");\n    expect(remixRequest.headers.get(\"cache-control\")).toBe(\n      \"max-age=300, s-maxage=3600\",\n    );\n    expect(remixRequest.headers.get(\"host\")).toBe(\"localhost:3000\");\n    expect(remixRequest.url).toBe(\"http://localhost:3000/foo/bar\");\n  });\n});\n"
  },
  {
    "path": "packages/react-router-express/index.ts",
    "content": "export type { GetLoadContextFunction, RequestHandler } from \"./server\";\nexport { createRequestHandler } from \"./server\";\n"
  },
  {
    "path": "packages/react-router-express/jest.config.js",
    "content": "/** @type {import('jest').Config} */\nmodule.exports = {\n  ...require(\"../../jest/jest.config.shared\"),\n  displayName: \"express\",\n};\n"
  },
  {
    "path": "packages/react-router-express/package.json",
    "content": "{\n  \"name\": \"@react-router/express\",\n  \"version\": \"7.13.1\",\n  \"description\": \"Express server request handler for React Router\",\n  \"bugs\": {\n    \"url\": \"https://github.com/remix-run/react-router/issues\"\n  },\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"https://github.com/remix-run/react-router\",\n    \"directory\": \"packages/react-router-express\"\n  },\n  \"license\": \"MIT\",\n  \"main\": \"dist/index.js\",\n  \"typings\": \"dist/index.d.ts\",\n  \"exports\": {\n    \".\": {\n      \"node\": {\n        \"types\": \"./dist/index.d.ts\",\n        \"module-sync\": \"./dist/index.mjs\",\n        \"default\": \"./dist/index.js\"\n      },\n      \"import\": {\n        \"types\": \"./dist/index.d.mts\",\n        \"default\": \"./dist/index.mjs\"\n      },\n      \"default\": {\n        \"types\": \"./dist/index.d.ts\",\n        \"default\": \"./dist/index.js\"\n      }\n    },\n    \"./package.json\": \"./package.json\"\n  },\n  \"scripts\": {\n    \"build\": \"wireit\",\n    \"typecheck\": \"tsc\"\n  },\n  \"wireit\": {\n    \"build\": {\n      \"command\": \"tsup\",\n      \"files\": [\n        \"../../pnpm-workspace.yaml\",\n        \"*.ts\",\n        \"tsconfig.json\",\n        \"package.json\"\n      ],\n      \"output\": [\n        \"dist/**\"\n      ]\n    }\n  },\n  \"dependencies\": {\n    \"@react-router/node\": \"workspace:*\"\n  },\n  \"devDependencies\": {\n    \"@types/express\": \"^4.17.9\",\n    \"@types/node\": \"^20.0.0\",\n    \"@types/supertest\": \"^2.0.10\",\n    \"express\": \"^4.19.2\",\n    \"node-mocks-http\": \"^1.10.1\",\n    \"supertest\": \"^6.3.3\",\n    \"tsup\": \"catalog:\",\n    \"typescript\": \"catalog:\",\n    \"wireit\": \"catalog:\"\n  },\n  \"peerDependencies\": {\n    \"express\": \"^4.17.1 || ^5\",\n    \"react-router\": \"workspace:*\",\n    \"typescript\": \"^5.1.0\"\n  },\n  \"peerDependenciesMeta\": {\n    \"typescript\": {\n      \"optional\": true\n    }\n  },\n  \"engines\": {\n    \"node\": \">=20.0.0\"\n  },\n  \"files\": [\n    \"dist/\",\n    \"CHANGELOG.md\",\n    \"LICENSE.md\",\n    \"README.md\"\n  ]\n}\n"
  },
  {
    "path": "packages/react-router-express/server.ts",
    "content": "// IDK why this is needed when it's in the tsconfig..........\n// YAY PROJECT REFERENCES!\n/// <reference lib=\"DOM.Iterable\" />\n\nimport type * as express from \"express\";\nimport type {\n  AppLoadContext,\n  ServerBuild,\n  UNSAFE_MiddlewareEnabled as MiddlewareEnabled,\n  RouterContextProvider,\n} from \"react-router\";\nimport { createRequestHandler as createRemixRequestHandler } from \"react-router\";\nimport {\n  createReadableStreamFromReadable,\n  writeReadableStreamToWritable,\n} from \"@react-router/node\";\n\ntype MaybePromise<T> = T | Promise<T>;\n\n/**\n * A function that returns the value to use as `context` in route `loader` and\n * `action` functions.\n *\n * You can think of this as an escape hatch that allows you to pass\n * environment/platform-specific values through to your loader/action, such as\n * values that are generated by Express middleware like `req.session`.\n */\nexport type GetLoadContextFunction = (\n  req: express.Request,\n  res: express.Response,\n) => MiddlewareEnabled extends true\n  ? MaybePromise<RouterContextProvider>\n  : MaybePromise<AppLoadContext>;\n\nexport type RequestHandler = (\n  req: express.Request,\n  res: express.Response,\n  next: express.NextFunction,\n) => Promise<void>;\n\n/**\n * Returns a request handler for Express that serves the response using Remix.\n */\nexport function createRequestHandler({\n  build,\n  getLoadContext,\n  mode = process.env.NODE_ENV,\n}: {\n  build: ServerBuild | (() => Promise<ServerBuild>);\n  getLoadContext?: GetLoadContextFunction;\n  mode?: string;\n}): RequestHandler {\n  let handleRequest = createRemixRequestHandler(build, mode);\n\n  return async (\n    req: express.Request,\n    res: express.Response,\n    next: express.NextFunction,\n  ) => {\n    try {\n      let request = createRemixRequest(req, res);\n      let loadContext = await getLoadContext?.(req, res);\n\n      let response = await handleRequest(request, loadContext);\n\n      await sendRemixResponse(res, response);\n    } catch (error: unknown) {\n      // Express doesn't support async functions, so we have to pass along the\n      // error manually using next().\n      next(error);\n    }\n  };\n}\n\nexport function createRemixHeaders(\n  requestHeaders: express.Request[\"headers\"],\n): Headers {\n  let headers = new Headers();\n\n  for (let [key, values] of Object.entries(requestHeaders)) {\n    if (values) {\n      if (Array.isArray(values)) {\n        for (let value of values) {\n          headers.append(key, value);\n        }\n      } else {\n        headers.set(key, values);\n      }\n    }\n  }\n\n  return headers;\n}\n\nexport function createRemixRequest(\n  req: express.Request,\n  res: express.Response,\n): Request {\n  // req.hostname doesn't include port information so grab that from\n  // `X-Forwarded-Host` or `Host`\n  let [, hostnamePortStr] = req.get(\"X-Forwarded-Host\")?.split(\":\") ?? [];\n  let [, hostPortStr] = req.get(\"host\")?.split(\":\") ?? [];\n  let hostnamePort = Number.parseInt(hostnamePortStr, 10);\n  let hostPort = Number.parseInt(hostPortStr, 10);\n  let port = Number.isSafeInteger(hostnamePort)\n    ? hostnamePort\n    : Number.isSafeInteger(hostPort)\n      ? hostPort\n      : \"\";\n  // Use req.hostname here as it respects the \"trust proxy\" setting\n  let resolvedHost = `${req.hostname}${port ? `:${port}` : \"\"}`;\n  // Use `req.originalUrl` so Remix is aware of the full path\n  let url = new URL(`${req.protocol}://${resolvedHost}${req.originalUrl}`);\n\n  // Abort action/loaders once we can no longer write a response\n  let controller: AbortController | null = new AbortController();\n  let init: RequestInit = {\n    method: req.method,\n    headers: createRemixHeaders(req.headers),\n    signal: controller.signal,\n  };\n\n  // Abort action/loaders once we can no longer write a response iff we have\n  // not yet sent a response (i.e., `close` without `finish`)\n  // `finish` -> done rendering the response\n  // `close` -> response can no longer be written to\n  res.on(\"finish\", () => (controller = null));\n  res.on(\"close\", () => controller?.abort());\n\n  if (req.method !== \"GET\" && req.method !== \"HEAD\") {\n    init.body = createReadableStreamFromReadable(req);\n    (init as { duplex: \"half\" }).duplex = \"half\";\n  }\n\n  return new Request(url.href, init);\n}\n\nexport async function sendRemixResponse(\n  res: express.Response,\n  nodeResponse: Response,\n): Promise<void> {\n  res.statusMessage = nodeResponse.statusText;\n  res.status(nodeResponse.status);\n\n  for (let [key, value] of nodeResponse.headers.entries()) {\n    res.append(key, value);\n  }\n\n  if (nodeResponse.headers.get(\"Content-Type\")?.match(/text\\/event-stream/i)) {\n    res.flushHeaders();\n  }\n\n  if (nodeResponse.body) {\n    await writeReadableStreamToWritable(nodeResponse.body, res);\n  } else {\n    res.end();\n  }\n}\n"
  },
  {
    "path": "packages/react-router-express/tsconfig.json",
    "content": "{\n  \"include\": [\"**/*.ts\"],\n  \"exclude\": [\"dist\", \"__tests__\", \"node_modules\"],\n  \"compilerOptions\": {\n    \"lib\": [\"DOM\", \"DOM.Iterable\", \"ES2022\"],\n    \"target\": \"ES2022\",\n    \"module\": \"ES2022\",\n    \"skipLibCheck\": true,\n\n    \"moduleResolution\": \"Bundler\",\n    \"allowSyntheticDefaultImports\": true,\n    \"strict\": true,\n    \"jsx\": \"react\",\n    \"declaration\": true,\n    \"emitDeclarationOnly\": true,\n    \"rootDir\": \".\",\n    \"outDir\": \"./dist\"\n  }\n}\n"
  },
  {
    "path": "packages/react-router-express/tsup.config.ts",
    "content": "import { defineConfig } from \"tsup\";\n\n// @ts-ignore - out of scope\nimport { createBanner } from \"../../build.utils.js\";\n\nimport pkg from \"./package.json\";\n\nconst entry = [\"index.ts\"];\n\nexport default defineConfig([\n  {\n    clean: true,\n    entry,\n    format: [\"cjs\", \"esm\"],\n    outDir: \"dist\",\n    dts: true,\n    banner: {\n      js: createBanner(pkg.name, pkg.version),\n    },\n  },\n]);\n"
  },
  {
    "path": "packages/react-router-express/typedoc.mjs",
    "content": "import { blockTags } from \"../../typedoc.mjs\";\n\n/** @type {Partial<import('typedoc').TypeDocOptions>} */\nconst config = {\n  entryPoints: [\"./index.ts\"],\n  blockTags,\n};\n\nexport default config;\n"
  },
  {
    "path": "packages/react-router-fs-routes/CHANGELOG.md",
    "content": "# `@react-router/fs-routes`\n\n## 7.13.1\n\n### Patch Changes\n\n- Updated dependencies:\n  - `@react-router/dev@7.13.1`\n\n## 7.13.0\n\n### Patch Changes\n\n- Fix route file paths when routes directory is outside of the app directory ([#13937](https://github.com/remix-run/react-router/pull/13937))\n- Updated dependencies:\n  - `@react-router/dev@7.13.0`\n\n## 7.12.0\n\n### Patch Changes\n\n- Updated dependencies:\n  - `@react-router/dev@7.12.0`\n\n## 7.11.0\n\n### Patch Changes\n\n- Updated dependencies:\n  - `@react-router/dev@7.11.0`\n\n## 7.10.1\n\n### Patch Changes\n\n- Updated dependencies:\n  - `@react-router/dev@7.10.1`\n\n## 7.10.0\n\n### Patch Changes\n\n- Updated dependencies:\n  - `@react-router/dev@7.10.0`\n\n## 7.9.6\n\n### Patch Changes\n\n- Updated dependencies:\n  - `@react-router/dev@7.9.6`\n\n## 7.9.5\n\n### Patch Changes\n\n- Updated dependencies:\n  - `@react-router/dev@7.9.5`\n\n## 7.9.4\n\n### Patch Changes\n\n- Updated dependencies:\n  - `@react-router/dev@7.9.4`\n\n## 7.9.3\n\n### Patch Changes\n\n- Updated dependencies:\n  - `@react-router/dev@7.9.3`\n\n## 7.9.2\n\n### Patch Changes\n\n- Updated dependencies:\n  - `@react-router/dev@7.9.2`\n\n## 7.9.1\n\n### Patch Changes\n\n- Updated dependencies:\n  - `@react-router/dev@7.9.1`\n\n## 7.9.0\n\n### Patch Changes\n\n- Updated dependencies:\n  - `@react-router/dev@7.9.0`\n\n## 7.8.2\n\n### Patch Changes\n\n- Updated dependencies:\n  - `@react-router/dev@7.8.2`\n\n## 7.8.1\n\n### Patch Changes\n\n- Updated dependencies:\n  - `@react-router/dev@7.8.1`\n\n## 7.8.0\n\n### Patch Changes\n\n- Updated dependencies:\n  - `@react-router/dev@7.8.0`\n\n## 7.7.1\n\n### Patch Changes\n\n- Updated dependencies:\n  - `@react-router/dev@7.7.1`\n\n## 7.7.0\n\n### Patch Changes\n\n- Updated dependencies:\n  - `@react-router/dev@7.7.0`\n\n## 7.6.3\n\n### Patch Changes\n\n- Use `replaceAll` for normalising windows file system slashes. ([#13738](https://github.com/remix-run/react-router/pull/13738))\n- Updated dependencies:\n  - `@react-router/dev@7.6.3`\n\n## 7.6.2\n\n### Patch Changes\n\n- Updated dependencies:\n  - `@react-router/dev@7.6.2`\n\n## 7.6.1\n\n### Patch Changes\n\n- Updated dependencies:\n  - `@react-router/dev@7.6.1`\n\n## 7.6.0\n\n### Patch Changes\n\n- Updated dependencies:\n  - `@react-router/dev@7.6.0`\n\n## 7.5.3\n\n### Patch Changes\n\n- Updated dependencies:\n  - `@react-router/dev@7.5.3`\n\n## 7.5.2\n\n### Patch Changes\n\n- Updated dependencies:\n  - `@react-router/dev@7.5.2`\n\n## 7.5.1\n\n### Patch Changes\n\n- Updated dependencies:\n  - `@react-router/dev@7.5.1`\n\n## 7.5.0\n\n### Patch Changes\n\n- Updated dependencies:\n  - `@react-router/dev@7.5.0`\n\n## 7.4.1\n\n### Patch Changes\n\n- Updated dependencies:\n  - `@react-router/dev@7.4.1`\n\n## 7.4.0\n\n### Patch Changes\n\n- Updated dependencies:\n  - `@react-router/dev@7.4.0`\n\n## 7.3.0\n\n### Patch Changes\n\n- Updated dependencies:\n  - `@react-router/dev@7.3.0`\n\n## 7.2.0\n\n### Patch Changes\n\n- Updated dependencies:\n  - `@react-router/dev@7.2.0`\n\n## 7.1.5\n\n### Patch Changes\n\n- Updated dependencies:\n  - `@react-router/dev@7.1.5`\n\n## 7.1.4\n\n### Patch Changes\n\n- Updated dependencies:\n  - `@react-router/dev@7.1.4`\n\n## 7.1.3\n\n### Patch Changes\n\n- Updated dependencies:\n  - `@react-router/dev@7.1.3`\n\n## 7.1.2\n\n### Patch Changes\n\n- Updated dependencies:\n  - `@react-router/dev@7.1.2`\n\n## 7.1.1\n\n### Patch Changes\n\n- Updated dependencies:\n  - `@react-router/dev@7.1.1`\n\n## 7.1.0\n\n### Patch Changes\n\n- Throw error in `flatRoutes` if routes directory is missing ([#12407](https://github.com/remix-run/react-router/pull/12407))\n- Updated dependencies:\n  - `@react-router/dev@7.1.0`\n\n## 7.0.2\n\n### Patch Changes\n\n- Updated dependencies:\n  - `@react-router/dev@7.0.2`\n\n## 7.0.1\n\n### Patch Changes\n\n- Updated dependencies:\n  - `@react-router/dev@7.0.1`\n\n## 7.0.0\n\nInitial release.\n\n### Patch Changes\n\n- Updated dependencies:\n  - `@react-router/dev@7.0.0`\n"
  },
  {
    "path": "packages/react-router-fs-routes/README.md",
    "content": "File system routing conventions for React Router\n\n```sh\nnpm install @react-router/fs-routes\n```\n"
  },
  {
    "path": "packages/react-router-fs-routes/__tests__/flatRoutes-test.ts",
    "content": "import { mkdirSync, rmSync, writeFileSync } from \"node:fs\";\nimport os from \"node:os\";\nimport path from \"node:path\";\n\nimport type { RouteManifestEntry } from \"../manifest\";\n\nimport {\n  flatRoutes,\n  flatRoutesUniversal,\n  getRoutePathConflictErrorMessage,\n  getRouteIdConflictErrorMessage,\n  getRouteSegments,\n} from \"../flatRoutes\";\nimport { normalizeSlashes } from \"../normalizeSlashes\";\n\nlet APP_DIR = path.join(\"test\", \"root\", \"app\");\n\ndescribe(\"flatRoutes\", () => {\n  describe(\"creates proper route paths\", () => {\n    let tests: [string, string | undefined][] = [\n      [\"routes.$\", \"routes/*\"],\n      [\"routes.sub.$\", \"routes/sub/*\"],\n      [\"routes.$slug\", \"routes/:slug\"],\n      [\"routes.sub.$slug\", \"routes/sub/:slug\"],\n      [\"$\", \"*\"],\n      [\"flat.$\", \"flat/*\"],\n      [\"$slug\", \":slug\"],\n      [\"nested/index\", \"nested\"],\n      [\"nested.$\", \"*\"],\n      [\"nested.$slug\", \":slug\"],\n      [\"nested._layout.$param\", \":param\"],\n\n      [\"flat.$slug\", \"flat/:slug\"],\n      [\"flat.sub\", \"flat/sub\"],\n      [\"flat._index\", \"flat\"],\n      [\"_index\", undefined],\n      [\"_layout/index\", undefined],\n      [\"_layout.test\", \"test\"],\n      [\"_layout.$param\", \":param\"],\n      [\"$slug[.]json\", \":slug.json\"],\n      [\"sub.[sitemap.xml]\", \"sub/sitemap.xml\"],\n      [\"posts.$slug.[image.jpg]\", \"posts/:slug/image.jpg\"],\n      [\"sub.[[]\", \"sub/[\"],\n      [\"sub.]\", \"sub/]\"],\n      [\"sub.[[]]\", \"sub/[]\"],\n      [\"beef]\", \"beef]\"],\n      [\"[index]\", \"index\"],\n      [\"test.inde[x]\", \"test/index\"],\n      [\"[i]ndex.[[].[[]]\", \"index/[/[]\"],\n\n      // Optional segment routes\n      [\"(routes).$\", \"routes?/*\"],\n      [\"(routes).(sub).$\", \"routes?/sub?/*\"],\n      [\"(routes).($slug)\", \"routes?/:slug?\"],\n      [\"(routes).sub.($slug)\", \"routes?/sub/:slug?\"],\n      [\"(nested).$\", \"nested?/*\"],\n      [\"(flat).$\", \"flat?/*\"],\n      [\"($slug)\", \":slug?\"],\n      [\"(nested).($slug)\", \"nested?/:slug?\"],\n      [\"(flat).($slug)\", \"flat?/:slug?\"],\n      [\"flat.(sub)\", \"flat/sub?\"],\n      [\"_layout.(test)\", \"test?\"],\n      [\"_layout.($user)\", \":user?\"],\n      [\"(nested)._layout.($param)\", \"nested?/:param?\"],\n      [\"($slug[.]json)\", \":slug.json?\"],\n      [\"(sub).([sitemap.xml])\", \"sub?/sitemap.xml?\"],\n      [\"(sub).[(sitemap.xml)]\", \"sub?/(sitemap.xml)\"],\n      [\"(posts).($slug).([image.jpg])\", \"posts?/:slug?/image.jpg?\"],\n      [\n        \"($[$dollabills]).([.]lol).(what).([$]).($up)\",\n        \":$dollabills?/.lol?/what?/$?/:up?\",\n      ],\n      [\"(sub).(])\", \"sub?/]?\"],\n      [\"(sub).([[]])\", \"sub?/[]?\"],\n      [\"(sub).([[])\", \"sub?/[?\"],\n      [\"(beef])\", \"beef]?\"],\n      [\"([index])\", \"index?\"],\n      [\"(test).(inde[x])\", \"test?/index?\"],\n      [\"([i]ndex).([[]).([[]])\", \"index?/[?/[]?\"],\n\n      // Opting out of parent layout\n      [\"user_.projects.$id.roadmap\", \"user/projects/:id/roadmap\"],\n      [\"app.projects_.$id.roadmap\", \"app/projects/:id/roadmap\"],\n      [\"shop_.projects_.$id.roadmap\", \"shop/projects/:id/roadmap\"],\n    ];\n\n    let manifest = flatRoutesUniversal(\n      APP_DIR,\n      tests.map((t) => path.join(APP_DIR, \"routes\", t[0] + \".tsx\")),\n    );\n\n    for (let [input, expected] of tests) {\n      it(`\"${input}\" -> \"${expected}\"`, () => {\n        if (input.endsWith(\"/route\") || input.endsWith(\"/index\")) {\n          input = input.replace(/\\/(route|index)$/, \"\");\n        }\n        let routeInfo = manifest[path.posix.join(\"routes\", input)];\n        expect(routeInfo.path).toBe(expected);\n      });\n    }\n\n    let invalidSlashFiles = [\n      \"($[$dollabills]).([.]lol)[/](what)/([$]).$\",\n      \"$[$dollabills].[.]lol[/]what/[$].$\",\n    ];\n\n    for (let invalid of invalidSlashFiles) {\n      test(\"should error when using `/` in a route segment\", () => {\n        let regex = new RegExp(\n          /Route segment (\".*?\") for (\".*?\") cannot contain \"\\/\"/,\n        );\n        expect(() => getRouteSegments(invalid)).toThrow(regex);\n      });\n    }\n\n    let invalidSplatFiles: string[] = [\n      \"routes/about.[*].tsx\",\n      \"routes/about.*.tsx\",\n      \"routes/about.[.[.*].].tsx\",\n    ];\n\n    for (let invalid of invalidSplatFiles) {\n      test(\"should error when using `*` in a route segment\", () => {\n        let regex = new RegExp(\n          /Route segment (\".*?\") for (\".*?\") cannot contain \"\\*\"/,\n        );\n        expect(() => getRouteSegments(invalid)).toThrow(regex);\n      });\n    }\n\n    let invalidParamFiles: string[] = [\n      \"routes/about.[:name].tsx\",\n      \"routes/about.:name.tsx\",\n    ];\n\n    for (let invalid of invalidParamFiles) {\n      test(\"should error when using `:` in a route segment\", () => {\n        let regex = new RegExp(\n          /Route segment (\".*?\") for (\".*?\") cannot contain \":\"/,\n        );\n        expect(() => getRouteSegments(invalid)).toThrow(regex);\n      });\n    }\n  });\n\n  describe(\"should return the correct route hierarchy\", () => {\n    // we'll add some files manually before running the tests\n    let testFiles: [string, Omit<RouteManifestEntry, \"file\">][] = [\n      [\n        \"routes/_auth.tsx\",\n        {\n          id: \"routes/_auth\",\n          parentId: \"root\",\n          path: undefined,\n        },\n      ],\n      [\n        \"routes/_auth.forgot-password.tsx\",\n        {\n          id: \"routes/_auth.forgot-password\",\n          parentId: \"routes/_auth\",\n          path: \"forgot-password\",\n        },\n      ],\n      [\n        \"routes/_auth.login.tsx\",\n        {\n          id: \"routes/_auth.login\",\n          parentId: \"routes/_auth\",\n          path: \"login\",\n        },\n      ],\n      [\n        \"routes/_auth.reset-password.tsx\",\n        {\n          id: \"routes/_auth.reset-password\",\n          parentId: \"routes/_auth\",\n          path: \"reset-password\",\n        },\n      ],\n      [\n        \"routes/_auth.signup.tsx\",\n        {\n          id: \"routes/_auth.signup\",\n          parentId: \"routes/_auth\",\n          path: \"signup\",\n        },\n      ],\n      [\n        \"routes/_landing/index.tsx\",\n        {\n          id: \"routes/_landing\",\n          parentId: \"root\",\n          path: undefined,\n        },\n      ],\n      [\n        \"routes/_landing._index/index.tsx\",\n        {\n          id: \"routes/_landing._index\",\n          parentId: \"routes/_landing\",\n          path: undefined,\n          index: true,\n        },\n      ],\n      [\n        \"routes/_landing.index.tsx\",\n        {\n          id: \"routes/_landing.index\",\n          parentId: \"routes/_landing\",\n          path: \"index\",\n        },\n      ],\n      [\n        \"routes/_about.tsx\",\n        {\n          id: \"routes/_about\",\n          parentId: \"root\",\n          path: undefined,\n        },\n      ],\n      [\n        \"routes/_about.faq.tsx\",\n        {\n          id: \"routes/_about.faq\",\n          parentId: \"routes/_about\",\n          path: \"faq\",\n        },\n      ],\n      [\n        \"routes/_about.$splat.tsx\",\n        {\n          id: \"routes/_about.$splat\",\n          parentId: \"routes/_about\",\n          path: \":splat\",\n        },\n      ],\n      [\n        \"routes/app.tsx\",\n        {\n          id: \"routes/app\",\n          parentId: \"root\",\n          path: \"app\",\n        },\n      ],\n      [\n        \"routes/app.calendar.$day.tsx\",\n        {\n          id: \"routes/app.calendar.$day\",\n          parentId: \"routes/app\",\n          path: \"calendar/:day\",\n        },\n      ],\n      [\n        \"routes/app.calendar._index.tsx\",\n        {\n          id: \"routes/app.calendar._index\",\n          index: true,\n          parentId: \"routes/app\",\n          path: \"calendar\",\n        },\n      ],\n      [\n        \"routes/app.projects.tsx\",\n        {\n          id: \"routes/app.projects\",\n          parentId: \"routes/app\",\n          path: \"projects\",\n        },\n      ],\n      [\n        \"routes/app.projects.$id.tsx\",\n        {\n          id: \"routes/app.projects.$id\",\n          parentId: \"routes/app.projects\",\n          path: \":id\",\n        },\n      ],\n      [\n        \"routes/app._pathless.tsx\",\n        {\n          id: \"routes/app._pathless\",\n          parentId: \"routes/app\",\n          path: undefined,\n        },\n      ],\n      [\n        \"routes/app._pathless._index.tsx\",\n        {\n          id: \"routes/app._pathless._index\",\n          parentId: \"routes/app._pathless\",\n          index: true,\n          path: undefined,\n        },\n      ],\n      [\n        \"routes/app._pathless.child.tsx\",\n        {\n          id: \"routes/app._pathless.child\",\n          parentId: \"routes/app._pathless\",\n          path: \"child\",\n        },\n      ],\n      [\n        \"routes/folder/route.tsx\",\n        {\n          id: \"routes/folder\",\n          parentId: \"root\",\n          path: \"folder\",\n        },\n      ],\n      [\n        \"routes/[route].tsx\",\n        {\n          id: \"routes/[route]\",\n          parentId: \"root\",\n          path: \"route\",\n        },\n      ],\n\n      // Opt out of parent layout\n      [\n        \"routes/app_.projects.$id.roadmap[.pdf].tsx\",\n        {\n          id: \"routes/app_.projects.$id.roadmap[.pdf]\",\n          parentId: \"root\",\n          path: \"app/projects/:id/roadmap.pdf\",\n        },\n      ],\n      [\n        \"routes/app_.projects.$id.roadmap.tsx\",\n        {\n          id: \"routes/app_.projects.$id.roadmap\",\n          parentId: \"root\",\n          path: \"app/projects/:id/roadmap\",\n        },\n      ],\n\n      [\n        \"routes/app.skip.tsx\",\n        {\n          id: \"routes/app.skip\",\n          parentId: \"routes/app\",\n          path: \"skip\",\n        },\n      ],\n      [\n        \"routes/app.skip_.layout.tsx\",\n        {\n          id: \"routes/app.skip_.layout\",\n          index: undefined,\n          parentId: \"routes/app\",\n          path: \"skip/layout\",\n        },\n      ],\n\n      [\n        \"routes/app_.skipall_._index.tsx\",\n        {\n          id: \"routes/app_.skipall_._index\",\n          index: true,\n          parentId: \"root\",\n          path: \"app/skipall\",\n        },\n      ],\n\n      // Escaping route segments\n      [\n        \"routes/_about.[$splat].tsx\",\n        {\n          id: \"routes/_about.[$splat]\",\n          parentId: \"routes/_about\",\n          path: \"$splat\",\n        },\n      ],\n      [\n        \"routes/_about.[[].tsx\",\n        {\n          id: \"routes/_about.[[]\",\n          parentId: \"routes/_about\",\n          path: \"[\",\n        },\n      ],\n      [\n        \"routes/_about.[]].tsx\",\n        {\n          id: \"routes/_about.[]]\",\n          parentId: \"routes/_about\",\n          path: \"]\",\n        },\n      ],\n      [\n        \"routes/_about.[.].tsx\",\n        {\n          id: \"routes/_about.[.]\",\n          parentId: \"routes/_about\",\n          path: \".\",\n        },\n      ],\n\n      // Optional route segments\n      [\n        \"routes/(nested)._layout.($slug).tsx\",\n        {\n          id: \"routes/(nested)._layout.($slug)\",\n          parentId: \"root\",\n          path: \"nested?/:slug?\",\n        },\n      ],\n      [\n        \"routes/(routes).$.tsx\",\n        {\n          id: \"routes/(routes).$\",\n          parentId: \"root\",\n          path: \"routes?/*\",\n        },\n      ],\n      [\n        \"routes/(routes).(sub).$.tsx\",\n        {\n          id: \"routes/(routes).(sub).$\",\n          parentId: \"root\",\n          path: \"routes?/sub?/*\",\n        },\n      ],\n      [\n        \"routes/(routes).($slug).tsx\",\n        {\n          id: \"routes/(routes).($slug)\",\n          parentId: \"root\",\n          path: \"routes?/:slug?\",\n        },\n      ],\n      [\n        \"routes/(routes).sub.($slug).tsx\",\n        {\n          id: \"routes/(routes).sub.($slug)\",\n          parentId: \"root\",\n          path: \"routes?/sub/:slug?\",\n        },\n      ],\n      [\n        \"routes/(nested).$.tsx\",\n        {\n          id: \"routes/(nested).$\",\n          parentId: \"root\",\n          path: \"nested?/*\",\n        },\n      ],\n      [\n        \"routes/(flat).$.tsx\",\n        {\n          id: \"routes/(flat).$\",\n          parentId: \"root\",\n          path: \"flat?/*\",\n        },\n      ],\n      [\n        \"routes/(flat).($slug).tsx\",\n        {\n          id: \"routes/(flat).($slug)\",\n          parentId: \"root\",\n          path: \"flat?/:slug?\",\n        },\n      ],\n      [\n        \"routes/flat.(sub).tsx\",\n        {\n          id: \"routes/flat.(sub)\",\n          parentId: \"root\",\n          path: \"flat/sub?\",\n        },\n      ],\n      [\n        \"routes/_layout.tsx\",\n        {\n          id: \"routes/_layout\",\n          parentId: \"root\",\n          path: undefined,\n        },\n      ],\n      [\n        \"routes/_layout.(test).tsx\",\n        {\n          id: \"routes/_layout.(test)\",\n          parentId: \"routes/_layout\",\n          path: \"test?\",\n        },\n      ],\n      [\n        \"routes/_layout.($slug).tsx\",\n        {\n          id: \"routes/_layout.($slug)\",\n          parentId: \"routes/_layout\",\n          path: \":slug?\",\n        },\n      ],\n\n      // Optional + escaped route segments\n      [\n        \"routes/([_index]).tsx\",\n        {\n          id: \"routes/([_index])\",\n          parentId: \"root\",\n          path: \"_index?\",\n        },\n      ],\n      [\n        \"routes/(_[i]ndex).([[]).([[]]).tsx\",\n        {\n          id: \"routes/(_[i]ndex).([[]).([[]])\",\n          parentId: \"root\",\n          path: \"_index?/[?/[]?\",\n        },\n      ],\n      [\n        \"routes/(sub).([[]).tsx\",\n        {\n          id: \"routes/(sub).([[])\",\n          parentId: \"root\",\n          path: \"sub?/[?\",\n        },\n      ],\n      [\n        \"routes/(sub).(]).tsx\",\n        {\n          id: \"routes/(sub).(])\",\n          parentId: \"root\",\n          path: \"sub?/]?\",\n        },\n      ],\n      [\n        \"routes/(sub).([[]]).tsx\",\n        {\n          id: \"routes/(sub).([[]])\",\n          parentId: \"root\",\n          path: \"sub?/[]?\",\n        },\n      ],\n      [\n        \"routes/(beef]).tsx\",\n        {\n          id: \"routes/(beef])\",\n          parentId: \"root\",\n          path: \"beef]?\",\n        },\n      ],\n      [\n        \"routes/(test).(inde[x]).tsx\",\n        {\n          id: \"routes/(test).(inde[x])\",\n          parentId: \"root\",\n          path: \"test?/index?\",\n        },\n      ],\n      [\n        \"routes/($[$dollabills]).([.]lol).(what).([$]).($up).tsx\",\n        {\n          id: \"routes/($[$dollabills]).([.]lol).(what).([$]).($up)\",\n          parentId: \"root\",\n          path: \":$dollabills?/.lol?/what?/$?/:up?\",\n        },\n      ],\n      [\n        \"routes/(posts).($slug).([image.jpg]).tsx\",\n        {\n          id: \"routes/(posts).($slug).([image.jpg])\",\n          parentId: \"root\",\n          path: \"posts?/:slug?/image.jpg?\",\n        },\n      ],\n      [\n        \"routes/(sub).([sitemap.xml]).tsx\",\n        {\n          id: \"routes/(sub).([sitemap.xml])\",\n          parentId: \"root\",\n          path: \"sub?/sitemap.xml?\",\n        },\n      ],\n      [\n        \"routes/(sub).[(sitemap.xml)].tsx\",\n        {\n          id: \"routes/(sub).[(sitemap.xml)]\",\n          parentId: \"root\",\n          path: \"sub?/(sitemap.xml)\",\n        },\n      ],\n      [\n        \"routes/($slug[.]json).tsx\",\n        {\n          id: \"routes/($slug[.]json)\",\n          parentId: \"root\",\n          path: \":slug.json?\",\n        },\n      ],\n\n      [\n        \"routes/[]otherstuff].tsx\",\n        {\n          id: \"routes/[]otherstuff]\",\n          parentId: \"root\",\n          path: \"otherstuff]\",\n        },\n      ],\n      [\n        \"routes/brand.tsx\",\n        {\n          id: \"routes/brand\",\n          parentId: \"root\",\n          path: \"brand\",\n        },\n      ],\n      [\n        \"routes/brand._index.tsx\",\n        {\n          id: \"routes/brand._index\",\n          parentId: \"routes/brand\",\n          index: true,\n        },\n      ],\n      [\n        \"routes/$.tsx\",\n        {\n          id: \"routes/$\",\n          parentId: \"root\",\n          path: \"*\",\n        },\n      ],\n    ];\n\n    let files: [string, RouteManifestEntry][] = testFiles.map(\n      ([file, route]) => {\n        return [file, { ...route, file }];\n      },\n    );\n\n    let routeManifest = flatRoutesUniversal(\n      APP_DIR,\n      files.map(([file]) => path.join(APP_DIR, file)),\n    );\n    let routes = Object.values(routeManifest);\n\n    test(\"route per file\", () => {\n      expect(routes).toHaveLength(files.length);\n    });\n\n    for (let [file, route] of files) {\n      test(`hierarchy for ${file} - ${route.path}`, () => {\n        expect(routes).toContainEqual(route);\n      });\n    }\n  });\n\n  describe(\"doesn't warn when there's not a route collision\", () => {\n    let consoleError = jest\n      .spyOn(global.console, \"error\")\n      .mockImplementation(() => {});\n\n    afterEach(consoleError.mockReset);\n\n    test(\"same number of segments and the same dynamic segment index\", () => {\n      let testFiles = [\n        path.join(APP_DIR, \"routes\", \"_user.$username.tsx\"),\n        path.join(APP_DIR, \"routes\", \"sneakers.$sneakerId.tsx\"),\n      ];\n\n      let routeManifest = flatRoutesUniversal(APP_DIR, testFiles);\n\n      let routes = Object.values(routeManifest);\n\n      expect(routes).toHaveLength(testFiles.length);\n      expect(consoleError).not.toHaveBeenCalled();\n    });\n  });\n\n  describe(\"warns when there's a route collision\", () => {\n    let consoleError = jest\n      .spyOn(global.console, \"error\")\n      .mockImplementation(() => {});\n\n    afterEach(consoleError.mockReset);\n\n    test(\"index files\", () => {\n      let testFiles = [\n        path.join(\"routes\", \"_dashboard._index.tsx\"),\n        path.join(\"routes\", \"_landing._index.tsx\"),\n        path.join(\"routes\", \"_index.tsx\"),\n      ];\n\n      // route manifest uses the full path\n      let fullPaths = testFiles.map((file) => path.join(APP_DIR, file));\n\n      // this is for the expected error message,\n      // which uses the relative path from the app directory internally\n      let normalizedTestFiles = testFiles.map((file) => normalizeSlashes(file));\n\n      let routeManifest = flatRoutesUniversal(APP_DIR, fullPaths);\n\n      let routes = Object.values(routeManifest);\n\n      expect(routes).toHaveLength(1);\n      expect(consoleError).toHaveBeenCalledWith(\n        getRoutePathConflictErrorMessage(\"/\", normalizedTestFiles),\n      );\n    });\n\n    test(\"folder/route.tsx matching folder.tsx\", () => {\n      let testFiles = [\n        path.join(\"routes\", \"dashboard\", \"route.tsx\"),\n        path.join(\"routes\", \"dashboard.tsx\"),\n      ];\n\n      // route manifest uses the full path\n      let fullPaths = testFiles.map((file) => path.join(APP_DIR, file));\n\n      // this is for the expected error message,\n      // which uses the relative path from the app directory internally\n      let normalizedTestFiles = testFiles.map((file) => normalizeSlashes(file));\n\n      let routeManifest = flatRoutesUniversal(APP_DIR, fullPaths);\n\n      let routes = Object.values(routeManifest);\n\n      expect(routes).toHaveLength(1);\n      expect(consoleError).toHaveBeenCalledWith(\n        getRouteIdConflictErrorMessage(\n          path.posix.join(\"routes\", \"dashboard\"),\n          normalizedTestFiles,\n        ),\n      );\n    });\n\n    test(\"pathless layouts should not collide\", () => {\n      let testFiles = [\n        path.join(APP_DIR, \"routes\", \"_a.tsx\"),\n        path.join(APP_DIR, \"routes\", \"_a._index.tsx\"),\n        path.join(APP_DIR, \"routes\", \"_a.a.tsx\"),\n        path.join(APP_DIR, \"routes\", \"_b.tsx\"),\n        path.join(APP_DIR, \"routes\", \"_b.b.tsx\"),\n      ];\n\n      let routeManifest = flatRoutesUniversal(APP_DIR, testFiles);\n\n      let routes = Object.values(routeManifest);\n\n      expect(consoleError).not.toHaveBeenCalled();\n      expect(routes).toHaveLength(5);\n\n      // When using folders and route.tsx files\n      testFiles = [\n        path.join(APP_DIR, \"routes\", \"_a\", \"route.tsx\"),\n        path.join(APP_DIR, \"routes\", \"_a._index\", \"route.tsx\"),\n        path.join(APP_DIR, \"routes\", \"_a.a\", \"route.tsx\"),\n        path.join(APP_DIR, \"routes\", \"_b\", \"route.tsx\"),\n        path.join(APP_DIR, \"routes\", \"_b.b\", \"route.tsx\"),\n      ];\n\n      routeManifest = flatRoutesUniversal(APP_DIR, testFiles);\n\n      routes = Object.values(routeManifest);\n\n      expect(consoleError).not.toHaveBeenCalled();\n      expect(routes).toHaveLength(5);\n    });\n\n    test(\"nested pathless layouts should not collide\", () => {\n      let testFiles = [\n        path.join(APP_DIR, \"routes\", \"nested._a.tsx\"),\n        path.join(APP_DIR, \"routes\", \"nested._a._index.tsx\"),\n        path.join(APP_DIR, \"routes\", \"nested._a.a.tsx\"),\n        path.join(APP_DIR, \"routes\", \"nested._b.tsx\"),\n        path.join(APP_DIR, \"routes\", \"nested._b.b.tsx\"),\n      ];\n\n      let routeManifest = flatRoutesUniversal(APP_DIR, testFiles);\n\n      let routes = Object.values(routeManifest);\n\n      expect(consoleError).not.toHaveBeenCalled();\n      expect(routes).toHaveLength(5);\n\n      // When using folders and route.tsx files\n      testFiles = [\n        path.join(APP_DIR, \"routes\", \"nested._a\", \"route.tsx\"),\n        path.join(APP_DIR, \"routes\", \"nested._a._index\", \"route.tsx\"),\n        path.join(APP_DIR, \"routes\", \"nested._a.a\", \"route.tsx\"),\n        path.join(APP_DIR, \"routes\", \"nested._b\", \"route.tsx\"),\n        path.join(APP_DIR, \"routes\", \"nested._b.b\", \"route.tsx\"),\n      ];\n\n      routeManifest = flatRoutesUniversal(APP_DIR, testFiles);\n\n      routes = Object.values(routeManifest);\n\n      expect(consoleError).not.toHaveBeenCalled();\n      expect(routes).toHaveLength(5);\n    });\n\n    test(\"legit collisions without nested pathless layouts should collide (paths)\", () => {\n      let testFiles = [\n        path.join(APP_DIR, \"routes\", \"nested._a.tsx\"),\n        path.join(APP_DIR, \"routes\", \"nested._a.a.tsx\"),\n        path.join(APP_DIR, \"routes\", \"nested._b.tsx\"),\n        path.join(APP_DIR, \"routes\", \"nested._b.a.tsx\"),\n      ];\n\n      let routeManifest = flatRoutesUniversal(APP_DIR, testFiles);\n\n      let routes = Object.values(routeManifest);\n\n      expect(consoleError).toHaveBeenCalledWith(\n        getRoutePathConflictErrorMessage(\"/nested/a\", [\n          \"routes/nested._a.a.tsx\",\n          \"routes/nested._b.a.tsx\",\n        ]),\n      );\n      expect(routes).toHaveLength(3);\n\n      // When using folders and route.tsx files\n      consoleError.mockClear();\n      testFiles = [\n        path.join(APP_DIR, \"routes\", \"nested._a\", \"route.tsx\"),\n        path.join(APP_DIR, \"routes\", \"nested._a.a\", \"route.tsx\"),\n        path.join(APP_DIR, \"routes\", \"nested._b\", \"route.tsx\"),\n        path.join(APP_DIR, \"routes\", \"nested._b.a\", \"route.tsx\"),\n      ];\n\n      routeManifest = flatRoutesUniversal(APP_DIR, testFiles);\n\n      routes = Object.values(routeManifest);\n\n      expect(consoleError).toHaveBeenCalledWith(\n        getRoutePathConflictErrorMessage(\"/nested/a\", [\n          \"routes/nested._a.a/route.tsx\",\n          \"routes/nested._b.a/route.tsx\",\n        ]),\n      );\n      expect(routes).toHaveLength(3);\n    });\n\n    test(\"legit collisions without nested pathless layouts should collide (index routes)\", () => {\n      let testFiles = [\n        path.join(APP_DIR, \"routes\", \"nested._a.tsx\"),\n        path.join(APP_DIR, \"routes\", \"nested._a._index.tsx\"),\n        path.join(APP_DIR, \"routes\", \"nested._b.tsx\"),\n        path.join(APP_DIR, \"routes\", \"nested._b._index.tsx\"),\n      ];\n\n      let routeManifest = flatRoutesUniversal(APP_DIR, testFiles);\n\n      let routes = Object.values(routeManifest);\n\n      expect(consoleError).toHaveBeenCalledWith(\n        getRoutePathConflictErrorMessage(\"/nested\", [\n          \"routes/nested._a._index.tsx\",\n          \"routes/nested._b._index.tsx\",\n        ]),\n      );\n      expect(routes).toHaveLength(3);\n\n      // When using folders and route.tsx files\n      consoleError.mockClear();\n      testFiles = [\n        path.join(APP_DIR, \"routes\", \"nested._a\", \"route.tsx\"),\n        path.join(APP_DIR, \"routes\", \"nested._a._index\", \"route.tsx\"),\n        path.join(APP_DIR, \"routes\", \"nested._b\", \"route.tsx\"),\n        path.join(APP_DIR, \"routes\", \"nested._b._index\", \"route.tsx\"),\n      ];\n\n      routeManifest = flatRoutesUniversal(APP_DIR, testFiles);\n\n      routes = Object.values(routeManifest);\n\n      expect(consoleError).toHaveBeenCalledWith(\n        getRoutePathConflictErrorMessage(\"/nested\", [\n          \"routes/nested._a._index/route.tsx\",\n          \"routes/nested._b._index/route.tsx\",\n        ]),\n      );\n      expect(routes).toHaveLength(3);\n    });\n  });\n\n  describe(\"throws expected errors\", () => {\n    let tempDir = path.join(\n      os.tmpdir(),\n      \"react-router-fs-routes-test\",\n      Math.random().toString(36).substring(2, 15),\n    );\n\n    beforeEach(() => {\n      mkdirSync(tempDir, { recursive: true });\n    });\n    afterEach(() => {\n      rmSync(tempDir, { recursive: true, force: true });\n    });\n\n    test(\"root route is not found\", () => {\n      expect(() => flatRoutes(tempDir)).toThrow(\n        `Could not find a root route module in the app directory: ${tempDir}`,\n      );\n    });\n\n    test(\"routes dir is not found\", () => {\n      const rootRoute = path.join(tempDir, \"root.tsx\");\n      writeFileSync(rootRoute, \"\");\n      expect(() => flatRoutes(tempDir)).toThrow(\n        `Could not find the routes directory: ${path.join(\n          tempDir,\n          \"routes\",\n        )}. Did you forget to create it?`,\n      );\n    });\n  });\n\n  describe(\"generates route manifest entry files relative to the app directory\", () => {\n    test(\"routes directory inside the app directory\", () => {\n      let routeFile = path.posix.join(APP_DIR, \"routes\", \"route.tsx\");\n      let routeInfo = flatRoutesUniversal(APP_DIR, [routeFile]);\n      let routes = Object.values(routeInfo);\n\n      expect(routes).toHaveLength(1);\n      expect(routes[0].file).toBe(\"routes/route.tsx\");\n    });\n\n    test(\"routes directory outside the app directory\", () => {\n      let routeFile = path.posix.join(APP_DIR, \"..\", \"routes\", \"route.tsx\");\n      let routeInfo = flatRoutesUniversal(APP_DIR, [routeFile]);\n      let routes = Object.values(routeInfo);\n\n      expect(routes).toHaveLength(1);\n      expect(routes[0].file).toBe(\"../routes/route.tsx\");\n    });\n  });\n});\n"
  },
  {
    "path": "packages/react-router-fs-routes/__tests__/routeManifestToRouteConfig-test.ts",
    "content": "import { route } from \"@react-router/dev/routes\";\n\nimport { routeManifestToRouteConfig } from \"../manifest\";\n\nconst clean = (obj: any) => cleanUndefined(cleanIds(obj));\n\nconst cleanUndefined = (obj: any) => JSON.parse(JSON.stringify(obj));\n\nconst cleanIds = (obj: any) =>\n  JSON.parse(\n    JSON.stringify(obj, function replacer(key, value) {\n      return key === \"id\" ? undefined : value;\n    }),\n  );\n\ndescribe(\"routeManifestToRouteConfig\", () => {\n  test(\"creates route config\", () => {\n    let routeManifestConfig = routeManifestToRouteConfig({\n      \"routes/home\": {\n        id: \"routes/home\",\n        parentId: \"root\",\n        path: \"/\",\n        file: \"routes/home.js\",\n      },\n      \"routes/inbox\": {\n        id: \"routes/inbox\",\n        parentId: \"root\",\n        path: \"inbox\",\n        file: \"routes/inbox.js\",\n      },\n      \"routes/inbox/index\": {\n        id: \"routes/inbox/index\",\n        parentId: \"routes/inbox\",\n        path: \"/\",\n        file: \"routes/inbox/index.js\",\n        index: true,\n      },\n      \"routes/inbox/$messageId\": {\n        id: \"routes/inbox/$messageId\",\n        parentId: \"routes/inbox\",\n        path: \":messageId\",\n        file: \"routes/inbox/$messageId.js\",\n        caseSensitive: true,\n      },\n    });\n    let routeConfig = [\n      route(\"/\", \"routes/home.js\"),\n      route(\"inbox\", \"routes/inbox.js\", [\n        route(\"/\", \"routes/inbox/index.js\", { index: true }),\n        route(\":messageId\", \"routes/inbox/$messageId.js\", {\n          caseSensitive: true,\n        }),\n      ]),\n    ];\n\n    expect(clean(routeManifestConfig)).toEqual(clean(routeConfig));\n\n    expect(cleanUndefined(routeManifestConfig)).toMatchInlineSnapshot(`\n      [\n        {\n          \"file\": \"routes/home.js\",\n          \"id\": \"routes/home\",\n          \"path\": \"/\",\n        },\n        {\n          \"children\": [\n            {\n              \"file\": \"routes/inbox/index.js\",\n              \"id\": \"routes/inbox/index\",\n              \"index\": true,\n              \"path\": \"/\",\n            },\n            {\n              \"caseSensitive\": true,\n              \"file\": \"routes/inbox/$messageId.js\",\n              \"id\": \"routes/inbox/$messageId\",\n              \"path\": \":messageId\",\n            },\n          ],\n          \"file\": \"routes/inbox.js\",\n          \"id\": \"routes/inbox\",\n          \"path\": \"inbox\",\n        },\n      ]\n    `);\n  });\n\n  test(\"creates route config with IDs\", () => {\n    let routeConfig = routeManifestToRouteConfig({\n      home: {\n        path: \"/\",\n        id: \"home\",\n        parentId: \"root\",\n        file: \"routes/home.js\",\n      },\n    });\n\n    expect(routeConfig[0].id).toEqual(\"home\");\n  });\n});\n"
  },
  {
    "path": "packages/react-router-fs-routes/flatRoutes.ts",
    "content": "import fs from \"node:fs\";\nimport path from \"node:path\";\nimport { makeRe } from \"minimatch\";\n\nimport type { RouteManifest, RouteManifestEntry } from \"./manifest\";\nimport { normalizeSlashes } from \"./normalizeSlashes\";\n\nexport const routeModuleExts = [\".js\", \".jsx\", \".ts\", \".tsx\", \".md\", \".mdx\"];\n\nexport let paramPrefixChar = \"$\" as const;\nexport let escapeStart = \"[\" as const;\nexport let escapeEnd = \"]\" as const;\n\nexport let optionalStart = \"(\" as const;\nexport let optionalEnd = \")\" as const;\n\nconst PrefixLookupTrieEndSymbol = Symbol(\"PrefixLookupTrieEndSymbol\");\ntype PrefixLookupNode = {\n  [key: string]: PrefixLookupNode;\n} & Record<typeof PrefixLookupTrieEndSymbol, boolean>;\n\nclass PrefixLookupTrie {\n  root: PrefixLookupNode = {\n    [PrefixLookupTrieEndSymbol]: false,\n  };\n\n  add(value: string) {\n    if (!value) throw new Error(\"Cannot add empty string to PrefixLookupTrie\");\n\n    let node = this.root;\n    for (let char of value) {\n      if (!node[char]) {\n        node[char] = {\n          [PrefixLookupTrieEndSymbol]: false,\n        };\n      }\n      node = node[char];\n    }\n    node[PrefixLookupTrieEndSymbol] = true;\n  }\n\n  findAndRemove(\n    prefix: string,\n    filter: (nodeValue: string) => boolean,\n  ): string[] {\n    let node = this.root;\n    for (let char of prefix) {\n      if (!node[char]) return [];\n      node = node[char];\n    }\n\n    return this.#findAndRemoveRecursive([], node, prefix, filter);\n  }\n\n  #findAndRemoveRecursive(\n    values: string[],\n    node: PrefixLookupNode,\n    prefix: string,\n    filter: (nodeValue: string) => boolean,\n  ): string[] {\n    for (let char of Object.keys(node)) {\n      this.#findAndRemoveRecursive(values, node[char], prefix + char, filter);\n    }\n\n    if (node[PrefixLookupTrieEndSymbol] && filter(prefix)) {\n      node[PrefixLookupTrieEndSymbol] = false;\n      values.push(prefix);\n    }\n\n    return values;\n  }\n}\n\nexport function flatRoutes(\n  appDirectory: string,\n  ignoredFilePatterns: string[] = [],\n  prefix = \"routes\",\n) {\n  let ignoredFileRegex = Array.from(new Set([\"**/.*\", ...ignoredFilePatterns]))\n    .map((re) => makeRe(re))\n    .filter((re: any): re is RegExp => !!re);\n  let routesDir = path.join(appDirectory, prefix);\n\n  let rootRoute = findFile(appDirectory, \"root\", routeModuleExts);\n\n  if (!rootRoute) {\n    throw new Error(\n      `Could not find a root route module in the app directory: ${appDirectory}`,\n    );\n  }\n\n  if (!fs.existsSync(routesDir)) {\n    throw new Error(\n      `Could not find the routes directory: ${routesDir}. Did you forget to create it?`,\n    );\n  }\n\n  // Only read the routes directory\n  let entries = fs.readdirSync(routesDir, {\n    withFileTypes: true,\n    encoding: \"utf-8\",\n  });\n\n  let routes: string[] = [];\n  for (let entry of entries) {\n    let filepath = normalizeSlashes(path.join(routesDir, entry.name));\n\n    let route: string | null = null;\n    // If it's a directory, don't recurse into it, instead just look for a route module\n    if (entry.isDirectory()) {\n      route = findRouteModuleForFolder(\n        appDirectory,\n        filepath,\n        ignoredFileRegex,\n      );\n    } else if (entry.isFile()) {\n      route = findRouteModuleForFile(appDirectory, filepath, ignoredFileRegex);\n    }\n\n    if (route) routes.push(route);\n  }\n\n  let routeManifest = flatRoutesUniversal(appDirectory, routes, prefix);\n  return routeManifest;\n}\n\nexport function flatRoutesUniversal(\n  appDirectory: string,\n  routes: string[],\n  prefix: string = \"routes\",\n): RouteManifest {\n  let urlConflicts = new Map<string, RouteManifestEntry[]>();\n  let routeManifest: RouteManifest = {};\n  let prefixLookup = new PrefixLookupTrie();\n  let uniqueRoutes = new Map<string, RouteManifestEntry>();\n  let routeIdConflicts = new Map<string, string[]>();\n  let normalizedApp = normalizeSlashes(appDirectory);\n  let appWithPrefix = path.posix.join(normalizedApp, prefix);\n\n  // id -> file\n  let routeIds = new Map<string, string>();\n\n  for (let file of routes) {\n    let normalizedFile = normalizeSlashes(file);\n    let routeExt = path.extname(normalizedFile);\n    let routeDir = path.dirname(normalizedFile);\n    let routeId =\n      routeDir === appWithPrefix\n        ? path.posix\n            .relative(normalizedApp, normalizedFile)\n            .slice(0, -routeExt.length)\n        : path.posix.relative(normalizedApp, routeDir);\n\n    let conflict = routeIds.get(routeId);\n    if (conflict) {\n      let currentConflicts = routeIdConflicts.get(routeId);\n      if (!currentConflicts) {\n        currentConflicts = [path.posix.relative(normalizedApp, conflict)];\n      }\n      currentConflicts.push(path.posix.relative(normalizedApp, normalizedFile));\n      routeIdConflicts.set(routeId, currentConflicts);\n      continue;\n    }\n\n    routeIds.set(routeId, normalizedFile);\n  }\n\n  let sortedRouteIds = Array.from(routeIds).sort(\n    ([a], [b]) => b.length - a.length,\n  );\n\n  for (let [routeId, file] of sortedRouteIds) {\n    let index = routeId.endsWith(\"_index\");\n    let [segments, raw] = getRouteSegments(routeId.slice(prefix.length + 1));\n    let pathname = createRoutePath(segments, raw, index);\n\n    routeManifest[routeId] = {\n      file: path.posix.relative(normalizedApp, file),\n      id: routeId,\n      path: pathname,\n    };\n    if (index) routeManifest[routeId].index = true;\n    let childRouteIds = prefixLookup.findAndRemove(routeId, (value) => {\n      return [\".\", \"/\"].includes(value.slice(routeId.length).charAt(0));\n    });\n    prefixLookup.add(routeId);\n\n    if (childRouteIds.length > 0) {\n      for (let childRouteId of childRouteIds) {\n        routeManifest[childRouteId].parentId = routeId;\n      }\n    }\n  }\n\n  // path creation\n  let parentChildrenMap = new Map<string, RouteManifestEntry[]>();\n  for (let [routeId] of sortedRouteIds) {\n    let config = routeManifest[routeId];\n    if (!config.parentId) continue;\n    let existingChildren = parentChildrenMap.get(config.parentId) || [];\n    existingChildren.push(config);\n    parentChildrenMap.set(config.parentId, existingChildren);\n  }\n\n  for (let [routeId] of sortedRouteIds) {\n    let config = routeManifest[routeId];\n    let originalPathname = config.path || \"\";\n    let pathname = config.path;\n    let parentConfig = config.parentId ? routeManifest[config.parentId] : null;\n    if (parentConfig?.path && pathname) {\n      pathname = pathname\n        .slice(parentConfig.path.length)\n        .replace(/^\\//, \"\")\n        .replace(/\\/$/, \"\");\n    }\n\n    if (!config.parentId) config.parentId = \"root\";\n    config.path = pathname || undefined;\n\n    /**\n     * We do not try to detect path collisions for pathless layout route\n     * files because, by definition, they create the potential for route\n     * collisions _at that level in the tree_.\n     *\n     * Consider example where a user may want multiple pathless layout routes\n     * for different subfolders\n     *\n     *   routes/\n     *     account.tsx\n     *     account._private.tsx\n     *     account._private.orders.tsx\n     *     account._private.profile.tsx\n     *     account._public.tsx\n     *     account._public.login.tsx\n     *     account._public.perks.tsx\n     *\n     * In order to support both a public and private layout for `/account/*`\n     * URLs, we are creating a mutually exclusive set of URLs beneath 2\n     * separate pathless layout routes.  In this case, the route paths for\n     * both account._public.tsx and account._private.tsx is the same\n     * (/account), but we're again not expecting to match at that level.\n     *\n     * By only ignoring this check when the final portion of the filename is\n     * pathless, we will still detect path collisions such as:\n     *\n     *   routes/parent._pathless.foo.tsx\n     *   routes/parent._pathless2.foo.tsx\n     *\n     * and\n     *\n     *   routes/parent._pathless/index.tsx\n     *   routes/parent._pathless2/index.tsx\n     */\n    let lastRouteSegment = config.id\n      .replace(new RegExp(`^${prefix}/`), \"\")\n      .split(\".\")\n      .pop();\n    let isPathlessLayoutRoute =\n      lastRouteSegment &&\n      lastRouteSegment.startsWith(\"_\") &&\n      lastRouteSegment !== \"_index\";\n    if (isPathlessLayoutRoute) {\n      continue;\n    }\n\n    let conflictRouteId = originalPathname + (config.index ? \"?index\" : \"\");\n    let conflict = uniqueRoutes.get(conflictRouteId);\n    uniqueRoutes.set(conflictRouteId, config);\n\n    if (conflict && (originalPathname || config.index)) {\n      let currentConflicts = urlConflicts.get(originalPathname);\n      if (!currentConflicts) currentConflicts = [conflict];\n      currentConflicts.push(config);\n      urlConflicts.set(originalPathname, currentConflicts);\n      continue;\n    }\n  }\n\n  if (routeIdConflicts.size > 0) {\n    for (let [routeId, files] of routeIdConflicts.entries()) {\n      console.error(getRouteIdConflictErrorMessage(routeId, files));\n    }\n  }\n\n  // report conflicts\n  if (urlConflicts.size > 0) {\n    for (let [path, routes] of urlConflicts.entries()) {\n      // delete all but the first route from the manifest\n      for (let i = 1; i < routes.length; i++) {\n        delete routeManifest[routes[i].id];\n      }\n      let files = routes.map((r) => r.file);\n      console.error(getRoutePathConflictErrorMessage(path, files));\n    }\n  }\n\n  return routeManifest;\n}\n\nfunction findRouteModuleForFile(\n  appDirectory: string,\n  filepath: string,\n  ignoredFileRegex: RegExp[],\n): string | null {\n  let relativePath = normalizeSlashes(path.relative(appDirectory, filepath));\n  let isIgnored = ignoredFileRegex.some((regex) => regex.test(relativePath));\n  if (isIgnored) return null;\n  return filepath;\n}\n\nfunction findRouteModuleForFolder(\n  appDirectory: string,\n  filepath: string,\n  ignoredFileRegex: RegExp[],\n): string | null {\n  let relativePath = path.relative(appDirectory, filepath);\n  let isIgnored = ignoredFileRegex.some((regex) => regex.test(relativePath));\n  if (isIgnored) return null;\n\n  let routeRouteModule = findFile(filepath, \"route\", routeModuleExts);\n  let routeIndexModule = findFile(filepath, \"index\", routeModuleExts);\n\n  // if both a route and index module exist, throw a conflict error\n  // preferring the route module over the index module\n  if (routeRouteModule && routeIndexModule) {\n    let [segments, raw] = getRouteSegments(\n      path.relative(appDirectory, filepath),\n    );\n    let routePath = createRoutePath(segments, raw, false);\n    console.error(\n      getRoutePathConflictErrorMessage(routePath || \"/\", [\n        routeRouteModule,\n        routeIndexModule,\n      ]),\n    );\n  }\n\n  return routeRouteModule || routeIndexModule || null;\n}\n\ntype State =\n  | // normal path segment normal character concatenation until we hit a special character or the end of the segment (i.e. `/`, `.`, '\\')\n  \"NORMAL\"\n  // we hit a `[` and are now in an escape sequence until we hit a `]` - take characters literally and skip isSegmentSeparator checks\n  | \"ESCAPE\"\n  // we hit a `(` and are now in an optional segment until we hit a `)` or an escape sequence\n  | \"OPTIONAL\"\n  // we previously were in a opt fional segment and hit a `[` and are now in an escape sequence until we hit a `]` - take characters literally and skip isSegmentSeparator checks - afterwards go back to OPTIONAL state\n  | \"OPTIONAL_ESCAPE\";\n\nexport function getRouteSegments(routeId: string): [string[], string[]] {\n  let routeSegments: string[] = [];\n  let rawRouteSegments: string[] = [];\n  let index = 0;\n  let routeSegment = \"\";\n  let rawRouteSegment = \"\";\n  let state: State = \"NORMAL\";\n\n  let pushRouteSegment = (segment: string, rawSegment: string) => {\n    if (!segment) return;\n\n    let notSupportedInRR = (segment: string, char: string) => {\n      throw new Error(\n        `Route segment \"${segment}\" for \"${routeId}\" cannot contain \"${char}\".\\n` +\n          `If this is something you need, upvote this proposal for React Router https://github.com/remix-run/react-router/discussions/9822.`,\n      );\n    };\n\n    if (rawSegment.includes(\"*\")) {\n      return notSupportedInRR(rawSegment, \"*\");\n    }\n\n    if (rawSegment.includes(\":\")) {\n      return notSupportedInRR(rawSegment, \":\");\n    }\n\n    if (rawSegment.includes(\"/\")) {\n      return notSupportedInRR(segment, \"/\");\n    }\n\n    routeSegments.push(segment);\n    rawRouteSegments.push(rawSegment);\n  };\n\n  while (index < routeId.length) {\n    let char = routeId[index];\n    index++; //advance to next char\n\n    switch (state) {\n      case \"NORMAL\": {\n        if (isSegmentSeparator(char)) {\n          pushRouteSegment(routeSegment, rawRouteSegment);\n          routeSegment = \"\";\n          rawRouteSegment = \"\";\n          state = \"NORMAL\";\n          break;\n        }\n        if (char === escapeStart) {\n          state = \"ESCAPE\";\n          rawRouteSegment += char;\n          break;\n        }\n        if (char === optionalStart) {\n          state = \"OPTIONAL\";\n          rawRouteSegment += char;\n          break;\n        }\n        if (!routeSegment && char === paramPrefixChar) {\n          if (index === routeId.length) {\n            routeSegment += \"*\";\n            rawRouteSegment += char;\n          } else {\n            routeSegment += \":\";\n            rawRouteSegment += char;\n          }\n          break;\n        }\n\n        routeSegment += char;\n        rawRouteSegment += char;\n        break;\n      }\n      case \"ESCAPE\": {\n        if (char === escapeEnd) {\n          state = \"NORMAL\";\n          rawRouteSegment += char;\n          break;\n        }\n\n        routeSegment += char;\n        rawRouteSegment += char;\n        break;\n      }\n      case \"OPTIONAL\": {\n        if (char === optionalEnd) {\n          routeSegment += \"?\";\n          rawRouteSegment += char;\n          state = \"NORMAL\";\n          break;\n        }\n\n        if (char === escapeStart) {\n          state = \"OPTIONAL_ESCAPE\";\n          rawRouteSegment += char;\n          break;\n        }\n\n        if (!routeSegment && char === paramPrefixChar) {\n          if (index === routeId.length) {\n            routeSegment += \"*\";\n            rawRouteSegment += char;\n          } else {\n            routeSegment += \":\";\n            rawRouteSegment += char;\n          }\n          break;\n        }\n\n        routeSegment += char;\n        rawRouteSegment += char;\n        break;\n      }\n      case \"OPTIONAL_ESCAPE\": {\n        if (char === escapeEnd) {\n          state = \"OPTIONAL\";\n          rawRouteSegment += char;\n          break;\n        }\n\n        routeSegment += char;\n        rawRouteSegment += char;\n        break;\n      }\n    }\n  }\n\n  // process remaining segment\n  pushRouteSegment(routeSegment, rawRouteSegment);\n  return [routeSegments, rawRouteSegments];\n}\n\nexport function createRoutePath(\n  routeSegments: string[],\n  rawRouteSegments: string[],\n  isIndex?: boolean,\n) {\n  let result: string[] = [];\n\n  if (isIndex) {\n    routeSegments = routeSegments.slice(0, -1);\n  }\n\n  for (let index = 0; index < routeSegments.length; index++) {\n    let segment = routeSegments[index];\n    let rawSegment = rawRouteSegments[index];\n\n    // skip pathless layout segments\n    if (segment.startsWith(\"_\") && rawSegment.startsWith(\"_\")) {\n      continue;\n    }\n\n    // remove trailing slash\n    if (segment.endsWith(\"_\") && rawSegment.endsWith(\"_\")) {\n      segment = segment.slice(0, -1);\n    }\n\n    result.push(segment);\n  }\n\n  return result.length ? result.join(\"/\") : undefined;\n}\n\nexport function getRoutePathConflictErrorMessage(\n  pathname: string,\n  routes: string[],\n) {\n  let [taken, ...others] = routes;\n\n  if (!pathname.startsWith(\"/\")) {\n    pathname = \"/\" + pathname;\n  }\n\n  return (\n    `⚠️ Route Path Collision: \"${pathname}\"\\n\\n` +\n    `The following routes all define the same URL, only the first one will be used\\n\\n` +\n    `🟢 ${taken}\\n` +\n    others.map((route) => `⭕️️ ${route}`).join(\"\\n\") +\n    \"\\n\"\n  );\n}\n\nexport function getRouteIdConflictErrorMessage(\n  routeId: string,\n  files: string[],\n) {\n  let [taken, ...others] = files;\n\n  return (\n    `⚠️ Route ID Collision: \"${routeId}\"\\n\\n` +\n    `The following routes all define the same Route ID, only the first one will be used\\n\\n` +\n    `🟢 ${taken}\\n` +\n    others.map((route) => `⭕️️ ${route}`).join(\"\\n\") +\n    \"\\n\"\n  );\n}\n\nexport function isSegmentSeparator(checkChar: string | undefined) {\n  if (!checkChar) return false;\n  return [\"/\", \".\", path.win32.sep].includes(checkChar);\n}\n\nfunction findFile(\n  dir: string,\n  basename: string,\n  extensions: string[],\n): string | undefined {\n  for (let ext of extensions) {\n    let name = basename + ext;\n    let file = path.join(dir, name);\n    if (fs.existsSync(file)) return file;\n  }\n\n  return undefined;\n}\n"
  },
  {
    "path": "packages/react-router-fs-routes/index.ts",
    "content": "import fs from \"node:fs\";\nimport path from \"node:path\";\nimport {\n  type RouteConfigEntry,\n  getAppDirectory,\n} from \"@react-router/dev/routes\";\n\nimport { routeManifestToRouteConfig } from \"./manifest\";\nimport { flatRoutes as flatRoutesImpl } from \"./flatRoutes\";\nimport { normalizeSlashes } from \"./normalizeSlashes\";\n\n/**\n * Creates route config from the file system using a convention that matches\n * [Remix v2's route file\n * naming](https://v2.remix.run/docs/file-conventions/routes), for use\n * within `routes.ts`.\n */\nexport async function flatRoutes(\n  options: {\n    /**\n     * An array of [minimatch](https://www.npmjs.com/package/minimatch) globs that match files to ignore.\n     * Defaults to `[]`.\n     */\n    ignoredRouteFiles?: string[];\n\n    /**\n     * The directory containing file system routes, relative to the app directory.\n     * Defaults to `\"./routes\"`.\n     */\n    rootDirectory?: string;\n  } = {},\n): Promise<RouteConfigEntry[]> {\n  let { ignoredRouteFiles = [], rootDirectory: userRootDirectory = \"routes\" } =\n    options;\n  let appDirectory = getAppDirectory();\n  let rootDirectory = path.resolve(appDirectory, userRootDirectory);\n  let relativeRootDirectory = path.relative(appDirectory, rootDirectory);\n  let prefix = normalizeSlashes(relativeRootDirectory);\n\n  let routes = fs.existsSync(rootDirectory)\n    ? flatRoutesImpl(appDirectory, ignoredRouteFiles, prefix)\n    : {};\n\n  return routeManifestToRouteConfig(routes);\n}\n"
  },
  {
    "path": "packages/react-router-fs-routes/jest.config.js",
    "content": "/** @type {import('jest').Config} */\nmodule.exports = {\n  ...require(\"../../jest/jest.config.shared\"),\n  displayName: \"fs-routes\",\n  setupFiles: [],\n};\n"
  },
  {
    "path": "packages/react-router-fs-routes/manifest.ts",
    "content": "import type { RouteConfigEntry } from \"@react-router/dev/routes\";\n\nexport interface RouteManifestEntry {\n  path?: string;\n  index?: boolean;\n  caseSensitive?: boolean;\n  id: string;\n  parentId?: string;\n  file: string;\n}\n\nexport interface RouteManifest {\n  [routeId: string]: RouteManifestEntry;\n}\n\nexport function routeManifestToRouteConfig(\n  routeManifest: RouteManifest,\n  rootId = \"root\",\n): RouteConfigEntry[] {\n  let routeConfigById: {\n    [id: string]: Omit<RouteConfigEntry, \"id\"> &\n      Required<Pick<RouteConfigEntry, \"id\">>;\n  } = {};\n\n  for (let id in routeManifest) {\n    let route = routeManifest[id];\n    routeConfigById[id] = {\n      id: route.id,\n      file: route.file,\n      path: route.path,\n      index: route.index,\n      caseSensitive: route.caseSensitive,\n    };\n  }\n\n  let routeConfig: RouteConfigEntry[] = [];\n\n  for (let id in routeConfigById) {\n    let route = routeConfigById[id];\n    let parentId = routeManifest[route.id].parentId;\n    if (parentId === rootId) {\n      routeConfig.push(route);\n    } else {\n      let parentRoute = parentId && routeConfigById[parentId];\n      if (parentRoute) {\n        parentRoute.children = parentRoute.children || [];\n        parentRoute.children.push(route);\n      }\n    }\n  }\n\n  return routeConfig;\n}\n"
  },
  {
    "path": "packages/react-router-fs-routes/normalizeSlashes.ts",
    "content": "import path from \"node:path\";\n\nexport function normalizeSlashes(file: string) {\n  return file.replaceAll(path.win32.sep, \"/\");\n}\n"
  },
  {
    "path": "packages/react-router-fs-routes/package.json",
    "content": "{\n  \"name\": \"@react-router/fs-routes\",\n  \"version\": \"7.13.1\",\n  \"description\": \"File system routing conventions for React Router, for use within routes.ts\",\n  \"bugs\": {\n    \"url\": \"https://github.com/remix-run/react-router/issues\"\n  },\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"https://github.com/remix-run/react-router\",\n    \"directory\": \"packages/react-router-fs-routes\"\n  },\n  \"license\": \"MIT\",\n  \"main\": \"dist/index.js\",\n  \"typings\": \"dist/index.d.ts\",\n  \"exports\": {\n    \".\": {\n      \"types\": \"./dist/index.d.ts\",\n      \"default\": \"./dist/index.js\"\n    },\n    \"./package.json\": \"./package.json\"\n  },\n  \"scripts\": {\n    \"build\": \"wireit\",\n    \"typecheck\": \"tsc\"\n  },\n  \"wireit\": {\n    \"build\": {\n      \"command\": \"tsup\",\n      \"files\": [\n        \"../../pnpm-workspace.yaml\",\n        \"*.ts\",\n        \"tsconfig.json\",\n        \"package.json\"\n      ],\n      \"output\": [\n        \"dist/**\"\n      ]\n    }\n  },\n  \"dependencies\": {\n    \"minimatch\": \"^9.0.0\"\n  },\n  \"devDependencies\": {\n    \"@react-router/dev\": \"workspace:*\",\n    \"tsup\": \"catalog:\",\n    \"typescript\": \"catalog:\",\n    \"wireit\": \"catalog:\"\n  },\n  \"peerDependencies\": {\n    \"@react-router/dev\": \"workspace:^\",\n    \"typescript\": \"^5.1.0\"\n  },\n  \"peerDependenciesMeta\": {\n    \"typescript\": {\n      \"optional\": true\n    }\n  },\n  \"engines\": {\n    \"node\": \">=20.0.0\"\n  },\n  \"files\": [\n    \"dist/\",\n    \"CHANGELOG.md\",\n    \"LICENSE.md\",\n    \"README.md\"\n  ]\n}\n"
  },
  {
    "path": "packages/react-router-fs-routes/tsconfig.json",
    "content": "{\n  \"include\": [\"**/*.ts\"],\n  \"exclude\": [\"dist\", \"__tests__\", \"node_modules\"],\n  \"compilerOptions\": {\n    \"lib\": [\"DOM\", \"DOM.Iterable\", \"ES2022\"],\n    \"target\": \"ES2022\",\n    \"module\": \"ES2022\",\n    \"skipLibCheck\": true,\n\n    \"moduleResolution\": \"Bundler\",\n    \"allowSyntheticDefaultImports\": true,\n    \"strict\": true,\n    \"jsx\": \"react\",\n    \"declaration\": true,\n    \"emitDeclarationOnly\": true,\n    \"rootDir\": \".\",\n    \"outDir\": \"./dist\"\n  }\n}\n"
  },
  {
    "path": "packages/react-router-fs-routes/tsup.config.ts",
    "content": "import { defineConfig } from \"tsup\";\n\n// @ts-ignore - out of scope\nimport { createBanner } from \"../../build.utils.js\";\n\nimport pkg from \"./package.json\";\n\nconst entry = [\"index.ts\"];\n\nexport default defineConfig([\n  {\n    clean: true,\n    entry,\n    format: [\"cjs\"],\n    outDir: \"dist\",\n    dts: true,\n    banner: {\n      js: createBanner(pkg.name, pkg.version),\n    },\n  },\n]);\n"
  },
  {
    "path": "packages/react-router-fs-routes/typedoc.mjs",
    "content": "import { blockTags } from \"../../typedoc.mjs\";\n\n/** @type {Partial<import('typedoc').TypeDocOptions>} */\nconst config = {\n  entryPoints: [\"./index.ts\"],\n  blockTags,\n};\n\nexport default config;\n"
  },
  {
    "path": "packages/react-router-node/.gitignore",
    "content": "# TODO: Remove in v2\nglobals.d.ts"
  },
  {
    "path": "packages/react-router-node/CHANGELOG.md",
    "content": "# `@react-router/node`\n\n## 7.13.1\n\n### Patch Changes\n\n- Updated dependencies:\n  - `react-router@7.13.1`\n\n## 7.13.0\n\n### Patch Changes\n\n- Updated dependencies:\n  - `react-router@7.13.0`\n\n## 7.12.0\n\n### Patch Changes\n\n- Updated dependencies:\n  - `react-router@7.12.0`\n\n## 7.11.0\n\n### Patch Changes\n\n- Updated dependencies:\n  - `react-router@7.11.0`\n\n## 7.10.1\n\n### Patch Changes\n\n- Updated dependencies:\n  - `react-router@7.10.1`\n\n## 7.10.0\n\n### Patch Changes\n\n- Updated dependencies:\n  - `react-router@7.10.0`\n\n## 7.9.6\n\n### Patch Changes\n\n- Updated dependencies:\n  - `react-router@7.9.6`\n\n## 7.9.5\n\n### Patch Changes\n\n- Updated dependencies:\n  - `react-router@7.9.5`\n\n## 7.9.4\n\n### Patch Changes\n\n- Validate format of incoming session ids in `createFileSessionStorage` ([#14426](https://github.com/remix-run/react-router/pull/14426))\n- Updated dependencies:\n  - `react-router@7.9.4`\n\n## 7.9.3\n\n### Patch Changes\n\n- Updated dependencies:\n  - `react-router@7.9.3`\n\n## 7.9.2\n\n### Patch Changes\n\n- Updated dependencies:\n  - `react-router@7.9.2`\n\n## 7.9.1\n\n### Patch Changes\n\n- Updated dependencies:\n  - `react-router@7.9.1`\n\n## 7.9.0\n\n### Minor Changes\n\n- Stabilize middleware and context APIs. ([#14215](https://github.com/remix-run/react-router/pull/14215))\n\n  We have removed the `unstable_` prefix from the following APIs and they are now considered stable and ready for production use:\n  - [`RouterContextProvider`](https://reactrouter.com/api/utils/RouterContextProvider)\n  - [`createContext`](https://reactrouter.com/api/utils/createContext)\n  - `createBrowserRouter` [`getContext`](https://reactrouter.com/api/data-routers/createBrowserRouter#optsgetcontext) option\n  - `<HydratedRouter>` [`getContext`](https://reactrouter.com/api/framework-routers/HydratedRouter#getcontext) prop\n\n  Please see the [Middleware Docs](https://reactrouter.com/how-to/middleware), the [Middleware RFC](https://github.com/remix-run/remix/discussions/7642), and the [Client-side Context RFC](https://github.com/remix-run/react-router/discussions/9856) for more information.\n\n### Patch Changes\n\n- Updated dependencies:\n  - `react-router@7.9.0`\n\n## 7.8.2\n\n### Patch Changes\n\n- Updated dependencies:\n  - `react-router@7.8.2`\n\n## 7.8.1\n\n### Patch Changes\n\n- Updated dependencies:\n  - `react-router@7.8.1`\n\n## 7.8.0\n\n### Patch Changes\n\n- \\[UNSTABLE] Change `getLoadContext` signature (`type GetLoadContextFunction`) when `future.unstable_middleware` is enabled so that it returns an `unstable_RouterContextProvider` instance instead of a `Map` used to contruct the instance internally ([#14097](https://github.com/remix-run/react-router/pull/14097))\n  - This also removes the `type unstable_InitialContext` export\n  - ⚠️ This is a breaking change if you have adopted middleware and are using a custom server with a `getLoadContext` function\n\n- Updated dependencies:\n  - `react-router@7.8.0`\n\n## 7.7.1\n\n### Patch Changes\n\n- Updated dependencies:\n  - `react-router@7.7.1`\n\n## 7.7.0\n\n### Patch Changes\n\n- Updated dependencies:\n  - `react-router@7.7.0`\n\n## 7.6.3\n\n### Patch Changes\n\n- Remove old \"install\" package exports ([#13762](https://github.com/remix-run/react-router/pull/13762))\n- Updated dependencies:\n  - `react-router@7.6.3`\n\n## 7.6.2\n\n### Patch Changes\n\n- Updated dependencies:\n  - `react-router@7.6.2`\n\n## 7.6.1\n\n### Patch Changes\n\n- Updated dependencies:\n  - `react-router@7.6.1`\n\n## 7.6.0\n\n### Patch Changes\n\n- Updated dependencies:\n  - `react-router@7.6.0`\n\n## 7.5.3\n\n### Patch Changes\n\n- Updated dependencies:\n  - `react-router@7.5.3`\n\n## 7.5.2\n\n### Patch Changes\n\n- Updated dependencies:\n  - `react-router@7.5.2`\n\n## 7.5.1\n\n### Patch Changes\n\n- Updated dependencies:\n  - `react-router@7.5.1`\n\n## 7.5.0\n\n### Patch Changes\n\n- Updated dependencies:\n  - `react-router@7.5.0`\n\n## 7.4.1\n\n### Patch Changes\n\n- Updated dependencies:\n  - `react-router@7.4.1`\n\n## 7.4.0\n\n### Patch Changes\n\n- Updated dependencies:\n  - `react-router@7.4.0`\n\n## 7.3.0\n\n### Patch Changes\n\n- Updated dependencies:\n  - `react-router@7.3.0`\n\n## 7.2.0\n\n### Patch Changes\n\n- Updated dependencies:\n  - `react-router@7.2.0`\n\n## 7.1.5\n\n### Patch Changes\n\n- Updated dependencies:\n  - `react-router@7.1.5`\n\n## 7.1.4\n\n### Patch Changes\n\n- Updated dependencies:\n  - `react-router@7.1.4`\n\n## 7.1.3\n\n### Patch Changes\n\n- Updated dependencies:\n  - `react-router@7.1.3`\n\n## 7.1.2\n\n### Patch Changes\n\n- Updated dependencies:\n  - `react-router@7.1.2`\n\n## 7.1.1\n\n### Patch Changes\n\n- Updated dependencies:\n  - `react-router@7.1.1`\n\n## 7.1.0\n\n### Patch Changes\n\n- Updated dependencies:\n  - `react-router@7.1.0`\n\n## 7.0.2\n\n### Patch Changes\n\n- Updated dependencies:\n  - `react-router@7.0.2`\n\n## 7.0.1\n\n### Patch Changes\n\n- Updated dependencies:\n  - `react-router@7.0.1`\n\n## 7.0.0\n\n### Major Changes\n\n- Remove single fetch future flag. ([#11522](https://github.com/remix-run/react-router/pull/11522))\n\n- For Remix consumers migrating to React Router, the `crypto` global from the [Web Crypto API](https://developer.mozilla.org/en-US/docs/Web/API/Web_Crypto_API) is now required when using cookie and session APIs. This means that the following APIs are provided from `react-router` rather than platform-specific packages: ([#11837](https://github.com/remix-run/react-router/pull/11837))\n  - `createCookie`\n  - `createCookieSessionStorage`\n  - `createMemorySessionStorage`\n  - `createSessionStorage`\n\n  For consumers running older versions of Node, the `installGlobals` function from `@remix-run/node` has been updated to define `globalThis.crypto`, using [Node's `require('node:crypto').webcrypto` implementation.](https://nodejs.org/api/webcrypto.html)\n\n  Since platform-specific packages no longer need to implement this API, the following low-level APIs have been removed:\n  - `createCookieFactory`\n  - `createSessionStorageFactory`\n  - `createCookieSessionStorageFactory`\n  - `createMemorySessionStorageFactory`\n\n- update minimum node version to 18 ([#11690](https://github.com/remix-run/react-router/pull/11690))\n\n- Add `exports` field to all packages ([#11675](https://github.com/remix-run/react-router/pull/11675))\n\n- node package no longer re-exports from react-router ([#11702](https://github.com/remix-run/react-router/pull/11702))\n\n- Drop support for Node 18, update minimum Node vestion to 20 ([#12171](https://github.com/remix-run/react-router/pull/12171))\n  - Remove `installGlobals()` as this should no longer be necessary\n\n### Patch Changes\n\n- Add createRequestListener to @react-router/node ([#12319](https://github.com/remix-run/react-router/pull/12319))\n- Remove unstable upload handler. ([#12015](https://github.com/remix-run/react-router/pull/12015))\n- Remove unneeded dependency on @web3-storage/multipart-parser ([#12274](https://github.com/remix-run/react-router/pull/12274))\n- Updated dependencies:\n  - `react-router@7.0.0`\n\n## 2.9.0\n\n### Minor Changes\n\n- Use undici as our fetch polyfill going forward ([#9106](https://github.com/remix-run/remix/pull/9106), [#9111](https://github.com/remix-run/remix/pull/9111))\n- Put `undici` fetch polyfill behind a new `installGlobals({ nativeFetch: true })` parameter ([#9198](https://github.com/remix-run/remix/pull/9198))\n  - `remix-serve` will default to using `undici` for the fetch polyfill if `future._unstable_singleFetch` is enabled because the single fetch implementation relies on the `undici` polyfill\n    - Any users opting into Single Fetch and managing their own polfill will need to pass the flag to `installGlobals` on their own to avoid runtime errors with Single Fetch\n\n### Patch Changes\n\n- Updated dependencies:\n  - `@remix-run/server-runtime@2.9.0`\n\n## 2.8.1\n\n### Patch Changes\n\n- Updated dependencies:\n  - `@remix-run/server-runtime@2.8.1`\n\n## 2.8.0\n\n### Patch Changes\n\n- Updated dependencies:\n  - `@remix-run/server-runtime@2.8.0`\n\n## 2.7.2\n\n### Patch Changes\n\n- Updated dependencies:\n  - `@remix-run/server-runtime@2.7.2`\n\n## 2.7.1\n\n### Patch Changes\n\n- Updated dependencies:\n  - `@remix-run/server-runtime@2.7.1`\n\n## 2.7.0\n\n### Patch Changes\n\n- Updated dependencies:\n  - `@remix-run/server-runtime@2.7.0`\n\n## 2.6.0\n\n### Patch Changes\n\n- Updated dependencies:\n  - `@remix-run/server-runtime@2.6.0`\n\n## 2.5.1\n\n### Patch Changes\n\n- Updated dependencies:\n  - `@remix-run/server-runtime@2.5.1`\n\n## 2.5.0\n\n### Patch Changes\n\n- Updated dependencies:\n  - `@remix-run/server-runtime@2.5.0`\n\n## 2.4.1\n\n### Patch Changes\n\n- Updated dependencies:\n  - `@remix-run/server-runtime@2.4.1`\n\n## 2.4.0\n\n### Minor Changes\n\n- Deprecate `DataFunctionArgs` in favor of `LoaderFunctionArgs`/`ActionFunctionArgs`. This is aimed at keeping the types aligned across server/client loaders/actions now that `clientLoader`/`clientActon` functions have `serverLoader`/`serverAction` parameters which differentiate `ClientLoaderFunctionArgs`/`ClientActionFunctionArgs`. ([#8173](https://github.com/remix-run/remix/pull/8173))\n\n### Patch Changes\n\n- Update to `@remix-run/web-fetch@4.4.2` ([#8231](https://github.com/remix-run/remix/pull/8231))\n- Updated dependencies:\n  - `@remix-run/server-runtime@2.4.0`\n\n## 2.3.1\n\n### Patch Changes\n\n- Updated dependencies:\n  - `@remix-run/server-runtime@2.3.1`\n\n## 2.3.0\n\n### Patch Changes\n\n- Updated dependencies:\n  - `@remix-run/server-runtime@2.3.0`\n\n## 2.2.0\n\n### Patch Changes\n\n- Updated dependencies:\n  - `@remix-run/server-runtime@2.2.0`\n\n## 2.1.0\n\n### Patch Changes\n\n- Updated dependencies:\n  - `@remix-run/server-runtime@2.1.0`\n\n## 2.0.1\n\n### Patch Changes\n\n- Switch from `crypto.randomBytes` to `crypto.webcrypto.getRandomValues` for file session storage ID generation ([#7203](https://github.com/remix-run/remix/pull/7203))\n- Use native `Blob` class instead of polyfill ([#7217](https://github.com/remix-run/remix/pull/7217))\n- Updated dependencies:\n  - `@remix-run/server-runtime@2.0.1`\n  - [`@remix-run/web-fetch@4.4.1`](https://github.com/remix-run/web-std-io/releases/tag/%40remix-run%2Fweb-fetch%404.4.1)\n\n## 2.0.0\n\n### Major Changes\n\n- Require Node >=18.0.0 ([#6939](https://github.com/remix-run/remix/pull/6939))\n\n- Stop exporting the `fetch` API in favor of using the version in the global scope - which can be polyfilled via `installGlobals` ([#7293](https://github.com/remix-run/remix/pull/7293))\n\n- Removed/adjusted types to prefer `unknown` over `any` and to align with underlying React Router types ([#7319](https://github.com/remix-run/remix/pull/7319), [#7354](https://github.com/remix-run/remix/pull/7354)):\n  - Renamed the `useMatches()` return type from `RouteMatch` to `UIMatch`\n  - Renamed `LoaderArgs`/`ActionArgs` to `LoaderFunctionArgs`/`ActionFunctionArgs`\n  - `AppData` changed from `any` to `unknown`\n  - `Location[\"state\"]` (`useLocation.state`) changed from `any` to `unknown`\n  - `UIMatch[\"data\"]` (`useMatches()[i].data`) changed from `any` to `unknown`\n  - `UIMatch[\"handle\"]` (`useMatches()[i].handle`) changed from `{ [k: string]: any }` to `unknown`\n  - `Fetcher[\"data\"]` (`useFetcher().data`) changed from `any` to `unknown`\n  - `MetaMatch.handle` (used in `meta()`) changed from `any` to `unknown`\n  - `AppData`/`RouteHandle` are no longer exported as they are just aliases for `unknown`\n\n- The route `meta` API now defaults to the new \"V2 Meta\" API ([#6958](https://github.com/remix-run/remix/pull/6958))\n  - Please refer to the ([docs](https://remix.run/docs/en/2.0.0/route/meta) and [Preparing for V2](https://remix.run/docs/en/2.0.0/start/v2#route-meta) guide for more information.\n\n- For preparation of using Node's built in fetch implementation, installing the fetch globals is now a responsibility of the app server ([#7009](https://github.com/remix-run/remix/pull/7009))\n  - If you are using `remix-serve`, nothing is required\n  - If you are using your own app server, you will need to install the globals yourself\n\n    ```js filename=server.js\n    import { installGlobals } from \"@remix-run/node\";\n\n    installGlobals();\n    ```\n\n- `source-map-support` is now a responsibility of the app server ([#7009](https://github.com/remix-run/remix/pull/7009))\n  - If you are using `remix-serve`, nothing is required\n  - If you are using your own app server, you will need to install [`source-map-support`](https://www.npmjs.com/package/source-map-support) yourself.\n\n    ```sh\n    npm i source-map-support\n    ```\n\n    ```js filename=server.js\n    import sourceMapSupport from \"source-map-support\";\n    sourceMapSupport.install();\n    ```\n\n- Removed support for \"magic exports\" from the `remix` package. This package can be removed from your `package.json` and you should update all imports to use the source `@remix-run/*` packages: ([#6895](https://github.com/remix-run/remix/pull/6895))\n\n  ```diff\n  - import type { ActionArgs } from \"remix\";\n  - import { json, useLoaderData } from \"remix\";\n  + import type { ActionArgs } from \"@remix-run/node\";\n  + import { json } from \"@remix-run/node\";\n  + import { useLoaderData } from \"@remix-run/react\";\n  ```\n\n### Minor Changes\n\n- Re-export the new `redirectDocument` method from React Router ([#7040](https://github.com/remix-run/remix/pull/7040), [#6842](https://github.com/remix-run/remix/pull/6842)) ([#7040](https://github.com/remix-run/remix/pull/7040))\n\n### Patch Changes\n\n- Remove `atob`/`btoa` polyfills in favor of built-in versions ([#7206](https://github.com/remix-run/remix/pull/7206))\n- Export proper `ErrorResponse` type for usage alongside `isRouteErrorResponse` ([#7244](https://github.com/remix-run/remix/pull/7244))\n- Add the rest of the Web Streams API to `installGlobals` ([#7321](https://github.com/remix-run/remix/pull/7321))\n- Ensures `fetch()` return is `instanceof global Response` by removing extended classes for `NodeRequest` and `NodeResponse` in favor of custom interface type cast ([#7109](https://github.com/remix-run/remix/pull/7109))\n- Remove recursion from stream utilities ([#7245](https://github.com/remix-run/remix/pull/7245))\n- Updated dependencies:\n  - `@remix-run/server-runtime@2.0.0`\n  - `@remix-run/web-fetch@4.4.0`\n  - `@remix-run/web-file@3.1.0`\n  - `@remix-run/web-stream@1.1.0`\n\n## 1.19.3\n\n### Patch Changes\n\n- Updated dependencies:\n  - `@remix-run/server-runtime@1.19.3`\n\n## 1.19.2\n\n### Patch Changes\n\n- Update to latest `@remix-run/web-*` packages ([#7026](https://github.com/remix-run/remix/pull/7026))\n- Updated dependencies:\n  - `@remix-run/server-runtime@1.19.2`\n\n## 1.19.1\n\n### Patch Changes\n\n- Updated dependencies:\n  - `@remix-run/server-runtime@1.19.1`\n\n## 1.19.0\n\n### Patch Changes\n\n- Upgrade to [`@remix-run/web-fetch@4.3.5`](https://github.com/remix-run/web-std-io/releases/tag/%40remix-run%2Fweb-fetch%404.3.5). Submitted empty file inputs are now correctly parsed out as empty `File` instances instead of being surfaced as an empty string via `request.formData()` ([#6816](https://github.com/remix-run/remix/pull/6816))\n- Updated dependencies:\n  - `@remix-run/server-runtime@1.19.0`\n\n## 1.18.1\n\n### Patch Changes\n\n- Updated dependencies:\n  - `@remix-run/server-runtime@1.18.1`\n\n## 1.18.0\n\n### Patch Changes\n\n- Updated dependencies:\n  - `@remix-run/server-runtime@1.18.0`\n\n## 1.17.1\n\n### Patch Changes\n\n- Updated dependencies:\n  - `@remix-run/server-runtime@1.17.1`\n\n## 1.17.0\n\n### Patch Changes\n\n- Add `HeadersArgs` type to be consistent with loaders/actions/meta and allows for using a `function` declaration in addition to an arrow function expression ([#6247](https://github.com/remix-run/remix/pull/6247))\n\n  ```tsx\n  import type { HeadersArgs } from \"@remix-run/node\"; // or cloudflare/deno\n\n  export function headers({ loaderHeaders }: HeadersArgs) {\n    return {\n      \"x-my-custom-thing\": loaderHeaders.get(\"x-my-custom-thing\") || \"fallback\",\n    };\n  }\n  ```\n\n- Fix `request.clone() instanceof Request` returning false. ([#6512](https://github.com/remix-run/remix/pull/6512))\n\n- Updated dependencies:\n  - `@remix-run/server-runtime@1.17.0`\n\n## 1.16.1\n\n### Patch Changes\n\n- Updated dependencies:\n  - `@remix-run/server-runtime@1.16.1`\n\n## 1.16.0\n\n### Patch Changes\n\n- add `@remix-run/node/install` side-effect to allow `node --require @remix-run/node/install` ([#6132](https://github.com/remix-run/remix/pull/6132))\n- add `logDevReady` as replacement for platforms that can't initialize async I/O outside of the request response lifecycle. ([#6204](https://github.com/remix-run/remix/pull/6204))\n- add missing files to published package ([#6179](https://github.com/remix-run/remix/pull/6179))\n- Updated dependencies:\n  - `@remix-run/server-runtime@1.16.0`\n\n## 1.15.0\n\n### Minor Changes\n\n- We have made a few changes to the API for route module `meta` functions when using the `future.v2_meta` flag. **These changes are _only_ breaking for users who have opted in.** ([#5746](https://github.com/remix-run/remix/pull/5746))\n  - `V2_HtmlMetaDescriptor` has been renamed to `V2_MetaDescriptor`\n  - The `meta` function's arguments have been simplified\n    - `parentsData` has been removed, as each route's loader data is available on the `data` property of its respective `match` object\n      ```tsx\n      // before\n      export function meta({ parentsData }) {\n        return [{ title: parentsData[\"routes/some-route\"].title }];\n      }\n      // after\n      export function meta({ matches }) {\n        return [\n          {\n            title: matches.find((match) => match.id === \"routes/some-route\")\n              .data.title,\n          },\n        ];\n      }\n      ```\n    - The `route` property on route matches has been removed, as relevant match data is attached directly to the match object\n      ```tsx\n      // before\n      export function meta({ matches }) {\n        const rootModule = matches.find((match) => match.route.id === \"root\");\n      }\n      // after\n      export function meta({ matches }) {\n        const rootModule = matches.find((match) => match.id === \"root\");\n      }\n      ```\n  - Added support for generating `<script type='application/ld+json' />` and meta-related `<link />` tags to document head via the route `meta` function when using the `v2_meta` future flag\n\n### Patch Changes\n\n- Updated dependencies:\n  - `@remix-run/server-runtime@1.15.0`\n\n## 1.14.3\n\n### Patch Changes\n\n- Updated dependencies:\n  - `@remix-run/server-runtime@1.14.3`\n\n## 1.14.2\n\n### Patch Changes\n\n- Updated dependencies:\n  - `@remix-run/server-runtime@1.14.2`\n\n## 1.14.1\n\n### Patch Changes\n\n- Updated dependencies:\n  - `@remix-run/server-runtime@1.14.1`\n\n## 1.14.0\n\n### Patch Changes\n\n- Updated dependencies:\n  - `@remix-run/server-runtime@1.14.0`\n\n## 1.13.0\n\n### Patch Changes\n\n- Updated dependencies:\n  - `@remix-run/server-runtime@1.13.0`\n\n## 1.12.0\n\n### Patch Changes\n\n- Updated dependencies:\n  - `@remix-run/server-runtime@1.12.0`\n\n## 1.11.1\n\n### Patch Changes\n\n- Updated dependencies:\n  - `@remix-run/server-runtime@1.11.1`\n\n## 1.11.0\n\n### Patch Changes\n\n- Introduces the `defer()` API from `@remix-run/router` with support for server-rendering and HTTP streaming. This utility allows you to defer values returned from `loader` functions by returning promises instead of resolved values. This has been refered to as _\"sending a promise over the wire\"_. ([#4920](https://github.com/remix-run/remix/pull/4920))\n\n  Informational Resources:\n  - <https://gist.github.com/jacob-ebey/9bde9546c1aafaa6bc8c242054b1be26>\n  - <https://github.com/remix-run/remix/blob/main/decisions/0004-streaming-apis.md>\n\n  Documentation Resources (better docs specific to Remix are in the works):\n  - <https://reactrouter.com/en/main/utils/defer>\n  - <https://reactrouter.com/en/main/components/await>\n  - <https://reactrouter.com/en/main/hooks/use-async-value>\n  - <https://reactrouter.com/en/main/hooks/use-async-error>\n\n- Updated dependencies:\n  - `@remix-run/server-runtime@1.11.0`\n\n## 1.10.1\n\n### Patch Changes\n\n- Updated dependencies:\n  - `@remix-run/server-runtime@1.10.1`\n\n## 1.10.0\n\n### Patch Changes\n\n- Export `V2_HtmlMetaDescriptor` and `V2_MetaFunction` types from runtime packages ([#4943](https://github.com/remix-run/remix/pull/4943))\n- Updated dependencies:\n  - `@remix-run/server-runtime@1.10.0`\n\n## 1.9.0\n\n### Patch Changes\n\n- Updated dependencies:\n  - `@remix-run/server-runtime@1.9.0`\n\n## 1.8.2\n\n### Patch Changes\n\n- Updated dependencies:\n  - `@remix-run/server-runtime@1.8.2`\n\n## 1.8.1\n\n### Patch Changes\n\n- Updated dependencies:\n  - `@remix-run/server-runtime@1.8.1`\n\n## 1.8.0\n\n### Minor Changes\n\n- Importing functions and types from the `remix` package is deprecated, and all ([#3284](https://github.com/remix-run/remix/pull/3284))\n  exported modules will be removed in the next major release. For more details,\n  [see the release notes for 1.4.0](https://github.com/remix-run/remix/releases/tag/v1.4.0)\n  where these changes were first announced.\n\n### Patch Changes\n\n- Update `@remix-run/web-fetch`. This addresses two bugs: ([#4644](https://github.com/remix-run/remix/pull/4644))\n  - It fixes a memory leak caused by unregistered listeners\n  - It adds support for custom `\"credentials\"` values (Remix does nothing with these at the moment, but they pass through for the consumer of the request to access if needed)\n- Updated dependencies:\n  - `@remix-run/server-runtime@1.8.0`\n\n## 1.7.6\n\n### Patch Changes\n\n- Updated dependencies:\n  - `@remix-run/server-runtime@1.7.6`\n\n## 1.7.5\n\n### Patch Changes\n\n- Updated dependencies:\n  - `@remix-run/server-runtime@1.7.5`\n\n## 1.7.4\n\n### Patch Changes\n\n- Updated dependencies:\n  - `@remix-run/server-runtime@1.7.4`\n\n## 1.7.3\n\n### Patch Changes\n\n- Updated dependencies:\n  - `@remix-run/server-runtime@1.7.3`\n  - `@remix-run/web-fetch@4.3.1`\n\n## 1.7.2\n\n### Patch Changes\n\n- Flush Node streams to address issues with libraries like `compression` that rely on chunk flushing ([#4235](https://github.com/remix-run/remix/pull/4235))\n- Updated dependencies:\n  - `@remix-run/server-runtime@1.7.2`\n\n## 1.7.1\n\n### Patch Changes\n\n- Updated dependencies:\n  - `@remix-run/server-runtime@1.7.1`\n\n## 1.7.0\n\n### Minor Changes\n\n- We've added a new type: `SerializeFrom`. This is used to infer the ([#4013](https://github.com/remix-run/remix/pull/4013))\n  JSON-serialized return type of loaders and actions.\n\n### Patch Changes\n\n- Fixed a bug when destroying `fileStorage` sessions to prevent deleting entire session directories\n- Updated dependencies:\n  - `@remix-run/server-runtime@1.7.0`\n\n## 1.6.8\n\n### Patch Changes\n\n- Updated dependencies:\n  - `@remix-run/server-runtime@1.6.8`\n\n## 1.6.7\n\n### Patch Changes\n\n- Updated dependencies:\n  - `@remix-run/server-runtime@1.6.7`\n\n## 1.6.6\n\n### Patch Changes\n\n- Updated dependencies:\n  - `@remix-run/server-runtime@1.6.6`\n\n## 1.6.5\n\n### Patch Changes\n\n- We enhanced the type signatures of `loader`/`action` and\n  `useLoaderData`/`useActionData` to make it possible to infer the data type\n  from return type of its related server function.\n\n  To enable this feature, you will need to use the `LoaderArgs` type from\n  `@remix-run/node` instead of typing the function directly:\n\n  ```diff\n  - import type { LoaderFunction } from \"@remix-run/node\";\n  + import type { LoaderArgs } from \"@remix-run/node\";\n\n  - export const loader: LoaderFunction = async (args) => {\n  -   return json<LoaderData>(data);\n  - }\n  + export async function loader(args: LoaderArgs) {\n  +   return json(data);\n  + }\n  ```\n\n  Then you can infer the loader data by using `typeof loader` as the type\n  variable in `useLoaderData`:\n\n  ```diff\n  - let data = useLoaderData() as LoaderData;\n  + let data = useLoaderData<typeof loader>();\n  ```\n\n  The API above is exactly the same for your route `action` and `useActionData`\n  via the `ActionArgs` type.\n\n  With this change you no longer need to manually define a `LoaderData` type\n  (huge time and typo saver!), and we serialize all values so that\n  `useLoaderData` can't return types that are impossible over the network, such\n  as `Date` objects or functions.\n\n  See the discussions in [#1254](https://github.com/remix-run/remix/pull/1254)\n  and [#3276](https://github.com/remix-run/remix/pull/3276) for more context.\n\n- Updated dependencies\n  - `@remix-run/server-runtime`\n"
  },
  {
    "path": "packages/react-router-node/README.md",
    "content": "Node.js platform abstractions for React Router\n\n```sh\nnpm install @react-router/node\n```\n"
  },
  {
    "path": "packages/react-router-node/__tests__/sessions-test.ts",
    "content": "/**\n * @jest-environment node\n */\n\nimport path from \"node:path\";\nimport { promises as fsp } from \"node:fs\";\nimport os from \"node:os\";\n\nimport { createFileSessionStorage, getFile } from \"../sessions/fileStorage\";\n\nfunction getCookieFromSetCookie(setCookie: string): string {\n  return setCookie.split(/;\\s*/)[0];\n}\n\ndescribe(\"File session storage\", () => {\n  let dir = path.join(os.tmpdir(), \"file-session-storage\");\n\n  beforeAll(async () => {\n    await fsp.mkdir(dir, { recursive: true });\n  });\n\n  afterAll(async () => {\n    await fsp.rm(dir, { recursive: true, force: true });\n  });\n\n  it(\"persists session data across requests\", async () => {\n    let { getSession, commitSession } = createFileSessionStorage({\n      dir,\n      cookie: { secrets: [\"secret1\"] },\n    });\n    let session = await getSession();\n    session.set(\"user\", \"mjackson\");\n    let setCookie = await commitSession(session);\n    session = await getSession(getCookieFromSetCookie(setCookie));\n\n    expect(session.get(\"user\")).toEqual(\"mjackson\");\n  });\n\n  it(\"returns an empty session for cookies that are not signed properly\", async () => {\n    let { getSession, commitSession } = createFileSessionStorage({\n      dir,\n      cookie: { secrets: [\"secret1\"] },\n    });\n    let session = await getSession();\n    session.set(\"user\", \"mjackson\");\n\n    expect(session.get(\"user\")).toBe(\"mjackson\");\n\n    let setCookie = await commitSession(session);\n    session = await getSession(\n      // Tamper with the cookie...\n      getCookieFromSetCookie(setCookie).slice(0, -1),\n    );\n\n    expect(session.get(\"user\")).toBeUndefined();\n  });\n\n  it(\"returns an empty session for invalid session ids\", async () => {\n    let spy = jest.spyOn(console, \"warn\").mockImplementation(() => {});\n    let { getSession, commitSession } = createFileSessionStorage({\n      dir,\n    });\n\n    let cookie = `__session=${btoa(JSON.stringify(\"0123456789abcdef\"))}`;\n    let session = await getSession(cookie);\n    session.set(\"user\", \"mjackson\");\n    expect(session.get(\"user\")).toBe(\"mjackson\");\n    let setCookie = await commitSession(session);\n    session = await getSession(getCookieFromSetCookie(setCookie));\n    expect(session.get(\"user\")).toBe(\"mjackson\");\n\n    cookie = `__session=${btoa(JSON.stringify(\"0123456789abcdeg\"))}`;\n    session = await getSession(cookie);\n    session.set(\"user\", \"mjackson\");\n    expect(session.get(\"user\")).toBe(\"mjackson\");\n\n    setCookie = await commitSession(session);\n    session = await getSession(getCookieFromSetCookie(setCookie));\n    expect(session.get(\"user\")).toBeUndefined();\n\n    spy.mockRestore();\n  });\n\n  it(\"doesn't destroy the entire session directory when destroying an empty file session\", async () => {\n    let { getSession, destroySession } = createFileSessionStorage({\n      dir,\n      cookie: { secrets: [\"secret1\"] },\n    });\n\n    let session = await getSession();\n\n    await expect(destroySession(session)).resolves.not.toThrow();\n  });\n\n  it(\"saves expires to file if expires provided to commitSession when creating new cookie\", async () => {\n    let { getSession, commitSession } = createFileSessionStorage({\n      dir,\n      cookie: { secrets: [\"secret1\"] },\n    });\n    let session = await getSession();\n    session.set(\"user\", \"mjackson\");\n    let date = new Date(Date.now() + 1000 * 60);\n    let cookieHeader = await commitSession(session, { expires: date });\n    let createdSession = await getSession(cookieHeader);\n\n    let { id } = createdSession;\n    let fileContents = await fsp.readFile(getFile(dir, id), \"utf8\");\n    let fileData = JSON.parse(fileContents);\n    expect(fileData.expires).toEqual(date.toISOString());\n  });\n\n  it(\"saves expires to file if maxAge provided to commitSession when creating new cookie\", async () => {\n    let { getSession, commitSession } = createFileSessionStorage({\n      dir,\n      cookie: { secrets: [\"secret1\"] },\n    });\n    let session = await getSession();\n    session.set(\"user\", \"mjackson\");\n    let cookieHeader = await commitSession(session, { maxAge: 60 });\n    let createdSession = await getSession(cookieHeader);\n\n    let { id } = createdSession;\n    let fileContents = await fsp.readFile(getFile(dir, id), \"utf8\");\n    let fileData = JSON.parse(fileContents);\n    expect(typeof fileData.expires).toBe(\"string\");\n  });\n\n  describe(\"when a new secret shows up in the rotation\", () => {\n    it(\"unsigns old session cookies using the old secret and encodes new cookies using the new secret\", async () => {\n      let { getSession, commitSession } = createFileSessionStorage({\n        dir,\n        cookie: { secrets: [\"secret1\"] },\n      });\n      let session = await getSession();\n      session.set(\"user\", \"mjackson\");\n      let setCookie = await commitSession(session);\n      session = await getSession(getCookieFromSetCookie(setCookie));\n\n      expect(session.get(\"user\")).toEqual(\"mjackson\");\n\n      // A new secret enters the rotation...\n      let storage = createFileSessionStorage({\n        dir,\n        cookie: { secrets: [\"secret2\", \"secret1\"] },\n      });\n      getSession = storage.getSession;\n      commitSession = storage.commitSession;\n\n      // Old cookies should still work with the old secret.\n      session = await getSession(getCookieFromSetCookie(setCookie));\n      expect(session.get(\"user\")).toEqual(\"mjackson\");\n\n      // New cookies should be signed using the new secret.\n      let setCookie2 = await commitSession(session);\n      expect(setCookie2).not.toEqual(setCookie);\n    });\n  });\n});\n"
  },
  {
    "path": "packages/react-router-node/index.ts",
    "content": "export { type RequestListenerOptions, createRequestListener } from \"./server\";\n\nexport { createFileSessionStorage } from \"./sessions/fileStorage\";\n\nexport {\n  createReadableStreamFromReadable,\n  readableStreamToString,\n  writeAsyncIterableToWritable,\n  writeReadableStreamToWritable,\n} from \"./stream\";\n"
  },
  {
    "path": "packages/react-router-node/jest.config.js",
    "content": "/** @type {import('jest').Config} */\nmodule.exports = {\n  ...require(\"../../jest/jest.config.shared\"),\n  displayName: \"node\",\n};\n"
  },
  {
    "path": "packages/react-router-node/package.json",
    "content": "{\n  \"name\": \"@react-router/node\",\n  \"version\": \"7.13.1\",\n  \"description\": \"Node.js platform abstractions for React Router\",\n  \"bugs\": {\n    \"url\": \"https://github.com/remix-run/react-router/issues\"\n  },\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"https://github.com/remix-run/react-router\",\n    \"directory\": \"packages/react-router-node\"\n  },\n  \"license\": \"MIT\",\n  \"types\": \"dist/index.d.ts\",\n  \"main\": \"dist/index.js\",\n  \"exports\": {\n    \".\": {\n      \"node\": {\n        \"types\": \"./dist/index.d.ts\",\n        \"module-sync\": \"./dist/index.mjs\",\n        \"default\": \"./dist/index.js\"\n      },\n      \"import\": {\n        \"types\": \"./dist/index.d.mts\",\n        \"default\": \"./dist/index.mjs\"\n      },\n      \"default\": {\n        \"types\": \"./dist/index.d.ts\",\n        \"default\": \"./dist/index.js\"\n      }\n    },\n    \"./package.json\": \"./package.json\"\n  },\n  \"scripts\": {\n    \"build\": \"wireit\",\n    \"typecheck\": \"tsc\"\n  },\n  \"wireit\": {\n    \"build\": {\n      \"command\": \"tsup\",\n      \"files\": [\n        \"../../pnpm-workspace.yaml\",\n        \"sessions/**\",\n        \"*.ts\",\n        \"tsconfig.json\",\n        \"package.json\"\n      ],\n      \"output\": [\n        \"dist/**\"\n      ]\n    }\n  },\n  \"dependencies\": {\n    \"@mjackson/node-fetch-server\": \"^0.2.0\"\n  },\n  \"devDependencies\": {\n    \"react-router\": \"workspace:*\",\n    \"tsup\": \"catalog:\",\n    \"typescript\": \"catalog:\",\n    \"wireit\": \"catalog:\"\n  },\n  \"peerDependencies\": {\n    \"react-router\": \"workspace:*\",\n    \"typescript\": \"^5.1.0\"\n  },\n  \"peerDependenciesMeta\": {\n    \"typescript\": {\n      \"optional\": true\n    }\n  },\n  \"engines\": {\n    \"node\": \">=20.0.0\"\n  },\n  \"files\": [\n    \"dist/\",\n    \"CHANGELOG.md\",\n    \"LICENSE.md\",\n    \"README.md\"\n  ]\n}\n"
  },
  {
    "path": "packages/react-router-node/server.ts",
    "content": "import type { RequestListener } from \"node:http\";\n\nimport type {\n  AppLoadContext,\n  ServerBuild,\n  UNSAFE_MiddlewareEnabled,\n  RouterContextProvider,\n} from \"react-router\";\nimport { createRequestHandler } from \"react-router\";\nimport type { ClientAddress } from \"@mjackson/node-fetch-server\";\nimport { createRequestListener as createRequestListener_ } from \"@mjackson/node-fetch-server\";\n\ntype MaybePromise<T> = T | Promise<T>;\n\nexport interface RequestListenerOptions {\n  build: ServerBuild | (() => ServerBuild | Promise<ServerBuild>);\n  getLoadContext?: (\n    request: Request,\n    client: ClientAddress,\n  ) => UNSAFE_MiddlewareEnabled extends true\n    ? MaybePromise<RouterContextProvider>\n    : MaybePromise<AppLoadContext>;\n  mode?: string;\n}\n\n/**\n * Creates a request listener that handles requests using Node's built-in HTTP server.\n *\n * @param options Options for creating a request listener.\n * @returns A request listener that can be used with `http.createServer`.\n */\nexport function createRequestListener(\n  options: RequestListenerOptions,\n): RequestListener {\n  let handleRequest = createRequestHandler(options.build, options.mode);\n\n  return createRequestListener_(async (request, client) => {\n    let loadContext = await options.getLoadContext?.(request, client);\n    return handleRequest(request, loadContext);\n  });\n}\n"
  },
  {
    "path": "packages/react-router-node/sessions/fileStorage.ts",
    "content": "import { promises as fsp } from \"node:fs\";\nimport * as path from \"node:path\";\nimport type {\n  SessionStorage,\n  SessionIdStorageStrategy,\n  SessionData,\n} from \"react-router\";\nimport { createSessionStorage } from \"react-router\";\n\ninterface FileSessionStorageOptions {\n  /**\n   * The Cookie used to store the session id on the client, or options used\n   * to automatically create one.\n   */\n  cookie?: SessionIdStorageStrategy[\"cookie\"];\n\n  /**\n   * The directory to use to store session files.\n   */\n  dir: string;\n}\n\n/**\n * Creates a SessionStorage that stores session data on a filesystem.\n *\n * The advantage of using this instead of cookie session storage is that\n * files may contain much more data than cookies.\n *\n * @see https://api.reactrouter.com/v7/functions/_react-router_node.createFileSessionStorage\n */\nexport function createFileSessionStorage<Data = SessionData, FlashData = Data>({\n  cookie,\n  dir,\n}: FileSessionStorageOptions): SessionStorage<Data, FlashData> {\n  return createSessionStorage({\n    cookie,\n    async createData(data, expires) {\n      let content = JSON.stringify({ data, expires });\n\n      while (true) {\n        let randomBytes = crypto.getRandomValues(new Uint8Array(8));\n        // This storage manages an id space of 2^64 ids, which is far greater\n        // than the maximum number of files allowed on an NTFS or ext4 volume\n        // (2^32). However, the larger id space should help to avoid collisions\n        // with existing ids when creating new sessions, which speeds things up.\n        let id = Buffer.from(randomBytes).toString(\"hex\");\n\n        try {\n          let file = getFile(dir, id);\n          if (!file) {\n            throw new Error(\"Error generating session\");\n          }\n          await fsp.mkdir(path.dirname(file), { recursive: true });\n          await fsp.writeFile(file, content, { encoding: \"utf-8\", flag: \"wx\" });\n          return id;\n        } catch (error: any) {\n          if (error.code !== \"EEXIST\") throw error;\n        }\n      }\n    },\n    async readData(id) {\n      try {\n        let file = getFile(dir, id);\n        if (!file) {\n          return null;\n        }\n        let content = JSON.parse(await fsp.readFile(file, \"utf-8\"));\n        let data = content.data;\n        let expires =\n          typeof content.expires === \"string\"\n            ? new Date(content.expires)\n            : null;\n\n        if (!expires || expires > new Date()) {\n          return data;\n        }\n\n        // Remove expired session data.\n        if (expires) await fsp.unlink(file);\n\n        return null;\n      } catch (error: any) {\n        if (error.code !== \"ENOENT\") throw error;\n        return null;\n      }\n    },\n    async updateData(id, data, expires) {\n      let content = JSON.stringify({ data, expires });\n      let file = getFile(dir, id);\n      if (!file) {\n        return;\n      }\n      await fsp.mkdir(path.dirname(file), { recursive: true });\n      await fsp.writeFile(file, content, \"utf-8\");\n    },\n    async deleteData(id) {\n      // Return early if the id is empty, otherwise we'll end up trying to\n      // unlink the dir, which will cause the EPERM error.\n      if (!id) {\n        return;\n      }\n      let file = getFile(dir, id);\n      if (!file) {\n        return;\n      }\n      try {\n        await fsp.unlink(file);\n      } catch (error: any) {\n        if (error.code !== \"ENOENT\") throw error;\n      }\n    },\n  });\n}\n\nexport function getFile(dir: string, id: string): string | null {\n  if (!/^[0-9a-f]{16}$/i.test(id)) {\n    return null;\n  }\n\n  // Divide the session id up into a directory (first 2 bytes) and filename\n  // (remaining 6 bytes) to reduce the chance of having very large directories,\n  // which should speed up file access. This is a maximum of 2^16 directories,\n  // each with 2^48 files.\n  return path.join(dir, id.slice(0, 4), id.slice(4));\n}\n"
  },
  {
    "path": "packages/react-router-node/stream.ts",
    "content": "import type { Readable, Writable } from \"node:stream\";\nimport { Stream } from \"node:stream\";\n\nexport async function writeReadableStreamToWritable(\n  stream: ReadableStream,\n  writable: Writable,\n) {\n  let reader = stream.getReader();\n  let flushable = writable as { flush?: Function };\n\n  try {\n    while (true) {\n      let { done, value } = await reader.read();\n\n      if (done) {\n        writable.end();\n        break;\n      }\n\n      writable.write(value);\n      if (typeof flushable.flush === \"function\") {\n        flushable.flush();\n      }\n    }\n  } catch (error: unknown) {\n    writable.destroy(error as Error);\n    throw error;\n  }\n}\n\nexport async function writeAsyncIterableToWritable(\n  iterable: AsyncIterable<Uint8Array>,\n  writable: Writable,\n) {\n  try {\n    for await (let chunk of iterable) {\n      writable.write(chunk);\n    }\n    writable.end();\n  } catch (error: any) {\n    writable.destroy(error);\n    throw error;\n  }\n}\n\nexport async function readableStreamToString(\n  stream: ReadableStream<Uint8Array>,\n  encoding?: BufferEncoding,\n) {\n  let reader = stream.getReader();\n  let chunks: Uint8Array[] = [];\n\n  while (true) {\n    let { done, value } = await reader.read();\n    if (done) {\n      break;\n    }\n    if (value) {\n      chunks.push(value);\n    }\n  }\n\n  return Buffer.concat(chunks).toString(encoding);\n}\n\nexport const createReadableStreamFromReadable = (\n  source: Readable & { readableHighWaterMark?: number },\n) => {\n  let pump = new StreamPump(source);\n  let stream = new ReadableStream(pump, pump);\n  return stream;\n};\n\nclass StreamPump {\n  public highWaterMark: number;\n  public accumulatedSize: number;\n  private stream: Stream & {\n    readableHighWaterMark?: number;\n    readable?: boolean;\n    resume?: () => void;\n    pause?: () => void;\n    destroy?: (error?: Error) => void;\n  };\n  private controller?: ReadableStreamController<Uint8Array>;\n\n  constructor(\n    stream: Stream & {\n      readableHighWaterMark?: number;\n      readable?: boolean;\n      resume?: () => void;\n      pause?: () => void;\n      destroy?: (error?: Error) => void;\n    },\n  ) {\n    this.highWaterMark =\n      stream.readableHighWaterMark ||\n      new Stream.Readable().readableHighWaterMark;\n    this.accumulatedSize = 0;\n    this.stream = stream;\n    this.enqueue = this.enqueue.bind(this);\n    this.error = this.error.bind(this);\n    this.close = this.close.bind(this);\n  }\n\n  size(chunk: Uint8Array) {\n    return chunk?.byteLength || 0;\n  }\n\n  start(controller: ReadableStreamController<Uint8Array>) {\n    this.controller = controller;\n    this.stream.on(\"data\", this.enqueue);\n    this.stream.once(\"error\", this.error);\n    this.stream.once(\"end\", this.close);\n    this.stream.once(\"close\", this.close);\n  }\n\n  pull() {\n    this.resume();\n  }\n\n  cancel(reason?: Error) {\n    if (this.stream.destroy) {\n      this.stream.destroy(reason);\n    }\n\n    this.stream.off(\"data\", this.enqueue);\n    this.stream.off(\"error\", this.error);\n    this.stream.off(\"end\", this.close);\n    this.stream.off(\"close\", this.close);\n  }\n\n  enqueue(chunk: Uint8Array | string) {\n    if (this.controller) {\n      try {\n        let bytes = chunk instanceof Uint8Array ? chunk : Buffer.from(chunk);\n\n        let available = (this.controller.desiredSize || 0) - bytes.byteLength;\n        this.controller.enqueue(bytes);\n        if (available <= 0) {\n          this.pause();\n        }\n      } catch (error: any) {\n        this.controller.error(\n          new Error(\n            \"Could not create Buffer, chunk must be of type string or an instance of Buffer, ArrayBuffer, or Array or an Array-like Object\",\n          ),\n        );\n        this.cancel();\n      }\n    }\n  }\n\n  pause() {\n    if (this.stream.pause) {\n      this.stream.pause();\n    }\n  }\n\n  resume() {\n    if (this.stream.readable && this.stream.resume) {\n      this.stream.resume();\n    }\n  }\n\n  close() {\n    if (this.controller) {\n      this.controller.close();\n      delete this.controller;\n    }\n  }\n\n  error(error: Error) {\n    if (this.controller) {\n      this.controller.error(error);\n      delete this.controller;\n    }\n  }\n}\n"
  },
  {
    "path": "packages/react-router-node/tsconfig.json",
    "content": "{\n  \"include\": [\"**/*.ts\"],\n  \"exclude\": [\"dist\", \"__tests__\", \"node_modules\"],\n  \"compilerOptions\": {\n    \"lib\": [\"DOM\", \"DOM.Iterable\", \"ES2022\"],\n    \"target\": \"ES2022\",\n    \"module\": \"Node16\",\n    \"moduleResolution\": \"Node16\",\n    \"allowSyntheticDefaultImports\": true,\n    \"strict\": true,\n    \"jsx\": \"react\",\n    \"declaration\": true,\n    \"rootDir\": \".\",\n    \"outDir\": \"./dist\",\n    \"noEmit\": true,\n    \"resolveJsonModule\": true,\n\n    // Avoid naming conflicts between lib.dom.d.ts and globals.ts\n    \"skipLibCheck\": true\n  }\n}\n"
  },
  {
    "path": "packages/react-router-node/tsup.config.ts",
    "content": "import { defineConfig } from \"tsup\";\n\n// @ts-ignore - out of scope\nimport { createBanner } from \"../../build.utils.js\";\n\nimport pkg from \"./package.json\";\n\nconst entry = [\"index.ts\"];\n\nexport default defineConfig([\n  {\n    clean: true,\n    entry,\n    format: [\"cjs\", \"esm\"],\n    outDir: \"dist\",\n    dts: true,\n    banner: {\n      js: createBanner(pkg.name, pkg.version),\n    },\n  },\n]);\n"
  },
  {
    "path": "packages/react-router-node/typedoc.mjs",
    "content": "import { blockTags } from \"../../typedoc.mjs\";\n\n/** @type {Partial<import('typedoc').TypeDocOptions>} */\nconst config = {\n  entryPoints: [\"./index.ts\"],\n  blockTags,\n};\n\nexport default config;\n"
  },
  {
    "path": "packages/react-router-remix-routes-option-adapter/CHANGELOG.md",
    "content": "# `@react-router/remix-config-routes-adapter`\n\n## 7.13.1\n\n### Patch Changes\n\n- Updated dependencies:\n  - `@react-router/dev@7.13.1`\n\n## 7.13.0\n\n### Patch Changes\n\n- Updated dependencies:\n  - `@react-router/dev@7.13.0`\n\n## 7.12.0\n\n### Patch Changes\n\n- Updated dependencies:\n  - `@react-router/dev@7.12.0`\n\n## 7.11.0\n\n### Patch Changes\n\n- Updated dependencies:\n  - `@react-router/dev@7.11.0`\n\n## 7.10.1\n\n### Patch Changes\n\n- Updated dependencies:\n  - `@react-router/dev@7.10.1`\n\n## 7.10.0\n\n### Patch Changes\n\n- Updated dependencies:\n  - `@react-router/dev@7.10.0`\n\n## 7.9.6\n\n### Patch Changes\n\n- Updated dependencies:\n  - `@react-router/dev@7.9.6`\n\n## 7.9.5\n\n### Patch Changes\n\n- Updated dependencies:\n  - `@react-router/dev@7.9.5`\n\n## 7.9.4\n\n### Patch Changes\n\n- Updated dependencies:\n  - `@react-router/dev@7.9.4`\n\n## 7.9.3\n\n### Patch Changes\n\n- Updated dependencies:\n  - `@react-router/dev@7.9.3`\n\n## 7.9.2\n\n### Patch Changes\n\n- Updated dependencies:\n  - `@react-router/dev@7.9.2`\n\n## 7.9.1\n\n### Patch Changes\n\n- Updated dependencies:\n  - `@react-router/dev@7.9.1`\n\n## 7.9.0\n\n### Patch Changes\n\n- Updated dependencies:\n  - `@react-router/dev@7.9.0`\n\n## 7.8.2\n\n### Patch Changes\n\n- Updated dependencies:\n  - `@react-router/dev@7.8.2`\n\n## 7.8.1\n\n### Patch Changes\n\n- Updated dependencies:\n  - `@react-router/dev@7.8.1`\n\n## 7.8.0\n\n### Patch Changes\n\n- Updated dependencies:\n  - `@react-router/dev@7.8.0`\n\n## 7.7.1\n\n### Patch Changes\n\n- Updated dependencies:\n  - `@react-router/dev@7.7.1`\n\n## 7.7.0\n\n### Minor Changes\n\n- Export `DefineRouteFunction` type alongside `DefineRoutesFunction` ([#13945](https://github.com/remix-run/react-router/pull/13945))\n\n### Patch Changes\n\n- Updated dependencies:\n  - `@react-router/dev@7.7.0`\n\n## 7.6.3\n\n### Patch Changes\n\n- Use `replaceAll` for normalising windows file system slashes. ([#13738](https://github.com/remix-run/react-router/pull/13738))\n- Updated dependencies:\n  - `@react-router/dev@7.6.3`\n\n## 7.6.2\n\n### Patch Changes\n\n- Updated dependencies:\n  - `@react-router/dev@7.6.2`\n\n## 7.6.1\n\n### Patch Changes\n\n- Updated dependencies:\n  - `@react-router/dev@7.6.1`\n\n## 7.6.0\n\n### Patch Changes\n\n- Updated dependencies:\n  - `@react-router/dev@7.6.0`\n\n## 7.5.3\n\n### Patch Changes\n\n- Updated dependencies:\n  - `@react-router/dev@7.5.3`\n\n## 7.5.2\n\n### Patch Changes\n\n- Updated dependencies:\n  - `@react-router/dev@7.5.2`\n\n## 7.5.1\n\n### Patch Changes\n\n- Updated dependencies:\n  - `@react-router/dev@7.5.1`\n\n## 7.5.0\n\n### Patch Changes\n\n- Updated dependencies:\n  - `@react-router/dev@7.5.0`\n\n## 7.4.1\n\n### Patch Changes\n\n- Updated dependencies:\n  - `@react-router/dev@7.4.1`\n\n## 7.4.0\n\n### Patch Changes\n\n- Updated dependencies:\n  - `@react-router/dev@7.4.0`\n\n## 7.3.0\n\n### Patch Changes\n\n- Updated dependencies:\n  - `@react-router/dev@7.3.0`\n\n## 7.2.0\n\n### Patch Changes\n\n- Updated dependencies:\n  - `@react-router/dev@7.2.0`\n\n## 7.1.5\n\n### Patch Changes\n\n- Updated dependencies:\n  - `@react-router/dev@7.1.5`\n\n## 7.1.4\n\n### Patch Changes\n\n- Updated dependencies:\n  - `@react-router/dev@7.1.4`\n\n## 7.1.3\n\n### Patch Changes\n\n- Updated dependencies:\n  - `@react-router/dev@7.1.3`\n\n## 7.1.2\n\n### Patch Changes\n\n- Updated dependencies:\n  - `@react-router/dev@7.1.2`\n\n## 7.1.1\n\n### Patch Changes\n\n- Updated dependencies:\n  - `@react-router/dev@7.1.1`\n\n## 7.1.0\n\n### Patch Changes\n\n- Updated dependencies:\n  - `@react-router/dev@7.1.0`\n\n## 7.0.2\n\n### Patch Changes\n\n- Updated dependencies:\n  - `@react-router/dev@7.0.2`\n\n## 7.0.1\n\n### Patch Changes\n\n- Updated dependencies:\n  - `@react-router/dev@7.0.1`\n\n## 7.0.0\n\nInitial release.\n"
  },
  {
    "path": "packages/react-router-remix-routes-option-adapter/README.md",
    "content": "# @react-router/remix-routes-option-adapter\n\nAdapter for [Remix's](https://remix.run) `routes` config option, for use within `routes.ts`.\n\n```sh\nnpm install @react-router/remix-routes-option-adapter\n```\n"
  },
  {
    "path": "packages/react-router-remix-routes-option-adapter/__tests__/defineRoutes-test.ts",
    "content": "import { defineRoutes } from \"../defineRoutes\";\n\ndescribe(\"defineRoutes\", () => {\n  it(\"returns an array of routes\", () => {\n    let routes = defineRoutes((route) => {\n      route(\"/\", \"routes/home.js\");\n      route(\"inbox\", \"routes/inbox.js\", () => {\n        route(\"/\", \"routes/inbox/index.js\", { index: true });\n        route(\":messageId\", \"routes/inbox/$messageId.js\");\n        route(\"archive\", \"routes/inbox/archive.js\");\n      });\n    });\n\n    expect(routes).toMatchInlineSnapshot(`\n      {\n        \"routes/home\": {\n          \"caseSensitive\": undefined,\n          \"file\": \"routes/home.js\",\n          \"id\": \"routes/home\",\n          \"index\": undefined,\n          \"parentId\": \"root\",\n          \"path\": \"/\",\n        },\n        \"routes/inbox\": {\n          \"caseSensitive\": undefined,\n          \"file\": \"routes/inbox.js\",\n          \"id\": \"routes/inbox\",\n          \"index\": undefined,\n          \"parentId\": \"root\",\n          \"path\": \"inbox\",\n        },\n        \"routes/inbox/$messageId\": {\n          \"caseSensitive\": undefined,\n          \"file\": \"routes/inbox/$messageId.js\",\n          \"id\": \"routes/inbox/$messageId\",\n          \"index\": undefined,\n          \"parentId\": \"routes/inbox\",\n          \"path\": \":messageId\",\n        },\n        \"routes/inbox/archive\": {\n          \"caseSensitive\": undefined,\n          \"file\": \"routes/inbox/archive.js\",\n          \"id\": \"routes/inbox/archive\",\n          \"index\": undefined,\n          \"parentId\": \"routes/inbox\",\n          \"path\": \"archive\",\n        },\n        \"routes/inbox/index\": {\n          \"caseSensitive\": undefined,\n          \"file\": \"routes/inbox/index.js\",\n          \"id\": \"routes/inbox/index\",\n          \"index\": true,\n          \"parentId\": \"routes/inbox\",\n          \"path\": \"/\",\n        },\n      }\n    `);\n  });\n\n  it(\"works with async data\", async () => {\n    // Read everything *before* calling defineRoutes.\n    let fakeDirectory = await Promise.resolve([\"one.md\", \"two.md\"]);\n    let routes = defineRoutes((route) => {\n      for (let file of fakeDirectory) {\n        route(file.replace(/\\.md$/, \"\"), file);\n      }\n    });\n\n    expect(routes).toMatchInlineSnapshot(`\n      {\n        \"one\": {\n          \"caseSensitive\": undefined,\n          \"file\": \"one.md\",\n          \"id\": \"one\",\n          \"index\": undefined,\n          \"parentId\": \"root\",\n          \"path\": \"one\",\n        },\n        \"two\": {\n          \"caseSensitive\": undefined,\n          \"file\": \"two.md\",\n          \"id\": \"two\",\n          \"index\": undefined,\n          \"parentId\": \"root\",\n          \"path\": \"two\",\n        },\n      }\n    `);\n  });\n\n  it(\"allows multiple routes with the same route module\", () => {\n    let routes = defineRoutes((route) => {\n      route(\"/user/:id\", \"routes/_index.tsx\", { id: \"user-by-id\" });\n      route(\"/user\", \"routes/_index.tsx\", { id: \"user\" });\n      route(\"/other\", \"routes/other-route.tsx\");\n    });\n\n    expect(routes).toMatchInlineSnapshot(`\n      {\n        \"routes/other-route\": {\n          \"caseSensitive\": undefined,\n          \"file\": \"routes/other-route.tsx\",\n          \"id\": \"routes/other-route\",\n          \"index\": undefined,\n          \"parentId\": \"root\",\n          \"path\": \"/other\",\n        },\n        \"user\": {\n          \"caseSensitive\": undefined,\n          \"file\": \"routes/_index.tsx\",\n          \"id\": \"user\",\n          \"index\": undefined,\n          \"parentId\": \"root\",\n          \"path\": \"/user\",\n        },\n        \"user-by-id\": {\n          \"caseSensitive\": undefined,\n          \"file\": \"routes/_index.tsx\",\n          \"id\": \"user-by-id\",\n          \"index\": undefined,\n          \"parentId\": \"root\",\n          \"path\": \"/user/:id\",\n        },\n      }\n    `);\n  });\n\n  it(\"throws an error on route id collisions\", () => {\n    // Two conflicting custom id's\n    let defineNonUniqueRoutes = () => {\n      defineRoutes((route) => {\n        route(\"/user/:id\", \"routes/user.tsx\", { id: \"user\" });\n        route(\"/user\", \"routes/user.tsx\", { id: \"user\" });\n        route(\"/other\", \"routes/other-route.tsx\");\n      });\n    };\n\n    expect(defineNonUniqueRoutes).toThrowErrorMatchingInlineSnapshot(\n      `\"Unable to define routes with duplicate route id: \"user\"\"`,\n    );\n\n    // Custom id conflicting with a later-defined auto-generated id\n    defineNonUniqueRoutes = () => {\n      defineRoutes((route) => {\n        route(\"/user/:id\", \"routes/user.tsx\", { id: \"routes/user\" });\n        route(\"/user\", \"routes/user.tsx\");\n      });\n    };\n\n    expect(defineNonUniqueRoutes).toThrowErrorMatchingInlineSnapshot(\n      `\"Unable to define routes with duplicate route id: \"routes/user\"\"`,\n    );\n\n    // Custom id conflicting with an earlier-defined auto-generated id\n    defineNonUniqueRoutes = () => {\n      defineRoutes((route) => {\n        route(\"/user\", \"routes/user.tsx\");\n        route(\"/user/:id\", \"routes/user.tsx\", { id: \"routes/user\" });\n      });\n    };\n\n    expect(defineNonUniqueRoutes).toThrowErrorMatchingInlineSnapshot(\n      `\"Unable to define routes with duplicate route id: \"routes/user\"\"`,\n    );\n  });\n});\n"
  },
  {
    "path": "packages/react-router-remix-routes-option-adapter/__tests__/routeManifestToRouteConfig-test.ts",
    "content": "import { route } from \"@react-router/dev/routes\";\n\nimport { routeManifestToRouteConfig } from \"../manifest\";\nimport { defineRoutes } from \"../defineRoutes\";\n\nconst clean = (obj: any) => cleanUndefined(cleanIds(obj));\n\nconst cleanUndefined = (obj: any) => JSON.parse(JSON.stringify(obj));\n\nconst cleanIds = (obj: any) =>\n  JSON.parse(\n    JSON.stringify(obj, function replacer(key, value) {\n      return key === \"id\" ? undefined : value;\n    }),\n  );\n\ndescribe(\"routeManifestToRouteConfig\", () => {\n  test(\"creates route config\", () => {\n    let remixRoutes = routeManifestToRouteConfig(\n      defineRoutes((route) => {\n        route(\"/\", \"routes/home.js\");\n        route(\"inbox\", \"routes/inbox.js\", () => {\n          route(\"/\", \"routes/inbox/index.js\", { index: true });\n          route(\":messageId\", \"routes/inbox/$messageId.js\", {\n            caseSensitive: true,\n          });\n        });\n      }),\n    );\n    let routeConfig = [\n      route(\"/\", \"routes/home.js\"),\n      route(\"inbox\", \"routes/inbox.js\", [\n        route(\"/\", \"routes/inbox/index.js\", { index: true }),\n        route(\":messageId\", \"routes/inbox/$messageId.js\", {\n          caseSensitive: true,\n        }),\n      ]),\n    ];\n\n    expect(clean(remixRoutes)).toEqual(clean(routeConfig));\n\n    expect(cleanUndefined(remixRoutes)).toMatchInlineSnapshot(`\n      [\n        {\n          \"file\": \"routes/home.js\",\n          \"id\": \"routes/home\",\n          \"path\": \"/\",\n        },\n        {\n          \"children\": [\n            {\n              \"file\": \"routes/inbox/index.js\",\n              \"id\": \"routes/inbox/index\",\n              \"index\": true,\n              \"path\": \"/\",\n            },\n            {\n              \"caseSensitive\": true,\n              \"file\": \"routes/inbox/$messageId.js\",\n              \"id\": \"routes/inbox/$messageId\",\n              \"path\": \":messageId\",\n            },\n          ],\n          \"file\": \"routes/inbox.js\",\n          \"id\": \"routes/inbox\",\n          \"path\": \"inbox\",\n        },\n      ]\n    `);\n  });\n\n  test(\"creates route config with IDs\", () => {\n    let configRoutes = routeManifestToRouteConfig(\n      defineRoutes((route) => {\n        route(\"/\", \"routes/home.js\", { id: \"home\" });\n      }),\n    );\n\n    expect(configRoutes[0].id).toEqual(\"home\");\n  });\n});\n"
  },
  {
    "path": "packages/react-router-remix-routes-option-adapter/defineRoutes.ts",
    "content": "import type { RouteManifest, RouteManifestEntry } from \"./manifest\";\nimport { normalizeSlashes } from \"./normalizeSlashes\";\n\nexport type DefineRoutesFunction = (\n  callback: (defineRoute: DefineRouteFunction) => void,\n) => RouteManifest;\n\ninterface DefineRouteOptions {\n  /**\n   * Should be `true` if the route `path` is case-sensitive. Defaults to\n   * `false`.\n   */\n  caseSensitive?: boolean;\n\n  /**\n   * Should be `true` if this is an index route that does not allow child routes.\n   */\n  index?: boolean;\n\n  /**\n   * An optional unique id string for this route. Use this if you need to aggregate\n   * two or more routes with the same route file.\n   */\n  id?: string;\n}\n\ninterface DefineRouteChildren {\n  (): void;\n}\n\nexport interface DefineRouteFunction {\n  (\n    /**\n     * The path this route uses to match the URL pathname.\n     */\n    path: string | undefined,\n\n    /**\n     * The path to the file that exports the React component rendered by this\n     * route as its default export, relative to the `app` directory.\n     */\n    file: string,\n\n    /**\n     * Options for defining routes, or a function for defining child routes.\n     */\n    optionsOrChildren?: DefineRouteOptions | DefineRouteChildren,\n\n    /**\n     * A function for defining child routes.\n     */\n    children?: DefineRouteChildren,\n  ): void;\n}\n\n/**\n * A function for defining routes programmatically, instead of using the\n * filesystem convention.\n */\nexport const defineRoutes: DefineRoutesFunction = (callback) => {\n  let routes: RouteManifest = Object.create(null);\n  let parentRoutes: RouteManifestEntry[] = [];\n  let alreadyReturned = false;\n\n  let defineRoute: DefineRouteFunction = (\n    path,\n    file,\n    optionsOrChildren,\n    children,\n  ) => {\n    if (alreadyReturned) {\n      throw new Error(\n        \"You tried to define routes asynchronously but started defining \" +\n          \"routes before the async work was done. Please await all async \" +\n          \"data before calling `defineRoutes()`\",\n      );\n    }\n\n    let options: DefineRouteOptions;\n    if (typeof optionsOrChildren === \"function\") {\n      // route(path, file, children)\n      options = {};\n      children = optionsOrChildren;\n    } else {\n      // route(path, file, options, children)\n      // route(path, file, options)\n      options = optionsOrChildren || {};\n    }\n\n    let route: RouteManifestEntry = {\n      path: path ? path : undefined,\n      index: options.index ? true : undefined,\n      caseSensitive: options.caseSensitive ? true : undefined,\n      id: options.id || createRouteId(file),\n      parentId:\n        parentRoutes.length > 0\n          ? parentRoutes[parentRoutes.length - 1].id\n          : \"root\",\n      file,\n    };\n\n    if (route.id in routes) {\n      throw new Error(\n        `Unable to define routes with duplicate route id: \"${route.id}\"`,\n      );\n    }\n\n    routes[route.id] = route;\n\n    if (children) {\n      parentRoutes.push(route);\n      children();\n      parentRoutes.pop();\n    }\n  };\n\n  callback(defineRoute);\n\n  alreadyReturned = true;\n\n  return routes;\n};\n\nfunction createRouteId(file: string) {\n  return normalizeSlashes(stripFileExtension(file));\n}\n\nfunction stripFileExtension(file: string) {\n  return file.replace(/\\.[a-z0-9]+$/i, \"\");\n}\n"
  },
  {
    "path": "packages/react-router-remix-routes-option-adapter/index.ts",
    "content": "import { type RouteConfigEntry } from \"@react-router/dev/routes\";\n\nimport { routeManifestToRouteConfig } from \"./manifest\";\nimport {\n  defineRoutes,\n  type DefineRoutesFunction,\n  type DefineRouteFunction,\n} from \"./defineRoutes\";\n\nexport type { DefineRoutesFunction, DefineRouteFunction };\n\n/**\n * Adapts routes defined using [Remix's `routes` config\n * option](https://v2.remix.run/docs/file-conventions/vite-config#routes) to\n * React Router's config format, for use within `routes.ts`.\n */\nexport async function remixRoutesOptionAdapter(\n  routes: (\n    defineRoutes: DefineRoutesFunction,\n  ) =>\n    | ReturnType<DefineRoutesFunction>\n    | Promise<ReturnType<DefineRoutesFunction>>,\n): Promise<RouteConfigEntry[]> {\n  let routeManifest = await routes(defineRoutes);\n  return routeManifestToRouteConfig(routeManifest);\n}\n"
  },
  {
    "path": "packages/react-router-remix-routes-option-adapter/jest.config.js",
    "content": "/** @type {import('jest').Config} */\nmodule.exports = {\n  ...require(\"../../jest/jest.config.shared\"),\n  displayName: \"remix-routes-option-adapter\",\n  setupFiles: [],\n};\n"
  },
  {
    "path": "packages/react-router-remix-routes-option-adapter/manifest.ts",
    "content": "import type { RouteConfigEntry } from \"@react-router/dev/routes\";\n\nexport interface RouteManifestEntry {\n  path?: string;\n  index?: boolean;\n  caseSensitive?: boolean;\n  id: string;\n  parentId?: string;\n  file: string;\n}\n\nexport interface RouteManifest {\n  [routeId: string]: RouteManifestEntry;\n}\n\nexport function routeManifestToRouteConfig(\n  routeManifest: RouteManifest,\n  rootId = \"root\",\n): RouteConfigEntry[] {\n  let routeConfigById: {\n    [id: string]: Omit<RouteConfigEntry, \"id\"> &\n      Required<Pick<RouteConfigEntry, \"id\">>;\n  } = {};\n\n  for (let id in routeManifest) {\n    let route = routeManifest[id];\n    routeConfigById[id] = {\n      id: route.id,\n      file: route.file,\n      path: route.path,\n      index: route.index,\n      caseSensitive: route.caseSensitive,\n    };\n  }\n\n  let routeConfig: RouteConfigEntry[] = [];\n\n  for (let id in routeConfigById) {\n    let route = routeConfigById[id];\n    let parentId = routeManifest[route.id].parentId;\n    if (parentId === rootId) {\n      routeConfig.push(route);\n    } else {\n      let parentRoute = parentId && routeConfigById[parentId];\n      if (parentRoute) {\n        parentRoute.children = parentRoute.children || [];\n        parentRoute.children.push(route);\n      }\n    }\n  }\n\n  return routeConfig;\n}\n"
  },
  {
    "path": "packages/react-router-remix-routes-option-adapter/normalizeSlashes.ts",
    "content": "import path from \"node:path\";\n\nexport function normalizeSlashes(file: string) {\n  return file.replaceAll(path.win32.sep, \"/\");\n}\n"
  },
  {
    "path": "packages/react-router-remix-routes-option-adapter/package.json",
    "content": "{\n  \"name\": \"@react-router/remix-routes-option-adapter\",\n  \"version\": \"7.13.1\",\n  \"description\": \"Adapter for Remix's \\\"routes\\\" config option, for use within routes.ts\",\n  \"bugs\": {\n    \"url\": \"https://github.com/remix-run/react-router/issues\"\n  },\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"https://github.com/remix-run/react-router\",\n    \"directory\": \"packages/react-router-remix-routes-option-adapter\"\n  },\n  \"license\": \"MIT\",\n  \"main\": \"dist/index.js\",\n  \"typings\": \"dist/index.d.ts\",\n  \"exports\": {\n    \".\": {\n      \"types\": \"./dist/index.d.ts\",\n      \"default\": \"./dist/index.js\"\n    },\n    \"./package.json\": \"./package.json\"\n  },\n  \"scripts\": {\n    \"build\": \"wireit\"\n  },\n  \"wireit\": {\n    \"build\": {\n      \"command\": \"tsup\",\n      \"files\": [\n        \"../../pnpm-workspace.yaml\",\n        \"*.ts\",\n        \"tsconfig.json\",\n        \"package.json\"\n      ],\n      \"output\": [\n        \"dist/**\"\n      ]\n    }\n  },\n  \"devDependencies\": {\n    \"@react-router/dev\": \"workspace:*\",\n    \"tsup\": \"catalog:\",\n    \"typescript\": \"catalog:\",\n    \"wireit\": \"catalog:\"\n  },\n  \"peerDependencies\": {\n    \"@react-router/dev\": \"workspace:^\",\n    \"typescript\": \"^5.1.0\"\n  },\n  \"peerDependenciesMeta\": {\n    \"typescript\": {\n      \"optional\": true\n    }\n  },\n  \"engines\": {\n    \"node\": \">=20.0.0\"\n  },\n  \"files\": [\n    \"dist/\",\n    \"CHANGELOG.md\",\n    \"LICENSE.md\",\n    \"README.md\"\n  ]\n}\n"
  },
  {
    "path": "packages/react-router-remix-routes-option-adapter/tsconfig.json",
    "content": "{\n  \"include\": [\"**/*.ts\"],\n  \"exclude\": [\"dist\", \"__tests__\", \"node_modules\"],\n  \"compilerOptions\": {\n    \"lib\": [\"DOM\", \"DOM.Iterable\", \"ES2022\"],\n    \"target\": \"ES2022\",\n    \"module\": \"ES2022\",\n    \"skipLibCheck\": true,\n\n    \"moduleResolution\": \"Bundler\",\n    \"allowSyntheticDefaultImports\": true,\n    \"strict\": true,\n    \"jsx\": \"react\",\n    \"declaration\": true,\n    \"emitDeclarationOnly\": true,\n    \"rootDir\": \".\",\n    \"outDir\": \"./dist\"\n  }\n}\n"
  },
  {
    "path": "packages/react-router-remix-routes-option-adapter/tsup.config.ts",
    "content": "import { defineConfig } from \"tsup\";\n\n// @ts-ignore - out of scope\nimport { createBanner } from \"../../build.utils.js\";\n\nimport pkg from \"./package.json\";\n\nconst entry = [\"index.ts\"];\n\nexport default defineConfig([\n  {\n    clean: true,\n    entry,\n    format: [\"cjs\"],\n    outDir: \"dist\",\n    dts: true,\n    banner: {\n      js: createBanner(pkg.name, pkg.version),\n    },\n  },\n]);\n"
  },
  {
    "path": "packages/react-router-remix-routes-option-adapter/typedoc.mjs",
    "content": "import { blockTags } from \"../../typedoc.mjs\";\n\n/** @type {Partial<import('typedoc').TypeDocOptions>} */\nconst config = {\n  entryPoints: [\"./index.ts\"],\n  blockTags,\n};\n\nexport default config;\n"
  },
  {
    "path": "packages/react-router-serve/CHANGELOG.md",
    "content": "# `@react-router/serve`\n\n## 7.13.1\n\n### Patch Changes\n\n- Updated dependencies:\n  - `react-router@7.13.1`\n  - `@react-router/node@7.13.1`\n  - `@react-router/express@7.13.1`\n\n## 7.13.0\n\n### Patch Changes\n\n- Updated dependencies:\n  - `react-router@7.13.0`\n  - `@react-router/node@7.13.0`\n  - `@react-router/express@7.13.0`\n\n## 7.12.0\n\n### Patch Changes\n\n- Updated dependencies:\n  - `react-router@7.12.0`\n  - `@react-router/node@7.12.0`\n  - `@react-router/express@7.12.0`\n\n## 7.11.0\n\n### Patch Changes\n\n- support custom entrypoints for RSC framework mode ([#14643](https://github.com/remix-run/react-router/pull/14643))\n- Update `compression` and `morgan` dependencies to address `on-headers` CVE: [GHSA-76c9-3jph-rj3q](https://github.com/advisories/GHSA-76c9-3jph-rj3q) ([#14652](https://github.com/remix-run/react-router/pull/14652))\n- Updated dependencies:\n  - `react-router@7.11.0`\n  - `@react-router/node@7.11.0`\n  - `@react-router/express@7.11.0`\n\n## 7.10.1\n\n### Patch Changes\n\n- Updated dependencies:\n  - `react-router@7.10.1`\n  - `@react-router/node@7.10.1`\n  - `@react-router/express@7.10.1`\n\n## 7.10.0\n\n### Patch Changes\n\n- Updated dependencies:\n  - `react-router@7.10.0`\n  - `@react-router/node@7.10.0`\n  - `@react-router/express@7.10.0`\n\n## 7.9.6\n\n### Patch Changes\n\n- Updated dependencies:\n  - `react-router@7.9.6`\n  - `@react-router/node@7.9.6`\n  - `@react-router/express@7.9.6`\n\n## 7.9.5\n\n### Patch Changes\n\n- Updated dependencies:\n  - `react-router@7.9.5`\n  - `@react-router/node@7.9.5`\n  - `@react-router/express@7.9.5`\n\n## 7.9.4\n\n### Patch Changes\n\n- Updated dependencies:\n  - `react-router@7.9.4`\n  - `@react-router/node@7.9.4`\n  - `@react-router/express@7.9.4`\n\n## 7.9.3\n\n### Patch Changes\n\n- Updated dependencies:\n  - `react-router@7.9.3`\n  - `@react-router/node@7.9.3`\n  - `@react-router/express@7.9.3`\n\n## 7.9.2\n\n### Patch Changes\n\n- disable compression for RSC responses for now ([#14381](https://github.com/remix-run/react-router/pull/14381))\n- Updated dependencies:\n  - `react-router@7.9.2`\n  - `@react-router/node@7.9.2`\n  - `@react-router/express@7.9.2`\n\n## 7.9.1\n\n### Patch Changes\n\n- Updated dependencies:\n  - `react-router@7.9.1`\n  - `@react-router/node@7.9.1`\n  - `@react-router/express@7.9.1`\n\n## 7.9.0\n\n### Patch Changes\n\n- Updated dependencies:\n  - `react-router@7.9.0`\n  - `@react-router/express@7.9.0`\n  - `@react-router/node@7.9.0`\n\n## 7.8.2\n\n### Patch Changes\n\n- Updated dependencies:\n  - `react-router@7.8.2`\n  - `@react-router/node@7.8.2`\n  - `@react-router/express@7.8.2`\n\n## 7.8.1\n\n### Patch Changes\n\n- Updated dependencies:\n  - `react-router@7.8.1`\n  - `@react-router/node@7.8.1`\n  - `@react-router/express@7.8.1`\n\n## 7.8.0\n\n### Patch Changes\n\n- Updated dependencies:\n  - `react-router@7.8.0`\n  - `@react-router/express@7.8.0`\n  - `@react-router/node@7.8.0`\n\n## 7.7.1\n\n### Patch Changes\n\n- Updated dependencies:\n  - `react-router@7.7.1`\n  - `@react-router/node@7.7.1`\n  - `@react-router/express@7.7.1`\n\n## 7.7.0\n\n### Patch Changes\n\n- Updated dependencies:\n  - `react-router@7.7.0`\n  - `@react-router/node@7.7.0`\n  - `@react-router/express@7.7.0`\n\n## 7.6.3\n\n### Patch Changes\n\n- Updated dependencies:\n  - `@react-router/node@7.6.3`\n  - `react-router@7.6.3`\n  - `@react-router/express@7.6.3`\n\n## 7.6.2\n\n### Patch Changes\n\n- Updated dependencies:\n  - `react-router@7.6.2`\n  - `@react-router/node@7.6.2`\n  - `@react-router/express@7.6.2`\n\n## 7.6.1\n\n### Patch Changes\n\n- Updated dependencies:\n  - `react-router@7.6.1`\n  - `@react-router/node@7.6.1`\n  - `@react-router/express@7.6.1`\n\n## 7.6.0\n\n### Patch Changes\n\n- Updated dependencies:\n  - `react-router@7.6.0`\n  - `@react-router/node@7.6.0`\n  - `@react-router/express@7.6.0`\n\n## 7.5.3\n\n### Patch Changes\n\n- Updated dependencies:\n  - `react-router@7.5.3`\n  - `@react-router/node@7.5.3`\n  - `@react-router/express@7.5.3`\n\n## 7.5.2\n\n### Patch Changes\n\n- Updated dependencies:\n  - `react-router@7.5.2`\n  - `@react-router/node@7.5.2`\n  - `@react-router/express@7.5.2`\n\n## 7.5.1\n\n### Patch Changes\n\n- Updated dependencies:\n  - `react-router@7.5.1`\n  - `@react-router/node@7.5.1`\n  - `@react-router/express@7.5.1`\n\n## 7.5.0\n\n### Patch Changes\n\n- Updated dependencies:\n  - `react-router@7.5.0`\n  - `@react-router/node@7.5.0`\n  - `@react-router/express@7.5.0`\n\n## 7.4.1\n\n### Patch Changes\n\n- Updated dependencies:\n  - `react-router@7.4.1`\n  - `@react-router/express@7.4.1`\n  - `@react-router/node@7.4.1`\n\n## 7.4.0\n\n### Patch Changes\n\n- Updated dependencies:\n  - `react-router@7.4.0`\n  - `@react-router/node@7.4.0`\n  - `@react-router/express@7.4.0`\n\n## 7.3.0\n\n### Patch Changes\n\n- Updated dependencies:\n  - `react-router@7.3.0`\n  - `@react-router/express@7.3.0`\n  - `@react-router/node@7.3.0`\n\n## 7.2.0\n\n### Patch Changes\n\n- Updated dependencies:\n  - `react-router@7.2.0`\n  - `@react-router/node@7.2.0`\n  - `@react-router/express@7.2.0`\n\n## 7.1.5\n\n### Patch Changes\n\n- Updated dependencies:\n  - `react-router@7.1.5`\n  - `@react-router/node@7.1.5`\n  - `@react-router/express@7.1.5`\n\n## 7.1.4\n\n### Patch Changes\n\n- Updated dependencies:\n  - `react-router@7.1.4`\n  - `@react-router/node@7.1.4`\n  - `@react-router/express@7.1.4`\n\n## 7.1.3\n\n### Patch Changes\n\n- Updated dependencies:\n  - `react-router@7.1.3`\n  - `@react-router/express@7.1.3`\n  - `@react-router/node@7.1.3`\n\n## 7.1.2\n\n### Patch Changes\n\n- Updated dependencies:\n  - `react-router@7.1.2`\n  - `@react-router/node@7.1.2`\n  - `@react-router/express@7.1.2`\n\n## 7.1.1\n\n### Patch Changes\n\n- Updated dependencies:\n  - `react-router@7.1.1`\n  - `@react-router/express@7.1.1`\n  - `@react-router/node@7.1.1`\n\n## 7.1.0\n\n### Patch Changes\n\n- Updated dependencies:\n  - `react-router@7.1.0`\n  - `@react-router/node@7.1.0`\n  - `@react-router/express@7.1.0`\n\n## 7.0.2\n\n### Patch Changes\n\n- Updated dependencies:\n  - `react-router@7.0.2`\n  - `@react-router/node@7.0.2`\n  - `@react-router/express@7.0.2`\n\n## 7.0.1\n\n### Patch Changes\n\n- Updated dependencies:\n  - `react-router@7.0.1`\n  - `@react-router/express@7.0.1`\n  - `@react-router/node@7.0.1`\n\n## 7.0.0\n\n### Major Changes\n\n- Remove single fetch future flag. ([#11522](https://github.com/remix-run/react-router/pull/11522))\n- update minimum node version to 18 ([#11690](https://github.com/remix-run/react-router/pull/11690))\n- Add `exports` field to all packages ([#11675](https://github.com/remix-run/react-router/pull/11675))\n- node package no longer re-exports from react-router ([#11702](https://github.com/remix-run/react-router/pull/11702))\n\n### Patch Changes\n\n- Update `express.static` configurations to support prerendering ([#11547](https://github.com/remix-run/react-router/pull/11547))\n  - Assets in the `build/client/assets` folder are served as before, with a 1-year immutable `Cache-Control` header\n  - Static files outside of assets, such as pre-rendered `.html` and `.data` files are not served with a specific `Cache-Control` header\n  - `.data` files are served with `Content-Type: text/x-turbo`\n    - For some reason, when adding this via `express.static`, it seems to also add a `Cache-Control: public, max-age=0` to `.data` files\n\n- Updated dependencies:\n  - `react-router@7.0.0`\n  - `@react-router/express@7.0.0`\n  - `@react-router/node@7.0.0`\n\n## 2.9.0\n\n### Minor Changes\n\n- Put `undici` fetch polyfill behind a new `installGlobals({ nativeFetch: true })` parameter ([#9198](https://github.com/remix-run/remix/pull/9198))\n  - `remix-serve` will default to using `undici` for the fetch polyfill if `future.unstable_singleFetch` is enabled because the single fetch implementation relies on the `undici` polyfill\n  - Any users opting into Single Fetch and managing their own polyfill will need to pass the flag to `installGlobals` on their own to avoid runtime errors with Single Fetch\n\n### Patch Changes\n\n- Updated dependencies:\n  - `@remix-run/node@2.9.0`\n  - `@remix-run/express@2.9.0`\n\n## 2.8.1\n\n### Patch Changes\n\n- Updated dependencies:\n  - `@remix-run/express@2.8.1`\n  - `@remix-run/node@2.8.1`\n\n## 2.8.0\n\n### Patch Changes\n\n- Updated dependencies:\n  - `@remix-run/express@2.8.0`\n  - `@remix-run/node@2.8.0`\n\n## 2.7.2\n\n### Patch Changes\n\n- Updated dependencies:\n  - `@remix-run/express@2.7.2`\n  - `@remix-run/node@2.7.2`\n\n## 2.7.1\n\n### Patch Changes\n\n- Updated dependencies:\n  - `@remix-run/express@2.7.1`\n  - `@remix-run/node@2.7.1`\n\n## 2.7.0\n\n### Patch Changes\n\n- Updated dependencies:\n  - `@remix-run/express@2.7.0`\n  - `@remix-run/node@2.7.0`\n\n## 2.6.0\n\n### Patch Changes\n\n- Updated dependencies:\n  - `@remix-run/node@2.6.0`\n  - `@remix-run/express@2.6.0`\n\n## 2.5.1\n\n### Patch Changes\n\n- Updated dependencies:\n  - `@remix-run/express@2.5.1`\n  - `@remix-run/node@2.5.1`\n\n## 2.5.0\n\n### Patch Changes\n\n- Don't try to load sourcemaps if they don't exist on disk ([#8446](https://github.com/remix-run/remix/pull/8446))\n- Updated dependencies:\n  - `@remix-run/node@2.5.0`\n  - `@remix-run/express@2.5.0`\n\n## 2.4.1\n\n### Patch Changes\n\n- Use node `fileURLToPath` to convert source map URL to path ([#8321](https://github.com/remix-run/remix/pull/8321))\n- Updated dependencies:\n  - `@remix-run/node@2.4.1`\n  - `@remix-run/express@2.4.1`\n\n## 2.4.0\n\n### Patch Changes\n\n- Fix source map loading when file has `?t=timestamp` suffix (rebuilds) ([#8174](https://github.com/remix-run/remix/pull/8174))\n- Updated dependencies:\n  - `@remix-run/node@2.4.0`\n  - `@remix-run/express@2.4.0`\n\n## 2.3.1\n\n### Patch Changes\n\n- Updated dependencies:\n  - `@remix-run/express@2.3.1`\n  - `@remix-run/node@2.3.1`\n\n## 2.3.0\n\n### Patch Changes\n\n- Updated dependencies:\n  - `@remix-run/express@2.3.0`\n  - `@remix-run/node@2.3.0`\n\n## 2.2.0\n\n### Patch Changes\n\n- Updated dependencies:\n  - `@remix-run/express@2.2.0`\n  - `@remix-run/node@2.2.0`\n\n## 2.1.0\n\n### Patch Changes\n\n- Updated dependencies:\n  - `@remix-run/express@2.1.0`\n  - `@remix-run/node@2.1.0`\n\n## 2.0.1\n\n### Patch Changes\n\n- Fix HMR for CJS projects using `remix-serve` and manual mode (`remix dev --manual`) ([#7487](https://github.com/remix-run/remix/pull/7487))\n  - By explicitly busting the `require` cache, `remix-serve` now correctly re-imports new server changes in CJS\n  - ESM projects were already working correctly and are not affected by this.\n- Fix error caused by partially written server build ([#7470](https://github.com/remix-run/remix/pull/7470))\n  - Previously, it was possible to trigger a reimport of the app server code before the new server build had completely been written. Reimporting the partially written server build caused issues related to `build.assets` being undefined and crashing when reading `build.assets.version`\n- Updated dependencies:\n  - `@remix-run/node@2.0.1`\n  - `@remix-run/express@2.0.1`\n\n## 2.0.0\n\n### Major Changes\n\n- `remix-serve` now picks an open port if 3000 is taken ([#7278](https://github.com/remix-run/remix/pull/7278))\n  - If `PORT` env var is set, `remix-serve` will use that port\n  - Otherwise, `remix-serve` picks an open port (3000 unless that is already taken)\n\n- Integrate manual mode in `remix-serve` ([#7231](https://github.com/remix-run/remix/pull/7231))\n\n- Remove undocumented `createApp` Node API ([#7229](https://github.com/remix-run/remix/pull/7229))\n  - `remix-serve` is a CLI, not a library\n\n- Require Node >=18.0.0 ([#6939](https://github.com/remix-run/remix/pull/6939))\n\n- Promote the `future.v2_dev` flag in `remix.config.js` to a root level `dev` config ([#7002](https://github.com/remix-run/remix/pull/7002))\n\n- Default to `serverModuleFormat: \"esm\"` and update `remix-serve` to use dynamic import to support ESM and CJS build outputs ([#6949](https://github.com/remix-run/remix/pull/6949))\n\n- Preserve dynamic imports in `remix-serve` for external bundle ([#7173](https://github.com/remix-run/remix/pull/7173))\n\n- For preparation of using Node's built in fetch implementation, installing the fetch globals is now a responsibility of the app server ([#7009](https://github.com/remix-run/remix/pull/7009))\n  - If you are using `remix-serve`, nothing is required\n  - If you are using your own app server, you will need to install the globals yourself\n\n    ```js filename=server.js\n    import { installGlobals } from \"@remix-run/node\";\n\n    installGlobals();\n    ```\n\n- `source-map-support` is now a responsibility of the app server ([#7009](https://github.com/remix-run/remix/pull/7009))\n  - If you are using `remix-serve`, nothing is required\n  - If you are using your own app server, you will need to install [`source-map-support`](https://www.npmjs.com/package/source-map-support) yourself.\n\n    ```sh\n    npm i source-map-support\n    ```\n\n    ```js filename=server.js\n    import sourceMapSupport from \"source-map-support\";\n    sourceMapSupport.install();\n    ```\n\n### Patch Changes\n\n- Update `remix-serve` usage error message to support ESM projects ([#7400](https://github.com/remix-run/remix/pull/7400))\n- Updated dependencies:\n  - `@remix-run/node@2.0.0`\n  - `@remix-run/express@2.0.0`\n\n## 1.19.3\n\n### Patch Changes\n\n- Updated dependencies:\n  - `@remix-run/express@1.19.3`\n  - `@remix-run/node@1.19.3`\n\n## 1.19.2\n\n### Patch Changes\n\n- Install `source-map-support` ([#7039](https://github.com/remix-run/remix/pull/7039))\n- Updated dependencies:\n  - `@remix-run/node@1.19.2`\n  - `@remix-run/express@1.19.2`\n\n## 1.19.1\n\n### Patch Changes\n\n- Updated dependencies:\n  - `@remix-run/express@1.19.1`\n  - `@remix-run/node@1.19.1`\n\n## 1.19.0\n\n### Patch Changes\n\n- Updated dependencies:\n  - `@remix-run/node@1.19.0`\n  - `@remix-run/express@1.19.0`\n\n## 1.18.1\n\n### Patch Changes\n\n- Updated dependencies:\n  - `@remix-run/node@1.18.1`\n  - `@remix-run/express@1.18.1`\n\n## 1.18.0\n\n### Minor Changes\n\n- stabilize v2 dev server ([#6615](https://github.com/remix-run/remix/pull/6615))\n\n### Patch Changes\n\n- fix(types): better tuple serialization types ([#6616](https://github.com/remix-run/remix/pull/6616))\n- Updated dependencies:\n  - `@remix-run/node@1.18.0`\n  - `@remix-run/express@1.18.0`\n\n## 1.17.1\n\n### Patch Changes\n\n- Updated dependencies:\n  - `@remix-run/express@1.17.1`\n  - `@remix-run/node@1.17.1`\n\n## 1.17.0\n\n### Patch Changes\n\n- Add `HeadersArgs` type to be consistent with loaders/actions/meta and allows for using a `function` declaration in addition to an arrow function expression ([#6247](https://github.com/remix-run/remix/pull/6247))\n\n  ```tsx\n  import type { HeadersArgs } from \"@remix-run/node\"; // or cloudflare/deno\n\n  export function headers({ loaderHeaders }: HeadersArgs) {\n    return {\n      \"x-my-custom-thing\": loaderHeaders.get(\"x-my-custom-thing\") || \"fallback\",\n    };\n  }\n  ```\n\n- Updated dependencies:\n  - `@remix-run/node@1.17.0`\n  - `@remix-run/express@1.17.0`\n\n## 1.16.1\n\n### Patch Changes\n\n- Updated dependencies:\n  - `@remix-run/node@1.16.1`\n  - `@remix-run/express@1.16.1`\n\n## 1.16.0\n\n### Patch Changes\n\n- add `@remix-run/node/install` side-effect to allow `node --require @remix-run/node/install` ([#6132](https://github.com/remix-run/remix/pull/6132))\n- Updated dependencies:\n  - `@remix-run/express@1.16.0`\n  - `@remix-run/node@1.16.0`\n\n## 1.15.0\n\n### Patch Changes\n\n- Updated dependencies:\n  - `@remix-run/express@1.15.0`\n\n## 1.14.3\n\n### Patch Changes\n\n- Updated dependencies:\n  - `@remix-run/express@1.14.3`\n\n## 1.14.2\n\n### Patch Changes\n\n- Updated dependencies:\n  - `@remix-run/express@1.14.2`\n\n## 1.14.1\n\n### Patch Changes\n\n- Updated dependencies:\n  - `@remix-run/express@1.14.1`\n\n## 1.14.0\n\n### Patch Changes\n\n- Allow configurable `NODE_ENV` with `remix-serve` ([#5540](https://github.com/remix-run/remix/pull/5540))\n- Sync `FutureConfig` interface between packages ([#5398](https://github.com/remix-run/remix/pull/5398))\n- Updated dependencies:\n  - `@remix-run/express@1.14.0`\n\n## 1.13.0\n\n### Patch Changes\n\n- Updated dependencies:\n  - `@remix-run/express@1.13.0`\n\n## 1.12.0\n\n### Minor Changes\n\n- Added a new development server available in the Remix config under the `unstable_dev` flag. [See the release notes](https://github.com/remix-run/remix/releases/tag/remix%401.12.0) for a full description. ([#5133](https://github.com/remix-run/remix/pull/5133))\n\n### Patch Changes\n\n- Updated dependencies:\n  - `@remix-run/express@1.12.0`\n\n## 1.11.1\n\n### Patch Changes\n\n- Updated dependencies:\n  - `@remix-run/express@1.11.1`\n\n## 1.11.0\n\n### Patch Changes\n\n- Introduces the `defer()` API from `@remix-run/router` with support for server-rendering and HTTP streaming. This utility allows you to defer values returned from `loader` functions by returning promises instead of resolved values. This has been refered to as _\"sending a promise over the wire\"_. ([#4920](https://github.com/remix-run/remix/pull/4920))\n\n  Informational Resources:\n  - <https://gist.github.com/jacob-ebey/9bde9546c1aafaa6bc8c242054b1be26>\n  - <https://github.com/remix-run/remix/blob/main/decisions/0004-streaming-apis.md>\n\n  Documentation Resources (better docs specific to Remix are in the works):\n  - <https://reactrouter.com/en/main/utils/defer>\n  - <https://reactrouter.com/en/main/components/await>\n  - <https://reactrouter.com/en/main/hooks/use-async-value>\n  - <https://reactrouter.com/en/main/hooks/use-async-error>\n\n- Updated dependencies:\n  - `@remix-run/express@1.11.0`\n\n## 1.10.1\n\n### Patch Changes\n\n- Updated dependencies:\n  - `@remix-run/express@1.10.1`\n\n## 1.10.0\n\n### Patch Changes\n\n- Updated dependencies:\n  - `@remix-run/express@1.10.0`\n\n## 1.9.0\n\n### Patch Changes\n\n- Fix `TypedResponse` so that Typescript correctly shows errors for incompatible types in `loader` and `action` functions. ([#4734](https://github.com/remix-run/remix/pull/4734))\n- Updated dependencies:\n  - `@remix-run/express@1.9.0`\n\n## 1.8.2\n\n### Patch Changes\n\n- Updated dependencies:\n  - `@remix-run/express@1.8.2`\n\n## 1.8.1\n\n### Patch Changes\n\n- Updated dependencies:\n  - `@remix-run/express@1.8.1`\n\n## 1.8.0\n\n### Patch Changes\n\n- Updated dependencies:\n  - `@remix-run/express@1.8.0`\n\n## 1.7.6\n\n### Patch Changes\n\n- Updated dependencies:\n  - `@remix-run/express@1.7.6`\n\n## 1.7.5\n\n### Patch Changes\n\n- Updated dependencies:\n  - `@remix-run/express@1.7.5`\n\n## 1.7.4\n\n### Patch Changes\n\n- Updated dependencies:\n  - `@remix-run/express@1.7.4`\n\n## 1.7.3\n\n### Patch Changes\n\n- Updated dependencies:\n  - `@remix-run/express@1.7.3`\n\n## 1.7.2\n\n### Patch Changes\n\n- Updated dependencies:\n  - `@remix-run/express@1.7.2`\n\n## 1.7.1\n\n### Patch Changes\n\n- Updated dependencies:\n  - `@remix-run/express@1.7.1`\n\n## 1.7.0\n\n### Minor Changes\n\n- We've added a new type: `SerializeFrom`. This is used to infer the ([#4013](https://github.com/remix-run/remix/pull/4013))\n  JSON-serialized return type of loaders and actions.\n- `MetaFunction` type can now infer `data` and `parentsData` types from route loaders ([#4022](https://github.com/remix-run/remix/pull/4022))\n\n### Patch Changes\n\n- Updated dependencies:\n  - `@remix-run/express@1.7.0`\n\n## 1.6.8\n\n### Patch Changes\n\n- Updated dependencies:\n  - `@remix-run/express@1.6.8`\n\n## 1.6.7\n\n### Patch Changes\n\n- Updated dependencies:\n  - `@remix-run/express@1.6.7`\n\n## 1.6.6\n\n### Patch Changes\n\n- Updated dependencies:\n  - `@remix-run/express@1.6.6`\n\n## 1.6.5\n\n### Patch Changes\n\n- Updated dependencies\n  - `@remix-run/express@1.6.5`\n"
  },
  {
    "path": "packages/react-router-serve/README.md",
    "content": "Node.js application server for React Router\n\n```sh\nnpm install @react-router/serve\n```\n"
  },
  {
    "path": "packages/react-router-serve/bin.js",
    "content": "#!/usr/bin/env node\n\n// If not already set, default `NODE_ENV=production` so React loads the proper\n// version in it's CJS entry script\nprocess.env.NODE_ENV = process.env.NODE_ENV ?? \"production\";\n\nrequire(\"./dist/cli\");\n"
  },
  {
    "path": "packages/react-router-serve/cli.ts",
    "content": "#!/usr/bin/env node\nimport fs from \"node:fs\";\nimport os from \"node:os\";\nimport path from \"node:path\";\nimport url from \"node:url\";\nimport type { ServerBuild } from \"react-router\";\nimport { createRequestHandler } from \"@react-router/express\";\nimport { createRequestListener } from \"@mjackson/node-fetch-server\";\nimport compression from \"compression\";\nimport express from \"express\";\nimport morgan from \"morgan\";\nimport sourceMapSupport from \"source-map-support\";\nimport getPort from \"get-port\";\n\nprocess.env.NODE_ENV = process.env.NODE_ENV ?? \"production\";\n\nsourceMapSupport.install({\n  retrieveSourceMap: function (source) {\n    let match = source.startsWith(\"file://\");\n    if (match) {\n      let filePath = url.fileURLToPath(source);\n      let sourceMapPath = `${filePath}.map`;\n      if (fs.existsSync(sourceMapPath)) {\n        return {\n          url: source,\n          map: fs.readFileSync(sourceMapPath, \"utf8\"),\n        };\n      }\n    }\n    return null;\n  },\n});\n\nrun();\n\ntype RSCServerBuildModule = {\n  default: {\n    fetch: (request: Request) => Response | Promise<Response>;\n  };\n  unstable_reactRouterServeConfig?: {\n    publicPath: string;\n    assetsBuildDirectory: string;\n  };\n};\n\ntype NormalizedBuild = {\n  fetch?: (request: Request) => Response | Promise<Response>;\n  publicPath: string;\n  assetsBuildDirectory: string;\n};\n\nfunction isRSCServerBuild(build: unknown): build is RSCServerBuildModule {\n  return Boolean(\n    typeof build === \"object\" &&\n      build &&\n      \"default\" in build &&\n      typeof build.default === \"object\" &&\n      build.default &&\n      \"fetch\" in build.default &&\n      typeof build.default.fetch === \"function\",\n  );\n}\n\nfunction parseNumber(raw?: string) {\n  if (raw === undefined) return undefined;\n  let maybe = Number(raw);\n  if (Number.isNaN(maybe)) return undefined;\n  return maybe;\n}\n\nasync function run() {\n  let port = parseNumber(process.env.PORT) ?? (await getPort({ port: 3000 }));\n\n  let buildPathArg = process.argv[2];\n\n  if (!buildPathArg) {\n    console.error(`\n  Usage: react-router-serve <server-build-path> - e.g. react-router-serve build/server/index.js`);\n    process.exit(1);\n  }\n\n  let buildPath = path.resolve(buildPathArg);\n\n  let buildModule = await import(url.pathToFileURL(buildPath).href);\n  let build: NormalizedBuild;\n  let isRSCBuild = false;\n\n  if ((isRSCBuild = isRSCServerBuild(buildModule))) {\n    const config = {\n      publicPath: \"/\",\n      assetsBuildDirectory: \"../client\",\n      ...(buildModule.unstable_reactRouterServeConfig || {}),\n    };\n    build = {\n      fetch: buildModule.default.fetch,\n      publicPath: config.publicPath,\n      assetsBuildDirectory: path.resolve(\n        path.dirname(buildPath),\n        config.assetsBuildDirectory,\n      ),\n    } satisfies NormalizedBuild;\n  } else {\n    build = buildModule as ServerBuild;\n  }\n\n  let onListen = () => {\n    let address =\n      process.env.HOST ||\n      Object.values(os.networkInterfaces())\n        .flat()\n        .find((ip) => String(ip?.family).includes(\"4\") && !ip?.internal)\n        ?.address;\n\n    if (!address) {\n      console.log(`[react-router-serve] http://localhost:${port}`);\n    } else {\n      console.log(\n        `[react-router-serve] http://localhost:${port} (http://${address}:${port})`,\n      );\n    }\n  };\n\n  let app = express();\n  app.disable(\"x-powered-by\");\n\n  if (!isRSCBuild) {\n    app.use(compression());\n  }\n\n  app.use(\n    path.posix.join(build.publicPath, \"assets\"),\n    express.static(path.join(build.assetsBuildDirectory, \"assets\"), {\n      immutable: true,\n      maxAge: \"1y\",\n    }),\n  );\n  app.use(build.publicPath, express.static(build.assetsBuildDirectory));\n  app.use(express.static(\"public\", { maxAge: \"1h\" }));\n  app.use(morgan(\"tiny\"));\n\n  if (build.fetch) {\n    app.all(\"*\", createRequestListener(build.fetch));\n  } else {\n    app.all(\n      \"*\",\n      createRequestHandler({\n        build: buildModule,\n        mode: process.env.NODE_ENV,\n      }),\n    );\n  }\n\n  let server = process.env.HOST\n    ? app.listen(port, process.env.HOST, onListen)\n    : app.listen(port, onListen);\n\n  [\"SIGTERM\", \"SIGINT\"].forEach((signal) => {\n    process.once(signal, () => server?.close(console.error));\n  });\n}\n"
  },
  {
    "path": "packages/react-router-serve/package.json",
    "content": "{\n  \"name\": \"@react-router/serve\",\n  \"version\": \"7.13.1\",\n  \"description\": \"Production application server for React Router\",\n  \"bugs\": {\n    \"url\": \"https://github.com/remix-run/react-router/issues\"\n  },\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"https://github.com/remix-run/react-router\",\n    \"directory\": \"packages/react-router-serve\"\n  },\n  \"license\": \"MIT\",\n  \"exports\": {\n    \"./package.json\": \"./package.json\"\n  },\n  \"bin\": {\n    \"react-router-serve\": \"bin.js\"\n  },\n  \"scripts\": {\n    \"build\": \"wireit\",\n    \"typecheck\": \"tsc\"\n  },\n  \"wireit\": {\n    \"build\": {\n      \"command\": \"tsup\",\n      \"files\": [\n        \"../../pnpm-workspace.yaml\",\n        \"*.ts\",\n        \"bin.js\",\n        \"tsconfig.json\",\n        \"package.json\"\n      ],\n      \"output\": [\n        \"dist/**\"\n      ]\n    }\n  },\n  \"dependencies\": {\n    \"@mjackson/node-fetch-server\": \"^0.2.0\",\n    \"@react-router/express\": \"workspace:*\",\n    \"@react-router/node\": \"workspace:*\",\n    \"compression\": \"^1.8.1\",\n    \"express\": \"^4.19.2\",\n    \"get-port\": \"5.1.1\",\n    \"morgan\": \"^1.10.1\",\n    \"source-map-support\": \"^0.5.21\"\n  },\n  \"peerDependencies\": {\n    \"react-router\": \"workspace:*\"\n  },\n  \"devDependencies\": {\n    \"@types/compression\": \"^1.8.1\",\n    \"@types/express\": \"^4.17.9\",\n    \"@types/morgan\": \"^1.9.10\",\n    \"@types/source-map-support\": \"^0.5.6\",\n    \"tsup\": \"catalog:\",\n    \"typescript\": \"catalog:\",\n    \"wireit\": \"catalog:\"\n  },\n  \"engines\": {\n    \"node\": \">=20.0.0\"\n  },\n  \"files\": [\n    \"dist/\",\n    \"bin.js\",\n    \"CHANGELOG.md\",\n    \"LICENSE.md\",\n    \"README.md\"\n  ]\n}\n"
  },
  {
    "path": "packages/react-router-serve/tsconfig.json",
    "content": "{\n  \"include\": [\"**/*.ts\"],\n  \"exclude\": [\"dist\", \"node_modules\"],\n  \"compilerOptions\": {\n    \"lib\": [\"DOM\", \"DOM.Iterable\", \"ES2022\"],\n    \"target\": \"ES2022\",\n    \"module\": \"ES2022\",\n    \"skipLibCheck\": true,\n\n    \"moduleResolution\": \"Bundler\",\n    \"allowSyntheticDefaultImports\": true,\n    \"strict\": true,\n    \"jsx\": \"react\",\n    \"declaration\": true,\n    \"emitDeclarationOnly\": true,\n    \"rootDir\": \".\",\n    \"outDir\": \"./dist\"\n  }\n}\n"
  },
  {
    "path": "packages/react-router-serve/tsup.config.ts",
    "content": "import { defineConfig } from \"tsup\";\n\n// @ts-ignore - out of scope\nimport { createBanner } from \"../../build.utils.js\";\n\nimport pkg from \"./package.json\";\n\nconst entry = [\"cli.ts\"];\n\nexport default defineConfig([\n  {\n    clean: true,\n    entry,\n    format: [\"cjs\"],\n    outDir: \"dist\",\n    dts: true,\n    banner: {\n      js: createBanner(pkg.name, pkg.version),\n    },\n  },\n]);\n"
  },
  {
    "path": "packages/react-router-serve/typedoc.mjs",
    "content": "import { blockTags } from \"../../typedoc.mjs\";\n\n/** @type {Partial<import('typedoc').TypeDocOptions>} */\nconst config = {\n  entryPoints: [\"./cli.ts\"],\n  blockTags,\n};\n\nexport default config;\n"
  },
  {
    "path": "patches/@changesets__assemble-release-plan.patch",
    "content": "diff --git a/dist/assemble-release-plan.cjs.dev.js b/dist/assemble-release-plan.cjs.dev.js\nindex e1376ca756d69816f8c79637ee7b45161f092167..314c42e8c39a34dacc3ed0c10bc7e62ca46de7d3 100644\n--- a/dist/assemble-release-plan.cjs.dev.js\n+++ b/dist/assemble-release-plan.cjs.dev.js\n@@ -254,10 +254,16 @@ function shouldBumpMajor({\n   preInfo,\n   onlyUpdatePeerDependentsWhenOutOfRange\n }) {\n+  // PATCH: Don't do peerDependency-driven major bumps because we release in lock step\n+  if (nextRelease.name === \"react-router\" || nextRelease.name.startsWith('@react-router/')) {\n+    return false;\n+  }\n+\n   // we check if it is a peerDependency because if it is, our dependent bump type might need to be major.\n   return depType === \"peerDependencies\" && nextRelease.type !== \"none\" && nextRelease.type !== \"patch\" && ( // 1. If onlyUpdatePeerDependentsWhenOutOfRange set to true, bump major if the version is leaving the range.\n   // 2. If onlyUpdatePeerDependentsWhenOutOfRange set to false, bump major regardless whether or not the version is leaving the range.\n-  !onlyUpdatePeerDependentsWhenOutOfRange || !semverSatisfies__default['default'](incrementVersion(nextRelease, preInfo), versionRange)) && ( // bump major only if the dependent doesn't already has a major release.\n+  // PATCH: pass includePrerelease to incrementVersion()\n+  !onlyUpdatePeerDependentsWhenOutOfRange || !semverSatisfies__default['default'](incrementVersion(nextRelease, preInfo), versionRange, { includePrerelease: true })) && ( // bump major only if the dependent doesn't already has a major release.\n   !releases.has(dependent) || releases.has(dependent) && releases.get(dependent).type !== \"major\");\n }\n \ndiff --git a/dist/assemble-release-plan.cjs.prod.js b/dist/assemble-release-plan.cjs.prod.js\nindex 3a83720644a94cdf6e62fa188a72c51c0384d00e..273f6bb9b46cf166f9d72058e524b4f3cbc05957 100644\n--- a/dist/assemble-release-plan.cjs.prod.js\n+++ b/dist/assemble-release-plan.cjs.prod.js\n@@ -130,7 +130,10 @@ function getDependencyVersionRanges(dependentPkgJSON, dependencyRelease) {\n }\n \n function shouldBumpMajor({dependent: dependent, depType: depType, versionRange: versionRange, releases: releases, nextRelease: nextRelease, preInfo: preInfo, onlyUpdatePeerDependentsWhenOutOfRange: onlyUpdatePeerDependentsWhenOutOfRange}) {\n-  return \"peerDependencies\" === depType && \"none\" !== nextRelease.type && \"patch\" !== nextRelease.type && (!onlyUpdatePeerDependentsWhenOutOfRange || !semverSatisfies__default.default(incrementVersion(nextRelease, preInfo), versionRange)) && (!releases.has(dependent) || releases.has(dependent) && \"major\" !== releases.get(dependent).type);\n+  if (nextRelease.name === \"react-router\" || nextRelease.name.startsWith('@react-router/')) {\n+    return false;\n+  }\n+  return \"peerDependencies\" === depType && \"none\" !== nextRelease.type && \"patch\" !== nextRelease.type && (!onlyUpdatePeerDependentsWhenOutOfRange || !semverSatisfies__default.default(incrementVersion(nextRelease, preInfo), versionRange, { includePrerelease: true })) && (!releases.has(dependent) || releases.has(dependent) && \"major\" !== releases.get(dependent).type);\n }\n \n function flattenReleases(changesets, packagesByName, ignoredPackages) {\ndiff --git a/dist/assemble-release-plan.esm.js b/dist/assemble-release-plan.esm.js\nindex 62891eb5dee97a33e6587514267c3cde5b314830..9a70c1ac86f530dc0cb3857d202675ed23d694b5 100644\n--- a/dist/assemble-release-plan.esm.js\n+++ b/dist/assemble-release-plan.esm.js\n@@ -243,10 +243,16 @@ function shouldBumpMajor({\n   preInfo,\n   onlyUpdatePeerDependentsWhenOutOfRange\n }) {\n+  // PATCH: Don't do peerDependency-driven major bumps because we release in lock step\n+  if (nextRelease.name === \"react-router\" || nextRelease.name.startsWith('@react-router/')) {\n+    return false;\n+  }\n+\n   // we check if it is a peerDependency because if it is, our dependent bump type might need to be major.\n   return depType === \"peerDependencies\" && nextRelease.type !== \"none\" && nextRelease.type !== \"patch\" && ( // 1. If onlyUpdatePeerDependentsWhenOutOfRange set to true, bump major if the version is leaving the range.\n   // 2. If onlyUpdatePeerDependentsWhenOutOfRange set to false, bump major regardless whether or not the version is leaving the range.\n-  !onlyUpdatePeerDependentsWhenOutOfRange || !semverSatisfies(incrementVersion(nextRelease, preInfo), versionRange)) && ( // bump major only if the dependent doesn't already has a major release.\n+  // PATCH: pass includePrerelease to incrementVersion()\n+  !onlyUpdatePeerDependentsWhenOutOfRange || !semverSatisfies(incrementVersion(nextRelease, preInfo), versionRange, { includePrerelease: true })) && ( // bump major only if the dependent doesn't already has a major release.\n   !releases.has(dependent) || releases.has(dependent) && releases.get(dependent).type !== \"major\");\n }\n \n"
  },
  {
    "path": "patches/@changesets__get-dependents-graph@1.3.6.patch",
    "content": "diff --git a/dist/get-dependents-graph.cjs.dev.js b/dist/get-dependents-graph.cjs.dev.js\nindex 94dde7b0aa903cf4aa099acfd2fc62f6243264d9..70407b017dd86b0363af989663ff83e1ba34a925 100644\n--- a/dist/get-dependents-graph.cjs.dev.js\n+++ b/dist/get-dependents-graph.cjs.dev.js\n@@ -10,8 +10,14 @@ function _interopDefault (e) { return e && e.__esModule ? e : { 'default': e };\n var Range__default = /*#__PURE__*/_interopDefault(Range);\n var chalk__default = /*#__PURE__*/_interopDefault(chalk);\n \n-// This is a modified version of the graph-getting in bolt\n-const DEPENDENCY_TYPES = [\"dependencies\", \"devDependencies\", \"peerDependencies\", \"optionalDependencies\"];\n+// This is a modified version of the graph-getting in bolt PATCH: Changesets\n+// will check all of our internal dependencies to ensure that the versions\n+// required match the current version in the repo. This doesn't work for peer\n+// dependencies in our case because the compat package requires a peer\n+// dependency of v4 or v5.\n+// https://twitter.com/AndaristRake/status/1532379909028466688\n+const DEPENDENCY_TYPES = [\"dependencies\", \"devDependencies\", \"optionalDependencies\"];\n+// const DEPENDENCY_TYPES = [\"dependencies\", \"devDependencies\", \"peerDependencies\", \"optionalDependencies\"];\n \n const getAllDependencies = config => {\n   const allDependencies = new Map();\ndiff --git a/dist/get-dependents-graph.cjs.prod.js b/dist/get-dependents-graph.cjs.prod.js\nindex 39b2dfe0c15d052f4e342a81626c4012a695867b..dc0fed7320dd1157b3f020c06d46530547070cda 100644\n--- a/dist/get-dependents-graph.cjs.prod.js\n+++ b/dist/get-dependents-graph.cjs.prod.js\n@@ -14,7 +14,7 @@ function _interopDefault(e) {\n \n var Range__default = _interopDefault(Range), chalk__default = _interopDefault(chalk);\n \n-const DEPENDENCY_TYPES = [ \"dependencies\", \"devDependencies\", \"peerDependencies\", \"optionalDependencies\" ], getAllDependencies = config => {\n+const DEPENDENCY_TYPES = [ \"dependencies\", \"devDependencies\", \"optionalDependencies\" ], getAllDependencies = config => {\n   const allDependencies = new Map;\n   for (const type of DEPENDENCY_TYPES) {\n     const deps = config[type];\ndiff --git a/dist/get-dependents-graph.esm.js b/dist/get-dependents-graph.esm.js\nindex 5f29582339d3a6a45ff882caf843ecf8a9d6c99d..1e4b7509e48cff7a2c702922ee5a6987e33959e0 100644\n--- a/dist/get-dependents-graph.esm.js\n+++ b/dist/get-dependents-graph.esm.js\n@@ -2,7 +2,7 @@ import Range from 'semver/classes/range';\n import chalk from 'chalk';\n \n // This is a modified version of the graph-getting in bolt\n-const DEPENDENCY_TYPES = [\"dependencies\", \"devDependencies\", \"peerDependencies\", \"optionalDependencies\"];\n+const DEPENDENCY_TYPES = [\"dependencies\", \"devDependencies\", \"optionalDependencies\"];\n \n const getAllDependencies = config => {\n   const allDependencies = new Map();\n"
  },
  {
    "path": "patches/@mdx-js__rollup.patch",
    "content": "diff --git a/lib/index.js b/lib/index.js\nindex ddbf4a0ebac3adb0c6277f4771ce413d8058a367..a412cc138011a53afa222ad7a4ae23d83ea18759 100644\n--- a/lib/index.js\n+++ b/lib/index.js\n@@ -80,7 +80,7 @@ export function rollup(options) {\n         ...rest\n       })\n     },\n-    async transform(value, path) {\n+    async transform(value, id) {\n       if (!formatAwareProcessors) {\n         formatAwareProcessors = createFormatAwareProcessors({\n           SourceMapGenerator,\n@@ -88,6 +88,7 @@ export function rollup(options) {\n         })\n       }\n \n+      const [path] = id.split('?')\n       const file = new VFile({path, value})\n \n       if (\n"
  },
  {
    "path": "playground/data/index.html",
    "content": "<!doctype html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"UTF-8\" />\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" />\n    <title>React Router (data mode)</title>\n  </head>\n  <body>\n    <div id=\"root\"></div>\n    <script type=\"module\" src=\"/src/main.tsx\"></script>\n  </body>\n</html>\n"
  },
  {
    "path": "playground/data/package.json",
    "content": "{\n  \"name\": \"@playground/data\",\n  \"private\": true,\n  \"version\": \"0.0.0\",\n  \"type\": \"module\",\n  \"scripts\": {\n    \"dev\": \"vite\",\n    \"build\": \"tsc -b && vite build\",\n    \"preview\": \"vite preview\"\n  },\n  \"dependencies\": {\n    \"react\": \"catalog:\",\n    \"react-dom\": \"catalog:\",\n    \"react-router\": \"workspace:*\"\n  },\n  \"devDependencies\": {\n    \"@types/react\": \"catalog:\",\n    \"@types/react-dom\": \"catalog:\",\n    \"@vitejs/plugin-react\": \"^4\",\n    \"typescript\": \"catalog:\",\n    \"vite\": \"6.4.1\"\n  }\n}\n"
  },
  {
    "path": "playground/data/src/main.tsx",
    "content": "import * as React from \"react\";\nimport * as ReactClient from \"react-dom/client\";\nimport { createBrowserRouter, useLoaderData } from \"react-router\";\nimport { RouterProvider } from \"react-router/dom\";\n\nconst router = createBrowserRouter([\n  {\n    id: \"index\",\n    path: \"/\",\n    loader() {\n      return { message: \"Hello React Router!\" };\n    },\n    Component() {\n      let data = useLoaderData();\n      return <h1>{data.message}</h1>;\n    },\n  },\n]);\n\nReactClient.createRoot(document.getElementById(\"root\")!).render(\n  <React.StrictMode>\n    <RouterProvider router={router} />\n  </React.StrictMode>,\n);\n"
  },
  {
    "path": "playground/data/tsconfig.json",
    "content": "{\n  \"include\": [\"src\"],\n  \"compilerOptions\": {\n    \"lib\": [\"DOM\", \"DOM.Iterable\", \"ES2022\"],\n    \"jsx\": \"react-jsx\",\n    \"target\": \"ES2022\",\n    \"module\": \"ESNext\",\n    \"moduleResolution\": \"Bundler\",\n    \"resolveJsonModule\": true,\n    \"verbatimModuleSyntax\": true,\n    \"esModuleInterop\": true,\n    \"strict\": true,\n    \"allowJs\": true,\n    \"skipLibCheck\": true,\n    \"noEmit\": true\n  }\n}\n"
  },
  {
    "path": "playground/data/vite.config.ts",
    "content": "import { defineConfig } from \"vite\";\nimport react from \"@vitejs/plugin-react\";\n\n// https://vite.dev/config/\nexport default defineConfig({\n  plugins: [react()],\n});\n"
  },
  {
    "path": "playground/framework/.gitignore",
    "content": "node_modules\n\n/build\n.env\n\n.react-router/"
  },
  {
    "path": "playground/framework/app/root.tsx",
    "content": "import {\n  Link,\n  Links,\n  Meta,\n  Outlet,\n  Scripts,\n  ScrollRestoration,\n} from \"react-router\";\n\nexport function Layout({ children }: { children: React.ReactNode }) {\n  return (\n    <html lang=\"en\">\n      <head>\n        <meta charSet=\"utf-8\" />\n        <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />\n\n        <Meta />\n        <Links />\n      </head>\n      <body>\n        <ul>\n          <li>\n            <Link prefetch=\"intent\" to=\"/\">\n              Home\n            </Link>\n          </li>\n          <li>\n            <Link prefetch=\"intent\" to=\"/products/abc\">\n              Product\n            </Link>\n          </li>\n        </ul>\n        {children}\n        <ScrollRestoration />\n        <Scripts />\n      </body>\n    </html>\n  );\n}\n\nexport default function App() {\n  return <Outlet />;\n}\n"
  },
  {
    "path": "playground/framework/app/routes/$.tsx",
    "content": "import { data } from \"react-router\";\n\nexport function loader() {\n  return data(null, { status: 404 });\n}\n"
  },
  {
    "path": "playground/framework/app/routes/_index.tsx",
    "content": "import type { Route } from \"./+types/_index\";\n\nexport function loader({ params }: Route.LoaderArgs) {\n  return { planet: \"world\", date: new Date(), fn: () => 1 };\n}\n\nexport default function Index({ loaderData }: Route.ComponentProps) {\n  return <h1>Hello, {loaderData.planet}!</h1>;\n}\n"
  },
  {
    "path": "playground/framework/app/routes/product.tsx",
    "content": "import type { Route } from \"./+types/product\";\n\nexport function loader({ params }: Route.LoaderArgs) {\n  return { name: `Super cool product #${params.id}` };\n}\n\nexport default function Component({ loaderData }: Route.ComponentProps) {\n  return <h1>{loaderData.name}</h1>;\n}\n"
  },
  {
    "path": "playground/framework/app/routes.ts",
    "content": "import { type RouteConfig, index, route } from \"@react-router/dev/routes\";\n\nexport default [\n  index(\"routes/_index.tsx\"),\n  route(\"products/:id\", \"routes/product.tsx\"),\n] satisfies RouteConfig;\n"
  },
  {
    "path": "playground/framework/package.json",
    "content": "{\n  \"name\": \"@playground/framework\",\n  \"version\": \"0.0.0\",\n  \"private\": true,\n  \"sideEffects\": false,\n  \"type\": \"module\",\n  \"scripts\": {\n    \"build\": \"react-router build\",\n    \"dev\": \"react-router dev\",\n    \"start\": \"react-router-serve ./build/server/index.js\",\n    \"typecheck\": \"react-router typegen && tsc\"\n  },\n  \"dependencies\": {\n    \"@react-router/node\": \"workspace:*\",\n    \"@react-router/serve\": \"workspace:*\",\n    \"isbot\": \"^5.1.11\",\n    \"react\": \"catalog:\",\n    \"react-dom\": \"catalog:\",\n    \"react-router\": \"workspace:*\"\n  },\n  \"devDependencies\": {\n    \"@react-router/dev\": \"workspace:*\",\n    \"@types/react\": \"catalog:\",\n    \"@types/react-dom\": \"catalog:\",\n    \"typescript\": \"catalog:\",\n    \"vite\": \"^6.3.0\",\n    \"vite-tsconfig-paths\": \"^4.2.1\"\n  },\n  \"engines\": {\n    \"node\": \">=20.0.0\"\n  }\n}\n"
  },
  {
    "path": "playground/framework/react-router.config.ts",
    "content": "import type { Config } from \"@react-router/dev/config\";\n\nexport default {\n  future: {\n    unstable_subResourceIntegrity: true,\n  },\n} satisfies Config;\n"
  },
  {
    "path": "playground/framework/tsconfig.json",
    "content": "{\n  \"include\": [\n    \"**/*.ts\",\n    \"**/*.tsx\",\n    \"**/.server/**/*.ts\",\n    \"**/.server/**/*.tsx\",\n    \"**/.client/**/*.ts\",\n    \"**/.client/**/*.tsx\",\n    \"./.react-router/types/**/*\"\n  ],\n  \"compilerOptions\": {\n    \"lib\": [\"DOM\", \"DOM.Iterable\", \"ES2022\"],\n    \"types\": [\"@react-router/node\", \"vite/client\"],\n    \"verbatimModuleSyntax\": true,\n    \"esModuleInterop\": true,\n    \"jsx\": \"react-jsx\",\n    \"module\": \"ESNext\",\n    \"moduleResolution\": \"Bundler\",\n    \"resolveJsonModule\": true,\n    \"target\": \"ES2022\",\n    \"strict\": true,\n    \"allowJs\": true,\n    \"skipLibCheck\": true,\n    \"baseUrl\": \".\",\n    \"paths\": {\n      \"~/*\": [\"./app/*\"]\n    },\n    \"noEmit\": true,\n    \"rootDirs\": [\".\", \"./.react-router/types\"]\n  }\n}\n"
  },
  {
    "path": "playground/framework/vite.config.ts",
    "content": "import { reactRouter } from \"@react-router/dev/vite\";\nimport { defineConfig } from \"vite\";\nimport tsconfigPaths from \"vite-tsconfig-paths\";\n\nexport default defineConfig({\n  plugins: [reactRouter(), tsconfigPaths()],\n});\n"
  },
  {
    "path": "playground/framework-express/.gitignore",
    "content": "node_modules\n\n/build\n.env\n\n.react-router/\n"
  },
  {
    "path": "playground/framework-express/app/root.tsx",
    "content": "import { Links, Meta, Outlet, Scripts, ScrollRestoration } from \"react-router\";\n\nexport function Layout({ children }: { children: React.ReactNode }) {\n  return (\n    <html lang=\"en\">\n      <head>\n        <meta charSet=\"utf-8\" />\n        <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />\n        <Meta />\n        <Links />\n      </head>\n      <body>\n        {children}\n        <ScrollRestoration />\n        <Scripts />\n      </body>\n    </html>\n  );\n}\n\nexport default function App() {\n  return <Outlet />;\n}\n"
  },
  {
    "path": "playground/framework-express/app/routes/_index.tsx",
    "content": "import type { MetaFunction } from \"react-router\";\n\nexport const meta: MetaFunction = () => {\n  return [\n    { title: \"New React Router App\" },\n    { name: \"description\", content: \"Welcome to React Router!\" },\n  ];\n};\n\nexport default function Index() {\n  return (\n    <div style={{ fontFamily: \"system-ui, sans-serif\", lineHeight: \"1.8\" }}>\n      <h1>Welcome to React Router</h1>\n    </div>\n  );\n}\n"
  },
  {
    "path": "playground/framework-express/app/routes.ts",
    "content": "import { type RouteConfig, index } from \"@react-router/dev/routes\";\n\nexport default [index(\"routes/_index.tsx\")] satisfies RouteConfig;\n"
  },
  {
    "path": "playground/framework-express/package.json",
    "content": "{\n  \"name\": \"@playground/framework-express\",\n  \"version\": \"0.0.0\",\n  \"private\": true,\n  \"sideEffects\": false,\n  \"type\": \"module\",\n  \"scripts\": {\n    \"build\": \"react-router build\",\n    \"dev\": \"node ./server.js\",\n    \"start\": \"cross-env NODE_ENV=production node ./server.js\",\n    \"typecheck\": \"react-router typegen && tsc\"\n  },\n  \"dependencies\": {\n    \"@react-router/express\": \"workspace:*\",\n    \"@react-router/node\": \"workspace:*\",\n    \"compression\": \"^1.8.1\",\n    \"express\": \"^4.19.2\",\n    \"isbot\": \"^5.1.11\",\n    \"morgan\": \"^1.10.1\",\n    \"react\": \"catalog:\",\n    \"react-dom\": \"catalog:\",\n    \"react-router\": \"workspace:*\"\n  },\n  \"devDependencies\": {\n    \"@react-router/dev\": \"workspace:*\",\n    \"@types/compression\": \"^1.8.1\",\n    \"@types/express\": \"^4.17.20\",\n    \"@types/morgan\": \"^1.9.10\",\n    \"@types/react\": \"catalog:\",\n    \"@types/react-dom\": \"catalog:\",\n    \"cross-env\": \"^7.0.3\",\n    \"typescript\": \"catalog:\",\n    \"vite\": \"^6.3.0\",\n    \"vite-tsconfig-paths\": \"^4.2.1\"\n  },\n  \"engines\": {\n    \"node\": \">=20.0.0\"\n  }\n}\n"
  },
  {
    "path": "playground/framework-express/react-router.config.ts",
    "content": "import type { Config } from \"@react-router/dev/config\";\n\nexport default {} satisfies Config;\n"
  },
  {
    "path": "playground/framework-express/server.js",
    "content": "import { createRequestHandler } from \"@react-router/express\";\nimport compression from \"compression\";\nimport express from \"express\";\nimport morgan from \"morgan\";\n\nconst viteDevServer =\n  process.env.NODE_ENV === \"production\"\n    ? undefined\n    : await import(\"vite\").then((vite) =>\n        vite.createServer({\n          server: { middlewareMode: true },\n        })\n      );\n\nconst reactRouterHandler = createRequestHandler({\n  build: viteDevServer\n    ? () => viteDevServer.ssrLoadModule(\"virtual:react-router/server-build\")\n    : await import(\"./build/server/index.js\"),\n});\n\nconst app = express();\n\napp.use(compression());\napp.disable(\"x-powered-by\");\n\nif (viteDevServer) {\n  app.use(viteDevServer.middlewares);\n} else {\n  app.use(\n    \"/assets\",\n    express.static(\"build/client/assets\", { immutable: true, maxAge: \"1y\" })\n  );\n}\n\napp.use(express.static(\"build/client\", { maxAge: \"1h\" }));\napp.use(morgan(\"tiny\"));\n\napp.all(\"*\", reactRouterHandler);\n\nconst port = process.env.PORT || 3000;\napp.listen(port, () =>\n  console.log(`Express server listening at http://localhost:${port}`)\n);\n"
  },
  {
    "path": "playground/framework-express/tsconfig.json",
    "content": "{\n  \"include\": [\n    \"**/*.ts\",\n    \"**/*.tsx\",\n    \"**/.server/**/*.ts\",\n    \"**/.server/**/*.tsx\",\n    \"**/.client/**/*.ts\",\n    \"**/.client/**/*.tsx\",\n    \"./.react-router/types/**/*\"\n  ],\n  \"compilerOptions\": {\n    \"lib\": [\"DOM\", \"DOM.Iterable\", \"ES2022\"],\n    \"types\": [\"@react-router/node\", \"vite/client\"],\n    \"verbatimModuleSyntax\": true,\n    \"esModuleInterop\": true,\n    \"jsx\": \"react-jsx\",\n    \"module\": \"ESNext\",\n    \"moduleResolution\": \"Bundler\",\n    \"resolveJsonModule\": true,\n    \"target\": \"ES2022\",\n    \"strict\": true,\n    \"allowJs\": true,\n    \"skipLibCheck\": true,\n    \"baseUrl\": \".\",\n    \"paths\": {\n      \"~/*\": [\"./app/*\"]\n    },\n    \"noEmit\": true,\n    \"rootDirs\": [\".\", \"./.react-router/types\"]\n  }\n}\n"
  },
  {
    "path": "playground/framework-express/vite.config.ts",
    "content": "import { reactRouter } from \"@react-router/dev/vite\";\nimport { defineConfig } from \"vite\";\nimport tsconfigPaths from \"vite-tsconfig-paths\";\n\nexport default defineConfig({\n  plugins: [reactRouter(), tsconfigPaths()],\n});\n"
  },
  {
    "path": "playground/framework-rolldown-vite/.gitignore",
    "content": "node_modules\n\n/build\n.env\n\n.react-router/\n"
  },
  {
    "path": "playground/framework-rolldown-vite/app/root.tsx",
    "content": "import { Links, Meta, Outlet, Scripts, ScrollRestoration } from \"react-router\";\n\nexport function Layout({ children }: { children: React.ReactNode }) {\n  return (\n    <html lang=\"en\">\n      <head>\n        <meta charSet=\"utf-8\" />\n        <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />\n        <Meta />\n        <Links />\n      </head>\n      <body>\n        {children}\n        <ScrollRestoration />\n        <Scripts />\n      </body>\n    </html>\n  );\n}\n\nexport default function App() {\n  return <Outlet />;\n}\n"
  },
  {
    "path": "playground/framework-rolldown-vite/app/routes/_index.tsx",
    "content": "import type { Route } from \"./+types/_index\";\n\nexport function loader({ params }: Route.LoaderArgs) {\n  return { planet: \"world\", date: new Date(), fn: () => 1 };\n}\n\nexport default function Index({ loaderData }: Route.ComponentProps) {\n  return <h1>Hello, {loaderData.planet}!</h1>;\n}\n"
  },
  {
    "path": "playground/framework-rolldown-vite/app/routes/product.tsx",
    "content": "import type { Route } from \"./+types/product\";\n\nexport function loader({ params }: Route.LoaderArgs) {\n  return { name: `Super cool product #${params.id}` };\n}\n\nexport default function Component({ loaderData }: Route.ComponentProps) {\n  return <h1>{loaderData.name}</h1>;\n}\n"
  },
  {
    "path": "playground/framework-rolldown-vite/app/routes.ts",
    "content": "import { type RouteConfig, index, route } from \"@react-router/dev/routes\";\n\nexport default [\n  index(\"routes/_index.tsx\"),\n  route(\"products/:id\", \"routes/product.tsx\"),\n] satisfies RouteConfig;\n"
  },
  {
    "path": "playground/framework-rolldown-vite/package.json",
    "content": "{\n  \"name\": \"@playground/framework-rolldown-vite\",\n  \"version\": \"0.0.0\",\n  \"private\": true,\n  \"sideEffects\": false,\n  \"type\": \"module\",\n  \"scripts\": {\n    \"build\": \"cross-env ROLLDOWN_OPTIONS_VALIDATION=loose react-router build\",\n    \"dev\": \"react-router dev\",\n    \"start\": \"react-router-serve ./build/server/index.js\",\n    \"typecheck\": \"react-router typegen && tsc\"\n  },\n  \"dependencies\": {\n    \"@react-router/node\": \"workspace:*\",\n    \"@react-router/serve\": \"workspace:*\",\n    \"cross-env\": \"^7.0.3\",\n    \"isbot\": \"^5.1.11\",\n    \"react\": \"catalog:\",\n    \"react-dom\": \"catalog:\",\n    \"react-router\": \"workspace:*\"\n  },\n  \"devDependencies\": {\n    \"@react-router/dev\": \"workspace:*\",\n    \"@types/react\": \"catalog:\",\n    \"@types/react-dom\": \"catalog:\",\n    \"typescript\": \"catalog:\",\n    \"vite\": \"npm:rolldown-vite@6.3.0-beta.3\",\n    \"vite-tsconfig-paths\": \"^4.2.1\"\n  },\n  \"overrides\": {\n    \"vite\": \"npm:rolldown-vite@6.3.0-beta.3\"\n  },\n  \"engines\": {\n    \"node\": \">=20.0.0\"\n  }\n}\n"
  },
  {
    "path": "playground/framework-rolldown-vite/react-router.config.ts",
    "content": "import type { Config } from \"@react-router/dev/config\";\n\nexport default {} satisfies Config;\n"
  },
  {
    "path": "playground/framework-rolldown-vite/tsconfig.json",
    "content": "{\n  \"include\": [\n    \"**/*.ts\",\n    \"**/*.tsx\",\n    \"**/.server/**/*.ts\",\n    \"**/.server/**/*.tsx\",\n    \"**/.client/**/*.ts\",\n    \"**/.client/**/*.tsx\",\n    \"./.react-router/types/**/*\"\n  ],\n  \"compilerOptions\": {\n    \"lib\": [\"DOM\", \"DOM.Iterable\", \"ES2022\"],\n    \"types\": [\"@react-router/node\", \"vite/client\"],\n    \"verbatimModuleSyntax\": true,\n    \"esModuleInterop\": true,\n    \"jsx\": \"react-jsx\",\n    \"module\": \"ESNext\",\n    \"moduleResolution\": \"Bundler\",\n    \"resolveJsonModule\": true,\n    \"target\": \"ES2022\",\n    \"strict\": true,\n    \"allowJs\": true,\n    \"skipLibCheck\": true,\n    \"baseUrl\": \".\",\n    \"paths\": {\n      \"~/*\": [\"./app/*\"]\n    },\n    \"noEmit\": true,\n    \"rootDirs\": [\".\", \"./.react-router/types\"]\n  }\n}\n"
  },
  {
    "path": "playground/framework-rolldown-vite/vite.config.ts",
    "content": "import { reactRouter } from \"@react-router/dev/vite\";\nimport { defineConfig, type UserConfig } from \"vite\";\nimport tsconfigPaths from \"vite-tsconfig-paths\";\n\nexport default defineConfig(({ isSsrBuild }) => {\n  const config: UserConfig = {\n    plugins: [\n      // @ts-expect-error `dev` depends on Vite 6, Plugin type is mismatched.\n      reactRouter(),\n      tsconfigPaths(),\n    ],\n    build: {\n      // Built-in minifier is still experimental\n      minify: isSsrBuild ? false : \"esbuild\",\n    },\n  };\n\n  return config;\n});\n"
  },
  {
    "path": "playground/framework-spa/.gitignore",
    "content": "node_modules\n\n/build\n.env\n\n.react-router/\n"
  },
  {
    "path": "playground/framework-spa/app/root.tsx",
    "content": "import { Links, Meta, Outlet, Scripts, ScrollRestoration } from \"react-router\";\n\nexport function Layout({ children }: { children: React.ReactNode }) {\n  return (\n    <html lang=\"en\">\n      <head>\n        <meta charSet=\"utf-8\" />\n        <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />\n        <Meta />\n        <Links />\n      </head>\n      <body>\n        {children}\n        <ScrollRestoration />\n        <Scripts />\n      </body>\n    </html>\n  );\n}\n\nexport default function App() {\n  return <Outlet />;\n}\n\nexport function HydrateFallback() {\n  return <p>Loading...</p>;\n}\n"
  },
  {
    "path": "playground/framework-spa/app/routes/_index.tsx",
    "content": "import type { MetaFunction } from \"react-router\";\n\nexport const meta: MetaFunction = () => {\n  return [\n    { title: \"New React Router App\" },\n    { name: \"description\", content: \"Welcome to React Router!\" },\n  ];\n};\n\nexport default function Index() {\n  return (\n    <div style={{ fontFamily: \"system-ui, sans-serif\", lineHeight: \"1.8\" }}>\n      <h1>Welcome to React Router</h1>\n    </div>\n  );\n}\n"
  },
  {
    "path": "playground/framework-spa/app/routes.ts",
    "content": "import { type RouteConfig, index } from \"@react-router/dev/routes\";\n\nexport default [index(\"routes/_index.tsx\")] satisfies RouteConfig;\n"
  },
  {
    "path": "playground/framework-spa/package.json",
    "content": "{\n  \"name\": \"@playground/framework-spa\",\n  \"version\": \"0.0.0\",\n  \"private\": true,\n  \"sideEffects\": false,\n  \"type\": \"module\",\n  \"scripts\": {\n    \"build\": \"react-router build\",\n    \"dev\": \"react-router dev\",\n    \"preview\": \"vite preview\",\n    \"typecheck\": \"react-router typegen && tsc\"\n  },\n  \"dependencies\": {\n    \"@react-router/node\": \"workspace:*\",\n    \"isbot\": \"^5\",\n    \"react\": \"catalog:\",\n    \"react-dom\": \"catalog:\",\n    \"react-router\": \"workspace:*\"\n  },\n  \"devDependencies\": {\n    \"@react-router/dev\": \"workspace:*\",\n    \"@types/react\": \"catalog:\",\n    \"@types/react-dom\": \"catalog:\",\n    \"typescript\": \"catalog:\",\n    \"vite\": \"^6.3.0\",\n    \"vite-tsconfig-paths\": \"^4.2.1\"\n  },\n  \"engines\": {\n    \"node\": \">=20.0.0\"\n  }\n}\n"
  },
  {
    "path": "playground/framework-spa/react-router.config.ts",
    "content": "import type { Config } from \"@react-router/dev/config\";\n\nexport default {\n  ssr: false,\n} satisfies Config;\n"
  },
  {
    "path": "playground/framework-spa/tsconfig.json",
    "content": "{\n  \"include\": [\"**/*.ts\", \"**/*.tsx\", \"./.react-router/types/**/*\"],\n  \"compilerOptions\": {\n    \"lib\": [\"DOM\", \"DOM.Iterable\", \"ES2022\"],\n    \"types\": [\"@react-router/node\", \"vite/client\"],\n    \"verbatimModuleSyntax\": true,\n    \"esModuleInterop\": true,\n    \"jsx\": \"react-jsx\",\n    \"module\": \"ESNext\",\n    \"moduleResolution\": \"Bundler\",\n    \"resolveJsonModule\": true,\n    \"target\": \"ES2022\",\n    \"strict\": true,\n    \"allowJs\": true,\n    \"skipLibCheck\": true,\n    \"baseUrl\": \".\",\n    \"paths\": {\n      \"~/*\": [\"./app/*\"]\n    },\n    \"noEmit\": true,\n    \"rootDirs\": [\".\", \"./.react-router/types\"]\n  }\n}\n"
  },
  {
    "path": "playground/framework-spa/vite.config.ts",
    "content": "import { reactRouter } from \"@react-router/dev/vite\";\nimport { defineConfig } from \"vite\";\nimport tsconfigPaths from \"vite-tsconfig-paths\";\n\nexport default defineConfig({\n  plugins: [reactRouter(), tsconfigPaths()],\n});\n"
  },
  {
    "path": "playground/framework-vite-5/.gitignore",
    "content": "node_modules\n\n/build\n.env\n\n.react-router/"
  },
  {
    "path": "playground/framework-vite-5/app/root.tsx",
    "content": "import { Links, Meta, Outlet, Scripts, ScrollRestoration } from \"react-router\";\n\nexport function Layout({ children }: { children: React.ReactNode }) {\n  return (\n    <html lang=\"en\">\n      <head>\n        <meta charSet=\"utf-8\" />\n        <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />\n        <Meta />\n        <Links />\n      </head>\n      <body>\n        {children}\n        <ScrollRestoration />\n        <Scripts />\n      </body>\n    </html>\n  );\n}\n\nexport default function App() {\n  return <Outlet />;\n}\n"
  },
  {
    "path": "playground/framework-vite-5/app/routes/_index.tsx",
    "content": "import type { Route } from \"./+types/_index\";\n\nexport function loader({ params }: Route.LoaderArgs) {\n  return { planet: \"world\", date: new Date(), fn: () => 1 };\n}\n\nexport default function Index({ loaderData }: Route.ComponentProps) {\n  return <h1>Hello, {loaderData.planet}!</h1>;\n}\n"
  },
  {
    "path": "playground/framework-vite-5/app/routes/product.tsx",
    "content": "import type { Route } from \"./+types/product\";\n\nexport function loader({ params }: Route.LoaderArgs) {\n  return { name: `Super cool product #${params.id}` };\n}\n\nexport default function Component({ loaderData }: Route.ComponentProps) {\n  return <h1>{loaderData.name}</h1>;\n}\n"
  },
  {
    "path": "playground/framework-vite-5/app/routes.ts",
    "content": "import { type RouteConfig, index, route } from \"@react-router/dev/routes\";\n\nexport default [\n  index(\"routes/_index.tsx\"),\n  route(\"products/:id\", \"routes/product.tsx\"),\n] satisfies RouteConfig;\n"
  },
  {
    "path": "playground/framework-vite-5/package.json",
    "content": "{\n  \"name\": \"@playground/framework-vite-5\",\n  \"version\": \"0.0.0\",\n  \"private\": true,\n  \"sideEffects\": false,\n  \"type\": \"module\",\n  \"scripts\": {\n    \"build\": \"react-router build\",\n    \"dev\": \"react-router dev\",\n    \"start\": \"react-router-serve ./build/server/index.js\",\n    \"typecheck\": \"react-router typegen && tsc\"\n  },\n  \"dependencies\": {\n    \"@react-router/node\": \"workspace:*\",\n    \"@react-router/serve\": \"workspace:*\",\n    \"isbot\": \"^5.1.11\",\n    \"react\": \"catalog:\",\n    \"react-dom\": \"catalog:\",\n    \"react-router\": \"workspace:*\"\n  },\n  \"devDependencies\": {\n    \"@react-router/dev\": \"workspace:*\",\n    \"@types/react\": \"catalog:\",\n    \"@types/react-dom\": \"catalog:\",\n    \"typescript\": \"catalog:\",\n    \"vite\": \"^5.1.0\",\n    \"vite-tsconfig-paths\": \"^4.2.1\"\n  },\n  \"engines\": {\n    \"node\": \">=20.0.0\"\n  }\n}\n"
  },
  {
    "path": "playground/framework-vite-5/react-router.config.ts",
    "content": "import type { Config } from \"@react-router/dev/config\";\n\nexport default {} satisfies Config;\n"
  },
  {
    "path": "playground/framework-vite-5/tsconfig.json",
    "content": "{\n  \"include\": [\n    \"**/*.ts\",\n    \"**/*.tsx\",\n    \"**/.server/**/*.ts\",\n    \"**/.server/**/*.tsx\",\n    \"**/.client/**/*.ts\",\n    \"**/.client/**/*.tsx\",\n    \"./.react-router/types/**/*\"\n  ],\n  \"compilerOptions\": {\n    \"lib\": [\"DOM\", \"DOM.Iterable\", \"ES2022\"],\n    \"types\": [\"@react-router/node\", \"vite/client\"],\n    \"verbatimModuleSyntax\": true,\n    \"esModuleInterop\": true,\n    \"jsx\": \"react-jsx\",\n    \"module\": \"ESNext\",\n    \"moduleResolution\": \"Bundler\",\n    \"resolveJsonModule\": true,\n    \"target\": \"ES2022\",\n    \"strict\": true,\n    \"allowJs\": true,\n    \"skipLibCheck\": true,\n    \"baseUrl\": \".\",\n    \"paths\": {\n      \"~/*\": [\"./app/*\"]\n    },\n    \"noEmit\": true,\n    \"rootDirs\": [\".\", \"./.react-router/types\"]\n  }\n}\n"
  },
  {
    "path": "playground/framework-vite-5/vite.config.ts",
    "content": "import { reactRouter } from \"@react-router/dev/vite\";\nimport { defineConfig } from \"vite\";\nimport tsconfigPaths from \"vite-tsconfig-paths\";\n\nexport default defineConfig({\n  plugins: [\n    // @ts-expect-error `dev` depends on Vite 6, Plugin type is mismatched.\n    reactRouter(),\n    tsconfigPaths(),\n  ],\n});\n"
  },
  {
    "path": "playground/framework-vite-7-beta/.gitignore",
    "content": "node_modules\n\n/build\n.env\n\n.react-router/"
  },
  {
    "path": "playground/framework-vite-7-beta/app/root.tsx",
    "content": "import {\n  Link,\n  Links,\n  Meta,\n  Outlet,\n  Scripts,\n  ScrollRestoration,\n} from \"react-router\";\n\nexport function Layout({ children }: { children: React.ReactNode }) {\n  return (\n    <html lang=\"en\">\n      <head>\n        <meta charSet=\"utf-8\" />\n        <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />\n\n        <Meta />\n        <Links />\n      </head>\n      <body>\n        <ul>\n          <li>\n            <Link prefetch=\"intent\" to=\"/\">\n              Home\n            </Link>\n          </li>\n          <li>\n            <Link prefetch=\"intent\" to=\"/products/abc\">\n              Product\n            </Link>\n          </li>\n        </ul>\n        {children}\n        <ScrollRestoration />\n        <Scripts />\n      </body>\n    </html>\n  );\n}\n\nexport default function App() {\n  return <Outlet />;\n}\n"
  },
  {
    "path": "playground/framework-vite-7-beta/app/routes/_index.tsx",
    "content": "import type { Route } from \"./+types/_index\";\n\nexport function loader({ params }: Route.LoaderArgs) {\n  return { planet: \"world\", date: new Date(), fn: () => 1 };\n}\n\nexport default function Index({ loaderData }: Route.ComponentProps) {\n  return <h1>Hello, {loaderData.planet}!</h1>;\n}\n"
  },
  {
    "path": "playground/framework-vite-7-beta/app/routes/product.tsx",
    "content": "import type { Route } from \"./+types/product\";\n\nexport function loader({ params }: Route.LoaderArgs) {\n  return { name: `Super cool product #${params.id}` };\n}\n\nexport default function Component({ loaderData }: Route.ComponentProps) {\n  return <h1>{loaderData.name}</h1>;\n}\n"
  },
  {
    "path": "playground/framework-vite-7-beta/app/routes.ts",
    "content": "import { type RouteConfig, index, route } from \"@react-router/dev/routes\";\n\nexport default [\n  index(\"routes/_index.tsx\"),\n  route(\"products/:id\", \"routes/product.tsx\"),\n] satisfies RouteConfig;\n"
  },
  {
    "path": "playground/framework-vite-7-beta/package.json",
    "content": "{\n  \"name\": \"@playground/framework-vite-7-beta\",\n  \"version\": \"0.0.0\",\n  \"private\": true,\n  \"sideEffects\": false,\n  \"type\": \"module\",\n  \"scripts\": {\n    \"build\": \"react-router build\",\n    \"dev\": \"react-router dev\",\n    \"start\": \"react-router-serve ./build/server/index.js\",\n    \"typecheck\": \"react-router typegen && tsc\"\n  },\n  \"dependencies\": {\n    \"@react-router/node\": \"workspace:*\",\n    \"@react-router/serve\": \"workspace:*\",\n    \"isbot\": \"^5.1.11\",\n    \"react\": \"catalog:\",\n    \"react-dom\": \"catalog:\",\n    \"react-router\": \"workspace:*\"\n  },\n  \"devDependencies\": {\n    \"@react-router/dev\": \"workspace:*\",\n    \"@types/react\": \"catalog:\",\n    \"@types/react-dom\": \"catalog:\",\n    \"typescript\": \"catalog:\",\n    \"vite\": \"7.0.0-beta.0\",\n    \"vite-tsconfig-paths\": \"^4.2.1\"\n  },\n  \"engines\": {\n    \"node\": \">=20.0.0\"\n  }\n}\n"
  },
  {
    "path": "playground/framework-vite-7-beta/react-router.config.ts",
    "content": "import type { Config } from \"@react-router/dev/config\";\n\nexport default {\n  future: {\n    unstable_subResourceIntegrity: true,\n    unstable_optimizeDeps: true,\n    v8_viteEnvironmentApi: true,\n  },\n} satisfies Config;\n"
  },
  {
    "path": "playground/framework-vite-7-beta/tsconfig.json",
    "content": "{\n  \"include\": [\n    \"**/*.ts\",\n    \"**/*.tsx\",\n    \"**/.server/**/*.ts\",\n    \"**/.server/**/*.tsx\",\n    \"**/.client/**/*.ts\",\n    \"**/.client/**/*.tsx\",\n    \"./.react-router/types/**/*\"\n  ],\n  \"compilerOptions\": {\n    \"lib\": [\"DOM\", \"DOM.Iterable\", \"ES2022\"],\n    \"types\": [\"@react-router/node\", \"vite/client\"],\n    \"verbatimModuleSyntax\": true,\n    \"esModuleInterop\": true,\n    \"jsx\": \"react-jsx\",\n    \"module\": \"ESNext\",\n    \"moduleResolution\": \"Bundler\",\n    \"resolveJsonModule\": true,\n    \"target\": \"ES2022\",\n    \"strict\": true,\n    \"allowJs\": true,\n    \"skipLibCheck\": true,\n    \"baseUrl\": \".\",\n    \"paths\": {\n      \"~/*\": [\"./app/*\"]\n    },\n    \"noEmit\": true,\n    \"rootDirs\": [\".\", \"./.react-router/types\"]\n  }\n}\n"
  },
  {
    "path": "playground/framework-vite-7-beta/vite.config.ts",
    "content": "import { reactRouter } from \"@react-router/dev/vite\";\nimport { defineConfig } from \"vite\";\nimport tsconfigPaths from \"vite-tsconfig-paths\";\n\nexport default defineConfig({\n  // @ts-expect-error `dev` depends on Vite 6, Plugin type is mismatched.\n  plugins: [reactRouter(), tsconfigPaths()],\n});\n"
  },
  {
    "path": "playground/middleware/.gitignore",
    "content": "node_modules\n\n/build\n.env\n\n.react-router/"
  },
  {
    "path": "playground/middleware/app/contexts.ts",
    "content": "import { createContext } from \"react-router\";\n\nexport const expressContext = createContext<string>(\"default\");\nexport const rootContext = createContext<string>();\nexport const aContext = createContext<string>();\nexport const bContext = createContext<string>(\"empty\");\n"
  },
  {
    "path": "playground/middleware/app/root.tsx",
    "content": "import {\n  Link,\n  Links,\n  Meta,\n  Outlet,\n  Scripts,\n  ScrollRestoration,\n} from \"react-router\";\nimport type { Route } from \"./+types/root\";\nimport { rootContext } from \"./contexts\";\n\nexport const middleware: Route.MiddlewareFunction[] = [\n  async ({ context }, next) => {\n    console.log(\"start root middleware\");\n    context.set(rootContext, \"ROOT\");\n    let res = await next();\n    console.log(\"end root middleware\");\n    return res;\n  },\n];\n\nexport const clientMiddleware: Route.ClientMiddlewareFunction[] = [\n  async ({ context }, next) => {\n    console.log(\"start root middleware\");\n    context.set(rootContext, \"ROOT\");\n    await next();\n    console.log(\"end root middleware\");\n  },\n];\n\nexport function Layout({ children }: { children: React.ReactNode }) {\n  return (\n    <html lang=\"en\">\n      <head>\n        <meta charSet=\"utf-8\" />\n        <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />\n        <Meta />\n        <Links />\n      </head>\n      <body>\n        <h1>Middleware playground</h1>\n        <nav>\n          <ul>\n            <li>\n              <Link to=\"/\">Go to /</Link>\n            </li>\n            <li>\n              Server middleware routes:\n              <ul>\n                <li>\n                  <Link to=\"/server/a\">Go to /server/a</Link>\n                </li>\n                <li>\n                  <Link to=\"/server/a/b\">Go to /server/a/b</Link>\n                </li>\n              </ul>\n            </li>\n            <li>\n              Client middleware routes:\n              <ul>\n                <li>\n                  <Link to=\"/client/a\">Go to /client/a</Link>\n                </li>\n                <li>\n                  <Link to=\"/client/a/b\">Go to /client/a/b</Link>\n                </li>\n              </ul>\n            </li>\n          </ul>\n        </nav>\n        {children}\n        <ScrollRestoration />\n        <Scripts />\n      </body>\n    </html>\n  );\n}\n\nexport default function App({ loaderData }: Route.ComponentProps) {\n  return <Outlet />;\n}\n"
  },
  {
    "path": "playground/middleware/app/routes/_index.tsx",
    "content": "import type { Route } from \"./+types/_index\";\n\nexport default function Index({}: Route.ComponentProps) {\n  return <></>;\n}\n"
  },
  {
    "path": "playground/middleware/app/routes/client.a.b.tsx",
    "content": "import { Outlet, createContext } from \"react-router\";\nimport type { Route } from \"./+types/client.a.b\";\nimport { aContext, bContext, rootContext } from \"~/contexts\";\n\nexport const clientMiddleware: Route.ClientMiddlewareFunction[] = [\n  async ({ context }, next) => {\n    console.log(\"start b middleware\");\n    context.set(bContext, \"B\");\n    await next();\n    console.log(\"end b middleware\");\n  },\n];\n\nexport async function clientLoader({ context }: Route.ClientLoaderArgs) {\n  await new Promise((r) => setTimeout(r, 200));\n  return JSON.stringify({\n    root: context.get(rootContext),\n    a: context.get(aContext),\n    b: context.get(bContext),\n  });\n}\n\nexport default function B({ loaderData }: Route.ComponentProps) {\n  return (\n    <>\n      <h2>B</h2>\n      <p>{loaderData}</p>\n      <Outlet />\n    </>\n  );\n}\n"
  },
  {
    "path": "playground/middleware/app/routes/client.a.tsx",
    "content": "import { Outlet } from \"react-router\";\nimport type { Route } from \"./+types/client.a\";\nimport { aContext, rootContext } from \"~/contexts\";\n\nexport const clientMiddleware: Route.ClientMiddlewareFunction[] = [\n  async ({ context }, next) => {\n    console.log(\"start a middleware\");\n    context.set(aContext, \"A\");\n    await next();\n    console.log(\"end a middleware\");\n  },\n];\n\nexport function clientLoader({ context }: Route.ClientLoaderArgs) {\n  return JSON.stringify({\n    root: context.get(rootContext),\n    a: context.get(aContext),\n  });\n}\n\nexport default function A({ loaderData }: Route.ComponentProps) {\n  return (\n    <>\n      <h1>A</h1>\n      <p>{loaderData}</p>\n      <Outlet />\n    </>\n  );\n}\n"
  },
  {
    "path": "playground/middleware/app/routes/server.a.b.tsx",
    "content": "import { Outlet, createContext } from \"react-router\";\nimport type { Route } from \"./+types/server.a.b\";\nimport { aContext, bContext, expressContext, rootContext } from \"~/contexts\";\n\nexport const middleware: Route.MiddlewareFunction[] = [\n  async ({ context }, next) => {\n    console.log(\"start b middleware\");\n    context.set(bContext, \"B\");\n    let res = await next();\n    console.log(\"end b middleware\");\n    return res;\n  },\n];\n\nexport async function loader({ context }: Route.LoaderArgs) {\n  await new Promise((r) => setTimeout(r, 200));\n  return JSON.stringify({\n    express: context.get(expressContext),\n    root: context.get(rootContext),\n    a: context.get(aContext),\n    b: context.get(bContext),\n  });\n}\n\nexport default function B({ loaderData }: Route.ComponentProps) {\n  return (\n    <>\n      <h2>B</h2>\n      <p>{loaderData}</p>\n      <Outlet />\n    </>\n  );\n}\n"
  },
  {
    "path": "playground/middleware/app/routes/server.a.tsx",
    "content": "import { Outlet, createContext } from \"react-router\";\nimport type { Route } from \"./+types/server.a\";\nimport { aContext, expressContext, rootContext } from \"~/contexts\";\n\nexport const middleware: Route.MiddlewareFunction[] = [\n  async ({ context }, next) => {\n    console.log(\"start a middleware\");\n    context.set(aContext, \"A\");\n    let res = await next();\n    console.log(\"end a middleware\");\n    return res;\n  },\n];\n\nexport function loader({ context }: Route.LoaderArgs) {\n  return JSON.stringify({\n    express: context.get(expressContext),\n    root: context.get(rootContext),\n    a: context.get(aContext),\n  });\n}\n\nexport default function A({ loaderData }: Route.ComponentProps) {\n  return (\n    <>\n      <h1>A</h1>\n      <p>{loaderData}</p>\n      <Outlet />\n    </>\n  );\n}\n"
  },
  {
    "path": "playground/middleware/app/routes.ts",
    "content": "import type { RouteConfig } from \"@react-router/dev/routes\";\nimport { flatRoutes } from \"@react-router/fs-routes\";\n\nexport default flatRoutes() satisfies RouteConfig;\n"
  },
  {
    "path": "playground/middleware/dev-server.ts",
    "content": "import express from \"express\";\n\nconst PORT = Number.parseInt(process.env.PORT || \"3000\");\n\nconst app = express();\n\nconsole.log(\"Starting development server\");\nconst viteDevServer = await import(\"vite\").then((vite) =>\n  vite.createServer({\n    server: { middlewareMode: true },\n    forceOptimizeDeps: process.argv.includes(\"--force\"),\n  })\n);\napp.use(viteDevServer.middlewares);\napp.use(async (req, res, next) => {\n  try {\n    const source = await viteDevServer.ssrLoadModule(\"./server.ts\");\n    return await source.app(req, res, next);\n  } catch (error) {\n    if (typeof error === \"object\" && error instanceof Error) {\n      viteDevServer.ssrFixStacktrace(error);\n    }\n    next(error);\n  }\n});\n\napp.listen(PORT, () => {\n  console.log(`Server is running on http://localhost:${PORT}`);\n});\n"
  },
  {
    "path": "playground/middleware/package.json",
    "content": "{\n  \"name\": \"@playground/framework\",\n  \"version\": \"0.0.0\",\n  \"private\": true,\n  \"sideEffects\": false,\n  \"type\": \"module\",\n  \"scripts\": {\n    \"build\": \"react-router build\",\n    \"dev\": \"cross-env NODE_ENV=development tsx dev-server.ts\",\n    \"start\": \"cross-env NODE_ENV=production node build/server/index.js\",\n    \"typecheck\": \"react-router typegen && tsc\"\n  },\n  \"dependencies\": {\n    \"@react-router/express\": \"workspace:*\",\n    \"@react-router/node\": \"workspace:*\",\n    \"compression\": \"^1.8.1\",\n    \"cross-env\": \"^7.0.3\",\n    \"express\": \"^4.19.2\",\n    \"isbot\": \"^5.1.11\",\n    \"morgan\": \"^1.10.1\",\n    \"react\": \"catalog:\",\n    \"react-dom\": \"catalog:\",\n    \"react-router\": \"workspace:*\"\n  },\n  \"devDependencies\": {\n    \"@react-router/dev\": \"workspace:*\",\n    \"@react-router/fs-routes\": \"workspace:*\",\n    \"@types/compression\": \"^1.8.1\",\n    \"@types/express\": \"^4.17.21\",\n    \"@types/express-serve-static-core\": \"^5.0.6\",\n    \"@types/morgan\": \"^1.9.10\",\n    \"@types/node\": \"^20.0.0\",\n    \"@types/react\": \"catalog:\",\n    \"@types/react-dom\": \"catalog:\",\n    \"tsx\": \"^4.19.3\",\n    \"typescript\": \"catalog:\",\n    \"vite\": \"^6.3.0\",\n    \"vite-tsconfig-paths\": \"^4.2.1\"\n  },\n  \"engines\": {\n    \"node\": \">=20.0.0\"\n  }\n}\n"
  },
  {
    "path": "playground/middleware/react-router.config.ts",
    "content": "import type { Config } from \"@react-router/dev/config\";\n\nexport default {\n  future: {\n    v8_middleware: true,\n    v8_splitRouteModules: true,\n  },\n} satisfies Config;\n"
  },
  {
    "path": "playground/middleware/server.ts",
    "content": "import { createRequestHandler } from \"@react-router/express\";\nimport compression from \"compression\";\nimport express from \"express\";\nimport morgan from \"morgan\";\nimport \"react-router\";\nimport { RouterContextProvider } from \"react-router\";\nimport { expressContext } from \"~/contexts\";\n\nexport const app = express();\napp.use(compression());\napp.disable(\"x-powered-by\");\n\nif (process.env.NODE_ENV === \"production\") {\n  app.use(\n    \"/assets\",\n    express.static(\"build/client/assets\", { immutable: true, maxAge: \"1y\" }),\n  );\n  app.use(express.static(\"build/client\", { maxAge: \"1h\" }));\n}\n\napp.use(morgan(\"tiny\"));\napp.use(\n  createRequestHandler({\n    build: () => import(\"virtual:react-router/server-build\"),\n    getLoadContext() {\n      return new RouterContextProvider(\n        new Map([[expressContext, \"Hello from Express\"]]),\n      );\n    },\n  }),\n);\n\nif (process.env.NODE_ENV === \"production\") {\n  console.log(\"Starting production server\");\n  const PORT = Number.parseInt(process.env.PORT || \"3000\");\n  app.listen(PORT, () => {\n    console.log(`Server is running on http://localhost:${PORT}`);\n  });\n}\n"
  },
  {
    "path": "playground/middleware/tsconfig.json",
    "content": "{\n  \"files\": [],\n  \"references\": [\n    { \"path\": \"./tsconfig.node.json\" },\n    { \"path\": \"./tsconfig.vite.json\" }\n  ],\n  \"compilerOptions\": {\n    \"checkJs\": true,\n    \"verbatimModuleSyntax\": true,\n    \"skipLibCheck\": true,\n    \"strict\": true,\n    \"noEmit\": true\n  }\n}\n"
  },
  {
    "path": "playground/middleware/tsconfig.node.json",
    "content": "{\n  \"extends\": \"./tsconfig.json\",\n  \"include\": [\n    \"**/*.ts\",\n    \"**/*.tsx\",\n    \"dev-server.ts\",\n    \"tailwind.config.ts\",\n    \"vite.config.ts\"\n  ],\n  \"compilerOptions\": {\n    \"composite\": true,\n    \"strict\": true,\n    \"types\": [\"node\"],\n    \"lib\": [\"ES2022\"],\n    \"target\": \"ES2022\",\n    \"module\": \"ES2022\",\n    \"moduleResolution\": \"bundler\"\n  }\n}\n"
  },
  {
    "path": "playground/middleware/tsconfig.vite.json",
    "content": "{\n  \"extends\": \"./tsconfig.json\",\n  \"include\": [\n    \"**/*.ts\",\n    \"**/*.tsx\",\n    \".react-router/types/**/*\",\n    \"app/**/*\",\n    \"app/**/.server/**/*\",\n    \"app/**/.client/**/*\"\n  ],\n  \"compilerOptions\": {\n    \"composite\": true,\n    \"strict\": true,\n    \"lib\": [\"DOM\", \"DOM.Iterable\", \"ES2022\"],\n    \"types\": [\"vite/client\"],\n    \"target\": \"ES2022\",\n    \"module\": \"ES2022\",\n    \"moduleResolution\": \"bundler\",\n    \"jsx\": \"react-jsx\",\n    \"baseUrl\": \".\",\n    \"rootDirs\": [\".\", \"./.react-router/types\"],\n    \"paths\": {\n      \"~/*\": [\"./app/*\"]\n    },\n    \"esModuleInterop\": true,\n    \"resolveJsonModule\": true\n  }\n}\n"
  },
  {
    "path": "playground/middleware/vite.config.ts",
    "content": "import { reactRouter } from \"@react-router/dev/vite\";\nimport { defineConfig } from \"vite\";\nimport tsconfigPaths from \"vite-tsconfig-paths\";\n\nexport default defineConfig(({ isSsrBuild }) => ({\n  build: {\n    minify: false,\n    rollupOptions: isSsrBuild\n      ? {\n          input: \"./server.ts\",\n        }\n      : undefined,\n  },\n  plugins: [reactRouter(), tsconfigPaths()],\n}));\n"
  },
  {
    "path": "playground/rsc-vite/.gitignore",
    "content": "dist\nnode_modules\n"
  },
  {
    "path": "playground/rsc-vite/package.json",
    "content": "{\n  \"name\": \"@playground/rsc-vite\",\n  \"version\": \"0.0.0\",\n  \"private\": true,\n  \"type\": \"module\",\n  \"scripts\": {\n    \"dev\": \"vite\",\n    \"build\": \"vite build --app\",\n    \"start\": \"cross-env NODE_ENV=production node server.js\",\n    \"typecheck\": \"tsc\"\n  },\n  \"devDependencies\": {\n    \"@types/express\": \"^5.0.0\",\n    \"@types/node\": \"^22.13.1\",\n    \"@types/react\": \"catalog:react-canary\",\n    \"@types/react-dom\": \"catalog:react-canary\",\n    \"@vitejs/plugin-react\": \"^4.5.2\",\n    \"@vitejs/plugin-rsc\": \"catalog:\",\n    \"cross-env\": \"^7.0.3\",\n    \"typescript\": \"catalog:\",\n    \"vite\": \"^6.3.0\"\n  },\n  \"dependencies\": {\n    \"@mjackson/node-fetch-server\": \"0.6.1\",\n    \"compression\": \"^1.8.1\",\n    \"express\": \"^4.21.2\",\n    \"react\": \"catalog:react-canary\",\n    \"react-dom\": \"catalog:react-canary\",\n    \"react-router\": \"workspace:*\",\n    \"react-server-dom-webpack\": \"catalog:react-canary\"\n  }\n}\n"
  },
  {
    "path": "playground/rsc-vite/server.js",
    "content": "import { parseArgs } from \"node:util\";\nimport { createRequestListener } from \"@mjackson/node-fetch-server\";\nimport compression from \"compression\";\nimport express from \"express\";\n\nimport rscRequestHandler from \"./dist/rsc/index.js\";\n\nconst app = express();\n\napp.use(compression());\napp.use(express.static(\"dist/client\"));\n\napp.get(\"/.well-known/appspecific/com.chrome.devtools.json\", (req, res) => {\n  res.status(404);\n  res.end();\n});\n\napp.use(createRequestListener(rscRequestHandler));\n\nconst { values } = parseArgs({\n  options: { p: { type: \"string\", default: \"3000\" } },\n  allowPositionals: true,\n});\n\nconst port = parseInt(values.p, 10);\napp.listen(port, () => {\n  console.log(`Server started on http://localhost:${port}`);\n});\n"
  },
  {
    "path": "playground/rsc-vite/src/counter.tsx",
    "content": "\"use client\";\n\nimport { useState } from \"react\";\n\nexport function Counter() {\n  const [count, setCount] = useState(0);\n\n  return (\n    <div>\n      <h1>Counter</h1>\n      <p>Current count: {count}</p>\n      <button type=\"button\" onClick={() => setCount(count + 1)}>\n        Increment\n      </button>\n    </div>\n  );\n}\n"
  },
  {
    "path": "playground/rsc-vite/src/entry.browser.tsx",
    "content": "import { startTransition, StrictMode } from \"react\";\nimport { hydrateRoot } from \"react-dom/client\";\nimport {\n  createFromReadableStream,\n  createTemporaryReferenceSet,\n  encodeReply,\n  setServerCallback,\n} from \"@vitejs/plugin-rsc/browser\";\nimport {\n  unstable_createCallServer as createCallServer,\n  unstable_getRSCStream as getRSCStream,\n  unstable_RSCHydratedRouter as RSCHydratedRouter,\n  type unstable_RSCPayload as RSCPayload,\n} from \"react-router/dom\";\n\nsetServerCallback(\n  createCallServer({\n    createFromReadableStream,\n    createTemporaryReferenceSet,\n    encodeReply,\n  }),\n);\n\ncreateFromReadableStream<RSCPayload>(getRSCStream()).then((payload) => {\n  // @ts-expect-error - on 18 types, requires 19.\n  startTransition(async () => {\n    const formState =\n      payload.type === \"render\" ? await payload.formState : undefined;\n\n    hydrateRoot(\n      document,\n      <StrictMode>\n        <RSCHydratedRouter\n          payload={payload}\n          createFromReadableStream={createFromReadableStream}\n        />\n      </StrictMode>,\n      {\n        // @ts-expect-error - no types for this yet\n        formState,\n      },\n    );\n  });\n});\n"
  },
  {
    "path": "playground/rsc-vite/src/entry.rsc.tsx",
    "content": "import {\n  createTemporaryReferenceSet,\n  decodeAction,\n  decodeFormState,\n  decodeReply,\n  loadServerAction,\n  renderToReadableStream,\n} from \"@vitejs/plugin-rsc/rsc\";\nimport { unstable_matchRSCServerRequest as matchRSCServerRequest } from \"react-router\";\n\nimport { routes } from \"./routes\";\n\nexport async function fetchServer(request: Request) {\n  return await matchRSCServerRequest({\n    createTemporaryReferenceSet,\n    decodeAction,\n    decodeFormState,\n    decodeReply,\n    loadServerAction,\n    request,\n    // @ts-expect-error\n    routes,\n    generateResponse(match, options) {\n      return new Response(renderToReadableStream(match.payload, options), {\n        status: match.statusCode,\n        headers: match.headers,\n      });\n    },\n  });\n}\n\nexport default async function handler(request: Request) {\n  const ssr = await import.meta.viteRsc.loadModule<\n    typeof import(\"./entry.ssr\")\n  >(\"ssr\", \"index\");\n  return ssr.default(request, await fetchServer(request));\n}\n\nif (import.meta.hot) {\n  import.meta.hot.accept();\n}\n"
  },
  {
    "path": "playground/rsc-vite/src/entry.ssr.tsx",
    "content": "import { createFromReadableStream } from \"@vitejs/plugin-rsc/ssr\";\n// @ts-expect-error\nimport * as ReactDomServer from \"react-dom/server.edge\";\nimport {\n  unstable_RSCStaticRouter as RSCStaticRouter,\n  unstable_routeRSCServerRequest as routeRSCServerRequest,\n} from \"react-router\";\n\nexport default async function handler(\n  request: Request,\n  serverResponse: Response,\n) {\n  const bootstrapScriptContent =\n    await import.meta.viteRsc.loadBootstrapScriptContent(\"index\");\n  return routeRSCServerRequest({\n    request,\n    serverResponse,\n    createFromReadableStream,\n    async renderHTML(getPayload, options) {\n      const payload = getPayload();\n\n      return ReactDomServer.renderToReadableStream(\n        <RSCStaticRouter getPayload={getPayload} />,\n        {\n          ...options,\n          bootstrapScriptContent,\n          signal: request.signal,\n          formState: await payload.formState,\n        },\n      );\n    },\n  });\n}\n"
  },
  {
    "path": "playground/rsc-vite/src/routes/about/about.client.tsx",
    "content": "\"use client\";\n\nimport {\n  type ClientLoaderFunctionArgs,\n  useLoaderData,\n  useRouteError,\n  Form,\n  useActionData,\n  type ClientActionFunctionArgs,\n  isRouteErrorResponse,\n} from \"react-router\";\n\nimport { Counter } from \"../../counter\";\n\nimport type { action, loader } from \"./about\";\n\nexport async function clientAction({ serverAction }: ClientActionFunctionArgs) {\n  console.log(\"action\");\n  let data = await serverAction<typeof action>();\n  return {\n    message: data.message + \" (mutated by client)\",\n  };\n}\n\nexport async function clientLoader({ serverLoader }: ClientLoaderFunctionArgs) {\n  const res = await serverLoader<typeof loader>();\n  return {\n    message: res.message + \" (mutated in clientLoader)\",\n  };\n}\n\nclientLoader.hydrate = true;\n\nexport default function AboutRoute() {\n  const loaderData = useLoaderData<typeof clientLoader>();\n  const actionData = useActionData<typeof clientAction>();\n\n  return (\n    <div style={{ border: \"1px solid black\", padding: \"10px\" }}>\n      <h2>About Route</h2>\n      <p>Loader data: {loaderData.message}</p>\n      <Counter />\n      <Form method=\"post\">\n        <button type=\"submit\">Submit</button>\n        {actionData ? <p>{actionData.message}</p> : null}\n      </Form>\n    </div>\n  );\n}\n\nexport function ErrorBoundary() {\n  let error = useRouteError();\n  return (\n    <>\n      <h1>Oooops</h1>\n      {isRouteErrorResponse(error) ? (\n        <p>\n          {error.status} {error.statusText} {error.data}\n        </p>\n      ) : (\n        <p>{String(error)}</p>\n      )}\n    </>\n  );\n}\n"
  },
  {
    "path": "playground/rsc-vite/src/routes/about/about.tsx",
    "content": "import { data } from \"react-router\";\n\nexport {\n  ErrorBoundary,\n  // clientLazy,\n  clientLoader,\n  clientAction,\n  default,\n} from \"./about.client\";\n\nexport function headers({\n  parentHeaders,\n  loaderHeaders,\n}: {\n  parentHeaders: Headers;\n  loaderHeaders: Headers;\n}) {\n  let headers = new Headers(parentHeaders);\n  loaderHeaders.forEach((value, name) => {\n    headers.append(name, value);\n  });\n  return headers;\n}\n\nexport async function action() {\n  // throw new Error(\"oops\");\n  // throw data(\"This is a test error\", 404);\n  await new Promise((r) => setTimeout(r, 500));\n  return {\n    message: `About route action ran at ${new Date().toISOString()}`,\n  };\n}\n\nexport async function loader() {\n  // throw new Error(\"oops\");\n  // throw data(\"This is a test error\", 404);\n  await new Promise((r) => setTimeout(r, 500));\n  return data(\n    {\n      message: `About route loader ran at ${new Date().toISOString()}`,\n    },\n    {\n      status: 201,\n      headers: {\n        \"x-root\": \"override\",\n        \"x-about\": \"yes\",\n      },\n    }\n  );\n}\n"
  },
  {
    "path": "playground/rsc-vite/src/routes/child/child.tsx",
    "content": "async function loader() {\n  await new Promise((r) => setTimeout(r, 500));\n  return {\n    message: `Child route loader ran at ${new Date().toISOString()}`,\n  };\n}\n\nexport async function Component() {\n  let loaderData = await loader();\n  return (\n    <div style={{ border: \"1px solid black\", padding: \"10px\" }}>\n      <h3>Child Route</h3>\n      <p>Loader data: {loaderData.message}</p>\n    </div>\n  );\n}\n"
  },
  {
    "path": "playground/rsc-vite/src/routes/home/home.client.css",
    "content": ".client-box {\n  border: 1px solid black;\n  padding: 5px;\n}\n"
  },
  {
    "path": "playground/rsc-vite/src/routes/home/home.client.tsx",
    "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { type ClientLoaderFunctionArgs, useLoaderData } from \"react-router\";\n\nimport { Counter } from \"../../counter\";\nimport \"./home.client.css\";\n\nimport type { loader } from \"./home\";\n\nexport async function clientLoader({ serverLoader }: ClientLoaderFunctionArgs) {\n  const res = await serverLoader<typeof loader>();\n\n  return {\n    client: true,\n    ...res,\n  };\n}\n\nexport default function Home() {\n  const { client, message } = useLoaderData<typeof clientLoader>();\n\n  return (\n    <main>\n      <h1>\n        {message} {String(client)}\n      </h1>\n      <Counter />\n    </main>\n  );\n}\n\nexport function HomeForm({ fn }: { fn: () => unknown }) {\n  // @ts-expect-error React types for the repo are set to v18\n  const [state, formAction, isPending] = React.useActionState(fn, null);\n\n  return (\n    <form action={formAction} className=\"client-box\">\n      <button type=\"submit\">\n        Log on server{isPending ? \" (pending)\" : null}\n      </button>\n      {state ? <p>Action state: {state}</p> : null}\n    </form>\n  );\n}\n\nexport function RedirectForm({ fn }: { fn: () => unknown }) {\n  // @ts-expect-error React types for the repo are set to v18\n  const [state, formAction, isPending] = React.useActionState(fn, null);\n\n  return (\n    <form action={formAction}>\n      <button type=\"submit\">Redirect{isPending ? \" (pending)\" : null}</button>\n      {state ? <p>Action state: {state}</p> : null}\n    </form>\n  );\n}\n"
  },
  {
    "path": "playground/rsc-vite/src/routes/home/home.css",
    "content": ".server-box-home {\n  border: 1px solid black;\n  padding: 10px;\n}\n"
  },
  {
    "path": "playground/rsc-vite/src/routes/home/home.tsx",
    "content": "import { HomeForm, RedirectForm } from \"./home.client\";\nexport { clientLoader } from \"./home.client\";\n\nimport { Counter } from \"../../counter\";\nimport { redirect } from \"react-router\";\nimport type { LoaderFunctionArgs } from \"react-router\";\n\nimport \"./home.css\";\n\nexport async function loader({ request }: LoaderFunctionArgs) {\n  await new Promise((r) => setTimeout(r, 500));\n  return {\n    message: `Home route loader ran at ${new Date().toISOString()}`,\n    wasRedirected:\n      new URL(request.url).searchParams.get(\"redirected\") === \"true\",\n  };\n}\n\nexport default function HomeRoute({\n  loaderData: { message, wasRedirected },\n}: {\n  loaderData: Awaited<ReturnType<typeof loader>>;\n}) {\n  const logOnServer = async () => {\n    \"use server\";\n    await new Promise((r) => setTimeout(r, 500));\n    console.log(\"Running action on server!\");\n    console.log(\n      `  data to prove that scoped vars work: ${message} and it is now ${new Date().toISOString()}`\n    );\n    return <div>{new Date().toISOString()}</div>;\n  };\n\n  const redirectOnServer = async () => {\n    \"use server\";\n    await new Promise((r) => setTimeout(r, 500));\n    if (wasRedirected) {\n      redirect(\"/\");\n    } else {\n      redirect(\"/?redirected=true\");\n    }\n  };\n\n  return (\n    <div className=\"server-box-home\">\n      <h2>Home Route</h2>\n      <p>Loader data: {message}</p>\n      <Counter />\n      <HomeForm fn={logOnServer} />\n      <RedirectForm fn={redirectOnServer} />\n    </div>\n  );\n}\n"
  },
  {
    "path": "playground/rsc-vite/src/routes/parent/parent.tsx",
    "content": "import { Outlet } from \"react-router\";\n\nexport function loader() {\n  return {\n    message: `Parent route loader ran at ${new Date().toISOString()}`,\n  };\n}\n\nexport default function ParentRoute({\n  loaderData,\n}: {\n  loaderData: Awaited<ReturnType<typeof loader>>;\n}) {\n  return (\n    <div style={{ border: \"1px solid black\", padding: \"10px\" }}>\n      <h2>Parent Route</h2>\n      <p>Loader data: {loaderData.message}</p>\n      <Outlet />\n    </div>\n  );\n}\n"
  },
  {
    "path": "playground/rsc-vite/src/routes/parent-index/parent-index.tsx",
    "content": "export async function loader() {\n  await new Promise((r) => setTimeout(r, 500));\n  return {\n    message: `Parent Index route loader ran at ${new Date().toISOString()}`,\n  };\n}\n\nexport default function ParentIndexRoute({\n  loaderData,\n}: {\n  loaderData: Awaited<ReturnType<typeof loader>>;\n}) {\n  return (\n    <div style={{ border: \"1px solid black\", padding: \"10px\" }}>\n      <h3>Parent Index Route</h3>\n      <p>Loader data: {loaderData.message}</p>\n    </div>\n  );\n}\n"
  },
  {
    "path": "playground/rsc-vite/src/routes/redirect.ts",
    "content": "import { redirectDocument } from \"react-router\";\n\nexport function loader() {\n  throw redirectDocument(\"/about?redirected\");\n}\n"
  },
  {
    "path": "playground/rsc-vite/src/routes/render-redirects.tsx",
    "content": "import { Link, redirect } from \"react-router\";\n\nexport default function RenderRedirect({\n  params: { id },\n}: {\n  params: { id?: string };\n}) {\n  if (id === \"redirect\") {\n    throw redirect(\"/render-redirect/redirected\");\n  }\n\n  if (id === \"external\") {\n    throw redirect(\"https://example.com/\");\n  }\n\n  return (\n    <>\n      <h1>{id || \"home\"}</h1>\n      <Link to=\"/render-redirect/redirect\">Redirect</Link>\n      <Link to=\"/render-redirect/external\">External</Link>\n    </>\n  );\n}\n"
  },
  {
    "path": "playground/rsc-vite/src/routes/root/root.client.tsx",
    "content": "\"use client\";\n\nimport {\n  useRouteError,\n  useNavigation,\n  isRouteErrorResponse,\n  Outlet,\n  ScrollRestoration,\n  Link,\n  Links,\n} from \"react-router\";\nimport { Counter } from \"../../counter\";\nimport type { loader } from \"./root\";\n\nexport default function RootRoute({\n  loaderData,\n}: {\n  loaderData: Awaited<ReturnType<typeof loader>>;\n}) {\n  return (\n    <div style={{ border: \"1px solid black\", padding: \"10px\" }}>\n      <h1>Root Route</h1>\n      <p>Loader data: {loaderData.message}</p>\n      {loaderData.counter}\n      <Outlet />\n    </div>\n  );\n}\n\nexport function ErrorReporter() {\n  const error = useRouteError();\n\n  if (typeof document !== \"undefined\") {\n    console.log(error);\n  }\n\n  if (isRouteErrorResponse(error)) {\n    return (\n      <div>\n        <h2>Error Response</h2>\n        <p>Status: {error.status}</p>\n        <p>Data: {JSON.stringify(error.data)}</p>\n      </div>\n    );\n  }\n\n  return (\n    <div>\n      <h2>Error</h2>\n      <p>{error instanceof Error ? error.message : String(error)}</p>\n    </div>\n  );\n}\n\nexport function NavigationState() {\n  let navigation = useNavigation();\n  return <p>Navigation state: {navigation.state}</p>;\n}\n\nexport function ErrorBoundary() {\n  return (\n    <>\n      <h1>Something went wrong!</h1>\n      <ErrorReporter />\n    </>\n  );\n}\n\nexport function shouldRevalidate({ nextUrl }: { nextUrl: URL }) {\n  return !nextUrl.pathname.endsWith(\"/about\");\n}\nexport function Layout({ children }: { children: React.ReactNode }) {\n  return (\n    <html lang=\"en\">\n      <head>\n        <meta charSet=\"utf-8\" />\n        <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />\n        <title>React Server</title>\n        <Links />\n      </head>\n      <body>\n        <div className=\"server-box\">\n          <header>\n            <Link to=\"/\">Home</Link>\n            {\" | \"}\n            <Link to=\"/about\">About</Link>\n            {\" | \"}\n            <Link to=\"/parent\">Parent</Link>\n            {\" | \"}\n            <Link to=\"/parent/child\">Child</Link>\n            {\" | \"}\n            <Link to=\"/redirect\">Redirect</Link>\n          </header>\n          <h1>Root Layout</h1>\n          <NavigationState />\n          <Counter />\n          {children}\n        </div>\n        <ScrollRestoration />\n      </body>\n    </html>\n  );\n}\n"
  },
  {
    "path": "playground/rsc-vite/src/routes/root/root.css",
    "content": ".server-box {\n  border: 1px solid black;\n  padding: 10px;\n}\n"
  },
  {
    "path": "playground/rsc-vite/src/routes/root/root.tsx",
    "content": "// import { Link, Links, Outlet, ScrollRestoration } from \"react-router\";\nimport { type MiddlewareFunction } from \"react-router\";\n\nimport { Counter } from \"../../counter\";\n// import { ErrorReporter, NavigationState } from \"./root.client\";\nimport \"./root.css\";\n\nexport {\n  shouldRevalidate,\n  default,\n  ErrorBoundary,\n  Layout,\n} from \"./root.client\";\n\nexport function headers() {\n  return new Headers({ \"x-root\": \"yes\" });\n}\n\nexport const middleware: MiddlewareFunction<Response>[] = [\n  async ({ request }, next) => {\n    console.log(\">>> RSC middleware\", request.url);\n    let res = await next();\n    console.log(\"<<< RSC middleware\", request.url);\n    return res;\n  },\n];\n\nexport async function loader() {\n  await new Promise((r) => setTimeout(r, 500));\n  return {\n    counter: <Counter key=\"counter\" />,\n    message: `Root route loader ran at ${new Date().toISOString()}`,\n  };\n}\n\n// export default function RootRoute({\n//   loaderData,\n// }: {\n//   loaderData: Awaited<ReturnType<typeof loader>>;\n// }) {\n//   return (\n//     <div style={{ border: \"1px solid black\", padding: \"10px\" }}>\n//       <h1>Root Route</h1>\n//       <p>Loader data: {loaderData.message}</p>\n//       {loaderData.counter}\n//       <Outlet />\n//     </div>\n//   );\n// }\n\nexport function HydrateFallback() {\n  return <h1>Loading...</h1>;\n}\n\n// export function ErrorBoundary() {\n//   return (\n//     <>\n//       <h1>Root - Something went wrong!</h1>\n//       <ErrorReporter />\n//     </>\n//   );\n// }\n\n// export function Layout({ children }: { children: React.ReactNode }) {\n//   return (\n//     <html lang=\"en\">\n//       <head>\n//         <meta charSet=\"utf-8\" />\n//         <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />\n//         <title>React Server</title>\n//         <Links />\n//       </head>\n//       <body>\n//         <div className=\"server-box\">\n//           <header>\n//             <Link to=\"/\">Home</Link>\n//             {\" | \"}\n//             <Link to=\"/about\">About</Link>\n//             {\" | \"}\n//             <Link to=\"/parent\">Parent</Link>\n//             {\" | \"}\n//             <Link to=\"/parent/child\">Child</Link>\n//             {\" | \"}\n//             <Link to=\"/redirect\">Redirect</Link>\n//           </header>\n//           <h1>Root Layout</h1>\n//           <NavigationState />\n//           <Counter />\n//           {children}\n//         </div>\n//         <ScrollRestoration />\n//       </body>\n//     </html>\n//   );\n// }\n"
  },
  {
    "path": "playground/rsc-vite/src/routes.ts",
    "content": "import type { unstable_RSCRouteConfig as RSCRouteConfig } from \"react-router\";\n\nexport const routes = [\n  {\n    id: \"root\",\n    path: \"\",\n    lazy: () => import(\"./routes/root/root\"),\n    children: [\n      {\n        id: \"home\",\n        index: true,\n        lazy: () => import(\"./routes/home/home\"),\n      },\n      {\n        id: \"about\",\n        path: \"about\",\n        lazy: () => import(\"./routes/about/about\"),\n      },\n      {\n        id: \"render-redirect\",\n        path: \"render-redirect/:id?\",\n        lazy: () => import(\"./routes/render-redirects\"),\n      },\n      {\n        id: \"parent\",\n        path: \"parent\",\n        lazy: () => import(\"./routes/parent/parent\"),\n        children: [\n          {\n            id: \"parent-index\",\n            index: true,\n            lazy: () => import(\"./routes/parent-index/parent-index\"),\n          },\n          {\n            id: \"child\",\n            path: \"child\",\n            // @ts-expect-error\n            lazy: () => import(\"./routes/child/child\"),\n          },\n        ],\n      },\n      {\n        id: \"redirect\",\n        path: \"redirect\",\n        lazy: () => import(\"./routes/redirect\"),\n      },\n    ],\n  },\n] satisfies RSCRouteConfig;\n"
  },
  {
    "path": "playground/rsc-vite/tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"allowImportingTsExtensions\": true,\n    \"strict\": true,\n    \"noUnusedLocals\": true,\n    \"noUnusedParameters\": true,\n    \"skipLibCheck\": true,\n    \"verbatimModuleSyntax\": true,\n    \"noEmit\": true,\n    \"moduleResolution\": \"Bundler\",\n    \"module\": \"ESNext\",\n    \"target\": \"ESNext\",\n    \"lib\": [\"ESNext\", \"DOM\", \"DOM.Iterable\"],\n    \"types\": [\"vite/client\", \"@vitejs/plugin-rsc/types\"],\n    \"jsx\": \"react-jsx\"\n  }\n}\n"
  },
  {
    "path": "playground/rsc-vite/vite.config.ts",
    "content": "import rsc from \"@vitejs/plugin-rsc\";\nimport react from \"@vitejs/plugin-react\";\nimport { defineConfig } from \"vite\";\n\nexport default defineConfig({\n  plugins: [\n    react(),\n    rsc({\n      entries: {\n        client: \"src/entry.browser.tsx\",\n        rsc: \"src/entry.rsc.tsx\",\n        ssr: \"src/entry.ssr.tsx\",\n      },\n    }),\n  ],\n});\n"
  },
  {
    "path": "playground/rsc-vite-framework/.gitignore",
    "content": "build\nnode_modules\n"
  },
  {
    "path": "playground/rsc-vite-framework/app/entry.rsc.ts",
    "content": "import defaultEntry from \"@react-router/dev/config/default-rsc-entries/entry.rsc\";\nimport { RouterContextProvider } from \"react-router\";\n\nexport default {\n  fetch(request: Request) {\n    console.log(\"custom entry.rsc\");\n\n    const requestContext = new RouterContextProvider();\n\n    return defaultEntry.fetch(request, requestContext);\n  },\n};\n\nif (import.meta.hot) {\n  import.meta.hot.accept();\n}\n"
  },
  {
    "path": "playground/rsc-vite-framework/app/entry.ssr.ts",
    "content": "import { generateHTML as defaultGenerateHTML } from \"@react-router/dev/config/default-rsc-entries/entry.ssr\";\n\nexport function generateHTML(request: Request, serverResponse: Response) {\n  console.log(\"custom entry.ssr\");\n\n  return defaultGenerateHTML(request, serverResponse);\n}\n"
  },
  {
    "path": "playground/rsc-vite-framework/app/root.css",
    "content": ".root__header {\n  color: rebeccapurple;\n}\n"
  },
  {
    "path": "playground/rsc-vite-framework/app/root.tsx",
    "content": "import type { Route } from \"./+types/root\";\n\nimport { Meta, Link, Outlet, isRouteErrorResponse } from \"react-router\";\nimport \"./root.css\";\n\nexport const meta = () => [{ title: \"React Router Vite\" }];\n\nexport function Layout({ children }: { children: React.ReactNode }) {\n  console.log(\"Layout\");\n  return (\n    <html lang=\"en\">\n      <head>\n        <meta charSet=\"utf-8\" />\n        <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />\n        <Meta />\n      </head>\n      <body>\n        <header>\n          <h1 className=\"root__header\">React Router Vite</h1>\n          <nav>\n            <ul>\n              <li>\n                <Link to=\"/\">Home</Link>\n              </li>\n              <li>\n                <Link to=\"/server-loader\">Server loader</Link>\n              </li>\n              <li>\n                <Link to=\"/client-loader\">Client loader</Link>\n              </li>\n              <li>\n                <Link to=\"/client-loader-hydrate\">Client loader hydrate</Link>\n              </li>\n              <li>\n                <Link to=\"/client-loader-without-server-loader\">\n                  Client loader without server loader\n                </Link>\n              </li>\n              <li>\n                <Link to=\"/mdx\">MDX</Link>\n              </li>\n              <li>\n                <Link to=\"/mdx-glob\">MDX glob</Link>\n              </li>\n            </ul>\n          </nav>\n        </header>\n        {children}\n      </body>\n    </html>\n  );\n}\n\nexport function ServerComponent() {\n  console.log(\"Root\");\n  return (\n    <>\n      <Outlet />\n    </>\n  );\n}\n\nexport function ErrorBoundary({ error }: Route.ErrorBoundaryProps) {\n  return (\n    <h1>\n      {isRouteErrorResponse(error)\n        ? `${error.status} ${error.statusText}`\n        : error instanceof Error\n          ? error.message\n          : \"Unknown Error\"}\n    </h1>\n  );\n}\n"
  },
  {
    "path": "playground/rsc-vite-framework/app/routes/_index/actions.ts",
    "content": "\"use server\";\n\nexport async function log() {\n  console.log(\"hello from server\");\n}\n"
  },
  {
    "path": "playground/rsc-vite-framework/app/routes/_index/route.tsx",
    "content": "import type { Route } from \"./+types/route\";\nimport { log } from \"./actions\";\nimport \"./styles.css\";\n\nexport function loader({}: Route.LoaderArgs) {\n  return \"hello, world\";\n}\n\nexport function ServerComponent({ loaderData }: Route.ComponentProps) {\n  return (\n    <main>\n      <h1 className=\"home__heading\">Home</h1>\n      <p>This is the home page.</p>\n      <p>loaderData: {loaderData}</p>\n      {/* @ts-expect-error React types for the repo are set to v18 */}\n      <form action={log}>\n        <button type=\"submit\">Submit</button>\n      </form>\n    </main>\n  );\n}\n"
  },
  {
    "path": "playground/rsc-vite-framework/app/routes/_index/styles.css",
    "content": ".home__heading {\n  color: green;\n}\n"
  },
  {
    "path": "playground/rsc-vite-framework/app/routes/_layout-a.route-a.tsx",
    "content": "import { Link } from \"react-router\";\n\nexport default function RouteA() {\n  return (\n    <div className=\"flex flex-col items-center\">\n      HERE IS ROUTE A!!!\n      <Link to=\"/route-b\" className=\"bg-blue-800 text-white px-8 py-1\">\n        Go to route B\n      </Link>\n    </div>\n  );\n}\n"
  },
  {
    "path": "playground/rsc-vite-framework/app/routes/_layout-a.tsx",
    "content": "import { Outlet } from \"react-router\";\n\nexport default function Layout() {\n  return (\n    <>\n      <p>Layout A</p>\n      <Outlet />\n    </>\n  );\n}\n"
  },
  {
    "path": "playground/rsc-vite-framework/app/routes/_layout-b.route-b.tsx",
    "content": "import { Link } from \"react-router\";\n\nexport default function RouteA() {\n  return (\n    <div className=\"flex flex-col items-center\">\n      HERE IS ROUTE B!!!\n      <Link to=\"/route-a\" className=\"bg-blue-800 text-white px-8 py-1\">\n        Go to route A\n      </Link>\n    </div>\n  );\n}\n"
  },
  {
    "path": "playground/rsc-vite-framework/app/routes/_layout-b.tsx",
    "content": "import { Outlet } from \"react-router\";\n\nexport default function Layout() {\n  return (\n    <>\n      <p>Layout B</p>\n      <Outlet />\n    </>\n  );\n}\n"
  },
  {
    "path": "playground/rsc-vite-framework/app/routes/client-loader/route.tsx",
    "content": "import type { Route } from \"./+types/route\";\n\nimport styles from \"./styles.module.css\";\n\nexport function loader() {\n  return \"hello, world from server loader\";\n}\n\nexport function clientLoader() {\n  return \"hello, world from client loader\";\n}\n\nexport default function ClientLoaderRoute({\n  loaderData,\n}: Route.ComponentProps) {\n  return (\n    <main>\n      <h1 className={styles.heading}>Client loader</h1>\n      <p>Loader data: {loaderData}</p>\n    </main>\n  );\n}\n"
  },
  {
    "path": "playground/rsc-vite-framework/app/routes/client-loader/styles.module.css",
    "content": ".heading {\n  color: cornflowerblue;\n}\n"
  },
  {
    "path": "playground/rsc-vite-framework/app/routes/client-loader-hydrate/route.tsx",
    "content": "import type { Route } from \"./+types/route\";\n\nimport styles from \"./styles.module.css\";\n\nexport function loader() {\n  return \"hello, world from server loader\";\n}\n\nexport function clientLoader() {\n  return \"hello, world from client loader\";\n}\n\nclientLoader.hydrate = true;\n\nexport default function ClientLoaderHydrateRoute({\n  loaderData,\n}: Route.ComponentProps) {\n  return (\n    <main>\n      <h1 className={styles.heading}>Client loader</h1>\n      <p>Loader data: {loaderData}</p>\n    </main>\n  );\n}\n"
  },
  {
    "path": "playground/rsc-vite-framework/app/routes/client-loader-hydrate/styles.module.css",
    "content": ".heading {\n  color: seagreen;\n}\n"
  },
  {
    "path": "playground/rsc-vite-framework/app/routes/client-loader-without-server-loader/route.tsx",
    "content": "import type { Route } from \"./+types/route\";\n\nexport function clientLoader() {\n  return \"hello, world from client loader\";\n}\n\nexport default function ClientLoaderWithoutServerLoaderRoute({\n  loaderData,\n}: Route.ComponentProps) {\n  return (\n    <main>\n      <h1>Client loader without server loader</h1>\n      <p>Loader data: {loaderData}</p>\n    </main>\n  );\n}\n"
  },
  {
    "path": "playground/rsc-vite-framework/app/routes/client-loader-without-server-loader/styles.module.css",
    "content": ".heading {\n  color: salmon;\n}\n"
  },
  {
    "path": "playground/rsc-vite-framework/app/routes/fixture.client-component/route.tsx",
    "content": "import type { Route } from \"./+types/route\";\n\nexport function loader() {\n  return {\n    message: <p>From the loader.</p>,\n  };\n}\n\nexport default function ClientComponent({ loaderData }: Route.ComponentProps) {\n  return (\n    <div>\n      <h1>Client Component</h1>\n      {loaderData.message}\n    </div>\n  );\n}\n"
  },
  {
    "path": "playground/rsc-vite-framework/app/routes/fixture.server-component/route.tsx",
    "content": "import type { Route } from \"./+types/route\";\n\nexport function loader() {\n  return {\n    message: <p key=\"loader\">From the loader.</p>,\n  };\n}\n\nexport async function ServerComponent({ loaderData }: Route.ComponentProps) {\n  const message = await Promise.resolve(\"From the component.\");\n  return (\n    <div>\n      <h1>Client Component</h1>\n      {loaderData.message}\n      {message}\n    </div>\n  );\n}\n"
  },
  {
    "path": "playground/rsc-vite-framework/app/routes/mdx/message.tsx",
    "content": "import { useLoaderData } from \"react-router\";\n\nexport function Message() {\n  const { message } = useLoaderData();\n  return <div>Loader data: {message}</div>;\n}\n"
  },
  {
    "path": "playground/rsc-vite-framework/app/routes/mdx/route.mdx",
    "content": "import { Message } from \"./message\";\n\nexport const meta = () => [{ title: \"MDX Route\" }];\n\nexport const loader = () => ({ message: \"Loader data\" });\n\n# This is an MDX route\n\nHello from an MDX route!\n\n<Message />\n"
  },
  {
    "path": "playground/rsc-vite-framework/app/routes/mdx-glob.$post/posts/hello/hello-component.module.css",
    "content": ".root {\n  color: green;\n}\n"
  },
  {
    "path": "playground/rsc-vite-framework/app/routes/mdx-glob.$post/posts/hello/hello-component.tsx",
    "content": "import styles from \"./hello-component.module.css\";\n\nexport function HelloComponent() {\n  return <p className={styles.root}>Hello Component</p>;\n}\n"
  },
  {
    "path": "playground/rsc-vite-framework/app/routes/mdx-glob.$post/posts/hello/hello.mdx",
    "content": "---\ntitle: Hello\n---\n\nimport { HelloComponent } from \"./hello-component\";\n\n# Hello\n\nThis is a blog post written in MDX.\n\n<HelloComponent />\n"
  },
  {
    "path": "playground/rsc-vite-framework/app/routes/mdx-glob.$post/posts/posts.ts",
    "content": "import nodePath from \"node:path\";\n\ntype BlogPost = {\n  Component: React.ComponentType;\n  path: string;\n  title: string;\n  slug: string;\n};\n\nasync function resolvePosts(): Promise<{\n  [slug: string]: () => Promise<BlogPost>;\n}> {\n  const rawPosts = (await import.meta.glob(\"./*/*.mdx\")) as Record<\n    string,\n    () => Promise<{\n      default: React.ComponentType;\n      frontmatter?: unknown;\n    }>\n  >;\n\n  return Object.fromEntries(\n    Object.entries(rawPosts).map(([importPath, loadPost]) => {\n      const slug = importPath.split(\"/\").pop()!.replace(\".mdx\", \"\");\n\n      return [\n        slug,\n        async (): Promise<BlogPost> => {\n          const post = await loadPost();\n\n          let title: string;\n          if (\n            post.frontmatter &&\n            typeof post.frontmatter === \"object\" &&\n            \"title\" in post.frontmatter &&\n            typeof post.frontmatter.title === \"string\"\n          ) {\n            title = post.frontmatter.title;\n          } else {\n            const prettyPath = nodePath.relative(\n              process.cwd(),\n              nodePath.resolve(import.meta.dirname, importPath),\n            );\n            console.error(\n              `Invalid frontmatter for ${prettyPath}: Missing title`,\n            );\n            title = \"Untitled Post\";\n          }\n\n          return {\n            Component: post.default,\n            path: `/mdx-glob/${slug}`,\n            title,\n            slug,\n          };\n        },\n      ];\n    }),\n  );\n}\n\nexport async function getPost(slug: string): Promise<BlogPost | null> {\n  const posts = await resolvePosts();\n  const loadPost = posts[slug];\n  return loadPost ? await loadPost() : null;\n}\n\nexport async function getPosts(): Promise<Record<string, BlogPost>> {\n  const posts = await resolvePosts();\n  return Object.fromEntries(\n    await Promise.all(\n      Object.entries(posts).map(async ([slug, loadPost]) => {\n        return [slug, await loadPost()];\n      }),\n    ),\n  );\n}\n"
  },
  {
    "path": "playground/rsc-vite-framework/app/routes/mdx-glob.$post/posts/world/world-component.module.css",
    "content": ".root {\n  color: rebeccapurple;\n}\n"
  },
  {
    "path": "playground/rsc-vite-framework/app/routes/mdx-glob.$post/posts/world/world-component.tsx",
    "content": "import styles from \"./world-component.module.css\";\n\nexport function WorldComponent() {\n  return <p className={styles.root}>World Component</p>;\n}\n"
  },
  {
    "path": "playground/rsc-vite-framework/app/routes/mdx-glob.$post/posts/world/world.mdx",
    "content": "---\ntitle: World\n---\n\nimport { WorldComponent } from \"./world-component\";\n\n# World\n\nThis is another blog post written in MDX.\n\n<WorldComponent />\n"
  },
  {
    "path": "playground/rsc-vite-framework/app/routes/mdx-glob.$post/route.tsx",
    "content": "import type { Route } from \"./+types/route\";\nimport { getPost } from \"./posts/posts\";\n\nexport async function loader({ params }: Route.LoaderArgs) {\n  const post = await getPost(params.post);\n\n  if (!post) {\n    throw new Response(\"Not Found\", { status: 404, statusText: \"Not Found\" });\n  }\n\n  return {\n    title: post.title,\n    element: <post.Component />,\n  };\n}\n\nexport function meta({ loaderData }: Route.MetaArgs) {\n  return [{ title: loaderData.title }];\n}\n\nexport function ServerComponent({ loaderData }: Route.ComponentProps) {\n  return loaderData.element;\n}\n"
  },
  {
    "path": "playground/rsc-vite-framework/app/routes/mdx-glob._index/route.tsx",
    "content": "import { Link } from \"react-router\";\nimport { getPosts } from \"../mdx-glob.$post/posts/posts\";\n\nexport async function ServerComponent() {\n  const posts = await getPosts();\n\n  return (\n    <>\n      <h1>MDX glob</h1>\n      <ul>\n        {Object.values(posts).map(({ path, title }) => (\n          <li key={path}>\n            <Link to={path}>{title}</Link>\n          </li>\n        ))}\n      </ul>\n    </>\n  );\n}\n"
  },
  {
    "path": "playground/rsc-vite-framework/app/routes/optimistic/actions.ts",
    "content": "\"use server\";\n\nimport { toggleLiked } from \"./liked\";\n\nexport async function toggleLikedAction() {\n  await new Promise((resolve) => setTimeout(resolve, 1000));\n  toggleLiked();\n}\n"
  },
  {
    "path": "playground/rsc-vite-framework/app/routes/optimistic/form.tsx",
    "content": "\"use client\";\n\nimport {\n  // @ts-expect-error React types for the repo are set to v18\n  useOptimistic,\n} from \"react\";\nimport { useHydrated } from \"remix-utils/use-hydrated\";\n\nexport function ToggleLikedForm({\n  liked,\n  toggleLikedAction,\n}: {\n  liked: boolean;\n  toggleLikedAction: () => Promise<void>;\n}) {\n  const hydrated = useHydrated();\n\n  const [optimisticLiked, setOptimisticLiked] = useOptimistic(liked);\n  const toggleLikedActionOptimistic = async () => {\n    // @ts-expect-error React types for the repo are set to v18\n    setOptimisticLiked((liked) => !liked);\n    await toggleLikedAction();\n  };\n\n  return (\n    // @ts-expect-error React types for the repo are set to v18\n    <form action={hydrated ? toggleLikedActionOptimistic : toggleLikedAction}>\n      <button type=\"submit\" className=\"btn\">\n        {optimisticLiked ? \"Unlike\" : \"Like\"}\n      </button>\n    </form>\n  );\n}\n"
  },
  {
    "path": "playground/rsc-vite-framework/app/routes/optimistic/liked.ts",
    "content": "let liked = false;\n\nexport function getLiked() {\n  return liked;\n}\n\nexport function toggleLiked() {\n  liked = !liked;\n}\n"
  },
  {
    "path": "playground/rsc-vite-framework/app/routes/optimistic/route.tsx",
    "content": "import { ToggleLikedForm } from \"./form\";\nimport { getLiked } from \"./liked\";\nimport { toggleLikedAction } from \"./actions\";\n\nexport function ServerComponent() {\n  return (\n    <div>\n      <h1>Server Component</h1>\n      <ToggleLikedForm\n        liked={getLiked()}\n        toggleLikedAction={toggleLikedAction}\n      />\n    </div>\n  );\n}\n"
  },
  {
    "path": "playground/rsc-vite-framework/app/routes/server-loader/route.tsx",
    "content": "import type { Route } from \"./+types/route\";\nimport styles from \"./styles.module.css\";\n\nexport function loader() {\n  return \"hello, world from server loader\";\n}\n\nexport default function ServerLoaderRoute({\n  loaderData,\n}: Route.ComponentProps) {\n  return (\n    <main>\n      <h1 className={styles.heading}>Server loader</h1>\n      <p>Loader data: {loaderData}</p>\n    </main>\n  );\n}\n"
  },
  {
    "path": "playground/rsc-vite-framework/app/routes/server-loader/styles.module.css",
    "content": ".heading {\n  color: burlywood;\n}\n"
  },
  {
    "path": "playground/rsc-vite-framework/app/routes.ts",
    "content": "import { type RouteConfig } from \"@react-router/dev/routes\";\nimport { flatRoutes } from \"@react-router/fs-routes\";\n\nexport default flatRoutes() satisfies RouteConfig;\n"
  },
  {
    "path": "playground/rsc-vite-framework/mdx.d.ts",
    "content": "declare module \"*.mdx\" {\n  export default (...args: any[]) => unknown;\n}\n"
  },
  {
    "path": "playground/rsc-vite-framework/package.json",
    "content": "{\n  \"name\": \"@playground/rsc-vite-framework\",\n  \"version\": \"0.0.0\",\n  \"private\": true,\n  \"type\": \"module\",\n  \"scripts\": {\n    \"dev\": \"react-router dev\",\n    \"dev:vite-middleware\": \"node start-vite-middleware.js\",\n    \"build\": \"react-router build\",\n    \"start:custom\": \"cross-env NODE_ENV=production node start.js\",\n    \"start\": \"react-router-serve ./build/server/index.js\",\n    \"typecheck\": \"react-router typegen && tsc\"\n  },\n  \"devDependencies\": {\n    \"@mdx-js/rollup\": \"^3.1.0\",\n    \"@react-router/dev\": \"workspace:*\",\n    \"@react-router/fs-routes\": \"workspace:*\",\n    \"@react-router/serve\": \"workspace:*\",\n    \"@types/express\": \"^5.0.0\",\n    \"@types/node\": \"^22.13.1\",\n    \"@types/react\": \"catalog:react-canary\",\n    \"@types/react-dom\": \"catalog:react-canary\",\n    \"@vitejs/plugin-rsc\": \"catalog:\",\n    \"cross-env\": \"^7.0.3\",\n    \"remark-frontmatter\": \"^5.0.0\",\n    \"remark-mdx-frontmatter\": \"^5.2.0\",\n    \"typescript\": \"catalog:\",\n    \"vite\": \"^6.3.0\"\n  },\n  \"dependencies\": {\n    \"@mjackson/node-fetch-server\": \"0.6.1\",\n    \"compression\": \"^1.8.1\",\n    \"express\": \"^4.21.2\",\n    \"react\": \"catalog:react-canary\",\n    \"react-dom\": \"catalog:react-canary\",\n    \"react-router\": \"workspace:*\",\n    \"react-server-dom-webpack\": \"catalog:react-canary\",\n    \"remix-utils\": \"^8.7.0\"\n  }\n}\n"
  },
  {
    "path": "playground/rsc-vite-framework/react-router.config.ts",
    "content": "import type { Config } from \"@react-router/dev/config\";\n\nexport default {} satisfies Config;\n"
  },
  {
    "path": "playground/rsc-vite-framework/start-vite-middleware.js",
    "content": "import { createRequestListener } from \"@mjackson/node-fetch-server\";\nimport express from \"express\";\n\nconst viteDevServer =\n  process.env.NODE_ENV === \"production\"\n    ? undefined\n    : await import(\"vite\").then(({ createServer }) =>\n        createServer({\n          server: {\n            middlewareMode: true,\n          },\n        }),\n      );\n\nconst app = express();\n\nif (viteDevServer) {\n  app.use(viteDevServer.middlewares);\n} else {\n  app.use(\n    \"/assets\",\n    express.static(\"build/client/assets\", { immutable: true, maxAge: \"1y\" }),\n  );\n  app.use(express.static(\"build/client\"));\n  app.all(\n    \"*\",\n    createRequestListener((await import(\"./build/server/index.js\")).default),\n  );\n}\n\nconst port = process.env.PORT || 3000;\napp.listen(port);\nconsole.log(`Server listening on port ${port} (http://localhost:${port})`);\n"
  },
  {
    "path": "playground/rsc-vite-framework/start.js",
    "content": "import { createRequestListener } from \"@mjackson/node-fetch-server\";\nimport express from \"express\";\nimport reactRouterRequestHandler from \"./build/server/index.js\";\n\nconst app = express();\n\napp.use(express.static(\"build/client\"));\n\napp.get(\"/.well-known/appspecific/com.chrome.devtools.json\", (_, res) => {\n  res.status(404);\n  res.send(\"Not Found\");\n});\n\napp.use(createRequestListener(reactRouterRequestHandler));\n\nconst port = process.env.PORT || 3000;\napp.listen(port);\nconsole.log(`Server listening on port ${port} (http://localhost:${port})`);\n"
  },
  {
    "path": "playground/rsc-vite-framework/tsconfig.json",
    "content": "{\n  \"include\": [\"**/*.ts\", \"**/*.tsx\", \"./.react-router/types/**/*\"],\n  \"compilerOptions\": {\n    \"allowImportingTsExtensions\": true,\n    \"strict\": true,\n    \"noUnusedLocals\": true,\n    \"noUnusedParameters\": true,\n    \"skipLibCheck\": true,\n    \"verbatimModuleSyntax\": true,\n    \"noEmit\": true,\n    \"moduleResolution\": \"Bundler\",\n    \"module\": \"ESNext\",\n    \"target\": \"ESNext\",\n    \"lib\": [\"ESNext\", \"DOM\", \"DOM.Iterable\"],\n    \"types\": [\n      \"vite/client\",\n      \"@vitejs/plugin-rsc/types\",\n      \"@react-router/dev/rsc-types\"\n    ],\n    \"jsx\": \"react-jsx\",\n    \"rootDirs\": [\".\", \"./.react-router/types\"]\n  }\n}\n"
  },
  {
    "path": "playground/rsc-vite-framework/vite.config.ts",
    "content": "import { defineConfig } from \"vite\";\nimport { unstable_reactRouterRSC as reactRouterRSC } from \"@react-router/dev/vite\";\nimport rsc from \"@vitejs/plugin-rsc\";\nimport mdx from \"@mdx-js/rollup\";\nimport remarkFrontmatter from \"remark-frontmatter\";\nimport remarkMdxFrontmatter from \"remark-mdx-frontmatter\";\n\nexport default defineConfig({\n  plugins: [\n    mdx({ remarkPlugins: [remarkFrontmatter, remarkMdxFrontmatter] }),\n    // @ts-ignore\n    reactRouterRSC({ __runningWithinTheReactRouterMonoRepo: true }),\n    rsc(),\n  ],\n});\n"
  },
  {
    "path": "playground/split-route-modules/.gitignore",
    "content": "node_modules\n\n/build\n.env\n\n.react-router/"
  },
  {
    "path": "playground/split-route-modules/app/root.tsx",
    "content": "import { Links, Meta, Outlet, Scripts, ScrollRestoration } from \"react-router\";\n\nexport function Layout({ children }: { children: React.ReactNode }) {\n  return (\n    <html lang=\"en\">\n      <head>\n        <meta charSet=\"utf-8\" />\n        <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />\n        <Meta />\n        <Links />\n      </head>\n      <body>\n        {children}\n        <ScrollRestoration />\n        <Scripts />\n      </body>\n    </html>\n  );\n}\n\nexport default function App() {\n  return <Outlet />;\n}\n"
  },
  {
    "path": "playground/split-route-modules/app/routes/_index.tsx",
    "content": "import { Link, type MetaFunction } from \"react-router\";\n\nexport const meta: MetaFunction = () => {\n  return [\n    { title: \"New React Router App\" },\n    { name: \"description\", content: \"Welcome to React Router!\" },\n  ];\n};\n\nexport default function Index() {\n  return (\n    <div style={{ fontFamily: \"system-ui, sans-serif\", lineHeight: \"1.8\" }}>\n      <ul>\n        <li>\n          <Link to=\"/splittable\">Splittable route</Link>\n        </li>\n        <li>\n          <Link to=\"/unsplittable\">Unsplittable route</Link>\n        </li>\n        <li>\n          <Link to=\"/semi-splittable\">Semi-splittable route</Link>\n        </li>\n      </ul>\n    </div>\n  );\n}\n"
  },
  {
    "path": "playground/split-route-modules/app/routes/semi-splittable.tsx",
    "content": "import type { Route } from \"./+types/semi-splittable\";\nimport { Form } from \"react-router\";\n\n// Dummy variable to prevent route exports from being split\nlet shared: null = null;\n\nexport const clientLoader = async () => {\n  return shared ?? \"Hello from unsplittable client loader\";\n};\n\nexport const clientAction = async () => {\n  return \"Hello from splittable client action\";\n};\n\nexport function HydrateFallback() {\n  return shared ?? <div>Loading...</div>;\n}\n\nexport default function SemiSplittableRoute({\n  loaderData,\n  actionData,\n}: Route.ComponentProps) {\n  return (\n    <>\n      <p>{loaderData}</p>\n      <p>{actionData}</p>\n      <Form method=\"post\">\n        <button>Submit</button>\n      </Form>\n    </>\n  );\n}\n"
  },
  {
    "path": "playground/split-route-modules/app/routes/splittable.tsx",
    "content": "import type { Route } from \"./+types/splittable\";\nimport { Form } from \"react-router\";\n\nexport const clientLoader = async () => {\n  return \"Hello from splittable client loader\";\n};\n\nexport const clientAction = async () => {\n  return \"Hello from splittable client action\";\n};\n\nexport function HydrateFallback() {\n  return <div>Loading...</div>;\n}\n\nexport default function SplittableRoute({\n  loaderData,\n  actionData,\n}: Route.ComponentProps) {\n  return (\n    <>\n      <p>{loaderData}</p>\n      <p>{actionData}</p>\n      <Form method=\"post\">\n        <button>Submit</button>\n      </Form>\n    </>\n  );\n}\n"
  },
  {
    "path": "playground/split-route-modules/app/routes/unsplittable.tsx",
    "content": "import type { Route } from \"./+types/unsplittable\";\nimport { Form } from \"react-router\";\n\n// Dummy variable to prevent route exports from being split\nlet shared: null = null;\n\nexport const clientLoader = async () => {\n  return shared ?? \"Hello from unsplittable client loader\";\n};\n\nexport const clientAction = async () => {\n  return shared ?? \"Hello from unsplittable client action\";\n};\n\nexport function HydrateFallback() {\n  return shared ?? <div>Loading...</div>;\n}\n\nexport default function UnsplittableRoute({\n  loaderData,\n  actionData,\n}: Route.ComponentProps) {\n  return (\n    <>\n      <p>{loaderData}</p>\n      <p>{actionData}</p>\n      <Form method=\"post\">\n        <button>Submit</button>\n      </Form>\n    </>\n  );\n}\n"
  },
  {
    "path": "playground/split-route-modules/app/routes.ts",
    "content": "import { type RouteConfig, index, route } from \"@react-router/dev/routes\";\n\nexport default [\n  index(\"routes/_index.tsx\"),\n  route(\"splittable\", \"routes/splittable.tsx\"),\n  route(\"unsplittable\", \"routes/unsplittable.tsx\"),\n  route(\"semi-splittable\", \"routes/semi-splittable.tsx\"),\n] satisfies RouteConfig;\n"
  },
  {
    "path": "playground/split-route-modules/package.json",
    "content": "{\n  \"name\": \"@playground/split-route-modules\",\n  \"version\": \"0.0.0\",\n  \"private\": true,\n  \"sideEffects\": false,\n  \"type\": \"module\",\n  \"scripts\": {\n    \"build\": \"react-router build\",\n    \"dev\": \"react-router dev\",\n    \"start\": \"react-router-serve ./build/server/index.js\",\n    \"typecheck\": \"react-router typegen && tsc\"\n  },\n  \"dependencies\": {\n    \"@react-router/node\": \"workspace:*\",\n    \"@react-router/serve\": \"workspace:*\",\n    \"isbot\": \"^5.1.11\",\n    \"react\": \"catalog:\",\n    \"react-dom\": \"catalog:\",\n    \"react-router\": \"workspace:*\"\n  },\n  \"devDependencies\": {\n    \"@react-router/dev\": \"workspace:*\",\n    \"@types/react\": \"catalog:\",\n    \"@types/react-dom\": \"catalog:\",\n    \"typescript\": \"catalog:\",\n    \"vite\": \"^6.3.0\",\n    \"vite-tsconfig-paths\": \"^4.2.1\"\n  },\n  \"engines\": {\n    \"node\": \">=18.0.0\"\n  }\n}\n"
  },
  {
    "path": "playground/split-route-modules/react-router.config.ts",
    "content": "import { Config } from \"@react-router/dev/config\";\n\nexport default {\n  future: {\n    v8_splitRouteModules: true,\n  },\n} satisfies Config;\n"
  },
  {
    "path": "playground/split-route-modules/tsconfig.json",
    "content": "{\n  \"include\": [\n    \"**/*.ts\",\n    \"**/*.tsx\",\n    \"**/.server/**/*.ts\",\n    \"**/.server/**/*.tsx\",\n    \"**/.client/**/*.ts\",\n    \"**/.client/**/*.tsx\",\n    \"./.react-router/types/**/*\"\n  ],\n  \"compilerOptions\": {\n    \"lib\": [\"DOM\", \"DOM.Iterable\", \"ES2022\"],\n    \"types\": [\"@react-router/node\", \"vite/client\"],\n    \"isolatedModules\": true,\n    \"esModuleInterop\": true,\n    \"jsx\": \"react-jsx\",\n    \"module\": \"ESNext\",\n    \"moduleResolution\": \"Bundler\",\n    \"resolveJsonModule\": true,\n    \"target\": \"ES2022\",\n    \"strict\": true,\n    \"allowJs\": true,\n    \"skipLibCheck\": true,\n    \"forceConsistentCasingInFileNames\": true,\n    \"baseUrl\": \".\",\n    \"paths\": {\n      \"~/*\": [\"./app/*\"]\n    },\n    \"noEmit\": true,\n    \"plugins\": [{ \"name\": \"@react-router/dev\" }],\n    \"rootDirs\": [\".\", \"./.react-router/types\"]\n  }\n}\n"
  },
  {
    "path": "playground/split-route-modules/vite.config.ts",
    "content": "import { reactRouter } from \"@react-router/dev/vite\";\nimport { defineConfig } from \"vite\";\nimport tsconfigPaths from \"vite-tsconfig-paths\";\n\nexport default defineConfig({\n  plugins: [reactRouter(), tsconfigPaths()],\n});\n"
  },
  {
    "path": "playground/split-route-modules-spa/.gitignore",
    "content": "node_modules\n\n/build\n.env\n\n.react-router/\n"
  },
  {
    "path": "playground/split-route-modules-spa/app/root.tsx",
    "content": "import { Links, Meta, Outlet, Scripts, ScrollRestoration } from \"react-router\";\n\nexport function Layout({ children }: { children: React.ReactNode }) {\n  return (\n    <html lang=\"en\">\n      <head>\n        <meta charSet=\"utf-8\" />\n        <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />\n        <Meta />\n        <Links />\n      </head>\n      <body>\n        {children}\n        <ScrollRestoration />\n        <Scripts />\n      </body>\n    </html>\n  );\n}\n\nexport default function App() {\n  return <Outlet />;\n}\n\nexport function HydrateFallback() {\n  return <p>Loading...</p>;\n}\n"
  },
  {
    "path": "playground/split-route-modules-spa/app/routes/_index.tsx",
    "content": "import { Link, type MetaFunction } from \"react-router\";\n\nexport const meta: MetaFunction = () => {\n  return [\n    { title: \"New React Router App\" },\n    { name: \"description\", content: \"Welcome to React Router!\" },\n  ];\n};\n\nexport default function Index() {\n  return (\n    <div style={{ fontFamily: \"system-ui, sans-serif\", lineHeight: \"1.8\" }}>\n      <ul>\n        <li>\n          <Link to=\"/splittable\">Splittable route</Link>\n        </li>\n        <li>\n          <Link to=\"/unsplittable\">Unsplittable route</Link>\n        </li>\n        <li>\n          <Link to=\"/semi-splittable\">Semi-splittable route</Link>\n        </li>\n      </ul>\n    </div>\n  );\n}\n"
  },
  {
    "path": "playground/split-route-modules-spa/app/routes/semi-splittable.tsx",
    "content": "import type { Route } from \"./+types/semi-splittable\";\nimport { Form } from \"react-router\";\n\n// Dummy variable to prevent route exports from being split\nlet shared: null = null;\n\nexport const clientLoader = async () => {\n  await new Promise((resolve) => setTimeout(resolve, 1000));\n  return shared ?? \"Hello from unsplittable client loader\";\n};\n\nexport const clientAction = async () => {\n  await new Promise((resolve) => setTimeout(resolve, 1000));\n  return \"Hello from splittable client action\";\n};\n\nexport default function SemiSplittableRoute({\n  loaderData,\n  actionData,\n}: Route.ComponentProps) {\n  return (\n    <>\n      <p>{loaderData}</p>\n      <p>{actionData}</p>\n      <Form method=\"post\">\n        <button>Submit</button>\n      </Form>\n    </>\n  );\n}\n"
  },
  {
    "path": "playground/split-route-modules-spa/app/routes/splittable.tsx",
    "content": "import type { Route } from \"./+types/splittable\";\nimport { Form } from \"react-router\";\n\nexport const clientLoader = async () => {\n  await new Promise((resolve) => setTimeout(resolve, 1000));\n  return \"Hello from splittable client loader\";\n};\n\nexport const clientAction = async () => {\n  await new Promise((resolve) => setTimeout(resolve, 1000));\n  return \"Hello from splittable client action\";\n};\n\nexport default function SplittableRoute({\n  loaderData,\n  actionData,\n}: Route.ComponentProps) {\n  return (\n    <>\n      <p>{loaderData}</p>\n      <p>{actionData}</p>\n      <Form method=\"post\">\n        <button>Submit</button>\n      </Form>\n    </>\n  );\n}\n"
  },
  {
    "path": "playground/split-route-modules-spa/app/routes/unsplittable.tsx",
    "content": "import type { Route } from \"./+types/unsplittable\";\nimport { Form } from \"react-router\";\n\n// Dummy variable to prevent route exports from being split\nlet shared: null = null;\n\nexport const clientLoader = async () => {\n  await new Promise((resolve) => setTimeout(resolve, 1000));\n  return shared ?? \"Hello from unsplittable client loader\";\n};\n\nexport const clientAction = async () => {\n  await new Promise((resolve) => setTimeout(resolve, 1000));\n  return shared ?? \"Hello from unsplittable client action\";\n};\n\nexport default function UnsplittableRoute({\n  loaderData,\n  actionData,\n}: Route.ComponentProps) {\n  return (\n    <>\n      <p>{loaderData}</p>\n      <p>{actionData}</p>\n      <Form method=\"post\">\n        <button>Submit</button>\n      </Form>\n    </>\n  );\n}\n"
  },
  {
    "path": "playground/split-route-modules-spa/app/routes.ts",
    "content": "import { type RouteConfig, index, route } from \"@react-router/dev/routes\";\n\nexport default [\n  index(\"routes/_index.tsx\"),\n  route(\"splittable\", \"routes/splittable.tsx\"),\n  route(\"unsplittable\", \"routes/unsplittable.tsx\"),\n  route(\"semi-splittable\", \"routes/semi-splittable.tsx\"),\n] satisfies RouteConfig;\n"
  },
  {
    "path": "playground/split-route-modules-spa/package.json",
    "content": "{\n  \"name\": \"@playground/split-route-modules-spa\",\n  \"version\": \"0.0.0\",\n  \"private\": true,\n  \"sideEffects\": false,\n  \"type\": \"module\",\n  \"scripts\": {\n    \"build\": \"react-router build\",\n    \"dev\": \"react-router dev\",\n    \"start\": \"vite preview\",\n    \"typecheck\": \"react-router typegen && tsc\"\n  },\n  \"dependencies\": {\n    \"@react-router/node\": \"workspace:*\",\n    \"isbot\": \"^5\",\n    \"react\": \"catalog:\",\n    \"react-dom\": \"catalog:\",\n    \"react-router\": \"workspace:*\"\n  },\n  \"devDependencies\": {\n    \"@react-router/dev\": \"workspace:*\",\n    \"@types/react\": \"catalog:\",\n    \"@types/react-dom\": \"catalog:\",\n    \"typescript\": \"catalog:\",\n    \"vite\": \"^6.3.0\",\n    \"vite-tsconfig-paths\": \"^4.2.1\"\n  },\n  \"engines\": {\n    \"node\": \">=20.0.0\"\n  }\n}\n"
  },
  {
    "path": "playground/split-route-modules-spa/react-router.config.ts",
    "content": "import type { Config } from \"@react-router/dev/config\";\n\nexport default {\n  ssr: false,\n  future: {\n    v8_splitRouteModules: true,\n  },\n} satisfies Config;\n"
  },
  {
    "path": "playground/split-route-modules-spa/tsconfig.json",
    "content": "{\n  \"include\": [\"**/*.ts\", \"**/*.tsx\", \"./.react-router/types/**/*\"],\n  \"compilerOptions\": {\n    \"lib\": [\"DOM\", \"DOM.Iterable\", \"ES2022\"],\n    \"types\": [\"@react-router/node\", \"vite/client\"],\n    \"verbatimModuleSyntax\": true,\n    \"esModuleInterop\": true,\n    \"jsx\": \"react-jsx\",\n    \"module\": \"ESNext\",\n    \"moduleResolution\": \"Bundler\",\n    \"resolveJsonModule\": true,\n    \"target\": \"ES2022\",\n    \"strict\": true,\n    \"allowJs\": true,\n    \"skipLibCheck\": true,\n    \"baseUrl\": \".\",\n    \"paths\": {\n      \"~/*\": [\"./app/*\"]\n    },\n    \"noEmit\": true,\n    \"rootDirs\": [\".\", \"./.react-router/types\"]\n  }\n}\n"
  },
  {
    "path": "playground/split-route-modules-spa/vite.config.ts",
    "content": "import { reactRouter } from \"@react-router/dev/vite\";\nimport { defineConfig } from \"vite\";\nimport tsconfigPaths from \"vite-tsconfig-paths\";\n\nexport default defineConfig({\n  plugins: [reactRouter(), tsconfigPaths()],\n});\n"
  },
  {
    "path": "playground/vite-plugin-cloudflare/.gitignore",
    "content": ".DS_Store\n/node_modules/\n*.tsbuildinfo\n\n# React Router\n/.react-router/\n/build/\n\n# Cloudflare\n.mf\n.wrangler\n"
  },
  {
    "path": "playground/vite-plugin-cloudflare/app/entry.server.tsx",
    "content": "import type { AppLoadContext, EntryContext } from \"react-router\";\nimport { ServerRouter } from \"react-router\";\nimport { isbot } from \"isbot\";\nimport { renderToReadableStream } from \"react-dom/server\";\n\nexport default async function handleRequest(\n  request: Request,\n  responseStatusCode: number,\n  responseHeaders: Headers,\n  routerContext: EntryContext,\n  _loadContext: AppLoadContext\n) {\n  let shellRendered = false;\n  const userAgent = request.headers.get(\"user-agent\");\n\n  const body = await renderToReadableStream(\n    <ServerRouter context={routerContext} url={request.url} />,\n    {\n      onError(error: unknown) {\n        responseStatusCode = 500;\n        // Log streaming rendering errors from inside the shell.  Don't log\n        // errors encountered during initial shell rendering since they'll\n        // reject and get logged in handleDocumentRequest.\n        if (shellRendered) {\n          console.error(error);\n        }\n      },\n    }\n  );\n  shellRendered = true;\n\n  // Ensure requests from bots and SPA Mode renders wait for all content to load before responding\n  // https://react.dev/reference/react-dom/server/renderToPipeableStream#waiting-for-all-content-to-load-for-crawlers-and-static-generation\n  if ((userAgent && isbot(userAgent)) || routerContext.isSpaMode) {\n    await body.allReady;\n  }\n\n  responseHeaders.set(\"Content-Type\", \"text/html\");\n  return new Response(body, {\n    headers: responseHeaders,\n    status: responseStatusCode,\n  });\n}\n"
  },
  {
    "path": "playground/vite-plugin-cloudflare/app/root.tsx",
    "content": "import { Links, Meta, Outlet, Scripts, ScrollRestoration } from \"react-router\";\n\nexport default function App() {\n  return (\n    <html lang=\"en\">\n      <head>\n        <meta charSet=\"utf-8\" />\n        <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />\n        <Meta />\n        <Links />\n      </head>\n      <body>\n        <Outlet />\n        <ScrollRestoration />\n        <Scripts />\n      </body>\n    </html>\n  );\n}\n"
  },
  {
    "path": "playground/vite-plugin-cloudflare/app/routes/_index.tsx",
    "content": "import type { MetaFunction } from \"react-router\";\nimport type { Route } from \"./+types/_index\"\n\nexport const meta: MetaFunction = () => {\n  return [\n    { title: \"New React Router App\" },\n    { name: \"description\", content: \"Welcome to React Router!\" },\n  ];\n};\n\nexport async function loader({ context }: Route.LoaderArgs) {\n  return {\n    message: context.cloudflare.env.VALUE_FROM_CLOUDFLARE,\n  };\n}\n\nexport default function Index({ loaderData }: Route.ComponentProps) {\n  return (\n    <div style={{ fontFamily: \"system-ui, sans-serif\", lineHeight: \"1.8\" }}>\n      <h1>Welcome to React Router</h1>\n      <p>{loaderData.message}</p>\n    </div>\n  );\n}\n"
  },
  {
    "path": "playground/vite-plugin-cloudflare/app/routes/static.tsx",
    "content": "import type { MetaFunction } from \"react-router\";\nimport { env } from \"cloudflare:workers\";\nimport type { Route } from \"./+types/static\";\n\nexport const meta: MetaFunction = () => {\n  return [\n    { title: \"New React Router App\" },\n    { name: \"description\", content: \"Welcome to React Router!\" },\n  ];\n};\n\nexport async function loader() {\n  return {\n    message: env.VALUE_FROM_CLOUDFLARE,\n  };\n}\n\nexport default function Static({ loaderData }: Route.ComponentProps) {\n  return (\n    <div style={{ fontFamily: \"system-ui, sans-serif\", lineHeight: \"1.8\" }}>\n      <h1>Welcome to React Router</h1>\n      <p>{loaderData.message}</p>\n    </div>\n  );\n}\n"
  },
  {
    "path": "playground/vite-plugin-cloudflare/app/routes.ts",
    "content": "import { type RouteConfig } from \"@react-router/dev/routes\";\nimport { flatRoutes } from \"@react-router/fs-routes\";\n\nexport default flatRoutes() satisfies RouteConfig;\n"
  },
  {
    "path": "playground/vite-plugin-cloudflare/package.json",
    "content": "{\n  \"name\": \"@playground/vite-plugin-cloudflare\",\n  \"version\": \"0.0.0\",\n  \"private\": true,\n  \"sideEffects\": false,\n  \"type\": \"module\",\n  \"scripts\": {\n    \"dev\": \"react-router dev\",\n    \"build\": \"react-router build\",\n    \"typegen\": \"wrangler types && react-router typegen\",\n    \"typecheck\": \"pnpm typegen && tsc\"\n  },\n  \"dependencies\": {\n    \"express\": \"^4.19.2\",\n    \"isbot\": \"^5.1.11\",\n    \"react\": \"catalog:\",\n    \"react-dom\": \"catalog:\",\n    \"react-router\": \"workspace:*\",\n    \"serialize-javascript\": \"^6.0.1\"\n  },\n  \"devDependencies\": {\n    \"@cloudflare/vite-plugin\": \"^1.9.0\",\n    \"@react-router/dev\": \"workspace:*\",\n    \"@react-router/fs-routes\": \"workspace:*\",\n    \"@types/node\": \"^20.0.0\",\n    \"@types/react\": \"catalog:\",\n    \"@types/react-dom\": \"catalog:\",\n    \"eslint\": \"^8.38.0\",\n    \"typescript\": \"catalog:\",\n    \"vite\": \"^6.3.0\",\n    \"vite-tsconfig-paths\": \"^4.2.1\",\n    \"wrangler\": \"^4.23.0\"\n  },\n  \"engines\": {\n    \"node\": \">=20.0.0\"\n  }\n}\n"
  },
  {
    "path": "playground/vite-plugin-cloudflare/react-router.config.ts",
    "content": "import type { Config } from \"@react-router/dev/config\";\n\nexport default {\n  future: {\n    v8_viteEnvironmentApi: true,\n    unstable_previewServerPrerendering: true,\n  },\n  prerender: [\"/static\"],\n} satisfies Config;\n"
  },
  {
    "path": "playground/vite-plugin-cloudflare/tsconfig.cloudflare.json",
    "content": "{\n  \"extends\": \"./tsconfig.json\",\n  \"include\": [\n    \".react-router/types/**/*\",\n    \"app/**/*\",\n    \"app/**/.server/**/*\",\n    \"app/**/.client/**/*\",\n    \"workers/**/*\",\n    \"worker-configuration.d.ts\"\n  ],\n  \"compilerOptions\": {\n    \"composite\": true,\n    \"strict\": true,\n    \"lib\": [\"DOM\", \"DOM.Iterable\", \"ES2022\"],\n    \"types\": [\"vite/client\"],\n    \"target\": \"ES2022\",\n    \"module\": \"ES2022\",\n    \"moduleResolution\": \"bundler\",\n    \"jsx\": \"react-jsx\",\n    \"baseUrl\": \".\",\n    \"rootDirs\": [\".\", \"./.react-router/types\"],\n    \"paths\": {\n      \"~/*\": [\"./app/*\"]\n    },\n    \"esModuleInterop\": true,\n    \"resolveJsonModule\": true\n  }\n}\n"
  },
  {
    "path": "playground/vite-plugin-cloudflare/tsconfig.json",
    "content": "{\n  \"files\": [],\n  \"references\": [\n    { \"path\": \"./tsconfig.node.json\" },\n    { \"path\": \"./tsconfig.cloudflare.json\" }\n  ],\n  \"compilerOptions\": {\n    \"checkJs\": true,\n    \"verbatimModuleSyntax\": true,\n    \"skipLibCheck\": true,\n    \"strict\": true,\n    \"noEmit\": true\n  }\n}\n"
  },
  {
    "path": "playground/vite-plugin-cloudflare/tsconfig.node.json",
    "content": "{\n  \"extends\": \"./tsconfig.json\",\n  \"include\": [\"react-router.config.ts\", \"vite.config.ts\"],\n  \"compilerOptions\": {\n    \"composite\": true,\n    \"strict\": true,\n    \"types\": [\"node\"],\n    \"lib\": [\"ES2022\"],\n    \"target\": \"ES2022\",\n    \"module\": \"ES2022\",\n    \"moduleResolution\": \"bundler\"\n  }\n}\n"
  },
  {
    "path": "playground/vite-plugin-cloudflare/vite.config.ts",
    "content": "import { reactRouter } from \"@react-router/dev/vite\";\nimport { defineConfig } from \"vite\";\nimport tsconfigPaths from \"vite-tsconfig-paths\";\nimport { cloudflare } from \"@cloudflare/vite-plugin\";\n\nexport default defineConfig({\n  plugins: [\n    cloudflare({ viteEnvironment: { name: \"ssr\" } }),\n    reactRouter(),\n    tsconfigPaths(),\n  ],\n});\n"
  },
  {
    "path": "playground/vite-plugin-cloudflare/workers/app.ts",
    "content": "import { createRequestHandler } from \"react-router\";\n\ndeclare global {\n  interface CloudflareEnvironment extends Env {}\n}\n\ndeclare module \"react-router\" {\n  export interface AppLoadContext {\n    cloudflare: {\n      env: CloudflareEnvironment;\n      ctx: ExecutionContext;\n    };\n  }\n}\n\nconst requestHandler = createRequestHandler(\n  () => import(\"virtual:react-router/server-build\"),\n  import.meta.env.MODE\n);\n\nexport default {\n  async fetch(request, env, ctx) {\n    return requestHandler(request, {\n      cloudflare: { env, ctx },\n    });\n  },\n} satisfies ExportedHandler<CloudflareEnvironment>;\n"
  },
  {
    "path": "playground/vite-plugin-cloudflare/wrangler.toml",
    "content": "name = \"react-router-app\"\ncompatibility_date = \"2025-03-17\"\nmain = \"./workers/app.ts\"\n\nassets = {}\n\n[vars]\nVALUE_FROM_CLOUDFLARE = \"Hello from Cloudflare\"\n"
  },
  {
    "path": "pnpm-workspace.yaml",
    "content": "packages:\n  - \"integration\"\n  - \"integration/helpers/*\"\n  - \"packages/*\"\n  - \"playground/*\"\n\ncatalog:\n  \"@types/react\": ^18.0.27\n  \"@types/react-dom\": ^18.0.10\n  \"@vitejs/plugin-rsc\": ~0.5.7\n  react: ^19.2.3\n  react-dom: ^19.2.3\n  react-server-dom-parcel: ^19.2.3\n  react-server-dom-webpack: ^19.2.3\n  typescript: ^5.4.5\n  tsup: ^8.3.0\n  wireit: 0.14.9\n\ncatalogs:\n  react-canary:\n    \"@types/react\": ^19.2.7\n    \"@types/react-dom\": ^19.2.3\n    react: ^19.2.2\n    react-dom: ^19.2.2\n    react-server-dom-parcel: ^19.2.2\n    react-server-dom-webpack: ^19.2.2\n"
  },
  {
    "path": "prettier.config.js",
    "content": "/**\n * @type {import('prettier').Options}\n */\nmodule.exports = {};\n"
  },
  {
    "path": "scripts/clean-v6-artifacts.sh",
    "content": "#!/bin/bash\n\nset -e\n\npnpm clean\n\necho \"Removing v6 artifacts...\"\nset -x\nrm -rf packages/react-router-dom-v5-compat/\nrm -f packages/react-router-dom/server.*\nset +x\n\necho \"Installing and building...\"\npnpm install --frozen-lockfile\npnpm build"
  },
  {
    "path": "scripts/close-feature-pr.md",
    "content": "To align with our new [Open Governance](https://remix.run/blog/rr-governance) model, we are now asking that all new features go through the [Proposal/RFC process](https://github.com/remix-run/react-router/blob/main/GOVERNANCE.md#new-feature-process) and that we don't open PRs until a proposal has been accepted and advanced to Stage 1.\n\nIf this feature doesn't have a Proposal, please [open one](https://github.com/remix-run/react-router/discussions/new?category=proposals) so we can evaluate/discuss the proposed feature. You can link to this PR as an example of a potential implementation and we can re-open it if the proposal advances.\n\nIf this PR already has a Proposal but it has not yet been accepted, let's continue the discussion in the Proposal until it gets accepted and then we can look to open a PR. Feel free to link to this PR or to a branch in a forked repo to show what a potential implementation might look like.\n\nIf you have any questions, you can always reach out on [Discord](https://rmx.as/discord). Thanks again for providing feedback and helping us make React Router even better!\n"
  },
  {
    "path": "scripts/close-no-repro-issue.md",
    "content": "To align with our new [Open Governance](https://remix.run/blog/rr-governance) model, we are now requiring that all issues have a [**minimal** and **runnable** reproduction](https://github.com/remix-run/react-router/blob/main/GOVERNANCE.md#bugissue-process). This should help us to focus on actionable issues and be more responsive to newly filed issues.\n\nTo get this re-opened, please add a reproduction to the Issue description and tag `@brophdawg11` or `@brookslybrand` in a comment so we can re-open.\n\nOr, if this was closed by mistake and there is a valid reproduction, please ensure that it is linked in the Issue description and tag `@brophdawg11` or `@brookslybrand` in a comment so we can re-open.\n\nIf you have any questions, you can always reach out on [Discord](https://rmx.as/discord). Thanks again for providing feedback and helping us make React Router even better!\n"
  },
  {
    "path": "scripts/close-no-repro-issues.md",
    "content": "To align with our new [Open Governance](https://remix.run/blog/rr-governance) model, we are now requiring that all issues have a [**minimal** and **runnable** reproduction](https://github.com/remix-run/react-router/blob/main/GOVERNANCE.md#bugissue-process). To that end, we're doing some housekeeping in the repo to clean up existing issues that do not have a valid reproduction. This should get us down to a more manageable number of issues and allow us to be more responsive to existing and newly filed issues.\n\nWe're using a GitHub actions script to identify issues without a reproduction by looking for a [StackBlitz](https://stackblitz.com/), [CodeSandbox](https://codesandbox.io/), or [GitHub](https://github.com) link in the issue body. This won't be perfect, so if this issue has a reproduction on another platform, please comment back on here, and we can re-open the issue. Similarly, if there's a reproduction buried in a comment, please move the link into the description and comment back. Please tag `@brophdawg11` or `@brookslybrand` in your comment so we get a notification as well 🙂.\n\nIf this issue did not have a reproduction but is still valid, or if you wish to start with a fresh issue, please [create a new issue](https://github.com/remix-run/react-router/issues/new?template=bug_report.yml) with a fresh reproduction against v7 and link to this issue in the new description.\n\nIf this is a feature request, please open a new [Proposal Discussion](https://github.com/remix-run/react-router/discussions/new?category=proposals) in React Router, and if it gets enough community support, it can be considered for implementation.\n\nIf you have any questions, you can always reach out on [Discord](https://rmx.as/discord). Thanks again for providing feedback and helping us make React Router even better!\n"
  },
  {
    "path": "scripts/close-no-repro-issues.ts",
    "content": "import { execSync } from \"node:child_process\";\nimport { parseArgs } from \"node:util\";\n\nconst sleep = (ms: number) =>\n  new Promise<void>((resolve) => setTimeout(resolve, ms));\n\nconst ignoredIssues = new Set([9991, 12570, 13607, 13659, 11940]);\n\nconst { values: args } = parseArgs({\n  options: {\n    dryRun: {\n      type: \"boolean\",\n      default: false,\n    },\n  },\n  strict: true,\n});\n\nrun();\n\nasync function run() {\n  let issuesCmd = `gh issue list --search \"is:issue state:open label:bug sort:created-asc\" --limit 250 --json number,body`;\n  console.log(`Executing command: ${issuesCmd}`);\n  let result = execSync(issuesCmd).toString();\n  let allIssues = JSON.parse(result) as { number: number; body: string }[];\n  let noReproIssues = allIssues.filter(({ number, body }) => {\n    return (\n      !ignoredIssues.has(number) &&\n      !/https?:\\/\\/stackblitz\\.com\\//.test(body) &&\n      !/https?:\\/\\/codesandbox\\.io\\//.test(body) &&\n      !/https?:\\/\\/github\\.com\\//.test(\n        body\n          // Remove uploaded image URLs and links to react router source code\n          // and new issue links before looking for git repo reproductions\n          .replace(\"https://github.com/user-attachments/\", \"\")\n          .replace(\"https://github.com/remix-run/react-router/\", \"\"),\n      )\n    );\n  });\n\n  console.log(\n    `Found ${noReproIssues.length} issues without a reproduction:\\n` +\n      noReproIssues.map((i) => i.number).join(\",\"),\n  );\n\n  for (let issue of noReproIssues) {\n    console.log(`--- Processing issue #${issue.number} ---`);\n    let commentCmd = `gh issue comment ${issue.number} -F ./scripts/close-no-repro-issues.md`;\n    let commentResult = runCmdIfTokenExists(commentCmd);\n    console.log(`Commented on issue #${issue.number}: ${commentResult}`);\n    await sleep(250);\n\n    let closeCmd = `gh issue close ${issue.number} -r \"not planned\"`;\n    runCmdIfTokenExists(closeCmd);\n    // No log here since the GH CLI already logs for issue close\n    await sleep(250);\n  }\n\n  console.log(\"Done!\");\n}\n\nfunction runCmdIfTokenExists(cmd: string) {\n  if (args.dryRun) {\n    console.log(`⚠️ Dry run, skipping command: ${cmd}`);\n    return \"<skipped>\";\n  }\n\n  if (process.env.CI !== \"true\") {\n    console.log(`⚠️ Local run without CI env var, skipping command: ${cmd}`);\n    return \"<skipped>\";\n  }\n\n  return execSync(cmd).toString();\n}\n"
  },
  {
    "path": "scripts/constants.js",
    "content": "const path = require(\"path\");\n\nconst ROOT_DIR = path.resolve(__dirname, \"..\");\nconst EXAMPLES_DIR = path.resolve(ROOT_DIR, \"examples\");\n\nmodule.exports = {\n  ROOT_DIR,\n  EXAMPLES_DIR,\n};\n"
  },
  {
    "path": "scripts/delete-pre-tags.sh",
    "content": "#!/bin/bash\n\necho \"Pruning tags before looking for tags to delete...\"\ngit fetch --prune --prune-tags\n\nPATTERN=\"@7\\.\\d\\+\\.\\d\\+-pre\"\n\nTAGS=$(git tag | grep -e \"${PATTERN}\")\n\nif [[ $TAGS == \"\" ]]; then\n  echo \"No tags to delete, exiting\"\n  exit 0\nfi\n\n# Delay setting this because if it's set when no tags exist the program exits\n# on the TAGS assignment above\nset -e\n\nNUM_TAGS=$(git tag | grep -e \"${PATTERN}\" | wc -l | sed 's/ //g')\nTAGS_LINE=$(git tag | grep -e \"${PATTERN}\" | tr '\\n' ' ')\n\necho \"\"\necho \"Found ${NUM_TAGS} tags to delete. To delete, run the following commands:\"\necho \"\"\necho \"git push origin --delete ${TAGS_LINE}\"\necho \"git fetch --prune --prune-tags\"\necho \"\"\necho \"\"\n\nset +e\n"
  },
  {
    "path": "scripts/docs.ts",
    "content": "import fs from \"node:fs\";\nimport path from \"node:path\";\nimport util from \"node:util\";\n\nimport fg from \"fast-glob\";\nimport dox from \"dox\";\nimport prettier from \"prettier\";\nimport { ReflectionKind, type JSONOutput } from \"typedoc\";\nimport ts from \"typescript\";\n\ntype UnknownTag = {\n  type: string;\n  string: string;\n  html: string;\n};\n\ntype ParamTag = {\n  type: \"param\";\n  string: string;\n  name: string;\n  description: string;\n  variable: boolean;\n  nonNullable: boolean;\n  nullable: boolean;\n  optional: boolean;\n};\n\ntype Tag = ParamTag | UnknownTag;\n\ntype ParsedComment = {\n  tags: Tag[];\n  description: {\n    full: string;\n    summary: string;\n    body: string;\n  };\n  isPrivate: boolean;\n  isConstructor: boolean;\n  isClass: boolean;\n  isEvent: boolean;\n  ignore: boolean;\n  line: number;\n  codeStart: number;\n  code: string;\n  ctx: { name: string } | false;\n};\n\nexport type GetArrayElementType<T extends readonly any[]> =\n  T extends readonly (infer U)[] ? U : never;\n\ntype Mode = GetArrayElementType<typeof MODES>;\ntype Category = GetArrayElementType<typeof CATEGORIES>;\n\ntype SimplifiedComment = {\n  category: Category;\n  name: string;\n  unstable: boolean;\n  codeLink: string;\n  modes: Mode[];\n  summary: string;\n  reference?: string;\n  example?: string;\n  signature?: string;\n  params: {\n    name: string;\n    description: string;\n    modes: Mode[];\n  }[];\n  returns?: string;\n  additionalExamples?: string;\n};\n\nconst MODES = [\"framework\", \"data\", \"declarative\"] as const;\nconst CATEGORIES = [\n  \"Components\",\n  \"Hooks\",\n  \"Framework Routers\",\n  \"Data Routers\",\n  \"Declarative Routers\",\n  \"RSC\",\n  \"Utils\",\n] as const;\n\nconst warn = (...args: any[]) => console.warn(\"⚠️ Warning:\", ...args);\n\nconst isClassApi = (c: SimplifiedComment) => c.name === \"RouterContextProvider\";\n\nconst isComponentApi = (c: SimplifiedComment) =>\n  c.category === \"Components\" ||\n  c.category === \"Framework Routers\" ||\n  c.category === \"Declarative Routers\" ||\n  c.name === \"RouterProvider\" ||\n  c.name === \"StaticRouterProvider\" ||\n  c.name === \"unstable_RSCStaticRouter\" ||\n  c.name === \"unstable_RSCHydratedRouter\";\n\n// Read a filename from standard input using the node parseArgs utility\n\nconst { values: args } = util.parseArgs({\n  args: process.argv.slice(2),\n  options: {\n    path: {\n      type: \"string\",\n      short: \"p\",\n    },\n    api: {\n      type: \"string\",\n      short: \"a\",\n    },\n    write: {\n      type: \"boolean\",\n      short: \"w\",\n    },\n    output: {\n      type: \"string\",\n      short: \"o\",\n    },\n    help: {\n      type: \"boolean\",\n      short: \"h\",\n    },\n  },\n  allowPositionals: true,\n});\n\nif (args.help) {\n  console.log(\"\\n\");\n  console.log(\n    \"Usage: docs.ts [--path <filepath-or-glob>] [--api <api1,api2,...>] [--write] [--output <output-dir>]\",\n  );\n  console.log(\n    '  --path, -p    File path or glob pattern to parse (default \"packages/**/*.{ts,tsx}\")',\n  );\n  console.log(\n    \"  --api, -a     Comma-separated list of specific APIs to generate\",\n  );\n  console.log(\"  --write, -w   Write markdown files to output directory\");\n  console.log(\"  --output, -o  Output directory (default: docs/api)\");\n  process.exit(0);\n}\n\n// Resolve file paths using glob patterns\nlet pathGlob = args.path || \"packages/**/*.{ts,tsx}\";\nconst filePaths = fg.sync(pathGlob, {\n  onlyFiles: true,\n  ignore: [\"**/node_modules/**\", \"**/__tests__/**\", \"**/dist/**\"],\n});\n\nif (filePaths.length === 0) {\n  console.error(`No files found matching pattern: ${pathGlob}`);\n  process.exit(1);\n}\n\n// Parse the API filter if provided\nlet apiFilter: string[] | null = null;\nif (args.api) {\n  apiFilter = args.api.split(\",\").map((name) => name.trim());\n}\n\n// Configure output directory\nconst outputDir = args.output || \"docs/api\";\n\n// Build lookup table for @link resolution\nconst repoApiLookup = buildRepoDocsLinks(outputDir);\nconst typedocLookup = buildTypedocLinks(outputDir);\n\nrun();\n\n// Generate markdown documentation for all matching files\nasync function run() {\n  for (let filePath of filePaths) {\n    console.log(`\\nProcessing file: ${filePath}`);\n    await generateMarkdownDocs(filePath, apiFilter, outputDir, args.write);\n  }\n}\n\nfunction buildRepoDocsLinks(outputDir: string): Map<string, string> {\n  const lookup = new Map<string, string>();\n\n  // Add existing files if output directory exists\n  if (!fs.existsSync(outputDir)) {\n    throw new Error(\n      `Docs directory does not exist for cross-linking: ${outputDir}`,\n    );\n  }\n\n  const markdownFiles = fg.sync(`${outputDir}/**/*.md`, {\n    onlyFiles: true,\n  });\n\n  markdownFiles.forEach((filePath) => {\n    const relativePath = path\n      .relative(outputDir, filePath)\n      .replace(/\\.md$/, \"\")\n      .replace(/\\\\/g, \"/\");\n    const apiName = path.basename(relativePath);\n\n    if (apiName !== \"index\") {\n      lookup.set(apiName, relativePath);\n    }\n  });\n\n  return lookup;\n}\n\nfunction buildTypedocLinks(outputDir: string) {\n  const lookup = new Map<string, { href: string; description?: string }>();\n\n  // Prerequisite: `typedoc` has been run first via `npm run docs`\n  if (fs.existsSync(\"public/dev/api.json\")) {\n    let apiData = JSON.parse(\n      fs.readFileSync(\"public/dev/api.json\", \"utf8\"),\n    ) as JSONOutput.ProjectReflection;\n\n    apiData.children\n      ?.filter((c) => c.kind === ReflectionKind.Module)\n      .forEach((child) => processTypedocModule(child, lookup));\n  } else {\n    warn(\n      'Typedoc API data not found at \"public/dev/api.json\", will not ' +\n        \"automatically cross-link to Reference Docs\",\n    );\n  }\n\n  return lookup;\n}\n\nfunction processTypedocModule(\n  child: JSONOutput.ReferenceReflection | JSONOutput.DeclarationReflection,\n  lookup: Map<string, { href: string; description?: string }>,\n  prefix: string[] = [],\n) {\n  let newPrefix = [...prefix, child.name];\n  let moduleName = newPrefix.join(\".\");\n  child.children?.forEach((subChild) => {\n    // Recurse into submodules\n    if (subChild.kind === ReflectionKind.Module) {\n      processTypedocModule(subChild, lookup, newPrefix);\n      return;\n    }\n\n    // The majority of documented APIs are from `react-router` so we filter it\n    // (and it's index export) out from module names so we can do:\n    //  - `{@link Form}` instead of `{@link react-router.Form}`\n    //  - `{@link dom.RouterProvider}` instead of `{@link react-router.dom.RouterProvider}`\n    let apiName = `${moduleName}.${subChild.name}`.replace(\n      /^react-router\\./,\n      \"\",\n    );\n    if (lookup.has(apiName)) {\n      warn(`Skipping duplicate ${apiName} in typedoc JSON`);\n      return;\n    }\n\n    let type =\n      subChild.kind === ReflectionKind.Enum\n        ? \"enums\"\n        : subChild.kind === ReflectionKind.Class\n          ? \"classes\"\n          : subChild.kind === ReflectionKind.Interface\n            ? \"interfaces\"\n            : subChild.kind === ReflectionKind.TypeAlias\n              ? \"types\"\n              : subChild.kind === ReflectionKind.Function\n                ? \"functions\"\n                : subChild.kind === ReflectionKind.Variable\n                  ? \"variables\"\n                  : undefined;\n\n    // Assigning an arrow function to a variable will be a \"variable\" here but\n    // typedoc will classify it as a \"function\".  We can identify these if they\n    // define `@params` or `@returns` tags in their JSDoc.\n    if (\n      type === \"variables\" &&\n      subChild.comment?.blockTags?.some(\n        (tag) => tag.tag === \"@param\" || tag.tag === \"@returns\",\n      )\n    ) {\n      type = \"functions\";\n    }\n\n    if (!type) {\n      warn(\n        `Skipping ${apiName} because it is not a function, class, enum, interface, or type`,\n      );\n      return;\n    }\n\n    let modulePath = moduleName.replace(/[@/]/g, \"_\");\n    let path = `${type}/${modulePath}.${subChild.name}.html`;\n    let url = `https://api.reactrouter.com/v7/${path}`;\n    lookup.set(apiName, { href: url });\n\n    // When this is an interface, also include it's child properties in the lookup\n    // table for use in cross-referencing param types.  We often document a property\n    // on the `interface` so that it shows up in the IDE hover states, but we want\n    // to leverage the same description for the `@param` JSDoc tag. Mostly needed\n    // for components who separate props out in an interface such as `LinkProps`\n    //\n    // /**\n    //  * @param {LinkProps.to} props.to\n    //  */\n    if (subChild.kind === ReflectionKind.Interface) {\n      subChild.children?.forEach((grandChild) => {\n        if (\n          grandChild.kind === ReflectionKind.Property &&\n          grandChild.comment &&\n          !grandChild.flags.isExternal\n        ) {\n          lookup.set(`${apiName}.${grandChild.name}`, {\n            href: `${url}#${grandChild.name}`,\n            description: getDeclarationDescription(grandChild),\n          });\n        }\n      });\n    }\n\n    if (subChild.kind === ReflectionKind.TypeAlias) {\n      if (subChild.type?.type === \"intersection\") {\n        subChild.type.types.forEach((t) => {\n          if (t.type === \"reflection\") {\n            t.declaration.children?.forEach((c) => {\n              if (\n                c.kind === ReflectionKind.Property &&\n                c.comment &&\n                !c.flags.isExternal\n              ) {\n                lookup.set(`${apiName}.${c.name}`, {\n                  href: `${url}#${c.name}`,\n                  description: getDeclarationDescription(c),\n                });\n              }\n            });\n          } else if (t.type === \"union\") {\n            // For now we don't try to flatten down unions and we can\n            // just point to the base type in our JSDoc comment\n            return;\n          } else if (t.type === \"reference\") {\n            // For now we don't try to flatten down intersections and we can\n            // just point to the base type in our JSDoc comment\n            return;\n          } else {\n            warn(`Unhandled TypeAlias type: ${t.type}`);\n          }\n        });\n      }\n    }\n  });\n}\n\nfunction getDeclarationDescription(child: JSONOutput.DeclarationReflection) {\n  if (!child.comment) {\n    throw new Error(\"Cannot generate description without a comment.\");\n  }\n  return child.comment.summary\n    .flatMap((s) => (s.kind === \"inline-tag\" ? `{${s.tag} ${s.text}}` : s.text))\n    .join(\"\");\n}\n\nasync function generateMarkdownDocs(\n  filepath: string,\n  apiFilter: string[] | null,\n  outputDir?: string,\n  writeFiles?: boolean,\n) {\n  let simplifiedComments = await parseDocComments(filepath, apiFilter);\n  simplifiedComments.forEach((comment) => {\n    // Generate markdown content for each public function\n    let markdownContent = generateMarkdownForComment(comment);\n    if (markdownContent) {\n      if (writeFiles && outputDir) {\n        // Write to file based on category\n        writeMarkdownFile(comment, markdownContent, outputDir);\n      } else {\n        // Print to console (existing behavior)\n        console.log(`\\n=== Markdown for ${comment.name} ===`);\n        console.log(markdownContent);\n        console.log(`=== End of ${comment.name} ===\\n`);\n      }\n    }\n  });\n}\n\nfunction writeMarkdownFile(\n  comment: SimplifiedComment,\n  markdownContent: string,\n  outputDir: string,\n) {\n  // Convert category to lowercase and replace spaces with hyphens for folder name\n  const categoryFolder = comment.category.toLowerCase().replace(/\\s+/g, \"-\");\n\n  // Create the full directory path\n  const targetDir = path.join(outputDir, categoryFolder);\n\n  // Ensure the directory exists\n  if (!fs.existsSync(targetDir)) {\n    fs.mkdirSync(targetDir, { recursive: true });\n  }\n\n  // Create the filename (e.g., useHref.md)\n  const filename = `${comment.name.replace(/^unstable_/, \"\")}.md`;\n  const filePath = path.join(targetDir, filename);\n\n  // Write the file\n  fs.writeFileSync(filePath, markdownContent, \"utf8\");\n  console.log(`✓ Written: ${filePath}`);\n}\n\nfunction generateMarkdownForComment(comment: SimplifiedComment): string {\n  let markdown = \"\";\n\n  // Skip functions without proper names\n  if (!comment.name || comment.name === \"undefined\") {\n    return \"\";\n  }\n\n  // Title with frontmatter\n  markdown += `---\\n`;\n  markdown += `title: ${comment.name.replace(/^unstable_/, \"\")}\\n`;\n  markdown += comment.unstable ? \"unstable: true\\n\" : \"\";\n  markdown += `---\\n\\n`;\n\n  markdown += `# ${comment.name}\\n\\n`;\n\n  markdown += `<!--\\n`;\n  markdown += `⚠️ ⚠️ IMPORTANT ⚠️ ⚠️ \\n\\n`;\n  markdown += `Thank you for helping improve our documentation!\\n\\n`;\n  markdown += `This file is auto-generated from the JSDoc comments in the source\\n`;\n  markdown += `code, so please edit the JSDoc comments in the file below and this\\n`;\n  markdown += `file will be re-generated once those changes are merged.\\n\\n`;\n  markdown += `${comment.codeLink}\\n`;\n  markdown += `-->\\n\\n`;\n\n  // Modes section\n  if (comment.modes && comment.modes.length > 0) {\n    markdown += `[MODES: ${comment.modes.join(\", \")}]\\n\\n`;\n  }\n\n  if (comment.unstable) {\n    if (comment.modes && comment.modes.length > 0) {\n      markdown += \"<br />\\n<br />\\n\\n\";\n    }\n    markdown +=\n      \"<docs-warning>This API is experimental and subject to breaking changes in \\n\" +\n      \"minor/patch releases. Please use with caution and pay **very** close attention \\n\" +\n      \"to release notes for relevant changes.</docs-warning>\\n\\n\";\n  }\n\n  // Summary section\n  markdown += `## Summary\\n\\n`;\n\n  // Generate reference documentation link from @reference tag or fallback to default\n  if (comment.reference) {\n    markdown += `[Reference Documentation ↗](${comment.reference})\\n\\n`;\n  }\n\n  // Clean up HTML tags from summary and convert to plain text\n  let summary = resolveLinkTags(comment.summary);\n  markdown += `${summary}\\n\\n`;\n\n  // Example section (if available)\n  if (comment.example) {\n    let example = resolveLinkTags(comment.example);\n    markdown += `\\`\\`\\`tsx\\n${example}\\n\\`\\`\\`\\n\\n`;\n  }\n\n  // Signature section\n  if (comment.signature) {\n    markdown += `## Signature\\n\\n`;\n    markdown += \"```tsx\\n\";\n    markdown += `${comment.signature}\\n`;\n    markdown += \"```\\n\\n\";\n  }\n\n  // Parameters section\n  if (comment.params && comment.params.length > 0) {\n    let heading = isComponentApi(comment) ? \"Props\" : \"Params\";\n    let showModes = comment.params.some((p) => p.modes && p.modes.length > 0);\n    markdown += `## ${heading}\\n\\n`;\n    comment.params.forEach((param, i) => {\n      // Only show modes for parameters if they differ from hook-level modes\n      // For now, we assume all parameters have the same modes as the hook\n      // This could be enhanced in the future if we need per-parameter mode support\n\n      // Clean up HTML tags from description\n      let description = resolveLinkTags(param.description);\n\n      // Skip options object param that is there for JSDoc since we will document each option on it own\n      let skippedObjectParams = [\n        [\"options\", \"Options\"],\n        [\"opts\", \"Options\"],\n        [\"props\", \"Props\"],\n      ];\n      for (let skipped of skippedObjectParams) {\n        if (param.name === skipped[0] && description === skipped[1]) {\n          if (!comment.params[i + 1].name.startsWith(skipped[0] + \".\")) {\n            throw new Error(\n              \"Expected docs for individual options: \" + comment.name,\n            );\n          }\n          return;\n        }\n      }\n\n      let paramName = isComponentApi(comment)\n        ? param.name.replace(/^props\\./, \"\")\n        : param.name;\n      markdown += `### ${paramName}\\n\\n`;\n      if (showModes) {\n        let modes = param.modes.length ? param.modes : MODES;\n        markdown += `[modes: ${modes.join(\", \")}]\\n\\n`;\n      }\n      markdown += `${description || \"_No documentation_\"}\\n\\n`;\n    });\n  }\n\n  // Returns section (if applicable/available)\n  if (comment.returns && !isComponentApi(comment)) {\n    markdown += `## Returns\\n\\n`;\n    if (comment.returns === \"{void}\") {\n      markdown += \"No return value.\\n\\n\";\n    } else {\n      markdown += `${resolveLinkTags(comment.returns)}\\n\\n`;\n    }\n  }\n\n  // Additional Examples section (if available)\n  if (comment.additionalExamples) {\n    let additionalExamples = resolveLinkTags(comment.additionalExamples);\n    markdown += `## Examples\\n\\n`;\n    markdown += `${additionalExamples}\\n\\n`;\n  }\n\n  return markdown;\n}\n\nasync function parseDocComments(filepath: string, apiFilter: string[] | null) {\n  let code = fs.readFileSync(filepath).toString();\n  let comments = dox.parseComments(code, { raw: true }) as ParsedComment[];\n  let filteredComments = comments.filter(\n    (c) =>\n      c.tags.some((t) => t.type === \"public\") &&\n      (!apiFilter || apiFilter.includes(getApiName(c))),\n  );\n\n  return Promise.all(filteredComments.map((c) => simplifyComment(c, filepath)));\n}\n\nfunction getApiName(comment: ParsedComment): string {\n  let name =\n    comment.tags.find((t) => t.type === \"name\")?.string ||\n    (comment.ctx ? comment.ctx.name : undefined);\n  if (name) {\n    return name;\n  }\n\n  let matches = comment.code.match(/^export const ([^:=]+)/);\n  if (matches) {\n    return matches[1].trim();\n  }\n\n  matches = comment.code.match(/^export function ([^<(]+)/);\n  if (matches) {\n    return matches[1].trim();\n  }\n\n  throw new Error(`Could not determine API name:\\n${comment.code}\\n`);\n}\n\nasync function simplifyComment(\n  comment: ParsedComment,\n  filepath: string,\n): Promise<SimplifiedComment> {\n  let name = getApiName(comment);\n  let unstable = name.startsWith(\"unstable_\");\n\n  let codeLink = `https://github.com/remix-run/react-router/blob/main/${filepath}`;\n\n  let categoryTags = comment.tags.filter((t) => t.type === \"category\");\n  if (categoryTags.length !== 1) {\n    throw new Error(`Expected a single category tag: ${name}`);\n  }\n  let category = categoryTags[0].string as Category;\n  if (!CATEGORIES.includes(category)) {\n    throw new Error(`Invalid @category tag for ${name}: \"${category}\"`);\n  }\n\n  let modes: Mode[] = [...MODES];\n  let modeTags = comment.tags.filter((t) => t.type === \"mode\");\n  if (modeTags.length > 0) {\n    modes = modeTags.map((mode) => mode.string as Mode);\n  }\n\n  let summary = comment.description.full;\n  if (!summary) {\n    throw new Error(`Expected a summary: ${name}`);\n  }\n\n  let example = comment.tags.find((t) => t.type === \"example\")?.string;\n  let additionalExamples = comment.tags.find(\n    (t) => t.type === \"additionalExamples\",\n  )?.string;\n\n  let reference = typedocLookup.get(name)?.href;\n  if (!reference) {\n    warn(\n      `Could not find API in typedoc reference docs, skipping reference link: ${name}`,\n    );\n  }\n\n  let signature = await getSignature(comment.code);\n\n  let params: SimplifiedComment[\"params\"] = [];\n  comment.tags.forEach((tag) => {\n    if (isParamTag(tag)) {\n      let description: string | undefined = tag.description;\n\n      let modes: Mode[] = [];\n      let modesRegex = /\\[modes: ([^\\]]+)\\]/;\n      let matches = description?.match(modesRegex);\n      if (matches) {\n        modes = matches[1]\n          .split(\",\")\n          .map((m) => m.trim() as Mode)\n          .filter((m) => MODES.includes(m));\n        description = description.replace(modesRegex, \"\").trim();\n      }\n\n      // If we have a type, we prefer to look up the referenced type description\n      matches = tag.string.match(/^\\{(.+)\\}\\s.*/);\n      if (matches) {\n        if (typedocLookup.get(matches[1])?.description) {\n          description = typedocLookup.get(matches[1])!.description;\n        } else {\n          throw new Error(\n            `Unable to find cross-referenced documentation for param type: ${matches[1]}`,\n          );\n        }\n      }\n\n      if (!description) {\n        throw new Error(`Expected a description for param: ${tag.name}`);\n      }\n\n      params.push({\n        name: tag.name,\n        description,\n        modes,\n      });\n    }\n  });\n\n  let returns = comment.tags.find((t) => t.type === \"returns\")?.string;\n\n  let simplifiedComment: SimplifiedComment = {\n    category,\n    name,\n    codeLink,\n    modes,\n    summary,\n    example,\n    additionalExamples,\n    reference,\n    signature,\n    unstable,\n    params,\n    returns,\n  };\n\n  if (\n    !simplifiedComment.returns &&\n    !isComponentApi(simplifiedComment) &&\n    !isClassApi(simplifiedComment)\n  ) {\n    throw new Error(`Expected a @returns tag for API: ${name}`);\n  }\n\n  return simplifiedComment;\n}\n\nfunction isParamTag(tag: Tag): tag is ParamTag {\n  return tag.type === \"param\";\n}\n\n// Parse the TypeScript code into an AST so we can remove the function body\n// and just grab the signature\nasync function getSignature(code: string) {\n  const ast = ts.createSourceFile(\"example.ts\", code, ts.ScriptTarget.Latest);\n  if (ast.statements.length === 0) {\n    throw new Error(`Expected one or more statements: ${code}`);\n  }\n\n  if (ts.isFunctionDeclaration(ast.statements[0])) {\n    let functionDeclaration = ast.statements[0];\n\n    let modifiedFunction = {\n      ...functionDeclaration,\n      modifiers: functionDeclaration.modifiers?.filter(\n        (m) => m.kind !== ts.SyntaxKind.ExportKeyword,\n      ),\n      body: ts.factory.createBlock([], false),\n    } as ts.FunctionDeclaration;\n\n    let newCode = ts\n      .createPrinter({ newLine: ts.NewLineKind.LineFeed })\n      .printNode(ts.EmitHint.Unspecified, modifiedFunction, ast);\n\n    let formatted = await prettier.format(newCode, { parser: \"typescript\" });\n\n    return formatted.replace(\"{}\", \"\").trim();\n  }\n\n  // TODO: Handle variable statements for forwardRef components\n  if (ts.isVariableStatement(ast.statements[0])) {\n    let api = code.match(/export const (\\w+)/);\n    warn(\n      `Skipping signature section for \\`export const\\` component: ${api?.[1]}`,\n    );\n    return;\n  }\n\n  if (ts.isClassDeclaration(ast.statements[0])) {\n    let api = code.match(/export class (\\w+)/);\n    warn(`Skipping signature section for \\`class\\` : ${api?.[1]}`);\n    return;\n  }\n\n  throw new Error(\"Unable to parse signature from code: \" + code);\n}\n\n/**\n * Resolves {@link ...} tags in JSDoc text and converts them to markdown links\n * @param text - The text containing {@link ...} tags\n * @returns Text with {@link ...} tags replaced by markdown links\n */\nfunction resolveLinkTags(text: string): string {\n  // Match {@link ApiName} as well as {@link ApiName | description}\n  const linkPattern = /\\{@link\\s+([^}]+)\\}/g;\n\n  return text.replace(linkPattern, (match, linkContent) => {\n    // Split on the pipe in case a different link text is specified after.\n    // This is not standard JSDoc syntax but instead something typedoc picks up\n    // from TSDoc. See:\n    //  - https://jsdoc.app/tags-inline-link\n    //  - https://typedoc.org/documents/Tags.__link_.html\n    const parts = linkContent.split(\"|\").map((p) => p.trim());\n    const apiName = parts[0];\n    const description = parts[1] || `\\`${apiName}\\``;\n\n    // Find a proper href for the @link\n    let href =\n      // Prefer exact docs for unstable APIs if they exist (they usually shouldn't)\n      repoApiLookup.get(apiName) ||\n      // But normally we don't include the `unstable_` prefix in the filename/URL\n      repoApiLookup.get(apiName.replace(/^unstable_/, \"\")) ||\n      // Fall through to typedocs if a repo doc doesn't exist\n      typedocLookup.get(apiName)?.href;\n\n    if (!href) {\n      // If not found, return as plain text with a warning\n      warn(`Could not resolve {@link ${apiName}} in documentation (${text})`);\n      return description;\n    }\n\n    href = /^http/.test(href) ? href : `../${href}`;\n    return `[${description}](${href})`;\n  });\n}\n"
  },
  {
    "path": "scripts/find-release-from-changeset.js",
    "content": "/**\n *\n * @param {string | undefined} publishedPackages\n * @param {string | undefined} packageVersionToFollow\n * @returns {string | undefined}\n */\nfunction findReleaseFromChangeset(publishedPackages, packageVersionToFollow) {\n  if (!publishedPackages) {\n    throw new Error(\"No published packages found\");\n  }\n\n  let packages = JSON.parse(publishedPackages);\n\n  if (!Array.isArray(packages)) {\n    throw new Error(\"Published packages is not an array\");\n  }\n\n  /** @see https://github.com/changesets/action#outputs */\n  /** @type { { name: string; version: string }[] }  */\n  let typed = packages.filter((pkg) => \"name\" in pkg && \"version\" in pkg);\n\n  let found = typed.find((pkg) => pkg.name === packageVersionToFollow);\n\n  if (!found) {\n    throw new Error(\n      `${packageVersionToFollow} was not found in the published packages`,\n    );\n  }\n\n  console.log(found.version);\n  return found.version;\n}\n\nfindReleaseFromChangeset(\n  process.env.PUBLISHED_PACKAGES,\n  process.env.PACKAGE_VERSION_TO_FOLLOW,\n);\n"
  },
  {
    "path": "scripts/finish-stable-release.sh",
    "content": "#!/bin/bash\n\nset -x\nset -e\n\nCURRENT_BRANCH=$(git rev-parse --abbrev-ref HEAD)\nif [[ \"${CURRENT_BRANCH}\" != \"release-next\" ]]; then\n  echo \"Error: Script must be run from the 'release-next' branch.\"\n  exit 1\nfi\n\nif [[ -n $(git status --porcelain) ]]; then\n  echo \"Error: Your git working directory is not clean. Please commit or stash your changes.\"\n  exit 1\nfi\n\ngit pull\ngit checkout main\ngit pull\ngit merge release-next --no-edit\n\nif [[ -n $(git status --porcelain) ]]; then\n  echo \"Error: Your git working directory is not clean after the merge to main.\"\n  exit 1\nfi\n\ngit push\n\ngit checkout dev\ngit pull\ngit merge release-next --no-edit\n\nif [[ -n $(git status --porcelain) ]]; then\n  echo \"Error: Your git working directory is not clean after the merge to dev.\"\n  exit 1\nfi\n\ngit push\n\ngit branch -d release-next\n\nif [[ -n $(git show-ref refs/heads/changeset-release/release-next) ]]; then\n  git branch -D changeset-release/release-next\nfi\n\n./scripts/delete-pre-tags.sh\n\nset +e\nset +x"
  },
  {
    "path": "scripts/playground.js",
    "content": "#!/usr/bin/env node\n\nlet { existsSync, readdirSync } = require(\"node:fs\");\nlet { cp } = require(\"node:fs/promises\");\nlet path = require(\"node:path\");\nlet prompts = require(\"prompts\");\nlet chalk = require(\"chalk\");\n\ncopyPlayground();\n\nasync function copyPlayground() {\n  let playgroundRootDir = path.join(__dirname, \"../playground\");\n\n  let { templateName, playgroundName } = await prompts([\n    {\n      type: \"select\",\n      name: \"templateName\",\n      message: \"Select a playground to copy\",\n      choices: readdirSync(playgroundRootDir).map((value) => ({ value })),\n    },\n    {\n      type: \"text\",\n      name: \"playgroundName\",\n      message: \"Enter a name for your playground\",\n      initial: (templateName) => `${templateName}-${Date.now()}`,\n    },\n  ]);\n\n  let srcDir = path.join(playgroundRootDir, templateName);\n  let destDir = path.join(__dirname, \"../playground-local\", playgroundName);\n  let relativeDestDir = destDir.replace(process.cwd(), \".\");\n\n  if (existsSync(destDir)) {\n    throw new Error(\n      `A local playground with the name \"${playgroundName}\" already exists. Delete it first or use a different name.`,\n    );\n  }\n  await cp(srcDir, destDir, { recursive: true });\n\n  console.log(\n    [\n      \"\",\n      chalk.green`Created local copy of \"${templateName}\"`,\n      chalk.green`To start playground, run:`,\n      \"\",\n      `cd ${relativeDestDir}`,\n      \"pnpm dev\",\n    ].join(\"\\n\"),\n  );\n}\n"
  },
  {
    "path": "scripts/publish.js",
    "content": "const path = require(\"path\");\nconst { execSync } = require(\"child_process\");\n\nconst jsonfile = require(\"jsonfile\");\nconst semver = require(\"semver\");\n\nconst rootDir = path.resolve(__dirname, \"..\");\n\n/**\n * @param {*} cond\n * @param {string} message\n * @returns {asserts cond}\n */\nfunction invariant(cond, message) {\n  if (!cond) throw new Error(message);\n}\n\n/**\n * @returns {string}\n */\nfunction getTaggedVersion() {\n  let output = execSync(\"git tag --list --points-at HEAD\").toString();\n  return output.replace(/^v|\\n+$/g, \"\");\n}\n\n/**\n * @param {string} packageName\n * @param {string|number} version\n */\nasync function ensureBuildVersion(packageName, version) {\n  let file = path.join(rootDir, \"packages\", packageName, \"package.json\");\n  let json = await jsonfile.readFile(file);\n  invariant(\n    json.version === version,\n    `Package ${packageName} is on version ${json.version}, but should be on ${version}`,\n  );\n}\n\n/**\n * @param {string} packageName\n * @param {string} tag\n */\nfunction publishBuild(packageName, tag, releaseBranch) {\n  let buildDir = path.join(rootDir, \"packages\", packageName);\n\n  let args = [\"--access public\"];\n  if (tag) {\n    args.push(`--tag ${tag}`);\n  }\n\n  if (tag === \"experimental\" || tag === \"nightly\") {\n    args.push(\"--no-git-checks\");\n  } else if (releaseBranch) {\n    args.push(`--publish-branch ${releaseBranch}`);\n  } else {\n    throw new Error(\n      \"Expected a release branch name to be provided for non-experimental/nightly releases\",\n    );\n  }\n  console.log();\n  console.log(`  pnpm publish ${buildDir} --tag ${tag} --access public`);\n  console.log();\n  execSync(`pnpm publish ${buildDir} ${args.join(\" \")}`, {\n    stdio: \"inherit\",\n  });\n}\n\n/**\n * @returns {Promise<1 | 0>}\n */\nasync function run() {\n  try {\n    // 0. Ensure we are in CI. We don't do this manually\n    invariant(\n      process.env.CI,\n      `You should always run the publish script from the CI environment!`,\n    );\n\n    // 1. Get the current tag, which has the release version number\n    let version = getTaggedVersion();\n    invariant(\n      version !== \"\",\n      \"Missing release version. Run the version script first.\",\n    );\n\n    // 2. Determine the appropriate npm tag to use\n    let releaseBranch;\n    let tag;\n    if (version.includes(\"experimental\")) {\n      tag = \"experimental\";\n    } else if (version.includes(\"nightly\")) {\n      tag = \"nightly\";\n    } else if (version.startsWith(\"6.\")) {\n      // !!! Note: publish.js is not used for prereleases and stable releases.\n      // We should be using the Changesets CI process for those.\n      // These code paths are only left here for emergency usages\n      releaseBranch = \"release-v6\";\n      tag = null;\n    } else if (version.startsWith(\"7.\")) {\n      // !!! Note: publish.js is not used for prereleases and stable releases.\n      // We should be using the Changesets CI process for those.\n      // These code paths are only left here for emergency usages\n      releaseBranch = \"release-next\";\n      tag = semver.prerelease(version) == null ? \"latest\" : \"pre\";\n    }\n\n    console.log();\n    console.log(`  Publishing version ${version} to npm with tag \"${tag}\"`);\n\n    // 3. Ensure build versions match the release version\n    await ensureBuildVersion(\"react-router\", version);\n    await ensureBuildVersion(\"react-router-dom\", version);\n    await ensureBuildVersion(\"react-router-dev\", version);\n    await ensureBuildVersion(\"react-router-express\", version);\n    await ensureBuildVersion(\"react-router-node\", version);\n    await ensureBuildVersion(\"react-router-serve\", version);\n    await ensureBuildVersion(\"react-router-architect\", version);\n    await ensureBuildVersion(\"react-router-cloudflare\", version);\n    await ensureBuildVersion(\"react-router-fs-routes\", version);\n    await ensureBuildVersion(\n      \"react-router-remix-routes-option-adapter\",\n      version,\n    );\n    await ensureBuildVersion(\"create-react-router\", version);\n\n    // 4. Publish to npm\n    publishBuild(\"react-router\", tag, releaseBranch);\n    publishBuild(\"react-router-dom\", tag, releaseBranch);\n    publishBuild(\"react-router-dev\", tag, releaseBranch);\n    publishBuild(\"react-router-express\", tag, releaseBranch);\n    publishBuild(\"react-router-node\", tag, releaseBranch);\n    publishBuild(\"react-router-serve\", tag, releaseBranch);\n    publishBuild(\"react-router-architect\", tag, releaseBranch);\n    publishBuild(\"react-router-cloudflare\", tag, releaseBranch);\n    publishBuild(\"react-router-fs-routes\", tag, releaseBranch);\n    publishBuild(\n      \"react-router-remix-routes-option-adapter\",\n      tag,\n      releaseBranch,\n    );\n    publishBuild(\"create-react-router\", tag, releaseBranch);\n  } catch (error) {\n    console.log();\n    console.error(`  ${error.message}`);\n    console.log();\n    return 1;\n  }\n\n  return 0;\n}\n\nrun().then((code) => {\n  process.exit(code);\n});\n"
  },
  {
    "path": "scripts/remove-prerelease-changelogs.mjs",
    "content": "import * as fs from \"node:fs\";\nimport path from \"node:path\";\nimport * as url from \"node:url\";\nimport { getPackagesSync } from \"@manypkg/get-packages\";\nimport remarkParse from \"remark-parse\";\nimport remarkGfm from \"remark-gfm\";\nimport rehypeStringify from \"remark-stringify\";\nimport { unified } from \"unified\";\nimport { remove } from \"unist-util-remove\";\n\nconst __dirname = url.fileURLToPath(new URL(\".\", import.meta.url));\nconst rootDir = path.join(__dirname, \"..\");\n\nmain();\n\nasync function main() {\n  if (isPrereleaseMode()) {\n    console.log(\"🚫 Skipping changelog removal in prerelease mode\");\n    return;\n  }\n  await removePreReleaseChangelogs();\n  console.log(\"✅ Removed pre-release changelogs\");\n}\n\nasync function removePreReleaseChangelogs() {\n  let allPackages = getPackagesSync(rootDir).packages;\n\n  /** @type {Promise<any>[]} */\n  let processes = [];\n  for (let pkg of allPackages) {\n    let changelogPath = path.join(pkg.dir, \"CHANGELOG.md\");\n    if (!fs.existsSync(changelogPath)) {\n      continue;\n    }\n    let changelogFileContents = fs.readFileSync(changelogPath, \"utf-8\");\n    processes.push(\n      (async () => {\n        let file = await unified()\n          // Since we have multiple versions of remark-parse, TS resolves to the\n          // wrong one\n          // @ts-expect-error\n          .use(remarkParse)\n          .use(remarkGfm)\n          .use(removePreReleaseSectionFromMarkdown)\n          // same problem\n          // @ts-expect-error\n          .use(rehypeStringify, {\n            bullet: \"-\",\n            emphasis: \"_\",\n            listItemIndent: \"one\",\n          })\n          .process(changelogFileContents);\n\n        let fileContents = file.toString();\n        await fs.promises.writeFile(changelogPath, fileContents, \"utf-8\");\n      })(),\n    );\n  }\n  return Promise.all(processes);\n}\n\nfunction removePreReleaseSectionFromMarkdown() {\n  /**\n   * @param {import('./unist').RootNode} tree\n   * @returns {Promise<void>}\n   */\n  async function transformer(tree) {\n    remove(\n      tree,\n      /**\n       * @param {import(\"./unist\").Node & { __REMOVE__?: boolean }} node\n       * @param {number | null | undefined} index\n       * @param {*} parent\n       */\n      (node, index, parent) => {\n        if (node.__REMOVE__ === true) return true;\n        if (\n          node.type === \"heading\" &&\n          node.depth === 2 &&\n          node.children[0].type === \"text\" &&\n          isPrereleaseVersion(node.children[0].value)\n        ) {\n          if (index == null || parent == null) return false;\n\n          let nextIdx = 1;\n          let nextNode = parent.children[index + 1];\n          let found = false;\n\n          /** @type {import('./unist').FlowNode[]} */\n          while (nextNode && !found) {\n            if (nextNode.type === \"heading\" && nextNode.depth === 2) {\n              found = true;\n              break;\n            }\n            nextNode.__REMOVE__ = true;\n            nextNode = parent.children[++nextIdx + index];\n          }\n          return true;\n        }\n\n        return false;\n      },\n    );\n  }\n  return transformer;\n}\n\n/**\n * @param {string} str\n * @returns\n */\nfunction isPrereleaseVersion(str) {\n  return /^(v?\\d+\\.){2}\\d+-[a-z]+\\.\\d+$/i.test(str.trim());\n}\n\nfunction isPrereleaseMode() {\n  try {\n    let prereleaseFilePath = path.join(rootDir, \".changeset\", \"pre.json\");\n    return fs.existsSync(prereleaseFilePath);\n  } catch (err) {\n    return false;\n  }\n}\n"
  },
  {
    "path": "scripts/start-prerelease.sh",
    "content": "#!/bin/bash\n\nset -x\nset -e\n\nif [[ -n $(git status --porcelain) ]]; then\n  echo \"Error: Your git working directory is not clean. Please commit or stash your changes.\"\n  exit 1\nfi\n\ngit checkout main\ngit pull\ngit checkout dev\ngit pull\ngit checkout -b release-next\ngit merge main --no-edit\n\nif [[ -n $(git status --porcelain) ]]; then\n  echo \"Error: Your git working directory is not clean after merging main ito release-next.\"\n  exit 1\nfi\n\npnpm changeset pre enter pre\ngit add .changeset/pre.json\ngit commit -m \"Enter prerelease mode\"\ngit push --set-upstream origin release-next\n\nset +e\nset +x"
  },
  {
    "path": "scripts/utils.js",
    "content": "const fsp = require(\"fs\").promises;\nconst path = require(\"path\");\nconst { execSync } = require(\"child_process\");\nconst jsonfile = require(\"jsonfile\");\n\nconst { ROOT_DIR, EXAMPLES_DIR } = require(\"./constants\");\n\n/**\n * @param {string} packageName\n * @param {string} [directory]\n * @returns {string}\n */\nfunction packageJson(packageName, directory) {\n  return path.join(ROOT_DIR, directory, packageName, \"package.json\");\n}\n\n/**\n * @param {string} packageName\n * @returns {Promise<string | undefined>}\n */\nasync function getPackageVersion(packageName) {\n  let file = packageJson(packageName, \"packages\");\n  let json = await jsonfile.readFile(file);\n  return json.version;\n}\n\n/**\n * @returns {void}\n */\nfunction ensureCleanWorkingDirectory() {\n  let status = execSync(`git status --porcelain`).toString().trim();\n  let lines = status.split(\"\\n\");\n  invariant(\n    lines.every((line) => line === \"\" || line.startsWith(\"?\")),\n    \"Working directory is not clean. Please commit or stash your changes.\",\n  );\n}\n\n/**\n * @param {string} packageName\n * @param {(json: import('type-fest').PackageJson) => any} transform\n */\nasync function updatePackageConfig(packageName, transform) {\n  let file = packageJson(packageName, \"packages\");\n  let json = await jsonfile.readFile(file);\n  transform(json);\n  await jsonfile.writeFile(file, json, { spaces: 2 });\n}\n\n/**\n * @param {string} example\n * @param {(json: import('type-fest').PackageJson) => any} transform\n */\nasync function updateExamplesPackageConfig(example, transform) {\n  let file = path.join(EXAMPLES_DIR, example, \"package.json\");\n  if (!(await fileExists(file))) return;\n\n  let json = await jsonfile.readFile(file);\n  transform(json);\n  await jsonfile.writeFile(file, json, { spaces: 2 });\n}\n\n/**\n * @param {string} filePath\n * @returns {Promise<boolean>}\n */\nasync function fileExists(filePath) {\n  try {\n    await fsp.stat(filePath);\n    return true;\n  } catch (_) {\n    return false;\n  }\n}\n\n/**\n * @param {*} cond\n * @param {string} message\n * @returns {asserts cond}\n */\nfunction invariant(cond, message) {\n  if (!cond) throw new Error(message);\n}\n\nmodule.exports = {\n  fileExists,\n  packageJson,\n  getPackageVersion,\n  ensureCleanWorkingDirectory,\n  invariant,\n  updatePackageConfig,\n  updateExamplesPackageConfig,\n};\n"
  },
  {
    "path": "scripts/version.js",
    "content": "const fs = require(\"node:fs\");\nconst { execSync } = require(\"child_process\");\nconst chalk = require(\"chalk\");\nconst semver = require(\"semver\");\n\nconst {\n  ensureCleanWorkingDirectory,\n  invariant,\n  updatePackageConfig,\n} = require(\"./utils\");\n\nasync function run() {\n  try {\n    let args = process.argv.slice(2);\n    let skipGit = args.includes(\"--skip-git\");\n\n    let givenVersion = args[0];\n    invariant(\n      givenVersion != null,\n      `Missing next version. Usage: node version.js [nextVersion]`,\n    );\n\n    // 0. Make sure the working directory is clean\n    if (!skipGit) {\n      ensureCleanWorkingDirectory();\n    }\n\n    // 1. Get the next version number\n    let version = semver.valid(givenVersion);\n    invariant(version != null, `Invalid version specifier: ${givenVersion}`);\n\n    // 2. Bump package versions\n    let packageDirNamesToBump = fs\n      .readdirSync(\"packages\")\n      .filter((name) => fs.statSync(`packages/${name}`).isDirectory());\n\n    for (let packageDirName of packageDirNamesToBump) {\n      let packageName;\n      await updatePackageConfig(packageDirName, (pkg) => {\n        packageName = pkg.name;\n        pkg.version = version;\n      });\n      console.log(\n        chalk.green(`  Updated ${packageName} to version ${version}`),\n      );\n    }\n\n    // 3. Commit and tag\n    if (!skipGit) {\n      execSync(`git commit --all --message=\"Version ${version}\"`);\n      execSync(`git tag -a -m \"Version ${version}\" v${version}`);\n      console.log(chalk.green(`  Committed and tagged version ${version}`));\n    }\n  } catch (error) {\n    console.log();\n    console.error(chalk.red(`  ${error.message}`));\n    console.log();\n    return 1;\n  }\n\n  return 0;\n}\n\nrun().then((code) => {\n  process.exit(code);\n});\n"
  },
  {
    "path": "tutorials/address-book/.gitignore",
    "content": ".DS_Store\n/node_modules/\n\n# React Router\n/.react-router/\n/build/\n"
  },
  {
    "path": "tutorials/address-book/README.md",
    "content": "# Welcome to React Router!\n\n- [React Router Docs](https://reactrouter.com/home)\n\n## Development\n\nFrom your terminal:\n\n```sh\nnpm run dev\n```\n\nThis starts your app in development mode, rebuilding assets on file changes.\n\n## Deployment\n\nFirst, build your app for production:\n\n```sh\nnpm run build\n```\n\nThen run the app in production mode:\n\n```sh\nnpm start\n```\n\nNow you'll need to pick a host to deploy it to.\n\n### DIY\n\nIf you're familiar with deploying node applications, the built-in React Router app server is production-ready.\n\nMake sure to deploy the output of `react-router build`\n\n- `build/server`\n- `build/client`\n"
  },
  {
    "path": "tutorials/address-book/app/app.css",
    "content": "html {\n  box-sizing: border-box;\n}\n*,\n*:before,\n*:after {\n  box-sizing: inherit;\n}\n\nbody {\n  font-family:\n    -apple-system, BlinkMacSystemFont, \"Segoe UI\", \"Roboto\", \"Oxygen\", \"Ubuntu\",\n    \"Cantarell\", \"Fira Sans\", \"Droid Sans\", \"Helvetica Neue\", sans-serif;\n  -webkit-font-smoothing: antialiased;\n  -moz-osx-font-smoothing: grayscale;\n}\n\ncode {\n  font-family:\n    source-code-pro, Menlo, Monaco, Consolas, \"Courier New\", monospace;\n}\n\nhtml,\nbody {\n  height: 100%;\n  margin: 0;\n  line-height: 1.5;\n  color: #121212;\n}\ntextarea,\ninput,\nbutton {\n  font-size: 1rem;\n  font-family: inherit;\n  border: none;\n  border-radius: 8px;\n  padding: 0.5rem 0.75rem;\n  box-shadow:\n    0 0px 1px hsla(0, 0%, 0%, 0.2),\n    0 1px 2px hsla(0, 0%, 0%, 0.2);\n  background-color: white;\n  line-height: 1.5;\n  margin: 0;\n}\nbutton {\n  color: #3992ff;\n  font-weight: 500;\n}\n\ntextarea:hover,\ninput:hover,\nbutton:hover {\n  box-shadow:\n    0 0px 1px hsla(0, 0%, 0%, 0.6),\n    0 1px 2px hsla(0, 0%, 0%, 0.2);\n}\n\nbutton:active {\n  box-shadow: 0 0px 1px hsla(0, 0%, 0%, 0.4);\n  transform: translateY(1px);\n}\n\n#contact h1 {\n  display: flex;\n  align-items: flex-start;\n  gap: 1rem;\n}\n#contact h1 form {\n  display: flex;\n  align-items: center;\n  margin-top: 0.25rem;\n}\n#contact h1 form button {\n  box-shadow: none;\n  font-size: 1.5rem;\n  font-weight: 400;\n  padding: 0;\n}\n#contact h1 form button[value=\"true\"] {\n  color: #a4a4a4;\n}\n#contact h1 form button[value=\"true\"]:hover,\n#contact h1 form button[value=\"false\"] {\n  color: #eeb004;\n}\n\nform[action$=\"destroy\"] button {\n  color: #f44250;\n}\n\n.sr-only {\n  position: absolute;\n  width: 1px;\n  height: 1px;\n  padding: 0;\n  margin: -1px;\n  overflow: hidden;\n  clip: rect(0, 0, 0, 0);\n  white-space: nowrap;\n  border-width: 0;\n}\n\nbody {\n  display: flex;\n  height: 100%;\n  width: 100%;\n}\n\n#sidebar {\n  width: 22rem;\n  background-color: #f7f7f7;\n  border-right: solid 1px #e3e3e3;\n  display: flex;\n  flex-direction: column;\n}\n\n#sidebar > * {\n  padding-left: 2rem;\n  padding-right: 2rem;\n}\n\n#sidebar h1 {\n  font-size: 1rem;\n  font-weight: 500;\n  display: flex;\n  align-items: center;\n  margin: 0;\n  padding: 1rem 2rem;\n  border-top: 1px solid #e3e3e3;\n  order: 1;\n  line-height: 1;\n}\n\n#sidebar h1 a {\n  color: #3992ff;\n  text-decoration: none;\n}\n\n#sidebar h1 a:hover {\n  text-decoration: underline;\n}\n\n#sidebar h1::before {\n  content: url(\"data:image/svg+xml,%3Csvg width='35' height='21' viewBox='0 0 602 360' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M481.36 180C481.36 196.572 474.639 211.572 463.757 222.42C452.875 233.28 437.845 240 421.24 240C404.635 240 389.605 246.708 378.735 257.568C367.853 268.428 361.12 283.428 361.12 300C361.12 316.572 354.399 331.572 343.517 342.42C332.635 353.28 317.605 360 301 360C284.395 360 269.365 353.28 258.495 342.42C247.613 331.572 240.88 316.572 240.88 300C240.88 283.428 247.613 268.428 258.495 257.568C269.365 246.708 284.395 240 301 240C317.605 240 332.635 233.28 343.517 222.42C354.399 211.572 361.12 196.572 361.12 180C361.12 146.856 334.21 120 301 120C284.395 120 269.365 113.28 258.495 102.42C247.613 91.572 240.88 76.572 240.88 60C240.88 43.428 247.613 28.428 258.495 17.568C269.365 6.708 284.395 0 301 0C334.21 0 361.12 26.856 361.12 60C361.12 76.572 367.853 91.572 378.735 102.42C389.605 113.28 404.635 120 421.24 120C454.45 120 481.36 146.856 481.36 180Z' fill='%23F44250'/%3E%3Cpath d='M240.88 180C240.88 146.862 213.964 120 180.76 120C147.557 120 120.64 146.862 120.64 180C120.64 213.137 147.557 240 180.76 240C213.964 240 240.88 213.137 240.88 180Z' fill='%23121212'/%3E%3Cpath d='M120.64 300C120.64 266.863 93.7234 240 60.52 240C27.3167 240 0.400024 266.863 0.400024 300C0.400024 333.138 27.3167 360 60.52 360C93.7234 360 120.64 333.138 120.64 300Z' fill='%23121212'/%3E%3Cpath d='M601.6 300C601.6 266.863 574.683 240 541.48 240C508.277 240 481.36 266.863 481.36 300C481.36 333.138 508.277 360 541.48 360C574.683 360 601.6 333.138 601.6 300Z' fill='%23121212'/%3E%3C/svg%3E\");\n  margin-right: 1rem;\n  position: relative;\n  top: 1px;\n}\n\n#sidebar > div {\n  display: flex;\n  align-items: center;\n  gap: 0.5rem;\n  padding-top: 1rem;\n  padding-bottom: 1rem;\n  border-bottom: 1px solid #e3e3e3;\n}\n\n#sidebar > div form {\n  position: relative;\n}\n\n#sidebar > div form input[type=\"search\"] {\n  box-sizing: border-box;\n  width: 100%;\n  padding-left: 2rem;\n  background-image: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' class='h-6 w-6' fill='none' viewBox='0 0 24 24' stroke='%23999' stroke-width='2'%3E%3Cpath stroke-linecap='round' stroke-linejoin='round' d='M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z' /%3E%3C/svg%3E\");\n  background-repeat: no-repeat;\n  background-position: 0.625rem 0.75rem;\n  background-size: 1rem;\n  position: relative;\n}\n\n#sidebar > div form input[type=\"search\"].loading {\n  background-image: none;\n}\n\n#search-spinner {\n  width: 1rem;\n  height: 1rem;\n  background-image: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 24 24'%3E%3Cpath stroke='%23000' strokeLinecap='round' strokeLinejoin='round' strokeWidth='2' d='M20 4v5h-.582m0 0a8.001 8.001 0 00-15.356 2m15.356-2H15M4 20v-5h.581m0 0a8.003 8.003 0 0015.357-2M4.581 15H9' /%3E%3C/svg%3E\");\n  animation: spin 1s infinite linear;\n  position: absolute;\n  left: 0.625rem;\n  top: 0.75rem;\n}\n\n@keyframes spin {\n  from {\n    transform: rotate(0deg);\n  }\n  to {\n    transform: rotate(360deg);\n  }\n}\n\n#sidebar nav {\n  flex: 1;\n  overflow: auto;\n  padding-top: 1rem;\n}\n\n#sidebar nav a span {\n  float: right;\n  color: #eeb004;\n}\n#sidebar nav a.active span {\n  color: inherit;\n}\n\ni {\n  color: #818181;\n}\n#sidebar nav .active i {\n  color: inherit;\n}\n\n#sidebar ul {\n  padding: 0;\n  margin: 0;\n  list-style: none;\n}\n\n#sidebar li {\n  margin: 0.25rem 0;\n}\n\n#sidebar nav a {\n  display: flex;\n  align-items: center;\n  justify-content: space-between;\n  overflow: hidden;\n\n  white-space: pre;\n  padding: 0.5rem;\n  border-radius: 8px;\n  color: inherit;\n  text-decoration: none;\n  gap: 1rem;\n  transition: background-color 100ms;\n}\n\n#sidebar nav a:hover {\n  background: #e3e3e3;\n}\n\n#sidebar nav a.active {\n  background: hsl(224, 98%, 58%);\n  color: white;\n}\n\n#sidebar nav a.pending {\n  animation: progress 2s infinite ease-in-out;\n  animation-delay: 200ms;\n}\n\n@keyframes progress {\n  0% {\n    background: #e3e3e3;\n  }\n  50% {\n    background: hsla(224, 98%, 58%, 0.5);\n  }\n  100% {\n    background: #e3e3e3;\n  }\n}\n\n#detail {\n  flex: 1;\n  padding: 2rem 4rem;\n  width: 100%;\n}\n\n#detail.loading {\n  opacity: 0.25;\n  transition: opacity 200ms;\n  transition-delay: 200ms;\n}\n\n#contact {\n  max-width: 40rem;\n  display: flex;\n}\n\n#contact h1 {\n  font-size: 2rem;\n  font-weight: 700;\n  margin: 0;\n  line-height: 1.2;\n}\n\n#contact h1 + p {\n  margin: 0;\n}\n\n#contact h1 + p + p {\n  white-space: break-spaces;\n}\n\n#contact h1:focus {\n  outline: none;\n  color: hsl(224, 98%, 58%);\n}\n\n#contact a[href*=\"twitter\"] {\n  display: flex;\n  font-size: 1.5rem;\n  color: #3992ff;\n  text-decoration: none;\n}\n#contact a[href*=\"twitter\"]:hover {\n  text-decoration: underline;\n}\n\n#contact img {\n  width: 12rem;\n  height: 12rem;\n  background: #c8c8c8;\n  margin-right: 2rem;\n  border-radius: 1.5rem;\n  object-fit: cover;\n}\n\n#contact h1 ~ div {\n  display: flex;\n  gap: 0.5rem;\n  margin: 1rem 0;\n}\n\n#contact-form {\n  display: flex;\n  max-width: 40rem;\n  flex-direction: column;\n  gap: 1rem;\n}\n#contact-form > p:first-child {\n  margin: 0;\n  padding: 0;\n}\n#contact-form > p:first-child > :nth-child(2) {\n  margin-right: 1rem;\n}\n#contact-form > p:first-child,\n#contact-form label {\n  display: flex;\n}\n#contact-form p:first-child span,\n#contact-form label span {\n  width: 8rem;\n}\n#contact-form p:first-child input,\n#contact-form label input,\n#contact-form label textarea {\n  flex-grow: 2;\n}\n\n#contact-form-avatar {\n  margin-right: 2rem;\n}\n\n#contact-form-avatar img {\n  width: 12rem;\n  height: 12rem;\n  background: hsla(0, 0%, 0%, 0.2);\n  border-radius: 1rem;\n}\n\n#contact-form-avatar input {\n  box-sizing: border-box;\n  width: 100%;\n}\n\n#contact-form p:last-child {\n  display: flex;\n  gap: 0.5rem;\n  margin: 0 0 0 8rem;\n}\n\n#contact-form p:last-child button[type=\"button\"] {\n  color: inherit;\n}\n\n#index-page {\n  margin: 2rem auto;\n  text-align: center;\n  color: #818181;\n}\n\n#index-page a {\n  color: inherit;\n}\n\n#index-page a:hover {\n  color: #121212;\n}\n\n#index-page:before {\n  display: block;\n  margin-bottom: 0.5rem;\n  /* width='364' height='97' viewBox='0 0 364 97' */\n  content: url(\"data:image/svg+xml,%3Csvg width='540' height='87' viewBox='0 0 1080 174' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M231.527 86.9999C231.527 94.9642 228.297 102.173 223.067 107.387C217.837 112.606 210.614 115.835 202.634 115.835C194.654 115.835 187.43 119.059 182.206 124.278C176.977 129.498 173.741 136.707 173.741 144.671C173.741 152.635 170.51 159.844 165.281 165.058C160.051 170.277 152.828 173.507 144.847 173.507C136.867 173.507 129.644 170.277 124.42 165.058C119.19 159.844 115.954 152.635 115.954 144.671C115.954 136.707 119.19 129.498 124.42 124.278C129.644 119.059 136.867 115.835 144.847 115.835C152.828 115.835 160.051 112.606 165.281 107.387C170.51 102.173 173.741 94.9642 173.741 86.9999C173.741 71.0711 160.808 58.1643 144.847 58.1643C136.867 58.1643 129.644 54.9347 124.42 49.7155C119.19 44.502 115.954 37.2931 115.954 29.3287C115.954 21.3643 119.19 14.1555 124.42 8.93622C129.644 3.71698 136.867 0.493164 144.847 0.493164C160.808 0.493164 173.741 13.4 173.741 29.3287C173.741 37.2931 176.977 44.502 182.206 49.7155C187.43 54.9347 194.654 58.1643 202.634 58.1643C218.594 58.1643 231.527 71.0711 231.527 86.9999Z' fill='%23F44250'/%3E%3Cpath d='M115.954 86.9996C115.954 71.0742 103.018 58.1641 87.0608 58.1641C71.1035 58.1641 58.1676 71.0742 58.1676 86.9996C58.1676 102.925 71.1035 115.835 87.0608 115.835C103.018 115.835 115.954 102.925 115.954 86.9996Z' fill='%23121212'/%3E%3Cpath d='M58.1676 144.671C58.1676 128.745 45.2316 115.835 29.2743 115.835C13.317 115.835 0.381104 128.745 0.381104 144.671C0.381104 160.596 13.317 173.506 29.2743 173.506C45.2316 173.506 58.1676 160.596 58.1676 144.671Z' fill='%23121212'/%3E%3Cpath d='M289.313 144.671C289.313 128.745 276.378 115.835 260.42 115.835C244.463 115.835 231.527 128.745 231.527 144.671C231.527 160.596 244.463 173.506 260.42 173.506C276.378 173.506 289.313 160.596 289.313 144.671Z' fill='%23121212'/%3E%3Cg clip-path='url(%23clip0_171_1761)'%3E%3Cpath d='M562.482 173.247C524.388 173.247 498.363 147.49 498.363 110.468C498.363 73.4455 524.388 47.6885 562.482 47.6885C600.576 47.6885 626.869 73.7135 626.869 110.468C626.869 147.222 600.576 173.247 562.482 173.247ZM562.482 144.007C579.386 144.007 587.703 130.319 587.703 110.468C587.703 90.6168 579.386 76.9289 562.482 76.9289C545.579 76.9289 537.529 90.6168 537.529 110.468C537.529 130.319 545.311 144.007 562.482 144.007Z' fill='%23121212'/%3E%3Cpath d='M833.64 141.116C824.217 141.116 819.237 136.684 819.237 126.156V74.8983H851.928V47.7792H819.237V1.15527H791.75L786.1 26.1978C783.343 36.4805 780.82 42.822 773.897 46.0821C773.105 46.4506 771.129 46.9976 769.409 47.3884C768.014 47.701 766.596 47.8573 765.167 47.8573H752.338V47.9243H734.832C723.578 47.9243 714.445 57.0459 714.445 68.3111V111.552C714.445 130.599 707.199 142.668 692.719 142.668C678.238 142.668 672.868 133.279 672.868 116.375V47.9243H634.249V125.765C634.249 151.254 644.442 173.248 676.63 173.248C691.915 173.248 703.895 167.231 711.096 157.182C712.145 155.72 714.445 156.49 714.445 158.276V170.022H753.332V83.8412C753.332 78.8953 757.34 74.8871 762.286 74.8871H779.882V136.952C779.882 164.663 797.89 173.248 817.842 173.248C833.908 173.248 844.436 169.374 853.58 162.441V136.126C846.1 139.453 839.725 141.116 833.629 141.116H833.64Z' fill='%23121212'/%3E%3Cpath d='M981.561 130.865C975.387 157.962 954.197 173.258 923.07 173.258C885.243 173.258 858.415 150.18 858.415 112.354C858.415 74.5281 885.779 47.6992 922.266 47.6992C961.699 47.6992 982.365 74.796 982.365 107.263V113.884H896.509C894.555 135.711 909.382 144.017 924.409 144.017C937.829 144.017 946.136 138.915 950.434 127.918L981.561 130.865ZM945.075 94.9372C944.271 83.1361 936.757 75.8567 921.998 75.8567C906.434 75.8567 899.188 82.321 897.045 94.9372H945.064H945.075Z' fill='%23121212'/%3E%3Cpath d='M1076.24 85.7486C1070.06 82.2652 1064.17 80.9142 1055.85 80.9142C1039.75 80.9142 1029.02 90.0358 1029.02 110.691V170.02H990.393V47.9225H1029.02V64.3235C1029.02 65.4623 1030.54 65.8195 1031.05 64.8035C1036.68 53.5718 1047.91 44.707 1062.03 44.707C1069.27 44.707 1075.45 46.8507 1078.66 49.5414L1076.25 85.7597L1076.24 85.7486Z' fill='%23121212'/%3E%3Cpath d='M547.321 31.5345V23.9983H522.457V31.5345H515.378V2.23828H542.14C553.562 2.23828 554.366 2.95282 554.366 13.1239C554.366 17.4111 553.472 18.5611 551.329 19.6553L549.408 20.6378L551.318 21.6426C553.595 22.8372 554.366 23.2391 554.366 30.0273V31.5345H547.332H547.321ZM522.457 18.3601H547.321V7.88763H522.457V18.349V18.3601Z' fill='%23121212'/%3E%3Cpath d='M578.493 2.23828H610.826V7.90996H580.067V14.5083H610.011V19.2868H580.067V25.8963H610.837V31.501L578.504 31.5345C575.344 31.5345 572.787 28.9778 572.787 25.8293V7.95462C572.787 4.80617 575.344 2.24945 578.493 2.24945V2.23828Z' fill='%23121212'/%3E%3Cpath d='M655.562 31.5345L653.151 26.3429H633.747L631.335 31.5345H624.58L637.007 4.75034C637.71 3.22078 639.262 2.23828 640.937 2.23828H645.927C647.613 2.23828 649.154 3.22078 649.857 4.75034L662.284 31.5345H655.529H655.562ZM643.46 8.06627C642.712 8.06627 642.053 8.49053 641.729 9.17158L635.968 21.5756H650.94L645.19 9.17158C644.878 8.49053 644.208 8.06627 643.46 8.06627Z' fill='%23121212'/%3E%3Cpath d='M694.862 32.4153C676.05 32.4153 675.313 32.4153 675.313 16.8852C675.313 1.35505 676.05 1.36621 694.862 1.36621C711.721 1.36621 713.764 2.06959 714.244 10.5325H707.333V7.01556H682.168V26.766H707.333V23.2714H714.244C713.775 31.7119 711.721 32.4153 694.862 32.4153Z' fill='%23121212'/%3E%3Cpath d='M745.282 31.5345V7.02795H729.16V2.23828H768.148V7.02795H752.026V31.5345H745.282Z' fill='%23121212'/%3E%3Cpath d='M454.419 169.819C450.935 165.264 448.792 154.814 447.452 137.397C446.112 118.104 437.806 113.817 422.532 113.817H392.254V169.83H347.494V0.986328H432.715C476.391 0.986328 498.106 21.6187 498.106 54.5882C498.106 79.2399 482.833 95.3171 462.201 98.0078C479.618 101.491 489.8 111.405 491.676 130.966C494.087 156.154 494.891 163.656 500.518 169.819H454.419ZM424.676 78.704C443.969 78.704 453.615 73.8808 453.615 58.3395C453.615 44.6739 443.969 37.4392 424.676 37.4392H392.254V78.7152H424.676V78.704Z' fill='%23121212'/%3E%3C/g%3E%3Cdefs%3E%3CclipPath id='clip0_171_1761'%3E%3Crect width='731.156' height='172.261' fill='white' transform='translate(347.494 0.986328)'/%3E%3C/clipPath%3E%3C/defs%3E%3C/svg%3E%0A\");\n}\n\n#error-page {\n  display: flex;\n  flex-direction: column;\n  align-items: center;\n  justify-content: center;\n  width: 100%;\n}\n\n#about {\n  flex: 1;\n  padding: 2rem 4rem;\n  width: 100%;\n}\n\n#loading-splash {\n  position: fixed;\n  top: 50%;\n  left: 50%;\n  transform: translate(-50%, -50%);\n  display: flex;\n  flex-direction: column;\n  align-items: center;\n}\n\n#loading-splash-spinner {\n  width: 3rem;\n  height: 3rem;\n  border: 0.5rem solid #e3e3e3;\n  border-top: 0.5rem solid #3992ff;\n  border-radius: 50%;\n  animation: spin 1s infinite linear;\n  margin-bottom: 1rem;\n}\n"
  },
  {
    "path": "tutorials/address-book/app/data.ts",
    "content": "////////////////////////////////////////////////////////////////////////////////\n// 🛑 Nothing in here has anything to do with React Router, it's just a fake database\n////////////////////////////////////////////////////////////////////////////////\n\nimport { matchSorter } from \"match-sorter\";\n// @ts-expect-error - no types, but it's a tiny function\nimport sortBy from \"sort-by\";\nimport invariant from \"tiny-invariant\";\n\ntype ContactMutation = {\n  id?: string;\n  first?: string;\n  last?: string;\n  avatar?: string;\n  twitter?: string;\n  notes?: string;\n  favorite?: boolean;\n};\n\nexport type ContactRecord = ContactMutation & {\n  id: string;\n  createdAt: string;\n};\n\n////////////////////////////////////////////////////////////////////////////////\n// This is just a fake DB table. In a real app you'd be talking to a real db or\n// fetching from an existing API.\nconst fakeContacts = {\n  records: {} as Record<string, ContactRecord>,\n\n  async getAll(): Promise<ContactRecord[]> {\n    return Object.keys(fakeContacts.records)\n      .map((key) => fakeContacts.records[key])\n      .sort(sortBy(\"-createdAt\", \"last\"));\n  },\n\n  async get(id: string): Promise<ContactRecord | null> {\n    return fakeContacts.records[id] || null;\n  },\n\n  async create(values: ContactMutation): Promise<ContactRecord> {\n    const id = values.id || Math.random().toString(36).substring(2, 9);\n    const createdAt = new Date().toISOString();\n    const newContact = { id, createdAt, ...values };\n    fakeContacts.records[id] = newContact;\n    return newContact;\n  },\n\n  async set(id: string, values: ContactMutation): Promise<ContactRecord> {\n    const contact = await fakeContacts.get(id);\n    invariant(contact, `No contact found for ${id}`);\n    const updatedContact = { ...contact, ...values };\n    fakeContacts.records[id] = updatedContact;\n    return updatedContact;\n  },\n\n  destroy(id: string): null {\n    delete fakeContacts.records[id];\n    return null;\n  },\n};\n\n////////////////////////////////////////////////////////////////////////////////\n// Handful of helper functions to be called from route loaders and actions\nexport async function getContacts(query?: string | null) {\n  await new Promise((resolve) => setTimeout(resolve, 500));\n  let contacts = await fakeContacts.getAll();\n  if (query) {\n    contacts = matchSorter(contacts, query, {\n      keys: [\"first\", \"last\"],\n    });\n  }\n  return contacts.sort(sortBy(\"last\", \"createdAt\"));\n}\n\nexport async function createEmptyContact() {\n  const contact = await fakeContacts.create({});\n  return contact;\n}\n\nexport async function getContact(id: string) {\n  return fakeContacts.get(id);\n}\n\nexport async function updateContact(id: string, updates: ContactMutation) {\n  const contact = await fakeContacts.get(id);\n  if (!contact) {\n    throw new Error(`No contact found for ${id}`);\n  }\n  await fakeContacts.set(id, { ...contact, ...updates });\n  return contact;\n}\n\nexport async function deleteContact(id: string) {\n  fakeContacts.destroy(id);\n}\n\n[\n  {\n    avatar:\n      \"https://sessionize.com/image/124e-400o400o2-wHVdAuNaxi8KJrgtN3ZKci.jpg\",\n    first: \"Shruti\",\n    last: \"Kapoor\",\n    twitter: \"@shrutikapoor08\",\n  },\n  {\n    avatar:\n      \"https://sessionize.com/image/1940-400o400o2-Enh9dnYmrLYhJSTTPSw3MH.jpg\",\n    first: \"Glenn\",\n    last: \"Reyes\",\n    twitter: \"@glnnrys\",\n  },\n  {\n    avatar:\n      \"https://sessionize.com/image/9273-400o400o2-3tyrUE3HjsCHJLU5aUJCja.jpg\",\n    first: \"Ryan\",\n    last: \"Florence\",\n  },\n  {\n    avatar:\n      \"https://sessionize.com/image/d14d-400o400o2-pyB229HyFPCnUcZhHf3kWS.png\",\n    first: \"Oscar\",\n    last: \"Newman\",\n    twitter: \"@__oscarnewman\",\n  },\n  {\n    avatar:\n      \"https://sessionize.com/image/fd45-400o400o2-fw91uCdGU9hFP334dnyVCr.jpg\",\n    first: \"Michael\",\n    last: \"Jackson\",\n  },\n  {\n    avatar:\n      \"https://sessionize.com/image/b07e-400o400o2-KgNRF3S9sD5ZR4UsG7hG4g.jpg\",\n    first: \"Christopher\",\n    last: \"Chedeau\",\n    twitter: \"@Vjeux\",\n  },\n  {\n    avatar:\n      \"https://sessionize.com/image/262f-400o400o2-UBPQueK3fayaCmsyUc1Ljf.jpg\",\n    first: \"Cameron\",\n    last: \"Matheson\",\n    twitter: \"@cmatheson\",\n  },\n  {\n    avatar:\n      \"https://sessionize.com/image/820b-400o400o2-Ja1KDrBAu5NzYTPLSC3GW8.jpg\",\n    first: \"Brooks\",\n    last: \"Lybrand\",\n    twitter: \"@BrooksLybrand\",\n  },\n  {\n    avatar:\n      \"https://sessionize.com/image/df38-400o400o2-JwbChVUj6V7DwZMc9vJEHc.jpg\",\n    first: \"Alex\",\n    last: \"Anderson\",\n    twitter: \"@ralex1993\",\n  },\n  {\n    avatar:\n      \"https://sessionize.com/image/5578-400o400o2-BMT43t5kd2U1XstaNnM6Ax.jpg\",\n    first: \"Kent C.\",\n    last: \"Dodds\",\n    twitter: \"@kentcdodds\",\n  },\n  {\n    avatar:\n      \"https://sessionize.com/image/c9d5-400o400o2-Sri5qnQmscaJXVB8m3VBgf.jpg\",\n    first: \"Nevi\",\n    last: \"Shah\",\n    twitter: \"@nevikashah\",\n  },\n  {\n    avatar:\n      \"https://sessionize.com/image/2694-400o400o2-MYYTsnszbLKTzyqJV17w2q.png\",\n    first: \"Andrew\",\n    last: \"Petersen\",\n  },\n  {\n    avatar:\n      \"https://sessionize.com/image/907a-400o400o2-9TM2CCmvrw6ttmJiTw4Lz8.jpg\",\n    first: \"Scott\",\n    last: \"Smerchek\",\n    twitter: \"@smerchek\",\n  },\n  {\n    avatar:\n      \"https://sessionize.com/image/08be-400o400o2-WtYGFFR1ZUJHL9tKyVBNPV.jpg\",\n    first: \"Giovanni\",\n    last: \"Benussi\",\n    twitter: \"@giovannibenussi\",\n  },\n  {\n    avatar:\n      \"https://sessionize.com/image/f814-400o400o2-n2ua5nM9qwZA2hiGdr1T7N.jpg\",\n    first: \"Igor\",\n    last: \"Minar\",\n    twitter: \"@IgorMinar\",\n  },\n  {\n    avatar:\n      \"https://sessionize.com/image/fb82-400o400o2-LbvwhTVMrYLDdN3z4iEFMp.jpeg\",\n    first: \"Brandon\",\n    last: \"Kish\",\n  },\n  {\n    avatar:\n      \"https://sessionize.com/image/fcda-400o400o2-XiYRtKK5Dvng5AeyC8PiUA.png\",\n    first: \"Arisa\",\n    last: \"Fukuzaki\",\n    twitter: \"@arisa_dev\",\n  },\n  {\n    avatar:\n      \"https://sessionize.com/image/c8c3-400o400o2-PR5UsgApAVEADZRixV4H8e.jpeg\",\n    first: \"Alexandra\",\n    last: \"Spalato\",\n    twitter: \"@alexadark\",\n  },\n  {\n    avatar:\n      \"https://sessionize.com/image/7594-400o400o2-hWtdCjbdFdLgE2vEXBJtyo.jpg\",\n    first: \"Cat\",\n    last: \"Johnson\",\n  },\n  {\n    avatar:\n      \"https://sessionize.com/image/5636-400o400o2-TWgi8vELMFoB3hB9uPw62d.jpg\",\n    first: \"Ashley\",\n    last: \"Narcisse\",\n    twitter: \"@_darkfadr\",\n  },\n  {\n    avatar:\n      \"https://sessionize.com/image/6aeb-400o400o2-Q5tAiuzKGgzSje9ZsK3Yu5.JPG\",\n    first: \"Edmund\",\n    last: \"Hung\",\n    twitter: \"@_edmundhung\",\n  },\n  {\n    avatar:\n      \"https://sessionize.com/image/30f1-400o400o2-wJBdJ6sFayjKmJycYKoHSe.jpg\",\n    first: \"Clifford\",\n    last: \"Fajardo\",\n    twitter: \"@cliffordfajard0\",\n  },\n  {\n    avatar:\n      \"https://sessionize.com/image/6faa-400o400o2-amseBRDkdg7wSK5tjsFDiG.jpg\",\n    first: \"Erick\",\n    last: \"Tamayo\",\n    twitter: \"@ericktamayo\",\n  },\n  {\n    avatar:\n      \"https://sessionize.com/image/feba-400o400o2-R4GE7eqegJNFf3cQ567obs.jpg\",\n    first: \"Paul\",\n    last: \"Bratslavsky\",\n    twitter: \"@codingthirty\",\n  },\n  {\n    avatar:\n      \"https://sessionize.com/image/c315-400o400o2-spjM5A6VVfVNnQsuwvX3DY.jpg\",\n    first: \"Pedro\",\n    last: \"Cattori\",\n    twitter: \"@pcattori\",\n  },\n  {\n    avatar:\n      \"https://sessionize.com/image/eec1-400o400o2-HkvWKLFqecmFxLwqR9KMRw.jpg\",\n    first: \"Andre\",\n    last: \"Landgraf\",\n    twitter: \"@AndreLandgraf94\",\n  },\n  {\n    avatar:\n      \"https://sessionize.com/image/c73a-400o400o2-4MTaTq6ftC15hqwtqUJmTC.jpg\",\n    first: \"Monica\",\n    last: \"Powell\",\n    twitter: \"@indigitalcolor\",\n  },\n  {\n    avatar:\n      \"https://sessionize.com/image/cef7-400o400o2-KBZUydbjfkfGACQmjbHEvX.jpeg\",\n    first: \"Brian\",\n    last: \"Lee\",\n    twitter: \"@brian_dlee\",\n  },\n  {\n    avatar:\n      \"https://sessionize.com/image/f83b-400o400o2-Pyw3chmeHMxGsNoj3nQmWU.jpg\",\n    first: \"Sean\",\n    last: \"McQuaid\",\n    twitter: \"@SeanMcQuaidCode\",\n  },\n  {\n    avatar:\n      \"https://sessionize.com/image/a9fc-400o400o2-JHBnWZRoxp7QX74Hdac7AZ.jpg\",\n    first: \"Shane\",\n    last: \"Walker\",\n    twitter: \"@swalker326\",\n  },\n  {\n    avatar:\n      \"https://sessionize.com/image/6644-400o400o2-aHnGHb5Pdu3D32MbfrnQbj.jpg\",\n    first: \"Jon\",\n    last: \"Jensen\",\n    twitter: \"@jenseng\",\n  },\n].forEach((contact) => {\n  fakeContacts.create({\n    ...contact,\n    id: `${contact.first\n      .toLowerCase()\n      .split(\" \")\n      .join(\"_\")}-${contact.last.toLocaleLowerCase()}`,\n  });\n});\n"
  },
  {
    "path": "tutorials/address-book/app/root.tsx",
    "content": "import {\n  Form,\n  Scripts,\n  ScrollRestoration,\n  isRouteErrorResponse,\n} from \"react-router\";\nimport type { Route } from \"./+types/root\";\n\nimport appStylesHref from \"./app.css?url\";\n\nexport default function App() {\n  return (\n    <>\n      <div id=\"sidebar\">\n        <h1>React Router Contacts</h1>\n        <div>\n          <Form id=\"search-form\" role=\"search\">\n            <input\n              aria-label=\"Search contacts\"\n              id=\"q\"\n              name=\"q\"\n              placeholder=\"Search\"\n              type=\"search\"\n            />\n            <div aria-hidden hidden={true} id=\"search-spinner\" />\n          </Form>\n          <Form method=\"post\">\n            <button type=\"submit\">New</button>\n          </Form>\n        </div>\n        <nav>\n          <ul>\n            <li>\n              <a href={`/contacts/1`}>Your Name</a>\n            </li>\n            <li>\n              <a href={`/contacts/2`}>Your Friend</a>\n            </li>\n          </ul>\n        </nav>\n      </div>\n    </>\n  );\n}\n\n// The Layout component is a special export for the root route.\n// It acts as your document's \"app shell\" for all route components, HydrateFallback, and ErrorBoundary\n// For more information, see https://reactrouter.com/explanation/special-files#layout-export\nexport function Layout({ children }: { children: React.ReactNode }) {\n  return (\n    <html lang=\"en\">\n      <head>\n        <meta charSet=\"utf-8\" />\n        <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />\n        <link rel=\"stylesheet\" href={appStylesHref} />\n      </head>\n      <body>\n        {children}\n        <ScrollRestoration />\n        <Scripts />\n      </body>\n    </html>\n  );\n}\n\n// The top most error boundary for the app, rendered when your app throws an error\n// For more information, see https://reactrouter.com/start/framework/route-module#errorboundary\nexport function ErrorBoundary({ error }: Route.ErrorBoundaryProps) {\n  let message = \"Oops!\";\n  let details = \"An unexpected error occurred.\";\n  let stack: string | undefined;\n\n  if (isRouteErrorResponse(error)) {\n    message = error.status === 404 ? \"404\" : \"Error\";\n    details =\n      error.status === 404\n        ? \"The requested page could not be found.\"\n        : error.statusText || details;\n  } else if (import.meta.env.DEV && error && error instanceof Error) {\n    details = error.message;\n    stack = error.stack;\n  }\n\n  return (\n    <main id=\"error-page\">\n      <h1>{message}</h1>\n      <p>{details}</p>\n      {stack && (\n        <pre>\n          <code>{stack}</code>\n        </pre>\n      )}\n    </main>\n  );\n}\n"
  },
  {
    "path": "tutorials/address-book/app/routes.ts",
    "content": "import type { RouteConfig } from \"@react-router/dev/routes\";\n\nexport default [] satisfies RouteConfig;\n"
  },
  {
    "path": "tutorials/address-book/package.json",
    "content": "{\n  \"private\": true,\n  \"type\": \"module\",\n  \"scripts\": {\n    \"build\": \"cross-env NODE_ENV=production react-router build\",\n    \"dev\": \"react-router dev\",\n    \"start\": \"cross-env NODE_ENV=production react-router-serve ./build/server/index.js\",\n    \"typecheck\": \"react-router typegen && tsc\"\n  },\n  \"dependencies\": {\n    \"@react-router/node\": \"*\",\n    \"@react-router/serve\": \"*\",\n    \"isbot\": \"^5.1.17\",\n    \"match-sorter\": \"^8.0.0\",\n    \"react\": \"^19.2.3\",\n    \"react-dom\": \"^19.2.3\",\n    \"react-router\": \"*\",\n    \"sort-by\": \"^1.2.0\",\n    \"tiny-invariant\": \"^1.3.3\"\n  },\n  \"devDependencies\": {\n    \"@react-router/dev\": \"*\",\n    \"@types/node\": \"^20\",\n    \"@types/react\": \"^19.2.7\",\n    \"@types/react-dom\": \"^19.2.3\",\n    \"cross-env\": \"^7.0.3\",\n    \"typescript\": \"^5.4.5\",\n    \"vite\": \"^5.4.11\"\n  },\n  \"engines\": {\n    \"node\": \">=20.0.0\"\n  }\n}\n"
  },
  {
    "path": "tutorials/address-book/react-router.config.ts",
    "content": "import { type Config } from \"@react-router/dev/config\";\n\nexport default {\n  ssr: false,\n} satisfies Config;\n"
  },
  {
    "path": "tutorials/address-book/tsconfig.json",
    "content": "{\n  \"include\": [\n    \"**/*\",\n    \"**/.server/**/*\",\n    \"**/.client/**/*\",\n    \".react-router/types/**/*\"\n  ],\n  \"compilerOptions\": {\n    \"lib\": [\"DOM\", \"DOM.Iterable\", \"ES2022\"],\n    \"types\": [\"node\", \"vite/client\"],\n    \"target\": \"ES2022\",\n    \"module\": \"ES2022\",\n    \"moduleResolution\": \"bundler\",\n    \"jsx\": \"react-jsx\",\n    \"rootDirs\": [\".\", \"./.react-router/types\"],\n    \"baseUrl\": \".\",\n    \"esModuleInterop\": true,\n    \"verbatimModuleSyntax\": true,\n    \"noEmit\": true,\n    \"resolveJsonModule\": true,\n    \"skipLibCheck\": true,\n    \"strict\": true\n  }\n}\n"
  },
  {
    "path": "tutorials/address-book/vite.config.ts",
    "content": "import { reactRouter } from \"@react-router/dev/vite\";\nimport { defineConfig } from \"vite\";\n\nexport default defineConfig({\n  plugins: [reactRouter()],\n});\n"
  },
  {
    "path": "typedoc.mjs",
    "content": "import { OptionDefaults } from \"typedoc\";\n\n/** @type {Partial<import('typedoc').TypeDocOptions>} */\nconst config = {\n  name: \"React Router API Reference\",\n  entryPoints: [\"packages/*\"],\n  entryPointStrategy: \"packages\",\n  includeVersion: false,\n  json: \"./public/dev/api.json\",\n  out: \"./public/dev\",\n  hostedBaseUrl: \"https://api.reactrouter.com/dev/\",\n};\n\nexport default config;\n\n// Because we use `entryPointStrategy: \"packages\"`, the root config is not\n// inherited by the child packages so we can export shared stuff here for them\n// to use. See https://typedoc.org/documents/Options.Input.html#entrypointstrategy\nexport const blockTags = [\n  ...OptionDefaults.blockTags,\n  \"@name\",\n  \"@mode\",\n  \"@additionalExamples\",\n];\n"
  }
]