[
  {
    "path": ".eslintignore",
    "content": "/node_modules/**\n/declarations\n/examples/**\n/cli.js\n/lib.js\n/lib.esm.js\n"
  },
  {
    "path": ".gitattributes",
    "content": "# .gitattributes\n# Makes sure all line endings are LF.\n\n*\ttext=auto eol=lf\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/bug_report.yaml",
    "content": "name: Report a bug\ndescription: ———\nlabels: [bug]\nbody:\n  - type: markdown\n    attributes:\n      value: |\n        # Thanks for reporting this bug!\n\n        Help us replicate and find a fix for the issue by filling in this form.\n  - type: textarea\n    attributes:\n      label: Description\n      description: |\n        Describe the issue and how to replicate it. If possible, please include\n        a minimal example to reproduce the issue.\n    validations:\n      required: true\n  - type: input\n    attributes:\n      label: Library version\n      description: |\n        Output of the `serve --version` command\n    validations:\n      required: true\n  - type: input\n    attributes:\n      label: Node version\n      description: Output of the `node --version` command\n    validations:\n      required: true\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/feature_request.yaml",
    "content": "name: Suggest an improvement or new feature\ndescription: ———\nlabels: [enhancement]\nbody:\n  - type: markdown\n    attributes:\n      value: |\n        # Thanks for filing this feature request!\n\n        Help us understanding this feature and the need for it better by filling in this form.\n  - type: textarea\n    attributes:\n      label: Description\n      description: Describe the feature in detail\n    validations:\n      required: true\n  - type: textarea\n    attributes:\n      label: Why\n      description: Why should we add this feature? What are potential use cases for it?\n    validations:\n      required: true\n  - type: textarea\n    attributes:\n      label: Alternatives\n      description: Describe the alternatives you have considered, or existing workarounds\n    validations:\n      required: true\n"
  },
  {
    "path": ".github/PULL_REQUEST_TEMPLATE/default.md",
    "content": "<!--\n\tHi there! Thanks for contributing! Please fill in this template to help us\n\treview and merge the PR as quickly and easily as possible!\n-->\n\n## Related Issues\n\n<!--\n\tIf this is a bug fix, or adds a feature mentioned in another issue, mention\n\tit as follows:\n\n\t- Closes #10\n\t- Fixes #15\n-->\n\n## Description\n\n<!--\n\tExplain what has been added/changed/removed, in\n\t[keepachangelog.com](https://keepachangelog.com) style.\n-->\n\n### Added\n\n<!--\n\t- Added a new method on the limiter object to reset the count for a certain IP [#10]\n-->\n\n### Changed\n\n<!--\n\t- Deprecated `global` option\n\t- Fixed test for deprecated options [#15]\n-->\n\n### Removed\n\n<!--\n\t- Removed deprecated `headers` option\n-->\n\n## Caveats/Problems/Issues\n\n<!--\n\tAny weird code/problems you faced while making this PR. Feel free to ask for\n\thelp with anything, especially if it's your first time contributing!\n-->\n\n## Checklist\n\n- [ ] The issues that this PR fixes/closes have been mentioned above.\n- [ ] What this PR adds/changes/removes has been explained.\n- [ ] All tests (`pnpm test`) pass.\n- [ ] The linter (`pnpm lint`) does not throw an errors.\n- [ ] All added/modified code has been commented, and\n      methods/classes/constants/types have been annotated with TSDoc comments.\n"
  },
  {
    "path": ".github/workflows/publish.yml",
    "content": "name: Publish to NPM\non:\n  release:\n    types: [created]\njobs:\n  build:\n    runs-on: ubuntu-latest\n    steps:\n      - name: Cache\n        id: node-modules\n        uses: actions/cache@v3\n        with:\n          path: node_modules\n          key: ${{ runner.os }}-node-modules-${{ hashFiles('**/yarn.lock') }}\n          restore-keys: |\n            ${{ runner.os }}-node-modules-\n\n      - name: Checkout\n        uses: actions/checkout@v3\n      - name: Setup Node\n        uses: actions/setup-node@v3\n        with:\n          node-version: '18.x'\n          registry-url: 'https://registry.npmjs.org'\n\n      - name: Install dependencies and build 🔧\n        run: yarn && yarn build\n\n      - name: Publish package on NPM 📦\n        run: npm publish\n        env:\n          NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}\n"
  },
  {
    "path": ".gitignore",
    "content": "# .gitignore\n# A list of files and folders that should not be tracked by Git.\n\nnode_modules/\ncoverage/\nbuild/\n.cache/\n.idea/\n.vscode/\n\n*.log\n*.tgz\n*.bak\n*.tmp\ncli.js\nlib.js\nlib.esm.js\ndeclarations\n"
  },
  {
    "path": ".npmrc",
    "content": "# .npmrc\n# Configuration for pnpm.\n\n# Uses the exact version instead of any within-patch-range version of an\n# installed package.\nsave-exact=true\n# Do not error out on missing peer dependencies.\nstrict-peer-dependencies=false\n"
  },
  {
    "path": ".prettierignore",
    "content": "node_modules/**\ndeclarations/**\nexamples/**\ncli.js\nlib.js\nlib.esm.js\n"
  },
  {
    "path": "LICENSE",
    "content": "MIT License\n\nCopyright (c) 2022 Mayke\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-vite\n\n<div>\n  <br />\n  <br />\n  <img width=\"320\" src=\"https://user-images.githubusercontent.com/2893850/200666584-3c825b6d-a91d-4ade-bb46-6523fc66416e.png\" />\n  <br />\n  <br />\n</div>\n\n`remix-vite` helps you serve [Remix](https://remix.run/) apps locally using [Vite](https://vitejs.dev/).\n\n## Usage\n\nThe quickest way to get started is to just run `npx remix-vite` in your project's directory.\n\nIf you prefer, you can also install the package globally (you'll need at least [Node LTS](https://github.com/nodejs/Release#release-schedule)):\n\n```bash\n> npm install --global remix-vite\n```\n\nOnce that's done, you can run this command inside your project's directory...\n\n```bash\n> remix-vite\n```\n\nNow you understand how the package works! :tada:\n\n## Custom Remix Server\n\nIf you want to use `remix-vite` with a custom remix server, you can do so by integrating your server with vite dev server.\n\nLet's say you have a custom Express server and you want to use it with `remix-vite`. Here is how you can do it:\n\n```js\nconst express = require('express');\nconst { createRequestHandler } = require('@remix-run/express');\nconst { createRemixViteDevServer, getRemixViteBuild } = require('remix-vite');\n\nconst app = express();\n\n// Create a remix-vite dev server.\ncreateRemixViteDevServer().then((remixViteDevServer) => {\n  // Use remix-vite dev server as middleware.\n  app.use(remixViteDevServer.middlewares);\n\n  app.all('*', async (req, res, next) => {\n    purgeRequireCache();\n\n    // Get the remix build generated by remix-vite.\n    const remixBuild = await getRemixViteBuild(remixViteDevServer);\n\n    // Create a remix express request handler.\n    const requestHandler = createRequestHandler({ build: remixBuild });\n\n    await requestHandler(req, res, next);\n  });\n\n  // Start the server.\n  app.listen(3000, () => {\n    console.log('Listening at http://localhost:3000');\n  });\n});\n\nfunction purgeRequireCache() {\n  // purge require cache on requests for \"server side HMR\" this won't let\n  // you have in-memory objects between requests in development.\n  for (const key in require.cache) {\n    delete require.cache[key];\n  }\n}\n```\n\n## Change default host and port\n\nIf you want to change the host and port just pass --host and --port flag to remix-vite. Default host is `localhost` and port is `3000`\n\n`> remix-vite --port=4000`\n\n## Issues\n\nIf you want a feature to be added, or wish to report a bug, please open an issue [here](https://github.com/sudomf/remix-vite/issues/new).\n\n## Author\n\nMayke Freitas ([@sudomf](https://twitter.com/maykedev))\n\n<div>\n  <a href=\"https://www.buymeacoffee.com/mayke\" style=\"display: block; max-width: 320px;\">\n    <img width=\"320\" src=\"./assets/yellow-button.png\" />\n  </div>\n</div>\n"
  },
  {
    "path": "config/husky/pre-commit",
    "content": "#!/bin/sh\n\n# config/husky/pre-commit\n# Run `lint-staged` before every commit.\n\n. \"$(dirname \"$0\")/_/husky.sh\"\n\nFORCE_COLOR=2 yarn lint-staged\n"
  },
  {
    "path": "examples/complex/.dockerignore",
    "content": "/node_modules\n*.log\n.DS_Store\n.env\n/.cache\n/public/build\n/build\n"
  },
  {
    "path": "examples/complex/.eslintrc.js",
    "content": "/** @type {import('@types/eslint').Linter.BaseConfig} */\nmodule.exports = {\n  extends: [\n    \"@remix-run/eslint-config\",\n    \"@remix-run/eslint-config/node\",\n    \"@remix-run/eslint-config/jest-testing-library\",\n    \"prettier\",\n  ],\n  env: {\n    \"cypress/globals\": true,\n  },\n  plugins: [\"cypress\"],\n  // we're using vitest which has a very similar API to jest\n  // (so the linting plugins work nicely), but it means we have to explicitly\n  // set the jest version.\n  settings: {\n    jest: {\n      version: 28,\n    },\n  },\n};\n"
  },
  {
    "path": "examples/complex/.gitignore",
    "content": "# We don't want lockfiles in stacks, as people could use a different package manager\n# This part will be removed by `remix.init`\npackage-lock.json\nyarn.lock\npnpm-lock.yaml\npnpm-lock.yml\n\nnode_modules\n\n/build\n/public/build\n.env\n\n/cypress/screenshots\n/cypress/videos\n/prisma/data.db\n/prisma/data.db-journal\n\n/app/styles/tailwind.css\n"
  },
  {
    "path": "examples/complex/.gitpod.Dockerfile",
    "content": "FROM gitpod/workspace-full\n\n# Install Fly\nRUN curl -L https://fly.io/install.sh | sh\nENV FLYCTL_INSTALL=\"/home/gitpod/.fly\"\nENV PATH=\"$FLYCTL_INSTALL/bin:$PATH\"\n\n# Install GitHub CLI\nRUN brew install gh\n"
  },
  {
    "path": "examples/complex/.gitpod.yml",
    "content": "# https://www.gitpod.io/docs/config-gitpod-file\n\nimage:\n  file: .gitpod.Dockerfile\n\nports:\n  - port: 3000\n    onOpen: notify\n\ntasks:\n  - name: Restore .env file\n    command: |\n      if [ -f .env ]; then\n        # If this workspace already has a .env, don't override it\n        # Local changes survive a workspace being opened and closed\n        # but they will not persist between separate workspaces for the same repo\n\n        echo \"Found .env in workspace\"\n      else\n        # There is no .env\n        if [ ! -n \"${ENV}\" ]; then\n          # There is no $ENV from a previous workspace\n          # Default to the example .env\n          echo \"Setting example .env\"\n\n          cp .env.example .env \n        else\n          # After making changes to .env, run this line to persist it to $ENV\n          #   eval $(gp env -e ENV=\"$(base64 .env | tr -d '\\n')\")\n          # \n          # Environment variables set this way are shared between all your workspaces for this repo\n          # The lines below will read $ENV and print a .env file\n\n          echo \"Restoring .env from Gitpod\"\n\n          echo \"${ENV}\" | base64 -d | tee .env > /dev/null\n        fi\n      fi\n\n  - init: npm install\n    command: npm run setup && npm run dev\n\nvscode:\n  extensions:\n    - ms-azuretools.vscode-docker\n    - esbenp.prettier-vscode\n    - dbaeumer.vscode-eslint\n    - bradlc.vscode-tailwindcss\n"
  },
  {
    "path": "examples/complex/.npmrc",
    "content": "legacy-peer-deps=true\n"
  },
  {
    "path": "examples/complex/.prettierignore",
    "content": "node_modules\n\n/build\n/public/build\n.env\n\n/app/styles/tailwind.css\n"
  },
  {
    "path": "examples/complex/Dockerfile",
    "content": "# base node image\nFROM node:16-bullseye-slim as base\n\n# set for base and all layer that inherit from it\nENV NODE_ENV production\n\n# Install openssl for Prisma\nRUN apt-get update && apt-get install -y openssl sqlite3\n\n# Install all node_modules, including dev dependencies\nFROM base as deps\n\nWORKDIR /myapp\n\nADD package.json .npmrc ./\nRUN npm install --production=false\n\n# Setup production node_modules\nFROM base as production-deps\n\nWORKDIR /myapp\n\nCOPY --from=deps /myapp/node_modules /myapp/node_modules\nADD package.json .npmrc ./\nRUN npm prune --production\n\n# Build the app\nFROM base as build\n\nWORKDIR /myapp\n\nCOPY --from=deps /myapp/node_modules /myapp/node_modules\n\nADD prisma .\nRUN npx prisma generate\n\nADD . .\nRUN npm run build\n\n# Finally, build the production image with minimal footprint\nFROM base\n\nENV DATABASE_URL=file:/data/sqlite.db\nENV PORT=\"8080\"\nENV NODE_ENV=\"production\"\n\n# add shortcut for connecting to database CLI\nRUN echo \"#!/bin/sh\\nset -x\\nsqlite3 \\$DATABASE_URL\" > /usr/local/bin/database-cli && chmod +x /usr/local/bin/database-cli\n\nWORKDIR /myapp\n\nCOPY --from=production-deps /myapp/node_modules /myapp/node_modules\nCOPY --from=build /myapp/node_modules/.prisma /myapp/node_modules/.prisma\n\nCOPY --from=build /myapp/build /myapp/build\nCOPY --from=build /myapp/public /myapp/public\nCOPY --from=build /myapp/package.json /myapp/package.json\nCOPY --from=build /myapp/start.sh /myapp/start.sh\nCOPY --from=build /myapp/prisma /myapp/prisma\n\nENTRYPOINT [ \"./start.sh\" ]\n"
  },
  {
    "path": "examples/complex/README.md",
    "content": "# Remix Indie Stack\n\n![The Remix Indie Stack](https://repository-images.githubusercontent.com/465928257/a241fa49-bd4d-485a-a2a5-5cb8e4ee0abf)\n\nLearn more about [Remix Stacks](https://remix.run/stacks).\n\n```\nnpx create-remix@latest --template remix-run/indie-stack\n```\n\n## What's in the stack\n\n- [Fly app deployment](https://fly.io) with [Docker](https://www.docker.com/)\n- Production-ready [SQLite Database](https://sqlite.org)\n- Healthcheck endpoint for [Fly backups region fallbacks](https://fly.io/docs/reference/configuration/#services-http_checks)\n- [GitHub Actions](https://github.com/features/actions) for deploy on merge to production and staging environments\n- Email/Password Authentication with [cookie-based sessions](https://remix.run/docs/en/v1/api/remix#createcookiesessionstorage)\n- Database ORM with [Prisma](https://prisma.io)\n- Styling with [Tailwind](https://tailwindcss.com/)\n- End-to-end testing with [Cypress](https://cypress.io)\n- Local third party request mocking with [MSW](https://mswjs.io)\n- Unit testing with [Vitest](https://vitest.dev) and [Testing Library](https://testing-library.com)\n- Code formatting with [Prettier](https://prettier.io)\n- Linting with [ESLint](https://eslint.org)\n- Static Types with [TypeScript](https://typescriptlang.org)\n\nNot a fan of bits of the stack? Fork it, change it, and use `npx create-remix --template your/repo`! Make it your own.\n\n## Quickstart\n\nClick this button to create a [Gitpod](https://gitpod.io) workspace with the project set up and Fly pre-installed\n\n[![Gitpod Ready-to-Code](https://img.shields.io/badge/Gitpod-Ready--to--Code-blue?logo=gitpod)](https://gitpod.io/from-referrer/)\n\n## Development\n\n- This step only applies if you've opted out of having the CLI install dependencies for you:\n\n  ```sh\n  npx remix init\n  ```\n\n- Initial setup: _If you just generated this project, this step has been done for you._\n\n  ```sh\n  npm run setup\n  ```\n\n- Start dev server:\n\n  ```sh\n  npm run dev\n  ```\n\nThis starts your app in development mode, rebuilding assets on file changes.\n\nThe database seed script creates a new user with some data you can use to get started:\n\n- Email: `rachel@remix.run`\n- Password: `racheliscool`\n\n### Relevant code:\n\nThis is a pretty simple note-taking app, but it's a good example of how you can build a full stack app with Prisma and Remix. The main functionality is creating users, logging in and out, and creating and deleting notes.\n\n- creating users, and logging in and out [./app/models/user.server.ts](./app/models/user.server.ts)\n- user sessions, and verifying them [./app/session.server.ts](./app/session.server.ts)\n- creating, and deleting notes [./app/models/note.server.ts](./app/models/note.server.ts)\n\n## Deployment\n\nThis Remix Stack comes with two GitHub Actions that handle automatically deploying your app to production and staging environments.\n\nPrior to your first deployment, you'll need to do a few things:\n\n- [Install Fly](https://fly.io/docs/getting-started/installing-flyctl/)\n\n- Sign up and log in to Fly\n\n  ```sh\n  fly auth signup\n  ```\n\n  > **Note:** If you have more than one Fly account, ensure that you are signed into the same account in the Fly CLI as you are in the browser. In your terminal, run `fly auth whoami` and ensure the email matches the Fly account signed into the browser.\n\n- Create two apps on Fly, one for staging and one for production:\n\n  ```sh\n  fly apps create indie-stack-template\n  fly apps create indie-stack-template-staging\n  ```\n\n  > **Note:** Make sure this name matches the `app` set in your `fly.toml` file. Otherwise, you will not be able to deploy.\n\n  - Initialize Git.\n\n  ```sh\n  git init\n  ```\n\n- Create a new [GitHub Repository](https://repo.new), and then add it as the remote for your project. **Do not push your app yet!**\n\n  ```sh\n  git remote add origin <ORIGIN_URL>\n  ```\n\n- Add a `FLY_API_TOKEN` to your GitHub repo. To do this, go to your user settings on Fly and create a new [token](https://web.fly.io/user/personal_access_tokens/new), then add it to [your repo secrets](https://docs.github.com/en/actions/security-guides/encrypted-secrets) with the name `FLY_API_TOKEN`.\n\n- Add a `SESSION_SECRET` to your fly app secrets, to do this you can run the following commands:\n\n  ```sh\n  fly secrets set SESSION_SECRET=$(openssl rand -hex 32) --app indie-stack-template\n  fly secrets set SESSION_SECRET=$(openssl rand -hex 32) --app indie-stack-template-staging\n  ```\n\n  If you don't have openssl installed, you can also use [1password](https://1password.com/password-generator/) to generate a random secret, just replace `$(openssl rand -hex 32)` with the generated secret.\n\n- Create a persistent volume for the sqlite database for both your staging and production environments. Run the following:\n\n  ```sh\n  fly volumes create data --size 1 --app indie-stack-template\n  fly volumes create data --size 1 --app indie-stack-template-staging\n  ```\n\nNow that everything is set up you can commit and push your changes to your repo. Every commit to your `main` branch will trigger a deployment to your production environment, and every commit to your `dev` branch will trigger a deployment to your staging environment.\n\n### Connecting to your database\n\nThe sqlite database lives at `/data/sqlite.db` in your deployed application. You can connect to the live database by running `fly ssh console -C database-cli`.\n\n### Getting Help with Deployment\n\nIf you run into any issues deploying to Fly, make sure you've followed all of the steps above and if you have, then post as many details about your deployment (including your app name) to [the Fly support community](https://community.fly.io). They're normally pretty responsive over there and hopefully can help resolve any of your deployment issues and questions.\n\n## GitHub Actions\n\nWe use GitHub Actions for continuous integration and deployment. Anything that gets into the `main` branch will be deployed to production after running tests/build/etc. Anything in the `dev` branch will be deployed to staging.\n\n## Testing\n\n### Cypress\n\nWe use Cypress for our End-to-End tests in this project. You'll find those in the `cypress` directory. As you make changes, add to an existing file or create a new file in the `cypress/e2e` directory to test your changes.\n\nWe use [`@testing-library/cypress`](https://testing-library.com/cypress) for selecting elements on the page semantically.\n\nTo run these tests in development, run `npm run test:e2e:dev` which will start the dev server for the app as well as the Cypress client. Make sure the database is running in docker as described above.\n\nWe have a utility for testing authenticated features without having to go through the login flow:\n\n```ts\ncy.login();\n// you are now logged in as a new user\n```\n\nWe also have a utility to auto-delete the user at the end of your test. Just make sure to add this in each test file:\n\n```ts\nafterEach(() => {\n  cy.cleanupUser();\n});\n```\n\nThat way, we can keep your local db clean and keep your tests isolated from one another.\n\n### Vitest\n\nFor lower level tests of utilities and individual components, we use `vitest`. We have DOM-specific assertion helpers via [`@testing-library/jest-dom`](https://testing-library.com/jest-dom).\n\n### Type Checking\n\nThis project uses TypeScript. It's recommended to get TypeScript set up for your editor to get a really great in-editor experience with type checking and auto-complete. To run type checking across the whole project, run `npm run typecheck`.\n\n### Linting\n\nThis project uses ESLint for linting. That is configured in `.eslintrc.js`.\n\n### Formatting\n\nWe use [Prettier](https://prettier.io/) for auto-formatting in this project. It's recommended to install an editor plugin (like the [VSCode Prettier plugin](https://marketplace.visualstudio.com/items?itemName=esbenp.prettier-vscode)) to get auto-formatting on save. There's also a `npm run format` script you can run to format all files in the project.\n"
  },
  {
    "path": "examples/complex/app/db.server.ts",
    "content": "import { PrismaClient } from \"@prisma/client\";\n\nlet prisma: PrismaClient;\n\ndeclare global {\n  var __db__: PrismaClient;\n}\n\n// this is needed because in development we don't want to restart\n// the server with every change, but we want to make sure we don't\n// create a new connection to the DB with every change either.\n// in production we'll have a single connection to the DB.\nif (process.env.NODE_ENV === \"production\") {\n  prisma = new PrismaClient();\n} else {\n  if (!global.__db__) {\n    global.__db__ = new PrismaClient();\n  }\n  prisma = global.__db__;\n  prisma.$connect();\n}\n\nexport { prisma };\n"
  },
  {
    "path": "examples/complex/app/entry.client.tsx",
    "content": "import { RemixBrowser } from \"@remix-run/react\";\nimport { startTransition, StrictMode } from \"react\";\nimport { hydrateRoot } from \"react-dom/client\";\n\nconst hydrate = () => {\n  startTransition(() => {\n    hydrateRoot(\n      document,\n      <StrictMode>\n        <RemixBrowser />\n      </StrictMode>\n    );\n  });\n};\n\nif (window.requestIdleCallback) {\n  window.requestIdleCallback(hydrate);\n} else {\n  // Safari doesn't support requestIdleCallback\n  // https://caniuse.com/requestidlecallback\n  window.setTimeout(hydrate, 1);\n}\n"
  },
  {
    "path": "examples/complex/app/entry.server.tsx",
    "content": "import { PassThrough } from \"stream\";\nimport type { EntryContext } from \"@remix-run/node\";\nimport { Response } from \"@remix-run/node\";\nimport { RemixServer } from \"@remix-run/react\";\nimport isbot from \"isbot\";\nimport { renderToPipeableStream } from \"react-dom/server\";\n\nconst ABORT_DELAY = 5000;\n\nexport default function handleRequest(\n  request: Request,\n  responseStatusCode: number,\n  responseHeaders: Headers,\n  remixContext: EntryContext\n) {\n  const callbackName = isbot(request.headers.get(\"user-agent\"))\n    ? \"onAllReady\"\n    : \"onShellReady\";\n\n  return new Promise(async (resolve, reject) => {\n    let didError = false;\n\n    const { pipe, abort } = renderToPipeableStream(\n      <RemixServer context={remixContext} url={request.url} />,\n      {\n        [callbackName]: () => {\n          const body = new PassThrough();\n\n          responseHeaders.set(\"Content-Type\", \"text/html\");\n\n          resolve(\n            new Response(body, {\n              headers: responseHeaders,\n              status: didError ? 500 : responseStatusCode,\n            })\n          );\n\n          pipe(body);\n        },\n        onShellError: (err: unknown) => {\n          reject(err);\n        },\n        onError: (error: unknown) => {\n          didError = true;\n\n          console.error(error);\n        },\n      }\n    );\n\n    setTimeout(abort, ABORT_DELAY);\n  });\n}\n"
  },
  {
    "path": "examples/complex/app/models/note.server.ts",
    "content": "import type { User, Note } from \"@prisma/client\";\n\nimport { prisma } from \"~/db.server\";\n\nexport type { Note } from \"@prisma/client\";\n\nexport function getNote({\n  id,\n  userId,\n}: Pick<Note, \"id\"> & {\n  userId: User[\"id\"];\n}) {\n  return prisma.note.findFirst({\n    select: { id: true, body: true, title: true },\n    where: { id, userId },\n  });\n}\n\nexport function getNoteListItems({ userId }: { userId: User[\"id\"] }) {\n  return prisma.note.findMany({\n    where: { userId },\n    select: { id: true, title: true },\n    orderBy: { updatedAt: \"desc\" },\n  });\n}\n\nexport function createNote({\n  body,\n  title,\n  userId,\n}: Pick<Note, \"body\" | \"title\"> & {\n  userId: User[\"id\"];\n}) {\n  return prisma.note.create({\n    data: {\n      title,\n      body,\n      user: {\n        connect: {\n          id: userId,\n        },\n      },\n    },\n  });\n}\n\nexport function deleteNote({\n  id,\n  userId,\n}: Pick<Note, \"id\"> & { userId: User[\"id\"] }) {\n  return prisma.note.deleteMany({\n    where: { id, userId },\n  });\n}\n"
  },
  {
    "path": "examples/complex/app/models/user.server.ts",
    "content": "import type { Password, User } from \"@prisma/client\";\nimport bcrypt from \"bcryptjs\";\n\nimport { prisma } from \"~/db.server\";\n\nexport type { User } from \"@prisma/client\";\n\nexport async function getUserById(id: User[\"id\"]) {\n  return prisma.user.findUnique({ where: { id } });\n}\n\nexport async function getUserByEmail(email: User[\"email\"]) {\n  return prisma.user.findUnique({ where: { email } });\n}\n\nexport async function createUser(email: User[\"email\"], password: string) {\n  const hashedPassword = await bcrypt.hash(password, 10);\n\n  return prisma.user.create({\n    data: {\n      email,\n      password: {\n        create: {\n          hash: hashedPassword,\n        },\n      },\n    },\n  });\n}\n\nexport async function deleteUserByEmail(email: User[\"email\"]) {\n  return prisma.user.delete({ where: { email } });\n}\n\nexport async function verifyLogin(\n  email: User[\"email\"],\n  password: Password[\"hash\"]\n) {\n  const userWithPassword = await prisma.user.findUnique({\n    where: { email },\n    include: {\n      password: true,\n    },\n  });\n\n  if (!userWithPassword || !userWithPassword.password) {\n    return null;\n  }\n\n  const isValid = await bcrypt.compare(\n    password,\n    userWithPassword.password.hash\n  );\n\n  if (!isValid) {\n    return null;\n  }\n\n  const { password: _password, ...userWithoutPassword } = userWithPassword;\n\n  return userWithoutPassword;\n}\n"
  },
  {
    "path": "examples/complex/app/root.tsx",
    "content": "import type { LinksFunction, LoaderArgs } from \"@remix-run/node\";\nimport { json } from \"@remix-run/node\";\nimport {\n  Links,\n  LiveReload,\n  Meta,\n  Outlet,\n  Scripts,\n  ScrollRestoration,\n} from \"@remix-run/react\";\nimport tailwindStylesheetUrl from \"./styles/tailwind.css\";\nimport { getUser } from \"./session.server\";\n\nexport const links: LinksFunction = () => {\n  return [{ rel: \"stylesheet\", href: tailwindStylesheetUrl }];\n};\n\nexport async function loader({ request }: LoaderArgs) {\n  return json({\n    user: await getUser(request),\n  });\n}\n\nexport default function App() {\n  return (\n    <html lang=\"en\" className=\"h-full\">\n      <head>\n        <Meta />\n        <Links />\n      </head>\n      <body className=\"h-full\">\n        <Outlet />\n        <ScrollRestoration />\n        <Scripts />\n        <LiveReload />\n      </body>\n    </html>\n  );\n}\n"
  },
  {
    "path": "examples/complex/app/routes/healthcheck.tsx",
    "content": "// learn more: https://fly.io/docs/reference/configuration/#services-http_checks\nimport type { LoaderArgs } from \"@remix-run/node\";\n\nimport { prisma } from \"~/db.server\";\n\nexport async function loader({ request }: LoaderArgs) {\n  const host =\n    request.headers.get(\"X-Forwarded-Host\") ?? request.headers.get(\"host\");\n\n  try {\n    const url = new URL(\"/\", `http://${host}`);\n    // if we can connect to the database and make a simple query\n    // and make a HEAD request to ourselves, then we're good.\n    await Promise.all([\n      prisma.user.count(),\n      fetch(url.toString(), { method: \"HEAD\" }).then((r) => {\n        if (!r.ok) return Promise.reject(r);\n      }),\n    ]);\n    return new Response(\"OK\");\n  } catch (error: unknown) {\n    console.log(\"healthcheck ❌\", { error });\n    return new Response(\"ERROR\", { status: 500 });\n  }\n}\n"
  },
  {
    "path": "examples/complex/app/routes/index.tsx",
    "content": "import { Link } from \"@remix-run/react\";\n\nimport { useOptionalUser } from \"~/utils\";\n\nexport default function Index() {\n  const user = useOptionalUser();\n  return (\n    <main className=\"relative min-h-screen bg-white sm:flex sm:items-center sm:justify-center\">\n      <div className=\"relative sm:pb-16 sm:pt-8\">\n        <div className=\"mx-auto max-w-7xl sm:px-6 lg:px-8\">\n          <div className=\"relative shadow-xl sm:overflow-hidden sm:rounded-2xl\">\n            <div className=\"absolute inset-0\">\n              <div className=\"absolute inset-0 bg-gray-900 mix-blend-multiply\" />\n            </div>\n            <div className=\"relative px-4 pt-16 pb-8 sm:px-6 sm:pt-24 sm:pb-14 lg:px-8 lg:pb-20 lg:pt-32\">\n              <h1 className=\"text-center text-6xl font-extrabold tracking-tight sm:text-8xl lg:text-9xl\">\n                <span className=\"block uppercase text-yellow-500 drop-shadow-md\">\n                  Remix Vite\n                </span>\n              </h1>\n              <p className=\"mx-auto mt-6 max-w-lg text-center text-xl text-white sm:max-w-3xl\">\n                A Vite server for Remix\n              </p>\n              <div className=\"mx-auto mt-10 max-w-sm sm:flex sm:max-w-none sm:justify-center\">\n                {user ? (\n                  <Link\n                    to=\"/notes\"\n                    className=\"flex items-center justify-center rounded-md border border-transparent bg-white px-4 py-3 text-base font-medium text-yellow-700 shadow-sm hover:bg-yellow-50 sm:px-8\"\n                  >\n                    View Notes for {user.email}\n                  </Link>\n                ) : (\n                  <div className=\"space-y-4 sm:mx-auto sm:inline-grid sm:grid-cols-2 sm:gap-5 sm:space-y-0\">\n                    <Link\n                      to=\"/join\"\n                      className=\"flex items-center justify-center rounded-md border border-transparent bg-white px-4 py-3 text-base font-medium text-yellow-700 shadow-sm hover:bg-yellow-50 sm:px-8\"\n                    >\n                      Sign up\n                    </Link>\n                    <Link\n                      to=\"/login\"\n                      className=\"flex items-center justify-center rounded-md bg-yellow-500 px-4 py-3 font-medium text-white hover:bg-yellow-600\"\n                    >\n                      Log In\n                    </Link>\n                  </div>\n                )}\n              </div>\n              <a href=\"https://remix.run\">\n                <img\n                  src=\"https://user-images.githubusercontent.com/1500684/158298926-e45dafff-3544-4b69-96d6-d3bcc33fc76a.svg\"\n                  alt=\"Remix\"\n                  className=\"mx-auto mt-16 w-full max-w-[12rem] md:max-w-[16rem]\"\n                />\n              </a>\n            </div>\n          </div>\n        </div>\n\n        <div className=\"mx-auto max-w-7xl py-2 px-4 sm:px-6 lg:px-8\">\n          <div className=\"mt-6 flex flex-wrap justify-center gap-8\">\n            {[\n              {\n                src: \"https://user-images.githubusercontent.com/1500684/157764397-ccd8ea10-b8aa-4772-a99b-35de937319e1.svg\",\n                alt: \"Fly.io\",\n                href: \"https://fly.io\",\n              },\n              {\n                src: \"https://user-images.githubusercontent.com/1500684/157764395-137ec949-382c-43bd-a3c0-0cb8cb22e22d.svg\",\n                alt: \"SQLite\",\n                href: \"https://sqlite.org\",\n              },\n              {\n                src: \"https://user-images.githubusercontent.com/1500684/157764484-ad64a21a-d7fb-47e3-8669-ec046da20c1f.svg\",\n                alt: \"Prisma\",\n                href: \"https://prisma.io\",\n              },\n              {\n                src: \"https://user-images.githubusercontent.com/1500684/157764276-a516a239-e377-4a20-b44a-0ac7b65c8c14.svg\",\n                alt: \"Tailwind\",\n                href: \"https://tailwindcss.com\",\n              },\n              {\n                src: \"https://user-images.githubusercontent.com/1500684/157764454-48ac8c71-a2a9-4b5e-b19c-edef8b8953d6.svg\",\n                alt: \"Cypress\",\n                href: \"https://www.cypress.io\",\n              },\n              {\n                src: \"https://user-images.githubusercontent.com/1500684/157772386-75444196-0604-4340-af28-53b236faa182.svg\",\n                alt: \"MSW\",\n                href: \"https://mswjs.io\",\n              },\n              {\n                src: \"https://user-images.githubusercontent.com/1500684/157772447-00fccdce-9d12-46a3-8bb4-fac612cdc949.svg\",\n                alt: \"Vitest\",\n                href: \"https://vitest.dev\",\n              },\n              {\n                src: \"https://user-images.githubusercontent.com/1500684/157772662-92b0dd3a-453f-4d18-b8be-9fa6efde52cf.png\",\n                alt: \"Testing Library\",\n                href: \"https://testing-library.com\",\n              },\n              {\n                src: \"https://user-images.githubusercontent.com/1500684/157772934-ce0a943d-e9d0-40f8-97f3-f464c0811643.svg\",\n                alt: \"Prettier\",\n                href: \"https://prettier.io\",\n              },\n              {\n                src: \"https://user-images.githubusercontent.com/1500684/157772990-3968ff7c-b551-4c55-a25c-046a32709a8e.svg\",\n                alt: \"ESLint\",\n                href: \"https://eslint.org\",\n              },\n              {\n                src: \"https://user-images.githubusercontent.com/1500684/157773063-20a0ed64-b9f8-4e0b-9d1e-0b65a3d4a6db.svg\",\n                alt: \"TypeScript\",\n                href: \"https://typescriptlang.org\",\n              },\n            ].map((img) => (\n              <a\n                key={img.href}\n                href={img.href}\n                className=\"flex h-16 w-32 justify-center p-1 grayscale transition hover:grayscale-0 focus:grayscale-0\"\n              >\n                <img alt={img.alt} src={img.src} className=\"object-contain\" />\n              </a>\n            ))}\n          </div>\n        </div>\n      </div>\n    </main>\n  );\n}\n"
  },
  {
    "path": "examples/complex/app/routes/join.tsx",
    "content": "import type { ActionArgs, LoaderArgs, MetaFunction } from \"@remix-run/node\";\nimport { json, redirect } from \"@remix-run/node\";\nimport { Form, Link, useActionData, useSearchParams } from \"@remix-run/react\";\nimport * as React from \"react\";\n\nimport { getUserId, createUserSession } from \"~/session.server\";\n\nimport { createUser, getUserByEmail } from \"~/models/user.server\";\nimport { safeRedirect, validateEmail } from \"~/utils\";\n\nexport async function loader({ request }: LoaderArgs) {\n  const userId = await getUserId(request);\n  if (userId) return redirect(\"/\");\n  return json({});\n}\n\nexport async function action({ request }: ActionArgs) {\n  const formData = await request.formData();\n  const email = formData.get(\"email\");\n  const password = formData.get(\"password\");\n  const redirectTo = safeRedirect(formData.get(\"redirectTo\"), \"/\");\n\n  if (!validateEmail(email)) {\n    return json(\n      { errors: { email: \"Email is invalid\", password: null } },\n      { status: 400 }\n    );\n  }\n\n  if (typeof password !== \"string\" || password.length === 0) {\n    return json(\n      { errors: { email: null, password: \"Password is required\" } },\n      { status: 400 }\n    );\n  }\n\n  if (password.length < 8) {\n    return json(\n      { errors: { email: null, password: \"Password is too short\" } },\n      { status: 400 }\n    );\n  }\n\n  const existingUser = await getUserByEmail(email);\n  if (existingUser) {\n    return json(\n      {\n        errors: {\n          email: \"A user already exists with this email\",\n          password: null,\n        },\n      },\n      { status: 400 }\n    );\n  }\n\n  const user = await createUser(email, password);\n\n  return createUserSession({\n    request,\n    userId: user.id,\n    remember: false,\n    redirectTo,\n  });\n}\n\nexport const meta: MetaFunction = () => {\n  return {\n    title: \"Sign Up\",\n  };\n};\n\nexport default function Join() {\n  const [searchParams] = useSearchParams();\n  const redirectTo = searchParams.get(\"redirectTo\") ?? undefined;\n  const actionData = useActionData<typeof action>();\n  const emailRef = React.useRef<HTMLInputElement>(null);\n  const passwordRef = React.useRef<HTMLInputElement>(null);\n\n  React.useEffect(() => {\n    if (actionData?.errors?.email) {\n      emailRef.current?.focus();\n    } else if (actionData?.errors?.password) {\n      passwordRef.current?.focus();\n    }\n  }, [actionData]);\n\n  return (\n    <div className=\"flex min-h-full flex-col justify-center\">\n      <div className=\"mx-auto w-full max-w-md px-8\">\n        <Form method=\"post\" className=\"space-y-6\">\n          <div>\n            <label\n              htmlFor=\"email\"\n              className=\"block text-sm font-medium text-gray-700\"\n            >\n              Email address\n            </label>\n            <div className=\"mt-1\">\n              <input\n                ref={emailRef}\n                id=\"email\"\n                required\n                autoFocus={true}\n                name=\"email\"\n                type=\"email\"\n                autoComplete=\"email\"\n                aria-invalid={actionData?.errors?.email ? true : undefined}\n                aria-describedby=\"email-error\"\n                className=\"w-full rounded border border-gray-500 px-2 py-1 text-lg\"\n              />\n              {actionData?.errors?.email && (\n                <div className=\"pt-1 text-red-700\" id=\"email-error\">\n                  {actionData.errors.email}\n                </div>\n              )}\n            </div>\n          </div>\n\n          <div>\n            <label\n              htmlFor=\"password\"\n              className=\"block text-sm font-medium text-gray-700\"\n            >\n              Password\n            </label>\n            <div className=\"mt-1\">\n              <input\n                id=\"password\"\n                ref={passwordRef}\n                name=\"password\"\n                type=\"password\"\n                autoComplete=\"new-password\"\n                aria-invalid={actionData?.errors?.password ? true : undefined}\n                aria-describedby=\"password-error\"\n                className=\"w-full rounded border border-gray-500 px-2 py-1 text-lg\"\n              />\n              {actionData?.errors?.password && (\n                <div className=\"pt-1 text-red-700\" id=\"password-error\">\n                  {actionData.errors.password}\n                </div>\n              )}\n            </div>\n          </div>\n\n          <input type=\"hidden\" name=\"redirectTo\" value={redirectTo} />\n          <button\n            type=\"submit\"\n            className=\"w-full rounded bg-blue-500  py-2 px-4 text-white hover:bg-blue-600 focus:bg-blue-400\"\n          >\n            Create Account\n          </button>\n          <div className=\"flex items-center justify-center\">\n            <div className=\"text-center text-sm text-gray-500\">\n              Already have an account?{\" \"}\n              <Link\n                className=\"text-blue-500 underline\"\n                to={{\n                  pathname: \"/login\",\n                  search: searchParams.toString(),\n                }}\n              >\n                Log in\n              </Link>\n            </div>\n          </div>\n        </Form>\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "examples/complex/app/routes/login.tsx",
    "content": "import type { ActionArgs, LoaderArgs, MetaFunction } from \"@remix-run/node\";\nimport { json, redirect } from \"@remix-run/node\";\nimport { Form, Link, useActionData, useSearchParams } from \"@remix-run/react\";\nimport * as React from \"react\";\n\nimport { createUserSession, getUserId } from \"~/session.server\";\nimport { verifyLogin } from \"~/models/user.server\";\nimport { safeRedirect, validateEmail } from \"~/utils\";\n\nexport async function loader({ request }: LoaderArgs) {\n  const userId = await getUserId(request);\n  if (userId) return redirect(\"/\");\n  return json({});\n}\n\nexport async function action({ request }: ActionArgs) {\n  const formData = await request.formData();\n  const email = formData.get(\"email\");\n  const password = formData.get(\"password\");\n  const redirectTo = safeRedirect(formData.get(\"redirectTo\"), \"/notes\");\n  const remember = formData.get(\"remember\");\n\n  if (!validateEmail(email)) {\n    return json(\n      { errors: { email: \"Email is invalid\", password: null } },\n      { status: 400 }\n    );\n  }\n\n  if (typeof password !== \"string\" || password.length === 0) {\n    return json(\n      { errors: { email: null, password: \"Password is required\" } },\n      { status: 400 }\n    );\n  }\n\n  if (password.length < 8) {\n    return json(\n      { errors: { email: null, password: \"Password is too short\" } },\n      { status: 400 }\n    );\n  }\n\n  const user = await verifyLogin(email, password);\n\n  if (!user) {\n    return json(\n      { errors: { email: \"Invalid email or password\", password: null } },\n      { status: 400 }\n    );\n  }\n\n  return createUserSession({\n    request,\n    userId: user.id,\n    remember: remember === \"on\" ? true : false,\n    redirectTo,\n  });\n}\n\nexport const meta: MetaFunction = () => {\n  return {\n    title: \"Login\",\n  };\n};\n\nexport default function LoginPage() {\n  const [searchParams] = useSearchParams();\n  const redirectTo = searchParams.get(\"redirectTo\") || \"/notes\";\n  const actionData = useActionData<typeof action>();\n  const emailRef = React.useRef<HTMLInputElement>(null);\n  const passwordRef = React.useRef<HTMLInputElement>(null);\n\n  React.useEffect(() => {\n    if (actionData?.errors?.email) {\n      emailRef.current?.focus();\n    } else if (actionData?.errors?.password) {\n      passwordRef.current?.focus();\n    }\n  }, [actionData]);\n\n  return (\n    <div className=\"flex min-h-full flex-col justify-center\">\n      <div className=\"mx-auto w-full max-w-md px-8\">\n        <Form method=\"post\" className=\"space-y-6\">\n          <div>\n            <label\n              htmlFor=\"email\"\n              className=\"block text-sm font-medium text-gray-700\"\n            >\n              Email address\n            </label>\n            <div className=\"mt-1\">\n              <input\n                ref={emailRef}\n                id=\"email\"\n                required\n                autoFocus={true}\n                name=\"email\"\n                type=\"email\"\n                autoComplete=\"email\"\n                aria-invalid={actionData?.errors?.email ? true : undefined}\n                aria-describedby=\"email-error\"\n                className=\"w-full rounded border border-gray-500 px-2 py-1 text-lg\"\n              />\n              {actionData?.errors?.email && (\n                <div className=\"pt-1 text-red-700\" id=\"email-error\">\n                  {actionData.errors.email}\n                </div>\n              )}\n            </div>\n          </div>\n\n          <div>\n            <label\n              htmlFor=\"password\"\n              className=\"block text-sm font-medium text-gray-700\"\n            >\n              Password\n            </label>\n            <div className=\"mt-1\">\n              <input\n                id=\"password\"\n                ref={passwordRef}\n                name=\"password\"\n                type=\"password\"\n                autoComplete=\"current-password\"\n                aria-invalid={actionData?.errors?.password ? true : undefined}\n                aria-describedby=\"password-error\"\n                className=\"w-full rounded border border-gray-500 px-2 py-1 text-lg\"\n              />\n              {actionData?.errors?.password && (\n                <div className=\"pt-1 text-red-700\" id=\"password-error\">\n                  {actionData.errors.password}\n                </div>\n              )}\n            </div>\n          </div>\n\n          <input type=\"hidden\" name=\"redirectTo\" value={redirectTo} />\n          <button\n            type=\"submit\"\n            className=\"w-full rounded bg-blue-500  py-2 px-4 text-white hover:bg-blue-600 focus:bg-blue-400\"\n          >\n            Log in\n          </button>\n          <div className=\"flex items-center justify-between\">\n            <div className=\"flex items-center\">\n              <input\n                id=\"remember\"\n                name=\"remember\"\n                type=\"checkbox\"\n                className=\"h-4 w-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500\"\n              />\n              <label\n                htmlFor=\"remember\"\n                className=\"ml-2 block text-sm text-gray-900\"\n              >\n                Remember me\n              </label>\n            </div>\n            <div className=\"text-center text-sm text-gray-500\">\n              Don't have an account?{\" \"}\n              <Link\n                className=\"text-blue-500 underline\"\n                to={{\n                  pathname: \"/join\",\n                  search: searchParams.toString(),\n                }}\n              >\n                Sign up\n              </Link>\n            </div>\n          </div>\n        </Form>\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "examples/complex/app/routes/logout.tsx",
    "content": "import type { ActionArgs } from \"@remix-run/node\";\nimport { redirect } from \"@remix-run/node\";\n\nimport { logout } from \"~/session.server\";\n\nexport async function action({ request }: ActionArgs) {\n  return logout(request);\n}\n\nexport async function loader() {\n  return redirect(\"/\");\n}\n"
  },
  {
    "path": "examples/complex/app/routes/notes/$noteId.tsx",
    "content": "import type { ActionArgs, LoaderArgs } from \"@remix-run/node\";\nimport { json, redirect } from \"@remix-run/node\";\nimport { Form, useCatch, useLoaderData } from \"@remix-run/react\";\nimport invariant from \"tiny-invariant\";\n\nimport { deleteNote, getNote } from \"~/models/note.server\";\nimport { requireUserId } from \"~/session.server\";\n\nexport async function loader({ request, params }: LoaderArgs) {\n  const userId = await requireUserId(request);\n  invariant(params.noteId, \"noteId not found\");\n\n  const note = await getNote({ userId, id: params.noteId });\n  if (!note) {\n    throw new Response(\"Not Found\", { status: 404 });\n  }\n  return json({ note });\n}\n\nexport async function action({ request, params }: ActionArgs) {\n  const userId = await requireUserId(request);\n  invariant(params.noteId, \"noteId not found\");\n\n  await deleteNote({ userId, id: params.noteId });\n\n  return redirect(\"/notes\");\n}\n\nexport default function NoteDetailsPage() {\n  const data = useLoaderData<typeof loader>();\n\n  return (\n    <div>\n      <h3 className=\"text-2xl font-bold\">{data.note.title}</h3>\n      <p className=\"py-6\">{data.note.body}</p>\n      <hr className=\"my-4\" />\n      <Form method=\"post\">\n        <button\n          type=\"submit\"\n          className=\"rounded bg-blue-500  py-2 px-4 text-white hover:bg-blue-600 focus:bg-blue-400\"\n        >\n          Delete\n        </button>\n      </Form>\n    </div>\n  );\n}\n\nexport function ErrorBoundary({ error }: { error: Error }) {\n  console.error(error);\n\n  return <div>An unexpected error occurred: {error.message}</div>;\n}\n\nexport function CatchBoundary() {\n  const caught = useCatch();\n\n  if (caught.status === 404) {\n    return <div>Note not found</div>;\n  }\n\n  throw new Error(`Unexpected caught response with status: ${caught.status}`);\n}\n"
  },
  {
    "path": "examples/complex/app/routes/notes/index.tsx",
    "content": "import { Link } from \"@remix-run/react\";\n\nexport default function NoteIndexPage() {\n  return (\n    <p>\n      No note selected. Select a note on the left, or{\" \"}\n      <Link to=\"new\" className=\"text-blue-500 underline\">\n        create a new note.\n      </Link>\n    </p>\n  );\n}\n"
  },
  {
    "path": "examples/complex/app/routes/notes/new.tsx",
    "content": "import type { ActionArgs } from \"@remix-run/node\";\nimport { json, redirect } from \"@remix-run/node\";\nimport { Form, useActionData } from \"@remix-run/react\";\nimport * as React from \"react\";\n\nimport { createNote } from \"~/models/note.server\";\nimport { requireUserId } from \"~/session.server\";\n\nexport async function action({ request }: ActionArgs) {\n  const userId = await requireUserId(request);\n\n  const formData = await request.formData();\n  const title = formData.get(\"title\");\n  const body = formData.get(\"body\");\n\n  if (typeof title !== \"string\" || title.length === 0) {\n    return json(\n      { errors: { title: \"Title is required\", body: null } },\n      { status: 400 }\n    );\n  }\n\n  if (typeof body !== \"string\" || body.length === 0) {\n    return json(\n      { errors: { title: null, body: \"Body is required\" } },\n      { status: 400 }\n    );\n  }\n\n  const note = await createNote({ title, body, userId });\n\n  return redirect(`/notes/${note.id}`);\n}\n\nexport default function NewNotePage() {\n  const actionData = useActionData<typeof action>();\n  const titleRef = React.useRef<HTMLInputElement>(null);\n  const bodyRef = React.useRef<HTMLTextAreaElement>(null);\n\n  React.useEffect(() => {\n    if (actionData?.errors?.title) {\n      titleRef.current?.focus();\n    } else if (actionData?.errors?.body) {\n      bodyRef.current?.focus();\n    }\n  }, [actionData]);\n\n  return (\n    <Form\n      method=\"post\"\n      style={{\n        display: \"flex\",\n        flexDirection: \"column\",\n        gap: 8,\n        width: \"100%\",\n      }}\n    >\n      <div>\n        <label className=\"flex w-full flex-col gap-1\">\n          <span>Title: </span>\n          <input\n            ref={titleRef}\n            name=\"title\"\n            className=\"flex-1 rounded-md border-2 border-blue-500 px-3 text-lg leading-loose\"\n            aria-invalid={actionData?.errors?.title ? true : undefined}\n            aria-errormessage={\n              actionData?.errors?.title ? \"title-error\" : undefined\n            }\n          />\n        </label>\n        {actionData?.errors?.title && (\n          <div className=\"pt-1 text-red-700\" id=\"title-error\">\n            {actionData.errors.title}\n          </div>\n        )}\n      </div>\n\n      <div>\n        <label className=\"flex w-full flex-col gap-1\">\n          <span>Body: </span>\n          <textarea\n            ref={bodyRef}\n            name=\"body\"\n            rows={8}\n            className=\"w-full flex-1 rounded-md border-2 border-blue-500 py-2 px-3 text-lg leading-6\"\n            aria-invalid={actionData?.errors?.body ? true : undefined}\n            aria-errormessage={\n              actionData?.errors?.body ? \"body-error\" : undefined\n            }\n          />\n        </label>\n        {actionData?.errors?.body && (\n          <div className=\"pt-1 text-red-700\" id=\"body-error\">\n            {actionData.errors.body}\n          </div>\n        )}\n      </div>\n\n      <div className=\"text-right\">\n        <button\n          type=\"submit\"\n          className=\"rounded bg-blue-500 py-2 px-4 text-white hover:bg-blue-600 focus:bg-blue-400\"\n        >\n          Save\n        </button>\n      </div>\n    </Form>\n  );\n}\n"
  },
  {
    "path": "examples/complex/app/routes/notes.tsx",
    "content": "import type { LoaderArgs } from \"@remix-run/node\";\nimport { json } from \"@remix-run/node\";\nimport { Form, Link, NavLink, Outlet, useLoaderData } from \"@remix-run/react\";\n\nimport { requireUserId } from \"~/session.server\";\nimport { useUser } from \"~/utils\";\nimport { getNoteListItems } from \"~/models/note.server\";\n\nexport async function loader({ request }: LoaderArgs) {\n  const userId = await requireUserId(request);\n  const noteListItems = await getNoteListItems({ userId });\n  return json({ noteListItems });\n}\n\nexport default function NotesPage() {\n  const data = useLoaderData<typeof loader>();\n  const user = useUser();\n\n  return (\n    <div className=\"flex h-full min-h-screen flex-col\">\n      <header className=\"flex items-center justify-between bg-slate-800 p-4 text-white\">\n        <h1 className=\"text-3xl font-bold\">\n          <Link to=\".\">Notes</Link>\n        </h1>\n        <p>{user.email}</p>\n        <Form action=\"/logout\" method=\"post\">\n          <button\n            type=\"submit\"\n            className=\"rounded bg-slate-600 py-2 px-4 text-blue-100 hover:bg-blue-500 active:bg-blue-600\"\n          >\n            Logout\n          </button>\n        </Form>\n      </header>\n\n      <main className=\"flex h-full bg-white\">\n        <div className=\"h-full w-80 border-r bg-gray-50\">\n          <Link to=\"new\" className=\"block p-4 text-xl text-blue-500\">\n            + New Note\n          </Link>\n\n          <hr />\n\n          {data.noteListItems.length === 0 ? (\n            <p className=\"p-4\">No notes yet</p>\n          ) : (\n            <ol>\n              {data.noteListItems.map((note) => (\n                <li key={note.id}>\n                  <NavLink\n                    className={({ isActive }) =>\n                      `block border-b p-4 text-xl ${isActive ? \"bg-white\" : \"\"}`\n                    }\n                    to={note.id}\n                  >\n                    📝 {note.title}\n                  </NavLink>\n                </li>\n              ))}\n            </ol>\n          )}\n        </div>\n\n        <div className=\"flex-1 p-6\">\n          <Outlet />\n        </div>\n      </main>\n    </div>\n  );\n}\n"
  },
  {
    "path": "examples/complex/app/session.server.ts",
    "content": "import { createCookieSessionStorage, redirect } from \"@remix-run/node\";\nimport invariant from \"tiny-invariant\";\n\nimport type { User } from \"~/models/user.server\";\nimport { getUserById } from \"~/models/user.server\";\n\ninvariant(process.env.SESSION_SECRET, \"SESSION_SECRET must be set\");\n\nexport const sessionStorage = createCookieSessionStorage({\n  cookie: {\n    name: \"__session\",\n    httpOnly: true,\n    path: \"/\",\n    sameSite: \"lax\",\n    secrets: [process.env.SESSION_SECRET],\n    secure: process.env.NODE_ENV === \"production\",\n  },\n});\n\nconst USER_SESSION_KEY = \"userId\";\n\nexport async function getSession(request: Request) {\n  const cookie = request.headers.get(\"Cookie\");\n  return sessionStorage.getSession(cookie);\n}\n\nexport async function getUserId(\n  request: Request\n): Promise<User[\"id\"] | undefined> {\n  const session = await getSession(request);\n  const userId = session.get(USER_SESSION_KEY);\n  return userId;\n}\n\nexport async function getUser(request: Request) {\n  const userId = await getUserId(request);\n  if (userId === undefined) return null;\n\n  const user = await getUserById(userId);\n  if (user) return user;\n\n  throw await logout(request);\n}\n\nexport async function requireUserId(\n  request: Request,\n  redirectTo: string = new URL(request.url).pathname\n) {\n  const userId = await getUserId(request);\n  if (!userId) {\n    const searchParams = new URLSearchParams([[\"redirectTo\", redirectTo]]);\n    throw redirect(`/login?${searchParams}`);\n  }\n  return userId;\n}\n\nexport async function requireUser(request: Request) {\n  const userId = await requireUserId(request);\n\n  const user = await getUserById(userId);\n  if (user) return user;\n\n  throw await logout(request);\n}\n\nexport async function createUserSession({\n  request,\n  userId,\n  remember,\n  redirectTo,\n}: {\n  request: Request;\n  userId: string;\n  remember: boolean;\n  redirectTo: string;\n}) {\n  const session = await getSession(request);\n  session.set(USER_SESSION_KEY, userId);\n  return redirect(redirectTo, {\n    headers: {\n      \"Set-Cookie\": await sessionStorage.commitSession(session, {\n        maxAge: remember\n          ? 60 * 60 * 24 * 7 // 7 days\n          : undefined,\n      }),\n    },\n  });\n}\n\nexport async function logout(request: Request) {\n  const session = await getSession(request);\n  return redirect(\"/\", {\n    headers: {\n      \"Set-Cookie\": await sessionStorage.destroySession(session),\n    },\n  });\n}\n"
  },
  {
    "path": "examples/complex/app/utils.test.ts",
    "content": "import { validateEmail } from \"./utils\";\n\ntest(\"validateEmail returns false for non-emails\", () => {\n  expect(validateEmail(undefined)).toBe(false);\n  expect(validateEmail(null)).toBe(false);\n  expect(validateEmail(\"\")).toBe(false);\n  expect(validateEmail(\"not-an-email\")).toBe(false);\n  expect(validateEmail(\"n@\")).toBe(false);\n});\n\ntest(\"validateEmail returns true for emails\", () => {\n  expect(validateEmail(\"kody@example.com\")).toBe(true);\n});\n"
  },
  {
    "path": "examples/complex/app/utils.ts",
    "content": "import { useMatches } from \"@remix-run/react\";\nimport { useMemo } from \"react\";\n\nimport type { User } from \"~/models/user.server\";\n\nconst DEFAULT_REDIRECT = \"/\";\n\n/**\n * This should be used any time the redirect path is user-provided\n * (Like the query string on our login/signup pages). This avoids\n * open-redirect vulnerabilities.\n * @param {string} to The redirect destination\n * @param {string} defaultRedirect The redirect to use if the to is unsafe.\n */\nexport function safeRedirect(\n  to: FormDataEntryValue | string | null | undefined,\n  defaultRedirect: string = DEFAULT_REDIRECT\n) {\n  if (!to || typeof to !== \"string\") {\n    return defaultRedirect;\n  }\n\n  if (!to.startsWith(\"/\") || to.startsWith(\"//\")) {\n    return defaultRedirect;\n  }\n\n  return to;\n}\n\n/**\n * This base hook is used in other hooks to quickly search for specific data\n * across all loader data using useMatches.\n * @param {string} id The route id\n * @returns {JSON|undefined} The router data or undefined if not found\n */\nexport function useMatchesData(\n  id: string\n): Record<string, unknown> | undefined {\n  const matchingRoutes = useMatches();\n  const route = useMemo(\n    () => matchingRoutes.find((route) => route.id === id),\n    [matchingRoutes, id]\n  );\n  return route?.data;\n}\n\nfunction isUser(user: any): user is User {\n  return user && typeof user === \"object\" && typeof user.email === \"string\";\n}\n\nexport function useOptionalUser(): User | undefined {\n  const data = useMatchesData(\"root\");\n  if (!data || !isUser(data.user)) {\n    return undefined;\n  }\n  return data.user;\n}\n\nexport function useUser(): User {\n  const maybeUser = useOptionalUser();\n  if (!maybeUser) {\n    throw new Error(\n      \"No user found in root loader, but user is required by useUser. If user is optional, try useOptionalUser instead.\"\n    );\n  }\n  return maybeUser;\n}\n\nexport function validateEmail(email: unknown): email is string {\n  return typeof email === \"string\" && email.length > 3 && email.includes(\"@\");\n}\n"
  },
  {
    "path": "examples/complex/custom-server.js",
    "content": "const express = require('express');\nconst { createRequestHandler } = require('@remix-run/express');\nconst { createRemixViteDevServer, getRemixViteBuild } = require('../../lib');\n\nconst app = express();\n\n// Create a remix-vite dev server.\ncreateRemixViteDevServer().then(remixViteDevServer => {\n  // Use remix-vite dev server as middleware.\n  app.use(remixViteDevServer.middlewares);\n\n  app.all('*', async (req, res, next) => {\n    // Get the remix build generated by remix-vite.\n    const remixBuild = await getRemixViteBuild(remixViteDevServer);\n  \n    // Create a remix express request handler.\n    const requestHandler = createRequestHandler({ build: remixBuild });\n  \n    await requestHandler(req, res, next);\n  });\n\n  // Start the server.\n  app.listen(3000, () => {\n    console.log('Listening at http://localhost:3000');\n  });\n});\n"
  },
  {
    "path": "examples/complex/cypress/.eslintrc.js",
    "content": "module.exports = {\n  parserOptions: {\n    tsconfigRootDir: __dirname,\n    project: \"./tsconfig.json\",\n  },\n};\n"
  },
  {
    "path": "examples/complex/cypress/e2e/smoke.cy.ts",
    "content": "import { faker } from \"@faker-js/faker\";\n\ndescribe(\"smoke tests\", () => {\n  afterEach(() => {\n    cy.cleanupUser();\n  });\n\n  it(\"should allow you to register and login\", () => {\n    const loginForm = {\n      email: `${faker.internet.userName()}@example.com`,\n      password: faker.internet.password(),\n    };\n\n    cy.then(() => ({ email: loginForm.email })).as(\"user\");\n\n    cy.visitAndCheck(\"/\");\n\n    cy.findByRole(\"link\", { name: /sign up/i }).click();\n\n    cy.findByRole(\"textbox\", { name: /email/i }).type(loginForm.email);\n    cy.findByLabelText(/password/i).type(loginForm.password);\n    cy.findByRole(\"button\", { name: /create account/i }).click();\n\n    cy.findByRole(\"link\", { name: /notes/i }).click();\n    cy.findByRole(\"button\", { name: /logout/i }).click();\n    cy.findByRole(\"link\", { name: /log in/i });\n  });\n\n  it(\"should allow you to make a note\", () => {\n    const testNote = {\n      title: faker.lorem.words(1),\n      body: faker.lorem.sentences(1),\n    };\n    cy.login();\n\n    cy.visitAndCheck(\"/\");\n\n    cy.findByRole(\"link\", { name: /notes/i }).click();\n    cy.findByText(\"No notes yet\");\n\n    cy.findByRole(\"link\", { name: /\\+ new note/i }).click();\n\n    cy.findByRole(\"textbox\", { name: /title/i }).type(testNote.title);\n    cy.findByRole(\"textbox\", { name: /body/i }).type(testNote.body);\n    cy.findByRole(\"button\", { name: /save/i }).click();\n\n    cy.findByRole(\"button\", { name: /delete/i }).click();\n\n    cy.findByText(\"No notes yet\");\n  });\n});\n"
  },
  {
    "path": "examples/complex/cypress/fixtures/example.json",
    "content": "{\n  \"name\": \"Using fixtures to represent data\",\n  \"email\": \"hello@cypress.io\",\n  \"body\": \"Fixtures are a great way to mock data for responses to routes\"\n}\n"
  },
  {
    "path": "examples/complex/cypress/support/commands.ts",
    "content": "import { faker } from \"@faker-js/faker\";\n\ndeclare global {\n  namespace Cypress {\n    interface Chainable {\n      /**\n       * Logs in with a random user. Yields the user and adds an alias to the user\n       *\n       * @returns {typeof login}\n       * @memberof Chainable\n       * @example\n       *    cy.login()\n       * @example\n       *    cy.login({ email: 'whatever@example.com' })\n       */\n      login: typeof login;\n\n      /**\n       * Deletes the current @user\n       *\n       * @returns {typeof cleanupUser}\n       * @memberof Chainable\n       * @example\n       *    cy.cleanupUser()\n       * @example\n       *    cy.cleanupUser({ email: 'whatever@example.com' })\n       */\n      cleanupUser: typeof cleanupUser;\n\n      /**\n       * Extends the standard visit command to wait for the page to load\n       *\n       * @returns {typeof visitAndCheck}\n       * @memberof Chainable\n       * @example\n       *    cy.visitAndCheck('/')\n       *  @example\n       *    cy.visitAndCheck('/', 500)\n       */\n      visitAndCheck: typeof visitAndCheck;\n    }\n  }\n}\n\nfunction login({\n  email = faker.internet.email(undefined, undefined, \"example.com\"),\n}: {\n  email?: string;\n} = {}) {\n  cy.then(() => ({ email })).as(\"user\");\n  cy.exec(\n    `npx ts-node --require tsconfig-paths/register ./cypress/support/create-user.ts \"${email}\"`\n  ).then(({ stdout }) => {\n    const cookieValue = stdout\n      .replace(/.*<cookie>(?<cookieValue>.*)<\\/cookie>.*/s, \"$<cookieValue>\")\n      .trim();\n    cy.setCookie(\"__session\", cookieValue);\n  });\n  return cy.get(\"@user\");\n}\n\nfunction cleanupUser({ email }: { email?: string } = {}) {\n  if (email) {\n    deleteUserByEmail(email);\n  } else {\n    cy.get(\"@user\").then((user) => {\n      const email = (user as { email?: string }).email;\n      if (email) {\n        deleteUserByEmail(email);\n      }\n    });\n  }\n  cy.clearCookie(\"__session\");\n}\n\nfunction deleteUserByEmail(email: string) {\n  cy.exec(\n    `npx ts-node --require tsconfig-paths/register ./cypress/support/delete-user.ts \"${email}\"`\n  );\n  cy.clearCookie(\"__session\");\n}\n\n// We're waiting a second because of this issue happen randomly\n// https://github.com/cypress-io/cypress/issues/7306\n// Also added custom types to avoid getting detached\n// https://github.com/cypress-io/cypress/issues/7306#issuecomment-1152752612\n// ===========================================================\nfunction visitAndCheck(url: string, waitTime: number = 1000) {\n  cy.visit(url);\n  cy.location(\"pathname\").should(\"contain\", url).wait(waitTime);\n}\n\nCypress.Commands.add(\"login\", login);\nCypress.Commands.add(\"cleanupUser\", cleanupUser);\nCypress.Commands.add(\"visitAndCheck\", visitAndCheck);\n"
  },
  {
    "path": "examples/complex/cypress/support/create-user.ts",
    "content": "// Use this to create a new user and login with that user\n// Simply call this with:\n// npx ts-node --require tsconfig-paths/register ./cypress/support/create-user.ts username@example.com\n// and it will log out the cookie value you can use to interact with the server\n// as that new user.\n\nimport { installGlobals } from \"@remix-run/node\";\nimport { parse } from \"cookie\";\n\nimport { createUser } from \"~/models/user.server\";\nimport { createUserSession } from \"~/session.server\";\n\ninstallGlobals();\n\nasync function createAndLogin(email: string) {\n  if (!email) {\n    throw new Error(\"email required for login\");\n  }\n  if (!email.endsWith(\"@example.com\")) {\n    throw new Error(\"All test emails must end in @example.com\");\n  }\n\n  const user = await createUser(email, \"myreallystrongpassword\");\n\n  const response = await createUserSession({\n    request: new Request(\"test://test\"),\n    userId: user.id,\n    remember: false,\n    redirectTo: \"/\",\n  });\n\n  const cookieValue = response.headers.get(\"Set-Cookie\");\n  if (!cookieValue) {\n    throw new Error(\"Cookie missing from createUserSession response\");\n  }\n  const parsedCookie = parse(cookieValue);\n  // we log it like this so our cypress command can parse it out and set it as\n  // the cookie value.\n  console.log(\n    `\n<cookie>\n  ${parsedCookie.__session}\n</cookie>\n  `.trim()\n  );\n}\n\ncreateAndLogin(process.argv[2]);\n"
  },
  {
    "path": "examples/complex/cypress/support/delete-user.ts",
    "content": "// Use this to delete a user by their email\n// Simply call this with:\n// npx ts-node --require tsconfig-paths/register ./cypress/support/delete-user.ts username@example.com\n// and that user will get deleted\n\nimport { PrismaClientKnownRequestError } from \"@prisma/client/runtime\";\nimport { installGlobals } from \"@remix-run/node\";\n\nimport { prisma } from \"~/db.server\";\n\ninstallGlobals();\n\nasync function deleteUser(email: string) {\n  if (!email) {\n    throw new Error(\"email required for login\");\n  }\n  if (!email.endsWith(\"@example.com\")) {\n    throw new Error(\"All test emails must end in @example.com\");\n  }\n\n  try {\n    await prisma.user.delete({ where: { email } });\n  } catch (error) {\n    if (\n      error instanceof PrismaClientKnownRequestError &&\n      error.code === \"P2025\"\n    ) {\n      console.log(\"User not found, so no need to delete\");\n    } else {\n      throw error;\n    }\n  } finally {\n    await prisma.$disconnect();\n  }\n}\n\ndeleteUser(process.argv[2]);\n"
  },
  {
    "path": "examples/complex/cypress/support/e2e.ts",
    "content": "import \"@testing-library/cypress/add-commands\";\nimport \"./commands\";\n\nCypress.on(\"uncaught:exception\", (err) => {\n  // Cypress and React Hydrating the document don't get along\n  // for some unknown reason. Hopefully we figure out why eventually\n  // so we can remove this.\n  if (\n    /hydrat/i.test(err.message) ||\n    /Minified React error #418/.test(err.message) ||\n    /Minified React error #423/.test(err.message)\n  ) {\n    return false;\n  }\n});\n"
  },
  {
    "path": "examples/complex/cypress/tsconfig.json",
    "content": "{\n  \"exclude\": [\n    \"../node_modules/@types/jest\",\n    \"../node_modules/@testing-library/jest-dom\"\n  ],\n  \"include\": [\n    \"e2e/**/*\",\n    \"support/**/*\",\n    \"../node_modules/cypress\",\n    \"../node_modules/@testing-library/cypress\"\n  ],\n  \"compilerOptions\": {\n    \"baseUrl\": \".\",\n    \"noEmit\": true,\n    \"types\": [\"node\", \"cypress\", \"@testing-library/cypress\"],\n    \"esModuleInterop\": true,\n    \"jsx\": \"react-jsx\",\n    \"moduleResolution\": \"node\",\n    \"target\": \"es2019\",\n    \"strict\": true,\n    \"skipLibCheck\": true,\n    \"resolveJsonModule\": true,\n    \"typeRoots\": [\"../types\", \"../node_modules/@types\"],\n\n    \"paths\": {\n      \"~/*\": [\"../app/*\"]\n    }\n  }\n}\n"
  },
  {
    "path": "examples/complex/cypress.config.ts",
    "content": "import { defineConfig } from \"cypress\";\n\nexport default defineConfig({\n  e2e: {\n    setupNodeEvents: (on, config) => {\n      const isDev = config.watchForFileChanges;\n      const port = process.env.PORT ?? (isDev ? \"3000\" : \"8811\");\n      const configOverrides: Partial<Cypress.PluginConfigOptions> = {\n        baseUrl: `http://localhost:${port}`,\n        video: !process.env.CI,\n        screenshotOnRunFailure: !process.env.CI,\n      };\n\n      // To use this:\n      // cy.task('log', whateverYouWantInTheTerminal)\n      on(\"task\", {\n        log: (message) => {\n          console.log(message);\n\n          return null;\n        },\n      });\n\n      return { ...config, ...configOverrides };\n    },\n  },\n});\n"
  },
  {
    "path": "examples/complex/fly.toml",
    "content": "app = \"indie-stack-template\"\n\nkill_signal = \"SIGINT\"\nkill_timeout = 5\nprocesses = []\n\n[experimental]\n  allowed_public_ports = []\n  auto_rollback = true\n  cmd = \"start.sh\"\n  entrypoint = \"sh\"\n\n[mounts]\n  source = \"data\"\n  destination = \"/data\"\n\n[[services]]\n  internal_port = 8080\n  processes = [\"app\"]\n  protocol = \"tcp\"\n  script_checks = []\n\n  [services.concurrency]\n    hard_limit = 25\n    soft_limit = 20\n    type = \"connections\"\n\n  [[services.ports]]\n    handlers = [\"http\"]\n    port = 80\n    force_https = true\n\n  [[services.ports]]\n    handlers = [\"tls\", \"http\"]\n    port = 443\n\n  [[services.tcp_checks]]\n    grace_period = \"1s\"\n    interval = \"15s\"\n    restart_limit = 0\n    timeout = \"2s\"\n\n  [[services.http_checks]]\n    interval = \"10s\"\n    grace_period = \"5s\"\n    method = \"get\"\n    path = \"/healthcheck\"\n    protocol = \"http\"\n    timeout = \"2s\"\n    tls_skip_verify = false\n    [services.http_checks.headers]\n"
  },
  {
    "path": "examples/complex/mocks/README.md",
    "content": "# Mocks\n\nUse this to mock any third party HTTP resources that you don't have running locally and want to have mocked for local development as well as tests.\n\nLearn more about how to use this at [mswjs.io](https://mswjs.io/)\n\nFor an extensive example, see the [source code for kentcdodds.com](https://github.com/kentcdodds/kentcdodds.com/blob/main/mocks/start.ts)\n"
  },
  {
    "path": "examples/complex/mocks/index.js",
    "content": "const { setupServer } = require(\"msw/node\");\n\nconst server = setupServer();\n\nserver.listen({ onUnhandledRequest: \"bypass\" });\nconsole.info(\"🔶 Mock server running\");\n\nprocess.once(\"SIGINT\", () => server.close());\nprocess.once(\"SIGTERM\", () => server.close());\n"
  },
  {
    "path": "examples/complex/package.json",
    "content": "{\n  \"name\": \"indie-stack-template\",\n  \"private\": true,\n  \"sideEffects\": false,\n  \"scripts\": {\n    \"build\": \"run-s build:*\",\n    \"build:css\": \"npm run generate:css -- --minify\",\n    \"build:remix\": \"remix build\",\n    \"dev\": \"run-p dev:*\",\n    \"dev:css\": \"npm run generate:css -- --watch\",\n    \"dev:remix\": \"remix watch\",\n    \"dev:server\": \"node ./build\",\n    \"format\": \"prettier --write .\",\n    \"generate:css\": \"tailwindcss -o ./app/styles/tailwind.css\",\n    \"lint\": \"eslint --cache --cache-location ./node_modules/.cache/eslint .\",\n    \"setup\": \"prisma generate && prisma migrate deploy && prisma db seed\",\n    \"start\": \"remix-serve build\",\n    \"start:mocks\": \"binode --require ./mocks -- @remix-run/serve:remix-serve build\",\n    \"test\": \"vitest\",\n    \"test:e2e:dev\": \"start-server-and-test dev http://localhost:3000 \\\"npx cypress open\\\"\",\n    \"pretest:e2e:run\": \"npm run build\",\n    \"test:e2e:run\": \"cross-env PORT=8811 start-server-and-test start:mocks http://localhost:8811 \\\"npx cypress run\\\"\",\n    \"typecheck\": \"tsc -b && tsc -b cypress\",\n    \"validate\": \"run-p \\\"test -- --run\\\" lint typecheck test:e2e:run\"\n  },\n  \"prettier\": {},\n  \"eslintIgnore\": [\n    \"/node_modules\",\n    \"/build\",\n    \"/public/build\"\n  ],\n  \"dependencies\": {\n    \"@prisma/client\": \"^4.3.1\",\n    \"@remix-run/node\": \"^1.10.0\",\n    \"@remix-run/react\": \"^1.10.0\",\n    \"@remix-run/serve\": \"^1.10.0\",\n    \"@remix-run/server-runtime\": \"^1.10.0\",\n    \"bcryptjs\": \"^2.4.3\",\n    \"dedent\": \"0.7.0\",\n    \"dotenv\": \"^16.0.3\",\n    \"isbot\": \"^3.5.3\",\n    \"javascript-time-ago\": \"2.5.9\",\n    \"react\": \"^18.2.0\",\n    \"react-dom\": \"^18.2.0\",\n    \"tiny-invariant\": \"^1.2.0\"\n  },\n  \"devDependencies\": {\n    \"@faker-js/faker\": \"^7.5.0\",\n    \"@remix-run/dev\": \"^1.10.0\",\n    \"@remix-run/eslint-config\": \"^1.10.0\",\n    \"@testing-library/cypress\": \"^8.0.3\",\n    \"@testing-library/dom\": \"^8.18.1\",\n    \"@testing-library/jest-dom\": \"^5.16.5\",\n    \"@testing-library/react\": \"^13.4.0\",\n    \"@testing-library/user-event\": \"^14.4.3\",\n    \"@types/bcryptjs\": \"^2.4.2\",\n    \"@types/eslint\": \"^8.4.6\",\n    \"@types/express\": \"^4.17.14\",\n    \"@types/jsesc\": \"^3.0.1\",\n    \"@types/node\": \"^18.7.18\",\n    \"@types/react\": \"^18.0.20\",\n    \"@types/react-dom\": \"^18.0.6\",\n    \"@vitejs/plugin-react\": \"^2.2.0\",\n    \"@vitest/coverage-c8\": \"^0.23.4\",\n    \"autoprefixer\": \"^10.4.11\",\n    \"binode\": \"^1.0.5\",\n    \"c8\": \"^7.12.0\",\n    \"cookie\": \"^0.5.0\",\n    \"cross-env\": \"^7.0.3\",\n    \"cypress\": \"^10.8.0\",\n    \"eslint\": \"^8.23.1\",\n    \"eslint-config-prettier\": \"^8.5.0\",\n    \"eslint-plugin-cypress\": \"^2.12.1\",\n    \"happy-dom\": \"^6.0.4\",\n    \"jsesc\": \"^3.0.2\",\n    \"msw\": \"^0.47.3\",\n    \"nodemon\": \"2.0.20\",\n    \"npm-run-all\": \"^4.1.5\",\n    \"postcss\": \"^8.4.16\",\n    \"prettier\": \"2.7.1\",\n    \"prettier-plugin-tailwindcss\": \"^0.1.13\",\n    \"prisma\": \"^4.3.1\",\n    \"start-server-and-test\": \"^1.14.0\",\n    \"tailwindcss\": \"^3.1.8\",\n    \"ts-morph\": \"^16.0.0\",\n    \"ts-node\": \"^10.9.1\",\n    \"tsconfig-paths\": \"^4.1.0\",\n    \"typescript\": \"^4.8.3\",\n    \"vite\": \"^3.2.2\",\n    \"vite-tsconfig-paths\": \"^3.5.2\",\n    \"vitest\": \"^0.23.4\"\n  },\n  \"resolutions\": {\n    \"react\": \"^18.2.0\",\n    \"react-dom\": \"^18.2.0\"\n  },\n  \"engines\": {\n    \"node\": \">=14\"\n  },\n  \"prisma\": {\n    \"seed\": \"ts-node --require tsconfig-paths/register prisma/seed.ts\"\n  }\n}\n"
  },
  {
    "path": "examples/complex/prisma/migrations/20220713162558_init/migration.sql",
    "content": "-- CreateTable\nCREATE TABLE \"User\" (\n    \"id\" TEXT NOT NULL PRIMARY KEY,\n    \"email\" TEXT NOT NULL,\n    \"createdAt\" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,\n    \"updatedAt\" DATETIME NOT NULL\n);\n\n-- CreateTable\nCREATE TABLE \"Password\" (\n    \"hash\" TEXT NOT NULL,\n    \"userId\" TEXT NOT NULL,\n    CONSTRAINT \"Password_userId_fkey\" FOREIGN KEY (\"userId\") REFERENCES \"User\" (\"id\") ON DELETE CASCADE ON UPDATE CASCADE\n);\n\n-- CreateTable\nCREATE TABLE \"Note\" (\n    \"id\" TEXT NOT NULL PRIMARY KEY,\n    \"title\" TEXT NOT NULL,\n    \"body\" TEXT NOT NULL,\n    \"createdAt\" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,\n    \"updatedAt\" DATETIME NOT NULL,\n    \"userId\" TEXT NOT NULL,\n    CONSTRAINT \"Note_userId_fkey\" FOREIGN KEY (\"userId\") REFERENCES \"User\" (\"id\") ON DELETE CASCADE ON UPDATE CASCADE\n);\n\n-- CreateIndex\nCREATE UNIQUE INDEX \"User_email_key\" ON \"User\"(\"email\");\n\n-- CreateIndex\nCREATE UNIQUE INDEX \"Password_userId_key\" ON \"Password\"(\"userId\");\n"
  },
  {
    "path": "examples/complex/prisma/migrations/migration_lock.toml",
    "content": "# Please do not edit this file manually\n# It should be added in your version-control system (i.e. Git)\nprovider = \"sqlite\""
  },
  {
    "path": "examples/complex/prisma/schema.prisma",
    "content": "datasource db {\n  provider = \"sqlite\"\n  url      = env(\"DATABASE_URL\")\n}\n\ngenerator client {\n  provider = \"prisma-client-js\"\n}\n\nmodel User {\n  id    String @id @default(cuid())\n  email String @unique\n\n  createdAt DateTime @default(now())\n  updatedAt DateTime @updatedAt\n\n  password Password?\n  notes    Note[]\n}\n\nmodel Password {\n  hash String\n\n  user   User   @relation(fields: [userId], references: [id], onDelete: Cascade, onUpdate: Cascade)\n  userId String @unique\n}\n\nmodel Note {\n  id    String @id @default(cuid())\n  title String\n  body  String\n\n  createdAt DateTime @default(now())\n  updatedAt DateTime @updatedAt\n\n  user   User   @relation(fields: [userId], references: [id], onDelete: Cascade, onUpdate: Cascade)\n  userId String\n}\n"
  },
  {
    "path": "examples/complex/prisma/seed.ts",
    "content": "import { PrismaClient } from \"@prisma/client\";\nimport bcrypt from \"bcryptjs\";\n\nconst prisma = new PrismaClient();\n\nasync function seed() {\n  const email = \"rachel@remix.run\";\n\n  // cleanup the existing database\n  await prisma.user.delete({ where: { email } }).catch(() => {\n    // no worries if it doesn't exist yet\n  });\n\n  const hashedPassword = await bcrypt.hash(\"racheliscool\", 10);\n\n  const user = await prisma.user.create({\n    data: {\n      email,\n      password: {\n        create: {\n          hash: hashedPassword,\n        },\n      },\n    },\n  });\n\n  await prisma.note.create({\n    data: {\n      title: \"My first note\",\n      body: \"Hello, world!\",\n      userId: user.id,\n    },\n  });\n\n  await prisma.note.create({\n    data: {\n      title: \"My second note\",\n      body: \"Hello, world!\",\n      userId: user.id,\n    },\n  });\n\n  console.log(`Database has been seeded. 🌱`);\n}\n\nseed()\n  .catch((e) => {\n    console.error(e);\n    process.exit(1);\n  })\n  .finally(async () => {\n    await prisma.$disconnect();\n  });\n"
  },
  {
    "path": "examples/complex/remix.config.js",
    "content": "/**\n * @type {import('@remix-run/dev').AppConfig}\n */\nmodule.exports = {\n  cacheDirectory: \"./node_modules/.cache/remix\",\n  ignoredRouteFiles: [\"**/.*\", \"**/*.css\", \"**/*.test.{js,jsx,ts,tsx}\"],\n  server: './custom-server.js'\n};\n"
  },
  {
    "path": "examples/complex/remix.env.d.ts",
    "content": "/// <reference types=\"@remix-run/dev\" />\n/// <reference types=\"@remix-run/node/globals\" />\n"
  },
  {
    "path": "examples/complex/remix.init/gitignore",
    "content": "node_modules\n\n/build\n/public/build\n.env\n\n/cypress/screenshots\n/cypress/videos\n/prisma/data.db\n/prisma/data.db-journal\n\n/app/styles/tailwind.css\n"
  },
  {
    "path": "examples/complex/remix.init/index.js",
    "content": "const { execSync } = require(\"child_process\");\nconst crypto = require(\"crypto\");\nconst fs = require(\"fs/promises\");\nconst path = require(\"path\");\n\nconst toml = require(\"@iarna/toml\");\nconst PackageJson = require(\"@npmcli/package-json\");\nconst semver = require(\"semver\");\nconst YAML = require(\"yaml\");\n\nconst cleanupCypressFiles = ({ fileEntries, isTypeScript, packageManager }) =>\n  fileEntries.flatMap(([filePath, content]) => {\n    let newContent = content.replace(\n      new RegExp(\"npx ts-node\", \"g\"),\n      isTypeScript ? `${packageManager.exec} ts-node` : \"node\"\n    );\n\n    if (!isTypeScript) {\n      newContent = newContent\n        .replace(new RegExp(\"create-user.ts\", \"g\"), \"create-user.js\")\n        .replace(new RegExp(\"delete-user.ts\", \"g\"), \"delete-user.js\");\n    }\n\n    return [fs.writeFile(filePath, newContent)];\n  });\n\nconst cleanupDeployWorkflow = (deployWorkflow, deployWorkflowPath) => {\n  delete deployWorkflow.jobs.typecheck;\n  deployWorkflow.jobs.deploy.needs = deployWorkflow.jobs.deploy.needs.filter(\n    (need) => need !== \"typecheck\"\n  );\n\n  return [fs.writeFile(deployWorkflowPath, YAML.stringify(deployWorkflow))];\n};\n\nconst cleanupVitestConfig = (vitestConfig, vitestConfigPath) => {\n  const newVitestConfig = vitestConfig.replace(\n    \"setup-test-env.ts\",\n    \"setup-test-env.js\"\n  );\n\n  return [fs.writeFile(vitestConfigPath, newVitestConfig)];\n};\n\nconst escapeRegExp = (string) =>\n  // $& means the whole matched string\n  string.replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\");\n\nconst getPackageManagerCommand = (packageManager) =>\n  // Inspired by https://github.com/nrwl/nx/blob/bd9b33eaef0393d01f747ea9a2ac5d2ca1fb87c6/packages/nx/src/utils/package-manager.ts#L38-L103\n  ({\n    npm: () => ({\n      exec: \"npx\",\n      lockfile: \"package-lock.json\",\n      run: (script, args) => `npm run ${script} ${args ? `-- ${args}` : \"\"}`,\n    }),\n    pnpm: () => {\n      const pnpmVersion = getPackageManagerVersion(\"pnpm\");\n      const includeDoubleDashBeforeArgs = semver.lt(pnpmVersion, \"7.0.0\");\n      const useExec = semver.gte(pnpmVersion, \"6.13.0\");\n\n      return {\n        exec: useExec ? \"pnpm exec\" : \"pnpx\",\n        lockfile: \"pnpm-lock.yaml\",\n        run: (script, args) =>\n          includeDoubleDashBeforeArgs\n            ? `pnpm run ${script} ${args ? `-- ${args}` : \"\"}`\n            : `pnpm run ${script} ${args || \"\"}`,\n      };\n    },\n    yarn: () => ({\n      exec: \"yarn\",\n      lockfile: \"yarn.lock\",\n      run: (script, args) => `yarn ${script} ${args || \"\"}`,\n    }),\n  }[packageManager]());\n\nconst getPackageManagerVersion = (packageManager) =>\n  // Copied over from https://github.com/nrwl/nx/blob/bd9b33eaef0393d01f747ea9a2ac5d2ca1fb87c6/packages/nx/src/utils/package-manager.ts#L105-L114\n  execSync(`${packageManager} --version`).toString(\"utf-8\").trim();\n\nconst getRandomString = (length) => crypto.randomBytes(length).toString(\"hex\");\n\nconst readFileIfNotTypeScript = (\n  isTypeScript,\n  filePath,\n  parseFunction = (result) => result\n) =>\n  isTypeScript\n    ? Promise.resolve()\n    : fs.readFile(filePath, \"utf-8\").then(parseFunction);\n\nconst removeUnusedDependencies = (dependencies, unusedDependencies) =>\n  Object.fromEntries(\n    Object.entries(dependencies).filter(\n      ([key]) => !unusedDependencies.includes(key)\n    )\n  );\n\nconst updatePackageJson = ({ APP_NAME, isTypeScript, packageJson }) => {\n  const {\n    devDependencies,\n    prisma: { seed: prismaSeed, ...prisma },\n    scripts: { typecheck, validate, ...scripts },\n  } = packageJson.content;\n\n  packageJson.update({\n    name: APP_NAME,\n    devDependencies: isTypeScript\n      ? devDependencies\n      : removeUnusedDependencies(devDependencies, [\"ts-node\"]),\n    prisma: isTypeScript\n      ? { ...prisma, seed: prismaSeed }\n      : {\n          ...prisma,\n          seed: prismaSeed\n            .replace(\"ts-node\", \"node\")\n            .replace(\"seed.ts\", \"seed.js\"),\n        },\n    scripts: isTypeScript\n      ? { ...scripts, typecheck, validate }\n      : { ...scripts, validate: validate.replace(\" typecheck\", \"\") },\n  });\n};\n\nconst main = async ({ isTypeScript, packageManager, rootDirectory }) => {\n  const pm = getPackageManagerCommand(packageManager);\n  const FILE_EXTENSION = isTypeScript ? \"ts\" : \"js\";\n\n  const README_PATH = path.join(rootDirectory, \"README.md\");\n  const FLY_TOML_PATH = path.join(rootDirectory, \"fly.toml\");\n  const EXAMPLE_ENV_PATH = path.join(rootDirectory, \".env.example\");\n  const ENV_PATH = path.join(rootDirectory, \".env\");\n  const DEPLOY_WORKFLOW_PATH = path.join(\n    rootDirectory,\n    \".github\",\n    \"workflows\",\n    \"deploy.yml\"\n  );\n  const DOCKERFILE_PATH = path.join(rootDirectory, \"Dockerfile\");\n  const CYPRESS_SUPPORT_PATH = path.join(rootDirectory, \"cypress\", \"support\");\n  const CYPRESS_COMMANDS_PATH = path.join(\n    CYPRESS_SUPPORT_PATH,\n    `commands.${FILE_EXTENSION}`\n  );\n  const CREATE_USER_COMMAND_PATH = path.join(\n    CYPRESS_SUPPORT_PATH,\n    `create-user.${FILE_EXTENSION}`\n  );\n  const DELETE_USER_COMMAND_PATH = path.join(\n    CYPRESS_SUPPORT_PATH,\n    `delete-user.${FILE_EXTENSION}`\n  );\n  const VITEST_CONFIG_PATH = path.join(\n    rootDirectory,\n    `vitest.config.${FILE_EXTENSION}`\n  );\n\n  const REPLACER = \"indie-stack-template\";\n\n  const DIR_NAME = path.basename(rootDirectory);\n  const SUFFIX = getRandomString(2);\n\n  const APP_NAME = (DIR_NAME + \"-\" + SUFFIX)\n    // get rid of anything that's not allowed in an app name\n    .replace(/[^a-zA-Z0-9-_]/g, \"-\");\n\n  const [\n    prodContent,\n    readme,\n    env,\n    dockerfile,\n    cypressCommands,\n    createUserCommand,\n    deleteUserCommand,\n    deployWorkflow,\n    vitestConfig,\n    packageJson,\n  ] = await Promise.all([\n    fs.readFile(FLY_TOML_PATH, \"utf-8\"),\n    fs.readFile(README_PATH, \"utf-8\"),\n    fs.readFile(EXAMPLE_ENV_PATH, \"utf-8\"),\n    fs.readFile(DOCKERFILE_PATH, \"utf-8\"),\n    fs.readFile(CYPRESS_COMMANDS_PATH, \"utf-8\"),\n    fs.readFile(CREATE_USER_COMMAND_PATH, \"utf-8\"),\n    fs.readFile(DELETE_USER_COMMAND_PATH, \"utf-8\"),\n    readFileIfNotTypeScript(isTypeScript, DEPLOY_WORKFLOW_PATH, (s) =>\n      YAML.parse(s)\n    ),\n    readFileIfNotTypeScript(isTypeScript, VITEST_CONFIG_PATH),\n    PackageJson.load(rootDirectory),\n  ]);\n\n  const newEnv = env.replace(\n    /^SESSION_SECRET=.*$/m,\n    `SESSION_SECRET=\"${getRandomString(16)}\"`\n  );\n\n  const prodToml = toml.parse(prodContent);\n  prodToml.app = prodToml.app.replace(REPLACER, APP_NAME);\n\n  const newReadme = readme.replace(\n    new RegExp(escapeRegExp(REPLACER), \"g\"),\n    APP_NAME\n  );\n\n  const newDockerfile = pm.lockfile\n    ? dockerfile.replace(\n        new RegExp(escapeRegExp(\"ADD package.json\"), \"g\"),\n        `ADD package.json ${pm.lockfile}`\n      )\n    : dockerfile;\n\n  updatePackageJson({ APP_NAME, isTypeScript, packageJson });\n\n  const fileOperationPromises = [\n    fs.writeFile(FLY_TOML_PATH, toml.stringify(prodToml)),\n    fs.writeFile(README_PATH, newReadme),\n    fs.writeFile(ENV_PATH, newEnv),\n    fs.writeFile(DOCKERFILE_PATH, newDockerfile),\n    ...cleanupCypressFiles({\n      fileEntries: [\n        [CYPRESS_COMMANDS_PATH, cypressCommands],\n        [CREATE_USER_COMMAND_PATH, createUserCommand],\n        [DELETE_USER_COMMAND_PATH, deleteUserCommand],\n      ],\n      isTypeScript,\n      packageManager: pm,\n    }),\n    packageJson.save(),\n    fs.copyFile(\n      path.join(rootDirectory, \"remix.init\", \"gitignore\"),\n      path.join(rootDirectory, \".gitignore\")\n    ),\n    fs.rm(path.join(rootDirectory, \".github\", \"ISSUE_TEMPLATE\"), {\n      recursive: true,\n    }),\n    fs.rm(path.join(rootDirectory, \".github\", \"dependabot.yml\")),\n    fs.rm(path.join(rootDirectory, \".github\", \"PULL_REQUEST_TEMPLATE.md\")),\n  ];\n\n  if (!isTypeScript) {\n    fileOperationPromises.push(\n      ...cleanupDeployWorkflow(deployWorkflow, DEPLOY_WORKFLOW_PATH)\n    );\n\n    fileOperationPromises.push(\n      ...cleanupVitestConfig(vitestConfig, VITEST_CONFIG_PATH)\n    );\n  }\n\n  await Promise.all(fileOperationPromises);\n\n  execSync(pm.run(\"setup\"), { cwd: rootDirectory, stdio: \"inherit\" });\n\n  execSync(pm.run(\"format\", \"--loglevel warn\"), {\n    cwd: rootDirectory,\n    stdio: \"inherit\",\n  });\n\n  console.log(\n    `Setup is complete. You're now ready to rock and roll 🤘\n\nStart development with \\`${pm.run(\"dev\")}\\`\n    `.trim()\n  );\n};\n\nmodule.exports = main;\n"
  },
  {
    "path": "examples/complex/remix.init/package.json",
    "content": "{\n  \"name\": \"remix.init\",\n  \"private\": true,\n  \"main\": \"index.js\",\n  \"license\": \"MIT\",\n  \"dependencies\": {\n    \"@iarna/toml\": \"^2.2.5\",\n    \"@npmcli/package-json\": \"^2.0.0\",\n    \"semver\": \"^7.3.7\",\n    \"yaml\": \"^2.1.1\"\n  }\n}\n"
  },
  {
    "path": "examples/complex/start.sh",
    "content": "#!/bin/sh\n\n# This file is how Fly starts the server (configured in fly.toml). Before starting\n# the server though, we need to run any prisma migrations that haven't yet been\n# run, which is why this file exists in the first place.\n# Learn more: https://community.fly.io/t/sqlite-not-getting-setup-properly/4386\n\nset -ex\nnpx prisma migrate deploy\nnpm run start\n"
  },
  {
    "path": "examples/complex/tailwind.config.js",
    "content": "/** @type {import('tailwindcss').Config} */\nmodule.exports = {\n  content: [\"./app/**/*.{ts,tsx,jsx,js}\"],\n  theme: {\n    extend: {},\n  },\n  plugins: [],\n};\n"
  },
  {
    "path": "examples/complex/test/setup-test-env.ts",
    "content": "import { installGlobals } from \"@remix-run/node\";\nimport \"@testing-library/jest-dom/extend-expect\";\n\ninstallGlobals();\n"
  },
  {
    "path": "examples/complex/tsconfig.json",
    "content": "{\n  \"exclude\": [\"./cypress\", \"./cypress.config.ts\"],\n  \"include\": [\"remix.env.d.ts\", \"**/*.ts\", \"**/*.tsx\"],\n  \"compilerOptions\": {\n    \"lib\": [\"DOM\", \"DOM.Iterable\", \"ES2019\"],\n    \"types\": [\"vitest/globals\"],\n    \"isolatedModules\": true,\n    \"esModuleInterop\": true,\n    \"jsx\": \"react-jsx\",\n    \"module\": \"CommonJS\",\n    \"moduleResolution\": \"node\",\n    \"resolveJsonModule\": true,\n    \"target\": \"ES2019\",\n    \"strict\": true,\n    \"allowJs\": true,\n    \"forceConsistentCasingInFileNames\": true,\n    \"baseUrl\": \".\",\n    \"paths\": {\n      \"~/*\": [\"./app/*\"]\n    },\n    \"skipLibCheck\": true,\n\n    // Remix takes care of building everything in `remix build`.\n    \"noEmit\": true\n  }\n}\n"
  },
  {
    "path": "examples/complex/vitest.config.ts",
    "content": "/// <reference types=\"vitest\" />\n/// <reference types=\"vite/client\" />\n\nimport react from \"@vitejs/plugin-react\";\nimport { defineConfig } from \"vite\";\nimport tsconfigPaths from \"vite-tsconfig-paths\";\n\nexport default defineConfig({\n  plugins: [react(), tsconfigPaths()],\n  test: {\n    globals: true,\n    environment: \"happy-dom\",\n    setupFiles: [\"./test/setup-test-env.ts\"],\n  },\n});\n"
  },
  {
    "path": "examples/simple/.eslintrc.js",
    "content": "/** @type {import('eslint').Linter.Config} */\nmodule.exports = {\n  extends: [\"@remix-run/eslint-config\", \"@remix-run/eslint-config/node\"],\n};\n"
  },
  {
    "path": "examples/simple/.gitignore",
    "content": "node_modules\n\n/.cache\n/build\n/public/build\n.env\n"
  },
  {
    "path": "examples/simple/README.md",
    "content": "# Welcome to Remix!\n\n- [Remix Docs](https://remix.run/docs)\n\n## Development\n\nFrom your terminal:\n\n```sh\nnpm run dev\n```\n\nThis starts your app in development mode, rebuilding assets on file changes.\n\n## Deployment\n\nFirst, build your app for production:\n\n```sh\nnpm run build\n```\n\nThen run the app in production mode:\n\n```sh\nnpm start\n```\n\nNow you'll need to pick a host to deploy it to.\n\n### DIY\n\nIf you're familiar with deploying node applications, the built-in Remix app server is production-ready.\n\nMake sure to deploy the output of `remix build`\n\n- `build/`\n- `public/build/`\n\n### Using a Template\n\nWhen you ran `npx create-remix@latest` there were a few choices for hosting. You can run that again to create a new project, then copy over your `app/` folder to the new project that's pre-configured for your target server.\n\n```sh\ncd ..\n# create a new project, and pick a pre-configured host\nnpx create-remix@latest\ncd my-new-remix-app\n# remove the new project's app (not the old one!)\nrm -rf app\n# copy your app over\ncp -R ../my-old-remix-app/app app\n```\n"
  },
  {
    "path": "examples/simple/app/entry.client.tsx",
    "content": "import { RemixBrowser } from \"@remix-run/react\";\nimport { startTransition, StrictMode } from \"react\";\nimport { hydrateRoot } from \"react-dom/client\";\n\nfunction hydrate() {\n  startTransition(() => {\n    hydrateRoot(\n      document,\n      <StrictMode>\n        <RemixBrowser />\n      </StrictMode>\n    );\n  });\n}\n\nif (window.requestIdleCallback) {\n  window.requestIdleCallback(hydrate);\n} else {\n  // Safari doesn't support requestIdleCallback\n  // https://caniuse.com/requestidlecallback\n  window.setTimeout(hydrate, 1);\n}\n"
  },
  {
    "path": "examples/simple/app/entry.server.tsx",
    "content": "import { PassThrough } from \"stream\";\nimport type { EntryContext } from \"@remix-run/node\";\nimport { Response } from \"@remix-run/node\";\nimport { RemixServer } from \"@remix-run/react\";\nimport isbot from \"isbot\";\nimport { renderToPipeableStream } from \"react-dom/server\";\n\nconst ABORT_DELAY = 5000;\n\nexport default function handleRequest(\n  request: Request,\n  responseStatusCode: number,\n  responseHeaders: Headers,\n  remixContext: EntryContext\n) {\n  return isbot(request.headers.get(\"user-agent\"))\n    ? handleBotRequest(\n        request,\n        responseStatusCode,\n        responseHeaders,\n        remixContext\n      )\n    : handleBrowserRequest(\n        request,\n        responseStatusCode,\n        responseHeaders,\n        remixContext\n      );\n}\n\nfunction handleBotRequest(\n  request: Request,\n  responseStatusCode: number,\n  responseHeaders: Headers,\n  remixContext: EntryContext\n) {\n  return new Promise((resolve, reject) => {\n    let didError = false;\n\n    const { pipe, abort } = renderToPipeableStream(\n      <RemixServer context={remixContext} url={request.url} />,\n      {\n        onAllReady() {\n          const body = new PassThrough();\n\n          responseHeaders.set(\"Content-Type\", \"text/html\");\n\n          resolve(\n            new Response(body, {\n              headers: responseHeaders,\n              status: didError ? 500 : responseStatusCode,\n            })\n          );\n\n          pipe(body);\n        },\n        onShellError(error: unknown) {\n          reject(error);\n        },\n        onError(error: unknown) {\n          didError = true;\n\n          console.error(error);\n        },\n      }\n    );\n\n    setTimeout(abort, ABORT_DELAY);\n  });\n}\n\nfunction handleBrowserRequest(\n  request: Request,\n  responseStatusCode: number,\n  responseHeaders: Headers,\n  remixContext: EntryContext\n) {\n  return new Promise((resolve, reject) => {\n    let didError = false;\n\n    const { pipe, abort } = renderToPipeableStream(\n      <RemixServer context={remixContext} url={request.url} />,\n      {\n        onShellReady() {\n          const body = new PassThrough();\n\n          responseHeaders.set(\"Content-Type\", \"text/html\");\n\n          resolve(\n            new Response(body, {\n              headers: responseHeaders,\n              status: didError ? 500 : responseStatusCode,\n            })\n          );\n\n          pipe(body);\n        },\n        onShellError(err: unknown) {\n          reject(err);\n        },\n        onError(error: unknown) {\n          didError = true;\n\n          console.error(error);\n        },\n      }\n    );\n\n    setTimeout(abort, ABORT_DELAY);\n  });\n}\n"
  },
  {
    "path": "examples/simple/app/root.tsx",
    "content": "import type { MetaFunction } from \"@remix-run/node\";\nimport {\n  Links,\n  LiveReload,\n  Meta,\n  Outlet,\n  Scripts,\n  ScrollRestoration,\n} from \"@remix-run/react\";\n\nexport const meta: MetaFunction = () => ({\n  charset: \"utf-8\",\n  title: \"New Remix App\",\n  viewport: \"width=device-width,initial-scale=1\",\n});\n\nexport default function App() {\n  return (\n    <html lang=\"en\">\n      <head>\n        <Meta />\n        <Links />\n      </head>\n      <body>\n        <Outlet />\n        <ScrollRestoration />\n        <Scripts />\n        <LiveReload />\n      </body>\n    </html>\n  );\n}\n"
  },
  {
    "path": "examples/simple/app/routes/index.tsx",
    "content": "import { useLoaderData } from \"@remix-run/react\";\n\nexport const loader = () => {\n  return {\n    foo: 'baz'\n  }\n};\n\nexport default function Index() {\n  const data = useLoaderData();\n\n  return (\n    <div style={{ fontFamily: \"system-ui, sans-serif\", lineHeight: \"1.4\" }}>\n      <h1>Welcome to Remix</h1>\n      <h3>{data.foo}</h3>\n      <ul>\n        <li>\n          <a\n            target=\"_blank\"\n            href=\"https://remix.run/tutorials/blog\"\n            rel=\"noreferrer\"\n          >\n            15m Quickstart Blog Tutorial\n          </a>\n        </li>\n        <li>\n          <a\n            target=\"_blank\"\n            href=\"https://remix.run/tutorials/jokes\"\n            rel=\"noreferrer\"\n          >\n            Deep Dive Jokes App Tutorial\n          </a>\n        </li>\n        <li>\n          <a target=\"_blank\" href=\"https://remix.run/docs\" rel=\"noreferrer\">\n            Remix Docs\n          </a>\n        </li>\n      </ul>\n    </div>\n  );\n}\n"
  },
  {
    "path": "examples/simple/package.json",
    "content": "{\n  \"private\": true,\n  \"sideEffects\": false,\n  \"scripts\": {\n    \"build\": \"remix build\",\n    \"dev\": \"remix dev\",\n    \"start\": \"remix-serve build\"\n  },\n  \"dependencies\": {\n    \"@remix-run/node\": \"^1.10.0\",\n    \"@remix-run/react\": \"^1.10.0\",\n    \"@remix-run/serve\": \"^1.10.0\",\n    \"isbot\": \"^3.5.4\",\n    \"javascript-time-ago\": \"2.5.9\",\n    \"react\": \"^18.2.0\",\n    \"react-dom\": \"^18.2.0\"\n  },\n  \"devDependencies\": {\n    \"@remix-run/dev\": \"^1.10.0\",\n    \"@remix-run/eslint-config\": \"^1.10.0\",\n    \"@types/react\": \"^18.0.15\",\n    \"@types/react-dom\": \"^18.0.6\",\n    \"eslint\": \"^8.23.1\",\n    \"typescript\": \"^4.7.4\"\n  },\n  \"engines\": {\n    \"node\": \">=14\"\n  }\n}\n"
  },
  {
    "path": "examples/simple/remix.config.js",
    "content": "/** @type {import('@remix-run/dev').AppConfig} */\nmodule.exports = {\n  ignoredRouteFiles: [\"**/.*\"],\n  // appDirectory: \"app\",\n  // assetsBuildDirectory: \"public/build\",\n  // serverBuildPath: \"build/index.js\",\n  // publicPath: \"/build/\",\n};\n"
  },
  {
    "path": "examples/simple/remix.env.d.ts",
    "content": "/// <reference types=\"@remix-run/dev\" />\n/// <reference types=\"@remix-run/node\" />\n"
  },
  {
    "path": "examples/simple/tsconfig.json",
    "content": "{\n  \"include\": [\"remix.env.d.ts\", \"**/*.ts\", \"**/*.tsx\"],\n  \"compilerOptions\": {\n    \"lib\": [\"DOM\", \"DOM.Iterable\", \"ES2019\"],\n    \"isolatedModules\": true,\n    \"esModuleInterop\": true,\n    \"jsx\": \"react-jsx\",\n    \"moduleResolution\": \"node\",\n    \"resolveJsonModule\": true,\n    \"target\": \"ES2019\",\n    \"strict\": true,\n    \"allowJs\": true,\n    \"forceConsistentCasingInFileNames\": true,\n    \"baseUrl\": \".\",\n    \"paths\": {\n      \"~/*\": [\"./app/*\"]\n    },\n\n    // Remix takes care of building everything in `remix build`.\n    \"noEmit\": true\n  }\n}\n"
  },
  {
    "path": "package.json",
    "content": "{\n  \"name\": \"remix-vite\",\n  \"version\": \"0.3.1\",\n  \"description\": \"Static file serving and directory listing\",\n  \"keywords\": [\n    \"remix\",\n    \"remix-vite\",\n    \"server\",\n    \"hmr\",\n    \"vite\",\n    \"react\",\n    \"hot\",\n    \"reload\"\n  ],\n  \"repository\": \"sudomf/remix-vite\",\n  \"license\": \"MIT\",\n  \"main\": \"./lib.js\",\n  \"module\": \"./lib.esm.js\",\n  \"types\": \"./declarations/src/entries/lib.d.ts\",\n  \"bin\": {\n    \"remix-vite\": \"./cli.js\"\n  },\n  \"files\": [\n    \"declarations/**/*.d.ts\",\n    \"cli.js\",\n    \"lib.js\",\n    \"lib.esm.js\"\n  ],\n  \"engines\": {\n    \"node\": \">= 14\"\n  },\n  \"scripts\": {\n    \"dev\": \"run-p dev:**\",\n    \"dev:watch\": \"node tools/build --dev\",\n    \"dev:serve\": \"cd examples/complex && nodemon ../../cli --host 0.0.0.0 --watch ../../cli.js\",\n    \"start\": \"node ./build/main.js\",\n    \"build\": \"node tools/build.js && tsc --emitDeclarationOnly\",\n    \"lint:code\": \"eslint --fix src/**/*.ts\",\n    \"lint:style\": \"prettier --write .\",\n    \"lint\": \"yarn lint:code && yarn lint:style\",\n    \"format\": \"prettier --write .\",\n    \"prepare\": \"husky install config/husky && yarn build\"\n  },\n  \"dependencies\": {\n    \"@babel/core\": \"7.20.2\",\n    \"@babel/generator\": \"7.20.2\",\n    \"@babel/parser\": \"7.20.2\",\n    \"@babel/traverse\": \"7.20.1\",\n    \"@babel/types\": \"7.20.2\",\n    \"@remix-run/dev\": \"^1.8.2\",\n    \"@remix-run/express\": \"^1.8.2\",\n    \"@remix-run/server-runtime\": \"^1.8.2\",\n    \"@vitejs/plugin-react\": \"2.2.0\",\n    \"args\": \"5.0.3\",\n    \"cross-fetch\": \"3.1.5\",\n    \"dotenv\": \"16.0.3\",\n    \"express\": \"4.18.2\",\n    \"jsesc\": \"3.0.2\",\n    \"vite\": \"3.2.2\",\n    \"vite-tsconfig-paths\": \"3.5.2\"\n  },\n  \"devDependencies\": {\n    \"@types/args\": \"5.0.0\",\n    \"@types/babel__core\": \"7.1.19\",\n    \"@types/babel__generator\": \"7.6.4\",\n    \"@types/babel__traverse\": \"7.18.2\",\n    \"@types/express\": \"4.17.14\",\n    \"@types/jsesc\": \"3.0.1\",\n    \"@vercel/style-guide\": \"3.0.0\",\n    \"chokidar\": \"3.5.3\",\n    \"esbuild\": \"0.15.13\",\n    \"eslint\": \"8.19.0\",\n    \"husky\": \"8.0.1\",\n    \"lint-staged\": \"13.0.3\",\n    \"nodemon\": \"2.0.20\",\n    \"npm-run-all\": \"4.1.5\",\n    \"prettier\": \"2.7.1\",\n    \"react\": \"18.2.0\",\n    \"react-dom\": \"18.2.0\",\n    \"typescript\": \"4.6.4\"\n  },\n  \"prettier\": \"@vercel/style-guide/prettier\",\n  \"eslintConfig\": {\n    \"extends\": [\n      \"./node_modules/@vercel/style-guide/eslint/node.js\",\n      \"./node_modules/@vercel/style-guide/eslint/typescript.js\"\n    ],\n    \"parserOptions\": {\n      \"project\": \"tsconfig.json\"\n    },\n    \"rules\": {\n      \"no-await-in-loop\": 0,\n      \"eslint-comments/disable-enable-pair\": 0,\n      \"@typescript-eslint/no-non-null-assertion\": 0,\n      \"@typescript-eslint/no-unsafe-assignment\": 0,\n      \"@typescript-eslint/no-unsafe-call\": 0,\n      \"@typescript-eslint/no-unsafe-member-access\": 0,\n      \"@typescript-eslint/no-misused-promises\": 0\n    }\n  },\n  \"lint-staged\": {\n    \"*.{json,css,scss,md,html,yml,yaml}\": [\n      \"prettier --write\"\n    ],\n    \".{js,ts}\": [\n      \"eslint --fix\"\n    ]\n  }\n}\n"
  },
  {
    "path": "src/constants.ts",
    "content": "export const SERVER_ENTRY_ID = 'server-entry';\nexport const SERVER_ASSETS_MANIFEST_ID = 'server-assets-manifest';\nexport const BROWSER_ASSETS_MANIFEST_ID = 'browser-assets-manifest';\n"
  },
  {
    "path": "src/entries/cli.ts",
    "content": "#!/usr/bin/env node\n/* eslint-disable no-console */\n/* eslint-disable @typescript-eslint/no-explicit-any */\nimport express from 'express';\nimport { config } from 'dotenv';\nimport { createRequestHandler } from '@remix-run/express';\nimport args from 'args';\nimport { createRemixViteDevServer, getRemixViteBuild } from '../vite';\n\nconfig();\n\nargs\n  .option('host', 'The host on which the app will be running', 'localhost')\n  .option('port', 'The port on which the app will be running', 3000);\n\nconst flags = args.parse(process.argv) as {\n  host: string;\n  port: number;\n};\n\nconst mode = 'development';\n\nasync function createServer() {\n  const app = express();\n\n  app.use(express.static('public', { maxAge: '1h' }));\n\n  const viteDevServer = await createRemixViteDevServer();\n\n  // use vite's connect instance as middleware\n  // if you use your own express router (express.Router()), you should use router.use\n  app.use(viteDevServer.middlewares);\n\n  app.all('*', async (req, res, next) => {\n    try {\n      purgeRequireCache();\n\n      const build = await getRemixViteBuild(viteDevServer);\n\n      const handler = createRequestHandler({\n        build,\n        mode,\n      });\n\n      return handler(req, res, next);\n    } catch (e: any) {\n      // If an error is caught, let Vite fix the stack trace so it maps back to\n      // your actual source code.\n      viteDevServer.ssrFixStacktrace(e as Error);\n      next(e);\n    }\n  });\n\n  app.listen(flags.port, flags.host, () => {\n    console.log(`🖲 remix-vite started at http://${flags.host}:${flags.port}`);\n  });\n}\n\ncreateServer().catch((e) => {\n  console.error(e);\n  process.exit(1);\n});\n\nfunction purgeRequireCache() {\n  // purge require cache on requests for \"server side HMR\" this won't let\n  // you have in-memory objects between requests in development.\n  for (const key in require.cache) {\n    delete require.cache[key];\n  }\n}\n"
  },
  {
    "path": "src/entries/lib.ts",
    "content": "export { getRemixViteBuild, createRemixViteDevServer } from '../vite';\n"
  },
  {
    "path": "src/plugins/hmr-fix.ts",
    "content": "import { getRouteByFilePath } from '../utils/general';\nimport { fixHmrCode } from '../utils/code';\nimport type { Plugin } from 'vite';\n\nexport const getHmrFixPlugin = (): Plugin => {\n  return {\n    name: 'remix-plugin-hmr-fix',\n    enforce: 'post',\n    async transform(code, id) {\n      const route = await getRouteByFilePath(id);\n\n      if (\n        route &&\n        id.endsWith('.tsx') &&\n        code.includes('if (import.meta.hot) {') &&\n        code.includes('window.$RefreshReg$ = prevRefreshReg;')\n      ) {\n        return fixHmrCode(id, code);\n      }\n    },\n  };\n};\n"
  },
  {
    "path": "src/plugins/inject.ts",
    "content": "import { getRouteByFilePath } from '../utils/general';\nimport type { Plugin } from 'vite';\n\nexport const getInjectPlugin = (): Plugin => {\n  return {\n    name: 'vite-plugin-remix-inject',\n    enforce: 'pre',\n\n    async transform(code, id) {\n      const route = await getRouteByFilePath(id);\n\n      if (!route) return;\n\n      if (route.id === 'root') {\n        return patchRoot(code);\n      }\n    },\n  };\n};\n\nconst patchRoot = (code: string) => {\n  return code\n    .replace(/<LiveReload.*?\\/>/, '')\n    .replace(/<Scripts.*\\/>/, viteScripts);\n};\n\nconst viteScripts = `\n<script type=\"module\" src=\"/@vite/client\" />\n<script\n  type=\"module\"\n  dangerouslySetInnerHTML={{\n    __html: \\`import RefreshRuntime from '/@react-refresh'\n  RefreshRuntime.injectIntoGlobalHook(window)\n  window.$RefreshReg$ = () => {}\n  window.$RefreshSig$ = () => (type) => type\n  window.__vite_plugin_react_preamble_installed__ = true\\`,\n  }}\n/>\n<Scripts />\n`;\n"
  },
  {
    "path": "src/plugins/remix.ts",
    "content": "/* eslint-disable @typescript-eslint/no-unnecessary-condition */\n/* eslint-disable @typescript-eslint/no-explicit-any */\n\nimport jsesc from 'jsesc';\nimport {\n  createVirtualModule,\n  getRemixConfig,\n  getRemixRouteModuleExports,\n  getVirtualModuleUrl,\n  resolveAppRelativeFilePath,\n  resolveFSPath,\n  resolveRelativeRouteFilePath,\n} from '../utils/general';\nimport {\n  BROWSER_ASSETS_MANIFEST_ID,\n  SERVER_ASSETS_MANIFEST_ID,\n  SERVER_ENTRY_ID,\n} from '../constants';\nimport type { RemixConfig } from '@remix-run/dev/dist/config';\nimport type { Plugin } from 'vite';\n\nexport const getRemixPlugin = async (): Promise<Plugin> => {\n  const config = await getRemixConfig();\n  const manifest = await getAssetManifest(config);\n  const serverEntryJs = getServerEntry(config);\n\n  const serverEntryVirtualModule = createVirtualModule(\n    SERVER_ENTRY_ID,\n    serverEntryJs,\n  );\n  const serverManifestVirtualModule = createVirtualModule(\n    SERVER_ASSETS_MANIFEST_ID,\n    `export default ${jsesc(manifest, { es6: true })};`,\n  );\n  const browserManifestVirtualModule = createVirtualModule(\n    BROWSER_ASSETS_MANIFEST_ID,\n    `window.__remixManifest=${jsesc(manifest, { es6: true })};`,\n  );\n\n  const virtualModules = [\n    serverEntryVirtualModule,\n    serverManifestVirtualModule,\n    browserManifestVirtualModule,\n  ];\n\n  return {\n    name: 'vite-plugin-remix',\n    enforce: 'pre',\n    resolveId(id) {\n      for (const virtualModule of virtualModules) {\n        if (id === virtualModule.virtualModuleId) {\n          return virtualModule.resolvedVirtualModuleId;\n        }\n      }\n    },\n    load(id) {\n      for (const virtualModule of virtualModules) {\n        if (id === virtualModule.resolvedVirtualModuleId) {\n          return virtualModule.code;\n        }\n      }\n    },\n  };\n};\n\nconst getServerEntry = (config: RemixConfig) => {\n  return `\n  import * as entryServer from ${JSON.stringify(\n    resolveFSPath(resolveAppRelativeFilePath(config.entryServerFile, config)),\n  )};\n  ${Object.keys(config.routes)\n    .map((key, index) => {\n      const route = config.routes[key]!;\n      return `import * as route${index} from ${JSON.stringify(\n        resolveFSPath(resolveRelativeRouteFilePath(route, config)),\n      )};`;\n    })\n    .join('\\n')}\n    export { default as assets } from ${JSON.stringify(\n      'virtual:server-assets-manifest',\n    )};\n    export const assetsBuildDirectory = ${JSON.stringify(\n      config.relativeAssetsBuildDirectory,\n    )};\n    ${\n      config.future\n        ? `export const future = ${JSON.stringify(config.future)}`\n        : ''\n    };\n    export const publicPath = ${JSON.stringify(config.publicPath)};\n    export const entry = { module: entryServer };\n    export const routes = {\n      ${Object.keys(config.routes)\n        .map((key, index) => {\n          const route = config.routes[key]!;\n          return `${JSON.stringify(key)}: {\n        id: ${JSON.stringify(route.id)},\n        parentId: ${JSON.stringify(route.parentId)},\n        path: ${JSON.stringify(route.path)},\n        index: ${JSON.stringify(route.index)},\n        caseSensitive: ${JSON.stringify(route.caseSensitive)},\n        module: route${index}\n      }`;\n        })\n        .join(',\\n  ')}\n    };`;\n};\n\nconst getAssetManifest = async (config: RemixConfig) => {\n  const routes: Record<string, any> = {};\n\n  for (const entry of Object.entries(config.routes)) {\n    const [key, route] = entry;\n    const sourceExports = await getRemixRouteModuleExports(route.id);\n\n    routes[key] = {\n      id: route.id,\n      parentId: route.parentId,\n      path: route.path,\n      index: route.index,\n      caseSensitive: route.caseSensitive,\n      module: resolveFSPath(resolveRelativeRouteFilePath(route, config)),\n      hasAction: sourceExports.includes('action'),\n      hasLoader: sourceExports.includes('loader'),\n      hasCatchBoundary: sourceExports.includes('CatchBoundary'),\n      hasErrorBoundary: sourceExports.includes('ErrorBoundary'),\n      imports: [],\n    };\n  }\n\n  return {\n    url: getVirtualModuleUrl(BROWSER_ASSETS_MANIFEST_ID),\n    version: Math.random(),\n    entry: {\n      module: resolveFSPath(\n        resolveAppRelativeFilePath(config.entryClientFile, config),\n      ),\n      imports: [],\n    },\n    routes,\n  };\n};\n"
  },
  {
    "path": "src/plugins/transform.ts",
    "content": "import {\n  getRemixRouteModuleExports,\n  getRouteByFilePath,\n} from '../utils/general';\nimport { filterExports } from '../utils/code';\nimport type { Plugin } from 'vite';\n\nconst BACKEND_ONLY_EXPORTS = ['loader', 'action'];\n\nexport const getTransformPlugin = (): Plugin => {\n  return {\n    name: 'vite-plugin-remix-transform',\n    enforce: 'pre',\n\n    // Skip process CSS as Remix doesn't pre-process CSS.\n    async resolveId(id) {\n      if (id.endsWith('.css') && !id.includes('?')) {\n        const target = await this.resolve(`${id}?url`);\n        return target;\n      }\n    },\n\n    async transform(code, id, options) {\n      // If it's SSR code, let's bypass it.\n      if (options?.ssr) return;\n\n      // If it's .server.<ext>, let's bypass it.\n      if (id.includes('.server.')) return 'export default {}';\n\n      const route = await getRouteByFilePath(id);\n\n      if (!route) return;\n\n      const theExports = await getRemixRouteModuleExports(route.id);\n\n      // If it's a route with no default component export, let's bypass it.\n      if (!theExports.includes('default')) return;\n\n      const frontendExports = theExports.filter(\n        (e) => !BACKEND_ONLY_EXPORTS.includes(e),\n      );\n\n      if (!frontendExports.length) return code;\n\n      const filtered = filterExports(id, code, frontendExports);\n      const result = filtered.code;\n\n      return {\n        code: result,\n        map: null,\n      };\n    },\n  };\n};\n"
  },
  {
    "path": "src/utils/code.ts",
    "content": "import { parse } from '@babel/parser';\nimport generate from '@babel/generator';\nimport traverse from '@babel/traverse';\nimport * as t from '@babel/types';\nimport type { NodePath } from '@babel/traverse';\nimport type { types as BabelTypes } from '@babel/core';\n\nexport const filterExports = (\n  _id: string,\n  source: string,\n  exports: string[],\n) => {\n  const document = parse(source, {\n    sourceType: 'module',\n    plugins: ['typescript', 'jsx'],\n  });\n\n  traverse(document, {\n    ExportNamedDeclaration: (path) => {\n      removeExports(path, exports);\n    },\n  });\n\n  return {\n    code: generate(document).code,\n    map: null,\n  };\n};\n\nfunction removeExports(\n  path: NodePath<BabelTypes.ExportNamedDeclaration>,\n  exports: string[],\n) {\n  const shouldRemoveExport = (exportName: string) =>\n    !exports.includes(exportName);\n\n  const specifiers = path.get(\n    'specifiers',\n  ) as NodePath<BabelTypes.ExportSpecifier>[];\n\n  if (specifiers.length) {\n    specifiers.forEach((specifier) => {\n      const name = t.isIdentifier(specifier.node.exported)\n        ? specifier.node.exported.name\n        : specifier.node.exported.value;\n      if (shouldRemoveExport(name)) {\n        specifier.remove();\n      }\n    });\n\n    if (path.node.specifiers.length < 1) {\n      path.remove();\n    }\n    return;\n  }\n\n  const declaration = path.get('declaration') as NodePath<\n    BabelTypes.FunctionDeclaration | BabelTypes.VariableDeclaration\n  >;\n  // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition\n  if (!declaration.node) {\n    return;\n  }\n\n  switch (declaration.node.type) {\n    case 'FunctionDeclaration': {\n      const name = declaration.node.id!.name;\n      if (shouldRemoveExport(name)) {\n        path.remove();\n      }\n      break;\n    }\n    case 'VariableDeclaration': {\n      const inner = declaration.get(\n        'declarations',\n      ) as NodePath<BabelTypes.VariableDeclarator>[];\n      inner.forEach((d) => {\n        if (d.node.id.type !== 'Identifier') {\n          return;\n        }\n        const name = d.node.id.name;\n        if (shouldRemoveExport(name)) {\n          d.remove();\n        }\n      });\n      break;\n    }\n    default: {\n      break;\n    }\n  }\n}\n\n/* The original injected code from react-refresh checks if all exports are likely\n components. AS we can have more than just components in a route file, we will\n replace the injected code with a patched version that only checks id it has at least\n one component.\n*/\nexport const fixHmrCode = (_id: string, source: string) => {\n  const document = parse(source, {\n    sourceType: 'module',\n  });\n\n  const newAst = parse(hmrCodePatch, {\n    sourceType: 'module',\n  }).program.body;\n\n  traverse(document, {\n    IfStatement(path) {\n      const { node } = path;\n      const code = generate(node).code;\n\n      if (\n        code.includes('import.meta.hot') &&\n        code.includes('window.$RefreshReg$ = prevRefreshReg;')\n      ) {\n        const consequent = node.consequent as BabelTypes.BlockStatement;\n        consequent.body = newAst;\n      }\n    },\n  });\n\n  return {\n    code: generate(document).code,\n  };\n};\n\nconst hmrCodePatch = `\n  let isReactRefreshBoundary = function(mod) {\n    if (mod == null || typeof mod !== \"object\") {\n      return false;\n    }\n    let hasExports = false;\n    let hasAtLeastOneComponent = true;\n    for (const exportName in mod) {\n      hasExports = true;\n      if (exportName === \"__esModule\") {\n        continue;\n      }\n      const desc = Object.getOwnPropertyDescriptor(mod, exportName);\n      if (desc && desc.get) {\n        return false;\n      }\n      const exportValue = mod[exportName];\n      if (RefreshRuntime.isLikelyComponentType(exportValue)) {\n        hasAtLeastOneComponent = true;\n      }\n    }\n    return hasExports && hasAtLeastOneComponent;\n  };\n  window.$RefreshReg$ = prevRefreshReg;\n  window.$RefreshSig$ = prevRefreshSig;\n  import.meta.hot.accept((mod) => {\n    if (isReactRefreshBoundary(mod)) {\n      if (!window.__vite_plugin_react_timeout) {\n        window.__vite_plugin_react_timeout = setTimeout(() => {\n          window.__vite_plugin_react_timeout = 0;\n          RefreshRuntime.performReactRefresh();\n        }, 30);\n      }\n    } else {\n      import.meta.hot.invalidate();\n    }\n  });`;\n"
  },
  {
    "path": "src/utils/general.ts",
    "content": "import path from 'path';\nimport { readConfig } from '@remix-run/dev/dist/config';\nimport { normalizePath as viteNormalizePath } from 'vite';\nimport { getRouteModuleExports } from '@remix-run/dev/dist/compiler/routeExports';\nimport type { RemixConfig } from '@remix-run/dev/dist/config';\n\ntype Route = RemixConfig['routes'][string];\n\nexport const getRemixConfig = async () => {\n  const config = await readConfig();\n  return config;\n};\n\nexport const getRemixRouteModuleExports = async (routeId: string) => {\n  const config = await getRemixConfig();\n  return getRouteModuleExports(config, routeId);\n};\n\nexport const getVirtualModuleUrl = (id: string) => `/@id/__x00__virtual:${id}`;\n\nexport const normalizePath = (p: string) => {\n  return viteNormalizePath(toUnixPath(p));\n};\n\nexport const createVirtualModule = (name: string, code: string) => {\n  const virtualModuleId = `virtual:${name}`;\n  const resolvedVirtualModuleId = `\\0${virtualModuleId}`;\n\n  return {\n    virtualModuleId,\n    resolvedVirtualModuleId,\n    code,\n  };\n};\n\nexport const getAppDirName = (config: RemixConfig) => {\n  return path.relative(process.cwd(), config.appDirectory);\n};\n\nexport const resolveAppRelativeFilePath = (\n  file: string,\n  config: RemixConfig,\n) => {\n  const appDir = getAppDirName(config);\n  return path.resolve(process.cwd(), appDir, file);\n};\n\nexport const resolveRelativeRouteFilePath = (\n  route: Route,\n  config: RemixConfig,\n) => {\n  const file = route.file;\n  const fullPath = resolveAppRelativeFilePath(file, config);\n\n  return normalizePath(fullPath);\n};\n\nexport const resolveFSPath = (filePath: string) => {\n  return `/@fs${normalizePath(filePath)}`;\n};\n\nexport const getRouteByFilePath = async (filePath: string) => {\n  const routesByFile = await getRoutesByFile();\n  return routesByFile.get(normalizePath(filePath));\n};\n\nexport const getRoutesByFile = async () => {\n  const config = await getRemixConfig();\n\n  const routesByFile: Map<string, Route> = Object.keys(config.routes).reduce(\n    (map, key) => {\n      const route = config.routes[key]!;\n      const file = resolveRelativeRouteFilePath(route, config);\n      map.set(file, route);\n      return map;\n    },\n    new Map<string, Route>(),\n  );\n\n  return routesByFile;\n};\n\nconst toUnixPath = (p: string) =>\n  // eslint-disable-next-line prefer-named-capture-group\n  p.replace(/[\\\\/]+/g, '/').replace(/^([a-zA-Z]+:|\\.\\/)/, '');\n"
  },
  {
    "path": "src/utils/version.ts",
    "content": "/* eslint-disable no-empty */\nimport fetch from 'cross-fetch';\nimport pkg from '../../package.json';\n\nexport const checkVersion = async () => {\n  try {\n    const res = await fetch(`https://registry.npmjs.org/${pkg.name}`);\n    const data = await res.json();\n    const latestVersion = data['dist-tags'].latest as string;\n\n    if (latestVersion !== pkg.version) {\n      // eslint-disable-next-line no-console\n      console.warn(\n        '\\x1b[33m%s\\x1b[0m', // yellow\n        `\nYour version of ${pkg.name} is out of date.\nLatest version is ${latestVersion}, but you have ${pkg.version}.\n\nPlease upgrade by running:\n\nnpm install -D ${pkg.name}@latest \n  or \nyarn add -D ${pkg.name}@latest\n\n`,\n      );\n    }\n  } catch {}\n};\n"
  },
  {
    "path": "src/vite.ts",
    "content": "import { createServer, mergeConfig } from 'vite';\nimport vitePluginReact from '@vitejs/plugin-react';\nimport tsconfigPaths from 'vite-tsconfig-paths';\nimport { getHmrFixPlugin } from './plugins/hmr-fix';\nimport { getInjectPlugin } from './plugins/inject';\nimport { getRemixPlugin } from './plugins/remix';\nimport { getTransformPlugin } from './plugins/transform';\nimport { SERVER_ENTRY_ID } from './constants';\nimport { checkVersion } from './utils/version';\nimport type { ViteDevServer, UserConfig } from 'vite';\nimport type { ServerBuild } from '@remix-run/server-runtime';\n\n/**\n * Get Remix build\n */\nexport const getRemixViteBuild = async (viteDevServer: ViteDevServer) => {\n  const build = await viteDevServer.ssrLoadModule(`virtual:${SERVER_ENTRY_ID}`);\n\n  return build as ServerBuild;\n};\n\n/**\n * Create remix-vite dev server\n */\nexport const createRemixViteDevServer = async (config?: UserConfig) => {\n  await checkVersion();\n\n  const remixPlugin = await getRemixPlugin();\n  const remixInject = getInjectPlugin();\n  const remixTransformPlugin = getTransformPlugin();\n  const remixHmrFix = getHmrFixPlugin();\n\n  // Create Vite server in middleware mode and configure the app type as\n  // 'custom', disabling Vite's own HTML serving logic so parent server\n  // can take control\n  return createServer(\n    mergeConfig(\n      {\n        server: {\n          fs: {\n            strict: false,\n          },\n          cors: true,\n          middlewareMode: true,\n        },\n        plugins: [\n          tsconfigPaths(),\n          remixInject,\n          remixPlugin,\n          remixTransformPlugin,\n          vitePluginReact(),\n          remixHmrFix,\n        ],\n        appType: 'custom',\n      } as UserConfig,\n      config || {},\n    ),\n  );\n};\n"
  },
  {
    "path": "tools/build.js",
    "content": "/* eslint-disable no-console */\nconst esbuild = require('esbuild');\nconst chokidar = require('chokidar');\nconst pkg = require('../package.json');\n\n/**\n * @type {import('esbuild').BuildOptions}\n */\nconst options = {\n  platform: 'node',\n  target: 'node14',\n  format: 'cjs',\n  write: true,\n  bundle: true,\n  external: Object.keys(pkg.dependencies),\n  watch: false,\n};\n\nconst build = async () => {\n  await Promise.all([\n    esbuild.build({\n      ...options,\n      entryPoints: ['src/entries/lib.ts', 'src/entries/cli.ts'],\n      outdir: './',\n      assetNames: '[name].[ext]',\n    }),\n    esbuild.build({\n      ...options,\n      entryPoints: ['src/entries/lib.ts'],\n      format: 'esm',\n      outfile: './lib.esm.js',\n    }),\n  ]);\n};\n\nbuild()\n  .then(() => {\n    if (process.argv.includes('--dev')) {\n      console.log('Watching for changes...');\n\n      const watcher = chokidar.watch('./src/**/*.ts', {\n        persistent: true,\n      });\n\n      watcher.on('change', async () => {\n        console.log('Rebuilding...');\n        await build();\n      });\n    }\n  })\n  .catch((e) => {\n    console.error(e);\n    process.exit(1);\n  });\n"
  },
  {
    "path": "tsconfig.json",
    "content": "{\n  \"extends\": \"@vercel/style-guide/typescript\",\n  \"compilerOptions\": {\n    \"lib\": [\"es2020\"],\n    \"target\": \"ES2016\",\n    \"module\": \"CommonJS\",\n    \"moduleResolution\": \"node\",\n    \"resolveJsonModule\": true,\n    \"declaration\": true,\n    \"declarationDir\": \"./declarations\"\n  },\n  \"include\": [\"src/\"]\n}\n"
  }
]