Full Code of sudomf/remix-vite for AI

main 4beedae0def2 cached
92 files
110.9 KB
31.7k tokens
84 symbols
1 requests
Download .txt
Repository: sudomf/remix-vite
Branch: main
Commit: 4beedae0def2
Files: 92
Total size: 110.9 KB

Directory structure:
gitextract_mjw2yo5y/

├── .eslintignore
├── .gitattributes
├── .github/
│   ├── ISSUE_TEMPLATE/
│   │   ├── bug_report.yaml
│   │   └── feature_request.yaml
│   ├── PULL_REQUEST_TEMPLATE/
│   │   └── default.md
│   └── workflows/
│       └── publish.yml
├── .gitignore
├── .npmrc
├── .prettierignore
├── LICENSE
├── README.md
├── config/
│   └── husky/
│       └── pre-commit
├── examples/
│   ├── complex/
│   │   ├── .dockerignore
│   │   ├── .eslintrc.js
│   │   ├── .gitignore
│   │   ├── .gitpod.Dockerfile
│   │   ├── .gitpod.yml
│   │   ├── .npmrc
│   │   ├── .prettierignore
│   │   ├── Dockerfile
│   │   ├── README.md
│   │   ├── app/
│   │   │   ├── db.server.ts
│   │   │   ├── entry.client.tsx
│   │   │   ├── entry.server.tsx
│   │   │   ├── models/
│   │   │   │   ├── note.server.ts
│   │   │   │   └── user.server.ts
│   │   │   ├── root.tsx
│   │   │   ├── routes/
│   │   │   │   ├── healthcheck.tsx
│   │   │   │   ├── index.tsx
│   │   │   │   ├── join.tsx
│   │   │   │   ├── login.tsx
│   │   │   │   ├── logout.tsx
│   │   │   │   ├── notes/
│   │   │   │   │   ├── $noteId.tsx
│   │   │   │   │   ├── index.tsx
│   │   │   │   │   └── new.tsx
│   │   │   │   └── notes.tsx
│   │   │   ├── session.server.ts
│   │   │   ├── utils.test.ts
│   │   │   └── utils.ts
│   │   ├── custom-server.js
│   │   ├── cypress/
│   │   │   ├── .eslintrc.js
│   │   │   ├── e2e/
│   │   │   │   └── smoke.cy.ts
│   │   │   ├── fixtures/
│   │   │   │   └── example.json
│   │   │   ├── support/
│   │   │   │   ├── commands.ts
│   │   │   │   ├── create-user.ts
│   │   │   │   ├── delete-user.ts
│   │   │   │   └── e2e.ts
│   │   │   └── tsconfig.json
│   │   ├── cypress.config.ts
│   │   ├── fly.toml
│   │   ├── mocks/
│   │   │   ├── README.md
│   │   │   └── index.js
│   │   ├── package.json
│   │   ├── prisma/
│   │   │   ├── migrations/
│   │   │   │   ├── 20220713162558_init/
│   │   │   │   │   └── migration.sql
│   │   │   │   └── migration_lock.toml
│   │   │   ├── schema.prisma
│   │   │   └── seed.ts
│   │   ├── remix.config.js
│   │   ├── remix.env.d.ts
│   │   ├── remix.init/
│   │   │   ├── gitignore
│   │   │   ├── index.js
│   │   │   └── package.json
│   │   ├── start.sh
│   │   ├── tailwind.config.js
│   │   ├── test/
│   │   │   └── setup-test-env.ts
│   │   ├── tsconfig.json
│   │   └── vitest.config.ts
│   └── simple/
│       ├── .eslintrc.js
│       ├── .gitignore
│       ├── README.md
│       ├── app/
│       │   ├── entry.client.tsx
│       │   ├── entry.server.tsx
│       │   ├── root.tsx
│       │   └── routes/
│       │       └── index.tsx
│       ├── package.json
│       ├── remix.config.js
│       ├── remix.env.d.ts
│       └── tsconfig.json
├── package.json
├── src/
│   ├── constants.ts
│   ├── entries/
│   │   ├── cli.ts
│   │   └── lib.ts
│   ├── plugins/
│   │   ├── hmr-fix.ts
│   │   ├── inject.ts
│   │   ├── remix.ts
│   │   └── transform.ts
│   ├── utils/
│   │   ├── code.ts
│   │   ├── general.ts
│   │   └── version.ts
│   └── vite.ts
├── tools/
│   └── build.js
└── tsconfig.json

================================================
FILE CONTENTS
================================================

================================================
FILE: .eslintignore
================================================
/node_modules/**
/declarations
/examples/**
/cli.js
/lib.js
/lib.esm.js


================================================
FILE: .gitattributes
================================================
# .gitattributes
# Makes sure all line endings are LF.

*	text=auto eol=lf


================================================
FILE: .github/ISSUE_TEMPLATE/bug_report.yaml
================================================
name: Report a bug
description: ———
labels: [bug]
body:
  - type: markdown
    attributes:
      value: |
        # Thanks for reporting this bug!

        Help us replicate and find a fix for the issue by filling in this form.
  - type: textarea
    attributes:
      label: Description
      description: |
        Describe the issue and how to replicate it. If possible, please include
        a minimal example to reproduce the issue.
    validations:
      required: true
  - type: input
    attributes:
      label: Library version
      description: |
        Output of the `serve --version` command
    validations:
      required: true
  - type: input
    attributes:
      label: Node version
      description: Output of the `node --version` command
    validations:
      required: true


================================================
FILE: .github/ISSUE_TEMPLATE/feature_request.yaml
================================================
name: Suggest an improvement or new feature
description: ———
labels: [enhancement]
body:
  - type: markdown
    attributes:
      value: |
        # Thanks for filing this feature request!

        Help us understanding this feature and the need for it better by filling in this form.
  - type: textarea
    attributes:
      label: Description
      description: Describe the feature in detail
    validations:
      required: true
  - type: textarea
    attributes:
      label: Why
      description: Why should we add this feature? What are potential use cases for it?
    validations:
      required: true
  - type: textarea
    attributes:
      label: Alternatives
      description: Describe the alternatives you have considered, or existing workarounds
    validations:
      required: true


================================================
FILE: .github/PULL_REQUEST_TEMPLATE/default.md
================================================
<!--
	Hi there! Thanks for contributing! Please fill in this template to help us
	review and merge the PR as quickly and easily as possible!
-->

## Related Issues

<!--
	If this is a bug fix, or adds a feature mentioned in another issue, mention
	it as follows:

	- Closes #10
	- Fixes #15
-->

## Description

<!--
	Explain what has been added/changed/removed, in
	[keepachangelog.com](https://keepachangelog.com) style.
-->

### Added

<!--
	- Added a new method on the limiter object to reset the count for a certain IP [#10]
-->

### Changed

<!--
	- Deprecated `global` option
	- Fixed test for deprecated options [#15]
-->

### Removed

<!--
	- Removed deprecated `headers` option
-->

## Caveats/Problems/Issues

<!--
	Any weird code/problems you faced while making this PR. Feel free to ask for
	help with anything, especially if it's your first time contributing!
-->

## Checklist

- [ ] The issues that this PR fixes/closes have been mentioned above.
- [ ] What this PR adds/changes/removes has been explained.
- [ ] All tests (`pnpm test`) pass.
- [ ] The linter (`pnpm lint`) does not throw an errors.
- [ ] All added/modified code has been commented, and
      methods/classes/constants/types have been annotated with TSDoc comments.


================================================
FILE: .github/workflows/publish.yml
================================================
name: Publish to NPM
on:
  release:
    types: [created]
jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - name: Cache
        id: node-modules
        uses: actions/cache@v3
        with:
          path: node_modules
          key: ${{ runner.os }}-node-modules-${{ hashFiles('**/yarn.lock') }}
          restore-keys: |
            ${{ runner.os }}-node-modules-

      - name: Checkout
        uses: actions/checkout@v3
      - name: Setup Node
        uses: actions/setup-node@v3
        with:
          node-version: '18.x'
          registry-url: 'https://registry.npmjs.org'

      - name: Install dependencies and build 🔧
        run: yarn && yarn build

      - name: Publish package on NPM 📦
        run: npm publish
        env:
          NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}


================================================
FILE: .gitignore
================================================
# .gitignore
# A list of files and folders that should not be tracked by Git.

node_modules/
coverage/
build/
.cache/
.idea/
.vscode/

*.log
*.tgz
*.bak
*.tmp
cli.js
lib.js
lib.esm.js
declarations


================================================
FILE: .npmrc
================================================
# .npmrc
# Configuration for pnpm.

# Uses the exact version instead of any within-patch-range version of an
# installed package.
save-exact=true
# Do not error out on missing peer dependencies.
strict-peer-dependencies=false


================================================
FILE: .prettierignore
================================================
node_modules/**
declarations/**
examples/**
cli.js
lib.js
lib.esm.js


================================================
FILE: LICENSE
================================================
MIT License

Copyright (c) 2022 Mayke

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.


================================================
FILE: README.md
================================================
# remix-vite

<div>
  <br />
  <br />
  <img width="320" src="https://user-images.githubusercontent.com/2893850/200666584-3c825b6d-a91d-4ade-bb46-6523fc66416e.png" />
  <br />
  <br />
</div>

`remix-vite` helps you serve [Remix](https://remix.run/) apps locally using [Vite](https://vitejs.dev/).

## Usage

The quickest way to get started is to just run `npx remix-vite` in your project's directory.

If you prefer, you can also install the package globally (you'll need at least [Node LTS](https://github.com/nodejs/Release#release-schedule)):

```bash
> npm install --global remix-vite
```

Once that's done, you can run this command inside your project's directory...

```bash
> remix-vite
```

Now you understand how the package works! :tada:

## Custom Remix Server

If you want to use `remix-vite` with a custom remix server, you can do so by integrating your server with vite dev server.

Let's say you have a custom Express server and you want to use it with `remix-vite`. Here is how you can do it:

```js
const express = require('express');
const { createRequestHandler } = require('@remix-run/express');
const { createRemixViteDevServer, getRemixViteBuild } = require('remix-vite');

const app = express();

// Create a remix-vite dev server.
createRemixViteDevServer().then((remixViteDevServer) => {
  // Use remix-vite dev server as middleware.
  app.use(remixViteDevServer.middlewares);

  app.all('*', async (req, res, next) => {
    purgeRequireCache();

    // Get the remix build generated by remix-vite.
    const remixBuild = await getRemixViteBuild(remixViteDevServer);

    // Create a remix express request handler.
    const requestHandler = createRequestHandler({ build: remixBuild });

    await requestHandler(req, res, next);
  });

  // Start the server.
  app.listen(3000, () => {
    console.log('Listening at http://localhost:3000');
  });
});

function purgeRequireCache() {
  // purge require cache on requests for "server side HMR" this won't let
  // you have in-memory objects between requests in development.
  for (const key in require.cache) {
    delete require.cache[key];
  }
}
```

## Change default host and port

If 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`

`> remix-vite --port=4000`

## Issues

If 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).

## Author

Mayke Freitas ([@sudomf](https://twitter.com/maykedev))

<div>
  <a href="https://www.buymeacoffee.com/mayke" style="display: block; max-width: 320px;">
    <img width="320" src="./assets/yellow-button.png" />
  </div>
</div>


================================================
FILE: config/husky/pre-commit
================================================
#!/bin/sh

# config/husky/pre-commit
# Run `lint-staged` before every commit.

. "$(dirname "$0")/_/husky.sh"

FORCE_COLOR=2 yarn lint-staged


================================================
FILE: examples/complex/.dockerignore
================================================
/node_modules
*.log
.DS_Store
.env
/.cache
/public/build
/build


================================================
FILE: examples/complex/.eslintrc.js
================================================
/** @type {import('@types/eslint').Linter.BaseConfig} */
module.exports = {
  extends: [
    "@remix-run/eslint-config",
    "@remix-run/eslint-config/node",
    "@remix-run/eslint-config/jest-testing-library",
    "prettier",
  ],
  env: {
    "cypress/globals": true,
  },
  plugins: ["cypress"],
  // we're using vitest which has a very similar API to jest
  // (so the linting plugins work nicely), but it means we have to explicitly
  // set the jest version.
  settings: {
    jest: {
      version: 28,
    },
  },
};


================================================
FILE: examples/complex/.gitignore
================================================
# We don't want lockfiles in stacks, as people could use a different package manager
# This part will be removed by `remix.init`
package-lock.json
yarn.lock
pnpm-lock.yaml
pnpm-lock.yml

node_modules

/build
/public/build
.env

/cypress/screenshots
/cypress/videos
/prisma/data.db
/prisma/data.db-journal

/app/styles/tailwind.css


================================================
FILE: examples/complex/.gitpod.Dockerfile
================================================
FROM gitpod/workspace-full

# Install Fly
RUN curl -L https://fly.io/install.sh | sh
ENV FLYCTL_INSTALL="/home/gitpod/.fly"
ENV PATH="$FLYCTL_INSTALL/bin:$PATH"

# Install GitHub CLI
RUN brew install gh


================================================
FILE: examples/complex/.gitpod.yml
================================================
# https://www.gitpod.io/docs/config-gitpod-file

image:
  file: .gitpod.Dockerfile

ports:
  - port: 3000
    onOpen: notify

tasks:
  - name: Restore .env file
    command: |
      if [ -f .env ]; then
        # If this workspace already has a .env, don't override it
        # Local changes survive a workspace being opened and closed
        # but they will not persist between separate workspaces for the same repo

        echo "Found .env in workspace"
      else
        # There is no .env
        if [ ! -n "${ENV}" ]; then
          # There is no $ENV from a previous workspace
          # Default to the example .env
          echo "Setting example .env"

          cp .env.example .env 
        else
          # After making changes to .env, run this line to persist it to $ENV
          #   eval $(gp env -e ENV="$(base64 .env | tr -d '\n')")
          # 
          # Environment variables set this way are shared between all your workspaces for this repo
          # The lines below will read $ENV and print a .env file

          echo "Restoring .env from Gitpod"

          echo "${ENV}" | base64 -d | tee .env > /dev/null
        fi
      fi

  - init: npm install
    command: npm run setup && npm run dev

vscode:
  extensions:
    - ms-azuretools.vscode-docker
    - esbenp.prettier-vscode
    - dbaeumer.vscode-eslint
    - bradlc.vscode-tailwindcss


================================================
FILE: examples/complex/.npmrc
================================================
legacy-peer-deps=true


================================================
FILE: examples/complex/.prettierignore
================================================
node_modules

/build
/public/build
.env

/app/styles/tailwind.css


================================================
FILE: examples/complex/Dockerfile
================================================
# base node image
FROM node:16-bullseye-slim as base

# set for base and all layer that inherit from it
ENV NODE_ENV production

# Install openssl for Prisma
RUN apt-get update && apt-get install -y openssl sqlite3

# Install all node_modules, including dev dependencies
FROM base as deps

WORKDIR /myapp

ADD package.json .npmrc ./
RUN npm install --production=false

# Setup production node_modules
FROM base as production-deps

WORKDIR /myapp

COPY --from=deps /myapp/node_modules /myapp/node_modules
ADD package.json .npmrc ./
RUN npm prune --production

# Build the app
FROM base as build

WORKDIR /myapp

COPY --from=deps /myapp/node_modules /myapp/node_modules

ADD prisma .
RUN npx prisma generate

ADD . .
RUN npm run build

# Finally, build the production image with minimal footprint
FROM base

ENV DATABASE_URL=file:/data/sqlite.db
ENV PORT="8080"
ENV NODE_ENV="production"

# add shortcut for connecting to database CLI
RUN echo "#!/bin/sh\nset -x\nsqlite3 \$DATABASE_URL" > /usr/local/bin/database-cli && chmod +x /usr/local/bin/database-cli

WORKDIR /myapp

COPY --from=production-deps /myapp/node_modules /myapp/node_modules
COPY --from=build /myapp/node_modules/.prisma /myapp/node_modules/.prisma

COPY --from=build /myapp/build /myapp/build
COPY --from=build /myapp/public /myapp/public
COPY --from=build /myapp/package.json /myapp/package.json
COPY --from=build /myapp/start.sh /myapp/start.sh
COPY --from=build /myapp/prisma /myapp/prisma

ENTRYPOINT [ "./start.sh" ]


================================================
FILE: examples/complex/README.md
================================================
# Remix Indie Stack

![The Remix Indie Stack](https://repository-images.githubusercontent.com/465928257/a241fa49-bd4d-485a-a2a5-5cb8e4ee0abf)

Learn more about [Remix Stacks](https://remix.run/stacks).

```
npx create-remix@latest --template remix-run/indie-stack
```

## What's in the stack

- [Fly app deployment](https://fly.io) with [Docker](https://www.docker.com/)
- Production-ready [SQLite Database](https://sqlite.org)
- Healthcheck endpoint for [Fly backups region fallbacks](https://fly.io/docs/reference/configuration/#services-http_checks)
- [GitHub Actions](https://github.com/features/actions) for deploy on merge to production and staging environments
- Email/Password Authentication with [cookie-based sessions](https://remix.run/docs/en/v1/api/remix#createcookiesessionstorage)
- Database ORM with [Prisma](https://prisma.io)
- Styling with [Tailwind](https://tailwindcss.com/)
- End-to-end testing with [Cypress](https://cypress.io)
- Local third party request mocking with [MSW](https://mswjs.io)
- Unit testing with [Vitest](https://vitest.dev) and [Testing Library](https://testing-library.com)
- Code formatting with [Prettier](https://prettier.io)
- Linting with [ESLint](https://eslint.org)
- Static Types with [TypeScript](https://typescriptlang.org)

Not a fan of bits of the stack? Fork it, change it, and use `npx create-remix --template your/repo`! Make it your own.

## Quickstart

Click this button to create a [Gitpod](https://gitpod.io) workspace with the project set up and Fly pre-installed

[![Gitpod Ready-to-Code](https://img.shields.io/badge/Gitpod-Ready--to--Code-blue?logo=gitpod)](https://gitpod.io/from-referrer/)

## Development

- This step only applies if you've opted out of having the CLI install dependencies for you:

  ```sh
  npx remix init
  ```

- Initial setup: _If you just generated this project, this step has been done for you._

  ```sh
  npm run setup
  ```

- Start dev server:

  ```sh
  npm run dev
  ```

This starts your app in development mode, rebuilding assets on file changes.

The database seed script creates a new user with some data you can use to get started:

- Email: `rachel@remix.run`
- Password: `racheliscool`

### Relevant code:

This 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.

- creating users, and logging in and out [./app/models/user.server.ts](./app/models/user.server.ts)
- user sessions, and verifying them [./app/session.server.ts](./app/session.server.ts)
- creating, and deleting notes [./app/models/note.server.ts](./app/models/note.server.ts)

## Deployment

This Remix Stack comes with two GitHub Actions that handle automatically deploying your app to production and staging environments.

Prior to your first deployment, you'll need to do a few things:

- [Install Fly](https://fly.io/docs/getting-started/installing-flyctl/)

- Sign up and log in to Fly

  ```sh
  fly auth signup
  ```

  > **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.

- Create two apps on Fly, one for staging and one for production:

  ```sh
  fly apps create indie-stack-template
  fly apps create indie-stack-template-staging
  ```

  > **Note:** Make sure this name matches the `app` set in your `fly.toml` file. Otherwise, you will not be able to deploy.

  - Initialize Git.

  ```sh
  git init
  ```

- Create a new [GitHub Repository](https://repo.new), and then add it as the remote for your project. **Do not push your app yet!**

  ```sh
  git remote add origin <ORIGIN_URL>
  ```

- 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`.

- Add a `SESSION_SECRET` to your fly app secrets, to do this you can run the following commands:

  ```sh
  fly secrets set SESSION_SECRET=$(openssl rand -hex 32) --app indie-stack-template
  fly secrets set SESSION_SECRET=$(openssl rand -hex 32) --app indie-stack-template-staging
  ```

  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.

- Create a persistent volume for the sqlite database for both your staging and production environments. Run the following:

  ```sh
  fly volumes create data --size 1 --app indie-stack-template
  fly volumes create data --size 1 --app indie-stack-template-staging
  ```

Now 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.

### Connecting to your database

The 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`.

### Getting Help with Deployment

If 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.

## GitHub Actions

We 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.

## Testing

### Cypress

We 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.

We use [`@testing-library/cypress`](https://testing-library.com/cypress) for selecting elements on the page semantically.

To 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.

We have a utility for testing authenticated features without having to go through the login flow:

```ts
cy.login();
// you are now logged in as a new user
```

We 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:

```ts
afterEach(() => {
  cy.cleanupUser();
});
```

That way, we can keep your local db clean and keep your tests isolated from one another.

### Vitest

For 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).

### Type Checking

This 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`.

### Linting

This project uses ESLint for linting. That is configured in `.eslintrc.js`.

### Formatting

We 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.


================================================
FILE: examples/complex/app/db.server.ts
================================================
import { PrismaClient } from "@prisma/client";

let prisma: PrismaClient;

declare global {
  var __db__: PrismaClient;
}

// this is needed because in development we don't want to restart
// the server with every change, but we want to make sure we don't
// create a new connection to the DB with every change either.
// in production we'll have a single connection to the DB.
if (process.env.NODE_ENV === "production") {
  prisma = new PrismaClient();
} else {
  if (!global.__db__) {
    global.__db__ = new PrismaClient();
  }
  prisma = global.__db__;
  prisma.$connect();
}

export { prisma };


================================================
FILE: examples/complex/app/entry.client.tsx
================================================
import { RemixBrowser } from "@remix-run/react";
import { startTransition, StrictMode } from "react";
import { hydrateRoot } from "react-dom/client";

const hydrate = () => {
  startTransition(() => {
    hydrateRoot(
      document,
      <StrictMode>
        <RemixBrowser />
      </StrictMode>
    );
  });
};

if (window.requestIdleCallback) {
  window.requestIdleCallback(hydrate);
} else {
  // Safari doesn't support requestIdleCallback
  // https://caniuse.com/requestidlecallback
  window.setTimeout(hydrate, 1);
}


================================================
FILE: examples/complex/app/entry.server.tsx
================================================
import { PassThrough } from "stream";
import type { EntryContext } from "@remix-run/node";
import { Response } from "@remix-run/node";
import { RemixServer } from "@remix-run/react";
import isbot from "isbot";
import { renderToPipeableStream } from "react-dom/server";

const ABORT_DELAY = 5000;

export default function handleRequest(
  request: Request,
  responseStatusCode: number,
  responseHeaders: Headers,
  remixContext: EntryContext
) {
  const callbackName = isbot(request.headers.get("user-agent"))
    ? "onAllReady"
    : "onShellReady";

  return new Promise(async (resolve, reject) => {
    let didError = false;

    const { pipe, abort } = renderToPipeableStream(
      <RemixServer context={remixContext} url={request.url} />,
      {
        [callbackName]: () => {
          const body = new PassThrough();

          responseHeaders.set("Content-Type", "text/html");

          resolve(
            new Response(body, {
              headers: responseHeaders,
              status: didError ? 500 : responseStatusCode,
            })
          );

          pipe(body);
        },
        onShellError: (err: unknown) => {
          reject(err);
        },
        onError: (error: unknown) => {
          didError = true;

          console.error(error);
        },
      }
    );

    setTimeout(abort, ABORT_DELAY);
  });
}


================================================
FILE: examples/complex/app/models/note.server.ts
================================================
import type { User, Note } from "@prisma/client";

import { prisma } from "~/db.server";

export type { Note } from "@prisma/client";

export function getNote({
  id,
  userId,
}: Pick<Note, "id"> & {
  userId: User["id"];
}) {
  return prisma.note.findFirst({
    select: { id: true, body: true, title: true },
    where: { id, userId },
  });
}

export function getNoteListItems({ userId }: { userId: User["id"] }) {
  return prisma.note.findMany({
    where: { userId },
    select: { id: true, title: true },
    orderBy: { updatedAt: "desc" },
  });
}

export function createNote({
  body,
  title,
  userId,
}: Pick<Note, "body" | "title"> & {
  userId: User["id"];
}) {
  return prisma.note.create({
    data: {
      title,
      body,
      user: {
        connect: {
          id: userId,
        },
      },
    },
  });
}

export function deleteNote({
  id,
  userId,
}: Pick<Note, "id"> & { userId: User["id"] }) {
  return prisma.note.deleteMany({
    where: { id, userId },
  });
}


================================================
FILE: examples/complex/app/models/user.server.ts
================================================
import type { Password, User } from "@prisma/client";
import bcrypt from "bcryptjs";

import { prisma } from "~/db.server";

export type { User } from "@prisma/client";

export async function getUserById(id: User["id"]) {
  return prisma.user.findUnique({ where: { id } });
}

export async function getUserByEmail(email: User["email"]) {
  return prisma.user.findUnique({ where: { email } });
}

export async function createUser(email: User["email"], password: string) {
  const hashedPassword = await bcrypt.hash(password, 10);

  return prisma.user.create({
    data: {
      email,
      password: {
        create: {
          hash: hashedPassword,
        },
      },
    },
  });
}

export async function deleteUserByEmail(email: User["email"]) {
  return prisma.user.delete({ where: { email } });
}

export async function verifyLogin(
  email: User["email"],
  password: Password["hash"]
) {
  const userWithPassword = await prisma.user.findUnique({
    where: { email },
    include: {
      password: true,
    },
  });

  if (!userWithPassword || !userWithPassword.password) {
    return null;
  }

  const isValid = await bcrypt.compare(
    password,
    userWithPassword.password.hash
  );

  if (!isValid) {
    return null;
  }

  const { password: _password, ...userWithoutPassword } = userWithPassword;

  return userWithoutPassword;
}


================================================
FILE: examples/complex/app/root.tsx
================================================
import type { LinksFunction, LoaderArgs } from "@remix-run/node";
import { json } from "@remix-run/node";
import {
  Links,
  LiveReload,
  Meta,
  Outlet,
  Scripts,
  ScrollRestoration,
} from "@remix-run/react";
import tailwindStylesheetUrl from "./styles/tailwind.css";
import { getUser } from "./session.server";

export const links: LinksFunction = () => {
  return [{ rel: "stylesheet", href: tailwindStylesheetUrl }];
};

export async function loader({ request }: LoaderArgs) {
  return json({
    user: await getUser(request),
  });
}

export default function App() {
  return (
    <html lang="en" className="h-full">
      <head>
        <Meta />
        <Links />
      </head>
      <body className="h-full">
        <Outlet />
        <ScrollRestoration />
        <Scripts />
        <LiveReload />
      </body>
    </html>
  );
}


================================================
FILE: examples/complex/app/routes/healthcheck.tsx
================================================
// learn more: https://fly.io/docs/reference/configuration/#services-http_checks
import type { LoaderArgs } from "@remix-run/node";

import { prisma } from "~/db.server";

export async function loader({ request }: LoaderArgs) {
  const host =
    request.headers.get("X-Forwarded-Host") ?? request.headers.get("host");

  try {
    const url = new URL("/", `http://${host}`);
    // if we can connect to the database and make a simple query
    // and make a HEAD request to ourselves, then we're good.
    await Promise.all([
      prisma.user.count(),
      fetch(url.toString(), { method: "HEAD" }).then((r) => {
        if (!r.ok) return Promise.reject(r);
      }),
    ]);
    return new Response("OK");
  } catch (error: unknown) {
    console.log("healthcheck ❌", { error });
    return new Response("ERROR", { status: 500 });
  }
}


================================================
FILE: examples/complex/app/routes/index.tsx
================================================
import { Link } from "@remix-run/react";

import { useOptionalUser } from "~/utils";

export default function Index() {
  const user = useOptionalUser();
  return (
    <main className="relative min-h-screen bg-white sm:flex sm:items-center sm:justify-center">
      <div className="relative sm:pb-16 sm:pt-8">
        <div className="mx-auto max-w-7xl sm:px-6 lg:px-8">
          <div className="relative shadow-xl sm:overflow-hidden sm:rounded-2xl">
            <div className="absolute inset-0">
              <div className="absolute inset-0 bg-gray-900 mix-blend-multiply" />
            </div>
            <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">
              <h1 className="text-center text-6xl font-extrabold tracking-tight sm:text-8xl lg:text-9xl">
                <span className="block uppercase text-yellow-500 drop-shadow-md">
                  Remix Vite
                </span>
              </h1>
              <p className="mx-auto mt-6 max-w-lg text-center text-xl text-white sm:max-w-3xl">
                A Vite server for Remix
              </p>
              <div className="mx-auto mt-10 max-w-sm sm:flex sm:max-w-none sm:justify-center">
                {user ? (
                  <Link
                    to="/notes"
                    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"
                  >
                    View Notes for {user.email}
                  </Link>
                ) : (
                  <div className="space-y-4 sm:mx-auto sm:inline-grid sm:grid-cols-2 sm:gap-5 sm:space-y-0">
                    <Link
                      to="/join"
                      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"
                    >
                      Sign up
                    </Link>
                    <Link
                      to="/login"
                      className="flex items-center justify-center rounded-md bg-yellow-500 px-4 py-3 font-medium text-white hover:bg-yellow-600"
                    >
                      Log In
                    </Link>
                  </div>
                )}
              </div>
              <a href="https://remix.run">
                <img
                  src="https://user-images.githubusercontent.com/1500684/158298926-e45dafff-3544-4b69-96d6-d3bcc33fc76a.svg"
                  alt="Remix"
                  className="mx-auto mt-16 w-full max-w-[12rem] md:max-w-[16rem]"
                />
              </a>
            </div>
          </div>
        </div>

        <div className="mx-auto max-w-7xl py-2 px-4 sm:px-6 lg:px-8">
          <div className="mt-6 flex flex-wrap justify-center gap-8">
            {[
              {
                src: "https://user-images.githubusercontent.com/1500684/157764397-ccd8ea10-b8aa-4772-a99b-35de937319e1.svg",
                alt: "Fly.io",
                href: "https://fly.io",
              },
              {
                src: "https://user-images.githubusercontent.com/1500684/157764395-137ec949-382c-43bd-a3c0-0cb8cb22e22d.svg",
                alt: "SQLite",
                href: "https://sqlite.org",
              },
              {
                src: "https://user-images.githubusercontent.com/1500684/157764484-ad64a21a-d7fb-47e3-8669-ec046da20c1f.svg",
                alt: "Prisma",
                href: "https://prisma.io",
              },
              {
                src: "https://user-images.githubusercontent.com/1500684/157764276-a516a239-e377-4a20-b44a-0ac7b65c8c14.svg",
                alt: "Tailwind",
                href: "https://tailwindcss.com",
              },
              {
                src: "https://user-images.githubusercontent.com/1500684/157764454-48ac8c71-a2a9-4b5e-b19c-edef8b8953d6.svg",
                alt: "Cypress",
                href: "https://www.cypress.io",
              },
              {
                src: "https://user-images.githubusercontent.com/1500684/157772386-75444196-0604-4340-af28-53b236faa182.svg",
                alt: "MSW",
                href: "https://mswjs.io",
              },
              {
                src: "https://user-images.githubusercontent.com/1500684/157772447-00fccdce-9d12-46a3-8bb4-fac612cdc949.svg",
                alt: "Vitest",
                href: "https://vitest.dev",
              },
              {
                src: "https://user-images.githubusercontent.com/1500684/157772662-92b0dd3a-453f-4d18-b8be-9fa6efde52cf.png",
                alt: "Testing Library",
                href: "https://testing-library.com",
              },
              {
                src: "https://user-images.githubusercontent.com/1500684/157772934-ce0a943d-e9d0-40f8-97f3-f464c0811643.svg",
                alt: "Prettier",
                href: "https://prettier.io",
              },
              {
                src: "https://user-images.githubusercontent.com/1500684/157772990-3968ff7c-b551-4c55-a25c-046a32709a8e.svg",
                alt: "ESLint",
                href: "https://eslint.org",
              },
              {
                src: "https://user-images.githubusercontent.com/1500684/157773063-20a0ed64-b9f8-4e0b-9d1e-0b65a3d4a6db.svg",
                alt: "TypeScript",
                href: "https://typescriptlang.org",
              },
            ].map((img) => (
              <a
                key={img.href}
                href={img.href}
                className="flex h-16 w-32 justify-center p-1 grayscale transition hover:grayscale-0 focus:grayscale-0"
              >
                <img alt={img.alt} src={img.src} className="object-contain" />
              </a>
            ))}
          </div>
        </div>
      </div>
    </main>
  );
}


================================================
FILE: examples/complex/app/routes/join.tsx
================================================
import type { ActionArgs, LoaderArgs, MetaFunction } from "@remix-run/node";
import { json, redirect } from "@remix-run/node";
import { Form, Link, useActionData, useSearchParams } from "@remix-run/react";
import * as React from "react";

import { getUserId, createUserSession } from "~/session.server";

import { createUser, getUserByEmail } from "~/models/user.server";
import { safeRedirect, validateEmail } from "~/utils";

export async function loader({ request }: LoaderArgs) {
  const userId = await getUserId(request);
  if (userId) return redirect("/");
  return json({});
}

export async function action({ request }: ActionArgs) {
  const formData = await request.formData();
  const email = formData.get("email");
  const password = formData.get("password");
  const redirectTo = safeRedirect(formData.get("redirectTo"), "/");

  if (!validateEmail(email)) {
    return json(
      { errors: { email: "Email is invalid", password: null } },
      { status: 400 }
    );
  }

  if (typeof password !== "string" || password.length === 0) {
    return json(
      { errors: { email: null, password: "Password is required" } },
      { status: 400 }
    );
  }

  if (password.length < 8) {
    return json(
      { errors: { email: null, password: "Password is too short" } },
      { status: 400 }
    );
  }

  const existingUser = await getUserByEmail(email);
  if (existingUser) {
    return json(
      {
        errors: {
          email: "A user already exists with this email",
          password: null,
        },
      },
      { status: 400 }
    );
  }

  const user = await createUser(email, password);

  return createUserSession({
    request,
    userId: user.id,
    remember: false,
    redirectTo,
  });
}

export const meta: MetaFunction = () => {
  return {
    title: "Sign Up",
  };
};

export default function Join() {
  const [searchParams] = useSearchParams();
  const redirectTo = searchParams.get("redirectTo") ?? undefined;
  const actionData = useActionData<typeof action>();
  const emailRef = React.useRef<HTMLInputElement>(null);
  const passwordRef = React.useRef<HTMLInputElement>(null);

  React.useEffect(() => {
    if (actionData?.errors?.email) {
      emailRef.current?.focus();
    } else if (actionData?.errors?.password) {
      passwordRef.current?.focus();
    }
  }, [actionData]);

  return (
    <div className="flex min-h-full flex-col justify-center">
      <div className="mx-auto w-full max-w-md px-8">
        <Form method="post" className="space-y-6">
          <div>
            <label
              htmlFor="email"
              className="block text-sm font-medium text-gray-700"
            >
              Email address
            </label>
            <div className="mt-1">
              <input
                ref={emailRef}
                id="email"
                required
                autoFocus={true}
                name="email"
                type="email"
                autoComplete="email"
                aria-invalid={actionData?.errors?.email ? true : undefined}
                aria-describedby="email-error"
                className="w-full rounded border border-gray-500 px-2 py-1 text-lg"
              />
              {actionData?.errors?.email && (
                <div className="pt-1 text-red-700" id="email-error">
                  {actionData.errors.email}
                </div>
              )}
            </div>
          </div>

          <div>
            <label
              htmlFor="password"
              className="block text-sm font-medium text-gray-700"
            >
              Password
            </label>
            <div className="mt-1">
              <input
                id="password"
                ref={passwordRef}
                name="password"
                type="password"
                autoComplete="new-password"
                aria-invalid={actionData?.errors?.password ? true : undefined}
                aria-describedby="password-error"
                className="w-full rounded border border-gray-500 px-2 py-1 text-lg"
              />
              {actionData?.errors?.password && (
                <div className="pt-1 text-red-700" id="password-error">
                  {actionData.errors.password}
                </div>
              )}
            </div>
          </div>

          <input type="hidden" name="redirectTo" value={redirectTo} />
          <button
            type="submit"
            className="w-full rounded bg-blue-500  py-2 px-4 text-white hover:bg-blue-600 focus:bg-blue-400"
          >
            Create Account
          </button>
          <div className="flex items-center justify-center">
            <div className="text-center text-sm text-gray-500">
              Already have an account?{" "}
              <Link
                className="text-blue-500 underline"
                to={{
                  pathname: "/login",
                  search: searchParams.toString(),
                }}
              >
                Log in
              </Link>
            </div>
          </div>
        </Form>
      </div>
    </div>
  );
}


================================================
FILE: examples/complex/app/routes/login.tsx
================================================
import type { ActionArgs, LoaderArgs, MetaFunction } from "@remix-run/node";
import { json, redirect } from "@remix-run/node";
import { Form, Link, useActionData, useSearchParams } from "@remix-run/react";
import * as React from "react";

import { createUserSession, getUserId } from "~/session.server";
import { verifyLogin } from "~/models/user.server";
import { safeRedirect, validateEmail } from "~/utils";

export async function loader({ request }: LoaderArgs) {
  const userId = await getUserId(request);
  if (userId) return redirect("/");
  return json({});
}

export async function action({ request }: ActionArgs) {
  const formData = await request.formData();
  const email = formData.get("email");
  const password = formData.get("password");
  const redirectTo = safeRedirect(formData.get("redirectTo"), "/notes");
  const remember = formData.get("remember");

  if (!validateEmail(email)) {
    return json(
      { errors: { email: "Email is invalid", password: null } },
      { status: 400 }
    );
  }

  if (typeof password !== "string" || password.length === 0) {
    return json(
      { errors: { email: null, password: "Password is required" } },
      { status: 400 }
    );
  }

  if (password.length < 8) {
    return json(
      { errors: { email: null, password: "Password is too short" } },
      { status: 400 }
    );
  }

  const user = await verifyLogin(email, password);

  if (!user) {
    return json(
      { errors: { email: "Invalid email or password", password: null } },
      { status: 400 }
    );
  }

  return createUserSession({
    request,
    userId: user.id,
    remember: remember === "on" ? true : false,
    redirectTo,
  });
}

export const meta: MetaFunction = () => {
  return {
    title: "Login",
  };
};

export default function LoginPage() {
  const [searchParams] = useSearchParams();
  const redirectTo = searchParams.get("redirectTo") || "/notes";
  const actionData = useActionData<typeof action>();
  const emailRef = React.useRef<HTMLInputElement>(null);
  const passwordRef = React.useRef<HTMLInputElement>(null);

  React.useEffect(() => {
    if (actionData?.errors?.email) {
      emailRef.current?.focus();
    } else if (actionData?.errors?.password) {
      passwordRef.current?.focus();
    }
  }, [actionData]);

  return (
    <div className="flex min-h-full flex-col justify-center">
      <div className="mx-auto w-full max-w-md px-8">
        <Form method="post" className="space-y-6">
          <div>
            <label
              htmlFor="email"
              className="block text-sm font-medium text-gray-700"
            >
              Email address
            </label>
            <div className="mt-1">
              <input
                ref={emailRef}
                id="email"
                required
                autoFocus={true}
                name="email"
                type="email"
                autoComplete="email"
                aria-invalid={actionData?.errors?.email ? true : undefined}
                aria-describedby="email-error"
                className="w-full rounded border border-gray-500 px-2 py-1 text-lg"
              />
              {actionData?.errors?.email && (
                <div className="pt-1 text-red-700" id="email-error">
                  {actionData.errors.email}
                </div>
              )}
            </div>
          </div>

          <div>
            <label
              htmlFor="password"
              className="block text-sm font-medium text-gray-700"
            >
              Password
            </label>
            <div className="mt-1">
              <input
                id="password"
                ref={passwordRef}
                name="password"
                type="password"
                autoComplete="current-password"
                aria-invalid={actionData?.errors?.password ? true : undefined}
                aria-describedby="password-error"
                className="w-full rounded border border-gray-500 px-2 py-1 text-lg"
              />
              {actionData?.errors?.password && (
                <div className="pt-1 text-red-700" id="password-error">
                  {actionData.errors.password}
                </div>
              )}
            </div>
          </div>

          <input type="hidden" name="redirectTo" value={redirectTo} />
          <button
            type="submit"
            className="w-full rounded bg-blue-500  py-2 px-4 text-white hover:bg-blue-600 focus:bg-blue-400"
          >
            Log in
          </button>
          <div className="flex items-center justify-between">
            <div className="flex items-center">
              <input
                id="remember"
                name="remember"
                type="checkbox"
                className="h-4 w-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500"
              />
              <label
                htmlFor="remember"
                className="ml-2 block text-sm text-gray-900"
              >
                Remember me
              </label>
            </div>
            <div className="text-center text-sm text-gray-500">
              Don't have an account?{" "}
              <Link
                className="text-blue-500 underline"
                to={{
                  pathname: "/join",
                  search: searchParams.toString(),
                }}
              >
                Sign up
              </Link>
            </div>
          </div>
        </Form>
      </div>
    </div>
  );
}


================================================
FILE: examples/complex/app/routes/logout.tsx
================================================
import type { ActionArgs } from "@remix-run/node";
import { redirect } from "@remix-run/node";

import { logout } from "~/session.server";

export async function action({ request }: ActionArgs) {
  return logout(request);
}

export async function loader() {
  return redirect("/");
}


================================================
FILE: examples/complex/app/routes/notes/$noteId.tsx
================================================
import type { ActionArgs, LoaderArgs } from "@remix-run/node";
import { json, redirect } from "@remix-run/node";
import { Form, useCatch, useLoaderData } from "@remix-run/react";
import invariant from "tiny-invariant";

import { deleteNote, getNote } from "~/models/note.server";
import { requireUserId } from "~/session.server";

export async function loader({ request, params }: LoaderArgs) {
  const userId = await requireUserId(request);
  invariant(params.noteId, "noteId not found");

  const note = await getNote({ userId, id: params.noteId });
  if (!note) {
    throw new Response("Not Found", { status: 404 });
  }
  return json({ note });
}

export async function action({ request, params }: ActionArgs) {
  const userId = await requireUserId(request);
  invariant(params.noteId, "noteId not found");

  await deleteNote({ userId, id: params.noteId });

  return redirect("/notes");
}

export default function NoteDetailsPage() {
  const data = useLoaderData<typeof loader>();

  return (
    <div>
      <h3 className="text-2xl font-bold">{data.note.title}</h3>
      <p className="py-6">{data.note.body}</p>
      <hr className="my-4" />
      <Form method="post">
        <button
          type="submit"
          className="rounded bg-blue-500  py-2 px-4 text-white hover:bg-blue-600 focus:bg-blue-400"
        >
          Delete
        </button>
      </Form>
    </div>
  );
}

export function ErrorBoundary({ error }: { error: Error }) {
  console.error(error);

  return <div>An unexpected error occurred: {error.message}</div>;
}

export function CatchBoundary() {
  const caught = useCatch();

  if (caught.status === 404) {
    return <div>Note not found</div>;
  }

  throw new Error(`Unexpected caught response with status: ${caught.status}`);
}


================================================
FILE: examples/complex/app/routes/notes/index.tsx
================================================
import { Link } from "@remix-run/react";

export default function NoteIndexPage() {
  return (
    <p>
      No note selected. Select a note on the left, or{" "}
      <Link to="new" className="text-blue-500 underline">
        create a new note.
      </Link>
    </p>
  );
}


================================================
FILE: examples/complex/app/routes/notes/new.tsx
================================================
import type { ActionArgs } from "@remix-run/node";
import { json, redirect } from "@remix-run/node";
import { Form, useActionData } from "@remix-run/react";
import * as React from "react";

import { createNote } from "~/models/note.server";
import { requireUserId } from "~/session.server";

export async function action({ request }: ActionArgs) {
  const userId = await requireUserId(request);

  const formData = await request.formData();
  const title = formData.get("title");
  const body = formData.get("body");

  if (typeof title !== "string" || title.length === 0) {
    return json(
      { errors: { title: "Title is required", body: null } },
      { status: 400 }
    );
  }

  if (typeof body !== "string" || body.length === 0) {
    return json(
      { errors: { title: null, body: "Body is required" } },
      { status: 400 }
    );
  }

  const note = await createNote({ title, body, userId });

  return redirect(`/notes/${note.id}`);
}

export default function NewNotePage() {
  const actionData = useActionData<typeof action>();
  const titleRef = React.useRef<HTMLInputElement>(null);
  const bodyRef = React.useRef<HTMLTextAreaElement>(null);

  React.useEffect(() => {
    if (actionData?.errors?.title) {
      titleRef.current?.focus();
    } else if (actionData?.errors?.body) {
      bodyRef.current?.focus();
    }
  }, [actionData]);

  return (
    <Form
      method="post"
      style={{
        display: "flex",
        flexDirection: "column",
        gap: 8,
        width: "100%",
      }}
    >
      <div>
        <label className="flex w-full flex-col gap-1">
          <span>Title: </span>
          <input
            ref={titleRef}
            name="title"
            className="flex-1 rounded-md border-2 border-blue-500 px-3 text-lg leading-loose"
            aria-invalid={actionData?.errors?.title ? true : undefined}
            aria-errormessage={
              actionData?.errors?.title ? "title-error" : undefined
            }
          />
        </label>
        {actionData?.errors?.title && (
          <div className="pt-1 text-red-700" id="title-error">
            {actionData.errors.title}
          </div>
        )}
      </div>

      <div>
        <label className="flex w-full flex-col gap-1">
          <span>Body: </span>
          <textarea
            ref={bodyRef}
            name="body"
            rows={8}
            className="w-full flex-1 rounded-md border-2 border-blue-500 py-2 px-3 text-lg leading-6"
            aria-invalid={actionData?.errors?.body ? true : undefined}
            aria-errormessage={
              actionData?.errors?.body ? "body-error" : undefined
            }
          />
        </label>
        {actionData?.errors?.body && (
          <div className="pt-1 text-red-700" id="body-error">
            {actionData.errors.body}
          </div>
        )}
      </div>

      <div className="text-right">
        <button
          type="submit"
          className="rounded bg-blue-500 py-2 px-4 text-white hover:bg-blue-600 focus:bg-blue-400"
        >
          Save
        </button>
      </div>
    </Form>
  );
}


================================================
FILE: examples/complex/app/routes/notes.tsx
================================================
import type { LoaderArgs } from "@remix-run/node";
import { json } from "@remix-run/node";
import { Form, Link, NavLink, Outlet, useLoaderData } from "@remix-run/react";

import { requireUserId } from "~/session.server";
import { useUser } from "~/utils";
import { getNoteListItems } from "~/models/note.server";

export async function loader({ request }: LoaderArgs) {
  const userId = await requireUserId(request);
  const noteListItems = await getNoteListItems({ userId });
  return json({ noteListItems });
}

export default function NotesPage() {
  const data = useLoaderData<typeof loader>();
  const user = useUser();

  return (
    <div className="flex h-full min-h-screen flex-col">
      <header className="flex items-center justify-between bg-slate-800 p-4 text-white">
        <h1 className="text-3xl font-bold">
          <Link to=".">Notes</Link>
        </h1>
        <p>{user.email}</p>
        <Form action="/logout" method="post">
          <button
            type="submit"
            className="rounded bg-slate-600 py-2 px-4 text-blue-100 hover:bg-blue-500 active:bg-blue-600"
          >
            Logout
          </button>
        </Form>
      </header>

      <main className="flex h-full bg-white">
        <div className="h-full w-80 border-r bg-gray-50">
          <Link to="new" className="block p-4 text-xl text-blue-500">
            + New Note
          </Link>

          <hr />

          {data.noteListItems.length === 0 ? (
            <p className="p-4">No notes yet</p>
          ) : (
            <ol>
              {data.noteListItems.map((note) => (
                <li key={note.id}>
                  <NavLink
                    className={({ isActive }) =>
                      `block border-b p-4 text-xl ${isActive ? "bg-white" : ""}`
                    }
                    to={note.id}
                  >
                    📝 {note.title}
                  </NavLink>
                </li>
              ))}
            </ol>
          )}
        </div>

        <div className="flex-1 p-6">
          <Outlet />
        </div>
      </main>
    </div>
  );
}


================================================
FILE: examples/complex/app/session.server.ts
================================================
import { createCookieSessionStorage, redirect } from "@remix-run/node";
import invariant from "tiny-invariant";

import type { User } from "~/models/user.server";
import { getUserById } from "~/models/user.server";

invariant(process.env.SESSION_SECRET, "SESSION_SECRET must be set");

export const sessionStorage = createCookieSessionStorage({
  cookie: {
    name: "__session",
    httpOnly: true,
    path: "/",
    sameSite: "lax",
    secrets: [process.env.SESSION_SECRET],
    secure: process.env.NODE_ENV === "production",
  },
});

const USER_SESSION_KEY = "userId";

export async function getSession(request: Request) {
  const cookie = request.headers.get("Cookie");
  return sessionStorage.getSession(cookie);
}

export async function getUserId(
  request: Request
): Promise<User["id"] | undefined> {
  const session = await getSession(request);
  const userId = session.get(USER_SESSION_KEY);
  return userId;
}

export async function getUser(request: Request) {
  const userId = await getUserId(request);
  if (userId === undefined) return null;

  const user = await getUserById(userId);
  if (user) return user;

  throw await logout(request);
}

export async function requireUserId(
  request: Request,
  redirectTo: string = new URL(request.url).pathname
) {
  const userId = await getUserId(request);
  if (!userId) {
    const searchParams = new URLSearchParams([["redirectTo", redirectTo]]);
    throw redirect(`/login?${searchParams}`);
  }
  return userId;
}

export async function requireUser(request: Request) {
  const userId = await requireUserId(request);

  const user = await getUserById(userId);
  if (user) return user;

  throw await logout(request);
}

export async function createUserSession({
  request,
  userId,
  remember,
  redirectTo,
}: {
  request: Request;
  userId: string;
  remember: boolean;
  redirectTo: string;
}) {
  const session = await getSession(request);
  session.set(USER_SESSION_KEY, userId);
  return redirect(redirectTo, {
    headers: {
      "Set-Cookie": await sessionStorage.commitSession(session, {
        maxAge: remember
          ? 60 * 60 * 24 * 7 // 7 days
          : undefined,
      }),
    },
  });
}

export async function logout(request: Request) {
  const session = await getSession(request);
  return redirect("/", {
    headers: {
      "Set-Cookie": await sessionStorage.destroySession(session),
    },
  });
}


================================================
FILE: examples/complex/app/utils.test.ts
================================================
import { validateEmail } from "./utils";

test("validateEmail returns false for non-emails", () => {
  expect(validateEmail(undefined)).toBe(false);
  expect(validateEmail(null)).toBe(false);
  expect(validateEmail("")).toBe(false);
  expect(validateEmail("not-an-email")).toBe(false);
  expect(validateEmail("n@")).toBe(false);
});

test("validateEmail returns true for emails", () => {
  expect(validateEmail("kody@example.com")).toBe(true);
});


================================================
FILE: examples/complex/app/utils.ts
================================================
import { useMatches } from "@remix-run/react";
import { useMemo } from "react";

import type { User } from "~/models/user.server";

const DEFAULT_REDIRECT = "/";

/**
 * This should be used any time the redirect path is user-provided
 * (Like the query string on our login/signup pages). This avoids
 * open-redirect vulnerabilities.
 * @param {string} to The redirect destination
 * @param {string} defaultRedirect The redirect to use if the to is unsafe.
 */
export function safeRedirect(
  to: FormDataEntryValue | string | null | undefined,
  defaultRedirect: string = DEFAULT_REDIRECT
) {
  if (!to || typeof to !== "string") {
    return defaultRedirect;
  }

  if (!to.startsWith("/") || to.startsWith("//")) {
    return defaultRedirect;
  }

  return to;
}

/**
 * This base hook is used in other hooks to quickly search for specific data
 * across all loader data using useMatches.
 * @param {string} id The route id
 * @returns {JSON|undefined} The router data or undefined if not found
 */
export function useMatchesData(
  id: string
): Record<string, unknown> | undefined {
  const matchingRoutes = useMatches();
  const route = useMemo(
    () => matchingRoutes.find((route) => route.id === id),
    [matchingRoutes, id]
  );
  return route?.data;
}

function isUser(user: any): user is User {
  return user && typeof user === "object" && typeof user.email === "string";
}

export function useOptionalUser(): User | undefined {
  const data = useMatchesData("root");
  if (!data || !isUser(data.user)) {
    return undefined;
  }
  return data.user;
}

export function useUser(): User {
  const maybeUser = useOptionalUser();
  if (!maybeUser) {
    throw new Error(
      "No user found in root loader, but user is required by useUser. If user is optional, try useOptionalUser instead."
    );
  }
  return maybeUser;
}

export function validateEmail(email: unknown): email is string {
  return typeof email === "string" && email.length > 3 && email.includes("@");
}


================================================
FILE: examples/complex/custom-server.js
================================================
const express = require('express');
const { createRequestHandler } = require('@remix-run/express');
const { createRemixViteDevServer, getRemixViteBuild } = require('../../lib');

const app = express();

// Create a remix-vite dev server.
createRemixViteDevServer().then(remixViteDevServer => {
  // Use remix-vite dev server as middleware.
  app.use(remixViteDevServer.middlewares);

  app.all('*', async (req, res, next) => {
    // Get the remix build generated by remix-vite.
    const remixBuild = await getRemixViteBuild(remixViteDevServer);
  
    // Create a remix express request handler.
    const requestHandler = createRequestHandler({ build: remixBuild });
  
    await requestHandler(req, res, next);
  });

  // Start the server.
  app.listen(3000, () => {
    console.log('Listening at http://localhost:3000');
  });
});


================================================
FILE: examples/complex/cypress/.eslintrc.js
================================================
module.exports = {
  parserOptions: {
    tsconfigRootDir: __dirname,
    project: "./tsconfig.json",
  },
};


================================================
FILE: examples/complex/cypress/e2e/smoke.cy.ts
================================================
import { faker } from "@faker-js/faker";

describe("smoke tests", () => {
  afterEach(() => {
    cy.cleanupUser();
  });

  it("should allow you to register and login", () => {
    const loginForm = {
      email: `${faker.internet.userName()}@example.com`,
      password: faker.internet.password(),
    };

    cy.then(() => ({ email: loginForm.email })).as("user");

    cy.visitAndCheck("/");

    cy.findByRole("link", { name: /sign up/i }).click();

    cy.findByRole("textbox", { name: /email/i }).type(loginForm.email);
    cy.findByLabelText(/password/i).type(loginForm.password);
    cy.findByRole("button", { name: /create account/i }).click();

    cy.findByRole("link", { name: /notes/i }).click();
    cy.findByRole("button", { name: /logout/i }).click();
    cy.findByRole("link", { name: /log in/i });
  });

  it("should allow you to make a note", () => {
    const testNote = {
      title: faker.lorem.words(1),
      body: faker.lorem.sentences(1),
    };
    cy.login();

    cy.visitAndCheck("/");

    cy.findByRole("link", { name: /notes/i }).click();
    cy.findByText("No notes yet");

    cy.findByRole("link", { name: /\+ new note/i }).click();

    cy.findByRole("textbox", { name: /title/i }).type(testNote.title);
    cy.findByRole("textbox", { name: /body/i }).type(testNote.body);
    cy.findByRole("button", { name: /save/i }).click();

    cy.findByRole("button", { name: /delete/i }).click();

    cy.findByText("No notes yet");
  });
});


================================================
FILE: examples/complex/cypress/fixtures/example.json
================================================
{
  "name": "Using fixtures to represent data",
  "email": "hello@cypress.io",
  "body": "Fixtures are a great way to mock data for responses to routes"
}


================================================
FILE: examples/complex/cypress/support/commands.ts
================================================
import { faker } from "@faker-js/faker";

declare global {
  namespace Cypress {
    interface Chainable {
      /**
       * Logs in with a random user. Yields the user and adds an alias to the user
       *
       * @returns {typeof login}
       * @memberof Chainable
       * @example
       *    cy.login()
       * @example
       *    cy.login({ email: 'whatever@example.com' })
       */
      login: typeof login;

      /**
       * Deletes the current @user
       *
       * @returns {typeof cleanupUser}
       * @memberof Chainable
       * @example
       *    cy.cleanupUser()
       * @example
       *    cy.cleanupUser({ email: 'whatever@example.com' })
       */
      cleanupUser: typeof cleanupUser;

      /**
       * Extends the standard visit command to wait for the page to load
       *
       * @returns {typeof visitAndCheck}
       * @memberof Chainable
       * @example
       *    cy.visitAndCheck('/')
       *  @example
       *    cy.visitAndCheck('/', 500)
       */
      visitAndCheck: typeof visitAndCheck;
    }
  }
}

function login({
  email = faker.internet.email(undefined, undefined, "example.com"),
}: {
  email?: string;
} = {}) {
  cy.then(() => ({ email })).as("user");
  cy.exec(
    `npx ts-node --require tsconfig-paths/register ./cypress/support/create-user.ts "${email}"`
  ).then(({ stdout }) => {
    const cookieValue = stdout
      .replace(/.*<cookie>(?<cookieValue>.*)<\/cookie>.*/s, "$<cookieValue>")
      .trim();
    cy.setCookie("__session", cookieValue);
  });
  return cy.get("@user");
}

function cleanupUser({ email }: { email?: string } = {}) {
  if (email) {
    deleteUserByEmail(email);
  } else {
    cy.get("@user").then((user) => {
      const email = (user as { email?: string }).email;
      if (email) {
        deleteUserByEmail(email);
      }
    });
  }
  cy.clearCookie("__session");
}

function deleteUserByEmail(email: string) {
  cy.exec(
    `npx ts-node --require tsconfig-paths/register ./cypress/support/delete-user.ts "${email}"`
  );
  cy.clearCookie("__session");
}

// We're waiting a second because of this issue happen randomly
// https://github.com/cypress-io/cypress/issues/7306
// Also added custom types to avoid getting detached
// https://github.com/cypress-io/cypress/issues/7306#issuecomment-1152752612
// ===========================================================
function visitAndCheck(url: string, waitTime: number = 1000) {
  cy.visit(url);
  cy.location("pathname").should("contain", url).wait(waitTime);
}

Cypress.Commands.add("login", login);
Cypress.Commands.add("cleanupUser", cleanupUser);
Cypress.Commands.add("visitAndCheck", visitAndCheck);


================================================
FILE: examples/complex/cypress/support/create-user.ts
================================================
// Use this to create a new user and login with that user
// Simply call this with:
// npx ts-node --require tsconfig-paths/register ./cypress/support/create-user.ts username@example.com
// and it will log out the cookie value you can use to interact with the server
// as that new user.

import { installGlobals } from "@remix-run/node";
import { parse } from "cookie";

import { createUser } from "~/models/user.server";
import { createUserSession } from "~/session.server";

installGlobals();

async function createAndLogin(email: string) {
  if (!email) {
    throw new Error("email required for login");
  }
  if (!email.endsWith("@example.com")) {
    throw new Error("All test emails must end in @example.com");
  }

  const user = await createUser(email, "myreallystrongpassword");

  const response = await createUserSession({
    request: new Request("test://test"),
    userId: user.id,
    remember: false,
    redirectTo: "/",
  });

  const cookieValue = response.headers.get("Set-Cookie");
  if (!cookieValue) {
    throw new Error("Cookie missing from createUserSession response");
  }
  const parsedCookie = parse(cookieValue);
  // we log it like this so our cypress command can parse it out and set it as
  // the cookie value.
  console.log(
    `
<cookie>
  ${parsedCookie.__session}
</cookie>
  `.trim()
  );
}

createAndLogin(process.argv[2]);


================================================
FILE: examples/complex/cypress/support/delete-user.ts
================================================
// Use this to delete a user by their email
// Simply call this with:
// npx ts-node --require tsconfig-paths/register ./cypress/support/delete-user.ts username@example.com
// and that user will get deleted

import { PrismaClientKnownRequestError } from "@prisma/client/runtime";
import { installGlobals } from "@remix-run/node";

import { prisma } from "~/db.server";

installGlobals();

async function deleteUser(email: string) {
  if (!email) {
    throw new Error("email required for login");
  }
  if (!email.endsWith("@example.com")) {
    throw new Error("All test emails must end in @example.com");
  }

  try {
    await prisma.user.delete({ where: { email } });
  } catch (error) {
    if (
      error instanceof PrismaClientKnownRequestError &&
      error.code === "P2025"
    ) {
      console.log("User not found, so no need to delete");
    } else {
      throw error;
    }
  } finally {
    await prisma.$disconnect();
  }
}

deleteUser(process.argv[2]);


================================================
FILE: examples/complex/cypress/support/e2e.ts
================================================
import "@testing-library/cypress/add-commands";
import "./commands";

Cypress.on("uncaught:exception", (err) => {
  // Cypress and React Hydrating the document don't get along
  // for some unknown reason. Hopefully we figure out why eventually
  // so we can remove this.
  if (
    /hydrat/i.test(err.message) ||
    /Minified React error #418/.test(err.message) ||
    /Minified React error #423/.test(err.message)
  ) {
    return false;
  }
});


================================================
FILE: examples/complex/cypress/tsconfig.json
================================================
{
  "exclude": [
    "../node_modules/@types/jest",
    "../node_modules/@testing-library/jest-dom"
  ],
  "include": [
    "e2e/**/*",
    "support/**/*",
    "../node_modules/cypress",
    "../node_modules/@testing-library/cypress"
  ],
  "compilerOptions": {
    "baseUrl": ".",
    "noEmit": true,
    "types": ["node", "cypress", "@testing-library/cypress"],
    "esModuleInterop": true,
    "jsx": "react-jsx",
    "moduleResolution": "node",
    "target": "es2019",
    "strict": true,
    "skipLibCheck": true,
    "resolveJsonModule": true,
    "typeRoots": ["../types", "../node_modules/@types"],

    "paths": {
      "~/*": ["../app/*"]
    }
  }
}


================================================
FILE: examples/complex/cypress.config.ts
================================================
import { defineConfig } from "cypress";

export default defineConfig({
  e2e: {
    setupNodeEvents: (on, config) => {
      const isDev = config.watchForFileChanges;
      const port = process.env.PORT ?? (isDev ? "3000" : "8811");
      const configOverrides: Partial<Cypress.PluginConfigOptions> = {
        baseUrl: `http://localhost:${port}`,
        video: !process.env.CI,
        screenshotOnRunFailure: !process.env.CI,
      };

      // To use this:
      // cy.task('log', whateverYouWantInTheTerminal)
      on("task", {
        log: (message) => {
          console.log(message);

          return null;
        },
      });

      return { ...config, ...configOverrides };
    },
  },
});


================================================
FILE: examples/complex/fly.toml
================================================
app = "indie-stack-template"

kill_signal = "SIGINT"
kill_timeout = 5
processes = []

[experimental]
  allowed_public_ports = []
  auto_rollback = true
  cmd = "start.sh"
  entrypoint = "sh"

[mounts]
  source = "data"
  destination = "/data"

[[services]]
  internal_port = 8080
  processes = ["app"]
  protocol = "tcp"
  script_checks = []

  [services.concurrency]
    hard_limit = 25
    soft_limit = 20
    type = "connections"

  [[services.ports]]
    handlers = ["http"]
    port = 80
    force_https = true

  [[services.ports]]
    handlers = ["tls", "http"]
    port = 443

  [[services.tcp_checks]]
    grace_period = "1s"
    interval = "15s"
    restart_limit = 0
    timeout = "2s"

  [[services.http_checks]]
    interval = "10s"
    grace_period = "5s"
    method = "get"
    path = "/healthcheck"
    protocol = "http"
    timeout = "2s"
    tls_skip_verify = false
    [services.http_checks.headers]


================================================
FILE: examples/complex/mocks/README.md
================================================
# Mocks

Use 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.

Learn more about how to use this at [mswjs.io](https://mswjs.io/)

For an extensive example, see the [source code for kentcdodds.com](https://github.com/kentcdodds/kentcdodds.com/blob/main/mocks/start.ts)


================================================
FILE: examples/complex/mocks/index.js
================================================
const { setupServer } = require("msw/node");

const server = setupServer();

server.listen({ onUnhandledRequest: "bypass" });
console.info("🔶 Mock server running");

process.once("SIGINT", () => server.close());
process.once("SIGTERM", () => server.close());


================================================
FILE: examples/complex/package.json
================================================
{
  "name": "indie-stack-template",
  "private": true,
  "sideEffects": false,
  "scripts": {
    "build": "run-s build:*",
    "build:css": "npm run generate:css -- --minify",
    "build:remix": "remix build",
    "dev": "run-p dev:*",
    "dev:css": "npm run generate:css -- --watch",
    "dev:remix": "remix watch",
    "dev:server": "node ./build",
    "format": "prettier --write .",
    "generate:css": "tailwindcss -o ./app/styles/tailwind.css",
    "lint": "eslint --cache --cache-location ./node_modules/.cache/eslint .",
    "setup": "prisma generate && prisma migrate deploy && prisma db seed",
    "start": "remix-serve build",
    "start:mocks": "binode --require ./mocks -- @remix-run/serve:remix-serve build",
    "test": "vitest",
    "test:e2e:dev": "start-server-and-test dev http://localhost:3000 \"npx cypress open\"",
    "pretest:e2e:run": "npm run build",
    "test:e2e:run": "cross-env PORT=8811 start-server-and-test start:mocks http://localhost:8811 \"npx cypress run\"",
    "typecheck": "tsc -b && tsc -b cypress",
    "validate": "run-p \"test -- --run\" lint typecheck test:e2e:run"
  },
  "prettier": {},
  "eslintIgnore": [
    "/node_modules",
    "/build",
    "/public/build"
  ],
  "dependencies": {
    "@prisma/client": "^4.3.1",
    "@remix-run/node": "^1.10.0",
    "@remix-run/react": "^1.10.0",
    "@remix-run/serve": "^1.10.0",
    "@remix-run/server-runtime": "^1.10.0",
    "bcryptjs": "^2.4.3",
    "dedent": "0.7.0",
    "dotenv": "^16.0.3",
    "isbot": "^3.5.3",
    "javascript-time-ago": "2.5.9",
    "react": "^18.2.0",
    "react-dom": "^18.2.0",
    "tiny-invariant": "^1.2.0"
  },
  "devDependencies": {
    "@faker-js/faker": "^7.5.0",
    "@remix-run/dev": "^1.10.0",
    "@remix-run/eslint-config": "^1.10.0",
    "@testing-library/cypress": "^8.0.3",
    "@testing-library/dom": "^8.18.1",
    "@testing-library/jest-dom": "^5.16.5",
    "@testing-library/react": "^13.4.0",
    "@testing-library/user-event": "^14.4.3",
    "@types/bcryptjs": "^2.4.2",
    "@types/eslint": "^8.4.6",
    "@types/express": "^4.17.14",
    "@types/jsesc": "^3.0.1",
    "@types/node": "^18.7.18",
    "@types/react": "^18.0.20",
    "@types/react-dom": "^18.0.6",
    "@vitejs/plugin-react": "^2.2.0",
    "@vitest/coverage-c8": "^0.23.4",
    "autoprefixer": "^10.4.11",
    "binode": "^1.0.5",
    "c8": "^7.12.0",
    "cookie": "^0.5.0",
    "cross-env": "^7.0.3",
    "cypress": "^10.8.0",
    "eslint": "^8.23.1",
    "eslint-config-prettier": "^8.5.0",
    "eslint-plugin-cypress": "^2.12.1",
    "happy-dom": "^6.0.4",
    "jsesc": "^3.0.2",
    "msw": "^0.47.3",
    "nodemon": "2.0.20",
    "npm-run-all": "^4.1.5",
    "postcss": "^8.4.16",
    "prettier": "2.7.1",
    "prettier-plugin-tailwindcss": "^0.1.13",
    "prisma": "^4.3.1",
    "start-server-and-test": "^1.14.0",
    "tailwindcss": "^3.1.8",
    "ts-morph": "^16.0.0",
    "ts-node": "^10.9.1",
    "tsconfig-paths": "^4.1.0",
    "typescript": "^4.8.3",
    "vite": "^3.2.2",
    "vite-tsconfig-paths": "^3.5.2",
    "vitest": "^0.23.4"
  },
  "resolutions": {
    "react": "^18.2.0",
    "react-dom": "^18.2.0"
  },
  "engines": {
    "node": ">=14"
  },
  "prisma": {
    "seed": "ts-node --require tsconfig-paths/register prisma/seed.ts"
  }
}


================================================
FILE: examples/complex/prisma/migrations/20220713162558_init/migration.sql
================================================
-- CreateTable
CREATE TABLE "User" (
    "id" TEXT NOT NULL PRIMARY KEY,
    "email" TEXT NOT NULL,
    "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
    "updatedAt" DATETIME NOT NULL
);

-- CreateTable
CREATE TABLE "Password" (
    "hash" TEXT NOT NULL,
    "userId" TEXT NOT NULL,
    CONSTRAINT "Password_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User" ("id") ON DELETE CASCADE ON UPDATE CASCADE
);

-- CreateTable
CREATE TABLE "Note" (
    "id" TEXT NOT NULL PRIMARY KEY,
    "title" TEXT NOT NULL,
    "body" TEXT NOT NULL,
    "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
    "updatedAt" DATETIME NOT NULL,
    "userId" TEXT NOT NULL,
    CONSTRAINT "Note_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User" ("id") ON DELETE CASCADE ON UPDATE CASCADE
);

-- CreateIndex
CREATE UNIQUE INDEX "User_email_key" ON "User"("email");

-- CreateIndex
CREATE UNIQUE INDEX "Password_userId_key" ON "Password"("userId");


================================================
FILE: examples/complex/prisma/migrations/migration_lock.toml
================================================
# Please do not edit this file manually
# It should be added in your version-control system (i.e. Git)
provider = "sqlite"

================================================
FILE: examples/complex/prisma/schema.prisma
================================================
datasource db {
  provider = "sqlite"
  url      = env("DATABASE_URL")
}

generator client {
  provider = "prisma-client-js"
}

model User {
  id    String @id @default(cuid())
  email String @unique

  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt

  password Password?
  notes    Note[]
}

model Password {
  hash String

  user   User   @relation(fields: [userId], references: [id], onDelete: Cascade, onUpdate: Cascade)
  userId String @unique
}

model Note {
  id    String @id @default(cuid())
  title String
  body  String

  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt

  user   User   @relation(fields: [userId], references: [id], onDelete: Cascade, onUpdate: Cascade)
  userId String
}


================================================
FILE: examples/complex/prisma/seed.ts
================================================
import { PrismaClient } from "@prisma/client";
import bcrypt from "bcryptjs";

const prisma = new PrismaClient();

async function seed() {
  const email = "rachel@remix.run";

  // cleanup the existing database
  await prisma.user.delete({ where: { email } }).catch(() => {
    // no worries if it doesn't exist yet
  });

  const hashedPassword = await bcrypt.hash("racheliscool", 10);

  const user = await prisma.user.create({
    data: {
      email,
      password: {
        create: {
          hash: hashedPassword,
        },
      },
    },
  });

  await prisma.note.create({
    data: {
      title: "My first note",
      body: "Hello, world!",
      userId: user.id,
    },
  });

  await prisma.note.create({
    data: {
      title: "My second note",
      body: "Hello, world!",
      userId: user.id,
    },
  });

  console.log(`Database has been seeded. 🌱`);
}

seed()
  .catch((e) => {
    console.error(e);
    process.exit(1);
  })
  .finally(async () => {
    await prisma.$disconnect();
  });


================================================
FILE: examples/complex/remix.config.js
================================================
/**
 * @type {import('@remix-run/dev').AppConfig}
 */
module.exports = {
  cacheDirectory: "./node_modules/.cache/remix",
  ignoredRouteFiles: ["**/.*", "**/*.css", "**/*.test.{js,jsx,ts,tsx}"],
  server: './custom-server.js'
};


================================================
FILE: examples/complex/remix.env.d.ts
================================================
/// <reference types="@remix-run/dev" />
/// <reference types="@remix-run/node/globals" />


================================================
FILE: examples/complex/remix.init/gitignore
================================================
node_modules

/build
/public/build
.env

/cypress/screenshots
/cypress/videos
/prisma/data.db
/prisma/data.db-journal

/app/styles/tailwind.css


================================================
FILE: examples/complex/remix.init/index.js
================================================
const { execSync } = require("child_process");
const crypto = require("crypto");
const fs = require("fs/promises");
const path = require("path");

const toml = require("@iarna/toml");
const PackageJson = require("@npmcli/package-json");
const semver = require("semver");
const YAML = require("yaml");

const cleanupCypressFiles = ({ fileEntries, isTypeScript, packageManager }) =>
  fileEntries.flatMap(([filePath, content]) => {
    let newContent = content.replace(
      new RegExp("npx ts-node", "g"),
      isTypeScript ? `${packageManager.exec} ts-node` : "node"
    );

    if (!isTypeScript) {
      newContent = newContent
        .replace(new RegExp("create-user.ts", "g"), "create-user.js")
        .replace(new RegExp("delete-user.ts", "g"), "delete-user.js");
    }

    return [fs.writeFile(filePath, newContent)];
  });

const cleanupDeployWorkflow = (deployWorkflow, deployWorkflowPath) => {
  delete deployWorkflow.jobs.typecheck;
  deployWorkflow.jobs.deploy.needs = deployWorkflow.jobs.deploy.needs.filter(
    (need) => need !== "typecheck"
  );

  return [fs.writeFile(deployWorkflowPath, YAML.stringify(deployWorkflow))];
};

const cleanupVitestConfig = (vitestConfig, vitestConfigPath) => {
  const newVitestConfig = vitestConfig.replace(
    "setup-test-env.ts",
    "setup-test-env.js"
  );

  return [fs.writeFile(vitestConfigPath, newVitestConfig)];
};

const escapeRegExp = (string) =>
  // $& means the whole matched string
  string.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");

const getPackageManagerCommand = (packageManager) =>
  // Inspired by https://github.com/nrwl/nx/blob/bd9b33eaef0393d01f747ea9a2ac5d2ca1fb87c6/packages/nx/src/utils/package-manager.ts#L38-L103
  ({
    npm: () => ({
      exec: "npx",
      lockfile: "package-lock.json",
      run: (script, args) => `npm run ${script} ${args ? `-- ${args}` : ""}`,
    }),
    pnpm: () => {
      const pnpmVersion = getPackageManagerVersion("pnpm");
      const includeDoubleDashBeforeArgs = semver.lt(pnpmVersion, "7.0.0");
      const useExec = semver.gte(pnpmVersion, "6.13.0");

      return {
        exec: useExec ? "pnpm exec" : "pnpx",
        lockfile: "pnpm-lock.yaml",
        run: (script, args) =>
          includeDoubleDashBeforeArgs
            ? `pnpm run ${script} ${args ? `-- ${args}` : ""}`
            : `pnpm run ${script} ${args || ""}`,
      };
    },
    yarn: () => ({
      exec: "yarn",
      lockfile: "yarn.lock",
      run: (script, args) => `yarn ${script} ${args || ""}`,
    }),
  }[packageManager]());

const getPackageManagerVersion = (packageManager) =>
  // Copied over from https://github.com/nrwl/nx/blob/bd9b33eaef0393d01f747ea9a2ac5d2ca1fb87c6/packages/nx/src/utils/package-manager.ts#L105-L114
  execSync(`${packageManager} --version`).toString("utf-8").trim();

const getRandomString = (length) => crypto.randomBytes(length).toString("hex");

const readFileIfNotTypeScript = (
  isTypeScript,
  filePath,
  parseFunction = (result) => result
) =>
  isTypeScript
    ? Promise.resolve()
    : fs.readFile(filePath, "utf-8").then(parseFunction);

const removeUnusedDependencies = (dependencies, unusedDependencies) =>
  Object.fromEntries(
    Object.entries(dependencies).filter(
      ([key]) => !unusedDependencies.includes(key)
    )
  );

const updatePackageJson = ({ APP_NAME, isTypeScript, packageJson }) => {
  const {
    devDependencies,
    prisma: { seed: prismaSeed, ...prisma },
    scripts: { typecheck, validate, ...scripts },
  } = packageJson.content;

  packageJson.update({
    name: APP_NAME,
    devDependencies: isTypeScript
      ? devDependencies
      : removeUnusedDependencies(devDependencies, ["ts-node"]),
    prisma: isTypeScript
      ? { ...prisma, seed: prismaSeed }
      : {
          ...prisma,
          seed: prismaSeed
            .replace("ts-node", "node")
            .replace("seed.ts", "seed.js"),
        },
    scripts: isTypeScript
      ? { ...scripts, typecheck, validate }
      : { ...scripts, validate: validate.replace(" typecheck", "") },
  });
};

const main = async ({ isTypeScript, packageManager, rootDirectory }) => {
  const pm = getPackageManagerCommand(packageManager);
  const FILE_EXTENSION = isTypeScript ? "ts" : "js";

  const README_PATH = path.join(rootDirectory, "README.md");
  const FLY_TOML_PATH = path.join(rootDirectory, "fly.toml");
  const EXAMPLE_ENV_PATH = path.join(rootDirectory, ".env.example");
  const ENV_PATH = path.join(rootDirectory, ".env");
  const DEPLOY_WORKFLOW_PATH = path.join(
    rootDirectory,
    ".github",
    "workflows",
    "deploy.yml"
  );
  const DOCKERFILE_PATH = path.join(rootDirectory, "Dockerfile");
  const CYPRESS_SUPPORT_PATH = path.join(rootDirectory, "cypress", "support");
  const CYPRESS_COMMANDS_PATH = path.join(
    CYPRESS_SUPPORT_PATH,
    `commands.${FILE_EXTENSION}`
  );
  const CREATE_USER_COMMAND_PATH = path.join(
    CYPRESS_SUPPORT_PATH,
    `create-user.${FILE_EXTENSION}`
  );
  const DELETE_USER_COMMAND_PATH = path.join(
    CYPRESS_SUPPORT_PATH,
    `delete-user.${FILE_EXTENSION}`
  );
  const VITEST_CONFIG_PATH = path.join(
    rootDirectory,
    `vitest.config.${FILE_EXTENSION}`
  );

  const REPLACER = "indie-stack-template";

  const DIR_NAME = path.basename(rootDirectory);
  const SUFFIX = getRandomString(2);

  const APP_NAME = (DIR_NAME + "-" + SUFFIX)
    // get rid of anything that's not allowed in an app name
    .replace(/[^a-zA-Z0-9-_]/g, "-");

  const [
    prodContent,
    readme,
    env,
    dockerfile,
    cypressCommands,
    createUserCommand,
    deleteUserCommand,
    deployWorkflow,
    vitestConfig,
    packageJson,
  ] = await Promise.all([
    fs.readFile(FLY_TOML_PATH, "utf-8"),
    fs.readFile(README_PATH, "utf-8"),
    fs.readFile(EXAMPLE_ENV_PATH, "utf-8"),
    fs.readFile(DOCKERFILE_PATH, "utf-8"),
    fs.readFile(CYPRESS_COMMANDS_PATH, "utf-8"),
    fs.readFile(CREATE_USER_COMMAND_PATH, "utf-8"),
    fs.readFile(DELETE_USER_COMMAND_PATH, "utf-8"),
    readFileIfNotTypeScript(isTypeScript, DEPLOY_WORKFLOW_PATH, (s) =>
      YAML.parse(s)
    ),
    readFileIfNotTypeScript(isTypeScript, VITEST_CONFIG_PATH),
    PackageJson.load(rootDirectory),
  ]);

  const newEnv = env.replace(
    /^SESSION_SECRET=.*$/m,
    `SESSION_SECRET="${getRandomString(16)}"`
  );

  const prodToml = toml.parse(prodContent);
  prodToml.app = prodToml.app.replace(REPLACER, APP_NAME);

  const newReadme = readme.replace(
    new RegExp(escapeRegExp(REPLACER), "g"),
    APP_NAME
  );

  const newDockerfile = pm.lockfile
    ? dockerfile.replace(
        new RegExp(escapeRegExp("ADD package.json"), "g"),
        `ADD package.json ${pm.lockfile}`
      )
    : dockerfile;

  updatePackageJson({ APP_NAME, isTypeScript, packageJson });

  const fileOperationPromises = [
    fs.writeFile(FLY_TOML_PATH, toml.stringify(prodToml)),
    fs.writeFile(README_PATH, newReadme),
    fs.writeFile(ENV_PATH, newEnv),
    fs.writeFile(DOCKERFILE_PATH, newDockerfile),
    ...cleanupCypressFiles({
      fileEntries: [
        [CYPRESS_COMMANDS_PATH, cypressCommands],
        [CREATE_USER_COMMAND_PATH, createUserCommand],
        [DELETE_USER_COMMAND_PATH, deleteUserCommand],
      ],
      isTypeScript,
      packageManager: pm,
    }),
    packageJson.save(),
    fs.copyFile(
      path.join(rootDirectory, "remix.init", "gitignore"),
      path.join(rootDirectory, ".gitignore")
    ),
    fs.rm(path.join(rootDirectory, ".github", "ISSUE_TEMPLATE"), {
      recursive: true,
    }),
    fs.rm(path.join(rootDirectory, ".github", "dependabot.yml")),
    fs.rm(path.join(rootDirectory, ".github", "PULL_REQUEST_TEMPLATE.md")),
  ];

  if (!isTypeScript) {
    fileOperationPromises.push(
      ...cleanupDeployWorkflow(deployWorkflow, DEPLOY_WORKFLOW_PATH)
    );

    fileOperationPromises.push(
      ...cleanupVitestConfig(vitestConfig, VITEST_CONFIG_PATH)
    );
  }

  await Promise.all(fileOperationPromises);

  execSync(pm.run("setup"), { cwd: rootDirectory, stdio: "inherit" });

  execSync(pm.run("format", "--loglevel warn"), {
    cwd: rootDirectory,
    stdio: "inherit",
  });

  console.log(
    `Setup is complete. You're now ready to rock and roll 🤘

Start development with \`${pm.run("dev")}\`
    `.trim()
  );
};

module.exports = main;


================================================
FILE: examples/complex/remix.init/package.json
================================================
{
  "name": "remix.init",
  "private": true,
  "main": "index.js",
  "license": "MIT",
  "dependencies": {
    "@iarna/toml": "^2.2.5",
    "@npmcli/package-json": "^2.0.0",
    "semver": "^7.3.7",
    "yaml": "^2.1.1"
  }
}


================================================
FILE: examples/complex/start.sh
================================================
#!/bin/sh

# This file is how Fly starts the server (configured in fly.toml). Before starting
# the server though, we need to run any prisma migrations that haven't yet been
# run, which is why this file exists in the first place.
# Learn more: https://community.fly.io/t/sqlite-not-getting-setup-properly/4386

set -ex
npx prisma migrate deploy
npm run start


================================================
FILE: examples/complex/tailwind.config.js
================================================
/** @type {import('tailwindcss').Config} */
module.exports = {
  content: ["./app/**/*.{ts,tsx,jsx,js}"],
  theme: {
    extend: {},
  },
  plugins: [],
};


================================================
FILE: examples/complex/test/setup-test-env.ts
================================================
import { installGlobals } from "@remix-run/node";
import "@testing-library/jest-dom/extend-expect";

installGlobals();


================================================
FILE: examples/complex/tsconfig.json
================================================
{
  "exclude": ["./cypress", "./cypress.config.ts"],
  "include": ["remix.env.d.ts", "**/*.ts", "**/*.tsx"],
  "compilerOptions": {
    "lib": ["DOM", "DOM.Iterable", "ES2019"],
    "types": ["vitest/globals"],
    "isolatedModules": true,
    "esModuleInterop": true,
    "jsx": "react-jsx",
    "module": "CommonJS",
    "moduleResolution": "node",
    "resolveJsonModule": true,
    "target": "ES2019",
    "strict": true,
    "allowJs": true,
    "forceConsistentCasingInFileNames": true,
    "baseUrl": ".",
    "paths": {
      "~/*": ["./app/*"]
    },
    "skipLibCheck": true,

    // Remix takes care of building everything in `remix build`.
    "noEmit": true
  }
}


================================================
FILE: examples/complex/vitest.config.ts
================================================
/// <reference types="vitest" />
/// <reference types="vite/client" />

import react from "@vitejs/plugin-react";
import { defineConfig } from "vite";
import tsconfigPaths from "vite-tsconfig-paths";

export default defineConfig({
  plugins: [react(), tsconfigPaths()],
  test: {
    globals: true,
    environment: "happy-dom",
    setupFiles: ["./test/setup-test-env.ts"],
  },
});


================================================
FILE: examples/simple/.eslintrc.js
================================================
/** @type {import('eslint').Linter.Config} */
module.exports = {
  extends: ["@remix-run/eslint-config", "@remix-run/eslint-config/node"],
};


================================================
FILE: examples/simple/.gitignore
================================================
node_modules

/.cache
/build
/public/build
.env


================================================
FILE: examples/simple/README.md
================================================
# Welcome to Remix!

- [Remix Docs](https://remix.run/docs)

## Development

From your terminal:

```sh
npm run dev
```

This starts your app in development mode, rebuilding assets on file changes.

## Deployment

First, build your app for production:

```sh
npm run build
```

Then run the app in production mode:

```sh
npm start
```

Now you'll need to pick a host to deploy it to.

### DIY

If you're familiar with deploying node applications, the built-in Remix app server is production-ready.

Make sure to deploy the output of `remix build`

- `build/`
- `public/build/`

### Using a Template

When 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.

```sh
cd ..
# create a new project, and pick a pre-configured host
npx create-remix@latest
cd my-new-remix-app
# remove the new project's app (not the old one!)
rm -rf app
# copy your app over
cp -R ../my-old-remix-app/app app
```


================================================
FILE: examples/simple/app/entry.client.tsx
================================================
import { RemixBrowser } from "@remix-run/react";
import { startTransition, StrictMode } from "react";
import { hydrateRoot } from "react-dom/client";

function hydrate() {
  startTransition(() => {
    hydrateRoot(
      document,
      <StrictMode>
        <RemixBrowser />
      </StrictMode>
    );
  });
}

if (window.requestIdleCallback) {
  window.requestIdleCallback(hydrate);
} else {
  // Safari doesn't support requestIdleCallback
  // https://caniuse.com/requestidlecallback
  window.setTimeout(hydrate, 1);
}


================================================
FILE: examples/simple/app/entry.server.tsx
================================================
import { PassThrough } from "stream";
import type { EntryContext } from "@remix-run/node";
import { Response } from "@remix-run/node";
import { RemixServer } from "@remix-run/react";
import isbot from "isbot";
import { renderToPipeableStream } from "react-dom/server";

const ABORT_DELAY = 5000;

export default function handleRequest(
  request: Request,
  responseStatusCode: number,
  responseHeaders: Headers,
  remixContext: EntryContext
) {
  return isbot(request.headers.get("user-agent"))
    ? handleBotRequest(
        request,
        responseStatusCode,
        responseHeaders,
        remixContext
      )
    : handleBrowserRequest(
        request,
        responseStatusCode,
        responseHeaders,
        remixContext
      );
}

function handleBotRequest(
  request: Request,
  responseStatusCode: number,
  responseHeaders: Headers,
  remixContext: EntryContext
) {
  return new Promise((resolve, reject) => {
    let didError = false;

    const { pipe, abort } = renderToPipeableStream(
      <RemixServer context={remixContext} url={request.url} />,
      {
        onAllReady() {
          const body = new PassThrough();

          responseHeaders.set("Content-Type", "text/html");

          resolve(
            new Response(body, {
              headers: responseHeaders,
              status: didError ? 500 : responseStatusCode,
            })
          );

          pipe(body);
        },
        onShellError(error: unknown) {
          reject(error);
        },
        onError(error: unknown) {
          didError = true;

          console.error(error);
        },
      }
    );

    setTimeout(abort, ABORT_DELAY);
  });
}

function handleBrowserRequest(
  request: Request,
  responseStatusCode: number,
  responseHeaders: Headers,
  remixContext: EntryContext
) {
  return new Promise((resolve, reject) => {
    let didError = false;

    const { pipe, abort } = renderToPipeableStream(
      <RemixServer context={remixContext} url={request.url} />,
      {
        onShellReady() {
          const body = new PassThrough();

          responseHeaders.set("Content-Type", "text/html");

          resolve(
            new Response(body, {
              headers: responseHeaders,
              status: didError ? 500 : responseStatusCode,
            })
          );

          pipe(body);
        },
        onShellError(err: unknown) {
          reject(err);
        },
        onError(error: unknown) {
          didError = true;

          console.error(error);
        },
      }
    );

    setTimeout(abort, ABORT_DELAY);
  });
}


================================================
FILE: examples/simple/app/root.tsx
================================================
import type { MetaFunction } from "@remix-run/node";
import {
  Links,
  LiveReload,
  Meta,
  Outlet,
  Scripts,
  ScrollRestoration,
} from "@remix-run/react";

export const meta: MetaFunction = () => ({
  charset: "utf-8",
  title: "New Remix App",
  viewport: "width=device-width,initial-scale=1",
});

export default function App() {
  return (
    <html lang="en">
      <head>
        <Meta />
        <Links />
      </head>
      <body>
        <Outlet />
        <ScrollRestoration />
        <Scripts />
        <LiveReload />
      </body>
    </html>
  );
}


================================================
FILE: examples/simple/app/routes/index.tsx
================================================
import { useLoaderData } from "@remix-run/react";

export const loader = () => {
  return {
    foo: 'baz'
  }
};

export default function Index() {
  const data = useLoaderData();

  return (
    <div style={{ fontFamily: "system-ui, sans-serif", lineHeight: "1.4" }}>
      <h1>Welcome to Remix</h1>
      <h3>{data.foo}</h3>
      <ul>
        <li>
          <a
            target="_blank"
            href="https://remix.run/tutorials/blog"
            rel="noreferrer"
          >
            15m Quickstart Blog Tutorial
          </a>
        </li>
        <li>
          <a
            target="_blank"
            href="https://remix.run/tutorials/jokes"
            rel="noreferrer"
          >
            Deep Dive Jokes App Tutorial
          </a>
        </li>
        <li>
          <a target="_blank" href="https://remix.run/docs" rel="noreferrer">
            Remix Docs
          </a>
        </li>
      </ul>
    </div>
  );
}


================================================
FILE: examples/simple/package.json
================================================
{
  "private": true,
  "sideEffects": false,
  "scripts": {
    "build": "remix build",
    "dev": "remix dev",
    "start": "remix-serve build"
  },
  "dependencies": {
    "@remix-run/node": "^1.10.0",
    "@remix-run/react": "^1.10.0",
    "@remix-run/serve": "^1.10.0",
    "isbot": "^3.5.4",
    "javascript-time-ago": "2.5.9",
    "react": "^18.2.0",
    "react-dom": "^18.2.0"
  },
  "devDependencies": {
    "@remix-run/dev": "^1.10.0",
    "@remix-run/eslint-config": "^1.10.0",
    "@types/react": "^18.0.15",
    "@types/react-dom": "^18.0.6",
    "eslint": "^8.23.1",
    "typescript": "^4.7.4"
  },
  "engines": {
    "node": ">=14"
  }
}


================================================
FILE: examples/simple/remix.config.js
================================================
/** @type {import('@remix-run/dev').AppConfig} */
module.exports = {
  ignoredRouteFiles: ["**/.*"],
  // appDirectory: "app",
  // assetsBuildDirectory: "public/build",
  // serverBuildPath: "build/index.js",
  // publicPath: "/build/",
};


================================================
FILE: examples/simple/remix.env.d.ts
================================================
/// <reference types="@remix-run/dev" />
/// <reference types="@remix-run/node" />


================================================
FILE: examples/simple/tsconfig.json
================================================
{
  "include": ["remix.env.d.ts", "**/*.ts", "**/*.tsx"],
  "compilerOptions": {
    "lib": ["DOM", "DOM.Iterable", "ES2019"],
    "isolatedModules": true,
    "esModuleInterop": true,
    "jsx": "react-jsx",
    "moduleResolution": "node",
    "resolveJsonModule": true,
    "target": "ES2019",
    "strict": true,
    "allowJs": true,
    "forceConsistentCasingInFileNames": true,
    "baseUrl": ".",
    "paths": {
      "~/*": ["./app/*"]
    },

    // Remix takes care of building everything in `remix build`.
    "noEmit": true
  }
}


================================================
FILE: package.json
================================================
{
  "name": "remix-vite",
  "version": "0.3.1",
  "description": "Static file serving and directory listing",
  "keywords": [
    "remix",
    "remix-vite",
    "server",
    "hmr",
    "vite",
    "react",
    "hot",
    "reload"
  ],
  "repository": "sudomf/remix-vite",
  "license": "MIT",
  "main": "./lib.js",
  "module": "./lib.esm.js",
  "types": "./declarations/src/entries/lib.d.ts",
  "bin": {
    "remix-vite": "./cli.js"
  },
  "files": [
    "declarations/**/*.d.ts",
    "cli.js",
    "lib.js",
    "lib.esm.js"
  ],
  "engines": {
    "node": ">= 14"
  },
  "scripts": {
    "dev": "run-p dev:**",
    "dev:watch": "node tools/build --dev",
    "dev:serve": "cd examples/complex && nodemon ../../cli --host 0.0.0.0 --watch ../../cli.js",
    "start": "node ./build/main.js",
    "build": "node tools/build.js && tsc --emitDeclarationOnly",
    "lint:code": "eslint --fix src/**/*.ts",
    "lint:style": "prettier --write .",
    "lint": "yarn lint:code && yarn lint:style",
    "format": "prettier --write .",
    "prepare": "husky install config/husky && yarn build"
  },
  "dependencies": {
    "@babel/core": "7.20.2",
    "@babel/generator": "7.20.2",
    "@babel/parser": "7.20.2",
    "@babel/traverse": "7.20.1",
    "@babel/types": "7.20.2",
    "@remix-run/dev": "^1.8.2",
    "@remix-run/express": "^1.8.2",
    "@remix-run/server-runtime": "^1.8.2",
    "@vitejs/plugin-react": "2.2.0",
    "args": "5.0.3",
    "cross-fetch": "3.1.5",
    "dotenv": "16.0.3",
    "express": "4.18.2",
    "jsesc": "3.0.2",
    "vite": "3.2.2",
    "vite-tsconfig-paths": "3.5.2"
  },
  "devDependencies": {
    "@types/args": "5.0.0",
    "@types/babel__core": "7.1.19",
    "@types/babel__generator": "7.6.4",
    "@types/babel__traverse": "7.18.2",
    "@types/express": "4.17.14",
    "@types/jsesc": "3.0.1",
    "@vercel/style-guide": "3.0.0",
    "chokidar": "3.5.3",
    "esbuild": "0.15.13",
    "eslint": "8.19.0",
    "husky": "8.0.1",
    "lint-staged": "13.0.3",
    "nodemon": "2.0.20",
    "npm-run-all": "4.1.5",
    "prettier": "2.7.1",
    "react": "18.2.0",
    "react-dom": "18.2.0",
    "typescript": "4.6.4"
  },
  "prettier": "@vercel/style-guide/prettier",
  "eslintConfig": {
    "extends": [
      "./node_modules/@vercel/style-guide/eslint/node.js",
      "./node_modules/@vercel/style-guide/eslint/typescript.js"
    ],
    "parserOptions": {
      "project": "tsconfig.json"
    },
    "rules": {
      "no-await-in-loop": 0,
      "eslint-comments/disable-enable-pair": 0,
      "@typescript-eslint/no-non-null-assertion": 0,
      "@typescript-eslint/no-unsafe-assignment": 0,
      "@typescript-eslint/no-unsafe-call": 0,
      "@typescript-eslint/no-unsafe-member-access": 0,
      "@typescript-eslint/no-misused-promises": 0
    }
  },
  "lint-staged": {
    "*.{json,css,scss,md,html,yml,yaml}": [
      "prettier --write"
    ],
    ".{js,ts}": [
      "eslint --fix"
    ]
  }
}


================================================
FILE: src/constants.ts
================================================
export const SERVER_ENTRY_ID = 'server-entry';
export const SERVER_ASSETS_MANIFEST_ID = 'server-assets-manifest';
export const BROWSER_ASSETS_MANIFEST_ID = 'browser-assets-manifest';


================================================
FILE: src/entries/cli.ts
================================================
#!/usr/bin/env node
/* eslint-disable no-console */
/* eslint-disable @typescript-eslint/no-explicit-any */
import express from 'express';
import { config } from 'dotenv';
import { createRequestHandler } from '@remix-run/express';
import args from 'args';
import { createRemixViteDevServer, getRemixViteBuild } from '../vite';

config();

args
  .option('host', 'The host on which the app will be running', 'localhost')
  .option('port', 'The port on which the app will be running', 3000);

const flags = args.parse(process.argv) as {
  host: string;
  port: number;
};

const mode = 'development';

async function createServer() {
  const app = express();

  app.use(express.static('public', { maxAge: '1h' }));

  const viteDevServer = await createRemixViteDevServer();

  // use vite's connect instance as middleware
  // if you use your own express router (express.Router()), you should use router.use
  app.use(viteDevServer.middlewares);

  app.all('*', async (req, res, next) => {
    try {
      purgeRequireCache();

      const build = await getRemixViteBuild(viteDevServer);

      const handler = createRequestHandler({
        build,
        mode,
      });

      return handler(req, res, next);
    } catch (e: any) {
      // If an error is caught, let Vite fix the stack trace so it maps back to
      // your actual source code.
      viteDevServer.ssrFixStacktrace(e as Error);
      next(e);
    }
  });

  app.listen(flags.port, flags.host, () => {
    console.log(`🖲 remix-vite started at http://${flags.host}:${flags.port}`);
  });
}

createServer().catch((e) => {
  console.error(e);
  process.exit(1);
});

function purgeRequireCache() {
  // purge require cache on requests for "server side HMR" this won't let
  // you have in-memory objects between requests in development.
  for (const key in require.cache) {
    delete require.cache[key];
  }
}


================================================
FILE: src/entries/lib.ts
================================================
export { getRemixViteBuild, createRemixViteDevServer } from '../vite';


================================================
FILE: src/plugins/hmr-fix.ts
================================================
import { getRouteByFilePath } from '../utils/general';
import { fixHmrCode } from '../utils/code';
import type { Plugin } from 'vite';

export const getHmrFixPlugin = (): Plugin => {
  return {
    name: 'remix-plugin-hmr-fix',
    enforce: 'post',
    async transform(code, id) {
      const route = await getRouteByFilePath(id);

      if (
        route &&
        id.endsWith('.tsx') &&
        code.includes('if (import.meta.hot) {') &&
        code.includes('window.$RefreshReg$ = prevRefreshReg;')
      ) {
        return fixHmrCode(id, code);
      }
    },
  };
};


================================================
FILE: src/plugins/inject.ts
================================================
import { getRouteByFilePath } from '../utils/general';
import type { Plugin } from 'vite';

export const getInjectPlugin = (): Plugin => {
  return {
    name: 'vite-plugin-remix-inject',
    enforce: 'pre',

    async transform(code, id) {
      const route = await getRouteByFilePath(id);

      if (!route) return;

      if (route.id === 'root') {
        return patchRoot(code);
      }
    },
  };
};

const patchRoot = (code: string) => {
  return code
    .replace(/<LiveReload.*?\/>/, '')
    .replace(/<Scripts.*\/>/, viteScripts);
};

const viteScripts = `
<script type="module" src="/@vite/client" />
<script
  type="module"
  dangerouslySetInnerHTML={{
    __html: \`import RefreshRuntime from '/@react-refresh'
  RefreshRuntime.injectIntoGlobalHook(window)
  window.$RefreshReg$ = () => {}
  window.$RefreshSig$ = () => (type) => type
  window.__vite_plugin_react_preamble_installed__ = true\`,
  }}
/>
<Scripts />
`;


================================================
FILE: src/plugins/remix.ts
================================================
/* eslint-disable @typescript-eslint/no-unnecessary-condition */
/* eslint-disable @typescript-eslint/no-explicit-any */

import jsesc from 'jsesc';
import {
  createVirtualModule,
  getRemixConfig,
  getRemixRouteModuleExports,
  getVirtualModuleUrl,
  resolveAppRelativeFilePath,
  resolveFSPath,
  resolveRelativeRouteFilePath,
} from '../utils/general';
import {
  BROWSER_ASSETS_MANIFEST_ID,
  SERVER_ASSETS_MANIFEST_ID,
  SERVER_ENTRY_ID,
} from '../constants';
import type { RemixConfig } from '@remix-run/dev/dist/config';
import type { Plugin } from 'vite';

export const getRemixPlugin = async (): Promise<Plugin> => {
  const config = await getRemixConfig();
  const manifest = await getAssetManifest(config);
  const serverEntryJs = getServerEntry(config);

  const serverEntryVirtualModule = createVirtualModule(
    SERVER_ENTRY_ID,
    serverEntryJs,
  );
  const serverManifestVirtualModule = createVirtualModule(
    SERVER_ASSETS_MANIFEST_ID,
    `export default ${jsesc(manifest, { es6: true })};`,
  );
  const browserManifestVirtualModule = createVirtualModule(
    BROWSER_ASSETS_MANIFEST_ID,
    `window.__remixManifest=${jsesc(manifest, { es6: true })};`,
  );

  const virtualModules = [
    serverEntryVirtualModule,
    serverManifestVirtualModule,
    browserManifestVirtualModule,
  ];

  return {
    name: 'vite-plugin-remix',
    enforce: 'pre',
    resolveId(id) {
      for (const virtualModule of virtualModules) {
        if (id === virtualModule.virtualModuleId) {
          return virtualModule.resolvedVirtualModuleId;
        }
      }
    },
    load(id) {
      for (const virtualModule of virtualModules) {
        if (id === virtualModule.resolvedVirtualModuleId) {
          return virtualModule.code;
        }
      }
    },
  };
};

const getServerEntry = (config: RemixConfig) => {
  return `
  import * as entryServer from ${JSON.stringify(
    resolveFSPath(resolveAppRelativeFilePath(config.entryServerFile, config)),
  )};
  ${Object.keys(config.routes)
    .map((key, index) => {
      const route = config.routes[key]!;
      return `import * as route${index} from ${JSON.stringify(
        resolveFSPath(resolveRelativeRouteFilePath(route, config)),
      )};`;
    })
    .join('\n')}
    export { default as assets } from ${JSON.stringify(
      'virtual:server-assets-manifest',
    )};
    export const assetsBuildDirectory = ${JSON.stringify(
      config.relativeAssetsBuildDirectory,
    )};
    ${
      config.future
        ? `export const future = ${JSON.stringify(config.future)}`
        : ''
    };
    export const publicPath = ${JSON.stringify(config.publicPath)};
    export const entry = { module: entryServer };
    export const routes = {
      ${Object.keys(config.routes)
        .map((key, index) => {
          const route = config.routes[key]!;
          return `${JSON.stringify(key)}: {
        id: ${JSON.stringify(route.id)},
        parentId: ${JSON.stringify(route.parentId)},
        path: ${JSON.stringify(route.path)},
        index: ${JSON.stringify(route.index)},
        caseSensitive: ${JSON.stringify(route.caseSensitive)},
        module: route${index}
      }`;
        })
        .join(',\n  ')}
    };`;
};

const getAssetManifest = async (config: RemixConfig) => {
  const routes: Record<string, any> = {};

  for (const entry of Object.entries(config.routes)) {
    const [key, route] = entry;
    const sourceExports = await getRemixRouteModuleExports(route.id);

    routes[key] = {
      id: route.id,
      parentId: route.parentId,
      path: route.path,
      index: route.index,
      caseSensitive: route.caseSensitive,
      module: resolveFSPath(resolveRelativeRouteFilePath(route, config)),
      hasAction: sourceExports.includes('action'),
      hasLoader: sourceExports.includes('loader'),
      hasCatchBoundary: sourceExports.includes('CatchBoundary'),
      hasErrorBoundary: sourceExports.includes('ErrorBoundary'),
      imports: [],
    };
  }

  return {
    url: getVirtualModuleUrl(BROWSER_ASSETS_MANIFEST_ID),
    version: Math.random(),
    entry: {
      module: resolveFSPath(
        resolveAppRelativeFilePath(config.entryClientFile, config),
      ),
      imports: [],
    },
    routes,
  };
};


================================================
FILE: src/plugins/transform.ts
================================================
import {
  getRemixRouteModuleExports,
  getRouteByFilePath,
} from '../utils/general';
import { filterExports } from '../utils/code';
import type { Plugin } from 'vite';

const BACKEND_ONLY_EXPORTS = ['loader', 'action'];

export const getTransformPlugin = (): Plugin => {
  return {
    name: 'vite-plugin-remix-transform',
    enforce: 'pre',

    // Skip process CSS as Remix doesn't pre-process CSS.
    async resolveId(id) {
      if (id.endsWith('.css') && !id.includes('?')) {
        const target = await this.resolve(`${id}?url`);
        return target;
      }
    },

    async transform(code, id, options) {
      // If it's SSR code, let's bypass it.
      if (options?.ssr) return;

      // If it's .server.<ext>, let's bypass it.
      if (id.includes('.server.')) return 'export default {}';

      const route = await getRouteByFilePath(id);

      if (!route) return;

      const theExports = await getRemixRouteModuleExports(route.id);

      // If it's a route with no default component export, let's bypass it.
      if (!theExports.includes('default')) return;

      const frontendExports = theExports.filter(
        (e) => !BACKEND_ONLY_EXPORTS.includes(e),
      );

      if (!frontendExports.length) return code;

      const filtered = filterExports(id, code, frontendExports);
      const result = filtered.code;

      return {
        code: result,
        map: null,
      };
    },
  };
};


================================================
FILE: src/utils/code.ts
================================================
import { parse } from '@babel/parser';
import generate from '@babel/generator';
import traverse from '@babel/traverse';
import * as t from '@babel/types';
import type { NodePath } from '@babel/traverse';
import type { types as BabelTypes } from '@babel/core';

export const filterExports = (
  _id: string,
  source: string,
  exports: string[],
) => {
  const document = parse(source, {
    sourceType: 'module',
    plugins: ['typescript', 'jsx'],
  });

  traverse(document, {
    ExportNamedDeclaration: (path) => {
      removeExports(path, exports);
    },
  });

  return {
    code: generate(document).code,
    map: null,
  };
};

function removeExports(
  path: NodePath<BabelTypes.ExportNamedDeclaration>,
  exports: string[],
) {
  const shouldRemoveExport = (exportName: string) =>
    !exports.includes(exportName);

  const specifiers = path.get(
    'specifiers',
  ) as NodePath<BabelTypes.ExportSpecifier>[];

  if (specifiers.length) {
    specifiers.forEach((specifier) => {
      const name = t.isIdentifier(specifier.node.exported)
        ? specifier.node.exported.name
        : specifier.node.exported.value;
      if (shouldRemoveExport(name)) {
        specifier.remove();
      }
    });

    if (path.node.specifiers.length < 1) {
      path.remove();
    }
    return;
  }

  const declaration = path.get('declaration') as NodePath<
    BabelTypes.FunctionDeclaration | BabelTypes.VariableDeclaration
  >;
  // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
  if (!declaration.node) {
    return;
  }

  switch (declaration.node.type) {
    case 'FunctionDeclaration': {
      const name = declaration.node.id!.name;
      if (shouldRemoveExport(name)) {
        path.remove();
      }
      break;
    }
    case 'VariableDeclaration': {
      const inner = declaration.get(
        'declarations',
      ) as NodePath<BabelTypes.VariableDeclarator>[];
      inner.forEach((d) => {
        if (d.node.id.type !== 'Identifier') {
          return;
        }
        const name = d.node.id.name;
        if (shouldRemoveExport(name)) {
          d.remove();
        }
      });
      break;
    }
    default: {
      break;
    }
  }
}

/* The original injected code from react-refresh checks if all exports are likely
 components. AS we can have more than just components in a route file, we will
 replace the injected code with a patched version that only checks id it has at least
 one component.
*/
export const fixHmrCode = (_id: string, source: string) => {
  const document = parse(source, {
    sourceType: 'module',
  });

  const newAst = parse(hmrCodePatch, {
    sourceType: 'module',
  }).program.body;

  traverse(document, {
    IfStatement(path) {
      const { node } = path;
      const code = generate(node).code;

      if (
        code.includes('import.meta.hot') &&
        code.includes('window.$RefreshReg$ = prevRefreshReg;')
      ) {
        const consequent = node.consequent as BabelTypes.BlockStatement;
        consequent.body = newAst;
      }
    },
  });

  return {
    code: generate(document).code,
  };
};

const hmrCodePatch = `
  let isReactRefreshBoundary = function(mod) {
    if (mod == null || typeof mod !== "object") {
      return false;
    }
    let hasExports = false;
    let hasAtLeastOneComponent = true;
    for (const exportName in mod) {
      hasExports = true;
      if (exportName === "__esModule") {
        continue;
      }
      const desc = Object.getOwnPropertyDescriptor(mod, exportName);
      if (desc && desc.get) {
        return false;
      }
      const exportValue = mod[exportName];
      if (RefreshRuntime.isLikelyComponentType(exportValue)) {
        hasAtLeastOneComponent = true;
      }
    }
    return hasExports && hasAtLeastOneComponent;
  };
  window.$RefreshReg$ = prevRefreshReg;
  window.$RefreshSig$ = prevRefreshSig;
  import.meta.hot.accept((mod) => {
    if (isReactRefreshBoundary(mod)) {
      if (!window.__vite_plugin_react_timeout) {
        window.__vite_plugin_react_timeout = setTimeout(() => {
          window.__vite_plugin_react_timeout = 0;
          RefreshRuntime.performReactRefresh();
        }, 30);
      }
    } else {
      import.meta.hot.invalidate();
    }
  });`;


================================================
FILE: src/utils/general.ts
================================================
import path from 'path';
import { readConfig } from '@remix-run/dev/dist/config';
import { normalizePath as viteNormalizePath } from 'vite';
import { getRouteModuleExports } from '@remix-run/dev/dist/compiler/routeExports';
import type { RemixConfig } from '@remix-run/dev/dist/config';

type Route = RemixConfig['routes'][string];

export const getRemixConfig = async () => {
  const config = await readConfig();
  return config;
};

export const getRemixRouteModuleExports = async (routeId: string) => {
  const config = await getRemixConfig();
  return getRouteModuleExports(config, routeId);
};

export const getVirtualModuleUrl = (id: string) => `/@id/__x00__virtual:${id}`;

export const normalizePath = (p: string) => {
  return viteNormalizePath(toUnixPath(p));
};

export const createVirtualModule = (name: string, code: string) => {
  const virtualModuleId = `virtual:${name}`;
  const resolvedVirtualModuleId = `\0${virtualModuleId}`;

  return {
    virtualModuleId,
    resolvedVirtualModuleId,
    code,
  };
};

export const getAppDirName = (config: RemixConfig) => {
  return path.relative(process.cwd(), config.appDirectory);
};

export const resolveAppRelativeFilePath = (
  file: string,
  config: RemixConfig,
) => {
  const appDir = getAppDirName(config);
  return path.resolve(process.cwd(), appDir, file);
};

export const resolveRelativeRouteFilePath = (
  route: Route,
  config: RemixConfig,
) => {
  const file = route.file;
  const fullPath = resolveAppRelativeFilePath(file, config);

  return normalizePath(fullPath);
};

export const resolveFSPath = (filePath: string) => {
  return `/@fs${normalizePath(filePath)}`;
};

export const getRouteByFilePath = async (filePath: string) => {
  const routesByFile = await getRoutesByFile();
  return routesByFile.get(normalizePath(filePath));
};

export const getRoutesByFile = async () => {
  const config = await getRemixConfig();

  const routesByFile: Map<string, Route> = Object.keys(config.routes).reduce(
    (map, key) => {
      const route = config.routes[key]!;
      const file = resolveRelativeRouteFilePath(route, config);
      map.set(file, route);
      return map;
    },
    new Map<string, Route>(),
  );

  return routesByFile;
};

const toUnixPath = (p: string) =>
  // eslint-disable-next-line prefer-named-capture-group
  p.replace(/[\\/]+/g, '/').replace(/^([a-zA-Z]+:|\.\/)/, '');


================================================
FILE: src/utils/version.ts
================================================
/* eslint-disable no-empty */
import fetch from 'cross-fetch';
import pkg from '../../package.json';

export const checkVersion = async () => {
  try {
    const res = await fetch(`https://registry.npmjs.org/${pkg.name}`);
    const data = await res.json();
    const latestVersion = data['dist-tags'].latest as string;

    if (latestVersion !== pkg.version) {
      // eslint-disable-next-line no-console
      console.warn(
        '\x1b[33m%s\x1b[0m', // yellow
        `
Your version of ${pkg.name} is out of date.
Latest version is ${latestVersion}, but you have ${pkg.version}.

Please upgrade by running:

npm install -D ${pkg.name}@latest 
  or 
yarn add -D ${pkg.name}@latest

`,
      );
    }
  } catch {}
};


================================================
FILE: src/vite.ts
================================================
import { createServer, mergeConfig } from 'vite';
import vitePluginReact from '@vitejs/plugin-react';
import tsconfigPaths from 'vite-tsconfig-paths';
import { getHmrFixPlugin } from './plugins/hmr-fix';
import { getInjectPlugin } from './plugins/inject';
import { getRemixPlugin } from './plugins/remix';
import { getTransformPlugin } from './plugins/transform';
import { SERVER_ENTRY_ID } from './constants';
import { checkVersion } from './utils/version';
import type { ViteDevServer, UserConfig } from 'vite';
import type { ServerBuild } from '@remix-run/server-runtime';

/**
 * Get Remix build
 */
export const getRemixViteBuild = async (viteDevServer: ViteDevServer) => {
  const build = await viteDevServer.ssrLoadModule(`virtual:${SERVER_ENTRY_ID}`);

  return build as ServerBuild;
};

/**
 * Create remix-vite dev server
 */
export const createRemixViteDevServer = async (config?: UserConfig) => {
  await checkVersion();

  const remixPlugin = await getRemixPlugin();
  const remixInject = getInjectPlugin();
  const remixTransformPlugin = getTransformPlugin();
  const remixHmrFix = getHmrFixPlugin();

  // Create Vite server in middleware mode and configure the app type as
  // 'custom', disabling Vite's own HTML serving logic so parent server
  // can take control
  return createServer(
    mergeConfig(
      {
        server: {
          fs: {
            strict: false,
          },
          cors: true,
          middlewareMode: true,
        },
        plugins: [
          tsconfigPaths(),
          remixInject,
          remixPlugin,
          remixTransformPlugin,
          vitePluginReact(),
          remixHmrFix,
        ],
        appType: 'custom',
      } as UserConfig,
      config || {},
    ),
  );
};


================================================
FILE: tools/build.js
================================================
/* eslint-disable no-console */
const esbuild = require('esbuild');
const chokidar = require('chokidar');
const pkg = require('../package.json');

/**
 * @type {import('esbuild').BuildOptions}
 */
const options = {
  platform: 'node',
  target: 'node14',
  format: 'cjs',
  write: true,
  bundle: true,
  external: Object.keys(pkg.dependencies),
  watch: false,
};

const build = async () => {
  await Promise.all([
    esbuild.build({
      ...options,
      entryPoints: ['src/entries/lib.ts', 'src/entries/cli.ts'],
      outdir: './',
      assetNames: '[name].[ext]',
    }),
    esbuild.build({
      ...options,
      entryPoints: ['src/entries/lib.ts'],
      format: 'esm',
      outfile: './lib.esm.js',
    }),
  ]);
};

build()
  .then(() => {
    if (process.argv.includes('--dev')) {
      console.log('Watching for changes...');

      const watcher = chokidar.watch('./src/**/*.ts', {
        persistent: true,
      });

      watcher.on('change', async () => {
        console.log('Rebuilding...');
        await build();
      });
    }
  })
  .catch((e) => {
    console.error(e);
    process.exit(1);
  });


================================================
FILE: tsconfig.json
================================================
{
  "extends": "@vercel/style-guide/typescript",
  "compilerOptions": {
    "lib": ["es2020"],
    "target": "ES2016",
    "module": "CommonJS",
    "moduleResolution": "node",
    "resolveJsonModule": true,
    "declaration": true,
    "declarationDir": "./declarations"
  },
  "include": ["src/"]
}
Download .txt
gitextract_mjw2yo5y/

├── .eslintignore
├── .gitattributes
├── .github/
│   ├── ISSUE_TEMPLATE/
│   │   ├── bug_report.yaml
│   │   └── feature_request.yaml
│   ├── PULL_REQUEST_TEMPLATE/
│   │   └── default.md
│   └── workflows/
│       └── publish.yml
├── .gitignore
├── .npmrc
├── .prettierignore
├── LICENSE
├── README.md
├── config/
│   └── husky/
│       └── pre-commit
├── examples/
│   ├── complex/
│   │   ├── .dockerignore
│   │   ├── .eslintrc.js
│   │   ├── .gitignore
│   │   ├── .gitpod.Dockerfile
│   │   ├── .gitpod.yml
│   │   ├── .npmrc
│   │   ├── .prettierignore
│   │   ├── Dockerfile
│   │   ├── README.md
│   │   ├── app/
│   │   │   ├── db.server.ts
│   │   │   ├── entry.client.tsx
│   │   │   ├── entry.server.tsx
│   │   │   ├── models/
│   │   │   │   ├── note.server.ts
│   │   │   │   └── user.server.ts
│   │   │   ├── root.tsx
│   │   │   ├── routes/
│   │   │   │   ├── healthcheck.tsx
│   │   │   │   ├── index.tsx
│   │   │   │   ├── join.tsx
│   │   │   │   ├── login.tsx
│   │   │   │   ├── logout.tsx
│   │   │   │   ├── notes/
│   │   │   │   │   ├── $noteId.tsx
│   │   │   │   │   ├── index.tsx
│   │   │   │   │   └── new.tsx
│   │   │   │   └── notes.tsx
│   │   │   ├── session.server.ts
│   │   │   ├── utils.test.ts
│   │   │   └── utils.ts
│   │   ├── custom-server.js
│   │   ├── cypress/
│   │   │   ├── .eslintrc.js
│   │   │   ├── e2e/
│   │   │   │   └── smoke.cy.ts
│   │   │   ├── fixtures/
│   │   │   │   └── example.json
│   │   │   ├── support/
│   │   │   │   ├── commands.ts
│   │   │   │   ├── create-user.ts
│   │   │   │   ├── delete-user.ts
│   │   │   │   └── e2e.ts
│   │   │   └── tsconfig.json
│   │   ├── cypress.config.ts
│   │   ├── fly.toml
│   │   ├── mocks/
│   │   │   ├── README.md
│   │   │   └── index.js
│   │   ├── package.json
│   │   ├── prisma/
│   │   │   ├── migrations/
│   │   │   │   ├── 20220713162558_init/
│   │   │   │   │   └── migration.sql
│   │   │   │   └── migration_lock.toml
│   │   │   ├── schema.prisma
│   │   │   └── seed.ts
│   │   ├── remix.config.js
│   │   ├── remix.env.d.ts
│   │   ├── remix.init/
│   │   │   ├── gitignore
│   │   │   ├── index.js
│   │   │   └── package.json
│   │   ├── start.sh
│   │   ├── tailwind.config.js
│   │   ├── test/
│   │   │   └── setup-test-env.ts
│   │   ├── tsconfig.json
│   │   └── vitest.config.ts
│   └── simple/
│       ├── .eslintrc.js
│       ├── .gitignore
│       ├── README.md
│       ├── app/
│       │   ├── entry.client.tsx
│       │   ├── entry.server.tsx
│       │   ├── root.tsx
│       │   └── routes/
│       │       └── index.tsx
│       ├── package.json
│       ├── remix.config.js
│       ├── remix.env.d.ts
│       └── tsconfig.json
├── package.json
├── src/
│   ├── constants.ts
│   ├── entries/
│   │   ├── cli.ts
│   │   └── lib.ts
│   ├── plugins/
│   │   ├── hmr-fix.ts
│   │   ├── inject.ts
│   │   ├── remix.ts
│   │   └── transform.ts
│   ├── utils/
│   │   ├── code.ts
│   │   ├── general.ts
│   │   └── version.ts
│   └── vite.ts
├── tools/
│   └── build.js
└── tsconfig.json
Download .txt
SYMBOL INDEX (84 symbols across 33 files)

FILE: examples/complex/app/entry.server.tsx
  constant ABORT_DELAY (line 8) | const ABORT_DELAY = 5000;
  function handleRequest (line 10) | function handleRequest(

FILE: examples/complex/app/models/note.server.ts
  function getNote (line 7) | function getNote({
  function getNoteListItems (line 19) | function getNoteListItems({ userId }: { userId: User["id"] }) {
  function createNote (line 27) | function createNote({
  function deleteNote (line 47) | function deleteNote({

FILE: examples/complex/app/models/user.server.ts
  function getUserById (line 8) | async function getUserById(id: User["id"]) {
  function getUserByEmail (line 12) | async function getUserByEmail(email: User["email"]) {
  function createUser (line 16) | async function createUser(email: User["email"], password: string) {
  function deleteUserByEmail (line 31) | async function deleteUserByEmail(email: User["email"]) {
  function verifyLogin (line 35) | async function verifyLogin(

FILE: examples/complex/app/root.tsx
  function loader (line 18) | async function loader({ request }: LoaderArgs) {
  function App (line 24) | function App() {

FILE: examples/complex/app/routes/healthcheck.tsx
  function loader (line 6) | async function loader({ request }: LoaderArgs) {

FILE: examples/complex/app/routes/index.tsx
  function Index (line 5) | function Index() {

FILE: examples/complex/app/routes/join.tsx
  function loader (line 11) | async function loader({ request }: LoaderArgs) {
  function action (line 17) | async function action({ request }: ActionArgs) {
  function Join (line 73) | function Join() {

FILE: examples/complex/app/routes/login.tsx
  function loader (line 10) | async function loader({ request }: LoaderArgs) {
  function action (line 16) | async function action({ request }: ActionArgs) {
  function LoginPage (line 67) | function LoginPage() {

FILE: examples/complex/app/routes/logout.tsx
  function action (line 6) | async function action({ request }: ActionArgs) {
  function loader (line 10) | async function loader() {

FILE: examples/complex/app/routes/notes.tsx
  function loader (line 9) | async function loader({ request }: LoaderArgs) {
  function NotesPage (line 15) | function NotesPage() {

FILE: examples/complex/app/routes/notes/$noteId.tsx
  function loader (line 9) | async function loader({ request, params }: LoaderArgs) {
  function action (line 20) | async function action({ request, params }: ActionArgs) {
  function NoteDetailsPage (line 29) | function NoteDetailsPage() {
  function ErrorBoundary (line 49) | function ErrorBoundary({ error }: { error: Error }) {
  function CatchBoundary (line 55) | function CatchBoundary() {

FILE: examples/complex/app/routes/notes/index.tsx
  function NoteIndexPage (line 3) | function NoteIndexPage() {

FILE: examples/complex/app/routes/notes/new.tsx
  function action (line 9) | async function action({ request }: ActionArgs) {
  function NewNotePage (line 35) | function NewNotePage() {

FILE: examples/complex/app/session.server.ts
  constant USER_SESSION_KEY (line 20) | const USER_SESSION_KEY = "userId";
  function getSession (line 22) | async function getSession(request: Request) {
  function getUserId (line 27) | async function getUserId(
  function getUser (line 35) | async function getUser(request: Request) {
  function requireUserId (line 45) | async function requireUserId(
  function requireUser (line 57) | async function requireUser(request: Request) {
  function createUserSession (line 66) | async function createUserSession({
  function logout (line 90) | async function logout(request: Request) {

FILE: examples/complex/app/utils.ts
  constant DEFAULT_REDIRECT (line 6) | const DEFAULT_REDIRECT = "/";
  function safeRedirect (line 15) | function safeRedirect(
  function useMatchesData (line 36) | function useMatchesData(
  function isUser (line 47) | function isUser(user: any): user is User {
  function useOptionalUser (line 51) | function useOptionalUser(): User | undefined {
  function useUser (line 59) | function useUser(): User {
  function validateEmail (line 69) | function validateEmail(email: unknown): email is string {

FILE: examples/complex/cypress/support/commands.ts
  type Chainable (line 5) | interface Chainable {
  function login (line 45) | function login({
  function cleanupUser (line 62) | function cleanupUser({ email }: { email?: string } = {}) {
  function deleteUserByEmail (line 76) | function deleteUserByEmail(email: string) {
  function visitAndCheck (line 88) | function visitAndCheck(url: string, waitTime: number = 1000) {

FILE: examples/complex/cypress/support/create-user.ts
  function createAndLogin (line 15) | async function createAndLogin(email: string) {

FILE: examples/complex/cypress/support/delete-user.ts
  function deleteUser (line 13) | async function deleteUser(email: string) {

FILE: examples/complex/prisma/migrations/20220713162558_init/migration.sql
  type "User" (line 2) | CREATE TABLE "User" (
  type "Password" (line 10) | CREATE TABLE "Password" (
  type "Note" (line 17) | CREATE TABLE "Note" (
  type "User" (line 28) | CREATE UNIQUE INDEX "User_email_key" ON "User"("email")
  type "Password" (line 31) | CREATE UNIQUE INDEX "Password_userId_key" ON "Password"("userId")

FILE: examples/complex/prisma/seed.ts
  function seed (line 6) | async function seed() {

FILE: examples/complex/remix.init/index.js
  constant YAML (line 9) | const YAML = require("yaml");

FILE: examples/simple/app/entry.client.tsx
  function hydrate (line 5) | function hydrate() {

FILE: examples/simple/app/entry.server.tsx
  constant ABORT_DELAY (line 8) | const ABORT_DELAY = 5000;
  function handleRequest (line 10) | function handleRequest(
  function handleBotRequest (line 31) | function handleBotRequest(
  function handleBrowserRequest (line 72) | function handleBrowserRequest(

FILE: examples/simple/app/root.tsx
  function App (line 17) | function App() {

FILE: examples/simple/app/routes/index.tsx
  function Index (line 9) | function Index() {

FILE: src/constants.ts
  constant SERVER_ENTRY_ID (line 1) | const SERVER_ENTRY_ID = 'server-entry';
  constant SERVER_ASSETS_MANIFEST_ID (line 2) | const SERVER_ASSETS_MANIFEST_ID = 'server-assets-manifest';
  constant BROWSER_ASSETS_MANIFEST_ID (line 3) | const BROWSER_ASSETS_MANIFEST_ID = 'browser-assets-manifest';

FILE: src/entries/cli.ts
  function createServer (line 23) | async function createServer() {
  function purgeRequireCache (line 64) | function purgeRequireCache() {

FILE: src/plugins/hmr-fix.ts
  method transform (line 9) | async transform(code, id) {

FILE: src/plugins/inject.ts
  method transform (line 9) | async transform(code, id) {

FILE: src/plugins/remix.ts
  method resolveId (line 49) | resolveId(id) {
  method load (line 56) | load(id) {

FILE: src/plugins/transform.ts
  constant BACKEND_ONLY_EXPORTS (line 8) | const BACKEND_ONLY_EXPORTS = ['loader', 'action'];
  method resolveId (line 16) | async resolveId(id) {
  method transform (line 23) | async transform(code, id, options) {

FILE: src/utils/code.ts
  function removeExports (line 30) | function removeExports(
  method IfStatement (line 109) | IfStatement(path) {

FILE: src/utils/general.ts
  type Route (line 7) | type Route = RemixConfig['routes'][string];
Condensed preview — 92 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (125K chars).
[
  {
    "path": ".eslintignore",
    "chars": 72,
    "preview": "/node_modules/**\n/declarations\n/examples/**\n/cli.js\n/lib.js\n/lib.esm.js\n"
  },
  {
    "path": ".gitattributes",
    "chars": 75,
    "preview": "# .gitattributes\n# Makes sure all line endings are LF.\n\n*\ttext=auto eol=lf\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/bug_report.yaml",
    "chars": 799,
    "preview": "name: Report a bug\ndescription: ———\nlabels: [bug]\nbody:\n  - type: markdown\n    attributes:\n      value: |\n        # Than"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/feature_request.yaml",
    "chars": 800,
    "preview": "name: Suggest an improvement or new feature\ndescription: ———\nlabels: [enhancement]\nbody:\n  - type: markdown\n    attribut"
  },
  {
    "path": ".github/PULL_REQUEST_TEMPLATE/default.md",
    "chars": 1249,
    "preview": "<!--\n\tHi there! Thanks for contributing! Please fill in this template to help us\n\treview and merge the PR as quickly and"
  },
  {
    "path": ".github/workflows/publish.yml",
    "chars": 803,
    "preview": "name: Publish to NPM\non:\n  release:\n    types: [created]\njobs:\n  build:\n    runs-on: ubuntu-latest\n    steps:\n      - na"
  },
  {
    "path": ".gitignore",
    "chars": 197,
    "preview": "# .gitignore\n# A list of files and folders that should not be tracked by Git.\n\nnode_modules/\ncoverage/\nbuild/\n.cache/\n.i"
  },
  {
    "path": ".npmrc",
    "chars": 226,
    "preview": "# .npmrc\n# Configuration for pnpm.\n\n# Uses the exact version instead of any within-patch-range version of an\n# installed"
  },
  {
    "path": ".prettierignore",
    "chars": 69,
    "preview": "node_modules/**\ndeclarations/**\nexamples/**\ncli.js\nlib.js\nlib.esm.js\n"
  },
  {
    "path": "LICENSE",
    "chars": 1062,
    "preview": "MIT License\n\nCopyright (c) 2022 Mayke\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof t"
  },
  {
    "path": "README.md",
    "chars": 2712,
    "preview": "# remix-vite\n\n<div>\n  <br />\n  <br />\n  <img width=\"320\" src=\"https://user-images.githubusercontent.com/2893850/20066658"
  },
  {
    "path": "config/husky/pre-commit",
    "chars": 142,
    "preview": "#!/bin/sh\n\n# config/husky/pre-commit\n# Run `lint-staged` before every commit.\n\n. \"$(dirname \"$0\")/_/husky.sh\"\n\nFORCE_COL"
  },
  {
    "path": "examples/complex/.dockerignore",
    "chars": 64,
    "preview": "/node_modules\n*.log\n.DS_Store\n.env\n/.cache\n/public/build\n/build\n"
  },
  {
    "path": "examples/complex/.eslintrc.js",
    "chars": 525,
    "preview": "/** @type {import('@types/eslint').Linter.BaseConfig} */\nmodule.exports = {\n  extends: [\n    \"@remix-run/eslint-config\","
  },
  {
    "path": "examples/complex/.gitignore",
    "chars": 331,
    "preview": "# We don't want lockfiles in stacks, as people could use a different package manager\n# This part will be removed by `rem"
  },
  {
    "path": "examples/complex/.gitpod.Dockerfile",
    "chars": 203,
    "preview": "FROM gitpod/workspace-full\n\n# Install Fly\nRUN curl -L https://fly.io/install.sh | sh\nENV FLYCTL_INSTALL=\"/home/gitpod/.f"
  },
  {
    "path": "examples/complex/.gitpod.yml",
    "chars": 1370,
    "preview": "# https://www.gitpod.io/docs/config-gitpod-file\n\nimage:\n  file: .gitpod.Dockerfile\n\nports:\n  - port: 3000\n    onOpen: no"
  },
  {
    "path": "examples/complex/.npmrc",
    "chars": 22,
    "preview": "legacy-peer-deps=true\n"
  },
  {
    "path": "examples/complex/.prettierignore",
    "chars": 66,
    "preview": "node_modules\n\n/build\n/public/build\n.env\n\n/app/styles/tailwind.css\n"
  },
  {
    "path": "examples/complex/Dockerfile",
    "chars": 1489,
    "preview": "# 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 pro"
  },
  {
    "path": "examples/complex/README.md",
    "chars": 7941,
    "preview": "# Remix Indie Stack\n\n![The Remix Indie Stack](https://repository-images.githubusercontent.com/465928257/a241fa49-bd4d-48"
  },
  {
    "path": "examples/complex/app/db.server.ts",
    "chars": 600,
    "preview": "import { PrismaClient } from \"@prisma/client\";\n\nlet prisma: PrismaClient;\n\ndeclare global {\n  var __db__: PrismaClient;\n"
  },
  {
    "path": "examples/complex/app/entry.client.tsx",
    "chars": 525,
    "preview": "import { RemixBrowser } from \"@remix-run/react\";\nimport { startTransition, StrictMode } from \"react\";\nimport { hydrateRo"
  },
  {
    "path": "examples/complex/app/entry.server.tsx",
    "chars": 1349,
    "preview": "import { PassThrough } from \"stream\";\nimport type { EntryContext } from \"@remix-run/node\";\nimport { Response } from \"@re"
  },
  {
    "path": "examples/complex/app/models/note.server.ts",
    "chars": 997,
    "preview": "import type { User, Note } from \"@prisma/client\";\n\nimport { prisma } from \"~/db.server\";\n\nexport type { Note } from \"@pr"
  },
  {
    "path": "examples/complex/app/models/user.server.ts",
    "chars": 1353,
    "preview": "import type { Password, User } from \"@prisma/client\";\nimport bcrypt from \"bcryptjs\";\n\nimport { prisma } from \"~/db.serve"
  },
  {
    "path": "examples/complex/app/root.tsx",
    "chars": 847,
    "preview": "import type { LinksFunction, LoaderArgs } from \"@remix-run/node\";\nimport { json } from \"@remix-run/node\";\nimport {\n  Lin"
  },
  {
    "path": "examples/complex/app/routes/healthcheck.tsx",
    "chars": 841,
    "preview": "// learn more: https://fly.io/docs/reference/configuration/#services-http_checks\nimport type { LoaderArgs } from \"@remix"
  },
  {
    "path": "examples/complex/app/routes/index.tsx",
    "chars": 5972,
    "preview": "import { Link } from \"@remix-run/react\";\n\nimport { useOptionalUser } from \"~/utils\";\n\nexport default function Index() {\n"
  },
  {
    "path": "examples/complex/app/routes/join.tsx",
    "chars": 5129,
    "preview": "import type { ActionArgs, LoaderArgs, MetaFunction } from \"@remix-run/node\";\nimport { json, redirect } from \"@remix-run/"
  },
  {
    "path": "examples/complex/app/routes/login.tsx",
    "chars": 5550,
    "preview": "import type { ActionArgs, LoaderArgs, MetaFunction } from \"@remix-run/node\";\nimport { json, redirect } from \"@remix-run/"
  },
  {
    "path": "examples/complex/app/routes/logout.tsx",
    "chars": 284,
    "preview": "import type { ActionArgs } from \"@remix-run/node\";\nimport { redirect } from \"@remix-run/node\";\n\nimport { logout } from \""
  },
  {
    "path": "examples/complex/app/routes/notes/$noteId.tsx",
    "chars": 1771,
    "preview": "import type { ActionArgs, LoaderArgs } from \"@remix-run/node\";\nimport { json, redirect } from \"@remix-run/node\";\nimport "
  },
  {
    "path": "examples/complex/app/routes/notes/index.tsx",
    "chars": 277,
    "preview": "import { Link } from \"@remix-run/react\";\n\nexport default function NoteIndexPage() {\n  return (\n    <p>\n      No note sel"
  },
  {
    "path": "examples/complex/app/routes/notes/new.tsx",
    "chars": 3125,
    "preview": "import type { ActionArgs } from \"@remix-run/node\";\nimport { json, redirect } from \"@remix-run/node\";\nimport { Form, useA"
  },
  {
    "path": "examples/complex/app/routes/notes.tsx",
    "chars": 2119,
    "preview": "import type { LoaderArgs } from \"@remix-run/node\";\nimport { json } from \"@remix-run/node\";\nimport { Form, Link, NavLink,"
  },
  {
    "path": "examples/complex/app/session.server.ts",
    "chars": 2394,
    "preview": "import { createCookieSessionStorage, redirect } from \"@remix-run/node\";\nimport invariant from \"tiny-invariant\";\n\nimport "
  },
  {
    "path": "examples/complex/app/utils.test.ts",
    "chars": 448,
    "preview": "import { validateEmail } from \"./utils\";\n\ntest(\"validateEmail returns false for non-emails\", () => {\n  expect(validateEm"
  },
  {
    "path": "examples/complex/app/utils.ts",
    "chars": 1983,
    "preview": "import { useMatches } from \"@remix-run/react\";\nimport { useMemo } from \"react\";\n\nimport type { User } from \"~/models/use"
  },
  {
    "path": "examples/complex/custom-server.js",
    "chars": 836,
    "preview": "const express = require('express');\nconst { createRequestHandler } = require('@remix-run/express');\nconst { createRemixV"
  },
  {
    "path": "examples/complex/cypress/.eslintrc.js",
    "chars": 110,
    "preview": "module.exports = {\n  parserOptions: {\n    tsconfigRootDir: __dirname,\n    project: \"./tsconfig.json\",\n  },\n};\n"
  },
  {
    "path": "examples/complex/cypress/e2e/smoke.cy.ts",
    "chars": 1476,
    "preview": "import { faker } from \"@faker-js/faker\";\n\ndescribe(\"smoke tests\", () => {\n  afterEach(() => {\n    cy.cleanupUser();\n  })"
  },
  {
    "path": "examples/complex/cypress/fixtures/example.json",
    "chars": 155,
    "preview": "{\n  \"name\": \"Using fixtures to represent data\",\n  \"email\": \"hello@cypress.io\",\n  \"body\": \"Fixtures are a great way to mo"
  },
  {
    "path": "examples/complex/cypress/support/commands.ts",
    "chars": 2663,
    "preview": "import { faker } from \"@faker-js/faker\";\n\ndeclare global {\n  namespace Cypress {\n    interface Chainable {\n      /**\n   "
  },
  {
    "path": "examples/complex/cypress/support/create-user.ts",
    "chars": 1367,
    "preview": "// Use this to create a new user and login with that user\n// Simply call this with:\n// npx ts-node --require tsconfig-pa"
  },
  {
    "path": "examples/complex/cypress/support/delete-user.ts",
    "chars": 973,
    "preview": "// Use this to delete a user by their email\n// Simply call this with:\n// npx ts-node --require tsconfig-paths/register ."
  },
  {
    "path": "examples/complex/cypress/support/e2e.ts",
    "chars": 450,
    "preview": "import \"@testing-library/cypress/add-commands\";\nimport \"./commands\";\n\nCypress.on(\"uncaught:exception\", (err) => {\n  // C"
  },
  {
    "path": "examples/complex/cypress/tsconfig.json",
    "chars": 661,
    "preview": "{\n  \"exclude\": [\n    \"../node_modules/@types/jest\",\n    \"../node_modules/@testing-library/jest-dom\"\n  ],\n  \"include\": [\n"
  },
  {
    "path": "examples/complex/cypress.config.ts",
    "chars": 704,
    "preview": "import { defineConfig } from \"cypress\";\n\nexport default defineConfig({\n  e2e: {\n    setupNodeEvents: (on, config) => {\n "
  },
  {
    "path": "examples/complex/fly.toml",
    "chars": 919,
    "preview": "app = \"indie-stack-template\"\n\nkill_signal = \"SIGINT\"\nkill_timeout = 5\nprocesses = []\n\n[experimental]\n  allowed_public_po"
  },
  {
    "path": "examples/complex/mocks/README.md",
    "chars": 363,
    "preview": "# Mocks\n\nUse this to mock any third party HTTP resources that you don't have running locally and want to have mocked for"
  },
  {
    "path": "examples/complex/mocks/index.js",
    "chars": 259,
    "preview": "const { setupServer } = require(\"msw/node\");\n\nconst server = setupServer();\n\nserver.listen({ onUnhandledRequest: \"bypass"
  },
  {
    "path": "examples/complex/package.json",
    "chars": 3264,
    "preview": "{\n  \"name\": \"indie-stack-template\",\n  \"private\": true,\n  \"sideEffects\": false,\n  \"scripts\": {\n    \"build\": \"run-s build:"
  },
  {
    "path": "examples/complex/prisma/migrations/20220713162558_init/migration.sql",
    "chars": 947,
    "preview": "-- CreateTable\nCREATE TABLE \"User\" (\n    \"id\" TEXT NOT NULL PRIMARY KEY,\n    \"email\" TEXT NOT NULL,\n    \"createdAt\" DATE"
  },
  {
    "path": "examples/complex/prisma/migrations/migration_lock.toml",
    "chars": 122,
    "preview": "# Please do not edit this file manually\n# It should be added in your version-control system (i.e. Git)\nprovider = \"sqlit"
  },
  {
    "path": "examples/complex/prisma/schema.prisma",
    "chars": 740,
    "preview": "datasource db {\n  provider = \"sqlite\"\n  url      = env(\"DATABASE_URL\")\n}\n\ngenerator client {\n  provider = \"prisma-client"
  },
  {
    "path": "examples/complex/prisma/seed.ts",
    "chars": 1017,
    "preview": "import { PrismaClient } from \"@prisma/client\";\nimport bcrypt from \"bcryptjs\";\n\nconst prisma = new PrismaClient();\n\nasync"
  },
  {
    "path": "examples/complex/remix.config.js",
    "chars": 229,
    "preview": "/**\n * @type {import('@remix-run/dev').AppConfig}\n */\nmodule.exports = {\n  cacheDirectory: \"./node_modules/.cache/remix\""
  },
  {
    "path": "examples/complex/remix.env.d.ts",
    "chars": 91,
    "preview": "/// <reference types=\"@remix-run/dev\" />\n/// <reference types=\"@remix-run/node/globals\" />\n"
  },
  {
    "path": "examples/complex/remix.init/gitignore",
    "chars": 144,
    "preview": "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/"
  },
  {
    "path": "examples/complex/remix.init/index.js",
    "chars": 8257,
    "preview": "const { execSync } = require(\"child_process\");\nconst crypto = require(\"crypto\");\nconst fs = require(\"fs/promises\");\ncons"
  },
  {
    "path": "examples/complex/remix.init/package.json",
    "chars": 225,
    "preview": "{\n  \"name\": \"remix.init\",\n  \"private\": true,\n  \"main\": \"index.js\",\n  \"license\": \"MIT\",\n  \"dependencies\": {\n    \"@iarna/t"
  },
  {
    "path": "examples/complex/start.sh",
    "chars": 360,
    "preview": "#!/bin/sh\n\n# This file is how Fly starts the server (configured in fly.toml). Before starting\n# the server though, we ne"
  },
  {
    "path": "examples/complex/tailwind.config.js",
    "chars": 156,
    "preview": "/** @type {import('tailwindcss').Config} */\nmodule.exports = {\n  content: [\"./app/**/*.{ts,tsx,jsx,js}\"],\n  theme: {\n   "
  },
  {
    "path": "examples/complex/test/setup-test-env.ts",
    "chars": 119,
    "preview": "import { installGlobals } from \"@remix-run/node\";\nimport \"@testing-library/jest-dom/extend-expect\";\n\ninstallGlobals();\n"
  },
  {
    "path": "examples/complex/tsconfig.json",
    "chars": 677,
    "preview": "{\n  \"exclude\": [\"./cypress\", \"./cypress.config.ts\"],\n  \"include\": [\"remix.env.d.ts\", \"**/*.ts\", \"**/*.tsx\"],\n  \"compiler"
  },
  {
    "path": "examples/complex/vitest.config.ts",
    "chars": 384,
    "preview": "/// <reference types=\"vitest\" />\n/// <reference types=\"vite/client\" />\n\nimport react from \"@vitejs/plugin-react\";\nimport"
  },
  {
    "path": "examples/simple/.eslintrc.js",
    "chars": 142,
    "preview": "/** @type {import('eslint').Linter.Config} */\nmodule.exports = {\n  extends: [\"@remix-run/eslint-config\", \"@remix-run/esl"
  },
  {
    "path": "examples/simple/.gitignore",
    "chars": 48,
    "preview": "node_modules\n\n/.cache\n/build\n/public/build\n.env\n"
  },
  {
    "path": "examples/simple/README.md",
    "chars": 1057,
    "preview": "# 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"
  },
  {
    "path": "examples/simple/app/entry.client.tsx",
    "chars": 521,
    "preview": "import { RemixBrowser } from \"@remix-run/react\";\nimport { startTransition, StrictMode } from \"react\";\nimport { hydrateRo"
  },
  {
    "path": "examples/simple/app/entry.server.tsx",
    "chars": 2580,
    "preview": "import { PassThrough } from \"stream\";\nimport type { EntryContext } from \"@remix-run/node\";\nimport { Response } from \"@re"
  },
  {
    "path": "examples/simple/app/root.tsx",
    "chars": 571,
    "preview": "import type { MetaFunction } from \"@remix-run/node\";\nimport {\n  Links,\n  LiveReload,\n  Meta,\n  Outlet,\n  Scripts,\n  Scro"
  },
  {
    "path": "examples/simple/app/routes/index.tsx",
    "chars": 946,
    "preview": "import { useLoaderData } from \"@remix-run/react\";\n\nexport const loader = () => {\n  return {\n    foo: 'baz'\n  }\n};\n\nexpor"
  },
  {
    "path": "examples/simple/package.json",
    "chars": 652,
    "preview": "{\n  \"private\": true,\n  \"sideEffects\": false,\n  \"scripts\": {\n    \"build\": \"remix build\",\n    \"dev\": \"remix dev\",\n    \"sta"
  },
  {
    "path": "examples/simple/remix.config.js",
    "chars": 241,
    "preview": "/** @type {import('@remix-run/dev').AppConfig} */\nmodule.exports = {\n  ignoredRouteFiles: [\"**/.*\"],\n  // appDirectory: "
  },
  {
    "path": "examples/simple/remix.env.d.ts",
    "chars": 83,
    "preview": "/// <reference types=\"@remix-run/dev\" />\n/// <reference types=\"@remix-run/node\" />\n"
  },
  {
    "path": "examples/simple/tsconfig.json",
    "chars": 541,
    "preview": "{\n  \"include\": [\"remix.env.d.ts\", \"**/*.ts\", \"**/*.tsx\"],\n  \"compilerOptions\": {\n    \"lib\": [\"DOM\", \"DOM.Iterable\", \"ES2"
  },
  {
    "path": "package.json",
    "chars": 2925,
    "preview": "{\n  \"name\": \"remix-vite\",\n  \"version\": \"0.3.1\",\n  \"description\": \"Static file serving and directory listing\",\n  \"keyword"
  },
  {
    "path": "src/constants.ts",
    "chars": 183,
    "preview": "export const SERVER_ENTRY_ID = 'server-entry';\nexport const SERVER_ASSETS_MANIFEST_ID = 'server-assets-manifest';\nexport"
  },
  {
    "path": "src/entries/cli.ts",
    "chars": 1876,
    "preview": "#!/usr/bin/env node\n/* eslint-disable no-console */\n/* eslint-disable @typescript-eslint/no-explicit-any */\nimport expre"
  },
  {
    "path": "src/entries/lib.ts",
    "chars": 71,
    "preview": "export { getRemixViteBuild, createRemixViteDevServer } from '../vite';\n"
  },
  {
    "path": "src/plugins/hmr-fix.ts",
    "chars": 575,
    "preview": "import { getRouteByFilePath } from '../utils/general';\nimport { fixHmrCode } from '../utils/code';\nimport type { Plugin "
  },
  {
    "path": "src/plugins/inject.ts",
    "chars": 932,
    "preview": "import { getRouteByFilePath } from '../utils/general';\nimport type { Plugin } from 'vite';\n\nexport const getInjectPlugin"
  },
  {
    "path": "src/plugins/remix.ts",
    "chars": 4229,
    "preview": "/* eslint-disable @typescript-eslint/no-unnecessary-condition */\n/* eslint-disable @typescript-eslint/no-explicit-any */"
  },
  {
    "path": "src/plugins/transform.ts",
    "chars": 1427,
    "preview": "import {\n  getRemixRouteModuleExports,\n  getRouteByFilePath,\n} from '../utils/general';\nimport { filterExports } from '."
  },
  {
    "path": "src/utils/code.ts",
    "chars": 4234,
    "preview": "import { parse } from '@babel/parser';\nimport generate from '@babel/generator';\nimport traverse from '@babel/traverse';\n"
  },
  {
    "path": "src/utils/general.ts",
    "chars": 2380,
    "preview": "import path from 'path';\nimport { readConfig } from '@remix-run/dev/dist/config';\nimport { normalizePath as viteNormaliz"
  },
  {
    "path": "src/utils/version.ts",
    "chars": 721,
    "preview": "/* eslint-disable no-empty */\nimport fetch from 'cross-fetch';\nimport pkg from '../../package.json';\n\nexport const check"
  },
  {
    "path": "src/vite.ts",
    "chars": 1742,
    "preview": "import { createServer, mergeConfig } from 'vite';\nimport vitePluginReact from '@vitejs/plugin-react';\nimport tsconfigPat"
  },
  {
    "path": "tools/build.js",
    "chars": 1128,
    "preview": "/* eslint-disable no-console */\nconst esbuild = require('esbuild');\nconst chokidar = require('chokidar');\nconst pkg = re"
  },
  {
    "path": "tsconfig.json",
    "chars": 301,
    "preview": "{\n  \"extends\": \"@vercel/style-guide/typescript\",\n  \"compilerOptions\": {\n    \"lib\": [\"es2020\"],\n    \"target\": \"ES2016\",\n "
  }
]

About this extraction

This page contains the full source code of the sudomf/remix-vite GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 92 files (110.9 KB), approximately 31.7k tokens, and a symbol index with 84 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!