Skip to content

Restructure into a Bun workspace: apps/server, apps/web, packages/shared - #82

Merged
firsttris merged 2 commits into
masterfrom
structure
Nov 4, 2026
Merged

firsttris merged 2 commits into
masterfrom
structure

Conversation

@firsttris

Copy link
Copy Markdown
Owner

Brings the folder layout in line with haben and namarr: one Bun workspace with apps/* and packages/*. Nothing the app does changes.

Two commits so the renames are easy to follow:

  1. Renames only (327 files, all R100): backend/ → apps/server/, frontend/ → apps/web/, shared/*.ts → packages/shared/src/, dev/setup.sh → scripts/dev/setup.sh, start.sh → scripts/start.sh.
  2. Paths and the workspace: configs, imports, scripts, workflows, Docker, docs.

Layout

package.json          workspaces [apps/*, packages/*, e2e], packageManager bun@1.4.4, scripts dev / demo
bun.lock              the one lockfile (replaces backend/, frontend/, e2e/bun.lock)
bunfig.toml           [install] linker = "isolated"
tsconfig.base.json    the options all tsconfigs share
apps/
  server/             the backend (was backend/)
  web/                the frontend (was frontend/), still carries the app's version
packages/
  shared/             @snapraid-ui/shared, sources in src/ (was shared/)
e2e/                  unchanged, now a workspace
scripts/
  start.sh            was ./start.sh (bun run dev, bun run demo)
  dev/setup.sh        was dev/setup.sh; the sandbox, bin and .cache now sit next to it
  install.sh          the systemd installer, unchanged
  build-binary.sh, social-preview/
docker/, docs/, config.example.json   unchanged places

Decisions

  • @shared/ stays as the import specifier. Importing @snapraid-ui/shared/… by name would touch 192 imports in 152 files. Instead @snapraid-ui/shared is a real workspace package (workspace:* dependency of server and web, exports: {"./*": "./src/*"}). The @shared/* path in both tsconfigs now points at the linked package, ./node_modules/@snapraid-ui/shared/src/*, not at ../shared/*. Bun (runtime and --compile), Vite and Vitest resolve it through the workspace link.
  • Isolated linker fixes the inlang problem. With linker = "isolated", apps/web/node_modules/@inlang/plugin-message-format is a link of the web package itself, so project.inlang/settings.json (./node_modules/@inlang/...) loads it unchanged. paraglide-js compile ran on a clean src/paraglide and produced every message module.
  • One lockfile, same versions. I built bun.lock by merging the three old lockfiles, not by resolving again. The one conflict was lru-cache (mqtt needs 10.4.3, the frontend has 11.5.3); it is nested as mqtt/lru-cache. After that Bun rewrote the file in its own format. Checks: the set of name@version in the new lock is identical to the union of the three old ones (936 entries, empty diff), and all 58 direct dependencies of the three packages are installed at the version their old lock had. bun install --frozen-lockfile passes. The Docker backend's --production --filter ./apps/server install gives the same 158 packages as before.
  • packageManager only in the root. bun-version-file: package.json in every job, and the Renovate regex now targets ^package\.json$.
  • Version stays in apps/web/package.json. The bump, release, version.ts and release.sh all read it there, so the release flow is unchanged.
  • Root install.sh is deleted. It only ran bun install in frontend/ and backend/, and bun install in the root does that now. Nothing linked to it: the public installer is the release asset releases/latest/download/install.sh, which release.yml copies from scripts/install.sh. That file and its URL are unchanged.
  • start.sh and dev/ moved to scripts/. bun run dev and bun run demo in the root call scripts/start.sh. README, docs, Renovate notes and the e2e demo harness are updated.
  • Default data directory unchanged. Without SNAPRAID_BASE_PATH, the backend run from the repository still uses snapraid/ in the repository's root. config.ts now goes ../../snapraid from apps/server instead of ../snapraid from backend. The image (env var) and the binary (its own default) are unaffected.
  • Paths inside the image and binary follow the repository. In the image: /app/apps/server (WORKDIR), /app/apps/web/.output, /app/packages/shared, /app/node_modules (the isolated store), /app/docs. In the binary the assets sit at /$bunfs/root/apps/web/… and /$bunfs/root/docs/…. app-files.ts still resolves everything from the repository's root (../../../ from apps/server/src). The data volume /app/snapraid and the env vars are unchanged.
  • Screenshots stay where they are. docs/screenshot*.png and docs/social-preview.png are referenced from the README, which is also the Docker Hub description, and are likely linked from outside. They stay put.
  • config.example.json stays in the root.

How no behaviour change was checked (locally, Bun 1.4.4)

Check Before (master) After
backend bun test 233 pass, 2 skip 233 pass, 2 skip
backend tsc ok ok
frontend Vitest 60 passed, 13 files 60 passed, 13 files
frontend biome check, typecheck (with paraglide), build ok ok
e2e --list / screenshots --list 34 / 14 34 / 14
e2e typecheck ok ok
tsc --showConfig (server, web, e2e) — the same compiler options, only the @shared/* target differs
client build .output/public 79 files the same 79 files, byte-identical (sha256)
mkdocs build --strict — ok
  • e2e local target: 32 passed, 2 skipped. An earlier run on this branch failed "deleted and encrypted files are restored…" with a strict-mode double toast; that test passed alone twice and changes.spec passed as a whole.
  • e2e binary target: 33 passed, 1 skipped. An earlier run had one "Target crashed" (Chromium); array.spec run alone passed.
  • For comparison, master local on the same machine: 29 passed, 3 failed, all three "Target crashed". So those crashes come from the environment (the shared disk was nearly full at times), not from this change.
  • Binary: --version prints 1.3.3 from the embedded apps/web/package.json. It serves the UI (200), /api/health, and the MCP endpoint lists all 14 doc pages.
  • bun run demo: builds the sandbox under scripts/dev/, seeds it, starts backend and Vite. Health, config and status answer, and Vite serves the page.
  • Screenshot harness: 3 tests of readme.spec.ts passed (scripts/dev sandbox, social preview with apps/web/public/favicon.svg). The pictures were not committed.
  • Docker stages, run by hand without Docker:
    • backend --production --filter ./apps/server install, then bun src/main.ts in apps/server with the image's layout serves UI and API;
    • frontend --filter ./apps/web install plus build gives a byte-identical client.
    • This caught a missing tsconfig.base.json in the build context, now copied in.

Not verified here

  • The server (SSR) bundle is not byte-identical. Rolldown names and splits chunks by module path, and the isolated store changes those paths (node_modules/.bun/...). Package versions and sources are the same. SSR is covered by the e2e runs.
  • No real docker build and no e2e image target: no Docker daemon and too little disk here. CI's image job is the real check.
  • CI workflows themselves: checked only by parsing the YAML. Not run: shellcheck, the systemd install job, bun --bun playwright install --with-deps, and the screenshots workflow in the Playwright image (its lockfile: bun.lock input relies on the action's grep for @playwright/test@x.y.z, which matches the root lock).
  • The Renovate config: only parsed, the validator was not run.
  • Stale version in bun.lock after a bump: the root bun.lock records the web workspace's version (1.3.3). After a bump, bun install --frozen-lockfile still passes (tested with 1.3.4), but the next plain bun install will rewrite that one line.
  • Old local dev/ sandboxes: an existing dev/ (bin, sandbox) in someone's clone is no longer ignored by git. Move dev/bin to scripts/dev/bin, or run scripts/dev/setup.sh --reset.

Generated by Claude Code

backend/ -> apps/server/, frontend/ -> apps/web/, shared/ -> packages/shared/src/,
dev/setup.sh -> scripts/dev/setup.sh, start.sh -> scripts/start.sh. Nothing but the
renames, so the history follows them; the paths are fixed in the next commit.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Y3rdfbWb1xDUdPRng7TX9Z
The paths the moves changed, and the workspace around them:

- Root package.json with workspaces (apps/*, packages/*, e2e) and packageManager,
  one bun.lock instead of three (every resolved version as before), bunfig.toml
  with the isolated linker, so apps/web/node_modules/@inlang/... is still where
  project.inlang/settings.json loads its plugin from, and tsconfig.base.json with
  the options all three tsconfigs share (tsc --showConfig: the same options).
- packages/shared is the workspace package @snapraid-ui/shared. The 192 imports in
  152 files keep their @shared/ specifier; the tsconfig paths now point at the linked
  package in node_modules instead of ../shared.
- The backend's default data directory stays snapraid/ in the repository's root
  (../../snapraid from apps/server); the server finds the built frontend, its
  package.json and docs/ at apps/web/... from the root, in the binary too.
- scripts/start.sh and scripts/dev/setup.sh, with bun run dev and bun run demo
  in the root; the old install.sh (bun install in two folders) is replaced by
  bun install in the root. The installer's URL (the release asset from
  scripts/install.sh) does not change.
- Dockerfile, .dockerignore, .gitignore, CI, release, bump, screenshots,
  upstream-release and Renovate, the e2e harness, build-binary.sh, docs, README.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Y3rdfbWb1xDUdPRng7TX9Z

Copy link
Copy Markdown
Owner Author

TDD review (41b2e8a): 1 finding with a failing test, no other regressions found

Finding: after a "Bump version" run, bun.lock keeps the old version

Where: bun.lock lines 26–28 (the "apps/web" workspace entry, "version": "1.3.3"), together with .github/workflows/bump.yml (version-file: apps/web/package.json).

Why: frontend/bun.lock on master had frontend/ as its root workspace (""), and Bun writes no version for the root workspace. In the new workspace lock apps/web is a member workspace, and Bun stores its version there. The shared bump workflow (firsttris/workflows bump-version.yml) rewrites the version file, plus a package-lock.json or Cargo.lock next to it. It does not touch bun.lock. So every release commit leaves bun.lock stale. --frozen-lockfile accepts that (checked with Bun 1.4.4), so CI, the image and the release keep working. But the next plain bun install, locally or in Renovate's lock maintenance, rewrites bun.lock and leaves a diff nobody asked for.

Test (script test, not committed: it simulates the shared workflow): bump-lock.test.sh <repo> <ref>. It takes the package.json/bun.lock/bunfig.toml files of the ref, raises the version in the version file the way the bump workflow does, runs bun install --lockfile-only, and expects bun.lock to be unchanged.

$ bump-lock.test.sh . origin/master
PASS origin/master: bun.lock unchanged after the bump
$ bump-lock.test.sh . origin/structure
--- before.lock
+++ ./bun.lock
@@ -25,7 +25,7 @@
     },
     "apps/web": {
       "name": "snapraid-ui",
-      "version": "1.3.3",
+      "version": "9.9.9",
FAIL origin/structure: the bump leaves bun.lock stale

Not fixed in this PR. Both fixes I can see need a decision first:
(a) teach the shared bump-version.yml in firsttris/workflows to update the workspace version in bun.lock (a bun.lock next to a package.json, or the root bun.lock for a workspace member), or
(b) make the root package.json the version source (Bun writes no version for the root workspace). That changes version.ts, build-binary.sh, the Dockerfile, release.yml/bump.yml and binary.spec.ts.

Checked, no regression (verified, not just read)

  • Image layout (Dockerfile stages rebuilt in a scratch directory: bun install --frozen-lockfile --production --filter ./apps/server, then the final stage's COPYs). apps/server/node_modules/@snapraid-ui/shared links to ../../../../packages/shared, and @shared/ resolves at runtime through the copied tsconfig.base.json and tsconfig.json. bun src/main.ts in /app/apps/server serves /api/health and the UI and reports version 1.3.3 from apps/web/package.json. --filter ./apps/web plus bun run build builds .output. The PR's CI built and ran the real image (e2e image 1/2 and 2/2 green).
  • Binary (scripts/build-binary.sh): --version prints 1.3.3 from /$bunfs/root/apps/web/package.json, and the embedded UI is served.
  • Default data folder without SNAPRAID_BASE_PATH: from apps/server it is <repo>/snapraid, the same folder as before.
  • scripts/dev/setup.sh --reset (with seed-demo and @shared/) works. bun run demo (fish scripts/start.sh --demo) starts the backend on :8080 and Vite on :3000.
  • Frozen lockfile: bun install --frozen-lockfile at the root makes no changes. The playwright-image action finds @playwright/test@1.64.1 in the root bun.lock.
  • Unchanged by this PR: scripts/install.sh and the systemd unit only use the release binary, so existing installs upgrade as before. The release binaries job runs the same commands as CI's green install job. bump.yml and release.yml version-file, the upstream-release.yml paths and the Renovate file patterns point to files that exist. CI's paths-ignore has no package paths.

Generated by Claude Code

Copy link
Copy Markdown
Owner Author

TDD-Review abgeschlossen.

  • 1 belegtes Finding: Nach dem Bump-Workflow blieb die Version von apps/web im neuen Root-bun.lock stehen. Gefixt im gemeinsamen Workflow (bump-version: also update the member's version in bun.lock workflows#39, v1 nachgezogen auf 35a6249), getestet mit tests/bump-version/test.sh (vorher rot, jetzt grün) und an einer Kopie dieses Repos.
  • Alle übrigen Pfade (Datenordner, Image, Binary, Demo, systemd-Install, Release, Renovate) geprüft und verworfen.
  • CI grün: backend, frontend, install, e2e local/binary/image je 2/2.

Merge per Squash.


Generated by Claude Code

@firsttris
firsttris merged commit f108ea7 into master Nov 4, 2026
9 checks passed
@firsttris
firsttris deleted the structure branch November 4, 2026 04:11
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants