├── .env.template ├── .envrc ├── .eslintrc.cjs ├── .gitignore ├── .npmrc ├── .vscode └── settings.json ├── LICENSE ├── OBJECTIVES.md ├── README.md ├── app ├── entry.client.tsx ├── entry.server.tsx ├── lib │ └── utilities.ts ├── root.tsx ├── routes │ └── _index.tsx ├── services │ ├── TodoRepo.ts │ └── index.ts └── types │ └── Todo.tsx ├── babel.config.json ├── flake.lock ├── flake.nix ├── package.json ├── pnpm-lock.yaml ├── public └── favicon.ico ├── remix.config.js ├── remix.env.d.ts ├── scripts └── dev.js ├── tsconfig.json └── vite.config.ts /.env.template: -------------------------------------------------------------------------------- 1 | HONEYCOMB_API_KEY= 2 | HONEYCOMB_SERVICE_NAME= -------------------------------------------------------------------------------- /.envrc: -------------------------------------------------------------------------------- 1 | use flake; 2 | -------------------------------------------------------------------------------- /.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | /** @type {import('eslint').Linter.Config} */ 2 | module.exports = { 3 | extends: ["@remix-run/eslint-config", "@remix-run/eslint-config/node"], 4 | }; 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | 3 | /.cache 4 | /build 5 | /public/build 6 | .env 7 | .direnv 8 | db.sqlite-wal 9 | db.sqlite-shm -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | shamefully-hoist=true -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.formatOnSave": true 3 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Effectful Technologies Inc 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /OBJECTIVES.md: -------------------------------------------------------------------------------- 1 | 1. basic effect integration with usage of Layers 2 | 2. database, potentially using @effect/sql 3 | 3. telemetry 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Using Effect with Remix 2 | 3 | - [Remix Docs](https://remix.run/docs) 4 | - [Effect Docs](https://effect.website/docs/introduction) 5 | - [Vite Docs](https://vitejs.dev/guide/) 6 | 7 | ## Project Goals 8 | 9 | Prototype an integration of Remix + Effect in such a way that the Effect runtime is only ever executed in the backend while keeping the frontend code minimal and fully type safe. 10 | 11 | This project aims to demonstrate integration of Effect in one typical setup where the user is not in control of the program entrypoints that are delegated to frameworks, similar scenarios are for example the usage of Next.js or equivalent frameworks. 12 | 13 | ## Development 14 | 15 | From your terminal: 16 | 17 | ```sh 18 | pnpm run dev 19 | ``` 20 | 21 | This starts your app in development mode, rebuilding assets on file changes. 22 | 23 | ## Deployment 24 | 25 | First, build your app for production: 26 | 27 | ```sh 28 | pnpm run build 29 | ``` 30 | 31 | Then run the app in production mode: 32 | 33 | ```sh 34 | pnpm run start 35 | ``` 36 | 37 | Now you'll need to pick a host to deploy it to. 38 | 39 | ## Telemetry 40 | 41 | If you want to see telemetry data this project is configured to work with [https://www.honeycomb.io/](https://www.honeycomb.io/), create an account if you don't have one (they have a very nice free tier) and write your project name & api key in a file called `.env`, follow the template `.env.template` 42 | 43 | Note: if you want to change the backend you use for tracing, for example using your own [grafana tempo](https://grafana.com/oss/tempo/) you'll need to edit `/services/Tracing.ts` accordingly (see [https://github.com/Effect-TS/opentelemetry](https://github.com/Effect-TS/opentelemetry)). 44 | 45 | ## Project Setup 46 | 47 | This project uses a nightly build of remix in order to use it together with vite. Apart from that it looks like a normal vite project. 48 | 49 | The key configurations for vite are found in the `vite.config.ts` file that looks like: 50 | 51 | ```ts 52 | import { unstable_vitePlugin as remix } from "@remix-run/dev"; 53 | import { defineConfig } from "vite"; 54 | import babel from "vite-plugin-babel"; 55 | import tsconfigPaths from "vite-tsconfig-paths"; 56 | 57 | export default defineConfig({ 58 | plugins: [ 59 | babel({ 60 | filter: new RegExp(/\.tsx?$/), 61 | }), 62 | remix(), 63 | tsconfigPaths(), 64 | ], 65 | build: { 66 | outDir: "build", 67 | copyPublicDir: false, 68 | minify: "terser", 69 | }, 70 | publicDir: "./public", 71 | }); 72 | ``` 73 | 74 | Namely here we are setting up Remix together with babel, in babel we use a plugin to annotate pure calls so that we can tree-shake loaders and actions that use higher order functions. 75 | 76 | In short this setup enables us to use and tree-shake the following pattern: 77 | 78 | ```tsx 79 | export const loader = effectLoader(effect); 80 | export const action = effectAction(effect); 81 | 82 | export default function Component() { 83 | // plain component 84 | } 85 | ``` 86 | 87 | ## Code Structure 88 | 89 | The project uses 4 main libraries of the effect ecosystem: 90 | 91 | - `effect` to handle all effectful operations 92 | - `@effect/schema` to define data models and handle serialization 93 | - `@effect/opentelemetry` to integrate with a telemetry dashboard 94 | - `@sqlfx/sqlite` to integrate with sqlite 95 | 96 | As of telemetry for simplicity we are using [https://www.honeycomb.io/](https://www.honeycomb.io/) but any open telemetry compatible service will work with minor changes to the code 97 | 98 | The directories are structured in the following way: 99 | 100 | - `/app` contains all the app code 101 | - `/lib` contains integration code with effect (and a temporary hack to load opentelemetry from esm due to improper es modules) 102 | - `/migrations` contains all the database migration code, those are automatically loaded by the sql client 103 | - `/routes` contains the remix routes, loaders and actions 104 | - `/services` contains the business logic encapsulated in effect services 105 | - `/database` contains the sqlite files 106 | -------------------------------------------------------------------------------- /app/entry.client.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * By default, Remix will handle hydrating your app on the client for you. 3 | * You are free to delete this file if you'd like to, but if you ever want it revealed again, you can run `npx remix reveal` ✨ 4 | * For more information, see https://remix.run/file-conventions/entry.client 5 | */ 6 | 7 | import { RemixBrowser } from "@remix-run/react"; 8 | import { startTransition, StrictMode } from "react"; 9 | import { hydrateRoot } from "react-dom/client"; 10 | 11 | startTransition(() => { 12 | hydrateRoot( 13 | document, 14 | 15 | 16 | 17 | ); 18 | }); 19 | -------------------------------------------------------------------------------- /app/entry.server.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * By default, Remix will handle generating the HTTP Response for you. 3 | * You are free to delete this file if you'd like to, but if you ever want it revealed again, you can run `npx remix reveal` ✨ 4 | * For more information, see https://remix.run/file-conventions/entry.server 5 | */ 6 | import "dotenv/config"; 7 | 8 | import { PassThrough } from "node:stream"; 9 | 10 | import type { EntryContext } from "@remix-run/node"; 11 | import { createReadableStreamFromReadable } from "@remix-run/node"; 12 | import { RemixServer } from "@remix-run/react"; 13 | import { isbot } from "isbot"; 14 | import { renderToPipeableStream } from "react-dom/server"; 15 | 16 | const ABORT_DELAY = 15_000; 17 | 18 | export default async function handleRequest( 19 | request: Request, 20 | responseStatusCode: number, 21 | responseHeaders: Headers, 22 | remixContext: EntryContext 23 | ) { 24 | return isbot(request.headers.get("user-agent")) 25 | ? handleBotRequest( 26 | request, 27 | responseStatusCode, 28 | responseHeaders, 29 | remixContext 30 | ) 31 | : handleBrowserRequest( 32 | request, 33 | responseStatusCode, 34 | responseHeaders, 35 | remixContext 36 | ); 37 | } 38 | 39 | function handleBotRequest( 40 | request: Request, 41 | responseStatusCode: number, 42 | responseHeaders: Headers, 43 | remixContext: EntryContext 44 | ) { 45 | return new Promise((resolve, reject) => { 46 | let shellRendered = false; 47 | const { pipe, abort } = renderToPipeableStream( 48 | , 53 | { 54 | onAllReady() { 55 | shellRendered = true; 56 | const body = new PassThrough(); 57 | const stream = createReadableStreamFromReadable(body); 58 | 59 | responseHeaders.set("Content-Type", "text/html"); 60 | 61 | resolve( 62 | new Response(stream, { 63 | headers: responseHeaders, 64 | status: responseStatusCode, 65 | }) 66 | ); 67 | 68 | pipe(body); 69 | }, 70 | onShellError(error: unknown) { 71 | reject(error); 72 | }, 73 | onError(error: unknown) { 74 | responseStatusCode = 500; 75 | // Log streaming rendering errors from inside the shell. Don't log 76 | // errors encountered during initial shell rendering since they'll 77 | // reject and get logged in handleDocumentRequest. 78 | if (shellRendered) { 79 | console.error(error); 80 | } 81 | }, 82 | } 83 | ); 84 | 85 | setTimeout(abort, ABORT_DELAY); 86 | }); 87 | } 88 | 89 | function handleBrowserRequest( 90 | request: Request, 91 | responseStatusCode: number, 92 | responseHeaders: Headers, 93 | remixContext: EntryContext 94 | ) { 95 | return new Promise((resolve, reject) => { 96 | let shellRendered = false; 97 | const { pipe, abort } = renderToPipeableStream( 98 | , 103 | { 104 | onShellReady() { 105 | shellRendered = true; 106 | const body = new PassThrough(); 107 | const stream = createReadableStreamFromReadable(body); 108 | 109 | responseHeaders.set("Content-Type", "text/html"); 110 | 111 | resolve( 112 | new Response(stream, { 113 | headers: responseHeaders, 114 | status: responseStatusCode, 115 | }) 116 | ); 117 | 118 | pipe(body); 119 | }, 120 | onShellError(error: unknown) { 121 | reject(error); 122 | }, 123 | onError(error: unknown) { 124 | responseStatusCode = 500; 125 | // Log streaming rendering errors from inside the shell. Don't log 126 | // errors encountered during initial shell rendering since they'll 127 | // reject and get logged in handleDocumentRequest. 128 | if (shellRendered) { 129 | console.error(error); 130 | } 131 | }, 132 | } 133 | ); 134 | 135 | setTimeout(abort, ABORT_DELAY); 136 | }); 137 | } 138 | -------------------------------------------------------------------------------- /app/lib/utilities.ts: -------------------------------------------------------------------------------- 1 | import type { LoaderFunction } from "@remix-run/node"; 2 | import { Effect, ManagedRuntime, Layer } from "effect"; 3 | 4 | export const makeRemixRuntime = (layer: Layer.Layer) => { 5 | const runtime = ManagedRuntime.make(layer); 6 | 7 | const loaderFunction = 8 | ( 9 | body: (...args: Parameters) => Effect.Effect 10 | ): { 11 | (...args: Parameters): Promise; 12 | } => 13 | (...args) => 14 | runtime.runPromise(body(...args)); 15 | 16 | return { loaderFunction }; 17 | }; 18 | -------------------------------------------------------------------------------- /app/root.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | Links, 3 | LiveReload, 4 | Meta, 5 | Outlet, 6 | Scripts, 7 | ScrollRestoration, 8 | } from "@remix-run/react"; 9 | 10 | export default function App() { 11 | return ( 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | ); 26 | } 27 | -------------------------------------------------------------------------------- /app/routes/_index.tsx: -------------------------------------------------------------------------------- 1 | import type { MetaFunction } from "@remix-run/node"; 2 | import { useLoaderData } from "@remix-run/react"; 3 | 4 | import "todomvc-app-css/index.css"; 5 | import "todomvc-common/base.css"; 6 | 7 | import { TodoRepo } from "~/services/TodoRepo"; 8 | import { loaderFunction } from "~/services/index"; 9 | import { Todo } from "../types/Todo"; 10 | 11 | export const meta: MetaFunction = () => { 12 | return [ 13 | { title: "Remixing Effect" }, 14 | { 15 | name: "description", 16 | content: "Integrate Effect & Remix for the greater good!", 17 | }, 18 | ]; 19 | }; 20 | 21 | export const TodoRow = ({ todo }: { todo: Todo.Encoded }) => { 22 | const isCompleted = todo.status === "COMPLETED"; 23 | return ( 24 |
  • 25 |
    26 | 32 | 33 |
    35 |
  • 36 | ); 37 | }; 38 | 39 | export const AddTodoForm = () => { 40 | return ( 41 |
    42 | 48 |
    49 | ); 50 | }; 51 | 52 | export const loader = loaderFunction(() => TodoRepo.getAllTodos); 53 | 54 | export default function Index() { 55 | const todos = useLoaderData(); 56 | 57 | return ( 58 |
    59 |
    60 |

    todos...

    61 | 62 |
    63 |
    64 | 65 | 66 |
      67 | {todos.map((todo) => ( 68 | 69 | ))} 70 |
    71 |
    72 |
    73 | ); 74 | } 75 | -------------------------------------------------------------------------------- /app/services/TodoRepo.ts: -------------------------------------------------------------------------------- 1 | import { Effect, Layer } from "effect"; 2 | import { Todo } from "~/types/Todo"; 3 | 4 | const makeTodoRepo = Effect.sync(() => { 5 | return { 6 | getAllTodos: Effect.gen(function* () { 7 | const todos = [ 8 | new Todo({ 9 | id: 1, 10 | createdAt: new Date(), 11 | status: "CREATED", 12 | title: "Well well well, look who's streaming!", 13 | }), 14 | ]; 15 | 16 | return yield* Todo.encodeArray(todos); 17 | }), 18 | }; 19 | }); 20 | 21 | export class TodoRepo extends Effect.Tag("@services/TodoRepo")< 22 | TodoRepo, 23 | Effect.Effect.Success 24 | >() { 25 | static Live = Layer.effect(this, makeTodoRepo); 26 | } 27 | -------------------------------------------------------------------------------- /app/services/index.ts: -------------------------------------------------------------------------------- 1 | import { Layer } from "effect"; 2 | import { makeRemixRuntime } from "~/lib/utilities"; 3 | import { TodoRepo } from "./TodoRepo"; 4 | 5 | export const { loaderFunction } = makeRemixRuntime( 6 | Layer.mergeAll(TodoRepo.Live) 7 | ); 8 | -------------------------------------------------------------------------------- /app/types/Todo.tsx: -------------------------------------------------------------------------------- 1 | import { Schema } from "@effect/schema"; 2 | import { Effect, flow } from "effect"; 3 | 4 | export class Todo extends Schema.Class("Todo")({ 5 | id: Schema.Number, 6 | title: Schema.String, 7 | createdAt: Schema.DateFromString, 8 | status: Schema.Literal("CREATED", "COMPLETED"), 9 | }) { 10 | static encodeArray = flow( 11 | Schema.encode(Schema.Array(this)), 12 | Effect.map((todos): ReadonlyArray => todos) 13 | ); 14 | } 15 | 16 | export namespace Todo { 17 | export interface Encoded extends Schema.Schema.Encoded {} 18 | } 19 | -------------------------------------------------------------------------------- /babel.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": [ 3 | [ 4 | "annotate-pure-calls" 5 | ] 6 | ], 7 | "presets": [ 8 | "@babel/preset-typescript" 9 | ] 10 | } -------------------------------------------------------------------------------- /flake.lock: -------------------------------------------------------------------------------- 1 | { 2 | "nodes": { 3 | "flake-utils": { 4 | "inputs": { 5 | "systems": "systems" 6 | }, 7 | "locked": { 8 | "lastModified": 1694529238, 9 | "narHash": "sha256-zsNZZGTGnMOf9YpHKJqMSsa0dXbfmxeoJ7xHlrt+xmY=", 10 | "owner": "numtide", 11 | "repo": "flake-utils", 12 | "rev": "ff7b65b44d01cf9ba6a71320833626af21126384", 13 | "type": "github" 14 | }, 15 | "original": { 16 | "owner": "numtide", 17 | "repo": "flake-utils", 18 | "type": "github" 19 | } 20 | }, 21 | "nixpkgs": { 22 | "locked": { 23 | "lastModified": 1697915759, 24 | "narHash": "sha256-WyMj5jGcecD+KC8gEs+wFth1J1wjisZf8kVZH13f1Zo=", 25 | "owner": "nixos", 26 | "repo": "nixpkgs", 27 | "rev": "51d906d2341c9e866e48c2efcaac0f2d70bfd43e", 28 | "type": "github" 29 | }, 30 | "original": { 31 | "owner": "nixos", 32 | "ref": "nixpkgs-unstable", 33 | "repo": "nixpkgs", 34 | "type": "github" 35 | } 36 | }, 37 | "root": { 38 | "inputs": { 39 | "flake-utils": "flake-utils", 40 | "nixpkgs": "nixpkgs" 41 | } 42 | }, 43 | "systems": { 44 | "locked": { 45 | "lastModified": 1681028828, 46 | "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", 47 | "owner": "nix-systems", 48 | "repo": "default", 49 | "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", 50 | "type": "github" 51 | }, 52 | "original": { 53 | "owner": "nix-systems", 54 | "repo": "default", 55 | "type": "github" 56 | } 57 | } 58 | }, 59 | "root": "root", 60 | "version": 7 61 | } 62 | -------------------------------------------------------------------------------- /flake.nix: -------------------------------------------------------------------------------- 1 | { 2 | inputs = { 3 | nixpkgs = { 4 | url = "github:nixos/nixpkgs/nixpkgs-unstable"; 5 | }; 6 | 7 | flake-utils = { 8 | url = "github:numtide/flake-utils"; 9 | }; 10 | }; 11 | 12 | outputs = { 13 | self, 14 | nixpkgs, 15 | flake-utils, 16 | ... 17 | }: 18 | flake-utils.lib.eachDefaultSystem (system: let 19 | pkgs = nixpkgs.legacyPackages.${system}; 20 | corepackEnable = pkgs.runCommand "corepack-enable" {} '' 21 | mkdir -p $out/bin 22 | ${pkgs.nodejs-18_x}/bin/corepack enable --install-directory $out/bin 23 | ''; 24 | in { 25 | formatter = pkgs.alejandra; 26 | 27 | devShells = { 28 | default = pkgs.mkShell { 29 | buildInputs = with pkgs; [ 30 | nodejs-18_x 31 | corepackEnable 32 | ]; 33 | }; 34 | }; 35 | }); 36 | } 37 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vite-remix", 3 | "private": true, 4 | "sideEffects": false, 5 | "type": "module", 6 | "license": "MIT", 7 | "scripts": { 8 | "build": "vite build && vite build --ssr", 9 | "dev": "NODE_ENV=development node scripts/dev.js", 10 | "start": "remix-serve build/index.js", 11 | "typecheck": "tsc", 12 | "clean": "rm -rf build && rm -rf public/build && rm -rf node_modules/.vite" 13 | }, 14 | "dependencies": { 15 | "@effect/schema": "^0.66.10", 16 | "@remix-run/react": "^2.9.1", 17 | "@remix-run/serve": "^2.9.1", 18 | "dotenv": "^16.4.5", 19 | "effect": "^3.0.7", 20 | "isbot": "^5.1.5", 21 | "react": "^18.3.1", 22 | "react-dom": "^18.3.1", 23 | "todomvc-app-css": "^2.4.3", 24 | "todomvc-common": "^1.0.5" 25 | }, 26 | "devDependencies": { 27 | "@babel/preset-typescript": "^7.24.1", 28 | "@remix-run/dev": "^2.9.1", 29 | "@remix-run/eslint-config": "^2.9.1", 30 | "@remix-run/node": "^2.9.1", 31 | "@types/react": "^18.3.1", 32 | "@types/react-dom": "^18.3.0", 33 | "babel-plugin-annotate-pure-calls": "^0.4.0", 34 | "eslint": "^9.1.1", 35 | "terser": "^5.30.4", 36 | "typescript": "^5.4.5", 37 | "vite": "^4.5.0", 38 | "vite-plugin-babel": "^1.2.0", 39 | "vite-tsconfig-paths": "^4.3.2" 40 | }, 41 | "engines": { 42 | "node": ">=18.0.0" 43 | } 44 | } -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mikearnaldi/effect-remix-stream/36f2e3e33cc6b0c403ffadc2fa76a04e16b036c5/public/favicon.ico -------------------------------------------------------------------------------- /remix.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('@remix-run/dev').AppConfig} */ 2 | export default { 3 | ignoredRouteFiles: ["**/.*"], 4 | // appDirectory: "app", 5 | // assetsBuildDirectory: "public/build", 6 | // publicPath: "/build/", 7 | // serverBuildPath: "build/index.js", 8 | }; 9 | -------------------------------------------------------------------------------- /remix.env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | /// -------------------------------------------------------------------------------- /scripts/dev.js: -------------------------------------------------------------------------------- 1 | import { fileURLToPath } from "url"; 2 | import { createServer } from "vite"; 3 | 4 | const __dirname = fileURLToPath(new URL("..", import.meta.url)); 5 | 6 | (async () => { 7 | const server = await createServer({ 8 | configFile: "vite.config.ts", 9 | root: __dirname, 10 | server: { 11 | port: 3000, 12 | }, 13 | }); 14 | await server.listen(); 15 | server.printUrls(); 16 | })(); 17 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": [ 3 | "remix.env.d.ts", 4 | "**/*.ts", 5 | "**/*.tsx", 6 | "vite.config.js" 7 | ], 8 | "compilerOptions": { 9 | "lib": [ 10 | "DOM", 11 | "DOM.Iterable", 12 | "ES2022" 13 | ], 14 | "isolatedModules": true, 15 | "esModuleInterop": true, 16 | "jsx": "react-jsx", 17 | "moduleResolution": "Bundler", 18 | "resolveJsonModule": true, 19 | "target": "ES2022", 20 | "module": "ES2022", 21 | "strict": true, 22 | "allowJs": true, 23 | "forceConsistentCasingInFileNames": true, 24 | "baseUrl": ".", 25 | "paths": { 26 | "~/*": [ 27 | "./app/*" 28 | ] 29 | }, 30 | "skipLibCheck": true, 31 | // Remix takes care of building everything in `remix build`. 32 | "noEmit": true 33 | } 34 | } -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import { vitePlugin } from "@remix-run/dev"; 2 | import { defineConfig } from "vite"; 3 | import babel from "vite-plugin-babel"; 4 | import tsconfigPaths from "vite-tsconfig-paths"; 5 | 6 | export default defineConfig({ 7 | plugins: [ 8 | babel({ 9 | filter: new RegExp(/\.tsx?$/), 10 | }), 11 | tsconfigPaths(), 12 | vitePlugin(), 13 | ], 14 | build: { 15 | outDir: "build", 16 | copyPublicDir: false, 17 | minify: "terser", 18 | }, 19 | publicDir: "./public", 20 | }); 21 | --------------------------------------------------------------------------------