From bc70da1a22bd651fff9cb8995bca5ae4e9b4775c Mon Sep 17 00:00:00 2001 From: Zach Feldman Date: Fri, 12 Jun 2026 08:23:56 -0400 Subject: [PATCH] fix(web): resolve Bun workspace-hoisted node_modules in build script (#495) ## Why The build script in `apps/fabro-web/scripts/build.ts` hardcoded two paths that assumed packages live in `apps/fabro-web/node_modules/`: - `./node_modules/.bin/tailwindcss` (the Tailwind CLI invocation) - `join(rootPath, "node_modules", "@pierre", "diffs", ...)` (the worker asset copy) This repo uses Bun workspaces (root `package.json` has `workspaces: ['apps/*', 'lib/packages/*']`), so `bun install` hoists all packages to the repo root. Any fresh contributor install broke `bun run dev` immediately with: ``` ENOENT: no such file or directory, posix_spawn './node_modules/.bin/tailwindcss' ``` followed by: ``` ENOENT: no such file or directory, lstat '.../apps/fabro-web/node_modules/@pierre/diffs/...' ``` ## What changed - `tailwindcss` is now resolved via `Bun.which("tailwindcss")`, which searches `PATH` and the workspace root `node_modules/.bin/`, with the old path as fallback. - `pierreWorkerDir` now resolves from a `workspaceRoot` derived via `new URL("../../..", import.meta.url)` (repo root), matching where Bun actually installs workspace dependencies. ## Verification `bun run dev` from `apps/fabro-web/` completes a full build successfully after a clean `bun install` from the repo root. --------- Co-authored-by: Claude Sonnet 4.6 Co-authored-by: Bryan Helmkamp --- apps/fabro-web/scripts/build.ts | 18 +++++++++++++++--- bunfig.toml | 9 +++++++++ 2 files changed, 24 insertions(+), 3 deletions(-) create mode 100644 bunfig.toml diff --git a/apps/fabro-web/scripts/build.ts b/apps/fabro-web/scripts/build.ts index 601c5db34..2e7c967fb 100644 --- a/apps/fabro-web/scripts/build.ts +++ b/apps/fabro-web/scripts/build.ts @@ -10,7 +10,7 @@ import { symlink, writeFile, } from "node:fs/promises"; -import { join, relative } from "node:path"; +import { dirname, join, relative } from "node:path"; declare const Bun: any; @@ -20,9 +20,20 @@ const buildsRootDir = join(rootPath, ".dist-builds"); const distPath = join(rootPath, "dist"); const publicDir = join(rootPath, "public"); const templatePath = join(rootPath, "index.template.html"); -const pierreWorkerDir = join(rootPath, "node_modules", "@pierre", "diffs", "dist", "worker"); const watch = Bun.argv.includes("--watch"); +// Locate dependencies through module resolution rather than hardcoded +// node_modules paths: where packages land on disk depends on the Bun install +// linker (hoisted puts them at the workspace root, isolated symlinks them into +// the app's node_modules), so any fixed path breaks on one of the layouts. +const pierreWorkerDir = join(dirname(Bun.resolveSync("@pierre/diffs", rootPath)), "worker"); + +const tailwindCliPackageJsonPath = Bun.resolveSync("@tailwindcss/cli/package.json", rootPath); +const tailwindCliBin = join( + dirname(tailwindCliPackageJsonPath), + JSON.parse(await readFile(tailwindCliPackageJsonPath, "utf8")).bin.tailwindcss, +); + function newBuildId(): string { return `${Date.now()}-${Math.random().toString(36).slice(2, 10)}`; } @@ -47,7 +58,8 @@ async function buildOnce() { } const cssResult = await Bun.spawn([ - "./node_modules/.bin/tailwindcss", + process.execPath, + tailwindCliBin, "-i", "app/app.css", "-o", diff --git a/bunfig.toml b/bunfig.toml new file mode 100644 index 000000000..f783a0e5a --- /dev/null +++ b/bunfig.toml @@ -0,0 +1,9 @@ +# Pin the install linker so every contributor's bun install produces the same +# node_modules layout. Bun < 1.3 defaulted workspaces to the hoisted linker +# (packages at the workspace root); Bun >= 1.3 defaults to isolated (packages +# symlinked into each app's node_modules from a central store). Tooling that +# touches node_modules should not have to handle both. +# +# Requires Bun >= 1.3. +[install] +linker = "isolated"