Skip to content

Commit

Permalink
feat: add withFileTypes option support for readdir and readdirSync
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
pizzafroide committed Nov 5, 2018
1 parent 0c854ec commit f03f3d1
Show file tree
Hide file tree
Showing 6 changed files with 130 additions and 42 deletions.
3 changes: 2 additions & 1 deletion src/__tests__/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ describe('memfs', () => {
});
it('Exports constructors', () => {
expect(typeof memfs.Stats).toBe('function');
expect(typeof memfs.Dirent).toBe('function');
expect(typeof memfs.ReadStream).toBe('function');
expect(typeof memfs.WriteStream).toBe('function');
expect(typeof memfs.FSWatcher).toBe('function');
Expand All @@ -34,4 +35,4 @@ describe('memfs', () => {
expect(typeof memfs[method]).toBe('function');
}
});
});
});
20 changes: 17 additions & 3 deletions src/__tests__/volume.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import {Link, Node, Stats} from "../node";
import {Link, Node, Stats, Dirent} from "../node";
import {Volume, filenameToSteps, StatWatcher} from "../volume";


Expand Down Expand Up @@ -732,14 +732,28 @@ describe('volume', () => {
xit('...');
});
describe('.readdirSync(path)', () => {
const vol = new Volume;
it('Returns simple list', () => {
const vol = new Volume;
vol.writeFileSync('/1.js', '123');
vol.writeFileSync('/2.js', '123');
const list = vol.readdirSync('/');
expect(list.length).toBe(2);
expect(list).toEqual(['1.js', '2.js']);
});
it('Returns a Dirent list', () => {
const vol = new Volume;
vol.writeFileSync('/1', '123');
vol.mkdirSync('/2');
const list = vol.readdirSync('/', { withFileTypes: true });
expect(list.length).toBe(2);
expect(list[0]).toBeInstanceOf(Dirent);
const dirent0 = list[0] as Dirent;
expect(dirent0.name).toBe('1');
expect(dirent0.isFile()).toBe(true);
const dirent1 = list[1] as Dirent;
expect(dirent1.name).toBe('2');
expect(dirent1.isDirectory()).toBe(true);
});
});
describe('.readdir(path, callback)', () => {
xit('...');
Expand Down Expand Up @@ -925,4 +939,4 @@ describe('volume', () => {
expect((new StatWatcher(vol)).vol).toBe(vol);
});
});
});
});
18 changes: 18 additions & 0 deletions src/encoding.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import errors = require('./internal/errors');

export type TDataOut = string | Buffer; // Data formats we give back to users.
export type TEncoding = 'ascii' | 'utf8' | 'utf16le' | 'ucs2' | 'base64' | 'latin1' | 'binary' | 'hex';
export type TEncodingExtended = TEncoding | 'buffer';

export const ENCODING_UTF8: TEncoding = 'utf8';

export function assertEncoding(encoding: string) {
if(encoding && !Buffer.isEncoding(encoding))
throw new errors.TypeError('ERR_INVALID_OPT_VALUE_ENCODING', encoding);
}

export function strToEncoding(str: string, encoding?: TEncodingExtended): TDataOut {
if(!encoding || (encoding === ENCODING_UTF8)) return str; // UTF-8
if(encoding === 'buffer') return new Buffer(str); // `buffer` encoding
return (new Buffer(str)).toString(encoding); // Custom encoding
}
5 changes: 3 additions & 2 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import {Stats} from './node';
import {Stats, Dirent} from './node';
import {Volume as _Volume, StatWatcher, FSWatcher, toUnixTimestamp, IReadStream, IWriteStream} from './volume';
import * as volume from './volume';
const {fsSyncMethods, fsAsyncMethods} = require('fs-monkey/lib/util/lists');
Expand All @@ -16,6 +16,7 @@ export const vol = new _Volume;
export interface IFs extends _Volume {
constants: typeof constants,
Stats: new (...args) => Stats,
Dirent: new (...args) => Dirent,
StatWatcher: new () => StatWatcher,
FSWatcher: new () => FSWatcher,
ReadStream: new (...args) => IReadStream,
Expand All @@ -24,7 +25,7 @@ export interface IFs extends _Volume {
}

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

// Bind FS methods.
for(const method of fsSyncMethods)
Expand Down
53 changes: 53 additions & 0 deletions src/node.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import process from './process';
import {constants, S} from "./constants";
import {Volume} from "./volume";
import {EventEmitter} from "events";
import {TEncodingExtended, strToEncoding, TDataOut} from './encoding';

const {S_IFMT, S_IFDIR, S_IFREG, S_IFBLK, S_IFCHR, S_IFLNK, S_IFIFO, S_IFSOCK, O_APPEND} = constants;


Expand Down Expand Up @@ -540,3 +542,54 @@ export class Stats {
return this._checkModeProperty(S_IFSOCK);
}
}

/**
* A directory entry, like `fs.Dirent`.
*/
export class Dirent {

static build(link: Link, encoding: TEncodingExtended) {
const dirent = new Dirent;
const {mode} = link.getNode();

dirent.name = strToEncoding(link.getName(), encoding);
dirent.mode = mode;

return dirent;
}

name: TDataOut = '';
private mode: number = 0;

private _checkModeProperty(property: number): boolean {
return (this.mode & S_IFMT) === property;
}

isDirectory(): boolean {
return this._checkModeProperty(S_IFDIR);
}

isFile(): boolean {
return this._checkModeProperty(S_IFREG);
}

isBlockDevice(): boolean {
return this._checkModeProperty(S_IFBLK);
}

isCharacterDevice(): boolean {
return this._checkModeProperty(S_IFCHR);
}

isSymbolicLink(): boolean {
return this._checkModeProperty(S_IFLNK);
}

isFIFO(): boolean {
return this._checkModeProperty(S_IFIFO);
}

isSocket(): boolean {
return this._checkModeProperty(S_IFSOCK);
}
}
73 changes: 37 additions & 36 deletions src/volume.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
import {resolve as resolveCrossPlatform} from 'path';
import * as pathModule from 'path';
import {Node, Link, File, Stats} from "./node";
import {Node, Link, File, Stats, Dirent} from "./node";
import {Buffer} from 'buffer';
import setImmediate from './setImmediate';
import process from './process';
import setTimeoutUnref, {TSetTimeout} from "./setTimeoutUnref";
import {Readable, Writable} from 'stream';
import {constants} from "./constants";
import {EventEmitter} from "events";
import {TEncoding, TEncodingExtended, TDataOut, assertEncoding, strToEncoding, ENCODING_UTF8} from './encoding';
import errors = require('./internal/errors');
import extend = require('fast-extend');
import util = require('util');
Expand Down Expand Up @@ -41,12 +42,9 @@ export interface IError extends Error {

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

// ---------------------------------------- Constants

const ENCODING_UTF8: TEncoding = 'utf8';

// Default modes for opening files.
const enum MODE {
FILE = 0o666,
Expand Down Expand Up @@ -198,11 +194,6 @@ export function flagsToNumber(flags: TFlags): number {

// ---------------------------------------- Options

function assertEncoding(encoding: string) {
if(encoding && !Buffer.isEncoding(encoding))
throw new errors.TypeError('ERR_INVALID_OPT_VALUE_ENCODING', encoding);
}

function getOptions <T extends IOptions> (defaults: T, options?: T|string): T {
let opts: T;
if(!options) return defaults;
Expand Down Expand Up @@ -349,6 +340,16 @@ const getMkdirOptions = options => {
return extend({}, mkdirDefaults, options);
}

// Options for `fs.readdir` and `fs.readdirSync`
export interface IReaddirOptions extends IOptions {
withFileTypes?: boolean,
};
const readdirDefaults: IReaddirOptions = {
encoding: 'utf8',
withFileTypes: false,
};
const getReaddirOptions = optsGenerator<IReaddirOptions>(readdirDefaults);
const getReaddirOptsAndCb = optsAndCbGenerator<IReaddirOptions, TDataOut[]|Dirent[]>(getReaddirOptions);


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

export function strToEncoding(str: string, encoding?: TEncodingExtended): TDataOut {
if(!encoding || (encoding === ENCODING_UTF8)) return str; // UTF-8
if(encoding === 'buffer') return new Buffer(str); // `buffer` encoding
return (new Buffer(str)).toString(encoding); // Custom encoding
}

export function bufferToEncoding(buffer: Buffer, encoding?: TEncodingExtended): TDataOut {
if(!encoding || (encoding === 'buffer')) return buffer;
else return buffer.toString(encoding);
Expand Down Expand Up @@ -1567,7 +1562,7 @@ export class Volume {
this.writeFile(id, data, opts, callback);
}

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

if (options.withFileTypes) {
const list: Dirent[] = [];
for(let name in link.children) {
list.push(Dirent.build(link.children[name], options.encoding));
}
if (!isWin && options.encoding !== 'buffer') list.sort((a, b) => {
if (a.name < b.name) return -1;
if (a.name > b.name) return 1;
return 0;
});
return list;
}

const list: TDataOut[] = [];
for(let name in link.children)
list.push(strToEncoding(name, encoding));
for(let name in link.children) {
list.push(strToEncoding(name, options.encoding));
}

if(!isWin && encoding !== 'buffer') list.sort();
if(!isWin && options.encoding !== 'buffer') list.sort();

return list;
}

readdirSync(path: TFilePath, options?: IOptions | string): TDataOut[] {
const opts = getDefaultOpts(options);
readdirSync(path: TFilePath, options?: IReaddirOptions | string): TDataOut[]|Dirent[] {
const opts = getReaddirOptions(options);
const filename = pathToFilename(path);
return this.readdirBase(filename, opts.encoding);
return this.readdirBase(filename, opts);
}

readdir(path: TFilePath, callback: TCallback<TDataOut[]>);
readdir(path: TFilePath, options: IOptions | string, callback: TCallback<TDataOut[]>);
readdir(path: TFilePath, callback: TCallback<TDataOut[]|Dirent[]>);
readdir(path: TFilePath, options: IReaddirOptions | string, callback: TCallback<TDataOut[]|Dirent[]>);
readdir(path: TFilePath, a?, b?) {
let options: IOptions | string = a;
let callback: TCallback<TDataOut[]> = b;

if(typeof a === 'function') {
callback = a;
options = optsDefaults;
}

const opts = getDefaultOpts(options);
const [options, callback] = getReaddirOptsAndCb(a, b);
const filename = pathToFilename(path);
this.wrapAsync(this.readdirBase, [filename, opts.encoding], callback);
this.wrapAsync(this.readdirBase, [filename, options], callback);
}

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

0 comments on commit f03f3d1

Please sign in to comment.