From a568b3aedb585c55d7eac1d3510c3158ed11d079 Mon Sep 17 00:00:00 2001 From: matt penrice Date: Sun, 29 Mar 2020 20:22:12 +0200 Subject: [PATCH 01/24] unionfs.use takes options --- src/union.ts | 34 +++++++++++++++++++--------------- 1 file changed, 19 insertions(+), 15 deletions(-) diff --git a/src/union.ts b/src/union.ts index c6d298fd..0dfa3599 100644 --- a/src/union.ts +++ b/src/union.ts @@ -72,12 +72,16 @@ const fsPromisesMethods = [ 'readFile' ] as const; +export type VolOptions = { + readonly?: boolean +} + /** * Union object represents a stack of filesystems */ export class Union { - private fss: IFS[] = []; + private fss: [IFS, VolOptions][] = []; public ReadStream: (typeof Readable) | (new (...args: any[]) => Readable) = Readable; public WriteStream: (typeof Writable) | (new (...args: any[]) => Writable) = Writable; @@ -119,7 +123,7 @@ export class Union { public watch = (...args) => { const watchers: FSWatcher[] = []; - for (const fs of this.fss) { + for (const [fs] of this.fss) { try { const watcher = fs.watch.apply(fs, args); watchers.push(watcher); @@ -133,7 +137,7 @@ export class Union { } public watchFile = (...args) => { - for (const fs of this.fss) { + for (const [fs] of this.fss) { try { fs.watchFile.apply(fs, args); } catch (e) { @@ -143,7 +147,7 @@ export class Union { } public existsSync = (path: string) => { - for (const fs of this.fss) { + for (const [fs] of this.fss) { try { if(fs.existsSync(path)) { return true @@ -199,7 +203,7 @@ export class Union { }; const j = this.fss.length - i - 1; - const fs = this.fss[j]; + const [fs] = this.fss[j]; const func = fs.readdir; if(!func) iterate(i + 1, Error('Method not supported: readdir')); @@ -212,7 +216,7 @@ export class Union { let lastError: IUnionFsError | null = null; let result = new Map(); for(let i = this.fss.length - 1; i >= 0; i--) { - const fs = this.fss[i]; + const [fs] = this.fss[i]; try { if(!fs.readdirSync) throw Error(`Method not supported: "readdirSync" with args "${args}"`); for (const res of fs.readdirSync.apply(fs, args)) { @@ -236,7 +240,7 @@ export class Union { let lastError: IUnionFsError | null = null; let result = new Map(); for(let i = this.fss.length - 1; i >= 0; i--) { - const fs = this.fss[i]; + const [fs] = this.fss[i]; try { if(!fs.promises || !fs.promises.readdir) throw Error(`Method not supported: "readdirSync" with args "${args}"`); for (const res of await fs.promises.readdir.apply(fs, args)) { @@ -274,7 +278,7 @@ export class Union { public createReadStream = (path: string) => { let lastError = null; - for (const fs of this.fss) { + for (const [fs] of this.fss) { try { if(!fs.createReadStream) throw Error(`Method not supported: "createReadStream"`); @@ -300,7 +304,7 @@ export class Union { public createWriteStream = (path: string) => { let lastError = null; - for (const fs of this.fss) { + for (const [fs] of this.fss) { try { if(!fs.createWriteStream) throw Error(`Method not supported: "createWriteStream"`); @@ -330,15 +334,15 @@ export class Union { * @param fs the filesystem interface to be added to the queue of FS's * @returns this instance of a unionFS */ - use(fs: IFS): this { - this.fss.push(fs); + use(fs: IFS, options: VolOptions = {readonly: false}): this { + this.fss.push([fs, options]); return this; } private syncMethod(method: string, args: any[]) { let lastError: IUnionFsError | null = null; for(let i = this.fss.length - 1; i >= 0; i--) { - const fs = this.fss[i]; + const [fs] = this.fss[i]; try { if(!fs[method]) throw Error(`Method not supported: "${method}" with args "${args}"`); return fs[method].apply(fs, args); @@ -383,7 +387,7 @@ export class Union { }; const j = this.fss.length - i - 1; - const fs = this.fss[j]; + const [fs] = this.fss[j]; const func = fs[method]; if(!func) iterate(i + 1, Error('Method not supported: ' + method)); @@ -396,9 +400,9 @@ export class Union { let lastError = null; for (let i = this.fss.length - 1; i >= 0; i--) { - const theFs = this.fss[i]; + const [fs] = this.fss[i]; - const promises = theFs.promises; + const promises = fs.promises; try { if (!promises || !promises[method]) { From a6a5205c5d3d6d4d6506a8fb7296ccdfa025ef80 Mon Sep 17 00:00:00 2001 From: matt penrice Date: Mon, 30 Mar 2020 18:01:39 +0200 Subject: [PATCH 02/24] some changes before switching approaches (and branches) --- src/__tests__/union.test.ts | 22 ++++++++++++++++++++++ src/union.ts | 8 ++++++-- 2 files changed, 28 insertions(+), 2 deletions(-) diff --git a/src/__tests__/union.test.ts b/src/__tests__/union.test.ts index 5e1ed7b1..ebf1ddf6 100644 --- a/src/__tests__/union.test.ts +++ b/src/__tests__/union.test.ts @@ -78,6 +78,28 @@ describe('union', () => { }); }); + // describe("copyFileSync", () => { + // it('copies from one memfs to another', () => { + // const vol1 = Volume.fromJSON({'/vol1': 'vol1'}); + // const vol2 = Volume.fromJSON({}); + // const ufs = new Union() as any; + // ufs.use(vol1 as any).use(vol2 as any) + // ufs.copyFileSync('/vol1', '/vol2'); + // expect(ufs.readFileSync('/vol2')).toEqual('vol1'); + // }) + // }); + + describe("writeFileSync", () => { + it('writes to the first vol', () => { + const vol1 = Volume.fromJSON({}); + const vol2 = Volume.fromJSON({}); + const ufs = new Union() as any; + ufs.use(vol1 as any).use(vol2 as any) + ufs.writeFileSync('/foo', 'bar') + expect(vol1.readFileSync('/foo', 'utf8')).toEqual('bar'); + }) + }); + describe("readdirSync", () => { it('reads one memfs correctly', () => { const vol = Volume.fromJSON({ diff --git a/src/union.ts b/src/union.ts index 0dfa3599..bb813575 100644 --- a/src/union.ts +++ b/src/union.ts @@ -44,6 +44,10 @@ const createFSProxy = (watchers: FSWatcher[]) => new Proxy({}, { } }); +const isWriteMethod = (method: string): boolean => { + return method.includes('write') +} + const fsPromisesMethods = [ 'access', 'copyFile', @@ -342,14 +346,14 @@ export class Union { private syncMethod(method: string, args: any[]) { let lastError: IUnionFsError | null = null; for(let i = this.fss.length - 1; i >= 0; i--) { - const [fs] = this.fss[i]; + const [fs, {readonly = false}] = this.fss[i]; try { if(!fs[method]) throw Error(`Method not supported: "${method}" with args "${args}"`); return fs[method].apply(fs, args); } catch(err) { err.prev = lastError; lastError = err; - if(!i) { // last one + if(!i && !(readonly && isWriteMethod(method))) { // last one throw err; } else { // Ignore error... From d706fcd3e980b4c450a9ef201a8edb046b5d6d78 Mon Sep 17 00:00:00 2001 From: matt penrice Date: Tue, 31 Mar 2020 13:48:15 +0200 Subject: [PATCH 03/24] adds readonly/writeonly --- package.json | 3 - src/__tests__/union.test.ts | 163 +++++++++++++++++++++++--- src/lists.ts | 117 +++++++++++++++++++ src/union.ts | 223 +++++++++++++++++++++++++----------- 4 files changed, 423 insertions(+), 83 deletions(-) create mode 100644 src/lists.ts diff --git a/package.json b/package.json index 830fc8c5..f6684729 100644 --- a/package.json +++ b/package.json @@ -28,9 +28,6 @@ "test-coverage": "jest --coverage", "semantic-release": "semantic-release" }, - "dependencies": { - "fs-monkey": "^1.0.0" - }, "devDependencies": { "semantic-release": "15.14.0", "@semantic-release/changelog": "3.0.6", diff --git a/src/__tests__/union.test.ts b/src/__tests__/union.test.ts index ebf1ddf6..2eaf2551 100644 --- a/src/__tests__/union.test.ts +++ b/src/__tests__/union.test.ts @@ -78,26 +78,51 @@ describe('union', () => { }); }); - // describe("copyFileSync", () => { - // it('copies from one memfs to another', () => { - // const vol1 = Volume.fromJSON({'/vol1': 'vol1'}); - // const vol2 = Volume.fromJSON({}); - // const ufs = new Union() as any; - // ufs.use(vol1 as any).use(vol2 as any) - // ufs.copyFileSync('/vol1', '/vol2'); - // expect(ufs.readFileSync('/vol2')).toEqual('vol1'); - // }) - // }); - - describe("writeFileSync", () => { - it('writes to the first vol', () => { + describe("readonly/writeonly", () => { + it('writes to the last vol added', () => { const vol1 = Volume.fromJSON({}); const vol2 = Volume.fromJSON({}); const ufs = new Union() as any; ufs.use(vol1 as any).use(vol2 as any) ufs.writeFileSync('/foo', 'bar') + expect(vol2.readFileSync('/foo', 'utf8')).toEqual('bar'); + }) + + it('writes to the latest added non-readonly vol', () => { + const vol1 = Volume.fromJSON({}); + const vol2 = Volume.fromJSON({}); + const ufs = new Union() as any; + ufs.use(vol1 as any).use(vol2 as any, {readonly: true}) + ufs.writeFileSync('/foo', 'bar') expect(vol1.readFileSync('/foo', 'utf8')).toEqual('bar'); }) + + it('writes to the latest added writeable vol', () => { + const vol1 = Volume.fromJSON({}); + const vol2 = Volume.fromJSON({}); + const ufs = new Union() as any; + ufs.use(vol1 as any).use(vol2 as any, {writeonly: true}) + ufs.writeFileSync('/foo', 'bar') + expect(vol2.readFileSync('/foo', 'utf8')).toEqual('bar'); + }) + + it('not throw error if write operation attempted with all volumes readonly', () => { + const vol1 = Volume.fromJSON({'/foo': 'bar'}); + const vol2 = Volume.fromJSON({'/foo': 'bar'}); + const ufs = new Union() as any; + ufs.use(vol1 as any, {readonly: true}).use(vol2 as any, {readonly: true}) + + expect(() => ufs.writeFileSync('/foo', 'bar')).not.toThrowError() + }) + + it('not throw error nor return value if read operation attempted with all volumes writeonly', () => { + const vol1 = Volume.fromJSON({'/foo': 'bar1'}); + const vol2 = Volume.fromJSON({'/foo': 'bar2'}); + const ufs = new Union() as any; + ufs.use(vol1 as any, {writeonly: true}).use(vol2 as any, {writeonly: true}) + + expect(ufs.readFileSync('/foo')).toBeUndefined() + }) }); describe("readdirSync", () => { @@ -254,6 +279,71 @@ describe('union', () => { }); + describe("readonly/writeonly", () => { + it('writes to the last vol added', (done) => { + const vol1 = Volume.fromJSON({}); + const vol2 = Volume.fromJSON({}); + const ufs = new Union() as any; + ufs.use(vol1 as any).use(vol2 as any) + ufs.writeFile('/foo', 'bar', (err, res) => { + vol2.readFile('/foo', 'utf8', (err, res) => { + expect(res).toEqual('bar'); + done() + }) + }); + }) + + it('writes to the latest added non-readonly vol', (done) => { + const vol1 = Volume.fromJSON({}); + const vol2 = Volume.fromJSON({}); + const ufs = new Union() as any; + ufs.use(vol1 as any).use(vol2 as any, {readonly: true}) + ufs.writeFile('/foo', 'bar', (err, res) => { + vol1.readFile('/foo', 'utf8', (err, res) => { + expect(res).toEqual('bar'); + done() + }) + }); + }) + + it('writes to the latest added writeable vol', (done) => { + const vol1 = Volume.fromJSON({}); + const vol2 = Volume.fromJSON({}); + const ufs = new Union() as any; + ufs.use(vol1 as any).use(vol2 as any, {writeonly: true}) + ufs.writeFile('/foo', 'bar', (err, res) => { + vol2.readFile('/foo', 'utf8', (err, res) => { + expect(res).toEqual('bar'); + done() + }) + }); + }) + + it('not throw error if write operation attempted with all volumes readonly', (done) => { + const vol1 = Volume.fromJSON({'/foo': 'bar'}); + const vol2 = Volume.fromJSON({'/foo': 'bar'}); + const ufs = new Union() as any; + ufs.use(vol1 as any, {readonly: true}).use(vol2 as any, {readonly: true}) + ufs.writeFile('/foo', 'bar', (err, res) => { + expect(err).toBeUndefined() + done() + }) + }) + + it('not throw error nor return value if read operation attempted with all volumes writeonly', (done) => { + const vol1 = Volume.fromJSON({'/foo': 'bar1'}); + const vol2 = Volume.fromJSON({'/foo': 'bar2'}); + const ufs = new Union() as any; + ufs.use(vol1 as any, {writeonly: true}).use(vol2 as any, {writeonly: true}) + + ufs.readFile('/foo', 'utf8', (err, res) => { + expect(err).toBeUndefined() + expect(res).toBeUndefined() + done() + }) + }) + }); + describe("readdir", () => { it('reads one memfs correctly', () => { const vol = Volume.fromJSON({ @@ -369,6 +459,53 @@ describe('union', () => { await expect(ufs.promises.readFile('/foo', 'utf8')).rejects.toThrowError(); }); + describe("readonly/writeonly", () => { + it('writes to the last vol added', () => { + const vol1 = Volume.fromJSON({}); + const vol2 = Volume.fromJSON({}); + const ufs = new Union() as any; + ufs.use(vol1 as any).use(vol2 as any) + ufs.writeFileSync('/foo', 'bar') + expect(vol2.readFileSync('/foo', 'utf8')).toEqual('bar'); + }) + + it('writes to the latest added non-readonly vol', () => { + const vol1 = Volume.fromJSON({}); + const vol2 = Volume.fromJSON({}); + const ufs = new Union() as any; + ufs.use(vol1 as any).use(vol2 as any, {readonly: true}) + ufs.writeFileSync('/foo', 'bar') + expect(vol1.readFileSync('/foo', 'utf8')).toEqual('bar'); + }) + + it('writes to the latest added writeable vol', () => { + const vol1 = Volume.fromJSON({}); + const vol2 = Volume.fromJSON({}); + const ufs = new Union() as any; + ufs.use(vol1 as any).use(vol2 as any, {writeonly: true}) + ufs.writeFileSync('/foo', 'bar') + expect(vol2.readFileSync('/foo', 'utf8')).toEqual('bar'); + }) + + it('not throw error if write operation attempted with all volumes readonly', () => { + const vol1 = Volume.fromJSON({'/foo': 'bar'}); + const vol2 = Volume.fromJSON({'/foo': 'bar'}); + const ufs = new Union() as any; + ufs.use(vol1 as any, {readonly: true}).use(vol2 as any, {readonly: true}) + + expect(() => ufs.writeFileSync('/foo', 'bar')).not.toThrowError() + }) + + it('not throw error nor return value if read operation attempted with all volumes writeonly', () => { + const vol1 = Volume.fromJSON({'/foo': 'bar1'}); + const vol2 = Volume.fromJSON({'/foo': 'bar2'}); + const ufs = new Union() as any; + ufs.use(vol1 as any, {writeonly: true}).use(vol2 as any, {writeonly: true}) + + expect(ufs.readFileSync('/foo')).toBeUndefined() + }) + }); + describe("readdir", () => { it('reads one memfs correctly', async () => { const vol = Volume.fromJSON({ diff --git a/src/lists.ts b/src/lists.ts new file mode 100644 index 00000000..10ec5161 --- /dev/null +++ b/src/lists.ts @@ -0,0 +1,117 @@ +export const fsSyncMethodsWriteonly = [ + "appendFileSync", + "chmodSync", + "chownSync", + "closeSync", + "copyFileSync", + "createWriteStream", + "fchmodSync", + "fchownSync", + "fdatasyncSync", + "fsyncSync", + "futimesSync", + "lchmodSync", + "lchownSync", + "linkSync", + "lstatSync", + "mkdirpSync", + "mkdirSync", + "mkdtempSync", + "renameSync", + "rmdirSync", + "symlinkSync", + "truncateSync", + "unlinkSync", + "utimesSync", + "writeFileSync", + "writeSync" +] as const; + +export const fsSyncMethodsReadonly = [ + "accessSync", + "createReadStream", + "existsSync", + "fstatSync", + "ftruncateSync", + "openSync", + "readdirSync", + "readFileSync", + "readlinkSync", + "readSync", + "realpathSync", + "statSync" +] as const; +export const fsAsyncMethodsReadonly = [ + "access", + "exists", + "fstat", + "open", + "read", + "readdir", + "readFile", + "readlink", + "realpath", + "unwatchFile", + "watch", + "watchFile" +] as const; +export const fsAsyncMethodsWriteonly = [ + "appendFile", + "chmod", + "chown", + "close", + "copyFile", + "fchmod", + "fchown", + "fdatasync", + "fsync", + "ftruncate", + "futimes", + "lchmod", + "lchown", + "link", + "lstat", + "mkdir", + "mkdirp", + "mkdtemp", + "rename", + "rmdir", + "stat", + "symlink", + "truncate", + "unlink", + "utimes", + "write", + "writeFile" +] as const; + +export const fsPromiseMethodsReadonly = [ + "access", + "open", + "opendir", + "readdir", + "readFile", + "readlink", + "realpath" +] as const; + +export const fsPromiseMethodsWriteonly = [ + "appendFile", + "chmod", + "chown", + "copyFile", + "lchmod", + "lchown", + "link", + "lstat", + "mkdir", + "mkdtemp", + "rename", + "rmdir", + "stat", + "symlink", + "truncate", + "unlink", + "utimes", + "writeFile", +] as const; diff --git a/src/union.ts b/src/union.ts index bb813575..276bfe1a 100644 --- a/src/union.ts +++ b/src/union.ts @@ -1,7 +1,7 @@ import { FSWatcher, Dirent } from "fs"; import {IFS} from "./fs"; import {Readable, Writable} from 'stream'; -const {fsAsyncMethods, fsSyncMethods} = require('fs-monkey/lib/util/lists'); +import {fsSyncMethodsWriteonly, fsSyncMethodsReadonly, fsAsyncMethodsWriteonly, fsAsyncMethodsReadonly, fsPromiseMethodsWriteonly, fsPromiseMethodsReadonly} from './lists' export interface IUnionFsError extends Error { prev?: IUnionFsError | null, @@ -44,40 +44,25 @@ const createFSProxy = (watchers: FSWatcher[]) => new Proxy({}, { } }); -const isWriteMethod = (method: string): boolean => { - return method.includes('write') -} - -const fsPromisesMethods = [ - 'access', - 'copyFile', - 'open', - 'opendir', - 'rename', - 'truncate', - 'rmdir', - 'mkdir', - 'readdir', - 'readlink', - 'symlink', - 'lstat', - 'stat', - 'link', - 'unlink', - 'chmod', - 'lchmod', - 'lchown', - 'chown', - 'utimes', - 'realpath', - 'mkdtemp', - 'writeFile', - 'appendFile', - 'readFile' -] as const; - export type VolOptions = { readonly?: boolean + writeonly?: boolean +} + +type SyncMethodNames = typeof fsSyncMethodsReadonly[number] & typeof fsSyncMethodsWriteonly[number] +type ASyncMethodNames = typeof fsSyncMethodsReadonly[number] & typeof fsAsyncMethodsWriteonly[number] +type PromiseMethodNames = typeof fsPromiseMethodsReadonly[number] & typeof fsPromiseMethodsWriteonly[number] +type FSMethod = (args: any) => any +type FSMethodStack = { + sync: { + [K in SyncMethodNames]: FSMethod | undefined + }, + async: { + [K in ASyncMethodNames]: FSMethod | undefined; + }, + promise: { + [K in PromiseMethodNames]: FSMethod | undefined; + }, } /** @@ -85,7 +70,7 @@ export type VolOptions = { */ export class Union { - private fss: [IFS, VolOptions][] = []; + private fss: [IFS, VolOptions, FSMethodStack][] = []; public ReadStream: (typeof Readable) | (new (...args: any[]) => Readable) = Readable; public WriteStream: (typeof Writable) | (new (...args: any[]) => Writable) = Writable; @@ -93,18 +78,18 @@ export class Union { private promises: {} = {}; constructor() { - for(let method of fsSyncMethods) { + for(let method of [...fsSyncMethodsReadonly, ...fsSyncMethodsWriteonly]) { if (!SPECIAL_METHODS.has(method)) { // check we don't already have a property for this method this[method] = (...args) => this.syncMethod(method, args); } } - for(let method of fsAsyncMethods) { + for(let method of [...fsAsyncMethodsReadonly, ...fsAsyncMethodsWriteonly]) { if (!SPECIAL_METHODS.has(method)) { // check we don't already have a property for this method this[method] = (...args) => this.asyncMethod(method, args); } } - for(let method of fsPromisesMethods) { + for(let method of [...fsPromiseMethodsReadonly, ...fsPromiseMethodsWriteonly]) { if(method ==='readdir') { this.promises[method] = this.readdirPromise; @@ -127,7 +112,8 @@ export class Union { public watch = (...args) => { const watchers: FSWatcher[] = []; - for (const [fs] of this.fss) { + for (const [fs, {writeonly}] of this.fss) { + if (writeonly) continue; try { const watcher = fs.watch.apply(fs, args); watchers.push(watcher); @@ -141,7 +127,8 @@ export class Union { } public watchFile = (...args) => { - for (const [fs] of this.fss) { + for (const [fs, {writeonly}] of this.fss) { + if (writeonly) continue; try { fs.watchFile.apply(fs, args); } catch (e) { @@ -151,7 +138,8 @@ export class Union { } public existsSync = (path: string) => { - for (const [fs] of this.fss) { + for (const [fs, {writeonly}] of this.fss) { + if (writeonly) continue; try { if(fs.existsSync(path)) { return true @@ -207,10 +195,11 @@ export class Union { }; const j = this.fss.length - i - 1; - const [fs] = this.fss[j]; + const [fs, {writeonly}] = this.fss[j]; const func = fs.readdir; if(!func) iterate(i + 1, Error('Method not supported: readdir')); + else if(writeonly) iterate(i + 1, Error(`Writeonly enabled for vol '${i}': readdir`)); else func.apply(fs, args); }; iterate(); @@ -220,7 +209,8 @@ export class Union { let lastError: IUnionFsError | null = null; let result = new Map(); for(let i = this.fss.length - 1; i >= 0; i--) { - const [fs] = this.fss[i]; + const [fs, {writeonly}] = this.fss[i]; + if (writeonly) continue; try { if(!fs.readdirSync) throw Error(`Method not supported: "readdirSync" with args "${args}"`); for (const res of fs.readdirSync.apply(fs, args)) { @@ -244,7 +234,8 @@ export class Union { let lastError: IUnionFsError | null = null; let result = new Map(); for(let i = this.fss.length - 1; i >= 0; i--) { - const [fs] = this.fss[i]; + const [fs, {writeonly}] = this.fss[i]; + if (writeonly) continue; try { if(!fs.promises || !fs.promises.readdir) throw Error(`Method not supported: "readdirSync" with args "${args}"`); for (const res of await fs.promises.readdir.apply(fs, args)) { @@ -282,7 +273,8 @@ export class Union { public createReadStream = (path: string) => { let lastError = null; - for (const [fs] of this.fss) { + for (const [fs, {writeonly}] of this.fss) { + if (writeonly) continue; try { if(!fs.createReadStream) throw Error(`Method not supported: "createReadStream"`); @@ -308,7 +300,8 @@ export class Union { public createWriteStream = (path: string) => { let lastError = null; - for (const [fs] of this.fss) { + for (const [fs, {readonly}] of this.fss) { + if (readonly) continue; try { if(!fs.createWriteStream) throw Error(`Method not supported: "createWriteStream"`); @@ -338,22 +331,125 @@ export class Union { * @param fs the filesystem interface to be added to the queue of FS's * @returns this instance of a unionFS */ - use(fs: IFS, options: VolOptions = {readonly: false}): this { - this.fss.push([fs, options]); + use(fs: IFS, options: VolOptions = {}): this { + this.fss.push([fs, options, this.createMethods(fs, options)]); return this; } + /** + * At the time of the [[use]] call, we create our sync, async and promise methods + * for performance reasons + * + * @param fs + * @param options + */ + private createMethods(fs: IFS, options: VolOptions): FSMethodStack { + const noop = undefined + const createFunc = (method: string) => { + if (!fs[method]) return (...args: any[]) => { throw new Error(`Method not supported: "${method}" with args "${args}"`); }; + return (...args: any[]) => fs[method as string].apply(fs, args); + } + switch (true) { + case options.readonly: + return { + sync: { + ...fsSyncMethodsReadonly.reduce((acc, method) => {acc[method] = createFunc(method); return acc;}, {}), + ...fsSyncMethodsWriteonly.reduce((acc, method) => {acc[method] = noop; return acc;}, {}), + }, + async: { + ...fsAsyncMethodsReadonly.reduce((acc, method) => {acc[method] = createFunc(method); return acc;}, {}), + ...fsAsyncMethodsWriteonly.reduce((acc, method) => {acc[method] = noop; return acc;}, {}), + }, + promise: { + ...fsPromiseMethodsReadonly.reduce((acc, method) => { + const promises = fs.promises; + if (!promises || !promises[method]) { + acc[method] = (...args: any) => { + throw Error(`Promise of method not supported: "${String(method)}" with args "${args}"`); + } + return acc + } + acc[method] = (...args: any) => promises[method as string].apply(fs, args); + return acc; + }, {}), + ...fsPromiseMethodsWriteonly.reduce((acc, method) => {acc[method] = noop; return acc;}, {}), + }, + } + case options.writeonly: + return { + sync: { + ...fsSyncMethodsReadonly.reduce((acc, method) => {acc[method] = noop; return acc;}, {}), + ...fsSyncMethodsWriteonly.reduce((acc, method) => {acc[method] = createFunc(method); return acc;}, {}), + }, + async: { + ...fsAsyncMethodsReadonly.reduce((acc, method) => {acc[method] = noop; return acc;}, {}), + ...fsAsyncMethodsWriteonly.reduce((acc, method) => {acc[method] = createFunc(method); return acc;}, {}), + }, + promise: { + ...fsPromiseMethodsReadonly.reduce((acc, method) => {acc[method] = noop; return acc;}, {}), + ...fsPromiseMethodsWriteonly.reduce((acc, method) => { + const promises = fs.promises; + if (!promises || !promises[method]) { + acc[method] = (...args: any) => { + throw Error(`Promise of method not supported: "${String(method)}" with args "${args}"`); + } + return acc + } + acc[method] = (...args: any) => promises[method as string].apply(fs, args); + return acc; + }, {}), + }, + } + default: + return { + sync: { + ...fsSyncMethodsReadonly.reduce((acc, method) => {acc[method] = createFunc(method); return acc;}, {}), + ...fsSyncMethodsWriteonly.reduce((acc, method) => {acc[method] = createFunc(method); return acc;}, {}), + }, + async: { + ...fsAsyncMethodsReadonly.reduce((acc, method) => {acc[method] = createFunc(method); return acc;}, {}), + ...fsAsyncMethodsWriteonly.reduce((acc, method) => {acc[method] = createFunc(method); return acc;}, {}), + }, + promise: { + ...fsPromiseMethodsReadonly.reduce((acc, method) => { + const promises = fs.promises; + if (!promises || !promises[method]) { + acc[method] = (...args: any) => { + throw Error(`Promise of method not supported: "${String(method)}" with args "${args}"`); + } + return acc + } + acc[method] = (...args: any) => promises[method as string].apply(fs, args); + return acc; + }, {}), + ...fsPromiseMethodsWriteonly.reduce((acc, method) => { + const promises = fs.promises; + if (!promises || !promises[method]) { + acc[method] = (...args: any) => { + throw Error(`Promise of method not supported: "${String(method)}" with args "${args}"`); + } + return acc + } + acc[method] = (...args: any) => promises[method as string].apply(fs, args); + return acc; + }, {}), + }, + } + } + } + private syncMethod(method: string, args: any[]) { + if (!this.fss.length) throw new Error('No file systems attached') let lastError: IUnionFsError | null = null; for(let i = this.fss.length - 1; i >= 0; i--) { - const [fs, {readonly = false}] = this.fss[i]; + const [_fs, _options, methodStack] = this.fss[i]; try { - if(!fs[method]) throw Error(`Method not supported: "${method}" with args "${args}"`); - return fs[method].apply(fs, args); + if (!methodStack['sync'][method]) continue; + return methodStack['sync'][method](...args) } catch(err) { err.prev = lastError; lastError = err; - if(!i && !(readonly && isWriteMethod(method))) { // last one + if(!i) { // last one throw err; } else { // Ignore error... @@ -380,7 +476,7 @@ export class Union { // Already tried all file systems, return the last error. if(i >= this.fss.length) { // last one - if(cb) cb(err || Error('No file systems attached.')); + if(cb) cb(err ?? (!this.fss.length ? new Error('No file systems attached.') : undefined)); return; } @@ -391,32 +487,25 @@ export class Union { }; const j = this.fss.length - i - 1; - const [fs] = this.fss[j]; - const func = fs[method]; + const [_fs, _options, fsMethodStack] = this.fss[j]; + const func = fsMethodStack['async'][method] - if(!func) iterate(i + 1, Error('Method not supported: ' + method)); - else func.apply(fs, args); + if(!func) iterate(i + 1); + else func(...args); }; iterate(); } async promiseMethod(method: string, args: any[]) { + if (!this.fss.length) throw new Error('No file systems attached') let lastError = null; for (let i = this.fss.length - 1; i >= 0; i--) { - const [fs] = this.fss[i]; - - const promises = fs.promises; + const [_fs, _options, fsMethodStack] = this.fss[i]; try { - if (!promises || !promises[method]) { - throw Error( - `Promise of method not supported: "${String(method)}" with args "${args}"` - ); - } - - // return promises[method](...args); - return await promises[method].apply(promises, args); + const func = fsMethodStack['promise'][method] + return await func(...args); } catch (err) { err.prev = lastError; lastError = err; From c4cd09c15ed5e2e4a07af2993323bdd556e3d0ed Mon Sep 17 00:00:00 2001 From: matt penrice Date: Tue, 31 Mar 2020 14:32:27 +0200 Subject: [PATCH 04/24] Adds demo, types and docs --- README.md | 14 ++++++++++++++ demo/writeonly.ts | 20 ++++++++++++++++++++ src/index.ts | 2 +- 3 files changed, 35 insertions(+), 1 deletion(-) create mode 100644 demo/writeonly.ts diff --git a/README.md b/README.md index 08cb5d6b..16e17274 100644 --- a/README.md +++ b/README.md @@ -20,6 +20,20 @@ ufs ufs.readFileSync(/* ... */); ``` +This module allows you mark volumes as `readonly`/`writeonly` to prevent unwanted mutating of volumes + +```js +import {ufs} from 'unionfs'; +import {fs as fs1} from 'memfs'; +import * as fs2 from 'fs'; + +ufs + .use(fs1, {readonly: true}) + .use(fs2, {writeonly: true}); + +ufs.writeFileSync(/* ... */); // fs2 will "collect" mutations; fs1 will remain unchanged +``` + Use this module with [`memfs`][memfs] and [`linkfs`][linkfs]. `memfs` allows you to create virtual in-memory file system. `linkfs` allows you to redirect `fs` paths. diff --git a/demo/writeonly.ts b/demo/writeonly.ts new file mode 100644 index 00000000..193bc4bd --- /dev/null +++ b/demo/writeonly.ts @@ -0,0 +1,20 @@ +/** + * This example overlays a writeonly memfs volume which will effectively "collect" write operations + * and leave the underlying volume unchanged. This is useful for test suite scenarios + * + * Please also see [[../src/__tests__/union.test.ts]] for option examples + */ + +import {ufs} from '../src'; +import {Volume} from 'memfs'; + +const vol1 = Volume.fromJSON({ + '/underlying_file': 'hello', +}); +const vol2 = Volume.fromJSON({}); + +ufs.use(vol1 as any).use(vol2 as any, {writeonly: true}) +ufs.writeFileSync('/foo', 'bar') + +console.log(vol2.readFileSync('/foo', 'utf8')) // bar +console.log(vol2.readFileSync('/underlying_file', 'utf8')) // error diff --git a/src/index.ts b/src/index.ts index 41ef8135..df0c9744 100644 --- a/src/index.ts +++ b/src/index.ts @@ -2,7 +2,7 @@ import {Union as _Union} from "./union"; import {IFS} from "./fs"; export interface IUnionFs extends IFS { - use(fs: IFS): this; + use: _Union['use'] } export const Union = _Union as any as (new () => IUnionFs); From ecfab70d350dccd51f7d1d15b4e07b613f7b810f Mon Sep 17 00:00:00 2001 From: matt penrice Date: Tue, 31 Mar 2020 14:52:00 +0200 Subject: [PATCH 05/24] test fix --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index d93ee32b..c50f9053 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,4 @@ package-lock.json /lib/ /demo/**/*.js /src/**/*.js +*.test.ts.test \ No newline at end of file From 752ff5d5854eaa1ead6520087aaea24667562956 Mon Sep 17 00:00:00 2001 From: matt penrice Date: Tue, 31 Mar 2020 16:11:51 +0200 Subject: [PATCH 06/24] test fix2 --- src/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/index.ts b/src/index.ts index df0c9744..c09fa332 100644 --- a/src/index.ts +++ b/src/index.ts @@ -2,7 +2,7 @@ import {Union as _Union} from "./union"; import {IFS} from "./fs"; export interface IUnionFs extends IFS { - use: _Union['use'] + use: (...args: Parameters<_Union['use']>) => this } export const Union = _Union as any as (new () => IUnionFs); From 80673bb5f4f344c15eae054f30529da4a8ee7aed Mon Sep 17 00:00:00 2001 From: matt penrice Date: Thu, 2 Apr 2020 09:47:41 +0200 Subject: [PATCH 07/24] =?UTF-8?q?wip:=20=F0=9F=92=AA=20adds=20prettier=20f?= =?UTF-8?q?or=20unionfs=20amongst=20others?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 5 +- prettier.config.js | 9 + src/__tests__/union.test.ts | 1157 +++++++++++++++++------------------ src/fs.ts | 146 ++--- src/index.ts | 10 +- src/lists.ts | 204 +++--- src/union.ts | 964 +++++++++++++++-------------- 7 files changed, 1282 insertions(+), 1213 deletions(-) create mode 100644 prettier.config.js diff --git a/package.json b/package.json index f6684729..01834fd8 100644 --- a/package.json +++ b/package.json @@ -23,13 +23,14 @@ }, "scripts": { "build": "tsc -p .", + "prettier": "prettier --ignore-path .gitignore --write \"src/**/*.{ts,js}\"", + "prettier:diff": "prettier -l \"src/**/*.{ts,js}\"", "test": "jest", "test-watch": "jest --watch", "test-coverage": "jest --coverage", "semantic-release": "semantic-release" }, "devDependencies": { - "semantic-release": "15.14.0", "@semantic-release/changelog": "3.0.6", "@semantic-release/git": "7.0.18", "@semantic-release/npm": "5.3.5", @@ -38,6 +39,8 @@ "jest": "25.2.3", "memfs": "3.1.2", "memory-fs": "0.5.0", + "prettier": "1.19.1", + "semantic-release": "15.14.0", "source-map-support": "0.5.16", "ts-jest": "25.2.1", "ts-node": "8.8.1", diff --git a/prettier.config.js b/prettier.config.js new file mode 100644 index 00000000..ce7efcaa --- /dev/null +++ b/prettier.config.js @@ -0,0 +1,9 @@ +module.exports = { + printWidth: 120, + tabWidth: 2, + useTabs: false, + semi: true, + singleQuote: true, + trailingComma: 'all', + bracketSpacing: true, +}; \ No newline at end of file diff --git a/src/__tests__/union.test.ts b/src/__tests__/union.test.ts index 2eaf2551..eca64cd5 100644 --- a/src/__tests__/union.test.ts +++ b/src/__tests__/union.test.ts @@ -1,625 +1,622 @@ -import {Union} from '..'; -import {Volume, createFsFromVolume} from 'memfs'; +import { Union } from '..'; +import { Volume, createFsFromVolume } from 'memfs'; import * as fs from 'fs'; describe('union', () => { - describe('Union', () => { - describe('sync methods', () => { - it('Basic one file system', () => { - const vol = Volume.fromJSON({'/foo': 'bar'}); - const ufs = new Union(); - ufs.use(vol as any); - expect(ufs.readFileSync('/foo', 'utf8')).toBe('bar'); - }); + describe('Union', () => { + describe('sync methods', () => { + it('Basic one file system', () => { + const vol = Volume.fromJSON({ '/foo': 'bar' }); + const ufs = new Union(); + ufs.use(vol as any); + expect(ufs.readFileSync('/foo', 'utf8')).toBe('bar'); + }); + + it('basic two filesystems', () => { + const vol = Volume.fromJSON({ '/foo': 'bar' }); + const vol2 = Volume.fromJSON({ '/foo': 'baz' }); + const ufs = new Union(); + ufs.use(vol as any); + ufs.use(vol2 as any); + + expect(ufs.readFileSync('/foo', 'utf8')).toBe('baz'); + }); + + it('File not found', () => { + const vol = Volume.fromJSON({ '/foo': 'bar' }); + const ufs = new Union(); + ufs.use(vol as any); + try { + ufs.readFileSync('/not-found', 'utf8'); + throw Error('This should not throw'); + } catch (err) { + expect(err.code).toBe('ENOENT'); + } + }); + + it('Method does not exist', () => { + const vol = Volume.fromJSON({ '/foo': 'bar' }); + const ufs = new Union(); + vol.readFileSync = undefined as any; + ufs.use(vol as any); + try { + ufs.readFileSync('/foo', 'utf8'); + throw Error('not_this'); + } catch (err) { + expect(err.message).not.toBe('not_this'); + } + }); + + describe('watch()', () => { + it('should create a watcher', () => { + const ufs = new Union().use(Volume.fromJSON({ 'foo.js': 'hello test' }, '/tmp') as any); + + const mockCallback = jest.fn(); + const writtenContent = 'hello world'; + const watcher = ufs.watch('/tmp/foo.js', mockCallback); + + ufs.writeFileSync('/tmp/foo.js', writtenContent); + + expect(mockCallback).toBeCalledTimes(2); + expect(mockCallback).toBeCalledWith('change', '/tmp/foo.js'); + + watcher.close(); + }); + }); - it('basic two filesystems', () => { - const vol = Volume.fromJSON({'/foo': 'bar'}); - const vol2 = Volume.fromJSON({'/foo': 'baz'}); - const ufs = new Union(); - ufs.use(vol as any); - ufs.use(vol2 as any); + describe('existsSync()', () => { + it('finds file on real file system', () => { + const ufs = new Union(); - expect(ufs.readFileSync('/foo', 'utf8')).toBe('baz'); - }); + ufs.use(fs).use(Volume.fromJSON({ 'foo.js': '' }, '/tmp') as any); - it('File not found', () => { - const vol = Volume.fromJSON({'/foo': 'bar'}); - const ufs = new Union(); - ufs.use(vol as any); - try { - ufs.readFileSync('/not-found', 'utf8'); - throw Error('This should not throw'); - } catch(err) { - expect(err.code).toBe('ENOENT'); - } - }); + expect(ufs.existsSync(__filename)).toBe(true); + expect(fs.existsSync(__filename)).toBe(true); + expect(ufs.existsSync('/tmp/foo.js')).toBe(true); + }); + }); + + describe('readonly/writeonly', () => { + it('writes to the last vol added', () => { + const vol1 = Volume.fromJSON({}); + const vol2 = Volume.fromJSON({}); + const ufs = new Union() as any; + ufs.use(vol1 as any).use(vol2 as any); + ufs.writeFileSync('/foo', 'bar'); + expect(vol2.readFileSync('/foo', 'utf8')).toEqual('bar'); + }); - it('Method does not exist', () => { - const vol = Volume.fromJSON({'/foo': 'bar'}); - const ufs = new Union(); - vol.readFileSync = undefined as any; - ufs.use(vol as any); - try { - ufs.readFileSync('/foo', 'utf8'); - throw Error('not_this'); - } catch(err) { - expect(err.message).not.toBe('not_this'); - } - }); + it('writes to the latest added non-readonly vol', () => { + const vol1 = Volume.fromJSON({}); + const vol2 = Volume.fromJSON({}); + const ufs = new Union() as any; + ufs.use(vol1 as any).use(vol2 as any, { readonly: true }); + ufs.writeFileSync('/foo', 'bar'); + expect(vol1.readFileSync('/foo', 'utf8')).toEqual('bar'); + }); - describe("watch()", () => { - it("should create a watcher", () => { - const ufs = new Union().use(Volume.fromJSON({"foo.js": "hello test"}, "/tmp") as any); + it('writes to the latest added writeable vol', () => { + const vol1 = Volume.fromJSON({}); + const vol2 = Volume.fromJSON({}); + const ufs = new Union() as any; + ufs.use(vol1 as any).use(vol2 as any, { writeonly: true }); + ufs.writeFileSync('/foo', 'bar'); + expect(vol2.readFileSync('/foo', 'utf8')).toEqual('bar'); + }); - const mockCallback = jest.fn(); - const writtenContent = "hello world"; - const watcher = ufs.watch("/tmp/foo.js", mockCallback); + it('not throw error if write operation attempted with all volumes readonly', () => { + const vol1 = Volume.fromJSON({ '/foo': 'bar' }); + const vol2 = Volume.fromJSON({ '/foo': 'bar' }); + const ufs = new Union() as any; + ufs.use(vol1 as any, { readonly: true }).use(vol2 as any, { readonly: true }); - ufs.writeFileSync("/tmp/foo.js", writtenContent); + expect(() => ufs.writeFileSync('/foo', 'bar')).not.toThrowError(); + }); - expect(mockCallback).toBeCalledTimes(2); - expect(mockCallback).toBeCalledWith('change', '/tmp/foo.js'); + it('not throw error nor return value if read operation attempted with all volumes writeonly', () => { + const vol1 = Volume.fromJSON({ '/foo': 'bar1' }); + const vol2 = Volume.fromJSON({ '/foo': 'bar2' }); + const ufs = new Union() as any; + ufs.use(vol1 as any, { writeonly: true }).use(vol2 as any, { writeonly: true }); - watcher.close(); - }) - }) + expect(ufs.readFileSync('/foo')).toBeUndefined(); + }); + }); + + describe('readdirSync', () => { + it('reads one memfs correctly', () => { + const vol = Volume.fromJSON({ + '/foo/bar': 'bar', + '/foo/baz': 'baz', + }); + const ufs = new Union(); + ufs.use(vol as any); + expect(ufs.readdirSync('/foo')).toEqual(['bar', 'baz']); + }); - describe('existsSync()', () => { - it('finds file on real file system', () => { - const ufs = new Union; + it('reads multiple memfs', () => { + const vol = Volume.fromJSON({ + '/foo/bar': 'bar', + '/foo/baz': 'baz', + }); + const vol2 = Volume.fromJSON({ + '/foo/qux': 'baz', + }); + + const ufs = new Union(); + ufs.use(vol as any); + ufs.use(vol2 as any); + expect(ufs.readdirSync('/foo')).toEqual(['bar', 'baz', 'qux']); + }); - ufs - .use(fs) - .use(Volume.fromJSON({"foo.js": ""}, "/tmp") as any) + it('reads dedupes multiple fss', () => { + const vol = Volume.fromJSON({ + '/foo/bar': 'bar', + '/foo/baz': 'baz', + }); + const vol2 = Volume.fromJSON({ + '/foo/baz': 'not baz', + '/foo/qux': 'baz', + }); + + const ufs = new Union(); + ufs.use(vol as any); + ufs.use(vol2 as any); + expect(ufs.readdirSync('/foo')).toEqual(['bar', 'baz', 'qux']); + }); - expect(ufs.existsSync(__filename)).toBe(true); - expect(fs.existsSync(__filename)).toBe(true); - expect(ufs.existsSync("/tmp/foo.js")).toBe(true); - }); - }); + it('reads other fss when one fails', () => { + const vol = Volume.fromJSON({ + '/foo/bar': 'bar', + '/foo/baz': 'baz', + }); + const vol2 = Volume.fromJSON({ + '/bar/baz': 'not baz', + '/bar/qux': 'baz', + }); + + const ufs = new Union(); + ufs.use(vol as any); + ufs.use(vol2 as any); + expect(ufs.readdirSync('/bar')).toEqual(['baz', 'qux']); + }); - describe("readonly/writeonly", () => { - it('writes to the last vol added', () => { - const vol1 = Volume.fromJSON({}); - const vol2 = Volume.fromJSON({}); - const ufs = new Union() as any; - ufs.use(vol1 as any).use(vol2 as any) - ufs.writeFileSync('/foo', 'bar') - expect(vol2.readFileSync('/foo', 'utf8')).toEqual('bar'); - }) - - it('writes to the latest added non-readonly vol', () => { - const vol1 = Volume.fromJSON({}); - const vol2 = Volume.fromJSON({}); - const ufs = new Union() as any; - ufs.use(vol1 as any).use(vol2 as any, {readonly: true}) - ufs.writeFileSync('/foo', 'bar') - expect(vol1.readFileSync('/foo', 'utf8')).toEqual('bar'); - }) - - it('writes to the latest added writeable vol', () => { - const vol1 = Volume.fromJSON({}); - const vol2 = Volume.fromJSON({}); - const ufs = new Union() as any; - ufs.use(vol1 as any).use(vol2 as any, {writeonly: true}) - ufs.writeFileSync('/foo', 'bar') - expect(vol2.readFileSync('/foo', 'utf8')).toEqual('bar'); - }) - - it('not throw error if write operation attempted with all volumes readonly', () => { - const vol1 = Volume.fromJSON({'/foo': 'bar'}); - const vol2 = Volume.fromJSON({'/foo': 'bar'}); - const ufs = new Union() as any; - ufs.use(vol1 as any, {readonly: true}).use(vol2 as any, {readonly: true}) - - expect(() => ufs.writeFileSync('/foo', 'bar')).not.toThrowError() - }) - - it('not throw error nor return value if read operation attempted with all volumes writeonly', () => { - const vol1 = Volume.fromJSON({'/foo': 'bar1'}); - const vol2 = Volume.fromJSON({'/foo': 'bar2'}); - const ufs = new Union() as any; - ufs.use(vol1 as any, {writeonly: true}).use(vol2 as any, {writeonly: true}) - - expect(ufs.readFileSync('/foo')).toBeUndefined() - }) - }); + it('honors the withFileTypes: true option', () => { + const vol = Volume.fromJSON({ + '/foo/bar': 'bar', + '/foo/zzz': 'zzz', + '/foo/baz': 'baz', + }); + + const ufs = new Union(); + ufs.use(vol as any); + const files = ufs.readdirSync('/foo', { withFileTypes: true }); + expect(files[0]).toBeInstanceOf(createFsFromVolume(vol).Dirent); + expect(files.map(f => f.name)).toEqual(['bar', 'baz', 'zzz']); + }); - describe("readdirSync", () => { - it('reads one memfs correctly', () => { - const vol = Volume.fromJSON({ - '/foo/bar': 'bar', - '/foo/baz': 'baz', - }); - const ufs = new Union(); - ufs.use(vol as any); - expect(ufs.readdirSync("/foo")).toEqual(["bar", "baz"]); - }); - - it('reads multiple memfs', () => { - const vol = Volume.fromJSON({ - '/foo/bar': 'bar', - '/foo/baz': 'baz', - }); - const vol2 = Volume.fromJSON({ - '/foo/qux': 'baz', - }); - - const ufs = new Union(); - ufs.use(vol as any); - ufs.use(vol2 as any); - expect(ufs.readdirSync("/foo")).toEqual(["bar", "baz", "qux"]); - }); - - it('reads dedupes multiple fss', () => { - const vol = Volume.fromJSON({ - '/foo/bar': 'bar', - '/foo/baz': 'baz', - }); - const vol2 = Volume.fromJSON({ - '/foo/baz': 'not baz', - '/foo/qux': 'baz', - }); - - const ufs = new Union(); - ufs.use(vol as any); - ufs.use(vol2 as any); - expect(ufs.readdirSync("/foo")).toEqual(["bar", "baz", "qux"]); - }); - - it("reads other fss when one fails", () => { - const vol = Volume.fromJSON({ - "/foo/bar": "bar", - "/foo/baz": "baz" - }); - const vol2 = Volume.fromJSON({ - "/bar/baz": "not baz", - "/bar/qux": "baz" - }); - - const ufs = new Union(); - ufs.use(vol as any); - ufs.use(vol2 as any); - expect(ufs.readdirSync("/bar")).toEqual(["baz", "qux"]); - }); - - it("honors the withFileTypes: true option", () => { - const vol = Volume.fromJSON({ - '/foo/bar': 'bar', - '/foo/zzz': 'zzz', - '/foo/baz': 'baz' - }); - - const ufs = new Union(); - ufs.use(vol as any); - const files = ufs.readdirSync("/foo", { withFileTypes: true }); - expect(files[0]).toBeInstanceOf(createFsFromVolume(vol).Dirent); - expect(files.map(f => f.name)).toEqual(['bar', 'baz', 'zzz']); - }); - - it("throws error when all fss fail", () => { - const vol = Volume.fromJSON({}); - const vol2 = Volume.fromJSON({}); - - const ufs = new Union(); - ufs.use(vol as any); - ufs.use(vol2 as any); - expect(() => ufs.readdirSync("/bar")).toThrow(); - }); - }); + it('throws error when all fss fail', () => { + const vol = Volume.fromJSON({}); + const vol2 = Volume.fromJSON({}); + + const ufs = new Union(); + ufs.use(vol as any); + ufs.use(vol2 as any); + expect(() => ufs.readdirSync('/bar')).toThrow(); }); - describe('async methods', () => { - it('Basic one file system', done => { - const vol = Volume.fromJSON({'/foo': 'bar'}); - const ufs = new Union(); - ufs.use(vol as any); - ufs.readFile('/foo', 'utf8', (err, data) => { - expect(err).toBe(null); - expect(data).toBe('bar'); - done(); - }); - }); - it('basic two filesystems', () => { - const vol = Volume.fromJSON({'/foo': 'bar'}); - const vol2 = Volume.fromJSON({'/foo': 'baz'}); - const ufs = new Union(); - ufs.use(vol as any); - ufs.use(vol2 as any); - ufs.readFile('/foo', 'utf8', (err, content) => { - expect(content).toBe('baz'); - }); - }); - it('File not found', done => { - const vol = Volume.fromJSON({'/foo': 'bar'}); - const ufs = new Union(); - ufs.use(vol as any); - ufs.readFile('/not-found', 'utf8', (err, data) => { - expect(err?.code).toBe('ENOENT'); - done(); - }); + }); + }); + describe('async methods', () => { + it('Basic one file system', done => { + const vol = Volume.fromJSON({ '/foo': 'bar' }); + const ufs = new Union(); + ufs.use(vol as any); + ufs.readFile('/foo', 'utf8', (err, data) => { + expect(err).toBe(null); + expect(data).toBe('bar'); + done(); + }); + }); + it('basic two filesystems', () => { + const vol = Volume.fromJSON({ '/foo': 'bar' }); + const vol2 = Volume.fromJSON({ '/foo': 'baz' }); + const ufs = new Union(); + ufs.use(vol as any); + ufs.use(vol2 as any); + ufs.readFile('/foo', 'utf8', (err, content) => { + expect(content).toBe('baz'); + }); + }); + it('File not found', done => { + const vol = Volume.fromJSON({ '/foo': 'bar' }); + const ufs = new Union(); + ufs.use(vol as any); + ufs.readFile('/not-found', 'utf8', (err, data) => { + expect(err?.code).toBe('ENOENT'); + done(); + }); + }); + + it('No callback provided', () => { + const vol = Volume.fromJSON({ '/foo': 'bar' }); + const ufs = new Union(); + ufs.use(vol as any); + try { + ufs.stat('/foo2', undefined as any); + } catch (err) { + expect(err).toBeInstanceOf(TypeError); + } + }); + + it('No file systems attached', done => { + const ufs = new Union(); + ufs.stat('/foo2', (err, data) => { + expect(err?.message).toBe('No file systems attached.'); + done(); + }); + }); + + it('callbacks are only called once', done => { + const vol = Volume.fromJSON({ + '/foo/bar': 'bar', + }); + const vol2 = Volume.fromJSON({ + '/foo/bar': 'baz', + }); + + const ufs = new Union(); + ufs.use(vol as any); + ufs.use(vol2 as any); + + const mockCallback = jest.fn(); + ufs.readFile('/foo/bar', 'utf8', () => { + mockCallback(); + expect(mockCallback).toBeCalledTimes(1); + done(); + }); + }); + + describe('readonly/writeonly', () => { + it('writes to the last vol added', done => { + const vol1 = Volume.fromJSON({}); + const vol2 = Volume.fromJSON({}); + const ufs = new Union() as any; + ufs.use(vol1 as any).use(vol2 as any); + ufs.writeFile('/foo', 'bar', (err, res) => { + vol2.readFile('/foo', 'utf8', (err, res) => { + expect(res).toEqual('bar'); + done(); }); + }); + }); - it('No callback provided', () => { - const vol = Volume.fromJSON({'/foo': 'bar'}); - const ufs = new Union(); - ufs.use(vol as any); - try { - ufs.stat('/foo2', undefined as any); - } catch(err) { - expect(err).toBeInstanceOf(TypeError); - } + it('writes to the latest added non-readonly vol', done => { + const vol1 = Volume.fromJSON({}); + const vol2 = Volume.fromJSON({}); + const ufs = new Union() as any; + ufs.use(vol1 as any).use(vol2 as any, { readonly: true }); + ufs.writeFile('/foo', 'bar', (err, res) => { + vol1.readFile('/foo', 'utf8', (err, res) => { + expect(res).toEqual('bar'); + done(); }); + }); + }); - it('No file systems attached', done => { - const ufs = new Union(); - ufs.stat('/foo2', (err, data) => { - expect(err?.message).toBe('No file systems attached.'); - done(); - }); + it('writes to the latest added writeable vol', done => { + const vol1 = Volume.fromJSON({}); + const vol2 = Volume.fromJSON({}); + const ufs = new Union() as any; + ufs.use(vol1 as any).use(vol2 as any, { writeonly: true }); + ufs.writeFile('/foo', 'bar', (err, res) => { + vol2.readFile('/foo', 'utf8', (err, res) => { + expect(res).toEqual('bar'); + done(); }); + }); + }); - it('callbacks are only called once', done => { - const vol = Volume.fromJSON({ - '/foo/bar': 'bar', - }); - const vol2 = Volume.fromJSON({ - '/foo/bar': 'baz', - }); - - const ufs = new Union(); - ufs.use(vol as any); - ufs.use(vol2 as any); - - const mockCallback = jest.fn(); - ufs.readFile("/foo/bar", "utf8", () => { - mockCallback(); - expect(mockCallback).toBeCalledTimes(1); - done(); - }); + it('not throw error if write operation attempted with all volumes readonly', done => { + const vol1 = Volume.fromJSON({ '/foo': 'bar' }); + const vol2 = Volume.fromJSON({ '/foo': 'bar' }); + const ufs = new Union() as any; + ufs.use(vol1 as any, { readonly: true }).use(vol2 as any, { readonly: true }); + ufs.writeFile('/foo', 'bar', (err, res) => { + expect(err).toBeUndefined(); + done(); + }); + }); - }); + it('not throw error nor return value if read operation attempted with all volumes writeonly', done => { + const vol1 = Volume.fromJSON({ '/foo': 'bar1' }); + const vol2 = Volume.fromJSON({ '/foo': 'bar2' }); + const ufs = new Union() as any; + ufs.use(vol1 as any, { writeonly: true }).use(vol2 as any, { writeonly: true }); + + ufs.readFile('/foo', 'utf8', (err, res) => { + expect(err).toBeUndefined(); + expect(res).toBeUndefined(); + done(); + }); + }); + }); + + describe('readdir', () => { + it('reads one memfs correctly', () => { + const vol = Volume.fromJSON({ + '/foo/bar': 'bar', + '/foo/baz': 'baz', + }); + const ufs = new Union(); + ufs.use(vol as any); + ufs.readdir('/foo', (err, files) => { + expect(files).toEqual(['bar', 'baz']); + }); + }); - describe("readonly/writeonly", () => { - it('writes to the last vol added', (done) => { - const vol1 = Volume.fromJSON({}); - const vol2 = Volume.fromJSON({}); - const ufs = new Union() as any; - ufs.use(vol1 as any).use(vol2 as any) - ufs.writeFile('/foo', 'bar', (err, res) => { - vol2.readFile('/foo', 'utf8', (err, res) => { - expect(res).toEqual('bar'); - done() - }) - }); - }) - - it('writes to the latest added non-readonly vol', (done) => { - const vol1 = Volume.fromJSON({}); - const vol2 = Volume.fromJSON({}); - const ufs = new Union() as any; - ufs.use(vol1 as any).use(vol2 as any, {readonly: true}) - ufs.writeFile('/foo', 'bar', (err, res) => { - vol1.readFile('/foo', 'utf8', (err, res) => { - expect(res).toEqual('bar'); - done() - }) - }); - }) - - it('writes to the latest added writeable vol', (done) => { - const vol1 = Volume.fromJSON({}); - const vol2 = Volume.fromJSON({}); - const ufs = new Union() as any; - ufs.use(vol1 as any).use(vol2 as any, {writeonly: true}) - ufs.writeFile('/foo', 'bar', (err, res) => { - vol2.readFile('/foo', 'utf8', (err, res) => { - expect(res).toEqual('bar'); - done() - }) - }); - }) - - it('not throw error if write operation attempted with all volumes readonly', (done) => { - const vol1 = Volume.fromJSON({'/foo': 'bar'}); - const vol2 = Volume.fromJSON({'/foo': 'bar'}); - const ufs = new Union() as any; - ufs.use(vol1 as any, {readonly: true}).use(vol2 as any, {readonly: true}) - ufs.writeFile('/foo', 'bar', (err, res) => { - expect(err).toBeUndefined() - done() - }) - }) - - it('not throw error nor return value if read operation attempted with all volumes writeonly', (done) => { - const vol1 = Volume.fromJSON({'/foo': 'bar1'}); - const vol2 = Volume.fromJSON({'/foo': 'bar2'}); - const ufs = new Union() as any; - ufs.use(vol1 as any, {writeonly: true}).use(vol2 as any, {writeonly: true}) - - ufs.readFile('/foo', 'utf8', (err, res) => { - expect(err).toBeUndefined() - expect(res).toBeUndefined() - done() - }) - }) - }); + it('reads multiple memfs correctly', done => { + const vol = Volume.fromJSON({ + '/foo/bar': 'bar', + '/foo/baz': 'baz', + }); + const vol2 = Volume.fromJSON({ + '/foo/qux': 'baz', + }); + + const ufs = new Union(); + ufs.use(vol as any); + ufs.use(vol2 as any); + ufs.readdir('/foo', (err, files) => { + expect(err).toBeNull(); + expect(files).toEqual(['bar', 'baz', 'qux']); + done(); + }); + }); - describe("readdir", () => { - it('reads one memfs correctly', () => { - const vol = Volume.fromJSON({ - '/foo/bar': 'bar', - '/foo/baz': 'baz' - }); - const ufs = new Union(); - ufs.use(vol as any); - ufs.readdir("/foo", (err, files) => { - expect(files).toEqual(["bar", "baz"]); - }); - }); - - it('reads multiple memfs correctly', done => { - const vol = Volume.fromJSON({ - '/foo/bar': 'bar', - '/foo/baz': 'baz', - }); - const vol2 = Volume.fromJSON({ - '/foo/qux': 'baz', - }); - - const ufs = new Union(); - ufs.use(vol as any); - ufs.use(vol2 as any); - ufs.readdir("/foo", (err, files) => { - expect(err).toBeNull(); - expect(files).toEqual(["bar", "baz", "qux"]); - done(); - }); - }); - - it("reads other fss when one fails", done => { - const vol = Volume.fromJSON({ - "/foo/bar": "bar", - "/foo/baz": "baz" - }); - const vol2 = Volume.fromJSON({ - "/bar/baz": "not baz", - "/bar/qux": "baz" - }); - - const ufs = new Union(); - ufs.use(vol as any); - ufs.use(vol2 as any); - ufs.readdir("/bar", (err, files) => { - expect(err).toBeNull(); - expect(files).toEqual(["baz", "qux"]); - done(); - }); - }); - - it("honors the withFileTypes: true option", done => { - const vol = Volume.fromJSON({ - '/foo/bar': 'bar', - '/foo/zzz': 'zzz', - '/foo/baz': 'baz' - }); - - const ufs = new Union(); - ufs.use(vol as any); - ufs.readdir("/foo", { withFileTypes: true }, (err, files) => { - expect(files[0]).toBeInstanceOf(createFsFromVolume(vol).Dirent); - expect(files.map(f => f.name)).toEqual(['bar', 'baz', 'zzz']); - done(); - }); - }); - - it("throws error when all fss fail", done => { - const vol = Volume.fromJSON({}); - const vol2 = Volume.fromJSON({}); - - const ufs = new Union(); - ufs.use(vol as any); - ufs.use(vol2 as any); - ufs.readdir("/bar", (err, files) => { - expect(err).not.toBeNull(); - done(); - }); - }); - }); + it('reads other fss when one fails', done => { + const vol = Volume.fromJSON({ + '/foo/bar': 'bar', + '/foo/baz': 'baz', + }); + const vol2 = Volume.fromJSON({ + '/bar/baz': 'not baz', + '/bar/qux': 'baz', + }); + + const ufs = new Union(); + ufs.use(vol as any); + ufs.use(vol2 as any); + ufs.readdir('/bar', (err, files) => { + expect(err).toBeNull(); + expect(files).toEqual(['baz', 'qux']); + done(); + }); }); - describe('promise methods', () => { - it('Basic one file system', async () => { - const vol = Volume.fromJSON({'/foo': 'bar'}); - const ufs = new Union(); - ufs.use(vol as any); - await expect(ufs.promises.readFile('/foo', 'utf8')).resolves.toBe('bar'); - }); - it('basic two filesystems', async () => { - const vol = Volume.fromJSON({'/foo': 'bar'}); - const vol2 = Volume.fromJSON({'/foo': 'baz'}); - const ufs = new Union(); - ufs.use(vol as any); - ufs.use(vol2 as any); + it('honors the withFileTypes: true option', done => { + const vol = Volume.fromJSON({ + '/foo/bar': 'bar', + '/foo/zzz': 'zzz', + '/foo/baz': 'baz', + }); + + const ufs = new Union(); + ufs.use(vol as any); + ufs.readdir('/foo', { withFileTypes: true }, (err, files) => { + expect(files[0]).toBeInstanceOf(createFsFromVolume(vol).Dirent); + expect(files.map(f => f.name)).toEqual(['bar', 'baz', 'zzz']); + done(); + }); + }); - await expect(ufs.promises.readFile('/foo', 'utf8')).resolves.toBe('baz'); - }); + it('throws error when all fss fail', done => { + const vol = Volume.fromJSON({}); + const vol2 = Volume.fromJSON({}); + + const ufs = new Union(); + ufs.use(vol as any); + ufs.use(vol2 as any); + ufs.readdir('/bar', (err, files) => { + expect(err).not.toBeNull(); + done(); + }); + }); + }); + }); + describe('promise methods', () => { + it('Basic one file system', async () => { + const vol = Volume.fromJSON({ '/foo': 'bar' }); + const ufs = new Union(); + ufs.use(vol as any); + await expect(ufs.promises.readFile('/foo', 'utf8')).resolves.toBe('bar'); + }); + + it('basic two filesystems', async () => { + const vol = Volume.fromJSON({ '/foo': 'bar' }); + const vol2 = Volume.fromJSON({ '/foo': 'baz' }); + const ufs = new Union(); + ufs.use(vol as any); + ufs.use(vol2 as any); + + await expect(ufs.promises.readFile('/foo', 'utf8')).resolves.toBe('baz'); + }); + + it('File not found', async () => { + const vol = Volume.fromJSON({ '/foo': 'bar' }); + const ufs = new Union(); + ufs.use(vol as any); + await expect(ufs.promises.readFile('/not-found', 'utf8')).rejects.toThrowError('ENOENT'); + }); + + it('Method does not exist', async () => { + const vol = Volume.fromJSON({ '/foo': 'bar' }); + const ufs = new Union(); + vol.promises.readFile = undefined as any; + ufs.use(vol as any); + await expect(ufs.promises.readFile('/foo', 'utf8')).rejects.toThrowError(); + }); + + describe('readonly/writeonly', () => { + it('writes to the last vol added', () => { + const vol1 = Volume.fromJSON({}); + const vol2 = Volume.fromJSON({}); + const ufs = new Union() as any; + ufs.use(vol1 as any).use(vol2 as any); + ufs.writeFileSync('/foo', 'bar'); + expect(vol2.readFileSync('/foo', 'utf8')).toEqual('bar'); + }); - it('File not found', async () => { - const vol = Volume.fromJSON({'/foo': 'bar'}); - const ufs = new Union(); - ufs.use(vol as any); - await expect(ufs.promises.readFile('/not-found', 'utf8')).rejects.toThrowError('ENOENT'); - }); + it('writes to the latest added non-readonly vol', () => { + const vol1 = Volume.fromJSON({}); + const vol2 = Volume.fromJSON({}); + const ufs = new Union() as any; + ufs.use(vol1 as any).use(vol2 as any, { readonly: true }); + ufs.writeFileSync('/foo', 'bar'); + expect(vol1.readFileSync('/foo', 'utf8')).toEqual('bar'); + }); - it('Method does not exist', async () => { - const vol = Volume.fromJSON({'/foo': 'bar'}); - const ufs = new Union(); - vol.promises.readFile = undefined as any; - ufs.use(vol as any); - await expect(ufs.promises.readFile('/foo', 'utf8')).rejects.toThrowError(); - }); + it('writes to the latest added writeable vol', () => { + const vol1 = Volume.fromJSON({}); + const vol2 = Volume.fromJSON({}); + const ufs = new Union() as any; + ufs.use(vol1 as any).use(vol2 as any, { writeonly: true }); + ufs.writeFileSync('/foo', 'bar'); + expect(vol2.readFileSync('/foo', 'utf8')).toEqual('bar'); + }); - describe("readonly/writeonly", () => { - it('writes to the last vol added', () => { - const vol1 = Volume.fromJSON({}); - const vol2 = Volume.fromJSON({}); - const ufs = new Union() as any; - ufs.use(vol1 as any).use(vol2 as any) - ufs.writeFileSync('/foo', 'bar') - expect(vol2.readFileSync('/foo', 'utf8')).toEqual('bar'); - }) - - it('writes to the latest added non-readonly vol', () => { - const vol1 = Volume.fromJSON({}); - const vol2 = Volume.fromJSON({}); - const ufs = new Union() as any; - ufs.use(vol1 as any).use(vol2 as any, {readonly: true}) - ufs.writeFileSync('/foo', 'bar') - expect(vol1.readFileSync('/foo', 'utf8')).toEqual('bar'); - }) - - it('writes to the latest added writeable vol', () => { - const vol1 = Volume.fromJSON({}); - const vol2 = Volume.fromJSON({}); - const ufs = new Union() as any; - ufs.use(vol1 as any).use(vol2 as any, {writeonly: true}) - ufs.writeFileSync('/foo', 'bar') - expect(vol2.readFileSync('/foo', 'utf8')).toEqual('bar'); - }) - - it('not throw error if write operation attempted with all volumes readonly', () => { - const vol1 = Volume.fromJSON({'/foo': 'bar'}); - const vol2 = Volume.fromJSON({'/foo': 'bar'}); - const ufs = new Union() as any; - ufs.use(vol1 as any, {readonly: true}).use(vol2 as any, {readonly: true}) - - expect(() => ufs.writeFileSync('/foo', 'bar')).not.toThrowError() - }) - - it('not throw error nor return value if read operation attempted with all volumes writeonly', () => { - const vol1 = Volume.fromJSON({'/foo': 'bar1'}); - const vol2 = Volume.fromJSON({'/foo': 'bar2'}); - const ufs = new Union() as any; - ufs.use(vol1 as any, {writeonly: true}).use(vol2 as any, {writeonly: true}) - - expect(ufs.readFileSync('/foo')).toBeUndefined() - }) - }); + it('not throw error if write operation attempted with all volumes readonly', () => { + const vol1 = Volume.fromJSON({ '/foo': 'bar' }); + const vol2 = Volume.fromJSON({ '/foo': 'bar' }); + const ufs = new Union() as any; + ufs.use(vol1 as any, { readonly: true }).use(vol2 as any, { readonly: true }); - describe("readdir", () => { - it('reads one memfs correctly', async () => { - const vol = Volume.fromJSON({ - '/foo/bar': 'bar', - '/foo/baz': 'baz', - }); - const ufs = new Union(); - ufs.use(vol as any); - await expect(ufs.promises.readdir("/foo")).resolves.toEqual(["bar", "baz"]); - }); - - it('reads multiple memfs', async () => { - const vol = Volume.fromJSON({ - '/foo/bar': 'bar', - '/foo/baz': 'baz', - }); - const vol2 = Volume.fromJSON({ - '/foo/qux': 'baz', - }); - - const ufs = new Union(); - ufs.use(vol as any); - ufs.use(vol2 as any); - await expect(ufs.promises.readdir("/foo")).resolves.toEqual(["bar", "baz", "qux"]); - }); - - it('reads dedupes multiple fss', async () => { - const vol = Volume.fromJSON({ - '/foo/bar': 'bar', - '/foo/baz': 'baz', - }); - const vol2 = Volume.fromJSON({ - '/foo/baz': 'not baz', - '/foo/qux': 'baz', - }); - - const ufs = new Union(); - ufs.use(vol as any); - ufs.use(vol2 as any); - await expect(ufs.promises.readdir("/foo")).resolves.toEqual(["bar", "baz", "qux"]); - }); - - it("reads other fss when one fails", async () => { - const vol = Volume.fromJSON({ - "/foo/bar": "bar", - "/foo/baz": "baz" - }); - const vol2 = Volume.fromJSON({ - "/bar/baz": "not baz", - "/bar/qux": "baz" - }); - - const ufs = new Union(); - ufs.use(vol as any); - ufs.use(vol2 as any); - await expect(ufs.promises.readdir("/bar")).resolves.toEqual(["baz", "qux"]); - }); - - it("honors the withFileTypes: true option", async () => { - const vol = Volume.fromJSON({ - '/foo/bar': 'bar', - '/foo/zzz': 'zzz', - '/foo/baz': 'baz' - }); - - const ufs = new Union(); - ufs.use(vol as any); - const files = await ufs.promises.readdir("/foo", { withFileTypes: true }); - expect(files[0]).toBeInstanceOf(createFsFromVolume(vol).Dirent); - expect(files.map(f => f.name)).toEqual(['bar', 'baz', 'zzz']); - }); - - it("throws error when all fss fail", async () => { - const vol = Volume.fromJSON({}); - const vol2 = Volume.fromJSON({}); - - const ufs = new Union(); - ufs.use(vol as any); - ufs.use(vol2 as any); - await expect(ufs.promises.readdir("/bar")).rejects.toThrow(); - }); - }); + expect(() => ufs.writeFileSync('/foo', 'bar')).not.toThrowError(); }); - describe("Streams", () => { - it("can create Readable Streams", () => { - const vol = Volume.fromJSON({'/foo': 'bar'}); - const ufs = new Union(); + it('not throw error nor return value if read operation attempted with all volumes writeonly', () => { + const vol1 = Volume.fromJSON({ '/foo': 'bar1' }); + const vol2 = Volume.fromJSON({ '/foo': 'bar2' }); + const ufs = new Union() as any; + ufs.use(vol1 as any, { writeonly: true }).use(vol2 as any, { writeonly: true }); - ufs.use(vol as any).use(fs); + expect(ufs.readFileSync('/foo')).toBeUndefined(); + }); + }); + + describe('readdir', () => { + it('reads one memfs correctly', async () => { + const vol = Volume.fromJSON({ + '/foo/bar': 'bar', + '/foo/baz': 'baz', + }); + const ufs = new Union(); + ufs.use(vol as any); + await expect(ufs.promises.readdir('/foo')).resolves.toEqual(['bar', 'baz']); + }); - expect(ufs.createReadStream).toBeInstanceOf(Function); - expect(vol.createReadStream("/foo")).toHaveProperty("_readableState"); - expect(fs.createReadStream(__filename)).toHaveProperty("_readableState"); + it('reads multiple memfs', async () => { + const vol = Volume.fromJSON({ + '/foo/bar': 'bar', + '/foo/baz': 'baz', + }); + const vol2 = Volume.fromJSON({ + '/foo/qux': 'baz', + }); + + const ufs = new Union(); + ufs.use(vol as any); + ufs.use(vol2 as any); + await expect(ufs.promises.readdir('/foo')).resolves.toEqual(['bar', 'baz', 'qux']); + }); - expect(ufs.createReadStream("/foo")).toHaveProperty("_readableState"); - expect(ufs.createReadStream(__filename)).toHaveProperty("_readableState"); - }); + it('reads dedupes multiple fss', async () => { + const vol = Volume.fromJSON({ + '/foo/bar': 'bar', + '/foo/baz': 'baz', + }); + const vol2 = Volume.fromJSON({ + '/foo/baz': 'not baz', + '/foo/qux': 'baz', + }); + + const ufs = new Union(); + ufs.use(vol as any); + ufs.use(vol2 as any); + await expect(ufs.promises.readdir('/foo')).resolves.toEqual(['bar', 'baz', 'qux']); + }); + + it('reads other fss when one fails', async () => { + const vol = Volume.fromJSON({ + '/foo/bar': 'bar', + '/foo/baz': 'baz', + }); + const vol2 = Volume.fromJSON({ + '/bar/baz': 'not baz', + '/bar/qux': 'baz', + }); + + const ufs = new Union(); + ufs.use(vol as any); + ufs.use(vol2 as any); + await expect(ufs.promises.readdir('/bar')).resolves.toEqual(['baz', 'qux']); + }); + + it('honors the withFileTypes: true option', async () => { + const vol = Volume.fromJSON({ + '/foo/bar': 'bar', + '/foo/zzz': 'zzz', + '/foo/baz': 'baz', + }); + + const ufs = new Union(); + ufs.use(vol as any); + const files = await ufs.promises.readdir('/foo', { withFileTypes: true }); + expect(files[0]).toBeInstanceOf(createFsFromVolume(vol).Dirent); + expect(files.map(f => f.name)).toEqual(['bar', 'baz', 'zzz']); + }); + + it('throws error when all fss fail', async () => { + const vol = Volume.fromJSON({}); + const vol2 = Volume.fromJSON({}); + + const ufs = new Union(); + ufs.use(vol as any); + ufs.use(vol2 as any); + await expect(ufs.promises.readdir('/bar')).rejects.toThrow(); + }); + }); + }); + + describe('Streams', () => { + it('can create Readable Streams', () => { + const vol = Volume.fromJSON({ '/foo': 'bar' }); + const ufs = new Union(); + + ufs.use(vol as any).use(fs); + + expect(ufs.createReadStream).toBeInstanceOf(Function); + expect(vol.createReadStream('/foo')).toHaveProperty('_readableState'); + expect(fs.createReadStream(__filename)).toHaveProperty('_readableState'); + + expect(ufs.createReadStream('/foo')).toHaveProperty('_readableState'); + expect(ufs.createReadStream(__filename)).toHaveProperty('_readableState'); + }); - it("can create Writable Streams", () => { - const vol = Volume.fromJSON({'/foo': 'bar'}); - const ufs = new Union(); - const realFile = __filename+".test" - ufs.use(vol as any).use(fs); + it('can create Writable Streams', () => { + const vol = Volume.fromJSON({ '/foo': 'bar' }); + const ufs = new Union(); + const realFile = __filename + '.test'; + ufs.use(vol as any).use(fs); - expect(ufs.createWriteStream).toBeInstanceOf(Function); - expect(vol.createWriteStream("/foo")).toHaveProperty("_writableState"); - expect(fs.createWriteStream(realFile)).toHaveProperty("_writableState"); + expect(ufs.createWriteStream).toBeInstanceOf(Function); + expect(vol.createWriteStream('/foo')).toHaveProperty('_writableState'); + expect(fs.createWriteStream(realFile)).toHaveProperty('_writableState'); - expect(ufs.createWriteStream("/foo")).toHaveProperty("_writableState"); - expect(ufs.createWriteStream(realFile)).toHaveProperty("_writableState"); + expect(ufs.createWriteStream('/foo')).toHaveProperty('_writableState'); + expect(ufs.createWriteStream(realFile)).toHaveProperty('_writableState'); - ufs.unlinkSync(realFile); - }) - }) + ufs.unlinkSync(realFile); + }); }); + }); }); diff --git a/src/fs.ts b/src/fs.ts index 47a88e44..efd71f3f 100644 --- a/src/fs.ts +++ b/src/fs.ts @@ -1,80 +1,80 @@ -import { Writable, Readable } from "stream"; -import * as fs from "fs"; +import { Writable, Readable } from 'stream'; +import * as fs from 'fs'; type FSMethods = - | "renameSync" - | "ftruncateSync" - | "truncateSync" - | "chownSync" - | "fchownSync" - | "lchownSync" - | "chmodSync" - | "fchmodSync" - | "lchmodSync" - | "statSync" - | "lstatSync" - | "fstatSync" - | "linkSync" - | "symlinkSync" - | "readlinkSync" - | "realpathSync" - | "unlinkSync" - | "rmdirSync" - | "mkdirSync" - | "readdirSync" - | "closeSync" - | "openSync" - | "utimesSync" - | "futimesSync" - | "fsyncSync" - | "writeSync" - | "readSync" - | "readFileSync" - | "writeFileSync" - | "appendFileSync" - | "existsSync" - | "accessSync" - | "createReadStream" - | "createWriteStream" - | "watchFile" - | "unwatchFile" - | "watch" - | "rename" - | "ftruncate" - | "truncate" - | "chown" - | "fchown" - | "lchown" - | "chmod" - | "fchmod" - | "lchmod" - | "stat" - | "lstat" - | "fstat" - | "link" - | "symlink" - | "readlink" - | "realpath" - | "unlink" - | "rmdir" - | "mkdir" - | "readdir" - | "close" - | "open" - | "utimes" - | "futimes" - | "fsync" - | "write" - | "read" - | "readFile" - | "writeFile" - | "appendFile" - | "exists" - | "access"; + | 'renameSync' + | 'ftruncateSync' + | 'truncateSync' + | 'chownSync' + | 'fchownSync' + | 'lchownSync' + | 'chmodSync' + | 'fchmodSync' + | 'lchmodSync' + | 'statSync' + | 'lstatSync' + | 'fstatSync' + | 'linkSync' + | 'symlinkSync' + | 'readlinkSync' + | 'realpathSync' + | 'unlinkSync' + | 'rmdirSync' + | 'mkdirSync' + | 'readdirSync' + | 'closeSync' + | 'openSync' + | 'utimesSync' + | 'futimesSync' + | 'fsyncSync' + | 'writeSync' + | 'readSync' + | 'readFileSync' + | 'writeFileSync' + | 'appendFileSync' + | 'existsSync' + | 'accessSync' + | 'createReadStream' + | 'createWriteStream' + | 'watchFile' + | 'unwatchFile' + | 'watch' + | 'rename' + | 'ftruncate' + | 'truncate' + | 'chown' + | 'fchown' + | 'lchown' + | 'chmod' + | 'fchmod' + | 'lchmod' + | 'stat' + | 'lstat' + | 'fstat' + | 'link' + | 'symlink' + | 'readlink' + | 'realpath' + | 'unlink' + | 'rmdir' + | 'mkdir' + | 'readdir' + | 'close' + | 'open' + | 'utimes' + | 'futimes' + | 'fsync' + | 'write' + | 'read' + | 'readFile' + | 'writeFile' + | 'appendFile' + | 'exists' + | 'access'; type FS = Pick; export interface IFS extends FS { - WriteStream: (typeof Writable) | (new (...args: any[]) => Writable); - ReadStream: (typeof Readable) | (new (...args: any[]) => Readable); + WriteStream: typeof Writable | (new (...args: any[]) => Writable); + ReadStream: typeof Readable | (new (...args: any[]) => Readable); } diff --git a/src/index.ts b/src/index.ts index c09fa332..acdba9fd 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,11 +1,11 @@ -import {Union as _Union} from "./union"; -import {IFS} from "./fs"; +import { Union as _Union } from './union'; +import { IFS } from './fs'; export interface IUnionFs extends IFS { - use: (...args: Parameters<_Union['use']>) => this + use: (...args: Parameters<_Union['use']>) => this; } -export const Union = _Union as any as (new () => IUnionFs); +export const Union = (_Union as any) as new () => IUnionFs; -export const ufs = (new _Union) as any as IUnionFs; +export const ufs = (new _Union() as any) as IUnionFs; export default ufs; diff --git a/src/lists.ts b/src/lists.ts index 10ec5161..1d683fb2 100644 --- a/src/lists.ts +++ b/src/lists.ts @@ -1,117 +1,117 @@ export const fsSyncMethodsWriteonly = [ - "appendFileSync", - "chmodSync", - "chownSync", - "closeSync", - "copyFileSync", - "createWriteStream", - "fchmodSync", - "fchownSync", - "fdatasyncSync", - "fsyncSync", - "futimesSync", - "lchmodSync", - "lchownSync", - "linkSync", - "lstatSync", - "mkdirpSync", - "mkdirSync", - "mkdtempSync", - "renameSync", - "rmdirSync", - "symlinkSync", - "truncateSync", - "unlinkSync", - "utimesSync", - "writeFileSync", - "writeSync" + 'appendFileSync', + 'chmodSync', + 'chownSync', + 'closeSync', + 'copyFileSync', + 'createWriteStream', + 'fchmodSync', + 'fchownSync', + 'fdatasyncSync', + 'fsyncSync', + 'futimesSync', + 'lchmodSync', + 'lchownSync', + 'linkSync', + 'lstatSync', + 'mkdirpSync', + 'mkdirSync', + 'mkdtempSync', + 'renameSync', + 'rmdirSync', + 'symlinkSync', + 'truncateSync', + 'unlinkSync', + 'utimesSync', + 'writeFileSync', + 'writeSync', ] as const; export const fsSyncMethodsReadonly = [ - "accessSync", - "createReadStream", - "existsSync", - "fstatSync", - "ftruncateSync", - "openSync", - "readdirSync", - "readFileSync", - "readlinkSync", - "readSync", - "realpathSync", - "statSync" + 'accessSync', + 'createReadStream', + 'existsSync', + 'fstatSync', + 'ftruncateSync', + 'openSync', + 'readdirSync', + 'readFileSync', + 'readlinkSync', + 'readSync', + 'realpathSync', + 'statSync', ] as const; export const fsAsyncMethodsReadonly = [ - "access", - "exists", - "fstat", - "open", - "read", - "readdir", - "readFile", - "readlink", - "realpath", - "unwatchFile", - "watch", - "watchFile" + 'access', + 'exists', + 'fstat', + 'open', + 'read', + 'readdir', + 'readFile', + 'readlink', + 'realpath', + 'unwatchFile', + 'watch', + 'watchFile', ] as const; export const fsAsyncMethodsWriteonly = [ - "appendFile", - "chmod", - "chown", - "close", - "copyFile", - "fchmod", - "fchown", - "fdatasync", - "fsync", - "ftruncate", - "futimes", - "lchmod", - "lchown", - "link", - "lstat", - "mkdir", - "mkdirp", - "mkdtemp", - "rename", - "rmdir", - "stat", - "symlink", - "truncate", - "unlink", - "utimes", - "write", - "writeFile" + 'appendFile', + 'chmod', + 'chown', + 'close', + 'copyFile', + 'fchmod', + 'fchown', + 'fdatasync', + 'fsync', + 'ftruncate', + 'futimes', + 'lchmod', + 'lchown', + 'link', + 'lstat', + 'mkdir', + 'mkdirp', + 'mkdtemp', + 'rename', + 'rmdir', + 'stat', + 'symlink', + 'truncate', + 'unlink', + 'utimes', + 'write', + 'writeFile', ] as const; export const fsPromiseMethodsReadonly = [ - "access", - "open", - "opendir", - "readdir", - "readFile", - "readlink", - "realpath" + 'access', + 'open', + 'opendir', + 'readdir', + 'readFile', + 'readlink', + 'realpath', ] as const; export const fsPromiseMethodsWriteonly = [ - "appendFile", - "chmod", - "chown", - "copyFile", - "lchmod", - "lchown", - "link", - "lstat", - "mkdir", - "mkdtemp", - "rename", - "rmdir", - "stat", - "symlink", - "truncate", - "unlink", - "utimes", - "writeFile", + 'appendFile', + 'chmod', + 'chown', + 'copyFile', + 'lchmod', + 'lchown', + 'link', + 'lstat', + 'mkdir', + 'mkdtemp', + 'rename', + 'rmdir', + 'stat', + 'symlink', + 'truncate', + 'unlink', + 'utimes', + 'writeFile', ] as const; diff --git a/src/union.ts b/src/union.ts index 276bfe1a..da9d0a51 100644 --- a/src/union.ts +++ b/src/union.ts @@ -1,522 +1,582 @@ -import { FSWatcher, Dirent } from "fs"; -import {IFS} from "./fs"; -import {Readable, Writable} from 'stream'; -import {fsSyncMethodsWriteonly, fsSyncMethodsReadonly, fsAsyncMethodsWriteonly, fsAsyncMethodsReadonly, fsPromiseMethodsWriteonly, fsPromiseMethodsReadonly} from './lists' +import { FSWatcher, Dirent } from 'fs'; +import { IFS } from './fs'; +import { Readable, Writable } from 'stream'; +import { + fsSyncMethodsWriteonly, + fsSyncMethodsReadonly, + fsAsyncMethodsWriteonly, + fsAsyncMethodsReadonly, + fsPromiseMethodsWriteonly, + fsPromiseMethodsReadonly, +} from './lists'; export interface IUnionFsError extends Error { - prev?: IUnionFsError | null, + prev?: IUnionFsError | null; } type readdirEntry = string | Buffer | Dirent; const SPECIAL_METHODS = new Set([ - "existsSync", - "readdir", - "readdirSync", - "createReadStream", - "createWriteStream", - "watch", - "watchFile", - "unwatchFile" + 'existsSync', + 'readdir', + 'readdirSync', + 'createReadStream', + 'createWriteStream', + 'watch', + 'watchFile', + 'unwatchFile', ]); -const createFSProxy = (watchers: FSWatcher[]) => new Proxy({}, { - get(_obj, property) { +const createFSProxy = (watchers: FSWatcher[]) => + new Proxy( + {}, + { + get(_obj, property) { const funcCallers: Array<[FSWatcher, Function]> = []; let prop: Function | undefined; for (const watcher of watchers) { - prop = watcher[property]; - // if we're a function we wrap it in a bigger caller; - if (typeof prop === "function") { - funcCallers.push([ watcher, prop ]); - } + prop = watcher[property]; + // if we're a function we wrap it in a bigger caller; + if (typeof prop === 'function') { + funcCallers.push([watcher, prop]); + } } if (funcCallers.length) { - return (...args) => { - for (const [ watcher, func ] of funcCallers) { - func.apply(watcher, args); - } + return (...args) => { + for (const [watcher, func] of funcCallers) { + func.apply(watcher, args); } + }; } else { - return prop; + return prop; } - } -}); + }, + }, + ); export type VolOptions = { - readonly?: boolean - writeonly?: boolean -} - -type SyncMethodNames = typeof fsSyncMethodsReadonly[number] & typeof fsSyncMethodsWriteonly[number] -type ASyncMethodNames = typeof fsSyncMethodsReadonly[number] & typeof fsAsyncMethodsWriteonly[number] -type PromiseMethodNames = typeof fsPromiseMethodsReadonly[number] & typeof fsPromiseMethodsWriteonly[number] -type FSMethod = (args: any) => any + readonly?: boolean; + writeonly?: boolean; +}; + +type SyncMethodNames = typeof fsSyncMethodsReadonly[number] & typeof fsSyncMethodsWriteonly[number]; +type ASyncMethodNames = typeof fsSyncMethodsReadonly[number] & typeof fsAsyncMethodsWriteonly[number]; +type PromiseMethodNames = typeof fsPromiseMethodsReadonly[number] & typeof fsPromiseMethodsWriteonly[number]; +type FSMethod = (args: any) => any; type FSMethodStack = { - sync: { - [K in SyncMethodNames]: FSMethod | undefined - }, - async: { - [K in ASyncMethodNames]: FSMethod | undefined; - }, - promise: { - [K in PromiseMethodNames]: FSMethod | undefined; - }, -} + sync: { + [K in SyncMethodNames]: FSMethod | undefined; + }; + async: { + [K in ASyncMethodNames]: FSMethod | undefined; + }; + promise: { + [K in PromiseMethodNames]: FSMethod | undefined; + }; +}; /** * Union object represents a stack of filesystems */ export class Union { + private fss: [IFS, VolOptions, FSMethodStack][] = []; - private fss: [IFS, VolOptions, FSMethodStack][] = []; + public ReadStream: typeof Readable | (new (...args: any[]) => Readable) = Readable; + public WriteStream: typeof Writable | (new (...args: any[]) => Writable) = Writable; - public ReadStream: (typeof Readable) | (new (...args: any[]) => Readable) = Readable; - public WriteStream: (typeof Writable) | (new (...args: any[]) => Writable) = Writable; + private promises: {} = {}; - private promises: {} = {}; - - constructor() { - for(let method of [...fsSyncMethodsReadonly, ...fsSyncMethodsWriteonly]) { - if (!SPECIAL_METHODS.has(method)) { // check we don't already have a property for this method - this[method] = (...args) => this.syncMethod(method, args); - } - } - for(let method of [...fsAsyncMethodsReadonly, ...fsAsyncMethodsWriteonly]) { - if (!SPECIAL_METHODS.has(method)) { // check we don't already have a property for this method - this[method] = (...args) => this.asyncMethod(method, args); - } - } + constructor() { + for (let method of [...fsSyncMethodsReadonly, ...fsSyncMethodsWriteonly]) { + if (!SPECIAL_METHODS.has(method)) { + // check we don't already have a property for this method + this[method] = (...args) => this.syncMethod(method, args); + } + } + for (let method of [...fsAsyncMethodsReadonly, ...fsAsyncMethodsWriteonly]) { + if (!SPECIAL_METHODS.has(method)) { + // check we don't already have a property for this method + this[method] = (...args) => this.asyncMethod(method, args); + } + } - for(let method of [...fsPromiseMethodsReadonly, ...fsPromiseMethodsWriteonly]) { - if(method ==='readdir') { - this.promises[method] = this.readdirPromise; + for (let method of [...fsPromiseMethodsReadonly, ...fsPromiseMethodsWriteonly]) { + if (method === 'readdir') { + this.promises[method] = this.readdirPromise; - continue; - } + continue; + } - this.promises[method] = (...args) => this.promiseMethod(method, args); - } - - for (let method of SPECIAL_METHODS.values()) { - // bind special methods to support - // const { method } = ufs; - this[method] = this[method].bind(this); - } + this.promises[method] = (...args) => this.promiseMethod(method, args); } - public unwatchFile = (...args) => { - throw new Error("unwatchFile is not supported, please use watchFile"); + for (let method of SPECIAL_METHODS.values()) { + // bind special methods to support + // const { method } = ufs; + this[method] = this[method].bind(this); } - - public watch = (...args) => { - const watchers: FSWatcher[] = []; - for (const [fs, {writeonly}] of this.fss) { - if (writeonly) continue; - try { - const watcher = fs.watch.apply(fs, args); - watchers.push(watcher); - } catch (e) { - // dunno what to do here... - } - } - - // return a proxy to call functions on these props - return createFSProxy(watchers); + } + + public unwatchFile = (...args) => { + throw new Error('unwatchFile is not supported, please use watchFile'); + }; + + public watch = (...args) => { + const watchers: FSWatcher[] = []; + for (const [fs, { writeonly }] of this.fss) { + if (writeonly) continue; + try { + const watcher = fs.watch.apply(fs, args); + watchers.push(watcher); + } catch (e) { + // dunno what to do here... + } } - public watchFile = (...args) => { - for (const [fs, {writeonly}] of this.fss) { - if (writeonly) continue; - try { - fs.watchFile.apply(fs, args); - } catch (e) { - // dunno what to do here... - } - } + // return a proxy to call functions on these props + return createFSProxy(watchers); + }; + + public watchFile = (...args) => { + for (const [fs, { writeonly }] of this.fss) { + if (writeonly) continue; + try { + fs.watchFile.apply(fs, args); + } catch (e) { + // dunno what to do here... + } } - - public existsSync = (path: string) => { - for (const [fs, {writeonly}] of this.fss) { - if (writeonly) continue; - try { - if(fs.existsSync(path)) { - return true - } - } catch (e) { - // ignore - } + }; + + public existsSync = (path: string) => { + for (const [fs, { writeonly }] of this.fss) { + if (writeonly) continue; + try { + if (fs.existsSync(path)) { + return true; } + } catch (e) { + // ignore + } + } - return false; - }; - - public readdir = (...args): void => { - let lastarg = args.length - 1; - let cb = args[lastarg]; - if(typeof cb !== 'function') { - cb = null; - lastarg++; - } - - let lastError: IUnionFsError | null = null; - let result = new Map(); - const iterate = (i = 0, error?: IUnionFsError | null) => { - if(error) { - error.prev = lastError; - lastError = error; - } - - // Already tried all file systems, return the last error. - if(i >= this.fss.length) { // last one - if(cb) { - cb(error || Error('No file systems attached.')); - }; - return; - } + return false; + }; - // Replace `callback` with our intermediate function. - args[lastarg] = (err, resArg: readdirEntry[]) => { - if(result.size === 0 && err) { - return iterate(i + 1, err); - } - if(resArg) { - for (const res of resArg) { - result.set(this.pathFromReaddirEntry(res), res); - } - } - - if (i === this.fss.length - 1) { - return cb(null, this.sortedArrayFromReaddirResult(result)); - } else { - return iterate(i + 1, error); - } - }; - - const j = this.fss.length - i - 1; - const [fs, {writeonly}] = this.fss[j]; - const func = fs.readdir; - - if(!func) iterate(i + 1, Error('Method not supported: readdir')); - else if(writeonly) iterate(i + 1, Error(`Writeonly enabled for vol '${i}': readdir`)); - else func.apply(fs, args); - }; - iterate(); - }; + public readdir = (...args): void => { + let lastarg = args.length - 1; + let cb = args[lastarg]; + if (typeof cb !== 'function') { + cb = null; + lastarg++; + } - public readdirSync = (...args): Array => { - let lastError: IUnionFsError | null = null; - let result = new Map(); - for(let i = this.fss.length - 1; i >= 0; i--) { - const [fs, {writeonly}] = this.fss[i]; - if (writeonly) continue; - try { - if(!fs.readdirSync) throw Error(`Method not supported: "readdirSync" with args "${args}"`); - for (const res of fs.readdirSync.apply(fs, args)) { - result.set(this.pathFromReaddirEntry(res), res); - } - } catch(err) { - err.prev = lastError; - lastError = err; - if(result.size === 0 && !i) { // last one - throw err; - } else { - // Ignore error... - // continue; - } - } + let lastError: IUnionFsError | null = null; + let result = new Map(); + const iterate = (i = 0, error?: IUnionFsError | null) => { + if (error) { + error.prev = lastError; + lastError = error; + } + + // Already tried all file systems, return the last error. + if (i >= this.fss.length) { + // last one + if (cb) { + cb(error || Error('No file systems attached.')); } - return this.sortedArrayFromReaddirResult(result); - }; + return; + } - public readdirPromise = async (...args): Promise> => { - let lastError: IUnionFsError | null = null; - let result = new Map(); - for(let i = this.fss.length - 1; i >= 0; i--) { - const [fs, {writeonly}] = this.fss[i]; - if (writeonly) continue; - try { - if(!fs.promises || !fs.promises.readdir) throw Error(`Method not supported: "readdirSync" with args "${args}"`); - for (const res of await fs.promises.readdir.apply(fs, args)) { - result.set(this.pathFromReaddirEntry(res), res); - } - } catch(err) { - err.prev = lastError; - lastError = err; - if(result.size === 0 && !i) { // last one - throw err; - } else { - // Ignore error... - // continue; - } - } + // Replace `callback` with our intermediate function. + args[lastarg] = (err, resArg: readdirEntry[]) => { + if (result.size === 0 && err) { + return iterate(i + 1, err); } - return this.sortedArrayFromReaddirResult(result); - }; - - private pathFromReaddirEntry = (readdirEntry: readdirEntry): string => { - if (readdirEntry instanceof Buffer || typeof readdirEntry === 'string') { - return String(readdirEntry); + if (resArg) { + for (const res of resArg) { + result.set(this.pathFromReaddirEntry(res), res); + } } - return readdirEntry.name; - }; - private sortedArrayFromReaddirResult = (readdirResult: Map): readdirEntry[] => { - const array: readdirEntry[] = []; - for (const key of Array.from(readdirResult.keys()).sort()) { - const value = readdirResult.get(key); - if (value !== undefined) array.push(value); + if (i === this.fss.length - 1) { + return cb(null, this.sortedArrayFromReaddirResult(result)); + } else { + return iterate(i + 1, error); } - return array - }; + }; - public createReadStream = (path: string) => { - let lastError = null; - for (const [fs, {writeonly}] of this.fss) { - if (writeonly) continue; - try { - if(!fs.createReadStream) throw Error(`Method not supported: "createReadStream"`); + const j = this.fss.length - i - 1; + const [fs, { writeonly }] = this.fss[j]; + const func = fs.readdir; - if (fs.existsSync && !fs.existsSync(path)) { - throw new Error(`file "${path}" does not exists`); - } - - const stream = fs.createReadStream(path); - if (!stream) { - throw new Error("no valid stream") - } - this.ReadStream = fs.ReadStream; - - return stream; - } - catch (err) { - lastError = err; - } + if (!func) iterate(i + 1, Error('Method not supported: readdir')); + else if (writeonly) iterate(i + 1, Error(`Writeonly enabled for vol '${i}': readdir`)); + else func.apply(fs, args); + }; + iterate(); + }; + + public readdirSync = (...args): Array => { + let lastError: IUnionFsError | null = null; + let result = new Map(); + for (let i = this.fss.length - 1; i >= 0; i--) { + const [fs, { writeonly }] = this.fss[i]; + if (writeonly) continue; + try { + if (!fs.readdirSync) throw Error(`Method not supported: "readdirSync" with args "${args}"`); + for (const res of fs.readdirSync.apply(fs, args)) { + result.set(this.pathFromReaddirEntry(res), res); } - - throw lastError; - } - - public createWriteStream = (path: string) => { - let lastError = null; - for (const [fs, {readonly}] of this.fss) { - if (readonly) continue; - try { - if(!fs.createWriteStream) throw Error(`Method not supported: "createWriteStream"`); - - fs.statSync(path); //we simply stat first to exit early for mocked fs'es - //TODO which filesystem to write to? - const stream = fs.createWriteStream(path); - if (!stream) { - throw new Error("no valid stream") - } - this.WriteStream = fs.WriteStream; - - return stream; - } - catch (err) { - lastError = err; - } + } catch (err) { + err.prev = lastError; + lastError = err; + if (result.size === 0 && !i) { + // last one + throw err; + } else { + // Ignore error... + // continue; } - - throw lastError; + } } - - /** - * Adds a filesystem to the list of filesystems in the union - * The new filesystem object is added as the last filesystem used - * when searching for a file. - * - * @param fs the filesystem interface to be added to the queue of FS's - * @returns this instance of a unionFS - */ - use(fs: IFS, options: VolOptions = {}): this { - this.fss.push([fs, options, this.createMethods(fs, options)]); - return this; - } - - /** - * At the time of the [[use]] call, we create our sync, async and promise methods - * for performance reasons - * - * @param fs - * @param options - */ - private createMethods(fs: IFS, options: VolOptions): FSMethodStack { - const noop = undefined - const createFunc = (method: string) => { - if (!fs[method]) return (...args: any[]) => { throw new Error(`Method not supported: "${method}" with args "${args}"`); }; - return (...args: any[]) => fs[method as string].apply(fs, args); + return this.sortedArrayFromReaddirResult(result); + }; + + public readdirPromise = async (...args): Promise> => { + let lastError: IUnionFsError | null = null; + let result = new Map(); + for (let i = this.fss.length - 1; i >= 0; i--) { + const [fs, { writeonly }] = this.fss[i]; + if (writeonly) continue; + try { + if (!fs.promises || !fs.promises.readdir) + throw Error(`Method not supported: "readdirSync" with args "${args}"`); + for (const res of await fs.promises.readdir.apply(fs, args)) { + result.set(this.pathFromReaddirEntry(res), res); } - switch (true) { - case options.readonly: - return { - sync: { - ...fsSyncMethodsReadonly.reduce((acc, method) => {acc[method] = createFunc(method); return acc;}, {}), - ...fsSyncMethodsWriteonly.reduce((acc, method) => {acc[method] = noop; return acc;}, {}), - }, - async: { - ...fsAsyncMethodsReadonly.reduce((acc, method) => {acc[method] = createFunc(method); return acc;}, {}), - ...fsAsyncMethodsWriteonly.reduce((acc, method) => {acc[method] = noop; return acc;}, {}), - }, - promise: { - ...fsPromiseMethodsReadonly.reduce((acc, method) => { - const promises = fs.promises; - if (!promises || !promises[method]) { - acc[method] = (...args: any) => { - throw Error(`Promise of method not supported: "${String(method)}" with args "${args}"`); - } - return acc - } - acc[method] = (...args: any) => promises[method as string].apply(fs, args); - return acc; - }, {}), - ...fsPromiseMethodsWriteonly.reduce((acc, method) => {acc[method] = noop; return acc;}, {}), - }, - } - case options.writeonly: - return { - sync: { - ...fsSyncMethodsReadonly.reduce((acc, method) => {acc[method] = noop; return acc;}, {}), - ...fsSyncMethodsWriteonly.reduce((acc, method) => {acc[method] = createFunc(method); return acc;}, {}), - }, - async: { - ...fsAsyncMethodsReadonly.reduce((acc, method) => {acc[method] = noop; return acc;}, {}), - ...fsAsyncMethodsWriteonly.reduce((acc, method) => {acc[method] = createFunc(method); return acc;}, {}), - }, - promise: { - ...fsPromiseMethodsReadonly.reduce((acc, method) => {acc[method] = noop; return acc;}, {}), - ...fsPromiseMethodsWriteonly.reduce((acc, method) => { - const promises = fs.promises; - if (!promises || !promises[method]) { - acc[method] = (...args: any) => { - throw Error(`Promise of method not supported: "${String(method)}" with args "${args}"`); - } - return acc - } - acc[method] = (...args: any) => promises[method as string].apply(fs, args); - return acc; - }, {}), - }, - } - default: - return { - sync: { - ...fsSyncMethodsReadonly.reduce((acc, method) => {acc[method] = createFunc(method); return acc;}, {}), - ...fsSyncMethodsWriteonly.reduce((acc, method) => {acc[method] = createFunc(method); return acc;}, {}), - }, - async: { - ...fsAsyncMethodsReadonly.reduce((acc, method) => {acc[method] = createFunc(method); return acc;}, {}), - ...fsAsyncMethodsWriteonly.reduce((acc, method) => {acc[method] = createFunc(method); return acc;}, {}), - }, - promise: { - ...fsPromiseMethodsReadonly.reduce((acc, method) => { - const promises = fs.promises; - if (!promises || !promises[method]) { - acc[method] = (...args: any) => { - throw Error(`Promise of method not supported: "${String(method)}" with args "${args}"`); - } - return acc - } - acc[method] = (...args: any) => promises[method as string].apply(fs, args); - return acc; - }, {}), - ...fsPromiseMethodsWriteonly.reduce((acc, method) => { - const promises = fs.promises; - if (!promises || !promises[method]) { - acc[method] = (...args: any) => { - throw Error(`Promise of method not supported: "${String(method)}" with args "${args}"`); - } - return acc - } - acc[method] = (...args: any) => promises[method as string].apply(fs, args); - return acc; - }, {}), - }, - } + } catch (err) { + err.prev = lastError; + lastError = err; + if (result.size === 0 && !i) { + // last one + throw err; + } else { + // Ignore error... + // continue; } + } } + return this.sortedArrayFromReaddirResult(result); + }; - private syncMethod(method: string, args: any[]) { - if (!this.fss.length) throw new Error('No file systems attached') - let lastError: IUnionFsError | null = null; - for(let i = this.fss.length - 1; i >= 0; i--) { - const [_fs, _options, methodStack] = this.fss[i]; - try { - if (!methodStack['sync'][method]) continue; - return methodStack['sync'][method](...args) - } catch(err) { - err.prev = lastError; - lastError = err; - if(!i) { // last one - throw err; - } else { - // Ignore error... - // continue; - } - } - } + private pathFromReaddirEntry = (readdirEntry: readdirEntry): string => { + if (readdirEntry instanceof Buffer || typeof readdirEntry === 'string') { + return String(readdirEntry); } - - private asyncMethod(method: string, args: any[]) { - let lastarg = args.length - 1; - let cb = args[lastarg]; - if(typeof cb !== 'function') { - cb = null; - lastarg++; + return readdirEntry.name; + }; + + private sortedArrayFromReaddirResult = (readdirResult: Map): readdirEntry[] => { + const array: readdirEntry[] = []; + for (const key of Array.from(readdirResult.keys()).sort()) { + const value = readdirResult.get(key); + if (value !== undefined) array.push(value); + } + return array; + }; + + public createReadStream = (path: string) => { + let lastError = null; + for (const [fs, { writeonly }] of this.fss) { + if (writeonly) continue; + try { + if (!fs.createReadStream) throw Error(`Method not supported: "createReadStream"`); + + if (fs.existsSync && !fs.existsSync(path)) { + throw new Error(`file "${path}" does not exists`); } - let lastError: IUnionFsError | null = null; - const iterate = (i = 0, err?: IUnionFsError) => { - if(err) { - err.prev = lastError; - lastError = err; - } + const stream = fs.createReadStream(path); + if (!stream) { + throw new Error('no valid stream'); + } + this.ReadStream = fs.ReadStream; - // Already tried all file systems, return the last error. - if(i >= this.fss.length) { // last one - if(cb) cb(err ?? (!this.fss.length ? new Error('No file systems attached.') : undefined)); - return; - } + return stream; + } catch (err) { + lastError = err; + } + } - // Replace `callback` with our intermediate function. - args[lastarg] = function(err) { - if(err) return iterate(i + 1, err); - if(cb) cb.apply(cb, arguments); - }; + throw lastError; + }; + + public createWriteStream = (path: string) => { + let lastError = null; + for (const [fs, { readonly }] of this.fss) { + if (readonly) continue; + try { + if (!fs.createWriteStream) throw Error(`Method not supported: "createWriteStream"`); + + fs.statSync(path); //we simply stat first to exit early for mocked fs'es + //TODO which filesystem to write to? + const stream = fs.createWriteStream(path); + if (!stream) { + throw new Error('no valid stream'); + } + this.WriteStream = fs.WriteStream; - const j = this.fss.length - i - 1; - const [_fs, _options, fsMethodStack] = this.fss[j]; - const func = fsMethodStack['async'][method] + return stream; + } catch (err) { + lastError = err; + } + } - if(!func) iterate(i + 1); - else func(...args); + throw lastError; + }; + + /** + * Adds a filesystem to the list of filesystems in the union + * The new filesystem object is added as the last filesystem used + * when searching for a file. + * + * @param fs the filesystem interface to be added to the queue of FS's + * @returns this instance of a unionFS + */ + use(fs: IFS, options: VolOptions = {}): this { + this.fss.push([fs, options, this.createMethods(fs, options)]); + return this; + } + + /** + * At the time of the [[use]] call, we create our sync, async and promise methods + * for performance reasons + * + * @param fs + * @param options + */ + private createMethods(fs: IFS, options: VolOptions): FSMethodStack { + const noop = undefined; + const createFunc = (method: string) => { + if (!fs[method]) + return (...args: any[]) => { + throw new Error(`Method not supported: "${method}" with args "${args}"`); + }; + return (...args: any[]) => fs[method as string].apply(fs, args); + }; + switch (true) { + case options.readonly: + return { + sync: { + ...fsSyncMethodsReadonly.reduce((acc, method) => { + acc[method] = createFunc(method); + return acc; + }, {}), + ...fsSyncMethodsWriteonly.reduce((acc, method) => { + acc[method] = noop; + return acc; + }, {}), + }, + async: { + ...fsAsyncMethodsReadonly.reduce((acc, method) => { + acc[method] = createFunc(method); + return acc; + }, {}), + ...fsAsyncMethodsWriteonly.reduce((acc, method) => { + acc[method] = noop; + return acc; + }, {}), + }, + promise: { + ...fsPromiseMethodsReadonly.reduce((acc, method) => { + const promises = fs.promises; + if (!promises || !promises[method]) { + acc[method] = (...args: any) => { + throw Error(`Promise of method not supported: "${String(method)}" with args "${args}"`); + }; + return acc; + } + acc[method] = (...args: any) => promises[method as string].apply(fs, args); + return acc; + }, {}), + ...fsPromiseMethodsWriteonly.reduce((acc, method) => { + acc[method] = noop; + return acc; + }, {}), + }, + }; + case options.writeonly: + return { + sync: { + ...fsSyncMethodsReadonly.reduce((acc, method) => { + acc[method] = noop; + return acc; + }, {}), + ...fsSyncMethodsWriteonly.reduce((acc, method) => { + acc[method] = createFunc(method); + return acc; + }, {}), + }, + async: { + ...fsAsyncMethodsReadonly.reduce((acc, method) => { + acc[method] = noop; + return acc; + }, {}), + ...fsAsyncMethodsWriteonly.reduce((acc, method) => { + acc[method] = createFunc(method); + return acc; + }, {}), + }, + promise: { + ...fsPromiseMethodsReadonly.reduce((acc, method) => { + acc[method] = noop; + return acc; + }, {}), + ...fsPromiseMethodsWriteonly.reduce((acc, method) => { + const promises = fs.promises; + if (!promises || !promises[method]) { + acc[method] = (...args: any) => { + throw Error(`Promise of method not supported: "${String(method)}" with args "${args}"`); + }; + return acc; + } + acc[method] = (...args: any) => promises[method as string].apply(fs, args); + return acc; + }, {}), + }, + }; + default: + return { + sync: { + ...fsSyncMethodsReadonly.reduce((acc, method) => { + acc[method] = createFunc(method); + return acc; + }, {}), + ...fsSyncMethodsWriteonly.reduce((acc, method) => { + acc[method] = createFunc(method); + return acc; + }, {}), + }, + async: { + ...fsAsyncMethodsReadonly.reduce((acc, method) => { + acc[method] = createFunc(method); + return acc; + }, {}), + ...fsAsyncMethodsWriteonly.reduce((acc, method) => { + acc[method] = createFunc(method); + return acc; + }, {}), + }, + promise: { + ...fsPromiseMethodsReadonly.reduce((acc, method) => { + const promises = fs.promises; + if (!promises || !promises[method]) { + acc[method] = (...args: any) => { + throw Error(`Promise of method not supported: "${String(method)}" with args "${args}"`); + }; + return acc; + } + acc[method] = (...args: any) => promises[method as string].apply(fs, args); + return acc; + }, {}), + ...fsPromiseMethodsWriteonly.reduce((acc, method) => { + const promises = fs.promises; + if (!promises || !promises[method]) { + acc[method] = (...args: any) => { + throw Error(`Promise of method not supported: "${String(method)}" with args "${args}"`); + }; + return acc; + } + acc[method] = (...args: any) => promises[method as string].apply(fs, args); + return acc; + }, {}), + }, }; - iterate(); + } + } + + private syncMethod(method: string, args: any[]) { + if (!this.fss.length) throw new Error('No file systems attached'); + let lastError: IUnionFsError | null = null; + for (let i = this.fss.length - 1; i >= 0; i--) { + const [_fs, _options, methodStack] = this.fss[i]; + try { + if (!methodStack['sync'][method]) continue; + return methodStack['sync'][method](...args); + } catch (err) { + err.prev = lastError; + lastError = err; + if (!i) { + // last one + throw err; + } else { + // Ignore error... + // continue; + } + } + } + } + + private asyncMethod(method: string, args: any[]) { + let lastarg = args.length - 1; + let cb = args[lastarg]; + if (typeof cb !== 'function') { + cb = null; + lastarg++; } - async promiseMethod(method: string, args: any[]) { - if (!this.fss.length) throw new Error('No file systems attached') - let lastError = null; - - for (let i = this.fss.length - 1; i >= 0; i--) { - - const [_fs, _options, fsMethodStack] = this.fss[i]; - try { - const func = fsMethodStack['promise'][method] - return await func(...args); - } catch (err) { - err.prev = lastError; - lastError = err; - if (!i) { - // last one - throw err; - } else { - // Ignore error... - // continue; - } - } + let lastError: IUnionFsError | null = null; + const iterate = (i = 0, err?: IUnionFsError) => { + if (err) { + err.prev = lastError; + lastError = err; + } + + // Already tried all file systems, return the last error. + if (i >= this.fss.length) { + // last one + if (cb) cb(err ?? (!this.fss.length ? new Error('No file systems attached.') : undefined)); + return; + } + + // Replace `callback` with our intermediate function. + args[lastarg] = function(err) { + if (err) return iterate(i + 1, err); + if (cb) cb.apply(cb, arguments); + }; + + const j = this.fss.length - i - 1; + const [_fs, _options, fsMethodStack] = this.fss[j]; + const func = fsMethodStack['async'][method]; + + if (!func) iterate(i + 1); + else func(...args); + }; + iterate(); + } + + async promiseMethod(method: string, args: any[]) { + if (!this.fss.length) throw new Error('No file systems attached'); + let lastError = null; + + for (let i = this.fss.length - 1; i >= 0; i--) { + const [_fs, _options, fsMethodStack] = this.fss[i]; + try { + const func = fsMethodStack['promise'][method]; + return await func(...args); + } catch (err) { + err.prev = lastError; + lastError = err; + if (!i) { + // last one + throw err; + } else { + // Ignore error... + // continue; } + } } + } } From 0d9f95793ef6372b0a23b790029e7e6d43b2fef5 Mon Sep 17 00:00:00 2001 From: matt penrice Date: Thu, 2 Apr 2020 12:46:26 +0200 Subject: [PATCH 08/24] =?UTF-8?q?wip:=20=F0=9F=92=AA=20addresses=20further?= =?UTF-8?q?=20pr=20comments?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/__tests__/union.test.ts | 7 ++ src/union.ts | 182 +++++++++++------------------------- yarn.lock | 7 +- 3 files changed, 66 insertions(+), 130 deletions(-) diff --git a/src/__tests__/union.test.ts b/src/__tests__/union.test.ts index eca64cd5..6e30d3c4 100644 --- a/src/__tests__/union.test.ts +++ b/src/__tests__/union.test.ts @@ -4,6 +4,13 @@ import * as fs from 'fs'; describe('union', () => { describe('Union', () => { + describe('options', () => { + it('throws error when readonly and writeonly both true', () => { + const union = new Union() + const vol1 = new Volume() + expect(() => union.use(vol1 as any, {readonly: true, writeonly: true})).toThrowError("Logically, options cannot contain both readonly and writeonly"); + }); + }) describe('sync methods', () => { it('Basic one file system', () => { const vol = Volume.fromJSON({ '/foo': 'bar' }); diff --git a/src/union.ts b/src/union.ts index da9d0a51..ca9c8f8e 100644 --- a/src/union.ts +++ b/src/union.ts @@ -346,6 +346,7 @@ export class Union { * @returns this instance of a unionFS */ use(fs: IFS, options: VolOptions = {}): this { + if (options.readonly && options.writeonly) throw new Error("Logically, options cannot contain both readonly and writeonly") this.fss.push([fs, options, this.createMethods(fs, options)]); return this; } @@ -357,7 +358,7 @@ export class Union { * @param fs * @param options */ - private createMethods(fs: IFS, options: VolOptions): FSMethodStack { + private createMethods(fs: IFS, {readonly, writeonly}: VolOptions): FSMethodStack { const noop = undefined; const createFunc = (method: string) => { if (!fs[method]) @@ -366,134 +367,57 @@ export class Union { }; return (...args: any[]) => fs[method as string].apply(fs, args); }; - switch (true) { - case options.readonly: - return { - sync: { - ...fsSyncMethodsReadonly.reduce((acc, method) => { - acc[method] = createFunc(method); - return acc; - }, {}), - ...fsSyncMethodsWriteonly.reduce((acc, method) => { - acc[method] = noop; - return acc; - }, {}), - }, - async: { - ...fsAsyncMethodsReadonly.reduce((acc, method) => { - acc[method] = createFunc(method); - return acc; - }, {}), - ...fsAsyncMethodsWriteonly.reduce((acc, method) => { - acc[method] = noop; - return acc; - }, {}), - }, - promise: { - ...fsPromiseMethodsReadonly.reduce((acc, method) => { - const promises = fs.promises; - if (!promises || !promises[method]) { - acc[method] = (...args: any) => { - throw Error(`Promise of method not supported: "${String(method)}" with args "${args}"`); - }; - return acc; - } - acc[method] = (...args: any) => promises[method as string].apply(fs, args); - return acc; - }, {}), - ...fsPromiseMethodsWriteonly.reduce((acc, method) => { - acc[method] = noop; - return acc; - }, {}), - }, - }; - case options.writeonly: - return { - sync: { - ...fsSyncMethodsReadonly.reduce((acc, method) => { - acc[method] = noop; - return acc; - }, {}), - ...fsSyncMethodsWriteonly.reduce((acc, method) => { - acc[method] = createFunc(method); - return acc; - }, {}), - }, - async: { - ...fsAsyncMethodsReadonly.reduce((acc, method) => { - acc[method] = noop; - return acc; - }, {}), - ...fsAsyncMethodsWriteonly.reduce((acc, method) => { - acc[method] = createFunc(method); - return acc; - }, {}), - }, - promise: { - ...fsPromiseMethodsReadonly.reduce((acc, method) => { - acc[method] = noop; - return acc; - }, {}), - ...fsPromiseMethodsWriteonly.reduce((acc, method) => { - const promises = fs.promises; - if (!promises || !promises[method]) { - acc[method] = (...args: any) => { - throw Error(`Promise of method not supported: "${String(method)}" with args "${args}"`); - }; - return acc; - } - acc[method] = (...args: any) => promises[method as string].apply(fs, args); - return acc; - }, {}), - }, - }; - default: - return { - sync: { - ...fsSyncMethodsReadonly.reduce((acc, method) => { - acc[method] = createFunc(method); - return acc; - }, {}), - ...fsSyncMethodsWriteonly.reduce((acc, method) => { - acc[method] = createFunc(method); - return acc; - }, {}), - }, - async: { - ...fsAsyncMethodsReadonly.reduce((acc, method) => { - acc[method] = createFunc(method); - return acc; - }, {}), - ...fsAsyncMethodsWriteonly.reduce((acc, method) => { - acc[method] = createFunc(method); - return acc; - }, {}), - }, - promise: { - ...fsPromiseMethodsReadonly.reduce((acc, method) => { - const promises = fs.promises; - if (!promises || !promises[method]) { - acc[method] = (...args: any) => { - throw Error(`Promise of method not supported: "${String(method)}" with args "${args}"`); - }; - return acc; - } - acc[method] = (...args: any) => promises[method as string].apply(fs, args); - return acc; - }, {}), - ...fsPromiseMethodsWriteonly.reduce((acc, method) => { - const promises = fs.promises; - if (!promises || !promises[method]) { - acc[method] = (...args: any) => { - throw Error(`Promise of method not supported: "${String(method)}" with args "${args}"`); - }; - return acc; - } - acc[method] = (...args: any) => promises[method as string].apply(fs, args); - return acc; - }, {}), - }, - }; + return { + sync: { + ...fsSyncMethodsReadonly.reduce((acc, method) => { + // acc[method] = createFunc(method); + acc[method] = writeonly ? noop : createFunc(method); + return acc; + }, {}), + ...fsSyncMethodsWriteonly.reduce((acc, method) => { + // acc[method] = noop; + acc[method] = readonly ? noop : createFunc(method); + return acc; + }, {}), + }, + async: { + ...fsAsyncMethodsReadonly.reduce((acc, method) => { + // acc[method] = createFunc(method); + acc[method] = writeonly ? noop : createFunc(method); + return acc; + }, {}), + ...fsAsyncMethodsWriteonly.reduce((acc, method) => { + // acc[method] = noop; + acc[method] = readonly ? noop : createFunc(method); + return acc; + }, {}), + }, + promise: { + ...fsPromiseMethodsReadonly.reduce((acc, method) => { + const promises = fs.promises; + if (!promises || !promises[method]) { + acc[method] = (...args: any) => { + throw Error(`Promise of method not supported: "${String(method)}" with args "${args}"`); + }; + return acc; + } + // acc[method] = (...args: any) => promises[method as string].apply(fs, args); + acc[method] = writeonly ? noop : (...args: any) => promises[method as string].apply(fs, args); + return acc; + }, {}), + ...fsPromiseMethodsWriteonly.reduce((acc, method) => { + const promises = fs.promises; + if (!promises || !promises[method]) { + acc[method] = (...args: any) => { + throw Error(`Promise of method not supported: "${String(method)}" with args "${args}"`); + }; + return acc; + } + // acc[method] = noop; + acc[method] = readonly ? noop : (...args: any) => promises[method as string].apply(fs, args); + return acc; + }, {}), + }, } } diff --git a/yarn.lock b/yarn.lock index 384ad373..c3bda3e3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2439,7 +2439,7 @@ fs-minipass@^1.2.5: dependencies: minipass "^2.6.0" -fs-monkey@1.0.0, fs-monkey@^1.0.0: +fs-monkey@1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/fs-monkey/-/fs-monkey-1.0.0.tgz#b1fe36b2d8a78463fd0b8fd1463b355952743bd0" integrity sha512-nxkkzQ5Ga+ETriXxIof4TncyMSzrV9jFIF+kGN16nw5CiAdWAnG/2FgM7CHhRenW1EBiDx+r1tf/P78HGKCgnA== @@ -5272,6 +5272,11 @@ prepend-http@^1.0.1: resolved "https://registry.yarnpkg.com/prepend-http/-/prepend-http-1.0.4.tgz#d4f4562b0ce3696e41ac52d0e002e57a635dc6dc" integrity sha1-1PRWKwzjaW5BrFLQ4ALlemNdxtw= +prettier@1.19.1: + version "1.19.1" + resolved "https://registry.yarnpkg.com/prettier/-/prettier-1.19.1.tgz#f7d7f5ff8a9cd872a7be4ca142095956a60797cb" + integrity sha512-s7PoyDv/II1ObgQunCbB9PdLmUcBZcnWOcxDh7O0N/UwDEsHyqkW+Qh28jW+mVuCdx7gLB0BotYI1Y6uI9iyew== + pretty-format@^25.1.0: version "25.1.0" resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-25.1.0.tgz#ed869bdaec1356fc5ae45de045e2c8ec7b07b0c8" From e535ca1c44caabb06017de548384bd8298dd0c12 Mon Sep 17 00:00:00 2001 From: matt penrice Date: Fri, 3 Apr 2020 13:59:36 +0200 Subject: [PATCH 09/24] adds dom lib to tsconfig --- tsconfig.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tsconfig.json b/tsconfig.json index 6a21a4df..90d7620f 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -2,7 +2,7 @@ "compilerOptions": { "target": "es5", "lib": [ - "es2015" + "es2015", "dom" ], "outDir": "lib", "module": "commonjs", From 205ce8cbf8e36e2ab51618c448d2f88d8e8517c6 Mon Sep 17 00:00:00 2001 From: matt penrice Date: Fri, 3 Apr 2020 14:04:47 +0200 Subject: [PATCH 10/24] export IFS for downstream projects --- src/index.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/index.ts b/src/index.ts index acdba9fd..334d08d6 100644 --- a/src/index.ts +++ b/src/index.ts @@ -9,3 +9,7 @@ export const Union = (_Union as any) as new () => IUnionFs; export const ufs = (new _Union() as any) as IUnionFs; export default ufs; +export { + IFS +} + From 85fdc1417787f7b0742ad70f980f83332703b50d Mon Sep 17 00:00:00 2001 From: matt penrice Date: Fri, 3 Apr 2020 14:20:00 +0200 Subject: [PATCH 11/24] export lists for downstream projects --- src/index.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/index.ts b/src/index.ts index 334d08d6..35e4a6b2 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,5 +1,6 @@ import { Union as _Union } from './union'; import { IFS } from './fs'; +export * from './lists' export interface IUnionFs extends IFS { use: (...args: Parameters<_Union['use']>) => this; From 57fcb015877a21d696d80d62313ebe7d8bfe36dd Mon Sep 17 00:00:00 2001 From: matt penrice Date: Fri, 3 Apr 2020 14:26:29 +0200 Subject: [PATCH 12/24] clean some commented out code --- src/union.ts | 7 ------- 1 file changed, 7 deletions(-) diff --git a/src/union.ts b/src/union.ts index ca9c8f8e..e0c91709 100644 --- a/src/union.ts +++ b/src/union.ts @@ -113,7 +113,6 @@ export class Union { for (let method of SPECIAL_METHODS.values()) { // bind special methods to support - // const { method } = ufs; this[method] = this[method].bind(this); } } @@ -370,24 +369,20 @@ export class Union { return { sync: { ...fsSyncMethodsReadonly.reduce((acc, method) => { - // acc[method] = createFunc(method); acc[method] = writeonly ? noop : createFunc(method); return acc; }, {}), ...fsSyncMethodsWriteonly.reduce((acc, method) => { - // acc[method] = noop; acc[method] = readonly ? noop : createFunc(method); return acc; }, {}), }, async: { ...fsAsyncMethodsReadonly.reduce((acc, method) => { - // acc[method] = createFunc(method); acc[method] = writeonly ? noop : createFunc(method); return acc; }, {}), ...fsAsyncMethodsWriteonly.reduce((acc, method) => { - // acc[method] = noop; acc[method] = readonly ? noop : createFunc(method); return acc; }, {}), @@ -401,7 +396,6 @@ export class Union { }; return acc; } - // acc[method] = (...args: any) => promises[method as string].apply(fs, args); acc[method] = writeonly ? noop : (...args: any) => promises[method as string].apply(fs, args); return acc; }, {}), @@ -413,7 +407,6 @@ export class Union { }; return acc; } - // acc[method] = noop; acc[method] = readonly ? noop : (...args: any) => promises[method as string].apply(fs, args); return acc; }, {}), From 25ffe9fc084f78bc1e9c561ae774afbed68b9ff9 Mon Sep 17 00:00:00 2001 From: matt penrice Date: Sat, 4 Apr 2020 08:31:33 +0200 Subject: [PATCH 13/24] PR comment amendments --- package.json | 2 -- src/index.ts | 8 ++------ tsconfig.json | 2 +- 3 files changed, 3 insertions(+), 9 deletions(-) diff --git a/package.json b/package.json index e0ba81f4..9820f7e6 100644 --- a/package.json +++ b/package.json @@ -23,8 +23,6 @@ }, "scripts": { "build": "tsc -p .", - "prettier": "prettier --ignore-path .gitignore --write \"src/**/*.{ts,js}\"", - "prettier:diff": "prettier -l \"src/**/*.{ts,js}\"", "test": "jest", "test-watch": "jest --watch", "test-coverage": "jest --coverage", diff --git a/src/index.ts b/src/index.ts index 35e4a6b2..99d5ac53 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,16 +1,12 @@ -import { Union as _Union } from './union'; +import { Union as _Union, VolOptions } from './union'; import { IFS } from './fs'; export * from './lists' export interface IUnionFs extends IFS { - use: (...args: Parameters<_Union['use']>) => this; + use: (fs: IFS, options: VolOptions) => this; } export const Union = (_Union as any) as new () => IUnionFs; export const ufs = (new _Union() as any) as IUnionFs; export default ufs; -export { - IFS -} - diff --git a/tsconfig.json b/tsconfig.json index 90d7620f..6a21a4df 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -2,7 +2,7 @@ "compilerOptions": { "target": "es5", "lib": [ - "es2015", "dom" + "es2015" ], "outDir": "lib", "module": "commonjs", From ae5d70abf88fdc5b015f5a1caf355db149a60fd2 Mon Sep 17 00:00:00 2001 From: matt penrice Date: Sat, 4 Apr 2020 08:37:52 +0200 Subject: [PATCH 14/24] Fix CI build --- src/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/index.ts b/src/index.ts index 99d5ac53..571ef27e 100644 --- a/src/index.ts +++ b/src/index.ts @@ -3,7 +3,7 @@ import { IFS } from './fs'; export * from './lists' export interface IUnionFs extends IFS { - use: (fs: IFS, options: VolOptions) => this; + use: (fs: IFS, options?: VolOptions) => this; } export const Union = (_Union as any) as new () => IUnionFs; From f8c365a69a4fd8183c4deb2bfa84d4abc423b3ab Mon Sep 17 00:00:00 2001 From: matt penrice Date: Sat, 4 Apr 2020 09:34:18 +0200 Subject: [PATCH 15/24] PR amendments --- README.md | 6 +-- src/__tests__/union.test.ts | 93 ++++++++++++++++------------------- src/lists.ts | 12 ++--- src/union.ts | 97 ++++++++++++++++++------------------- 4 files changed, 100 insertions(+), 108 deletions(-) diff --git a/README.md b/README.md index 16e17274..bc1c3393 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,7 @@ ufs ufs.readFileSync(/* ... */); ``` -This module allows you mark volumes as `readonly`/`writeonly` to prevent unwanted mutating of volumes +This module allows you mark volumes as `readable`/`writeable` (both defaulting to true) to prevent unwanted mutating of volumes ```js import {ufs} from 'unionfs'; @@ -28,8 +28,8 @@ import {fs as fs1} from 'memfs'; import * as fs2 from 'fs'; ufs - .use(fs1, {readonly: true}) - .use(fs2, {writeonly: true}); + .use(fs1, {writeable: false}) + .use(fs2, {readable: false}); ufs.writeFileSync(/* ... */); // fs2 will "collect" mutations; fs1 will remain unchanged ``` diff --git a/src/__tests__/union.test.ts b/src/__tests__/union.test.ts index 6e30d3c4..a4bc7743 100644 --- a/src/__tests__/union.test.ts +++ b/src/__tests__/union.test.ts @@ -4,13 +4,6 @@ import * as fs from 'fs'; describe('union', () => { describe('Union', () => { - describe('options', () => { - it('throws error when readonly and writeonly both true', () => { - const union = new Union() - const vol1 = new Volume() - expect(() => union.use(vol1 as any, {readonly: true, writeonly: true})).toThrowError("Logically, options cannot contain both readonly and writeonly"); - }); - }) describe('sync methods', () => { it('Basic one file system', () => { const vol = Volume.fromJSON({ '/foo': 'bar' }); @@ -83,21 +76,21 @@ describe('union', () => { }); }); - describe('readonly/writeonly', () => { + describe('readable/writeable', () => { it('writes to the last vol added', () => { const vol1 = Volume.fromJSON({}); const vol2 = Volume.fromJSON({}); - const ufs = new Union() as any; + const ufs = new Union(); ufs.use(vol1 as any).use(vol2 as any); ufs.writeFileSync('/foo', 'bar'); expect(vol2.readFileSync('/foo', 'utf8')).toEqual('bar'); }); - it('writes to the latest added non-readonly vol', () => { + it('writes to the latest added volume with writeable=false vol', () => { const vol1 = Volume.fromJSON({}); const vol2 = Volume.fromJSON({}); - const ufs = new Union() as any; - ufs.use(vol1 as any).use(vol2 as any, { readonly: true }); + const ufs = new Union(); + ufs.use(vol1 as any).use(vol2 as any, { writeable: false }); ufs.writeFileSync('/foo', 'bar'); expect(vol1.readFileSync('/foo', 'utf8')).toEqual('bar'); }); @@ -105,26 +98,26 @@ describe('union', () => { it('writes to the latest added writeable vol', () => { const vol1 = Volume.fromJSON({}); const vol2 = Volume.fromJSON({}); - const ufs = new Union() as any; - ufs.use(vol1 as any).use(vol2 as any, { writeonly: true }); + const ufs = new Union(); + ufs.use(vol1 as any).use(vol2 as any, { readable: false }); ufs.writeFileSync('/foo', 'bar'); expect(vol2.readFileSync('/foo', 'utf8')).toEqual('bar'); }); - it('not throw error if write operation attempted with all volumes readonly', () => { + it('not throw error if write operation attempted with all volumes writeable=false', () => { const vol1 = Volume.fromJSON({ '/foo': 'bar' }); const vol2 = Volume.fromJSON({ '/foo': 'bar' }); - const ufs = new Union() as any; - ufs.use(vol1 as any, { readonly: true }).use(vol2 as any, { readonly: true }); + const ufs = new Union(); + ufs.use(vol1 as any, { writeable: false }).use(vol2 as any, { writeable: false }); expect(() => ufs.writeFileSync('/foo', 'bar')).not.toThrowError(); }); - it('not throw error nor return value if read operation attempted with all volumes writeonly', () => { + it('not throw error nor return value if read operation attempted with all volumes readable=false', () => { const vol1 = Volume.fromJSON({ '/foo': 'bar1' }); const vol2 = Volume.fromJSON({ '/foo': 'bar2' }); - const ufs = new Union() as any; - ufs.use(vol1 as any, { writeonly: true }).use(vol2 as any, { writeonly: true }); + const ufs = new Union(); + ufs.use(vol1 as any, { readable: false }).use(vol2 as any, { readable: false }); expect(ufs.readFileSync('/foo')).toBeUndefined(); }); @@ -283,13 +276,13 @@ describe('union', () => { }); }); - describe('readonly/writeonly', () => { + describe('readable/writeable', () => { it('writes to the last vol added', done => { const vol1 = Volume.fromJSON({}); const vol2 = Volume.fromJSON({}); - const ufs = new Union() as any; + const ufs = new Union(); ufs.use(vol1 as any).use(vol2 as any); - ufs.writeFile('/foo', 'bar', (err, res) => { + ufs.writeFile('/foo', 'bar', (err) => { vol2.readFile('/foo', 'utf8', (err, res) => { expect(res).toEqual('bar'); done(); @@ -297,12 +290,12 @@ describe('union', () => { }); }); - it('writes to the latest added non-readonly vol', done => { + it('writes to the latest added volume without writeable=false', done => { const vol1 = Volume.fromJSON({}); const vol2 = Volume.fromJSON({}); - const ufs = new Union() as any; - ufs.use(vol1 as any).use(vol2 as any, { readonly: true }); - ufs.writeFile('/foo', 'bar', (err, res) => { + const ufs = new Union(); + ufs.use(vol1 as any).use(vol2 as any, { writeable: false }); + ufs.writeFile('/foo', 'bar', (err) => { vol1.readFile('/foo', 'utf8', (err, res) => { expect(res).toEqual('bar'); done(); @@ -313,9 +306,9 @@ describe('union', () => { it('writes to the latest added writeable vol', done => { const vol1 = Volume.fromJSON({}); const vol2 = Volume.fromJSON({}); - const ufs = new Union() as any; - ufs.use(vol1 as any).use(vol2 as any, { writeonly: true }); - ufs.writeFile('/foo', 'bar', (err, res) => { + const ufs = new Union(); + ufs.use(vol1 as any).use(vol2 as any, { readable: false }); + ufs.writeFile('/foo', 'bar', (err) => { vol2.readFile('/foo', 'utf8', (err, res) => { expect(res).toEqual('bar'); done(); @@ -323,22 +316,22 @@ describe('union', () => { }); }); - it('not throw error if write operation attempted with all volumes readonly', done => { + it('not throw error if write operation attempted with all volumes writeable=false', done => { const vol1 = Volume.fromJSON({ '/foo': 'bar' }); const vol2 = Volume.fromJSON({ '/foo': 'bar' }); - const ufs = new Union() as any; - ufs.use(vol1 as any, { readonly: true }).use(vol2 as any, { readonly: true }); - ufs.writeFile('/foo', 'bar', (err, res) => { + const ufs = new Union(); + ufs.use(vol1 as any, { writeable: false }).use(vol2 as any, { writeable: false }); + ufs.writeFile('/foo', 'bar', (err) => { expect(err).toBeUndefined(); done(); }); }); - it('not throw error nor return value if read operation attempted with all volumes writeonly', done => { + it('not throw error nor return value if read operation attempted with all volumes readable=false', done => { const vol1 = Volume.fromJSON({ '/foo': 'bar1' }); const vol2 = Volume.fromJSON({ '/foo': 'bar2' }); - const ufs = new Union() as any; - ufs.use(vol1 as any, { writeonly: true }).use(vol2 as any, { writeonly: true }); + const ufs = new Union(); + ufs.use(vol1 as any, { readable: false }).use(vol2 as any, { readable: false }); ufs.readFile('/foo', 'utf8', (err, res) => { expect(err).toBeUndefined(); @@ -463,21 +456,21 @@ describe('union', () => { await expect(ufs.promises.readFile('/foo', 'utf8')).rejects.toThrowError(); }); - describe('readonly/writeonly', () => { + describe('readable/writeable', () => { it('writes to the last vol added', () => { const vol1 = Volume.fromJSON({}); const vol2 = Volume.fromJSON({}); - const ufs = new Union() as any; + const ufs = new Union(); ufs.use(vol1 as any).use(vol2 as any); ufs.writeFileSync('/foo', 'bar'); expect(vol2.readFileSync('/foo', 'utf8')).toEqual('bar'); }); - it('writes to the latest added non-readonly vol', () => { + it('writes to the latest added volume without writable=false', () => { const vol1 = Volume.fromJSON({}); const vol2 = Volume.fromJSON({}); - const ufs = new Union() as any; - ufs.use(vol1 as any).use(vol2 as any, { readonly: true }); + const ufs = new Union(); + ufs.use(vol1 as any).use(vol2 as any, { writeable: false }); ufs.writeFileSync('/foo', 'bar'); expect(vol1.readFileSync('/foo', 'utf8')).toEqual('bar'); }); @@ -485,26 +478,26 @@ describe('union', () => { it('writes to the latest added writeable vol', () => { const vol1 = Volume.fromJSON({}); const vol2 = Volume.fromJSON({}); - const ufs = new Union() as any; - ufs.use(vol1 as any).use(vol2 as any, { writeonly: true }); + const ufs = new Union(); + ufs.use(vol1 as any).use(vol2 as any, { readable: false }); ufs.writeFileSync('/foo', 'bar'); expect(vol2.readFileSync('/foo', 'utf8')).toEqual('bar'); }); - it('not throw error if write operation attempted with all volumes readonly', () => { + it('not throw error if write operation attempted with all volumes writeable=false', () => { const vol1 = Volume.fromJSON({ '/foo': 'bar' }); const vol2 = Volume.fromJSON({ '/foo': 'bar' }); - const ufs = new Union() as any; - ufs.use(vol1 as any, { readonly: true }).use(vol2 as any, { readonly: true }); + const ufs = new Union(); + ufs.use(vol1 as any, { writeable: false }).use(vol2 as any, { writeable: false }); expect(() => ufs.writeFileSync('/foo', 'bar')).not.toThrowError(); }); - it('not throw error nor return value if read operation attempted with all volumes writeonly', () => { + it('not throw error nor return value if read operation attempted with all volumes readable=false', () => { const vol1 = Volume.fromJSON({ '/foo': 'bar1' }); const vol2 = Volume.fromJSON({ '/foo': 'bar2' }); - const ufs = new Union() as any; - ufs.use(vol1 as any, { writeonly: true }).use(vol2 as any, { writeonly: true }); + const ufs = new Union(); + ufs.use(vol1 as any, { readable: false }).use(vol2 as any, { readable: false }); expect(ufs.readFileSync('/foo')).toBeUndefined(); }); diff --git a/src/lists.ts b/src/lists.ts index 1d683fb2..23cc0f95 100644 --- a/src/lists.ts +++ b/src/lists.ts @@ -1,4 +1,4 @@ -export const fsSyncMethodsWriteonly = [ +export const fsSyncMethodsWrite = [ 'appendFileSync', 'chmodSync', 'chownSync', @@ -27,7 +27,7 @@ export const fsSyncMethodsWriteonly = [ 'writeSync', ] as const; -export const fsSyncMethodsReadonly = [ +export const fsSyncMethodsRead = [ 'accessSync', 'createReadStream', 'existsSync', @@ -41,7 +41,7 @@ export const fsSyncMethodsReadonly = [ 'realpathSync', 'statSync', ] as const; -export const fsAsyncMethodsReadonly = [ +export const fsAsyncMethodsRead = [ 'access', 'exists', 'fstat', @@ -55,7 +55,7 @@ export const fsAsyncMethodsReadonly = [ 'watch', 'watchFile', ] as const; -export const fsAsyncMethodsWriteonly = [ +export const fsAsyncMethodsWrite = [ 'appendFile', 'chmod', 'chown', @@ -85,7 +85,7 @@ export const fsAsyncMethodsWriteonly = [ 'writeFile', ] as const; -export const fsPromiseMethodsReadonly = [ +export const fsPromiseMethodsRead = [ 'access', 'open', 'opendir', @@ -95,7 +95,7 @@ export const fsPromiseMethodsReadonly = [ 'realpath', ] as const; -export const fsPromiseMethodsWriteonly = [ +export const fsPromiseMethodsWrite = [ 'appendFile', 'chmod', 'chown', diff --git a/src/union.ts b/src/union.ts index e0c91709..aec4d042 100644 --- a/src/union.ts +++ b/src/union.ts @@ -2,12 +2,12 @@ import { FSWatcher, Dirent } from 'fs'; import { IFS } from './fs'; import { Readable, Writable } from 'stream'; import { - fsSyncMethodsWriteonly, - fsSyncMethodsReadonly, - fsAsyncMethodsWriteonly, - fsAsyncMethodsReadonly, - fsPromiseMethodsWriteonly, - fsPromiseMethodsReadonly, + fsSyncMethodsWrite, + fsSyncMethodsRead, + fsAsyncMethodsWrite, + fsAsyncMethodsRead, + fsPromiseMethodsWrite, + fsPromiseMethodsRead, } from './lists'; export interface IUnionFsError extends Error { @@ -56,13 +56,13 @@ const createFSProxy = (watchers: FSWatcher[]) => ); export type VolOptions = { - readonly?: boolean; - writeonly?: boolean; + readable?: boolean; + writeable?: boolean; }; -type SyncMethodNames = typeof fsSyncMethodsReadonly[number] & typeof fsSyncMethodsWriteonly[number]; -type ASyncMethodNames = typeof fsSyncMethodsReadonly[number] & typeof fsAsyncMethodsWriteonly[number]; -type PromiseMethodNames = typeof fsPromiseMethodsReadonly[number] & typeof fsPromiseMethodsWriteonly[number]; +type SyncMethodNames = typeof fsSyncMethodsRead[number] & typeof fsSyncMethodsWrite[number]; +type ASyncMethodNames = typeof fsSyncMethodsRead[number] & typeof fsAsyncMethodsWrite[number]; +type PromiseMethodNames = typeof fsPromiseMethodsRead[number] & typeof fsPromiseMethodsWrite[number]; type FSMethod = (args: any) => any; type FSMethodStack = { sync: { @@ -88,20 +88,20 @@ export class Union { private promises: {} = {}; constructor() { - for (let method of [...fsSyncMethodsReadonly, ...fsSyncMethodsWriteonly]) { + for (let method of [...fsSyncMethodsRead, ...fsSyncMethodsWrite]) { if (!SPECIAL_METHODS.has(method)) { // check we don't already have a property for this method this[method] = (...args) => this.syncMethod(method, args); } } - for (let method of [...fsAsyncMethodsReadonly, ...fsAsyncMethodsWriteonly]) { + for (let method of [...fsAsyncMethodsRead, ...fsAsyncMethodsWrite]) { if (!SPECIAL_METHODS.has(method)) { // check we don't already have a property for this method this[method] = (...args) => this.asyncMethod(method, args); } } - for (let method of [...fsPromiseMethodsReadonly, ...fsPromiseMethodsWriteonly]) { + for (let method of [...fsPromiseMethodsRead, ...fsPromiseMethodsWrite]) { if (method === 'readdir') { this.promises[method] = this.readdirPromise; @@ -123,8 +123,8 @@ export class Union { public watch = (...args) => { const watchers: FSWatcher[] = []; - for (const [fs, { writeonly }] of this.fss) { - if (writeonly) continue; + for (const [fs, { readable }] of this.fss) { + if (readable === false) continue; try { const watcher = fs.watch.apply(fs, args); watchers.push(watcher); @@ -138,8 +138,8 @@ export class Union { }; public watchFile = (...args) => { - for (const [fs, { writeonly }] of this.fss) { - if (writeonly) continue; + for (const [fs, { readable }] of this.fss) { + if (readable === false) continue; try { fs.watchFile.apply(fs, args); } catch (e) { @@ -149,8 +149,8 @@ export class Union { }; public existsSync = (path: string) => { - for (const [fs, { writeonly }] of this.fss) { - if (writeonly) continue; + for (const [fs, { readable }] of this.fss) { + if (readable === false) continue; try { if (fs.existsSync(path)) { return true; @@ -207,11 +207,11 @@ export class Union { }; const j = this.fss.length - i - 1; - const [fs, { writeonly }] = this.fss[j]; + const [fs, { readable }] = this.fss[j]; const func = fs.readdir; if (!func) iterate(i + 1, Error('Method not supported: readdir')); - else if (writeonly) iterate(i + 1, Error(`Writeonly enabled for vol '${i}': readdir`)); + else if (readable === false) iterate(i + 1, Error(`Readable disabled for vol '${i}': readdir`)); else func.apply(fs, args); }; iterate(); @@ -221,8 +221,8 @@ export class Union { let lastError: IUnionFsError | null = null; let result = new Map(); for (let i = this.fss.length - 1; i >= 0; i--) { - const [fs, { writeonly }] = this.fss[i]; - if (writeonly) continue; + const [fs, { readable }] = this.fss[i]; + if (readable === false) continue; try { if (!fs.readdirSync) throw Error(`Method not supported: "readdirSync" with args "${args}"`); for (const res of fs.readdirSync.apply(fs, args)) { @@ -247,8 +247,8 @@ export class Union { let lastError: IUnionFsError | null = null; let result = new Map(); for (let i = this.fss.length - 1; i >= 0; i--) { - const [fs, { writeonly }] = this.fss[i]; - if (writeonly) continue; + const [fs, { readable }] = this.fss[i]; + if (readable === false) continue; try { if (!fs.promises || !fs.promises.readdir) throw Error(`Method not supported: "readdirSync" with args "${args}"`); @@ -288,8 +288,8 @@ export class Union { public createReadStream = (path: string) => { let lastError = null; - for (const [fs, { writeonly }] of this.fss) { - if (writeonly) continue; + for (const [fs, { readable }] of this.fss) { + if (readable === false) continue; try { if (!fs.createReadStream) throw Error(`Method not supported: "createReadStream"`); @@ -314,8 +314,8 @@ export class Union { public createWriteStream = (path: string) => { let lastError = null; - for (const [fs, { readonly }] of this.fss) { - if (readonly) continue; + for (const [fs, { writeable }] of this.fss) { + if (writeable === false) continue; try { if (!fs.createWriteStream) throw Error(`Method not supported: "createWriteStream"`); @@ -345,10 +345,9 @@ export class Union { * @returns this instance of a unionFS */ use(fs: IFS, options: VolOptions = {}): this { - if (options.readonly && options.writeonly) throw new Error("Logically, options cannot contain both readonly and writeonly") - this.fss.push([fs, options, this.createMethods(fs, options)]); - return this; - } + this.fss.push([fs, options, this.createMethods(fs, options)]); + return this; + } /** * At the time of the [[use]] call, we create our sync, async and promise methods @@ -357,7 +356,7 @@ export class Union { * @param fs * @param options */ - private createMethods(fs: IFS, {readonly, writeonly}: VolOptions): FSMethodStack { + private createMethods(fs: IFS, {readable = true, writeable = true}: VolOptions): FSMethodStack { const noop = undefined; const createFunc = (method: string) => { if (!fs[method]) @@ -368,27 +367,27 @@ export class Union { }; return { sync: { - ...fsSyncMethodsReadonly.reduce((acc, method) => { - acc[method] = writeonly ? noop : createFunc(method); + ...fsSyncMethodsRead.reduce((acc, method) => { + acc[method] = readable ? createFunc(method) : noop; return acc; }, {}), - ...fsSyncMethodsWriteonly.reduce((acc, method) => { - acc[method] = readonly ? noop : createFunc(method); + ...fsSyncMethodsWrite.reduce((acc, method) => { + acc[method] = writeable ? createFunc(method) : noop; return acc; }, {}), - }, - async: { - ...fsAsyncMethodsReadonly.reduce((acc, method) => { - acc[method] = writeonly ? noop : createFunc(method); + }, + async: { + ...fsAsyncMethodsRead.reduce((acc, method) => { + acc[method] = readable ? createFunc(method) : noop; return acc; }, {}), - ...fsAsyncMethodsWriteonly.reduce((acc, method) => { - acc[method] = readonly ? noop : createFunc(method); + ...fsAsyncMethodsWrite.reduce((acc, method) => { + acc[method] = writeable ? createFunc(method) : noop; return acc; }, {}), }, promise: { - ...fsPromiseMethodsReadonly.reduce((acc, method) => { + ...fsPromiseMethodsRead.reduce((acc, method) => { const promises = fs.promises; if (!promises || !promises[method]) { acc[method] = (...args: any) => { @@ -396,10 +395,10 @@ export class Union { }; return acc; } - acc[method] = writeonly ? noop : (...args: any) => promises[method as string].apply(fs, args); + acc[method] = readable ? (...args: any) => promises[method as string].apply(fs, args) : noop; return acc; }, {}), - ...fsPromiseMethodsWriteonly.reduce((acc, method) => { + ...fsPromiseMethodsWrite.reduce((acc, method) => { const promises = fs.promises; if (!promises || !promises[method]) { acc[method] = (...args: any) => { @@ -407,7 +406,7 @@ export class Union { }; return acc; } - acc[method] = readonly ? noop : (...args: any) => promises[method as string].apply(fs, args); + acc[method] = writeable ? (...args: any) => promises[method as string].apply(fs, args) : noop; return acc; }, {}), }, From cea8aa855b2d0af2a6144ec98013b689630625e8 Mon Sep 17 00:00:00 2001 From: matt penrice Date: Fri, 10 Apr 2020 13:00:07 +0200 Subject: [PATCH 16/24] Goes back to fs-based internals --- src/union.ts | 165 +++++++++++++++++++++++++++------------------------ 1 file changed, 88 insertions(+), 77 deletions(-) diff --git a/src/union.ts b/src/union.ts index aec4d042..19258faf 100644 --- a/src/union.ts +++ b/src/union.ts @@ -60,27 +60,20 @@ export type VolOptions = { writeable?: boolean; }; -type SyncMethodNames = typeof fsSyncMethodsRead[number] & typeof fsSyncMethodsWrite[number]; -type ASyncMethodNames = typeof fsSyncMethodsRead[number] & typeof fsAsyncMethodsWrite[number]; -type PromiseMethodNames = typeof fsPromiseMethodsRead[number] & typeof fsPromiseMethodsWrite[number]; -type FSMethod = (args: any) => any; -type FSMethodStack = { - sync: { - [K in SyncMethodNames]: FSMethod | undefined; - }; - async: { - [K in ASyncMethodNames]: FSMethod | undefined; - }; - promise: { - [K in PromiseMethodNames]: FSMethod | undefined; - }; -}; +class SkipMethodError extends Error { + __proto__: Error; + constructor() { + const trueProto = new.target.prototype; + super('Method has been marked as noop by user options'); + this.__proto__ = trueProto; + } +} /** * Union object represents a stack of filesystems */ export class Union { - private fss: [IFS, VolOptions, FSMethodStack][] = []; + private fss: [IFS, VolOptions][] = []; public ReadStream: typeof Readable | (new (...args: any[]) => Readable) = Readable; public WriteStream: typeof Writable | (new (...args: any[]) => Writable) = Writable; @@ -345,9 +338,9 @@ export class Union { * @returns this instance of a unionFS */ use(fs: IFS, options: VolOptions = {}): this { - this.fss.push([fs, options, this.createMethods(fs, options)]); - return this; - } + this.fss.push([this.createFS(fs, options), options]); + return this; + } /** * At the time of the [[use]] call, we create our sync, async and promise methods @@ -356,72 +349,76 @@ export class Union { * @param fs * @param options */ - private createMethods(fs: IFS, {readable = true, writeable = true}: VolOptions): FSMethodStack { - const noop = undefined; - const createFunc = (method: string) => { + private createFS(fs: IFS, { readable = true, writeable = true }: VolOptions): IFS { + const noop = (..._args: any[]) => { + throw new SkipMethodError(); + }; + const createFunc = (method: string): any => { if (!fs[method]) return (...args: any[]) => { throw new Error(`Method not supported: "${method}" with args "${args}"`); }; return (...args: any[]) => fs[method as string].apply(fs, args); }; + return { - sync: { - ...fsSyncMethodsRead.reduce((acc, method) => { - acc[method] = readable ? createFunc(method) : noop; - return acc; - }, {}), - ...fsSyncMethodsWrite.reduce((acc, method) => { - acc[method] = writeable ? createFunc(method) : noop; - return acc; - }, {}), - }, - async: { - ...fsAsyncMethodsRead.reduce((acc, method) => { - acc[method] = readable ? createFunc(method) : noop; - return acc; - }, {}), - ...fsAsyncMethodsWrite.reduce((acc, method) => { - acc[method] = writeable ? createFunc(method) : noop; - return acc; - }, {}), - }, - promise: { - ...fsPromiseMethodsRead.reduce((acc, method) => { - const promises = fs.promises; - if (!promises || !promises[method]) { - acc[method] = (...args: any) => { - throw Error(`Promise of method not supported: "${String(method)}" with args "${args}"`); - }; + ...fs, + ...fsSyncMethodsRead.reduce((acc, method) => { + acc[method] = readable ? createFunc(method) : noop; + return acc; + }, {} as Record>), + ...fsSyncMethodsWrite.reduce((acc, method) => { + acc[method] = writeable ? createFunc(method) : noop; + return acc; + }, {} as Record>), + ...fsAsyncMethodsRead.reduce((acc, method) => { + acc[method] = readable ? createFunc(method) : noop; + return acc; + }, {} as Record>), + ...fsAsyncMethodsWrite.reduce((acc, method) => { + acc[method] = writeable ? createFunc(method) : noop; + return acc; + }, {} as Record>), + ...{ + promises: { + ...fs.promises, + ...fsPromiseMethodsRead.reduce((acc, method) => { + const promises = fs.promises; + if (!promises || !promises[method]) { + acc[method] = (...args: any) => { + throw Error(`Promise of method not supported: "${String(method)}" with args "${args}"`); + }; + return acc; + } + acc[method] = readable ? (...args: any) => promises[method as string].apply(fs, args) : noop; return acc; - } - acc[method] = readable ? (...args: any) => promises[method as string].apply(fs, args) : noop; - return acc; - }, {}), - ...fsPromiseMethodsWrite.reduce((acc, method) => { - const promises = fs.promises; - if (!promises || !promises[method]) { - acc[method] = (...args: any) => { - throw Error(`Promise of method not supported: "${String(method)}" with args "${args}"`); - }; + }, {} as Record>), + ...fsPromiseMethodsWrite.reduce((acc, method) => { + const promises = fs.promises; + if (!promises || !promises[method]) { + acc[method] = (...args: any) => { + throw Error(`Promise of method not supported: "${String(method)}" with args "${args}"`); + }; + return acc; + } + acc[method] = writeable ? (...args: any) => promises[method as string].apply(fs, args) : noop; return acc; - } - acc[method] = writeable ? (...args: any) => promises[method as string].apply(fs, args) : noop; - return acc; - }, {}), + }, {} as Record>), + }, }, - } + }; } private syncMethod(method: string, args: any[]) { if (!this.fss.length) throw new Error('No file systems attached'); let lastError: IUnionFsError | null = null; for (let i = this.fss.length - 1; i >= 0; i--) { - const [_fs, _options, methodStack] = this.fss[i]; + const [fs] = this.fss[i]; try { - if (!methodStack['sync'][method]) continue; - return methodStack['sync'][method](...args); + if (!fs[method]) throw Error(`Method not supported: "${method}" with args "${args}"`); + return fs[method](...args); } catch (err) { + if (err instanceof SkipMethodError) continue; err.prev = lastError; lastError = err; if (!i) { @@ -458,31 +455,45 @@ export class Union { } // Replace `callback` with our intermediate function. - args[lastarg] = function(err) { + args[lastarg] = function (err) { + if (err instanceof SkipMethodError) return iterate(i + 1); if (err) return iterate(i + 1, err); if (cb) cb.apply(cb, arguments); }; const j = this.fss.length - i - 1; - const [_fs, _options, fsMethodStack] = this.fss[j]; - const func = fsMethodStack['async'][method]; - - if (!func) iterate(i + 1); - else func(...args); + const [fs] = this.fss[j]; + const func = fs[method]; + + if (!func) iterate(i + 1, Error('Method not supported: ' + method)); + else { + try { + func(...args); + } catch (err) { + if (err instanceof SkipMethodError) return iterate(i + 1); + throw err + } + } }; iterate(); } async promiseMethod(method: string, args: any[]) { - if (!this.fss.length) throw new Error('No file systems attached'); let lastError = null; for (let i = this.fss.length - 1; i >= 0; i--) { - const [_fs, _options, fsMethodStack] = this.fss[i]; + const [theFs] = this.fss[i]; + + const promises = theFs.promises; + try { - const func = fsMethodStack['promise'][method]; - return await func(...args); + if (!promises || !promises[method]) { + throw Error(`Promise of method not supported: "${String(method)}" with args "${args}"`); + } + + return await promises[method].apply(promises, args); } catch (err) { + if (err instanceof SkipMethodError) continue; err.prev = lastError; lastError = err; if (!i) { From 50d5b055454e85988fa1ce1ec194c2a3a2639a60 Mon Sep 17 00:00:00 2001 From: matt penrice Date: Sat, 11 Apr 2020 11:41:43 +0200 Subject: [PATCH 17/24] changes following pr --- README.md | 4 +-- src/__tests__/union.test.ts | 34 ++++++++++----------- src/union.ts | 59 ++++++++++++++++++------------------- 3 files changed, 48 insertions(+), 49 deletions(-) diff --git a/README.md b/README.md index bc1c3393..7ee78d73 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,7 @@ ufs ufs.readFileSync(/* ... */); ``` -This module allows you mark volumes as `readable`/`writeable` (both defaulting to true) to prevent unwanted mutating of volumes +This module allows you mark volumes as `readable`/`writable` (both defaulting to true) to prevent unwanted mutating of volumes ```js import {ufs} from 'unionfs'; @@ -28,7 +28,7 @@ import {fs as fs1} from 'memfs'; import * as fs2 from 'fs'; ufs - .use(fs1, {writeable: false}) + .use(fs1, {writable: false}) .use(fs2, {readable: false}); ufs.writeFileSync(/* ... */); // fs2 will "collect" mutations; fs1 will remain unchanged diff --git a/src/__tests__/union.test.ts b/src/__tests__/union.test.ts index a4bc7743..4126f732 100644 --- a/src/__tests__/union.test.ts +++ b/src/__tests__/union.test.ts @@ -76,7 +76,7 @@ describe('union', () => { }); }); - describe('readable/writeable', () => { + describe('readable/writable', () => { it('writes to the last vol added', () => { const vol1 = Volume.fromJSON({}); const vol2 = Volume.fromJSON({}); @@ -86,16 +86,16 @@ describe('union', () => { expect(vol2.readFileSync('/foo', 'utf8')).toEqual('bar'); }); - it('writes to the latest added volume with writeable=false vol', () => { + it('writes to the latest added volume with writable=false vol', () => { const vol1 = Volume.fromJSON({}); const vol2 = Volume.fromJSON({}); const ufs = new Union(); - ufs.use(vol1 as any).use(vol2 as any, { writeable: false }); + ufs.use(vol1 as any).use(vol2 as any, { writable: false }); ufs.writeFileSync('/foo', 'bar'); expect(vol1.readFileSync('/foo', 'utf8')).toEqual('bar'); }); - it('writes to the latest added writeable vol', () => { + it('writes to the latest added writable vol', () => { const vol1 = Volume.fromJSON({}); const vol2 = Volume.fromJSON({}); const ufs = new Union(); @@ -104,11 +104,11 @@ describe('union', () => { expect(vol2.readFileSync('/foo', 'utf8')).toEqual('bar'); }); - it('not throw error if write operation attempted with all volumes writeable=false', () => { + it('not throw error if write operation attempted with all volumes writable=false', () => { const vol1 = Volume.fromJSON({ '/foo': 'bar' }); const vol2 = Volume.fromJSON({ '/foo': 'bar' }); const ufs = new Union(); - ufs.use(vol1 as any, { writeable: false }).use(vol2 as any, { writeable: false }); + ufs.use(vol1 as any, { writable: false }).use(vol2 as any, { writable: false }); expect(() => ufs.writeFileSync('/foo', 'bar')).not.toThrowError(); }); @@ -276,7 +276,7 @@ describe('union', () => { }); }); - describe('readable/writeable', () => { + describe('readable/writable', () => { it('writes to the last vol added', done => { const vol1 = Volume.fromJSON({}); const vol2 = Volume.fromJSON({}); @@ -290,11 +290,11 @@ describe('union', () => { }); }); - it('writes to the latest added volume without writeable=false', done => { + it('writes to the latest added volume without writable=false', done => { const vol1 = Volume.fromJSON({}); const vol2 = Volume.fromJSON({}); const ufs = new Union(); - ufs.use(vol1 as any).use(vol2 as any, { writeable: false }); + ufs.use(vol1 as any).use(vol2 as any, { writable: false }); ufs.writeFile('/foo', 'bar', (err) => { vol1.readFile('/foo', 'utf8', (err, res) => { expect(res).toEqual('bar'); @@ -303,7 +303,7 @@ describe('union', () => { }); }); - it('writes to the latest added writeable vol', done => { + it('writes to the latest added writable vol', done => { const vol1 = Volume.fromJSON({}); const vol2 = Volume.fromJSON({}); const ufs = new Union(); @@ -316,11 +316,11 @@ describe('union', () => { }); }); - it('not throw error if write operation attempted with all volumes writeable=false', done => { + it('not throw error if write operation attempted with all volumes writable=false', done => { const vol1 = Volume.fromJSON({ '/foo': 'bar' }); const vol2 = Volume.fromJSON({ '/foo': 'bar' }); const ufs = new Union(); - ufs.use(vol1 as any, { writeable: false }).use(vol2 as any, { writeable: false }); + ufs.use(vol1 as any, { writable: false }).use(vol2 as any, { writable: false }); ufs.writeFile('/foo', 'bar', (err) => { expect(err).toBeUndefined(); done(); @@ -456,7 +456,7 @@ describe('union', () => { await expect(ufs.promises.readFile('/foo', 'utf8')).rejects.toThrowError(); }); - describe('readable/writeable', () => { + describe('readable/writable', () => { it('writes to the last vol added', () => { const vol1 = Volume.fromJSON({}); const vol2 = Volume.fromJSON({}); @@ -470,12 +470,12 @@ describe('union', () => { const vol1 = Volume.fromJSON({}); const vol2 = Volume.fromJSON({}); const ufs = new Union(); - ufs.use(vol1 as any).use(vol2 as any, { writeable: false }); + ufs.use(vol1 as any).use(vol2 as any, { writable: false }); ufs.writeFileSync('/foo', 'bar'); expect(vol1.readFileSync('/foo', 'utf8')).toEqual('bar'); }); - it('writes to the latest added writeable vol', () => { + it('writes to the latest added writable vol', () => { const vol1 = Volume.fromJSON({}); const vol2 = Volume.fromJSON({}); const ufs = new Union(); @@ -484,11 +484,11 @@ describe('union', () => { expect(vol2.readFileSync('/foo', 'utf8')).toEqual('bar'); }); - it('not throw error if write operation attempted with all volumes writeable=false', () => { + it('not throw error if write operation attempted with all volumes writable=false', () => { const vol1 = Volume.fromJSON({ '/foo': 'bar' }); const vol2 = Volume.fromJSON({ '/foo': 'bar' }); const ufs = new Union(); - ufs.use(vol1 as any, { writeable: false }).use(vol2 as any, { writeable: false }); + ufs.use(vol1 as any, { writable: false }).use(vol2 as any, { writable: false }); expect(() => ufs.writeFileSync('/foo', 'bar')).not.toThrowError(); }); diff --git a/src/union.ts b/src/union.ts index 19258faf..8c3e4c7e 100644 --- a/src/union.ts +++ b/src/union.ts @@ -57,10 +57,11 @@ const createFSProxy = (watchers: FSWatcher[]) => export type VolOptions = { readable?: boolean; - writeable?: boolean; + writable?: boolean; }; class SkipMethodError extends Error { + // ts weirdness - https://github.com/Microsoft/TypeScript/issues/13965#issuecomment-278570200 __proto__: Error; constructor() { const trueProto = new.target.prototype; @@ -307,8 +308,8 @@ export class Union { public createWriteStream = (path: string) => { let lastError = null; - for (const [fs, { writeable }] of this.fss) { - if (writeable === false) continue; + for (const [fs, { writable }] of this.fss) { + if (writable === false) continue; try { if (!fs.createWriteStream) throw Error(`Method not supported: "createWriteStream"`); @@ -349,7 +350,7 @@ export class Union { * @param fs * @param options */ - private createFS(fs: IFS, { readable = true, writeable = true }: VolOptions): IFS { + private createFS(fs: IFS, { readable = true, writable = true }: VolOptions): IFS { const noop = (..._args: any[]) => { throw new SkipMethodError(); }; @@ -368,7 +369,7 @@ export class Union { return acc; }, {} as Record>), ...fsSyncMethodsWrite.reduce((acc, method) => { - acc[method] = writeable ? createFunc(method) : noop; + acc[method] = writable ? createFunc(method) : noop; return acc; }, {} as Record>), ...fsAsyncMethodsRead.reduce((acc, method) => { @@ -376,35 +377,33 @@ export class Union { return acc; }, {} as Record>), ...fsAsyncMethodsWrite.reduce((acc, method) => { - acc[method] = writeable ? createFunc(method) : noop; + acc[method] = writable ? createFunc(method) : noop; return acc; }, {} as Record>), - ...{ - promises: { - ...fs.promises, - ...fsPromiseMethodsRead.reduce((acc, method) => { - const promises = fs.promises; - if (!promises || !promises[method]) { - acc[method] = (...args: any) => { - throw Error(`Promise of method not supported: "${String(method)}" with args "${args}"`); - }; - return acc; - } - acc[method] = readable ? (...args: any) => promises[method as string].apply(fs, args) : noop; + promises: { + ...fs.promises, + ...fsPromiseMethodsRead.reduce((acc, method) => { + const promises = fs.promises; + if (!promises || !promises[method]) { + acc[method] = (...args: any) => { + throw Error(`Promise of method not supported: "${String(method)}" with args "${args}"`); + }; return acc; - }, {} as Record>), - ...fsPromiseMethodsWrite.reduce((acc, method) => { - const promises = fs.promises; - if (!promises || !promises[method]) { - acc[method] = (...args: any) => { - throw Error(`Promise of method not supported: "${String(method)}" with args "${args}"`); - }; - return acc; - } - acc[method] = writeable ? (...args: any) => promises[method as string].apply(fs, args) : noop; + } + acc[method] = readable ? (...args: any) => promises[method as string].apply(fs, args) : noop; + return acc; + }, {} as Record>), + ...fsPromiseMethodsWrite.reduce((acc, method) => { + const promises = fs.promises; + if (!promises || !promises[method]) { + acc[method] = (...args: any) => { + throw Error(`Promise of method not supported: "${String(method)}" with args "${args}"`); + }; return acc; - }, {} as Record>), - }, + } + acc[method] = writable ? (...args: any) => promises[method as string].apply(fs, args) : noop; + return acc; + }, {} as Record>), }, }; } From 047cd19fb6ae1830a377a11be76c269bbc7c50dc Mon Sep 17 00:00:00 2001 From: matt penrice Date: Sat, 11 Apr 2020 12:03:34 +0200 Subject: [PATCH 18/24] removes reduce casts --- src/union.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/union.ts b/src/union.ts index 8c3e4c7e..25ae9c50 100644 --- a/src/union.ts +++ b/src/union.ts @@ -367,19 +367,19 @@ export class Union { ...fsSyncMethodsRead.reduce((acc, method) => { acc[method] = readable ? createFunc(method) : noop; return acc; - }, {} as Record>), + }, {}), ...fsSyncMethodsWrite.reduce((acc, method) => { acc[method] = writable ? createFunc(method) : noop; return acc; - }, {} as Record>), + }, {}), ...fsAsyncMethodsRead.reduce((acc, method) => { acc[method] = readable ? createFunc(method) : noop; return acc; - }, {} as Record>), + }, {}), ...fsAsyncMethodsWrite.reduce((acc, method) => { acc[method] = writable ? createFunc(method) : noop; return acc; - }, {} as Record>), + }, {}), promises: { ...fs.promises, ...fsPromiseMethodsRead.reduce((acc, method) => { @@ -392,7 +392,7 @@ export class Union { } acc[method] = readable ? (...args: any) => promises[method as string].apply(fs, args) : noop; return acc; - }, {} as Record>), + }, {}), ...fsPromiseMethodsWrite.reduce((acc, method) => { const promises = fs.promises; if (!promises || !promises[method]) { @@ -403,7 +403,7 @@ export class Union { } acc[method] = writable ? (...args: any) => promises[method as string].apply(fs, args) : noop; return acc; - }, {} as Record>), + }, {}), }, }; } From 31e282c57f7a3cadd936989bf19f3c299fdca893 Mon Sep 17 00:00:00 2001 From: matt penrice Date: Sat, 11 Apr 2020 13:11:19 +0200 Subject: [PATCH 19/24] Removes skiperror handling --- src/__tests__/union.test.ts | 25 ++++++++++++------------- src/union.ts | 31 +++++++++---------------------- 2 files changed, 21 insertions(+), 35 deletions(-) diff --git a/src/__tests__/union.test.ts b/src/__tests__/union.test.ts index 4126f732..aa056af3 100644 --- a/src/__tests__/union.test.ts +++ b/src/__tests__/union.test.ts @@ -104,22 +104,22 @@ describe('union', () => { expect(vol2.readFileSync('/foo', 'utf8')).toEqual('bar'); }); - it('not throw error if write operation attempted with all volumes writable=false', () => { + it('throw error if write operation attempted with all volumes writable=false', () => { const vol1 = Volume.fromJSON({ '/foo': 'bar' }); const vol2 = Volume.fromJSON({ '/foo': 'bar' }); const ufs = new Union(); ufs.use(vol1 as any, { writable: false }).use(vol2 as any, { writable: false }); - expect(() => ufs.writeFileSync('/foo', 'bar')).not.toThrowError(); + expect(() => ufs.writeFileSync('/foo', 'bar')).toThrowError(); }); - it('not throw error nor return value if read operation attempted with all volumes readable=false', () => { + it('throw error if read operation attempted with all volumes readable=false', () => { const vol1 = Volume.fromJSON({ '/foo': 'bar1' }); const vol2 = Volume.fromJSON({ '/foo': 'bar2' }); const ufs = new Union(); ufs.use(vol1 as any, { readable: false }).use(vol2 as any, { readable: false }); - expect(ufs.readFileSync('/foo')).toBeUndefined(); + expect(() => ufs.readFileSync('/foo')).toThrowError(); }); }); @@ -316,26 +316,25 @@ describe('union', () => { }); }); - it('not throw error if write operation attempted with all volumes writable=false', done => { + it('throw error if write operation attempted with all volumes writable=false', done => { const vol1 = Volume.fromJSON({ '/foo': 'bar' }); const vol2 = Volume.fromJSON({ '/foo': 'bar' }); const ufs = new Union(); ufs.use(vol1 as any, { writable: false }).use(vol2 as any, { writable: false }); ufs.writeFile('/foo', 'bar', (err) => { - expect(err).toBeUndefined(); + expect(err).toBeInstanceOf(Error); done(); }); }); - it('not throw error nor return value if read operation attempted with all volumes readable=false', done => { + it('throw error if read operation attempted with all volumes readable=false', done => { const vol1 = Volume.fromJSON({ '/foo': 'bar1' }); const vol2 = Volume.fromJSON({ '/foo': 'bar2' }); const ufs = new Union(); ufs.use(vol1 as any, { readable: false }).use(vol2 as any, { readable: false }); ufs.readFile('/foo', 'utf8', (err, res) => { - expect(err).toBeUndefined(); - expect(res).toBeUndefined(); + expect(err).toBeInstanceOf(Error); done(); }); }); @@ -484,22 +483,22 @@ describe('union', () => { expect(vol2.readFileSync('/foo', 'utf8')).toEqual('bar'); }); - it('not throw error if write operation attempted with all volumes writable=false', () => { + it('throw error if write operation attempted with all volumes writable=false', () => { const vol1 = Volume.fromJSON({ '/foo': 'bar' }); const vol2 = Volume.fromJSON({ '/foo': 'bar' }); const ufs = new Union(); ufs.use(vol1 as any, { writable: false }).use(vol2 as any, { writable: false }); - expect(() => ufs.writeFileSync('/foo', 'bar')).not.toThrowError(); + expect(() => ufs.writeFileSync('/foo', 'bar')).toThrowError(); }); - it('not throw error nor return value if read operation attempted with all volumes readable=false', () => { + it('throw error if read operation attempted with all volumes readable=false', () => { const vol1 = Volume.fromJSON({ '/foo': 'bar1' }); const vol2 = Volume.fromJSON({ '/foo': 'bar2' }); const ufs = new Union(); ufs.use(vol1 as any, { readable: false }).use(vol2 as any, { readable: false }); - expect(ufs.readFileSync('/foo')).toBeUndefined(); + expect(() => ufs.readFileSync('/foo')).toThrowError(); }); }); diff --git a/src/union.ts b/src/union.ts index 25ae9c50..031ebe29 100644 --- a/src/union.ts +++ b/src/union.ts @@ -60,15 +60,6 @@ export type VolOptions = { writable?: boolean; }; -class SkipMethodError extends Error { - // ts weirdness - https://github.com/Microsoft/TypeScript/issues/13965#issuecomment-278570200 - __proto__: Error; - constructor() { - const trueProto = new.target.prototype; - super('Method has been marked as noop by user options'); - this.__proto__ = trueProto; - } -} /** * Union object represents a stack of filesystems @@ -351,8 +342,8 @@ export class Union { * @param options */ private createFS(fs: IFS, { readable = true, writable = true }: VolOptions): IFS { - const noop = (..._args: any[]) => { - throw new SkipMethodError(); + const createErroringFn = (state: 'readable' | 'writable') => (...args: any[]) => { + throw new Error(`Filesystem is not ${state}`); }; const createFunc = (method: string): any => { if (!fs[method]) @@ -365,19 +356,19 @@ export class Union { return { ...fs, ...fsSyncMethodsRead.reduce((acc, method) => { - acc[method] = readable ? createFunc(method) : noop; + acc[method] = readable ? createFunc(method) : createErroringFn('writable'); return acc; }, {}), ...fsSyncMethodsWrite.reduce((acc, method) => { - acc[method] = writable ? createFunc(method) : noop; + acc[method] = writable ? createFunc(method) : createErroringFn('readable'); return acc; }, {}), ...fsAsyncMethodsRead.reduce((acc, method) => { - acc[method] = readable ? createFunc(method) : noop; + acc[method] = readable ? createFunc(method) : createErroringFn('writable'); return acc; }, {}), ...fsAsyncMethodsWrite.reduce((acc, method) => { - acc[method] = writable ? createFunc(method) : noop; + acc[method] = writable ? createFunc(method) : createErroringFn('readable'); return acc; }, {}), promises: { @@ -390,7 +381,7 @@ export class Union { }; return acc; } - acc[method] = readable ? (...args: any) => promises[method as string].apply(fs, args) : noop; + acc[method] = readable ? (...args: any) => promises[method as string].apply(fs, args) : createErroringFn('writable'); return acc; }, {}), ...fsPromiseMethodsWrite.reduce((acc, method) => { @@ -401,7 +392,7 @@ export class Union { }; return acc; } - acc[method] = writable ? (...args: any) => promises[method as string].apply(fs, args) : noop; + acc[method] = writable ? (...args: any) => promises[method as string].apply(fs, args) : createErroringFn('readable'); return acc; }, {}), }, @@ -417,7 +408,6 @@ export class Union { if (!fs[method]) throw Error(`Method not supported: "${method}" with args "${args}"`); return fs[method](...args); } catch (err) { - if (err instanceof SkipMethodError) continue; err.prev = lastError; lastError = err; if (!i) { @@ -455,7 +445,6 @@ export class Union { // Replace `callback` with our intermediate function. args[lastarg] = function (err) { - if (err instanceof SkipMethodError) return iterate(i + 1); if (err) return iterate(i + 1, err); if (cb) cb.apply(cb, arguments); }; @@ -469,8 +458,7 @@ export class Union { try { func(...args); } catch (err) { - if (err instanceof SkipMethodError) return iterate(i + 1); - throw err + iterate(i + 1, err); } } }; @@ -492,7 +480,6 @@ export class Union { return await promises[method].apply(promises, args); } catch (err) { - if (err instanceof SkipMethodError) continue; err.prev = lastError; lastError = err; if (!i) { From 7b76f94d90eba965a678b009f0e82ebc59fc7624 Mon Sep 17 00:00:00 2001 From: matt penrice Date: Sun, 19 Apr 2020 11:55:35 +0200 Subject: [PATCH 20/24] changes following pr --- src/__tests__/union.test.ts | 141 ++++++++++++++++++++---------------- src/index.ts | 1 - src/union.ts | 14 ++-- 3 files changed, 85 insertions(+), 71 deletions(-) diff --git a/src/__tests__/union.test.ts b/src/__tests__/union.test.ts index aa056af3..1327c603 100644 --- a/src/__tests__/union.test.ts +++ b/src/__tests__/union.test.ts @@ -76,6 +76,28 @@ describe('union', () => { }); }); + describe('when none of the volumes are readable', () => { + it('throws error when calling a read method', () => { + const vol1 = Volume.fromJSON({ '/foo': 'bar1' }); + const vol2 = Volume.fromJSON({ '/foo': 'bar2' }); + const ufs = new Union(); + ufs.use(vol1 as any, { readable: false }).use(vol2 as any, { readable: false }); + + expect(() => ufs.readFileSync('/foo')).toThrowError(); + }); + }); + + describe('when none of the volumes are writable', () => { + it('throws error when calling a write method', () => { + const vol1 = Volume.fromJSON({ '/foo': 'bar' }); + const vol2 = Volume.fromJSON({ '/foo': 'bar' }); + const ufs = new Union(); + ufs.use(vol1 as any, { writable: false }).use(vol2 as any, { writable: false }); + + expect(() => ufs.writeFileSync('/foo', 'bar')).toThrowError(); + }); + }); + describe('readable/writable', () => { it('writes to the last vol added', () => { const vol1 = Volume.fromJSON({}); @@ -86,40 +108,24 @@ describe('union', () => { expect(vol2.readFileSync('/foo', 'utf8')).toEqual('bar'); }); - it('writes to the latest added volume with writable=false vol', () => { - const vol1 = Volume.fromJSON({}); - const vol2 = Volume.fromJSON({}); - const ufs = new Union(); - ufs.use(vol1 as any).use(vol2 as any, { writable: false }); - ufs.writeFileSync('/foo', 'bar'); - expect(vol1.readFileSync('/foo', 'utf8')).toEqual('bar'); - }); - it('writes to the latest added writable vol', () => { const vol1 = Volume.fromJSON({}); const vol2 = Volume.fromJSON({}); const ufs = new Union(); ufs.use(vol1 as any).use(vol2 as any, { readable: false }); ufs.writeFileSync('/foo', 'bar'); - expect(vol2.readFileSync('/foo', 'utf8')).toEqual('bar'); - }); - - it('throw error if write operation attempted with all volumes writable=false', () => { - const vol1 = Volume.fromJSON({ '/foo': 'bar' }); - const vol2 = Volume.fromJSON({ '/foo': 'bar' }); - const ufs = new Union(); - ufs.use(vol1 as any, { writable: false }).use(vol2 as any, { writable: false }); - expect(() => ufs.writeFileSync('/foo', 'bar')).toThrowError(); + expect(vol2.readFileSync('/foo', 'utf8')).toEqual('bar'); }); - it('throw error if read operation attempted with all volumes readable=false', () => { - const vol1 = Volume.fromJSON({ '/foo': 'bar1' }); - const vol2 = Volume.fromJSON({ '/foo': 'bar2' }); + it('reads from the latest added readable vol', () => { + const vol1 = Volume.fromJSON({'/foo': 'bar'}); + const vol2 = Volume.fromJSON({}); const ufs = new Union(); - ufs.use(vol1 as any, { readable: false }).use(vol2 as any, { readable: false }); + ufs.use(vol1 as any).use(vol2 as any, { readable: false }); - expect(() => ufs.readFileSync('/foo')).toThrowError(); + expect(ufs.readFileSync('/foo', 'utf8')).toEqual('bar'); + expect(vol1.readFileSync('/foo', 'utf8')).toEqual('bar'); }); }); @@ -276,6 +282,33 @@ describe('union', () => { }); }); + describe('when none of the volumes are readable', () => { + it('throws error when calling a read method', done => { + const vol1 = Volume.fromJSON({ '/foo': 'bar1' }); + const vol2 = Volume.fromJSON({ '/foo': 'bar2' }); + const ufs = new Union(); + ufs.use(vol1 as any, { readable: false }).use(vol2 as any, { readable: false }); + + ufs.readFile('/foo', 'utf8', (err, res) => { + expect(err).toBeInstanceOf(Error); + done(); + }); + }); + }); + + describe('when none of the volumes are writable', () => { + it('throws error when calling a write method', done => { + const vol1 = Volume.fromJSON({ '/foo': 'bar' }); + const vol2 = Volume.fromJSON({ '/foo': 'bar' }); + const ufs = new Union(); + ufs.use(vol1 as any, { writable: false }).use(vol2 as any, { writable: false }); + ufs.writeFile('/foo', 'bar', (err) => { + expect(err).toBeInstanceOf(Error); + done(); + }); + }); + }); + describe('readable/writable', () => { it('writes to the last vol added', done => { const vol1 = Volume.fromJSON({}); @@ -316,28 +349,6 @@ describe('union', () => { }); }); - it('throw error if write operation attempted with all volumes writable=false', done => { - const vol1 = Volume.fromJSON({ '/foo': 'bar' }); - const vol2 = Volume.fromJSON({ '/foo': 'bar' }); - const ufs = new Union(); - ufs.use(vol1 as any, { writable: false }).use(vol2 as any, { writable: false }); - ufs.writeFile('/foo', 'bar', (err) => { - expect(err).toBeInstanceOf(Error); - done(); - }); - }); - - it('throw error if read operation attempted with all volumes readable=false', done => { - const vol1 = Volume.fromJSON({ '/foo': 'bar1' }); - const vol2 = Volume.fromJSON({ '/foo': 'bar2' }); - const ufs = new Union(); - ufs.use(vol1 as any, { readable: false }).use(vol2 as any, { readable: false }); - - ufs.readFile('/foo', 'utf8', (err, res) => { - expect(err).toBeInstanceOf(Error); - done(); - }); - }); }); describe('readdir', () => { @@ -455,6 +466,28 @@ describe('union', () => { await expect(ufs.promises.readFile('/foo', 'utf8')).rejects.toThrowError(); }); + describe('when none of the volumes are readable', () => { + it('throws error when calling a read method', () => { + const vol1 = Volume.fromJSON({ '/foo': 'bar1' }); + const vol2 = Volume.fromJSON({ '/foo': 'bar2' }); + const ufs = new Union(); + ufs.use(vol1 as any, { readable: false }).use(vol2 as any, { readable: false }); + + expect(() => ufs.readFileSync('/foo')).toThrowError(); + }); + }); + + describe('when none of the volumes are writable', () => { + it('throws error when calling a write method', () => { + const vol1 = Volume.fromJSON({ '/foo': 'bar' }); + const vol2 = Volume.fromJSON({ '/foo': 'bar' }); + const ufs = new Union(); + ufs.use(vol1 as any, { writable: false }).use(vol2 as any, { writable: false }); + + expect(() => ufs.writeFileSync('/foo', 'bar')).toThrowError(); + }); + }); + describe('readable/writable', () => { it('writes to the last vol added', () => { const vol1 = Volume.fromJSON({}); @@ -482,24 +515,6 @@ describe('union', () => { ufs.writeFileSync('/foo', 'bar'); expect(vol2.readFileSync('/foo', 'utf8')).toEqual('bar'); }); - - it('throw error if write operation attempted with all volumes writable=false', () => { - const vol1 = Volume.fromJSON({ '/foo': 'bar' }); - const vol2 = Volume.fromJSON({ '/foo': 'bar' }); - const ufs = new Union(); - ufs.use(vol1 as any, { writable: false }).use(vol2 as any, { writable: false }); - - expect(() => ufs.writeFileSync('/foo', 'bar')).toThrowError(); - }); - - it('throw error if read operation attempted with all volumes readable=false', () => { - const vol1 = Volume.fromJSON({ '/foo': 'bar1' }); - const vol2 = Volume.fromJSON({ '/foo': 'bar2' }); - const ufs = new Union(); - ufs.use(vol1 as any, { readable: false }).use(vol2 as any, { readable: false }); - - expect(() => ufs.readFileSync('/foo')).toThrowError(); - }); }); describe('readdir', () => { diff --git a/src/index.ts b/src/index.ts index 571ef27e..4dd478b4 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,6 +1,5 @@ import { Union as _Union, VolOptions } from './union'; import { IFS } from './fs'; -export * from './lists' export interface IUnionFs extends IFS { use: (fs: IFS, options?: VolOptions) => this; diff --git a/src/union.ts b/src/union.ts index 031ebe29..35826c36 100644 --- a/src/union.ts +++ b/src/union.ts @@ -350,25 +350,25 @@ export class Union { return (...args: any[]) => { throw new Error(`Method not supported: "${method}" with args "${args}"`); }; - return (...args: any[]) => fs[method as string].apply(fs, args); + return (...args: any[]) => fs[method as string](...args); }; return { ...fs, ...fsSyncMethodsRead.reduce((acc, method) => { - acc[method] = readable ? createFunc(method) : createErroringFn('writable'); + acc[method] = readable ? createFunc(method) : createErroringFn('readable'); return acc; }, {}), ...fsSyncMethodsWrite.reduce((acc, method) => { - acc[method] = writable ? createFunc(method) : createErroringFn('readable'); + acc[method] = writable ? createFunc(method) : createErroringFn('writable'); return acc; }, {}), ...fsAsyncMethodsRead.reduce((acc, method) => { - acc[method] = readable ? createFunc(method) : createErroringFn('writable'); + acc[method] = readable ? createFunc(method) : createErroringFn('readable'); return acc; }, {}), ...fsAsyncMethodsWrite.reduce((acc, method) => { - acc[method] = writable ? createFunc(method) : createErroringFn('readable'); + acc[method] = writable ? createFunc(method) : createErroringFn('writable'); return acc; }, {}), promises: { @@ -381,7 +381,7 @@ export class Union { }; return acc; } - acc[method] = readable ? (...args: any) => promises[method as string].apply(fs, args) : createErroringFn('writable'); + acc[method] = readable ? (...args: any) => promises[method as string].apply(fs, args) : createErroringFn('readable'); return acc; }, {}), ...fsPromiseMethodsWrite.reduce((acc, method) => { @@ -392,7 +392,7 @@ export class Union { }; return acc; } - acc[method] = writable ? (...args: any) => promises[method as string].apply(fs, args) : createErroringFn('readable'); + acc[method] = writable ? (...args: any) => promises[method as string].apply(fs, args) : createErroringFn('writable'); return acc; }, {}), }, From 7bac293befdce9405b8ca1e93b50d62aa00cdda5 Mon Sep 17 00:00:00 2001 From: matt penrice Date: Wed, 22 Apr 2020 14:42:38 +0200 Subject: [PATCH 21/24] removes mkdirp from lists --- src/lists.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/lists.ts b/src/lists.ts index 23cc0f95..bf9f1a97 100644 --- a/src/lists.ts +++ b/src/lists.ts @@ -14,7 +14,6 @@ export const fsSyncMethodsWrite = [ 'lchownSync', 'linkSync', 'lstatSync', - 'mkdirpSync', 'mkdirSync', 'mkdtempSync', 'renameSync', @@ -72,7 +71,6 @@ export const fsAsyncMethodsWrite = [ 'link', 'lstat', 'mkdir', - 'mkdirp', 'mkdtemp', 'rename', 'rmdir', From 793b5bf058e47210502c249475034ad6fde210bf Mon Sep 17 00:00:00 2001 From: matt penrice Date: Wed, 22 Apr 2020 15:11:56 +0200 Subject: [PATCH 22/24] remove opendir from lists --- src/lists.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/lists.ts b/src/lists.ts index bf9f1a97..b2f655ad 100644 --- a/src/lists.ts +++ b/src/lists.ts @@ -86,7 +86,6 @@ export const fsAsyncMethodsWrite = [ export const fsPromiseMethodsRead = [ 'access', 'open', - 'opendir', 'readdir', 'readFile', 'readlink', From 3bf499316050bfe165b777c2aff8dbb4153a144c Mon Sep 17 00:00:00 2001 From: matt penrice Date: Wed, 22 Apr 2020 16:51:04 +0200 Subject: [PATCH 23/24] adds opendir back, oops --- src/lists.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/lists.ts b/src/lists.ts index b2f655ad..bf9f1a97 100644 --- a/src/lists.ts +++ b/src/lists.ts @@ -86,6 +86,7 @@ export const fsAsyncMethodsWrite = [ export const fsPromiseMethodsRead = [ 'access', 'open', + 'opendir', 'readdir', 'readFile', 'readlink', From f535a98140b0bcae082437a7264d658287a8b0d0 Mon Sep 17 00:00:00 2001 From: matt penrice Date: Fri, 24 Apr 2020 11:04:55 +0200 Subject: [PATCH 24/24] comments following pr --- .gitignore | 3 +- demo/writeonly.ts | 2 +- src/__tests__/union.test.ts | 106 +++++++++++++++++++++++------------- 3 files changed, 70 insertions(+), 41 deletions(-) diff --git a/.gitignore b/.gitignore index c50f9053..5c53b9d0 100644 --- a/.gitignore +++ b/.gitignore @@ -6,5 +6,4 @@ coverage package-lock.json /lib/ /demo/**/*.js -/src/**/*.js -*.test.ts.test \ No newline at end of file +/src/**/*.js \ No newline at end of file diff --git a/demo/writeonly.ts b/demo/writeonly.ts index 193bc4bd..ce5463d1 100644 --- a/demo/writeonly.ts +++ b/demo/writeonly.ts @@ -13,7 +13,7 @@ const vol1 = Volume.fromJSON({ }); const vol2 = Volume.fromJSON({}); -ufs.use(vol1 as any).use(vol2 as any, {writeonly: true}) +ufs.use(vol1 as any).use(vol2 as any, {writable: false}) ufs.writeFileSync('/foo', 'bar') console.log(vol2.readFileSync('/foo', 'utf8')) // bar diff --git a/src/__tests__/union.test.ts b/src/__tests__/union.test.ts index 1327c603..2ef50839 100644 --- a/src/__tests__/union.test.ts +++ b/src/__tests__/union.test.ts @@ -1,6 +1,7 @@ import { Union } from '..'; import { Volume, createFsFromVolume } from 'memfs'; import * as fs from 'fs'; +import { isMainThread } from 'worker_threads'; describe('union', () => { describe('Union', () => { @@ -77,7 +78,7 @@ describe('union', () => { }); describe('when none of the volumes are readable', () => { - it('throws error when calling a read method', () => { + it('throws an error when calling a read method', () => { const vol1 = Volume.fromJSON({ '/foo': 'bar1' }); const vol2 = Volume.fromJSON({ '/foo': 'bar2' }); const ufs = new Union(); @@ -88,7 +89,7 @@ describe('union', () => { }); describe('when none of the volumes are writable', () => { - it('throws error when calling a write method', () => { + it('throws an error when calling a write method', () => { const vol1 = Volume.fromJSON({ '/foo': 'bar' }); const vol2 = Volume.fromJSON({ '/foo': 'bar' }); const ufs = new Union(); @@ -105,10 +106,11 @@ describe('union', () => { const ufs = new Union(); ufs.use(vol1 as any).use(vol2 as any); ufs.writeFileSync('/foo', 'bar'); + expect(vol2.readFileSync('/foo', 'utf8')).toEqual('bar'); }); - it('writes to the latest added writable vol', () => { + it('writes to the last vol added', () => { const vol1 = Volume.fromJSON({}); const vol2 = Volume.fromJSON({}); const ufs = new Union(); @@ -118,6 +120,15 @@ describe('union', () => { expect(vol2.readFileSync('/foo', 'utf8')).toEqual('bar'); }); + it('reads from the last vol added', () => { + const vol1 = Volume.fromJSON({'/foo': 'bar'}); + const vol2 = Volume.fromJSON({'/foo': 'bar2'}); + const ufs = new Union(); + ufs.use(vol1 as any).use(vol2 as any); + + expect(ufs.readFileSync('/foo', 'utf8')).toEqual('bar2'); + }); + it('reads from the latest added readable vol', () => { const vol1 = Volume.fromJSON({'/foo': 'bar'}); const vol2 = Volume.fromJSON({}); @@ -125,7 +136,6 @@ describe('union', () => { ufs.use(vol1 as any).use(vol2 as any, { readable: false }); expect(ufs.readFileSync('/foo', 'utf8')).toEqual('bar'); - expect(vol1.readFileSync('/foo', 'utf8')).toEqual('bar'); }); }); @@ -201,7 +211,7 @@ describe('union', () => { expect(files.map(f => f.name)).toEqual(['bar', 'baz', 'zzz']); }); - it('throws error when all fss fail', () => { + it('throws an error when all fss fail', () => { const vol = Volume.fromJSON({}); const vol2 = Volume.fromJSON({}); @@ -283,7 +293,7 @@ describe('union', () => { }); describe('when none of the volumes are readable', () => { - it('throws error when calling a read method', done => { + it('throws an error when calling a read method', done => { const vol1 = Volume.fromJSON({ '/foo': 'bar1' }); const vol2 = Volume.fromJSON({ '/foo': 'bar2' }); const ufs = new Union(); @@ -297,7 +307,7 @@ describe('union', () => { }); describe('when none of the volumes are writable', () => { - it('throws error when calling a write method', done => { + it('throws an error when calling a write method', done => { const vol1 = Volume.fromJSON({ '/foo': 'bar' }); const vol2 = Volume.fromJSON({ '/foo': 'bar' }); const ufs = new Union(); @@ -323,32 +333,44 @@ describe('union', () => { }); }); - it('writes to the latest added volume without writable=false', done => { + it('writes to the latest added writable vol', done => { const vol1 = Volume.fromJSON({}); const vol2 = Volume.fromJSON({}); const ufs = new Union(); - ufs.use(vol1 as any).use(vol2 as any, { writable: false }); + ufs.use(vol1 as any).use(vol2 as any, {writable: false}); ufs.writeFile('/foo', 'bar', (err) => { - vol1.readFile('/foo', 'utf8', (err, res) => { + ufs.readFile('/foo', 'utf8', (err, res) => { expect(res).toEqual('bar'); - done(); + vol1.readFile('/foo', 'utf8', (err, res) => { + expect(res).toEqual('bar'); + done(); + }); }); }); }); - it('writes to the latest added writable vol', done => { - const vol1 = Volume.fromJSON({}); - const vol2 = Volume.fromJSON({}); + it('reads from the last vol added', done => { + const vol1 = Volume.fromJSON({'/foo': 'bar'}); + const vol2 = Volume.fromJSON({'/foo': 'bar2'}); const ufs = new Union(); - ufs.use(vol1 as any).use(vol2 as any, { readable: false }); - ufs.writeFile('/foo', 'bar', (err) => { - vol2.readFile('/foo', 'utf8', (err, res) => { - expect(res).toEqual('bar'); - done(); - }); + ufs.use(vol1 as any).use(vol2 as any); + ufs.readFile('/foo', 'utf8', (err, res) => { + expect(res).toEqual('bar2'); + done(); }); - }); + }) + + it('reads from the latest added readable vol', done => { + const vol1 = Volume.fromJSON({'/foo': 'bar'}); + const vol2 = Volume.fromJSON({'/foo': 'bar2'}); + const ufs = new Union(); + ufs.use(vol1 as any).use(vol2 as any, {readable: false}); + ufs.readFile('/foo', 'utf8', (err, res) => { + expect(res).toEqual('bar'); + done(); + }); + }) }); describe('readdir', () => { @@ -419,7 +441,7 @@ describe('union', () => { }); }); - it('throws error when all fss fail', done => { + it('throws an error when all fss fail', done => { const vol = Volume.fromJSON({}); const vol2 = Volume.fromJSON({}); @@ -467,54 +489,62 @@ describe('union', () => { }); describe('when none of the volumes are readable', () => { - it('throws error when calling a read method', () => { + it('throws an error when calling a read method', async () => { const vol1 = Volume.fromJSON({ '/foo': 'bar1' }); const vol2 = Volume.fromJSON({ '/foo': 'bar2' }); const ufs = new Union(); ufs.use(vol1 as any, { readable: false }).use(vol2 as any, { readable: false }); - expect(() => ufs.readFileSync('/foo')).toThrowError(); + await expect(ufs.promises.readFile('/foo')).rejects.toThrowError(); }); }); describe('when none of the volumes are writable', () => { - it('throws error when calling a write method', () => { + it('throws an error when calling a write method', async () => { const vol1 = Volume.fromJSON({ '/foo': 'bar' }); const vol2 = Volume.fromJSON({ '/foo': 'bar' }); const ufs = new Union(); ufs.use(vol1 as any, { writable: false }).use(vol2 as any, { writable: false }); - expect(() => ufs.writeFileSync('/foo', 'bar')).toThrowError(); + await expect(ufs.promises.writeFile('/foo', 'bar')).rejects.toThrowError(); }); }); describe('readable/writable', () => { - it('writes to the last vol added', () => { + it('writes to the last vol added', async () => { const vol1 = Volume.fromJSON({}); const vol2 = Volume.fromJSON({}); const ufs = new Union(); ufs.use(vol1 as any).use(vol2 as any); ufs.writeFileSync('/foo', 'bar'); - expect(vol2.readFileSync('/foo', 'utf8')).toEqual('bar'); + await expect(vol2.promises.readFile('/foo', 'utf8')).resolves.toEqual('bar'); }); - it('writes to the latest added volume without writable=false', () => { + it('writes to the latest added writable vol', async () => { const vol1 = Volume.fromJSON({}); const vol2 = Volume.fromJSON({}); const ufs = new Union(); ufs.use(vol1 as any).use(vol2 as any, { writable: false }); ufs.writeFileSync('/foo', 'bar'); - expect(vol1.readFileSync('/foo', 'utf8')).toEqual('bar'); + await expect(vol1.promises.readFile('/foo', 'utf8')).resolves.toEqual('bar'); }); - it('writes to the latest added writable vol', () => { - const vol1 = Volume.fromJSON({}); - const vol2 = Volume.fromJSON({}); + it('reads from the last vol added', async () => { + const vol1 = Volume.fromJSON({'/foo': 'bar'}); + const vol2 = Volume.fromJSON({'/foo': 'bar2'}); const ufs = new Union(); - ufs.use(vol1 as any).use(vol2 as any, { readable: false }); - ufs.writeFileSync('/foo', 'bar'); - expect(vol2.readFileSync('/foo', 'utf8')).toEqual('bar'); - }); + ufs.use(vol1 as any).use(vol2 as any); + await expect(ufs.promises.readFile('/foo', 'utf8')).resolves.toEqual('bar2'); + + }) + + it('reads from the latest added readable vol', async () => { + const vol1 = Volume.fromJSON({'/foo': 'bar'}); + const vol2 = Volume.fromJSON({'/foo': 'bar2'}); + const ufs = new Union(); + ufs.use(vol1 as any).use(vol2 as any, {readable: false}); + await expect(ufs.promises.readFile('/foo', 'utf8')).resolves.toEqual('bar'); + }) }); describe('readdir', () => { @@ -589,7 +619,7 @@ describe('union', () => { expect(files.map(f => f.name)).toEqual(['bar', 'baz', 'zzz']); }); - it('throws error when all fss fail', async () => { + it('throws an error when all fss fail', async () => { const vol = Volume.fromJSON({}); const vol2 = Volume.fromJSON({});