From 80cf80380757b1cb08c5ae6af828b8aff1b8cb93 Mon Sep 17 00:00:00 2001 From: mausworks Date: Wed, 24 Nov 2021 02:13:42 +0100 Subject: [PATCH] feat: support the `throwIfNoEntry` option This option is available since node v14 and is quite useful --- src/__tests__/promises.test.ts | 34 +++++++++++ src/__tests__/volume.test.ts | 34 +++++++++++ src/promises.ts | 3 +- src/volume.ts | 101 ++++++++++++++++++++++----------- 4 files changed, 138 insertions(+), 34 deletions(-) diff --git a/src/__tests__/promises.test.ts b/src/__tests__/promises.test.ts index 34888852..39225e30 100644 --- a/src/__tests__/promises.test.ts +++ b/src/__tests__/promises.test.ts @@ -149,6 +149,40 @@ describe('Promises API', () => { return expect(fileHandle.stat()).rejects.toBeInstanceOf(Error); }); }); + describe('.stat(path, options)', () => { + const { promises: vol } = new Volume(); + + it('Does not reject when entry does not exist if throwIfNoEntry is false', async () => { + const stat = await vol.stat('/no', { throwIfNoEntry: false }); + expect(stat).toBeUndefined(); + }); + it('Rejects when entry does not exist if throwIfNoEntry is true', async () => { + await expect(vol.stat('/foo', { throwIfNoEntry: true })).rejects.toBeInstanceOf(Error); + }); + it('Rejects when entry does not exist if throwIfNoEntry is not specified', async () => { + await expect(vol.stat('/foo')).rejects.toBeInstanceOf(Error); + }); + it('Rejects when entry does not exist if throwIfNoEntry is explicitly undefined', async () => { + await expect(vol.stat('/foo', { throwIfNoEntry: undefined })).rejects.toBeInstanceOf(Error); + }); + }); + describe('.lstat(path, options)', () => { + const { promises: vol } = new Volume(); + + it('Does not throw when entry does not exist if throwIfNoEntry is false', async () => { + const stat = await vol.lstat('/foo', { throwIfNoEntry: false }); + expect(stat).toBeUndefined(); + }); + it('Rejects when entry does not exist if throwIfNoEntry is true', async () => { + await expect(vol.lstat('/foo', { throwIfNoEntry: true })).rejects.toBeInstanceOf(Error); + }); + it('Rejects when entry does not exist if throwIfNoEntry is not specified', async () => { + await expect(vol.lstat('/foo')).rejects.toBeInstanceOf(Error); + }); + it('Rejects when entry does not exist if throwIfNoEntry is explicitly undefined', async () => { + await expect(vol.lstat('/foo', { throwIfNoEntry: undefined })).rejects.toBeInstanceOf(Error); + }); + }); describe('truncate([len])', () => { const vol = new Volume(); const { promises } = vol; diff --git a/src/__tests__/volume.test.ts b/src/__tests__/volume.test.ts index 6a0946d0..3db5198c 100644 --- a/src/__tests__/volume.test.ts +++ b/src/__tests__/volume.test.ts @@ -695,6 +695,40 @@ describe('volume', () => { }); }); }); + describe('.statSync(path, options)', () => { + const vol = new Volume(); + + it('Does not throw when entry does not exist if throwIfNoEntry is false', () => { + const stat = vol.statSync('/foo', { throwIfNoEntry: false }); + expect(stat).toBeUndefined(); + }); + it('Throws when entry does not exist if throwIfNoEntry is true', () => { + expect(() => vol.statSync('/foo', { throwIfNoEntry: true })).toThrow(); + }); + it('Throws when entry does not exist if throwIfNoEntry is not specified', () => { + expect(() => vol.statSync('/foo')).toThrow(); + }); + it('Throws when entry does not exist if throwIfNoEntry is explicitly undefined', () => { + expect(() => vol.statSync('/foo', { throwIfNoEntry: undefined })).toThrow(); + }); + }); + describe('.lstatSync(path, options)', () => { + const vol = new Volume(); + + it('Does not throw when entry does not exist if throwIfNoEntry is false', () => { + const stat = vol.lstatSync('/foo', { throwIfNoEntry: false }); + expect(stat).toBeUndefined(); + }); + it('Throws when entry does not exist if throwIfNoEntry is true', () => { + expect(() => vol.lstatSync('/foo', { throwIfNoEntry: true })).toThrow(); + }); + it('Throws when entry does not exist if throwIfNoEntry is not specified', () => { + expect(() => vol.lstatSync('/foo')).toThrow(); + }); + it('Throws when entry does not exist if throwIfNoEntry is explicitly undefined', () => { + expect(() => vol.lstatSync('/foo', { throwIfNoEntry: undefined })).toThrow(); + }); + }); describe('.lstatSync(path)', () => { const vol = new Volume(); const dojo = vol.root.createChild('dojo.js'); diff --git a/src/promises.ts b/src/promises.ts index cf2af80e..5bd2fca9 100644 --- a/src/promises.ts +++ b/src/promises.ts @@ -14,6 +14,7 @@ import { IWriteFileOptions, IStatOptions, IRmOptions, + IFStatOptions, } from './volume'; import Stats from './Stats'; import Dirent from './Dirent'; @@ -134,7 +135,7 @@ export class FileHandle implements IFileHandle { return promisify(this.vol, 'readFile')(this.fd, options); } - stat(options?: IStatOptions): Promise { + stat(options?: IFStatOptions): Promise { return promisify(this.vol, 'fstat')(this.fd, options); } diff --git a/src/volume.ts b/src/volume.ts index 9d2b8408..d3b5ca55 100644 --- a/src/volume.ts +++ b/src/volume.ts @@ -365,10 +365,17 @@ const readdirDefaults: IReaddirOptions = { const getReaddirOptions = optsGenerator(readdirDefaults); const getReaddirOptsAndCb = optsAndCbGenerator(getReaddirOptions); -// Options for `fs.fstat`, `fs.fstatSync`, `fs.lstat`, `fs.lstatSync`, `fs.stat`, and `fs.statSync` +// Options for `fs.lstat`, `fs.lstatSync`, `fs.stat`, and `fs.statSync` export interface IStatOptions { bigint?: boolean; + throwIfNoEntry?: boolean; } + +// Options for `fs.fstat`, fs.fstatSync +export interface IFStatOptions { + bigint?: boolean; +} + const statDefaults: IStatOptions = { bigint: false, }; @@ -1499,50 +1506,78 @@ export class Volume { this.wrapAsync(this.realpathBase, [pathFilename, opts.encoding], callback); } - private lstatBase(filename: string, bigint: false): Stats; - private lstatBase(filename: string, bigint: true): Stats; - private lstatBase(filename: string, bigint: boolean = false): Stats { + private lstatBase(filename: string, bigint: false, throwIfNoEntry: true): Stats; + private lstatBase(filename: string, bigint: true, throwIfNoEntry: true): Stats; + private lstatBase(filename: string, bigint: true, throwIfNoEntry: false): Stats | undefined; + private lstatBase(filename: string, bigint: false, throwIfNoEntry: false): Stats | undefined; + private lstatBase(filename: string, bigint = false, throwIfNoEntry = false): Stats | undefined { const link = this.getLink(filenameToSteps(filename)); - if (!link) throw createError(ENOENT, 'lstat', filename); - return Stats.build(link.getNode(), bigint); + + if (link) { + return Stats.build(link.getNode(), bigint); + } else if (!throwIfNoEntry) { + return undefined; + } else { + throw createError(ENOENT, 'lstat', filename); + } } lstatSync(path: PathLike): Stats; - lstatSync(path: PathLike, options: { bigint: false }): Stats; - lstatSync(path: PathLike, options: { bigint: true }): Stats; - lstatSync(path: PathLike, options?: IStatOptions): Stats { - return this.lstatBase(pathToFilename(path), getStatOptions(options).bigint as any); + lstatSync(path: PathLike, options: { throwIfNoEntry?: true | undefined }): Stats; + lstatSync(path: PathLike, options: { bigint: false; throwIfNoEntry?: true | undefined }): Stats; + lstatSync(path: PathLike, options: { bigint: true; throwIfNoEntry?: true | undefined }): Stats; + lstatSync(path: PathLike, options: { throwIfNoEntry: false }): Stats | undefined; + lstatSync(path: PathLike, options: { bigint: false; throwIfNoEntry: false }): Stats | undefined; + lstatSync(path: PathLike, options: { bigint: true; throwIfNoEntry: false }): Stats | undefined; + lstatSync(path: PathLike, options?: IStatOptions): Stats | undefined { + const { throwIfNoEntry = true, bigint = false } = getStatOptions(options); + + return this.lstatBase(pathToFilename(path), bigint as any, throwIfNoEntry as any); } - lstat(path: PathLike, callback: TCallback); - lstat(path: PathLike, options: IStatOptions, callback: TCallback); - lstat(path: PathLike, a: TCallback | IStatOptions, b?: TCallback) { - const [opts, callback] = getStatOptsAndCb(a, b); - this.wrapAsync(this.lstatBase, [pathToFilename(path), opts.bigint], callback); + lstat(path: PathLike, callback: TCallback): void; + lstat(path: PathLike, options: IStatOptions, callback: TCallback): void; + lstat(path: PathLike, a: TCallback | IStatOptions, b?: TCallback): void { + const [{ throwIfNoEntry = true, bigint = false }, callback] = getStatOptsAndCb(a, b); + this.wrapAsync(this.lstatBase, [pathToFilename(path), bigint, throwIfNoEntry], callback); } private statBase(filename: string): Stats; - private statBase(filename: string, bigint: false): Stats; - private statBase(filename: string, bigint: true): Stats; - private statBase(filename: string, bigint: boolean = false): Stats { + private statBase(filename: string, bigint: false, throwIfNoEntry: true): Stats; + private statBase(filename: string, bigint: true, throwIfNoEntry: true): Stats; + private statBase(filename: string, bigint: true, throwIfNoEntry: false): Stats | undefined; + private statBase(filename: string, bigint: false, throwIfNoEntry: false): Stats | undefined; + private statBase(filename: string, bigint = false, throwIfNoEntry = true): Stats | undefined { const link = this.getResolvedLink(filenameToSteps(filename)); - if (!link) throw createError(ENOENT, 'stat', filename); - return Stats.build(link.getNode(), bigint); + if (link) { + return Stats.build(link.getNode(), bigint); + } else if (!throwIfNoEntry) { + return undefined; + } else { + throw createError(ENOENT, 'stat', filename); + } } statSync(path: PathLike): Stats; - statSync(path: PathLike, options: { bigint: false }): Stats; - statSync(path: PathLike, options: { bigint: true }): Stats; - statSync(path: PathLike, options?: IStatOptions): Stats { - return this.statBase(pathToFilename(path), getStatOptions(options).bigint as any); + statSync(path: PathLike, options: { throwIfNoEntry?: true }): Stats; + statSync(path: PathLike, options: { throwIfNoEntry: false }): Stats | undefined; + statSync(path: PathLike, options: { bigint: false; throwIfNoEntry?: true }): Stats; + statSync(path: PathLike, options: { bigint: true; throwIfNoEntry?: true }): Stats; + statSync(path: PathLike, options: { bigint: false; throwIfNoEntry: false }): Stats | undefined; + statSync(path: PathLike, options: { bigint: true; throwIfNoEntry: false }): Stats | undefined; + statSync(path: PathLike, options?: IStatOptions): Stats | undefined { + const { bigint = true, throwIfNoEntry = true } = getStatOptions(options); + + return this.statBase(pathToFilename(path), bigint as any, throwIfNoEntry as any); } - stat(path: PathLike, callback: TCallback); - stat(path: PathLike, options: IStatOptions, callback: TCallback); - stat(path: PathLike, a: TCallback | IStatOptions, b?: TCallback) { - const [opts, callback] = getStatOptsAndCb(a, b); - this.wrapAsync(this.statBase, [pathToFilename(path), opts.bigint], callback); + stat(path: PathLike, callback: TCallback): void; + stat(path: PathLike, options: IStatOptions, callback: TCallback): void; + stat(path: PathLike, a: TCallback | IStatOptions, b?: TCallback): void { + const [{ bigint = false, throwIfNoEntry = true }, callback] = getStatOptsAndCb(a, b); + + this.wrapAsync(this.statBase, [pathToFilename(path), bigint, throwIfNoEntry], callback); } private fstatBase(fd: number): Stats; @@ -1557,13 +1592,13 @@ export class Volume { fstatSync(fd: number): Stats; fstatSync(fd: number, options: { bigint: false }): Stats; fstatSync(fd: number, options: { bigint: true }): Stats; - fstatSync(fd: number, options?: IStatOptions): Stats { + fstatSync(fd: number, options?: IFStatOptions): Stats { return this.fstatBase(fd, getStatOptions(options).bigint as any); } - fstat(fd: number, callback: TCallback); - fstat(fd: number, options: IStatOptions, callback: TCallback); - fstat(fd: number, a: TCallback | IStatOptions, b?: TCallback) { + fstat(fd: number, callback: TCallback): void; + fstat(fd: number, options: IFStatOptions, callback: TCallback): void; + fstat(fd: number, a: TCallback | IFStatOptions, b?: TCallback): void { const [opts, callback] = getStatOptsAndCb(a, b); this.wrapAsync(this.fstatBase, [fd, opts.bigint], callback); }