Skip to content

Commit f03f3d1

Browse files
committed
feat: add withFileTypes option support for readdir and readdirSync
Add `withFileTypes` option support for `fs.readdir` and `fs.readdirSync`. Add new `fs.Dirent` class (obviously) which is similar to `fs.Stats` but also need the `encoding` option for constructing the name. Move all encoding related types and functions to new `encoding.ts` file.
1 parent 0c854ec commit f03f3d1

File tree

6 files changed

+130
-42
lines changed

6 files changed

+130
-42
lines changed

src/__tests__/index.test.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ describe('memfs', () => {
1818
});
1919
it('Exports constructors', () => {
2020
expect(typeof memfs.Stats).toBe('function');
21+
expect(typeof memfs.Dirent).toBe('function');
2122
expect(typeof memfs.ReadStream).toBe('function');
2223
expect(typeof memfs.WriteStream).toBe('function');
2324
expect(typeof memfs.FSWatcher).toBe('function');
@@ -34,4 +35,4 @@ describe('memfs', () => {
3435
expect(typeof memfs[method]).toBe('function');
3536
}
3637
});
37-
});
38+
});

src/__tests__/volume.test.ts

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import {Link, Node, Stats} from "../node";
1+
import {Link, Node, Stats, Dirent} from "../node";
22
import {Volume, filenameToSteps, StatWatcher} from "../volume";
33

44

@@ -732,14 +732,28 @@ describe('volume', () => {
732732
xit('...');
733733
});
734734
describe('.readdirSync(path)', () => {
735-
const vol = new Volume;
736735
it('Returns simple list', () => {
736+
const vol = new Volume;
737737
vol.writeFileSync('/1.js', '123');
738738
vol.writeFileSync('/2.js', '123');
739739
const list = vol.readdirSync('/');
740740
expect(list.length).toBe(2);
741741
expect(list).toEqual(['1.js', '2.js']);
742742
});
743+
it('Returns a Dirent list', () => {
744+
const vol = new Volume;
745+
vol.writeFileSync('/1', '123');
746+
vol.mkdirSync('/2');
747+
const list = vol.readdirSync('/', { withFileTypes: true });
748+
expect(list.length).toBe(2);
749+
expect(list[0]).toBeInstanceOf(Dirent);
750+
const dirent0 = list[0] as Dirent;
751+
expect(dirent0.name).toBe('1');
752+
expect(dirent0.isFile()).toBe(true);
753+
const dirent1 = list[1] as Dirent;
754+
expect(dirent1.name).toBe('2');
755+
expect(dirent1.isDirectory()).toBe(true);
756+
});
743757
});
744758
describe('.readdir(path, callback)', () => {
745759
xit('...');
@@ -925,4 +939,4 @@ describe('volume', () => {
925939
expect((new StatWatcher(vol)).vol).toBe(vol);
926940
});
927941
});
928-
});
942+
});

src/encoding.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import errors = require('./internal/errors');
2+
3+
export type TDataOut = string | Buffer; // Data formats we give back to users.
4+
export type TEncoding = 'ascii' | 'utf8' | 'utf16le' | 'ucs2' | 'base64' | 'latin1' | 'binary' | 'hex';
5+
export type TEncodingExtended = TEncoding | 'buffer';
6+
7+
export const ENCODING_UTF8: TEncoding = 'utf8';
8+
9+
export function assertEncoding(encoding: string) {
10+
if(encoding && !Buffer.isEncoding(encoding))
11+
throw new errors.TypeError('ERR_INVALID_OPT_VALUE_ENCODING', encoding);
12+
}
13+
14+
export function strToEncoding(str: string, encoding?: TEncodingExtended): TDataOut {
15+
if(!encoding || (encoding === ENCODING_UTF8)) return str; // UTF-8
16+
if(encoding === 'buffer') return new Buffer(str); // `buffer` encoding
17+
return (new Buffer(str)).toString(encoding); // Custom encoding
18+
}

src/index.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import {Stats} from './node';
1+
import {Stats, Dirent} from './node';
22
import {Volume as _Volume, StatWatcher, FSWatcher, toUnixTimestamp, IReadStream, IWriteStream} from './volume';
33
import * as volume from './volume';
44
const {fsSyncMethods, fsAsyncMethods} = require('fs-monkey/lib/util/lists');
@@ -16,6 +16,7 @@ export const vol = new _Volume;
1616
export interface IFs extends _Volume {
1717
constants: typeof constants,
1818
Stats: new (...args) => Stats,
19+
Dirent: new (...args) => Dirent,
1920
StatWatcher: new () => StatWatcher,
2021
FSWatcher: new () => FSWatcher,
2122
ReadStream: new (...args) => IReadStream,
@@ -24,7 +25,7 @@ export interface IFs extends _Volume {
2425
}
2526

2627
export function createFsFromVolume(vol: _Volume): IFs {
27-
const fs = {F_OK, R_OK, W_OK, X_OK, constants, Stats} as any as IFs;
28+
const fs = {F_OK, R_OK, W_OK, X_OK, constants, Stats, Dirent} as any as IFs;
2829

2930
// Bind FS methods.
3031
for(const method of fsSyncMethods)

src/node.ts

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ import process from './process';
22
import {constants, S} from "./constants";
33
import {Volume} from "./volume";
44
import {EventEmitter} from "events";
5+
import {TEncodingExtended, strToEncoding, TDataOut} from './encoding';
6+
57
const {S_IFMT, S_IFDIR, S_IFREG, S_IFBLK, S_IFCHR, S_IFLNK, S_IFIFO, S_IFSOCK, O_APPEND} = constants;
68

79

@@ -540,3 +542,54 @@ export class Stats {
540542
return this._checkModeProperty(S_IFSOCK);
541543
}
542544
}
545+
546+
/**
547+
* A directory entry, like `fs.Dirent`.
548+
*/
549+
export class Dirent {
550+
551+
static build(link: Link, encoding: TEncodingExtended) {
552+
const dirent = new Dirent;
553+
const {mode} = link.getNode();
554+
555+
dirent.name = strToEncoding(link.getName(), encoding);
556+
dirent.mode = mode;
557+
558+
return dirent;
559+
}
560+
561+
name: TDataOut = '';
562+
private mode: number = 0;
563+
564+
private _checkModeProperty(property: number): boolean {
565+
return (this.mode & S_IFMT) === property;
566+
}
567+
568+
isDirectory(): boolean {
569+
return this._checkModeProperty(S_IFDIR);
570+
}
571+
572+
isFile(): boolean {
573+
return this._checkModeProperty(S_IFREG);
574+
}
575+
576+
isBlockDevice(): boolean {
577+
return this._checkModeProperty(S_IFBLK);
578+
}
579+
580+
isCharacterDevice(): boolean {
581+
return this._checkModeProperty(S_IFCHR);
582+
}
583+
584+
isSymbolicLink(): boolean {
585+
return this._checkModeProperty(S_IFLNK);
586+
}
587+
588+
isFIFO(): boolean {
589+
return this._checkModeProperty(S_IFIFO);
590+
}
591+
592+
isSocket(): boolean {
593+
return this._checkModeProperty(S_IFSOCK);
594+
}
595+
}

src/volume.ts

Lines changed: 37 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
11
import {resolve as resolveCrossPlatform} from 'path';
22
import * as pathModule from 'path';
3-
import {Node, Link, File, Stats} from "./node";
3+
import {Node, Link, File, Stats, Dirent} from "./node";
44
import {Buffer} from 'buffer';
55
import setImmediate from './setImmediate';
66
import process from './process';
77
import setTimeoutUnref, {TSetTimeout} from "./setTimeoutUnref";
88
import {Readable, Writable} from 'stream';
99
import {constants} from "./constants";
1010
import {EventEmitter} from "events";
11+
import {TEncoding, TEncodingExtended, TDataOut, assertEncoding, strToEncoding, ENCODING_UTF8} from './encoding';
1112
import errors = require('./internal/errors');
1213
import extend = require('fast-extend');
1314
import util = require('util');
@@ -41,12 +42,9 @@ export interface IError extends Error {
4142

4243
export type TFilePath = string | Buffer | URL;
4344
export type TFileId = TFilePath | number; // Number is used as a file descriptor.
44-
export type TDataOut = string | Buffer; // Data formats we give back to users.
4545
export type TData = TDataOut | Uint8Array; // Data formats users can give us.
4646
export type TFlags = string | number;
4747
export type TMode = string | number; // Mode can be a String, although docs say it should be a Number.
48-
export type TEncoding = 'ascii' | 'utf8' | 'utf16le' | 'ucs2' | 'base64' | 'latin1' | 'binary' | 'hex';
49-
export type TEncodingExtended = TEncoding | 'buffer';
5048
export type TTime = number | string | Date;
5149
export type TCallback<TData> = (error?: IError, data?: TData) => void;
5250
// type TCallbackWrite = (err?: IError, bytesWritten?: number, source?: Buffer) => void;
@@ -55,8 +53,6 @@ export type TCallback<TData> = (error?: IError, data?: TData) => void;
5553

5654
// ---------------------------------------- Constants
5755

58-
const ENCODING_UTF8: TEncoding = 'utf8';
59-
6056
// Default modes for opening files.
6157
const enum MODE {
6258
FILE = 0o666,
@@ -198,11 +194,6 @@ export function flagsToNumber(flags: TFlags): number {
198194

199195
// ---------------------------------------- Options
200196

201-
function assertEncoding(encoding: string) {
202-
if(encoding && !Buffer.isEncoding(encoding))
203-
throw new errors.TypeError('ERR_INVALID_OPT_VALUE_ENCODING', encoding);
204-
}
205-
206197
function getOptions <T extends IOptions> (defaults: T, options?: T|string): T {
207198
let opts: T;
208199
if(!options) return defaults;
@@ -349,6 +340,16 @@ const getMkdirOptions = options => {
349340
return extend({}, mkdirDefaults, options);
350341
}
351342

343+
// Options for `fs.readdir` and `fs.readdirSync`
344+
export interface IReaddirOptions extends IOptions {
345+
withFileTypes?: boolean,
346+
};
347+
const readdirDefaults: IReaddirOptions = {
348+
encoding: 'utf8',
349+
withFileTypes: false,
350+
};
351+
const getReaddirOptions = optsGenerator<IReaddirOptions>(readdirDefaults);
352+
const getReaddirOptsAndCb = optsAndCbGenerator<IReaddirOptions, TDataOut[]|Dirent[]>(getReaddirOptions);
352353

353354

354355
// ---------------------------------------- Utility functions
@@ -420,12 +421,6 @@ export function dataToBuffer(data: TData, encoding: string = ENCODING_UTF8): Buf
420421
else return Buffer.from(String(data), encoding);
421422
}
422423

423-
export function strToEncoding(str: string, encoding?: TEncodingExtended): TDataOut {
424-
if(!encoding || (encoding === ENCODING_UTF8)) return str; // UTF-8
425-
if(encoding === 'buffer') return new Buffer(str); // `buffer` encoding
426-
return (new Buffer(str)).toString(encoding); // Custom encoding
427-
}
428-
429424
export function bufferToEncoding(buffer: Buffer, encoding?: TEncodingExtended): TDataOut {
430425
if(!encoding || (encoding === 'buffer')) return buffer;
431426
else return buffer.toString(encoding);
@@ -1567,7 +1562,7 @@ export class Volume {
15671562
this.writeFile(id, data, opts, callback);
15681563
}
15691564

1570-
private readdirBase(filename: string, encoding: TEncodingExtended): TDataOut[] {
1565+
private readdirBase(filename: string, options: IReaddirOptions): TDataOut[]|Dirent[] {
15711566
const steps = filenameToSteps(filename);
15721567
const link: Link = this.getResolvedLink(steps);
15731568
if(!link) throwError(ENOENT, 'readdir', filename);
@@ -1576,35 +1571,41 @@ export class Volume {
15761571
if(!node.isDirectory())
15771572
throwError(ENOTDIR, 'scandir', filename);
15781573

1574+
if (options.withFileTypes) {
1575+
const list: Dirent[] = [];
1576+
for(let name in link.children) {
1577+
list.push(Dirent.build(link.children[name], options.encoding));
1578+
}
1579+
if (!isWin && options.encoding !== 'buffer') list.sort((a, b) => {
1580+
if (a.name < b.name) return -1;
1581+
if (a.name > b.name) return 1;
1582+
return 0;
1583+
});
1584+
return list;
1585+
}
1586+
15791587
const list: TDataOut[] = [];
1580-
for(let name in link.children)
1581-
list.push(strToEncoding(name, encoding));
1588+
for(let name in link.children) {
1589+
list.push(strToEncoding(name, options.encoding));
1590+
}
15821591

1583-
if(!isWin && encoding !== 'buffer') list.sort();
1592+
if(!isWin && options.encoding !== 'buffer') list.sort();
15841593

15851594
return list;
15861595
}
15871596

1588-
readdirSync(path: TFilePath, options?: IOptions | string): TDataOut[] {
1589-
const opts = getDefaultOpts(options);
1597+
readdirSync(path: TFilePath, options?: IReaddirOptions | string): TDataOut[]|Dirent[] {
1598+
const opts = getReaddirOptions(options);
15901599
const filename = pathToFilename(path);
1591-
return this.readdirBase(filename, opts.encoding);
1600+
return this.readdirBase(filename, opts);
15921601
}
15931602

1594-
readdir(path: TFilePath, callback: TCallback<TDataOut[]>);
1595-
readdir(path: TFilePath, options: IOptions | string, callback: TCallback<TDataOut[]>);
1603+
readdir(path: TFilePath, callback: TCallback<TDataOut[]|Dirent[]>);
1604+
readdir(path: TFilePath, options: IReaddirOptions | string, callback: TCallback<TDataOut[]|Dirent[]>);
15961605
readdir(path: TFilePath, a?, b?) {
1597-
let options: IOptions | string = a;
1598-
let callback: TCallback<TDataOut[]> = b;
1599-
1600-
if(typeof a === 'function') {
1601-
callback = a;
1602-
options = optsDefaults;
1603-
}
1604-
1605-
const opts = getDefaultOpts(options);
1606+
const [options, callback] = getReaddirOptsAndCb(a, b);
16061607
const filename = pathToFilename(path);
1607-
this.wrapAsync(this.readdirBase, [filename, opts.encoding], callback);
1608+
this.wrapAsync(this.readdirBase, [filename, options], callback);
16081609
}
16091610

16101611
private readlinkBase(filename: string, encoding: TEncodingExtended): TDataOut {

0 commit comments

Comments
 (0)