[
  {
    "path": ".eslintrc",
    "content": "{\n  \"extends\": [\n    \"eslint:recommended\",\n    \"plugin:@typescript-eslint/recommended\",\n    \"plugin:react/recommended\",\n    \"prettier\"\n  ],\n  \"ignorePatterns\": [\"node_modules/\", \"testing-app\"],\n  \"parser\": \"@typescript-eslint/parser\",\n  \"parserOptions\": {\n    \"ecmaVersion\": \"latest\",\n    \"sourceType\": \"module\"\n  },\n  \"plugins\": [\"@typescript-eslint\", \"react\", \"prettier\"],\n  \"rules\": {\n    \"@typescript-eslint/no-unnecessary-type-constraint\": \"off\",\n    \"@typescript-eslint/no-explicit-any\": \"off\",\n    \"no-console\": \"error\"\n  },\n  \"settings\": {\n    \"react\": {\n      \"version\": \"18\"\n    }\n  }\n}\n"
  },
  {
    "path": ".github/FUNDING.yml",
    "content": "# These are supported funding model platforms\n\ngithub: [AlemTuzlak]\npatreon: # Replace with a single Patreon username\nopen_collective: # Replace with a single Open Collective username\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\nlfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry\ncustom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']\n"
  },
  {
    "path": ".github/workflows/publish-commit.yaml",
    "content": "name: 🚀 pkg-pr-new\non: [push, pull_request]\n\njobs:\n  build:\n    runs-on: ubuntu-latest\n\n    steps:\n      - name: Checkout code\n        uses: actions/checkout@v2\n\n      - run: corepack enable\n      - uses: actions/setup-node@v4\n        with:\n          node-version: 20\n          cache: \"npm\"\n\n      - name: Install dependencies\n        run: npm install\n\n      - name: Build\n        run: npm run build\n\n      - run: npx pkg-pr-new publish\n"
  },
  {
    "path": ".github/workflows/publish.yaml",
    "content": "name: Publish Package to npmjs\non:\n  release:\n    types: [published]\n  workflow_dispatch:\njobs:\n  npm-publish:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v4\n      # Setup .npmrc file to publish to npm\n      - uses: actions/setup-node@v4\n        with:\n          node-version: \"20.x\"\n          registry-url: \"https://registry.npmjs.org\"\n      - run: npm ci\n      - run: npm publish\n        env:\n          NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}"
  },
  {
    "path": ".github/workflows/validate.yaml",
    "content": "name: 🚀 Validation Pipeline\nconcurrency:\n  group: ${{ github.repository }}-${{ github.workflow }}-${{ github.ref }}\n  cancel-in-progress: true\non:\n  push:\n    branches: [main]\n  pull_request:\n    branches: [main]\n\npermissions:\n  actions: write\n  contents: read\n  # Required to put a comment into the pull-request\n  pull-requests: write\njobs:\n#  lint:\n#    name: ⬣ Biome lint\n#    runs-on: ubuntu-latest\n#    steps:\n#     - name: ⬇️ Checkout repo\n#       uses: actions/checkout@v4\n#     - name: Setup Biome\n#       uses: biomejs/setup-biome@v2\n#     - name: Run Biome\n#       run: biome ci .\n\n  typecheck:\n    name: 🔎 Type check\n    runs-on: ubuntu-latest\n    steps:\n      - name: 🛑 Cancel Previous Runs\n        uses: styfle/cancel-workflow-action@0.12.1\n      - name: ⬇️ Checkout repo\n        uses: actions/checkout@v4\n      - name: ⎔ Setup node\n        uses: actions/setup-node@v4\n        with:\n          node-version: 20\n      - name: 📥 Download deps\n        uses: bahmutov/npm-install@v1\n        with:\n          useLockFile: false\n      - name: 🔎 Type check\n        run: npm run typecheck\n\n  vitest:\n    name: ⚡ Unit Tests\n    runs-on: ubuntu-latest\n    steps:\n      - name: 🛑 Cancel Previous Runs\n        uses: styfle/cancel-workflow-action@0.12.1\n      - name: ⬇️ Checkout repo\n        uses: actions/checkout@v4\n      - name: ⎔ Setup node\n        uses: actions/setup-node@v4\n        with:\n          node-version: 20\n      - name: 📥 Download deps\n        uses: bahmutov/npm-install@v1\n        with:\n          useLockFile: false\n      - name: Install dotenv cli\n        run: npm install -g dotenv-cli\n      - name: ⚡ Run vitest\n        run: npm run test\n#      - name: \"Report Coverage\"\n        # Only works if you set `reportOnFailure: true` in your vite config as specified above\n#        if: always()\n#        uses: davelosert/vitest-coverage-report-action@v2\n "
  },
  {
    "path": ".gitignore",
    "content": "# Logs\nlogs\n*.log\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\nlerna-debug.log*\n\n# Diagnostic reports (https://nodejs.org/api/report.html)\nreport.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json\n\n# Runtime data\npids\n*.pid\n*.seed\n*.pid.lock\n\n# Directory for instrumented libs generated by jscoverage/JSCover\nlib-cov\n\n# Coverage directory used by tools like istanbul\ncoverage\n*.lcov\n\n# nyc test coverage\n.nyc_output\n\n# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)\n.grunt\n\n# Bower dependency directory (https://bower.io/)\nbower_components\n\n# node-waf configuration\n.lock-wscript\n\n# Compiled binary addons (https://nodejs.org/api/addons.html)\nbuild/Release\n\n# Dependency directories\nnode_modules/\njspm_packages/\n\n# TypeScript v1 declaration files\ntypings/\n\n# TypeScript cache\n*.tsbuildinfo\n\n# Optional npm cache directory\n.npm\n\n# Optional eslint cache\n.eslintcache\n\n# Microbundle cache\n.rpt2_cache/\n.rts2_cache_cjs/\n.rts2_cache_es/\n.rts2_cache_umd/\n\n# Optional REPL history\n.node_repl_history\n\n# Output of 'npm pack'\n*.tgz\n\n# Yarn Integrity file\n.yarn-integrity\n\n# dotenv environment variables file\n.env\n.env.test\n\n# parcel-bundler cache (https://parceljs.org/)\n.cache\n\n# Next.js build output\n.next\n\n# Nuxt.js build / generate output\n.nuxt\ndist\n\n# Gatsby files\n.cache/\n# Comment in the public line in if your project uses Gatsby and *not* Next.js\n# https://nextjs.org/blog/next-9-1#public-directory-support\n# public\n\n# vuepress build output\n.vuepress/dist\n\n# Serverless directories\n.serverless/\n\n# FuseBox cache\n.fusebox/\n\n# DynamoDB Local files\n.dynamodb/\n\n# TernJS port file\n.tern-port\n\n/build\n.history\n.react-router\n.turbo\n"
  },
  {
    "path": ".prettierignore",
    "content": "node_modules\n/build"
  },
  {
    "path": ".prettierrc",
    "content": "{\n  \n}"
  },
  {
    "path": "CODE_OF_CONDUCT.md",
    "content": "# Contributor Covenant Code of Conduct\n\n## Our Pledge\n\nWe as members, contributors, and leaders pledge to make participation in our\ncommunity a harassment-free experience for everyone, regardless of age, body\nsize, visible or invisible disability, ethnicity, sex characteristics, gender\nidentity and expression, level of experience, education, socio-economic status,\nnationality, personal appearance, race, religion, or sexual identity\nand orientation.\n\nWe pledge to act and interact in ways that contribute to an open, welcoming,\ndiverse, inclusive, and healthy community.\n\n## Our Standards\n\nExamples of behavior that contributes to a positive environment for our\ncommunity include:\n\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,\n  and learning from the experience\n* Focusing on what is best not just for us as individuals, but for the\n  overall community\n\nExamples of unacceptable behavior include:\n\n* The use of sexualized language or imagery, and sexual attention or\n  advances of any kind\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\n  address, without their explicit permission\n* Other conduct which could reasonably be considered inappropriate in a\n  professional setting\n\n## Enforcement Responsibilities\n\nCommunity leaders are responsible for clarifying and enforcing our standards of\nacceptable behavior and will take appropriate and fair corrective action in\nresponse to any behavior that they deem inappropriate, threatening, offensive,\nor harmful.\n\nCommunity leaders have the right and responsibility to remove, edit, or reject\ncomments, commits, code, wiki edits, issues, and other contributions that are\nnot aligned to this Code of Conduct, and will communicate reasons for moderation\ndecisions when appropriate.\n\n## Scope\n\nThis Code of Conduct applies within all community spaces, and also applies when\nan individual is officially representing the community in public spaces.\nExamples of representing our community include using an official e-mail address,\nposting via an official social media account, or acting as an appointed\nrepresentative at an online or offline event.\n\n## Enforcement\n\nInstances of abusive, harassing, or otherwise unacceptable behavior may be\nreported to the community leaders responsible for enforcement at\n.\nAll complaints will be reviewed and investigated promptly and fairly.\n\nAll community leaders are obligated to respect the privacy and security of the\nreporter of any incident.\n\n## Enforcement Guidelines\n\nCommunity leaders will follow these Community Impact Guidelines in determining\nthe consequences for any action they deem in violation of this Code of Conduct:\n\n### 1. Correction\n\n**Community Impact**: Use of inappropriate language or other behavior deemed\nunprofessional or unwelcome in the community.\n\n**Consequence**: A private, written warning from community leaders, providing\nclarity around the nature of the violation and an explanation of why the\nbehavior was inappropriate. A public apology may be requested.\n\n### 2. Warning\n\n**Community Impact**: A violation through a single incident or series\nof actions.\n\n**Consequence**: A warning with consequences for continued behavior. No\ninteraction with the people involved, including unsolicited interaction with\nthose enforcing the Code of Conduct, for a specified period of time. This\nincludes avoiding interactions in community spaces as well as external channels\nlike social media. Violating these terms may lead to a temporary or\npermanent ban.\n\n### 3. Temporary Ban\n\n**Community Impact**: A serious violation of community standards, including\nsustained inappropriate behavior.\n\n**Consequence**: A temporary ban from any sort of interaction or public\ncommunication with the community for a specified period of time. No public or\nprivate interaction with the people involved, including unsolicited interaction\nwith those enforcing the Code of Conduct, is allowed during this period.\nViolating these terms may lead to a permanent ban.\n\n### 4. Permanent Ban\n\n**Community Impact**: Demonstrating a pattern of violation of community\nstandards, including sustained inappropriate behavior,  harassment of an\nindividual, or aggression toward or disparagement of classes of individuals.\n\n**Consequence**: A permanent ban from any sort of public interaction within\nthe community.\n\n## Attribution\n\nThis Code of Conduct is adapted from the [Contributor Covenant][homepage],\nversion 2.0, available at\nhttps://www.contributor-covenant.org/version/2/0/code_of_conduct.html.\n\nCommunity Impact Guidelines were inspired by [Mozilla's code of conduct\nenforcement ladder](https://github.com/mozilla/diversity).\n\n[homepage]: https://www.contributor-covenant.org\n\nFor answers to common questions about this code of conduct, see the FAQ at\nhttps://www.contributor-covenant.org/faq. Translations are available at\nhttps://www.contributor-covenant.org/translations."
  },
  {
    "path": "LICENSE",
    "content": "MIT License\n\nCopyright (c) 2023 Code Forge\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": "# remix-hook-form\n\n![GitHub Repo stars](https://img.shields.io/github/stars/forge-42/remix-hook-form?style=social)\n![npm](https://img.shields.io/npm/v/remix-hook-form?style=plastic)\n![GitHub](https://img.shields.io/github/license/forge-42/remix-hook-form?style=plastic)\n![npm](https://img.shields.io/npm/dy/remix-hook-form?style=plastic) \n![npm](https://img.shields.io/npm/dw/remix-hook-form?style=plastic) \n![GitHub top language](https://img.shields.io/github/languages/top/forge-42/remix-hook-form?style=plastic) \n\nRemix-hook-form is a powerful and lightweight wrapper around [react-hook-form](https://react-hook-form.com/) that streamlines the process of working with forms and form data in your [React Router](https://reactrouter.com) applications. With a comprehensive set of hooks and utilities, you'll be able to easily leverage the flexibility of react-hook-form without the headache of boilerplate code.\n\nAnd the best part? Remix-hook-form has zero dependencies, making it easy to integrate into your existing projects and workflows. Say goodbye to bloated dependencies and hello to a cleaner, more efficient development process with Remix-hook-form. \n\nOh, and did we mention that this is fully Progressively enhanced? That's right, you can use this with or without javascript!\n\n## Remix.run support\nVersions older than 6.0.0 are compatible with [Remix.run](https://remix.run) applications. If you are using Remix.run, please use version 5.1.1 or lower.\n\n## Install\n\n```bash\nnpm install remix-hook-form react-hook-form\n```\n\n## Basic usage\n\nHere is an example usage of remix-hook-form. It will work with **and without** JS. Before running the example, ensure to install additional dependencies:\n\n```bash\nnpm install zod @hookform/resolvers\n```\n\n```ts\nimport { useRemixForm, getValidatedFormData } from \"remix-hook-form\";\nimport { Form } from \"react-router\";\nimport { zodResolver } from \"@hookform/resolvers/zod\";\nimport * as zod from \"zod\";\nimport type { Route } from \"./+types/home\";\n\n\nconst schema = zod.object({\n  name: zod.string().min(1),\n  email: zod.string().email().min(1),\n});\n\ntype FormData = zod.infer<typeof schema>;\n\nconst resolver = zodResolver(schema);\n\nexport const action = async ({ request }: Route.ActionArgs) => {\n  const { errors, data, receivedValues: defaultValues } =\n    await getValidatedFormData<FormData>(request, resolver);\n  if (errors) {\n    // The keys \"errors\" and \"defaultValues\" are picked up automatically by useRemixForm\n    return { errors, defaultValues };\n  }\n\n  // Do something with the data\n  return data;\n};\n\nexport default function MyForm() {\n  const {\n    handleSubmit,\n    formState: { errors },\n    register,\n  } = useRemixForm<FormData>({\n    mode: \"onSubmit\",\n    resolver,\n  });\n\n  return (\n    <Form onSubmit={handleSubmit} method=\"POST\">\n      <label>\n        Name:\n        <input type=\"text\" {...register(\"name\")} />\n        {errors.name && <p>{errors.name.message}</p>}\n      </label>\n      <label>\n        Email:\n        <input type=\"email\" {...register(\"email\")} />\n        {errors.email && <p>{errors.email.message}</p>}\n      </label>\n      <button type=\"submit\">Submit</button>\n    </Form>\n  );\n}\n```\n\n## Serialization of values client => server\n\nBy default, all values are serialized to strings before being sent to the server. This is because that is how form data works, it only accepts strings, nulls or files, this means that even strings would get \"double stringified\" and become strings like this:\n```ts\nconst string = \"'123'\";\n```\nThis helps with the fact that validation on the server can't know if your stringified values received from the client are actually strings or numbers or dates or whatever.\n\nFor example, if you send this formData to the server:\n\n```ts\nconst formData = {\n  name: \"123\",\n  age: 30,\n  hobbies: [\"Reading\", \"Writing\", \"Coding\"],\n  boolean: true,\n  a: null,\n  // this gets omitted because it's undefined\n  b: undefined,\n  numbers: [1, 2, 3],\n  other: {\n    skills: [\"testing\", \"testing\"],\n    something: \"else\",\n  },\n};\n```\n\nIt would be sent to the server as:\n```ts\n{\n  name: \"123\",\n  age: \"30\",\n  hobbies: \"[\\\"Reading\\\",\\\"Writing\\\",\\\"Coding\\\"]\",\n  boolean: \"true\",\n  a: \"null\",\n  numbers: \"[1,2,3]\",\n  other: \"{\\\"skills\\\":[\\\"testing\\\",\\\"testing\\\"],\\\"something\\\":\\\"else\\\"}\",\n}\n```\n\nThen the server does not know if the `name` property used to be a string or a number, your validation schema would fail if it parsed it back to a number and you expected it to be a string. Conversely, if you didn't parse the rest of this data you wouldn't have objects,\narrays etc. but strings. \n\nThe double stringification helps with this as it would correctly parse the data back to the original types, but it also means that you have to use the helpers provided by this package to parse the data back to the original types.\n\n\n\nThis is the default behavior, but you can change this behavior by setting the `stringifyAllValues` prop to `false` in the `useRemixForm` hook.\n\n```ts\nconst { handleSubmit, formState, register } = useRemixForm({\n  mode: \"onSubmit\",\n  resolver,\n  stringifyAllValues: false,\n});\n```\n\nThis only affects strings really as it either double stringifies them or it doesn't. The bigger impact of all of this is on the server side.\n\nBy default all the server helpers expect the data to be double stringified which allows the utils to parse the data back to the original types easily. If you don't want to double stringify the data then you can set the `preserveStringified` prop to `true` in the `getValidatedFormData` function.\n\n```ts\n// Third argument is preserveStringified and is false by default\nconst { errors, data } = await getValidatedFormData(request, resolver, true);\n```\nBecause the data by default is double stringified the data returned by the util and sent to your validator would look like this:\n\n```ts\nconst data = {\n  name: \"123\",\n  age: 30,\n  hobbies: [\"Reading\", \"Writing\", \"Coding\"],\n  boolean: true,\n  a: null,\n  // this gets omitted because it's undefined\n  b: undefined,\n  numbers: [1, 2, 3],\n  other: {\n    skills: [\"testing\", \"testing\"],\n    something: \"else\",\n  },\n};\n```\n\nIf you set `preserveStringified` to `true` then the data would look like this:\n\n```ts\nconst data = {\n  name: \"123\",\n  age: \"30\",\n  hobbies: [\"Reading\", \"Writing\", \"Coding\"],\n  boolean: \"true\",\n  a: \"null\",\n  numbers: [\"1\",\"2\",\"3\"],\n  other: {\n    skills: [\"testing\", \"testing\"],\n    something: \"else\",\n  },\n};\n\n```\n\nThis means that your validator would have to handle all the type conversions and validations for all the different types of data. This is a lot of work and it's not worth it usually, the best place to use this approach if you store the info in searchParams. If you want to handle it like this what you can do is use something like `coerce` from `zod` to convert the data to the correct type before checking it.\n\n```ts\nimport { z } from \"zod\";\n\nconst formDataZodSchema = z.object({\n  name: z.string().min(1),\n  // converts the string to a number\n  age: z.coerce.number().int().positive(), \n});\n\ntype SchemaFormData = z.infer<typeof formDataZodSchema>;\n\nconst resolver = zodResolver(formDataZodSchema);\n\nexport const action = async ({ request }: Route.ActionArgs) => {\n  const { errors, data } = await getValidatedFormData<SchemaFormData>(\n    request,\n    resolver,\n    true,\n  );\n  if (errors) {\n    return { errors };\n  }\n  // Do something with the data\n};\n```\n\n \n\n## Fetcher usage\n\nYou can pass in a fetcher as an optional prop and `useRemixForm` will use that fetcher to submit the data and read the errors instead of the default behavior. For more info see the docs on `useRemixForm` below.\n\n\n## Video example and tutorial\n\nIf you wish to learn in depth on how form handling works in React router/Remix.run and want an example using this package I have prepared a video tutorial on how to do it. It's a bit long but it covers everything \nyou need to know about form handling in React Router/Remix. It also covers how to use this package. You can find it here:\n\nhttps://youtu.be/iom5nnj29sY?si=l52WRE2bqpkS2QUh\n\n\n## Middleware mode\n\nFrom v7 you can use middleware to extract the form data and access it anywhere in your actions and loaders.  \nAll you have to do is set it up in your `root.tsx` file like this:\n\n```ts\nimport { unstable_extractFormDataMiddleware } from \"remix-hook-form/middleware\";\n\nexport const unstable_middleware = [unstable_extractFormDataMiddleware()];\n```\n\n\nAnd then access it in your actions and loaders like this:\n\n\n```ts\nimport { getFormData, getValidatedFormData } from \"remix-hook-form/middleware\";\n\nexport const loader = async ({ context }: LoaderFunctionArgs) => {\n  const searchParamsFormData = await getFormData(context); \n  return { result: \"success\" };\n};\nexport const action = async ({ context }: ActionFunctionArgs) => {\n  // OR:   const formData = await getFormData(context); \n  const { data, errors, receivedValues } = await getValidatedFormData<FormData>(\n    context,\n    resolver,\n  );\n  if (errors) {\n    return { errors, receivedValues };\n  } \n  return { result: \"success\" };\n};\n```\n\n## API's\n\n### getValidatedFormData\n\nNow supports no-js form submissions!\n\nIf you made a GET request instead of a POST request and you are using this inside of a loader it will try to extract the data from the search params\n\nIf the form is submitted without js it will try to parse the formData object and covert it to the same format as the data object returned by `useRemixForm`. If the form is submitted with js it will automatically extract the data from the request object and validate it.\n\ngetValidatedFormData is a utility function that can be used to validate form data in your action. It takes two arguments: the request/formData object and the resolver function. It returns an object with three properties: `errors`, `receivedValues` and `data`. If there are no errors, `errors` will be `undefined`. If there are errors, `errors` will be an object with the same shape as the `errors` object returned by `useRemixForm`. If there are no errors, `data` will be an object with the same shape as the `data` object returned by `useRemixForm`.\n\nThe `receivedValues` property allows you to set the default values of your form to the values that were received from the request object. This is useful if you want to display the form again with the values that were submitted by the user when there is no JS present\n \n #### Example with errors only\n If you don't want the form to persist submitted values in the case of validation errors then you can just return the `errors` object directly from the action.\n```jsx\n/** all the same code from above */\n\nexport const action = async ({ request }: Route.ActionArgs) => {\n  // Takes the request from the frontend, parses and validates it and returns the data\n  const { errors, data } =\n    await getValidatedFormData<FormData>(request, resolver);\n  if (errors) {\n    return { errors };\n  }\n  // Do something with the data\n};\n```\n\n#### Example with errors and receivedValues\nIf your action returrns `defaultValues` key then it will be automatically used by `useRemixForm` to populate the default values of the form.\n```jsx\n/** all the same code from above */\n\nexport const action = async ({ request }: Route.ActionArgs) => {\n  // Takes the request from the frontend, parses and validates it and returns the data\n  const { errors, data, receivedValues: defaultValues } =\n    await getValidatedFormData<FormData>(request, resolver);\n  if (errors) {\n    return { errors, defaultValues };\n  }\n  // Do something with the data\n};\n\n```\n\n### validateFormData\n\nvalidateFormData is a utility function that can be used to validate form data in your action. It takes two arguments: the formData object and the resolver function. It returns an object with two properties: `errors` and `data`. If there are no errors, `errors` will be `undefined`. If there are errors, `errors` will be an object with the same shape as the `errors` object returned by `useRemixForm`. If there are no errors, `data` will be an object with the same shape as the `data` object returned by `useRemixForm`.\n\nThe difference between `validateFormData` and `getValidatedFormData` is that `validateFormData` only validates the data while the `getValidatedFormData` function also extracts the data automatically from the request object assuming you were using the default setup.\n\n```jsx\n/** all the same code from above */\n\nexport const action = async ({ request }: Route.ActionArgs) => {\n  // Lets assume you get the data in a different way here but still want to validate it\n  const formData = await yourWayOfGettingFormData(request);\n  // Takes the request from the frontend, parses and validates it and returns the data\n  const { errors, data } =\n    await validateFormData<FormData>(formData, resolver);\n  if (errors) {\n    return { errors };\n  }\n  // Do something with the data\n};\n\n```\n\n### createFormData\n\ncreateFormData is a utility function that can be used to create a FormData object from the data returned by the handleSubmit function from `react-hook-form`. It takes one argument, the `data` from the `handleSubmit` function and it converts everything it can to strings and appends files as well. It returns a FormData object.\n\n```jsx\n/** all the same code from above */\n\nexport default function MyForm() {\n  const { ... } = useRemixForm({\n    ...,\n    submitHandlers: {\n      onValid: data => {\n        // This will create a FormData instance ready to be sent to the server, by default all your data is converted to a string before sent\n        const formData = createFormData(data); \n        // Do something with the formData\n      }\n    }\n  });\n\n  return (\n   ...\n  );\n}\n\n```\n\n### parseFormData\n\nparseFormData is a utility function that can be used to parse the data submitted to the action by the handleSubmit function from `react-hook-form`. It takes two arguments, first one is the `request` submitted from the frontend and the second one is `preserveStringified`, the form data you submit will be cast to strings because that is how form data works, when retrieving it you can either keep everything as strings or let the helper try to parse it back to original types (eg number to string), default is `false`. It returns an object that contains unvalidated `data` submitted from the frontend.\n\n\n```jsx\n/** all the same code from above */\n\nexport const action = async ({ request }: Route.ActionArgs) => {\n  // Allows you to get the data from the request object without having to validate it\n  const formData = await parseFormData(request);\n  // formData.age will be a number\n  const formDataStringified = await parseFormData(request, true);\n  // formDataStringified.age will be a string\n  // Do something with the data\n};\n\n```\n\n\n### getFormDataFromSearchParams\n\nIf you're using a GET request formData is not available on the request so you can use this method to extract your formData from the search parameters assuming you set all your data in the search parameters\n\n## Hooks\n\n### useRemixForm\n\n`useRemixForm` is a hook that can be used to create a form in your React Router / Remix application. It's basically the same as react-hook-form's [`useForm`](https://www.react-hook-form.com/api/useform/) hook, with the following differences:\n\n**Additional options**\n- `submitHandlers`: an object containing two properties:\n  - `onValid`: can be passed into the function to override the default behavior of the `handleSubmit` success case provided by the hook. If you need to pass additional data not tracked in the form, you'll need to manually merge it here with your form data to be submitted, as the `submitData` hook option is ignored in this case.\n  - `onInvalid`: can be passed into the function to override the default behavior of the `handleSubmit` error case provided by the hook.\n- `submitConfig`: allows you to pass additional configuration to the `useSubmit` function from React Router / Remix, such as `{ replace: true }` to replace the current history entry instead of pushing a new one. The `submitConfig` trumps `Form` props from React Router / Remix. The following props will be used from `Form` if no submitConfig is provided:\n  - `method`\n  - `action`\n  - `encType`\n- `submitData`: allows you to pass additional data to the backend when the form is submitted.\n- `fetcher`: if provided then this fetcher will be used to submit data and get a response (errors / defaultValues) instead of React Router/Remix's `useSubmit` and `useActionData` hooks.\n\n**`register` will respect default values returned from the action**\n\nIf the React Router/Remix hook `useActionData` returns an object with `defaultValues` these will automatically be used as the default value when calling the `register` function. This is useful when the form has errors and you want to persist the values when JS is not enabled. If a `fetcher` is provided default values will be read from the fetcher's data.\n\n**`handleSubmit`**\n\nThe returned `handleSubmit` function does two additional things\n- The success case is provided by default where when the form is validated by the provided resolver, and it has no errors, it will automatically submit the form to the current route using a POST request. The data will be sent as `formData` to the action function.\n- The data that is sent is automatically wrapped into a formData object and passed to the server ready to be used. Easiest way to consume it is by using the `parseFormData` or `getValidatedFormData` function from the `remix-hook-form` package.\n\n**`formState.errors`**\n\nThe `errors` object inside `formState` is automatically populated with the errors returned by the action. If the action returns an `errors` key in it's data then that value will be used to populate errors, otherwise the whole action response is assumed to be the errors object. If a `fetcher` is provided then errors are read from the fetcher's data.\n\n#### Examples\n\n**Overriding the default onValid and onInvalid cases**\n\n```jsx\n  const { ... } = useRemixForm({\n    ...ALL_THE_SAME_CONFIG_AS_REACT_HOOK_FORM,\n    submitHandlers: {\n      onValid: data => { \n        // Do something with the formData\n      },\n      onInvalid: errors => {\n        // Do something with the errors\n      }\n    }\n  });\n\n```\n\n**Overriding the submit from remix to do something else**\n\n```jsx\n  const { ... } = useRemixForm({\n    ...ALL_THE_SAME_CONFIG_AS_REACT_HOOK_FORM,\n    submitConfig: {\n      replace: true,\n      method: \"PUT\",\n      action: \"/api/youraction\",\n    },\n  });\n\n```\n\n**Passing additional data to the backend**\n\n```jsx\n  const { ... } = useRemixForm({\n    ...ALL_THE_SAME_CONFIG_AS_REACT_HOOK_FORM,\n    submitData: {\n      someFieldsOutsideTheForm: \"someValue\"\n    },\n  });\n\n  // or if customizing with `onValid` handler\n  const { ... } = useRemixForm({\n    ...ALL_THE_SAME_CONFIG_AS_REACT_HOOK_FORM,\n    submitHandlers: {\n      onValid: data => { \n        const mergedData = { ...data, someFieldsOutsideTheForm: \"someValue\" }\n        const formData = createFormData(mergedData);\n        // ... submit\n      }\n    }\n  });\n```\n\n### RemixFormProvider\n\nIdentical to the [`FormProvider`](https://react-hook-form.com/api/formprovider/) from `react-hook-form`, but it also returns the changed `formState.errors` and `handleSubmit` object.\n```jsx\nexport default function Form() {\n  const methods = useRemixForm();\n \n  return (\n    <RemixFormProvider {...methods} > // pass all methods into the context\n      <form onSubmit={methods.handleSubmit}>\n        <button type=\"submit\" />\n      </form>\n    </RemixFormProvider>\n  );\n}\n\n```\n\n### useRemixFormContext\n\nExactly the same as [`useFormContext`](https://react-hook-form.com/api/useformcontext/) from `react-hook-form` but it also returns the changed `formState.errors` and `handleSubmit` object.\n\n```jsx\nexport default function Form() {\n  const methods = useRemixForm();\n \n\n  return (\n    <RemixFormProvider {...methods} > // pass all methods into the context\n      <form onSubmit={methods.handleSubmit}>\n        <NestedInput />\n        <button type=\"submit\" />\n      </form>\n    </RemixFormProvider>\n  );\n}\n\nconst NestedInput = () => {\n  const { register } = useRemixFormContext(); // retrieve all hook methods\n  return <input {...register(\"test\")} />;\n}\n\n```\n\n\n## Support \n\nIf you like the project, please consider supporting us by giving a ⭐️ on Github.\n## License\n\nMIT\n\n## Bugs\n\nIf you find a bug, please file an issue on [our issue tracker on GitHub](https://github.com/Code-Forge-Net/remix-hook-form/issues)\n\n\n## Contributing\n\nThank you for considering contributing to Remix-hook-form! We welcome any contributions, big or small, including bug reports, feature requests, documentation improvements, or code changes.\n\nTo get started, please fork this repository and make your changes in a new branch. Once you're ready to submit your changes, please open a pull request with a clear description of your changes and any related issues or pull requests.\n\nPlease note that all contributions are subject to our [Code of Conduct](https://github.com/Code-Forge-Net/remix-hook-form/blob/main/CODE_OF_CONDUCT.md). By participating, you are expected to uphold this code.\n\nWe appreciate your time and effort in contributing to Remix-hook-form and helping to make it a better tool for the community!\n\n"
  },
  {
    "path": "SECURITY.MD",
    "content": "# Security Policy\n\n## Supported Versions\n\n| Version | Supported          |\n| ------- | ------------------ | \n| 1.0.x   | :white_check_mark: |\n\n## Reporting a Vulnerability\n\nIn case of a vulnerability please reach out to active maintainers of the project and report it to them. "
  },
  {
    "path": "package.json",
    "content": "{\n  \"name\": \"remix-hook-form\",\n  \"version\": \"7.1.1\",\n  \"description\": \"Utility wrapper around react-hook-form for use with react-router v7+\",\n  \"type\": \"module\",\n  \"main\": \"./dist/index.cjs\",\n  \"module\": \"./dist/index.js\",\n  \"exports\": {\n    \".\": {\n      \"types\": \"./dist/index.d.ts\",\n      \"import\": {\n        \"types\": \"./dist/index.d.ts\",\n        \"import\": \"./dist/index.js\",\n        \"default\": \"./dist/index.js\"\n      },\n      \"require\": {\n        \"types\": \"./dist/index.d.cts\",\n        \"import\": \"./dist/index.cjs\",\n        \"require\": \"./dist/index.cjs\"\n      }\n    },\n    \"./middleware\": {\n      \"types\": \"./dist/middleware/index.d.ts\",\n      \"import\": {\n        \"types\": \"./dist/middleware/index.d.ts\",\n        \"import\": \"./dist/middleware/index.js\",\n        \"default\": \"./dist/middleware/index.js\"\n      },\n      \"require\": {\n        \"types\": \"./dist/middleware/index.d.cts\",\n        \"import\": \"./dist/middleware/index.cjs\",\n        \"require\": \"./dist/middleware/index.cjs\"\n      }\n    }\n  },\n  \"typings\": \"./dist/index.d.ts\",\n  \"files\": [\n    \"dist\"\n  ],\n  \"workspaces\": [\n    \".\",\n    \"test-apps/*\"\n  ],\n  \"scripts\": {\n    \"build\": \"tsup src/index.ts src/middleware/index.ts --format cjs,esm --dts --clean\",\n    \"react-router-dev\": \"npm run dev -w test-apps/react-router\",\n    \"build:dev\": \"tsup src/index.ts src/middleware/index.ts --format cjs,esm --dts\",\n    \"build:dev:watch\": \"npm run build:dev -- --watch\",\n    \"dev\": \"npm-run-all -s build:dev -p react-router-dev build:dev:watch\",\n    \"vite\": \"npm run build --watch -m development\",\n    \"prepublishOnly\": \"npm run build\",\n    \"test\": \"vitest run\",\n    \"typecheck\": \"tsc\",\n    \"validate\": \"npm run lint && npm run tsc && npm run test\",\n    \"lint\": \"eslint \\\"src/**/*.+(ts|tsx)\\\"\",\n    \"lint:fix\": \"npm run lint -- --fix\",\n    \"prettier\": \"prettier src --check\",\n    \"prettier:fix\": \"prettier src --write\",\n    \"format-code\": \"npm run prettier:fix & npm run lint:fix\"\n  },\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"git+https://github.com/forge-42/remix-hook-form.git\"\n  },\n  \"keywords\": [\n    \"React\",\n    \"react-router\",\n    \"react-router v7\",\n    \"react-hook-form\",\n    \"hooks\",\n    \"remix\",\n    \"remix react-hook-form\",\n    \"react-router react-hook-form\",\n    \"forms\"\n  ],\n  \"author\": \"Alem Tuzlak\",\n  \"license\": \"MIT\",\n  \"bugs\": {\n    \"url\": \"https://github.com/forge-42/remix-hook-form/issues\"\n  },\n  \"homepage\": \"https://github.com/forge-42/remix-hook-form#readme\",\n  \"peerDependencies\": {\n    \"react\": \"^18.2.0 || ^19.0.0\",\n    \"react-dom\": \"^18.2.0 || ^19.0.0\",\n    \"react-hook-form\": \"^7.55.0\",\n    \"react-router\": \">=7.5.0\"\n  },\n  \"readme\": \"https://github.com/forge42dev/remix-hook-form#readme\",\n  \"devDependencies\": {\n    \"@hookform/resolvers\": \"^3.1.0\",\n    \"@testing-library/react\": \"^14.0.0\",\n    \"@types/node\": \"^18.15.11\",\n    \"@types/react\": \"^18.0.34\",\n    \"@vitest/coverage-v8\": \"^2.0.3\",\n    \"babel-eslint\": \"^10.1.0\",\n    \"eslint-config-prettier\": \"^9.0.0\",\n    \"eslint-plugin-prettier\": \"^5.0.0\",\n    \"happy-dom\": \"^9.5.0\",\n    \"npm-run-all\": \"^4.1.5\",\n    \"prettier\": \"^3.0.1\",\n    \"react\": \"^18.2.0\",\n    \"react-dom\": \"^18.2.0\",\n    \"react-hook-form\": \"^7.51.0\",\n    \"rollup\": \"^3.20.2\",\n    \"rollup-plugin-typescript2\": \"^0.34.1\",\n    \"tsup\": \"^8.3.5\",\n    \"typescript\": \"^5.0.4\",\n    \"vitest\": \"^2.0.3\",\n    \"zod\": \"^3.21.4\"\n  }\n}"
  },
  {
    "path": "pull_request_template.md",
    "content": "# Description\n\nPlease include a summary of the change and which issue is fixed. Please also include relevant motivation and context. List any dependencies that are required for this change.\n\nFixes # (issue)\n\nIf this is a new feature please add a description of what was added and why below:\n\n\n## Type of change\n\nPlease delete options that are not relevant.\n\n- [ ] Bug fix (non-breaking change which fixes an issue)\n- [ ] New feature (non-breaking change which adds functionality)\n- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected)\n- [ ] This change requires a documentation update\n\n# How Has This Been Tested?\n\nPlease describe the tests that you ran to verify your changes. Provide instructions so we can reproduce. Please also list any relevant details for your test configuration\n\n- [ ] Unit tests\n\n# Checklist:\n\n- [ ] My code follows the guidelines of this project\n- [ ] I have performed a self-review of my own code\n- [ ] I have commented my code, particularly in hard-to-understand areas\n- [ ] I have made corresponding changes to the documentation\n- [ ] My changes generate no new warnings or errors\n- [ ] I have added tests that prove my fix is effective or that my feature works\n- [ ] New and existing unit tests pass locally with my changes\n- [ ] Any dependent changes have been merged and published in downstream modules"
  },
  {
    "path": "src/hook/index.test.tsx",
    "content": "import {\n  act,\n  cleanup,\n  fireEvent,\n  render,\n  renderHook,\n  waitFor,\n} from \"@testing-library/react\";\nimport React from \"react\";\nimport { type Navigation, useFetcher } from \"react-router\";\nimport { RemixFormProvider, useRemixForm, useRemixFormContext } from \"./index\";\n\nconst submitMock = vi.fn();\nconst fetcherSubmitMock = vi.fn();\n\nconst useActionDataMock = vi.hoisted(() => vi.fn());\n\nconst useNavigationMock = vi.hoisted(() =>\n  vi.fn<() => Pick<Navigation, \"state\" | \"formData\" | \"json\">>(() => ({\n    state: \"idle\",\n    formData: undefined,\n    json: undefined,\n  })),\n);\n\nconst useHrefMock = vi.hoisted(() => vi.fn());\n\nvi.mock(\"react-router\", () => ({\n  useSubmit: () => submitMock,\n  useActionData: useActionDataMock,\n  useFetcher: () => ({ submit: fetcherSubmitMock, data: {} }),\n  useNavigation: useNavigationMock,\n  useHref: useHrefMock,\n}));\n\ndescribe(\"useRemixForm\", () => {\n  it(\"should return all the same output that react-hook-form returns\", () => {\n    const { result } = renderHook(() => useRemixForm({}));\n    expect(result.current.register).toBeInstanceOf(Function);\n    expect(result.current.unregister).toBeInstanceOf(Function);\n    expect(result.current.setValue).toBeInstanceOf(Function);\n    expect(result.current.getValues).toBeInstanceOf(Function);\n    expect(result.current.trigger).toBeInstanceOf(Function);\n    expect(result.current.reset).toBeInstanceOf(Function);\n    expect(result.current.clearErrors).toBeInstanceOf(Function);\n    expect(result.current.setError).toBeInstanceOf(Function);\n    expect(result.current.formState).toEqual({\n      disabled: false,\n      dirtyFields: {},\n      isDirty: false,\n      isSubmitSuccessful: false,\n      isSubmitted: false,\n      isSubmitting: false,\n      isValid: false,\n      isValidating: false,\n      validatingFields: {},\n      touchedFields: {},\n      submitCount: 0,\n      isLoading: false,\n      errors: {},\n    });\n    expect(result.current.handleSubmit).toBeInstanceOf(Function);\n  });\n\n  it(\"should call onSubmit function when the form is valid\", async () => {\n    const onValid = vi.fn();\n    const onInvalid = vi.fn();\n\n    const { result } = renderHook(() =>\n      useRemixForm({\n        resolver: () => ({ values: {}, errors: {} }),\n        submitHandlers: {\n          onValid,\n          onInvalid,\n        },\n      }),\n    );\n\n    act(() => {\n      // biome-ignore lint/suspicious/noExplicitAny: <explanation>\n      result.current.handleSubmit({} as any);\n    });\n    await waitFor(() => {\n      expect(onValid).toHaveBeenCalled();\n    });\n  });\n\n  it(\"should reset isSubmitSuccessful after submission if reset is called\", async () => {\n    const { result } = renderHook(() =>\n      useRemixForm({\n        resolver: () => ({ values: {}, errors: {} }),\n      }),\n    );\n\n    act(() => {\n      // biome-ignore lint/suspicious/noExplicitAny: <explanation>\n      result.current.handleSubmit({} as any);\n    });\n    await waitFor(() => {\n      expect(result.current.formState.isSubmitSuccessful).toBe(true);\n    });\n    act(() => {\n      result.current.reset();\n    });\n    expect(result.current.formState.isSubmitSuccessful).toBe(false);\n  });\n\n  it(\"should submit the form data to the server when the form is valid\", async () => {\n    const { result } = renderHook(() =>\n      useRemixForm({\n        resolver: () => ({ values: {}, errors: {} }),\n        submitConfig: {\n          action: \"/submit\",\n        },\n      }),\n    );\n\n    act(() => {\n      // biome-ignore lint/suspicious/noExplicitAny: <explanation>\n      result.current.handleSubmit({} as any);\n    });\n    await waitFor(() => {\n      expect(submitMock).toHaveBeenCalledWith(expect.any(FormData), {\n        method: \"post\",\n        action: \"/submit\",\n      });\n    });\n  });\n\n  it(\"should submit the form data to the server using a fetcher when the form is valid\", async () => {\n    const {\n      result: { current: fetcher },\n    } = renderHook(() => useFetcher());\n    const { result } = renderHook(() =>\n      useRemixForm({\n        fetcher,\n        resolver: () => ({ values: {}, errors: {} }),\n        submitConfig: {\n          action: \"/submit\",\n        },\n      }),\n    );\n\n    act(() => {\n      // biome-ignore lint/suspicious/noExplicitAny: <explanation>\n      result.current.handleSubmit({} as any);\n    });\n    await waitFor(() => {\n      expect(fetcherSubmitMock).toHaveBeenCalledWith(expect.any(FormData), {\n        method: \"post\",\n        action: \"/submit\",\n      });\n    });\n  });\n\n  it(\"should remove origin and basename from the action\", async () => {\n    submitMock.mockReset();\n    useHrefMock.mockImplementation((to) => {\n      if (to === \"/\") {\n        return \"/my-basename\";\n      }\n    });\n    vi.spyOn(window, \"location\", \"get\").mockReturnValueOnce({\n      origin: \"http://example.com\",\n    } as any);\n\n    const { result } = renderHook(() =>\n      useRemixForm({\n        resolver: () => ({ values: {}, errors: {} }),\n      }),\n    );\n\n    act(() => {\n      // biome-ignore lint/suspicious/noExplicitAny: <explanation>\n      result.current.handleSubmit({\n        currentTarget: {\n          action: \"http://example.com/my-basename/basename-test-submit\",\n        },\n      } as any);\n    });\n    await waitFor(() => {\n      expect(submitMock).toHaveBeenCalledWith(expect.any(FormData), {\n        method: \"post\",\n        action: \"/basename-test-submit\",\n      });\n    });\n  });\n\n  it(\"should not re-render on validation if isValidating is not being accessed\", async () => {\n    const renderHookWithCount = () => {\n      let count = 0;\n      const renderCount = () => count;\n      const result = renderHook(() => {\n        count++;\n        return useRemixForm({\n          mode: \"onChange\",\n          resolver: () => ({\n            values: {\n              name: \"\",\n            },\n            errors: {},\n          }),\n        });\n      });\n      return { renderCount, ...result };\n    };\n\n    const { result, renderCount } = renderHookWithCount();\n\n    await act(async () => {\n      result.current.setValue(\"name\", \"John\", { shouldValidate: true });\n      await new Promise((resolve) => setTimeout(resolve, 0));\n    });\n\n    await act(async () => {\n      result.current.setValue(\"name\", \"Bob\", { shouldValidate: true });\n      await new Promise((resolve) => setTimeout(resolve, 0));\n    });\n\n    expect(renderCount()).toBe(1);\n  });\n\n  it(\"should re-render on validation if isValidating is being accessed\", async () => {\n    const renderHookWithCount = () => {\n      let count = 0;\n      const renderCount = () => count;\n      const result = renderHook(() => {\n        count++;\n        return useRemixForm({\n          mode: \"onChange\",\n          resolver: () => ({\n            values: {\n              name: \"\",\n            },\n            errors: {},\n          }),\n        });\n      });\n      return { renderCount, ...result };\n    };\n\n    const { result, renderCount } = renderHookWithCount();\n\n    // Accessing isValidating\n    // eslint-disable-next-line @typescript-eslint/no-unused-vars\n    const isValidating = result.current.formState.isValidating;\n\n    await act(async () => {\n      result.current.setValue(\"name\", \"John\", { shouldValidate: true });\n      await new Promise((resolve) => setTimeout(resolve, 0));\n    });\n\n    await act(async () => {\n      result.current.setValue(\"name\", \"Bob\", { shouldValidate: true });\n      await new Promise((resolve) => setTimeout(resolve, 0));\n    });\n\n    expect(renderCount()).toBe(3);\n  });\n\n  it(\"should not flash incorrect isSubmitting status\", async () => {\n    submitMock.mockReset();\n    useNavigationMock.mockClear();\n\n    const { result, rerender } = renderHook(() =>\n      useRemixForm({\n        resolver: () => ({ values: {}, errors: {} }),\n        submitConfig: {\n          action: \"/submit\",\n        },\n      }),\n    );\n\n    expect(result.current.formState.isSubmitting).toBe(false);\n\n    act(() => {\n      // biome-ignore lint/suspicious/noExplicitAny: <explanation>\n      result.current.handleSubmit({} as any);\n    });\n    expect(result.current.formState.isSubmitting).toBe(true);\n\n    await waitFor(() => expect(submitMock).toHaveBeenCalledTimes(1));\n\n    expect(result.current.formState.isSubmitting).toBe(true);\n\n    expect(submitMock).toHaveBeenCalledWith(expect.any(FormData), {\n      method: \"post\",\n      action: \"/submit\",\n    });\n\n    useNavigationMock.mockReturnValue({\n      state: \"submitting\",\n      formData: new FormData(),\n      json: undefined,\n    });\n    rerender();\n\n    expect(result.current.formState.isSubmitting).toBe(true);\n\n    useNavigationMock.mockReturnValue({\n      state: \"idle\",\n      formData: undefined,\n      json: undefined,\n    });\n    rerender();\n\n    expect(result.current.formState.isSubmitting).toBe(false);\n  });\n\n  it(\"should reset isSubmitting when the form is submitted using encType: application/json\", async () => {\n    submitMock.mockReset();\n    useNavigationMock.mockClear();\n\n    const { result, rerender } = renderHook(() =>\n      useRemixForm({\n        resolver: () => ({ values: {}, errors: {} }),\n        submitConfig: {\n          action: \"/submit\",\n          encType: \"application/json\",\n        },\n      }),\n    );\n\n    expect(result.current.formState.isSubmitting).toBe(false);\n\n    act(() => {\n      result.current.handleSubmit({} as any);\n    });\n    expect(result.current.formState.isSubmitting).toBe(true);\n\n    await waitFor(() => expect(submitMock).toHaveBeenCalledTimes(1));\n\n    expect(result.current.formState.isSubmitting).toBe(true);\n\n    expect(submitMock).toHaveBeenCalledWith(\n      {},\n      {\n        method: \"post\",\n        action: \"/submit\",\n        encType: \"application/json\",\n      },\n    );\n\n    useNavigationMock.mockReturnValue({\n      state: \"submitting\",\n      formData: undefined,\n      json: {},\n    });\n    rerender();\n\n    expect(result.current.formState.isSubmitting).toBe(true);\n\n    useNavigationMock.mockReturnValue({\n      state: \"idle\",\n      formData: undefined,\n      json: undefined,\n    });\n    rerender();\n\n    expect(result.current.formState.isSubmitting).toBe(false);\n  });\n\n  it(\"should return defaultValue from the register function\", async () => {\n    const { result, rerender } = renderHook(() =>\n      useRemixForm({\n        resolver: () => ({\n          values: { name: \"\", address: { street: \"\" } },\n          errors: {},\n        }),\n        defaultValues: {\n          name: \"Default name\",\n          address: {\n            street: \"Default street\",\n          },\n        },\n      }),\n    );\n\n    let nameFieldProps = result.current.register(\"name\");\n    let streetFieldProps = result.current.register(\"address.street\");\n\n    expect(nameFieldProps.defaultValue).toBe(\"Default name\");\n    expect(nameFieldProps.defaultChecked).toBe(undefined);\n    expect(streetFieldProps.defaultValue).toBe(\"Default street\");\n    expect(streetFieldProps.defaultChecked).toBe(undefined);\n\n    useActionDataMock.mockReturnValue({\n      defaultValues: {\n        name: \"Updated name\",\n        address: {\n          street: \"Updated street\",\n        },\n      },\n      errors: { name: \"Enter another name\" },\n    });\n\n    rerender();\n\n    nameFieldProps = result.current.register(\"name\");\n    streetFieldProps = result.current.register(\"address.street\");\n\n    expect(nameFieldProps.defaultValue).toBe(\"Updated name\");\n    expect(nameFieldProps.defaultChecked).toBe(undefined);\n    expect(streetFieldProps.defaultValue).toBe(\"Updated street\");\n    expect(streetFieldProps.defaultChecked).toBe(undefined);\n  });\n\n  it(\"should return defaultChecked from the register function when a boolean\", async () => {\n    const { result, rerender } = renderHook(() =>\n      useRemixForm({\n        resolver: () => ({\n          values: { name: \"\", address: { street: \"\" }, boolean: true },\n          errors: {},\n        }),\n        defaultValues: {\n          name: \"Default name\",\n          boolean: true,\n          address: {\n            street: \"Default street\",\n          },\n        },\n      }),\n    );\n\n    let booleanFieldProps = result.current.register(\"boolean\");\n\n    expect(booleanFieldProps.defaultChecked).toBe(true);\n    expect(booleanFieldProps.defaultValue).toBe(undefined);\n\n    useActionDataMock.mockReturnValue({\n      defaultValues: {\n        name: \"Updated name\",\n        address: {\n          street: \"Updated street\",\n        },\n        boolean: false,\n      },\n      errors: { name: \"Enter another name\" },\n    });\n\n    rerender();\n\n    booleanFieldProps = result.current.register(\"boolean\");\n    expect(booleanFieldProps.defaultChecked).toBe(false);\n    expect(booleanFieldProps.defaultValue).toBe(undefined);\n  });\n});\n\nafterEach(cleanup);\n\ndescribe(\"RemixFormProvider\", () => {\n  it(\"should allow the user to submit via the useRemixForm handleSubmit using the context\", () => {\n    const { result } = renderHook(() =>\n      useRemixForm({\n        resolver: () => ({ values: {}, errors: {} }),\n        submitConfig: {\n          action: \"/submit\",\n        },\n      }),\n    );\n    const spy = vi.spyOn(result.current, \"handleSubmit\");\n\n    const TestComponent = () => {\n      const { handleSubmit } = useRemixFormContext();\n      return <form onSubmit={handleSubmit} data-testid=\"test\" />;\n    };\n\n    const { getByTestId } = render(\n      <RemixFormProvider {...result.current}>\n        <TestComponent />\n      </RemixFormProvider>,\n    );\n\n    const form = getByTestId(\"test\") as HTMLFormElement;\n    fireEvent.submit(form);\n\n    expect(spy).toHaveBeenCalled();\n  });\n});\n"
  },
  {
    "path": "src/hook/index.tsx",
    "content": "import React, {\n  type FormEvent,\n  type ReactNode,\n  useEffect,\n  useMemo,\n  useState,\n} from \"react\";\nimport {\n  type DefaultValues,\n  type FieldValues,\n  FormProvider,\n  type FormState,\n  type KeepStateOptions,\n  type Path,\n  type RegisterOptions,\n  type SubmitErrorHandler,\n  type SubmitHandler,\n  type UseFormHandleSubmit,\n  type UseFormProps,\n  type UseFormReturn,\n  get,\n  useForm,\n  useFormContext,\n} from \"react-hook-form\";\nimport {\n  type FetcherWithComponents,\n  type FormEncType,\n  type FormMethod,\n  type SubmitFunction,\n  useActionData,\n  useHref,\n  useNavigation,\n  useSubmit,\n} from \"react-router\";\n\nimport { createFormData } from \"../utilities\";\n\nexport type SubmitFunctionOptions = Parameters<SubmitFunction>[1];\n\nexport interface UseRemixFormOptions<\n  TFieldValues extends FieldValues,\n  // biome-ignore lint/suspicious/noExplicitAny: defaults to any type\n  TContext = any,\n  TTransformedValues = TFieldValues,\n> extends UseFormProps<TFieldValues, TContext, TTransformedValues> {\n  submitHandlers?: {\n    onValid?: SubmitHandler<TTransformedValues>;\n    onInvalid?: SubmitErrorHandler<TFieldValues>;\n  };\n  submitConfig?: SubmitFunctionOptions;\n  submitData?: FieldValues;\n  fetcher?: FetcherWithComponents<unknown>;\n  /**\n   * If true, all values will be stringified before being sent to the server, otherwise everything but strings will be stringified (default: true)\n   */\n  stringifyAllValues?: boolean;\n}\nexport const useRemixForm = <\n  TFieldValues extends FieldValues,\n  // biome-ignore lint/suspicious/noExplicitAny: defaults to any type\n  TContext = any,\n  TTransformedValues = TFieldValues,\n>({\n  submitHandlers,\n  submitConfig,\n  submitData,\n  fetcher,\n  stringifyAllValues = true,\n  ...formProps\n}: UseRemixFormOptions<TFieldValues, TContext, TTransformedValues>) => {\n  const [isSubmittedSuccessfully, setIsSubmittedSuccessfully] = useState(false);\n  const basename = useHref(\"/\");\n  const actionSubmit = useSubmit();\n  const actionData = useActionData();\n  const submit = fetcher?.submit ?? actionSubmit;\n  // biome-ignore lint/suspicious/noExplicitAny: <explanation>\n  const data: any = fetcher?.data ?? actionData;\n  const methods = useForm({ ...formProps, errors: data?.errors });\n  const navigation = useNavigation();\n  // Either it's submitted to an action or submitted to a fetcher (or neither)\n  const isSubmittingForm = useMemo(() => {\n    const navigationIsSubmitting =\n      navigation.state !== \"idle\" &&\n      (navigation.formData ?? navigation.json) !== undefined;\n\n    const fetcherIsSubmitting =\n      fetcher?.state !== \"idle\" &&\n      (fetcher?.formData ?? fetcher?.json) !== undefined;\n\n    return navigationIsSubmitting || fetcherIsSubmitting;\n  }, [\n    navigation.state,\n    navigation.formData,\n    navigation.json,\n    fetcher?.state,\n    fetcher?.formData,\n    fetcher?.json,\n  ]);\n\n  // A state to keep track whether we're actually submitting the form through the network\n  const [isSubmittingNetwork, setIsSubmittingNetwork] = useState(false);\n  // When the network submission is done, set the state to `false`\n  useEffect(() => {\n    if (!isSubmittingForm) {\n      setIsSubmittingNetwork(false);\n    }\n  }, [isSubmittingForm]);\n\n  // Submits the data to the server when form is valid\n  const onSubmit = useMemo(\n    () =>\n      (\n        data: TTransformedValues,\n        // biome-ignore lint/suspicious/noExplicitAny: <explanation>\n        e: any,\n        formEncType?: FormEncType,\n        formMethod?: FormMethod,\n        formAction?: string,\n      ) => {\n        setIsSubmittingNetwork(true);\n        setIsSubmittedSuccessfully(true);\n        const encType = submitConfig?.encType ?? formEncType;\n        const method = submitConfig?.method ?? formMethod ?? \"post\";\n        const action = submitConfig?.action ?? formAction;\n        const submitPayload = { ...data, ...submitData };\n        const formData =\n          encType === \"application/json\"\n            ? submitPayload\n            : createFormData(submitPayload, stringifyAllValues);\n\n        submit(formData, {\n          ...submitConfig,\n          method,\n          encType,\n          action,\n        });\n      },\n    [submit, submitConfig, submitData, stringifyAllValues],\n  );\n\n  // eslint-disable-next-line @typescript-eslint/no-empty-function\n  const onInvalid = useMemo(() => () => {}, []);\n\n  // React-hook-form uses lazy property getters to avoid re-rendering when properties\n  // that aren't being used change. Using getters here preservers that lazy behavior.\n  const formState: FormState<TFieldValues> = useMemo(\n    () => ({\n      get isDirty() {\n        return methods.formState.isDirty;\n      },\n      get isLoading() {\n        return methods.formState.isLoading;\n      },\n      get isSubmitted() {\n        return methods.formState.isSubmitted;\n      },\n      get isSubmitSuccessful() {\n        return isSubmittedSuccessfully || methods.formState.isSubmitSuccessful;\n      },\n      get isSubmitting() {\n        return isSubmittingNetwork || methods.formState.isSubmitting;\n      },\n      get isValidating() {\n        return methods.formState.isValidating;\n      },\n      get isValid() {\n        return methods.formState.isValid;\n      },\n      get disabled() {\n        return methods.formState.disabled;\n      },\n      get submitCount() {\n        return methods.formState.submitCount;\n      },\n      get defaultValues() {\n        return methods.formState.defaultValues;\n      },\n      get dirtyFields() {\n        return methods.formState.dirtyFields;\n      },\n      get touchedFields() {\n        return methods.formState.touchedFields;\n      },\n      get validatingFields() {\n        return methods.formState.validatingFields;\n      },\n      get errors() {\n        return methods.formState.errors;\n      },\n    }),\n    [methods.formState, isSubmittedSuccessfully, isSubmittingNetwork],\n  );\n  const reset = useMemo(\n    () =>\n      (\n        values?: TFieldValues | DefaultValues<TFieldValues> | undefined,\n        options?: KeepStateOptions,\n      ) => {\n        setIsSubmittedSuccessfully(false);\n        methods.reset(values, options);\n      },\n    [methods.reset],\n  );\n\n  const register = useMemo(\n    () =>\n      (\n        name: Path<TFieldValues>,\n        options?: RegisterOptions<TFieldValues> & {\n          disableProgressiveEnhancement?: boolean;\n        },\n      ) => {\n        const defaultValue =\n          get(data?.defaultValues, name) ??\n          get(methods.formState.defaultValues, name);\n        return {\n          ...methods.register(name, options),\n          ...(!options?.disableProgressiveEnhancement && {\n            defaultValue:\n              typeof defaultValue === \"string\" ? defaultValue : undefined,\n            defaultChecked:\n              typeof defaultValue === \"boolean\" ? defaultValue : undefined,\n          }),\n        };\n      },\n    [methods.register, data?.defaultValues, methods.formState.defaultValues],\n  );\n\n  const handleSubmit = useMemo(\n    () => (e?: FormEvent<HTMLFormElement>) => {\n      const encType = e?.currentTarget?.enctype as FormEncType | undefined;\n      const method = e?.currentTarget?.method as FormMethod | undefined;\n      const action = e?.currentTarget?.action.replace(\n        `${window.location.origin}${basename === \"/\" ? \"\" : basename}`,\n        \"\",\n      );\n\n      const onValidHandler = submitHandlers?.onValid ?? onSubmit;\n      const onInvalidHandler = submitHandlers?.onInvalid ?? onInvalid;\n\n      return methods.handleSubmit(\n        (data, e) => onValidHandler(data, e, encType, method, action),\n        onInvalidHandler,\n      )(e);\n    },\n    [methods.handleSubmit, submitHandlers, onSubmit, onInvalid, basename],\n  );\n\n  const hookReturn = useMemo(\n    () => ({\n      ...methods,\n      handleSubmit,\n      reset,\n      register,\n      formState,\n    }),\n    [methods, handleSubmit, reset, register, formState],\n  );\n\n  return hookReturn;\n};\nexport type UseRemixFormReturn<\n  TFieldValues extends FieldValues = FieldValues,\n  // biome-ignore lint/suspicious/noExplicitAny: defaults to any type\n  TContext = any,\n  TTransformedValues = TFieldValues,\n> = UseFormReturn<TFieldValues, TContext, TTransformedValues> & {\n  handleSubmit: ReturnType<typeof useRemixForm>[\"handleSubmit\"];\n  reset: ReturnType<typeof useRemixForm>[\"reset\"];\n  register: ReturnType<typeof useRemixForm>[\"register\"];\n};\ninterface RemixFormProviderProps<\n  TFieldValues extends FieldValues = FieldValues,\n  // biome-ignore lint/suspicious/noExplicitAny: defaults to any type\n  TContext = any,\n  TTransformedValues = TFieldValues,\n> extends Omit<\n    UseFormReturn<TFieldValues, TContext, TTransformedValues>,\n    \"handleSubmit\" | \"reset\"\n  > {\n  children: ReactNode;\n  // biome-ignore lint/suspicious/noExplicitAny: <explanation>\n  handleSubmit: any;\n  // biome-ignore lint/suspicious/noExplicitAny: <explanation>\n  register: any;\n  // biome-ignore lint/suspicious/noExplicitAny: <explanation>\n  reset: any;\n}\nexport const RemixFormProvider = <\n  TFieldValues extends FieldValues = FieldValues,\n  // biome-ignore lint/suspicious/noExplicitAny: defaults to any type\n  TContext = any,\n  TTransformedValues = TFieldValues,\n>({\n  children,\n  ...props\n}: RemixFormProviderProps<TFieldValues, TContext, TTransformedValues>) => {\n  return <FormProvider {...props}>{children}</FormProvider>;\n};\nexport const useRemixFormContext = <\n  TFieldValues extends FieldValues,\n  // biome-ignore lint/suspicious/noExplicitAny: defaults to any type\n  TContext = any,\n  TTransformedValues = TFieldValues,\n>() => {\n  const methods = useFormContext<TFieldValues, TContext, TTransformedValues>();\n  return {\n    ...methods,\n    // biome-ignore lint/suspicious/noExplicitAny: <explanation>\n    handleSubmit: methods.handleSubmit as any as ReturnType<\n      UseFormHandleSubmit<TFieldValues, TTransformedValues>\n    >,\n  };\n};\n"
  },
  {
    "path": "src/index.ts",
    "content": "export {\n  parseFormData,\n  createFormData,\n  getValidatedFormData,\n  validateFormData,\n  getFormDataFromSearchParams,\n  generateFormData,\n} from \"./utilities\";\nexport * from \"./hook\";\nexport type { UseRemixFormOptions } from \"./hook\";\n"
  },
  {
    "path": "src/middleware/index.ts",
    "content": "import type { FieldValues, Resolver } from \"react-hook-form\";\nimport {\n  // data as dataFn,\n  type unstable_MiddlewareFunction,\n  type unstable_RouterContextProvider,\n  unstable_createContext,\n} from \"react-router\";\nimport {\n  getFormData as getFormDataUtility,\n  validateFormData,\n} from \"../utilities\";\n\nconst formDataContext = unstable_createContext<unknown>();\n\nexport function unstable_extractFormDataMiddleware({\n  preserveStringified = false,\n} = {}): unstable_MiddlewareFunction {\n  return async function extractFormDataMiddleware({ request, context }, next) {\n    const cloneRequest = request.clone();\n    const { receivedValues: formData } = await getFormDataUtility(\n      cloneRequest,\n      preserveStringified,\n    );\n\n    context.set(formDataContext, formData);\n    return next();\n\n    /*  if (res instanceof Response && res.status === 422) {\n      console.log(\"middleware response 422\", res);\n\n      return new Response(res.body, { status: 200 });\n    } */\n  };\n}\n\nexport const getFormData = (context: unstable_RouterContextProvider) =>\n  context.get(formDataContext);\n\nexport const getValidatedFormData = async <\n  TFieldValues extends FieldValues,\n  // biome-ignore lint/suspicious/noExplicitAny: defaults to any type\n  TContext = any,\n  TTransformedValues = TFieldValues,\n>(\n  context: unstable_RouterContextProvider,\n  resolver: Resolver<TFieldValues, TContext, TTransformedValues>,\n) => {\n  const formData = context.get(formDataContext);\n  const data = await validateFormData<\n    TFieldValues,\n    TContext,\n    TTransformedValues\n  >(formData, resolver);\n  /* if (errors) {\n    throw dataFn(\n      { errors, receivedValues: formData },\n      {\n        status: 422,\n        headers: new Headers({ \"X-Remix-Hook-Form-Validation-Error\": \"true\" }),\n      },\n    );\n  } */\n  return { ...data, receivedValues: formData };\n};\n"
  },
  {
    "path": "src/utilities/index.test.ts",
    "content": "import { array, boolean, coerce, date, number, object, string } from \"zod\";\nimport {\n  createFormData,\n  generateFormData,\n  getFormDataFromSearchParams,\n  getValidatedFormData,\n  isGet,\n  parseFormData,\n  validateFormData,\n} from \"./index\";\n\nimport { zodResolver } from \"@hookform/resolvers/zod\";\n\ndescribe(\"createFormData\", () => {\n  it(\"should create a FormData object with the provided data\", () => {\n    const data = {\n      name: \"John Doe\",\n      age: 30,\n      bool: true,\n      object: {\n        test: \"1\",\n        number: 2,\n      },\n      array: [1, 2, 3],\n    };\n    const formData = createFormData(data);\n\n    expect(formData.get(\"name\")).toEqual(JSON.stringify(data.name));\n    expect(formData.get(\"age\")).toEqual(data.age.toString());\n    expect(formData.get(\"object\")).toEqual(JSON.stringify(data.object));\n    expect(formData.get(\"array\")).toEqual(JSON.stringify(data.array));\n    expect(formData.get(\"bool\")).toEqual(data.bool.toString());\n  });\n\n  it(\"should create a FormData object with the provided data and remove undefined values\", () => {\n    const data = {\n      name: \"John Doe\",\n      age: 30,\n      bool: true,\n      b: undefined,\n      a: null,\n      object: {\n        test: \"1\",\n        number: 2,\n      },\n      array: [1, 2, 3],\n    };\n    const formData = createFormData(data);\n\n    expect(formData.get(\"name\")).toEqual(JSON.stringify(data.name));\n    expect(formData.get(\"age\")).toEqual(data.age.toString());\n    expect(formData.get(\"object\")).toEqual(JSON.stringify(data.object));\n    expect(formData.get(\"array\")).toEqual(JSON.stringify(data.array));\n    expect(formData.get(\"bool\")).toEqual(data.bool.toString());\n    expect(formData.get(\"b\")).toEqual(null);\n    expect(formData.get(\"a\")).toEqual(\"null\");\n  });\n\n  it(\"should handle null data\", () => {\n    // biome-ignore lint/suspicious/noExplicitAny: <explanation>\n    const formData = createFormData(null as any);\n    expect(formData).toBeTruthy();\n  });\n\n  it(\"should handle undefined data\", () => {\n    // biome-ignore lint/suspicious/noExplicitAny: <explanation>\n    const formData = createFormData(undefined as any);\n    expect(formData).toBeTruthy();\n  });\n\n  it(\"should handle Date data\", () => {\n    const date = \"2024-01-01T00:00:00.000Z\";\n    const formData = createFormData({ date: new Date(date) }, false);\n    expect(formData.get(\"date\")).toEqual(date);\n  });\n});\n\ndescribe(\"parseFormData\", () => {\n  // Mock the Request and formData methods\n  beforeAll(() => {\n    global.Request = vi.fn();\n    global.Request.prototype.formData = vi.fn();\n  });\n\n  // Reset the mocks after each test\n  afterEach(() => {\n    vi.resetAllMocks();\n  });\n\n  it(\"should parse the data from the request object\", async () => {\n    const data = {\n      name: \"John Doe\",\n      age: \"30\",\n    };\n    const request = new Request(\"http://localhost:3000\");\n    const requestFormDataSpy = vi.spyOn(request, \"formData\");\n    requestFormDataSpy.mockResolvedValueOnce(createFormData(data));\n    const parsedData = await parseFormData<typeof data>(request);\n    expect(parsedData).toEqual(data);\n  });\n\n  it(\"should return an empty object if no formData exists\", async () => {\n    const request = new Request(\"http://localhost:3000\");\n    const requestFormDataSpy = vi.spyOn(request, \"formData\");\n    requestFormDataSpy.mockResolvedValueOnce(createFormData({}));\n    const parsedData = await parseFormData(request);\n    expect(parsedData).toEqual({});\n  });\n\n  it(\"should return formData if NO js was used and formData was passed as is\", async () => {\n    const formData = new FormData();\n    formData.append(\"name\", \"John Doe\");\n    formData.append(\"age\", \"30\");\n    formData.append(\"hobbies.0\", \"Reading\");\n    formData.append(\"hobbies.1\", \"Writing\");\n    formData.append(\"hobbies.2\", \"Coding\");\n    formData.append(\"other.skills.0\", \"testing\");\n    formData.append(\"other.skills.1\", \"testing\");\n    formData.append(\"other.something\", \"else\");\n    const request = new Request(\"http://localhost:3000\");\n    const requestFormDataSpy = vi.spyOn(request, \"formData\");\n    requestFormDataSpy.mockResolvedValueOnce(formData);\n    const parsedData = await parseFormData(request);\n    expect(parsedData).toEqual({\n      name: \"John Doe\",\n      age: 30,\n      hobbies: [\"Reading\", \"Writing\", \"Coding\"],\n      other: {\n        skills: [\"testing\", \"testing\"],\n        something: \"else\",\n      },\n    });\n  });\n\n  it(\"should handle multiple file input\", async () => {\n    const request = new Request(\"http://localhost:3000\");\n    const requestFormDataSpy = vi.spyOn(request, \"formData\");\n    const blob = new Blob([\"Hello, world!\"], { type: \"text/plain\" });\n    const mockFormData = new FormData();\n    mockFormData.append(\"files\", blob);\n    mockFormData.append(\"files\", blob);\n    requestFormDataSpy.mockResolvedValueOnce(mockFormData);\n    const data = await parseFormData<{ files: Blob[] }>(request);\n    expect(data.files).toBeTypeOf(\"object\");\n    expect(Array.isArray(data.files)).toBe(true);\n    expect(data.files).toHaveLength(2);\n    expect(data.files[0]).toBeInstanceOf(File);\n    expect(data.files[1]).toBeInstanceOf(File);\n  });\n\n  it(\"should not throw an error when a file is passed in\", async () => {\n    const request = new Request(\"http://localhost:3000\");\n    const requestFormDataSpy = vi.spyOn(request, \"formData\");\n    const blob = new Blob([\"Hello, world!\"], { type: \"text/plain\" });\n    const mockFormData = new FormData();\n    mockFormData.append(\"file\", blob);\n    requestFormDataSpy.mockResolvedValueOnce(mockFormData);\n    const data = await parseFormData<{ file: Blob }>(request);\n    expect(data.file).toBeTypeOf(\"object\");\n  });\n});\n\ndescribe(\"generateFormData\", () => {\n  it(\"should generate an output object for flat form data\", () => {\n    const formData = new FormData();\n    formData.append(\"name\", \"John Doe\");\n    formData.append(\"email\", \"johndoe@example.com\");\n\n    const expectedOutput = {\n      name: \"John Doe\",\n      email: \"johndoe@example.com\",\n    };\n\n    expect(generateFormData(formData)).toEqual(expectedOutput);\n  });\n\n  it(\"should generate an output object for nested form data\", () => {\n    const formData = new FormData();\n    formData.append(\"user.name.first\", \"John\");\n    formData.append(\"user.name.last\", \"Doe\");\n    formData.append(\"user.email\", \"johndoe@example.com\");\n\n    const expectedOutput = {\n      user: {\n        name: {\n          first: \"John\",\n          last: \"Doe\",\n        },\n        email: \"johndoe@example.com\",\n      },\n    };\n\n    expect(generateFormData(formData)).toEqual(expectedOutput);\n  });\n\n  it(\"should generate an output object with arrays for integer indexes\", () => {\n    const formData = new FormData();\n    formData.append(\"user.roles.0\", \"admin\");\n    formData.append(\"user.roles.1\", \"editor\");\n    formData.append(\"user.roles.2\", \"contributor\");\n\n    const expectedOutput = {\n      user: {\n        roles: [\"admin\", \"editor\", \"contributor\"],\n      },\n    };\n\n    expect(generateFormData(formData)).toEqual(expectedOutput);\n  });\n});\n\ndescribe(\"isGet\", () => {\n  it(\"returns true if the request method is GET\", () => {\n    const request = { method: \"GET\" };\n    expect(isGet(request)).toBe(true);\n  });\n\n  it(\"returns true if the request method is get\", () => {\n    const request = { method: \"get\" };\n    expect(isGet(request)).toBe(true);\n  });\n\n  it(\"returns false if the request method is POST\", () => {\n    const request = { method: \"POST\" };\n    expect(isGet(request)).toBe(false);\n  });\n});\n\ndescribe(\"getFormDataFromSearchParams\", () => {\n  it(\"should return an empty FormData object when there are no search params\", () => {\n    const request = {\n      url: \"http://localhost:3000/\",\n    };\n\n    const formData = getFormDataFromSearchParams(request);\n    expect(formData).toStrictEqual({});\n  });\n\n  it(\"should return a FormData object with search params\", () => {\n    const request = {\n      url: \"http://localhost:3000/?name=John+Doe&email=johndoe@example.com\",\n    };\n\n    const formData = getFormDataFromSearchParams(request);\n    expect(formData).toStrictEqual({\n      name: \"John Doe\",\n      email: \"johndoe@example.com\",\n    });\n  });\n\n  it(\"should return a FormData object with nested search params\", () => {\n    const request = {\n      url: \"http://localhost:3000/?user.name.first=John&user.name.last=Doe&user.email=johndoe@example.com\",\n    };\n\n    const formData = getFormDataFromSearchParams(request);\n\n    expect(formData).toStrictEqual({\n      user: {\n        name: {\n          first: \"John\",\n          last: \"Doe\",\n        },\n        email: \"johndoe@example.com\",\n      },\n    });\n  });\n\n  it(\"should convert array search params to FormData object with arrays\", () => {\n    const request = {\n      url: \"http://localhost:3000/?colors[]=red&colors[]=green&colors[]=blue&numbers[0]=1&numbers[1]=2&numbers[2]=3\",\n    };\n    const formData = getFormDataFromSearchParams(request);\n\n    expect(formData).toStrictEqual({\n      colors: [\"red\", \"green\", \"blue\"],\n      numbers: [1, 2, 3],\n    });\n  });\n});\n\ndescribe(\"validateFormData\", () => {\n  it(\"should return an error when parsing date object without coerce\", async () => {\n    const formData = createFormData({\n      date: new Date(2024, 1, 1),\n    });\n    const returnData = await validateFormData(\n      formData,\n      zodResolver(object({ date: date() })),\n    );\n    expect(returnData.errors).toStrictEqual({\n      date: {\n        message: \"Expected date, received string\",\n        type: \"invalid_type\",\n        ref: undefined,\n      },\n    });\n    expect(returnData.data).toStrictEqual(undefined);\n  });\n\n  it(\"should return an error when parsing date object with coerce\", async () => {\n    const formData = createFormData({\n      date: new Date(2024, 1, 1),\n    });\n    const returnData = await validateFormData(\n      formData,\n      zodResolver(object({ date: coerce.date() })),\n    );\n    expect(returnData.errors).toStrictEqual({\n      date: {\n        message: \"Invalid date\",\n        type: \"invalid_date\",\n        ref: undefined,\n      },\n    });\n    expect(returnData.data).toStrictEqual(undefined);\n  });\n\n  it(\"should return a correct value when formatting as object\", async () => {\n    const formData = createFormData(\n      {\n        date: new Date(2024, 1, 1),\n      },\n      false,\n    );\n    const returnData = await validateFormData(\n      formData,\n      zodResolver(object({ date: coerce.date() })),\n    );\n    expect(returnData.errors).toStrictEqual(undefined);\n    expect(returnData.data).toStrictEqual({\n      date: new Date(2024, 1, 1),\n    });\n  });\n\n  it(\"should return an empty error object and valid data if there are no errors\", async () => {\n    const formData = {\n      name: \"John Doe\",\n      email: \"email@email.com\",\n    };\n\n    const returnData = await validateFormData(\n      formData,\n      zodResolver(object({ name: string(), email: string().email() })),\n    );\n    expect(returnData.errors).toStrictEqual(undefined);\n    expect(returnData.data).toStrictEqual(formData);\n  });\n\n  it(\"should return an error object and no data if there are errors\", async () => {\n    const formData = {\n      email: \"email\",\n      name: \"John Doe\",\n    };\n    const returnData = await validateFormData(\n      formData,\n      zodResolver(object({ name: string(), email: string().email() })),\n    );\n    expect(returnData.errors).toStrictEqual({\n      email: {\n        message: \"Invalid email\",\n        type: \"invalid_string\",\n        ref: undefined,\n      },\n    });\n    expect(returnData.data).toStrictEqual(undefined);\n  });\n});\n\ndescribe(\"createFormData\", () => {\n  it(\"accepts a file array and maps it properly\", async () => {\n    const formData = createFormData({\n      files: [new File([\"test\"], \"test.txt\"), new File([\"test2\"], \"test2.txt\")],\n    });\n    const files = formData.getAll(\"files\");\n    expect(files).toHaveLength(2);\n    expect(files[0]).toBeInstanceOf(File);\n    expect(files[1]).toBeInstanceOf(File);\n  });\n\n  it(\"accepts a blob array and maps it properly\", async () => {\n    const formData = createFormData({\n      files: [\n        new Blob([\"test\"], { type: \"text/plain\" }),\n        new Blob([\"test2\"], { type: \"text/plain\" }),\n      ],\n    });\n    const files = formData.getAll(\"files\");\n    expect(files).toHaveLength(2);\n    expect(files[0]).toBeInstanceOf(Blob);\n    expect(files[1]).toBeInstanceOf(Blob);\n  });\n\n  it(\"accepts a file and adds it properly to formData\", async () => {\n    const formData = createFormData({\n      file: new File([\"test\"], \"test.txt\"),\n    });\n    const file = formData.get(\"file\");\n    expect(file).toBeInstanceOf(File);\n  });\n\n  it(\"accepts a blob and adds it properly to formData\", async () => {\n    const formData = createFormData({\n      file: new Blob([\"test\"], { type: \"text/plain\" }),\n    });\n    const file = formData.get(\"file\");\n    expect(file).toBeInstanceOf(Blob);\n  });\n\n  it(\"doesn't mess up types when passed from frontend to backend\", async () => {\n    const formData = createFormData({\n      name: \"123\",\n      age: 30,\n      hobbies: [\"Reading\", \"Writing\", \"Coding\"],\n      boolean: true,\n      numbers: [1, 2, 3],\n      other: {\n        skills: [\"testing\", \"testing\"],\n        something: \"else\",\n      },\n    });\n    const request = new Request(\"http://localhost:3000\", { method: \"POST\" });\n    const requestFormDataSpy = vi.spyOn(request, \"formData\");\n\n    requestFormDataSpy.mockResolvedValueOnce(formData);\n    const parsed = await parseFormData<typeof formData>(request);\n\n    expect(parsed).toStrictEqual({\n      name: \"123\",\n      age: 30,\n      hobbies: [\"Reading\", \"Writing\", \"Coding\"],\n      boolean: true,\n      numbers: [1, 2, 3],\n      other: {\n        skills: [\"testing\", \"testing\"],\n        something: \"else\",\n      },\n    });\n  });\n\n  it(\"doesn't send undefined values to the backend but sends null values\", async () => {\n    const formData = createFormData({\n      name: \"123\",\n      age: 30,\n      hobbies: [\"Reading\", \"Writing\", \"Coding\"],\n      boolean: true,\n      a: null,\n      b: undefined,\n      numbers: [1, 2, 3],\n      other: {\n        skills: [\"testing\", \"testing\"],\n        something: \"else\",\n      },\n    });\n    const request = new Request(\"http://localhost:3000\", { method: \"POST\" });\n    const requestFormDataSpy = vi.spyOn(request, \"formData\");\n\n    requestFormDataSpy.mockResolvedValueOnce(formData);\n    const parsed = await parseFormData<typeof formData>(request);\n\n    expect(parsed).toStrictEqual({\n      name: \"123\",\n      age: 30,\n      hobbies: [\"Reading\", \"Writing\", \"Coding\"],\n      boolean: true,\n      a: null,\n      numbers: [1, 2, 3],\n      other: {\n        skills: [\"testing\", \"testing\"],\n        something: \"else\",\n      },\n    });\n  });\n});\n\ndescribe(\"getValidatedFormData\", () => {\n  it(\"gets valid form data from formData\", async () => {\n    const formData = {\n      name: \"John Doe\",\n      age: 30,\n      hobbies: [\"Reading\", \"Writing\", \"Coding\"],\n      boolean: true,\n      numbers: [1, 2, 3],\n      other: {\n        skills: [\"testing\", \"testing\"],\n        something: \"else\",\n      },\n    };\n    const data = createFormData(formData);\n    const schema = object({\n      name: string(),\n      age: number(),\n      boolean: boolean(),\n      numbers: array(number()),\n      hobbies: array(string()),\n      other: object({\n        skills: array(string()),\n        something: string(),\n      }),\n    });\n    const validatedFormData = await getValidatedFormData(\n      data,\n      zodResolver(schema),\n    );\n    expect(validatedFormData).toStrictEqual({\n      data: formData,\n      receivedValues: formData,\n      errors: undefined,\n    });\n  });\n\n  it(\"gets valid form data from a GET request\", async () => {\n    const request = {\n      method: \"GET\",\n      url: \"http://localhost:3000/?user.name=john&colors[]=red&colors[]=green&colors[]=blue&numbers[0]=1&numbers[1]=2&numbers[2]=3\",\n    };\n\n    const schema = object({\n      user: object({\n        name: string(),\n      }),\n      colors: array(string()),\n      numbers: array(number()),\n    });\n    const formData = await getValidatedFormData(\n      // biome-ignore lint/suspicious/noExplicitAny: <explanation>\n      request as any,\n      zodResolver(schema),\n    );\n    expect(formData).toStrictEqual({\n      data: {\n        user: {\n          name: \"john\",\n        },\n        colors: [\"red\", \"green\", \"blue\"],\n        numbers: [1, 2, 3],\n      },\n      receivedValues: {\n        user: {\n          name: \"john\",\n        },\n        colors: [\"red\", \"green\", \"blue\"],\n        numbers: [1, 2, 3],\n      },\n      errors: undefined,\n    });\n  });\n\n  it(\"gets valid form data from a POST request when it is js\", async () => {\n    const formData = {\n      name: \"John Doe\",\n      age: 30,\n      hobbies: [\"Reading\", \"Writing\", \"Coding\"],\n      boolean: true,\n      numbers: [1, 2, 3],\n      other: {\n        skills: [\"testing\", \"testing\"],\n        something: \"else\",\n      },\n    };\n    const request = new Request(\"http://localhost:3000\", { method: \"POST\" });\n    const requestFormDataSpy = vi.spyOn(request, \"formData\");\n    const data = createFormData(formData);\n    requestFormDataSpy.mockResolvedValueOnce(data);\n\n    const schema = object({\n      name: string(),\n      age: number(),\n      boolean: boolean(),\n      numbers: array(number()),\n      hobbies: array(string()),\n      other: object({\n        skills: array(string()),\n        something: string(),\n      }),\n    });\n    const validatedFormData = await getValidatedFormData(\n      // biome-ignore lint/suspicious/noExplicitAny: <explanation>\n      request as any,\n      zodResolver(schema),\n    );\n    expect(validatedFormData).toStrictEqual({\n      data: formData,\n      receivedValues: formData,\n      errors: undefined,\n    });\n  });\n\n  it(\"gets valid form data from a POST request when it is no js\", async () => {\n    const formData = new FormData();\n    formData.append(\"name\", \"John Doe\");\n    formData.append(\"age\", \"30\");\n    formData.append(\"hobbies.0\", \"Reading\");\n    formData.append(\"hobbies.1\", \"Writing\");\n    formData.append(\"hobbies.2\", \"Coding\");\n    formData.append(\"other.skills.0\", \"testing\");\n    formData.append(\"other.skills.1\", \"testing\");\n    formData.append(\"other.something\", \"else\");\n    const request = new Request(\"http://localhost:3000\", { method: \"POST\" });\n    const requestFormDataSpy = vi.spyOn(request, \"formData\");\n    requestFormDataSpy.mockResolvedValueOnce(formData);\n\n    const schema = object({\n      name: string(),\n      age: number(),\n      hobbies: array(string()),\n      other: object({\n        skills: array(string()),\n        something: string(),\n      }),\n    });\n    const returnData = await getValidatedFormData(\n      // biome-ignore lint/suspicious/noExplicitAny: <explanation>\n      request as any,\n      zodResolver(schema),\n    );\n    expect(returnData).toStrictEqual({\n      data: {\n        name: \"John Doe\",\n        age: 30,\n        hobbies: [\"Reading\", \"Writing\", \"Coding\"],\n        other: {\n          skills: [\"testing\", \"testing\"],\n          something: \"else\",\n        },\n      },\n      receivedValues: {\n        name: \"John Doe\",\n        age: 30,\n        hobbies: [\"Reading\", \"Writing\", \"Coding\"],\n        other: {\n          skills: [\"testing\", \"testing\"],\n          something: \"else\",\n        },\n      },\n      errors: undefined,\n    });\n  });\n});\n"
  },
  {
    "path": "src/utilities/index.ts",
    "content": "import type { FieldErrors, FieldValues, Resolver } from \"react-hook-form\";\n\nconst tryParseJSON = (value: string | File | Blob) => {\n  if (value instanceof File || value instanceof Blob) {\n    return value;\n  }\n  try {\n    const json = JSON.parse(value);\n\n    return json;\n  } catch (e) {\n    return value;\n  }\n};\n\n/**\n * Generates an output object from the given form data, where the keys in the output object retain\n * the structure of the keys in the form data. Keys containing integer indexes are treated as arrays.\n */\nexport const generateFormData = (\n  formData: FormData | URLSearchParams,\n  preserveStringified = false,\n) => {\n  // Initialize an empty output object.\n  // biome-ignore lint/suspicious/noExplicitAny: <explanation>\n  const outputObject: Record<any, any> = {};\n\n  // See if a key is repeated, and then handle that in a special case\n  const keyCounts: Record<string, number> = {};\n  for (const key of formData.keys()) {\n    keyCounts[key] = (keyCounts[key] ?? 0) + 1;\n  }\n\n  // Iterate through each key-value pair in the form data.\n  for (const [key, value] of formData.entries()) {\n    // Get the current key's count\n    const keyCount = keyCounts[key];\n\n    // Try to convert data to the original type, otherwise return the original value\n    const data = preserveStringified ? value : tryParseJSON(value);\n    // Split the key into an array of parts.\n    const keyParts = key.split(\".\");\n    // Initialize a variable to point to the current object in the output object.\n    let currentObject = outputObject;\n\n    // Iterate through each key part except for the last one.\n    for (let i = 0; i < keyParts.length - 1; i++) {\n      // Get the current key part.\n      const keyPart = keyParts[i];\n      // If the current object doesn't have a property with the current key part,\n      // initialize it as an object or array depending on whether the next key part is a valid integer index or not.\n      if (!currentObject[keyPart]) {\n        currentObject[keyPart] = /^\\d+$/.test(keyParts[i + 1]) ? [] : {};\n      }\n      // Move the current object pointer to the next level of the output object.\n      currentObject = currentObject[keyPart];\n    }\n\n    // Get the last key part.\n    const lastKeyPart = keyParts[keyParts.length - 1];\n    const lastKeyPartIsArray = /\\[\\d*\\]$|\\[\\]$/.test(lastKeyPart);\n\n    // Handles array[] or array[0] cases\n    if (lastKeyPartIsArray) {\n      const key = lastKeyPart.replace(/\\[\\d*\\]$|\\[\\]$/, \"\");\n      if (!currentObject[key]) {\n        currentObject[key] = [];\n      }\n\n      currentObject[key].push(data);\n    }\n    // Handles array.foo.0 cases\n    else {\n      // If the last key part is a valid integer index, push the value to the current array.\n      if (/^\\d+$/.test(lastKeyPart)) {\n        currentObject.push(data);\n      }\n      // Otherwise, set a property on the current object with the last key part and the corresponding value.\n      else {\n        if (keyCount > 1) {\n          if (!currentObject[key]) {\n            currentObject[key] = [];\n          }\n          currentObject[key].push(data);\n        } else {\n          currentObject[lastKeyPart] = data;\n        }\n      }\n    }\n  }\n\n  // Return the output object.\n  return outputObject;\n};\n\nexport const getFormDataFromSearchParams = <T extends FieldValues>(\n  request: Pick<Request, \"url\">,\n  preserveStringified = false,\n): T => {\n  const searchParams = new URL(request.url).searchParams;\n\n  return generateFormData(searchParams, preserveStringified);\n};\n\nexport const isGet = (request: Pick<Request, \"method\">) =>\n  request.method === \"GET\" || request.method === \"get\";\n\ntype ReturnData<\n  TFieldValues extends FieldValues,\n  TTransformedValues = TFieldValues,\n> =\n  | {\n      data: TTransformedValues;\n      errors: undefined;\n      receivedValues: Partial<TFieldValues>;\n    }\n  | {\n      data: undefined;\n      errors: FieldErrors<TFieldValues>;\n      receivedValues: Partial<TFieldValues>;\n    };\n/**\n * Parses the data from an HTTP request and validates it against a schema. Works in both loaders and actions, in loaders it extracts the data from the search params.\n * In actions it extracts it from request formData.\n *\n * @returns A Promise that resolves to an object containing the validated data or any errors that occurred during validation.\n */\nexport const getValidatedFormData = async <\n  TFieldValues extends FieldValues,\n  // biome-ignore lint/suspicious/noExplicitAny: any by default\n  TContext = any,\n  TTransformedValues = TFieldValues,\n>(\n  request: Request | FormData,\n  resolver: Resolver<TFieldValues, TContext, TTransformedValues>,\n  preserveStringified = false,\n): Promise<ReturnData<TFieldValues, TTransformedValues>> => {\n  const { receivedValues } = await getFormData<TFieldValues>(\n    request,\n    preserveStringified,\n  );\n\n  const data = await validateFormData(receivedValues, resolver);\n\n  return { ...data, receivedValues };\n};\n\n/**\n * Parses the data from an HTTP request depending on the request method and returns the parsed form data.\n *\n * @returns A Promise that resolves to an object containing the data\n */\nexport const getFormData = async <T extends FieldValues>(\n  request: Request | FormData,\n  preserveStringified = false,\n) => {\n  const receivedValues =\n    \"url\" in request && isGet(request)\n      ? getFormDataFromSearchParams<T>(request, preserveStringified)\n      : await parseFormData<T>(request, preserveStringified);\n\n  return { receivedValues };\n};\n\n/**\n * Helper method used in actions to validate the form data parsed from the frontend using zod and return a json error if validation fails.\n * @param data Data to validate\n * @param resolver Schema to validate and cast the data with\n * @returns Returns the validated data if successful, otherwise returns the error object\n */\nexport const validateFormData = async <\n  TFieldValues extends FieldValues,\n  TContext,\n  TTransformedValues = TFieldValues,\n>(\n  // biome-ignore lint/suspicious/noExplicitAny: <explanation>\n  data: any,\n  resolver: Resolver<TFieldValues, TContext, TTransformedValues>,\n) => {\n  const dataToValidate =\n    data instanceof FormData ? Object.fromEntries(data) : data;\n  const { errors, values } = await resolver(dataToValidate, {} as TContext, {\n    shouldUseNativeValidation: false,\n    fields: {},\n  });\n\n  if (Object.keys(errors).length > 0) {\n    return { errors: errors as FieldErrors<TFieldValues>, data: undefined };\n  }\n\n  return { errors: undefined, data: values as TTransformedValues };\n};\n/**\n  Creates a new instance of FormData with the specified data and key.\n  @template T - The type of the data parameter. It can be any type of FieldValues.\n  @param {T} data - The data to be added to the FormData. It can be either an object of type FieldValues.\n  @param {boolean} stringifyAll - Should the form data be stringified or not (default: true) eg: {a: '\"string\"', b: \"1\"} vs {a: \"string\", b: \"1\"}\n  @returns {FormData} - The FormData object with the data added to it.\n*/\nexport const createFormData = <T extends FieldValues>(\n  data: T,\n  stringifyAll = true,\n): FormData => {\n  const formData = new FormData();\n  if (!data) {\n    return formData;\n  }\n  for (const [key, value] of Object.entries(data)) {\n    // Skip undefined values\n    if (value === undefined) {\n      continue;\n    }\n    // Handle FileList\n    if (typeof FileList !== \"undefined\" && value instanceof FileList) {\n      for (let i = 0; i < value.length; i++) {\n        formData.append(key, value[i]);\n      }\n      continue;\n    }\n    // Handle array of File and Blob objects\n    if (\n      Array.isArray(value) &&\n      value.length > 0 &&\n      value.every((item) => item instanceof File || item instanceof Blob)\n    ) {\n      for (let i = 0; i < value.length; i++) {\n        formData.append(key, value[i]);\n      }\n      continue;\n    }\n    // Handle File or Blob\n    if (value instanceof File || value instanceof Blob) {\n      formData.append(key, value);\n      continue;\n    }\n    // Stringify all values if set\n    if (stringifyAll) {\n      formData.append(key, JSON.stringify(value));\n      continue;\n    }\n    // Handle strings\n    if (typeof value === \"string\") {\n      formData.append(key, value);\n      continue;\n    }\n    // Handle dates\n    if (value instanceof Date) {\n      formData.append(key, value.toISOString());\n      continue;\n    }\n    // Handle all the other values\n\n    formData.append(key, JSON.stringify(value));\n  }\n\n  return formData;\n};\n\n/**\nParses the specified Request object's FormData to retrieve the data associated with the specified key.\nOr parses the specified FormData to retrieve the data \n  */\n\n// biome-ignore lint/complexity/noUselessTypeConstraint: <explanation>\nexport const parseFormData = async <T extends unknown>(\n  request: Request | FormData,\n  preserveStringified = false,\n): Promise<T> => {\n  const formData =\n    request instanceof Request ? await request.formData() : request;\n  return generateFormData(formData, preserveStringified);\n};\n"
  },
  {
    "path": "test-apps/react-router/.dockerignore",
    "content": ".react-router\nbuild\nnode_modules\nREADME.md"
  },
  {
    "path": "test-apps/react-router/.gitignore",
    "content": ".env\n!.env.example\n.DS_Store\n.react-router\nbuild\nnode_modules\n*.tsbuildinfo\n"
  },
  {
    "path": "test-apps/react-router/Dockerfile",
    "content": "FROM node:20-alpine as development-dependencies-env\nCOPY . /app\nWORKDIR /app\nRUN npm ci\n\nFROM node:20-alpine as production-dependencies-env\nCOPY ./package.json package-lock.json /app/\nWORKDIR /app\nRUN npm ci --omit=dev\n\nFROM node:20-alpine AS build-env\nCOPY . /app/\nCOPY --from=development-dependencies-env /app/node_modules /app/node_modules\nWORKDIR /app\nRUN npm run build\n\nFROM node:20-alpine\nCOPY ./package.json package-lock.json /app/\nCOPY --from=production-dependencies-env /app/node_modules /app/node_modules\nCOPY --from=build-env /app/build /app/build\nWORKDIR /app\nCMD [\"npm\", \"run\", \"start\"]"
  },
  {
    "path": "test-apps/react-router/Dockerfile.bun",
    "content": "FROM oven/bun:1 as dependencies-env\nCOPY . /app\n\nFROM dependencies-env as development-dependencies-env\nCOPY ./package.json bun.lockb /app/\nWORKDIR /app\nRUN bun i --frozen-lockfile\n\nFROM dependencies-env as production-dependencies-env\nCOPY ./package.json bun.lockb /app/\nWORKDIR /app\nRUN bun i --production\n\nFROM dependencies-env AS build-env\nCOPY ./package.json bun.lockb /app/\nCOPY --from=development-dependencies-env /app/node_modules /app/node_modules\nWORKDIR /app\nRUN bun run build\n\nFROM dependencies-env\nCOPY ./package.json bun.lockb /app/\nCOPY --from=production-dependencies-env /app/node_modules /app/node_modules\nCOPY --from=build-env /app/build /app/build\nWORKDIR /app\nCMD [\"bun\", \"run\", \"start\"]"
  },
  {
    "path": "test-apps/react-router/Dockerfile.pnpm",
    "content": "FROM node:20-alpine as dependencies-env\nRUN npm i -g pnpm\nCOPY . /app\n\nFROM dependencies-env as development-dependencies-env\nCOPY ./package.json pnpm-lock.yaml /app/\nWORKDIR /app\nRUN pnpm i --frozen-lockfile\n\nFROM dependencies-env as production-dependencies-env\nCOPY ./package.json pnpm-lock.yaml /app/\nWORKDIR /app\nRUN pnpm i --prod --frozen-lockfile\n\nFROM dependencies-env AS build-env\nCOPY ./package.json pnpm-lock.yaml /app/\nCOPY --from=development-dependencies-env /app/node_modules /app/node_modules\nWORKDIR /app\nRUN pnpm build\n\nFROM dependencies-env\nCOPY ./package.json pnpm-lock.yaml /app/\nCOPY --from=production-dependencies-env /app/node_modules /app/node_modules\nCOPY --from=build-env /app/build /app/build\nWORKDIR /app\nCMD [\"pnpm\", \"start\"]"
  },
  {
    "path": "test-apps/react-router/README.md",
    "content": "# Welcome to React Router!\n\nA modern, production-ready template for building full-stack React applications using React Router.\n\n## Features\n\n- 🚀 Server-side rendering\n- ⚡️ Hot Module Replacement (HMR)\n- 📦 Asset bundling and optimization\n- 🔄 Data loading and mutations\n- 🔒 TypeScript by default\n- 🎉 TailwindCSS for styling\n- 📖 [React Router docs](https://reactrouter.com/)\n\n## Getting Started\n\n### Installation\n\nInstall the dependencies:\n\n```bash\nnpm install\n```\n\n### Development\n\nStart the development server with HMR:\n\n```bash\nnpm run dev\n```\n\nYour application will be available at `http://localhost:5173`.\n\n## Building for Production\n\nCreate a production build:\n\n```bash\nnpm run build\n```\n\n## Deployment\n\n### Docker Deployment\n\nThis template includes three Dockerfiles optimized for different package managers:\n\n- `Dockerfile` - for npm\n- `Dockerfile.pnpm` - for pnpm\n- `Dockerfile.bun` - for bun\n\nTo build and run using Docker:\n\n```bash\n# For npm\ndocker build -t my-app .\n\n# For pnpm\ndocker build -f Dockerfile.pnpm -t my-app .\n\n# For bun\ndocker build -f Dockerfile.bun -t my-app .\n\n# Run the container\ndocker run -p 3000:3000 my-app\n```\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### DIY Deployment\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```\n├── package.json\n├── package-lock.json (or pnpm-lock.yaml, or bun.lockb)\n├── build/\n│   ├── client/    # Static assets\n│   └── server/    # Server-side code\n```\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\n---\n\nBuilt with ❤️ using React Router.\n"
  },
  {
    "path": "test-apps/react-router/app/app.css",
    "content": "@tailwind base;\n@tailwind components;\n@tailwind utilities;\n\nhtml,\nbody {\n  @apply bg-white dark:bg-gray-950;\n\n  @media (prefers-color-scheme: dark) {\n    color-scheme: dark;\n  }\n}\n"
  },
  {
    "path": "test-apps/react-router/app/root.tsx",
    "content": "import {\n  Links,\n  Meta,\n  Outlet,\n  Scripts,\n  ScrollRestoration,\n  isRouteErrorResponse,\n} from \"react-router\";\n\nimport type { Route } from \"./+types/root\";\nimport \"./app.css\";\nimport { unstable_extractFormDataMiddleware } from \"remix-hook-form/middleware\";\n\nexport const loader = ({ context }: Route.LoaderArgs) => {\n  return null;\n};\n\nexport const links: Route.LinksFunction = () => [\n  { rel: \"preconnect\", href: \"https://fonts.googleapis.com\" },\n  {\n    rel: \"preconnect\",\n    href: \"https://fonts.gstatic.com\",\n    crossOrigin: \"anonymous\",\n  },\n  {\n    rel: \"stylesheet\",\n    href: \"https://fonts.googleapis.com/css2?family=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900&display=swap\",\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        {children}\n        <ScrollRestoration />\n        <Scripts />\n      </body>\n    </html>\n  );\n}\n\nexport default function App() {\n  return <Outlet />;\n}\n\nexport const unstable_middleware = [unstable_extractFormDataMiddleware()];\n\nexport function ErrorBoundary({ error }: Route.ErrorBoundaryProps) {\n  let message = \"Oops!\";\n  let details = \"An unexpected error occurred.\";\n  let stack: string | undefined;\n  console.log(error);\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 className=\"pt-16 p-4 container mx-auto\">\n      <h1>{message}</h1>\n      <p>{details}</p>\n      {stack && (\n        <pre className=\"w-full p-4 overflow-x-auto\">\n          <code>{stack}</code>\n        </pre>\n      )}\n    </main>\n  );\n}\n"
  },
  {
    "path": "test-apps/react-router/app/routes/home.tsx",
    "content": "import { zodResolver } from \"@hookform/resolvers/zod\";\nimport { type ClientActionFunctionArgs, Form, useFetcher } from \"react-router\";\nimport {\n  type ActionFunctionArgs,\n  type LoaderFunctionArgs,\n  data,\n} from \"react-router\";\nimport {\n  RemixFormProvider,\n  getFormDataFromSearchParams,\n  useRemixForm,\n} from \"remix-hook-form\";\nimport { getFormData, getValidatedFormData } from \"remix-hook-form/middleware\";\nimport { z } from \"zod\";\n\nconst fileListSchema = z.any().refine((files) => {\n  return (\n    typeof files === \"object\" &&\n    Symbol.iterator in files &&\n    !Array.isArray(files) &&\n    Array.from(files).every((file: any) => file instanceof File)\n  );\n});\n\nconst FormDataZodSchema = z.object({\n  email: z.string().trim().nonempty(\"validation.required\"),\n  password: z.string().trim().nonempty(\"validation.required\"),\n  number: z.number({ coerce: true }).int().positive(),\n  redirectTo: z.string().optional(),\n  boolean: z.boolean().optional(),\n  date: z.date().or(z.string()),\n  null: z.null(),\n  files: fileListSchema.optional(),\n  options: z.array(z.string()).optional(),\n  checkboxes: z.array(z.string()).optional(),\n});\n\nconst resolver = zodResolver(FormDataZodSchema);\ntype FormData = z.infer<typeof FormDataZodSchema>;\n\nexport const loader = async ({ context }: LoaderFunctionArgs) => {\n  const searchParamsFormData = await getFormData(context);\n  return { result: \"success\" };\n};\nexport const action = async ({ context }: ActionFunctionArgs) => {\n  const { data, errors, receivedValues } = await getValidatedFormData<FormData>(\n    context,\n    resolver,\n  );\n  if (errors) {\n    return { errors, receivedValues };\n  }\n\n  console.log(\n    \"File names:\",\n    // since files is of type \"any\", we need to assert its type here\n    data.files?.map((file: File) => file.name).join(\", \"),\n  );\n\n  console.log(\"Selected options:\", data.options);\n\n  return { result: \"success\" };\n};\n\nexport default function Index() {\n  const fetcher = useFetcher();\n  const methods = useRemixForm({\n    resolver,\n    fetcher,\n    defaultValues: {\n      redirectTo: undefined,\n      number: 4,\n      email: \"test@test.com\",\n      password: \"test\",\n      date: new Date(),\n      boolean: true,\n      null: null,\n      files: undefined,\n      options: undefined,\n      checkboxes: undefined,\n    },\n\n    submitData: { test: \"test\" },\n  });\n  const { register, handleSubmit, formState, watch, setError } = methods;\n\n  const checkbox = watch(\"boolean\");\n  return (\n    <RemixFormProvider {...methods}>\n      <p>Add a thing...</p>\n      <Form method=\"POST\" encType=\"multipart/form-data\" onSubmit={handleSubmit}>\n        <label>\n          Boolean\n          <input type=\"checkbox\" {...register(\"boolean\")} />\n          {formState.errors.boolean?.message}\n        </label>\n        <label>\n          number\n          <input type=\"number\" {...register(\"number\")} />\n          {formState.errors.number?.message}\n        </label>\n        <label>\n          Multiple Files\n          <input type=\"file\" {...register(\"files\")} multiple />\n          {formState.errors.files?.message}\n        </label>\n        <label>\n          Selected Options\n          <select {...register(\"options\")} multiple>\n            <option value=\"option1\">Option 1</option>\n            <option value=\"option2\">Option 2</option>\n            <option value=\"option3\">Option 3</option>\n          </select>\n          {formState.errors.options?.message}\n        </label>\n        <label>\n          Checkboxes\n          <fieldset>\n            <legend>Select your preferences:</legend>\n            <label>\n              <input\n                type=\"checkbox\"\n                value=\"preference1\"\n                {...register(\"checkboxes\")}\n              />\n              Preference 1\n            </label>\n            <label>\n              <input\n                type=\"checkbox\"\n                value=\"preference2\"\n                {...register(\"checkboxes\")}\n              />\n              Preference 2\n            </label>\n            <label>\n              <input\n                type=\"checkbox\"\n                value=\"preference3\"\n                {...register(\"checkboxes\")}\n              />{\" \"}\n              Preference 3\n            </label>\n          </fieldset>\n          {formState.errors.checkboxes?.message}\n        </label>\n\n        <div>\n          <button formMethod=\"post\" type=\"submit\" className=\"button\">\n            Add\n          </button>\n        </div>\n      </Form>\n    </RemixFormProvider>\n  );\n}\n"
  },
  {
    "path": "test-apps/react-router/app/routes.ts",
    "content": "import { type RouteConfig, index } from \"@react-router/dev/routes\";\n\nexport default [index(\"routes/home.tsx\")] satisfies RouteConfig;\n"
  },
  {
    "path": "test-apps/react-router/app/welcome/welcome.tsx",
    "content": "import logoDark from \"./logo-dark.svg\";\nimport logoLight from \"./logo-light.svg\";\n\nexport function Welcome() {\n  return (\n    <main className=\"flex items-center justify-center pt-16 pb-4\">\n      <div className=\"flex-1 flex flex-col items-center gap-16 min-h-0\">\n        <header className=\"flex flex-col items-center gap-9\">\n          <div className=\"w-[500px] max-w-[100vw] p-4\">\n            <img\n              src={logoLight}\n              alt=\"React Router\"\n              className=\"block w-full dark:hidden\"\n            />\n            <img\n              src={logoDark}\n              alt=\"React Router\"\n              className=\"hidden w-full dark:block\"\n            />\n          </div>\n        </header>\n        <div className=\"max-w-[300px] w-full space-y-6 px-4\">\n          <nav className=\"rounded-3xl border border-gray-200 p-6 dark:border-gray-700 space-y-4\">\n            <p className=\"leading-6 text-gray-700 dark:text-gray-200 text-center\">\n              What&apos;s next?\n            </p>\n            <ul>\n              {resources.map(({ href, text, icon }) => (\n                <li key={href}>\n                  <a\n                    className=\"group flex items-center gap-3 self-stretch p-3 leading-normal text-blue-700 hover:underline dark:text-blue-500\"\n                    href={href}\n                    target=\"_blank\"\n                    rel=\"noreferrer\"\n                  >\n                    {icon}\n                    {text}\n                  </a>\n                </li>\n              ))}\n            </ul>\n          </nav>\n        </div>\n      </div>\n    </main>\n  );\n}\n\nconst resources = [\n  {\n    href: \"https://reactrouter.com/docs\",\n    text: \"React Router Docs\",\n    icon: (\n      <svg\n        xmlns=\"http://www.w3.org/2000/svg\"\n        width=\"24\"\n        height=\"20\"\n        viewBox=\"0 0 20 20\"\n        fill=\"none\"\n        className=\"stroke-gray-600 group-hover:stroke-current dark:stroke-gray-300\"\n      >\n        <path\n          d=\"M9.99981 10.0751V9.99992M17.4688 17.4688C15.889 19.0485 11.2645 16.9853 7.13958 12.8604C3.01467 8.73546 0.951405 4.11091 2.53116 2.53116C4.11091 0.951405 8.73546 3.01467 12.8604 7.13958C16.9853 11.2645 19.0485 15.889 17.4688 17.4688ZM2.53132 17.4688C0.951566 15.8891 3.01483 11.2645 7.13974 7.13963C11.2647 3.01471 15.8892 0.951453 17.469 2.53121C19.0487 4.11096 16.9854 8.73551 12.8605 12.8604C8.73562 16.9853 4.11107 19.0486 2.53132 17.4688Z\"\n          strokeWidth=\"1.5\"\n          strokeLinecap=\"round\"\n        />\n      </svg>\n    ),\n  },\n  {\n    href: \"https://rmx.as/discord\",\n    text: \"Join Discord\",\n    icon: (\n      <svg\n        xmlns=\"http://www.w3.org/2000/svg\"\n        width=\"24\"\n        height=\"20\"\n        viewBox=\"0 0 24 20\"\n        fill=\"none\"\n        className=\"stroke-gray-600 group-hover:stroke-current dark:stroke-gray-300\"\n      >\n        <path\n          d=\"M15.0686 1.25995L14.5477 1.17423L14.2913 1.63578C14.1754 1.84439 14.0545 2.08275 13.9422 2.31963C12.6461 2.16488 11.3406 2.16505 10.0445 2.32014C9.92822 2.08178 9.80478 1.84975 9.67412 1.62413L9.41449 1.17584L8.90333 1.25995C7.33547 1.51794 5.80717 1.99419 4.37748 2.66939L4.19 2.75793L4.07461 2.93019C1.23864 7.16437 0.46302 11.3053 0.838165 15.3924L0.868838 15.7266L1.13844 15.9264C2.81818 17.1714 4.68053 18.1233 6.68582 18.719L7.18892 18.8684L7.50166 18.4469C7.96179 17.8268 8.36504 17.1824 8.709 16.4944L8.71099 16.4904C10.8645 17.0471 13.128 17.0485 15.2821 16.4947C15.6261 17.1826 16.0293 17.8269 16.4892 18.4469L16.805 18.8725L17.3116 18.717C19.3056 18.105 21.1876 17.1751 22.8559 15.9238L23.1224 15.724L23.1528 15.3923C23.5873 10.6524 22.3579 6.53306 19.8947 2.90714L19.7759 2.73227L19.5833 2.64518C18.1437 1.99439 16.6386 1.51826 15.0686 1.25995ZM16.6074 10.7755L16.6074 10.7756C16.5934 11.6409 16.0212 12.1444 15.4783 12.1444C14.9297 12.1444 14.3493 11.6173 14.3493 10.7877C14.3493 9.94885 14.9378 9.41192 15.4783 9.41192C16.0471 9.41192 16.6209 9.93851 16.6074 10.7755ZM8.49373 12.1444C7.94513 12.1444 7.36471 11.6173 7.36471 10.7877C7.36471 9.94885 7.95323 9.41192 8.49373 9.41192C9.06038 9.41192 9.63892 9.93712 9.6417 10.7815C9.62517 11.6239 9.05462 12.1444 8.49373 12.1444Z\"\n          strokeWidth=\"1.5\"\n        />\n      </svg>\n    ),\n  },\n];\n"
  },
  {
    "path": "test-apps/react-router/package.json",
    "content": "{\n  \"name\": \"react-router-app\",\n  \"private\": true,\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 --build --noEmit\"\n  },\n  \"dependencies\": {\n    \"@react-router/node\": \"^7.5.0\",\n    \"@react-router/serve\": \"^7.5.0\",\n    \"isbot\": \"^5.1.17\",\n    \"react\": \"^18.3.1\",\n    \"react-dom\": \"^18.3.1\",\n    \"react-router\": \"^7.5.0\",\n    \"react-hook-form\": \"7.55.0\",\n    \"remix-hook-form\": \"*\"\n  },\n  \"devDependencies\": {\n    \"@react-router/dev\": \"^7.5.0\",\n    \"@types/node\": \"^20\",\n    \"@types/react\": \"^18.3.12\",\n    \"@types/react-dom\": \"^18.3.1\",\n    \"autoprefixer\": \"^10.4.20\",\n    \"postcss\": \"^8.4.49\",\n    \"tailwindcss\": \"^3.4.15\",\n    \"typescript\": \"^5.6.3\",\n    \"vite\": \"^5.4.11\",\n    \"vite-tsconfig-paths\": \"^5.1.2\",\n    \"react-router-devtools\": \"^1.1.9\"\n  }\n}"
  },
  {
    "path": "test-apps/react-router/react-router.config.ts",
    "content": "import type { Config } from \"@react-router/dev/config\";\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"
  },
  {
    "path": "test-apps/react-router/tailwind.config.ts",
    "content": "import type { Config } from \"tailwindcss\";\n\nexport default {\n  content: [\"./app/**/{**,.client,.server}/**/*.{js,jsx,ts,tsx}\"],\n  theme: {\n    extend: {\n      fontFamily: {\n        sans: [\n          '\"Inter\"',\n          \"ui-sans-serif\",\n          \"system-ui\",\n          \"sans-serif\",\n          '\"Apple Color Emoji\"',\n          '\"Segoe UI Emoji\"',\n          '\"Segoe UI Symbol\"',\n          '\"Noto Color Emoji\"',\n        ],\n      },\n    },\n  },\n  plugins: [],\n} satisfies Config;\n"
  },
  {
    "path": "test-apps/react-router/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    \"paths\": {\n      \"~/*\": [\"./app/*\"]\n    },\n    \"esModuleInterop\": true,\n    \"forceConsistentCasingInFileNames\": true,\n    \"isolatedModules\": true,\n    \"noEmit\": true,\n    \"resolveJsonModule\": true,\n    \"skipLibCheck\": true,\n    \"strict\": true\n  }\n}\n"
  },
  {
    "path": "test-apps/react-router/vite.config.ts",
    "content": "import { reactRouter } from \"@react-router/dev/vite\";\nimport autoprefixer from \"autoprefixer\";\nimport { reactRouterDevTools } from \"react-router-devtools\";\nimport tailwindcss from \"tailwindcss\";\nimport { defineConfig } from \"vite\";\nimport tsconfigPaths from \"vite-tsconfig-paths\";\n\nexport default defineConfig({\n  css: {\n    postcss: {\n      plugins: [tailwindcss, autoprefixer],\n    },\n  },\n  plugins: [reactRouterDevTools(), reactRouter(), tsconfigPaths()],\n});\n"
  },
  {
    "path": "tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    /* Visit https://aka.ms/tsconfig.json to read more about this file */\n\n    /* Projects */\n    // \"incremental\": true,                              /* Enable incremental compilation */\n    // \"composite\": true,                                /* Enable constraints that allow a TypeScript project to be used with project references. */\n    // \"tsBuildInfoFile\": \"./\",                          /* Specify the folder for .tsbuildinfo incremental compilation files. */\n    // \"disableSourceOfProjectReferenceRedirect\": true,  /* Disable preferring source files instead of declaration files when referencing composite projects */\n    // \"disableSolutionSearching\": true,                 /* Opt a project out of multi-project reference checking when editing. */\n    // \"disableReferencedProjectLoad\": true,             /* Reduce the number of projects loaded automatically by TypeScript. */\n\n    /* Language and Environment */\n    \"target\": \"es2018\" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */,\n    \"lib\": [\n      \"dom\",\n      \"dom.iterable\",\n      \"esnext\"\n    ] /* Specify a set of bundled library declaration files that describe the target runtime environment. */,\n    \"jsx\": \"react\" /* Specify what JSX code is generated. */,\n\n    \"module\": \"es2015\" /* Specify what module code is generated. */,\n    \"rootDir\": \"./src\" /* Specify the root folder within your source files. */,\n    \"moduleResolution\": \"node\" /* Specify how TypeScript looks up a file from a given module specifier. */,\n    \"types\": [\n      \"vitest/globals\"\n    ] /* Specify type package names to be included without being referenced in a source file. */,\n\n    /* Emit */\n    \"declaration\": true /* Generate .d.ts files from TypeScript and JavaScript files in your project. */,\n    \"outDir\": \"./dist\" /* Specify an output folder for all emitted files. */,\n    \"esModuleInterop\": true /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables `allowSyntheticDefaultImports` for type compatibility. */,\n    // \"preserveSymlinks\": true,                         /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */\n    \"forceConsistentCasingInFileNames\": true /* Ensure that casing is correct in imports. */,\n\n    /* Type Checking */\n    \"strict\": true /* Enable all strict type-checking options. */,\n    \"skipLibCheck\": true /* Skip type checking all .d.ts files. */,\n    \"verbatimModuleSyntax\": true /* Require `type` prefix for type-only imports */\n  },\n  \"exclude\": [\n    \"./bin/**/*.test.ts\",\n    \"./dist/**/*\",\n    \"./bin/mocks/**/*\",\n    \"./*.config.ts\",\n    \"./src/testing-app/**\",\n    \"./test-apps\"\n  ]\n}\n"
  },
  {
    "path": "vitest.config.ts",
    "content": "import { defineConfig } from \"vitest/config\";\n\nexport default defineConfig({\n  test: {\n    globals: true,\n    environment: \"happy-dom\",\n  },\n});\n"
  }
]