diff --git a/README.md b/README.md index 575d1eb53..365f08ddf 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# memfs +# memfs 2.0 In-memory file-system with [Node's `fs` API](https://nodejs.org/api/fs.html). @@ -12,6 +12,7 @@ In-memory file-system with [Node's `fs` API](https://nodejs.org/api/fs.html). - Implements *soft links* (aka symlinks, symbolic links) - More testing coming soon* - Permissions may* be implemented in the future + - Can be used in browser, see `memfs-webpack` Usage: @@ -67,6 +68,9 @@ import {Volume} from 'memfs'; const vol = Volume.fromJSON({'/foo': 'bar'}); vol.readFileSync('/foo'); // bar + +const vol2 = Volume.fromJSON({'/foo': 'bar 2'}); +vol2.readFileSync('/foo'); // bar 2 ``` Use `memfs` together with [`unionfs`][unionfs] to create one filesystem @@ -112,11 +116,18 @@ filesystem operations but are slightly different. import {vol, fs} from 'memfs'; ``` -`vol` is an instance of `Volume` constructor, it is a default volume created -for your convenience. `fs` in and *fs-like* object created from `vol` using +`vol` is an instance of `Volume` constructor, it is the default volume created +for your convenience. `fs` is an *fs-like* object created from `vol` using `createFsFromVolume(vol)`, see reference below. -#### `Volume` +All contents of the `fs` object are also exported individually, so you can use +`memfs` just like you would use the `fs` module: + +```js +import {readFileSync, F_OK, ReadStream} from 'memfs'; +``` + +#### `Volume` Constructor `Volume` is a constructor function for creating new volumes: @@ -176,105 +187,132 @@ Exports the whole contents of the volume recursively to a flat JSON object. If this argument is omitted, the whole volume is exported. `paths` can be an array of paths. A path can be a string, `Buffer` or an `URL` object. -`json` is an optional object parameter where the list of files will be added. +`json` is an optional object parameter which will be populated with the exported files. `isRelative` is boolean that specifies if returned paths should be relative. ###### `vol.mkdirp(path, callback)` -Creates a directory tree recursively. `path` is specifies a directory to +Creates a directory tree recursively. `path` specifies a directory to create and can be a string, `Buffer`, or an `URL` object. `callback` is called on completion and may receive only one argument - an `Error` object. ###### `vol.mkdirpSync(path)` -A synchronous version of `vol.mkdirp`. This method throws. - -#### FS API Status - - - [x] Constants - - [x] `FSWatcher` - - [x] `ReadStream` - - [x] `WriteStream` - - [x] `Stats` - - [x] `access(path[, mode], callback)` - - Does not check permissions - - [x] `accessSync(path[, mode])` - - Does not check permissions - - [x] `appendFile(file, data[, options], callback)` - - [x] `appendFileSync(file, data[, options])` - - [x] `chmod(path, mode, callback)` - - [x] `chmodSync(path, mode)` - - [x] `chown(path, uid, gid, callback)` - - [x] `chownSync(path, uid, gid)` - - [x] `close(fd, callback)` - - [x] `closeSync(fd)` - - [x] `createReadStream(path[, options])` - - [x] `createWriteStream(path[, options])` - - [x] `exists(path, callback)` - - [x] `existsSync(path)` - - [x] `fchmod(fd, mode, callback)` - - [x] `fchmodSync(fd, mode)` - - [x] `fchown(fd, uid, gid, callback)` - - [x] `fchownSync(fd, uid, gid)` - - [x] `fdatasync(fd, callback)` - - [x] `fdatasyncSync(fd)` - - [x] `fstat(fd, callback)` - - [x] `fstatSync(fd)` - - [x] `fsync(fd, callback)` - - [x] `fsyncSync(fd)` - - [x] `ftruncate(fd[, len], callback)` - - [x] `ftruncateSync(fd[, len])` - - [x] `futimes(fd, atime, mtime, callback)` - - [x] `futimesSync(fd, atime, mtime)` - - [x] `lchmod(path, mode, callback)` - - [x] `lchmodSync(path, mode)` - - [x] `lchown(path, uid, gid, callback)` - - [x] `lchownSync(path, uid, gid)` - - [x] `link(existingPath, newPath, callback)` - - [x] `linkSync(existingPath, newPath)` - - [x] `lstat(path, callback)` - - [x] `lstatSync(path)` - - [x] `mkdir(path[, mode], callback)` - - [x] `mkdirSync(path[, mode])` - - [x] `mkdtemp(prefix[, options], callback)` - - [x] `mkdtempSync(prefix[, options])` - - [x] `open(path, flags[, mode], callback)` - - [x] `openSync(path, flags[, mode])` - - [x] `read(fd, buffer, offset, length, position, callback)` - - [x] `readSync(fd, buffer, offset, length, position)` - - [x] `readdir(path[, options], callback)` - - [x] `readdirSync(path[, options])` - - [x] `readFile(path[, options], callback)` - - [x] `readFileSync(path[, options])` - - [x] `readlink(path[, options], callback)` - - [x] `readlinkSync(path[, options])` - - [x] `realpath(path[, options], callback)` - - [x] `realpathSync(path[, options])` - - Caching not implemented - - [x] `rename(oldPath, newPath, callback)` - - [x] `renameSync(oldPath, newPath)` - - [x] `rmdir(path, callback)` - - [x] `rmdirSync(path)` - - [x] `stat(path, callback)` - - [x] `statSync(path)` - - [x] `symlink(target, path[, type], callback)` - - [x] `symlinkSync(target, path[, type])` - - [x] `truncate(path[, len], callback)` - - [x] `truncateSync(path[, len])` - - [x] `unlink(path, callback)` - - [x] `unlinkSync(path)` - - [x] `utimes(path, atime, mtime, callback)` - - [x] `utimesSync(path, atime, mtime)` - - [x] `watch(filename[, options][, listener])` - - [x] `watchFile(filename[, options], listener)` - - [x] `unwatchFile(filename[, listener])` - - [x] `write(fd, buffer[, offset[, length[, position]]], callback)` - - [x] `write(fd, string[, position[, encoding]], callback)` - - [x] `writeFile(file, data[, options], callback)` - - [x] `writeFileSync(file, data[, options])` - - [x] `writeSync(fd, buffer[, offset[, length[, position]]])` - - [x] `writeSync(fd, string[, position[, encoding]])` +A synchronous version of `vol.mkdirp()`. This method throws. + +#### `createFsFromVolume(vol)` + +Returns an *fs-like* object created from a `Volume` instance `vol. + +```js +import {createFsFromVolume, Volume} from 'memfs'; + +const vol = new Volume; +const fs = createFsFromVolume(vol); +``` + +The idea behind the *fs-like* object is to make it identical to the one +you get from `require('fs')`. Here are some things this function does: + + - Binds all methods, so you can do: + + ```js + const {createFileSync, readFileSync} = fs; + ``` + + - Adds constructor functions `fs.Stats`, `fs.ReadStream`, `fs.WriteStream`, `fs.FileWatcher`, `fs.FSWatcher`. + - Adds constants `fs.constants`, `fs.F_OK`, etc. + +# API Status + +All of the [Node's `fs` API](https://nodejs.org/api/fs.html) is implemented. +Some error messages may be inaccurate. File permissions are currently not +implemented (you have access to any file), basically `fs.access()` is a no-op. + + - [x] Constants + - [x] `FSWatcher` + - [x] `ReadStream` + - [x] `WriteStream` + - [x] `Stats` + - [x] `access(path[, mode], callback)` + - Does not check permissions + - [x] `accessSync(path[, mode])` + - Does not check permissions + - [x] `appendFile(file, data[, options], callback)` + - [x] `appendFileSync(file, data[, options])` + - [x] `chmod(path, mode, callback)` + - [x] `chmodSync(path, mode)` + - [x] `chown(path, uid, gid, callback)` + - [x] `chownSync(path, uid, gid)` + - [x] `close(fd, callback)` + - [x] `closeSync(fd)` + - [x] `createReadStream(path[, options])` + - [x] `createWriteStream(path[, options])` + - [x] `exists(path, callback)` + - [x] `existsSync(path)` + - [x] `fchmod(fd, mode, callback)` + - [x] `fchmodSync(fd, mode)` + - [x] `fchown(fd, uid, gid, callback)` + - [x] `fchownSync(fd, uid, gid)` + - [x] `fdatasync(fd, callback)` + - [x] `fdatasyncSync(fd)` + - [x] `fstat(fd, callback)` + - [x] `fstatSync(fd)` + - [x] `fsync(fd, callback)` + - [x] `fsyncSync(fd)` + - [x] `ftruncate(fd[, len], callback)` + - [x] `ftruncateSync(fd[, len])` + - [x] `futimes(fd, atime, mtime, callback)` + - [x] `futimesSync(fd, atime, mtime)` + - [x] `lchmod(path, mode, callback)` + - [x] `lchmodSync(path, mode)` + - [x] `lchown(path, uid, gid, callback)` + - [x] `lchownSync(path, uid, gid)` + - [x] `link(existingPath, newPath, callback)` + - [x] `linkSync(existingPath, newPath)` + - [x] `lstat(path, callback)` + - [x] `lstatSync(path)` + - [x] `mkdir(path[, mode], callback)` + - [x] `mkdirSync(path[, mode])` + - [x] `mkdtemp(prefix[, options], callback)` + - [x] `mkdtempSync(prefix[, options])` + - [x] `open(path, flags[, mode], callback)` + - [x] `openSync(path, flags[, mode])` + - [x] `read(fd, buffer, offset, length, position, callback)` + - [x] `readSync(fd, buffer, offset, length, position)` + - [x] `readdir(path[, options], callback)` + - [x] `readdirSync(path[, options])` + - [x] `readFile(path[, options], callback)` + - [x] `readFileSync(path[, options])` + - [x] `readlink(path[, options], callback)` + - [x] `readlinkSync(path[, options])` + - [x] `realpath(path[, options], callback)` + - [x] `realpathSync(path[, options])` + - Caching not implemented + - [x] `rename(oldPath, newPath, callback)` + - [x] `renameSync(oldPath, newPath)` + - [x] `rmdir(path, callback)` + - [x] `rmdirSync(path)` + - [x] `stat(path, callback)` + - [x] `statSync(path)` + - [x] `symlink(target, path[, type], callback)` + - [x] `symlinkSync(target, path[, type])` + - [x] `truncate(path[, len], callback)` + - [x] `truncateSync(path[, len])` + - [x] `unlink(path, callback)` + - [x] `unlinkSync(path)` + - [x] `utimes(path, atime, mtime, callback)` + - [x] `utimesSync(path, atime, mtime)` + - [x] `watch(filename[, options][, listener])` + - [x] `watchFile(filename[, options], listener)` + - [x] `unwatchFile(filename[, listener])` + - [x] `write(fd, buffer[, offset[, length[, position]]], callback)` + - [x] `write(fd, string[, position[, encoding]], callback)` + - [x] `writeFile(file, data[, options], callback)` + - [x] `writeFileSync(file, data[, options])` + - [x] `writeSync(fd, buffer[, offset[, length[, position]]])` + - [x] `writeSync(fd, string[, position[, encoding]])` [npm-url]: https://www.npmjs.com/package/memfs @@ -283,3 +321,34 @@ A synchronous version of `vol.mkdirp`. This method throws. [unionfs]: https://github.com/streamich/unionfs [linkfs]: https://github.com/streamich/linkfs [fs-monkey]: https://github.com/streamich/fs-monkey + + + + + +# License + +This is free and unencumbered software released into the public domain. + +Anyone is free to copy, modify, publish, use, compile, sell, or +distribute this software, either in source code form or as a compiled +binary, for any purpose, commercial or non-commercial, and by any +means. + +In jurisdictions that recognize copyright laws, the author or authors +of this software dedicate any and all copyright interest in the +software to the public domain. We make this dedication for the benefit +of the public at large and to the detriment of our heirs and +successors. We intend this dedication to be an overt act of +relinquishment in perpetuity of all present and future rights to this +software under copyright law. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR +OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, +ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +OTHER DEALINGS IN THE SOFTWARE. + +For more information, please refer to diff --git a/demo/mountSync.ts b/demo/mountSync.ts new file mode 100644 index 000000000..d93e17004 --- /dev/null +++ b/demo/mountSync.ts @@ -0,0 +1,10 @@ +import {Volume} from '../src/index'; + + +const vol = new Volume; +vol.mountSync('/test', { + 'foo': 'bar', +}); + + +console.log(vol.toJSON()); \ No newline at end of file diff --git a/lib/index.js b/lib/index.js index c488881c3..0b2d16666 100644 --- a/lib/index.js +++ b/lib/index.js @@ -8,45 +8,31 @@ var __assign = (this && this.__assign) || Object.assign || function(t) { return t; }; Object.defineProperty(exports, "__esModule", { value: true }); -var constants_1 = require("./constants"); var node_1 = require("./node"); var volume_1 = require("./volume"); -exports.constants = constants_1.constants; -exports.F_OK = constants_1.constants.F_OK; -exports.R_OK = constants_1.constants.R_OK; -exports.W_OK = constants_1.constants.W_OK; -exports.X_OK = constants_1.constants.X_OK; +var volume = require("./volume"); +var _a = require('fs-money/lib/util/lists'), fsSyncMethods = _a.fsSyncMethods, fsAsyncMethods = _a.fsAsyncMethods; +var constants_1 = require("./constants"); +var F_OK = constants_1.constants.F_OK, R_OK = constants_1.constants.R_OK, W_OK = constants_1.constants.W_OK, X_OK = constants_1.constants.X_OK; +exports.Volume = volume_1.Volume; // Default volume. exports.vol = new volume_1.Volume; -// List of `fs.js` methods, used to export bound (`.bind`) method list, just like `require('fs')` would do. -var FS_METHODS = [ - 'open', 'openSync', - 'close', 'closeSync', - 'readFile', 'readFileSync', - 'write', 'writeSync', - 'writeFile', 'writeFileSync', - 'link', 'linkSync', - 'unlink', 'unlinkSync', - 'symlink', 'symlinkSync', - 'realpath', 'realpathSync', - 'stat', 'statSync', - 'lstat', 'lstatSync', - 'fstat', 'fstatSync', - 'rename', 'renameSync', - 'exists', 'existsSync', - 'access', 'accessSync', - 'readdir', 'readdirSync', - 'watchFile', 'unwatchFile', - 'createReadStream', -]; -function createFsFromVolume(volume) { - var fs = {}; +function createFsFromVolume(vol) { + var fs = { F_OK: F_OK, R_OK: R_OK, W_OK: W_OK, X_OK: X_OK, constants: constants_1.constants, Stats: node_1.Stats }; // Bind FS methods. - for (var _i = 0, FS_METHODS_1 = FS_METHODS; _i < FS_METHODS_1.length; _i++) { - var method = FS_METHODS_1[_i]; - fs[method] = volume[method].bind(volume); + for (var _i = 0, fsSyncMethods_1 = fsSyncMethods; _i < fsSyncMethods_1.length; _i++) { + var method = fsSyncMethods_1[_i]; + fs[method] = vol[method].bind(vol); + } + for (var _a = 0, fsAsyncMethods_1 = fsAsyncMethods; _a < fsAsyncMethods_1.length; _a++) { + var method = fsAsyncMethods_1[_a]; + fs[method] = vol[method].bind(vol); } - fs.Stats = node_1.Stats; + fs.StatWatcher = volume_1.StatWatcher.bind(null, vol); + fs.FSWatcher = volume_1.FSWatcher.bind(null, vol); + fs.WriteStream = volume.WriteStream.bind(null, vol); + fs.ReadStream = volume.ReadStream.bind(null, vol); + fs._toUnixTimestamp = volume_1.toUnixTimestamp; return fs; } exports.createFsFromVolume = createFsFromVolume; diff --git a/lib/volume.js b/lib/volume.js index 1cdb3bddc..e1bc5a7fe 100644 --- a/lib/volume.js +++ b/lib/volume.js @@ -211,9 +211,10 @@ var getRealpathOptions = optsGenerator(realpathDefaults); var getRealpathOptsAndCb = optsAndCbGenerator(getRealpathOptions); // ---------------------------------------- Utility functions function pathToFilename(path) { - // TODO: Add support for the new URL object. - if ((typeof path !== 'string') && !buffer_1.Buffer.isBuffer(path)) - throw new TypeError(ERRSTR.PATH_STR); + if ((typeof path !== 'string') && !buffer_1.Buffer.isBuffer(path)) { + if (!require('url') || !(path instanceof require('url').URL)) + throw new TypeError(ERRSTR.PATH_STR); + } var pathString = String(path); nullCheck(pathString); return pathString; @@ -319,6 +320,7 @@ function toUnixTimestamp(time) { } throw new Error('Cannot parse time: ' + time); } +exports.toUnixTimestamp = toUnixTimestamp; /** * Returns optional argument and callback * @param arg Argument or callback value @@ -435,14 +437,46 @@ var Volume = (function () { throwError(ENOENT, funcName, filename); return link; }; + // Just like `getLink`, but also dereference/resolves symbolic links. + Volume.prototype.getResolvedLink = function (filenameOrSteps) { + var steps = typeof filenameOrSteps === 'string' + ? filenameToSteps(filenameOrSteps) + : filenameOrSteps; + var link = this.root; + var i = 0; + while (i < steps.length) { + var step = steps[i]; + link = link.getChild(step); + if (!link) + return null; + var node = link.getNode(); + if (node.isSymlink()) { + steps = node.symlink.concat(steps.slice(i + 1)); + link = this.root; + i = 0; + continue; + } + i++; + } + return link; + }; // Just like `getLinkOrThrow`, but also dereference/resolves symbolic links. Volume.prototype.getResolvedLinkOrThrow = function (filename, funcName) { - var link = this.getLinkOrThrow(filename, funcName); - link = this.resolveSymlinks(link); + var link = this.getResolvedLink(filename); if (!link) throwError(ENOENT, funcName, filename); return link; }; + Volume.prototype.resolveSymlinks = function (link) { + // let node: Node = link.getNode(); + // while(link && node.isSymlink()) { + // link = this.getLink(node.symlink); + // if(!link) return null; + // node = link.getNode(); + // } + // return link; + return this.getResolvedLink(link.steps.slice(1)); + }; // Just like `getLinkOrThrow`, but also verifies that the link is a directory. Volume.prototype.getLinkAsDirOrThrow = function (filename, funcName) { var link = this.getLinkOrThrow(filename, funcName); @@ -463,16 +497,6 @@ var Volume = (function () { throwError(ENOTDIR, funcName, path_1.sep + steps.join(path_1.sep)); return link; }; - Volume.prototype.resolveSymlinks = function (link) { - var node = link.getNode(); - while (link && node.isSymlink()) { - link = this.getLink(node.symlink); - if (!link) - return null; - node = link.getNode(); - } - return link; - }; Volume.prototype.getFileByFd = function (fd) { return this.fds[String(fd)]; }; @@ -520,23 +544,50 @@ var Volume = (function () { } }); }; - Volume.prototype._toJSON = function (link, json) { + Volume.prototype._toJSON = function (link, json, path) { if (link === void 0) { link = this.root; } if (json === void 0) { json = {}; } for (var name_2 in link.children) { var child = link.getChild(name_2); var node = child.getNode(); if (node.isFile()) { - json[child.getPath()] = node.getString(); + var filename = child.getPath(); + if (path) + filename = path_1.relative(path, filename); + json[filename] = node.getString(); } else if (node.isDirectory()) { - this._toJSON(child, json); + this._toJSON(child, json, path); } } return json; }; - Volume.prototype.toJSON = function () { - return this._toJSON(); + Volume.prototype.toJSON = function (paths, json, isRelative) { + if (json === void 0) { json = {}; } + if (isRelative === void 0) { isRelative = false; } + var links = []; + if (paths) { + if (!(paths instanceof Array)) + paths = [paths]; + for (var _i = 0, paths_1 = paths; _i < paths_1.length; _i++) { + var path_2 = paths_1[_i]; + var filename = pathToFilename(path_2); + var link = this.getResolvedLink(filename); + if (!link) + continue; + links.push(link); + } + } + else { + links.push(this.root); + } + if (!links.length) + return json; + for (var _a = 0, links_1 = links; _a < links_1.length; _a++) { + var link = links_1[_a]; + this._toJSON(link, json, isRelative ? link.getPath() : ''); + } + return json; }; // fromJSON(json: {[filename: string]: string}, cwd: string = '/') { Volume.prototype.fromJSON = function (json, cwd) { @@ -552,9 +603,9 @@ var Volume = (function () { this.writeFileSync(filename, data); } }; + // Legacy interface Volume.prototype.mountSync = function (mountpoint, json) { - var json2 = {}; - // this.importJSON(json); + this.fromJSON(json, mountpoint); }; Volume.prototype.openLink = function (link, flagsNum, resolveSymlinks) { if (resolveSymlinks === void 0) { resolveSymlinks = true; } @@ -577,13 +628,15 @@ var Volume = (function () { file.truncate(); return file; }; - Volume.prototype.openFile = function (fileName, flagsNum, modeNum, resolveSymlinks) { + Volume.prototype.openFile = function (filename, flagsNum, modeNum, resolveSymlinks) { if (resolveSymlinks === void 0) { resolveSymlinks = true; } - var steps = filenameToSteps(fileName); - var link = this.getLink(steps); + var steps = filenameToSteps(filename); + // let link: Link = this.getLink(steps); + var link = this.getResolvedLink(steps); // Try creating a new file, if it does not exist. if (!link) { - var dirLink = this.getLinkParent(steps); + // const dirLink: Link = this.getLinkParent(steps); + var dirLink = this.getResolvedLink(steps.slice(0, steps.length - 1)); if ((flagsNum & O_CREAT) && (typeof modeNum === 'number')) { link = this.createLink(dirLink, steps[steps.length - 1], false, modeNum); } @@ -595,7 +648,7 @@ var Volume = (function () { if (resolveSymlinks === void 0) { resolveSymlinks = true; } var file = this.openFile(filename, flagsNum, modeNum, resolveSymlinks); if (!file) - throw createError(ENOENT, 'open', filename); + throwError(ENOENT, 'open', filename); return file.fd; }; Volume.prototype.openSync = function (path, flags, mode) { diff --git a/lib/volume.test.js b/lib/volume.test.js index 2809bc81f..dc2b7325b 100644 --- a/lib/volume.test.js +++ b/lib/volume.test.js @@ -88,6 +88,43 @@ describe('volume', function () { '/dir/dir2/hello.txt': 'world', }); }); + it('Specify export path', function () { + var vol = volume_1.Volume.fromJSON({ + '/foo': 'bar', + '/dir/a': 'b', + }); + chai_1.expect(vol.toJSON('/dir')).to.eql({ + '/dir/a': 'b', + }); + }); + it('Specify multiple export paths', function () { + var vol = volume_1.Volume.fromJSON({ + '/foo': 'bar', + '/dir/a': 'b', + '/dir2/a': 'b', + '/dir2/c': 'd', + }); + chai_1.expect(vol.toJSON(['/dir2', '/dir'])).to.eql({ + '/dir/a': 'b', + '/dir2/a': 'b', + '/dir2/c': 'd', + }); + }); + it('Accumulate exports on supplied object', function () { + var vol = volume_1.Volume.fromJSON({ + '/foo': 'bar', + }); + var obj = {}; + chai_1.expect(vol.toJSON('/', obj)).to.equal(obj); + }); + it('Export empty volume', function () { + var vol = volume_1.Volume.fromJSON({}); + chai_1.expect(vol.toJSON()).to.eql({}); + }); + it('Exporting non-existing path', function () { + var vol = volume_1.Volume.fromJSON({}); + chai_1.expect(vol.toJSON('/lol')).to.eql({}); + }); }); describe('.fromJSON(json[, cwd])', function () { it('Files at root', function () { @@ -105,7 +142,7 @@ describe('volume', function () { 'hello': 'world', 'app.js': 'console.log(123)', }; - vol.fromJSON(json); + vol.fromJSON(json, '/'); chai_1.expect(vol.toJSON()).to.eql({ '/hello': 'world', '/app.js': 'console.log(123)', @@ -503,6 +540,32 @@ describe('volume', function () { vol.symlinkSync('/jquery.js', '/test2.js'); chai_1.expect(vol.readFileSync('/test2.js').toString()).to.equal(data); }); + describe('Complex, deep, multi-step symlinks get resolved', function () { + it('Symlink to a folder', function () { + var vol = volume_1.Volume.fromJSON({ '/a1/a2/a3/a4/a5/hello.txt': 'world!' }); + vol.symlinkSync('/a1', '/b1'); + chai_1.expect(vol.readFileSync('/b1/a2/a3/a4/a5/hello.txt', 'utf8')).to.equal('world!'); + }); + it('Symlink to a folder to a folder', function () { + var vol = volume_1.Volume.fromJSON({ '/a1/a2/a3/a4/a5/hello.txt': 'world!' }); + vol.symlinkSync('/a1', '/b1'); + vol.symlinkSync('/b1', '/c1'); + vol.openSync('/c1/a2/a3/a4/a5/hello.txt', 'r'); + }); + it('Multiple hops to folders', function () { + var vol = volume_1.Volume.fromJSON({ + '/a1/a2/a3/a4/a5/hello.txt': 'world a', + '/b1/b2/b3/b4/b5/hello.txt': 'world b', + '/c1/c2/c3/c4/c5/hello.txt': 'world c', + }); + vol.symlinkSync('/a1/a2', '/b1/l'); + vol.symlinkSync('/b1/l', '/b1/b2/b3/ok'); + vol.symlinkSync('/b1/b2/b3/ok', '/c1/a'); + vol.symlinkSync('/c1/a', '/c1/c2/c3/c4/c5/final'); + vol.openSync('/c1/c2/c3/c4/c5/final/a3/a4/a5/hello.txt', 'r'); + chai_1.expect(vol.readFileSync('/c1/c2/c3/c4/c5/final/a3/a4/a5/hello.txt', 'utf8')).to.equal('world a'); + }); + }); }); describe('.symlink(target, path[, type], callback)', function () { xit('...'); diff --git a/package.json b/package.json index feeb42ca6..0e7d849f8 100644 --- a/package.json +++ b/package.json @@ -1,27 +1,39 @@ { "name": "memfs", - "version": "2.0.1", + "version": "2.0.3", "description": "In-memory file-system with Node's fs API.", "main": "lib/index.js", "keywords": [ - "fs", "fs.js", "memory-fs", "memfs", - "file", "file system", "mount", "memory", - "in-memory", "virtual", "test", "testing", "mock"], + "fs", + "fs.js", + "memory-fs", + "memfs", + "file", + "file system", + "mount", + "memory", + "in-memory", + "virtual", + "test", + "testing", + "mock" + ], "repository": { - "type" : "git", - "url" : "https://github.com/streamich/memfs.git" + "type": "git", + "url": "https://github.com/streamich/memfs.git" }, "scripts": { - "build": "npm run build-ts && npm run build-js", - "build-ts": "./node_modules/.bin/gulp build-ts", - "build-js": "./node_modules/.bin/babel src --out-dir lib", - "test": "npm run test-coverage", - "test-basic": "./node_modules/.bin/mocha --require ts-node/register src/**/*.test.ts src/**/*.test.tsx", - "test-watch": "./node_modules/.bin/mocha --require ts-node/register src/**/*.test.ts src/**/*.test.tsx --watch", - "test-coverage": "nyc --per-file mocha --require ts-node/register --require source-map-support/register --full-trace --bail src/**/*.test.ts" + "build": "npm run build-ts && npm run build-js", + "build-ts": "./node_modules/.bin/gulp build-ts", + "build-js": "./node_modules/.bin/babel src --out-dir lib", + "test": "npm run test-coverage", + "test-basic": "./node_modules/.bin/mocha --require ts-node/register src/**/*.test.ts src/**/*.test.tsx", + "test-watch": "./node_modules/.bin/mocha --require ts-node/register src/**/*.test.ts src/**/*.test.tsx --watch", + "test-coverage": "nyc --per-file mocha --require ts-node/register --require source-map-support/register --full-trace --bail src/**/*.test.ts" }, "dependencies": { - "fast-extend": "0.0.2" + "fast-extend": "0.0.2", + "fs-monkey": "0.1.1" }, "devDependencies": { "mocha": "3.4.2", @@ -34,7 +46,6 @@ "gulp-typescript": "3.2.1", "source-map-support": "0.4.15", "nyc": "11.1.0", - "@types/mocha": "2.2.41", "@types/chai": "4.0.1", "@types/node": "8.0.17" diff --git a/src/index.ts b/src/index.ts index 40ce15cd9..b5e89c712 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,59 +1,45 @@ -import {constants as consts} from './constants'; import {Stats} from './node'; -import {Volume as _Volume} from './volume'; +import {Volume as _Volume, StatWatcher, FSWatcher, toUnixTimestamp} from './volume'; +import * as volume from './volume'; +const {fsSyncMethods, fsAsyncMethods} = require('fs-money/lib/util/lists'); +import {constants} from './constants'; +const {F_OK, R_OK, W_OK, X_OK} = constants; export const Volume = _Volume; -export const constants = consts; -export const F_OK = consts.F_OK; -export const R_OK = consts.R_OK; -export const W_OK = consts.W_OK; -export const X_OK = consts.X_OK; - - // Default volume. export const vol = new _Volume; -// List of `fs.js` methods, used to export bound (`.bind`) method list, just like `require('fs')` would do. -const FS_METHODS = [ - 'open', 'openSync', - 'close', 'closeSync', - 'readFile', 'readFileSync', - 'write', 'writeSync', - 'writeFile', 'writeFileSync', - 'link', 'linkSync', - 'unlink', 'unlinkSync', - 'symlink', 'symlinkSync', - 'realpath', 'realpathSync', - 'stat', 'statSync', - 'lstat', 'lstatSync', - 'fstat', 'fstatSync', - 'rename', 'renameSync', - 'exists', 'existsSync', - 'access', 'accessSync', - 'readdir', 'readdirSync', - 'watchFile', 'unwatchFile', - 'createReadStream', -]; - export interface IFs extends _Volume { + constants: typeof constants, Stats: new (...args) => Stats, + StatWatcher, + FSWatcher, + ReadStream, + WriteStream, + _toUnixTimestamp, } -export function createFsFromVolume(volume: _Volume): IFs { - const fs = {} as any as IFs; +export function createFsFromVolume(vol: _Volume): IFs { + const fs = {F_OK, R_OK, W_OK, X_OK, constants, Stats} as any as IFs; // Bind FS methods. - for(const method of FS_METHODS) { - fs[method] = volume[method].bind(volume); - } + for(const method of fsSyncMethods) fs[method] = vol[method].bind(vol); + for(const method of fsAsyncMethods) fs[method] = vol[method].bind(vol); + + fs.StatWatcher = StatWatcher.bind(null, vol); + fs.FSWatcher = FSWatcher.bind(null, vol); + fs.WriteStream = (volume as any).WriteStream.bind(null, vol); + fs.ReadStream = (volume as any).ReadStream.bind(null, vol); + + fs._toUnixTimestamp = toUnixTimestamp; - fs.Stats = Stats; return fs; } export const fs: IFs = createFsFromVolume(vol); +declare let module; module.exports = {...module.exports, ...fs}; diff --git a/src/volume.ts b/src/volume.ts index 4842ac5ef..28015c85f 100644 --- a/src/volume.ts +++ b/src/volume.ts @@ -402,7 +402,7 @@ function isFd(path): boolean { } // converts Date or number to a fractional UNIX timestamp -function toUnixTimestamp(time) { +export function toUnixTimestamp(time) { if(typeof time === 'string' && (+time == (time as any))) { return +time; }