Skip to content

Commit 6af4a41

Browse files
authored
Merge branch 'master' into copilot/fix-1135
2 parents daaf632 + 3ee2531 commit 6af4a41

File tree

14 files changed

+697
-283
lines changed

14 files changed

+697
-283
lines changed

CHANGELOG.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,10 @@
1+
# [4.29.0](https://github.com/streamich/memfs/compare/v4.28.1...v4.29.0) (2025-08-01)
2+
3+
4+
### Features
5+
6+
* add missing Node.js fs APIs with proper TypeScript types and stubs ([280f317](https://github.com/streamich/memfs/commit/280f31701a219deed4b131c573e5fbd14cc50912))
7+
18
## [4.28.1](https://github.com/streamich/memfs/compare/v4.28.0...v4.28.1) (2025-08-01)
29

310

docs/missing-apis.md

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
# Missing Node.js fs APIs in memfs
2+
3+
This document lists the Node.js filesystem APIs that are not yet implemented in memfs.
4+
5+
## APIs with stub implementations (throw "Not implemented")
6+
7+
These APIs are defined with proper TypeScript types but currently throw "Not implemented" errors when called:
8+
9+
### File System Statistics
10+
11+
- `fs.statfs(path[, options], callback)` - Get file system statistics
12+
- `fs.statfsSync(path[, options])` - Synchronous version of statfs
13+
- `fs.promises.statfs(path[, options])` - Promise-based statfs
14+
15+
### File as Blob (Node.js 19+)
16+
17+
- `fs.openAsBlob(path[, options])` - Open a file as a Blob object for web API compatibility
18+
19+
### Pattern Matching (Node.js 20+)
20+
21+
- `fs.glob(pattern[, options], callback)` - Find files matching a glob pattern
22+
- `fs.globSync(pattern[, options])` - Synchronous version of glob
23+
- `fs.promises.glob(pattern[, options])` - Promise-based glob
24+
25+
## Implementation Status
26+
27+
### ✅ Fully Implemented APIs
28+
29+
All core Node.js fs APIs are implemented including:
30+
31+
- Basic file operations (read, write, open, close, etc.)
32+
- Directory operations (mkdir, rmdir, readdir, etc.)
33+
- File metadata (stat, lstat, fstat, chmod, chown, utimes, etc.)
34+
- Symbolic links (symlink, readlink, etc.)
35+
- File watching (watch, watchFile, unwatchFile)
36+
- Streams (createReadStream, createWriteStream)
37+
- File copying (copyFile, cp)
38+
- File truncation (truncate, ftruncate)
39+
40+
### 🚧 Stubbed APIs (not implemented)
41+
42+
- `statfs` / `statfsSync` - File system statistics
43+
- `openAsBlob` - Open file as Blob
44+
- `glob` / `globSync` - Pattern matching
45+
46+
## Usage
47+
48+
When calling these unimplemented APIs, they will throw an error:
49+
50+
```javascript
51+
const { Volume } = require('memfs');
52+
const vol = new Volume();
53+
54+
try {
55+
vol.globSync('*.js');
56+
} catch (err) {
57+
console.log(err.message); // "Not implemented"
58+
}
59+
```
60+
61+
## TypeScript Support
62+
63+
All missing APIs have proper TypeScript type definitions:
64+
65+
```typescript
66+
interface IGlobOptions {
67+
cwd?: string | URL;
68+
exclude?: string | string[];
69+
maxdepth?: number;
70+
withFileTypes?: boolean;
71+
}
72+
73+
interface IOpenAsBlobOptions {
74+
type?: string;
75+
}
76+
```
77+
78+
## Contributing
79+
80+
To implement any of these missing APIs:
81+
82+
1. Replace the `notImplemented` stub with actual implementation
83+
2. Add tests for the new functionality
84+
3. Update this documentation
85+
86+
## References
87+
88+
- [Node.js fs API Documentation](https://nodejs.org/api/fs.html)
89+
- [memfs source code](https://github.com/streamich/memfs)

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "memfs",
3-
"version": "4.28.1",
3+
"version": "4.29.0",
44
"description": "In-memory file-system with Node's fs API.",
55
"keywords": [
66
"fs",

src/fsa-to-node/FsaNodeFs.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -801,6 +801,7 @@ export class FsaNodeFs extends FsaNodeCore implements FsCallbackApi, FsSynchrono
801801
public readonly opendir: FsCallbackApi['opendir'] = notImplemented;
802802
public readonly readv: FsCallbackApi['readv'] = notImplemented;
803803
public readonly statfs: FsCallbackApi['statfs'] = notImplemented;
804+
public readonly glob: FsCallbackApi['glob'] = notImplemented;
804805

805806
/**
806807
* @todo Watchers could be implemented in the future on top of `FileSystemObserver`,
@@ -1088,6 +1089,7 @@ export class FsaNodeFs extends FsaNodeCore implements FsCallbackApi, FsSynchrono
10881089
public readonly opendirSync: FsSynchronousApi['opendirSync'] = notImplemented;
10891090
public readonly statfsSync: FsSynchronousApi['statfsSync'] = notImplemented;
10901091
public readonly readvSync: FsSynchronousApi['readvSync'] = notImplemented;
1092+
public readonly globSync: FsSynchronousApi['globSync'] = notImplemented;
10911093

10921094
public readonly symlinkSync: FsSynchronousApi['symlinkSync'] = notSupported;
10931095
public readonly linkSync: FsSynchronousApi['linkSync'] = notSupported;

src/node/FsPromises.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,7 @@ export class FsPromises implements FsPromisesApi {
137137
public readonly opendir = promisify(this.fs, 'opendir');
138138
public readonly statfs = promisify(this.fs, 'statfs');
139139
public readonly lutimes = promisify(this.fs, 'lutimes');
140+
public readonly glob = promisify(this.fs, 'glob');
140141
public readonly access = promisify(this.fs, 'access');
141142
public readonly chmod = promisify(this.fs, 'chmod');
142143
public readonly chown = promisify(this.fs, 'chown');
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import { Volume } from '../volume';
2+
3+
describe('Missing APIs', () => {
4+
let vol: Volume;
5+
6+
beforeEach(() => {
7+
vol = new Volume();
8+
});
9+
10+
describe('glob APIs', () => {
11+
it('globSync should throw "Not implemented"', () => {
12+
expect(() => vol.globSync('*.js')).toThrow('Not implemented');
13+
});
14+
15+
it('glob should throw "Not implemented"', () => {
16+
expect(() => vol.glob('*.js', () => {})).toThrow('Not implemented');
17+
});
18+
19+
it('promises.glob should throw "Not implemented"', async () => {
20+
await expect(vol.promises.glob('*.js')).rejects.toThrow('Not implemented');
21+
});
22+
});
23+
24+
describe('openAsBlob API', () => {
25+
it('should throw "Not implemented"', () => {
26+
expect(() => vol.openAsBlob('/test/file.txt')).toThrow('Not implemented');
27+
});
28+
});
29+
30+
describe('statfs APIs', () => {
31+
it('statfsSync should throw "Not implemented"', () => {
32+
expect(() => vol.statfsSync('/test')).toThrow('Not implemented');
33+
});
34+
35+
it('statfs should throw "Not implemented"', () => {
36+
expect(() => vol.statfs('/test', () => {})).toThrow('Not implemented');
37+
});
38+
39+
it('promises.statfs should throw "Not implemented"', async () => {
40+
await expect(vol.promises.statfs('/test')).rejects.toThrow('Not implemented');
41+
});
42+
});
43+
});
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
import { of } from '../../../thingies';
2+
import { memfs } from '../../../';
3+
4+
describe('.openAsBlob()', () => {
5+
it('can read a text file as blob', async () => {
6+
const { fs } = memfs({ '/dir/test.txt': 'Hello, World!' });
7+
const blob = await fs.openAsBlob('/dir/test.txt');
8+
expect(blob).toBeInstanceOf(Blob);
9+
expect(blob.size).toBe(13);
10+
expect(blob.type).toBe('');
11+
12+
const text = await blob.text();
13+
expect(text).toBe('Hello, World!');
14+
});
15+
16+
it('can read a binary file as blob', async () => {
17+
const binaryData = Buffer.from([0x89, 0x50, 0x4e, 0x47]); // PNG header
18+
const { fs } = memfs({ '/image.png': binaryData });
19+
const blob = await fs.openAsBlob('/image.png');
20+
expect(blob).toBeInstanceOf(Blob);
21+
expect(blob.size).toBe(4);
22+
23+
const arrayBuffer = await blob.arrayBuffer();
24+
const view = new Uint8Array(arrayBuffer);
25+
expect(Array.from(view)).toEqual([0x89, 0x50, 0x4e, 0x47]);
26+
});
27+
28+
it('can specify a mime type', async () => {
29+
const { fs } = memfs({ '/data.json': '{"key": "value"}' });
30+
const blob = await fs.openAsBlob('/data.json', { type: 'application/json' });
31+
expect(blob).toBeInstanceOf(Blob);
32+
expect(blob.type).toBe('application/json');
33+
34+
const text = await blob.text();
35+
expect(text).toBe('{"key": "value"}');
36+
});
37+
38+
it('handles empty files', async () => {
39+
const { fs } = memfs({ '/empty.txt': '' });
40+
const blob = await fs.openAsBlob('/empty.txt');
41+
expect(blob).toBeInstanceOf(Blob);
42+
expect(blob.size).toBe(0);
43+
44+
const text = await blob.text();
45+
expect(text).toBe('');
46+
});
47+
48+
it('throws if file does not exist', async () => {
49+
const { fs } = memfs({ '/dir/test.txt': 'content' });
50+
const [, err] = await of(fs.openAsBlob('/dir/test-NOT-FOUND.txt'));
51+
expect(err).toBeInstanceOf(Error);
52+
expect((<any>err).code).toBe('ENOENT');
53+
});
54+
55+
it('throws EISDIR if path is a directory', async () => {
56+
const { fs } = memfs({ '/dir/test.txt': 'content' });
57+
const [, err] = await of(fs.openAsBlob('/dir'));
58+
expect(err).toBeInstanceOf(Error);
59+
expect((<any>err).code).toBe('EISDIR');
60+
});
61+
62+
it('works with Buffer paths', async () => {
63+
const { fs } = memfs({ '/test.txt': 'buffer path test' });
64+
const pathBuffer = Buffer.from('/test.txt');
65+
const blob = await fs.openAsBlob(pathBuffer);
66+
expect(blob).toBeInstanceOf(Blob);
67+
68+
const text = await blob.text();
69+
expect(text).toBe('buffer path test');
70+
});
71+
72+
it('works with different path formats', async () => {
73+
const { fs } = memfs({ '/path-test.txt': 'path format test' });
74+
const blob = await fs.openAsBlob('/path-test.txt');
75+
expect(blob).toBeInstanceOf(Blob);
76+
77+
const text = await blob.text();
78+
expect(text).toBe('path format test');
79+
});
80+
81+
it('handles large files', async () => {
82+
const largeContent = 'x'.repeat(10000);
83+
const { fs } = memfs({ '/large.txt': largeContent });
84+
const blob = await fs.openAsBlob('/large.txt');
85+
expect(blob).toBeInstanceOf(Blob);
86+
expect(blob.size).toBe(10000);
87+
88+
const text = await blob.text();
89+
expect(text).toBe(largeContent);
90+
});
91+
92+
it('can read file through symlink', async () => {
93+
const { fs } = memfs({ '/original.txt': 'symlink test' });
94+
fs.symlinkSync('/original.txt', '/link.txt');
95+
96+
const blob = await fs.openAsBlob('/link.txt');
97+
expect(blob).toBeInstanceOf(Blob);
98+
99+
const text = await blob.text();
100+
expect(text).toBe('symlink test');
101+
});
102+
});

src/node/lists/fsCallbackApiList.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ export const fsCallbackApiList: Array<keyof FsCallbackApi> = [
77
'chown',
88
'close',
99
'copyFile',
10+
'cp',
1011
'createReadStream',
1112
'createWriteStream',
1213
'exists',
@@ -24,6 +25,7 @@ export const fsCallbackApiList: Array<keyof FsCallbackApi> = [
2425
'mkdir',
2526
'mkdtemp',
2627
'open',
28+
'openAsBlob',
2729
'opendir',
2830
'read',
2931
'readv',
@@ -35,6 +37,7 @@ export const fsCallbackApiList: Array<keyof FsCallbackApi> = [
3537
'rm',
3638
'rmdir',
3739
'stat',
40+
'statfs',
3841
'symlink',
3942
'truncate',
4043
'unlink',

src/node/types/FsCallbackApi.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,10 @@ export interface FsCallbackApi {
4242
(fd: number, len: number, callback: misc.TCallback<void>): void;
4343
};
4444
futimes: (fd: number, atime: misc.TTime, mtime: misc.TTime, callback: misc.TCallback<void>) => void;
45+
glob: {
46+
(pattern: string, callback: misc.TCallback<string[]>): void;
47+
(pattern: string, options: opts.IGlobOptions, callback: misc.TCallback<string[]>): void;
48+
};
4549
lchmod: (path: misc.PathLike, mode: misc.TMode, callback: misc.TCallback<void>) => void;
4650
lchown: (path: misc.PathLike, uid: number, gid: number, callback: misc.TCallback<void>) => void;
4751
lutimes: (

src/node/types/FsPromisesApi.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,4 +38,5 @@ export interface FsPromisesApi {
3838
options?: opts.IWatchOptions,
3939
) => AsyncIterableIterator<{ eventType: string; filename: string | Buffer }>;
4040
writeFile: (id: misc.TFileHandle, data: misc.TPromisesData, options?: opts.IWriteFileOptions) => Promise<void>;
41+
glob: (pattern: string, options?: opts.IGlobOptions) => Promise<string[]>;
4142
}

0 commit comments

Comments
 (0)