@ecopages/file-system
Runtime file system utilities for Ecopages. The implementation is Node's node:fs and crypto on every runtime, including Bun.
Features
- Node implementation:
node:fs/promises.glob(),crypto, andnode:fson every runtime. - One interface: Call sites import
fileSystemand do not choose an adapter. Tests replace it withinstallFileSystemand can wrap it withcreateRecordingFileSystemto observe every path argument. - Recursive watch:
watch()opens one native recursive watcher per minimal directory root, rather than one watcher for every subdirectory. File and missing roots share non-recursive parent watches. It indexes existing entries without emitting initial changes, coalesces native events for 50 ms, supports adding paths to a running watch, and maps canonical paths back to the requested roots. - Type Safe: Full TypeScript support with a consistent API.
Installation
bun add @ecopages/file-system
Usage
import { fileSystem } from '@ecopages/file-system';
// Glob files
const files = await fileSystem.glob(['**/*.ts'], { cwd: './src' });
// Read files
const content = await fileSystem.readFile('file.txt');
// Hash files
const hash = fileSystem.hash('file.txt');
// Write files
fileSystem.write('output.txt', 'Hello World');
// Check existence
if (fileSystem.exists('file.txt')) {
// ...
}
// Watch a tree, skipping dependencies
const watcher = fileSystem.watch(['./src'], {
ignored: (path) => path.includes('node_modules'),
onChange: ({ type, path }) => console.log(type, path),
onError: (error) => console.error(error),
});
await watcher.ready;
await watcher.add(['./generated']);
await watcher.close();
watch() reports file changes as add, change or unlink, and directory changes as addDir or unlinkDir. Atomic replacement of an existing file is reported as one change. If a native event omits its filename, watch() reconciles the native root and can emit a rescan event for each mapped requested root; consumers should invalidate those roots once. A missing root is watched from its nearest existing parent and begins reporting once it is created. add() extends the running subscription and indexes entries already present under the new paths without reporting them. If registration fails, the added paths are rolled back so callers can retry them. Permission-denied children are skipped during indexing so accessible siblings remain watched. Symlinked roots are reported using the path passed to watch(). The optional ignored predicate runs before existing subdirectory contents are indexed, so ignored subtrees are not scanned. A path the predicate ignores when called without stats is also skipped by the native recursive watcher; on Linux, where Node opens one inotify watch per entry of a recursive root, that keeps trees such as node_modules from costing kernel watches. Return true without stats only for paths that stay ignored, because a predicate that widens later can only do so for answers given with stats. A directory removed while it is being indexed is treated as gone, not as an error.
Paths
@ecopages/file-system/path holds the path convention of Ecopages: every path uses forward slashes, and an absolute Windows path keeps its drive letter as given (D:/app/src/page.tsx); use path.isWithin rather than a string prefix to test containment. Its operations mirror node:path (join, resolve, relative, dirname, basename, extname, isAbsolute, normalize, parse, format, matchesGlob) and always return that form. Paths that fileSystem returns, from realpath, glob, readdir, readlink, mkdtemp, tempDir and watch, are in it too.
import { path } from '@ecopages/file-system/path';
const page = path.join(rootDir, 'src', 'pages', 'index.tsx');
const here = path.fromFileUrl(import.meta.url);
const module = await import(path.toFileUrl(page).href);
| Export | Description |
|---|---|
path.cwd() | The working directory, in this form |
path.isWithin(directory, file) | Whether file is directory or below it |
path.fromNative(path) | Convert a native path, such as import.meta.dirname |
path.toNative(path) | Convert to backslashes on Windows, for a tool that needs them |
path.fromFileUrl(url) | Convert a file: URL to a path |
path.toFileUrl(path) | Convert a path to a file: URL, as import() needs on Windows |
createPath('posix' | 'win32') | The same operations for one host flavor, for tests |
API
| Method | Description |
|---|---|
glob(patterns, options) | Find files matching glob patterns |
readFile(path) | Read file as string (async) |
readFileSync(path) | Read file as string (sync) |
readFileAsBuffer(path) | Read file as Buffer |
write(path, content) | Write content to file |
writeAsync(path, content) | Write content to file (async) |
exists(path) | Check if path exists |
ensureDir(path) | Ensure directory exists |
copyDir(src, dest) | Copy directory recursively |
copyFile(src, dest) | Copy single file |
remove(path) | Remove file or directory |
removeAsync(path) | Remove file or directory (async) |
hash(path) | Get hash of file contents |
gzipFile(path) | Gzip a single file |
gzipDir(path, extensions) | Gzip files in directory |
isDirectory(path) | Check if path is directory |
realpath(path) | Resolve symlinks to canonical path |
stat(path) | Kind, size and mtime, following symlinks |
lstat(path) | Kind, size and mtime of the path itself |
readdir(path, options) | List entries with their kind |
readlink(path) | Read a symlink's stored target |
mkdtemp(prefix) | Create a uniquely named directory |
tempDir() | The OS directory for temporary files |
rename(src, dest) | Move a file or directory |
verifyFileExists(path) | Throw if file doesn't exist |
watch(paths, options) | Watch paths; the returned handle can add more |
installFileSystem(next) | Replace the process-wide fileSystem (tests) |
createRecordingFileSystem(inner) | Wrap inner and record every path argument |
stat, lstat and readdir return plain objects (isFile, isDirectory, isSymbolicLink, plus size and mtimeMs for stats). readdir(path, { recursive: true }) also lists subdirectories, naming each entry by its path relative to path. stat, lstat, readdir, readlink, mkdtemp and rename throw Node's error unchanged, so a missing path can be detected by error.code === 'ENOENT'.