Skip to content

chore: migrate to bun#13

Merged
rsbh merged 6 commits intomainfrom
chore_use_bun
Feb 20, 2026
Merged

chore: migrate to bun#13
rsbh merged 6 commits intomainfrom
chore_use_bun

Conversation

@rsbh
Copy link
Copy Markdown
Member

@rsbh rsbh commented Feb 20, 2026

Summary

  • Migrate package manager from pnpm to bun (workspaces, lockfile)
  • Replace tsup bundler with Bun.build() API (target: node, output runs on node)
  • Replace relative imports with @/ path aliases across 33 files
  • Remove tsx devDependency (bun runs TS natively)
  • Rewrite Dockerfile with oven/bun:1.3 base image
  • Add serve CLI command (build + start) for production containers
  • Add /api/health endpoint for K8s readiness/liveness probes

Notes

  • Users only need node to run Chronicle CLI (#!/usr/bin/env node)
  • bun is a dev/build dependency only
  • Docker image uses bun internally for install and build

Test plan

  • bun install — clean install
  • bun build-cli.ts — CLI builds
  • node bin/chronicle.js --help — CLI runs on node
  • node bin/chronicle.js build — Next.js production build passes (271 pages)
  • docker build . — Docker image builds
  • docker run with content volume — serve command works
  • K8s /api/health probe returns 200

🤖 Generated with Claude Code

rsbh and others added 5 commits February 20, 2026 12:08
- Remove packageManager and engines from root package.json
- Add workspaces to root package.json (replaces pnpm-workspace.yaml)
- Remove engines from packages/chronicle/package.json
- Delete pnpm-workspace.yaml and pnpm-lock.yaml
- Generate bun.lock via bun install

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Create build-cli.ts using Bun.build() API (target: node, format: esm)
- Update build:cli script to use bun build-cli.ts
- Delete tsup.config.ts
- Remove tsup from devDependencies

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Convert ~54 relative imports across 33 files to use @/* aliases
(tsconfig paths already configured). Only remaining relative import
is ../../.source/server (outside src/).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Rewrite Dockerfile with oven/bun:1.3 base image
- Replace pnpm with bun install/build
- Add serve command (build + start) for production containers
- Add /api/health endpoint for K8s readiness/liveness probes
- Default CMD changed from dev to serve

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Feb 20, 2026

📝 Walkthrough

Summary by CodeRabbit

  • New Features

    • Added a new "serve" command for production deployments with automatic build and server startup.
    • Added a health check endpoint for monitoring.
  • Chores

    • Migrated build and packaging workflows from pnpm to Bun.
    • Converted project to use workspace-style layout and updated internal module resolution (import paths).

Walkthrough

Migrates tooling from pnpm/tsup to Bun (Dockerfile, package.json, build scripts), standardizes imports to @/ aliases across the chronicle package, adds a new CLI serve command, and introduces a /api/health endpoint.

Changes

Cohort / File(s) Summary
Build & Package Manager Migration
Dockerfile, package.json, packages/chronicle/package.json, packages/chronicle/build-cli.ts, packages/chronicle/tsup.config.ts
Switched from pnpm+tsup to Bun: base image -> oven/bun:1.3, use bun install and bun build-cli.ts, added new Bun build script, removed tsup.config.ts, updated Docker multi-stage copy/runner and CMD from dev to serve.
Workspace Configuration
package.json, pnpm-workspace.yaml
Moved workspace globs into package.json (workspaces: ["packages/*","examples/*"]) and removed pnpm-workspace.yaml and packageManager field.
CLI Commands & Index
packages/chronicle/src/cli/index.ts, packages/chronicle/src/cli/commands/serve.ts, packages/chronicle/src/cli/commands/start.ts, packages/chronicle/src/cli/commands/build.ts, packages/chronicle/src/cli/commands/dev.ts, packages/chronicle/src/cli/commands/init.ts, packages/chronicle/src/cli/utils/*, packages/chronicle/build-cli.ts
Added serve CLI command (builds Next.js, sets CHRONICLE_CONTENT_DIR, handles signals, starts production server) and registered it; updated various CLI imports to @/ aliases; included Bun-based CLI build entry.
Import Path Standardization (App & API Routes)
packages/chronicle/src/app/..., packages/chronicle/src/app/api/...
Replaced relative imports with @/ path aliases across Next.js pages, layouts, and API route files (including new /api/health route).
Import Path Standardization (Components, UI & Themes)
packages/chronicle/src/components/..., packages/chronicle/src/components/ui/..., packages/chronicle/src/themes/...
Updated component and theme files to use @/ aliases for imports and types (default and paper themes, layouts, pages, UI components).
Import Path Standardization (Lib & Types)
packages/chronicle/src/lib/..., packages/chronicle/src/components/api/..., packages/chronicle/src/components/mdx/index.tsx
Converted library and shared type imports to @/ aliases (config, openapi, schema, snippet-generators, types, api-routes).
New API Endpoint
packages/chronicle/src/app/api/health/route.ts
Added GET handler returning { status: 'ok' } for health checks.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  actor User
  participant CLI as Chronicle CLI
  participant OS as OS / Signals
  participant Builder as Next.js Build (child)
  participant Server as Next.js Start (prod server)

  User->>CLI: run `chronicle serve --port <n> --content <dir>`
  CLI->>CLI: resolve content dir, load CLI config, set env CHRONICLE_CONTENT_DIR
  CLI->>Builder: spawn `next build`
  OS->>CLI: (SIGINT/SIGTERM) if received
  CLI->>Builder: forward signal -> terminate build
  Builder-->>CLI: exit (success)
  CLI->>Server: spawn `next start --port <n>`
  Server-->>User: serve production app on port <n>
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Suggested reviewers

  • rohilsurana
🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title 'chore: migrate to bun' accurately describes the main objective: migrating the project's package manager and build tooling from pnpm/tsup to bun.
Description check ✅ Passed The description comprehensively outlines the changeset with clear sections for migration goals, implementation details, and testing status, directly related to the changes in the PR.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch chore_use_bun

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
Dockerfile (1)

18-35: ⚠️ Potential issue | 🟠 Major

Runner container runs as root — add a non-root USER directive.

The previous Dockerfile had chown -R node:node /app and USER node, both of which were dropped. The Trivy scanner (DS-0002) flags this: no USER command with a non-root user is present in the image. Running production containers as root is a security risk and often blocked by Kubernetes PodSecurityAdmission policies.

The symlink into /usr/local/bin/ must happen before the user is switched since it requires root, but that is already the case here.

🔒 Proposed fix — add non-root user to runner
 # --- runner ---
 FROM base AS runner
 WORKDIR /app/packages/chronicle
 
 COPY --from=builder /app /app
 
-RUN chmod +x bin/chronicle.js
-RUN ln -s /app/packages/chronicle/bin/chronicle.js /usr/local/bin/chronicle
-
-RUN mkdir -p /app/content && ln -s /app/content /app/packages/chronicle/content
+RUN chmod +x bin/chronicle.js \
+    && ln -s /app/packages/chronicle/bin/chronicle.js /usr/local/bin/chronicle \
+    && mkdir -p /app/content \
+    && ln -s /app/content /app/packages/chronicle/content \
+    && chown -R bun:bun /app
 
 VOLUME /app/content
 
 ENV CHRONICLE_CONTENT_DIR=./content
-WORKDIR /app/packages/chronicle
 
 EXPOSE 3000
 
+USER bun
+
 ENTRYPOINT ["chronicle"]
 CMD ["serve", "--port", "3000"]
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Dockerfile` around lines 18 - 35, The runner stage in the Dockerfile
currently leaves the container running as root (ENTRYPOINT ["chronicle"]) which
triggers security scanners; add a non-root user switch: after creating the
/usr/local/bin/chronicle symlink and after ensuring ownership of /app is set
(e.g., chown -R node:node /app), add a USER directive to run as that non-root
user (USER node or similar) so the container no longer runs as root; ensure the
symlink creation (ln -s /usr/local/bin/chronicle) remains before the USER change
and that CHRONICLE_CONTENT_DIR and VOLUME usage still work under the non-root
user.
🧹 Nitpick comments (2)
packages/chronicle/src/app/api/health/route.ts (1)

1-3: Health probe is liveness-only; ensure K8s readiness semantics are met.

The endpoint always returns { status: 'ok' } unconditionally, making it suitable as a liveness probe (HTTP server is running). As a readiness probe it offers no guarantee that the app can actually serve content — it doesn't verify config loading, content source availability, or any other initialization.

For this documentation site the risk is low if all dependencies are resolved at build time, but if Chronicle reads config or OpenAPI specs from disk at runtime, K8s could route traffic before those are available. Consider either:

  • Keeping this as a liveness probe and introducing a separate /api/ready endpoint that performs a lightweight sanity check (e.g., loadConfig() succeeds), or
  • Documenting explicitly that this probe only signals HTTP availability, not content readiness.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/chronicle/src/app/api/health/route.ts` around lines 1 - 3, The GET
handler in route.ts currently always returns a static Response.json({ status:
'ok' }) making it only a liveness probe; add a proper readiness check instead of
or alongside this by implementing a new readiness endpoint (e.g., export
function READY() or export function GET for /api/ready) which calls your
initialization sanity checks (e.g., loadConfig(), validateOpenApiSpecs(), or
another isReady() helper) and returns 200 only if those succeed and 503
otherwise; update the existing GET in route.ts to remain as the liveness probe
or document its liveness-only semantics so Kubernetes can use /api/ready for
readiness checks.
packages/chronicle/src/cli/index.ts (1)

6-6: Stale process.on signal handlers in serve build phase.

From the serve.ts implementation (provided as context), process.on('SIGINT', ...) and process.on('SIGTERM', ...) are registered against buildChild but are never removed after the build completes and startChild is spawned. During next start, SIGINT/SIGTERM will trigger both the stale no-op handler and whatever attachLifecycleHandlers registers — functionally correct only because killing an already-closed child is silently ignored, but it leaks listeners.

♻️ Suggested fix in serve.ts
- process.on('SIGINT', () => buildChild.kill('SIGINT'))
- process.on('SIGTERM', () => buildChild.kill('SIGTERM'))

+ const onSigInt  = () => buildChild.kill('SIGINT')
+ const onSigTerm = () => buildChild.kill('SIGTERM')
+ process.on('SIGINT',  onSigInt)
+ process.on('SIGTERM', onSigTerm)

  buildChild.on('close', (code) => {
+   process.off('SIGINT',  onSigInt)
+   process.off('SIGTERM', onSigTerm)
    if (code !== 0) {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/chronicle/src/cli/index.ts` at line 6, The serve build phase
registers SIGINT/SIGTERM handlers on process for buildChild which remain after
the build completes and startChild is spawned; update the build flow in serve
(where buildChild is created) to register the handlers as named functions and
remove them (using process.off/removeListener) immediately after the build
finishes and before spawning startChild so the stale handlers do not leak;
ensure the same named handlers are not reused by attachLifecycleHandlers for
startChild to avoid double-registration and that any cleanup runs on both normal
completion and error paths.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@package.json`:
- Around line 1-5: Restore the Node engine constraint in package.json by adding
an "engines" field with "node": ">=22" to ensure end users get a warning on
older Node versions; optionally add "packageManager": "bun@1.3.x" to pin Bun for
contributors and enable corepack enforcement. Update the top-level package.json
(the file that currently contains name/private/workspaces) to include these keys
so tooling and install-time checks can surface the required runtime and
contributor environment. Ensure the exact string is "node": ">=22" and the
optional packageManager matches "bun@1.3.x".

In `@packages/chronicle/build-cli.ts`:
- Around line 3-11: Bun.build(...) returns a BuildOutput with a success flag
that must be checked; update the build invocation in build-cli.ts to capture the
result (the value returned by Bun.build) and inspect result.success, logging
result.errors or result.logs via console.error or processLogger and calling
process.exit(1) (or throwing) when success is false so the script fails non‑zero
when the bundle is broken; reference the Bun.build call and the
BuildOutput.result.success/result.errors fields when adding the check.

In `@packages/chronicle/src/cli/commands/serve.ts`:
- Around line 33-34: Replace the persistent signal listeners for the build phase
with one-time handlers: change the process.on('SIGINT', ...) and
process.on('SIGTERM', ...) registrations that call buildChild.kill(...) to
process.once(...) so the build-phase SIGINT/SIGTERM handlers remove themselves
after first invocation; this prevents stale buildChild.kill(...) calls from
running later (and potentially throwing ESRCH) after buildChild has exited and
ensures attachLifecycleHandlers(startChild)'s listeners run without being
interrupted by stale handlers targeting startChild.

---

Outside diff comments:
In `@Dockerfile`:
- Around line 18-35: The runner stage in the Dockerfile currently leaves the
container running as root (ENTRYPOINT ["chronicle"]) which triggers security
scanners; add a non-root user switch: after creating the
/usr/local/bin/chronicle symlink and after ensuring ownership of /app is set
(e.g., chown -R node:node /app), add a USER directive to run as that non-root
user (USER node or similar) so the container no longer runs as root; ensure the
symlink creation (ln -s /usr/local/bin/chronicle) remains before the USER change
and that CHRONICLE_CONTENT_DIR and VOLUME usage still work under the non-root
user.

---

Duplicate comments:
In `@packages/chronicle/src/cli/commands/start.ts`:
- Around line 5-7: This file repeats the same alias import and ambient
PACKAGE_ROOT declaration problem as build.ts; update start.ts to match the fix
used in build.ts by avoiding duplicate '@/cli/utils' imports and removing or
replacing the ambiguous declare const PACKAGE_ROOT with the concrete
runtime/config source you used in build.ts (e.g., import or read from the shared
config/env helper). Specifically adjust where resolveContentDir, loadCLIConfig,
and attachLifecycleHandlers are imported so they come from the single canonical
module, and replace the declare const PACKAGE_ROOT usage with the same explicit
value-access approach you implemented in build.ts.

---

Nitpick comments:
In `@packages/chronicle/src/app/api/health/route.ts`:
- Around line 1-3: The GET handler in route.ts currently always returns a static
Response.json({ status: 'ok' }) making it only a liveness probe; add a proper
readiness check instead of or alongside this by implementing a new readiness
endpoint (e.g., export function READY() or export function GET for /api/ready)
which calls your initialization sanity checks (e.g., loadConfig(),
validateOpenApiSpecs(), or another isReady() helper) and returns 200 only if
those succeed and 503 otherwise; update the existing GET in route.ts to remain
as the liveness probe or document its liveness-only semantics so Kubernetes can
use /api/ready for readiness checks.

In `@packages/chronicle/src/cli/index.ts`:
- Line 6: The serve build phase registers SIGINT/SIGTERM handlers on process for
buildChild which remain after the build completes and startChild is spawned;
update the build flow in serve (where buildChild is created) to register the
handlers as named functions and remove them (using process.off/removeListener)
immediately after the build finishes and before spawning startChild so the stale
handlers do not leak; ensure the same named handlers are not reused by
attachLifecycleHandlers for startChild to avoid double-registration and that any
cleanup runs on both normal completion and error paths.

…cleanup

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
package.json (1)

1-8: LGTM — workspaces correctly replaces pnpm-workspace.yaml, and engines.node is preserved.

One optional follow-up from the previous review cycle: consider pinning the Bun version with "packageManager": "bun@1.3.x" so corepack (or contributors) get a hard enforcement signal instead of silently using whatever Bun version is installed locally.

✨ Optional addition
 {
   "name": "chronicle",
   "private": true,
   "workspaces": ["packages/*", "examples/*"],
+  "packageManager": "bun@1.3.x",
   "engines": {
     "node": ">=22"
   }
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@package.json` around lines 1 - 8, Add a top-level "packageManager" field to
package.json to pin Bun (e.g. "bun@1.3.x") so contributors get enforced tooling
via corepack; modify the existing package.json object that currently contains
"name", "private", "workspaces", and "engines" by adding the "packageManager"
key alongside them (ensure the value matches the desired Bun semver like
"bun@1.3.x" and is consistent with your CI/tooling).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/chronicle/build-cli.ts`:
- Around line 3-11: The build currently bakes PACKAGE_ROOT using
path.resolve(import.meta.dir) at build time in Bun.build, which embeds an
absolute developer path; change this so PACKAGE_ROOT is resolved at runtime
instead: remove or stop defining PACKAGE_ROOT in the Bun.build.define block and
instead compute the package root inside the CLI code (e.g., in src/cli/index.ts
where you join PACKAGE_ROOT with 'node_modules/.bin/next') using a runtime base
computed from import.meta.url (or an equivalent __dirname fallback) so the
installed package can locate its own node_modules relative to where it is
installed rather than the build machine. Ensure you also remove the redundant
path.resolve(import.meta.dir) usage.

---

Duplicate comments:
In `@packages/chronicle/build-cli.ts`:
- Around line 13-15: The build-failure handling using result.success, iterating
result.logs with console.error, and calling process.exit(1) is correct; no
change required—keep the existing logic as-is (leave the result.success check,
the for-loop over result.logs, and the process.exit(1) call intact).

---

Nitpick comments:
In `@package.json`:
- Around line 1-8: Add a top-level "packageManager" field to package.json to pin
Bun (e.g. "bun@1.3.x") so contributors get enforced tooling via corepack; modify
the existing package.json object that currently contains "name", "private",
"workspaces", and "engines" by adding the "packageManager" key alongside them
(ensure the value matches the desired Bun semver like "bun@1.3.x" and is
consistent with your CI/tooling).

@rsbh rsbh merged commit 551881e into main Feb 20, 2026
1 check passed
@rsbh rsbh deleted the chore_use_bun branch March 16, 2026 04:47
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