brainy/src/universal/fs.ts
David Snelling b7e5f84af6 fix: prevent fs adapter from throwing on import in browser
- Add BrowserFS no-op implementation instead of throwing immediately
- Methods still throw when called (not at import time)
- exists() returns false instead of throwing
- readdir() returns empty arrays instead of throwing

This allows Brainy to be imported in browser environments without errors
2025-09-17 16:22:02 -07:00

171 lines
No EOL
5.5 KiB
TypeScript

/**
* Universal File System implementation
* Framework-friendly: Trusts that frameworks provide fs polyfills
* Works in all environments: Browser (via framework), Node.js, Serverless
*/
import { isNode } from '../utils/environment.js'
let nodeFs: any = null
// Dynamic import for Node.js fs (only in Node.js environment)
if (isNode()) {
try {
// Use node: protocol to prevent bundler polyfilling (requires Node 22+)
// Import main module and access promises to avoid subpath issues with bundlers
const fs = await import('node:fs')
nodeFs = fs.promises
} catch {
// Ignore import errors in non-Node environments
}
}
/**
* Universal file operations interface
*/
export interface UniversalFS {
readFile(path: string, encoding?: string): Promise<string>
writeFile(path: string, data: string, encoding?: string): Promise<void>
mkdir(path: string, options?: { recursive?: boolean }): Promise<void>
exists(path: string): Promise<boolean>
readdir(path: string): Promise<string[]>
readdir(path: string, options: { withFileTypes: true }): Promise<{ name: string, isDirectory(): boolean, isFile(): boolean }[]>
unlink(path: string): Promise<void>
stat(path: string): Promise<{ isFile(): boolean, isDirectory(): boolean }>
access(path: string, mode?: number): Promise<void>
}
/**
* Node.js implementation using fs/promises
*/
class NodeFS implements UniversalFS {
async readFile(path: string, encoding = 'utf-8'): Promise<string> {
return await nodeFs.readFile(path, encoding)
}
async writeFile(path: string, data: string, encoding = 'utf-8'): Promise<void> {
await nodeFs.writeFile(path, data, encoding)
}
async mkdir(path: string, options = { recursive: true }): Promise<void> {
await nodeFs.mkdir(path, options)
}
async exists(path: string): Promise<boolean> {
try {
await nodeFs.access(path)
return true
} catch {
return false
}
}
async readdir(path: string): Promise<string[]>
async readdir(path: string, options: { withFileTypes: true }): Promise<{ name: string, isDirectory(): boolean, isFile(): boolean }[]>
async readdir(path: string, options?: { withFileTypes?: boolean }): Promise<string[] | { name: string, isDirectory(): boolean, isFile(): boolean }[]> {
if (options?.withFileTypes) {
return await nodeFs.readdir(path, { withFileTypes: true })
}
return await nodeFs.readdir(path)
}
async unlink(path: string): Promise<void> {
await nodeFs.unlink(path)
}
async stat(path: string): Promise<{ isFile(): boolean, isDirectory(): boolean }> {
const stats = await nodeFs.stat(path)
return {
isFile: () => stats.isFile(),
isDirectory: () => stats.isDirectory()
}
}
async access(path: string, mode?: number): Promise<void> {
await nodeFs.access(path, mode)
}
}
// Browser-safe no-op implementation
class BrowserFS implements UniversalFS {
async readFile(path: string, encoding = 'utf-8'): Promise<string> {
throw new Error('File system operations not available in browser. Use OPFS, Memory, or S3 storage adapters instead.')
}
async writeFile(path: string, data: string, encoding = 'utf-8'): Promise<void> {
throw new Error('File system operations not available in browser. Use OPFS, Memory, or S3 storage adapters instead.')
}
async mkdir(path: string, options = { recursive: true }): Promise<void> {
throw new Error('File system operations not available in browser. Use OPFS, Memory, or S3 storage adapters instead.')
}
async exists(path: string): Promise<boolean> {
return false // Always return false in browser
}
async readdir(path: string): Promise<string[]>
async readdir(path: string, options: { withFileTypes: true }): Promise<{ name: string, isDirectory(): boolean, isFile(): boolean }[]>
async readdir(path: string, options?: { withFileTypes?: boolean }): Promise<string[] | { name: string, isDirectory(): boolean, isFile(): boolean }[]> {
if (options?.withFileTypes) {
return []
}
return []
}
async unlink(path: string): Promise<void> {
throw new Error('File system operations not available in browser. Use OPFS, Memory, or S3 storage adapters instead.')
}
async stat(path: string): Promise<{ isFile(): boolean, isDirectory(): boolean }> {
throw new Error('File system operations not available in browser. Use OPFS, Memory, or S3 storage adapters instead.')
}
async access(path: string, mode?: number): Promise<void> {
throw new Error('File system operations not available in browser. Use OPFS, Memory, or S3 storage adapters instead.')
}
}
// Create the appropriate filesystem implementation
let fsImpl: UniversalFS
if (isNode() && nodeFs) {
fsImpl = new NodeFS()
} else {
// Use browser-safe no-op implementation instead of throwing
fsImpl = new BrowserFS()
}
// Export the filesystem operations
export const readFile = fsImpl.readFile.bind(fsImpl)
export const writeFile = fsImpl.writeFile.bind(fsImpl)
export const mkdir = fsImpl.mkdir.bind(fsImpl)
export const exists = fsImpl.exists.bind(fsImpl)
export const readdir = fsImpl.readdir.bind(fsImpl)
export const unlink = fsImpl.unlink.bind(fsImpl)
export const stat = fsImpl.stat.bind(fsImpl)
export const access = fsImpl.access.bind(fsImpl)
// Default export with promises namespace compatibility
export default {
readFile,
writeFile,
mkdir,
exists,
readdir,
unlink,
stat,
access
}
// Named export for fs/promises compatibility
export const promises = {
readFile,
writeFile,
mkdir,
exists,
readdir,
unlink,
stat,
access
}