diff --git a/bun.lock b/bun.lock
index c12e6b5..b04f809 100644
--- a/bun.lock
+++ b/bun.lock
@@ -49,6 +49,7 @@
"packages/nano-remix": {
"name": "@workshop/nano-remix",
"dependencies": {
+ "bun-plugin-tailwind": "^0.0.15",
"hono": "catalog:",
},
"devDependencies": {
@@ -119,6 +120,19 @@
"typescript": "^5",
},
},
+ "packages/werewolf-ui": {
+ "name": "werewolfUI",
+ "version": "0.1.0",
+ "dependencies": {
+ "bun-plugin-tailwind": "^0.0.15",
+ "hono": "^4.8.3",
+ "lucide-static": "^0.525.0",
+ "tailwindcss": "^4.0.6",
+ },
+ "devDependencies": {
+ "@types/bun": "latest",
+ },
+ },
},
"catalog": {
"hono": "^4.8.0",
@@ -184,6 +198,8 @@
"@types/node": ["@types/node@24.0.3", "", { "dependencies": { "undici-types": "~7.8.0" } }, "sha512-R4I/kzCYAdRLzfiCabn9hxWfbuHS573x+r0dJMkkzThEa7pbrcDWK+9zu3e7aBOouf+rQAciqPFMnxwr0aWgKg=="],
+ "@types/react": ["@types/react@19.1.8", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-AwAfQ2Wa5bCx9WP8nZL2uMZWod7J7/JSplxbTmBQ5ms6QpqNYm672H0Vu9ZVKVngQ+ii4R/byguVEUZQyeg44g=="],
+
"@types/ws": ["@types/ws@8.18.1", "", { "dependencies": { "@types/node": "*" } }, "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg=="],
"@vladfrangu/async_event_emitter": ["@vladfrangu/async_event_emitter@2.4.6", "", {}, "sha512-RaI5qZo6D2CVS6sTHFKg1v5Ohq/+Bo2LZ5gzUEwZ/WkHhwtGTCB/sVLw8ijOkAUxasZ+WshN/Rzj4ywsABJ5ZA=="],
@@ -246,6 +262,8 @@
"buffer-more-ints": ["buffer-more-ints@0.0.2", "", {}, "sha512-PDgX2QJgUc5+Jb2xAoBFP5MxhtVUmZHR33ak+m/SDxRdCrbnX1BggRIaxiW7ImwfmO4iJeCQKN18ToSXWGjYkA=="],
+ "bun-plugin-tailwind": ["bun-plugin-tailwind@0.0.15", "", { "peerDependencies": { "typescript": "^5.0.0" } }, "sha512-qtAXMNGG4R0UGGI8zWrqm2B7BdXqx48vunJXBPzfDOHPA5WkRUZdTSbE7TFwO4jLhYqSE23YMWsM9NhE6ovobw=="],
+
"bun-types": ["bun-types@1.2.17", "", { "dependencies": { "@types/node": "*" } }, "sha512-ElC7ItwT3SCQwYZDYoAH+q6KT4Fxjl8DtZ6qDulUFBmXA8YB4xo+l54J9ZJN+k2pphfn9vk7kfubeSd5QfTVJQ=="],
"bytes": ["bytes@3.0.0", "", {}, "sha512-pMhOfFDPiv9t5jjIXkHosWmkSyQbvsgEVNkz0ERHbuLh2T/7j4Mqqpz523Fe8MVY89KC6Sh/QfS2sM+SjgFDcw=="],
@@ -290,6 +308,8 @@
"crypto2": ["crypto2@2.0.0", "", { "dependencies": { "babel-runtime": "6.26.0", "node-rsa": "0.4.2", "util.promisify": "1.0.0" } }, "sha512-jdXdAgdILldLOF53md25FiQ6ybj2kUFTiRjs7msKTUoZrzgT/M1FPX5dYGJjbbwFls+RJIiZxNTC02DE/8y0ZQ=="],
+ "csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="],
+
"data-view-buffer": ["data-view-buffer@1.0.2", "", { "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", "is-data-view": "^1.0.2" } }, "sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ=="],
"data-view-byte-length": ["data-view-byte-length@1.0.2", "", { "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", "is-data-view": "^1.0.2" } }, "sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ=="],
@@ -498,6 +518,8 @@
"lodash.snakecase": ["lodash.snakecase@4.1.1", "", {}, "sha512-QZ1d4xoBHYUeuouhEq3lk3Uq7ldgyFXGBhg04+oRLnIz8o9T65Eh+8YdroUwn846zchkA9yDsDl5CVVaV2nqYw=="],
+ "lucide-static": ["lucide-static@0.525.0", "", {}, "sha512-dPQiibOV/kRv/UnaNFbiTxdDFZ267rIjHVWLv6GoUXVD5YSW71cyF4tYJVD27zSb0OOWdeWrqZsuBtRaYc4FHw=="],
+
"lusca": ["lusca@1.6.1", "", { "dependencies": { "tsscmp": "^1.0.5" } }, "sha512-+JzvUMH/rsE/4XfHdDOl70bip0beRcHSviYATQM0vtls59uVtdn1JMu4iD7ZShBpAmFG8EnaA+PrYG9sECMIOQ=="],
"luxon": ["luxon@3.6.1", "", {}, "sha512-tJLxrKJhO2ukZ5z0gyjY1zPh3Rh88Ej9P7jNrZiHMUXHae1yvI2imgOZtL1TO8TW6biMMKfTtAOoEJANgtWBMQ=="],
@@ -656,6 +678,8 @@
"tailwind": ["tailwind@4.0.0", "", { "dependencies": { "@babel/runtime": "7.3.4", "ajv": "6.10.0", "app-root-path": "2.1.0", "async-retry": "1.2.3", "body-parser": "1.18.3", "commands-events": "1.0.4", "compression": "1.7.3", "content-type": "1.0.4", "cors": "2.8.5", "crypto2": "2.0.0", "datasette": "1.0.1", "draht": "1.0.1", "express": "4.16.4 ", "flaschenpost": "1.1.3", "hase": "2.0.0", "json-lines": "1.0.0", "limes": "2.0.0", "lodash": "4.17.11", "lusca": "1.6.1", "morgan": "1.9.1", "nocache": "2.0.0", "partof": "1.0.0", "processenv": "1.1.0", "stethoskop": "1.0.0", "timer2": "1.0.0", "uuidv4": "3.0.1", "ws": "6.2.0" } }, "sha512-LlUNoD/5maFG1h5kQ6/hXfFPdcnYw+1Z7z+kUD/W/E71CUMwcnrskxiBM8c3G8wmPsD1VvCuqGYMHviI8+yrmg=="],
+ "tailwindcss": ["tailwindcss@4.1.11", "", {}, "sha512-2E9TBm6MDD/xKYe+dvJZAmg3yxIEDNRc0jwlNyDg/4Fil2QcSLjFKGVff0lAf1jjeaArlG/M75Ey/EYr/OJtBA=="],
+
"timer2": ["timer2@1.0.0", "", {}, "sha512-UOZql+P2ET0da+B7V3/RImN3IhC5ghb+9cpecfUhmYGIm0z73dDr3A781nBLnFYmRzeT1AmoT4w9Lgr8n7n7xg=="],
"toidentifier": ["toidentifier@1.0.1", "", {}, "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA=="],
@@ -706,6 +730,8 @@
"w3c-keyname": ["w3c-keyname@2.2.8", "", {}, "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ=="],
+ "werewolfUI": ["werewolfUI@workspace:packages/werewolf-ui"],
+
"which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="],
"which-boxed-primitive": ["which-boxed-primitive@1.1.1", "", { "dependencies": { "is-bigint": "^1.1.0", "is-boolean-object": "^1.2.1", "is-number-object": "^1.1.1", "is-string": "^1.1.1", "is-symbol": "^1.1.1" } }, "sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA=="],
@@ -750,6 +776,10 @@
"@sapphire/shapeshift/lodash": ["lodash@4.17.21", "", {}, "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="],
+ "@workshop/nano-remix/@types/bun": ["@types/bun@1.2.18", "", { "dependencies": { "bun-types": "1.2.18" } }, "sha512-Xf6RaWVheyemaThV0kUfaAUvCNokFr+bH8Jxp+tTZfx7dAPA8z9ePnP9S9+Vspzuxxx9JRAXhnyccRj3GyCMdQ=="],
+
+ "@workshop/nano-remix/hono": ["hono@4.8.4", "", {}, "sha512-KOIBp1+iUs0HrKztM4EHiB2UtzZDTBihDtOF5K6+WaJjCPeaW4Q92R8j63jOhvJI5+tZSMuKD9REVEXXY9illg=="],
+
"ajv/fast-deep-equal": ["fast-deep-equal@2.0.1", "", {}, "sha512-bCK/2Z4zLidyB4ReuIsvALH6w31YfAQDmXMqMx6FyfHqvBxtjC0eRumeSu4Bs3XtXwpyIywtSTrVT99BxY1f9w=="],
"amqplib/readable-stream": ["readable-stream@1.1.14", "", { "dependencies": { "core-util-is": "~1.0.0", "inherits": "~2.0.1", "isarray": "0.0.1", "string_decoder": "~0.10.x" } }, "sha512-+MeVjFf4L44XUkhM1eYbD8fyEsxcV81pqMSR5gblfcLCHfZvbrqy4/qYHE+/R5HoBUT11WV5O08Cr1n3YXkWVQ=="],
@@ -806,6 +836,10 @@
"string_decoder/safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="],
+ "werewolfUI/@types/bun": ["@types/bun@1.2.18", "", { "dependencies": { "bun-types": "1.2.18" } }, "sha512-Xf6RaWVheyemaThV0kUfaAUvCNokFr+bH8Jxp+tTZfx7dAPA8z9ePnP9S9+Vspzuxxx9JRAXhnyccRj3GyCMdQ=="],
+
+ "werewolfUI/hono": ["hono@4.8.4", "", {}, "sha512-KOIBp1+iUs0HrKztM4EHiB2UtzZDTBihDtOF5K6+WaJjCPeaW4Q92R8j63jOhvJI5+tZSMuKD9REVEXXY9illg=="],
+
"which-builtin-type/isarray": ["isarray@2.0.5", "", {}, "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw=="],
"@modelcontextprotocol/sdk/express/accepts": ["accepts@2.0.0", "", { "dependencies": { "mime-types": "^3.0.0", "negotiator": "^1.0.0" } }, "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng=="],
@@ -854,6 +888,8 @@
"@openai/agents/openai/ws": ["ws@8.18.2", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-DMricUmwGZUVr++AEAe2uiVM7UoO9MAVZMDu05UQOaUII0lp+zOzLLU4Xqh/JvTqklB1T4uELaaPBKyjE1r4fQ=="],
+ "@workshop/nano-remix/@types/bun/bun-types": ["bun-types@1.2.18", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-04+Eha5NP7Z0A9YgDAzMk5PHR16ZuLVa83b26kH5+cp1qZW4F6FmAURngE7INf4tKOvCE69vYvDEwoNl1tGiWw=="],
+
"amqplib/readable-stream/inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="],
"amqplib/readable-stream/string_decoder": ["string_decoder@0.10.31", "", {}, "sha512-ev2QzSzWPYmy9GuqfIVildA4OdcGLeFZQrq5ys6RtiuF+RQQiZWr8TZNyAcuVXyQRYfEO+MsoB/1BuQVhOJuoQ=="],
@@ -868,6 +904,8 @@
"morgan/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="],
+ "werewolfUI/@types/bun/bun-types": ["bun-types@1.2.18", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-04+Eha5NP7Z0A9YgDAzMk5PHR16ZuLVa83b26kH5+cp1qZW4F6FmAURngE7INf4tKOvCE69vYvDEwoNl1tGiWw=="],
+
"@modelcontextprotocol/sdk/express/accepts/negotiator": ["negotiator@1.0.0", "", {}, "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg=="],
"@modelcontextprotocol/sdk/express/body-parser/bytes": ["bytes@3.1.2", "", {}, "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg=="],
diff --git a/packages/nano-remix/package.json b/packages/nano-remix/package.json
index d683cd7..f5fca66 100644
--- a/packages/nano-remix/package.json
+++ b/packages/nano-remix/package.json
@@ -11,6 +11,7 @@
"typescript": "^5"
},
"dependencies": {
+ "bun-plugin-tailwind": "^0.0.15",
"hono": "catalog:"
},
"devDependencies": {
diff --git a/packages/nano-remix/scripts/build.ts b/packages/nano-remix/scripts/build.ts
new file mode 100644
index 0000000..8d50b4d
--- /dev/null
+++ b/packages/nano-remix/scripts/build.ts
@@ -0,0 +1,65 @@
+import { mkdirSync } from "node:fs"
+import { join, extname, dirname, basename } from "node:path"
+
+if (!import.meta.main) throw new Error("This script is intended to be run as a cli tool.")
+
+type BuildRouteOptions = {
+ distDir: string
+ routeName: string
+ filepath: string
+}
+/*
+ * Builds dynamic routes in a separate process to avoid Bun.build corruption.
+ * Running Bun.build in the same process as nanoRemix causes build failures after the first run.
+ * Shelling out to this script isolates the build process and prevents this issue.
+ */
+const buildDynamicRoute = async ({ distDir, routeName, filepath }: BuildRouteOptions) => {
+ const outDir = dirname(routeName)
+ const filename = basename(routeName) + extname(filepath)
+ const dynamicRouteFilepath = join(distDir, "routes", outDir, filename)
+ await mkdirSync(dirname(dynamicRouteFilepath), { recursive: true })
+
+ // Only import the Component so that tree-shaking will get rid of the server-side code
+ const code = `
+import Component from "${filepath}"
+import { wrapComponentWithLoader} from "@workshop/nano-remix"
+import { render } from 'hono/jsx/dom'
+
+const root = document.getElementById('root')
+const WrappedComponent = wrapComponentWithLoader(Component)
+render(, root)`
+
+ await Bun.write(dynamicRouteFilepath, code)
+
+ const result = await Bun.build({
+ entrypoints: [dynamicRouteFilepath],
+ outdir: join(distDir, outDir),
+ sourcemap: "inline",
+ target: "browser",
+ splitting: true,
+ plugins: [await import("bun-plugin-tailwind").then((m) => m.default)],
+ })
+
+ if (!result.success) {
+ throw new Error(`Build for "${routeName}" failed with exit code ${result.logs}.`)
+ }
+
+ return result
+}
+
+const args = process.argv.slice(2)
+
+if (args.length < 3) {
+ console.error("Usage: bun run scripts/build.ts ")
+ process.exit(1)
+}
+
+const [distDir, routeName, filepath] = args as [string, string, string]
+
+try {
+ await buildDynamicRoute({ distDir, routeName, filepath })
+ console.log(`✅ Successfully built route: ${routeName}`)
+} catch (error) {
+ console.error(`❌ Build failed:`, error)
+ process.exit(1)
+}
diff --git a/packages/nano-remix/src/buildDynamicRout.ts b/packages/nano-remix/src/buildDynamicRout.ts
new file mode 100644
index 0000000..63668a5
--- /dev/null
+++ b/packages/nano-remix/src/buildDynamicRout.ts
@@ -0,0 +1,29 @@
+import { join } from "node:path"
+
+type BuildRouteOptions = {
+ distDir: string
+ routeName: string
+ filepath: string
+}
+
+export const buildDynamicRoute = async ({ distDir, routeName, filepath }: BuildRouteOptions) => {
+ const scriptPath = join(import.meta.dirname, "../scripts/build.ts")
+
+ const proc = Bun.spawn({
+ cmd: ["bun", "run", scriptPath, distDir, routeName, filepath],
+ stdout: "pipe",
+ stderr: "pipe",
+ })
+
+ const exitCode = await proc.exited
+
+ if (exitCode !== 0) {
+ const stderr = await new Response(proc.stderr).text()
+ throw new Error(`Build process failed with exit code ${exitCode}: ${stderr}`)
+ }
+
+ const stdout = await new Response(proc.stdout).text()
+ console.log(stdout)
+
+ return { success: true }
+}
diff --git a/packages/nano-remix/src/nanoRemix.ts b/packages/nano-remix/src/nanoRemix.ts
index 7c1bed1..c10451e 100644
--- a/packages/nano-remix/src/nanoRemix.ts
+++ b/packages/nano-remix/src/nanoRemix.ts
@@ -1,6 +1,6 @@
import { renderServer } from "@/renderServer"
-import { mkdirSync } from "node:fs"
-import { join, extname, dirname, basename } from "node:path"
+import { buildDynamicRoute } from "./buildDynamicRout"
+import { join, extname } from "node:path"
type Options = {
routesDir?: string
@@ -29,11 +29,14 @@ export const nanoRemix = async (req: Request, options: Options = {}) => {
}
const routeName = route.name === "/" ? "/index" : route.name
+
+ // If the the route includes an extension it is a static file that we serve from the distDir
if (!ext) {
- await buildDynamicRoute(distDir, routeName, route.filePath) // Eventually this should be running only on initial build and when a route changes
+ await buildDynamicRoute({ distDir, routeName, filepath: route.filePath }) // Eventually this should be running only on initial build and when a route changes
return await renderServer(req, route)
} else {
const file = Bun.file(join(distDir, routeName + ext))
+
if (!(await file.exists())) {
return new Response("File Not Found", {
status: 404,
@@ -43,47 +46,3 @@ export const nanoRemix = async (req: Request, options: Options = {}) => {
return new Response(file)
}
}
-
-const buildDynamicRoute = async (distDir: string, routeName: string, filepath: string) => {
- const outDir = dirname(routeName)
- const filename = basename(routeName) + extname(filepath)
- const dynamicRouteFilepath = join(distDir, "routes", outDir, filename)
- await mkdirSync(dirname(dynamicRouteFilepath), { recursive: true })
-
- // Only import the Component so that tree-shaking will get rid of the server-side code
- const code = `
-import Component from "${filepath}"
-import { wrapComponentWithLoader} from "@workshop/nano-remix"
-import { render } from 'hono/jsx/dom'
-
-const root = document.getElementById('root')
-const WrappedComponent = wrapComponentWithLoader(Component)
-render(, root)`
-
- await Bun.write(dynamicRouteFilepath, code)
-
- // I would rather use Bun.build, but fails when run twice https://github.com/oven-sh/bun/issues/11123
- const proc = Bun.spawn({
- cmd: ["bun", "build", "--sourcemap=inline", "--outdir", join(distDir, outDir), dynamicRouteFilepath],
- stdout: "pipe",
- stderr: "pipe",
- })
-
- // Read the bundled output
- let bundled = ""
- for await (const chunk of proc.stdout) {
- bundled += new TextDecoder().decode(chunk)
- }
-
- let errorOutput = ""
- for await (const chunk of proc.stderr) {
- errorOutput += new TextDecoder().decode(chunk)
- }
-
- const exitCode = await proc.exited
- if (exitCode !== 0) {
- throw new Error(`Built for "${routeName}" failed with exit code ${exitCode}. ${errorOutput}`)
- }
-
- return bundled
-}