From e4a9ed97fb39eb218807d031718bbf774ac8233f Mon Sep 17 00:00:00 2001 From: Mopsgamer <79159094+Mopsgamer@users.noreply.github.com> Date: Thu, 18 Dec 2025 16:59:01 +0100 Subject: [PATCH 01/12] fix: relative readFile --- packages/fs-core/src/Superblock.ts | 17 ++++++++++------- .../src/__tests__/volume/readFile.test.ts | 10 ++++++++++ packages/fs-node/src/volume.ts | 1 - 3 files changed, 20 insertions(+), 8 deletions(-) diff --git a/packages/fs-core/src/Superblock.ts b/packages/fs-core/src/Superblock.ts index e898521c3..e47166546 100644 --- a/packages/fs-core/src/Superblock.ts +++ b/packages/fs-core/src/Superblock.ts @@ -97,6 +97,8 @@ export class Superblock { this.root = root; } + protected cwd: string = process.cwd(); + createLink(): Link; createLink(parent: Link, name: string, isDirectory?: boolean, mode?: number): Link; createLink(parent?: Link, name?: string, isDirectory: boolean = false, mode?: number): Link { @@ -196,7 +198,7 @@ export class Superblock { steps = stepsOrFilenameOrLink.steps; filename = pathSep + steps.join(pathSep); } else if (typeof stepsOrFilenameOrLink === 'string') { - steps = filenameToSteps(stepsOrFilenameOrLink); + steps = filenameToSteps(stepsOrFilenameOrLink, this.cwd); filename = stepsOrFilenameOrLink; } else { steps = stepsOrFilenameOrLink; @@ -235,7 +237,7 @@ export class Superblock { if (node.isSymlink() && (resolveSymlinks || i < steps.length - 1)) { const resolvedPath = isAbsolute(node.symlink) ? node.symlink : pathJoin(dirname(curr.getPath()), node.symlink); // Relative to symlink's parent - steps = filenameToSteps(resolvedPath).concat(steps.slice(i + 1)); + steps = filenameToSteps(resolvedPath, this.cwd).concat(steps.slice(i + 1)); curr = this.root; i = 0; continue; @@ -323,7 +325,7 @@ export class Superblock { getLinkParentAsDirOrThrow(filenameOrSteps: string | string[], funcName?: string): Link { const steps: string[] = ( - filenameOrSteps instanceof Array ? filenameOrSteps : filenameToSteps(filenameOrSteps) + filenameOrSteps instanceof Array ? filenameOrSteps : filenameToSteps(filenameOrSteps, this.cwd) ).slice(0, -1); const filename: string = pathSep + steps.join(pathSep); const link = this.getLinkOrThrow(filename, funcName); @@ -406,6 +408,7 @@ export class Superblock { // TODO: `cwd` should probably not invoke `process.cwd()`. fromJSON(json: DirectoryJSON, cwd: string = process.cwd()) { + this.cwd = cwd; for (let filename in json) { const data = json[filename]; filename = resolve(filename, cwd); @@ -493,7 +496,7 @@ export class Superblock { modeNum: number | undefined, resolveSymlinks: boolean = true, ): File { - const steps = filenameToSteps(filename); + const steps = filenameToSteps(filename, this.cwd); let link: Link | null; try { link = resolveSymlinks ? this.getResolvedLinkOrThrow(filename, 'open') : this.getLinkOrThrow(filename, 'open'); @@ -632,7 +635,7 @@ export class Superblock { }; public readonly symlink = (targetFilename: string, pathFilename: string): Link => { - const pathSteps = filenameToSteps(pathFilename); + const pathSteps = filenameToSteps(pathFilename, this.cwd); // Check if directory exists, where we about to create a symlink. let dirLink; try { @@ -708,7 +711,7 @@ export class Superblock { }; public readonly mkdir = (filename: string, modeNum: number): void => { - const steps = filenameToSteps(filename); + const steps = filenameToSteps(filename, this.cwd); // This will throw if user tries to create root dir `fs.mkdirSync('/')`. if (!steps.length) throw createError(ERROR_CODE.EEXIST, 'mkdir', filename); const dir = this.getLinkParentAsDirOrThrow(filename, 'mkdir'); @@ -725,7 +728,7 @@ export class Superblock { */ public readonly mkdirp = (filename: string, modeNum: number): string | undefined => { let created = false; - const steps = filenameToSteps(filename); + const steps = filenameToSteps(filename, this.cwd); let curr: Link | null = null; let i = steps.length; // Find the longest subpath of filename that still exists: diff --git a/packages/fs-node/src/__tests__/volume/readFile.test.ts b/packages/fs-node/src/__tests__/volume/readFile.test.ts index 7256e280d..2e8ac0ddf 100644 --- a/packages/fs-node/src/__tests__/volume/readFile.test.ts +++ b/packages/fs-node/src/__tests__/volume/readFile.test.ts @@ -2,6 +2,16 @@ import { of } from 'thingies'; import { memfs } from '../util'; describe('.readFile()', () => { + it('can read a file in cwd', async () => { + const { fs } = memfs({ 'test.txt': '01234567' }, '/dir'); + await expect(fs.promises.readFile('/dir/test.txt', { encoding: 'utf8' })).resolves.toBe('01234567'); + }); + + it('can read a relative file in cwd', async () => { + const { fs } = memfs({ 'test.txt': '01234567' }, '/dir'); + await expect(fs.promises.readFile('test.txt', { encoding: 'utf8' })).resolves.toBe('01234567'); + }); + it('can read a file', async () => { const { fs } = memfs({ '/dir/test.txt': '01234567' }); const data = await fs.promises.readFile('/dir/test.txt', { encoding: 'utf8' }); diff --git a/packages/fs-node/src/volume.ts b/packages/fs-node/src/volume.ts index 85f642689..1d518de8f 100644 --- a/packages/fs-node/src/volume.ts +++ b/packages/fs-node/src/volume.ts @@ -984,7 +984,6 @@ export class Volume implements FsCallbackApi, FsSynchronousApi { }; private readonly _readdir = (filename: string, options: opts.IReaddirOptions): TDataOut[] | Dirent[] => { - const steps = filenameToSteps(filename); const link: Link = this._core.getResolvedLinkOrThrow(filename, 'scandir'); const node = link.getNode(); if (!node.isDirectory()) throw createError(ERROR_CODE.ENOTDIR, 'scandir', filename); From c707dcd721b6314223244ce97e8aabccfd7bfa78 Mon Sep 17 00:00:00 2001 From: Mopsgamer <79159094+Mopsgamer@users.noreply.github.com> Date: Thu, 18 Dec 2025 17:02:52 +0100 Subject: [PATCH 02/12] test: relative ./test.txt --- packages/fs-node/src/__tests__/volume/readFile.test.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/fs-node/src/__tests__/volume/readFile.test.ts b/packages/fs-node/src/__tests__/volume/readFile.test.ts index 2e8ac0ddf..b292322ac 100644 --- a/packages/fs-node/src/__tests__/volume/readFile.test.ts +++ b/packages/fs-node/src/__tests__/volume/readFile.test.ts @@ -10,6 +10,7 @@ describe('.readFile()', () => { it('can read a relative file in cwd', async () => { const { fs } = memfs({ 'test.txt': '01234567' }, '/dir'); await expect(fs.promises.readFile('test.txt', { encoding: 'utf8' })).resolves.toBe('01234567'); + await expect(fs.promises.readFile('./test.txt', { encoding: 'utf8' })).resolves.toBe('01234567'); }); it('can read a file', async () => { From b25e669dbc89f1b7002281a2c416512faea62be7 Mon Sep 17 00:00:00 2001 From: Mopsgamer <79159094+Mopsgamer@users.noreply.github.com> Date: Sun, 1 Feb 2026 12:47:35 +0100 Subject: [PATCH 03/12] test: add more and describe --- docs/node/relative-paths.md | 9 +++++++ packages/fs-node/src/__tests__/util.ts | 8 +++--- .../src/__tests__/volume/opendir.test.ts | 26 +++++++++++++++++++ .../src/__tests__/volume/readdirSync.test.ts | 10 +++++++ packages/fs-node/src/util.ts | 1 + 5 files changed, 50 insertions(+), 4 deletions(-) diff --git a/docs/node/relative-paths.md b/docs/node/relative-paths.md index 33d0d0285..580788a8b 100644 --- a/docs/node/relative-paths.md +++ b/docs/node/relative-paths.md @@ -23,3 +23,12 @@ is one folder that exists in all `memfs` volumes: ```js process.chdir('/'); ``` + +# Experimental support + +PR [1124](https://github.com/streamich/memfs/pull/1224) adds partial support for +relative paths. Tests are passing on the following methods: + +- `fs.promises.readFile` +- `vol.readdirSync` +- `vol.opendir` diff --git a/packages/fs-node/src/__tests__/util.ts b/packages/fs-node/src/__tests__/util.ts index bbb5250d1..dde356685 100644 --- a/packages/fs-node/src/__tests__/util.ts +++ b/packages/fs-node/src/__tests__/util.ts @@ -9,13 +9,13 @@ export const multitest = (_done: (err?: Error) => void, times: number) => { }; }; -export const create = (json: { [s: string]: string } = { '/foo': 'bar' }) => { - const vol = Volume.fromJSON(json); +export const create = (json: { [s: string]: string } = { '/foo': 'bar' }, cwd?: string) => { + const vol = Volume.fromJSON(json, cwd); return vol; }; -export const createFs = (json?) => { - const vol = create(json); +export const createFs = (json?, cwd?: string) => { + const vol = create(json, cwd); return vol; }; diff --git a/packages/fs-node/src/__tests__/volume/opendir.test.ts b/packages/fs-node/src/__tests__/volume/opendir.test.ts index 36c00b52b..adb8c7eda 100644 --- a/packages/fs-node/src/__tests__/volume/opendir.test.ts +++ b/packages/fs-node/src/__tests__/volume/opendir.test.ts @@ -23,6 +23,32 @@ describe('opendir', () => { }); }); + it('should provide relative fs.opendirSync (synchronous)', () => { + const vol = Volume.fromJSON({ + '/test/file1.txt': 'content1', + '/test/file2.txt': 'content2', + }, '/test'); + + const dir = vol.opendirSync('.'); + expect(dir).toBeDefined(); + expect(typeof dir.read).toBe('function'); + expect(typeof dir.close).toBe('function'); + expect(typeof dir.readSync).toBe('function'); + expect(typeof dir.closeSync).toBe('function'); + + const entries: string[] = []; + let entry; + while ((entry = dir.readSync()) !== null) { + entries.push(entry.name); + } + + expect(entries).toContain('file1.txt'); + expect(entries).toContain('file2.txt'); + expect(entries.length).toBe(2); + + dir.closeSync(); + }); + it('should provide fs.opendirSync (synchronous)', () => { const vol = new Volume(); vol.mkdirSync('/test'); diff --git a/packages/fs-node/src/__tests__/volume/readdirSync.test.ts b/packages/fs-node/src/__tests__/volume/readdirSync.test.ts index 5d29d2884..e3dd8b6ef 100644 --- a/packages/fs-node/src/__tests__/volume/readdirSync.test.ts +++ b/packages/fs-node/src/__tests__/volume/readdirSync.test.ts @@ -30,6 +30,16 @@ describe('readdirSync()', () => { expect(dirs).toEqual([]); }); + it('reads relative dir', () => { + const vol = create({ + '/foo/bar/file': 'content', + '/foo/bar/file2': 'content2', + }, '/foo'); + const files = vol.readdirSync('bar'); + + expect(files).toEqual(["file", "file2"]); + }); + it('respects symlinks', () => { const vol = create({ '/a/a': 'a', diff --git a/packages/fs-node/src/util.ts b/packages/fs-node/src/util.ts index 5ab246534..42c0d62f4 100644 --- a/packages/fs-node/src/util.ts +++ b/packages/fs-node/src/util.ts @@ -82,6 +82,7 @@ export function pathToFilename(path: misc.PathLike): string { } const pathString = String(path); + if (pathString === ".") return "./" nullCheck(pathString); // return slash(pathString); return pathString; From 48584a7c851ca685933b430d242b10cc486d2029 Mon Sep 17 00:00:00 2001 From: Mopsgamer <79159094+Mopsgamer@users.noreply.github.com> Date: Sun, 1 Feb 2026 13:04:05 +0100 Subject: [PATCH 04/12] test: readFileSync --- docs/node/relative-paths.md | 1 + packages/fs-node/src/__tests__/volume/readFile.test.ts | 3 +++ 2 files changed, 4 insertions(+) diff --git a/docs/node/relative-paths.md b/docs/node/relative-paths.md index 580788a8b..8727ad7ab 100644 --- a/docs/node/relative-paths.md +++ b/docs/node/relative-paths.md @@ -30,5 +30,6 @@ PR [1124](https://github.com/streamich/memfs/pull/1224) adds partial support for relative paths. Tests are passing on the following methods: - `fs.promises.readFile` +- `fs.readFileSync` - `vol.readdirSync` - `vol.opendir` diff --git a/packages/fs-node/src/__tests__/volume/readFile.test.ts b/packages/fs-node/src/__tests__/volume/readFile.test.ts index b292322ac..ffebdc854 100644 --- a/packages/fs-node/src/__tests__/volume/readFile.test.ts +++ b/packages/fs-node/src/__tests__/volume/readFile.test.ts @@ -5,12 +5,15 @@ describe('.readFile()', () => { it('can read a file in cwd', async () => { const { fs } = memfs({ 'test.txt': '01234567' }, '/dir'); await expect(fs.promises.readFile('/dir/test.txt', { encoding: 'utf8' })).resolves.toBe('01234567'); + await expect(fs.readFileSync('/dir/test.txt', { encoding: 'utf8' })).toBe('01234567'); }); it('can read a relative file in cwd', async () => { const { fs } = memfs({ 'test.txt': '01234567' }, '/dir'); await expect(fs.promises.readFile('test.txt', { encoding: 'utf8' })).resolves.toBe('01234567'); await expect(fs.promises.readFile('./test.txt', { encoding: 'utf8' })).resolves.toBe('01234567'); + await expect(fs.readFileSync('test.txt', { encoding: 'utf8' })).toBe('01234567'); + await expect(fs.readFileSync('./test.txt', { encoding: 'utf8' })).toBe('01234567'); }); it('can read a file', async () => { From 54e94826186dd54552d908a49580d2d41f9c3a35 Mon Sep 17 00:00:00 2001 From: Mopsgamer <79159094+Mopsgamer@users.noreply.github.com> Date: Sun, 1 Feb 2026 13:19:03 +0100 Subject: [PATCH 05/12] fix: default cwd test: statSync --- docs/node/relative-paths.md | 1 + packages/fs-node/src/__tests__/volume/statSync.test.ts | 4 ++-- packages/fs-node/src/volume.ts | 4 ++-- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/docs/node/relative-paths.md b/docs/node/relative-paths.md index 8727ad7ab..f25e06107 100644 --- a/docs/node/relative-paths.md +++ b/docs/node/relative-paths.md @@ -33,3 +33,4 @@ relative paths. Tests are passing on the following methods: - `fs.readFileSync` - `vol.readdirSync` - `vol.opendir` +- `vol.statSync` diff --git a/packages/fs-node/src/__tests__/volume/statSync.test.ts b/packages/fs-node/src/__tests__/volume/statSync.test.ts index 1559b2d5b..75ad0c9f5 100644 --- a/packages/fs-node/src/__tests__/volume/statSync.test.ts +++ b/packages/fs-node/src/__tests__/volume/statSync.test.ts @@ -8,8 +8,8 @@ describe('.statSync(...)', () => { vol.writeFileSync('/c/index.js', 'alert(123);'); vol.symlinkSync('/c', '/a/b'); - const stats = vol.statSync('/a/b/index.js'); - expect(stats.size).toBe(11); + expect(vol.statSync('/a/b/index.js').size).toBe(11); + expect(vol.statSync('a/b/index.js').size).toBe(11); }); it('returns rdev', () => { diff --git a/packages/fs-node/src/volume.ts b/packages/fs-node/src/volume.ts index 1d518de8f..7f4ad1ec4 100644 --- a/packages/fs-node/src/volume.ts +++ b/packages/fs-node/src/volume.ts @@ -176,10 +176,10 @@ function validateGid(gid: number) { * `Volume` represents a file system. */ export class Volume implements FsCallbackApi, FsSynchronousApi { - public static readonly fromJSON = (json: DirectoryJSON, cwd?: string): Volume => + public static readonly fromJSON = (json: DirectoryJSON, cwd: string = '/'): Volume => new Volume(Superblock.fromJSON(json, cwd)); - public static readonly fromNestedJSON = (json: NestedDirectoryJSON, cwd?: string): Volume => + public static readonly fromNestedJSON = (json: NestedDirectoryJSON, cwd: string = '/'): Volume => new Volume(Superblock.fromNestedJSON(json, cwd)); StatWatcher: new () => StatWatcher; From 00732f28974082e045a8ec2f6cfea0032e2fa22a Mon Sep 17 00:00:00 2001 From: Mopsgamer <79159094+Mopsgamer@users.noreply.github.com> Date: Sun, 1 Feb 2026 14:48:27 +0100 Subject: [PATCH 06/12] feat: cwd + mountpoint options test: cwd and relative cwd --- packages/fs-core/src/Superblock.ts | 49 ++++++++++++++----- packages/fs-core/src/index.ts | 2 +- packages/fs-node/src/__tests__/util.ts | 15 +++--- .../fs-node/src/__tests__/volume/cwd.test.ts | 38 ++++++++++++++ .../src/__tests__/volume/opendir.test.ts | 11 +++-- .../src/__tests__/volume/readdirSync.test.ts | 13 +++-- packages/fs-node/src/util.ts | 2 +- packages/fs-node/src/volume.ts | 27 ++++++---- packages/memfs/src/index.ts | 8 ++- 9 files changed, 124 insertions(+), 41 deletions(-) create mode 100644 packages/fs-node/src/__tests__/volume/cwd.test.ts diff --git a/packages/fs-core/src/Superblock.ts b/packages/fs-core/src/Superblock.ts index e47166546..e43339ff0 100644 --- a/packages/fs-core/src/Superblock.ts +++ b/packages/fs-core/src/Superblock.ts @@ -28,21 +28,41 @@ const pathJoin = posix ? posix.join : join; const { O_RDONLY, O_WRONLY, O_RDWR, O_CREAT, O_EXCL, O_TRUNC, O_APPEND, O_DIRECTORY } = constants; +/** + * Represents options for creating a Superblock from JSON. + */ +export type SuperBlockFromJsonOptions = { + /** + * Current working directory, absolute path. + * Methods will use this as the base path for relative paths. + * + * @default process.cwd() + */ + cwd?: string; + /** + * This is the mount point where the JSON structure will be mounted. + * It is used as the base path for all paths in the JSON structure. + * + * @default SuperBlockFromJsonOptions.cwd + */ + mountpoint?: string; +}; + /** * Represents a filesystem superblock, which is the root of a virtual * filesystem in Linux. * @see https://lxr.linux.no/linux+v3.11.2/include/linux/fs.h#L1242 */ export class Superblock { - static fromJSON(json: DirectoryJSON, cwd?: string): Superblock { + static fromJSON(json: DirectoryJSON, options?: SuperBlockFromJsonOptions): Superblock { const vol = new Superblock(); - vol.fromJSON(json, cwd); + vol.fromJSON(json, options); return vol; } - static fromNestedJSON(json: NestedDirectoryJSON, cwd?: string): Superblock { + static fromNestedJSON(json: NestedDirectoryJSON, options?: SuperBlockFromJsonOptions): Superblock { const vol = new Superblock(); - vol.fromNestedJSON(json, cwd); + vol.fromNestedJSON(json, options); return vol; } @@ -97,7 +117,13 @@ export class Superblock { this.root = root; } - protected cwd: string = process.cwd(); + protected _cwd: string = process.cwd(); + get cwd(): string { + return this._cwd; + } + set cwd(value: string) { + this._cwd = value; + } createLink(): Link; createLink(parent: Link, name: string, isDirectory?: boolean, mode?: number): Link; @@ -406,12 +432,11 @@ export class Superblock { return json; } - // TODO: `cwd` should probably not invoke `process.cwd()`. - fromJSON(json: DirectoryJSON, cwd: string = process.cwd()) { - this.cwd = cwd; + fromJSON(json: DirectoryJSON, options?: SuperBlockFromJsonOptions) { + this.cwd = options?.cwd ?? process.cwd(); for (let filename in json) { const data = json[filename]; - filename = resolve(filename, cwd); + filename = resolve(filename, options?.mountpoint ?? this.cwd); if (typeof data === 'string' || data instanceof Buffer) { const dir = dirname(filename); this.mkdirp(dir, MODE.DIR); @@ -423,8 +448,8 @@ export class Superblock { } } - fromNestedJSON(json: NestedDirectoryJSON, cwd?: string) { - this.fromJSON(flattenJSON(json), cwd); + fromNestedJSON(json: NestedDirectoryJSON, options?: SuperBlockFromJsonOptions) { + this.fromJSON(flattenJSON(json), options); } reset() { @@ -441,7 +466,7 @@ export class Superblock { // Legacy interface mountSync(mountpoint: string, json: DirectoryJSON) { - this.fromJSON(json, mountpoint); + this.fromJSON(json, { mountpoint: mountpoint }); } openLink(link: Link, flagsNum: number, resolveSymlinks: boolean = true): File { diff --git a/packages/fs-core/src/index.ts b/packages/fs-core/src/index.ts index c31e5b8c5..294f15f6a 100644 --- a/packages/fs-core/src/index.ts +++ b/packages/fs-core/src/index.ts @@ -5,7 +5,7 @@ export * from './result'; export { Node, type NodeEvent } from './Node'; export { Link, type LinkEvent } from './Link'; export { File } from './File'; -export { Superblock } from './Superblock'; +export { Superblock, SuperBlockFromJsonOptions } from './Superblock'; export { dataToBuffer, filenameToSteps, diff --git a/packages/fs-node/src/__tests__/util.ts b/packages/fs-node/src/__tests__/util.ts index dde356685..e45a4eb6c 100644 --- a/packages/fs-node/src/__tests__/util.ts +++ b/packages/fs-node/src/__tests__/util.ts @@ -1,5 +1,6 @@ import { Volume } from '..'; import { Link, Node, NestedDirectoryJSON } from '@jsonjoy.com/fs-core'; +import type { SuperBlockFromJsonOptions } from '@jsonjoy.com/fs-core/lib/Superblock'; export const multitest = (_done: (err?: Error) => void, times: number) => { let err; @@ -9,18 +10,20 @@ export const multitest = (_done: (err?: Error) => void, times: number) => { }; }; -export const create = (json: { [s: string]: string } = { '/foo': 'bar' }, cwd?: string) => { - const vol = Volume.fromJSON(json, cwd); +export const create = (json: { [s: string]: string } = { '/foo': 'bar' }, options?: SuperBlockFromJsonOptions) => { + options ??= {}; + options.cwd ??= '/'; + const vol = Volume.fromJSON(json, options); return vol; }; -export const createFs = (json?, cwd?: string) => { - const vol = create(json, cwd); +export const createFs = (json?, options?: SuperBlockFromJsonOptions) => { + const vol = create(json, options); return vol; }; -export const memfs = (json: NestedDirectoryJSON = {}, cwd: string = '/') => { - const vol = Volume.fromNestedJSON(json, cwd); +export const memfs = (json: NestedDirectoryJSON = {}, options?: SuperBlockFromJsonOptions) => { + const vol = Volume.fromNestedJSON(json, options); return { fs: vol, vol }; }; diff --git a/packages/fs-node/src/__tests__/volume/cwd.test.ts b/packages/fs-node/src/__tests__/volume/cwd.test.ts new file mode 100644 index 000000000..d6b8f40b5 --- /dev/null +++ b/packages/fs-node/src/__tests__/volume/cwd.test.ts @@ -0,0 +1,38 @@ +import { create } from '../util'; + +describe('Volume.cwd', () => { + it('Supports absolute path changes', () => { + const vol = create( + { + '/file': 'root', + '/a/file': 'a', + '/a/b/file': 'b', + }, + { + cwd: '/', + }, + ); + expect(vol.readFileSync('./file', 'utf8')).toEqual('root'); + vol.cwd = '/a'; + expect(vol.readFileSync('./file', 'utf8')).toEqual('a'); + vol.cwd = '/a/b'; + expect(vol.readFileSync('./file', 'utf8')).toEqual('b'); + }); + it('Supports relative path changes', () => { + const vol = create( + { + file: 'root', + 'a/file': 'a', + 'a/b/file': 'b', + }, + { + cwd: process.cwd(), + }, + ); + expect(vol.readFileSync('./file', 'utf8')).toEqual('root'); + vol.cwd = 'a'; + expect(vol.readFileSync('./file', 'utf8')).toEqual('a'); + vol.cwd = 'a/b'; + expect(vol.readFileSync('./file', 'utf8')).toEqual('b'); + }); +}); diff --git a/packages/fs-node/src/__tests__/volume/opendir.test.ts b/packages/fs-node/src/__tests__/volume/opendir.test.ts index adb8c7eda..a32924ff6 100644 --- a/packages/fs-node/src/__tests__/volume/opendir.test.ts +++ b/packages/fs-node/src/__tests__/volume/opendir.test.ts @@ -24,10 +24,13 @@ describe('opendir', () => { }); it('should provide relative fs.opendirSync (synchronous)', () => { - const vol = Volume.fromJSON({ - '/test/file1.txt': 'content1', - '/test/file2.txt': 'content2', - }, '/test'); + const vol = Volume.fromJSON( + { + '/test/file1.txt': 'content1', + '/test/file2.txt': 'content2', + }, + '/test', + ); const dir = vol.opendirSync('.'); expect(dir).toBeDefined(); diff --git a/packages/fs-node/src/__tests__/volume/readdirSync.test.ts b/packages/fs-node/src/__tests__/volume/readdirSync.test.ts index e3dd8b6ef..1c6f07e37 100644 --- a/packages/fs-node/src/__tests__/volume/readdirSync.test.ts +++ b/packages/fs-node/src/__tests__/volume/readdirSync.test.ts @@ -31,13 +31,16 @@ describe('readdirSync()', () => { }); it('reads relative dir', () => { - const vol = create({ - '/foo/bar/file': 'content', - '/foo/bar/file2': 'content2', - }, '/foo'); + const vol = create( + { + '/foo/bar/file': 'content', + '/foo/bar/file2': 'content2', + }, + '/foo', + ); const files = vol.readdirSync('bar'); - expect(files).toEqual(["file", "file2"]); + expect(files).toEqual(['file', 'file2']); }); it('respects symlinks', () => { diff --git a/packages/fs-node/src/util.ts b/packages/fs-node/src/util.ts index 42c0d62f4..8b0160088 100644 --- a/packages/fs-node/src/util.ts +++ b/packages/fs-node/src/util.ts @@ -82,7 +82,7 @@ export function pathToFilename(path: misc.PathLike): string { } const pathString = String(path); - if (pathString === ".") return "./" + if (pathString === '.') return './'; nullCheck(pathString); // return slash(pathString); return pathString; diff --git a/packages/fs-node/src/volume.ts b/packages/fs-node/src/volume.ts index 7f4ad1ec4..5544b7143 100644 --- a/packages/fs-node/src/volume.ts +++ b/packages/fs-node/src/volume.ts @@ -12,6 +12,7 @@ import { FanOutUnsubscribe } from 'thingies/lib/fanout'; import { Link, Superblock, + SuperBlockFromJsonOptions, DirectoryJSON, NestedDirectoryJSON, ERROR_CODE, @@ -23,7 +24,7 @@ import { validateFd, Ok, Result, -} from '@jsonjoy.com/fs-core'; +} from '@jsonjoy.com/fs-core/lib/index'; import { isWin } from '@jsonjoy.com/fs-core/lib/util'; import Stats from './Stats'; import Dirent from './Dirent'; @@ -176,11 +177,11 @@ function validateGid(gid: number) { * `Volume` represents a file system. */ export class Volume implements FsCallbackApi, FsSynchronousApi { - public static readonly fromJSON = (json: DirectoryJSON, cwd: string = '/'): Volume => - new Volume(Superblock.fromJSON(json, cwd)); + public static readonly fromJSON = (json: DirectoryJSON, options?: SuperBlockFromJsonOptions): Volume => + new Volume(Superblock.fromJSON(json, options)); - public static readonly fromNestedJSON = (json: NestedDirectoryJSON, cwd: string = '/'): Volume => - new Volume(Superblock.fromNestedJSON(json, cwd)); + public static readonly fromNestedJSON = (json: NestedDirectoryJSON, options?: SuperBlockFromJsonOptions): Volume => + new Volume(Superblock.fromNestedJSON(json, options)); StatWatcher: new () => StatWatcher; ReadStream: new (...args) => misc.IReadStream; @@ -253,6 +254,12 @@ export class Volume implements FsCallbackApi, FsSynchronousApi { this.realpathSync.native = realpathSyncImpl as any; } + get cwd(): string { + return this._core.cwd; + } + set cwd(value: string) { + this._core.cwd = value; + } private wrapAsync(method: (...args: Args) => void, args: Args, callback: misc.TCallback) { validateCallback(callback); Promise.resolve().then(() => { @@ -275,17 +282,17 @@ export class Volume implements FsCallbackApi, FsSynchronousApi { return this._core.toJSON(paths, json, isRelative, asBuffer); } - fromJSON(json: DirectoryJSON, cwd?: string) { - return this._core.fromJSON(json, cwd); + fromJSON(json: DirectoryJSON, options?: SuperBlockFromJsonOptions) { + return this._core.fromJSON(json, options); } - fromNestedJSON(json: NestedDirectoryJSON, cwd?: string) { - return this._core.fromNestedJSON(json, cwd); + fromNestedJSON(json: NestedDirectoryJSON, options?: SuperBlockFromJsonOptions) { + return this._core.fromNestedJSON(json, options); } // Legacy interface mountSync(mountpoint: string, json: DirectoryJSON) { - this._core.fromJSON(json, mountpoint); + this._core.fromJSON(json, { mountpoint: mountpoint }); } public openSync = (path: PathLike, flags: TFlags, mode: TMode = MODE.DEFAULT): number => { diff --git a/packages/memfs/src/index.ts b/packages/memfs/src/index.ts index ec4f4aef1..b4636c588 100644 --- a/packages/memfs/src/index.ts +++ b/packages/memfs/src/index.ts @@ -13,6 +13,7 @@ import { DirectoryJSON, NestedDirectoryJSON } from '@jsonjoy.com/fs-core'; import { constants } from '@jsonjoy.com/fs-node-utils'; import type { FsPromisesApi } from '@jsonjoy.com/fs-node-utils'; import type * as misc from '@jsonjoy.com/fs-node-utils/lib/types/misc'; +import type { SuperBlockFromJsonOptions } from '@jsonjoy.com/fs-core/lib/Superblock'; const { F_OK, R_OK, W_OK, X_OK } = constants; @@ -78,8 +79,11 @@ export const fs: IFs = createFsFromVolume(vol); * @returns A `memfs` file system instance, which is a drop-in replacement for * the `fs` module. */ -export const memfs = (json: NestedDirectoryJSON = {}, cwd: string = '/'): { fs: IFs; vol: Volume } => { - const vol = Volume.fromNestedJSON(json, cwd); +export const memfs = ( + json: NestedDirectoryJSON = {}, + options?: SuperBlockFromJsonOptions, +): { fs: IFs; vol: Volume } => { + const vol = Volume.fromNestedJSON(json, options); const fs = createFsFromVolume(vol); return { fs, vol }; }; From 5793194fad04cf3ef33464d2d17469eac969e2b3 Mon Sep 17 00:00:00 2001 From: Mopsgamer <79159094+Mopsgamer@users.noreply.github.com> Date: Sun, 1 Feb 2026 14:58:58 +0100 Subject: [PATCH 07/12] refactor: lowercase b --- packages/fs-core/src/Superblock.ts | 12 ++++++------ packages/fs-core/src/index.ts | 2 +- packages/fs-node/src/__tests__/util.ts | 8 ++++---- packages/fs-node/src/volume.ts | 10 +++++----- packages/memfs/src/index.ts | 4 ++-- 5 files changed, 18 insertions(+), 18 deletions(-) diff --git a/packages/fs-core/src/Superblock.ts b/packages/fs-core/src/Superblock.ts index e43339ff0..03da2b1bf 100644 --- a/packages/fs-core/src/Superblock.ts +++ b/packages/fs-core/src/Superblock.ts @@ -31,7 +31,7 @@ const { O_RDONLY, O_WRONLY, O_RDWR, O_CREAT, O_EXCL, O_TRUNC, O_APPEND, O_DIRECT /** * Represents options for creating a Superblock from JSON. */ -export type SuperBlockFromJsonOptions = { +export type SuperblockFromJsonOptions = { /** * Current working directory, absolute path. * Methods will use this as the base path for relative paths. @@ -43,7 +43,7 @@ export type SuperBlockFromJsonOptions = { * This is the mount point where the JSON structure will be mounted. * It is used as the base path for all paths in the JSON structure. * - * @default SuperBlockFromJsonOptions.cwd + * @default SuperblockFromJsonOptions.cwd */ mountpoint?: string; }; @@ -54,13 +54,13 @@ export type SuperBlockFromJsonOptions = { * @see https://lxr.linux.no/linux+v3.11.2/include/linux/fs.h#L1242 */ export class Superblock { - static fromJSON(json: DirectoryJSON, options?: SuperBlockFromJsonOptions): Superblock { + static fromJSON(json: DirectoryJSON, options?: SuperblockFromJsonOptions): Superblock { const vol = new Superblock(); vol.fromJSON(json, options); return vol; } - static fromNestedJSON(json: NestedDirectoryJSON, options?: SuperBlockFromJsonOptions): Superblock { + static fromNestedJSON(json: NestedDirectoryJSON, options?: SuperblockFromJsonOptions): Superblock { const vol = new Superblock(); vol.fromNestedJSON(json, options); return vol; @@ -432,7 +432,7 @@ export class Superblock { return json; } - fromJSON(json: DirectoryJSON, options?: SuperBlockFromJsonOptions) { + fromJSON(json: DirectoryJSON, options?: SuperblockFromJsonOptions) { this.cwd = options?.cwd ?? process.cwd(); for (let filename in json) { const data = json[filename]; @@ -448,7 +448,7 @@ export class Superblock { } } - fromNestedJSON(json: NestedDirectoryJSON, options?: SuperBlockFromJsonOptions) { + fromNestedJSON(json: NestedDirectoryJSON, options?: SuperblockFromJsonOptions) { this.fromJSON(flattenJSON(json), options); } diff --git a/packages/fs-core/src/index.ts b/packages/fs-core/src/index.ts index 294f15f6a..262356026 100644 --- a/packages/fs-core/src/index.ts +++ b/packages/fs-core/src/index.ts @@ -5,7 +5,7 @@ export * from './result'; export { Node, type NodeEvent } from './Node'; export { Link, type LinkEvent } from './Link'; export { File } from './File'; -export { Superblock, SuperBlockFromJsonOptions } from './Superblock'; +export { Superblock, SuperblockFromJsonOptions } from './Superblock'; export { dataToBuffer, filenameToSteps, diff --git a/packages/fs-node/src/__tests__/util.ts b/packages/fs-node/src/__tests__/util.ts index e45a4eb6c..98c4230c0 100644 --- a/packages/fs-node/src/__tests__/util.ts +++ b/packages/fs-node/src/__tests__/util.ts @@ -1,6 +1,6 @@ import { Volume } from '..'; import { Link, Node, NestedDirectoryJSON } from '@jsonjoy.com/fs-core'; -import type { SuperBlockFromJsonOptions } from '@jsonjoy.com/fs-core/lib/Superblock'; +import type { SuperblockFromJsonOptions } from '@jsonjoy.com/fs-core/lib/Superblock'; export const multitest = (_done: (err?: Error) => void, times: number) => { let err; @@ -10,19 +10,19 @@ export const multitest = (_done: (err?: Error) => void, times: number) => { }; }; -export const create = (json: { [s: string]: string } = { '/foo': 'bar' }, options?: SuperBlockFromJsonOptions) => { +export const create = (json: { [s: string]: string } = { '/foo': 'bar' }, options?: SuperblockFromJsonOptions) => { options ??= {}; options.cwd ??= '/'; const vol = Volume.fromJSON(json, options); return vol; }; -export const createFs = (json?, options?: SuperBlockFromJsonOptions) => { +export const createFs = (json?, options?: SuperblockFromJsonOptions) => { const vol = create(json, options); return vol; }; -export const memfs = (json: NestedDirectoryJSON = {}, options?: SuperBlockFromJsonOptions) => { +export const memfs = (json: NestedDirectoryJSON = {}, options?: SuperblockFromJsonOptions) => { const vol = Volume.fromNestedJSON(json, options); return { fs: vol, vol }; }; diff --git a/packages/fs-node/src/volume.ts b/packages/fs-node/src/volume.ts index 5544b7143..80c3fd289 100644 --- a/packages/fs-node/src/volume.ts +++ b/packages/fs-node/src/volume.ts @@ -12,7 +12,7 @@ import { FanOutUnsubscribe } from 'thingies/lib/fanout'; import { Link, Superblock, - SuperBlockFromJsonOptions, + SuperblockFromJsonOptions, DirectoryJSON, NestedDirectoryJSON, ERROR_CODE, @@ -177,10 +177,10 @@ function validateGid(gid: number) { * `Volume` represents a file system. */ export class Volume implements FsCallbackApi, FsSynchronousApi { - public static readonly fromJSON = (json: DirectoryJSON, options?: SuperBlockFromJsonOptions): Volume => + public static readonly fromJSON = (json: DirectoryJSON, options?: SuperblockFromJsonOptions): Volume => new Volume(Superblock.fromJSON(json, options)); - public static readonly fromNestedJSON = (json: NestedDirectoryJSON, options?: SuperBlockFromJsonOptions): Volume => + public static readonly fromNestedJSON = (json: NestedDirectoryJSON, options?: SuperblockFromJsonOptions): Volume => new Volume(Superblock.fromNestedJSON(json, options)); StatWatcher: new () => StatWatcher; @@ -282,11 +282,11 @@ export class Volume implements FsCallbackApi, FsSynchronousApi { return this._core.toJSON(paths, json, isRelative, asBuffer); } - fromJSON(json: DirectoryJSON, options?: SuperBlockFromJsonOptions) { + fromJSON(json: DirectoryJSON, options?: SuperblockFromJsonOptions) { return this._core.fromJSON(json, options); } - fromNestedJSON(json: NestedDirectoryJSON, options?: SuperBlockFromJsonOptions) { + fromNestedJSON(json: NestedDirectoryJSON, options?: SuperblockFromJsonOptions) { return this._core.fromNestedJSON(json, options); } diff --git a/packages/memfs/src/index.ts b/packages/memfs/src/index.ts index b4636c588..5cdb031ce 100644 --- a/packages/memfs/src/index.ts +++ b/packages/memfs/src/index.ts @@ -13,7 +13,7 @@ import { DirectoryJSON, NestedDirectoryJSON } from '@jsonjoy.com/fs-core'; import { constants } from '@jsonjoy.com/fs-node-utils'; import type { FsPromisesApi } from '@jsonjoy.com/fs-node-utils'; import type * as misc from '@jsonjoy.com/fs-node-utils/lib/types/misc'; -import type { SuperBlockFromJsonOptions } from '@jsonjoy.com/fs-core/lib/Superblock'; +import type { SuperblockFromJsonOptions } from '@jsonjoy.com/fs-core/lib/Superblock'; const { F_OK, R_OK, W_OK, X_OK } = constants; @@ -81,7 +81,7 @@ export const fs: IFs = createFsFromVolume(vol); */ export const memfs = ( json: NestedDirectoryJSON = {}, - options?: SuperBlockFromJsonOptions, + options?: SuperblockFromJsonOptions, ): { fs: IFs; vol: Volume } => { const vol = Volume.fromNestedJSON(json, options); const fs = createFsFromVolume(vol); From 7e2a2c44e753821ed0cf9ffd04a435f7f11e5dad Mon Sep 17 00:00:00 2001 From: Mopsgamer <79159094+Mopsgamer@users.noreply.github.com> Date: Sun, 1 Feb 2026 15:55:48 +0100 Subject: [PATCH 08/12] test: pass --- docs/node/usage.md | 2 +- packages/fs-core/src/Superblock.ts | 7 ++++--- .../CoreFileSystemDirectoryHandle.test.ts | 4 ++-- .../__tests__/CoreFileSystemFileHandle.test.ts | 18 +++++++++--------- .../src/__tests__/CoreFileSystemHandle.test.ts | 4 ++-- .../NodeFileSystemDirectoryHandle.test.ts | 10 +++++----- .../__tests__/NodeFileSystemFileHandle.test.ts | 18 +++++++++--------- .../src/__tests__/NodeFileSystemHandle.test.ts | 2 +- .../NodeFileSystemSyncAccessHandle.test.ts | 2 +- packages/fs-node/src/__tests__/volume.test.ts | 4 ++-- .../src/__tests__/volume/opendir.test.ts | 2 +- .../src/__tests__/volume/readFile.test.ts | 4 ++-- .../src/__tests__/volume/readdirSync.test.ts | 2 +- 13 files changed, 40 insertions(+), 39 deletions(-) diff --git a/docs/node/usage.md b/docs/node/usage.md index e9e49f5c0..7aebadd5e 100644 --- a/docs/node/usage.md +++ b/docs/node/usage.md @@ -19,7 +19,7 @@ const json = { './src/index.js': '2', './node_modules/debug/index.js': '3', }; -vol.fromJSON(json, '/app'); +vol.fromJSON(json, { cwd: '/app' }); fs.readFileSync('/app/README.md', 'utf8'); // 1 vol.readFileSync('/app/src/index.js', 'utf8'); // 2 diff --git a/packages/fs-core/src/Superblock.ts b/packages/fs-core/src/Superblock.ts index 03da2b1bf..4bec83588 100644 --- a/packages/fs-core/src/Superblock.ts +++ b/packages/fs-core/src/Superblock.ts @@ -117,7 +117,7 @@ export class Superblock { this.root = root; } - protected _cwd: string = process.cwd(); + protected _cwd: string = '/'; get cwd(): string { return this._cwd; } @@ -433,10 +433,11 @@ export class Superblock { } fromJSON(json: DirectoryJSON, options?: SuperblockFromJsonOptions) { - this.cwd = options?.cwd ?? process.cwd(); + this.cwd = options?.cwd ?? '/'; + const mountpoint = options?.mountpoint ?? this.cwd; for (let filename in json) { const data = json[filename]; - filename = resolve(filename, options?.mountpoint ?? this.cwd); + filename = resolve(filename, mountpoint); if (typeof data === 'string' || data instanceof Buffer) { const dir = dirname(filename); this.mkdirp(dir, MODE.DIR); diff --git a/packages/fs-fsa/src/__tests__/CoreFileSystemDirectoryHandle.test.ts b/packages/fs-fsa/src/__tests__/CoreFileSystemDirectoryHandle.test.ts index 330943ebb..9cd8a0a29 100644 --- a/packages/fs-fsa/src/__tests__/CoreFileSystemDirectoryHandle.test.ts +++ b/packages/fs-fsa/src/__tests__/CoreFileSystemDirectoryHandle.test.ts @@ -5,7 +5,7 @@ import { CoreFileSystemHandle } from '../CoreFileSystemHandle'; import { onlyOnNode20 } from './util'; const setup = (json: DirectoryJSON = {}) => { - const core = Superblock.fromJSON(json, '/'); + const core = Superblock.fromJSON(json, { cwd: '/' }); const dir = new CoreFileSystemDirectoryHandle(core, '/', { mode: 'readwrite' }); return { dir, core }; }; @@ -192,7 +192,7 @@ onlyOnNode20('CoreFileSystemDirectoryHandle', () => { const { dir: dir1 } = setup({ 'file1.txt': 'content' }); // Create completely different core and root path - const core2 = Superblock.fromJSON({ 'different/file2.txt': 'content' }, '/'); + const core2 = Superblock.fromJSON({ 'different/file2.txt': 'content' }, { cwd: '/' }); // Use a different path that does not start with dir1's path const dir2 = new CoreFileSystemDirectoryHandle(core2, '/some-completely-different-path/', { mode: 'readwrite' }); diff --git a/packages/fs-fsa/src/__tests__/CoreFileSystemFileHandle.test.ts b/packages/fs-fsa/src/__tests__/CoreFileSystemFileHandle.test.ts index d3b2c7d0b..29a53943f 100644 --- a/packages/fs-fsa/src/__tests__/CoreFileSystemFileHandle.test.ts +++ b/packages/fs-fsa/src/__tests__/CoreFileSystemFileHandle.test.ts @@ -4,7 +4,7 @@ import { CoreFileSystemFileHandle } from '../CoreFileSystemFileHandle'; import { onlyOnNode20 } from './util'; const setup = (json: DirectoryJSON = {}) => { - const core = Superblock.fromJSON(json, '/'); + const core = Superblock.fromJSON(json, { cwd: '/' }); const dir = new CoreFileSystemDirectoryHandle(core, '/', { mode: 'readwrite' }); return { dir, core }; }; @@ -84,7 +84,7 @@ onlyOnNode20('CoreFileSystemFileHandle', () => { }); test('returns function when sync handle allowed', async () => { - const core = Superblock.fromJSON({ 'test.txt': 'content' }, '/'); + const core = Superblock.fromJSON({ 'test.txt': 'content' }, { cwd: '/' }); const dir = new CoreFileSystemDirectoryHandle(core, '/', { mode: 'readwrite', syncHandleAllowed: true, @@ -116,7 +116,7 @@ onlyOnNode20('CoreFileSystemFileHandle', () => { describe('file locking', () => { describe('sync access handle locking', () => { test('creates sync access handle successfully', async () => { - const core = Superblock.fromJSON({ 'test.txt': 'content' }, '/'); + const core = Superblock.fromJSON({ 'test.txt': 'content' }, { cwd: '/' }); const dir = new CoreFileSystemDirectoryHandle(core, '/', { mode: 'readwrite', syncHandleAllowed: true, @@ -128,7 +128,7 @@ onlyOnNode20('CoreFileSystemFileHandle', () => { }); test('throws NoModificationAllowedError when creating sync handle while file is locked', async () => { - const core = Superblock.fromJSON({ 'test.txt': 'content' }, '/'); + const core = Superblock.fromJSON({ 'test.txt': 'content' }, { cwd: '/' }); const dir = new CoreFileSystemDirectoryHandle(core, '/', { mode: 'readwrite', syncHandleAllowed: true, @@ -153,7 +153,7 @@ onlyOnNode20('CoreFileSystemFileHandle', () => { }); test('allows creating sync handle after previous one is closed', async () => { - const core = Superblock.fromJSON({ 'test.txt': 'content' }, '/'); + const core = Superblock.fromJSON({ 'test.txt': 'content' }, { cwd: '/' }); const dir = new CoreFileSystemDirectoryHandle(core, '/', { mode: 'readwrite', syncHandleAllowed: true, @@ -180,7 +180,7 @@ onlyOnNode20('CoreFileSystemFileHandle', () => { }); test('throws NoModificationAllowedError when creating writable stream while sync handle is open', async () => { - const core = Superblock.fromJSON({ 'test.txt': 'content' }, '/'); + const core = Superblock.fromJSON({ 'test.txt': 'content' }, { cwd: '/' }); const dir = new CoreFileSystemDirectoryHandle(core, '/', { mode: 'readwrite', syncHandleAllowed: true, @@ -205,7 +205,7 @@ onlyOnNode20('CoreFileSystemFileHandle', () => { }); test('allows creating writable stream after sync handle is closed', async () => { - const core = Superblock.fromJSON({ 'test.txt': 'content' }, '/'); + const core = Superblock.fromJSON({ 'test.txt': 'content' }, { cwd: '/' }); const dir = new CoreFileSystemDirectoryHandle(core, '/', { mode: 'readwrite', syncHandleAllowed: true, @@ -222,7 +222,7 @@ onlyOnNode20('CoreFileSystemFileHandle', () => { }); test('throws NoModificationAllowedError when creating sync handle while writable stream is open', async () => { - const core = Superblock.fromJSON({ 'test.txt': 'content' }, '/'); + const core = Superblock.fromJSON({ 'test.txt': 'content' }, { cwd: '/' }); const dir = new CoreFileSystemDirectoryHandle(core, '/', { mode: 'readwrite', syncHandleAllowed: true, @@ -247,7 +247,7 @@ onlyOnNode20('CoreFileSystemFileHandle', () => { }); test('allows creating sync handle after writable stream is closed', async () => { - const core = Superblock.fromJSON({ 'test.txt': 'content' }, '/'); + const core = Superblock.fromJSON({ 'test.txt': 'content' }, { cwd: '/' }); const dir = new CoreFileSystemDirectoryHandle(core, '/', { mode: 'readwrite', syncHandleAllowed: true, diff --git a/packages/fs-fsa/src/__tests__/CoreFileSystemHandle.test.ts b/packages/fs-fsa/src/__tests__/CoreFileSystemHandle.test.ts index 43241e2a2..daff5dab6 100644 --- a/packages/fs-fsa/src/__tests__/CoreFileSystemHandle.test.ts +++ b/packages/fs-fsa/src/__tests__/CoreFileSystemHandle.test.ts @@ -3,7 +3,7 @@ import { CoreFileSystemDirectoryHandle } from '../CoreFileSystemDirectoryHandle' import { onlyOnNode20 } from './util'; const setup = (json: DirectoryJSON = {}) => { - const core = Superblock.fromJSON(json, '/'); + const core = Superblock.fromJSON(json, { cwd: '/' }); const dir = new CoreFileSystemDirectoryHandle(core, '/', { mode: 'readwrite' }); return { dir, core }; }; @@ -66,7 +66,7 @@ onlyOnNode20('CoreFileSystemHandle', () => { }); test('queryPermission denies readwrite when context only allows read', async () => { - const core = Superblock.fromJSON({ 'test.txt': 'content' }, '/'); + const core = Superblock.fromJSON({ 'test.txt': 'content' }, { cwd: '/' }); const dir = new CoreFileSystemDirectoryHandle(core, '/', { mode: 'read' }); const file = await dir.getFileHandle('test.txt'); diff --git a/packages/fs-node-to-fsa/src/__tests__/NodeFileSystemDirectoryHandle.test.ts b/packages/fs-node-to-fsa/src/__tests__/NodeFileSystemDirectoryHandle.test.ts index 854c3040f..fbf61e05e 100644 --- a/packages/fs-node-to-fsa/src/__tests__/NodeFileSystemDirectoryHandle.test.ts +++ b/packages/fs-node-to-fsa/src/__tests__/NodeFileSystemDirectoryHandle.test.ts @@ -5,7 +5,7 @@ import { NodeFileSystemHandle } from '../NodeFileSystemHandle'; import { onlyOnNode20 } from './util'; const setup = (json: DirectoryJSON = {}) => { - const { fs } = memfs(json, '/'); + const { fs } = memfs(json, { cwd: '/' }); const dir = new NodeFileSystemDirectoryHandle(fs as any, '/', { mode: 'readwrite' }); return { dir, fs }; }; @@ -162,7 +162,7 @@ onlyOnNode20('NodeFileSystemDirectoryHandle', () => { }); test('throws if not in "readwrite" mode and attempting to create a directory', async () => { - const { fs } = memfs({}, '/'); + const { fs } = memfs({}, { cwd: '/' }); const dir = new NodeFileSystemDirectoryHandle(fs as any, '/', { mode: 'read' }); try { await dir.getDirectoryHandle('test', { create: true }); @@ -274,7 +274,7 @@ onlyOnNode20('NodeFileSystemDirectoryHandle', () => { }); test('throws if not in "readwrite" mode and attempting to create a file', async () => { - const { fs } = memfs({}, '/'); + const { fs } = memfs({}, { cwd: '/' }); const dir = new NodeFileSystemDirectoryHandle(fs as any, '/', { mode: 'read' }); try { await dir.getFileHandle('test', { create: true }); @@ -364,7 +364,7 @@ onlyOnNode20('NodeFileSystemDirectoryHandle', () => { }); test('throws if not in "readwrite" mode and attempting to remove a file', async () => { - const { fs } = memfs({ a: 'b' }, '/'); + const { fs } = memfs({ a: 'b' }, { cwd: '/' }); const dir = new NodeFileSystemDirectoryHandle(fs as any, '/', { mode: 'read' }); try { await dir.removeEntry('a'); @@ -379,7 +379,7 @@ onlyOnNode20('NodeFileSystemDirectoryHandle', () => { }); test('throws if not in "readwrite" mode and attempting to remove a folder', async () => { - const { fs } = memfs({ a: null }, '/'); + const { fs } = memfs({ a: null }, { cwd: '/' }); const dir = new NodeFileSystemDirectoryHandle(fs as any, '/', { mode: 'read' }); try { await dir.removeEntry('a'); diff --git a/packages/fs-node-to-fsa/src/__tests__/NodeFileSystemFileHandle.test.ts b/packages/fs-node-to-fsa/src/__tests__/NodeFileSystemFileHandle.test.ts index 4d1d6645d..1c7a1f117 100644 --- a/packages/fs-node-to-fsa/src/__tests__/NodeFileSystemFileHandle.test.ts +++ b/packages/fs-node-to-fsa/src/__tests__/NodeFileSystemFileHandle.test.ts @@ -3,7 +3,7 @@ import { NodeFileSystemDirectoryHandle } from '../NodeFileSystemDirectoryHandle' import { onlyOnNode20 } from './util'; const setup = (json: DirectoryJSON = {}) => { - const { fs, vol } = memfs(json, '/'); + const { fs, vol } = memfs(json, { cwd: '/' }); const dir = new NodeFileSystemDirectoryHandle(fs as any, '/', { mode: 'readwrite' }); return { dir, fs, vol }; }; @@ -27,7 +27,7 @@ onlyOnNode20('NodeFileSystemFileHandle', () => { describe('.createWritable()', () => { test('throws if not in "readwrite" mode', async () => { - const { fs } = memfs({ 'file.txt': 'abc' }, '/'); + const { fs } = memfs({ 'file.txt': 'abc' }, { cwd: '/' }); const dir = new NodeFileSystemDirectoryHandle(fs as any, '/', { mode: 'read' }); const entry = await dir.getFileHandle('file.txt'); try { @@ -197,7 +197,7 @@ onlyOnNode20('NodeFileSystemFileHandle', () => { describe('file locking', () => { describe('sync access handle locking', () => { test('creates sync access handle successfully', async () => { - const { fs } = memfs({ 'test.txt': 'content' }, '/'); + const { fs } = memfs({ 'test.txt': 'content' }, { cwd: '/' }); const dir = new NodeFileSystemDirectoryHandle(fs as any, '/', { syncHandleAllowed: true, mode: 'readwrite', @@ -209,7 +209,7 @@ onlyOnNode20('NodeFileSystemFileHandle', () => { }); test('throws NoModificationAllowedError when creating sync handle while file is locked', async () => { - const { fs } = memfs({ 'file.txt': 'content' }, '/'); + const { fs } = memfs({ 'file.txt': 'content' }, { cwd: '/' }); const dir = new NodeFileSystemDirectoryHandle(fs as any, '/', { syncHandleAllowed: true, mode: 'readwrite', @@ -230,7 +230,7 @@ onlyOnNode20('NodeFileSystemFileHandle', () => { }); test('allows creating sync handle after previous one is closed', async () => { - const { fs } = memfs({ 'file.txt': 'content' }, '/'); + const { fs } = memfs({ 'file.txt': 'content' }, { cwd: '/' }); const dir = new NodeFileSystemDirectoryHandle(fs as any, '/', { syncHandleAllowed: true, mode: 'readwrite', @@ -257,7 +257,7 @@ onlyOnNode20('NodeFileSystemFileHandle', () => { }); test('throws NoModificationAllowedError when creating writable stream while sync handle is open', async () => { - const { fs } = memfs({ 'file.txt': 'content' }, '/'); + const { fs } = memfs({ 'file.txt': 'content' }, { cwd: '/' }); const dir = new NodeFileSystemDirectoryHandle(fs as any, '/', { syncHandleAllowed: true, mode: 'readwrite', @@ -278,7 +278,7 @@ onlyOnNode20('NodeFileSystemFileHandle', () => { }); test('allows creating writable stream after sync handle is closed', async () => { - const { fs } = memfs({ 'file.txt': 'content' }, '/'); + const { fs } = memfs({ 'file.txt': 'content' }, { cwd: '/' }); const dir = new NodeFileSystemDirectoryHandle(fs as any, '/', { syncHandleAllowed: true, mode: 'readwrite', @@ -295,7 +295,7 @@ onlyOnNode20('NodeFileSystemFileHandle', () => { }); test('throws NoModificationAllowedError when creating sync handle while writable stream is open', async () => { - const { fs } = memfs({ 'file.txt': 'content' }, '/'); + const { fs } = memfs({ 'file.txt': 'content' }, { cwd: '/' }); const dir = new NodeFileSystemDirectoryHandle(fs as any, '/', { syncHandleAllowed: true, mode: 'readwrite', @@ -316,7 +316,7 @@ onlyOnNode20('NodeFileSystemFileHandle', () => { }); test('allows creating sync handle after writable stream is closed', async () => { - const { fs } = memfs({ 'file.txt': 'content' }, '/'); + const { fs } = memfs({ 'file.txt': 'content' }, { cwd: '/' }); const dir = new NodeFileSystemDirectoryHandle(fs as any, '/', { syncHandleAllowed: true, mode: 'readwrite', diff --git a/packages/fs-node-to-fsa/src/__tests__/NodeFileSystemHandle.test.ts b/packages/fs-node-to-fsa/src/__tests__/NodeFileSystemHandle.test.ts index a516044d8..bea2a5822 100644 --- a/packages/fs-node-to-fsa/src/__tests__/NodeFileSystemHandle.test.ts +++ b/packages/fs-node-to-fsa/src/__tests__/NodeFileSystemHandle.test.ts @@ -3,7 +3,7 @@ import { NodeFileSystemDirectoryHandle } from '../NodeFileSystemDirectoryHandle' import { onlyOnNode20 } from './util'; const setup = (json: DirectoryJSON = {}) => { - const { fs } = memfs(json, '/'); + const { fs } = memfs(json, { cwd: '/' }); const dir = new NodeFileSystemDirectoryHandle(fs as any, '/', { mode: 'readwrite' }); return { dir, fs }; }; diff --git a/packages/fs-node-to-fsa/src/__tests__/NodeFileSystemSyncAccessHandle.test.ts b/packages/fs-node-to-fsa/src/__tests__/NodeFileSystemSyncAccessHandle.test.ts index 6a6bd006a..4bd36d157 100644 --- a/packages/fs-node-to-fsa/src/__tests__/NodeFileSystemSyncAccessHandle.test.ts +++ b/packages/fs-node-to-fsa/src/__tests__/NodeFileSystemSyncAccessHandle.test.ts @@ -4,7 +4,7 @@ import { NodeFileSystemSyncAccessHandle } from '../NodeFileSystemSyncAccessHandl import { onlyOnNode20 } from './util'; const setup = (json: DirectoryJSON = {}) => { - const { fs } = memfs(json, '/'); + const { fs } = memfs(json, { cwd: '/' }); const dir = new NodeFileSystemDirectoryHandle(fs as any, '/', { syncHandleAllowed: true, mode: 'readwrite' }); return { dir, fs }; }; diff --git a/packages/fs-node/src/__tests__/volume.test.ts b/packages/fs-node/src/__tests__/volume.test.ts index a49db509f..062eeff19 100644 --- a/packages/fs-node/src/__tests__/volume.test.ts +++ b/packages/fs-node/src/__tests__/volume.test.ts @@ -225,7 +225,7 @@ describe('volume', () => { 'app.js': 'console.log(123)', dir: null, }; - vol.fromJSON(json, '/'); + vol.fromJSON(json, { cwd: '/' }); expect(vol.toJSON()).toEqual({ '/hello': 'world', '/app.js': 'console.log(123)', @@ -463,7 +463,7 @@ describe('volume', () => { expect(fd).toBeGreaterThan(0); done(); }); - }, 100); + }); it('Error on file not found', done => { vol.open('/non-existing-file.txt', 'r', (err, fd) => { expect(err).toHaveProperty('code', 'ENOENT'); diff --git a/packages/fs-node/src/__tests__/volume/opendir.test.ts b/packages/fs-node/src/__tests__/volume/opendir.test.ts index a32924ff6..33b9ebf50 100644 --- a/packages/fs-node/src/__tests__/volume/opendir.test.ts +++ b/packages/fs-node/src/__tests__/volume/opendir.test.ts @@ -29,7 +29,7 @@ describe('opendir', () => { '/test/file1.txt': 'content1', '/test/file2.txt': 'content2', }, - '/test', + { cwd: '/test' }, ); const dir = vol.opendirSync('.'); diff --git a/packages/fs-node/src/__tests__/volume/readFile.test.ts b/packages/fs-node/src/__tests__/volume/readFile.test.ts index ffebdc854..2738c6b98 100644 --- a/packages/fs-node/src/__tests__/volume/readFile.test.ts +++ b/packages/fs-node/src/__tests__/volume/readFile.test.ts @@ -3,13 +3,13 @@ import { memfs } from '../util'; describe('.readFile()', () => { it('can read a file in cwd', async () => { - const { fs } = memfs({ 'test.txt': '01234567' }, '/dir'); + const { fs } = memfs({ 'test.txt': '01234567' }, { cwd: '/dir' }); await expect(fs.promises.readFile('/dir/test.txt', { encoding: 'utf8' })).resolves.toBe('01234567'); await expect(fs.readFileSync('/dir/test.txt', { encoding: 'utf8' })).toBe('01234567'); }); it('can read a relative file in cwd', async () => { - const { fs } = memfs({ 'test.txt': '01234567' }, '/dir'); + const { fs } = memfs({ 'test.txt': '01234567' }, { cwd: '/dir' }); await expect(fs.promises.readFile('test.txt', { encoding: 'utf8' })).resolves.toBe('01234567'); await expect(fs.promises.readFile('./test.txt', { encoding: 'utf8' })).resolves.toBe('01234567'); await expect(fs.readFileSync('test.txt', { encoding: 'utf8' })).toBe('01234567'); diff --git a/packages/fs-node/src/__tests__/volume/readdirSync.test.ts b/packages/fs-node/src/__tests__/volume/readdirSync.test.ts index 1c6f07e37..b5ea47bdd 100644 --- a/packages/fs-node/src/__tests__/volume/readdirSync.test.ts +++ b/packages/fs-node/src/__tests__/volume/readdirSync.test.ts @@ -36,7 +36,7 @@ describe('readdirSync()', () => { '/foo/bar/file': 'content', '/foo/bar/file2': 'content2', }, - '/foo', + { cwd: '/foo' }, ); const files = vol.readdirSync('bar'); From 4402a096a2d06eb661415164afc783f85ce36cbc Mon Sep 17 00:00:00 2001 From: Mopsgamer <79159094+Mopsgamer@users.noreply.github.com> Date: Sun, 1 Feb 2026 16:00:55 +0100 Subject: [PATCH 09/12] docs: FromJsonOptions accepts relative --- packages/fs-core/src/Superblock.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/fs-core/src/Superblock.ts b/packages/fs-core/src/Superblock.ts index 4bec83588..eeacf17ca 100644 --- a/packages/fs-core/src/Superblock.ts +++ b/packages/fs-core/src/Superblock.ts @@ -33,14 +33,14 @@ const { O_RDONLY, O_WRONLY, O_RDWR, O_CREAT, O_EXCL, O_TRUNC, O_APPEND, O_DIRECT */ export type SuperblockFromJsonOptions = { /** - * Current working directory, absolute path. + * Current working directory; absolute or relative path. * Methods will use this as the base path for relative paths. * * @default process.cwd() */ cwd?: string; /** - * This is the mount point where the JSON structure will be mounted. + * This is the mount point; absolute or relative path. * It is used as the base path for all paths in the JSON structure. * * @default SuperblockFromJsonOptions.cwd From b6998aa6fb9b32a75c2bf100c7d29c44cf5bda7c Mon Sep 17 00:00:00 2001 From: Mopsgamer <79159094+Mopsgamer@users.noreply.github.com> Date: Tue, 3 Feb 2026 21:48:24 +0100 Subject: [PATCH 10/12] refactor: revert options --- docs/node/usage.md | 2 +- packages/fs-core/src/Superblock.ts | 41 +++++-------------- packages/fs-core/src/index.ts | 2 +- .../CoreFileSystemDirectoryHandle.test.ts | 4 +- .../CoreFileSystemFileHandle.test.ts | 18 ++++---- .../__tests__/CoreFileSystemHandle.test.ts | 4 +- .../NodeFileSystemDirectoryHandle.test.ts | 10 ++--- .../NodeFileSystemFileHandle.test.ts | 18 ++++---- .../__tests__/NodeFileSystemHandle.test.ts | 2 +- .../NodeFileSystemSyncAccessHandle.test.ts | 2 +- packages/fs-node/src/__tests__/util.ts | 15 +++---- packages/fs-node/src/__tests__/volume.test.ts | 2 +- .../fs-node/src/__tests__/volume/cwd.test.ts | 8 +--- .../src/__tests__/volume/opendir.test.ts | 2 +- .../src/__tests__/volume/readFile.test.ts | 4 +- .../src/__tests__/volume/readdirSync.test.ts | 2 +- packages/fs-node/src/volume.ts | 19 ++++----- packages/memfs/src/index.ts | 8 +--- 18 files changed, 65 insertions(+), 98 deletions(-) diff --git a/docs/node/usage.md b/docs/node/usage.md index 7aebadd5e..e9e49f5c0 100644 --- a/docs/node/usage.md +++ b/docs/node/usage.md @@ -19,7 +19,7 @@ const json = { './src/index.js': '2', './node_modules/debug/index.js': '3', }; -vol.fromJSON(json, { cwd: '/app' }); +vol.fromJSON(json, '/app'); fs.readFileSync('/app/README.md', 'utf8'); // 1 vol.readFileSync('/app/src/index.js', 'utf8'); // 2 diff --git a/packages/fs-core/src/Superblock.ts b/packages/fs-core/src/Superblock.ts index eeacf17ca..71cb87dab 100644 --- a/packages/fs-core/src/Superblock.ts +++ b/packages/fs-core/src/Superblock.ts @@ -28,41 +28,21 @@ const pathJoin = posix ? posix.join : join; const { O_RDONLY, O_WRONLY, O_RDWR, O_CREAT, O_EXCL, O_TRUNC, O_APPEND, O_DIRECTORY } = constants; -/** - * Represents options for creating a Superblock from JSON. - */ -export type SuperblockFromJsonOptions = { - /** - * Current working directory; absolute or relative path. - * Methods will use this as the base path for relative paths. - * - * @default process.cwd() - */ - cwd?: string; - /** - * This is the mount point; absolute or relative path. - * It is used as the base path for all paths in the JSON structure. - * - * @default SuperblockFromJsonOptions.cwd - */ - mountpoint?: string; -}; - /** * Represents a filesystem superblock, which is the root of a virtual * filesystem in Linux. * @see https://lxr.linux.no/linux+v3.11.2/include/linux/fs.h#L1242 */ export class Superblock { - static fromJSON(json: DirectoryJSON, options?: SuperblockFromJsonOptions): Superblock { + static fromJSON(json: DirectoryJSON, cwd?: string): Superblock { const vol = new Superblock(); - vol.fromJSON(json, options); + vol.fromJSON(json, cwd); return vol; } - static fromNestedJSON(json: NestedDirectoryJSON, options?: SuperblockFromJsonOptions): Superblock { + static fromNestedJSON(json: NestedDirectoryJSON, cwd?: string): Superblock { const vol = new Superblock(); - vol.fromNestedJSON(json, options); + vol.fromNestedJSON(json, cwd); return vol; } @@ -432,12 +412,11 @@ export class Superblock { return json; } - fromJSON(json: DirectoryJSON, options?: SuperblockFromJsonOptions) { - this.cwd = options?.cwd ?? '/'; - const mountpoint = options?.mountpoint ?? this.cwd; + fromJSON(json: DirectoryJSON, cwd?: string) { + this.cwd = cwd ?? '/'; for (let filename in json) { const data = json[filename]; - filename = resolve(filename, mountpoint); + filename = resolve(filename, this.cwd); if (typeof data === 'string' || data instanceof Buffer) { const dir = dirname(filename); this.mkdirp(dir, MODE.DIR); @@ -449,8 +428,8 @@ export class Superblock { } } - fromNestedJSON(json: NestedDirectoryJSON, options?: SuperblockFromJsonOptions) { - this.fromJSON(flattenJSON(json), options); + fromNestedJSON(json: NestedDirectoryJSON, cwd?: string) { + this.fromJSON(flattenJSON(json), cwd); } reset() { @@ -467,7 +446,7 @@ export class Superblock { // Legacy interface mountSync(mountpoint: string, json: DirectoryJSON) { - this.fromJSON(json, { mountpoint: mountpoint }); + this.fromJSON(json, mountpoint); } openLink(link: Link, flagsNum: number, resolveSymlinks: boolean = true): File { diff --git a/packages/fs-core/src/index.ts b/packages/fs-core/src/index.ts index 262356026..c31e5b8c5 100644 --- a/packages/fs-core/src/index.ts +++ b/packages/fs-core/src/index.ts @@ -5,7 +5,7 @@ export * from './result'; export { Node, type NodeEvent } from './Node'; export { Link, type LinkEvent } from './Link'; export { File } from './File'; -export { Superblock, SuperblockFromJsonOptions } from './Superblock'; +export { Superblock } from './Superblock'; export { dataToBuffer, filenameToSteps, diff --git a/packages/fs-fsa/src/__tests__/CoreFileSystemDirectoryHandle.test.ts b/packages/fs-fsa/src/__tests__/CoreFileSystemDirectoryHandle.test.ts index 9cd8a0a29..330943ebb 100644 --- a/packages/fs-fsa/src/__tests__/CoreFileSystemDirectoryHandle.test.ts +++ b/packages/fs-fsa/src/__tests__/CoreFileSystemDirectoryHandle.test.ts @@ -5,7 +5,7 @@ import { CoreFileSystemHandle } from '../CoreFileSystemHandle'; import { onlyOnNode20 } from './util'; const setup = (json: DirectoryJSON = {}) => { - const core = Superblock.fromJSON(json, { cwd: '/' }); + const core = Superblock.fromJSON(json, '/'); const dir = new CoreFileSystemDirectoryHandle(core, '/', { mode: 'readwrite' }); return { dir, core }; }; @@ -192,7 +192,7 @@ onlyOnNode20('CoreFileSystemDirectoryHandle', () => { const { dir: dir1 } = setup({ 'file1.txt': 'content' }); // Create completely different core and root path - const core2 = Superblock.fromJSON({ 'different/file2.txt': 'content' }, { cwd: '/' }); + const core2 = Superblock.fromJSON({ 'different/file2.txt': 'content' }, '/'); // Use a different path that does not start with dir1's path const dir2 = new CoreFileSystemDirectoryHandle(core2, '/some-completely-different-path/', { mode: 'readwrite' }); diff --git a/packages/fs-fsa/src/__tests__/CoreFileSystemFileHandle.test.ts b/packages/fs-fsa/src/__tests__/CoreFileSystemFileHandle.test.ts index 29a53943f..d3b2c7d0b 100644 --- a/packages/fs-fsa/src/__tests__/CoreFileSystemFileHandle.test.ts +++ b/packages/fs-fsa/src/__tests__/CoreFileSystemFileHandle.test.ts @@ -4,7 +4,7 @@ import { CoreFileSystemFileHandle } from '../CoreFileSystemFileHandle'; import { onlyOnNode20 } from './util'; const setup = (json: DirectoryJSON = {}) => { - const core = Superblock.fromJSON(json, { cwd: '/' }); + const core = Superblock.fromJSON(json, '/'); const dir = new CoreFileSystemDirectoryHandle(core, '/', { mode: 'readwrite' }); return { dir, core }; }; @@ -84,7 +84,7 @@ onlyOnNode20('CoreFileSystemFileHandle', () => { }); test('returns function when sync handle allowed', async () => { - const core = Superblock.fromJSON({ 'test.txt': 'content' }, { cwd: '/' }); + const core = Superblock.fromJSON({ 'test.txt': 'content' }, '/'); const dir = new CoreFileSystemDirectoryHandle(core, '/', { mode: 'readwrite', syncHandleAllowed: true, @@ -116,7 +116,7 @@ onlyOnNode20('CoreFileSystemFileHandle', () => { describe('file locking', () => { describe('sync access handle locking', () => { test('creates sync access handle successfully', async () => { - const core = Superblock.fromJSON({ 'test.txt': 'content' }, { cwd: '/' }); + const core = Superblock.fromJSON({ 'test.txt': 'content' }, '/'); const dir = new CoreFileSystemDirectoryHandle(core, '/', { mode: 'readwrite', syncHandleAllowed: true, @@ -128,7 +128,7 @@ onlyOnNode20('CoreFileSystemFileHandle', () => { }); test('throws NoModificationAllowedError when creating sync handle while file is locked', async () => { - const core = Superblock.fromJSON({ 'test.txt': 'content' }, { cwd: '/' }); + const core = Superblock.fromJSON({ 'test.txt': 'content' }, '/'); const dir = new CoreFileSystemDirectoryHandle(core, '/', { mode: 'readwrite', syncHandleAllowed: true, @@ -153,7 +153,7 @@ onlyOnNode20('CoreFileSystemFileHandle', () => { }); test('allows creating sync handle after previous one is closed', async () => { - const core = Superblock.fromJSON({ 'test.txt': 'content' }, { cwd: '/' }); + const core = Superblock.fromJSON({ 'test.txt': 'content' }, '/'); const dir = new CoreFileSystemDirectoryHandle(core, '/', { mode: 'readwrite', syncHandleAllowed: true, @@ -180,7 +180,7 @@ onlyOnNode20('CoreFileSystemFileHandle', () => { }); test('throws NoModificationAllowedError when creating writable stream while sync handle is open', async () => { - const core = Superblock.fromJSON({ 'test.txt': 'content' }, { cwd: '/' }); + const core = Superblock.fromJSON({ 'test.txt': 'content' }, '/'); const dir = new CoreFileSystemDirectoryHandle(core, '/', { mode: 'readwrite', syncHandleAllowed: true, @@ -205,7 +205,7 @@ onlyOnNode20('CoreFileSystemFileHandle', () => { }); test('allows creating writable stream after sync handle is closed', async () => { - const core = Superblock.fromJSON({ 'test.txt': 'content' }, { cwd: '/' }); + const core = Superblock.fromJSON({ 'test.txt': 'content' }, '/'); const dir = new CoreFileSystemDirectoryHandle(core, '/', { mode: 'readwrite', syncHandleAllowed: true, @@ -222,7 +222,7 @@ onlyOnNode20('CoreFileSystemFileHandle', () => { }); test('throws NoModificationAllowedError when creating sync handle while writable stream is open', async () => { - const core = Superblock.fromJSON({ 'test.txt': 'content' }, { cwd: '/' }); + const core = Superblock.fromJSON({ 'test.txt': 'content' }, '/'); const dir = new CoreFileSystemDirectoryHandle(core, '/', { mode: 'readwrite', syncHandleAllowed: true, @@ -247,7 +247,7 @@ onlyOnNode20('CoreFileSystemFileHandle', () => { }); test('allows creating sync handle after writable stream is closed', async () => { - const core = Superblock.fromJSON({ 'test.txt': 'content' }, { cwd: '/' }); + const core = Superblock.fromJSON({ 'test.txt': 'content' }, '/'); const dir = new CoreFileSystemDirectoryHandle(core, '/', { mode: 'readwrite', syncHandleAllowed: true, diff --git a/packages/fs-fsa/src/__tests__/CoreFileSystemHandle.test.ts b/packages/fs-fsa/src/__tests__/CoreFileSystemHandle.test.ts index daff5dab6..43241e2a2 100644 --- a/packages/fs-fsa/src/__tests__/CoreFileSystemHandle.test.ts +++ b/packages/fs-fsa/src/__tests__/CoreFileSystemHandle.test.ts @@ -3,7 +3,7 @@ import { CoreFileSystemDirectoryHandle } from '../CoreFileSystemDirectoryHandle' import { onlyOnNode20 } from './util'; const setup = (json: DirectoryJSON = {}) => { - const core = Superblock.fromJSON(json, { cwd: '/' }); + const core = Superblock.fromJSON(json, '/'); const dir = new CoreFileSystemDirectoryHandle(core, '/', { mode: 'readwrite' }); return { dir, core }; }; @@ -66,7 +66,7 @@ onlyOnNode20('CoreFileSystemHandle', () => { }); test('queryPermission denies readwrite when context only allows read', async () => { - const core = Superblock.fromJSON({ 'test.txt': 'content' }, { cwd: '/' }); + const core = Superblock.fromJSON({ 'test.txt': 'content' }, '/'); const dir = new CoreFileSystemDirectoryHandle(core, '/', { mode: 'read' }); const file = await dir.getFileHandle('test.txt'); diff --git a/packages/fs-node-to-fsa/src/__tests__/NodeFileSystemDirectoryHandle.test.ts b/packages/fs-node-to-fsa/src/__tests__/NodeFileSystemDirectoryHandle.test.ts index fbf61e05e..854c3040f 100644 --- a/packages/fs-node-to-fsa/src/__tests__/NodeFileSystemDirectoryHandle.test.ts +++ b/packages/fs-node-to-fsa/src/__tests__/NodeFileSystemDirectoryHandle.test.ts @@ -5,7 +5,7 @@ import { NodeFileSystemHandle } from '../NodeFileSystemHandle'; import { onlyOnNode20 } from './util'; const setup = (json: DirectoryJSON = {}) => { - const { fs } = memfs(json, { cwd: '/' }); + const { fs } = memfs(json, '/'); const dir = new NodeFileSystemDirectoryHandle(fs as any, '/', { mode: 'readwrite' }); return { dir, fs }; }; @@ -162,7 +162,7 @@ onlyOnNode20('NodeFileSystemDirectoryHandle', () => { }); test('throws if not in "readwrite" mode and attempting to create a directory', async () => { - const { fs } = memfs({}, { cwd: '/' }); + const { fs } = memfs({}, '/'); const dir = new NodeFileSystemDirectoryHandle(fs as any, '/', { mode: 'read' }); try { await dir.getDirectoryHandle('test', { create: true }); @@ -274,7 +274,7 @@ onlyOnNode20('NodeFileSystemDirectoryHandle', () => { }); test('throws if not in "readwrite" mode and attempting to create a file', async () => { - const { fs } = memfs({}, { cwd: '/' }); + const { fs } = memfs({}, '/'); const dir = new NodeFileSystemDirectoryHandle(fs as any, '/', { mode: 'read' }); try { await dir.getFileHandle('test', { create: true }); @@ -364,7 +364,7 @@ onlyOnNode20('NodeFileSystemDirectoryHandle', () => { }); test('throws if not in "readwrite" mode and attempting to remove a file', async () => { - const { fs } = memfs({ a: 'b' }, { cwd: '/' }); + const { fs } = memfs({ a: 'b' }, '/'); const dir = new NodeFileSystemDirectoryHandle(fs as any, '/', { mode: 'read' }); try { await dir.removeEntry('a'); @@ -379,7 +379,7 @@ onlyOnNode20('NodeFileSystemDirectoryHandle', () => { }); test('throws if not in "readwrite" mode and attempting to remove a folder', async () => { - const { fs } = memfs({ a: null }, { cwd: '/' }); + const { fs } = memfs({ a: null }, '/'); const dir = new NodeFileSystemDirectoryHandle(fs as any, '/', { mode: 'read' }); try { await dir.removeEntry('a'); diff --git a/packages/fs-node-to-fsa/src/__tests__/NodeFileSystemFileHandle.test.ts b/packages/fs-node-to-fsa/src/__tests__/NodeFileSystemFileHandle.test.ts index 1c7a1f117..4d1d6645d 100644 --- a/packages/fs-node-to-fsa/src/__tests__/NodeFileSystemFileHandle.test.ts +++ b/packages/fs-node-to-fsa/src/__tests__/NodeFileSystemFileHandle.test.ts @@ -3,7 +3,7 @@ import { NodeFileSystemDirectoryHandle } from '../NodeFileSystemDirectoryHandle' import { onlyOnNode20 } from './util'; const setup = (json: DirectoryJSON = {}) => { - const { fs, vol } = memfs(json, { cwd: '/' }); + const { fs, vol } = memfs(json, '/'); const dir = new NodeFileSystemDirectoryHandle(fs as any, '/', { mode: 'readwrite' }); return { dir, fs, vol }; }; @@ -27,7 +27,7 @@ onlyOnNode20('NodeFileSystemFileHandle', () => { describe('.createWritable()', () => { test('throws if not in "readwrite" mode', async () => { - const { fs } = memfs({ 'file.txt': 'abc' }, { cwd: '/' }); + const { fs } = memfs({ 'file.txt': 'abc' }, '/'); const dir = new NodeFileSystemDirectoryHandle(fs as any, '/', { mode: 'read' }); const entry = await dir.getFileHandle('file.txt'); try { @@ -197,7 +197,7 @@ onlyOnNode20('NodeFileSystemFileHandle', () => { describe('file locking', () => { describe('sync access handle locking', () => { test('creates sync access handle successfully', async () => { - const { fs } = memfs({ 'test.txt': 'content' }, { cwd: '/' }); + const { fs } = memfs({ 'test.txt': 'content' }, '/'); const dir = new NodeFileSystemDirectoryHandle(fs as any, '/', { syncHandleAllowed: true, mode: 'readwrite', @@ -209,7 +209,7 @@ onlyOnNode20('NodeFileSystemFileHandle', () => { }); test('throws NoModificationAllowedError when creating sync handle while file is locked', async () => { - const { fs } = memfs({ 'file.txt': 'content' }, { cwd: '/' }); + const { fs } = memfs({ 'file.txt': 'content' }, '/'); const dir = new NodeFileSystemDirectoryHandle(fs as any, '/', { syncHandleAllowed: true, mode: 'readwrite', @@ -230,7 +230,7 @@ onlyOnNode20('NodeFileSystemFileHandle', () => { }); test('allows creating sync handle after previous one is closed', async () => { - const { fs } = memfs({ 'file.txt': 'content' }, { cwd: '/' }); + const { fs } = memfs({ 'file.txt': 'content' }, '/'); const dir = new NodeFileSystemDirectoryHandle(fs as any, '/', { syncHandleAllowed: true, mode: 'readwrite', @@ -257,7 +257,7 @@ onlyOnNode20('NodeFileSystemFileHandle', () => { }); test('throws NoModificationAllowedError when creating writable stream while sync handle is open', async () => { - const { fs } = memfs({ 'file.txt': 'content' }, { cwd: '/' }); + const { fs } = memfs({ 'file.txt': 'content' }, '/'); const dir = new NodeFileSystemDirectoryHandle(fs as any, '/', { syncHandleAllowed: true, mode: 'readwrite', @@ -278,7 +278,7 @@ onlyOnNode20('NodeFileSystemFileHandle', () => { }); test('allows creating writable stream after sync handle is closed', async () => { - const { fs } = memfs({ 'file.txt': 'content' }, { cwd: '/' }); + const { fs } = memfs({ 'file.txt': 'content' }, '/'); const dir = new NodeFileSystemDirectoryHandle(fs as any, '/', { syncHandleAllowed: true, mode: 'readwrite', @@ -295,7 +295,7 @@ onlyOnNode20('NodeFileSystemFileHandle', () => { }); test('throws NoModificationAllowedError when creating sync handle while writable stream is open', async () => { - const { fs } = memfs({ 'file.txt': 'content' }, { cwd: '/' }); + const { fs } = memfs({ 'file.txt': 'content' }, '/'); const dir = new NodeFileSystemDirectoryHandle(fs as any, '/', { syncHandleAllowed: true, mode: 'readwrite', @@ -316,7 +316,7 @@ onlyOnNode20('NodeFileSystemFileHandle', () => { }); test('allows creating sync handle after writable stream is closed', async () => { - const { fs } = memfs({ 'file.txt': 'content' }, { cwd: '/' }); + const { fs } = memfs({ 'file.txt': 'content' }, '/'); const dir = new NodeFileSystemDirectoryHandle(fs as any, '/', { syncHandleAllowed: true, mode: 'readwrite', diff --git a/packages/fs-node-to-fsa/src/__tests__/NodeFileSystemHandle.test.ts b/packages/fs-node-to-fsa/src/__tests__/NodeFileSystemHandle.test.ts index bea2a5822..a516044d8 100644 --- a/packages/fs-node-to-fsa/src/__tests__/NodeFileSystemHandle.test.ts +++ b/packages/fs-node-to-fsa/src/__tests__/NodeFileSystemHandle.test.ts @@ -3,7 +3,7 @@ import { NodeFileSystemDirectoryHandle } from '../NodeFileSystemDirectoryHandle' import { onlyOnNode20 } from './util'; const setup = (json: DirectoryJSON = {}) => { - const { fs } = memfs(json, { cwd: '/' }); + const { fs } = memfs(json, '/'); const dir = new NodeFileSystemDirectoryHandle(fs as any, '/', { mode: 'readwrite' }); return { dir, fs }; }; diff --git a/packages/fs-node-to-fsa/src/__tests__/NodeFileSystemSyncAccessHandle.test.ts b/packages/fs-node-to-fsa/src/__tests__/NodeFileSystemSyncAccessHandle.test.ts index 4bd36d157..6a6bd006a 100644 --- a/packages/fs-node-to-fsa/src/__tests__/NodeFileSystemSyncAccessHandle.test.ts +++ b/packages/fs-node-to-fsa/src/__tests__/NodeFileSystemSyncAccessHandle.test.ts @@ -4,7 +4,7 @@ import { NodeFileSystemSyncAccessHandle } from '../NodeFileSystemSyncAccessHandl import { onlyOnNode20 } from './util'; const setup = (json: DirectoryJSON = {}) => { - const { fs } = memfs(json, { cwd: '/' }); + const { fs } = memfs(json, '/'); const dir = new NodeFileSystemDirectoryHandle(fs as any, '/', { syncHandleAllowed: true, mode: 'readwrite' }); return { dir, fs }; }; diff --git a/packages/fs-node/src/__tests__/util.ts b/packages/fs-node/src/__tests__/util.ts index 98c4230c0..f91761499 100644 --- a/packages/fs-node/src/__tests__/util.ts +++ b/packages/fs-node/src/__tests__/util.ts @@ -1,6 +1,5 @@ import { Volume } from '..'; import { Link, Node, NestedDirectoryJSON } from '@jsonjoy.com/fs-core'; -import type { SuperblockFromJsonOptions } from '@jsonjoy.com/fs-core/lib/Superblock'; export const multitest = (_done: (err?: Error) => void, times: number) => { let err; @@ -10,20 +9,18 @@ export const multitest = (_done: (err?: Error) => void, times: number) => { }; }; -export const create = (json: { [s: string]: string } = { '/foo': 'bar' }, options?: SuperblockFromJsonOptions) => { - options ??= {}; - options.cwd ??= '/'; - const vol = Volume.fromJSON(json, options); +export const create = (json: { [s: string]: string } = { '/foo': 'bar' }, cwd?: string) => { + const vol = Volume.fromJSON(json, cwd ?? '/'); return vol; }; -export const createFs = (json?, options?: SuperblockFromJsonOptions) => { - const vol = create(json, options); +export const createFs = (json?, cwd?: string) => { + const vol = create(json, cwd); return vol; }; -export const memfs = (json: NestedDirectoryJSON = {}, options?: SuperblockFromJsonOptions) => { - const vol = Volume.fromNestedJSON(json, options); +export const memfs = (json: NestedDirectoryJSON = {}, cwd?: string) => { + const vol = Volume.fromNestedJSON(json, cwd); return { fs: vol, vol }; }; diff --git a/packages/fs-node/src/__tests__/volume.test.ts b/packages/fs-node/src/__tests__/volume.test.ts index 062eeff19..840b1608b 100644 --- a/packages/fs-node/src/__tests__/volume.test.ts +++ b/packages/fs-node/src/__tests__/volume.test.ts @@ -225,7 +225,7 @@ describe('volume', () => { 'app.js': 'console.log(123)', dir: null, }; - vol.fromJSON(json, { cwd: '/' }); + vol.fromJSON(json, '/'); expect(vol.toJSON()).toEqual({ '/hello': 'world', '/app.js': 'console.log(123)', diff --git a/packages/fs-node/src/__tests__/volume/cwd.test.ts b/packages/fs-node/src/__tests__/volume/cwd.test.ts index d6b8f40b5..76a63d0b2 100644 --- a/packages/fs-node/src/__tests__/volume/cwd.test.ts +++ b/packages/fs-node/src/__tests__/volume/cwd.test.ts @@ -8,9 +8,7 @@ describe('Volume.cwd', () => { '/a/file': 'a', '/a/b/file': 'b', }, - { - cwd: '/', - }, + '/', ); expect(vol.readFileSync('./file', 'utf8')).toEqual('root'); vol.cwd = '/a'; @@ -25,9 +23,7 @@ describe('Volume.cwd', () => { 'a/file': 'a', 'a/b/file': 'b', }, - { - cwd: process.cwd(), - }, + process.cwd(), ); expect(vol.readFileSync('./file', 'utf8')).toEqual('root'); vol.cwd = 'a'; diff --git a/packages/fs-node/src/__tests__/volume/opendir.test.ts b/packages/fs-node/src/__tests__/volume/opendir.test.ts index 33b9ebf50..a32924ff6 100644 --- a/packages/fs-node/src/__tests__/volume/opendir.test.ts +++ b/packages/fs-node/src/__tests__/volume/opendir.test.ts @@ -29,7 +29,7 @@ describe('opendir', () => { '/test/file1.txt': 'content1', '/test/file2.txt': 'content2', }, - { cwd: '/test' }, + '/test', ); const dir = vol.opendirSync('.'); diff --git a/packages/fs-node/src/__tests__/volume/readFile.test.ts b/packages/fs-node/src/__tests__/volume/readFile.test.ts index 2738c6b98..ffebdc854 100644 --- a/packages/fs-node/src/__tests__/volume/readFile.test.ts +++ b/packages/fs-node/src/__tests__/volume/readFile.test.ts @@ -3,13 +3,13 @@ import { memfs } from '../util'; describe('.readFile()', () => { it('can read a file in cwd', async () => { - const { fs } = memfs({ 'test.txt': '01234567' }, { cwd: '/dir' }); + const { fs } = memfs({ 'test.txt': '01234567' }, '/dir'); await expect(fs.promises.readFile('/dir/test.txt', { encoding: 'utf8' })).resolves.toBe('01234567'); await expect(fs.readFileSync('/dir/test.txt', { encoding: 'utf8' })).toBe('01234567'); }); it('can read a relative file in cwd', async () => { - const { fs } = memfs({ 'test.txt': '01234567' }, { cwd: '/dir' }); + const { fs } = memfs({ 'test.txt': '01234567' }, '/dir'); await expect(fs.promises.readFile('test.txt', { encoding: 'utf8' })).resolves.toBe('01234567'); await expect(fs.promises.readFile('./test.txt', { encoding: 'utf8' })).resolves.toBe('01234567'); await expect(fs.readFileSync('test.txt', { encoding: 'utf8' })).toBe('01234567'); diff --git a/packages/fs-node/src/__tests__/volume/readdirSync.test.ts b/packages/fs-node/src/__tests__/volume/readdirSync.test.ts index b5ea47bdd..1c6f07e37 100644 --- a/packages/fs-node/src/__tests__/volume/readdirSync.test.ts +++ b/packages/fs-node/src/__tests__/volume/readdirSync.test.ts @@ -36,7 +36,7 @@ describe('readdirSync()', () => { '/foo/bar/file': 'content', '/foo/bar/file2': 'content2', }, - { cwd: '/foo' }, + '/foo', ); const files = vol.readdirSync('bar'); diff --git a/packages/fs-node/src/volume.ts b/packages/fs-node/src/volume.ts index 80c3fd289..fddc500f7 100644 --- a/packages/fs-node/src/volume.ts +++ b/packages/fs-node/src/volume.ts @@ -12,7 +12,6 @@ import { FanOutUnsubscribe } from 'thingies/lib/fanout'; import { Link, Superblock, - SuperblockFromJsonOptions, DirectoryJSON, NestedDirectoryJSON, ERROR_CODE, @@ -177,11 +176,11 @@ function validateGid(gid: number) { * `Volume` represents a file system. */ export class Volume implements FsCallbackApi, FsSynchronousApi { - public static readonly fromJSON = (json: DirectoryJSON, options?: SuperblockFromJsonOptions): Volume => - new Volume(Superblock.fromJSON(json, options)); + public static readonly fromJSON = (json: DirectoryJSON, cwd?: string): Volume => + new Volume(Superblock.fromJSON(json, cwd)); - public static readonly fromNestedJSON = (json: NestedDirectoryJSON, options?: SuperblockFromJsonOptions): Volume => - new Volume(Superblock.fromNestedJSON(json, options)); + public static readonly fromNestedJSON = (json: NestedDirectoryJSON, cwd?: string): Volume => + new Volume(Superblock.fromNestedJSON(json, cwd)); StatWatcher: new () => StatWatcher; ReadStream: new (...args) => misc.IReadStream; @@ -282,17 +281,17 @@ export class Volume implements FsCallbackApi, FsSynchronousApi { return this._core.toJSON(paths, json, isRelative, asBuffer); } - fromJSON(json: DirectoryJSON, options?: SuperblockFromJsonOptions) { - return this._core.fromJSON(json, options); + fromJSON(json: DirectoryJSON, cwd?: string) { + return this._core.fromJSON(json, cwd); } - fromNestedJSON(json: NestedDirectoryJSON, options?: SuperblockFromJsonOptions) { - return this._core.fromNestedJSON(json, options); + fromNestedJSON(json: NestedDirectoryJSON, cwd?: string) { + return this._core.fromNestedJSON(json, cwd); } // Legacy interface mountSync(mountpoint: string, json: DirectoryJSON) { - this._core.fromJSON(json, { mountpoint: mountpoint }); + this._core.fromJSON(json, mountpoint); } public openSync = (path: PathLike, flags: TFlags, mode: TMode = MODE.DEFAULT): number => { diff --git a/packages/memfs/src/index.ts b/packages/memfs/src/index.ts index 5cdb031ce..dd56c9654 100644 --- a/packages/memfs/src/index.ts +++ b/packages/memfs/src/index.ts @@ -13,7 +13,6 @@ import { DirectoryJSON, NestedDirectoryJSON } from '@jsonjoy.com/fs-core'; import { constants } from '@jsonjoy.com/fs-node-utils'; import type { FsPromisesApi } from '@jsonjoy.com/fs-node-utils'; import type * as misc from '@jsonjoy.com/fs-node-utils/lib/types/misc'; -import type { SuperblockFromJsonOptions } from '@jsonjoy.com/fs-core/lib/Superblock'; const { F_OK, R_OK, W_OK, X_OK } = constants; @@ -79,11 +78,8 @@ export const fs: IFs = createFsFromVolume(vol); * @returns A `memfs` file system instance, which is a drop-in replacement for * the `fs` module. */ -export const memfs = ( - json: NestedDirectoryJSON = {}, - options?: SuperblockFromJsonOptions, -): { fs: IFs; vol: Volume } => { - const vol = Volume.fromNestedJSON(json, options); +export const memfs = (json: NestedDirectoryJSON = {}, cwd?: string): { fs: IFs; vol: Volume } => { + const vol = Volume.fromNestedJSON(json, cwd); const fs = createFsFromVolume(vol); return { fs, vol }; }; From 20c700b1d2502fb7bfbd6efc4ccafccfb192d7e7 Mon Sep 17 00:00:00 2001 From: Mopsgamer <79159094+Mopsgamer@users.noreply.github.com> Date: Tue, 3 Feb 2026 21:57:06 +0100 Subject: [PATCH 11/12] refactor: avoid changes --- packages/fs-node/src/__tests__/util.ts | 2 +- packages/fs-node/src/volume.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/fs-node/src/__tests__/util.ts b/packages/fs-node/src/__tests__/util.ts index f91761499..3c70724d0 100644 --- a/packages/fs-node/src/__tests__/util.ts +++ b/packages/fs-node/src/__tests__/util.ts @@ -10,7 +10,7 @@ export const multitest = (_done: (err?: Error) => void, times: number) => { }; export const create = (json: { [s: string]: string } = { '/foo': 'bar' }, cwd?: string) => { - const vol = Volume.fromJSON(json, cwd ?? '/'); + const vol = Volume.fromJSON(json, cwd); return vol; }; diff --git a/packages/fs-node/src/volume.ts b/packages/fs-node/src/volume.ts index fddc500f7..4e53673cb 100644 --- a/packages/fs-node/src/volume.ts +++ b/packages/fs-node/src/volume.ts @@ -23,7 +23,7 @@ import { validateFd, Ok, Result, -} from '@jsonjoy.com/fs-core/lib/index'; +} from '@jsonjoy.com/fs-core'; import { isWin } from '@jsonjoy.com/fs-core/lib/util'; import Stats from './Stats'; import Dirent from './Dirent'; From 1c7fb79b341b93648933b79dd2016a488f023ee3 Mon Sep 17 00:00:00 2001 From: Mopsgamer <79159094+Mopsgamer@users.noreply.github.com> Date: Fri, 27 Mar 2026 19:45:06 +0100 Subject: [PATCH 12/12] sync --- package.json | 2 +- packages/fs-core/package.json | 2 +- packages/fs-core/src/Node.ts | 4 +- packages/fs-core/src/Superblock.ts | 30 ++++--- .../src/__tests__/Superblock.process.test.ts | 86 +++++++++++++++++++ packages/fs-core/src/index.ts | 1 + packages/fs-core/src/process.ts | 2 +- packages/fs-core/src/util.ts | 1 + packages/fs-fsa-to-node/package.json | 2 +- packages/fs-fsa/package.json | 2 +- packages/fs-node-builtins/package.json | 2 +- packages/fs-node-to-fsa/package.json | 2 +- packages/fs-node-utils/package.json | 2 +- packages/fs-node-utils/src/types/misc.ts | 4 +- packages/fs-node/package.json | 2 +- packages/fs-node/src/FileHandle.ts | 4 +- packages/fs-node/src/__tests__/glob.test.ts | 18 ++++ packages/fs-node/src/__tests__/util.ts | 2 +- .../src/__tests__/volume.process.test.ts | 44 ++++++++++ .../src/__tests__/volume/FileHandle.test.ts | 34 ++++++++ packages/fs-node/src/glob.ts | 5 +- packages/fs-node/src/volume.ts | 45 +++++++--- packages/fs-print/package.json | 2 +- packages/fs-snapshot/package.json | 2 +- packages/memfs/package.json | 2 +- .../memfs/src/__tests__/memfs.process.test.ts | 53 ++++++++++++ packages/memfs/src/index.ts | 27 ++++-- 27 files changed, 334 insertions(+), 48 deletions(-) create mode 100644 packages/fs-core/src/__tests__/Superblock.process.test.ts create mode 100644 packages/fs-node/src/__tests__/volume.process.test.ts create mode 100644 packages/memfs/src/__tests__/memfs.process.test.ts diff --git a/package.json b/package.json index 1928c5519..519031bf5 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "memfs-monorepo", - "version": "4.56.10", + "version": "4.57.1", "private": true, "description": "In-memory file-system with Node's fs API.", "keywords": [ diff --git a/packages/fs-core/package.json b/packages/fs-core/package.json index 073c06e23..505342ee5 100644 --- a/packages/fs-core/package.json +++ b/packages/fs-core/package.json @@ -3,7 +3,7 @@ "publishConfig": { "access": "public" }, - "version": "4.56.10", + "version": "4.57.1", "description": "Core filesystem primitives: Node, Link, File, Superblock", "author": { "name": "streamich", diff --git a/packages/fs-core/src/Node.ts b/packages/fs-core/src/Node.ts index d9511b065..ade1fb5ab 100644 --- a/packages/fs-core/src/Node.ts +++ b/packages/fs-core/src/Node.ts @@ -49,9 +49,11 @@ export class Node { // Path to another node, if this is a symlink. symlink: string; - constructor(ino: number, mode: number = 0o666) { + constructor(ino: number, mode: number = 0o666, uid: number = getuid(), gid: number = getgid()) { this.mode = mode; this.ino = ino; + this._uid = uid; + this._gid = gid; } public set ctime(ctime: Date) { diff --git a/packages/fs-core/src/Superblock.ts b/packages/fs-core/src/Superblock.ts index 71cb87dab..b5d4e4806 100644 --- a/packages/fs-core/src/Superblock.ts +++ b/packages/fs-core/src/Superblock.ts @@ -3,7 +3,7 @@ import { Node } from './Node'; import { Link } from './Link'; import { File } from './File'; import { Buffer } from '@jsonjoy.com/fs-node-builtins/lib/internal/buffer'; -import process from './process'; +import defaultProcess, { type IProcess } from './process'; import { constants } from '@jsonjoy.com/fs-node-utils'; import { ERRSTR, FLAGS, MODE } from '@jsonjoy.com/fs-node-utils'; import { @@ -34,14 +34,14 @@ const { O_RDONLY, O_WRONLY, O_RDWR, O_CREAT, O_EXCL, O_TRUNC, O_APPEND, O_DIRECT * @see https://lxr.linux.no/linux+v3.11.2/include/linux/fs.h#L1242 */ export class Superblock { - static fromJSON(json: DirectoryJSON, cwd?: string): Superblock { - const vol = new Superblock(); + static fromJSON(json: DirectoryJSON, cwd?: string, opts?: { process?: IProcess }): Superblock { + const vol = new Superblock(opts); vol.fromJSON(json, cwd); return vol; } - static fromNestedJSON(json: NestedDirectoryJSON, cwd?: string): Superblock { - const vol = new Superblock(); + static fromNestedJSON(json: NestedDirectoryJSON, cwd?: string, opts?: { process?: IProcess }): Superblock { + const vol = new Superblock(opts); vol.fromNestedJSON(json, cwd); return vol; } @@ -84,7 +84,11 @@ export class Superblock { // Current number of open files. openFiles = 0; - constructor(props = {}) { + /** The `process`-like object used by this filesystem instance. */ + readonly process: IProcess; + + constructor(opts: { process?: IProcess } = {}) { + this.process = opts.process ?? defaultProcess; const root = this.createLink(); root.setNode(this.createNode(constants.S_IFDIR | 0o777)); @@ -152,7 +156,9 @@ export class Superblock { } createNode(mode: number): Node { - const node = new Node(this.newInoNumber(), mode); + const uid = this.process.getuid?.() ?? 0; + const gid = this.process.getgid?.() ?? 0; + const node = new Node(this.newInoNumber(), mode, uid, gid); this.inodes[node.ino] = node; return node; } @@ -213,11 +219,13 @@ export class Superblock { let curr: Link | null = this.root; let i = 0; + const uid = this.process.getuid?.() ?? 0; + const gid = this.process.getgid?.() ?? 0; while (i < steps.length) { let node: Node = curr.getNode(); // Check access permissions if current link is a directory if (node.isDirectory()) { - if (checkAccess && !node.canExecute()) { + if (checkAccess && !node.canExecute(uid, gid)) { return Err(createStatError(ERROR_CODE.EACCES, funcName, filename)); } } else { @@ -255,7 +263,7 @@ export class Superblock { if (checkExistence && !node.isDirectory() && i < steps.length - 1) { // On Windows, use ENOENT for consistency with Node.js behavior // On other platforms, use ENOTDIR which is more semantically correct - const errorCode = process.platform === 'win32' ? ERROR_CODE.ENOENT : ERROR_CODE.ENOTDIR; + const errorCode = this.process.platform === 'win32' ? ERROR_CODE.ENOENT : ERROR_CODE.ENOTDIR; return Err(createStatError(errorCode, funcName, filename)); } @@ -412,8 +420,8 @@ export class Superblock { return json; } - fromJSON(json: DirectoryJSON, cwd?: string) { - this.cwd = cwd ?? '/'; + fromJSON(json: DirectoryJSON, cwd: string = "/") { + this.cwd = cwd; for (let filename in json) { const data = json[filename]; filename = resolve(filename, this.cwd); diff --git a/packages/fs-core/src/__tests__/Superblock.process.test.ts b/packages/fs-core/src/__tests__/Superblock.process.test.ts new file mode 100644 index 000000000..6ccf945dd --- /dev/null +++ b/packages/fs-core/src/__tests__/Superblock.process.test.ts @@ -0,0 +1,86 @@ +import { Superblock } from '../Superblock'; +import type { IProcess } from '../process'; + +const makeProcess = (overrides: Partial = {}): IProcess => ({ + cwd: () => '/', + platform: 'linux', + emitWarning: () => {}, + env: {}, + ...overrides, +}); + +describe('Superblock with custom process', () => { + describe('fromJSON / fromNestedJSON', () => { + it('uses custom cwd() when no cwd argument is given', () => { + const customProcess = makeProcess({ cwd: () => '/custom' }); + const sb = Superblock.fromJSON({ 'file.txt': 'hello' }, undefined, { process: customProcess }); + const link = sb.getResolvedLink('/custom/file.txt'); + expect(link).not.toBeNull(); + expect(link!.getNode().getString()).toBe('hello'); + }); + + it('uses provided cwd argument instead of process.cwd()', () => { + const customProcess = makeProcess({ cwd: () => '/ignored' }); + const sb = Superblock.fromJSON({ 'file.txt': 'hi' }, '/explicit', { process: customProcess }); + const link = sb.getResolvedLink('/explicit/file.txt'); + expect(link).not.toBeNull(); + }); + + it('fromNestedJSON uses custom cwd()', () => { + const customProcess = makeProcess({ cwd: () => '/nested' }); + const sb = Superblock.fromNestedJSON({ 'a/b.txt': 'data' }, undefined, { process: customProcess }); + const link = sb.getResolvedLink('/nested/a/b.txt'); + expect(link).not.toBeNull(); + }); + }); + + describe('createNode', () => { + it('uses custom getuid() and getgid() for new nodes', () => { + const customProcess = makeProcess({ getuid: () => 1234, getgid: () => 5678 }); + const sb = new Superblock({ process: customProcess }); + const node = sb.createNode(0o644); + expect(node.uid).toBe(1234); + expect(node.gid).toBe(5678); + }); + + it('defaults uid/gid to 0 when getuid/getgid are not defined', () => { + const customProcess = makeProcess(); + const sb = new Superblock({ process: customProcess }); + const node = sb.createNode(0o644); + expect(node.uid).toBe(0); + expect(node.gid).toBe(0); + }); + }); + + describe('walk (platform-dependent error codes)', () => { + it('returns ENOTDIR on non-win32 platform when traversing through a file', () => { + const customProcess = makeProcess({ platform: 'linux' }); + const sb = Superblock.fromJSON({ '/dir/file.txt': 'content' }, '/', { process: customProcess }); + const result = sb.walk('/dir/file.txt/child', false, true, false); + expect(result.ok).toBe(false); + if (!result.ok) expect(result.err.code).toBe('ENOTDIR'); + }); + + it('returns ENOENT on win32 platform when traversing through a file', () => { + const customProcess = makeProcess({ platform: 'win32' }); + const sb = Superblock.fromJSON({ '/dir/file.txt': 'content' }, '/', { process: customProcess }); + const result = sb.walk('/dir/file.txt/child', false, true, false); + expect(result.ok).toBe(false); + if (!result.ok) expect(result.err.code).toBe('ENOENT'); + }); + }); + + describe('process property', () => { + it('exposes the stored process object', () => { + const customProcess = makeProcess({ platform: 'win32' }); + const sb = new Superblock({ process: customProcess }); + expect(sb.process).toBe(customProcess); + }); + + it('defaults to the global process when no process option is given', () => { + const sb = new Superblock(); + expect(typeof sb.process.cwd).toBe('function'); + expect(typeof sb.process.platform).toBe('string'); + }); + }); +}); diff --git a/packages/fs-core/src/index.ts b/packages/fs-core/src/index.ts index c31e5b8c5..3ea8737dc 100644 --- a/packages/fs-core/src/index.ts +++ b/packages/fs-core/src/index.ts @@ -6,6 +6,7 @@ export { Node, type NodeEvent } from './Node'; export { Link, type LinkEvent } from './Link'; export { File } from './File'; export { Superblock } from './Superblock'; +export type { IProcess } from './process'; export { dataToBuffer, filenameToSteps, diff --git a/packages/fs-core/src/process.ts b/packages/fs-core/src/process.ts index 8c70721d8..40062a7d3 100644 --- a/packages/fs-core/src/process.ts +++ b/packages/fs-core/src/process.ts @@ -6,7 +6,7 @@ export interface IProcess { cwd(): string; platform: string; emitWarning: (message: string, type: string) => void; - env: {}; + env: Record; } /** diff --git a/packages/fs-core/src/util.ts b/packages/fs-core/src/util.ts index 265d5714c..75e373acf 100644 --- a/packages/fs-core/src/util.ts +++ b/packages/fs-core/src/util.ts @@ -113,6 +113,7 @@ export function pathToFilename(path: misc.PathLike): string { path = getPathFromURLPosix(path); } const pathString = String(path); + if (pathString === '.') return './'; nullCheck(pathString); return pathString; } diff --git a/packages/fs-fsa-to-node/package.json b/packages/fs-fsa-to-node/package.json index 3c691f4d3..a0b3d404f 100644 --- a/packages/fs-fsa-to-node/package.json +++ b/packages/fs-fsa-to-node/package.json @@ -3,7 +3,7 @@ "publishConfig": { "access": "public" }, - "version": "4.56.10", + "version": "4.57.1", "description": "Adapter to convert File System Access API to Node.js fs API", "author": { "name": "streamich", diff --git a/packages/fs-fsa/package.json b/packages/fs-fsa/package.json index 241030893..874ebeeb4 100644 --- a/packages/fs-fsa/package.json +++ b/packages/fs-fsa/package.json @@ -3,7 +3,7 @@ "publishConfig": { "access": "public" }, - "version": "4.56.10", + "version": "4.57.1", "description": "File System Access API implementation backed by core filesystem primitives", "author": { "name": "streamich", diff --git a/packages/fs-node-builtins/package.json b/packages/fs-node-builtins/package.json index 3cd932ef3..4d6ad9fe8 100644 --- a/packages/fs-node-builtins/package.json +++ b/packages/fs-node-builtins/package.json @@ -3,7 +3,7 @@ "publishConfig": { "access": "public" }, - "version": "4.56.10", + "version": "4.57.1", "description": "Node.js standard library dependencies for fs-related packages", "author": { "name": "streamich", diff --git a/packages/fs-node-to-fsa/package.json b/packages/fs-node-to-fsa/package.json index 8ec145cc7..1aa525359 100644 --- a/packages/fs-node-to-fsa/package.json +++ b/packages/fs-node-to-fsa/package.json @@ -3,7 +3,7 @@ "publishConfig": { "access": "public" }, - "version": "4.56.10", + "version": "4.57.1", "description": "Adapter to convert Node.js fs API to File System Access API", "author": { "name": "streamich", diff --git a/packages/fs-node-utils/package.json b/packages/fs-node-utils/package.json index 3916dfa62..91c4241c4 100644 --- a/packages/fs-node-utils/package.json +++ b/packages/fs-node-utils/package.json @@ -3,7 +3,7 @@ "publishConfig": { "access": "public" }, - "version": "4.56.10", + "version": "4.57.1", "description": "Utility functions for Node.js fs module", "author": { "name": "streamich", diff --git a/packages/fs-node-utils/src/types/misc.ts b/packages/fs-node-utils/src/types/misc.ts index 36a9ad407..20e2f253a 100644 --- a/packages/fs-node-utils/src/types/misc.ts +++ b/packages/fs-node-utils/src/types/misc.ts @@ -148,8 +148,8 @@ export interface IFileHandle extends EventEmitter { chmod(mode: TMode): Promise; chown(uid: number, gid: number): Promise; close(): Promise; - createReadStream(options: IFileHandleReadStreamOptions): IReadStream; - createWriteStream(options: IFileHandleWriteStreamOptions): IWriteStream; + createReadStream(options?: IFileHandleReadStreamOptions): IReadStream; + createWriteStream(options?: IFileHandleWriteStreamOptions): IWriteStream; datasync(): Promise; readableWebStream(options?: IReadableWebStreamOptions): ReadableStream; read( diff --git a/packages/fs-node/package.json b/packages/fs-node/package.json index 5efba3c83..249855594 100644 --- a/packages/fs-node/package.json +++ b/packages/fs-node/package.json @@ -3,7 +3,7 @@ "publishConfig": { "access": "public" }, - "version": "4.56.10", + "version": "4.57.1", "description": "In-memory filesystem with Node.js fs-compatible API", "author": { "name": "streamich", diff --git a/packages/fs-node/src/FileHandle.ts b/packages/fs-node/src/FileHandle.ts index e20b38cfd..4a8ee36c5 100644 --- a/packages/fs-node/src/FileHandle.ts +++ b/packages/fs-node/src/FileHandle.ts @@ -86,11 +86,11 @@ export class FileHandle extends EventEmitter implements IFileHandle { return promisify(this.fs, 'fdatasync')(this.fd); } - createReadStream(options: opts.IFileHandleReadStreamOptions): IReadStream { + createReadStream(options?: opts.IFileHandleReadStreamOptions): IReadStream { return this.fs.createReadStream('', { ...options, fd: this }); } - createWriteStream(options: opts.IFileHandleWriteStreamOptions): IWriteStream { + createWriteStream(options?: opts.IFileHandleWriteStreamOptions): IWriteStream { return this.fs.createWriteStream('', { ...options, fd: this }); } diff --git a/packages/fs-node/src/__tests__/glob.test.ts b/packages/fs-node/src/__tests__/glob.test.ts index 83ae8557c..2524d9082 100644 --- a/packages/fs-node/src/__tests__/glob.test.ts +++ b/packages/fs-node/src/__tests__/glob.test.ts @@ -71,6 +71,24 @@ describe('glob APIs', () => { const results = vol.globSync('*.xyz', { cwd: '/test' }); expect(results).toEqual([]); }); + + it('should match files with leading ./ in pattern', () => { + const { vol } = setup(); + const results = vol.globSync('./file1.js', { cwd: '/test' }); + expect(results).toEqual(['file1.js']); + }); + + it('should match files with leading ./ wildcard pattern', () => { + const { vol } = setup(); + const results = vol.globSync('./*.js', { cwd: '/test' }); + expect(results).toEqual(['file1.js']); + }); + + it('should match nested files with leading ./ and ** pattern', () => { + const { vol } = setup(); + const results = vol.globSync('./**/*.js', { cwd: '/test' }); + expect(results.sort()).toEqual(['file1.js', 'subdir/nested.js']); + }); }); describe('glob (callback)', () => { diff --git a/packages/fs-node/src/__tests__/util.ts b/packages/fs-node/src/__tests__/util.ts index 3c70724d0..b1514cf3a 100644 --- a/packages/fs-node/src/__tests__/util.ts +++ b/packages/fs-node/src/__tests__/util.ts @@ -14,7 +14,7 @@ export const create = (json: { [s: string]: string } = { '/foo': 'bar' }, cwd?: return vol; }; -export const createFs = (json?, cwd?: string) => { +export const createFs = (json?: any, cwd?: string) => { const vol = create(json, cwd); return vol; }; diff --git a/packages/fs-node/src/__tests__/volume.process.test.ts b/packages/fs-node/src/__tests__/volume.process.test.ts new file mode 100644 index 000000000..fef3f5c2b --- /dev/null +++ b/packages/fs-node/src/__tests__/volume.process.test.ts @@ -0,0 +1,44 @@ +import { Volume } from '../volume'; +import type { IProcess } from '@jsonjoy.com/fs-core'; + +const makeProcess = (overrides: Partial = {}): IProcess => ({ + cwd: () => '/', + platform: 'linux', + emitWarning: () => {}, + env: {}, + ...overrides, +}); + +describe('Volume with custom process', () => { + describe('Volume.fromJSON', () => { + it('uses custom cwd() from process when no cwd is given', () => { + const customProcess = makeProcess({ cwd: () => '/app' }); + const vol = Volume.fromJSON({ 'data.txt': 'hello' }, undefined, { process: customProcess }); + expect(vol.readFileSync('/app/data.txt', 'utf8')).toBe('hello'); + }); + + it('uses explicit cwd over process.cwd()', () => { + const customProcess = makeProcess({ cwd: () => '/ignored' }); + const vol = Volume.fromJSON({ 'data.txt': 'hi' }, '/explicit', { process: customProcess }); + expect(vol.readFileSync('/explicit/data.txt', 'utf8')).toBe('hi'); + }); + }); + + describe('Volume.fromNestedJSON', () => { + it('uses custom cwd() from process when no cwd is given', () => { + const customProcess = makeProcess({ cwd: () => '/nested' }); + const vol = Volume.fromNestedJSON({ 'sub/file.txt': 'content' }, undefined, { process: customProcess }); + expect(vol.readFileSync('/nested/sub/file.txt', 'utf8')).toBe('content'); + }); + }); + + describe('custom getuid / getgid', () => { + it('stores uid and gid from custom process on created files', () => { + const customProcess = makeProcess({ getuid: () => 42, getgid: () => 99 }); + const vol = Volume.fromJSON({ '/file.txt': 'data' }, '/', { process: customProcess }); + const stat = vol.statSync('/file.txt'); + expect(stat.uid).toBe(42); + expect(stat.gid).toBe(99); + }); + }); +}); diff --git a/packages/fs-node/src/__tests__/volume/FileHandle.test.ts b/packages/fs-node/src/__tests__/volume/FileHandle.test.ts index a62a717d4..289a64cf4 100644 --- a/packages/fs-node/src/__tests__/volume/FileHandle.test.ts +++ b/packages/fs-node/src/__tests__/volume/FileHandle.test.ts @@ -404,4 +404,38 @@ describe('FileHandle', () => { await handle.close(); }); }); + + describe('createReadStream()', () => { + it('allows close after streaming via pipeline', async () => { + const { Writable, pipeline } = await import('node:stream'); + const { promisify } = await import('node:util'); + const pipelineAsync = promisify(pipeline); + + const fs = createFs(); + fs.writeFileSync('/test', 'teststring'); + const handle = await fs.promises.open('/test', 'r'); + + const s = handle.createReadStream(); + await pipelineAsync(s, new Writable({ write: (_chunk, _encoding, cb) => cb() })); + + await expect(handle.close()).resolves.toBeUndefined(); + }); + }); + + describe('createWriteStream()', () => { + it('allows close after streaming via pipeline', async () => { + const { Readable, pipeline } = await import('node:stream'); + const { promisify } = await import('node:util'); + const pipelineAsync = promisify(pipeline); + + const fs = createFs(); + fs.writeFileSync('/test', ''); + const handle = await fs.promises.open('/test', 'w'); + + const readable = Readable.from(['hello']); + await pipelineAsync(readable, handle.createWriteStream()); + + await expect(handle.close()).resolves.toBeUndefined(); + }); + }); }); diff --git a/packages/fs-node/src/glob.ts b/packages/fs-node/src/glob.ts index 9ae52e823..e475d8bec 100644 --- a/packages/fs-node/src/glob.ts +++ b/packages/fs-node/src/glob.ts @@ -96,8 +96,9 @@ export function globSync(fs: any, pattern: string, options: IGlobOptions = {}): const dirResults = walkDirectory(fs, dir, [patternBasename], { ...globOptions, cwd: dir }); results.push(...dirResults.map(r => posix.resolve(dir, r))); } else { - // Handle relative patterns - const dirResults = walkDirectory(fs, resolvedCwd, [pattern], globOptions); + // Handle relative patterns - normalize by stripping leading './' + const normalizedPattern = pattern.replace(/^\.\//, ''); + const dirResults = walkDirectory(fs, resolvedCwd, [normalizedPattern], globOptions); results.push(...dirResults); } diff --git a/packages/fs-node/src/volume.ts b/packages/fs-node/src/volume.ts index 4e53673cb..aa4617b55 100644 --- a/packages/fs-node/src/volume.ts +++ b/packages/fs-node/src/volume.ts @@ -23,6 +23,7 @@ import { validateFd, Ok, Result, + type IProcess, } from '@jsonjoy.com/fs-core'; import { isWin } from '@jsonjoy.com/fs-core/lib/util'; import Stats from './Stats'; @@ -176,11 +177,14 @@ function validateGid(gid: number) { * `Volume` represents a file system. */ export class Volume implements FsCallbackApi, FsSynchronousApi { - public static readonly fromJSON = (json: DirectoryJSON, cwd?: string): Volume => - new Volume(Superblock.fromJSON(json, cwd)); + public static readonly fromJSON = (json: DirectoryJSON, cwd?: string, opts?: { process?: IProcess }): Volume => + new Volume(Superblock.fromJSON(json, cwd, opts)); - public static readonly fromNestedJSON = (json: NestedDirectoryJSON, cwd?: string): Volume => - new Volume(Superblock.fromNestedJSON(json, cwd)); + public static readonly fromNestedJSON = ( + json: NestedDirectoryJSON, + cwd?: string, + opts?: { process?: IProcess }, + ): Volume => new Volume(Superblock.fromNestedJSON(json, cwd, opts)); StatWatcher: new () => StatWatcher; ReadStream: new (...args) => misc.IReadStream; @@ -259,6 +263,7 @@ export class Volume implements FsCallbackApi, FsSynchronousApi { set cwd(value: string) { this._core.cwd = value; } + private wrapAsync(method: (...args: Args) => void, args: Args, callback: misc.TCallback) { validateCallback(callback); Promise.resolve().then(() => { @@ -1708,6 +1713,7 @@ function FsReadStream(vol, path, options) { Readable.call(this, options); this.path = pathToFilename(path); + this._fileHandle = options.fd && typeof options.fd !== 'number' ? options.fd : null; this.fd = options.fd === undefined ? null : typeof options.fd !== 'number' ? options.fd.fd : options.fd; this.flags = options.flags === undefined ? 'r' : options.flags; this.mode = options.mode === undefined ? 0o666 : options.mode; @@ -1841,10 +1847,17 @@ FsReadStream.prototype.close = function (cb) { this.closed = true; } - this._vol.close(this.fd, er => { - if (er) this.emit('error', er); - else this.emit('close'); - }); + if (this._fileHandle) { + this._fileHandle.close().then( + () => this.emit('close'), + er => this.emit('error', er), + ); + } else { + this._vol.close(this.fd, er => { + if (er) this.emit('error', er); + else this.emit('close'); + }); + } this.fd = null; }; @@ -1877,6 +1890,7 @@ function FsWriteStream(vol, path, options) { Writable.call(this, options); this.path = pathToFilename(path); + this._fileHandle = options.fd && typeof options.fd !== 'number' ? options.fd : null; this.fd = options.fd === undefined ? null : typeof options.fd !== 'number' ? options.fd.fd : options.fd; this.flags = options.flags === undefined ? 'w' : options.flags; this.mode = options.mode === undefined ? 0o666 : options.mode; @@ -2007,10 +2021,17 @@ FsWriteStream.prototype.close = function (cb) { this.closed = true; } - this._vol.close(this.fd, er => { - if (er) this.emit('error', er); - else this.emit('close'); - }); + if (this._fileHandle) { + this._fileHandle.close().then( + () => this.emit('close'), + er => this.emit('error', er), + ); + } else { + this._vol.close(this.fd, er => { + if (er) this.emit('error', er); + else this.emit('close'); + }); + } this.fd = null; }; diff --git a/packages/fs-print/package.json b/packages/fs-print/package.json index dbf1ce5bd..4094888fd 100644 --- a/packages/fs-print/package.json +++ b/packages/fs-print/package.json @@ -3,7 +3,7 @@ "publishConfig": { "access": "public" }, - "version": "4.56.10", + "version": "4.57.1", "description": "File system tree printer - print a file system structure as a tree", "author": { "name": "streamich", diff --git a/packages/fs-snapshot/package.json b/packages/fs-snapshot/package.json index 8f93e6c02..e089813b2 100644 --- a/packages/fs-snapshot/package.json +++ b/packages/fs-snapshot/package.json @@ -3,7 +3,7 @@ "publishConfig": { "access": "public" }, - "version": "4.56.10", + "version": "4.57.1", "description": "File system snapshot - serialize and deserialize file system trees to binary or JSON", "author": { "name": "streamich", diff --git a/packages/memfs/package.json b/packages/memfs/package.json index 142b7c891..bd3034030 100644 --- a/packages/memfs/package.json +++ b/packages/memfs/package.json @@ -1,6 +1,6 @@ { "name": "memfs", - "version": "4.56.10", + "version": "4.57.1", "publishConfig": { "access": "public" }, diff --git a/packages/memfs/src/__tests__/memfs.process.test.ts b/packages/memfs/src/__tests__/memfs.process.test.ts new file mode 100644 index 000000000..0dacab297 --- /dev/null +++ b/packages/memfs/src/__tests__/memfs.process.test.ts @@ -0,0 +1,53 @@ +import { memfs } from '../index'; +import type { IProcess } from '../index'; + +const makeProcess = (overrides: Partial = {}): IProcess => ({ + cwd: () => '/', + platform: 'linux', + emitWarning: () => {}, + env: {}, + ...overrides, +}); + +describe('memfs() with custom process', () => { + it('accepts a string as second argument (backward compat)', () => { + const { fs } = memfs({ 'file.txt': 'hello' }, '/app'); + expect(fs.readFileSync('/app/file.txt', 'utf8')).toBe('hello'); + }); + + it('accepts an object with cwd as second argument', () => { + const { fs } = memfs({ 'file.txt': 'hello' }, { cwd: '/app' }); + expect(fs.readFileSync('/app/file.txt', 'utf8')).toBe('hello'); + }); + + it('uses process.cwd() from options when no cwd is specified', () => { + const customProcess = makeProcess({ cwd: () => '/from-process' }); + const { fs } = memfs({ 'file.txt': 'hi' }, { process: customProcess }); + expect(fs.readFileSync('/from-process/file.txt', 'utf8')).toBe('hi'); + }); + + it('uses cwd from options, ignoring process.cwd()', () => { + const customProcess = makeProcess({ cwd: () => '/ignored' }); + const { fs } = memfs({ 'file.txt': 'hi' }, { cwd: '/explicit', process: customProcess }); + expect(fs.readFileSync('/explicit/file.txt', 'utf8')).toBe('hi'); + }); + + it('uses custom getuid and getgid from process', () => { + const customProcess = makeProcess({ getuid: () => 777, getgid: () => 888 }); + const { fs } = memfs({ '/file.txt': 'data' }, { process: customProcess }); + const stat = fs.statSync('/file.txt'); + expect(stat.uid).toBe(777); + expect(stat.gid).toBe(888); + }); + + it('defaults to / cwd when no options are given', () => { + const { fs } = memfs({ '/file.txt': 'data' }); + expect(fs.readFileSync('/file.txt', 'utf8')).toBe('data'); + }); + + it('exports IProcess type', () => { + // This is a type-level test - just verifying the export compiles + const p: IProcess = makeProcess(); + expect(typeof p.cwd).toBe('function'); + }); +}); diff --git a/packages/memfs/src/index.ts b/packages/memfs/src/index.ts index dd56c9654..f7e2234f4 100644 --- a/packages/memfs/src/index.ts +++ b/packages/memfs/src/index.ts @@ -9,7 +9,7 @@ import { fsCallbackApiList, } from '@jsonjoy.com/fs-node'; import type { IWriteStream } from '@jsonjoy.com/fs-node'; -import { DirectoryJSON, NestedDirectoryJSON } from '@jsonjoy.com/fs-core'; +import { DirectoryJSON, NestedDirectoryJSON, type IProcess } from '@jsonjoy.com/fs-core'; import { constants } from '@jsonjoy.com/fs-node-utils'; import type { FsPromisesApi } from '@jsonjoy.com/fs-node-utils'; import type * as misc from '@jsonjoy.com/fs-node-utils/lib/types/misc'; @@ -17,6 +17,7 @@ import type * as misc from '@jsonjoy.com/fs-node-utils/lib/types/misc'; const { F_OK, R_OK, W_OK, X_OK } = constants; export { DirectoryJSON, NestedDirectoryJSON, Volume }; +export type { IProcess }; // Default volume. export const vol = new Volume(); @@ -68,18 +69,34 @@ export function createFsFromVolume(vol: Volume): IFs { export const fs: IFs = createFsFromVolume(vol); +/** Options for creating a memfs instance. */ +export interface MemfsOptions { + /** Custom working directory for resolving relative paths. Defaults to `'/'`. */ + cwd?: string; + /** Custom `process`-like object for controlling platform, uid, gid, and cwd behavior. */ + process?: IProcess; +} + /** * Creates a new file system instance. * * @param json File system structure expressed as a JSON object. * Use `null` for empty directories and empty string for empty files. - * @param cwd Current working directory. The JSON structure will be created - * relative to this path. + * @param cwdOrOpts Current working directory (string) or options object. + * The JSON structure will be created relative to the cwd path. * @returns A `memfs` file system instance, which is a drop-in replacement for * the `fs` module. */ -export const memfs = (json: NestedDirectoryJSON = {}, cwd?: string): { fs: IFs; vol: Volume } => { - const vol = Volume.fromNestedJSON(json, cwd); +export const memfs = ( + json: NestedDirectoryJSON = {}, + cwdOrOpts: string | MemfsOptions = '/', +): { fs: IFs; vol: Volume } => { + const opts: MemfsOptions = typeof cwdOrOpts === 'string' ? { cwd: cwdOrOpts } : cwdOrOpts; + // When no explicit cwd is given but a custom process is provided, let the + // Superblock use that process's cwd(). Otherwise default to '/' so the + // convenience function keeps its opinionated virtual-root default. + const cwd = opts.cwd ?? (opts.process ? undefined : '/'); + const vol = Volume.fromNestedJSON(json, cwd, { process: opts.process }); const fs = createFsFromVolume(vol); return { fs, vol }; };