├── .gitignore ├── .nvmrc ├── .travis.yml ├── .vscode └── settings.json ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── codecov.yml ├── demo ├── 12.ts ├── JSON.ts ├── createReadStream.ts ├── createWriteStream.ts ├── localstorage.ts ├── mountSync.ts ├── permissions.ts ├── readFileSync.ts ├── relative-path.ts ├── rename.ts ├── symlink.ts ├── throw-error.ts ├── toJSON.ts ├── watch.ts └── write.ts ├── docs ├── api-status.md ├── dependencies.md ├── reference.md └── relative-paths.md ├── package-lock.json ├── package.json ├── prettier.config.js ├── renovate.json ├── src ├── Dirent.ts ├── Stats.ts ├── __tests__ │ ├── hasBigInt.js │ ├── index.test.ts │ ├── node.test.ts │ ├── process.test.ts │ ├── promises.test.ts │ ├── setImmediate.test.ts │ ├── setTimeoutUnref.test.ts │ ├── util.ts │ ├── volume.test.ts │ ├── volume │ │ ├── ReadStream.test.ts │ │ ├── WriteStream.test.ts │ │ ├── __snapshots__ │ │ │ ├── mkdirSync.test.ts.snap │ │ │ ├── renameSync.test.ts.snap │ │ │ └── writeSync.test.ts.snap │ │ ├── appendFile.test.ts │ │ ├── appendFileSync.test.ts │ │ ├── closeSync.test.ts │ │ ├── copyFile.test.ts │ │ ├── copyFileSync.test.ts │ │ ├── exists.test.ts │ │ ├── existsSync.test.ts │ │ ├── mkdirSync.test.ts │ │ ├── openSync.test.ts │ │ ├── readSync.test.ts │ │ ├── readdirSync.test.ts │ │ ├── rename.test.ts │ │ ├── renameSync.test.ts │ │ ├── write.test.ts │ │ ├── writeFileSync.test.ts │ │ └── writeSync.test.ts │ └── wasmfs.test.ts ├── constants.ts ├── encoding.ts ├── getBigInt.js ├── index.ts ├── internal │ └── errors.ts ├── node.ts ├── process.ts ├── promises.ts ├── setImmediate.ts ├── setTimeoutUnref.ts ├── volume-localstorage.ts ├── volume.ts └── wasm.ts ├── tsconfig.json ├── tslint.json └── yarn.lock /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | node_modules 3 | /.idea 4 | .nyc_output 5 | coverage 6 | /lib/ 7 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | 8.16.1 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | os: 3 | - linux 4 | cache: 5 | yarn: true 6 | directories: 7 | - ~/.npm 8 | notifications: 9 | email: false 10 | node_js: 11 | - '11' 12 | - '10' 13 | - '8' 14 | script: 15 | - yarn test 16 | - yarn build 17 | matrix: 18 | allow_failures: [] 19 | fast_finish: true 20 | after_success: 21 | - yarn semantic-release 22 | branches: 23 | except: 24 | - /^v\d+\.\d+\.\d+$/ 25 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.tabSize": 2 3 | } -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## [2.15.5](https://github.com/streamich/memfs/compare/v2.15.4...v2.15.5) (2019-07-16) 2 | 3 | ### Bug Fixes 4 | 5 | - check for process ([8b9b00c](https://github.com/streamich/memfs/commit/8b9b00c)) 6 | - check for process ([#396](https://github.com/streamich/memfs/issues/396)) ([2314dad](https://github.com/streamich/memfs/commit/2314dad)) 7 | 8 | ## [2.15.4](https://github.com/streamich/memfs/compare/v2.15.3...v2.15.4) (2019-06-01) 9 | 10 | ### Bug Fixes 11 | 12 | - 🐛 accept `null` as value in `fromJSON` functions ([9e1af7d](https://github.com/streamich/memfs/commit/9e1af7d)) 13 | - 🐛 annotate return type of `toJSON` functions ([6609840](https://github.com/streamich/memfs/commit/6609840)) 14 | 15 | ## [2.15.3](https://github.com/streamich/memfs/compare/v2.15.2...v2.15.3) (2019-06-01) 16 | 17 | ### Bug Fixes 18 | 19 | - 🐛 mocks process.emitWarning for browser compatibility ([e3456b2](https://github.com/streamich/memfs/commit/e3456b2)), closes [#374](https://github.com/streamich/memfs/issues/374) 20 | 21 | ## [2.15.2](https://github.com/streamich/memfs/compare/v2.15.1...v2.15.2) (2019-02-16) 22 | 23 | ### Bug Fixes 24 | 25 | - 🐛 BigInt type handling ([c640f25](https://github.com/streamich/memfs/commit/c640f25)) 26 | 27 | ## [2.15.1](https://github.com/streamich/memfs/compare/v2.15.0...v2.15.1) (2019-02-09) 28 | 29 | ### Bug Fixes 30 | 31 | - 🐛 show directory path when throwing EISDIR in mkdir ([9dc7007](https://github.com/streamich/memfs/commit/9dc7007)) 32 | - 🐛 throw when creating root directory ([f77fa8b](https://github.com/streamich/memfs/commit/f77fa8b)), closes [#325](https://github.com/streamich/memfs/issues/325) 33 | 34 | # [2.15.0](https://github.com/streamich/memfs/compare/v2.14.2...v2.15.0) (2019-01-27) 35 | 36 | ### Features 37 | 38 | - **volume:** add env variable to suppress fs.promise api warnings ([e6b6d0a](https://github.com/streamich/memfs/commit/e6b6d0a)) 39 | 40 | ## [2.14.2](https://github.com/streamich/memfs/compare/v2.14.1...v2.14.2) (2018-12-11) 41 | 42 | ### Bug Fixes 43 | 44 | - fds to start from 0x7fffffff instead of 0xffffffff ([#277](https://github.com/streamich/memfs/issues/277)) ([31e44ba](https://github.com/streamich/memfs/commit/31e44ba)) 45 | 46 | ## [2.14.1](https://github.com/streamich/memfs/compare/v2.14.0...v2.14.1) (2018-11-29) 47 | 48 | ### Bug Fixes 49 | 50 | - don't copy legacy files into dist ([ab8ffbb](https://github.com/streamich/memfs/commit/ab8ffbb)), closes [#263](https://github.com/streamich/memfs/issues/263) 51 | 52 | # [2.14.0](https://github.com/streamich/memfs/compare/v2.13.1...v2.14.0) (2018-11-12) 53 | 54 | ### Features 55 | 56 | - add bigint option support ([00a017e](https://github.com/streamich/memfs/commit/00a017e)) 57 | 58 | ## [2.13.1](https://github.com/streamich/memfs/compare/v2.13.0...v2.13.1) (2018-11-11) 59 | 60 | ### Bug Fixes 61 | 62 | - 🐛 don't install semantic-release, incompat with old Node ([cd2b69c](https://github.com/streamich/memfs/commit/cd2b69c)) 63 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. 6 | 7 | ## Our Standards 8 | 9 | Examples of behavior that contributes to creating a positive environment include: 10 | 11 | - Using welcoming and inclusive language 12 | - Being respectful of differing viewpoints and experiences 13 | - Gracefully accepting constructive criticism 14 | - Focusing on what is best for the community 15 | - Showing empathy towards other community members 16 | 17 | Examples of unacceptable behavior by participants include: 18 | 19 | - The use of sexualized language or imagery and unwelcome sexual attention or advances 20 | - Trolling, insulting/derogatory comments, and personal or political attacks 21 | - Public or private harassment 22 | - Publishing others' private information, such as a physical or electronic address, without explicit permission 23 | - Other conduct which could reasonably be considered inappropriate in a professional setting 24 | 25 | ## Our Responsibilities 26 | 27 | Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. 28 | 29 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. 30 | 31 | ## Scope 32 | 33 | This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. 34 | 35 | ## Enforcement 36 | 37 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team [at](https://github.com/streamich/memfs/issues). The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. 38 | 39 | Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. 40 | 41 | ## Attribution 42 | 43 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] 44 | 45 | [homepage]: http://contributor-covenant.org 46 | [version]: http://contributor-covenant.org/version/1/4/ 47 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to `memfs` 2 | 3 | 1. Use [Angular convention](https://github.com/angular/angular/blob/master/CONTRIBUTING.md#-commit-message-guidelines) for commit messages. 4 | 2. Add tests for for code you add. 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | This is free and unencumbered software released into the public domain. 2 | 3 | Anyone is free to copy, modify, publish, use, compile, sell, or 4 | distribute this software, either in source code form or as a compiled 5 | binary, for any purpose, commercial or non-commercial, and by any 6 | means. 7 | 8 | In jurisdictions that recognize copyright laws, the author or authors 9 | of this software dedicate any and all copyright interest in the 10 | software to the public domain. We make this dedication for the benefit 11 | of the public at large and to the detriment of our heirs and 12 | successors. We intend this dedication to be an overt act of 13 | relinquishment in perpetuity of all present and future rights to this 14 | software under copyright law. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR 20 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 21 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | For more information, please refer to 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # `@wasmer/wasmfs` 2 | 3 | Isomorphic library to provide a sandboxed [node `fs`](https://nodejs.org/api/fs.html) implementation for Node and Browsers. 📂 4 | 5 | ## Table of Contents 6 | 7 | - [Features](#features) 8 | - [Installation](#installation) 9 | - [Quick Start](#quick-start) 10 | - [Reference API](#reference-api) 11 | - [Contributing](#contributing) 12 | 13 | ## Features 14 | 15 | This project forks [memfs](https://github.com/streamich/memfs) with custom fixes to work properly with the WebAssembly/WASI ecosystem. 16 | 17 | This package provides the following features: 18 | 19 | - In-memory file-system with Node's fs API using [memfs](https://github.com/streamich/memfs). 🗄️ 20 | - Scaffolds common files used by the [Wasmer Runtime](https://github.com/wasmerio/wasmer) (e.g I/O Device files like `/dev/stdout`), to provide a similar experience to the Wasmer Runtime. 🔌 21 | - Provides convienence functions for grabbing Input / Output. ↔️ 22 | - Allows overriding read/write of individual files to allow for custom implementations. 🛠️ 23 | 24 | ## Installation 25 | 26 | For installing `@wasmer/wasmfs`, just run this command in your shell: 27 | 28 | ```bash 29 | npm install --save @wasmer/wasmfs 30 | ``` 31 | 32 | ## Quick Start 33 | 34 | ```js 35 | import { WasmFs } from '@wasmer/wasmfs'; 36 | 37 | const wasmFs = new WasmFs(); 38 | 39 | wasmFs.fs.writeFileSync('/dev/stdout', 'Quick Start!'); 40 | 41 | wasmFs.getStdOut().then(response => { 42 | console.log(response); // Would log: 'Quick Start!' 43 | }); 44 | ``` 45 | 46 | For a larger end-to-end example, please see the [`@wasmer/wasm-terminal` package](https://github.com/wasmerio/wasmer-js/tree/master/packages/wasm-terminal). 47 | 48 | ## Reference API 49 | 50 | `wasmFs.fs` 51 | 52 | [memfs](https://github.com/streamich/memfs)' [node fs](https://nodejs.org/api/fs.html) implementation object. See the [node fs documentation](https://nodejs.org/api/fs.html) for API usage. 53 | 54 | **NOTE:** The functions on this `fs` implementation can easily be overriden to provide custom functionality when your wasm module (running with [`@wasmer/wasi`](https://github.com/wasmerio/wasmer-js/tree/master/packages/wasi)) tries to do file system operations. For example: 55 | 56 | ```js 57 | const wasmFs = new WasmFs(); 58 | 59 | const originalWriteFileSync = wasmFs.fs.writeFileSync; 60 | wasmFs.fs.writeFileSync = (path, text) => { 61 | console.log('File written:', path); 62 | originalWriteFileSync(path, text); 63 | }; 64 | 65 | wasmFs.fs.writeFileSync('/dev/stdout', 'Quick Start!'); 66 | 67 | // Would log: "File written: /dev/stdout" 68 | ``` 69 | 70 | --- 71 | 72 | `wasmFs.getStdOut()` 73 | 74 | Function that returns a promise that resolves a string. With the file contents of `/dev/stdout`. 75 | 76 | ## Contributing 77 | 78 | This project follows the [all-contributors](https://github.com/kentcdodds/all-contributors) specification. 79 | 80 | Contributions of any kind are welcome! 👍 81 | -------------------------------------------------------------------------------- /codecov.yml: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wasmerio/wasmfs/d9de610fa2763fa02be250f943db94390307ef02/codecov.yml -------------------------------------------------------------------------------- /demo/12.ts: -------------------------------------------------------------------------------- 1 | const memfs = require('../lib'); 2 | 3 | memfs.fs.writeFileSync( 4 | '/watson.json', 5 | JSON.stringify({ 6 | ocorrencia_id: 9001, 7 | }), 8 | ); 9 | 10 | console.log(memfs.fs.readFileSync('/watson.json', 'utf8')); 11 | -------------------------------------------------------------------------------- /demo/JSON.ts: -------------------------------------------------------------------------------- 1 | import { Volume } from '../src/volume'; 2 | 3 | const vol = Volume.fromJSON( 4 | { 5 | './src/index.js': ` 6 | import React from 'react'; 7 | import {render} from 'react-dom'; 8 | import {App} from './components/app'; 9 | 10 | const el = document.createElement('div'); 11 | document.body.appendChild(el); 12 | render(el, React.createElement(App, {})); 13 | `, 14 | 15 | './README.md': ` 16 | # Hello World 17 | 18 | This is some super cool project. 19 | `, 20 | 21 | '.node_modules/EMPTY': '', 22 | }, 23 | '/app', 24 | ); 25 | 26 | console.log(vol.toJSON()); 27 | console.log(vol.readFileSync('/app/src/index.js', 'utf8')); 28 | console.log(vol.readFileSync('/app/README.md', 'utf8')); 29 | -------------------------------------------------------------------------------- /demo/createReadStream.ts: -------------------------------------------------------------------------------- 1 | import { vol } from '../src/index'; 2 | 3 | vol.writeFileSync('/readme', '# Hello World'); 4 | const rs = vol.createReadStream('/readme', 'utf8'); 5 | rs.on('data', data => { 6 | console.log('data', data.toString()); 7 | }); 8 | -------------------------------------------------------------------------------- /demo/createWriteStream.ts: -------------------------------------------------------------------------------- 1 | import { vol } from '../src/index'; 2 | 3 | const ws = vol.createWriteStream('/readme', 'utf8'); 4 | ws.end('lol'); 5 | ws.on('finish', () => { 6 | console.log(vol.readFileSync('/readme').toString()); 7 | }); 8 | -------------------------------------------------------------------------------- /demo/localstorage.ts: -------------------------------------------------------------------------------- 1 | import { createVolume } from '../src/volume-localstorage'; 2 | 3 | const obj = {}; 4 | const Volume = createVolume('default', obj); 5 | 6 | const vol = new Volume(); 7 | vol.fromJSON({ '/foo': 'bar', '/foo2': 'bar2' }); 8 | // vol.unlinkSync('/foo'); 9 | 10 | console.log(obj); 11 | console.log(vol.toJSON()); 12 | -------------------------------------------------------------------------------- /demo/mountSync.ts: -------------------------------------------------------------------------------- 1 | import { Volume } from '../src/index'; 2 | 3 | const vol = new Volume(); 4 | vol.mountSync('/test', { 5 | foo: 'bar', 6 | }); 7 | 8 | console.log(vol.toJSON()); 9 | -------------------------------------------------------------------------------- /demo/permissions.ts: -------------------------------------------------------------------------------- 1 | import { Volume } from '../src/index'; 2 | 3 | const vol = Volume.fromJSON({ '/foo': 'bar' }); 4 | 5 | console.log(vol.readFileSync('/foo', 'utf8')); 6 | 7 | vol.chmodSync('/foo', 0); 8 | 9 | console.log(vol.readFileSync('/foo', 'utf8')); 10 | -------------------------------------------------------------------------------- /demo/readFileSync.ts: -------------------------------------------------------------------------------- 1 | import { fs } from '../src/index'; 2 | 3 | fs.writeFileSync('/test.txt', 'hello...'); 4 | console.log(fs.readFileSync('/test.txt', 'utf8')); 5 | -------------------------------------------------------------------------------- /demo/relative-path.ts: -------------------------------------------------------------------------------- 1 | import { Volume } from '../src/volume'; 2 | 3 | const vol = Volume.fromJSON({ './README': 'Hello' }); 4 | 5 | console.log(vol.toJSON()); 6 | console.log(vol.readdirSync('/home')); 7 | -------------------------------------------------------------------------------- /demo/rename.ts: -------------------------------------------------------------------------------- 1 | import { Volume } from '../src/volume'; 2 | 3 | const vol = Volume.fromJSON({ '/foo/foo': 'bar' }); 4 | vol.renameSync('/foo/foo', '/foo/foo2'); 5 | console.log(vol.toJSON()); 6 | -------------------------------------------------------------------------------- /demo/symlink.ts: -------------------------------------------------------------------------------- 1 | import { vol } from '../src'; 2 | 3 | vol.fromJSON({ '/a1/a2/a3/a4/a5/hello.txt': 'world!' }); 4 | console.log(vol.readFileSync('/a1/a2/a3/a4/a5/hello.txt', 'utf8')); 5 | 6 | vol.symlinkSync('/a1/a2/a3/a4/a5/hello.txt', '/link'); 7 | console.log(vol.readFileSync('/link', 'utf8')); 8 | 9 | vol.symlinkSync('/a1', '/b1'); 10 | console.log(vol.readFileSync('/b1/a2/a3/a4/a5/hello.txt', 'utf8')); 11 | 12 | vol.symlinkSync('/a1/a2', '/b2'); 13 | console.log(vol.readFileSync('/b2/a3/a4/a5/hello.txt', 'utf8')); 14 | 15 | vol.symlinkSync('/b2', '/c2'); 16 | console.log(vol.readFileSync('/c2/a3/a4/a5/hello.txt', 'utf8')); 17 | 18 | vol.mkdirpSync('/d1/d2'); 19 | vol.symlinkSync('/c2', '/d1/d2/to-c2'); 20 | console.log(vol.readFileSync('/d1/d2/to-c2/a3/a4/a5/hello.txt', 'utf8')); 21 | -------------------------------------------------------------------------------- /demo/throw-error.ts: -------------------------------------------------------------------------------- 1 | const errors = require('../src/internal/errors'); 2 | 3 | const err = new errors.TypeError('ENOENT', 'Test'); 4 | console.log(err); 5 | -------------------------------------------------------------------------------- /demo/toJSON.ts: -------------------------------------------------------------------------------- 1 | import { Volume } from '../src/index'; 2 | 3 | const vol = Volume.fromJSON({ 4 | '/foo': 'bar', 5 | '/dir1/file.js': '// comment...', 6 | '/dir2/index.js': 'process', 7 | '/dir2/main.js': 'console.log(123)', 8 | }); 9 | console.log(vol.toJSON()); 10 | 11 | console.log(vol.toJSON('/dir2')); 12 | 13 | console.log(vol.toJSON('/dir1')); 14 | 15 | console.log(vol.toJSON(['/dir2', '/dir1'])); 16 | 17 | console.log(vol.toJSON('/')); 18 | 19 | let a = { a: 1 }; 20 | console.log(vol.toJSON('/dir1', a)); 21 | 22 | console.log(vol.toJSON('/dir2', {}, true)); 23 | -------------------------------------------------------------------------------- /demo/watch.ts: -------------------------------------------------------------------------------- 1 | import * as fs from 'fs'; 2 | import * as path from 'path'; 3 | import { Volume } from '../src/volume'; 4 | 5 | // const filename = path.join(__dirname, '../test/mock/text.txt'); 6 | // fs.watch(filename, (event, filename) => { 7 | // console.log(event, filename); 8 | // }); 9 | 10 | const vol = Volume.fromJSON({ '/hello.txt': 'World' }); 11 | vol.watch('/hello.txt', {}, (event, filename) => { 12 | console.log(event, filename); 13 | console.log(vol.readFileSync('/hello.txt', 'utf8')); 14 | }); 15 | 16 | vol.appendFileSync('/hello.txt', '!'); 17 | 18 | setTimeout(() => { 19 | vol.appendFileSync('/hello.txt', ' OK?'); 20 | }, 1000); 21 | -------------------------------------------------------------------------------- /demo/write.ts: -------------------------------------------------------------------------------- 1 | import { fs } from '../src/index'; 2 | 3 | const fd = fs.openSync('/test.txt', 'w'); 4 | const data = '123'; 5 | fs.write(fd, Buffer.from(data), (err, bytes, buf) => { 6 | // console.log(err, bytes, buf); 7 | fs.closeSync(fd); 8 | }); 9 | -------------------------------------------------------------------------------- /docs/api-status.md: -------------------------------------------------------------------------------- 1 | # API Status 2 | 3 | All of the [Node's `fs` API](https://nodejs.org/api/fs.html) is implemented. 4 | Some error messages may be inaccurate. File permissions are currently not 5 | implemented (you have access to any file), basically `fs.access()` is a no-op. 6 | 7 | - [x] Promises 8 | - [x] Constants 9 | - [x] `FSWatcher` 10 | - [x] `ReadStream` 11 | - [x] `WriteStream` 12 | - [x] `Stats` 13 | - [x] `Dirent` 14 | - [x] `access(path[, mode], callback)` 15 | - Does not check permissions 16 | - [x] `accessSync(path[, mode])` 17 | - Does not check permissions 18 | - [x] `appendFile(file, data[, options], callback)` 19 | - [x] `appendFileSync(file, data[, options])` 20 | - [x] `chmod(path, mode, callback)` 21 | - [x] `chmodSync(path, mode)` 22 | - [x] `chown(path, uid, gid, callback)` 23 | - [x] `chownSync(path, uid, gid)` 24 | - [x] `fs.copyFile(src, dest[, flags], callback)` 25 | - [x] `fs.copyFileSync(src, dest[, flags])` 26 | - Honors `COPYFILE_EXCL` 27 | - Ignores `COPYFILE_FICLONE` *copy-on-write* — always creates a copy. 28 | - If `COPYFILE_FICLONE_FORCE` flag specified, will always fail with `ENOSYS`. 29 | - [x] `close(fd, callback)` 30 | - [x] `closeSync(fd)` 31 | - [x] `createReadStream(path[, options])` 32 | - [x] `createWriteStream(path[, options])` 33 | - [x] `exists(path, callback)` 34 | - [x] `existsSync(path)` 35 | - [x] `fchmod(fd, mode, callback)` 36 | - [x] `fchmodSync(fd, mode)` 37 | - [x] `fchown(fd, uid, gid, callback)` 38 | - [x] `fchownSync(fd, uid, gid)` 39 | - [x] `fdatasync(fd, callback)` 40 | - [x] `fdatasyncSync(fd)` 41 | - [x] `fstat(fd, callback)` 42 | - [x] `fstatSync(fd)` 43 | - [x] `fsync(fd, callback)` 44 | - [x] `fsyncSync(fd)` 45 | - [x] `ftruncate(fd[, len], callback)` 46 | - [x] `ftruncateSync(fd[, len])` 47 | - [x] `futimes(fd, atime, mtime, callback)` 48 | - [x] `futimesSync(fd, atime, mtime)` 49 | - [x] `lchmod(path, mode, callback)` 50 | - [x] `lchmodSync(path, mode)` 51 | - [x] `lchown(path, uid, gid, callback)` 52 | - [x] `lchownSync(path, uid, gid)` 53 | - [x] `link(existingPath, newPath, callback)` 54 | - [x] `linkSync(existingPath, newPath)` 55 | - [x] `lstat(path, callback)` 56 | - [x] `lstatSync(path)` 57 | - [x] `mkdir(path[, options], callback)` 58 | - [x] `mkdirSync(path[, options])` 59 | - [x] `mkdtemp(prefix[, options], callback)` 60 | - [x] `mkdtempSync(prefix[, options])` 61 | - [x] `open(path, flags[, mode], callback)` 62 | - [x] `openSync(path, flags[, mode])` 63 | - [x] `read(fd, buffer, offset, length, position, callback)` 64 | - [x] `readSync(fd, buffer, offset, length, position)` 65 | - [x] `readdir(path[, options], callback)` 66 | - [x] `readdirSync(path[, options])` 67 | - [x] `readFile(path[, options], callback)` 68 | - [x] `readFileSync(path[, options])` 69 | - [x] `readlink(path[, options], callback)` 70 | - [x] `readlinkSync(path[, options])` 71 | - [x] `realpath(path[, options], callback)` 72 | - [x] `realpathSync(path[, options])` 73 | - Caching not implemented 74 | - [x] `rename(oldPath, newPath, callback)` 75 | - [x] `renameSync(oldPath, newPath)` 76 | - [x] `rmdir(path, callback)` 77 | - [x] `rmdirSync(path)` 78 | - [x] `stat(path, callback)` 79 | - [x] `statSync(path)` 80 | - [x] `symlink(target, path[, type], callback)` 81 | - [x] `symlinkSync(target, path[, type])` 82 | - [x] `truncate(path[, len], callback)` 83 | - [x] `truncateSync(path[, len])` 84 | - [x] `unlink(path, callback)` 85 | - [x] `unlinkSync(path)` 86 | - [x] `utimes(path, atime, mtime, callback)` 87 | - [x] `utimesSync(path, atime, mtime)` 88 | - [x] `watch(filename[, options][, listener])` 89 | - [x] `watchFile(filename[, options], listener)` 90 | - [x] `unwatchFile(filename[, listener])` 91 | - [x] `write(fd, buffer[, offset[, length[, position]]], callback)` 92 | - [x] `write(fd, string[, position[, encoding]], callback)` 93 | - [x] `writeFile(file, data[, options], callback)` 94 | - [x] `writeFileSync(file, data[, options])` 95 | - [x] `writeSync(fd, buffer[, offset[, length[, position]]])` 96 | - [x] `writeSync(fd, string[, position[, encoding]])` 97 | -------------------------------------------------------------------------------- /docs/dependencies.md: -------------------------------------------------------------------------------- 1 | # Dependencies 2 | 3 | This package depends on the following Node modules: `buffer`, `events`, 4 | `streams`, `path`. 5 | 6 | It also uses `process` and `setImmediate` globals, but mocks them, if not 7 | available. 8 | 9 | It uses `Promise` when available and throws when `promises` property is 10 | accessed in an environment that does not support this ES2015 feature. 11 | 12 | It uses `BigInt` when available and throws when `bigint` option is used 13 | in an environment that does not support this ESNext feature. 14 | -------------------------------------------------------------------------------- /docs/reference.md: -------------------------------------------------------------------------------- 1 | # Reference 2 | 3 | ## `vol` vs `fs` 4 | 5 | This package exports `vol` and `fs` objects which both can be used for 6 | filesystem operations but are slightly different. 7 | 8 | ```js 9 | import {vol, fs} from 'memfs'; 10 | ``` 11 | 12 | `vol` is an instance of `Volume` constructor, it is the default volume created 13 | for your convenience. `fs` is an *fs-like* object created from `vol` using 14 | `createFsFromVolume(vol)`, see reference below. 15 | 16 | All contents of the `fs` object are also exported individually, so you can use 17 | `memfs` just like you would use the `fs` module: 18 | 19 | ```js 20 | import {readFileSync, F_OK, ReadStream} from 'memfs'; 21 | ``` 22 | 23 | ## `Volume` Constructor 24 | 25 | `Volume` is a constructor function for creating new volumes: 26 | 27 | ```js 28 | import {Volume} from 'memfs'; 29 | const vol = new Volume; 30 | ``` 31 | 32 | `Volume` implements all [Node's filesystem methods](https://nodejs.org/api/fs.html): 33 | 34 | ```js 35 | vol.writeFileSync('/foo', 'bar'); 36 | ``` 37 | 38 | But it does not hold constants and its methods are not bound to `vol`: 39 | 40 | ```js 41 | vol.F_OK; // undefined 42 | ``` 43 | 44 | A new volume can be create using the `Volume.fromJSON` convenience method: 45 | 46 | ```js 47 | const vol = Volume.fromJSON({ 48 | '/app/index.js': '...', 49 | '/app/package.json': '...', 50 | }); 51 | ``` 52 | 53 | It is just a shorthand for `vol.fromJSON`, see below. 54 | 55 | ## `Volume` instance `vol` 56 | 57 | #### `vol.fromJSON(json[, cwd])` 58 | 59 | Adds files from a flat `json` object to the volume `vol`. The `cwd` argument 60 | is optional and is used to compute absolute file paths, if a file path is 61 | given in a relative form. 62 | 63 | **Note:** To remove all existing files, use `vol.reset()` method. 64 | 65 | ```js 66 | vol.fromJSON({ 67 | './index.js': '...', 68 | './package.json': '...', 69 | }, '/app'); 70 | ``` 71 | 72 | #### `vol.mountSync(cwd, json)` 73 | 74 | Legacy method, which is just an alias for `vol.fromJSON`. 75 | 76 | #### `vol.toJSON([paths[, json[, isRelative]]])` 77 | 78 | Exports the whole contents of the volume recursively to a flat JSON object. 79 | 80 | `paths` is an optional argument that specifies one or more paths to be exported. 81 | If this argument is omitted, the whole volume is exported. `paths` can be 82 | an array of paths. A path can be a string, `Buffer` or an `URL` object. 83 | 84 | `json` is an optional object parameter which will be populated with the exported files. 85 | 86 | `isRelative` is boolean that specifies if returned paths should be relative. 87 | 88 | **Note:** JSON contains only files, empty folders will be absent. 89 | 90 | #### `vol.reset()` 91 | 92 | Removes all files from the volume. 93 | 94 | ```js 95 | vol.fromJSON({'/index.js': '...'}); 96 | vol.toJSON(); // {'/index.js': '...' } 97 | vol.reset(); 98 | vol.toJSON(); // {} 99 | ``` 100 | 101 | #### `vol.mkdirp(path[, mode], callback)` 102 | 103 | > DEPRECATED: This method will be removed in next major release. 104 | > Use `vol.mkdir(..., {recursive: true}, callback)` instead. 105 | 106 | Legacy interface, which now uses the `recursive` option of `vol.mkdir`. 107 | 108 | Creates a directory tree recursively. `path` specifies a directory to 109 | create and can be a string, `Buffer`, or an `URL` object. `callback` is 110 | called on completion and may receive only one argument - an `Error` object. 111 | 112 | #### `vol.mkdirpSync(path[, mode])` 113 | 114 | > DEPRECATED: This method will be removed in next major release. 115 | > Use `vol.mkdirSync(..., {recursive: true})` instead. 116 | 117 | Legacy interface, which now uses the `recursive` option of `vol.mkdirSync`. 118 | 119 | A synchronous version of `vol.mkdirp()`. This method throws. 120 | 121 | ## `createFsFromVolume(vol)` 122 | 123 | Returns an *fs-like* object created from a `Volume` instance `vol`. 124 | 125 | ```js 126 | import {createFsFromVolume, Volume} from 'memfs'; 127 | 128 | const vol = new Volume; 129 | const fs = createFsFromVolume(vol); 130 | ``` 131 | 132 | The idea behind the *fs-like* object is to make it identical to the one 133 | you get from `require('fs')`. Here are some things this function does: 134 | 135 | - Binds all methods, so you can do: 136 | 137 | ```js 138 | const {createFileSync, readFileSync} = fs; 139 | ``` 140 | 141 | - Adds constants `fs.constants`, `fs.F_OK`, etc. 142 | 143 | ## Experimental fs.promise api warnings 144 | 145 | Supress warnings when using the promise api of fs by setting 146 | 147 | ```js 148 | process.env.MEMFS_DONT_WARN = true; 149 | ``` 150 | -------------------------------------------------------------------------------- /docs/relative-paths.md: -------------------------------------------------------------------------------- 1 | # Relative paths 2 | 3 | If you work with *absolute* paths, you should get what you expect from `memfs`. 4 | 5 | You can also use *relative* paths but the gotcha is that then `memfs` needs 6 | to somehow resolve those relative paths into absolute paths. `memfs` will use 7 | the value of `process.cwd()` to resolve the relative paths. The problem is 8 | that `process.cwd()` specifies the *current working directory* of your 9 | on-disk filesystem and you will probably not have that directory available in your 10 | `memfs` volume. 11 | 12 | The best solution is to always use absolute paths. Alternatively, you can use 13 | `mkdir` method to recursively create the current working directory in your 14 | volume: 15 | 16 | ```js 17 | vol.mkdirSync(process.cwd(), { recursive: true }); 18 | ``` 19 | 20 | Or, you can set the current working directory to `/`, which 21 | is one folder that exists in all `memfs` volumes: 22 | 23 | ```js 24 | process.chdir('/'); 25 | ``` 26 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@wasmer/wasmfs", 3 | "version": "0.2.0", 4 | "description": "In-memory file-system with Node's fs API.", 5 | "main": "lib/index.js", 6 | "types": "lib/index.d.ts", 7 | "files": [ 8 | "lib" 9 | ], 10 | "scripts": { 11 | "clean": "rimraf lib types", 12 | "build": "tsc -p . && cpy src/*.js lib", 13 | "test": "jest", 14 | "test:coverage": "jest --coverage", 15 | "test:watch": "jest --watch", 16 | "watch": "watch \"npm run build\" ./src", 17 | "semantic-release": "semantic-release", 18 | "prettier": "prettier --ignore-path .gitignore --write \"src/**/*.{ts,js}\"", 19 | "prettier:diff": "prettier -l \"src/**/*.{ts,js}\"", 20 | "tslint": "tslint \"src/**/*.ts\" -t verbose", 21 | "precommit": "pretty-quick --staged", 22 | "prepush": "npm run prettier:diff && npm run tslint" 23 | }, 24 | "repository": { 25 | "type": "git", 26 | "url": "https://github.com/streamich/memfs.git" 27 | }, 28 | "dependencies": { 29 | "fast-extend": "0.0.2", 30 | "fs-monkey": "^0.3.3" 31 | }, 32 | "devDependencies": { 33 | "@semantic-release/changelog": "3.0.4", 34 | "@semantic-release/git": "7.0.16", 35 | "@semantic-release/npm": "5.2.0", 36 | "@types/jest": "23.3.14", 37 | "@types/node": "10.14.20", 38 | "cpy-cli": "2.0.0", 39 | "husky": "1.3.1", 40 | "jest": "24.9.0", 41 | "prettier": "1.18.2", 42 | "pretty-quick": "1.11.1", 43 | "rimraf": "2.7.1", 44 | "semantic-release": "15.13.24", 45 | "ts-jest": "24.0.2", 46 | "ts-node": "8.4.1", 47 | "tslint": "5.20.0", 48 | "tslint-config-common": "1.6.0", 49 | "typescript": "3.6.2" 50 | }, 51 | "config": { 52 | "commitizen": { 53 | "path": "git-cz" 54 | } 55 | }, 56 | "jest": { 57 | "moduleFileExtensions": [ 58 | "ts", 59 | "tsx", 60 | "js", 61 | "jsx" 62 | ], 63 | "transform": { 64 | "^.+\\.tsx?$": "ts-jest" 65 | }, 66 | "testRegex": ".*/__tests__/.*\\.(test|spec)\\.(jsx?|tsx?)$" 67 | }, 68 | "release": { 69 | "verifyConditions": [ 70 | "@semantic-release/changelog", 71 | "@semantic-release/npm", 72 | "@semantic-release/git" 73 | ], 74 | "prepare": [ 75 | "@semantic-release/changelog", 76 | "@semantic-release/npm", 77 | "@semantic-release/git" 78 | ] 79 | }, 80 | "keywords": [ 81 | "fs", 82 | "filesystem", 83 | "fs.js", 84 | "memory-fs", 85 | "memfs", 86 | "file", 87 | "file system", 88 | "mount", 89 | "memory", 90 | "in-memory", 91 | "virtual", 92 | "test", 93 | "testing", 94 | "mock" 95 | ] 96 | } 97 | -------------------------------------------------------------------------------- /prettier.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | printWidth: 120, 3 | tabWidth: 2, 4 | useTabs: false, 5 | semi: true, 6 | singleQuote: true, 7 | trailingComma: 'all', 8 | bracketSpacing: true, 9 | }; 10 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["config:base"], 3 | "automerge": true, 4 | "pinVersions": false, 5 | "major": { 6 | "automerge": false 7 | }, 8 | "devDependencies": { 9 | "automerge": true, 10 | "pinVersions": true 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/Dirent.ts: -------------------------------------------------------------------------------- 1 | import { Link } from './node'; 2 | import { constants } from './constants'; 3 | import { TEncodingExtended, strToEncoding, TDataOut } from './encoding'; 4 | 5 | const { S_IFMT, S_IFDIR, S_IFREG, S_IFBLK, S_IFCHR, S_IFLNK, S_IFIFO, S_IFSOCK } = constants; 6 | 7 | /** 8 | * A directory entry, like `fs.Dirent`. 9 | */ 10 | export class Dirent { 11 | static build(link: Link, encoding: TEncodingExtended) { 12 | const dirent = new Dirent(); 13 | const { mode } = link.getNode(); 14 | 15 | dirent.name = strToEncoding(link.getName(), encoding); 16 | dirent.mode = mode; 17 | 18 | return dirent; 19 | } 20 | 21 | name: TDataOut = ''; 22 | private mode: number = 0; 23 | 24 | private _checkModeProperty(property: number): boolean { 25 | return (this.mode & S_IFMT) === property; 26 | } 27 | 28 | isDirectory(): boolean { 29 | return this._checkModeProperty(S_IFDIR); 30 | } 31 | 32 | isFile(): boolean { 33 | return this._checkModeProperty(S_IFREG); 34 | } 35 | 36 | isBlockDevice(): boolean { 37 | return this._checkModeProperty(S_IFBLK); 38 | } 39 | 40 | isCharacterDevice(): boolean { 41 | return this._checkModeProperty(S_IFCHR); 42 | } 43 | 44 | isSymbolicLink(): boolean { 45 | return this._checkModeProperty(S_IFLNK); 46 | } 47 | 48 | isFIFO(): boolean { 49 | return this._checkModeProperty(S_IFIFO); 50 | } 51 | 52 | isSocket(): boolean { 53 | return this._checkModeProperty(S_IFSOCK); 54 | } 55 | } 56 | 57 | export default Dirent; 58 | -------------------------------------------------------------------------------- /src/Stats.ts: -------------------------------------------------------------------------------- 1 | import { Node } from './node'; 2 | import { constants } from './constants'; 3 | import getBigInt from './getBigInt'; 4 | 5 | const { S_IFMT, S_IFDIR, S_IFREG, S_IFBLK, S_IFCHR, S_IFLNK, S_IFIFO, S_IFSOCK } = constants; 6 | 7 | export type TStatNumber = number | bigint; 8 | 9 | /** 10 | * Statistics about a file/directory, like `fs.Stats`. 11 | */ 12 | export class Stats { 13 | static build(node: Node, bigint: false): Stats; 14 | static build(node: Node, bigint: true): Stats; 15 | static build(node: Node, bigint?: boolean): Stats; 16 | static build(node: Node, bigint: boolean = false): Stats { 17 | const stats = new Stats(); 18 | const { uid, gid, atime, mtime, ctime } = node; 19 | 20 | const getStatNumber = !bigint ? number => number : getBigInt; 21 | 22 | // Copy all values on Stats from Node, so that if Node values 23 | // change, values on Stats would still be the old ones, 24 | // just like in Node fs. 25 | 26 | stats.uid = getStatNumber(uid); 27 | stats.gid = getStatNumber(gid); 28 | 29 | stats.rdev = getStatNumber(0); 30 | stats.blksize = getStatNumber(4096); 31 | stats.ino = getStatNumber(node.ino); 32 | stats.size = getStatNumber(node.getSize()); 33 | stats.blocks = getStatNumber(1); 34 | 35 | stats.atime = atime; 36 | stats.mtime = mtime; 37 | stats.ctime = ctime; 38 | stats.birthtime = ctime; 39 | 40 | stats.atimeMs = getStatNumber(atime.getTime()); 41 | stats.mtimeMs = getStatNumber(mtime.getTime()); 42 | const ctimeMs = getStatNumber(ctime.getTime()); 43 | stats.ctimeMs = ctimeMs; 44 | stats.birthtimeMs = ctimeMs; 45 | 46 | stats.dev = getStatNumber(0); 47 | stats.mode = getStatNumber(node.mode); 48 | stats.nlink = getStatNumber(node.nlink); 49 | 50 | return stats; 51 | } 52 | 53 | uid: T; 54 | gid: T; 55 | 56 | rdev: T; 57 | blksize: T; 58 | ino: T; 59 | size: T; 60 | blocks: T; 61 | 62 | atime: Date; 63 | mtime: Date; 64 | ctime: Date; 65 | birthtime: Date; 66 | 67 | atimeMs: T; 68 | mtimeMs: T; 69 | ctimeMs: T; 70 | birthtimeMs: T; 71 | 72 | dev: T; 73 | mode: T; 74 | nlink: T; 75 | 76 | private _checkModeProperty(property: number): boolean { 77 | return (Number(this.mode) & S_IFMT) === property; 78 | } 79 | 80 | isDirectory(): boolean { 81 | return this._checkModeProperty(S_IFDIR); 82 | } 83 | 84 | isFile(): boolean { 85 | return this._checkModeProperty(S_IFREG); 86 | } 87 | 88 | isBlockDevice(): boolean { 89 | return this._checkModeProperty(S_IFBLK); 90 | } 91 | 92 | isCharacterDevice(): boolean { 93 | return this._checkModeProperty(S_IFCHR); 94 | } 95 | 96 | isSymbolicLink(): boolean { 97 | return this._checkModeProperty(S_IFLNK); 98 | } 99 | 100 | isFIFO(): boolean { 101 | return this._checkModeProperty(S_IFIFO); 102 | } 103 | 104 | isSocket(): boolean { 105 | return this._checkModeProperty(S_IFSOCK); 106 | } 107 | } 108 | 109 | export default Stats; 110 | -------------------------------------------------------------------------------- /src/__tests__/hasBigInt.js: -------------------------------------------------------------------------------- 1 | exports.default = typeof BigInt === 'function'; 2 | -------------------------------------------------------------------------------- /src/__tests__/index.test.ts: -------------------------------------------------------------------------------- 1 | import { Volume } from '../volume'; 2 | import { constants } from '../constants'; 3 | const memfs = require('../index'); 4 | import { fsSyncMethods, fsAsyncMethods } from 'fs-monkey/lib/util/lists'; 5 | 6 | describe('memfs', () => { 7 | it('Exports Volume constructor', () => { 8 | expect(typeof memfs.Volume).toBe('function'); 9 | expect(memfs.Volume).toBe(Volume); 10 | }); 11 | it('Exports constants', () => { 12 | expect(memfs.F_OK).toBe(constants.F_OK); 13 | expect(memfs.R_OK).toBe(constants.R_OK); 14 | expect(memfs.W_OK).toBe(constants.W_OK); 15 | expect(memfs.X_OK).toBe(constants.X_OK); 16 | expect(memfs.constants).toEqual(constants); 17 | }); 18 | it('Exports constructors', () => { 19 | expect(typeof memfs.Stats).toBe('function'); 20 | expect(typeof memfs.Dirent).toBe('function'); 21 | expect(typeof memfs.ReadStream).toBe('function'); 22 | expect(typeof memfs.WriteStream).toBe('function'); 23 | expect(typeof memfs.FSWatcher).toBe('function'); 24 | expect(typeof memfs.StatWatcher).toBe('function'); 25 | }); 26 | it('Exports _toUnixTimestamp', () => { 27 | expect(typeof memfs._toUnixTimestamp).toBe('function'); 28 | }); 29 | it("Exports all Node's filesystem API methods", () => { 30 | for (const method of fsSyncMethods) { 31 | expect(typeof memfs[method]).toBe('function'); 32 | } 33 | for (const method of fsAsyncMethods) { 34 | expect(typeof memfs[method]).toBe('function'); 35 | } 36 | }); 37 | it('Exports promises API', () => { 38 | expect(typeof memfs.promises).toBe('object'); 39 | }); 40 | }); 41 | -------------------------------------------------------------------------------- /src/__tests__/node.test.ts: -------------------------------------------------------------------------------- 1 | import { Node } from '../node'; 2 | import { constants, S } from '../constants'; 3 | 4 | describe('node.ts', () => { 5 | describe('Node', () => { 6 | const node = new Node(1); 7 | it('properly sets mode with permission respected', () => { 8 | const node = new Node(1, 0o755); 9 | expect(node.perm).toBe(0o755); 10 | expect(node.mode).toBe(constants.S_IFREG | 0o755); 11 | expect(node.isFile()).toBe(true); // Make sure we still know it's a file 12 | }); 13 | it('Setting/getting buffer creates a copy', () => { 14 | const buf = Buffer.from([1, 2, 3]); 15 | node.setBuffer(buf); 16 | expect(buf === node.getBuffer()).toBe(false); // Objects not equal, so copy. 17 | expect(buf.toJSON()).toEqual(node.getBuffer().toJSON()); 18 | }); 19 | describe('.write(buf, off, len, pos)', () => { 20 | it('Simple write into empty node', () => { 21 | const node = new Node(1); 22 | node.write(Buffer.from([1, 2, 3])); 23 | expect(node.getBuffer().equals(Buffer.from([1, 2, 3]))).toBe(true); 24 | }); 25 | it('Append to the end', () => { 26 | const node = new Node(1); 27 | node.write(Buffer.from([1, 2])); 28 | node.write(Buffer.from([3, 4]), 0, 2, 2); 29 | const result = Buffer.from([1, 2, 3, 4]); 30 | expect(node.getBuffer().equals(result)).toBe(true); 31 | }); 32 | it('Overwrite part of the buffer', () => { 33 | const node = new Node(1); 34 | node.write(Buffer.from([1, 2, 3])); 35 | node.write(Buffer.from([4, 5, 6]), 1, 2, 1); 36 | const result = Buffer.from([1, 5, 6]); 37 | expect(node.getBuffer().equals(result)).toBe(true); 38 | }); 39 | it('Overwrite part of the buffer and extend', () => { 40 | const node = new Node(1); 41 | node.write(Buffer.from([1, 2, 3])); 42 | node.write(Buffer.from([4, 5, 6, 7]), 0, 4, 2); 43 | const result = Buffer.from([1, 2, 4, 5, 6, 7]); 44 | expect(node.getBuffer().equals(result)).toBe(true); 45 | }); 46 | it('Write outside the space of the buffer', () => { 47 | const node = new Node(1); 48 | node.write(Buffer.from([1, 2, 3])); 49 | node.write(Buffer.from([7, 8, 9]), 0, 3, 6); 50 | node.write(Buffer.from([4, 5, 6]), 0, 3, 3); 51 | const result = Buffer.from([1, 2, 3, 4, 5, 6, 7, 8, 9]); 52 | expect(node.getBuffer().equals(result)).toBe(true); 53 | }); 54 | }); 55 | describe('.read(buf, off, len, pos)', () => { 56 | it('Simple one byte read', () => { 57 | const node = new Node(1); 58 | node.write(Buffer.from([1, 2, 3])); 59 | const buf = Buffer.allocUnsafe(1); 60 | node.read(buf, 0, 1, 1); 61 | expect(buf.equals(Buffer.from([2]))).toBe(true); 62 | }); 63 | }); 64 | describe('.chmod(perm)', () => { 65 | const node = new Node(1); 66 | expect(node.perm).toBe(0o666); 67 | expect(node.isFile()).toBe(true); 68 | node.chmod(0o600); 69 | expect(node.perm).toBe(0o600); 70 | expect(node.isFile()).toBe(true); 71 | }); 72 | }); 73 | }); 74 | -------------------------------------------------------------------------------- /src/__tests__/process.test.ts: -------------------------------------------------------------------------------- 1 | import _process, { createProcess } from '../process'; 2 | 3 | describe('process', () => { 4 | describe('createProcess', () => { 5 | const proc = createProcess(); 6 | it('Exports default object', () => { 7 | expect(typeof _process).toBe('object'); 8 | }); 9 | it('.getuid() and .getgid()', () => { 10 | expect(typeof proc.getuid()).toBe('number'); 11 | expect(typeof proc.getgid()).toBe('number'); 12 | }); 13 | it('.cwd()', () => { 14 | expect(typeof proc.cwd()).toBe('string'); 15 | }); 16 | it('.nextTick()', done => { 17 | expect(typeof proc.nextTick).toBe('function'); 18 | proc.nextTick(done); 19 | }); 20 | it('.env', () => { 21 | expect(typeof proc.env).toBe('object'); 22 | expect(!!proc.env.MEMFS_DONT_WARN).toBe(false); 23 | }); 24 | }); 25 | describe('using MEMFS_DONT_WARN', () => { 26 | it('should be assignable to the process.env, and returned by createProcess', () => { 27 | process.env.MEMFS_DONT_WARN = 'true'; 28 | const proc = createProcess(); 29 | expect(!!proc.env.MEMFS_DONT_WARN).toBe(true); 30 | delete process.env.MEMFS_DONT_WARN; 31 | }); 32 | it('should by default show warnings (in volume.ts)', () => { 33 | const proc = createProcess(); 34 | const promisesWarn = !proc.env.MEMFS_DONT_WARN; 35 | expect(promisesWarn).toBe(true); 36 | }); 37 | }); 38 | }); 39 | -------------------------------------------------------------------------------- /src/__tests__/promises.test.ts: -------------------------------------------------------------------------------- 1 | import { Volume } from '../volume'; 2 | 3 | describe('Promises API', () => { 4 | describe('FileHandle', () => { 5 | it('API should have a FileHandle property', () => { 6 | const vol = new Volume(); 7 | const { promises } = vol; 8 | expect(typeof promises.FileHandle).toBe('function'); 9 | }); 10 | describe('fd', () => { 11 | it('FileHandle should have a fd property', async () => { 12 | const vol = new Volume(); 13 | const { promises } = vol; 14 | vol.fromJSON({ 15 | '/foo': 'bar', 16 | }); 17 | const fileHandle = await promises.open('/foo', 'r'); 18 | expect(typeof fileHandle.fd).toEqual('number'); 19 | await fileHandle.close(); 20 | }); 21 | }); 22 | describe('appendFile(data[, options])', () => { 23 | const vol = new Volume(); 24 | const { promises } = vol; 25 | vol.fromJSON({ 26 | '/foo': 'bar', 27 | }); 28 | it('Append data to an existing file', async () => { 29 | const fileHandle = await promises.open('/foo', 'a'); 30 | await fileHandle.appendFile('baz'); 31 | expect(vol.readFileSync('/foo').toString()).toEqual('barbaz'); 32 | await fileHandle.close(); 33 | }); 34 | it('Reject when the file handle was closed', async () => { 35 | const fileHandle = await promises.open('/foo', 'a'); 36 | await fileHandle.close(); 37 | return expect(fileHandle.appendFile('/foo', 'baz')).rejects.toBeInstanceOf(Error); 38 | }); 39 | }); 40 | describe('chmod(mode)', () => { 41 | const vol = new Volume(); 42 | const { promises } = vol; 43 | vol.fromJSON({ 44 | '/foo': 'bar', 45 | }); 46 | it('Change mode of existing file', async () => { 47 | const fileHandle = await promises.open('/foo', 'a'); 48 | await fileHandle.chmod(0o444); 49 | expect(vol.statSync('/foo').mode & 0o777).toEqual(0o444); 50 | await fileHandle.close(); 51 | }); 52 | it('Reject when the file handle was closed', async () => { 53 | const fileHandle = await promises.open('/foo', 'a'); 54 | await fileHandle.close(); 55 | return expect(fileHandle.chmod(0o666)).rejects.toBeInstanceOf(Error); 56 | }); 57 | }); 58 | describe('chown(uid, gid)', () => { 59 | const vol = new Volume(); 60 | const { promises } = vol; 61 | vol.fromJSON({ 62 | '/foo': 'bar', 63 | }); 64 | const { uid, gid } = vol.statSync('/foo'); 65 | it('Change uid and gid of existing file', async () => { 66 | const fileHandle = await promises.open('/foo', 'a'); 67 | await fileHandle.chown(uid + 1, gid + 1); 68 | const stats = vol.statSync('/foo'); 69 | expect(stats.uid).toEqual(uid + 1); 70 | expect(stats.gid).toEqual(gid + 1); 71 | await fileHandle.close(); 72 | }); 73 | it('Reject when the file handle was closed', async () => { 74 | const fileHandle = await promises.open('/foo', 'a'); 75 | await fileHandle.close(); 76 | return expect(fileHandle.chown(uid + 2, gid + 2)).rejects.toBeInstanceOf(Error); 77 | }); 78 | }); 79 | // close(): covered by all other tests 80 | describe('datasync()', () => { 81 | const vol = new Volume(); 82 | const { promises } = vol; 83 | vol.fromJSON({ 84 | '/foo': 'bar', 85 | }); 86 | it('Synchronize data with an existing file', async () => { 87 | const fileHandle = await promises.open('/foo', 'r+'); 88 | await fileHandle.datasync(); 89 | expect(vol.readFileSync('/foo').toString()).toEqual('bar'); 90 | await fileHandle.close(); 91 | }); 92 | it('Reject when the file handle was closed', async () => { 93 | const fileHandle = await promises.open('/foo', 'r+'); 94 | await fileHandle.close(); 95 | return expect(fileHandle.datasync()).rejects.toBeInstanceOf(Error); 96 | }); 97 | }); 98 | describe('read(buffer, offset, length, position)', () => { 99 | const vol = new Volume(); 100 | const { promises } = vol; 101 | vol.fromJSON({ 102 | '/foo': 'bar', 103 | }); 104 | it('Read data from an existing file', async () => { 105 | const fileHandle = await promises.open('/foo', 'r+'); 106 | const buff = Buffer.from('foo'); 107 | const { bytesRead, buffer } = await fileHandle.read(buff, 0, 42, 0); 108 | expect(bytesRead).toEqual(3); 109 | expect(buffer).toBe(buff); 110 | await fileHandle.close(); 111 | }); 112 | it('Reject when the file handle was closed', async () => { 113 | const fileHandle = await promises.open('/foo', 'r+'); 114 | await fileHandle.close(); 115 | return expect(fileHandle.read(Buffer.from('foo'), 0, 42, 0)).rejects.toBeInstanceOf(Error); 116 | }); 117 | }); 118 | describe('readFile([options])', () => { 119 | const vol = new Volume(); 120 | const { promises } = vol; 121 | vol.fromJSON({ 122 | '/foo': 'bar', 123 | }); 124 | it('Read data from an existing file', async () => { 125 | const fileHandle = await promises.open('/foo', 'r+'); 126 | expect((await fileHandle.readFile()).toString()).toEqual('bar'); 127 | await fileHandle.close(); 128 | }); 129 | it('Reject when the file handle was closed', async () => { 130 | const fileHandle = await promises.open('/foo', 'r+'); 131 | await fileHandle.close(); 132 | return expect(fileHandle.readFile()).rejects.toBeInstanceOf(Error); 133 | }); 134 | }); 135 | describe('stat()', () => { 136 | const vol = new Volume(); 137 | const { promises } = vol; 138 | vol.fromJSON({ 139 | '/foo': 'bar', 140 | }); 141 | it('Return stats of an existing file', async () => { 142 | const fileHandle = await promises.open('/foo', 'r+'); 143 | expect((await fileHandle.stat()).isFile()).toEqual(true); 144 | await fileHandle.close(); 145 | }); 146 | it('Reject when the file handle was closed', async () => { 147 | const fileHandle = await promises.open('/foo', 'r+'); 148 | await fileHandle.close(); 149 | return expect(fileHandle.stat()).rejects.toBeInstanceOf(Error); 150 | }); 151 | }); 152 | describe('truncate([len])', () => { 153 | const vol = new Volume(); 154 | const { promises } = vol; 155 | vol.fromJSON({ 156 | '/foo': '0123456789', 157 | }); 158 | it('Truncate an existing file', async () => { 159 | const fileHandle = await promises.open('/foo', 'r+'); 160 | await fileHandle.truncate(5); 161 | expect(vol.readFileSync('/foo').toString()).toEqual('01234'); 162 | await fileHandle.close(); 163 | }); 164 | it('Reject when the file handle was closed', async () => { 165 | const fileHandle = await promises.open('/foo', 'r+'); 166 | await fileHandle.close(); 167 | return expect(fileHandle.truncate(5)).rejects.toBeInstanceOf(Error); 168 | }); 169 | }); 170 | describe('utimes(atime, mtime)', () => { 171 | const vol = new Volume(); 172 | const { promises } = vol; 173 | vol.fromJSON({ 174 | '/foo': '0123456789', 175 | }); 176 | const fttDeparture = new Date(1985, 9, 26, 1, 21); // ftt stands for "first time travel" :-) 177 | const fttArrival = new Date(fttDeparture.getTime() + 60000); 178 | it('Changes times of an existing file', async () => { 179 | const fileHandle = await promises.open('/foo', 'r+'); 180 | await fileHandle.utimes(fttArrival, fttDeparture); 181 | const stats = vol.statSync('/foo'); 182 | expect(stats.atime).toEqual(new Date(fttArrival as any)); 183 | expect(stats.mtime).toEqual(new Date(fttDeparture as any)); 184 | await fileHandle.close(); 185 | }); 186 | it('Reject when the file handle was closed', async () => { 187 | const fileHandle = await promises.open('/foo', 'r+'); 188 | await fileHandle.close(); 189 | return expect(fileHandle.utimes(fttArrival, fttDeparture)).rejects.toBeInstanceOf(Error); 190 | }); 191 | }); 192 | describe('write(buffer[, offset[, length[, position]]])', () => { 193 | const vol = new Volume(); 194 | const { promises } = vol; 195 | vol.fromJSON({ 196 | '/foo': 'bar', 197 | }); 198 | it('Write data to an existing file', async () => { 199 | const fileHandle = await promises.open('/foo', 'w'); 200 | await fileHandle.write(Buffer.from('foo')); 201 | expect(vol.readFileSync('/foo').toString()).toEqual('foo'); 202 | await fileHandle.close(); 203 | }); 204 | it('Reject when the file handle was closed', async () => { 205 | const fileHandle = await promises.open('/foo', 'w'); 206 | await fileHandle.close(); 207 | return expect(fileHandle.write(Buffer.from('foo'))).rejects.toBeInstanceOf(Error); 208 | }); 209 | }); 210 | describe('writeFile(data[, options])', () => { 211 | const vol = new Volume(); 212 | const { promises } = vol; 213 | vol.fromJSON({ 214 | '/foo': 'bar', 215 | }); 216 | it('Write data to an existing file', async () => { 217 | const fileHandle = await promises.open('/foo', 'w'); 218 | await fileHandle.writeFile('foo'); 219 | expect(vol.readFileSync('/foo').toString()).toEqual('foo'); 220 | await fileHandle.close(); 221 | }); 222 | it('Reject when the file handle was closed', async () => { 223 | const fileHandle = await promises.open('/foo', 'w'); 224 | await fileHandle.close(); 225 | return expect(fileHandle.writeFile('foo')).rejects.toBeInstanceOf(Error); 226 | }); 227 | }); 228 | }); 229 | describe('access(path[, mode])', () => { 230 | const vol = new Volume(); 231 | const { promises } = vol; 232 | vol.fromJSON({ 233 | '/foo': 'bar', 234 | }); 235 | it('Resolve when file exists', () => { 236 | return expect(promises.access('/foo')).resolves.toBeUndefined(); 237 | }); 238 | it('Reject when file does not exist', () => { 239 | return expect(promises.access('/bar')).rejects.toBeInstanceOf(Error); 240 | }); 241 | }); 242 | describe('appendFile(path, data[, options])', () => { 243 | it('Append data to existing file', async () => { 244 | const vol = new Volume(); 245 | const { promises } = vol; 246 | vol.fromJSON({ 247 | '/foo': 'bar', 248 | }); 249 | await promises.appendFile('/foo', 'baz'); 250 | expect(vol.readFileSync('/foo').toString()).toEqual('barbaz'); 251 | }); 252 | it('Append data to existing file using FileHandle', async () => { 253 | const vol = new Volume(); 254 | const { promises } = vol; 255 | vol.fromJSON({ 256 | '/foo': 'bar', 257 | }); 258 | const fileHandle = await promises.open('/foo', 'a'); 259 | await promises.appendFile(fileHandle, 'baz'); 260 | await fileHandle.close(); 261 | expect(vol.readFileSync('/foo').toString()).toEqual('barbaz'); 262 | }); 263 | it('Reject when trying to write on a directory', () => { 264 | const vol = new Volume(); 265 | const { promises } = vol; 266 | vol.fromJSON({ 267 | '/foo': null, 268 | }); 269 | return expect(promises.appendFile('/foo', 'bar')).rejects.toBeInstanceOf(Error); 270 | }); 271 | }); 272 | describe('chmod(path, mode)', () => { 273 | const vol = new Volume(); 274 | const { promises } = vol; 275 | vol.fromJSON({ 276 | '/foo': 'bar', 277 | }); 278 | it('Change mode of existing file', async () => { 279 | await promises.chmod('/foo', 0o444); 280 | expect(vol.statSync('/foo').mode & 0o777).toEqual(0o444); 281 | }); 282 | it('Reject when file does not exist', () => { 283 | return expect(promises.chmod('/bar', 0o444)).rejects.toBeInstanceOf(Error); 284 | }); 285 | }); 286 | describe('chown(path, uid, gid)', () => { 287 | const vol = new Volume(); 288 | const { promises } = vol; 289 | vol.fromJSON({ 290 | '/foo': 'bar', 291 | }); 292 | it('Change uid and gid of existing file', async () => { 293 | const { uid, gid } = vol.statSync('/foo'); 294 | await promises.chown('/foo', uid + 1, gid + 1); 295 | const stats = vol.statSync('/foo'); 296 | expect(stats.uid).toEqual(uid + 1); 297 | expect(stats.gid).toEqual(gid + 1); 298 | }); 299 | it('Reject when file does not exist', () => { 300 | return expect(promises.chown('/bar', 0, 0)).rejects.toBeInstanceOf(Error); 301 | }); 302 | }); 303 | describe('copyFile(src, dest[, flags])', () => { 304 | const vol = new Volume(); 305 | const { promises } = vol; 306 | vol.fromJSON({ 307 | '/foo': 'bar', 308 | }); 309 | it('Copy existing file', async () => { 310 | await promises.copyFile('/foo', '/bar'); 311 | expect(vol.readFileSync('/bar').toString()).toEqual('bar'); 312 | }); 313 | it('Reject when file does not exist', () => { 314 | return expect(promises.copyFile('/baz', '/qux')).rejects.toBeInstanceOf(Error); 315 | }); 316 | }); 317 | describe('lchmod(path, mode)', () => { 318 | const vol = new Volume(); 319 | const { promises } = vol; 320 | vol.fromJSON({ 321 | '/foo': 'bar', 322 | }); 323 | vol.symlinkSync('/foo', '/bar'); 324 | it('Change mode of existing file', async () => { 325 | await promises.lchmod('/bar', 0o444); 326 | expect(vol.statSync('/foo').mode & 0o777).toEqual(0o666); 327 | expect(vol.lstatSync('/bar').mode & 0o777).toEqual(0o444); 328 | }); 329 | it('Reject when file does not exist', () => { 330 | return expect(promises.lchmod('/baz', 0o444)).rejects.toBeInstanceOf(Error); 331 | }); 332 | }); 333 | describe('lchown(path, uid, gid)', () => { 334 | const vol = new Volume(); 335 | const { promises } = vol; 336 | vol.fromJSON({ 337 | '/foo': 'bar', 338 | }); 339 | vol.symlinkSync('/foo', '/bar'); 340 | it('Change uid and gid of existing file', async () => { 341 | const fooStatsBefore = vol.statSync('/foo'); 342 | const { uid, gid } = vol.statSync('/bar'); 343 | await promises.lchown('/bar', uid + 1, gid + 1); 344 | const fooStatsAfter = vol.statSync('/foo'); 345 | expect(fooStatsAfter.uid).toEqual(fooStatsBefore.uid); 346 | expect(fooStatsAfter.gid).toEqual(fooStatsBefore.gid); 347 | const stats = vol.lstatSync('/bar'); 348 | expect(stats.uid).toEqual(uid + 1); 349 | expect(stats.gid).toEqual(gid + 1); 350 | }); 351 | it('Reject when file does not exist', () => { 352 | return expect(promises.lchown('/baz', 0, 0)).rejects.toBeInstanceOf(Error); 353 | }); 354 | }); 355 | describe('link(existingPath, newPath)', () => { 356 | const vol = new Volume(); 357 | const { promises } = vol; 358 | vol.fromJSON({ 359 | '/foo': 'bar', 360 | }); 361 | it('Create hard link on existing file', async () => { 362 | await promises.link('/foo', '/bar'); 363 | expect(vol.existsSync('/bar')).toEqual(true); 364 | }); 365 | it('Reject when file does not exist', () => { 366 | return expect(promises.link('/baz', '/qux')).rejects.toBeInstanceOf(Error); 367 | }); 368 | }); 369 | describe('lstat(path)', () => { 370 | const vol = new Volume(); 371 | const { promises } = vol; 372 | vol.fromJSON({ 373 | '/foo': 'bar', 374 | }); 375 | vol.symlinkSync('/foo', '/bar'); 376 | it('Get stats on an existing symbolic link', async () => { 377 | const stats = await promises.lstat('/bar'); 378 | expect(stats.isSymbolicLink()).toEqual(true); 379 | }); 380 | it('Reject when symbolic link does not exist', () => { 381 | return expect(promises.lstat('/baz')).rejects.toBeInstanceOf(Error); 382 | }); 383 | }); 384 | describe('mkdir(path[, options])', () => { 385 | const vol = new Volume(); 386 | const { promises } = vol; 387 | it('Creates a directory', async () => { 388 | await promises.mkdir('/foo'); 389 | expect(vol.statSync('/foo').isDirectory()).toEqual(true); 390 | }); 391 | it('Reject when a file already exists', () => { 392 | vol.writeFileSync('/bar', 'bar'); 393 | return expect(promises.mkdir('/bar')).rejects.toBeInstanceOf(Error); 394 | }); 395 | }); 396 | describe('mkdtemp(prefix[, options])', () => { 397 | const vol = new Volume(); 398 | const { promises } = vol; 399 | it('Creates a temporary directory', async () => { 400 | const tmp = await promises.mkdtemp('/foo'); 401 | expect(vol.statSync(tmp).isDirectory()).toEqual(true); 402 | }); 403 | it('Reject when parent directory does not exist', () => { 404 | return expect(promises.mkdtemp('/foo/bar')).rejects.toBeInstanceOf(Error); 405 | }); 406 | }); 407 | describe('open(path, flags[, mode])', () => { 408 | const vol = new Volume(); 409 | const { promises } = vol; 410 | vol.fromJSON({ 411 | '/foo': 'bar', 412 | }); 413 | it('Open an existing file', async () => { 414 | expect(await promises.open('/foo', 'r')).toBeInstanceOf(promises.FileHandle); 415 | }); 416 | it('Reject when file does not exist', () => { 417 | return expect(promises.open('/bar', 'r')).rejects.toBeInstanceOf(Error); 418 | }); 419 | }); 420 | describe('readdir(path[, options])', () => { 421 | const vol = new Volume(); 422 | const { promises } = vol; 423 | vol.fromJSON({ 424 | '/foo': null, 425 | '/foo/bar': 'bar', 426 | '/foo/baz': 'baz', 427 | }); 428 | it('Read an existing directory', async () => { 429 | expect(await promises.readdir('/foo')).toEqual(['bar', 'baz']); 430 | }); 431 | it('Reject when directory does not exist', () => { 432 | return expect(promises.readdir('/bar')).rejects.toBeInstanceOf(Error); 433 | }); 434 | }); 435 | describe('readFile(id[, options])', () => { 436 | it('Read existing file', async () => { 437 | const vol = new Volume(); 438 | const { promises } = vol; 439 | vol.fromJSON({ 440 | '/foo': 'bar', 441 | }); 442 | expect((await promises.readFile('/foo')).toString()).toEqual('bar'); 443 | }); 444 | it('Read existing file using FileHandle', async () => { 445 | const vol = new Volume(); 446 | const { promises } = vol; 447 | vol.fromJSON({ 448 | '/foo': 'bar', 449 | }); 450 | const fileHandle = await promises.open('/foo', 'r'); 451 | expect((await promises.readFile(fileHandle)).toString()).toEqual('bar'); 452 | await fileHandle.close(); 453 | }); 454 | it('Reject when file does not exist', () => { 455 | const vol = new Volume(); 456 | const { promises } = vol; 457 | return expect(promises.readFile('/foo')).rejects.toBeInstanceOf(Error); 458 | }); 459 | }); 460 | describe('readlink(path[, options])', () => { 461 | const vol = new Volume(); 462 | const { promises } = vol; 463 | vol.symlinkSync('/foo', '/bar'); 464 | it('Read an existing symbolic link', async () => { 465 | expect((await promises.readlink('/bar')).toString()).toEqual('/foo'); 466 | }); 467 | it('Reject when symbolic link does not exist', () => { 468 | return expect(promises.readlink('/foo')).rejects.toBeInstanceOf(Error); 469 | }); 470 | }); 471 | describe('realpath(path[, options])', () => { 472 | const vol = new Volume(); 473 | const { promises } = vol; 474 | vol.fromJSON({ 475 | '/foo': null, 476 | '/foo/bar': null, 477 | '/foo/baz': 'baz', 478 | }); 479 | vol.symlinkSync('/foo/baz', '/foo/qux'); 480 | it('Return real path of existing file', async () => { 481 | expect((await promises.realpath('/foo/bar/../qux')).toString()).toEqual('/foo/baz'); 482 | }); 483 | it('Reject when file does not exist', () => { 484 | return expect(promises.realpath('/bar')).rejects.toBeInstanceOf(Error); 485 | }); 486 | }); 487 | describe('rename(oldPath, newPath)', () => { 488 | it('Rename existing file', async () => { 489 | const vol = new Volume(); 490 | const { promises } = vol; 491 | vol.fromJSON({ 492 | '/foo': 'bar', 493 | }); 494 | await promises.rename('/foo', '/bar'); 495 | expect(vol.readFileSync('/bar').toString()).toEqual('bar'); 496 | }); 497 | it('Reject when file does not exist', () => { 498 | const vol = new Volume(); 499 | const { promises } = vol; 500 | vol.fromJSON({ 501 | '/foo': 'bar', 502 | }); 503 | return expect(promises.rename('/bar', '/baz')).rejects.toBeInstanceOf(Error); 504 | }); 505 | }); 506 | describe('rmdir(path)', () => { 507 | const vol = new Volume(); 508 | const { promises } = vol; 509 | vol.fromJSON({ 510 | '/foo': null, 511 | }); 512 | it('Remove an existing directory', async () => { 513 | await promises.rmdir('/foo'); 514 | expect(vol.existsSync('/foo')).toEqual(false); 515 | }); 516 | it('Reject when directory does not exist', () => { 517 | return expect(promises.rmdir('/bar')).rejects.toBeInstanceOf(Error); 518 | }); 519 | }); 520 | describe('stat(path)', () => { 521 | const vol = new Volume(); 522 | const { promises } = vol; 523 | vol.fromJSON({ 524 | '/foo': null, 525 | }); 526 | it('Return stats of an existing directory', async () => { 527 | expect((await promises.stat('/foo')).isDirectory()).toEqual(true); 528 | }); 529 | it('Reject when directory does not exist', () => { 530 | return expect(promises.stat('/bar')).rejects.toBeInstanceOf(Error); 531 | }); 532 | }); 533 | describe('symlink(target, path[, type])', () => { 534 | it('Create symbolic link', async () => { 535 | const vol = new Volume(); 536 | const { promises } = vol; 537 | vol.fromJSON({ 538 | '/foo': 'bar', 539 | }); 540 | await promises.symlink('/foo', '/bar'); 541 | expect(vol.lstatSync('/bar').isSymbolicLink()).toEqual(true); 542 | }); 543 | it('Reject when file already exists', () => { 544 | const vol = new Volume(); 545 | const { promises } = vol; 546 | vol.fromJSON({ 547 | '/foo': 'bar', 548 | }); 549 | return expect(promises.symlink('/bar', '/foo')).rejects.toBeInstanceOf(Error); 550 | }); 551 | }); 552 | describe('truncate(path[, len])', () => { 553 | const vol = new Volume(); 554 | const { promises } = vol; 555 | vol.fromJSON({ 556 | '/foo': '0123456789', 557 | }); 558 | it('Truncate an existing file', async () => { 559 | await promises.truncate('/foo', 5); 560 | expect(vol.readFileSync('/foo').toString()).toEqual('01234'); 561 | }); 562 | it('Reject when file does not exist', () => { 563 | return expect(promises.truncate('/bar')).rejects.toBeInstanceOf(Error); 564 | }); 565 | }); 566 | describe('unlink(path)', () => { 567 | const vol = new Volume(); 568 | const { promises } = vol; 569 | vol.fromJSON({ 570 | '/foo': 'bar', 571 | }); 572 | it('Unlink an existing file', async () => { 573 | await promises.unlink('/foo'); 574 | expect(vol.existsSync('/foo')).toEqual(false); 575 | }); 576 | it('Reject when file does not exist', () => { 577 | return expect(promises.unlink('/bar')).rejects.toBeInstanceOf(Error); 578 | }); 579 | }); 580 | describe('utimes(path, atime, mtime)', () => { 581 | const vol = new Volume(); 582 | const { promises } = vol; 583 | vol.fromJSON({ 584 | '/foo': 'bar', 585 | }); 586 | const fttDeparture = new Date(1985, 9, 26, 1, 21); 587 | const fttArrival = new Date(fttDeparture.getTime() + 60000); 588 | it('Changes times of an existing file', async () => { 589 | await promises.utimes('/foo', fttArrival, fttDeparture); 590 | const stats = vol.statSync('/foo'); 591 | expect(stats.atime).toEqual(new Date(fttArrival as any)); 592 | expect(stats.mtime).toEqual(new Date(fttDeparture as any)); 593 | }); 594 | it('Reject when file does not exist', () => { 595 | return expect(promises.utimes('/bar', fttArrival, fttDeparture)).rejects.toBeInstanceOf(Error); 596 | }); 597 | }); 598 | describe('writeFile(id, data[, options])', () => { 599 | it('Write data to an existing file', async () => { 600 | const vol = new Volume(); 601 | const { promises } = vol; 602 | vol.fromJSON({ 603 | '/foo': '', 604 | }); 605 | await promises.writeFile('/foo', 'bar'); 606 | expect(vol.readFileSync('/foo').toString()).toEqual('bar'); 607 | }); 608 | it('Write data to existing file using FileHandle', async () => { 609 | const vol = new Volume(); 610 | const { promises } = vol; 611 | vol.fromJSON({ 612 | '/foo': '', 613 | }); 614 | const fileHandle = await promises.open('/foo', 'w'); 615 | await promises.writeFile(fileHandle, 'bar'); 616 | expect(vol.readFileSync('/foo').toString()).toEqual('bar'); 617 | await fileHandle.close(); 618 | }); 619 | it('Reject when trying to write on a directory', () => { 620 | const vol = new Volume(); 621 | const { promises } = vol; 622 | vol.fromJSON({ 623 | '/foo': null, 624 | }); 625 | return expect(promises.writeFile('/foo', 'bar')).rejects.toBeInstanceOf(Error); 626 | }); 627 | }); 628 | }); 629 | -------------------------------------------------------------------------------- /src/__tests__/setImmediate.test.ts: -------------------------------------------------------------------------------- 1 | import setImmediate from '../setImmediate'; 2 | 3 | describe('setImmediate', () => { 4 | it('Is a function', () => { 5 | expect(typeof setImmediate).toBe('function'); 6 | }); 7 | it('Execute callback on next event loop cycle', done => { 8 | setImmediate(done); 9 | }); 10 | }); 11 | -------------------------------------------------------------------------------- /src/__tests__/setTimeoutUnref.test.ts: -------------------------------------------------------------------------------- 1 | import setTimeoutUnref from '../setTimeoutUnref'; 2 | 3 | describe('setTimeoutUnref', () => { 4 | it('Executes callback', done => { 5 | setTimeoutUnref(done); 6 | }); 7 | }); 8 | -------------------------------------------------------------------------------- /src/__tests__/util.ts: -------------------------------------------------------------------------------- 1 | import { createFsFromVolume, Volume } from '..'; 2 | 3 | export const create = (json: { [s: string]: string } = { '/foo': 'bar' }) => { 4 | const vol = Volume.fromJSON(json); 5 | return vol; 6 | }; 7 | 8 | export const createFs = (json?: any) => { 9 | return createFsFromVolume(create(json)); 10 | }; 11 | -------------------------------------------------------------------------------- /src/__tests__/volume.test.ts: -------------------------------------------------------------------------------- 1 | import { URL } from 'url'; 2 | import { Link, Node } from '../node'; 3 | import Stats from '../Stats'; 4 | import Dirent from '../Dirent'; 5 | import { Volume, filenameToSteps, StatWatcher } from '../volume'; 6 | import { constants } from '../constants'; 7 | import hasBigInt from './hasBigInt'; 8 | 9 | describe('volume', () => { 10 | describe('filenameToSteps(filename): string[]', () => { 11 | it('/ -> []', () => { 12 | expect(filenameToSteps('/')).toEqual([]); 13 | }); 14 | it('/test -> ["test"]', () => { 15 | expect(filenameToSteps('/test')).toEqual(['test']); 16 | }); 17 | it('/usr/bin/node.sh -> ["usr", "bin", "node.sh"]', () => { 18 | expect(filenameToSteps('/usr/bin/node.sh')).toEqual(['usr', 'bin', 'node.sh']); 19 | }); 20 | it('/dir/file.txt -> ["dir", "file.txt"]', () => { 21 | expect(filenameToSteps('/dir/file.txt')).toEqual(['dir', 'file.txt']); 22 | }); 23 | it('/dir/./file.txt -> ["dir", "file.txt"]', () => { 24 | expect(filenameToSteps('/dir/./file.txt')).toEqual(['dir', 'file.txt']); 25 | }); 26 | it('/dir/../file.txt -> ["file.txt"]', () => { 27 | expect(filenameToSteps('/dir/../file.txt')).toEqual(['file.txt']); 28 | }); 29 | }); 30 | describe('Volume', () => { 31 | it('.genRndStr()', () => { 32 | const vol = new Volume(); 33 | for (let i = 0; i < 100; i++) { 34 | const str = vol.genRndStr(); 35 | expect(typeof str === 'string').toBe(true); 36 | expect(str.length).toBe(6); 37 | } 38 | }); 39 | describe('.getLink(steps)', () => { 40 | const vol = new Volume(); 41 | it('[] - Get the root link', () => { 42 | const link = vol.getLink([]); 43 | expect(link).toBeInstanceOf(Link); 44 | expect(link).toBe(vol.root); 45 | }); 46 | it('["child.sh"] - Get a child link', () => { 47 | const link1 = vol.root.createChild('child.sh'); 48 | const link2 = vol.getLink(['child.sh']); 49 | expect(link1).toBe(link2); 50 | }); 51 | it('["dir", "child.sh"] - Get a child link in a dir', () => { 52 | const dir = vol.root.createChild('dir'); 53 | const link1 = dir.createChild('child.sh'); 54 | const node2 = vol.getLink(['dir', 'child.sh']); 55 | expect(link1).toBe(node2); 56 | }); 57 | }); 58 | describe('i-nodes', () => { 59 | it('i-node numbers are unique', () => { 60 | const vol = Volume.fromJSON({ 61 | '/1': 'foo', 62 | '/2': 'bar', 63 | }); 64 | const stat1 = vol.statSync('/1'); 65 | const stat2 = vol.statSync('/2'); 66 | expect(stat1.ino === stat2.ino).toBe(false); 67 | }); 68 | }); 69 | 70 | describe('.toJSON()', () => { 71 | it('Single file', () => { 72 | const vol = new Volume(); 73 | vol.writeFileSync('/test', 'Hello'); 74 | expect(vol.toJSON()).toEqual({ '/test': 'Hello' }); 75 | }); 76 | 77 | it('Multiple files', () => { 78 | const vol = new Volume(); 79 | vol.writeFileSync('/test', 'Hello'); 80 | vol.writeFileSync('/test2', 'Hello2'); 81 | vol.writeFileSync('/test.txt', 'Hello3'); 82 | expect(vol.toJSON()).toEqual({ 83 | '/test': 'Hello', 84 | '/test2': 'Hello2', 85 | '/test.txt': 'Hello3', 86 | }); 87 | }); 88 | 89 | it('With folders, skips empty folders', () => { 90 | const vol = new Volume(); 91 | vol.writeFileSync('/test', 'Hello'); 92 | vol.mkdirSync('/dir'); 93 | vol.mkdirSync('/dir/dir2'); 94 | 95 | // Folder `/dir3` will be empty, and should not be in the JSON aoutput. 96 | vol.mkdirSync('/dir3'); 97 | 98 | vol.writeFileSync('/dir/abc', 'abc'); 99 | vol.writeFileSync('/dir/abc2', 'abc2'); 100 | vol.writeFileSync('/dir/dir2/hello.txt', 'world'); 101 | expect(vol.toJSON()).toEqual({ 102 | '/test': 'Hello', 103 | '/dir/abc': 'abc', 104 | '/dir/abc2': 'abc2', 105 | '/dir/dir2/hello.txt': 'world', 106 | '/dir3': null, 107 | }); 108 | }); 109 | 110 | it('Specify export path', () => { 111 | const vol = Volume.fromJSON({ 112 | '/foo': 'bar', 113 | '/dir/a': 'b', 114 | }); 115 | expect(vol.toJSON('/dir')).toEqual({ 116 | '/dir/a': 'b', 117 | }); 118 | }); 119 | 120 | it('Specify multiple export paths', () => { 121 | const vol = Volume.fromJSON({ 122 | '/foo': 'bar', 123 | '/dir/a': 'b', 124 | '/dir2/a': 'b', 125 | '/dir2/c': 'd', 126 | }); 127 | expect(vol.toJSON(['/dir2', '/dir'])).toEqual({ 128 | '/dir/a': 'b', 129 | '/dir2/a': 'b', 130 | '/dir2/c': 'd', 131 | }); 132 | }); 133 | 134 | it('Accumulate exports on supplied object', () => { 135 | const vol = Volume.fromJSON({ 136 | '/foo': 'bar', 137 | }); 138 | const obj = {}; 139 | expect(vol.toJSON('/', obj)).toBe(obj); 140 | }); 141 | 142 | it('Export empty volume', () => { 143 | const vol = Volume.fromJSON({}); 144 | expect(vol.toJSON()).toEqual({}); 145 | }); 146 | 147 | it('Exporting non-existing path', () => { 148 | const vol = Volume.fromJSON({}); 149 | expect(vol.toJSON('/lol')).toEqual({}); 150 | }); 151 | 152 | it('Serializes empty dirs as null', () => { 153 | const vol = Volume.fromJSON({ 154 | '/dir': null, 155 | }); 156 | 157 | expect(vol.toJSON()).toEqual({ 158 | '/dir': null, 159 | }); 160 | }); 161 | 162 | it('Serializes only empty dirs', () => { 163 | const vol = Volume.fromJSON({ 164 | '/dir': null, 165 | '/dir/dir2': null, 166 | '/dir/dir2/foo': null, 167 | '/empty': null, 168 | }); 169 | 170 | expect(vol.toJSON()).toEqual({ 171 | '/dir/dir2/foo': null, 172 | '/empty': null, 173 | }); 174 | }); 175 | }); 176 | 177 | describe('.fromJSON(json[, cwd])', () => { 178 | it('Files at root', () => { 179 | const vol = new Volume(); 180 | const json = { 181 | '/hello': 'world', 182 | '/app.js': 'console.log(123)', 183 | }; 184 | vol.fromJSON(json); 185 | expect(vol.toJSON()).toEqual(json); 186 | }); 187 | 188 | it('Files at root with relative paths', () => { 189 | const vol = new Volume(); 190 | const json = { 191 | hello: 'world', 192 | 'app.js': 'console.log(123)', 193 | }; 194 | vol.fromJSON(json, '/'); 195 | expect(vol.toJSON()).toEqual({ 196 | '/hello': 'world', 197 | '/app.js': 'console.log(123)', 198 | }); 199 | }); 200 | 201 | it('Deeply nested tree', () => { 202 | const vol = new Volume(); 203 | const json = { 204 | '/dir/file': '...', 205 | '/dir/dir/dir2/hello.sh': 'world', 206 | '/hello.js': 'console.log(123)', 207 | '/dir/dir/test.txt': 'Windows', 208 | }; 209 | vol.fromJSON(json); 210 | expect(vol.toJSON()).toEqual(json); 211 | }); 212 | 213 | it('Invalid JSON throws error', () => { 214 | try { 215 | const vol = new Volume(); 216 | const json = { 217 | '/dir/file': '...', 218 | '/dir': 'world', 219 | }; 220 | vol.fromJSON(json); 221 | throw Error('This should not throw'); 222 | } catch (error) { 223 | // Check for both errors, because in JavaScript we the `json` map's key order is not guaranteed. 224 | expect(error.code === 'EISDIR' || error.code === 'ENOTDIR').toBe(true); 225 | } 226 | }); 227 | 228 | it('Invalid JSON throws error 2', () => { 229 | try { 230 | const vol = new Volume(); 231 | const json = { 232 | '/dir': 'world', 233 | '/dir/file': '...', 234 | }; 235 | vol.fromJSON(json); 236 | throw Error('This should not throw'); 237 | } catch (error) { 238 | // Check for both errors, because in JavaScript we the `json` map's key order is not guaranteed. 239 | expect(error.code === 'EISDIR' || error.code === 'ENOTDIR').toBe(true); 240 | } 241 | }); 242 | 243 | it('creates a folder if values is not a string', () => { 244 | const vol = Volume.fromJSON({ 245 | '/dir': null, 246 | }); 247 | const stat = vol.statSync('/dir'); 248 | 249 | expect(stat.isDirectory()).toBe(true); 250 | expect(vol.readdirSync('/dir')).toEqual([]); 251 | }); 252 | }); 253 | 254 | describe('.reset()', () => { 255 | it('Remove all files', () => { 256 | const vol = new Volume(); 257 | const json = { 258 | '/hello': 'world', 259 | '/app.js': 'console.log(123)', 260 | }; 261 | vol.fromJSON(json); 262 | vol.reset(); 263 | expect(vol.toJSON()).toEqual({}); 264 | }); 265 | it('File operations should work after reset', () => { 266 | const vol = new Volume(); 267 | const json = { 268 | '/hello': 'world', 269 | }; 270 | vol.fromJSON(json); 271 | vol.reset(); 272 | vol.writeFileSync('/good', 'bye'); 273 | expect(vol.toJSON()).toEqual({ 274 | '/good': 'bye', 275 | }); 276 | }); 277 | }); 278 | describe('.openSync(path, flags[, mode])', () => { 279 | const vol = new Volume(); 280 | it('Create new file at root (/test.txt)', () => { 281 | const fd = vol.openSync('/test.txt', 'w'); 282 | expect(vol.root.getChild('test.txt')).toBeInstanceOf(Link); 283 | expect(typeof fd).toBe('number'); 284 | expect(fd).toBeGreaterThan(0); 285 | }); 286 | it('Create new directory at root (/)', () => { 287 | vol.mkdirSync('/abc'); 288 | const fd = vol.openSync('/abc', constants.O_DIRECTORY); 289 | // expect(vol.root.getChild('test.txt')).toBeInstanceOf(Link); 290 | expect(typeof fd).toBe('number'); 291 | expect(fd).toBeGreaterThan(0); 292 | }); 293 | it('Error on file not found', () => { 294 | try { 295 | vol.openSync('/non-existing-file.txt', 'r'); 296 | throw Error('This should not throw'); 297 | } catch (err) { 298 | expect(err.code).toBe('ENOENT'); 299 | } 300 | }); 301 | it('Invalid path correct error code', () => { 302 | try { 303 | (vol as any).openSync(123, 'r'); 304 | throw Error('This should not throw'); 305 | } catch (err) { 306 | expect(err).toBeInstanceOf(TypeError); 307 | expect(err.message).toBe('path must be a string or Buffer'); 308 | } 309 | }); 310 | it('Invalid flags correct error code', () => { 311 | try { 312 | (vol as any).openSync('/non-existing-file.txt'); 313 | throw Error('This should not throw'); 314 | } catch (err) { 315 | expect(err.code).toBe('ERR_INVALID_OPT_VALUE'); 316 | } 317 | }); 318 | it('Invalid mode correct error code', () => { 319 | try { 320 | vol.openSync('/non-existing-file.txt', 'r', 'adfasdf'); 321 | throw Error('This should not throw'); 322 | } catch (err) { 323 | expect(err).toBeInstanceOf(TypeError); 324 | expect(err.message).toBe('mode must be an int'); 325 | } 326 | }); 327 | it('Open multiple files', () => { 328 | const fd1 = vol.openSync('/1.json', 'w'); 329 | const fd2 = vol.openSync('/2.json', 'w'); 330 | const fd3 = vol.openSync('/3.json', 'w'); 331 | const fd4 = vol.openSync('/4.json', 'w'); 332 | expect(typeof fd1).toBe('number'); 333 | expect(fd1 !== fd2).toBe(true); 334 | expect(fd2 !== fd3).toBe(true); 335 | expect(fd3 !== fd4).toBe(true); 336 | }); 337 | }); 338 | describe('.open(path, flags[, mode], callback)', () => { 339 | const vol = new Volume(); 340 | vol.mkdirSync('/test-dir'); 341 | it('Create new file at root (/test.txt)', done => { 342 | vol.open('/test.txt', 'w', (err, fd) => { 343 | expect(err).toBe(null); 344 | expect(vol.root.getChild('test.txt')).toBeInstanceOf(Link); 345 | expect(typeof fd).toBe('number'); 346 | expect(fd).toBeGreaterThan(0); 347 | done(); 348 | }); 349 | }); 350 | it('Error on file not found', done => { 351 | vol.open('/non-existing-file.txt', 'r', (err, fd) => { 352 | expect(err.code).toBe('ENOENT'); 353 | done(); 354 | }); 355 | }); 356 | it('Invalid path correct error code thrown synchronously', done => { 357 | try { 358 | (vol as any).open(123, 'r', (err, fd) => { 359 | throw Error('This should not throw'); 360 | }); 361 | throw Error('This should not throw'); 362 | } catch (err) { 363 | expect(err).toBeInstanceOf(TypeError); 364 | expect(err.message).toBe('path must be a string or Buffer'); 365 | done(); 366 | } 367 | }); 368 | it('Invalid flags correct error code thrown synchronously', done => { 369 | try { 370 | (vol as any).open('/non-existing-file.txt', undefined, () => { 371 | throw Error('This should not throw'); 372 | }); 373 | throw Error('This should not throw'); 374 | } catch (err) { 375 | expect(err.code).toBe('ERR_INVALID_OPT_VALUE'); 376 | done(); 377 | } 378 | }); 379 | it('Invalid mode correct error code thrown synchronously', done => { 380 | try { 381 | (vol as any).openSync('/non-existing-file.txt', 'r', 'adfasdf', () => { 382 | throw Error('This should not throw'); 383 | }); 384 | throw Error('This should not throw'); 385 | } catch (err) { 386 | expect(err).toBeInstanceOf(TypeError); 387 | expect(err.message).toBe('mode must be an int'); 388 | done(); 389 | } 390 | }); 391 | it('Properly sets permissions from mode when creating a new file', done => { 392 | vol.writeFileSync('/a.txt', 'foo'); 393 | const stats = vol.statSync('/a.txt'); 394 | // Write a new file, copying the mode from the old file 395 | vol.open('/b.txt', 'w', stats.mode, (err, fd) => { 396 | expect(err).toBe(null); 397 | expect(vol.root.getChild('b.txt')).toBeInstanceOf(Link); 398 | expect(typeof fd).toBe('number'); 399 | expect( 400 | vol.root 401 | .getChild('b.txt') 402 | .getNode() 403 | .canWrite(), 404 | ).toBe(true); 405 | done(); 406 | }); 407 | }); 408 | it('Error on incorrect flags for directory', done => { 409 | vol.open('/test-dir', 'r+', (err, fd) => { 410 | expect(err.code).toBe('EISDIR'); 411 | done(); 412 | }); 413 | }); 414 | it('Properly opens directory as read-only', done => { 415 | vol.open('/test-dir', 'r', (err, fd) => { 416 | expect(err).toBe(null); 417 | expect(typeof fd).toBe('number'); 418 | done(); 419 | }); 420 | }); 421 | }); 422 | describe('.close(fd, callback)', () => { 423 | const vol = new Volume(); 424 | it('Closes file without errors', done => { 425 | vol.open('/test.txt', 'w', (err, fd) => { 426 | expect(err).toBe(null); 427 | vol.close(fd, err => { 428 | expect(err).toBe(null); 429 | done(); 430 | }); 431 | }); 432 | }); 433 | }); 434 | describe('.read(fd, buffer, offset, length, position, callback)', () => { 435 | xit('...', () => {}); 436 | }); 437 | describe('.readFileSync(path[, options])', () => { 438 | const vol = new Volume(); 439 | const data = 'trololo'; 440 | const fileNode = (vol as any).createLink(vol.root, 'text.txt').getNode(); 441 | fileNode.setString(data); 442 | it('Read file at root (/text.txt)', () => { 443 | const buf = vol.readFileSync('/text.txt'); 444 | const str = buf.toString(); 445 | expect(buf).toBeInstanceOf(Buffer); 446 | expect(str).toBe(data); 447 | }); 448 | it('Read file with path passed as URL', () => { 449 | const str = vol.readFileSync(new URL('file:///text.txt')).toString(); 450 | expect(str).toBe(data); 451 | }); 452 | it('Specify encoding as string', () => { 453 | const str = vol.readFileSync('/text.txt', 'utf8'); 454 | expect(str).toBe(data); 455 | }); 456 | it('Specify encoding in object', () => { 457 | const str = vol.readFileSync('/text.txt', { encoding: 'utf8' }); 458 | expect(str).toBe(data); 459 | }); 460 | it('Read file deep in tree (/dir1/dir2/test-file)', () => { 461 | const dir1 = (vol as any).createLink(vol.root, 'dir1', true); 462 | const dir2 = (vol as any).createLink(dir1, 'dir2', true); 463 | const fileNode = (vol as any).createLink(dir2, 'test-file').getNode(); 464 | const data = 'aaaaaa'; 465 | fileNode.setString(data); 466 | 467 | const str = vol.readFileSync('/dir1/dir2/test-file').toString(); 468 | expect(str).toBe(data); 469 | }); 470 | it('Invalid options should throw', () => { 471 | try { 472 | // Expecting this line to throw 473 | vol.readFileSync('/text.txt', 123 as any); 474 | throw Error('This should not throw'); 475 | } catch (err) { 476 | expect(err).toBeInstanceOf(TypeError); 477 | // TODO: Check the right error message. 478 | } 479 | }); 480 | it('Attempt to read a directory should throw EISDIR', () => { 481 | const vol = new Volume(); 482 | vol.mkdirSync('/test'); 483 | const fn = () => vol.readFileSync('/test'); 484 | expect(fn).toThrowError('EISDIR'); 485 | }); 486 | it('Attempt to read a non-existing file should throw ENOENT', () => { 487 | const fn = () => vol.readFileSync('/pizza.txt'); 488 | expect(fn).toThrowError('ENOENT'); 489 | }); 490 | }); 491 | describe('.readFile(path[, options], callback)', () => { 492 | const vol = new Volume(); 493 | const data = 'asdfasdf asdfasdf asdf'; 494 | const fileNode = (vol as any).createLink(vol.root, 'file.txt').getNode(); 495 | fileNode.setString(data); 496 | it('Read file at root (/file.txt)', done => { 497 | vol.readFile('/file.txt', 'utf8', (err, str) => { 498 | expect(err).toBe(null); 499 | expect(str).toBe(data); 500 | done(); 501 | }); 502 | }); 503 | }); 504 | describe('.writeSync(fd, str, position, encoding)', () => { 505 | const vol = new Volume(); 506 | it('Simple write to a file descriptor', () => { 507 | const fd = vol.openSync('/test.txt', 'w+'); 508 | const data = 'hello'; 509 | const bytes = vol.writeSync(fd, data); 510 | vol.closeSync(fd); 511 | expect(bytes).toBe(data.length); 512 | expect(vol.readFileSync('/test.txt', 'utf8')).toBe(data); 513 | }); 514 | it('Multiple writes to a file', () => { 515 | const fd = vol.openSync('/multi.txt', 'w+'); 516 | const datas = ['hello', ' ', 'world', '!']; 517 | let bytes = 0; 518 | for (const data of datas) { 519 | const b = vol.writeSync(fd, data); 520 | expect(b).toBe(data.length); 521 | bytes += b; 522 | } 523 | vol.closeSync(fd); 524 | const result = datas.join(''); 525 | expect(bytes).toBe(result.length); 526 | expect(vol.readFileSync('/multi.txt', 'utf8')).toBe(result); 527 | }); 528 | it('Overwrite part of file', () => { 529 | const fd = vol.openSync('/overwrite.txt', 'w+'); 530 | vol.writeSync(fd, 'martini'); 531 | vol.writeSync(fd, 'Armagedon', 1, 'utf8'); 532 | vol.closeSync(fd); 533 | expect(vol.readFileSync('/overwrite.txt', 'utf8')).toBe('mArmagedon'); 534 | }); 535 | }); 536 | describe('.write(fd, buffer, offset, length, position, callback)', () => { 537 | it('Simple write to a file descriptor', done => { 538 | const vol = new Volume(); 539 | const fd = vol.openSync('/test.txt', 'w+'); 540 | const data = 'hello'; 541 | vol.write(fd, Buffer.from(data), (err, bytes, buf) => { 542 | vol.closeSync(fd); 543 | expect(err).toBe(null); 544 | expect(vol.readFileSync('/test.txt', 'utf8')).toBe(data); 545 | done(); 546 | }); 547 | }); 548 | }); 549 | describe('.writeFile(path, data[, options], callback)', () => { 550 | const vol = new Volume(); 551 | const data = 'asdfasidofjasdf'; 552 | it('Create a file at root (/writeFile.json)', done => { 553 | vol.writeFile('/writeFile.json', data, err => { 554 | expect(err).toBe(null); 555 | const str = vol.root 556 | .getChild('writeFile.json') 557 | .getNode() 558 | .getString(); 559 | expect(str).toBe(data); 560 | done(); 561 | }); 562 | }); 563 | it('Throws error when no callback provided', () => { 564 | try { 565 | vol.writeFile('/asdf.txt', 'asdf', 'utf8', undefined); 566 | throw Error('This should not throw'); 567 | } catch (err) { 568 | expect(err.message).toBe('callback must be a function'); 569 | } 570 | }); 571 | }); 572 | describe('.symlinkSync(target, path[, type])', () => { 573 | const vol = new Volume(); 574 | const jquery = (vol as any).createLink(vol.root, 'jquery.js').getNode(); 575 | const data = '"use strict";'; 576 | jquery.setString(data); 577 | it('Create a symlink', () => { 578 | vol.symlinkSync('/jquery.js', '/test.js'); 579 | expect(vol.root.getChild('test.js')).toBeInstanceOf(Link); 580 | expect( 581 | vol.root 582 | .getChild('test.js') 583 | .getNode() 584 | .isSymlink(), 585 | ).toBe(true); 586 | }); 587 | it('Read from symlink', () => { 588 | vol.symlinkSync('/jquery.js', '/test2.js'); 589 | expect(vol.readFileSync('/test2.js').toString()).toBe(data); 590 | }); 591 | describe('Complex, deep, multi-step symlinks get resolved', () => { 592 | it('Symlink to a folder', () => { 593 | const vol = Volume.fromJSON({ '/a1/a2/a3/a4/a5/hello.txt': 'world!' }); 594 | vol.symlinkSync('/a1', '/b1'); 595 | expect(vol.readFileSync('/b1/a2/a3/a4/a5/hello.txt', 'utf8')).toBe('world!'); 596 | }); 597 | it('Symlink to a folder to a folder', () => { 598 | const vol = Volume.fromJSON({ '/a1/a2/a3/a4/a5/hello.txt': 'world!' }); 599 | vol.symlinkSync('/a1', '/b1'); 600 | vol.symlinkSync('/b1', '/c1'); 601 | vol.openSync('/c1/a2/a3/a4/a5/hello.txt', 'r'); 602 | }); 603 | it('Multiple hops to folders', () => { 604 | const vol = Volume.fromJSON({ 605 | '/a1/a2/a3/a4/a5/hello.txt': 'world a', 606 | '/b1/b2/b3/b4/b5/hello.txt': 'world b', 607 | '/c1/c2/c3/c4/c5/hello.txt': 'world c', 608 | }); 609 | vol.symlinkSync('/a1/a2', '/b1/l'); 610 | vol.symlinkSync('/b1/l', '/b1/b2/b3/ok'); 611 | vol.symlinkSync('/b1/b2/b3/ok', '/c1/a'); 612 | vol.symlinkSync('/c1/a', '/c1/c2/c3/c4/c5/final'); 613 | vol.openSync('/c1/c2/c3/c4/c5/final/a3/a4/a5/hello.txt', 'r'); 614 | expect(vol.readFileSync('/c1/c2/c3/c4/c5/final/a3/a4/a5/hello.txt', 'utf8')).toBe('world a'); 615 | }); 616 | }); 617 | }); 618 | describe('.symlink(target, path[, type], callback)', () => { 619 | xit('...', () => {}); 620 | }); 621 | describe('.realpathSync(path[, options])', () => { 622 | const vol = new Volume(); 623 | const mootools = vol.root.createChild('mootools.js'); 624 | const data = 'String.prototype...'; 625 | mootools.getNode().setString(data); 626 | 627 | const symlink = vol.root.createChild('mootools.link.js'); 628 | symlink.getNode().makeSymlink(['mootools.js']); 629 | 630 | it('Symlink works', () => { 631 | const resolved = vol.resolveSymlinks(symlink); 632 | expect(resolved).toBe(mootools); 633 | }); 634 | it('Basic one-jump symlink resolves', () => { 635 | const path = vol.realpathSync('/mootools.link.js'); 636 | expect(path).toBe('/mootools.js'); 637 | }); 638 | it('Basic one-jump symlink with /./ and /../ in path', () => { 639 | const path = vol.realpathSync('/./lol/../mootools.link.js'); 640 | expect(path).toBe('/mootools.js'); 641 | }); 642 | }); 643 | describe('.realpath(path[, options], callback)', () => { 644 | const vol = new Volume(); 645 | const mootools = vol.root.createChild('mootools.js'); 646 | const data = 'String.prototype...'; 647 | mootools.getNode().setString(data); 648 | 649 | const symlink = vol.root.createChild('mootools.link.js'); 650 | symlink.getNode().makeSymlink(['mootools.js']); 651 | 652 | it('Basic one-jump symlink resolves', done => { 653 | vol.realpath('/mootools.link.js', (err, path) => { 654 | expect(path).toBe('/mootools.js'); 655 | done(); 656 | }); 657 | }); 658 | it('Basic one-jump symlink with /./ and /../ in path', () => { 659 | vol.realpath('/./lol/../mootools.link.js', (err, path) => { 660 | expect(path).toBe('/mootools.js'); 661 | }); 662 | }); 663 | }); 664 | describe('.lstatSync(path)', () => { 665 | const vol = new Volume(); 666 | const dojo = vol.root.createChild('dojo.js'); 667 | const data = '(function(){})();'; 668 | dojo.getNode().setString(data); 669 | 670 | it('Returns basic file stats', () => { 671 | const stats = vol.lstatSync('/dojo.js'); 672 | expect(stats).toBeInstanceOf(Stats); 673 | expect(stats.size).toBe(data.length); 674 | expect(stats.isFile()).toBe(true); 675 | expect(stats.isDirectory()).toBe(false); 676 | }); 677 | it('Returns file stats using BigInt', () => { 678 | if (hasBigInt) { 679 | const stats = vol.lstatSync('/dojo.js', { bigint: true }); 680 | expect(typeof stats.ino).toBe('bigint'); 681 | } else { 682 | expect(() => vol.lstatSync('/dojo.js', { bigint: true })).toThrowError(); 683 | } 684 | }); 685 | it('Stats on symlink returns results about the symlink', () => { 686 | vol.symlinkSync('/dojo.js', '/link.js'); 687 | const stats = vol.lstatSync('/link.js'); 688 | expect(stats.isSymbolicLink()).toBe(true); 689 | expect(stats.isFile()).toBe(false); 690 | expect(stats.size).toBe(0); 691 | }); 692 | }); 693 | describe('.lstat(path, callback)', () => { 694 | xit('...', () => {}); 695 | }); 696 | describe('.statSync(path)', () => { 697 | const vol = new Volume(); 698 | const dojo = vol.root.createChild('dojo.js'); 699 | const data = '(function(){})();'; 700 | dojo.getNode().setString(data); 701 | it('Returns basic file stats', () => { 702 | const stats = vol.statSync('/dojo.js'); 703 | expect(stats).toBeInstanceOf(Stats); 704 | expect(stats.size).toBe(data.length); 705 | expect(stats.isFile()).toBe(true); 706 | expect(stats.isDirectory()).toBe(false); 707 | }); 708 | it('Returns file stats using BigInt', () => { 709 | if (hasBigInt) { 710 | const stats = vol.statSync('/dojo.js', { bigint: true }); 711 | expect(typeof stats.ino).toBe('bigint'); 712 | } else { 713 | expect(() => vol.statSync('/dojo.js', { bigint: true })).toThrowError(); 714 | } 715 | }); 716 | it('Stats on symlink returns results about the resolved file', () => { 717 | vol.symlinkSync('/dojo.js', '/link.js'); 718 | const stats = vol.statSync('/link.js'); 719 | expect(stats.isSymbolicLink()).toBe(false); 720 | expect(stats.isFile()).toBe(true); 721 | expect(stats.size).toBe(data.length); 722 | }); 723 | it('Modification new write', done => { 724 | vol.writeFileSync('/mtime.txt', '1'); 725 | const stats1 = vol.statSync('/mtime.txt'); 726 | setTimeout(() => { 727 | vol.writeFileSync('/mtime.txt', '2'); 728 | const stats2 = vol.statSync('/mtime.txt'); 729 | expect(stats2.mtimeMs).toBeGreaterThan(stats1.mtimeMs); 730 | done(); 731 | }, 2); 732 | }); 733 | }); 734 | describe('.stat(path, callback)', () => { 735 | xit('...', () => {}); 736 | }); 737 | describe('.fstatSync(fd)', () => { 738 | const vol = new Volume(); 739 | const dojo = vol.root.createChild('dojo.js'); 740 | const data = '(function(){})();'; 741 | dojo.getNode().setString(data); 742 | 743 | it('Returns basic file stats', () => { 744 | const fd = vol.openSync('/dojo.js', 'r'); 745 | const stats = vol.fstatSync(fd); 746 | expect(stats).toBeInstanceOf(Stats); 747 | expect(stats.size).toBe(data.length); 748 | expect(stats.isFile()).toBe(true); 749 | expect(stats.isDirectory()).toBe(false); 750 | }); 751 | it('Returns file stats using BigInt', () => { 752 | const fd = vol.openSync('/dojo.js', 'r'); 753 | if (hasBigInt) { 754 | const stats = vol.fstatSync(fd, { bigint: true }); 755 | expect(typeof stats.ino).toBe('bigint'); 756 | } else { 757 | expect(() => vol.fstatSync(fd, { bigint: true })).toThrowError(); 758 | } 759 | }); 760 | }); 761 | describe('.fstat(fd, callback)', () => { 762 | xit('...', () => {}); 763 | }); 764 | describe('.linkSync(existingPath, newPath)', () => { 765 | const vol = new Volume(); 766 | it('Create a new link', () => { 767 | const data = '123'; 768 | vol.writeFileSync('/1.txt', data); 769 | vol.linkSync('/1.txt', '/2.txt'); 770 | expect(vol.readFileSync('/1.txt', 'utf8')).toBe(data); 771 | expect(vol.readFileSync('/2.txt', 'utf8')).toBe(data); 772 | }); 773 | it('nlink property of i-node increases when new link is created', () => { 774 | vol.writeFileSync('/a.txt', '123'); 775 | vol.linkSync('/a.txt', '/b.txt'); 776 | vol.linkSync('/a.txt', '/c.txt'); 777 | const stats = vol.statSync('/b.txt'); 778 | expect(stats.nlink).toBe(3); 779 | }); 780 | }); 781 | describe('.link(existingPath, newPath, callback)', () => { 782 | xit('...', () => {}); 783 | }); 784 | describe('.readdirSync(path)', () => { 785 | it('Returns simple list', () => { 786 | const vol = new Volume(); 787 | vol.writeFileSync('/1.js', '123'); 788 | vol.writeFileSync('/2.js', '123'); 789 | const list = vol.readdirSync('/'); 790 | expect(list.length).toBe(2); 791 | expect(list).toEqual(['1.js', '2.js']); 792 | }); 793 | it('Returns a Dirent list', () => { 794 | const vol = new Volume(); 795 | vol.writeFileSync('/1', '123'); 796 | vol.mkdirSync('/2'); 797 | const list = vol.readdirSync('/', { withFileTypes: true }); 798 | expect(list.length).toBe(2); 799 | expect(list[0]).toBeInstanceOf(Dirent); 800 | const dirent0 = list[0] as Dirent; 801 | expect(dirent0.name).toBe('1'); 802 | expect(dirent0.isFile()).toBe(true); 803 | const dirent1 = list[1] as Dirent; 804 | expect(dirent1.name).toBe('2'); 805 | expect(dirent1.isDirectory()).toBe(true); 806 | }); 807 | }); 808 | describe('.readdir(path, callback)', () => { 809 | xit('...', () => {}); 810 | }); 811 | describe('.readlinkSync(path[, options])', () => { 812 | it('Simple symbolic link to one file', () => { 813 | const vol = new Volume(); 814 | vol.writeFileSync('/1', '123'); 815 | vol.symlinkSync('/1', '/2'); 816 | const res = vol.readlinkSync('/2'); 817 | expect(res).toBe('/1'); 818 | }); 819 | }); 820 | describe('.readlink(path[, options], callback)', () => { 821 | it('Simple symbolic link to one file', done => { 822 | const vol = new Volume(); 823 | vol.writeFileSync('/1', '123'); 824 | vol.symlink('/1', '/2', err => { 825 | vol.readlink('/2', (err, res) => { 826 | expect(res).toBe('/1'); 827 | done(); 828 | }); 829 | }); 830 | }); 831 | }); 832 | describe('.fsyncSync(fd)', () => { 833 | const vol = new Volume(); 834 | const fd = vol.openSync('/lol', 'w'); 835 | it('Executes without crashing', () => { 836 | vol.fsyncSync(fd); 837 | }); 838 | }); 839 | describe('.fsync(fd, callback)', () => { 840 | const vol = new Volume(); 841 | const fd = vol.openSync('/lol', 'w'); 842 | it('Executes without crashing', done => { 843 | vol.fsync(fd, done); 844 | }); 845 | }); 846 | describe('.ftruncateSync(fd[, len])', () => { 847 | const vol = new Volume(); 848 | it('Truncates to 0 single file', () => { 849 | const fd = vol.openSync('/trunky', 'w'); 850 | vol.writeFileSync(fd, '12345'); 851 | expect(vol.readFileSync('/trunky', 'utf8')).toBe('12345'); 852 | vol.ftruncateSync(fd); 853 | expect(vol.readFileSync('/trunky', 'utf8')).toBe(''); 854 | }); 855 | }); 856 | describe('.ftruncate(fd[, len], callback)', () => { 857 | xit('...', () => {}); 858 | }); 859 | describe('.truncateSync(path[, len])', () => { 860 | const vol = new Volume(); 861 | it('Truncates to 0 single file', () => { 862 | const fd = vol.openSync('/trunky', 'w'); 863 | vol.writeFileSync(fd, '12345'); 864 | expect(vol.readFileSync('/trunky', 'utf8')).toBe('12345'); 865 | vol.truncateSync('/trunky'); 866 | expect(vol.readFileSync('/trunky', 'utf8')).toBe(''); 867 | }); 868 | it('Partial truncate', () => { 869 | const fd = vol.openSync('/1', 'w'); 870 | vol.writeFileSync(fd, '12345'); 871 | expect(vol.readFileSync('/1', 'utf8')).toBe('12345'); 872 | vol.truncateSync('/1', 2); 873 | expect(vol.readFileSync('/1', 'utf8')).toBe('12'); 874 | }); 875 | }); 876 | describe('.truncate(path[, len], callback)', () => { 877 | xit('...', () => {}); 878 | }); 879 | describe('.utimesSync(path, atime, mtime)', () => { 880 | const vol = new Volume(); 881 | it('Set times on file', () => { 882 | vol.writeFileSync('/lol', '12345'); 883 | vol.utimesSync('/lol', 1234, 12345); 884 | const stats = vol.statSync('/lol'); 885 | expect(Math.round(stats.atime.getTime() / 1000)).toBe(1234); 886 | expect(Math.round(stats.mtime.getTime() / 1000)).toBe(12345); 887 | }); 888 | }); 889 | describe('.utimes(path, atime, mtime, callback)', () => { 890 | xit('...', () => {}); 891 | }); 892 | describe('.mkdirSync(path[, options])', () => { 893 | it('Create dir at root', () => { 894 | const vol = new Volume(); 895 | vol.mkdirSync('/test'); 896 | const child = vol.root.getChild('test'); 897 | expect(child).toBeInstanceOf(Link); 898 | expect(child.getNode().isDirectory()).toBe(true); 899 | }); 900 | it('Create 2 levels deep folders', () => { 901 | const vol = new Volume(); 902 | vol.mkdirSync('/dir1'); 903 | vol.mkdirSync('/dir1/dir2'); 904 | const dir1 = vol.root.getChild('dir1'); 905 | expect(dir1).toBeInstanceOf(Link); 906 | expect(dir1.getNode().isDirectory()).toBe(true); 907 | const dir2 = dir1.getChild('dir2'); 908 | expect(dir2).toBeInstanceOf(Link); 909 | expect(dir2.getNode().isDirectory()).toBe(true); 910 | expect(dir2.getPath()).toBe('/dir1/dir2'); 911 | }); 912 | it('Create /dir1/dir2/dir3 recursively', () => { 913 | const vol = new Volume(); 914 | vol.mkdirSync('/dir1/dir2/dir3', { recursive: true }); 915 | const dir1 = vol.root.getChild('dir1'); 916 | const dir2 = dir1.getChild('dir2'); 917 | const dir3 = dir2.getChild('dir3'); 918 | expect(dir1).toBeInstanceOf(Link); 919 | expect(dir2).toBeInstanceOf(Link); 920 | expect(dir3).toBeInstanceOf(Link); 921 | expect(dir1.getNode().isDirectory()).toBe(true); 922 | expect(dir2.getNode().isDirectory()).toBe(true); 923 | expect(dir3.getNode().isDirectory()).toBe(true); 924 | }); 925 | }); 926 | describe('.mkdir(path[, mode], callback)', () => { 927 | xit('...', () => {}); 928 | xit('Create /dir1/dir2/dir3', () => {}); 929 | }); 930 | describe('.mkdtempSync(prefix[, options])', () => { 931 | it('Create temp dir at root', () => { 932 | const vol = new Volume(); 933 | const name = vol.mkdtempSync('/tmp-'); 934 | vol.writeFileSync(name + '/file.txt', 'lol'); 935 | expect(vol.toJSON()).toEqual({ [name + '/file.txt']: 'lol' }); 936 | }); 937 | }); 938 | describe('.mkdtemp(prefix[, options], callback)', () => { 939 | xit('Create temp dir at root', () => {}); 940 | }); 941 | describe('.rmdirSync(path)', () => { 942 | it('Remove single dir', () => { 943 | const vol = new Volume(); 944 | vol.mkdirSync('/dir'); 945 | expect( 946 | vol.root 947 | .getChild('dir') 948 | .getNode() 949 | .isDirectory(), 950 | ).toBe(true); 951 | vol.rmdirSync('/dir'); 952 | expect(!!vol.root.getChild('dir')).toBe(false); 953 | }); 954 | }); 955 | describe('.rmdir(path, callback)', () => { 956 | xit('Remove single dir', () => {}); 957 | }); 958 | describe('.watchFile(path[, options], listener)', () => { 959 | it('Calls listener on .writeFile', done => { 960 | const vol = new Volume(); 961 | vol.writeFileSync('/lol.txt', '1'); 962 | setTimeout(() => { 963 | vol.watchFile('/lol.txt', { interval: 1 }, (curr, prev) => { 964 | vol.unwatchFile('/lol.txt'); 965 | done(); 966 | }); 967 | vol.writeFileSync('/lol.txt', '2'); 968 | }, 1); 969 | }); 970 | xit('Multiple listeners for one file', () => {}); 971 | }); 972 | describe('.unwatchFile(path[, listener])', () => { 973 | it('Stops watching before .writeFile', done => { 974 | const vol = new Volume(); 975 | vol.writeFileSync('/lol.txt', '1'); 976 | setTimeout(() => { 977 | let listenerCalled = false; 978 | vol.watchFile('/lol.txt', { interval: 1 }, (curr, prev) => { 979 | listenerCalled = true; 980 | }); 981 | vol.unwatchFile('/lol.txt'); 982 | vol.writeFileSync('/lol.txt', '2'); 983 | setTimeout(() => { 984 | expect(listenerCalled).toBe(false); 985 | done(); 986 | }, 10); 987 | }, 1); 988 | }); 989 | }); 990 | describe('.promises', () => { 991 | it('Have a promises property', () => { 992 | const vol = new Volume(); 993 | expect(typeof vol.promises).toBe('object'); 994 | }); 995 | }); 996 | }); 997 | describe('StatWatcher', () => { 998 | it('.vol points to current volume', () => { 999 | const vol = new Volume(); 1000 | expect(new StatWatcher(vol).vol).toBe(vol); 1001 | }); 1002 | }); 1003 | }); 1004 | -------------------------------------------------------------------------------- /src/__tests__/volume/ReadStream.test.ts: -------------------------------------------------------------------------------- 1 | import { createFs } from '../util'; 2 | 3 | describe('ReadStream', () => { 4 | it('fs has ReadStream constructor', () => { 5 | const fs = createFs(); 6 | expect(typeof fs.ReadStream).toEqual('function'); 7 | }); 8 | it('ReadStream has constructor and prototype property', () => { 9 | const fs = createFs(); 10 | expect(typeof fs.ReadStream.constructor).toEqual('function'); 11 | expect(typeof fs.ReadStream.prototype).toEqual('object'); 12 | }); 13 | it('Can read basic file', done => { 14 | const fs = createFs({ '/a': 'b' }); 15 | const rs = new fs.ReadStream('/a', 'utf8'); 16 | rs.on('data', data => { 17 | expect(String(data)).toEqual('b'); 18 | done(); 19 | }); 20 | }); 21 | }); 22 | -------------------------------------------------------------------------------- /src/__tests__/volume/WriteStream.test.ts: -------------------------------------------------------------------------------- 1 | import { createFs } from '../util'; 2 | 3 | describe('WriteStream', () => { 4 | it('fs has WriteStream constructor', () => { 5 | const fs = createFs(); 6 | expect(typeof fs.WriteStream).toBe('function'); 7 | }); 8 | it('WriteStream has constructor and prototype property', () => { 9 | const fs = createFs(); 10 | expect(typeof fs.WriteStream.constructor).toBe('function'); 11 | expect(typeof fs.WriteStream.prototype).toBe('object'); 12 | }); 13 | it('Can write basic file', done => { 14 | const fs = createFs({ '/a': 'b' }); 15 | const ws = new fs.WriteStream('/a', 'utf8'); 16 | ws.end('d'); 17 | ws.on('finish', () => { 18 | expect(fs.readFileSync('/a', 'utf8')).toBe('d'); 19 | done(); 20 | }); 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /src/__tests__/volume/__snapshots__/mkdirSync.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`mkdirSync throws when creating root directory 1`] = `"EISDIR: illegal operation on a directory, mkdir '/'"`; 4 | 5 | exports[`mkdirSync throws when re-creating existing directory 1`] = `"EEXIST: file already exists, mkdir '/new-dir'"`; 6 | -------------------------------------------------------------------------------- /src/__tests__/volume/__snapshots__/renameSync.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`renameSync(fromPath, toPath) Throws if path is of wrong type 1`] = `"path must be a string or Buffer"`; 4 | 5 | exports[`renameSync(fromPath, toPath) Throws on no params 1`] = `"path must be a string or Buffer"`; 6 | 7 | exports[`renameSync(fromPath, toPath) Throws on only one param 1`] = `"path must be a string or Buffer"`; 8 | -------------------------------------------------------------------------------- /src/__tests__/volume/__snapshots__/writeSync.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`.writeSync(fd, buffer, offset, length, position) Write string to file 1`] = ` 4 | Object { 5 | "/foo": "test", 6 | } 7 | `; 8 | -------------------------------------------------------------------------------- /src/__tests__/volume/appendFile.test.ts: -------------------------------------------------------------------------------- 1 | import { create } from '../util'; 2 | 3 | describe('appendFile(file, data[, options], callback)', () => { 4 | it('Simple write to non-existing file', done => { 5 | const vol = create(); 6 | vol.appendFile('/test', 'hello', (err, res) => { 7 | expect(vol.readFileSync('/test', 'utf8')).toEqual('hello'); 8 | done(); 9 | }); 10 | }); 11 | it('Append to existing file', done => { 12 | const vol = create({ '/a': 'b' }); 13 | vol.appendFile('/a', 'c', (err, res) => { 14 | expect(vol.readFileSync('/a', 'utf8')).toEqual('bc'); 15 | done(); 16 | }); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /src/__tests__/volume/appendFileSync.test.ts: -------------------------------------------------------------------------------- 1 | import { create } from '../util'; 2 | 3 | describe('appendFileSync(file, data, options)', () => { 4 | it('Simple write to non-existing file', () => { 5 | const vol = create(); 6 | vol.appendFileSync('/test', 'hello'); 7 | expect(vol.readFileSync('/test', 'utf8')).toEqual('hello'); 8 | }); 9 | it('Append to existing file', () => { 10 | const vol = create({ '/a': 'b' }); 11 | vol.appendFileSync('/a', 'c'); 12 | expect(vol.readFileSync('/a', 'utf8')).toEqual('bc'); 13 | }); 14 | }); 15 | -------------------------------------------------------------------------------- /src/__tests__/volume/closeSync.test.ts: -------------------------------------------------------------------------------- 1 | import { Volume } from '../..'; 2 | 3 | describe('.closeSync(fd)', () => { 4 | const vol = new Volume(); 5 | it('Closes file without errors', () => { 6 | const fd = vol.openSync('/test.txt', 'w'); 7 | vol.closeSync(fd); 8 | }); 9 | it('Correct error when file descriptor is not a number', () => { 10 | const vol = Volume.fromJSON({ '/foo': 'bar' }); 11 | try { 12 | const fd = vol.openSync('/foo', 'r'); 13 | vol.closeSync(String(fd) as any); 14 | throw Error('This should not throw'); 15 | } catch (err) { 16 | expect(err.message).toEqual('fd must be a file descriptor'); 17 | } 18 | }); 19 | it('Closing file descriptor that does not exist', () => { 20 | const vol = new Volume(); 21 | try { 22 | vol.closeSync(1234); 23 | throw Error('This should not throw'); 24 | } catch (err) { 25 | expect(err.code).toEqual('EBADF'); 26 | } 27 | }); 28 | it('Closing same file descriptor twice throws EBADF', () => { 29 | const fd = vol.openSync('/test.txt', 'w'); 30 | vol.closeSync(fd); 31 | try { 32 | vol.closeSync(fd); 33 | throw Error('This should not throw'); 34 | } catch (err) { 35 | expect(err.code).toEqual('EBADF'); 36 | } 37 | }); 38 | it('Closing a file decreases the number of open files', () => { 39 | const fd = vol.openSync('/test.txt', 'w'); 40 | const openFiles = vol.openFiles; 41 | vol.closeSync(fd); 42 | expect(openFiles).toBeGreaterThan(vol.openFiles); 43 | }); 44 | it('When closing a file, its descriptor is added to the pool of descriptors to be reused', () => { 45 | const fd = vol.openSync('/test.txt', 'w'); 46 | const usedFdLength = vol.releasedFds.length; 47 | vol.closeSync(fd); 48 | expect(usedFdLength).toBeLessThan(vol.releasedFds.length); 49 | }); 50 | }); 51 | -------------------------------------------------------------------------------- /src/__tests__/volume/copyFile.test.ts: -------------------------------------------------------------------------------- 1 | import { create } from '../util'; 2 | import { constants } from '../../constants'; 3 | 4 | describe('copyFile(src, dest[, flags], callback)', () => { 5 | it('method exists', () => { 6 | const vol = create(); 7 | 8 | expect(typeof vol.copyFile).toBe('function'); 9 | }); 10 | 11 | it('copies a file', done => { 12 | const vol = create({ 13 | '/foo': 'hello world', 14 | }); 15 | 16 | expect(vol.readFileSync('/foo', 'utf8')).toBe('hello world'); 17 | expect(() => { 18 | vol.readFileSync('/bar', 'utf8'); 19 | }).toThrow(); 20 | 21 | vol.copyFile('/foo', '/bar', (err, result) => { 22 | expect(!!err).toBe(false); 23 | expect(result).toBe(undefined); 24 | 25 | expect(vol.readFileSync('/foo', 'utf8')).toBe('hello world'); 26 | expect(vol.readFileSync('/bar', 'utf8')).toBe('hello world'); 27 | done(); 28 | }); 29 | }); 30 | 31 | it('honors COPYFILE_EXCL flag', done => { 32 | const vol = create({ 33 | '/foo': 'hello world', 34 | '/bar': 'already exists', 35 | }); 36 | 37 | vol.copyFile('/foo', '/bar', constants.COPYFILE_EXCL, (err, result) => { 38 | expect(err).toBeInstanceOf(Error); 39 | expect(err.message).toContain('EEXIST'); 40 | expect(result).toBe(undefined); 41 | 42 | expect(vol.readFileSync('/foo', 'utf8')).toBe('hello world'); 43 | expect(vol.readFileSync('/bar', 'utf8')).toBe('already exists'); 44 | done(); 45 | }); 46 | }); 47 | }); 48 | -------------------------------------------------------------------------------- /src/__tests__/volume/copyFileSync.test.ts: -------------------------------------------------------------------------------- 1 | import { create } from '../util'; 2 | import { constants } from '../../constants'; 3 | 4 | describe('copyFileSync(src, dest[, flags])', () => { 5 | it('method exists', () => { 6 | const vol = create(); 7 | 8 | expect(typeof vol.copyFileSync).toBe('function'); 9 | }); 10 | 11 | it('throws on incorrect path arguments', () => { 12 | const vol = create(); 13 | 14 | expect(() => { 15 | (vol as any).copyFileSync(); 16 | }).toThrow(); 17 | 18 | expect(() => { 19 | (vol as any).copyFileSync(1); 20 | }).toThrow(); 21 | 22 | expect(() => { 23 | (vol as any).copyFileSync(1, 2); 24 | }).toThrow(); 25 | 26 | expect(() => { 27 | (vol as any).copyFileSync({}, {}); 28 | }).toThrow(); 29 | }); 30 | 31 | it('copies file', () => { 32 | const vol = create({ 33 | '/foo': 'hello world', 34 | }); 35 | 36 | vol.copyFileSync('/foo', '/bar'); 37 | 38 | expect(vol.readFileSync('/bar', 'utf8')).toBe('hello world'); 39 | }); 40 | 41 | describe('when COPYFILE_EXCL flag set', () => { 42 | it('should copy file, if destination does not exit', () => { 43 | const vol = create({ 44 | '/foo': 'hello world', 45 | }); 46 | 47 | vol.copyFileSync('/foo', '/bar', constants.COPYFILE_EXCL); 48 | 49 | expect(vol.readFileSync('/foo', 'utf8')).toBe('hello world'); 50 | expect(vol.readFileSync('/bar', 'utf8')).toBe('hello world'); 51 | }); 52 | 53 | it('should throw, if file already exists', () => { 54 | const vol = create({ 55 | '/foo': 'hello world', 56 | '/bar': 'no hello', 57 | }); 58 | 59 | expect(() => { 60 | vol.copyFileSync('/foo', '/bar', constants.COPYFILE_EXCL); 61 | }).toThrowError(/EEXIST/); 62 | 63 | expect(vol.readFileSync('/foo', 'utf8')).toBe('hello world'); 64 | expect(vol.readFileSync('/bar', 'utf8')).toBe('no hello'); 65 | }); 66 | }); 67 | 68 | describe('when COPYFILE_FICLONE flag set', () => { 69 | it('copies file', () => { 70 | const vol = create({ 71 | '/foo': 'hello world', 72 | }); 73 | 74 | vol.copyFileSync('/foo', '/bar', constants.COPYFILE_FICLONE); 75 | 76 | expect(vol.readFileSync('/foo', 'utf8')).toBe('hello world'); 77 | expect(vol.readFileSync('/bar', 'utf8')).toBe('hello world'); 78 | }); 79 | }); 80 | 81 | describe('when COPYFILE_FICLONE_FORCE flag set', () => { 82 | it('always fails with ENOSYS', () => { 83 | const vol = create({ 84 | '/foo': 'hello world', 85 | }); 86 | 87 | expect(() => { 88 | vol.copyFileSync('/foo', '/bar', constants.COPYFILE_FICLONE_FORCE); 89 | }).toThrowError(/ENOSYS/); 90 | 91 | expect(vol.readFileSync('/foo', 'utf8')).toBe('hello world'); 92 | }); 93 | }); 94 | }); 95 | -------------------------------------------------------------------------------- /src/__tests__/volume/exists.test.ts: -------------------------------------------------------------------------------- 1 | import { create } from '../util'; 2 | 3 | describe('exists(path, callback)', () => { 4 | const vol = create(); 5 | it('Returns true if file exists', done => { 6 | vol.exists('/foo', exists => { 7 | expect(exists).toEqual(true); 8 | done(); 9 | }); 10 | }); 11 | it('Returns false if file does not exist', done => { 12 | vol.exists('/foo2', exists => { 13 | expect(exists).toEqual(false); 14 | done(); 15 | }); 16 | }); 17 | it('Throws correct error if callback not provided', done => { 18 | try { 19 | vol.exists('/foo', undefined); 20 | throw new Error('not_this'); 21 | } catch (err) { 22 | expect(err.message).toEqual('callback must be a function'); 23 | done(); 24 | } 25 | }); 26 | it('invalid path type should throw', () => { 27 | try { 28 | vol.exists(123 as any, () => {}); 29 | throw new Error('not_this'); 30 | } catch (err) { 31 | expect(err.message !== 'not_this').toEqual(true); 32 | } 33 | }); 34 | }); 35 | -------------------------------------------------------------------------------- /src/__tests__/volume/existsSync.test.ts: -------------------------------------------------------------------------------- 1 | import { create } from '../util'; 2 | 3 | describe('existsSync(path)', () => { 4 | const vol = create(); 5 | it('Returns true if file exists', () => { 6 | const result = vol.existsSync('/foo'); 7 | expect(result).toEqual(true); 8 | }); 9 | it('Returns false if file does not exist', () => { 10 | const result = vol.existsSync('/foo2'); 11 | expect(result).toEqual(false); 12 | }); 13 | it('invalid path type should not throw', () => { 14 | expect(vol.existsSync(123 as any)).toEqual(false); 15 | }); 16 | }); 17 | -------------------------------------------------------------------------------- /src/__tests__/volume/mkdirSync.test.ts: -------------------------------------------------------------------------------- 1 | import { create } from '../util'; 2 | 3 | describe('mkdirSync', () => { 4 | it('can create a directory', () => { 5 | const vol = create(); 6 | 7 | vol.mkdirSync('/new-dir'); 8 | const stat = vol.statSync('/new-dir'); 9 | 10 | expect(stat.isDirectory()).toBe(true); 11 | }); 12 | 13 | it('root directory is directory', () => { 14 | const vol = create(); 15 | const stat = vol.statSync('/'); 16 | 17 | expect(stat.isDirectory()).toBe(true); 18 | }); 19 | 20 | it('throws when re-creating existing directory', () => { 21 | const vol = create(); 22 | 23 | vol.mkdirSync('/new-dir'); 24 | 25 | let error; 26 | try { 27 | vol.mkdirSync('/new-dir'); 28 | } catch (err) { 29 | error = err; 30 | } 31 | 32 | expect(error).toBeInstanceOf(Error); 33 | expect(error.message).toMatchSnapshot(); 34 | }); 35 | 36 | /** 37 | * See issue #325 38 | * https://github.com/streamich/memfs/issues/325 39 | */ 40 | it('throws when creating root directory', () => { 41 | const vol = create(); 42 | 43 | let error; 44 | try { 45 | vol.mkdirSync('/'); 46 | } catch (err) { 47 | error = err; 48 | } 49 | 50 | expect(error).toBeInstanceOf(Error); 51 | expect(error.message).toMatchSnapshot(); 52 | }); 53 | }); 54 | -------------------------------------------------------------------------------- /src/__tests__/volume/openSync.test.ts: -------------------------------------------------------------------------------- 1 | import { fs } from '../..'; 2 | 3 | describe('openSync(path, mode[, flags])', () => { 4 | it('should return a file descriptor', () => { 5 | const fd = fs.openSync('/foo', 'w'); 6 | expect(typeof fd).toEqual('number'); 7 | }); 8 | }); 9 | -------------------------------------------------------------------------------- /src/__tests__/volume/readSync.test.ts: -------------------------------------------------------------------------------- 1 | import { create } from '../util'; 2 | 3 | describe('.readSync(fd, buffer, offset, length, position)', () => { 4 | it('Basic read file', () => { 5 | const vol = create({ '/test.txt': '01234567' }); 6 | const buf = Buffer.alloc(3, 0); 7 | const bytes = vol.readSync(vol.openSync('/test.txt', 'r'), buf, 0, 3, 3); 8 | expect(bytes).toBe(3); 9 | expect(buf.equals(Buffer.from('345'))).toBe(true); 10 | }); 11 | xit('Read more than buffer space', () => {}); 12 | xit('Read over file boundary', () => {}); 13 | xit('Read multiple times, caret position should adjust', () => {}); 14 | xit('Negative tests', () => {}); 15 | }); 16 | -------------------------------------------------------------------------------- /src/__tests__/volume/readdirSync.test.ts: -------------------------------------------------------------------------------- 1 | import { create } from '../util'; 2 | 3 | describe('readdirSync()', () => { 4 | it('returns a single directory', () => { 5 | const vol = create({ 6 | '/foo/bar': 'baz', 7 | }); 8 | const dirs = vol.readdirSync('/'); 9 | 10 | expect(dirs).toEqual(['foo']); 11 | }); 12 | 13 | it('returns multiple directories', () => { 14 | const vol = create({ 15 | '/foo/bar': 'baz', 16 | '/tro/lo': 'lo', 17 | '/ab/ra': 'kadabra', 18 | }); 19 | const dirs = vol.readdirSync('/'); 20 | 21 | (dirs as any).sort(); 22 | 23 | expect(dirs).toEqual(['ab', 'foo', 'tro']); 24 | }); 25 | 26 | it('returns empty array when dir empty', () => { 27 | const vol = create({}); 28 | const dirs = vol.readdirSync('/'); 29 | 30 | expect(dirs).toEqual([]); 31 | }); 32 | 33 | it('respects symlinks', () => { 34 | const vol = create({ 35 | '/a/a': 'a', 36 | '/a/aa': 'aa', 37 | '/b/b': 'b', 38 | }); 39 | 40 | vol.symlinkSync('/a', '/b/b/b'); 41 | 42 | const dirs = vol.readdirSync('/b/b/b'); 43 | 44 | (dirs as any).sort(); 45 | 46 | expect(dirs).toEqual(['a', 'aa']); 47 | }); 48 | 49 | it('respects recursive symlinks', () => { 50 | const vol = create({}); 51 | 52 | vol.symlinkSync('/', '/foo'); 53 | 54 | const dirs = vol.readdirSync('/foo'); 55 | 56 | expect(dirs).toEqual(['foo']); 57 | }); 58 | }); 59 | -------------------------------------------------------------------------------- /src/__tests__/volume/rename.test.ts: -------------------------------------------------------------------------------- 1 | import { create } from '../util'; 2 | 3 | describe('renameSync(fromPath, toPath)', () => { 4 | it('Renames a simple case', done => { 5 | const vol = create({ '/foo': 'bar' }); 6 | vol.rename('/foo', '/foo2', (err, res) => { 7 | expect(vol.toJSON()).toEqual({ '/foo2': 'bar' }); 8 | done(); 9 | }); 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /src/__tests__/volume/renameSync.test.ts: -------------------------------------------------------------------------------- 1 | import { create } from '../util'; 2 | 3 | describe('renameSync(fromPath, toPath)', () => { 4 | it('Renames a file', () => { 5 | const vol = create({ '/foo': 'bar' }); 6 | expect( 7 | vol.root 8 | .getChild('foo') 9 | .getNode() 10 | .isFile(), 11 | ).toBe(true); 12 | vol.renameSync('/foo', '/baz'); 13 | expect(vol.root.getChild('foo')).toBeUndefined(); 14 | expect( 15 | vol.root 16 | .getChild('baz') 17 | .getNode() 18 | .isFile(), 19 | ).toBe(true); 20 | expect(vol.readFileSync('/baz', 'utf8')).toBe('bar'); 21 | }); 22 | it('Rename file two levels deep', () => { 23 | const vol = create({ '/1/2': 'foobar' }); 24 | vol.renameSync('/1/2', '/1/3'); 25 | expect(vol.toJSON()).toEqual({ '/1/3': 'foobar' }); 26 | }); 27 | it('Rename file three levels deep', () => { 28 | const vol = create({ 29 | '/foo1': 'bar', 30 | '/foo2/foo': 'bar', 31 | '/foo3/foo/foo': 'bar', 32 | }); 33 | vol.renameSync('/foo3/foo/foo', '/foo3/foo/foo2'); 34 | expect(vol.toJSON()).toEqual({ 35 | '/foo1': 'bar', 36 | '/foo2/foo': 'bar', 37 | '/foo3/foo/foo2': 'bar', 38 | }); 39 | }); 40 | it('Throws on no params', () => { 41 | const vol = create(); 42 | expect(() => { 43 | (vol as any).renameSync(); 44 | }).toThrowErrorMatchingSnapshot(); 45 | }); 46 | it('Throws on only one param', () => { 47 | const vol = create({ '/foo': 'bar' }); 48 | expect(() => { 49 | (vol as any).renameSync('/foo'); 50 | }).toThrowErrorMatchingSnapshot(); 51 | }); 52 | it('Throws if path is of wrong type', () => { 53 | const vol = create({ '/foo': 'bar' }); 54 | expect(() => { 55 | (vol as any).renameSync('/foo', 123); 56 | }).toThrowErrorMatchingSnapshot(); 57 | }); 58 | }); 59 | -------------------------------------------------------------------------------- /src/__tests__/volume/write.test.ts: -------------------------------------------------------------------------------- 1 | import { Volume } from '../..'; 2 | 3 | const create = (json = { '/foo': 'bar' }) => { 4 | const vol = Volume.fromJSON(json); 5 | return vol; 6 | }; 7 | 8 | describe('write(fs, str, position, encoding, callback)', () => { 9 | it('Simple write to file', done => { 10 | const vol = create(); 11 | const fd = vol.openSync('/test', 'w'); 12 | vol.write(fd, 'lol', 0, 'utf8', (err, bytes, str) => { 13 | expect(err).toEqual(null); 14 | expect(bytes).toEqual(3); 15 | expect(str).toEqual('lol'); 16 | expect(vol.readFileSync('/test', 'utf8')).toEqual('lol'); 17 | done(); 18 | }); 19 | }); 20 | }); 21 | -------------------------------------------------------------------------------- /src/__tests__/volume/writeFileSync.test.ts: -------------------------------------------------------------------------------- 1 | import { create } from '../util'; 2 | import { Node } from '../../node'; 3 | 4 | describe('writeFileSync(path, data[, options])', () => { 5 | const data = 'asdfasidofjasdf'; 6 | it('Create a file at root (/writeFileSync.txt)', () => { 7 | const vol = create(); 8 | vol.writeFileSync('/writeFileSync.txt', data); 9 | 10 | const node = vol.root.getChild('writeFileSync.txt').getNode(); 11 | expect(node).toBeInstanceOf(Node); 12 | expect(node.getString()).toBe(data); 13 | }); 14 | it('Write to file by file descriptor', () => { 15 | const vol = create(); 16 | const fd = vol.openSync('/writeByFd.txt', 'w'); 17 | vol.writeFileSync(fd, data); 18 | const node = vol.root.getChild('writeByFd.txt').getNode(); 19 | expect(node).toBeInstanceOf(Node); 20 | expect(node.getString()).toBe(data); 21 | }); 22 | it('Write to two files (second by fd)', () => { 23 | const vol = create(); 24 | 25 | // 1 26 | vol.writeFileSync('/1.txt', '123'); 27 | 28 | // 2, 3, 4 29 | const fd2 = vol.openSync('/2.txt', 'w'); 30 | const fd3 = vol.openSync('/3.txt', 'w'); 31 | const fd4 = vol.openSync('/4.txt', 'w'); 32 | 33 | vol.writeFileSync(fd2, '456'); 34 | 35 | expect( 36 | vol.root 37 | .getChild('1.txt') 38 | .getNode() 39 | .getString(), 40 | ).toBe('123'); 41 | expect( 42 | vol.root 43 | .getChild('2.txt') 44 | .getNode() 45 | .getString(), 46 | ).toBe('456'); 47 | }); 48 | it('Write at relative path that does not exist throws correct error', () => { 49 | const vol = create(); 50 | try { 51 | vol.writeFileSync('a/b', 'c'); 52 | throw new Error('not_this'); 53 | } catch (err) { 54 | expect(err.code).toBe('ENOENT'); 55 | } 56 | }); 57 | }); 58 | -------------------------------------------------------------------------------- /src/__tests__/volume/writeSync.test.ts: -------------------------------------------------------------------------------- 1 | import { create } from '../util'; 2 | import { Volume } from '../../volume'; 3 | 4 | describe('.writeSync(fd, buffer, offset, length, position)', () => { 5 | let vol: Volume; 6 | 7 | beforeEach(() => { 8 | vol = create({}); 9 | }); 10 | 11 | it('Write binary data to file', () => { 12 | const fd = vol.openSync('/data.bin', 'w+'); 13 | const bytes = vol.writeSync(fd, Buffer.from([1, 2, 3])); 14 | vol.closeSync(fd); 15 | expect(bytes).toBe(3); 16 | expect(Buffer.from([1, 2, 3]).equals(vol.readFileSync('/data.bin') as Buffer)).toBe(true); 17 | }); 18 | it('Write string to file', () => { 19 | const fd = vol.openSync('/foo', 'w'); 20 | vol.writeSync(fd, 'test'); 21 | expect(vol.toJSON()).toMatchSnapshot(); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /src/__tests__/wasmfs.test.ts: -------------------------------------------------------------------------------- 1 | import { WasmFs } from '../index'; 2 | 3 | describe('wasmfs', () => { 4 | let wasmfs: WasmFs; 5 | 6 | beforeEach(async () => { 7 | wasmfs = new WasmFs(); 8 | }); 9 | 10 | it('should have stdin, stdout, and stderr', async () => { 11 | expect(wasmfs.fs.existsSync('/dev/stdin')).toBe(true); 12 | expect(wasmfs.fs.existsSync('/dev/stdout')).toBe(true); 13 | expect(wasmfs.fs.existsSync('/dev/stderr')).toBe(true); 14 | }); 15 | 16 | it('should be able to retrieve stdout', async () => { 17 | const stdout = 'test'; 18 | wasmfs.fs.writeFileSync('/dev/stdout', stdout); 19 | 20 | const response = await wasmfs.getStdOut(); 21 | expect(response).toBe(stdout); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /src/constants.ts: -------------------------------------------------------------------------------- 1 | export const constants = { 2 | O_RDONLY: 0, 3 | O_WRONLY: 1, 4 | O_RDWR: 2, 5 | S_IFMT: 61440, 6 | S_IFREG: 32768, 7 | S_IFDIR: 16384, 8 | S_IFCHR: 8192, 9 | S_IFBLK: 24576, 10 | S_IFIFO: 4096, 11 | S_IFLNK: 40960, 12 | S_IFSOCK: 49152, 13 | O_CREAT: 64, 14 | O_EXCL: 128, 15 | O_NOCTTY: 256, 16 | O_TRUNC: 512, 17 | O_APPEND: 1024, 18 | O_DIRECTORY: 65536, 19 | O_NOATIME: 262144, 20 | O_NOFOLLOW: 131072, 21 | O_SYNC: 1052672, 22 | O_DIRECT: 16384, 23 | O_NONBLOCK: 2048, 24 | S_IRWXU: 448, 25 | S_IRUSR: 256, 26 | S_IWUSR: 128, 27 | S_IXUSR: 64, 28 | S_IRWXG: 56, 29 | S_IRGRP: 32, 30 | S_IWGRP: 16, 31 | S_IXGRP: 8, 32 | S_IRWXO: 7, 33 | S_IROTH: 4, 34 | S_IWOTH: 2, 35 | S_IXOTH: 1, 36 | 37 | F_OK: 0, 38 | R_OK: 4, 39 | W_OK: 2, 40 | X_OK: 1, 41 | 42 | UV_FS_SYMLINK_DIR: 1, 43 | UV_FS_SYMLINK_JUNCTION: 2, 44 | 45 | UV_FS_COPYFILE_EXCL: 1, 46 | UV_FS_COPYFILE_FICLONE: 2, 47 | UV_FS_COPYFILE_FICLONE_FORCE: 4, 48 | COPYFILE_EXCL: 1, 49 | COPYFILE_FICLONE: 2, 50 | COPYFILE_FICLONE_FORCE: 4, 51 | }; 52 | 53 | export const enum S { 54 | ISUID = 0b100000000000, // (04000) set-user-ID (set process effective user ID on execve(2)) 55 | ISGID = 0b10000000000, // (02000) set-group-ID (set process effective group ID on execve(2); mandatory locking, as described in fcntl(2); take a new file's group from parent directory, as described in chown(2) and mkdir(2)) 56 | ISVTX = 0b1000000000, // (01000) sticky bit (restricted deletion flag, as described in unlink(2)) 57 | IRUSR = 0b100000000, // (00400) read by owner 58 | IWUSR = 0b10000000, // (00200) write by owner 59 | IXUSR = 0b1000000, // (00100) execute/search by owner 60 | IRGRP = 0b100000, // (00040) read by group 61 | IWGRP = 0b10000, // (00020) write by group 62 | IXGRP = 0b1000, // (00010) execute/search by group 63 | IROTH = 0b100, // (00004) read by others 64 | IWOTH = 0b10, // (00002) write by others 65 | IXOTH = 0b1, // (00001) execute/search by others 66 | } 67 | -------------------------------------------------------------------------------- /src/encoding.ts: -------------------------------------------------------------------------------- 1 | import * as errors from './internal/errors'; 2 | 3 | export type TDataOut = string | Buffer; // Data formats we give back to users. 4 | export type TEncoding = 'ascii' | 'utf8' | 'utf16le' | 'ucs2' | 'base64' | 'latin1' | 'binary' | 'hex'; 5 | export type TEncodingExtended = TEncoding | 'buffer'; 6 | 7 | export const ENCODING_UTF8: TEncoding = 'utf8'; 8 | 9 | export function assertEncoding(encoding: string) { 10 | if (encoding && !Buffer.isEncoding(encoding)) throw new errors.TypeError('ERR_INVALID_OPT_VALUE_ENCODING', encoding); 11 | } 12 | 13 | export function strToEncoding(str: string, encoding?: TEncodingExtended): TDataOut { 14 | if (!encoding || encoding === ENCODING_UTF8) return str; // UTF-8 15 | if (encoding === 'buffer') return new Buffer(str); // `buffer` encoding 16 | return new Buffer(str).toString(encoding); // Custom encoding 17 | } 18 | -------------------------------------------------------------------------------- /src/getBigInt.js: -------------------------------------------------------------------------------- 1 | if (typeof BigInt === 'function') exports.default = BigInt; 2 | else 3 | exports.default = function BigIntNotSupported() { 4 | throw new Error('BigInt is not supported in this environment.'); 5 | }; 6 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import Stats from './Stats'; 2 | import Dirent from './Dirent'; 3 | import { Volume as _Volume, StatWatcher, FSWatcher, toUnixTimestamp, IReadStream, IWriteStream } from './volume'; 4 | import { IPromisesAPI } from './promises'; 5 | export { WasmFs } from './wasm'; 6 | const { fsSyncMethods, fsAsyncMethods } = require('fs-monkey/lib/util/lists'); 7 | import { constants } from './constants'; 8 | const { F_OK, R_OK, W_OK, X_OK } = constants; 9 | 10 | export const Volume = _Volume; 11 | 12 | // Default volume. 13 | export const vol = new _Volume(); 14 | 15 | export interface IFs extends _Volume { 16 | constants: typeof constants; 17 | Stats: new (...args: any[]) => Stats; 18 | Dirent: new (...arg: any[]) => Dirent; 19 | StatWatcher: new () => StatWatcher; 20 | FSWatcher: new () => FSWatcher; 21 | ReadStream: new (...args: any[]) => IReadStream; 22 | WriteStream: new (...args: any[]) => IWriteStream; 23 | promises: IPromisesAPI; 24 | _toUnixTimestamp: any; 25 | } 26 | 27 | export function createFsFromVolume(vol: _Volume): IFs { 28 | const fs = ({ F_OK, R_OK, W_OK, X_OK, constants, Stats, Dirent } as any) as IFs; 29 | 30 | // Bind FS methods. 31 | for (const method of fsSyncMethods) 32 | if (typeof (vol as any)[method] === 'function') (fs as any)[method] = (vol as any)[method].bind(vol); 33 | for (const method of fsAsyncMethods) 34 | if (typeof (vol as any)[method] === 'function') (fs as any)[method] = (vol as any)[method].bind(vol); 35 | 36 | fs.StatWatcher = vol.StatWatcher; 37 | fs.FSWatcher = vol.FSWatcher; 38 | fs.WriteStream = vol.WriteStream; 39 | fs.ReadStream = vol.ReadStream; 40 | fs.promises = vol.promises; 41 | 42 | fs._toUnixTimestamp = toUnixTimestamp; 43 | 44 | return fs; 45 | } 46 | 47 | export const fs: IFs = createFsFromVolume(vol); 48 | // @ts-ignore 49 | declare let module; 50 | module.exports = { ...module.exports, ...fs }; 51 | 52 | module.exports.semantic = true; 53 | -------------------------------------------------------------------------------- /src/internal/errors.ts: -------------------------------------------------------------------------------- 1 | // The whole point behind this internal module is to allow Node.js to no 2 | // longer be forced to treat every error message change as a semver-major 3 | // change. The NodeError classes here all expose a `code` property whose 4 | // value statically and permanently identifies the error. While the error 5 | // message may change, the code should not. 6 | 7 | const kCode = typeof Symbol === 'undefined' ? '_kCode' : (Symbol as any)('code'); 8 | const messages = {}; // new Map(); 9 | 10 | // Lazily loaded 11 | let assert = null; 12 | let util = null; 13 | 14 | function makeNodeError(Base) { 15 | return class NodeError extends Base { 16 | constructor(key, ...args) { 17 | super(message(key, args)); 18 | this.code = key; 19 | this[kCode] = key; 20 | this.name = `${super.name} [${this[kCode]}]`; 21 | } 22 | }; 23 | } 24 | 25 | class AssertionError extends global.Error { 26 | generatedMessage: any; 27 | name: any; 28 | code: any; 29 | actual: any; 30 | expected: any; 31 | operator: any; 32 | 33 | constructor(options) { 34 | if (typeof options !== 'object' || options === null) { 35 | throw new exports.TypeError('ERR_INVALID_ARG_TYPE', 'options', 'object'); 36 | } 37 | if (options.message) { 38 | super(options.message); 39 | } else { 40 | if (util === null) { 41 | util = require('util'); 42 | } 43 | super( 44 | `${util.inspect(options.actual).slice(0, 128)} ` + 45 | `${options.operator} ${util.inspect(options.expected).slice(0, 128)}`, 46 | ); 47 | } 48 | 49 | this.generatedMessage = !options.message; 50 | this.name = 'AssertionError [ERR_ASSERTION]'; 51 | this.code = 'ERR_ASSERTION'; 52 | this.actual = options.actual; 53 | this.expected = options.expected; 54 | this.operator = options.operator; 55 | Error.captureStackTrace(this, options.stackStartFunction); 56 | } 57 | } 58 | 59 | function message(key, args) { 60 | if (assert === null) { 61 | assert = require('assert'); 62 | } 63 | assert.strictEqual(typeof key, 'string'); 64 | // const msg = messages.get(key); 65 | const msg = messages[key]; 66 | assert(msg, `An invalid error message key was used: ${key}.`); 67 | let fmt; 68 | if (typeof msg === 'function') { 69 | fmt = msg; 70 | } else { 71 | if (util === null) { 72 | util = require('util'); 73 | } 74 | fmt = util.format; 75 | if (args === undefined || args.length === 0) return msg; 76 | args.unshift(msg); 77 | } 78 | return String(fmt.apply(null, args)); 79 | } 80 | 81 | // Utility function for registering the error codes. Only used here. Exported 82 | // *only* to allow for testing. 83 | function E(sym, val) { 84 | messages[sym] = typeof val === 'function' ? val : String(val); 85 | } 86 | 87 | export const Error = makeNodeError(global.Error); 88 | export const TypeError = makeNodeError(global.TypeError); 89 | export const RangeError = makeNodeError(global.RangeError); 90 | 91 | export { 92 | message, 93 | AssertionError, 94 | E, // This is exported only to facilitate testing. 95 | }; 96 | 97 | // To declare an error message, use the E(sym, val) function above. The sym 98 | // must be an upper case string. The val can be either a function or a string. 99 | // The return value of the function must be a string. 100 | // Examples: 101 | // E('EXAMPLE_KEY1', 'This is the error value'); 102 | // E('EXAMPLE_KEY2', (a, b) => return `${a} ${b}`); 103 | // 104 | // Once an error code has been assigned, the code itself MUST NOT change and 105 | // any given error code must never be reused to identify a different error. 106 | // 107 | // Any error code added here should also be added to the documentation 108 | // 109 | // Note: Please try to keep these in alphabetical order 110 | E('ERR_ARG_NOT_ITERABLE', '%s must be iterable'); 111 | E('ERR_ASSERTION', '%s'); 112 | E('ERR_BUFFER_OUT_OF_BOUNDS', bufferOutOfBounds); 113 | E('ERR_CHILD_CLOSED_BEFORE_REPLY', 'Child closed before reply received'); 114 | E('ERR_CONSOLE_WRITABLE_STREAM', 'Console expects a writable stream instance for %s'); 115 | E('ERR_CPU_USAGE', 'Unable to obtain cpu usage %s'); 116 | E('ERR_DNS_SET_SERVERS_FAILED', (err, servers) => `c-ares failed to set servers: "${err}" [${servers}]`); 117 | E('ERR_FALSY_VALUE_REJECTION', 'Promise was rejected with falsy value'); 118 | E('ERR_ENCODING_NOT_SUPPORTED', enc => `The "${enc}" encoding is not supported`); 119 | E('ERR_ENCODING_INVALID_ENCODED_DATA', enc => `The encoded data was not valid for encoding ${enc}`); 120 | E('ERR_HTTP_HEADERS_SENT', 'Cannot render headers after they are sent to the client'); 121 | E('ERR_HTTP_INVALID_STATUS_CODE', 'Invalid status code: %s'); 122 | E('ERR_HTTP_TRAILER_INVALID', 'Trailers are invalid with this transfer encoding'); 123 | E('ERR_INDEX_OUT_OF_RANGE', 'Index out of range'); 124 | E('ERR_INVALID_ARG_TYPE', invalidArgType); 125 | E('ERR_INVALID_ARRAY_LENGTH', (name, len, actual) => { 126 | assert.strictEqual(typeof actual, 'number'); 127 | return `The array "${name}" (length ${actual}) must be of length ${len}.`; 128 | }); 129 | E('ERR_INVALID_BUFFER_SIZE', 'Buffer size must be a multiple of %s'); 130 | E('ERR_INVALID_CALLBACK', 'Callback must be a function'); 131 | E('ERR_INVALID_CHAR', 'Invalid character in %s'); 132 | E('ERR_INVALID_CURSOR_POS', 'Cannot set cursor row without setting its column'); 133 | E('ERR_INVALID_FD', '"fd" must be a positive integer: %s'); 134 | E('ERR_INVALID_FILE_URL_HOST', 'File URL host must be "localhost" or empty on %s'); 135 | E('ERR_INVALID_FILE_URL_PATH', 'File URL path %s'); 136 | E('ERR_INVALID_HANDLE_TYPE', 'This handle type cannot be sent'); 137 | E('ERR_INVALID_IP_ADDRESS', 'Invalid IP address: %s'); 138 | E('ERR_INVALID_OPT_VALUE', (name, value) => { 139 | return `The value "${String(value)}" is invalid for option "${name}"`; 140 | }); 141 | E('ERR_INVALID_OPT_VALUE_ENCODING', value => `The value "${String(value)}" is invalid for option "encoding"`); 142 | E('ERR_INVALID_REPL_EVAL_CONFIG', 'Cannot specify both "breakEvalOnSigint" and "eval" for REPL'); 143 | E('ERR_INVALID_SYNC_FORK_INPUT', 'Asynchronous forks do not support Buffer, Uint8Array or string input: %s'); 144 | E('ERR_INVALID_THIS', 'Value of "this" must be of type %s'); 145 | E('ERR_INVALID_TUPLE', '%s must be an iterable %s tuple'); 146 | E('ERR_INVALID_URL', 'Invalid URL: %s'); 147 | E('ERR_INVALID_URL_SCHEME', expected => `The URL must be ${oneOf(expected, 'scheme')}`); 148 | E('ERR_IPC_CHANNEL_CLOSED', 'Channel closed'); 149 | E('ERR_IPC_DISCONNECTED', 'IPC channel is already disconnected'); 150 | E('ERR_IPC_ONE_PIPE', 'Child process can have only one IPC pipe'); 151 | E('ERR_IPC_SYNC_FORK', 'IPC cannot be used with synchronous forks'); 152 | E('ERR_MISSING_ARGS', missingArgs); 153 | E('ERR_MULTIPLE_CALLBACK', 'Callback called multiple times'); 154 | E('ERR_NAPI_CONS_FUNCTION', 'Constructor must be a function'); 155 | E('ERR_NAPI_CONS_PROTOTYPE_OBJECT', 'Constructor.prototype must be an object'); 156 | E('ERR_NO_CRYPTO', 'Node.js is not compiled with OpenSSL crypto support'); 157 | E('ERR_NO_LONGER_SUPPORTED', '%s is no longer supported'); 158 | E('ERR_PARSE_HISTORY_DATA', 'Could not parse history data in %s'); 159 | E('ERR_SOCKET_ALREADY_BOUND', 'Socket is already bound'); 160 | E('ERR_SOCKET_BAD_PORT', 'Port should be > 0 and < 65536'); 161 | E('ERR_SOCKET_BAD_TYPE', 'Bad socket type specified. Valid types are: udp4, udp6'); 162 | E('ERR_SOCKET_CANNOT_SEND', 'Unable to send data'); 163 | E('ERR_SOCKET_CLOSED', 'Socket is closed'); 164 | E('ERR_SOCKET_DGRAM_NOT_RUNNING', 'Not running'); 165 | E('ERR_STDERR_CLOSE', 'process.stderr cannot be closed'); 166 | E('ERR_STDOUT_CLOSE', 'process.stdout cannot be closed'); 167 | E('ERR_STREAM_WRAP', 'Stream has StringDecoder set or is in objectMode'); 168 | E('ERR_TLS_CERT_ALTNAME_INVALID', "Hostname/IP does not match certificate's altnames: %s"); 169 | E('ERR_TLS_DH_PARAM_SIZE', size => `DH parameter size ${size} is less than 2048`); 170 | E('ERR_TLS_HANDSHAKE_TIMEOUT', 'TLS handshake timeout'); 171 | E('ERR_TLS_RENEGOTIATION_FAILED', 'Failed to renegotiate'); 172 | E('ERR_TLS_REQUIRED_SERVER_NAME', '"servername" is required parameter for Server.addContext'); 173 | E('ERR_TLS_SESSION_ATTACK', 'TSL session renegotiation attack detected'); 174 | E('ERR_TRANSFORM_ALREADY_TRANSFORMING', 'Calling transform done when still transforming'); 175 | E('ERR_TRANSFORM_WITH_LENGTH_0', 'Calling transform done when writableState.length != 0'); 176 | E('ERR_UNKNOWN_ENCODING', 'Unknown encoding: %s'); 177 | E('ERR_UNKNOWN_SIGNAL', 'Unknown signal: %s'); 178 | E('ERR_UNKNOWN_STDIN_TYPE', 'Unknown stdin file type'); 179 | E('ERR_UNKNOWN_STREAM_TYPE', 'Unknown stream file type'); 180 | E('ERR_V8BREAKITERATOR', 'Full ICU data not installed. ' + 'See https://github.com/nodejs/node/wiki/Intl'); 181 | 182 | function invalidArgType(name, expected, actual) { 183 | assert(name, 'name is required'); 184 | 185 | // determiner: 'must be' or 'must not be' 186 | let determiner; 187 | if (expected.includes('not ')) { 188 | determiner = 'must not be'; 189 | expected = expected.split('not ')[1]; 190 | } else { 191 | determiner = 'must be'; 192 | } 193 | 194 | let msg; 195 | if (Array.isArray(name)) { 196 | const names = name.map(val => `"${val}"`).join(', '); 197 | msg = `The ${names} arguments ${determiner} ${oneOf(expected, 'type')}`; 198 | } else if (name.includes(' argument')) { 199 | // for the case like 'first argument' 200 | msg = `The ${name} ${determiner} ${oneOf(expected, 'type')}`; 201 | } else { 202 | const type = name.includes('.') ? 'property' : 'argument'; 203 | msg = `The "${name}" ${type} ${determiner} ${oneOf(expected, 'type')}`; 204 | } 205 | 206 | // if actual value received, output it 207 | if (arguments.length >= 3) { 208 | msg += `. Received type ${actual !== null ? typeof actual : 'null'}`; 209 | } 210 | return msg; 211 | } 212 | 213 | function missingArgs(...args) { 214 | assert(args.length > 0, 'At least one arg needs to be specified'); 215 | let msg = 'The '; 216 | const len = args.length; 217 | args = args.map(a => `"${a}"`); 218 | switch (len) { 219 | case 1: 220 | msg += `${args[0]} argument`; 221 | break; 222 | case 2: 223 | msg += `${args[0]} and ${args[1]} arguments`; 224 | break; 225 | default: 226 | msg += args.slice(0, len - 1).join(', '); 227 | msg += `, and ${args[len - 1]} arguments`; 228 | break; 229 | } 230 | return `${msg} must be specified`; 231 | } 232 | 233 | function oneOf(expected, thing) { 234 | assert(expected, 'expected is required'); 235 | assert(typeof thing === 'string', 'thing is required'); 236 | if (Array.isArray(expected)) { 237 | const len = expected.length; 238 | assert(len > 0, 'At least one expected value needs to be specified'); 239 | // tslint:disable-next-line 240 | expected = expected.map(i => String(i)); 241 | if (len > 2) { 242 | return `one of ${thing} ${expected.slice(0, len - 1).join(', ')}, or ` + expected[len - 1]; 243 | } else if (len === 2) { 244 | return `one of ${thing} ${expected[0]} or ${expected[1]}`; 245 | } else { 246 | return `of ${thing} ${expected[0]}`; 247 | } 248 | } else { 249 | return `of ${thing} ${String(expected)}`; 250 | } 251 | } 252 | 253 | function bufferOutOfBounds(name, isWriting) { 254 | if (isWriting) { 255 | return 'Attempt to write outside buffer bounds'; 256 | } else { 257 | return `"${name}" is outside of buffer bounds`; 258 | } 259 | } 260 | -------------------------------------------------------------------------------- /src/node.ts: -------------------------------------------------------------------------------- 1 | import process from './process'; 2 | import { constants, S } from './constants'; 3 | import { Volume } from './volume'; 4 | import { EventEmitter } from 'events'; 5 | import { TEncodingExtended, strToEncoding, TDataOut } from './encoding'; 6 | import Stats from './Stats'; 7 | 8 | const { S_IFMT, S_IFDIR, S_IFREG, S_IFBLK, S_IFCHR, S_IFLNK, S_IFIFO, S_IFSOCK, O_APPEND } = constants; 9 | 10 | export const SEP = '/'; 11 | 12 | /** 13 | * Node in a file system (like i-node, v-node). 14 | */ 15 | export class Node extends EventEmitter { 16 | // i-node number. 17 | ino: number; 18 | 19 | // User ID and group ID. 20 | uid: number = process.getuid(); 21 | gid: number = process.getgid(); 22 | 23 | atime = new Date(); 24 | mtime = new Date(); 25 | ctime = new Date(); 26 | 27 | // data: string = ''; 28 | buf: Buffer = null; 29 | 30 | perm = 0o666; // Permissions `chmod`, `fchmod` 31 | 32 | mode = S_IFREG; // S_IFDIR, S_IFREG, etc.. (file by default?) 33 | 34 | // Number of hard links pointing at this Node. 35 | nlink = 1; 36 | 37 | // Steps to another node, if this node is a symlink. 38 | symlink: string[] = null; 39 | 40 | constructor(ino: number, perm: number = 0o666) { 41 | super(); 42 | this.perm = perm; 43 | this.mode |= perm; 44 | this.ino = ino; 45 | } 46 | 47 | getString(encoding = 'utf8'): string { 48 | return this.getBuffer().toString(encoding); 49 | } 50 | 51 | setString(str: string) { 52 | // this.setBuffer(Buffer.from(str, 'utf8')); 53 | this.buf = Buffer.from(str, 'utf8'); 54 | this.touch(); 55 | } 56 | 57 | getBuffer(): Buffer { 58 | if (!this.buf) this.setBuffer(Buffer.allocUnsafe(0)); 59 | return Buffer.from(this.buf); // Return a copy. 60 | } 61 | 62 | setBuffer(buf: Buffer) { 63 | this.buf = Buffer.from(buf); // Creates a copy of data. 64 | this.touch(); 65 | } 66 | 67 | getSize(): number { 68 | return this.buf ? this.buf.length : 0; 69 | } 70 | 71 | setModeProperty(property: number) { 72 | this.mode = (this.mode & ~S_IFMT) | property; 73 | } 74 | 75 | setIsFile() { 76 | this.setModeProperty(S_IFREG); 77 | } 78 | 79 | setIsDirectory() { 80 | this.setModeProperty(S_IFDIR); 81 | } 82 | 83 | setIsSymlink() { 84 | this.setModeProperty(S_IFLNK); 85 | } 86 | 87 | isFile() { 88 | return (this.mode & S_IFMT) === S_IFREG; 89 | } 90 | 91 | isDirectory() { 92 | return (this.mode & S_IFMT) === S_IFDIR; 93 | } 94 | 95 | isSymlink() { 96 | // return !!this.symlink; 97 | return (this.mode & S_IFMT) === S_IFLNK; 98 | } 99 | 100 | makeSymlink(steps: string[]) { 101 | this.symlink = steps; 102 | this.setIsSymlink(); 103 | } 104 | 105 | write(buf: Buffer, off: number = 0, len: number = buf.length, pos: number = 0): number { 106 | if (!this.buf) this.buf = Buffer.allocUnsafe(0); 107 | 108 | if (pos + len > this.buf.length) { 109 | const newBuf = Buffer.allocUnsafe(pos + len); 110 | this.buf.copy(newBuf, 0, 0, this.buf.length); 111 | this.buf = newBuf; 112 | } 113 | 114 | buf.copy(this.buf, pos, off, off + len); 115 | 116 | this.touch(); 117 | 118 | return len; 119 | } 120 | 121 | // Returns the number of bytes read. 122 | read(buf: Buffer | Uint8Array, off: number = 0, len: number = buf.byteLength, pos: number = 0): number { 123 | if (!this.buf) this.buf = Buffer.allocUnsafe(0); 124 | 125 | let actualLen = len; 126 | if (actualLen > buf.byteLength) { 127 | actualLen = buf.byteLength; 128 | } 129 | if (actualLen + pos > this.buf.length) { 130 | actualLen = this.buf.length - pos; 131 | } 132 | 133 | this.buf.copy(buf as Buffer, off, pos, pos + actualLen); 134 | return actualLen; 135 | } 136 | 137 | truncate(len: number = 0) { 138 | if (!len) this.buf = Buffer.allocUnsafe(0); 139 | else { 140 | if (!this.buf) this.buf = Buffer.allocUnsafe(0); 141 | if (len <= this.buf.length) { 142 | this.buf = this.buf.slice(0, len); 143 | } else { 144 | const buf = Buffer.allocUnsafe(0); 145 | this.buf.copy(buf); 146 | buf.fill(0, len); 147 | } 148 | } 149 | 150 | this.touch(); 151 | } 152 | 153 | chmod(perm: number) { 154 | this.perm = perm; 155 | this.mode = (this.mode & ~0o777) | perm; 156 | this.touch(); 157 | } 158 | 159 | chown(uid: number, gid: number) { 160 | this.uid = uid; 161 | this.gid = gid; 162 | this.touch(); 163 | } 164 | 165 | touch() { 166 | this.mtime = new Date(); 167 | this.emit('change', this); 168 | } 169 | 170 | canRead(uid: number = process.getuid(), gid: number = process.getgid()): boolean { 171 | if (this.perm & S.IROTH) { 172 | return true; 173 | } 174 | 175 | if (gid === this.gid) { 176 | if (this.perm & S.IRGRP) { 177 | return true; 178 | } 179 | } 180 | 181 | if (uid === this.uid) { 182 | if (this.perm & S.IRUSR) { 183 | return true; 184 | } 185 | } 186 | 187 | return false; 188 | } 189 | 190 | canWrite(uid: number = process.getuid(), gid: number = process.getgid()): boolean { 191 | if (this.perm & S.IWOTH) { 192 | return true; 193 | } 194 | 195 | if (gid === this.gid) { 196 | if (this.perm & S.IWGRP) { 197 | return true; 198 | } 199 | } 200 | 201 | if (uid === this.uid) { 202 | if (this.perm & S.IWUSR) { 203 | return true; 204 | } 205 | } 206 | 207 | return false; 208 | } 209 | 210 | del() { 211 | this.emit('delete', this); 212 | } 213 | 214 | toJSON() { 215 | return { 216 | ino: this.ino, 217 | uid: this.uid, 218 | gid: this.gid, 219 | atime: this.atime.getTime(), 220 | mtime: this.mtime.getTime(), 221 | ctime: this.ctime.getTime(), 222 | perm: this.perm, 223 | mode: this.mode, 224 | nlink: this.nlink, 225 | symlink: this.symlink, 226 | data: this.getString(), 227 | }; 228 | } 229 | } 230 | 231 | /** 232 | * Represents a hard link that points to an i-node `node`. 233 | */ 234 | export class Link extends EventEmitter { 235 | vol: Volume; 236 | 237 | parent: Link = null; 238 | 239 | children: { [child: string]: Link } = {}; 240 | 241 | // Path to this node as Array: ['usr', 'bin', 'node']. 242 | steps: string[] = []; 243 | 244 | // "i-node" of this hard link. 245 | node: Node = null; 246 | 247 | // "i-node" number of the node. 248 | ino: number = 0; 249 | 250 | // Number of children. 251 | length: number = 0; 252 | 253 | constructor(vol: Volume, parent: Link, name: string) { 254 | super(); 255 | this.vol = vol; 256 | this.parent = parent; 257 | this.steps = parent ? parent.steps.concat([name]) : [name]; 258 | } 259 | 260 | setNode(node: Node) { 261 | this.node = node; 262 | this.ino = node.ino; 263 | } 264 | 265 | getNode(): Node { 266 | return this.node; 267 | } 268 | 269 | createChild(name: string, node: Node = this.vol.createNode()): Link { 270 | const link = new Link(this.vol, this, name); 271 | link.setNode(node); 272 | 273 | if (node.isDirectory()) { 274 | // link.setChild('.', link); 275 | // link.getNode().nlink++; 276 | // link.setChild('..', this); 277 | // this.getNode().nlink++; 278 | } 279 | 280 | this.setChild(name, link); 281 | 282 | return link; 283 | } 284 | 285 | setChild(name: string, link: Link = new Link(this.vol, this, name)): Link { 286 | this.children[name] = link; 287 | link.parent = this; 288 | this.length++; 289 | 290 | this.emit('child:add', link, this); 291 | 292 | return link; 293 | } 294 | 295 | deleteChild(link: Link) { 296 | delete this.children[link.getName()]; 297 | this.length--; 298 | 299 | this.emit('child:delete', link, this); 300 | } 301 | 302 | getChild(name: string): Link { 303 | return this.children[name]; 304 | } 305 | 306 | getPath(): string { 307 | return this.steps.join(SEP); 308 | } 309 | 310 | getName(): string { 311 | return this.steps[this.steps.length - 1]; 312 | } 313 | 314 | // del() { 315 | // const parent = this.parent; 316 | // if(parent) { 317 | // parent.deleteChild(link); 318 | // } 319 | // this.parent = null; 320 | // this.vol = null; 321 | // } 322 | 323 | /** 324 | * Walk the tree path and return the `Link` at that location, if any. 325 | * @param steps {string[]} Desired location. 326 | * @param stop {number} Max steps to go into. 327 | * @param i {number} Current step in the `steps` array. 328 | * @returns {any} 329 | */ 330 | walk(steps: string[], stop: number = steps.length, i: number = 0): Link { 331 | if (i >= steps.length) return this; 332 | if (i >= stop) return this; 333 | 334 | const step = steps[i]; 335 | const link = this.getChild(step); 336 | if (!link) return null; 337 | return link.walk(steps, stop, i + 1); 338 | } 339 | 340 | toJSON() { 341 | return { 342 | steps: this.steps, 343 | ino: this.ino, 344 | children: Object.keys(this.children), 345 | }; 346 | } 347 | } 348 | 349 | /** 350 | * Represents an open file (file descriptor) that points to a `Link` (Hard-link) and a `Node`. 351 | */ 352 | export class File { 353 | fd: number; 354 | 355 | /** 356 | * Hard link that this file opened. 357 | * @type {any} 358 | */ 359 | link: Link = null; 360 | 361 | /** 362 | * Reference to a `Node`. 363 | * @type {Node} 364 | */ 365 | node: Node = null; 366 | 367 | /** 368 | * A cursor/offset position in a file, where data will be written on write. 369 | * User can "seek" this position. 370 | */ 371 | position: number = 0; 372 | 373 | // Flags used when opening the file. 374 | flags: number; 375 | 376 | /** 377 | * Open a Link-Node pair. `node` is provided separately as that might be a different node 378 | * rather the one `link` points to, because it might be a symlink. 379 | * @param link 380 | * @param node 381 | * @param flags 382 | * @param fd 383 | */ 384 | constructor(link: Link, node: Node, flags: number, fd: number) { 385 | this.link = link; 386 | this.node = node; 387 | this.flags = flags; 388 | this.fd = fd; 389 | } 390 | 391 | getString(encoding = 'utf8'): string { 392 | return this.node.getString(); 393 | } 394 | 395 | setString(str: string) { 396 | this.node.setString(str); 397 | } 398 | 399 | getBuffer(): Buffer { 400 | return this.node.getBuffer(); 401 | } 402 | 403 | setBuffer(buf: Buffer) { 404 | this.node.setBuffer(buf); 405 | } 406 | 407 | getSize(): number { 408 | return this.node.getSize(); 409 | } 410 | 411 | truncate(len?: number) { 412 | this.node.truncate(len); 413 | } 414 | 415 | seekTo(position: number) { 416 | this.position = position; 417 | } 418 | 419 | stats(): Stats { 420 | return Stats.build(this.node) as Stats; 421 | } 422 | 423 | write(buf: Buffer, offset: number = 0, length: number = buf.length, position?: number): number { 424 | if (typeof position !== 'number') position = this.position; 425 | if (this.flags & O_APPEND) position = this.getSize(); 426 | const bytes = this.node.write(buf, offset, length, position); 427 | this.position = position + bytes; 428 | return bytes; 429 | } 430 | 431 | read(buf: Buffer | Uint8Array, offset: number = 0, length: number = buf.byteLength, position?: number): number { 432 | if (typeof position !== 'number') position = this.position; 433 | const bytes = this.node.read(buf, offset, length, position); 434 | this.position = position + bytes; 435 | return bytes; 436 | } 437 | 438 | chmod(perm: number) { 439 | this.node.chmod(perm); 440 | } 441 | 442 | chown(uid: number, gid: number) { 443 | this.node.chown(uid, gid); 444 | } 445 | } 446 | -------------------------------------------------------------------------------- /src/process.ts: -------------------------------------------------------------------------------- 1 | // Here we mock the global `process` variable in case we are not in Node's environment. 2 | 3 | export interface IProcess { 4 | getuid(): number; 5 | 6 | getgid(): number; 7 | 8 | cwd(): string; 9 | 10 | platform: string; 11 | nextTick: (callback: (...args) => void, ...args) => void; 12 | emitWarning: (message: string, type: string) => void; 13 | env: { 14 | MEMFS_DONT_WARN?: boolean; 15 | }; 16 | } 17 | 18 | /** 19 | * Looks to return a `process` object, if one is available. 20 | * 21 | * The global `process` is returned if defined; 22 | * otherwise `require('process')` is attempted. 23 | * 24 | * If that fails, `undefined` is returned. 25 | * 26 | * @return {IProcess | undefined} 27 | */ 28 | const maybeReturnProcess = (): IProcess | undefined => { 29 | if (typeof process !== 'undefined') { 30 | return process; 31 | } 32 | 33 | try { 34 | return require('process'); 35 | } catch { 36 | return undefined; 37 | } 38 | }; 39 | 40 | export function createProcess(): IProcess { 41 | const p: IProcess = maybeReturnProcess() || ({} as IProcess); 42 | 43 | if (!p.getuid) p.getuid = () => 0; 44 | if (!p.getgid) p.getgid = () => 0; 45 | if (!p.cwd) p.cwd = () => '/'; 46 | if (!p.nextTick) p.nextTick = require('./setImmediate').default; 47 | if (!p.emitWarning) 48 | p.emitWarning = (message, type) => { 49 | // tslint:disable-next-line:no-console 50 | console.warn(`${type}${type ? ': ' : ''}${message}`); 51 | }; 52 | if (!p.env) p.env = {}; 53 | return p; 54 | } 55 | 56 | export default createProcess(); 57 | -------------------------------------------------------------------------------- /src/promises.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Volume, 3 | TFilePath, 4 | TData, 5 | TMode, 6 | TFlags, 7 | TFlagsCopy, 8 | TSymlinkType, 9 | TTime, 10 | IOptions, 11 | IAppendFileOptions, 12 | IMkdirOptions, 13 | IReaddirOptions, 14 | IReadFileOptions, 15 | IRealpathOptions, 16 | IWriteFileOptions, 17 | IStatOptions, 18 | } from './volume'; 19 | import Stats from './Stats'; 20 | import Dirent from './Dirent'; 21 | import { TDataOut } from './encoding'; 22 | 23 | function promisify( 24 | vol: Volume, 25 | fn: string, 26 | getResult: (result: any) => any = input => input, 27 | ): (...args) => Promise { 28 | return (...args) => 29 | new Promise((resolve, reject) => { 30 | vol[fn].bind(vol)(...args, (error, result) => { 31 | if (error) return reject(error); 32 | return resolve(getResult(result)); 33 | }); 34 | }); 35 | } 36 | 37 | export interface TFileHandleReadResult { 38 | bytesRead: number; 39 | buffer: Buffer | Uint8Array; 40 | } 41 | 42 | export interface TFileHandleWriteResult { 43 | bytesWritten: number; 44 | buffer: Buffer | Uint8Array; 45 | } 46 | 47 | export interface IFileHandle { 48 | fd: number; 49 | appendFile(data: TData, options?: IAppendFileOptions | string): Promise; 50 | chmod(mode: TMode): Promise; 51 | chown(uid: number, gid: number): Promise; 52 | close(): Promise; 53 | datasync(): Promise; 54 | read(buffer: Buffer | Uint8Array, offset: number, length: number, position: number): Promise; 55 | readFile(options?: IReadFileOptions | string): Promise; 56 | stat(options?: IStatOptions): Promise; 57 | truncate(len?: number): Promise; 58 | utimes(atime: TTime, mtime: TTime): Promise; 59 | write( 60 | buffer: Buffer | Uint8Array, 61 | offset?: number, 62 | length?: number, 63 | position?: number, 64 | ): Promise; 65 | writeFile(data: TData, options?: IWriteFileOptions): Promise; 66 | } 67 | 68 | export type TFileHandle = TFilePath | IFileHandle; 69 | 70 | export interface IPromisesAPI { 71 | FileHandle; 72 | access(path: TFilePath, mode?: number): Promise; 73 | appendFile(path: TFileHandle, data: TData, options?: IAppendFileOptions | string): Promise; 74 | chmod(path: TFilePath, mode: TMode): Promise; 75 | chown(path: TFilePath, uid: number, gid: number): Promise; 76 | copyFile(src: TFilePath, dest: TFilePath, flags?: TFlagsCopy): Promise; 77 | lchmod(path: TFilePath, mode: TMode): Promise; 78 | lchown(path: TFilePath, uid: number, gid: number): Promise; 79 | link(existingPath: TFilePath, newPath: TFilePath): Promise; 80 | lstat(path: TFilePath, options?: IStatOptions): Promise; 81 | mkdir(path: TFilePath, options?: TMode | IMkdirOptions): Promise; 82 | mkdtemp(prefix: string, options?: IOptions): Promise; 83 | open(path: TFilePath, flags: TFlags, mode?: TMode): Promise; 84 | readdir(path: TFilePath, options?: IReaddirOptions | string): Promise; 85 | readFile(id: TFileHandle, options?: IReadFileOptions | string): Promise; 86 | readlink(path: TFilePath, options?: IOptions): Promise; 87 | realpath(path: TFilePath, options?: IRealpathOptions | string): Promise; 88 | rename(oldPath: TFilePath, newPath: TFilePath): Promise; 89 | rmdir(path: TFilePath): Promise; 90 | stat(path: TFilePath, options?: IStatOptions): Promise; 91 | symlink(target: TFilePath, path: TFilePath, type?: TSymlinkType): Promise; 92 | truncate(path: TFilePath, len?: number): Promise; 93 | unlink(path: TFilePath): Promise; 94 | utimes(path: TFilePath, atime: TTime, mtime: TTime): Promise; 95 | writeFile(id: TFileHandle, data: TData, options?: IWriteFileOptions): Promise; 96 | } 97 | 98 | export class FileHandle implements IFileHandle { 99 | private vol: Volume; 100 | 101 | fd: number; 102 | 103 | constructor(vol: Volume, fd: number) { 104 | this.vol = vol; 105 | this.fd = fd; 106 | } 107 | 108 | appendFile(data: TData, options?: IAppendFileOptions | string): Promise { 109 | return promisify(this.vol, 'appendFile')(this.fd, data, options); 110 | } 111 | 112 | chmod(mode: TMode): Promise { 113 | return promisify(this.vol, 'fchmod')(this.fd, mode); 114 | } 115 | 116 | chown(uid: number, gid: number): Promise { 117 | return promisify(this.vol, 'fchown')(this.fd, uid, gid); 118 | } 119 | 120 | close(): Promise { 121 | return promisify(this.vol, 'close')(this.fd); 122 | } 123 | 124 | datasync(): Promise { 125 | return promisify(this.vol, 'fdatasync')(this.fd); 126 | } 127 | 128 | read(buffer: Buffer | Uint8Array, offset: number, length: number, position: number): Promise { 129 | return promisify(this.vol, 'read', bytesRead => ({ bytesRead, buffer }))(this.fd, buffer, offset, length, position); 130 | } 131 | 132 | readFile(options?: IReadFileOptions | string): Promise { 133 | return promisify(this.vol, 'readFile')(this.fd, options); 134 | } 135 | 136 | stat(options?: IStatOptions): Promise { 137 | return promisify(this.vol, 'fstat')(this.fd, options); 138 | } 139 | 140 | sync(): Promise { 141 | return promisify(this.vol, 'fsync')(this.fd); 142 | } 143 | 144 | truncate(len?: number): Promise { 145 | return promisify(this.vol, 'ftruncate')(this.fd, len); 146 | } 147 | 148 | utimes(atime: TTime, mtime: TTime): Promise { 149 | return promisify(this.vol, 'futimes')(this.fd, atime, mtime); 150 | } 151 | 152 | write( 153 | buffer: Buffer | Uint8Array, 154 | offset?: number, 155 | length?: number, 156 | position?: number, 157 | ): Promise { 158 | return promisify(this.vol, 'write', bytesWritten => ({ bytesWritten, buffer }))( 159 | this.fd, 160 | buffer, 161 | offset, 162 | length, 163 | position, 164 | ); 165 | } 166 | 167 | writeFile(data: TData, options?: IWriteFileOptions): Promise { 168 | return promisify(this.vol, 'writeFile')(this.fd, data, options); 169 | } 170 | } 171 | 172 | export default function createPromisesApi(vol: Volume): null | IPromisesAPI { 173 | if (typeof Promise === 'undefined') return null; 174 | return { 175 | FileHandle, 176 | 177 | access(path: TFilePath, mode?: number): Promise { 178 | return promisify(vol, 'access')(path, mode); 179 | }, 180 | 181 | appendFile(path: TFileHandle, data: TData, options?: IAppendFileOptions | string): Promise { 182 | return promisify(vol, 'appendFile')(path instanceof FileHandle ? path.fd : (path as TFilePath), data, options); 183 | }, 184 | 185 | chmod(path: TFilePath, mode: TMode): Promise { 186 | return promisify(vol, 'chmod')(path, mode); 187 | }, 188 | 189 | chown(path: TFilePath, uid: number, gid: number): Promise { 190 | return promisify(vol, 'chown')(path, uid, gid); 191 | }, 192 | 193 | copyFile(src: TFilePath, dest: TFilePath, flags?: TFlagsCopy): Promise { 194 | return promisify(vol, 'copyFile')(src, dest, flags); 195 | }, 196 | 197 | lchmod(path: TFilePath, mode: TMode): Promise { 198 | return promisify(vol, 'lchmod')(path, mode); 199 | }, 200 | 201 | lchown(path: TFilePath, uid: number, gid: number): Promise { 202 | return promisify(vol, 'lchown')(path, uid, gid); 203 | }, 204 | 205 | link(existingPath: TFilePath, newPath: TFilePath): Promise { 206 | return promisify(vol, 'link')(existingPath, newPath); 207 | }, 208 | 209 | lstat(path: TFilePath, options?: IStatOptions): Promise { 210 | return promisify(vol, 'lstat')(path, options); 211 | }, 212 | 213 | mkdir(path: TFilePath, options?: TMode | IMkdirOptions): Promise { 214 | return promisify(vol, 'mkdir')(path, options); 215 | }, 216 | 217 | mkdtemp(prefix: string, options?: IOptions): Promise { 218 | return promisify(vol, 'mkdtemp')(prefix, options); 219 | }, 220 | 221 | open(path: TFilePath, flags: TFlags, mode?: TMode): Promise { 222 | return promisify(vol, 'open', fd => new FileHandle(vol, fd))(path, flags, mode); 223 | }, 224 | 225 | readdir(path: TFilePath, options?: IReaddirOptions | string): Promise { 226 | return promisify(vol, 'readdir')(path, options); 227 | }, 228 | 229 | readFile(id: TFileHandle, options?: IReadFileOptions | string): Promise { 230 | return promisify(vol, 'readFile')(id instanceof FileHandle ? id.fd : (id as TFilePath), options); 231 | }, 232 | 233 | readlink(path: TFilePath, options?: IOptions): Promise { 234 | return promisify(vol, 'readlink')(path, options); 235 | }, 236 | 237 | realpath(path: TFilePath, options?: IRealpathOptions | string): Promise { 238 | return promisify(vol, 'realpath')(path, options); 239 | }, 240 | 241 | rename(oldPath: TFilePath, newPath: TFilePath): Promise { 242 | return promisify(vol, 'rename')(oldPath, newPath); 243 | }, 244 | 245 | rmdir(path: TFilePath): Promise { 246 | return promisify(vol, 'rmdir')(path); 247 | }, 248 | 249 | stat(path: TFilePath, options?: IStatOptions): Promise { 250 | return promisify(vol, 'stat')(path, options); 251 | }, 252 | 253 | symlink(target: TFilePath, path: TFilePath, type?: TSymlinkType): Promise { 254 | return promisify(vol, 'symlink')(target, path, type); 255 | }, 256 | 257 | truncate(path: TFilePath, len?: number): Promise { 258 | return promisify(vol, 'truncate')(path, len); 259 | }, 260 | 261 | unlink(path: TFilePath): Promise { 262 | return promisify(vol, 'unlink')(path); 263 | }, 264 | 265 | utimes(path: TFilePath, atime: TTime, mtime: TTime): Promise { 266 | return promisify(vol, 'utimes')(path, atime, mtime); 267 | }, 268 | 269 | writeFile(id: TFileHandle, data: TData, options?: IWriteFileOptions): Promise { 270 | return promisify(vol, 'writeFile')(id instanceof FileHandle ? id.fd : (id as TFilePath), data, options); 271 | }, 272 | }; 273 | } 274 | -------------------------------------------------------------------------------- /src/setImmediate.ts: -------------------------------------------------------------------------------- 1 | type TSetImmediate = (callback: (...args) => void, args?) => void; 2 | let _setImmediate: TSetImmediate; 3 | 4 | if (typeof setImmediate === 'function') _setImmediate = setImmediate.bind(global); 5 | else _setImmediate = setTimeout.bind(global); 6 | 7 | export default _setImmediate as TSetImmediate; 8 | -------------------------------------------------------------------------------- /src/setTimeoutUnref.ts: -------------------------------------------------------------------------------- 1 | export type TSetTimeout = (callback: (...args) => void, time?: number, args?: any[]) => any; 2 | 3 | /** 4 | * `setTimeoutUnref` is just like `setTimeout`, 5 | * only in Node's environment it will "unref" its macro task. 6 | */ 7 | function setTimeoutUnref(callback, time?, args?): object { 8 | const ref = setTimeout.apply(null, arguments); 9 | if (ref && typeof ref === 'object' && typeof ref.unref === 'function') ref.unref(); 10 | return ref; 11 | } 12 | 13 | export default setTimeoutUnref; 14 | -------------------------------------------------------------------------------- /src/volume-localstorage.ts: -------------------------------------------------------------------------------- 1 | import { Volume } from './volume'; 2 | import { Link, Node } from './node'; 3 | 4 | export interface IStore { 5 | setItem(key: string, json); 6 | getItem(key: string); 7 | removeItem(key: string); 8 | } 9 | 10 | export class ObjectStore { 11 | obj: object; 12 | 13 | constructor(obj) { 14 | this.obj = obj; 15 | } 16 | 17 | setItem(key: string, json) { 18 | this.obj[key] = JSON.stringify(json); 19 | } 20 | 21 | getItem(key: string) { 22 | const data = this.obj[key]; 23 | if (typeof data === void 0) return void 0; 24 | return JSON.parse(data); 25 | } 26 | 27 | removeItem(key: string) { 28 | delete this.obj[key]; 29 | } 30 | } 31 | 32 | export function createVolume(namespace: string, LS: Storage | object = localStorage): new (...args) => Volume { 33 | const store = new ObjectStore(LS); 34 | const key = (type, id) => `memfs.${namespace}.${type}.${id}`; 35 | 36 | class NodeLocalStorage extends Node { 37 | private _key: string; 38 | 39 | get Key(): string { 40 | if (!this._key) this._key = key('ino', this.ino); 41 | return this._key; 42 | } 43 | 44 | sync() { 45 | store.setItem(this.Key, this.toJSON()); 46 | } 47 | 48 | touch() { 49 | super.touch(); 50 | this.sync(); 51 | } 52 | 53 | del() { 54 | super.del(); 55 | store.removeItem(this.Key); 56 | } 57 | } 58 | 59 | class LinkLocalStorage extends Link { 60 | private _key: string; 61 | 62 | get Key(): string { 63 | if (!this._key) this._key = key('link', this.getPath()); 64 | return this._key; 65 | } 66 | 67 | sync() { 68 | store.setItem(this.Key, this.toJSON()); 69 | } 70 | } 71 | 72 | return class VolumeLocalStorage extends Volume { 73 | constructor() { 74 | super({ 75 | Node: NodeLocalStorage, 76 | Link: LinkLocalStorage, 77 | }); 78 | } 79 | 80 | createLink(parent?, name?, isDirectory?, perm?) { 81 | const link = super.createLink(parent, name, isDirectory, perm); 82 | store.setItem(key('link', link.getPath()), link.toJSON()); 83 | return link; 84 | } 85 | 86 | deleteLink(link) { 87 | store.removeItem(key('link', link.getPath())); 88 | return super.deleteLink(link); 89 | } 90 | }; 91 | } 92 | -------------------------------------------------------------------------------- /src/wasm.ts: -------------------------------------------------------------------------------- 1 | // An alternative fs for the browser and testing 2 | 3 | import { createFsFromVolume, IFs } from './index'; 4 | import { Volume, filenameToSteps } from './volume'; 5 | 6 | const assert = (cond: boolean, message: string) => { 7 | if (!cond) { 8 | throw new Error(message); 9 | } 10 | }; 11 | 12 | export class WasmFs { 13 | volume: Volume; 14 | fs: IFs; 15 | 16 | constructor() { 17 | this.volume = new Volume(); 18 | this.fs = createFsFromVolume(this.volume); 19 | this.fromJSON({ 20 | '/dev/stdin': '', 21 | '/dev/stdout': '', 22 | '/dev/stderr': '', 23 | }); 24 | } 25 | 26 | toJSON() { 27 | return this.volume.toJSON(); 28 | } 29 | 30 | fromJSON(fsJson: any) { 31 | this.volume = Volume.fromJSON(fsJson); 32 | // @ts-ignore 33 | this.volume.releasedFds = [0, 1, 2]; 34 | 35 | const fdErr = this.volume.openSync('/dev/stderr', 'w'); 36 | const fdOut = this.volume.openSync('/dev/stdout', 'w'); 37 | const fdIn = this.volume.openSync('/dev/stdin', 'r'); 38 | assert(fdErr === 2, `invalid handle for stderr: ${fdErr}`); 39 | assert(fdOut === 1, `invalid handle for stdout: ${fdOut}`); 40 | assert(fdIn === 0, `invalid handle for stdin: ${fdIn}`); 41 | 42 | this.fs = createFsFromVolume(this.volume); 43 | } 44 | 45 | async getStdOut() { 46 | const promise = new Promise(resolve => { 47 | resolve(this.fs.readFileSync('/dev/stdout', 'utf8')); 48 | }); 49 | return promise; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": ["es5", "dom", "es2015.promise"], 5 | "module": "commonjs", 6 | "removeComments": false, 7 | "noImplicitAny": false, 8 | "sourceMap": false, 9 | "outDir": "lib", 10 | "declaration": true 11 | }, 12 | "include": ["src"], 13 | "exclude": ["src/__tests__"] 14 | } 15 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "tslint-config-common", 3 | "rules": { 4 | "no-invalid-this": false, 5 | "variable-name": false, 6 | "no-inferrable-types": false, 7 | "curly": false, 8 | "forin": false, 9 | "no-dynamic-delete": false, 10 | "unified-signatures": false 11 | } 12 | } 13 | --------------------------------------------------------------------------------