├── .dockerignore
├── .gitignore
├── Dockerfile
├── app
├── db.server.ts
├── entry.client.tsx
├── entry.server.tsx
├── meta.ts
├── root.tsx
└── routes
│ ├── counter.tsx
│ └── index.tsx
├── build.d.ts
├── bun.lockb
├── fly.toml
├── package.json
├── public
└── favicon.ico
├── remix.config.js
├── remix.d.ts
├── server.ts
└── tsconfig.json
/.dockerignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | node_modules.bun
3 |
4 | # Remix output
5 | .cache
6 | /build/
7 | /public/build/
8 |
9 | # Application output
10 | db.sqlite
11 |
12 | ## Fly configuration
13 | fly.toml
14 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | node_modules.bun
3 |
4 | # Remix output
5 | .cache
6 | /build/
7 | /public/build/
8 |
9 | # Application output
10 | db.sqlite
11 |
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM jarredsumner/bun:edge as deps
2 | RUN mkdir /application/
3 | WORKDIR /application/
4 |
5 | ADD package.json bun.lockb ./
6 | RUN bun install
7 |
8 | FROM node:18-bullseye-slim as remix
9 | RUN mkdir /application/
10 | WORKDIR /application/
11 |
12 | COPY --from=deps /opt/bun/bin/bun /bin/bun
13 | COPY --from=deps /application/node_modules /application/node_modules
14 |
15 | ADD . ./
16 |
17 | RUN bun run build
18 |
19 |
20 | FROM jarredsumner/bun:edge
21 | RUN mkdir /application/
22 | WORKDIR /application/
23 |
24 | COPY --from=deps /application/node_modules /application/node_modules
25 | COPY --from=deps /application/package.json /application/package.json
26 | COPY --from=deps /application/bun.lockb /application/bun.lockb
27 | COPY --from=remix /application/build /application/build
28 | COPY --from=remix /application/public /application/public
29 |
30 |
31 | ADD server.ts ./
32 |
33 | EXPOSE 3000
34 | CMD ["run", "start"]
--------------------------------------------------------------------------------
/app/db.server.ts:
--------------------------------------------------------------------------------
1 | import * as Sqlite from "bun:sqlite";
2 |
3 | declare global {
4 | var db: Sqlite.Database;
5 | }
6 |
7 | export const db: Sqlite.Database =
8 | globalThis.db ||
9 | (globalThis.db = new Sqlite.Database(process.env.DB_FILE || "db.sqlite"));
10 | db.run(`
11 | CREATE TABLE IF NOT EXISTS counter (
12 | id INTEGER PRIMARY KEY,
13 | count INTEGER
14 | );
15 | `);
16 | db.run(`
17 | INSERT OR IGNORE INTO counter (id, count) VALUES (1, 0)
18 | `);
19 |
--------------------------------------------------------------------------------
/app/entry.client.tsx:
--------------------------------------------------------------------------------
1 | import * as RemixReact from "@remix-run/react";
2 | import * as ReactDOM from "react-dom/client";
3 |
4 | ReactDOM.hydrateRoot(document, );
5 |
--------------------------------------------------------------------------------
/app/entry.server.tsx:
--------------------------------------------------------------------------------
1 | import type * as RemixServer from "@remix-run/server-runtime";
2 | import * as RemixReact from "@remix-run/react";
3 | import { renderToString } from "react-dom/server";
4 |
5 | export default async function handleRequest(
6 | request: Request,
7 | responseStatusCode: number,
8 | responseHeaders: Headers,
9 | remixContext: RemixServer.EntryContext
10 | ) {
11 | const markup = renderToString(
12 |
13 | );
14 |
15 | responseHeaders.set("Content-Type", "text/html");
16 |
17 | return new Response("" + markup, {
18 | status: responseStatusCode,
19 | headers: responseHeaders,
20 | });
21 | }
22 |
--------------------------------------------------------------------------------
/app/meta.ts:
--------------------------------------------------------------------------------
1 | import type { HtmlMetaDescriptor } from "@remix-run/server-runtime";
2 |
3 | const defaultTitle = "Remix Bun.js";
4 | const titleTemplate = `${defaultTitle} | %s`;
5 | const defaultDescription = "A example of Remix running on Bun.js.";
6 |
7 | export function create({
8 | title,
9 | description,
10 | ...rest
11 | }: HtmlMetaDescriptor = {}): HtmlMetaDescriptor {
12 | return {
13 | ...rest,
14 | title: title ? titleTemplate.replace("%s", title) : defaultTitle,
15 | description: description || defaultDescription,
16 | charset: "utf-8",
17 | viewport: "width=device-width,initial-scale=1",
18 | };
19 | }
20 |
--------------------------------------------------------------------------------
/app/root.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 | import type * as RemixServer from "@remix-run/server-runtime";
3 | import * as RemixReact from "@remix-run/react";
4 | import NProgress from "nprogress";
5 | import nProgressStylesHref from "nprogress/nprogress.css";
6 | import * as Meta from "~/meta";
7 |
8 | export const meta: RemixServer.MetaFunction = () => Meta.create();
9 |
10 | let latencyTotal = 0;
11 | let latencyCount = 0;
12 | let onLatencyChange: undefined | ((latency: number) => void);
13 | if (typeof document !== "undefined") {
14 | const originalFetch = window.fetch;
15 | window.fetch = (async (...args) => {
16 | let start = Date.now();
17 | let response = await originalFetch(...args);
18 | let end = Date.now();
19 |
20 | latencyCount++;
21 | latencyTotal += end - start;
22 | onLatencyChange?.(latencyTotal / latencyCount);
23 |
24 | return response;
25 | }) as typeof fetch;
26 | }
27 |
28 | function App({ children }: React.PropsWithChildren<{}>) {
29 | const transition = RemixReact.useTransition();
30 | const fetchers = RemixReact.useFetchers();
31 | const [latency, setLatency] = React.useState(0);
32 |
33 | React.useEffect(() => {
34 | onLatencyChange = setLatency;
35 | return () => {
36 | onLatencyChange = undefined;
37 | };
38 | }, []);
39 |
40 | const state = React.useMemo<"idle" | "loading">(
41 | function getGlobalState() {
42 | const states = [
43 | transition.state,
44 | ...fetchers.map((fetcher) => fetcher.state),
45 | ];
46 | if (states.every((state) => state === "idle")) return "idle";
47 | return "loading";
48 | },
49 | [transition.state, fetchers]
50 | );
51 |
52 | React.useEffect(() => {
53 | switch (state) {
54 | case "loading":
55 | NProgress.start();
56 | default:
57 | NProgress.done();
58 | }
59 | }, [state]);
60 |
61 | React.useEffect(() => {
62 | return () => {
63 | NProgress.remove();
64 | };
65 | }, []);
66 |
67 | return (
68 |
69 |
70 |
71 |
72 |
73 |
112 |
113 |
114 |
115 |
116 |
117 | {Meta.create().title}
118 |
123 |
124 | {children}
125 |
126 |
127 |
128 |
129 |
130 | );
131 | }
132 |
133 | export default function Root() {
134 | return (
135 |
136 |
137 |
138 | );
139 | }
140 |
141 | export function CatchBoundary() {
142 | const caught = RemixReact.useCatch();
143 |
144 | return (
145 |
146 | {caught.status}
147 | {caught.statusText && {caught.statusText}
}
148 |
149 | );
150 | }
151 |
152 | export function ErrorBoundary({ error }: { error: Error }) {
153 | console.log(error);
154 |
155 | return (
156 |
157 |
158 | Internal Server Error
159 | Something went wrong that we haven't accounted for.
160 |
161 |
162 | );
163 | }
164 |
--------------------------------------------------------------------------------
/app/routes/counter.tsx:
--------------------------------------------------------------------------------
1 | import * as RemixServer from "@remix-run/server-runtime";
2 | import * as RemixReact from "@remix-run/react";
3 | import { db } from "~/db.server";
4 |
5 | type LoaderData = {
6 | count: number;
7 | };
8 |
9 | export const loader: RemixServer.LoaderFunction = () => {
10 | const row = db
11 | .query(`SELECT count FROM counter WHERE id = 1`)
12 | .get();
13 | const count = row?.count || 0;
14 |
15 | return { count };
16 | };
17 |
18 | export default function Counter() {
19 | const { count } = RemixReact.useLoaderData();
20 |
21 | return (
22 |
23 | Counter
24 |
25 | This is a counter stored in SQLite. Click the button to increment it.
26 |
27 | Count: {count}
28 |
29 |
30 |
31 |
32 | );
33 | }
34 |
35 | export const action: RemixServer.ActionFunction = () => {
36 | db.transaction(() => {
37 | db.run(`UPDATE counter SET count = count + 1 WHERE id = 1`);
38 | })();
39 | return null;
40 | };
41 |
--------------------------------------------------------------------------------
/app/routes/index.tsx:
--------------------------------------------------------------------------------
1 | import * as RemixReact from "@remix-run/react";
2 |
3 | export default function Index() {
4 | return (
5 |
6 | Hello, Bun!
7 |
8 | This is a simple example of a{" "}
9 |
10 | Remix
11 | {" "}
12 | application running on{" "}
13 |
14 | Bun
15 |
16 | .
17 |
18 |
19 | It's deployed to{" "}
20 |
21 | Fly.io
22 | {" "}
23 | with a volume to store a SQLite DB.
24 |
25 |
26 | Head over to the{" "}
27 | counter page to view{" "}
28 | bun:sqlite
in action.
29 |
30 |
31 | );
32 | }
33 |
--------------------------------------------------------------------------------
/build.d.ts:
--------------------------------------------------------------------------------
1 | import type { ServerBuild } from "@remix-run/server-runtime";
2 |
3 | export const assets: ServerBuild["assets"];
4 | export const entry: ServerBuild["entry"];
5 | export const routes: ServerBuild["routes"];
6 |
--------------------------------------------------------------------------------
/bun.lockb:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jacob-ebey/remix-bun-testing/5badf75742578c7b6032decf076c3b33554a87ce/bun.lockb
--------------------------------------------------------------------------------
/fly.toml:
--------------------------------------------------------------------------------
1 | # fly.toml file generated for bun-remix-test on 2022-07-05T18:43:43-07:00
2 |
3 | app = "bun-remix-test"
4 |
5 | kill_signal = "SIGINT"
6 | kill_timeout = 5
7 | processes = []
8 |
9 | [env]
10 | DB_FILE = "/data/db.sqlite"
11 | NODE_ENV = "production"
12 |
13 | [mounts]
14 | destination = "/data"
15 | source = "bun_remix_test_db_volume"
16 |
17 | [experimental]
18 | allowed_public_ports = []
19 | auto_rollback = true
20 |
21 | [[services]]
22 | http_checks = []
23 | internal_port = 3000
24 | processes = ["app"]
25 | protocol = "tcp"
26 | script_checks = []
27 |
28 | [services.concurrency]
29 | hard_limit = 1000
30 | soft_limit = 1000
31 | type = "connections"
32 |
33 | [[services.ports]]
34 | force_https = true
35 | handlers = ["http"]
36 | port = 80
37 |
38 | [[services.ports]]
39 | handlers = ["tls", "http"]
40 | port = 443
41 |
42 | [[services.tcp_checks]]
43 | grace_period = "1s"
44 | interval = "15s"
45 | restart_limit = 0
46 | timeout = "2s"
47 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "bun-remix",
3 | "scripts": {
4 | "start": "bun run ./server.ts",
5 | "build": "bun run node_modules/@remix-run/dev/dist/cli.js build",
6 | "dev:remix": "bun run node_modules/@remix-run/dev/dist/cli.js watch",
7 | "dev:server": "bun run ./server.ts dev",
8 | "dev": "run-p dev:*"
9 | },
10 | "dependencies": {
11 | "@remix-run/react": "0.0.0-experimental-43ebb527",
12 | "@remix-run/server-runtime": "0.0.0-experimental-43ebb527",
13 | "nprogress": "^0.2.0",
14 | "react": "^18.2.0",
15 | "react-dom": "^18.2.0",
16 | "remix-flat-routes": "^0.4.3"
17 | },
18 | "devDependencies": {
19 | "@remix-run/dev": "0.0.0-experimental-43ebb527",
20 | "@types/nprogress": "^0.2.0",
21 | "@types/react": "^18.0.14",
22 | "@types/react-dom": "^18.0.6",
23 | "bun-types": "^0.0.83",
24 | "npm-run-all": "^4.1.5"
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jacob-ebey/remix-bun-testing/5badf75742578c7b6032decf076c3b33554a87ce/public/favicon.ico
--------------------------------------------------------------------------------
/remix.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import("@remix-run/dev").AppConfig} */
2 | const config = {
3 | serverModuleFormat: "esm",
4 | devServerPort: 8002,
5 | };
6 |
7 | module.exports = config;
8 |
--------------------------------------------------------------------------------
/remix.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 | ///
3 | ///
4 | ///
5 |
--------------------------------------------------------------------------------
/server.ts:
--------------------------------------------------------------------------------
1 | import * as fs from "fs";
2 | import * as path from "path";
3 |
4 | import { createRequestHandler } from "@remix-run/server-runtime";
5 | import * as build from "./build";
6 |
7 | const mode = process.argv[2] === "dev" ? "development" : "production";
8 |
9 | let requestHandler = createRequestHandler(build, mode);
10 |
11 | setInterval(() => {
12 | Bun.gc(true);
13 | }, 9000);
14 |
15 | async function handler(request: Request): Promise {
16 | if (mode === "development") {
17 | let newBuild = await import("./build"); // <- This is the segfault source
18 | requestHandler = createRequestHandler(newBuild, mode);
19 | }
20 |
21 | const file = tryServeStaticFile("public", request);
22 | if (file) return file;
23 |
24 | return requestHandler(request);
25 | }
26 |
27 | const server = Bun.serve({
28 | port: 3000,
29 | fetch: mode === "development" ? liveReload(handler) : handler,
30 | });
31 |
32 | console.log(`Server started at ${server.hostname}`);
33 |
34 | function tryServeStaticFile(
35 | staticDir: string,
36 | request: Request
37 | ): Response | undefined {
38 | const url = new URL(request.url);
39 |
40 | if (url.pathname.length < 2) return undefined;
41 |
42 | const filePath = path.join(staticDir, url.pathname);
43 |
44 | if (fs.existsSync(filePath)) {
45 | const file = Bun.file(filePath);
46 | return new Response(file, {
47 | headers: {
48 | "Content-Type": file.type,
49 | "Cache-Control": "public, max-age=31536000",
50 | },
51 | });
52 | }
53 |
54 | return undefined;
55 | }
56 |
57 | function liveReload(callback: TFunc) {
58 | const registry = new Map([...Loader.registry.entries()]);
59 | function reload() {
60 | if (Loader.registry.size !== registry.size) {
61 | for (const key of Loader.registry.keys()) {
62 | if (!registry.has(key)) {
63 | Loader.registry.delete(key);
64 | }
65 | }
66 | }
67 | }
68 |
69 | return async (...args: unknown[]) => {
70 | reload();
71 | return callback(...args);
72 | };
73 | }
74 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "lib": ["DOM", "DOM.Iterable", "ES2019"],
4 | "module": "esnext",
5 | "target": "esnext",
6 | "moduleResolution": "node",
7 | "jsx": "react-jsx",
8 | "baseUrl": ".",
9 | "paths": {
10 | "~/*": ["./app/*"]
11 | },
12 | "allowJs": true,
13 | "forceConsistentCasingInFileNames": true,
14 | "strict": true,
15 | "esModuleInterop": true,
16 | "isolatedModules": true,
17 | "noEmit": true,
18 | "resolveJsonModule": true
19 | },
20 | "include": ["**/*.ts", "**/*.tsx"],
21 | "exclude": ["node_modules"]
22 | }
23 |
--------------------------------------------------------------------------------