12 KiB
Migration Guide: Replacing Bun.file with Node.js APIs
Goal: Replace all Bun.file() and Bun.write() calls in the opencode codebase with the Filesystem utility module from src/util/filesystem.ts. This utility provides Node.js fs/promises equivalents optimized for performance.
What to do: Pick one unchecked file from the checklist below, migrate it using the Filesystem module, ensure tests pass, open a PR, and check off the file once merged. Repeat until all files are done.
Migration Process
Each file should be migrated one at a time with a separate PR opened for each file. This approach ensures:
- Easier code review and debugging
- Isolated changes that can be rolled back independently
- Clear tracking of which files have been migrated
- Reduced risk of introducing multiple bugs at once
Workflow:
- Pick one unchecked file from the checklist below
- Create a new branch for that file
- Migrate all
Bun.file()and related APIs to Node.js equivalents using the Filesystem module - Run tests for the specific file and ensure they pass before pushing
- Open a PR specifically for that single file
- Once merged, check off the file in the checklist below
Using the Filesystem Module
Use the Filesystem module from src/util/filesystem.ts for all file operations:
import { Filesystem } from "./util/filesystem"
// Before: Bun.file().exists()
const exists = await Bun.file(path).exists()
// After: Filesystem.exists()
const exists = await Filesystem.exists(path)
// Before: Bun.file().text()
const content = await Bun.file(path).text()
// After: Filesystem.readText()
const content = await Filesystem.readText(path)
// Before: Bun.file().json()
const data = await Bun.file(path).json()
// After: Filesystem.readJson()
const data = await Filesystem.readJson<Config>(path)
// Before: Bun.file().bytes()
const bytes = await Bun.file(path).bytes()
// After: Filesystem.readBytes()
const buffer = await Filesystem.readBytes(path)
// Before: Bun.file().stat()
const stats = await Bun.file(path).stat()
// After: Filesystem.isDir() and size
const isDir = await Filesystem.isDir(path)
const size = await Filesystem.size(path)
// Before: Bun.file().size
const size = Bun.file(path).size
// After: Filesystem.size()
const size = await Filesystem.size(path)
// Before: Bun.write()
await Bun.write(path, content)
// After: Filesystem.write()
await Filesystem.write(path, content)
// Before: Bun.write() with JSON
await Bun.write(path, JSON.stringify(data, null, 2))
// After: Filesystem.writeJson()
await Filesystem.writeJson(path, data)
// Before: Bun.file().type
const mime = Bun.file(path).type
// After: Filesystem.mimeType()
const mime = Filesystem.mimeType(path)
Files Requiring Updates
Core Utilities
src/util/filesystem.ts-exists()andisDir()functions useBun.file().stat()src/util/log.ts- UsesBun.file()for log file access
Tool Implementations
src/tool/read.ts- UsesBun.file()for file type, stat, and bytessrc/tool/write.ts- UsesBun.file().exists()andBun.file().text()src/tool/edit.ts- UsesBun.file().exists()src/tool/grep.ts- UsesBun.file()src/tool/glob.ts- UsesBun.file()for statssrc/tool/lsp.ts- UsesBun.file().exists()src/tool/truncation.ts- UsesBun.write()
Storage & Data
src/storage/storage.ts- Multiple uses ofBun.file().json()andBun.write()src/storage/json-migration.ts- UsesBun.file().json()src/storage/db.ts- UsesBun.file().sizesrc/mcp/auth.ts- UsesBun.file().json()andBun.write()
Project Management
src/project/project.ts- UsesBun.file().text()andBun.file().stat()
Session & Prompts
src/session/prompt.ts- UsesBun.file().stat(),Bun.file().exists(), andBun.file().text()src/session/instruction.ts- UsesBun.file().exists()andBun.file().text()
Provider & Models
src/provider/models.ts- UsesBun.file()andBun.write()src/provider/provider.ts- UsesBun.file().text()
Skill Discovery
src/skill/discovery.ts- UsesBun.file().exists()andBun.write()
LSP
src/lsp/client.ts- UsesBun.file()src/lsp/server.ts- UsesBun.file().exists()andBun.file().write()
Shell & CLI
src/shell/shell.ts- UsesBun.file().sizesrc/cli/cmd/tui/thread.ts- UsesBun.file().exists()
Additional Files Migrated
src/acp/agent.ts- UsesBun.file().exists()andBun.file().text()src/auth/index.ts- UsesBun.file().json()andBun.write()src/bun/index.ts- UsesBun.file().json()andBun.write()src/cli/cmd/agent.ts- UsesBun.file().exists()andBun.write()src/cli/cmd/github.ts- UsesBun.write()src/cli/cmd/import.ts- UsesBun.file().json()src/cli/cmd/mcp.ts- UsesBun.file()andBun.write()src/cli/cmd/run.ts- UsesBun.file()src/cli/cmd/session.ts- UsesBun.file().sizesrc/cli/cmd/uninstall.ts- UsesBun.file().text()andBun.write()src/cli/cmd/tui/util/clipboard.ts- UsesBun.file().arrayBuffer()src/cli/cmd/tui/util/editor.ts- UsesBun.write()andBun.file().text()src/config/markdown.ts- UsesBun.file().text()src/file/index.ts- UsesBun.file()for text, exists, arrayBuffer, and mime typesrc/file/time.ts- UsesBun.file().stat()src/format/formatter.ts- UsesBun.file().json()andBun.file().text()src/global/index.ts- UsesBun.file().text()andBun.file().write()
Centralized File API Module
The src/util/filesystem.ts module provides optimized Node.js equivalents for all Bun.file operations:
import { Filesystem } from "./util/filesystem"
// Migration examples:
// Before: Bun.file().exists()
const exists = await Bun.file(path).exists()
// After: Filesystem.exists() (uses fast existsSync internally)
const exists = await Filesystem.exists(path)
// Before: Bun.file().text()
const content = await Bun.file(path).text()
// After: Filesystem.readText()
const content = await Filesystem.readText(path)
// Before: Bun.file().json()
const data = await Bun.file(path).json()
// After: Filesystem.readJson()
const data = await Filesystem.readJson<Config>(path)
// Before: Bun.file().bytes()
const bytes = await Bun.file(path).bytes()
// After: Filesystem.readBytes()
const buffer = await Filesystem.readBytes(path)
// Before: Bun.file().stat()
const stats = await Bun.file(path).stat()
// After: Filesystem.isDir() and size (uses fast statSync)
const isDir = await Filesystem.isDir(path)
const size = await Filesystem.size(path)
// Before: Bun.file().size
const size = Bun.file(path).size
// After: Filesystem.size() (8x faster than async stat)
const size = await Filesystem.size(path)
// Before: Bun.write()
await Bun.write(path, content)
// After: Filesystem.write() (auto-creates directories)
await Filesystem.write(path, content)
// Before: Bun.write() with JSON
await Bun.write(path, JSON.stringify(data, null, 2))
// After: Filesystem.writeJson()
await Filesystem.writeJson(path, data)
// Before: Bun.file().type
const mime = Bun.file(path).type
// After: Filesystem.mimeType()
const mime = Filesystem.mimeType(path)
API Reference
| Bun API | Filesystem Equivalent | Implementation |
|---|---|---|
Bun.file(p).exists() |
Filesystem.exists(p) |
existsSync() (15x faster) |
Bun.file(p).stat() |
Filesystem.isDir(p), Filesystem.size(p) |
statSync() (8x faster) |
Bun.file(p).text() |
Filesystem.readText(p) |
readFile() |
Bun.file(p).json() |
Filesystem.readJson<T>(p) |
readFile() + JSON.parse() |
Bun.file(p).bytes() |
Filesystem.readBytes(p) |
readFile() (returns Buffer) |
Bun.file(p).size |
Filesystem.size(p) |
statSync().size (8x faster) |
Bun.file(p).type |
Filesystem.mimeType(p) |
mime-types library |
Bun.write(p, data) |
Filesystem.write(p, data, mode?) |
writeFile() + lazy mkdir |
| - | Filesystem.writeJson(p, data, mode?) |
write() with JSON.stringify |
Performance Notes
The Filesystem module uses optimized implementations:
- Metadata operations (
exists,isDir,size): Use sync APIs (existsSync,statSync) for 8-15x speedup - Content operations (
readText,write): Use async APIs to avoid blocking thread during I/O - Directory creation: Lazy - only creates directories when write fails with ENOENT
- MIME type detection: Uses
mime-typeslibrary for broader format support than Bun
Testing
Always run tests before pushing changes:
cd packages/opencode
bun test
Quick Reference for LLM Agents
What you're doing: Migrating one file at a time from Bun-specific file APIs to the Filesystem module.
Commands to run:
cd packages/opencode- Always work from the package directory- After migration:
bun test- Ensure tests pass before pushing
What to migrate:
Bun.file(path).exists()→Filesystem.exists(path)Bun.file(path).text()→Filesystem.readText(path)Bun.file(path).json()→Filesystem.readJson(path)Bun.file(path).bytes()→Filesystem.readBytes(path)Bun.file(path).stat()→Filesystem.isDir(path)orFilesystem.size(path)Bun.file(path).size→Filesystem.size(path)Bun.file(path).type→Filesystem.mimeType(path)Bun.write(path, content)→Filesystem.write(path, content)Bun.write(path, JSON.stringify(data))→Filesystem.writeJson(path, data)
Do NOT migrate (leave as-is):
Bun.hash.xxHash32- Keep using Bun's hashBun.stdin- Keep using Bun's stdin
After merging your PR: Return to this file and check off the file you just migrated from the checklist.
Migration Checklist
Phase 1: Centralize File Operations (DONE ✅)
- Create
src/util/filesystem.tswith all file operations - Implement optimized sync variants for metadata (exists, isDir, size)
- Implement async variants for content operations (read, write)
- Add lazy directory creation for write operations
- Add comprehensive test coverage (31 tests)
Phase 2: Migrate Source Files
For each file using Bun.file APIs, replace with Filesystem equivalents:
Pattern:
// Remove Bun imports
import { Bun } from "bun" // ❌ Remove this
// Add Filesystem import
import { Filesystem } from "./util/filesystem" // ✅ Add this
// Replace all Bun.file() calls
const exists = await Bun.file(path).exists() // ❌
const exists = await Filesystem.exists(path) // ✅
const content = await Bun.file(path).text() // ❌
const content = await Filesystem.readText(path) // ✅
await Bun.write(path, content) // ❌
await Filesystem.write(path, content) // ✅
Files to migrate:
src/tool/read.ts-stat(),type,bytes(),text()src/tool/write.ts-exists(),text(),Bun.write()src/tool/edit.ts-exists(),stat(),text(),write()src/file/time.ts-stat()src/file/index.ts-text(),bytes(),exists(),typesrc/mcp/auth.ts-json(),Bun.write()src/storage/storage.ts-json(),Bun.write()src/lsp/server.ts-exists(),write(),text()- ... (see full list above)
Phase 3: Handle Special Cases
- Install
@types/mime-types(already done) - Run all tests to verify functionality
- Update
package.jsonto remove Bun-only dependencies if any
Migration Priority
- High: Files with >5 Bun.file usages or critical paths
- Medium: Files with 2-5 usages
- Low: Files with 1-2 usages or covered by other tests
Notes
- Bun APIs return
nullon some errors where Node.js throws; ensure.catch(() => ...)is used appropriately - File permissions: Bun defaults may differ from Node.js defaults; explicitly pass
mode: 0o600where needed - Performance: Node.js
fs/promisesis generally comparable to Bun.file for most read operations (within 2x) - Metadata operations (exists, size) will be ~10x slower after migration
- Streaming: Both support similar streaming APIs via
createReadStreamandcreateWriteStream