Skip to content

Commit 280f317

Browse files
Copilotstreamich
andcommitted
feat: add missing Node.js fs APIs with proper TypeScript types and stubs
Co-authored-by: streamich <[email protected]>
1 parent 1a3345c commit 280f317

File tree

9 files changed

+158
-0
lines changed

9 files changed

+158
-0
lines changed

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)

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+
});

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
}

src/node/types/FsSynchronousApi.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ export interface FsSynchronousApi {
2121
fsyncSync: (fd: number) => void;
2222
ftruncateSync: (fd: number, len?: number) => void;
2323
futimesSync: (fd: number, atime: misc.TTime, mtime: misc.TTime) => void;
24+
globSync: (pattern: string, options?: opts.IGlobOptions) => string[];
2425
lchmodSync: (path: misc.PathLike, mode: misc.TMode) => void;
2526
lchownSync: (path: misc.PathLike, uid: number, gid: number) => void;
2627
lutimesSync: (path: misc.PathLike, atime: number | string | Date, time: number | string | Date) => void;

src/node/types/options.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -200,6 +200,17 @@ export interface IOpenAsBlobOptions {
200200
type?: string;
201201
}
202202

203+
export interface IGlobOptions {
204+
/** Current working directory. */
205+
cwd?: string | URL;
206+
/** Exclude patterns. */
207+
exclude?: string | string[];
208+
/** Maximum search depth. */
209+
maxdepth?: number;
210+
/** Whether to include symbolic links. */
211+
withFileTypes?: boolean;
212+
}
213+
203214
export interface IOpendirOptions extends IOptions {
204215
/**
205216
* Number of directory entries that are buffered internally when reading from

src/node/volume.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1449,6 +1449,12 @@ export class Volume implements FsCallbackApi, FsSynchronousApi {
14491449
/** @todo Implement openAsBlob */
14501450
public openAsBlob: FsCallbackApi['openAsBlob'] = notImplemented;
14511451

1452+
/** @todo Implement glob */
1453+
public glob: FsCallbackApi['glob'] = notImplemented;
1454+
1455+
/** @todo Implement globSync */
1456+
public globSync: FsSynchronousApi['globSync'] = notImplemented;
1457+
14521458
private readonly _opendir = (filename: string, options: opts.IOpendirOptions): Dir => {
14531459
const link: Link = this._core.getResolvedLinkOrThrow(filename, 'scandir');
14541460
const node = link.getNode();

0 commit comments

Comments
 (0)