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 <noreply@anthropic.com>
Co-authored-by: Bryan Helmkamp <bryan@brynary.com>
This commit is contained in:
Zach Feldman 2026-06-12 08:23:56 -04:00 committed by GitHub
parent eff3a5a9cb
commit bc70da1a22
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 24 additions and 3 deletions

View file

@ -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",

9
bunfig.toml Normal file
View file

@ -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"