From 06b9217fee06b488a08949677cc85596cf4ddf22 Mon Sep 17 00:00:00 2001 From: Corey Johnson Date: Mon, 7 Jul 2025 15:30:58 -0700 Subject: [PATCH] werewolf --- bun.lock | 38 ++++++++++++ packages/nano-remix/package.json | 1 + packages/nano-remix/scripts/build.ts | 65 +++++++++++++++++++++ packages/nano-remix/src/buildDynamicRout.ts | 29 +++++++++ packages/nano-remix/src/nanoRemix.ts | 53 ++--------------- 5 files changed, 139 insertions(+), 47 deletions(-) create mode 100644 packages/nano-remix/scripts/build.ts create mode 100644 packages/nano-remix/src/buildDynamicRout.ts 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 -}