├── .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 |