;
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 |
34 |
35 |
36 | );
37 | };
38 |
39 | export const AddTodoForm = () => {
40 | return (
41 |
49 | );
50 | };
51 |
52 | export const loader = loaderFunction(() => TodoRepo.getAllTodos);
53 |
54 | export default function Index() {
55 | const todos = useLoaderData();
56 |
57 | return (
58 |
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 |
--------------------------------------------------------------------------------