@neodx/vfs
Version:
Simple virtual file system - working dir context, lazy changes, different modes, integrations and moreover
272 lines (265 loc) • 8.99 kB
TypeScript
import { Logger } from '@neodx/log';
/**
* Implementations for critical base VFS operations.
* All methods accept absolute paths.
*/
interface VfsBackend {
/** Read file content or return `null` if file does not exist. */
read: (path: string) => Asyncable<Buffer | null>;
/** Write file content. */
write: (path: string, content: VfsContentLike) => Asyncable<void>;
/** Check if an entry exists. */
exists: (path: string) => Asyncable<boolean>;
/** Delete an entry (recursively if directory). */
delete: (path: string) => Asyncable<void>;
/** Read directory entries (non-recursive). */
readDir: (path: string) => Asyncable<VfsDirent[]>;
/** Check if an entry is a directory. */
isDir: (path: string) => Asyncable<boolean>;
/** Check if an entry is a file. */
isFile: (path: string) => Asyncable<boolean>;
__?: unknown;
}
/**
* A `node:fs.Dirent` compatible interface.
*/
interface VfsDirent {
isFile(): boolean;
isDirectory(): boolean;
isSymbolicLink(): boolean;
name: string;
}
interface CreateVfsContextParams {
log?: VfsLogger;
logLevel?: VfsLogMethod;
path: string;
backend: VfsBackend;
parent?: VfsContext;
}
declare const createVfsContext: ({
parent,
logLevel,
log,
...params
}: CreateVfsContextParams) => VfsContext;
/**
* End users should not use context, it's for internal use only.
* Contains all changes, API for working with them, FS backend, and other useful stuff.
*/
interface VfsContext {
path: string;
resolve(...to: string[]): string;
relative(path: string): string;
get(path: string): VfsChangeMeta | null;
/**
* We will sync ALL changes from the current context AND all descendants.
* Changes from ancestors will be ignored.
*/
getAllDirectChanges(): VfsChangeMeta[];
/**
* We will sync ALL changes from the current context AND all descendants AND all ancestors.
*/
getAllChanges(): VfsChangeMeta[];
getRelativeChanges(path: string): VfsChangeMeta[];
/** Remove file from context. */
unregister(path: string): void;
/** Set associated file temporal content. */
writePathContent(path: string, content: VfsContentLike): void;
/** Mark path as deleted. */
deletePath(path: string, deleted: boolean): void;
/** Handles any vfs errors. */
catch(message: string, error: unknown): Error;
readonly log: Logger<VfsLogMethod>;
readonly backend: VfsBackend;
/** @internal */
__: {
vfs?: BaseVfs;
kind: typeof kind;
parent?: VfsContext;
plugins: VfsPlugin<any>[];
children: VfsContext[];
getAll: () => VfsContext[];
getStore: () => Map<string, VfsChangeMeta>;
getScoped: () => VfsContext[];
getAncestors: () => VfsContext[];
getDescendants: () => VfsContext[];
register: (path: string, meta: RegisteredMeta) => void;
unregister: (path: string) => void;
};
}
interface RegisteredMeta extends Omit<VfsChangeMeta, 'path' | 'relativePath' | 'content'> {
content: VfsContentLike | null;
}
interface VfsChangeMeta extends VfsFileMeta {
content: Buffer | null;
deleted?: boolean;
/**
* Indicates that the file or directory was updated after deletion.
* It could be used to ensure the correct order of operations.
*/
updatedAfterDelete?: boolean;
}
declare const kind: unique symbol;
type PublicVfs<Vfs extends BaseVfs> = Vfs & PublicVfsApi<Vfs>;
interface PublicVfsApi<Vfs extends BaseVfs> {
/**
* Creates a new vfs instance under the specified path.
* All plugins will be inherited.
* All changes are two-way synced.
*/
child: (path: string) => PublicVfs<Vfs>;
/**
* Immutable extension of the current vfs instance with the specified plugins.
* You can use it in any order and any number of times.
* @example
* const vfs = createHeadlessVfs('/root/path');
* const enhanced = vfs.pipe(glob(), json());
* const superEnhanced = vfs.pipe(json()).pipe(glob()).pipe(prettier(), eslint());
*/
pipe: Pipe<Vfs>;
}
interface PrivateVfsApi<Vfs extends BaseVfs> extends PrivateVfsHookApi<Vfs> {
context: VfsContext;
}
interface PrivateVfsHooks<Vfs extends BaseVfs> {
beforeApplyFile: (action: VfsFileAction, vfs: Vfs) => Asyncable<void>;
beforeApply: (actions: VfsFileAction[], vfs: Vfs) => Asyncable<void>;
/**
* Hook will be called after marking file and all nested files as deleted
*/
afterDelete: (path: string, vfs: Vfs) => Asyncable<void>;
}
type PrivateVfsHookApi<Vfs extends BaseVfs> = {
[K in keyof PrivateVfsHooks<Vfs>]: (handler: PrivateVfsHooks<Vfs>[K]) => void;
};
interface VfsPlugin<Extensions> {
<Vfs extends BaseVfs & Partial<Extensions>>(vfs: Vfs, api: PrivateVfsApi<Vfs>): Vfs & Extensions;
}
declare function createVfsPlugin<const Extensions extends Record<keyof any, any>>(
name: string,
handler: <Vfs extends BaseVfs>(
vfs: Vfs & Partial<Extensions>,
api: PrivateVfsApi<Vfs>
) => Vfs & Partial<Extensions>
): VfsPlugin<Extensions>;
/**
* Base virtual file system interface.
*/
interface BaseVfs {
/**
* Applies all changes to the underlying vfs backend.
*/
apply(): Promise<void>;
/** Is current VFS virtual? */
readonly virtual: boolean;
/** Is current VFS readonly? */
readonly readonly: boolean;
/** VFS logger */
readonly log: Logger<VfsLogMethod>;
/** Absolute path to current dir */
readonly path: string;
/** Absolute path to parent directory */
readonly dirname: string;
/**
* Resolves an absolute path to the current directory.
* Uses `path.resolve` internally.
*
* @example createVfs('/root/path').resolve('subdir') // -> '/root/path/subdir'
* @example createVfs('/root/path').resolve('subdir', 'file.txt') // -> '/root/path/subdir/file.txt'
* @example createVfs('/root/path').resolve('/other/path') // -> '/other/path'
*/
resolve(...to: string[]): string;
/**
* Returns a relative path to the current directory.
*
* @example createVfs('/root/path').relative('/root/path/subdir') // -> 'subdir'
* @example createVfs('/root/path').relative('relative/file.txt') // -> 'relative/file.txt'
*/
relative(path: string): string;
/** Safe file read. Returns null if file doesn't exist or some error occurred. */
tryRead(path: string): Promise<Buffer | null>;
tryRead(path: string, encoding: BufferEncoding): Promise<string | null>;
/** Read file content. Throws an error if something went wrong. */
read(path: string): Promise<Buffer>;
read(path: string, encoding: BufferEncoding): Promise<string>;
/**
* Write file content.
* If the file doesn't exist, it will be created, including all parent directories.
* Otherwise, the file will be overwritten with new content.
*/
write(path: string, content: VfsContentLike): Promise<void>;
/** Rename file or directory. */
rename(from: string, ...to: string[]): Promise<void>;
/**
* Read directory children.
* @param path Path to directory. If not specified, the current directory will be used.
* @param params.withFileTypes If true, returns `VfsDirent[]` instead of `string[]`.
*/
readDir(path?: string): Promise<string[]>;
readDir(params: { withFileTypes: true }): Promise<VfsDirent[]>;
readDir(
path: string,
params: {
withFileTypes: true;
}
): Promise<VfsDirent[]>;
/** Check if a path exists. */
exists(path?: string): Promise<boolean>;
/** Check if a path is a file. */
isFile(path: string): Promise<boolean>;
/** Check if a path is a directory. */
isDir(path: string): Promise<boolean>;
/** Delete file or directory (recursively). */
delete(path: string): Promise<void>;
}
type MergeVfsPlugins<Vfs extends BaseVfs, Plugins extends [...unknown[]]> = Plugins extends [
VfsPlugin<infer Extensions>,
...infer Rest
]
? MergeVfsPlugins<Vfs & Extensions, Rest>
: PublicVfs<Vfs>;
interface Pipe<This extends BaseVfs> {
<Plugins extends [...VfsPlugin<unknown>[]]>(...plugins: Plugins): MergeVfsPlugins<This, Plugins>;
}
type VfsContentLike = Buffer | string;
type VfsFileAction = VfsFileWrite | VfsFileUpdate | VfsFileDelete;
interface VfsFileUpdate extends VfsFileMeta {
type: 'update';
updatedAfterDelete?: boolean;
content: Buffer;
}
interface VfsFileWrite extends VfsFileMeta {
type: 'create';
updatedAfterDelete?: boolean;
content: Buffer;
}
interface VfsFileDelete extends VfsFileMeta {
type: 'delete';
}
interface VfsFileMeta {
path: string;
relativePath: string;
}
type VfsLogger = Logger<VfsLogMethod>;
type VfsLogMethod = 'debug' | 'info' | 'warn' | 'error';
type Asyncable<T> = T | Promise<T>;
export {
type Asyncable as A,
type BaseVfs as B,
type CreateVfsContextParams as C,
type PublicVfs as P,
type VfsContentLike as V,
type VfsContext as a,
type VfsLogger as b,
type VfsLogMethod as c,
type VfsBackend as d,
type VfsDirent as e,
createVfsContext as f,
type VfsFileAction as g,
type VfsFileDelete as h,
type VfsFileMeta as i,
type VfsFileUpdate as j,
type VfsFileWrite as k,
createVfsPlugin as l,
type VfsPlugin as m
};