├── .github
└── workflows
│ └── test.yml
├── .gitignore
├── CHANGELOG.md
├── LICENSE
├── README.md
├── benchmark
├── copy.js
├── find.js
├── inspect_tree.js
├── remove.js
└── utils.js
├── index.d.ts
├── lib
├── append.js
├── copy.js
├── dir.js
├── exists.js
├── file.js
├── find.js
├── inspect.js
├── inspect_tree.js
├── jetpack.js
├── list.js
├── move.js
├── read.js
├── remove.js
├── rename.js
├── streams.js
├── symlink.js
├── tmp_dir.js
├── utils
│ ├── fs.js
│ ├── matcher.js
│ ├── mode.js
│ ├── promisify.js
│ ├── tree_walker.js
│ └── validate.js
└── write.js
├── main.js
├── package.json
├── spec
├── append.spec.ts
├── assert_path.ts
├── copy.spec.ts
├── cwd.spec.ts
├── dir.spec.ts
├── exists.spec.ts
├── file.spec.ts
├── find.spec.ts
├── helper.ts
├── inspect.spec.ts
├── inspect_tree.spec.ts
├── list.spec.ts
├── log.spec.ts
├── move.spec.ts
├── path.spec.ts
├── read.spec.ts
├── remove.spec.ts
├── rename.spec.ts
├── streams.spec.ts
├── symlink.spec.ts
├── tmp_dir.spec.ts
├── utils
│ ├── fs.spec.ts
│ ├── matcher.spec.ts
│ ├── tree_walker.spec.ts
│ └── validate.spec.ts
├── write.spec.ts
└── write_atomic.spec.ts
├── tsconfig.json
└── types.d.ts
/.github/workflows/test.yml:
--------------------------------------------------------------------------------
1 | name: Unit Tests
2 |
3 | on:
4 | push:
5 | branches: [ master ]
6 | pull_request:
7 | branches: [ master ]
8 |
9 | jobs:
10 | test_on_linux:
11 | name: Test on Linux
12 |
13 | runs-on: ubuntu-latest
14 |
15 | strategy:
16 | matrix:
17 | node-version: [14.x, 16.x, 18.x]
18 |
19 | steps:
20 | - uses: actions/checkout@v3
21 | - name: Use Node.js ${{ matrix.node-version }}
22 | uses: actions/setup-node@v3
23 | with:
24 | node-version: ${{ matrix.node-version }}
25 | - name: Install dependencies
26 | run: npm install
27 | - name: Run tests
28 | run: npm run coverage
29 | - name: Upload coverage to Codecov
30 | uses: codecov/codecov-action@v3
31 |
32 | test_on_windows:
33 | name: Test on Windows
34 |
35 | runs-on: windows-latest
36 |
37 | strategy:
38 | matrix:
39 | node-version: [14.x, 16.x, 18.x]
40 |
41 | steps:
42 | - uses: actions/checkout@v3
43 | - name: Use Node.js ${{ matrix.node-version }}
44 | uses: actions/setup-node@v3
45 | with:
46 | node-version: ${{ matrix.node-version }}
47 | - name: Install dependencies
48 | run: npm install
49 | - name: Run tests
50 | run: npm test
51 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /node_modules
2 | /coverage
3 | /.nyc_output
4 | package-lock.json
5 | *.log
6 | .DS_Store
7 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # 5.1.0 (2022-10-24)
2 | - `filter` function of `find()` method now receives also `absolutePath` property of each file
3 |
4 | # 5.0.0 (2022-09-15)
5 | - **(breaking change)** Dropped support for all node.js engines older than v14
6 | - **(possibly breaking change)** `inspect("some-file", { times: true })` now will return also `birthTime` of a file
7 | - **(possibly breaking change)** Under the hood switched from `rimraf` library to using node-native recursive files removal
8 |
9 | # 4.3.1 (2022-01-12)
10 | - Fixed `find()` crash when stumbled upon invalid symlink
11 |
12 | # 4.3.0 (2021-11-30)
13 | - `find()` accepts `filter` function that allows you to perform more refined filtering of the results
14 |
15 | # 4.2.0 (2021-09-27)
16 | - For `find()` method you don't need to declare `matching` property, will default to `"*"` when not declared (thanks @gutenye)
17 |
18 | # 4.1.1 (2021-08-07)
19 | - Documentation improvements
20 |
21 | # 4.1.0 (2020-11-13)
22 | - `findAsync()` is now 5 times faster, and `find()` 2 times faster
23 | - `inspectTree()` now sorts results alphabetically, directories firsts, files second
24 | - Refactored internals of methods `find()`, `copy()` and `inspectTree()`
25 |
26 | # 4.0.1 (2020-10-27)
27 | - `inspectTree()` behaves better in concurrency terms (opens only few files at once)
28 |
29 | # 4.0.0 (2020-10-22)
30 | - Package published to npm registry now contains only the essential files (e.g. no tests are shipped), to make the smallest footprint possible
31 |
32 | # 3.2.0 (2020-10-15)
33 | - Ability to create temporary directories with `tmpDir()` method
34 |
35 | # 3.1.0 (2020-07-19)
36 | - `move()` can move file or directory between devices (thanks @papb)
37 |
38 | # 3.0.0 (2020-07-15)
39 | - **(breaking change)** `move()` and `rename()` default overwrite behaviour have changed, now by default both methods throw error if destination path already exists.
40 |
41 | # 2.4.0 (2020-05-15)
42 | - `write()` can accept `mode` as a parameter
43 |
44 | # 2.3.0 (2020-05-03)
45 | - `inspectTree()` now supports `times` option.
46 |
47 | # 2.2.3 (2019-10-21)
48 | - Amended TypeScript definitions thanks to @orta
49 |
50 | # 2.2.2 (2019-02-19)
51 | - Use rimraf as internal implementation of `remove()` to fix https://github.com/szwacz/fs-jetpack/issues/80
52 |
53 | # 2.2.1 (2019-01-20)
54 | - `find()` no longer crashes on ENOENT (e.g. when one of the files has been deleted by other process).
55 | - Added check if a parameter `newName` passed to `rename()` isn't a path.
56 |
57 | # 2.2.0 (2018-10-13)
58 | - Added ignoreCase option to `find()` and `copy()` methods.
59 |
60 | # 2.1.1 (2018-09-19)
61 | - Rename file types.ts -> types.d.ts to fix https://github.com/szwacz/fs-jetpack/issues/72
62 |
63 | # 2.1.0 (2018-07-26)
64 | - From now on the library ships with TypeScript type definitions.
65 |
66 | # 2.0.0 (2018-07-10)
67 | - **(breaking change)** removed `symlinks` config option of `find()` method. Now `find()` always follows symlinks.
68 |
69 | # 1.3.1 (2018-07-09)
70 | - Fixed bug in `existsAsync()`.
71 |
72 | # 1.3.0 (2018-02-09)
73 | - `overwrite` function passed to `copyAsync()` can return promise.
74 |
75 | # 1.2.0 (2017-08-10)
76 | - Added symlinks search option to `find()`.
77 |
78 | # 1.1.0 (2017-06-18)
79 | - Parameter `overwrite` of `copy()` method now supports functions, to allow you individually decide if each file overwrite should happen.
80 |
81 | # 1.0.0 (2017-05-14)
82 | - API declared stable. From now on braking changes will be minimized and whenever possible preceded by deprecation periods.
83 | - Codebase updated to ES6
84 |
85 | # 0.13.3 (2017-03-25)
86 | - `removeAsync()` retries deletion attempts for errors like `EBUSY`.
87 |
88 | # 0.13.2 (2017-03-21)
89 | - Nested directory creation handles well race condition
90 |
91 | # 0.13.1 (2017-03-16)
92 | - Added lacking promise rejection handler for `copyAsync()`.
93 |
94 | # 0.13.0 (2017-03-15)
95 | - **(breaking change)** Dropped support for node.js 0.10 and 0.12
96 | - **(possibly breaking change)** fs-jetpack no longer uses libraries `mkdirp` and `rimraf`, those have been replaced with in-house implementations doing the same task. The new implementations are simpler than original libraries, so some edge cases might emerge after upgrading (please file an issue if you stumbled upon such case).
97 | - Started using native promises instead of `Q` library
98 |
99 | # 0.12.0 (2017-02-19)
100 | - **(breaking change)** Changes in `symlinks` option passed to `inspect()`.
101 | - Added `symlinks` option to `inspectTree()`.
102 | - Removed controversial edge case behaviour for `exists()`.
103 |
104 | # 0.11.0 (2017-02-09)
105 | - Added input validation for the whole API
106 | - **(breaking change)** Removed already deprecated option `buf` for `read()` method
107 |
108 | # 0.10.5 (2016-12-07)
109 | - Fixed `find()` bug when `directories` is set to `true` and only negation glob is used.
110 |
111 | # 0.10.4 (2016-12-06)
112 | - Fixed matcher edge cases, improved matcher tests (affects `find()` and `copy()` methods).
113 |
114 | # 0.10.3 (2016-11-23)
115 | - Fixed directory tree traversal bug which was causing problems for `findAsync()` and `copyAsync()`.
116 |
117 | # 0.10.2 (2016-11-08)
118 | - Fixed `console.log(jetpack)` for node v6.6.0 or newer.
119 |
120 | # 0.10.1 (2016-11-01)
121 | - Bugfixed case when `copyAsync()` was leaving open read stream if write stream errored.
122 | - Tests ported from jasmine to mocha.
123 |
124 | # 0.10.0 (2016-10-17)
125 | - `copyAsync()` uses only streams (much more memory efficient).
126 | - `find()` supports `recursive` option.
127 |
128 | # 0.9.2 (2016-06-27)
129 | - Updated third party dependencies to quell minimatch intallation warnings.
130 |
131 | # 0.9.1 (2016-05-21)
132 | - Bug-fixed `jetpack.read('nonexistent_file', 'json')`.
133 |
134 | # 0.9.0 (2016-05-10)
135 | - **(breaking change)** `read()`, `list()`, `inspect()` and `inspectTree()` returns `undefined` instead of `null` if path doesn't exist.
136 | - More sane edge cases for `dir()`, `file()` and `list()`.
137 |
138 | # 0.8.0 (2016-04-09)
139 | - **(breaking change)** `find()` now distinguishes between files and directories and by default searches only for files (previously searched for both).
140 | - **(breaking change)** `find()` no longer can be configured with `returnAs` parameter and returns always relative paths (previously returned absolute).
141 | - **(breaking change)** `list()` no longer accepts `useInspect` as a parameter. To achieve old behaviour use `jetpack.list()` with `Array.map()`.
142 | - **(deprecation)** Don't do `jetpack.read('sth', 'buf')`, do `jetpack.read('sth', 'buffer')` instead.
143 | - `remove()`, `list()` and `find()` now can be called without provided `path`, and defaults to CWD in that case.
144 |
145 | # 0.7.3 (2016-03-21)
146 | - Bugfixed `copy()` with symlink overwrite
147 |
148 | # 0.7.2 (2016-03-09)
149 | - Fixed .dotfiles copying
150 |
151 | # 0.7.1 (2015-12-17)
152 | - Updated third party dependencies.
153 |
154 | # 0.7.0 (2015-07-20)
155 | - **(breaking change)** `matching` option in `copy()` and `find()` resolves glob patterns to the folder you want copy or find stuff in (previously CWD was used).
156 |
157 | # 0.6.5 (2015-06-19)
158 | - `exists()` can handle ENOTDIR error.
159 |
160 | # 0.6.3 and 0.6.4 (2015-04-18)
161 | - Added support for symbolic links.
162 |
163 | # 0.6.2 (2015-04-07)
164 | - Option `matching` in `copy()` and `find()` now accepts patterns anchored to CWD.
165 |
166 | # 0.6.1 (2015-04-03)
167 | - Option `matching` in `copy()` and `find()` now accepts negation patterns (e.g. `!some/file.txt`).
168 |
169 | # 0.6.0 (2015-03-30)
170 | - Lots of code refactoring
171 | - **(breaking change)** `dir()` no longer has `exists` option.
172 | - **(breaking change)** `file()` no longer has `exists` and `empty` options.
173 | - **(breaking change)** `safe` option for `write()` renamed to `atomic` (and uses new algorithm under the hood).
174 | - **(breaking change)** `safe` option for `read()` dropped (`atomic` while writing is enough).
175 | - **(breaking change)** In `copy()` options `only` and `allBut` have been replaced by option `matching`.
176 | - **(breaking change)** In `remove()` options `only` and `allBut` have been dropped (to do the same use `find()`, and then remove).
177 | - **(breaking change)** Default jsonIndent changed form 0 to 2.
178 | - `find()` method added.
179 | - More telling errors when `read()` failed while parsing JSON.
180 |
181 | # 0.5.3 (2015-01-06)
182 | - `inspect()` can return file access/modify/change time and mode.
183 |
184 | # 0.5.2 (2014-09-21)
185 | - `inspect()` checksum of empty file is now `null`.
186 |
187 | # 0.5.1 (2014-09-21)
188 | - `cwd()` accepts many arguments as path parts.
189 |
190 | # 0.5.0 (2014-08-31)
191 | - **(breaking change)** Method `tree()` renamed to `inspectTree()`.
192 | - **(breaking change)** Parameters passed to `list()` has changed.
193 | - Methods `inspect()` and `inspectTree()` can calculate md5 and sha1 checksums.
194 | - Added aliases to `fs.createReadStream()` and `fs.createWriteStream()`.
195 |
196 | # 0.4.1 (2014-07-16)
197 | - `copy()` now copies also file permissions on unix systems.
198 | - `append()` can specify file mode if file doesn't exist.
199 | - Can indent saved JSON data.
200 |
201 | # 0.4.0 (2014-07-14)
202 | - Changelog starts here.
203 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2013-2021 Jakub Szwacz
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy of
6 | this software and associated documentation files (the "Software"), to deal in
7 | the Software without restriction, including without limitation the rights to
8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
9 | the Software, and to permit persons to whom the Software is furnished to do so,
10 | subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
21 |
--------------------------------------------------------------------------------
/benchmark/copy.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | const utils = require("./utils");
4 |
5 | const testDir = utils.prepareJetpackTestDir();
6 | const toCopyDir = testDir.dir("to-copy");
7 | let timer;
8 |
9 | const test = (testConfig) => {
10 | console.log("");
11 |
12 | return utils
13 | .prepareFiles(toCopyDir, testConfig)
14 | .then(utils.waitAWhile)
15 | .then(() => {
16 | timer = utils.startTimer("jetpack.copy()");
17 | toCopyDir.copy(".", testDir.path("copied-jetpack-sync"));
18 | timer();
19 | return utils.waitAWhile();
20 | })
21 | .then(() => {
22 | timer = utils.startTimer("jetpack.copyAsync()");
23 | return toCopyDir.copyAsync(".", testDir.path("copied-jetpack-async"));
24 | })
25 | .then(() => {
26 | timer();
27 | return utils.waitAWhile();
28 | })
29 | .then(() => {
30 | timer = utils.startTimer("Native cp -R");
31 | return utils.exec(
32 | `cp -R ${toCopyDir.path()} ${testDir.path("copied-native")}`
33 | );
34 | })
35 | .then(() => {
36 | timer();
37 | return utils.cleanAfterTest();
38 | })
39 | .catch((err) => {
40 | console.log(err);
41 | });
42 | };
43 |
44 | const testConfigs = [
45 | {
46 | files: 10000,
47 | size: 1000,
48 | },
49 | {
50 | files: 50,
51 | size: 1000 * 1000 * 10,
52 | },
53 | ];
54 |
55 | const runNext = () => {
56 | if (testConfigs.length > 0) {
57 | test(testConfigs.pop()).then(runNext);
58 | }
59 | };
60 |
61 | runNext();
62 |
--------------------------------------------------------------------------------
/benchmark/find.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | const utils = require("./utils");
4 |
5 | const testDir = utils.prepareJetpackTestDir();
6 | let timer;
7 |
8 | const test = (testConfig) => {
9 | const dirJet = testDir.dir("some-tree");
10 |
11 | console.log("");
12 |
13 | return utils
14 | .prepareFiles(dirJet, testConfig)
15 | .then(utils.waitAWhile)
16 | .then(() => {
17 | timer = utils.startTimer("jetpack.find()");
18 | const results = dirJet.find(".", { matching: "1*.txt" });
19 | timer();
20 | })
21 | .then(() => {
22 | timer = utils.startTimer("jetpack.findAsync()");
23 | return dirJet.findAsync(".", { matching: "1*.txt" });
24 | })
25 | .then(() => {
26 | timer();
27 | timer = utils.startTimer("native find");
28 | return utils.exec(`find ${dirJet.path()} -name 1\*.txt`);
29 | })
30 | .then((results) => {
31 | timer();
32 | return utils.cleanAfterTest();
33 | })
34 | .catch((err) => {
35 | console.log(err);
36 | });
37 | };
38 |
39 | const testConfigs = [
40 | {
41 | files: 10000,
42 | filesPerNestedDir: 1000,
43 | size: 100,
44 | },
45 | ];
46 |
47 | const runNext = () => {
48 | if (testConfigs.length > 0) {
49 | test(testConfigs.pop()).then(runNext);
50 | }
51 | };
52 |
53 | runNext();
54 |
--------------------------------------------------------------------------------
/benchmark/inspect_tree.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | const utils = require("./utils");
4 |
5 | const testDir = utils.prepareJetpackTestDir();
6 | let timer;
7 | let jetpackTime;
8 | let nativeTime;
9 |
10 | const test = (testConfig) => {
11 | const dirJet = testDir.dir("some-tree");
12 |
13 | console.log("");
14 |
15 | return utils
16 | .prepareFiles(dirJet, testConfig)
17 | .then(utils.waitAWhile)
18 | .then(() => {
19 | timer = utils.startTimer("jetpack.inspectTree()");
20 | const tree = dirJet.inspectTree(".", { checksum: "md5" });
21 | timer();
22 | console.log("md5", tree.md5);
23 | utils.showMemoryUsage();
24 | })
25 | .then(utils.waitAWhile)
26 | .then(() => {
27 | timer = utils.startTimer("jetpack.inspectTreeAsync()");
28 | return dirJet.inspectTreeAsync(".", { checksum: "md5" });
29 | })
30 | .then((tree) => {
31 | timer();
32 | console.log("md5", tree.md5);
33 | utils.showMemoryUsage();
34 | return utils.cleanAfterTest();
35 | })
36 | .catch((err) => {
37 | console.log(err);
38 | });
39 | };
40 |
41 | const testConfigs = [
42 | {
43 | files: 10000,
44 | filesPerNestedDir: 1000,
45 | size: 1000,
46 | },
47 | {
48 | files: 1000,
49 | filesPerNestedDir: 50,
50 | size: 10000000,
51 | },
52 | ];
53 |
54 | const runNext = () => {
55 | if (testConfigs.length > 0) {
56 | test(testConfigs.pop()).then(runNext);
57 | }
58 | };
59 |
60 | runNext();
61 |
--------------------------------------------------------------------------------
/benchmark/remove.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | const utils = require("./utils");
4 |
5 | const testDir = utils.prepareJetpackTestDir();
6 | let timer;
7 |
8 | const test = (testConfig) => {
9 | const dirJet = testDir.dir("to-be-removed-by-jetpack");
10 | const dirNative = testDir.dir("to-be-removed-by-native");
11 |
12 | console.log("");
13 |
14 | return utils
15 | .prepareFiles(dirJet, testConfig)
16 | .then(() => {
17 | return utils.prepareFiles(dirNative, testConfig);
18 | })
19 | .then(utils.waitAWhile)
20 | .then(() => {
21 | timer = utils.startTimer("jetpack.removeAsync()");
22 | return dirJet.removeAsync();
23 | })
24 | .then(() => {
25 | timer();
26 | return utils.waitAWhile();
27 | })
28 | .then(() => {
29 | timer = utils.startTimer("Native rm -rf");
30 | return utils.exec(`rm -rf ${dirNative.path()}`);
31 | })
32 | .then(() => {
33 | timer();
34 | return utils.cleanAfterTest();
35 | })
36 | .catch((err) => {
37 | console.log(err);
38 | });
39 | };
40 |
41 | const testConfigs = [
42 | {
43 | files: 10000,
44 | size: 1000,
45 | },
46 | ];
47 |
48 | const runNext = () => {
49 | if (testConfigs.length > 0) {
50 | test(testConfigs.pop()).then(runNext);
51 | }
52 | };
53 |
54 | runNext();
55 |
--------------------------------------------------------------------------------
/benchmark/utils.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | const os = require("os");
4 | const childProcess = require("child_process");
5 | const prettyBytes = require("pretty-bytes");
6 | const promisify = require("../lib/utils/promisify");
7 | const jetpack = require("..");
8 |
9 | const testDirPath = () => {
10 | return `${os.tmpdir()}/jetpack-benchmark`;
11 | };
12 |
13 | const prepareJetpackTestDir = () => {
14 | return jetpack.dir(testDirPath(), { empty: true });
15 | };
16 |
17 | const prepareFiles = (jetpackDir, creationConfig) => {
18 | return new Promise((resolve, reject) => {
19 | let count = 0;
20 | let countFilesInThisDir = 0;
21 | const content = Buffer.alloc(creationConfig.size, "x");
22 |
23 | const makeOneFile = () => {
24 | jetpackDir.fileAsync(`${count}.txt`, { content }).then(() => {
25 | count += 1;
26 | countFilesInThisDir += 1;
27 | if (count < creationConfig.files) {
28 | if (
29 | creationConfig.filesPerNestedDir &&
30 | countFilesInThisDir === creationConfig.filesPerNestedDir
31 | ) {
32 | countFilesInThisDir = 0;
33 | jetpackDir = jetpackDir.cwd("subdir");
34 | }
35 | makeOneFile();
36 | } else {
37 | resolve();
38 | }
39 | }, reject);
40 | };
41 |
42 | console.log(
43 | `Preparing ${creationConfig.files} test files (${prettyBytes(
44 | creationConfig.size
45 | )} each)...`
46 | );
47 | makeOneFile();
48 | });
49 | };
50 |
51 | const startTimer = (startMessage) => {
52 | const start = Date.now();
53 | process.stdout.write(`${startMessage} ... `);
54 |
55 | const stop = () => {
56 | const time = Date.now() - start;
57 | console.log(`${time}ms`);
58 | return time;
59 | };
60 |
61 | return stop;
62 | };
63 |
64 | const waitAWhile = () => {
65 | return new Promise((resolve) => {
66 | console.log("Waiting 5s to allow hardware buffers be emptied...");
67 | setTimeout(resolve, 5000);
68 | });
69 | };
70 |
71 | const showMemoryUsage = () => {
72 | const used = process.memoryUsage();
73 | for (let key in used) {
74 | console.log(
75 | `${key} ${Math.round((used[key] / 1024 / 1024) * 100) / 100} MB`
76 | );
77 | }
78 | };
79 |
80 | const cleanAfterTest = () => {
81 | console.log("Cleaning up after test...");
82 | return jetpack.removeAsync(testDirPath());
83 | };
84 |
85 | module.exports = {
86 | prepareJetpackTestDir,
87 | prepareFiles,
88 | startTimer,
89 | waitAWhile,
90 | exec: promisify(childProcess.exec),
91 | showMemoryUsage,
92 | cleanAfterTest,
93 | };
94 |
--------------------------------------------------------------------------------
/index.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
3 | import { FSJetpack } from "./types";
4 |
5 | declare const jetpack: FSJetpack;
6 |
7 | export = jetpack;
8 |
--------------------------------------------------------------------------------
/lib/append.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | const fs = require("./utils/fs");
4 | const write = require("./write");
5 | const validate = require("./utils/validate");
6 |
7 | const validateInput = (methodName, path, data, options) => {
8 | const methodSignature = `${methodName}(path, data, [options])`;
9 | validate.argument(methodSignature, "path", path, ["string"]);
10 | validate.argument(methodSignature, "data", data, ["string", "buffer"]);
11 | validate.options(methodSignature, "options", options, {
12 | mode: ["string", "number"],
13 | });
14 | };
15 |
16 | // ---------------------------------------------------------
17 | // SYNC
18 | // ---------------------------------------------------------
19 |
20 | const appendSync = (path, data, options) => {
21 | try {
22 | fs.appendFileSync(path, data, options);
23 | } catch (err) {
24 | if (err.code === "ENOENT") {
25 | // Parent directory doesn't exist, so just pass the task to `write`,
26 | // which will create the folder and file.
27 | write.sync(path, data, options);
28 | } else {
29 | throw err;
30 | }
31 | }
32 | };
33 |
34 | // ---------------------------------------------------------
35 | // ASYNC
36 | // ---------------------------------------------------------
37 |
38 | const appendAsync = (path, data, options) => {
39 | return new Promise((resolve, reject) => {
40 | fs.appendFile(path, data, options)
41 | .then(resolve)
42 | .catch((err) => {
43 | if (err.code === "ENOENT") {
44 | // Parent directory doesn't exist, so just pass the task to `write`,
45 | // which will create the folder and file.
46 | write.async(path, data, options).then(resolve, reject);
47 | } else {
48 | reject(err);
49 | }
50 | });
51 | });
52 | };
53 |
54 | // ---------------------------------------------------------
55 | // API
56 | // ---------------------------------------------------------
57 |
58 | exports.validateInput = validateInput;
59 | exports.sync = appendSync;
60 | exports.async = appendAsync;
61 |
--------------------------------------------------------------------------------
/lib/copy.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | const pathUtil = require("path");
4 | const fs = require("./utils/fs");
5 | const dir = require("./dir");
6 | const exists = require("./exists");
7 | const inspect = require("./inspect");
8 | const write = require("./write");
9 | const matcher = require("./utils/matcher");
10 | const fileMode = require("./utils/mode");
11 | const treeWalker = require("./utils/tree_walker");
12 | const validate = require("./utils/validate");
13 |
14 | const validateInput = (methodName, from, to, options) => {
15 | const methodSignature = `${methodName}(from, to, [options])`;
16 | validate.argument(methodSignature, "from", from, ["string"]);
17 | validate.argument(methodSignature, "to", to, ["string"]);
18 | validate.options(methodSignature, "options", options, {
19 | overwrite: ["boolean", "function"],
20 | matching: ["string", "array of string"],
21 | ignoreCase: ["boolean"],
22 | });
23 | };
24 |
25 | const parseOptions = (options, from) => {
26 | const opts = options || {};
27 | const parsedOptions = {};
28 |
29 | if (opts.ignoreCase === undefined) {
30 | opts.ignoreCase = false;
31 | }
32 |
33 | parsedOptions.overwrite = opts.overwrite;
34 |
35 | if (opts.matching) {
36 | parsedOptions.allowedToCopy = matcher.create(
37 | from,
38 | opts.matching,
39 | opts.ignoreCase
40 | );
41 | } else {
42 | parsedOptions.allowedToCopy = () => {
43 | // Default behaviour - copy everything.
44 | return true;
45 | };
46 | }
47 |
48 | return parsedOptions;
49 | };
50 |
51 | const generateNoSourceError = (path) => {
52 | const err = new Error(`Path to copy doesn't exist ${path}`);
53 | err.code = "ENOENT";
54 | return err;
55 | };
56 |
57 | const generateDestinationExistsError = (path) => {
58 | const err = new Error(`Destination path already exists ${path}`);
59 | err.code = "EEXIST";
60 | return err;
61 | };
62 |
63 | const inspectOptions = {
64 | mode: true,
65 | symlinks: "report",
66 | times: true,
67 | absolutePath: true,
68 | };
69 |
70 | const shouldThrowDestinationExistsError = (context) => {
71 | return (
72 | typeof context.opts.overwrite !== "function" &&
73 | context.opts.overwrite !== true
74 | );
75 | };
76 |
77 | // ---------------------------------------------------------
78 | // Sync
79 | // ---------------------------------------------------------
80 |
81 | const checksBeforeCopyingSync = (from, to, opts) => {
82 | if (!exists.sync(from)) {
83 | throw generateNoSourceError(from);
84 | }
85 |
86 | if (exists.sync(to) && !opts.overwrite) {
87 | throw generateDestinationExistsError(to);
88 | }
89 | };
90 |
91 | const canOverwriteItSync = (context) => {
92 | if (typeof context.opts.overwrite === "function") {
93 | const destInspectData = inspect.sync(context.destPath, inspectOptions);
94 | return context.opts.overwrite(context.srcInspectData, destInspectData);
95 | }
96 | return context.opts.overwrite === true;
97 | };
98 |
99 | const copyFileSync = (srcPath, destPath, mode, context) => {
100 | const data = fs.readFileSync(srcPath);
101 | try {
102 | fs.writeFileSync(destPath, data, { mode, flag: "wx" });
103 | } catch (err) {
104 | if (err.code === "ENOENT") {
105 | write.sync(destPath, data, { mode });
106 | } else if (err.code === "EEXIST") {
107 | if (canOverwriteItSync(context)) {
108 | fs.writeFileSync(destPath, data, { mode });
109 | } else if (shouldThrowDestinationExistsError(context)) {
110 | throw generateDestinationExistsError(context.destPath);
111 | }
112 | } else {
113 | throw err;
114 | }
115 | }
116 | };
117 |
118 | const copySymlinkSync = (from, to) => {
119 | const symlinkPointsAt = fs.readlinkSync(from);
120 | try {
121 | fs.symlinkSync(symlinkPointsAt, to);
122 | } catch (err) {
123 | // There is already file/symlink with this name on destination location.
124 | // Must erase it manually, otherwise system won't allow us to place symlink there.
125 | if (err.code === "EEXIST") {
126 | fs.unlinkSync(to);
127 | // Retry...
128 | fs.symlinkSync(symlinkPointsAt, to);
129 | } else {
130 | throw err;
131 | }
132 | }
133 | };
134 |
135 | const copyItemSync = (srcPath, srcInspectData, destPath, opts) => {
136 | const context = { srcPath, destPath, srcInspectData, opts };
137 | const mode = fileMode.normalizeFileMode(srcInspectData.mode);
138 | if (srcInspectData.type === "dir") {
139 | dir.createSync(destPath, { mode });
140 | } else if (srcInspectData.type === "file") {
141 | copyFileSync(srcPath, destPath, mode, context);
142 | } else if (srcInspectData.type === "symlink") {
143 | copySymlinkSync(srcPath, destPath);
144 | }
145 | };
146 |
147 | const copySync = (from, to, options) => {
148 | const opts = parseOptions(options, from);
149 |
150 | checksBeforeCopyingSync(from, to, opts);
151 |
152 | treeWalker.sync(from, { inspectOptions }, (srcPath, srcInspectData) => {
153 | const rel = pathUtil.relative(from, srcPath);
154 | const destPath = pathUtil.resolve(to, rel);
155 | if (opts.allowedToCopy(srcPath, destPath, srcInspectData)) {
156 | copyItemSync(srcPath, srcInspectData, destPath, opts);
157 | }
158 | });
159 | };
160 |
161 | // ---------------------------------------------------------
162 | // Async
163 | // ---------------------------------------------------------
164 |
165 | const checksBeforeCopyingAsync = (from, to, opts) => {
166 | return exists
167 | .async(from)
168 | .then((srcPathExists) => {
169 | if (!srcPathExists) {
170 | throw generateNoSourceError(from);
171 | } else {
172 | return exists.async(to);
173 | }
174 | })
175 | .then((destPathExists) => {
176 | if (destPathExists && !opts.overwrite) {
177 | throw generateDestinationExistsError(to);
178 | }
179 | });
180 | };
181 |
182 | const canOverwriteItAsync = (context) => {
183 | return new Promise((resolve, reject) => {
184 | if (typeof context.opts.overwrite === "function") {
185 | inspect
186 | .async(context.destPath, inspectOptions)
187 | .then((destInspectData) => {
188 | resolve(
189 | context.opts.overwrite(context.srcInspectData, destInspectData)
190 | );
191 | })
192 | .catch(reject);
193 | } else {
194 | resolve(context.opts.overwrite === true);
195 | }
196 | });
197 | };
198 |
199 | const copyFileAsync = (srcPath, destPath, mode, context, runOptions) => {
200 | return new Promise((resolve, reject) => {
201 | const runOpts = runOptions || {};
202 |
203 | let flags = "wx";
204 | if (runOpts.overwrite) {
205 | flags = "w";
206 | }
207 |
208 | const readStream = fs.createReadStream(srcPath);
209 | const writeStream = fs.createWriteStream(destPath, { mode, flags });
210 |
211 | readStream.on("error", reject);
212 |
213 | writeStream.on("error", (err) => {
214 | // Force read stream to close, since write stream errored
215 | // read stream serves us no purpose.
216 | readStream.resume();
217 |
218 | if (err.code === "ENOENT") {
219 | // Some parent directory doesn't exits. Create it and retry.
220 | dir
221 | .createAsync(pathUtil.dirname(destPath))
222 | .then(() => {
223 | copyFileAsync(srcPath, destPath, mode, context).then(
224 | resolve,
225 | reject
226 | );
227 | })
228 | .catch(reject);
229 | } else if (err.code === "EEXIST") {
230 | canOverwriteItAsync(context)
231 | .then((canOverwite) => {
232 | if (canOverwite) {
233 | copyFileAsync(srcPath, destPath, mode, context, {
234 | overwrite: true,
235 | }).then(resolve, reject);
236 | } else if (shouldThrowDestinationExistsError(context)) {
237 | reject(generateDestinationExistsError(destPath));
238 | } else {
239 | resolve();
240 | }
241 | })
242 | .catch(reject);
243 | } else {
244 | reject(err);
245 | }
246 | });
247 |
248 | writeStream.on("finish", resolve);
249 |
250 | readStream.pipe(writeStream);
251 | });
252 | };
253 |
254 | const copySymlinkAsync = (from, to) => {
255 | return fs.readlink(from).then((symlinkPointsAt) => {
256 | return new Promise((resolve, reject) => {
257 | fs.symlink(symlinkPointsAt, to)
258 | .then(resolve)
259 | .catch((err) => {
260 | if (err.code === "EEXIST") {
261 | // There is already file/symlink with this name on destination location.
262 | // Must erase it manually, otherwise system won't allow us to place symlink there.
263 | fs.unlink(to)
264 | .then(() => {
265 | // Retry...
266 | return fs.symlink(symlinkPointsAt, to);
267 | })
268 | .then(resolve, reject);
269 | } else {
270 | reject(err);
271 | }
272 | });
273 | });
274 | });
275 | };
276 |
277 | const copyItemAsync = (srcPath, srcInspectData, destPath, opts) => {
278 | const context = { srcPath, destPath, srcInspectData, opts };
279 | const mode = fileMode.normalizeFileMode(srcInspectData.mode);
280 | if (srcInspectData.type === "dir") {
281 | return dir.createAsync(destPath, { mode });
282 | } else if (srcInspectData.type === "file") {
283 | return copyFileAsync(srcPath, destPath, mode, context);
284 | } else if (srcInspectData.type === "symlink") {
285 | return copySymlinkAsync(srcPath, destPath);
286 | }
287 | // Ha! This is none of supported file system entities. What now?
288 | // Just continuing without actually copying sounds sane.
289 | return Promise.resolve();
290 | };
291 |
292 | const copyAsync = (from, to, options) => {
293 | return new Promise((resolve, reject) => {
294 | const opts = parseOptions(options, from);
295 |
296 | checksBeforeCopyingAsync(from, to, opts)
297 | .then(() => {
298 | let allFilesDelivered = false;
299 | let filesInProgress = 0;
300 |
301 | treeWalker.async(
302 | from,
303 | { inspectOptions },
304 | (srcPath, item) => {
305 | if (item) {
306 | const rel = pathUtil.relative(from, srcPath);
307 | const destPath = pathUtil.resolve(to, rel);
308 | if (opts.allowedToCopy(srcPath, item, destPath)) {
309 | filesInProgress += 1;
310 | copyItemAsync(srcPath, item, destPath, opts)
311 | .then(() => {
312 | filesInProgress -= 1;
313 | if (allFilesDelivered && filesInProgress === 0) {
314 | resolve();
315 | }
316 | })
317 | .catch(reject);
318 | }
319 | }
320 | },
321 | (err) => {
322 | if (err) {
323 | reject(err);
324 | } else {
325 | allFilesDelivered = true;
326 | if (allFilesDelivered && filesInProgress === 0) {
327 | resolve();
328 | }
329 | }
330 | }
331 | );
332 | })
333 | .catch(reject);
334 | });
335 | };
336 |
337 | // ---------------------------------------------------------
338 | // API
339 | // ---------------------------------------------------------
340 |
341 | exports.validateInput = validateInput;
342 | exports.sync = copySync;
343 | exports.async = copyAsync;
344 |
--------------------------------------------------------------------------------
/lib/dir.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | const pathUtil = require("path");
4 | const fs = require("./utils/fs");
5 | const modeUtil = require("./utils/mode");
6 | const validate = require("./utils/validate");
7 | const remove = require("./remove");
8 |
9 | const validateInput = (methodName, path, criteria) => {
10 | const methodSignature = `${methodName}(path, [criteria])`;
11 | validate.argument(methodSignature, "path", path, ["string"]);
12 | validate.options(methodSignature, "criteria", criteria, {
13 | empty: ["boolean"],
14 | mode: ["string", "number"],
15 | });
16 | };
17 |
18 | const getCriteriaDefaults = (passedCriteria) => {
19 | const criteria = passedCriteria || {};
20 | if (typeof criteria.empty !== "boolean") {
21 | criteria.empty = false;
22 | }
23 | if (criteria.mode !== undefined) {
24 | criteria.mode = modeUtil.normalizeFileMode(criteria.mode);
25 | }
26 | return criteria;
27 | };
28 |
29 | const generatePathOccupiedByNotDirectoryError = (path) => {
30 | return new Error(
31 | `Path ${path} exists but is not a directory. Halting jetpack.dir() call for safety reasons.`
32 | );
33 | };
34 |
35 | // ---------------------------------------------------------
36 | // Sync
37 | // ---------------------------------------------------------
38 |
39 | const checkWhatAlreadyOccupiesPathSync = (path) => {
40 | let stat;
41 |
42 | try {
43 | stat = fs.statSync(path);
44 | } catch (err) {
45 | // Detection if path already exists
46 | if (err.code !== "ENOENT") {
47 | throw err;
48 | }
49 | }
50 |
51 | if (stat && !stat.isDirectory()) {
52 | throw generatePathOccupiedByNotDirectoryError(path);
53 | }
54 |
55 | return stat;
56 | };
57 |
58 | const createBrandNewDirectorySync = (path, opts) => {
59 | const options = opts || {};
60 |
61 | try {
62 | fs.mkdirSync(path, options.mode);
63 | } catch (err) {
64 | if (err.code === "ENOENT") {
65 | // Parent directory doesn't exist. Need to create it first.
66 | createBrandNewDirectorySync(pathUtil.dirname(path), options);
67 | // Now retry creating this directory.
68 | fs.mkdirSync(path, options.mode);
69 | } else if (err.code === "EEXIST") {
70 | // The path already exists. We're fine.
71 | } else {
72 | throw err;
73 | }
74 | }
75 | };
76 |
77 | const checkExistingDirectoryFulfillsCriteriaSync = (path, stat, criteria) => {
78 | const checkMode = () => {
79 | const mode = modeUtil.normalizeFileMode(stat.mode);
80 | if (criteria.mode !== undefined && criteria.mode !== mode) {
81 | fs.chmodSync(path, criteria.mode);
82 | }
83 | };
84 |
85 | const checkEmptiness = () => {
86 | if (criteria.empty) {
87 | // Delete everything inside this directory
88 | const list = fs.readdirSync(path);
89 | list.forEach((filename) => {
90 | remove.sync(pathUtil.resolve(path, filename));
91 | });
92 | }
93 | };
94 |
95 | checkMode();
96 | checkEmptiness();
97 | };
98 |
99 | const dirSync = (path, passedCriteria) => {
100 | const criteria = getCriteriaDefaults(passedCriteria);
101 | const stat = checkWhatAlreadyOccupiesPathSync(path);
102 | if (stat) {
103 | checkExistingDirectoryFulfillsCriteriaSync(path, stat, criteria);
104 | } else {
105 | createBrandNewDirectorySync(path, criteria);
106 | }
107 | };
108 |
109 | // ---------------------------------------------------------
110 | // Async
111 | // ---------------------------------------------------------
112 |
113 | const checkWhatAlreadyOccupiesPathAsync = (path) => {
114 | return new Promise((resolve, reject) => {
115 | fs.stat(path)
116 | .then((stat) => {
117 | if (stat.isDirectory()) {
118 | resolve(stat);
119 | } else {
120 | reject(generatePathOccupiedByNotDirectoryError(path));
121 | }
122 | })
123 | .catch((err) => {
124 | if (err.code === "ENOENT") {
125 | // Path doesn't exist
126 | resolve(undefined);
127 | } else {
128 | // This is other error that nonexistent path, so end here.
129 | reject(err);
130 | }
131 | });
132 | });
133 | };
134 |
135 | // Delete all files and directores inside given directory
136 | const emptyAsync = (path) => {
137 | return new Promise((resolve, reject) => {
138 | fs.readdir(path)
139 | .then((list) => {
140 | const doOne = (index) => {
141 | if (index === list.length) {
142 | resolve();
143 | } else {
144 | const subPath = pathUtil.resolve(path, list[index]);
145 | remove.async(subPath).then(() => {
146 | doOne(index + 1);
147 | });
148 | }
149 | };
150 |
151 | doOne(0);
152 | })
153 | .catch(reject);
154 | });
155 | };
156 |
157 | const checkExistingDirectoryFulfillsCriteriaAsync = (path, stat, criteria) => {
158 | return new Promise((resolve, reject) => {
159 | const checkMode = () => {
160 | const mode = modeUtil.normalizeFileMode(stat.mode);
161 | if (criteria.mode !== undefined && criteria.mode !== mode) {
162 | return fs.chmod(path, criteria.mode);
163 | }
164 | return Promise.resolve();
165 | };
166 |
167 | const checkEmptiness = () => {
168 | if (criteria.empty) {
169 | return emptyAsync(path);
170 | }
171 | return Promise.resolve();
172 | };
173 |
174 | checkMode().then(checkEmptiness).then(resolve, reject);
175 | });
176 | };
177 |
178 | const createBrandNewDirectoryAsync = (path, opts) => {
179 | const options = opts || {};
180 |
181 | return new Promise((resolve, reject) => {
182 | fs.mkdir(path, options.mode)
183 | .then(resolve)
184 | .catch((err) => {
185 | if (err.code === "ENOENT") {
186 | // Parent directory doesn't exist. Need to create it first.
187 | createBrandNewDirectoryAsync(pathUtil.dirname(path), options)
188 | .then(() => {
189 | // Now retry creating this directory.
190 | return fs.mkdir(path, options.mode);
191 | })
192 | .then(resolve)
193 | .catch((err2) => {
194 | if (err2.code === "EEXIST") {
195 | // Hmm, something other have already created the directory?
196 | // No problem for us.
197 | resolve();
198 | } else {
199 | reject(err2);
200 | }
201 | });
202 | } else if (err.code === "EEXIST") {
203 | // The path already exists. We're fine.
204 | resolve();
205 | } else {
206 | reject(err);
207 | }
208 | });
209 | });
210 | };
211 |
212 | const dirAsync = (path, passedCriteria) => {
213 | return new Promise((resolve, reject) => {
214 | const criteria = getCriteriaDefaults(passedCriteria);
215 |
216 | checkWhatAlreadyOccupiesPathAsync(path)
217 | .then((stat) => {
218 | if (stat !== undefined) {
219 | return checkExistingDirectoryFulfillsCriteriaAsync(
220 | path,
221 | stat,
222 | criteria
223 | );
224 | }
225 | return createBrandNewDirectoryAsync(path, criteria);
226 | })
227 | .then(resolve, reject);
228 | });
229 | };
230 |
231 | // ---------------------------------------------------------
232 | // API
233 | // ---------------------------------------------------------
234 |
235 | exports.validateInput = validateInput;
236 | exports.sync = dirSync;
237 | exports.createSync = createBrandNewDirectorySync;
238 | exports.async = dirAsync;
239 | exports.createAsync = createBrandNewDirectoryAsync;
240 |
--------------------------------------------------------------------------------
/lib/exists.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | const fs = require("./utils/fs");
4 | const validate = require("./utils/validate");
5 |
6 | const validateInput = (methodName, path) => {
7 | const methodSignature = `${methodName}(path)`;
8 | validate.argument(methodSignature, "path", path, ["string"]);
9 | };
10 |
11 | // ---------------------------------------------------------
12 | // Sync
13 | // ---------------------------------------------------------
14 |
15 | const existsSync = (path) => {
16 | try {
17 | const stat = fs.statSync(path);
18 | if (stat.isDirectory()) {
19 | return "dir";
20 | } else if (stat.isFile()) {
21 | return "file";
22 | }
23 | return "other";
24 | } catch (err) {
25 | if (err.code !== "ENOENT") {
26 | throw err;
27 | }
28 | }
29 |
30 | return false;
31 | };
32 |
33 | // ---------------------------------------------------------
34 | // Async
35 | // ---------------------------------------------------------
36 |
37 | const existsAsync = (path) => {
38 | return new Promise((resolve, reject) => {
39 | fs.stat(path)
40 | .then((stat) => {
41 | if (stat.isDirectory()) {
42 | resolve("dir");
43 | } else if (stat.isFile()) {
44 | resolve("file");
45 | } else {
46 | resolve("other");
47 | }
48 | })
49 | .catch((err) => {
50 | if (err.code === "ENOENT") {
51 | resolve(false);
52 | } else {
53 | reject(err);
54 | }
55 | });
56 | });
57 | };
58 |
59 | // ---------------------------------------------------------
60 | // API
61 | // ---------------------------------------------------------
62 |
63 | exports.validateInput = validateInput;
64 | exports.sync = existsSync;
65 | exports.async = existsAsync;
66 |
--------------------------------------------------------------------------------
/lib/file.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | const fs = require("./utils/fs");
4 | const modeUtil = require("./utils/mode");
5 | const validate = require("./utils/validate");
6 | const write = require("./write");
7 |
8 | const validateInput = (methodName, path, criteria) => {
9 | const methodSignature = `${methodName}(path, [criteria])`;
10 | validate.argument(methodSignature, "path", path, ["string"]);
11 | validate.options(methodSignature, "criteria", criteria, {
12 | content: ["string", "buffer", "object", "array"],
13 | jsonIndent: ["number"],
14 | mode: ["string", "number"],
15 | });
16 | };
17 |
18 | const getCriteriaDefaults = (passedCriteria) => {
19 | const criteria = passedCriteria || {};
20 | if (criteria.mode !== undefined) {
21 | criteria.mode = modeUtil.normalizeFileMode(criteria.mode);
22 | }
23 | return criteria;
24 | };
25 |
26 | const generatePathOccupiedByNotFileError = (path) => {
27 | return new Error(
28 | `Path ${path} exists but is not a file. Halting jetpack.file() call for safety reasons.`
29 | );
30 | };
31 |
32 | // ---------------------------------------------------------
33 | // Sync
34 | // ---------------------------------------------------------
35 |
36 | const checkWhatAlreadyOccupiesPathSync = (path) => {
37 | let stat;
38 |
39 | try {
40 | stat = fs.statSync(path);
41 | } catch (err) {
42 | // Detection if path exists
43 | if (err.code !== "ENOENT") {
44 | throw err;
45 | }
46 | }
47 |
48 | if (stat && !stat.isFile()) {
49 | throw generatePathOccupiedByNotFileError(path);
50 | }
51 |
52 | return stat;
53 | };
54 |
55 | const checkExistingFileFulfillsCriteriaSync = (path, stat, criteria) => {
56 | const mode = modeUtil.normalizeFileMode(stat.mode);
57 |
58 | const checkContent = () => {
59 | if (criteria.content !== undefined) {
60 | write.sync(path, criteria.content, {
61 | mode,
62 | jsonIndent: criteria.jsonIndent,
63 | });
64 | return true;
65 | }
66 | return false;
67 | };
68 |
69 | const checkMode = () => {
70 | if (criteria.mode !== undefined && criteria.mode !== mode) {
71 | fs.chmodSync(path, criteria.mode);
72 | }
73 | };
74 |
75 | const contentReplaced = checkContent();
76 | if (!contentReplaced) {
77 | checkMode();
78 | }
79 | };
80 |
81 | const createBrandNewFileSync = (path, criteria) => {
82 | let content = "";
83 | if (criteria.content !== undefined) {
84 | content = criteria.content;
85 | }
86 | write.sync(path, content, {
87 | mode: criteria.mode,
88 | jsonIndent: criteria.jsonIndent,
89 | });
90 | };
91 |
92 | const fileSync = (path, passedCriteria) => {
93 | const criteria = getCriteriaDefaults(passedCriteria);
94 | const stat = checkWhatAlreadyOccupiesPathSync(path);
95 | if (stat !== undefined) {
96 | checkExistingFileFulfillsCriteriaSync(path, stat, criteria);
97 | } else {
98 | createBrandNewFileSync(path, criteria);
99 | }
100 | };
101 |
102 | // ---------------------------------------------------------
103 | // Async
104 | // ---------------------------------------------------------
105 |
106 | const checkWhatAlreadyOccupiesPathAsync = (path) => {
107 | return new Promise((resolve, reject) => {
108 | fs.stat(path)
109 | .then((stat) => {
110 | if (stat.isFile()) {
111 | resolve(stat);
112 | } else {
113 | reject(generatePathOccupiedByNotFileError(path));
114 | }
115 | })
116 | .catch((err) => {
117 | if (err.code === "ENOENT") {
118 | // Path doesn't exist.
119 | resolve(undefined);
120 | } else {
121 | // This is other error. Must end here.
122 | reject(err);
123 | }
124 | });
125 | });
126 | };
127 |
128 | const checkExistingFileFulfillsCriteriaAsync = (path, stat, criteria) => {
129 | const mode = modeUtil.normalizeFileMode(stat.mode);
130 |
131 | const checkContent = () => {
132 | return new Promise((resolve, reject) => {
133 | if (criteria.content !== undefined) {
134 | write
135 | .async(path, criteria.content, {
136 | mode,
137 | jsonIndent: criteria.jsonIndent,
138 | })
139 | .then(() => {
140 | resolve(true);
141 | })
142 | .catch(reject);
143 | } else {
144 | resolve(false);
145 | }
146 | });
147 | };
148 |
149 | const checkMode = () => {
150 | if (criteria.mode !== undefined && criteria.mode !== mode) {
151 | return fs.chmod(path, criteria.mode);
152 | }
153 | return undefined;
154 | };
155 |
156 | return checkContent().then((contentReplaced) => {
157 | if (!contentReplaced) {
158 | return checkMode();
159 | }
160 | return undefined;
161 | });
162 | };
163 |
164 | const createBrandNewFileAsync = (path, criteria) => {
165 | let content = "";
166 | if (criteria.content !== undefined) {
167 | content = criteria.content;
168 | }
169 |
170 | return write.async(path, content, {
171 | mode: criteria.mode,
172 | jsonIndent: criteria.jsonIndent,
173 | });
174 | };
175 |
176 | const fileAsync = (path, passedCriteria) => {
177 | return new Promise((resolve, reject) => {
178 | const criteria = getCriteriaDefaults(passedCriteria);
179 |
180 | checkWhatAlreadyOccupiesPathAsync(path)
181 | .then((stat) => {
182 | if (stat !== undefined) {
183 | return checkExistingFileFulfillsCriteriaAsync(path, stat, criteria);
184 | }
185 | return createBrandNewFileAsync(path, criteria);
186 | })
187 | .then(resolve, reject);
188 | });
189 | };
190 |
191 | // ---------------------------------------------------------
192 | // API
193 | // ---------------------------------------------------------
194 |
195 | exports.validateInput = validateInput;
196 | exports.sync = fileSync;
197 | exports.async = fileAsync;
198 |
--------------------------------------------------------------------------------
/lib/find.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | const pathUtil = require("path");
4 | const treeWalker = require("./utils/tree_walker");
5 | const inspect = require("./inspect");
6 | const matcher = require("./utils/matcher");
7 | const validate = require("./utils/validate");
8 |
9 | const validateInput = (methodName, path, options) => {
10 | const methodSignature = `${methodName}([path], options)`;
11 | validate.argument(methodSignature, "path", path, ["string"]);
12 | validate.options(methodSignature, "options", options, {
13 | matching: ["string", "array of string"],
14 | filter: ["function"],
15 | files: ["boolean"],
16 | directories: ["boolean"],
17 | recursive: ["boolean"],
18 | ignoreCase: ["boolean"],
19 | });
20 | };
21 |
22 | const normalizeOptions = (options) => {
23 | const opts = options || {};
24 | // defaults:
25 | if (opts.matching === undefined) {
26 | opts.matching = "*";
27 | }
28 | if (opts.files === undefined) {
29 | opts.files = true;
30 | }
31 | if (opts.ignoreCase === undefined) {
32 | opts.ignoreCase = false;
33 | }
34 | if (opts.directories === undefined) {
35 | opts.directories = false;
36 | }
37 | if (opts.recursive === undefined) {
38 | opts.recursive = true;
39 | }
40 | return opts;
41 | };
42 |
43 | const processFoundPaths = (foundPaths, cwd) => {
44 | return foundPaths.map((path) => {
45 | return pathUtil.relative(cwd, path);
46 | });
47 | };
48 |
49 | const generatePathDoesntExistError = (path) => {
50 | const err = new Error(`Path you want to find stuff in doesn't exist ${path}`);
51 | err.code = "ENOENT";
52 | return err;
53 | };
54 |
55 | const generatePathNotDirectoryError = (path) => {
56 | const err = new Error(
57 | `Path you want to find stuff in must be a directory ${path}`
58 | );
59 | err.code = "ENOTDIR";
60 | return err;
61 | };
62 |
63 | // ---------------------------------------------------------
64 | // Sync
65 | // ---------------------------------------------------------
66 |
67 | const findSync = (path, options) => {
68 | const foundAbsolutePaths = [];
69 | const matchesAnyOfGlobs = matcher.create(
70 | path,
71 | options.matching,
72 | options.ignoreCase
73 | );
74 |
75 | let maxLevelsDeep = Infinity;
76 | if (options.recursive === false) {
77 | maxLevelsDeep = 1;
78 | }
79 |
80 | treeWalker.sync(
81 | path,
82 | {
83 | maxLevelsDeep,
84 | symlinks: "follow",
85 | inspectOptions: { times: true, absolutePath: true },
86 | },
87 | (itemPath, item) => {
88 | if (item && itemPath !== path && matchesAnyOfGlobs(itemPath)) {
89 | const weHaveMatch =
90 | (item.type === "file" && options.files === true) ||
91 | (item.type === "dir" && options.directories === true);
92 |
93 | if (weHaveMatch) {
94 | if (options.filter) {
95 | const passedThroughFilter = options.filter(item);
96 | if (passedThroughFilter) {
97 | foundAbsolutePaths.push(itemPath);
98 | }
99 | } else {
100 | foundAbsolutePaths.push(itemPath);
101 | }
102 | }
103 | }
104 | }
105 | );
106 |
107 | foundAbsolutePaths.sort();
108 |
109 | return processFoundPaths(foundAbsolutePaths, options.cwd);
110 | };
111 |
112 | const findSyncInit = (path, options) => {
113 | const entryPointInspect = inspect.sync(path, { symlinks: "follow" });
114 | if (entryPointInspect === undefined) {
115 | throw generatePathDoesntExistError(path);
116 | } else if (entryPointInspect.type !== "dir") {
117 | throw generatePathNotDirectoryError(path);
118 | }
119 |
120 | return findSync(path, normalizeOptions(options));
121 | };
122 |
123 | // ---------------------------------------------------------
124 | // Async
125 | // ---------------------------------------------------------
126 |
127 | const findAsync = (path, options) => {
128 | return new Promise((resolve, reject) => {
129 | const foundAbsolutePaths = [];
130 | const matchesAnyOfGlobs = matcher.create(
131 | path,
132 | options.matching,
133 | options.ignoreCase
134 | );
135 |
136 | let maxLevelsDeep = Infinity;
137 | if (options.recursive === false) {
138 | maxLevelsDeep = 1;
139 | }
140 |
141 | let waitingForFiltersToFinish = 0;
142 | let treeWalkerDone = false;
143 |
144 | const maybeDone = () => {
145 | if (treeWalkerDone && waitingForFiltersToFinish === 0) {
146 | foundAbsolutePaths.sort();
147 | resolve(processFoundPaths(foundAbsolutePaths, options.cwd));
148 | }
149 | };
150 |
151 | treeWalker.async(
152 | path,
153 | {
154 | maxLevelsDeep,
155 | symlinks: "follow",
156 | inspectOptions: { times: true, absolutePath: true },
157 | },
158 | (itemPath, item) => {
159 | if (item && itemPath !== path && matchesAnyOfGlobs(itemPath)) {
160 | const weHaveMatch =
161 | (item.type === "file" && options.files === true) ||
162 | (item.type === "dir" && options.directories === true);
163 |
164 | if (weHaveMatch) {
165 | if (options.filter) {
166 | const passedThroughFilter = options.filter(item);
167 | const isPromise = typeof passedThroughFilter.then === "function";
168 | if (isPromise) {
169 | waitingForFiltersToFinish += 1;
170 | passedThroughFilter
171 | .then((passedThroughFilterResult) => {
172 | if (passedThroughFilterResult) {
173 | foundAbsolutePaths.push(itemPath);
174 | }
175 | waitingForFiltersToFinish -= 1;
176 | maybeDone();
177 | })
178 | .catch((err) => {
179 | reject(err);
180 | });
181 | } else if (passedThroughFilter) {
182 | foundAbsolutePaths.push(itemPath);
183 | }
184 | } else {
185 | foundAbsolutePaths.push(itemPath);
186 | }
187 | }
188 | }
189 | },
190 | (err) => {
191 | if (err) {
192 | reject(err);
193 | } else {
194 | treeWalkerDone = true;
195 | maybeDone();
196 | }
197 | }
198 | );
199 | });
200 | };
201 |
202 | const findAsyncInit = (path, options) => {
203 | return inspect
204 | .async(path, { symlinks: "follow" })
205 | .then((entryPointInspect) => {
206 | if (entryPointInspect === undefined) {
207 | throw generatePathDoesntExistError(path);
208 | } else if (entryPointInspect.type !== "dir") {
209 | throw generatePathNotDirectoryError(path);
210 | }
211 | return findAsync(path, normalizeOptions(options));
212 | });
213 | };
214 |
215 | // ---------------------------------------------------------
216 | // API
217 | // ---------------------------------------------------------
218 |
219 | exports.validateInput = validateInput;
220 | exports.sync = findSyncInit;
221 | exports.async = findAsyncInit;
222 |
--------------------------------------------------------------------------------
/lib/inspect.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | const crypto = require("crypto");
4 | const pathUtil = require("path");
5 | const fs = require("./utils/fs");
6 | const validate = require("./utils/validate");
7 |
8 | const supportedChecksumAlgorithms = ["md5", "sha1", "sha256", "sha512"];
9 |
10 | const symlinkOptions = ["report", "follow"];
11 |
12 | const validateInput = (methodName, path, options) => {
13 | const methodSignature = `${methodName}(path, [options])`;
14 | validate.argument(methodSignature, "path", path, ["string"]);
15 | validate.options(methodSignature, "options", options, {
16 | checksum: ["string"],
17 | mode: ["boolean"],
18 | times: ["boolean"],
19 | absolutePath: ["boolean"],
20 | symlinks: ["string"],
21 | });
22 |
23 | if (
24 | options &&
25 | options.checksum !== undefined &&
26 | supportedChecksumAlgorithms.indexOf(options.checksum) === -1
27 | ) {
28 | throw new Error(
29 | `Argument "options.checksum" passed to ${methodSignature} must have one of values: ${supportedChecksumAlgorithms.join(
30 | ", "
31 | )}`
32 | );
33 | }
34 |
35 | if (
36 | options &&
37 | options.symlinks !== undefined &&
38 | symlinkOptions.indexOf(options.symlinks) === -1
39 | ) {
40 | throw new Error(
41 | `Argument "options.symlinks" passed to ${methodSignature} must have one of values: ${symlinkOptions.join(
42 | ", "
43 | )}`
44 | );
45 | }
46 | };
47 |
48 | const createInspectObj = (path, options, stat) => {
49 | const obj = {};
50 |
51 | obj.name = pathUtil.basename(path);
52 |
53 | if (stat.isFile()) {
54 | obj.type = "file";
55 | obj.size = stat.size;
56 | } else if (stat.isDirectory()) {
57 | obj.type = "dir";
58 | } else if (stat.isSymbolicLink()) {
59 | obj.type = "symlink";
60 | } else {
61 | obj.type = "other";
62 | }
63 |
64 | if (options.mode) {
65 | obj.mode = stat.mode;
66 | }
67 |
68 | if (options.times) {
69 | obj.accessTime = stat.atime;
70 | obj.modifyTime = stat.mtime;
71 | obj.changeTime = stat.ctime;
72 | obj.birthTime = stat.birthtime;
73 | }
74 |
75 | if (options.absolutePath) {
76 | obj.absolutePath = path;
77 | }
78 |
79 | return obj;
80 | };
81 |
82 | // ---------------------------------------------------------
83 | // Sync
84 | // ---------------------------------------------------------
85 |
86 | const fileChecksum = (path, algo) => {
87 | const hash = crypto.createHash(algo);
88 | const data = fs.readFileSync(path);
89 | hash.update(data);
90 | return hash.digest("hex");
91 | };
92 |
93 | const addExtraFieldsSync = (path, inspectObj, options) => {
94 | if (inspectObj.type === "file" && options.checksum) {
95 | inspectObj[options.checksum] = fileChecksum(path, options.checksum);
96 | } else if (inspectObj.type === "symlink") {
97 | inspectObj.pointsAt = fs.readlinkSync(path);
98 | }
99 | };
100 |
101 | const inspectSync = (path, options) => {
102 | let statOperation = fs.lstatSync;
103 | let stat;
104 | const opts = options || {};
105 |
106 | if (opts.symlinks === "follow") {
107 | statOperation = fs.statSync;
108 | }
109 |
110 | try {
111 | stat = statOperation(path);
112 | } catch (err) {
113 | // Detection if path exists
114 | if (err.code === "ENOENT") {
115 | // Doesn't exist. Return undefined instead of throwing.
116 | return undefined;
117 | }
118 | throw err;
119 | }
120 |
121 | const inspectObj = createInspectObj(path, opts, stat);
122 | addExtraFieldsSync(path, inspectObj, opts);
123 |
124 | return inspectObj;
125 | };
126 |
127 | // ---------------------------------------------------------
128 | // Async
129 | // ---------------------------------------------------------
130 |
131 | const fileChecksumAsync = (path, algo) => {
132 | return new Promise((resolve, reject) => {
133 | const hash = crypto.createHash(algo);
134 | const s = fs.createReadStream(path);
135 | s.on("data", (data) => {
136 | hash.update(data);
137 | });
138 | s.on("end", () => {
139 | resolve(hash.digest("hex"));
140 | });
141 | s.on("error", reject);
142 | });
143 | };
144 |
145 | const addExtraFieldsAsync = (path, inspectObj, options) => {
146 | if (inspectObj.type === "file" && options.checksum) {
147 | return fileChecksumAsync(path, options.checksum).then((checksum) => {
148 | inspectObj[options.checksum] = checksum;
149 | return inspectObj;
150 | });
151 | } else if (inspectObj.type === "symlink") {
152 | return fs.readlink(path).then((linkPath) => {
153 | inspectObj.pointsAt = linkPath;
154 | return inspectObj;
155 | });
156 | }
157 | return Promise.resolve(inspectObj);
158 | };
159 |
160 | const inspectAsync = (path, options) => {
161 | return new Promise((resolve, reject) => {
162 | let statOperation = fs.lstat;
163 | const opts = options || {};
164 |
165 | if (opts.symlinks === "follow") {
166 | statOperation = fs.stat;
167 | }
168 |
169 | statOperation(path)
170 | .then((stat) => {
171 | const inspectObj = createInspectObj(path, opts, stat);
172 | addExtraFieldsAsync(path, inspectObj, opts).then(resolve, reject);
173 | })
174 | .catch((err) => {
175 | // Detection if path exists
176 | if (err.code === "ENOENT") {
177 | // Doesn't exist. Return undefined instead of throwing.
178 | resolve(undefined);
179 | } else {
180 | reject(err);
181 | }
182 | });
183 | });
184 | };
185 |
186 | // ---------------------------------------------------------
187 | // API
188 | // ---------------------------------------------------------
189 |
190 | exports.supportedChecksumAlgorithms = supportedChecksumAlgorithms;
191 | exports.symlinkOptions = symlinkOptions;
192 | exports.validateInput = validateInput;
193 | exports.sync = inspectSync;
194 | exports.async = inspectAsync;
195 |
--------------------------------------------------------------------------------
/lib/inspect_tree.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | const crypto = require("crypto");
4 | const pathUtil = require("path");
5 | const inspect = require("./inspect");
6 | const list = require("./list");
7 | const validate = require("./utils/validate");
8 | const treeWalker = require("./utils/tree_walker");
9 |
10 | const validateInput = (methodName, path, options) => {
11 | const methodSignature = `${methodName}(path, [options])`;
12 | validate.argument(methodSignature, "path", path, ["string"]);
13 | validate.options(methodSignature, "options", options, {
14 | checksum: ["string"],
15 | relativePath: ["boolean"],
16 | times: ["boolean"],
17 | symlinks: ["string"],
18 | });
19 |
20 | if (
21 | options &&
22 | options.checksum !== undefined &&
23 | inspect.supportedChecksumAlgorithms.indexOf(options.checksum) === -1
24 | ) {
25 | throw new Error(
26 | `Argument "options.checksum" passed to ${methodSignature} must have one of values: ${inspect.supportedChecksumAlgorithms.join(
27 | ", "
28 | )}`
29 | );
30 | }
31 |
32 | if (
33 | options &&
34 | options.symlinks !== undefined &&
35 | inspect.symlinkOptions.indexOf(options.symlinks) === -1
36 | ) {
37 | throw new Error(
38 | `Argument "options.symlinks" passed to ${methodSignature} must have one of values: ${inspect.symlinkOptions.join(
39 | ", "
40 | )}`
41 | );
42 | }
43 | };
44 |
45 | const relativePathInTree = (parentInspectObj, inspectObj) => {
46 | if (parentInspectObj === undefined) {
47 | return ".";
48 | }
49 | return parentInspectObj.relativePath + "/" + inspectObj.name;
50 | };
51 |
52 | // Creates checksum of a directory by using
53 | // checksums and names of all its children.
54 | const checksumOfDir = (inspectList, algo) => {
55 | const hash = crypto.createHash(algo);
56 | inspectList.forEach((inspectObj) => {
57 | hash.update(inspectObj.name + inspectObj[algo]);
58 | });
59 | return hash.digest("hex");
60 | };
61 |
62 | const calculateTreeDependentProperties = (
63 | parentInspectObj,
64 | inspectObj,
65 | options
66 | ) => {
67 | if (options.relativePath) {
68 | inspectObj.relativePath = relativePathInTree(parentInspectObj, inspectObj);
69 | }
70 |
71 | if (inspectObj.type === "dir") {
72 | inspectObj.children.forEach((childInspectObj) => {
73 | calculateTreeDependentProperties(inspectObj, childInspectObj, options);
74 | });
75 |
76 | inspectObj.size = 0;
77 | inspectObj.children.sort((a, b) => {
78 | if (a.type === "dir" && b.type === "file") {
79 | return -1;
80 | }
81 | if (a.type === "file" && b.type === "dir") {
82 | return 1;
83 | }
84 | return a.name.localeCompare(b.name);
85 | });
86 | inspectObj.children.forEach((child) => {
87 | inspectObj.size += child.size || 0;
88 | });
89 |
90 | if (options.checksum) {
91 | inspectObj[options.checksum] = checksumOfDir(
92 | inspectObj.children,
93 | options.checksum
94 | );
95 | }
96 | }
97 | };
98 |
99 | const findParentInTree = (treeNode, pathChain, item) => {
100 | const name = pathChain[0];
101 | if (pathChain.length > 1) {
102 | const itemInTreeForPathChain = treeNode.children.find((child) => {
103 | return child.name === name;
104 | });
105 | return findParentInTree(itemInTreeForPathChain, pathChain.slice(1), item);
106 | }
107 | return treeNode;
108 | };
109 |
110 | // ---------------------------------------------------------
111 | // Sync
112 | // ---------------------------------------------------------
113 |
114 | const inspectTreeSync = (path, opts) => {
115 | const options = opts || {};
116 | let tree;
117 |
118 | treeWalker.sync(path, { inspectOptions: options }, (itemPath, item) => {
119 | if (item) {
120 | if (item.type === "dir") {
121 | item.children = [];
122 | }
123 | const relativePath = pathUtil.relative(path, itemPath);
124 | if (relativePath === "") {
125 | tree = item;
126 | } else {
127 | const parentItem = findParentInTree(
128 | tree,
129 | relativePath.split(pathUtil.sep),
130 | item
131 | );
132 | parentItem.children.push(item);
133 | }
134 | }
135 | });
136 |
137 | if (tree) {
138 | calculateTreeDependentProperties(undefined, tree, options);
139 | }
140 |
141 | return tree;
142 | };
143 |
144 | // ---------------------------------------------------------
145 | // Async
146 | // ---------------------------------------------------------
147 |
148 | const inspectTreeAsync = (path, opts) => {
149 | const options = opts || {};
150 | let tree;
151 |
152 | return new Promise((resolve, reject) => {
153 | treeWalker.async(
154 | path,
155 | { inspectOptions: options },
156 | (itemPath, item) => {
157 | if (item) {
158 | if (item.type === "dir") {
159 | item.children = [];
160 | }
161 | const relativePath = pathUtil.relative(path, itemPath);
162 | if (relativePath === "") {
163 | tree = item;
164 | } else {
165 | const parentItem = findParentInTree(
166 | tree,
167 | relativePath.split(pathUtil.sep),
168 | item
169 | );
170 | parentItem.children.push(item);
171 | }
172 | }
173 | },
174 | (err) => {
175 | if (err) {
176 | reject(err);
177 | } else {
178 | if (tree) {
179 | calculateTreeDependentProperties(undefined, tree, options);
180 | }
181 | resolve(tree);
182 | }
183 | }
184 | );
185 | });
186 | };
187 |
188 | // ---------------------------------------------------------
189 | // API
190 | // ---------------------------------------------------------
191 |
192 | exports.validateInput = validateInput;
193 | exports.sync = inspectTreeSync;
194 | exports.async = inspectTreeAsync;
195 |
--------------------------------------------------------------------------------
/lib/jetpack.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | const util = require("util");
4 | const pathUtil = require("path");
5 | const append = require("./append");
6 | const dir = require("./dir");
7 | const file = require("./file");
8 | const find = require("./find");
9 | const inspect = require("./inspect");
10 | const inspectTree = require("./inspect_tree");
11 | const copy = require("./copy");
12 | const exists = require("./exists");
13 | const list = require("./list");
14 | const move = require("./move");
15 | const read = require("./read");
16 | const remove = require("./remove");
17 | const rename = require("./rename");
18 | const symlink = require("./symlink");
19 | const streams = require("./streams");
20 | const tmpDir = require("./tmp_dir");
21 | const write = require("./write");
22 |
23 | // The Jetpack Context object.
24 | // It provides the public API, and resolves all paths regarding to
25 | // passed cwdPath, or default process.cwd() if cwdPath was not specified.
26 | const jetpackContext = (cwdPath) => {
27 | const getCwdPath = () => {
28 | return cwdPath || process.cwd();
29 | };
30 |
31 | const cwd = function () {
32 | // return current CWD if no arguments specified...
33 | if (arguments.length === 0) {
34 | return getCwdPath();
35 | }
36 |
37 | // ...create new CWD context otherwise
38 | const args = Array.prototype.slice.call(arguments);
39 | const pathParts = [getCwdPath()].concat(args);
40 | return jetpackContext(pathUtil.resolve.apply(null, pathParts));
41 | };
42 |
43 | // resolves path to inner CWD path of this jetpack instance
44 | const resolvePath = (path) => {
45 | return pathUtil.resolve(getCwdPath(), path);
46 | };
47 |
48 | const getPath = function () {
49 | // add CWD base path as first element of arguments array
50 | Array.prototype.unshift.call(arguments, getCwdPath());
51 | return pathUtil.resolve.apply(null, arguments);
52 | };
53 |
54 | const normalizeOptions = (options) => {
55 | const opts = options || {};
56 | opts.cwd = getCwdPath();
57 | return opts;
58 | };
59 |
60 | // API
61 |
62 | const api = {
63 | cwd,
64 | path: getPath,
65 |
66 | append: (path, data, options) => {
67 | append.validateInput("append", path, data, options);
68 | append.sync(resolvePath(path), data, options);
69 | },
70 | appendAsync: (path, data, options) => {
71 | append.validateInput("appendAsync", path, data, options);
72 | return append.async(resolvePath(path), data, options);
73 | },
74 |
75 | copy: (from, to, options) => {
76 | copy.validateInput("copy", from, to, options);
77 | copy.sync(resolvePath(from), resolvePath(to), options);
78 | },
79 | copyAsync: (from, to, options) => {
80 | copy.validateInput("copyAsync", from, to, options);
81 | return copy.async(resolvePath(from), resolvePath(to), options);
82 | },
83 |
84 | createWriteStream: (path, options) => {
85 | return streams.createWriteStream(resolvePath(path), options);
86 | },
87 | createReadStream: (path, options) => {
88 | return streams.createReadStream(resolvePath(path), options);
89 | },
90 |
91 | dir: (path, criteria) => {
92 | dir.validateInput("dir", path, criteria);
93 | const normalizedPath = resolvePath(path);
94 | dir.sync(normalizedPath, criteria);
95 | return cwd(normalizedPath);
96 | },
97 | dirAsync: (path, criteria) => {
98 | dir.validateInput("dirAsync", path, criteria);
99 | return new Promise((resolve, reject) => {
100 | const normalizedPath = resolvePath(path);
101 | dir.async(normalizedPath, criteria).then(() => {
102 | resolve(cwd(normalizedPath));
103 | }, reject);
104 | });
105 | },
106 |
107 | exists: (path) => {
108 | exists.validateInput("exists", path);
109 | return exists.sync(resolvePath(path));
110 | },
111 | existsAsync: (path) => {
112 | exists.validateInput("existsAsync", path);
113 | return exists.async(resolvePath(path));
114 | },
115 |
116 | file: (path, criteria) => {
117 | file.validateInput("file", path, criteria);
118 | file.sync(resolvePath(path), criteria);
119 | return api;
120 | },
121 | fileAsync: (path, criteria) => {
122 | file.validateInput("fileAsync", path, criteria);
123 | return new Promise((resolve, reject) => {
124 | file.async(resolvePath(path), criteria).then(() => {
125 | resolve(api);
126 | }, reject);
127 | });
128 | },
129 |
130 | find: (startPath, options) => {
131 | // startPath is optional parameter, if not specified move rest of params
132 | // to proper places and default startPath to CWD.
133 | if (typeof options === "undefined" && typeof startPath === "object") {
134 | options = startPath;
135 | startPath = ".";
136 | }
137 | find.validateInput("find", startPath, options);
138 | return find.sync(resolvePath(startPath), normalizeOptions(options));
139 | },
140 | findAsync: (startPath, options) => {
141 | // startPath is optional parameter, if not specified move rest of params
142 | // to proper places and default startPath to CWD.
143 | if (typeof options === "undefined" && typeof startPath === "object") {
144 | options = startPath;
145 | startPath = ".";
146 | }
147 | find.validateInput("findAsync", startPath, options);
148 | return find.async(resolvePath(startPath), normalizeOptions(options));
149 | },
150 |
151 | inspect: (path, fieldsToInclude) => {
152 | inspect.validateInput("inspect", path, fieldsToInclude);
153 | return inspect.sync(resolvePath(path), fieldsToInclude);
154 | },
155 | inspectAsync: (path, fieldsToInclude) => {
156 | inspect.validateInput("inspectAsync", path, fieldsToInclude);
157 | return inspect.async(resolvePath(path), fieldsToInclude);
158 | },
159 |
160 | inspectTree: (path, options) => {
161 | inspectTree.validateInput("inspectTree", path, options);
162 | return inspectTree.sync(resolvePath(path), options);
163 | },
164 | inspectTreeAsync: (path, options) => {
165 | inspectTree.validateInput("inspectTreeAsync", path, options);
166 | return inspectTree.async(resolvePath(path), options);
167 | },
168 |
169 | list: (path) => {
170 | list.validateInput("list", path);
171 | return list.sync(resolvePath(path || "."));
172 | },
173 | listAsync: (path) => {
174 | list.validateInput("listAsync", path);
175 | return list.async(resolvePath(path || "."));
176 | },
177 |
178 | move: (from, to, options) => {
179 | move.validateInput("move", from, to, options);
180 | move.sync(resolvePath(from), resolvePath(to), options);
181 | },
182 | moveAsync: (from, to, options) => {
183 | move.validateInput("moveAsync", from, to, options);
184 | return move.async(resolvePath(from), resolvePath(to), options);
185 | },
186 |
187 | read: (path, returnAs) => {
188 | read.validateInput("read", path, returnAs);
189 | return read.sync(resolvePath(path), returnAs);
190 | },
191 | readAsync: (path, returnAs) => {
192 | read.validateInput("readAsync", path, returnAs);
193 | return read.async(resolvePath(path), returnAs);
194 | },
195 |
196 | remove: (path) => {
197 | remove.validateInput("remove", path);
198 | // If path not specified defaults to CWD
199 | remove.sync(resolvePath(path || "."));
200 | },
201 | removeAsync: (path) => {
202 | remove.validateInput("removeAsync", path);
203 | // If path not specified defaults to CWD
204 | return remove.async(resolvePath(path || "."));
205 | },
206 |
207 | rename: (path, newName, options) => {
208 | rename.validateInput("rename", path, newName, options);
209 | rename.sync(resolvePath(path), newName, options);
210 | },
211 | renameAsync: (path, newName, options) => {
212 | rename.validateInput("renameAsync", path, newName, options);
213 | return rename.async(resolvePath(path), newName, options);
214 | },
215 |
216 | symlink: (symlinkValue, path) => {
217 | symlink.validateInput("symlink", symlinkValue, path);
218 | symlink.sync(symlinkValue, resolvePath(path));
219 | },
220 | symlinkAsync: (symlinkValue, path) => {
221 | symlink.validateInput("symlinkAsync", symlinkValue, path);
222 | return symlink.async(symlinkValue, resolvePath(path));
223 | },
224 |
225 | tmpDir: (options) => {
226 | tmpDir.validateInput("tmpDir", options);
227 | const pathOfCreatedDirectory = tmpDir.sync(getCwdPath(), options);
228 | return cwd(pathOfCreatedDirectory);
229 | },
230 | tmpDirAsync: (options) => {
231 | tmpDir.validateInput("tmpDirAsync", options);
232 | return new Promise((resolve, reject) => {
233 | tmpDir.async(getCwdPath(), options).then((pathOfCreatedDirectory) => {
234 | resolve(cwd(pathOfCreatedDirectory));
235 | }, reject);
236 | });
237 | },
238 |
239 | write: (path, data, options) => {
240 | write.validateInput("write", path, data, options);
241 | write.sync(resolvePath(path), data, options);
242 | },
243 | writeAsync: (path, data, options) => {
244 | write.validateInput("writeAsync", path, data, options);
245 | return write.async(resolvePath(path), data, options);
246 | },
247 | };
248 |
249 | if (util.inspect.custom !== undefined) {
250 | // Without this console.log(jetpack) throws obscure error. Details:
251 | // https://github.com/szwacz/fs-jetpack/issues/29
252 | // https://nodejs.org/api/util.html#util_custom_inspection_functions_on_objects
253 | api[util.inspect.custom] = () => {
254 | return `[fs-jetpack CWD: ${getCwdPath()}]`;
255 | };
256 | }
257 |
258 | return api;
259 | };
260 |
261 | module.exports = jetpackContext;
262 |
--------------------------------------------------------------------------------
/lib/list.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | const fs = require("./utils/fs");
4 | const validate = require("./utils/validate");
5 |
6 | const validateInput = (methodName, path) => {
7 | const methodSignature = `${methodName}(path)`;
8 | validate.argument(methodSignature, "path", path, ["string", "undefined"]);
9 | };
10 |
11 | // ---------------------------------------------------------
12 | // Sync
13 | // ---------------------------------------------------------
14 |
15 | const listSync = (path) => {
16 | try {
17 | return fs.readdirSync(path);
18 | } catch (err) {
19 | if (err.code === "ENOENT") {
20 | // Doesn't exist. Return undefined instead of throwing.
21 | return undefined;
22 | }
23 | throw err;
24 | }
25 | };
26 |
27 | // ---------------------------------------------------------
28 | // Async
29 | // ---------------------------------------------------------
30 |
31 | const listAsync = (path) => {
32 | return new Promise((resolve, reject) => {
33 | fs.readdir(path)
34 | .then((list) => {
35 | resolve(list);
36 | })
37 | .catch((err) => {
38 | if (err.code === "ENOENT") {
39 | // Doesn't exist. Return undefined instead of throwing.
40 | resolve(undefined);
41 | } else {
42 | reject(err);
43 | }
44 | });
45 | });
46 | };
47 |
48 | // ---------------------------------------------------------
49 | // API
50 | // ---------------------------------------------------------
51 |
52 | exports.validateInput = validateInput;
53 | exports.sync = listSync;
54 | exports.async = listAsync;
55 |
--------------------------------------------------------------------------------
/lib/move.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | const pathUtil = require("path");
4 | const fs = require("./utils/fs");
5 | const validate = require("./utils/validate");
6 | const copy = require("./copy");
7 | const dir = require("./dir");
8 | const exists = require("./exists");
9 | const remove = require("./remove");
10 |
11 | const validateInput = (methodName, from, to, options) => {
12 | const methodSignature = `${methodName}(from, to, [options])`;
13 | validate.argument(methodSignature, "from", from, ["string"]);
14 | validate.argument(methodSignature, "to", to, ["string"]);
15 | validate.options(methodSignature, "options", options, {
16 | overwrite: ["boolean"],
17 | });
18 | };
19 |
20 | const parseOptions = (options) => {
21 | const opts = options || {};
22 | return opts;
23 | };
24 |
25 | const generateDestinationExistsError = (path) => {
26 | const err = new Error(`Destination path already exists ${path}`);
27 | err.code = "EEXIST";
28 | return err;
29 | };
30 |
31 | const generateSourceDoesntExistError = (path) => {
32 | const err = new Error(`Path to move doesn't exist ${path}`);
33 | err.code = "ENOENT";
34 | return err;
35 | };
36 |
37 | // ---------------------------------------------------------
38 | // Sync
39 | // ---------------------------------------------------------
40 |
41 | const moveSync = (from, to, options) => {
42 | const opts = parseOptions(options);
43 |
44 | if (exists.sync(to) !== false && opts.overwrite !== true) {
45 | throw generateDestinationExistsError(to);
46 | }
47 |
48 | // We now have permission to overwrite, since either `opts.overwrite` is true
49 | // or the destination does not exist (in which overwriting is irrelevant).
50 |
51 | try {
52 | // If destination is a file, `fs.renameSync` will overwrite it.
53 | fs.renameSync(from, to);
54 | } catch (err) {
55 | if (err.code === "EISDIR" || err.code === "EPERM") {
56 | // Looks like the destination path is a directory in the same device,
57 | // so we can remove it and call `fs.renameSync` again.
58 | remove.sync(to);
59 | fs.renameSync(from, to);
60 | } else if (err.code === "EXDEV") {
61 | // The destination path is in another device.
62 | copy.sync(from, to, { overwrite: true });
63 | remove.sync(from);
64 | } else if (err.code === "ENOENT") {
65 | // This can be caused by either the source not existing or one or more folders
66 | // in the destination path not existing.
67 | if (!exists.sync(from)) {
68 | throw generateSourceDoesntExistError(from);
69 | }
70 |
71 | // One or more directories in the destination path don't exist.
72 | dir.createSync(pathUtil.dirname(to));
73 | // Retry the attempt
74 | fs.renameSync(from, to);
75 | } else {
76 | // We can't make sense of this error. Rethrow it.
77 | throw err;
78 | }
79 | }
80 | };
81 |
82 | // ---------------------------------------------------------
83 | // Async
84 | // ---------------------------------------------------------
85 |
86 | const ensureDestinationPathExistsAsync = (to) => {
87 | return new Promise((resolve, reject) => {
88 | const destDir = pathUtil.dirname(to);
89 | exists
90 | .async(destDir)
91 | .then((dstExists) => {
92 | if (!dstExists) {
93 | dir.createAsync(destDir).then(resolve, reject);
94 | } else {
95 | // Hah, no idea.
96 | reject();
97 | }
98 | })
99 | .catch(reject);
100 | });
101 | };
102 |
103 | const moveAsync = (from, to, options) => {
104 | const opts = parseOptions(options);
105 |
106 | return new Promise((resolve, reject) => {
107 | exists.async(to).then((destinationExists) => {
108 | if (destinationExists !== false && opts.overwrite !== true) {
109 | reject(generateDestinationExistsError(to));
110 | } else {
111 | // We now have permission to overwrite, since either `opts.overwrite` is true
112 | // or the destination does not exist (in which overwriting is irrelevant).
113 | // If destination is a file, `fs.rename` will overwrite it.
114 | fs.rename(from, to)
115 | .then(resolve)
116 | .catch((err) => {
117 | if (err.code === "EISDIR" || err.code === "EPERM") {
118 | // Looks like the destination path is a directory in the same device,
119 | // so we can remove it and call `fs.rename` again.
120 | remove
121 | .async(to)
122 | .then(() => fs.rename(from, to))
123 | .then(resolve, reject);
124 | } else if (err.code === "EXDEV") {
125 | // The destination path is in another device.
126 | copy
127 | .async(from, to, { overwrite: true })
128 | .then(() => remove.async(from))
129 | .then(resolve, reject);
130 | } else if (err.code === "ENOENT") {
131 | // This can be caused by either the source not existing or one or more folders
132 | // in the destination path not existing.
133 | exists
134 | .async(from)
135 | .then((srcExists) => {
136 | if (!srcExists) {
137 | reject(generateSourceDoesntExistError(from));
138 | } else {
139 | // One or more directories in the destination path don't exist.
140 | ensureDestinationPathExistsAsync(to)
141 | .then(() => {
142 | // Retry the attempt
143 | return fs.rename(from, to);
144 | })
145 | .then(resolve, reject);
146 | }
147 | })
148 | .catch(reject);
149 | } else {
150 | // Something unknown. Rethrow original error.
151 | reject(err);
152 | }
153 | });
154 | }
155 | });
156 | });
157 | };
158 |
159 | // ---------------------------------------------------------
160 | // API
161 | // ---------------------------------------------------------
162 |
163 | exports.validateInput = validateInput;
164 | exports.sync = moveSync;
165 | exports.async = moveAsync;
166 |
--------------------------------------------------------------------------------
/lib/read.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | const fs = require("./utils/fs");
4 | const validate = require("./utils/validate");
5 |
6 | const supportedReturnAs = ["utf8", "buffer", "json", "jsonWithDates"];
7 |
8 | const validateInput = (methodName, path, returnAs) => {
9 | const methodSignature = `${methodName}(path, returnAs)`;
10 | validate.argument(methodSignature, "path", path, ["string"]);
11 | validate.argument(methodSignature, "returnAs", returnAs, [
12 | "string",
13 | "undefined",
14 | ]);
15 |
16 | if (returnAs && supportedReturnAs.indexOf(returnAs) === -1) {
17 | throw new Error(
18 | `Argument "returnAs" passed to ${methodSignature} must have one of values: ${supportedReturnAs.join(
19 | ", "
20 | )}`
21 | );
22 | }
23 | };
24 |
25 | // Matches strings generated by Date.toJSON()
26 | // which is called to serialize date to JSON.
27 | const jsonDateParser = (key, value) => {
28 | const reISO =
29 | /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2}(?:\.\d*))(?:Z|(\+|-)([\d|:]*))?$/;
30 | if (typeof value === "string") {
31 | if (reISO.exec(value)) {
32 | return new Date(value);
33 | }
34 | }
35 | return value;
36 | };
37 |
38 | const makeNicerJsonParsingError = (path, err) => {
39 | const nicerError = new Error(
40 | `JSON parsing failed while reading ${path} [${err}]`
41 | );
42 | nicerError.originalError = err;
43 | return nicerError;
44 | };
45 |
46 | // ---------------------------------------------------------
47 | // SYNC
48 | // ---------------------------------------------------------
49 |
50 | const readSync = (path, returnAs) => {
51 | const retAs = returnAs || "utf8";
52 | let data;
53 |
54 | let encoding = "utf8";
55 | if (retAs === "buffer") {
56 | encoding = null;
57 | }
58 |
59 | try {
60 | data = fs.readFileSync(path, { encoding });
61 | } catch (err) {
62 | if (err.code === "ENOENT") {
63 | // If file doesn't exist return undefined instead of throwing.
64 | return undefined;
65 | }
66 | // Otherwise rethrow the error
67 | throw err;
68 | }
69 |
70 | try {
71 | if (retAs === "json") {
72 | data = JSON.parse(data);
73 | } else if (retAs === "jsonWithDates") {
74 | data = JSON.parse(data, jsonDateParser);
75 | }
76 | } catch (err) {
77 | throw makeNicerJsonParsingError(path, err);
78 | }
79 |
80 | return data;
81 | };
82 |
83 | // ---------------------------------------------------------
84 | // ASYNC
85 | // ---------------------------------------------------------
86 |
87 | const readAsync = (path, returnAs) => {
88 | return new Promise((resolve, reject) => {
89 | const retAs = returnAs || "utf8";
90 | let encoding = "utf8";
91 | if (retAs === "buffer") {
92 | encoding = null;
93 | }
94 |
95 | fs.readFile(path, { encoding })
96 | .then((data) => {
97 | // Make final parsing of the data before returning.
98 | try {
99 | if (retAs === "json") {
100 | resolve(JSON.parse(data));
101 | } else if (retAs === "jsonWithDates") {
102 | resolve(JSON.parse(data, jsonDateParser));
103 | } else {
104 | resolve(data);
105 | }
106 | } catch (err) {
107 | reject(makeNicerJsonParsingError(path, err));
108 | }
109 | })
110 | .catch((err) => {
111 | if (err.code === "ENOENT") {
112 | // If file doesn't exist return undefined instead of throwing.
113 | resolve(undefined);
114 | } else {
115 | // Otherwise throw
116 | reject(err);
117 | }
118 | });
119 | });
120 | };
121 |
122 | // ---------------------------------------------------------
123 | // API
124 | // ---------------------------------------------------------
125 |
126 | exports.validateInput = validateInput;
127 | exports.sync = readSync;
128 | exports.async = readAsync;
129 |
--------------------------------------------------------------------------------
/lib/remove.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | const fs = require("./utils/fs");
4 | const validate = require("./utils/validate");
5 |
6 | const validateInput = (methodName, path) => {
7 | const methodSignature = `${methodName}([path])`;
8 | validate.argument(methodSignature, "path", path, ["string", "undefined"]);
9 | };
10 |
11 | // ---------------------------------------------------------
12 | // Sync
13 | // ---------------------------------------------------------
14 |
15 | const removeSync = (path) => {
16 | fs.rmSync(path, {
17 | recursive: true,
18 | force: true,
19 | maxRetries: 3,
20 | });
21 | };
22 |
23 | // ---------------------------------------------------------
24 | // Async
25 | // ---------------------------------------------------------
26 |
27 | const removeAsync = (path) => {
28 | return fs.rm(path, {
29 | recursive: true,
30 | force: true,
31 | maxRetries: 3,
32 | });
33 | };
34 |
35 | // ---------------------------------------------------------
36 | // API
37 | // ---------------------------------------------------------
38 |
39 | exports.validateInput = validateInput;
40 | exports.sync = removeSync;
41 | exports.async = removeAsync;
42 |
--------------------------------------------------------------------------------
/lib/rename.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | const pathUtil = require("path");
4 | const move = require("./move");
5 | const validate = require("./utils/validate");
6 |
7 | const validateInput = (methodName, path, newName, options) => {
8 | const methodSignature = `${methodName}(path, newName, [options])`;
9 | validate.argument(methodSignature, "path", path, ["string"]);
10 | validate.argument(methodSignature, "newName", newName, ["string"]);
11 | validate.options(methodSignature, "options", options, {
12 | overwrite: ["boolean"],
13 | });
14 |
15 | if (pathUtil.basename(newName) !== newName) {
16 | throw new Error(
17 | `Argument "newName" passed to ${methodSignature} should be a filename, not a path. Received "${newName}"`
18 | );
19 | }
20 | };
21 |
22 | // ---------------------------------------------------------
23 | // Sync
24 | // ---------------------------------------------------------
25 |
26 | const renameSync = (path, newName, options) => {
27 | const newPath = pathUtil.join(pathUtil.dirname(path), newName);
28 | move.sync(path, newPath, options);
29 | };
30 |
31 | // ---------------------------------------------------------
32 | // Async
33 | // ---------------------------------------------------------
34 |
35 | const renameAsync = (path, newName, options) => {
36 | const newPath = pathUtil.join(pathUtil.dirname(path), newName);
37 | return move.async(path, newPath, options);
38 | };
39 |
40 | // ---------------------------------------------------------
41 | // API
42 | // ---------------------------------------------------------
43 |
44 | exports.validateInput = validateInput;
45 | exports.sync = renameSync;
46 | exports.async = renameAsync;
47 |
--------------------------------------------------------------------------------
/lib/streams.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | const fs = require("fs");
4 |
5 | exports.createWriteStream = fs.createWriteStream;
6 | exports.createReadStream = fs.createReadStream;
7 |
--------------------------------------------------------------------------------
/lib/symlink.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | const pathUtil = require("path");
4 | const fs = require("./utils/fs");
5 | const validate = require("./utils/validate");
6 | const dir = require("./dir");
7 |
8 | const validateInput = (methodName, symlinkValue, path) => {
9 | const methodSignature = `${methodName}(symlinkValue, path)`;
10 | validate.argument(methodSignature, "symlinkValue", symlinkValue, ["string"]);
11 | validate.argument(methodSignature, "path", path, ["string"]);
12 | };
13 |
14 | // ---------------------------------------------------------
15 | // Sync
16 | // ---------------------------------------------------------
17 |
18 | const symlinkSync = (symlinkValue, path) => {
19 | try {
20 | fs.symlinkSync(symlinkValue, path);
21 | } catch (err) {
22 | if (err.code === "ENOENT") {
23 | // Parent directories don't exist. Just create them and retry.
24 | dir.createSync(pathUtil.dirname(path));
25 | fs.symlinkSync(symlinkValue, path);
26 | } else {
27 | throw err;
28 | }
29 | }
30 | };
31 |
32 | // ---------------------------------------------------------
33 | // Async
34 | // ---------------------------------------------------------
35 |
36 | const symlinkAsync = (symlinkValue, path) => {
37 | return new Promise((resolve, reject) => {
38 | fs.symlink(symlinkValue, path)
39 | .then(resolve)
40 | .catch((err) => {
41 | if (err.code === "ENOENT") {
42 | // Parent directories don't exist. Just create them and retry.
43 | dir
44 | .createAsync(pathUtil.dirname(path))
45 | .then(() => {
46 | return fs.symlink(symlinkValue, path);
47 | })
48 | .then(resolve, reject);
49 | } else {
50 | reject(err);
51 | }
52 | });
53 | });
54 | };
55 |
56 | // ---------------------------------------------------------
57 | // API
58 | // ---------------------------------------------------------
59 |
60 | exports.validateInput = validateInput;
61 | exports.sync = symlinkSync;
62 | exports.async = symlinkAsync;
63 |
--------------------------------------------------------------------------------
/lib/tmp_dir.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | const pathUtil = require("path");
4 | const os = require("os");
5 | const crypto = require("crypto");
6 | const dir = require("./dir");
7 | const fs = require("./utils/fs");
8 | const validate = require("./utils/validate");
9 |
10 | const validateInput = (methodName, options) => {
11 | const methodSignature = `${methodName}([options])`;
12 | validate.options(methodSignature, "options", options, {
13 | prefix: ["string"],
14 | basePath: ["string"],
15 | });
16 | };
17 |
18 | const getOptionsDefaults = (passedOptions, cwdPath) => {
19 | passedOptions = passedOptions || {};
20 | const options = {};
21 | if (typeof passedOptions.prefix !== "string") {
22 | options.prefix = "";
23 | } else {
24 | options.prefix = passedOptions.prefix;
25 | }
26 | if (typeof passedOptions.basePath === "string") {
27 | options.basePath = pathUtil.resolve(cwdPath, passedOptions.basePath);
28 | } else {
29 | options.basePath = os.tmpdir();
30 | }
31 | return options;
32 | };
33 |
34 | const randomStringLength = 32;
35 |
36 | // ---------------------------------------------------------
37 | // Sync
38 | // ---------------------------------------------------------
39 |
40 | const tmpDirSync = (cwdPath, passedOptions) => {
41 | const options = getOptionsDefaults(passedOptions, cwdPath);
42 | const randomString = crypto
43 | .randomBytes(randomStringLength / 2)
44 | .toString("hex");
45 | const dirPath = pathUtil.join(
46 | options.basePath,
47 | options.prefix + randomString
48 | );
49 | // Let's assume everything will go well, do the directory fastest way possible
50 | try {
51 | fs.mkdirSync(dirPath);
52 | } catch (err) {
53 | // Something went wrong, try to recover by using more sophisticated approach
54 | if (err.code === "ENOENT") {
55 | dir.sync(dirPath);
56 | } else {
57 | throw err;
58 | }
59 | }
60 | return dirPath;
61 | };
62 |
63 | // ---------------------------------------------------------
64 | // Async
65 | // ---------------------------------------------------------
66 |
67 | const tmpDirAsync = (cwdPath, passedOptions) => {
68 | return new Promise((resolve, reject) => {
69 | const options = getOptionsDefaults(passedOptions, cwdPath);
70 | crypto.randomBytes(randomStringLength / 2, (err, bytes) => {
71 | if (err) {
72 | reject(err);
73 | } else {
74 | const randomString = bytes.toString("hex");
75 | const dirPath = pathUtil.join(
76 | options.basePath,
77 | options.prefix + randomString
78 | );
79 | // Let's assume everything will go well, do the directory fastest way possible
80 | fs.mkdir(dirPath, (err) => {
81 | if (err) {
82 | // Something went wrong, try to recover by using more sophisticated approach
83 | if (err.code === "ENOENT") {
84 | dir.async(dirPath).then(() => {
85 | resolve(dirPath);
86 | }, reject);
87 | } else {
88 | reject(err);
89 | }
90 | } else {
91 | resolve(dirPath);
92 | }
93 | });
94 | }
95 | });
96 | });
97 | };
98 |
99 | // ---------------------------------------------------------
100 | // API
101 | // ---------------------------------------------------------
102 |
103 | exports.validateInput = validateInput;
104 | exports.sync = tmpDirSync;
105 | exports.async = tmpDirAsync;
106 |
--------------------------------------------------------------------------------
/lib/utils/fs.js:
--------------------------------------------------------------------------------
1 | // Adater module exposing all `fs` methods with promises instead of callbacks.
2 |
3 | "use strict";
4 |
5 | const fs = require("fs");
6 | const promisify = require("./promisify");
7 |
8 | const isCallbackMethod = (key) => {
9 | return [
10 | typeof fs[key] === "function",
11 | !key.match(/Sync$/),
12 | !key.match(/^[A-Z]/),
13 | !key.match(/^create/),
14 | !key.match(/^(un)?watch/),
15 | ].every(Boolean);
16 | };
17 |
18 | const adaptMethod = (name) => {
19 | const original = fs[name];
20 | return promisify(original);
21 | };
22 |
23 | const adaptAllMethods = () => {
24 | const adapted = {};
25 |
26 | Object.keys(fs).forEach((key) => {
27 | if (isCallbackMethod(key)) {
28 | if (key === "exists") {
29 | // fs.exists() does not follow standard
30 | // Node callback conventions, and has
31 | // no error object in the callback
32 | adapted.exists = () => {
33 | throw new Error("fs.exists() is deprecated");
34 | };
35 | } else {
36 | adapted[key] = adaptMethod(key);
37 | }
38 | } else {
39 | adapted[key] = fs[key];
40 | }
41 | });
42 |
43 | return adapted;
44 | };
45 |
46 | module.exports = adaptAllMethods();
47 |
--------------------------------------------------------------------------------
/lib/utils/matcher.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | const Minimatch = require("minimatch").Minimatch;
4 |
5 | const convertPatternToAbsolutePath = (basePath, pattern) => {
6 | // All patterns without slash are left as they are, if pattern contain
7 | // any slash we need to turn it into absolute path.
8 | const hasSlash = pattern.indexOf("/") !== -1;
9 | const isAbsolute = /^!?\//.test(pattern);
10 | const isNegated = /^!/.test(pattern);
11 | let separator;
12 |
13 | if (!isAbsolute && hasSlash) {
14 | // Throw out meaningful characters from the beginning ("!", "./").
15 | const patternWithoutFirstCharacters = pattern
16 | .replace(/^!/, "")
17 | .replace(/^\.\//, "");
18 |
19 | if (/\/$/.test(basePath)) {
20 | separator = "";
21 | } else {
22 | separator = "/";
23 | }
24 |
25 | if (isNegated) {
26 | return `!${basePath}${separator}${patternWithoutFirstCharacters}`;
27 | }
28 | return `${basePath}${separator}${patternWithoutFirstCharacters}`;
29 | }
30 |
31 | return pattern;
32 | };
33 |
34 | exports.create = (basePath, patterns, ignoreCase) => {
35 | let normalizedPatterns;
36 |
37 | if (typeof patterns === "string") {
38 | normalizedPatterns = [patterns];
39 | } else {
40 | normalizedPatterns = patterns;
41 | }
42 |
43 | const matchers = normalizedPatterns
44 | .map((pattern) => {
45 | return convertPatternToAbsolutePath(basePath, pattern);
46 | })
47 | .map((pattern) => {
48 | return new Minimatch(pattern, {
49 | matchBase: true,
50 | nocomment: true,
51 | nocase: ignoreCase || false,
52 | dot: true,
53 | windowsPathsNoEscape: true,
54 | });
55 | });
56 |
57 | const performMatch = (absolutePath) => {
58 | let mode = "matching";
59 | let weHaveMatch = false;
60 | let currentMatcher;
61 | let i;
62 |
63 | for (i = 0; i < matchers.length; i += 1) {
64 | currentMatcher = matchers[i];
65 |
66 | if (currentMatcher.negate) {
67 | mode = "negation";
68 | if (i === 0) {
69 | // There are only negated patterns in the set,
70 | // so make everything matching by default and
71 | // start to reject stuff.
72 | weHaveMatch = true;
73 | }
74 | }
75 |
76 | if (
77 | mode === "negation" &&
78 | weHaveMatch &&
79 | !currentMatcher.match(absolutePath)
80 | ) {
81 | // One negation match is enought to know we can reject this one.
82 | return false;
83 | }
84 |
85 | if (mode === "matching" && !weHaveMatch) {
86 | weHaveMatch = currentMatcher.match(absolutePath);
87 | }
88 | }
89 |
90 | return weHaveMatch;
91 | };
92 |
93 | return performMatch;
94 | };
95 |
--------------------------------------------------------------------------------
/lib/utils/mode.js:
--------------------------------------------------------------------------------
1 | // Logic for unix file mode operations.
2 |
3 | "use strict";
4 |
5 | // Converts mode to string 3 characters long.
6 | exports.normalizeFileMode = (mode) => {
7 | let modeAsString;
8 | if (typeof mode === "number") {
9 | modeAsString = mode.toString(8);
10 | } else {
11 | modeAsString = mode;
12 | }
13 | return modeAsString.substring(modeAsString.length - 3);
14 | };
15 |
--------------------------------------------------------------------------------
/lib/utils/promisify.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | module.exports = (fn) => {
4 | return function () {
5 | const length = arguments.length;
6 | const args = new Array(length);
7 |
8 | for (let i = 0; i < length; i += 1) {
9 | args[i] = arguments[i];
10 | }
11 |
12 | return new Promise((resolve, reject) => {
13 | args.push((err, data) => {
14 | if (err) {
15 | reject(err);
16 | } else {
17 | resolve(data);
18 | }
19 | });
20 |
21 | fn.apply(null, args);
22 | });
23 | };
24 | };
25 |
--------------------------------------------------------------------------------
/lib/utils/tree_walker.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | const fs = require("fs");
4 | const pathUtil = require("path");
5 | const inspect = require("../inspect");
6 | const list = require("../list");
7 |
8 | const fileType = (dirent) => {
9 | if (dirent.isDirectory()) {
10 | return "dir";
11 | }
12 | if (dirent.isFile()) {
13 | return "file";
14 | }
15 | if (dirent.isSymbolicLink()) {
16 | return "symlink";
17 | }
18 | return "other";
19 | };
20 |
21 | // ---------------------------------------------------------
22 | // SYNC
23 | // ---------------------------------------------------------
24 |
25 | const initialWalkSync = (path, options, callback) => {
26 | if (options.maxLevelsDeep === undefined) {
27 | options.maxLevelsDeep = Infinity;
28 | }
29 | const performInspectOnEachNode = options.inspectOptions !== undefined;
30 | if (options.symlinks) {
31 | if (options.inspectOptions === undefined) {
32 | options.inspectOptions = { symlinks: options.symlinks };
33 | } else {
34 | options.inspectOptions.symlinks = options.symlinks;
35 | }
36 | }
37 |
38 | const walkSync = (path, currentLevel) => {
39 | fs.readdirSync(path, { withFileTypes: true }).forEach((direntItem) => {
40 | const withFileTypesNotSupported = typeof direntItem === "string";
41 |
42 | let fileItemPath;
43 | if (withFileTypesNotSupported) {
44 | fileItemPath = pathUtil.join(path, direntItem);
45 | } else {
46 | fileItemPath = pathUtil.join(path, direntItem.name);
47 | }
48 |
49 | let fileItem;
50 | if (performInspectOnEachNode) {
51 | fileItem = inspect.sync(fileItemPath, options.inspectOptions);
52 | } else if (withFileTypesNotSupported) {
53 | // New "withFileTypes" API not supported, need to do extra inspect
54 | // on each node, to know if this is a directory or a file.
55 | const inspectObject = inspect.sync(
56 | fileItemPath,
57 | options.inspectOptions
58 | );
59 | fileItem = { name: inspectObject.name, type: inspectObject.type };
60 | } else {
61 | const type = fileType(direntItem);
62 | if (type === "symlink" && options.symlinks === "follow") {
63 | const symlinkPointsTo = fs.statSync(fileItemPath);
64 | fileItem = { name: direntItem.name, type: fileType(symlinkPointsTo) };
65 | } else {
66 | fileItem = { name: direntItem.name, type };
67 | }
68 | }
69 |
70 | if (fileItem !== undefined) {
71 | callback(fileItemPath, fileItem);
72 | if (fileItem.type === "dir" && currentLevel < options.maxLevelsDeep) {
73 | walkSync(fileItemPath, currentLevel + 1);
74 | }
75 | }
76 | });
77 | };
78 |
79 | const item = inspect.sync(path, options.inspectOptions);
80 | if (item) {
81 | if (performInspectOnEachNode) {
82 | callback(path, item);
83 | } else {
84 | // Return simplified object, not full inspect object
85 | callback(path, { name: item.name, type: item.type });
86 | }
87 | if (item.type === "dir") {
88 | walkSync(path, 1);
89 | }
90 | } else {
91 | callback(path, undefined);
92 | }
93 | };
94 |
95 | // ---------------------------------------------------------
96 | // ASYNC
97 | // ---------------------------------------------------------
98 |
99 | const maxConcurrentOperations = 5;
100 |
101 | const initialWalkAsync = (path, options, callback, doneCallback) => {
102 | if (options.maxLevelsDeep === undefined) {
103 | options.maxLevelsDeep = Infinity;
104 | }
105 | const performInspectOnEachNode = options.inspectOptions !== undefined;
106 | if (options.symlinks) {
107 | if (options.inspectOptions === undefined) {
108 | options.inspectOptions = { symlinks: options.symlinks };
109 | } else {
110 | options.inspectOptions.symlinks = options.symlinks;
111 | }
112 | }
113 |
114 | const concurrentOperationsQueue = [];
115 | let nowDoingConcurrentOperations = 0;
116 |
117 | const checkConcurrentOperations = () => {
118 | if (
119 | concurrentOperationsQueue.length === 0 &&
120 | nowDoingConcurrentOperations === 0
121 | ) {
122 | doneCallback();
123 | } else if (
124 | concurrentOperationsQueue.length > 0 &&
125 | nowDoingConcurrentOperations < maxConcurrentOperations
126 | ) {
127 | const operation = concurrentOperationsQueue.pop();
128 | nowDoingConcurrentOperations += 1;
129 | operation();
130 | }
131 | };
132 |
133 | const whenConcurrencySlotAvailable = (operation) => {
134 | concurrentOperationsQueue.push(operation);
135 | checkConcurrentOperations();
136 | };
137 |
138 | const concurrentOperationDone = () => {
139 | nowDoingConcurrentOperations -= 1;
140 | checkConcurrentOperations();
141 | };
142 |
143 | const walkAsync = (path, currentLevel) => {
144 | const goDeeperIfDir = (fileItemPath, fileItem) => {
145 | if (fileItem.type === "dir" && currentLevel < options.maxLevelsDeep) {
146 | walkAsync(fileItemPath, currentLevel + 1);
147 | }
148 | };
149 |
150 | whenConcurrencySlotAvailable(() => {
151 | fs.readdir(path, { withFileTypes: true }, (err, files) => {
152 | if (err) {
153 | doneCallback(err);
154 | } else {
155 | files.forEach((direntItem) => {
156 | const withFileTypesNotSupported = typeof direntItem === "string";
157 |
158 | let fileItemPath;
159 | if (withFileTypesNotSupported) {
160 | fileItemPath = pathUtil.join(path, direntItem);
161 | } else {
162 | fileItemPath = pathUtil.join(path, direntItem.name);
163 | }
164 |
165 | if (performInspectOnEachNode || withFileTypesNotSupported) {
166 | whenConcurrencySlotAvailable(() => {
167 | inspect
168 | .async(fileItemPath, options.inspectOptions)
169 | .then((fileItem) => {
170 | if (fileItem !== undefined) {
171 | if (performInspectOnEachNode) {
172 | callback(fileItemPath, fileItem);
173 | } else {
174 | callback(fileItemPath, {
175 | name: fileItem.name,
176 | type: fileItem.type,
177 | });
178 | }
179 | goDeeperIfDir(fileItemPath, fileItem);
180 | }
181 | concurrentOperationDone();
182 | })
183 | .catch((err) => {
184 | doneCallback(err);
185 | });
186 | });
187 | } else {
188 | const type = fileType(direntItem);
189 | if (type === "symlink" && options.symlinks === "follow") {
190 | whenConcurrencySlotAvailable(() => {
191 | fs.stat(fileItemPath, (err, symlinkPointsTo) => {
192 | if (err) {
193 | doneCallback(err);
194 | } else {
195 | const fileItem = {
196 | name: direntItem.name,
197 | type: fileType(symlinkPointsTo),
198 | };
199 | callback(fileItemPath, fileItem);
200 | goDeeperIfDir(fileItemPath, fileItem);
201 | concurrentOperationDone();
202 | }
203 | });
204 | });
205 | } else {
206 | const fileItem = { name: direntItem.name, type };
207 | callback(fileItemPath, fileItem);
208 | goDeeperIfDir(fileItemPath, fileItem);
209 | }
210 | }
211 | });
212 | concurrentOperationDone();
213 | }
214 | });
215 | });
216 | };
217 |
218 | inspect
219 | .async(path, options.inspectOptions)
220 | .then((item) => {
221 | if (item) {
222 | if (performInspectOnEachNode) {
223 | callback(path, item);
224 | } else {
225 | // Return simplified object, not full inspect object
226 | callback(path, { name: item.name, type: item.type });
227 | }
228 | if (item.type === "dir") {
229 | walkAsync(path, 1);
230 | } else {
231 | doneCallback();
232 | }
233 | } else {
234 | callback(path, undefined);
235 | doneCallback();
236 | }
237 | })
238 | .catch((err) => {
239 | doneCallback(err);
240 | });
241 | };
242 |
243 | // ---------------------------------------------------------
244 | // API
245 | // ---------------------------------------------------------
246 |
247 | exports.sync = initialWalkSync;
248 | exports.async = initialWalkAsync;
249 |
--------------------------------------------------------------------------------
/lib/utils/validate.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | const prettyPrintTypes = (types) => {
4 | const addArticle = (str) => {
5 | const vowels = ["a", "e", "i", "o", "u"];
6 | if (vowels.indexOf(str[0]) !== -1) {
7 | return `an ${str}`;
8 | }
9 | return `a ${str}`;
10 | };
11 |
12 | return types.map(addArticle).join(" or ");
13 | };
14 |
15 | const isArrayOfNotation = (typeDefinition) => {
16 | return /array of /.test(typeDefinition);
17 | };
18 |
19 | const extractTypeFromArrayOfNotation = (typeDefinition) => {
20 | // The notation is e.g. 'array of string'
21 | return typeDefinition.split(" of ")[1];
22 | };
23 |
24 | const isValidTypeDefinition = (typeStr) => {
25 | if (isArrayOfNotation(typeStr)) {
26 | return isValidTypeDefinition(extractTypeFromArrayOfNotation(typeStr));
27 | }
28 |
29 | return [
30 | "string",
31 | "number",
32 | "boolean",
33 | "array",
34 | "object",
35 | "buffer",
36 | "null",
37 | "undefined",
38 | "function",
39 | ].some((validType) => {
40 | return validType === typeStr;
41 | });
42 | };
43 |
44 | const detectType = (value) => {
45 | if (value === null) {
46 | return "null";
47 | }
48 | if (Array.isArray(value)) {
49 | return "array";
50 | }
51 | if (Buffer.isBuffer(value)) {
52 | return "buffer";
53 | }
54 |
55 | return typeof value;
56 | };
57 |
58 | const onlyUniqueValuesInArrayFilter = (value, index, self) => {
59 | return self.indexOf(value) === index;
60 | };
61 |
62 | const detectTypeDeep = (value) => {
63 | let type = detectType(value);
64 | let typesInArray;
65 |
66 | if (type === "array") {
67 | typesInArray = value
68 | .map((element) => {
69 | return detectType(element);
70 | })
71 | .filter(onlyUniqueValuesInArrayFilter);
72 | type += ` of ${typesInArray.join(", ")}`;
73 | }
74 |
75 | return type;
76 | };
77 |
78 | const validateArray = (argumentValue, typeToCheck) => {
79 | const allowedTypeInArray = extractTypeFromArrayOfNotation(typeToCheck);
80 |
81 | if (detectType(argumentValue) !== "array") {
82 | return false;
83 | }
84 |
85 | return argumentValue.every((element) => {
86 | return detectType(element) === allowedTypeInArray;
87 | });
88 | };
89 |
90 | const validateArgument = (
91 | methodName,
92 | argumentName,
93 | argumentValue,
94 | argumentMustBe
95 | ) => {
96 | const isOneOfAllowedTypes = argumentMustBe.some((type) => {
97 | if (!isValidTypeDefinition(type)) {
98 | throw new Error(`Unknown type "${type}"`);
99 | }
100 |
101 | if (isArrayOfNotation(type)) {
102 | return validateArray(argumentValue, type);
103 | }
104 |
105 | return type === detectType(argumentValue);
106 | });
107 |
108 | if (!isOneOfAllowedTypes) {
109 | throw new Error(
110 | `Argument "${argumentName}" passed to ${methodName} must be ${prettyPrintTypes(
111 | argumentMustBe
112 | )}. Received ${detectTypeDeep(argumentValue)}`
113 | );
114 | }
115 | };
116 |
117 | const validateOptions = (methodName, optionsObjName, obj, allowedOptions) => {
118 | if (obj !== undefined) {
119 | validateArgument(methodName, optionsObjName, obj, ["object"]);
120 | Object.keys(obj).forEach((key) => {
121 | const argName = `${optionsObjName}.${key}`;
122 | if (allowedOptions[key] !== undefined) {
123 | validateArgument(methodName, argName, obj[key], allowedOptions[key]);
124 | } else {
125 | throw new Error(
126 | `Unknown argument "${argName}" passed to ${methodName}`
127 | );
128 | }
129 | });
130 | }
131 | };
132 |
133 | module.exports = {
134 | argument: validateArgument,
135 | options: validateOptions,
136 | };
137 |
--------------------------------------------------------------------------------
/lib/write.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | const pathUtil = require("path");
4 | const fs = require("./utils/fs");
5 | const validate = require("./utils/validate");
6 | const dir = require("./dir");
7 |
8 | const validateInput = (methodName, path, data, options) => {
9 | const methodSignature = `${methodName}(path, data, [options])`;
10 | validate.argument(methodSignature, "path", path, ["string"]);
11 | validate.argument(methodSignature, "data", data, [
12 | "string",
13 | "buffer",
14 | "object",
15 | "array",
16 | ]);
17 | validate.options(methodSignature, "options", options, {
18 | mode: ["string", "number"],
19 | atomic: ["boolean"],
20 | jsonIndent: ["number"],
21 | });
22 | };
23 |
24 | // Temporary file extensions used for atomic file overwriting.
25 | const newExt = ".__new__";
26 |
27 | const serializeToJsonMaybe = (data, jsonIndent) => {
28 | let indent = jsonIndent;
29 | if (typeof indent !== "number") {
30 | indent = 2;
31 | }
32 |
33 | if (typeof data === "object" && !Buffer.isBuffer(data) && data !== null) {
34 | return JSON.stringify(data, null, indent);
35 | }
36 |
37 | return data;
38 | };
39 |
40 | // ---------------------------------------------------------
41 | // SYNC
42 | // ---------------------------------------------------------
43 |
44 | const writeFileSync = (path, data, options) => {
45 | try {
46 | fs.writeFileSync(path, data, options);
47 | } catch (err) {
48 | if (err.code === "ENOENT") {
49 | // Means parent directory doesn't exist, so create it and try again.
50 | dir.createSync(pathUtil.dirname(path));
51 | fs.writeFileSync(path, data, options);
52 | } else {
53 | throw err;
54 | }
55 | }
56 | };
57 |
58 | const writeAtomicSync = (path, data, options) => {
59 | // we are assuming there is file on given path, and we don't want
60 | // to touch it until we are sure our data has been saved correctly,
61 | // so write the data into temporary file...
62 | writeFileSync(path + newExt, data, options);
63 | // ...next rename temp file to replace real path.
64 | fs.renameSync(path + newExt, path);
65 | };
66 |
67 | const writeSync = (path, data, options) => {
68 | const opts = options || {};
69 | const processedData = serializeToJsonMaybe(data, opts.jsonIndent);
70 |
71 | let writeStrategy = writeFileSync;
72 | if (opts.atomic) {
73 | writeStrategy = writeAtomicSync;
74 | }
75 | writeStrategy(path, processedData, { mode: opts.mode });
76 | };
77 |
78 | // ---------------------------------------------------------
79 | // ASYNC
80 | // ---------------------------------------------------------
81 |
82 | const writeFileAsync = (path, data, options) => {
83 | return new Promise((resolve, reject) => {
84 | fs.writeFile(path, data, options)
85 | .then(resolve)
86 | .catch((err) => {
87 | // First attempt to write a file ended with error.
88 | // Check if this is not due to nonexistent parent directory.
89 | if (err.code === "ENOENT") {
90 | // Parent directory doesn't exist, so create it and try again.
91 | dir
92 | .createAsync(pathUtil.dirname(path))
93 | .then(() => {
94 | return fs.writeFile(path, data, options);
95 | })
96 | .then(resolve, reject);
97 | } else {
98 | // Nope, some other error, throw it.
99 | reject(err);
100 | }
101 | });
102 | });
103 | };
104 |
105 | const writeAtomicAsync = (path, data, options) => {
106 | return new Promise((resolve, reject) => {
107 | // We are assuming there is file on given path, and we don't want
108 | // to touch it until we are sure our data has been saved correctly,
109 | // so write the data into temporary file...
110 | writeFileAsync(path + newExt, data, options)
111 | .then(() => {
112 | // ...next rename temp file to real path.
113 | return fs.rename(path + newExt, path);
114 | })
115 | .then(resolve, reject);
116 | });
117 | };
118 |
119 | const writeAsync = (path, data, options) => {
120 | const opts = options || {};
121 | const processedData = serializeToJsonMaybe(data, opts.jsonIndent);
122 |
123 | let writeStrategy = writeFileAsync;
124 | if (opts.atomic) {
125 | writeStrategy = writeAtomicAsync;
126 | }
127 | return writeStrategy(path, processedData, { mode: opts.mode });
128 | };
129 |
130 | // ---------------------------------------------------------
131 | // API
132 | // ---------------------------------------------------------
133 |
134 | exports.validateInput = validateInput;
135 | exports.sync = writeSync;
136 | exports.async = writeAsync;
137 |
--------------------------------------------------------------------------------
/main.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | const jetpack = require("./lib/jetpack");
4 |
5 | module.exports = jetpack();
6 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "fs-jetpack",
3 | "description": "Better file system API",
4 | "version": "5.1.0",
5 | "author": "Jakub Szwacz ",
6 | "dependencies": {
7 | "minimatch": "^5.1.0"
8 | },
9 | "devDependencies": {
10 | "@types/chai": "^4.3.3",
11 | "@types/fs-extra": "^5.0.4",
12 | "@types/mocha": "^9.1.1",
13 | "@types/node": "^18.7.18",
14 | "chai": "^4.3.6",
15 | "fs-extra": "^5.0.0",
16 | "istanbul": "^0.4.5",
17 | "mocha": "^10.0.0",
18 | "nyc": "^15.1.0",
19 | "prettier": "^2.7.1",
20 | "pretty-bytes": "^5.1.0",
21 | "release-assist": "^2.0.0",
22 | "ts-node": "^10.9.1",
23 | "typescript": "^4.8.3"
24 | },
25 | "scripts": {
26 | "test": "mocha -r ts-node/register \"spec/**/*.spec.ts\"",
27 | "coverage": "nyc --reporter=lcov npm run test",
28 | "prettier": "prettier --write \"./**/*.{js,ts}\"",
29 | "release-start": "release-assist --start",
30 | "release-finish": "release-assist --finish"
31 | },
32 | "main": "main.js",
33 | "files": [
34 | "lib",
35 | "index.d.ts",
36 | "types.d.ts"
37 | ],
38 | "types": "index.d.ts",
39 | "homepage": "https://github.com/szwacz/fs-jetpack",
40 | "repository": {
41 | "type": "git",
42 | "url": "https://github.com/szwacz/fs-jetpack.git"
43 | },
44 | "license": "MIT",
45 | "keywords": [
46 | "fs",
47 | "file system"
48 | ]
49 | }
--------------------------------------------------------------------------------
/spec/append.spec.ts:
--------------------------------------------------------------------------------
1 | import * as fse from "fs-extra";
2 | import { expect } from "chai";
3 | import path from "./assert_path";
4 | import helper from "./helper";
5 | import * as jetpack from "..";
6 |
7 | describe("append", () => {
8 | beforeEach(helper.setCleanTestCwd);
9 | afterEach(helper.switchBackToCorrectCwd);
10 |
11 | describe("appends String to file", () => {
12 | const preparations = () => {
13 | fse.writeFileSync("file.txt", "abc");
14 | };
15 |
16 | const expectations = () => {
17 | path("file.txt").shouldBeFileWithContent("abcxyz");
18 | };
19 |
20 | it("sync", () => {
21 | preparations();
22 | jetpack.append("file.txt", "xyz");
23 | expectations();
24 | });
25 |
26 | it("async", (done) => {
27 | preparations();
28 | jetpack.appendAsync("file.txt", "xyz").then(() => {
29 | expectations();
30 | done();
31 | });
32 | });
33 | });
34 |
35 | describe("appends Buffer to file", () => {
36 | const preparations = () => {
37 | fse.writeFileSync("file.bin", new Buffer([11]));
38 | };
39 |
40 | const expectations = () => {
41 | path("file.bin").shouldBeFileWithContent(new Buffer([11, 22]));
42 | };
43 |
44 | it("sync", () => {
45 | preparations();
46 | jetpack.append("file.bin", new Buffer([22]));
47 | expectations();
48 | });
49 |
50 | it("async", (done) => {
51 | preparations();
52 | jetpack.appendAsync("file.bin", new Buffer([22])).then(() => {
53 | expectations();
54 | done();
55 | });
56 | });
57 | });
58 |
59 | describe("if file doesn't exist creates it", () => {
60 | const expectations = () => {
61 | path("file.txt").shouldBeFileWithContent("xyz");
62 | };
63 |
64 | it("sync", () => {
65 | jetpack.append("file.txt", "xyz");
66 | expectations();
67 | });
68 |
69 | it("async", (done) => {
70 | jetpack.appendAsync("file.txt", "xyz").then(() => {
71 | expectations();
72 | done();
73 | });
74 | });
75 | });
76 |
77 | describe("if parent directory doesn't exist creates it", () => {
78 | const expectations = () => {
79 | path("dir/dir/file.txt").shouldBeFileWithContent("xyz");
80 | };
81 |
82 | it("sync", () => {
83 | jetpack.append("dir/dir/file.txt", "xyz");
84 | expectations();
85 | });
86 |
87 | it("async", (done) => {
88 | jetpack.appendAsync("dir/dir/file.txt", "xyz").then(() => {
89 | expectations();
90 | done();
91 | });
92 | });
93 | });
94 |
95 | describe("respects internal CWD of jetpack instance", () => {
96 | const preparations = () => {
97 | fse.outputFileSync("a/b.txt", "abc");
98 | };
99 |
100 | const expectations = () => {
101 | path("a/b.txt").shouldBeFileWithContent("abcxyz");
102 | };
103 |
104 | it("sync", () => {
105 | const jetContext = jetpack.cwd("a");
106 | preparations();
107 | jetContext.append("b.txt", "xyz");
108 | expectations();
109 | });
110 |
111 | it("async", (done) => {
112 | const jetContext = jetpack.cwd("a");
113 | preparations();
114 | jetContext.appendAsync("b.txt", "xyz").then(() => {
115 | expectations();
116 | done();
117 | });
118 | });
119 | });
120 |
121 | describe("input validation", () => {
122 | const tests = [
123 | { type: "sync", method: jetpack.append as any, methodName: "append" },
124 | {
125 | type: "async",
126 | method: jetpack.appendAsync as any,
127 | methodName: "appendAsync",
128 | },
129 | ];
130 |
131 | describe('"path" argument', () => {
132 | tests.forEach((test) => {
133 | it(test.type, () => {
134 | expect(() => {
135 | test.method(undefined, "xyz");
136 | }).to.throw(
137 | `Argument "path" passed to ${test.methodName}(path, data, [options]) must be a string. Received undefined`
138 | );
139 | });
140 | });
141 | });
142 |
143 | describe('"data" argument', () => {
144 | tests.forEach((test) => {
145 | it(test.type, () => {
146 | expect(() => {
147 | test.method("abc");
148 | }).to.throw(
149 | `Argument "data" passed to ${test.methodName}(path, data, [options]) must be a string or a buffer. Received undefined`
150 | );
151 | });
152 | });
153 | });
154 |
155 | describe('"options" object', () => {
156 | describe('"mode" argument', () => {
157 | tests.forEach((test) => {
158 | it(test.type, () => {
159 | expect(() => {
160 | test.method("abc", "xyz", { mode: true });
161 | }).to.throw(
162 | `Argument "options.mode" passed to ${test.methodName}(path, data, [options]) must be a string or a number. Received boolean`
163 | );
164 | });
165 | });
166 | });
167 | });
168 | });
169 |
170 | if (process.platform !== "win32") {
171 | describe("sets file mode on created file (unix only)", () => {
172 | const expectations = () => {
173 | path("file.txt").shouldHaveMode("711");
174 | };
175 |
176 | it("sync", () => {
177 | jetpack.append("file.txt", "abc", { mode: "711" });
178 | expectations();
179 | });
180 |
181 | it("async", (done) => {
182 | jetpack.appendAsync("file.txt", "abc", { mode: "711" }).then(() => {
183 | expectations();
184 | done();
185 | });
186 | });
187 | });
188 | }
189 | });
190 |
--------------------------------------------------------------------------------
/spec/assert_path.ts:
--------------------------------------------------------------------------------
1 | import * as fs from "fs";
2 |
3 | const areBuffersEqual = (bufA: Buffer, bufB: Buffer) => {
4 | const len = bufA.length;
5 | if (len !== bufB.length) {
6 | return false;
7 | }
8 | for (let i = 0; i < len; i += 1) {
9 | if (bufA.readUInt8(i) !== bufB.readUInt8(i)) {
10 | return false;
11 | }
12 | }
13 | return true;
14 | };
15 |
16 | export default (path: string) => {
17 | return {
18 | shouldNotExist: () => {
19 | let message;
20 | try {
21 | fs.statSync(path);
22 | message = `Path ${path} should NOT exist`;
23 | } catch (err) {
24 | if (err.code !== "ENOENT") {
25 | throw err;
26 | }
27 | }
28 | if (message) {
29 | throw new Error(message);
30 | }
31 | },
32 |
33 | shouldBeDirectory: () => {
34 | let message;
35 | let stat;
36 | try {
37 | stat = fs.statSync(path);
38 | if (!stat.isDirectory()) {
39 | message = `Path ${path} should be a directory`;
40 | }
41 | } catch (err) {
42 | if (err.code === "ENOENT") {
43 | message = `Path ${path} should exist`;
44 | } else {
45 | throw err;
46 | }
47 | }
48 | if (message) {
49 | throw new Error(message);
50 | }
51 | },
52 |
53 | shouldBeFileWithContent: (expectedContent: any) => {
54 | let message;
55 | let content;
56 |
57 | const generateMessage = (expected: string, found: string) => {
58 | message = `File ${path} should have content "${expected}" but found "${found}"`;
59 | };
60 |
61 | try {
62 | if (Buffer.isBuffer(expectedContent)) {
63 | content = fs.readFileSync(path);
64 | if (!areBuffersEqual(expectedContent, content)) {
65 | generateMessage(
66 | expectedContent.toString("hex"),
67 | content.toString("hex")
68 | );
69 | }
70 | } else {
71 | content = fs.readFileSync(path, "utf8");
72 | if (content !== expectedContent) {
73 | generateMessage(expectedContent, content);
74 | }
75 | }
76 | } catch (err) {
77 | if (err.code === "ENOENT") {
78 | message = `File ${path} should exist`;
79 | } else {
80 | throw err;
81 | }
82 | }
83 | if (message) {
84 | throw new Error(message);
85 | }
86 | },
87 |
88 | shouldHaveMode: (expectedMode: any) => {
89 | let mode;
90 | let message;
91 |
92 | try {
93 | mode = fs.statSync(path).mode.toString(8);
94 | mode = mode.substring(mode.length - 3);
95 | if (mode !== expectedMode) {
96 | message = `Path ${path} should have mode "${expectedMode}" but have instead "${mode}"`;
97 | }
98 | } catch (err) {
99 | if (err.code === "ENOENT") {
100 | message = `Path ${path} should exist`;
101 | } else {
102 | throw err;
103 | }
104 | }
105 | if (message) {
106 | throw new Error(message);
107 | }
108 | },
109 | };
110 | };
111 |
--------------------------------------------------------------------------------
/spec/cwd.spec.ts:
--------------------------------------------------------------------------------
1 | import * as pathUtil from "path";
2 | import { expect } from "chai";
3 | import * as jetpack from "..";
4 |
5 | describe("cwd", () => {
6 | it("returns the same path as process.cwd for main instance of jetpack", () => {
7 | expect(jetpack.cwd()).to.equal(process.cwd());
8 | });
9 |
10 | it("can create new context with different cwd", () => {
11 | let jetCwd = jetpack.cwd("/"); // absolute path
12 | expect(jetCwd.cwd()).to.equal(pathUtil.resolve(process.cwd(), "/"));
13 |
14 | jetCwd = jetpack.cwd("../.."); // relative path
15 | expect(jetCwd.cwd()).to.equal(pathUtil.resolve(process.cwd(), "../.."));
16 |
17 | expect(jetpack.cwd()).to.equal(process.cwd()); // cwd of main lib should be intact
18 | });
19 |
20 | it("cwd contexts can be created recursively", () => {
21 | const jetCwd1 = jetpack.cwd("..");
22 | expect(jetCwd1.cwd()).to.equal(pathUtil.resolve(process.cwd(), ".."));
23 |
24 | const jetCwd2 = jetCwd1.cwd("..");
25 | expect(jetCwd2.cwd()).to.equal(pathUtil.resolve(process.cwd(), "../.."));
26 | });
27 |
28 | it("cwd can join path parts", () => {
29 | const jetCwd = jetpack.cwd("a", "b", "c");
30 | expect(jetCwd.cwd()).to.equal(
31 | pathUtil.resolve(process.cwd(), "a", "b", "c")
32 | );
33 | });
34 | });
35 |
--------------------------------------------------------------------------------
/spec/dir.spec.ts:
--------------------------------------------------------------------------------
1 | import * as fse from "fs-extra";
2 | import { expect } from "chai";
3 | import * as pathUtil from "path";
4 | import path from "./assert_path";
5 | import helper from "./helper";
6 | import * as jetpack from "..";
7 | import { FSJetpack } from "../types";
8 |
9 | describe("dir", () => {
10 | beforeEach(helper.setCleanTestCwd);
11 | afterEach(helper.switchBackToCorrectCwd);
12 |
13 | describe("creates directory if it doesn't exist", () => {
14 | const expectations = () => {
15 | path("x").shouldBeDirectory();
16 | };
17 |
18 | it("sync", () => {
19 | jetpack.dir("x");
20 | expectations();
21 | });
22 |
23 | it("async", (done) => {
24 | jetpack.dirAsync("x").then(() => {
25 | expectations();
26 | done();
27 | });
28 | });
29 | });
30 |
31 | describe("does nothing if directory already exists", () => {
32 | const preparations = () => {
33 | fse.mkdirsSync("x");
34 | };
35 |
36 | const expectations = () => {
37 | path("x").shouldBeDirectory();
38 | };
39 |
40 | it("sync", () => {
41 | preparations();
42 | jetpack.dir("x");
43 | expectations();
44 | });
45 |
46 | it("async", (done) => {
47 | preparations();
48 | jetpack.dirAsync("x").then(() => {
49 | expectations();
50 | done();
51 | });
52 | });
53 | });
54 |
55 | describe("creates nested directories if necessary", () => {
56 | const expectations = () => {
57 | path("a/b/c").shouldBeDirectory();
58 | };
59 |
60 | it("sync", () => {
61 | jetpack.dir("a/b/c");
62 | expectations();
63 | });
64 |
65 | it("async", (done) => {
66 | jetpack.dirAsync("a/b/c").then(() => {
67 | expectations();
68 | done();
69 | });
70 | });
71 | });
72 |
73 | describe("handles well two calls racing to create the same directory", () => {
74 | const expectations = () => {
75 | path("a/b/c").shouldBeDirectory();
76 | };
77 |
78 | it("async", (done) => {
79 | let doneCount = 0;
80 | const check = () => {
81 | doneCount += 1;
82 | if (doneCount === 2) {
83 | expectations();
84 | done();
85 | }
86 | };
87 | jetpack.dirAsync("a/b/c").then(check);
88 | jetpack.dirAsync("a/b/c").then(check);
89 | });
90 | });
91 |
92 | describe("doesn't touch directory content by default", () => {
93 | const preparations = () => {
94 | fse.mkdirsSync("a/b");
95 | fse.outputFileSync("a/c.txt", "abc");
96 | };
97 |
98 | const expectations = () => {
99 | path("a/b").shouldBeDirectory();
100 | path("a/c.txt").shouldBeFileWithContent("abc");
101 | };
102 |
103 | it("sync", () => {
104 | preparations();
105 | jetpack.dir("a");
106 | expectations();
107 | });
108 |
109 | it("async", (done) => {
110 | preparations();
111 | jetpack.dirAsync("a").then(() => {
112 | expectations();
113 | done();
114 | });
115 | });
116 | });
117 |
118 | describe("makes directory empty if that option specified", () => {
119 | const preparations = () => {
120 | fse.outputFileSync("a/b/file.txt", "abc");
121 | };
122 |
123 | const expectations = () => {
124 | path("a/b/file.txt").shouldNotExist();
125 | path("a").shouldBeDirectory();
126 | };
127 |
128 | it("sync", () => {
129 | preparations();
130 | jetpack.dir("a", { empty: true });
131 | expectations();
132 | });
133 |
134 | it("async", (done) => {
135 | preparations();
136 | jetpack.dirAsync("a", { empty: true }).then(() => {
137 | expectations();
138 | done();
139 | });
140 | });
141 | });
142 |
143 | describe("throws if given path is something other than directory", () => {
144 | const preparations = () => {
145 | fse.outputFileSync("a", "abc");
146 | };
147 |
148 | const expectations = (err: any) => {
149 | expect(err.message).to.have.string("exists but is not a directory");
150 | };
151 |
152 | it("sync", () => {
153 | preparations();
154 | try {
155 | jetpack.dir("a");
156 | throw new Error("Expected error to be thrown");
157 | } catch (err) {
158 | expectations(err);
159 | }
160 | });
161 |
162 | it("async", (done) => {
163 | preparations();
164 | jetpack.dirAsync("a").catch((err) => {
165 | expectations(err);
166 | done();
167 | });
168 | });
169 | });
170 |
171 | describe("respects internal CWD of jetpack instance", () => {
172 | const expectations = () => {
173 | path("a/b").shouldBeDirectory();
174 | };
175 |
176 | it("sync", () => {
177 | const jetContext = jetpack.cwd("a");
178 | jetContext.dir("b");
179 | expectations();
180 | });
181 |
182 | it("async", (done) => {
183 | const jetContext = jetpack.cwd("a");
184 | jetContext.dirAsync("b").then(() => {
185 | expectations();
186 | done();
187 | });
188 | });
189 | });
190 |
191 | describe("returns jetack instance pointing on this directory", () => {
192 | const expectations = (jetpackContext: FSJetpack) => {
193 | expect(jetpackContext.cwd()).to.equal(pathUtil.resolve("a"));
194 | };
195 |
196 | it("sync", () => {
197 | expectations(jetpack.dir("a"));
198 | });
199 |
200 | it("async", (done) => {
201 | jetpack.dirAsync("a").then((jetpackContext) => {
202 | expectations(jetpackContext);
203 | done();
204 | });
205 | });
206 | });
207 |
208 | if (process.platform !== "win32") {
209 | describe("sets mode to newly created directory (unix only)", () => {
210 | const expectations = () => {
211 | path("a").shouldHaveMode("511");
212 | };
213 |
214 | it("sync, mode passed as string", () => {
215 | jetpack.dir("a", { mode: "511" });
216 | expectations();
217 | });
218 |
219 | it("sync, mode passed as number", () => {
220 | jetpack.dir("a", { mode: 0o511 });
221 | expectations();
222 | });
223 |
224 | it("async, mode passed as string", (done) => {
225 | jetpack.dirAsync("a", { mode: "511" }).then(() => {
226 | expectations();
227 | done();
228 | });
229 | });
230 |
231 | it("async, mode passed as number", (done) => {
232 | jetpack.dirAsync("a", { mode: 0o511 }).then(() => {
233 | expectations();
234 | done();
235 | });
236 | });
237 | });
238 |
239 | describe("sets desired mode to every created directory (unix only)", () => {
240 | const expectations = () => {
241 | path("a").shouldHaveMode("711");
242 | path("a/b").shouldHaveMode("711");
243 | };
244 |
245 | it("sync", () => {
246 | jetpack.dir("a/b", { mode: "711" });
247 | expectations();
248 | });
249 |
250 | it("async", (done) => {
251 | jetpack.dirAsync("a/b", { mode: "711" }).then(() => {
252 | expectations();
253 | done();
254 | });
255 | });
256 | });
257 |
258 | describe("changes mode of existing directory to desired (unix only)", () => {
259 | const preparations = () => {
260 | fse.mkdirSync("a", "777");
261 | };
262 | const expectations = () => {
263 | path("a").shouldHaveMode("511");
264 | };
265 |
266 | it("sync", () => {
267 | preparations();
268 | jetpack.dir("a", { mode: "511" });
269 | expectations();
270 | });
271 |
272 | it("async", (done) => {
273 | preparations();
274 | jetpack.dirAsync("a", { mode: "511" }).then(() => {
275 | expectations();
276 | done();
277 | });
278 | });
279 | });
280 |
281 | describe("leaves mode of directory intact by default (unix only)", () => {
282 | const preparations = () => {
283 | fse.mkdirSync("a", "700");
284 | };
285 |
286 | const expectations = () => {
287 | path("a").shouldHaveMode("700");
288 | };
289 |
290 | it("sync", () => {
291 | preparations();
292 | jetpack.dir("a");
293 | expectations();
294 | });
295 |
296 | it("async", (done) => {
297 | preparations();
298 | jetpack.dirAsync("a").then(() => {
299 | expectations();
300 | done();
301 | });
302 | });
303 | });
304 | } else {
305 | describe("specyfying mode have no effect and throws no error (windows only)", () => {
306 | const expectations = () => {
307 | path("x").shouldBeDirectory();
308 | };
309 |
310 | it("sync", () => {
311 | jetpack.dir("x", { mode: "511" });
312 | expectations();
313 | });
314 |
315 | it("async", (done) => {
316 | jetpack.dirAsync("x", { mode: "511" }).then(() => {
317 | expectations();
318 | done();
319 | });
320 | });
321 | });
322 | }
323 |
324 | describe("input validation", () => {
325 | const tests = [
326 | { type: "sync", method: jetpack.dir as any, methodName: "dir" },
327 | {
328 | type: "async",
329 | method: jetpack.dirAsync as any,
330 | methodName: "dirAsync",
331 | },
332 | ];
333 |
334 | describe('"path" argument', () => {
335 | tests.forEach((test) => {
336 | it(test.type, () => {
337 | expect(() => {
338 | test.method(undefined);
339 | }).to.throw(
340 | `Argument "path" passed to ${test.methodName}(path, [criteria]) must be a string. Received undefined`
341 | );
342 | });
343 | });
344 | });
345 |
346 | describe('"criteria" object', () => {
347 | describe('"empty" argument', () => {
348 | tests.forEach((test) => {
349 | it(test.type, () => {
350 | expect(() => {
351 | test.method("abc", { empty: 1 });
352 | }).to.throw(
353 | `Argument "criteria.empty" passed to ${test.methodName}(path, [criteria]) must be a boolean. Received number`
354 | );
355 | });
356 | });
357 | });
358 | describe('"mode" argument', () => {
359 | tests.forEach((test) => {
360 | it(test.type, () => {
361 | expect(() => {
362 | test.method("abc", { mode: true });
363 | }).to.throw(
364 | `Argument "criteria.mode" passed to ${test.methodName}(path, [criteria]) must be a string or a number. Received boolean`
365 | );
366 | });
367 | });
368 | });
369 | });
370 | });
371 | });
372 |
--------------------------------------------------------------------------------
/spec/exists.spec.ts:
--------------------------------------------------------------------------------
1 | import * as fse from "fs-extra";
2 | import { expect } from "chai";
3 | import helper from "./helper";
4 | import * as jetpack from "..";
5 | import { ExistsResult } from "../types";
6 |
7 | describe("exists", () => {
8 | beforeEach(helper.setCleanTestCwd);
9 | afterEach(helper.switchBackToCorrectCwd);
10 |
11 | describe("returns false if file doesn't exist", () => {
12 | const expectations = (exists: ExistsResult) => {
13 | expect(exists).to.equal(false);
14 | };
15 |
16 | it("sync", () => {
17 | expectations(jetpack.exists("file.txt"));
18 | });
19 |
20 | it("async", (done) => {
21 | jetpack.existsAsync("file.txt").then((exists) => {
22 | expectations(exists);
23 | done();
24 | });
25 | });
26 | });
27 |
28 | describe("returns 'dir' if directory exists on given path", () => {
29 | const preparations = () => {
30 | fse.mkdirsSync("a");
31 | };
32 |
33 | const expectations = (exists: ExistsResult) => {
34 | expect(exists).to.equal("dir");
35 | };
36 |
37 | it("sync", () => {
38 | preparations();
39 | expectations(jetpack.exists("a"));
40 | });
41 |
42 | it("async", (done) => {
43 | preparations();
44 | jetpack.existsAsync("a").then((exists) => {
45 | expectations(exists);
46 | done();
47 | });
48 | });
49 | });
50 |
51 | describe("returns 'file' if file exists on given path", () => {
52 | const preparations = () => {
53 | fse.outputFileSync("text.txt", "abc");
54 | };
55 |
56 | const expectations = (exists: ExistsResult) => {
57 | expect(exists).to.equal("file");
58 | };
59 |
60 | it("sync", () => {
61 | preparations();
62 | expectations(jetpack.exists("text.txt"));
63 | });
64 |
65 | it("async", (done) => {
66 | preparations();
67 | jetpack.existsAsync("text.txt").then((exists) => {
68 | expectations(exists);
69 | done();
70 | });
71 | });
72 | });
73 |
74 | describe("respects internal CWD of jetpack instance", () => {
75 | const preparations = () => {
76 | fse.outputFileSync("a/text.txt", "abc");
77 | };
78 |
79 | const expectations = (exists: ExistsResult) => {
80 | expect(exists).to.equal("file");
81 | };
82 |
83 | it("sync", () => {
84 | const jetContext = jetpack.cwd("a");
85 | preparations();
86 | expectations(jetContext.exists("text.txt"));
87 | });
88 |
89 | it("async", (done) => {
90 | const jetContext = jetpack.cwd("a");
91 | preparations();
92 | jetContext.existsAsync("text.txt").then((exists) => {
93 | expectations(exists);
94 | done();
95 | });
96 | });
97 | });
98 |
99 | describe("input validation", () => {
100 | const tests = [
101 | { type: "sync", method: jetpack.exists, methodName: "exists" },
102 | { type: "async", method: jetpack.existsAsync, methodName: "existsAsync" },
103 | ];
104 |
105 | describe('"path" argument', () => {
106 | tests.forEach((test) => {
107 | it(test.type, () => {
108 | expect(() => {
109 | test.method(undefined);
110 | }).to.throw(
111 | `Argument "path" passed to ${test.methodName}(path) must be a string. Received undefined`
112 | );
113 | });
114 | });
115 | });
116 | });
117 | });
118 |
--------------------------------------------------------------------------------
/spec/helper.ts:
--------------------------------------------------------------------------------
1 | import * as os from "os";
2 | import * as crypto from "crypto";
3 | import * as fse from "fs-extra";
4 |
5 | const originalCwd = process.cwd();
6 | const createdDirectories: string[] = [];
7 |
8 | process.on("exit", () => {
9 | // In case something went wrong and some temp
10 | // directories are still on the disk.
11 | createdDirectories.forEach((path) => {
12 | fse.removeSync(path);
13 | });
14 | });
15 |
16 | const setCleanTestCwd = () => {
17 | const random = crypto.randomBytes(16).toString("hex");
18 | const path = `${os.tmpdir()}/fs-jetpack-test-${random}`;
19 | fse.mkdirSync(path);
20 | createdDirectories.push(path);
21 | process.chdir(path);
22 | };
23 |
24 | const switchBackToCorrectCwd = () => {
25 | const path = createdDirectories.pop();
26 | process.chdir(originalCwd);
27 | try {
28 | fse.removeSync(path);
29 | } catch (err) {
30 | // On Windows platform sometimes removal of the directory leads to error:
31 | // Error: ENOTEMPTY: directory not empty, rmdir
32 | // Let's retry the attempt.
33 | fse.removeSync(path);
34 | }
35 | };
36 |
37 | const parseMode = (modeAsNumber: number) => {
38 | const mode = modeAsNumber.toString(8);
39 | return mode.substring(mode.length - 3);
40 | };
41 |
42 | // Converts paths to windows or unix formats depending on platform running.
43 | function osSep(path: string): string;
44 | function osSep(path: string[]): string[];
45 | function osSep(path: any): any {
46 | if (Array.isArray(path)) {
47 | return path.map(osSep);
48 | }
49 |
50 | if (process.platform === "win32") {
51 | return path.replace(/\//g, "\\");
52 | }
53 | return path.replace(/\\/g, "/");
54 | }
55 |
56 | export default {
57 | setCleanTestCwd,
58 | switchBackToCorrectCwd,
59 | parseMode,
60 | osSep,
61 | };
62 |
--------------------------------------------------------------------------------
/spec/list.spec.ts:
--------------------------------------------------------------------------------
1 | import * as fse from "fs-extra";
2 | import { expect } from "chai";
3 | import path from "./assert_path";
4 | import helper from "./helper";
5 | import * as jetpack from "..";
6 |
7 | describe("list", () => {
8 | beforeEach(helper.setCleanTestCwd);
9 | afterEach(helper.switchBackToCorrectCwd);
10 |
11 | describe("lists file names in given path", () => {
12 | const preparations = () => {
13 | fse.mkdirsSync("dir/empty");
14 | fse.outputFileSync("dir/empty.txt", "");
15 | fse.outputFileSync("dir/file.txt", "abc");
16 | fse.outputFileSync("dir/subdir/file.txt", "defg");
17 | };
18 |
19 | const expectations = (data: string[]) => {
20 | expect(data).to.eql(["empty", "empty.txt", "file.txt", "subdir"]);
21 | };
22 |
23 | it("sync", () => {
24 | preparations();
25 | expectations(jetpack.list("dir"));
26 | });
27 |
28 | it("async", (done) => {
29 | preparations();
30 | jetpack.listAsync("dir").then((listAsync) => {
31 | expectations(listAsync);
32 | done();
33 | });
34 | });
35 | });
36 |
37 | describe("lists CWD if no path parameter passed", () => {
38 | const preparations = () => {
39 | fse.mkdirsSync("dir/a");
40 | fse.outputFileSync("dir/b", "");
41 | };
42 |
43 | const expectations = (data: string[]) => {
44 | expect(data).to.eql(["a", "b"]);
45 | };
46 |
47 | it("sync", () => {
48 | const jetContext = jetpack.cwd("dir");
49 | preparations();
50 | expectations(jetContext.list());
51 | });
52 |
53 | it("async", (done) => {
54 | const jetContext = jetpack.cwd("dir");
55 | preparations();
56 | jetContext.listAsync().then((list) => {
57 | expectations(list);
58 | done();
59 | });
60 | });
61 | });
62 |
63 | describe("returns undefined if path doesn't exist", () => {
64 | const expectations = (data: any) => {
65 | expect(data).to.equal(undefined);
66 | };
67 |
68 | it("sync", () => {
69 | expectations(jetpack.list("nonexistent"));
70 | });
71 |
72 | it("async", (done) => {
73 | jetpack.listAsync("nonexistent").then((data) => {
74 | expectations(data);
75 | done();
76 | });
77 | });
78 | });
79 |
80 | describe("throws if given path is not a directory", () => {
81 | const preparations = () => {
82 | fse.outputFileSync("file.txt", "abc");
83 | };
84 |
85 | const expectations = (err: any) => {
86 | expect(err.code).to.equal("ENOTDIR");
87 | };
88 |
89 | it("sync", () => {
90 | preparations();
91 | try {
92 | jetpack.list("file.txt");
93 | throw new Error("Expected error to be thrown");
94 | } catch (err) {
95 | expectations(err);
96 | }
97 | });
98 |
99 | it("async", (done) => {
100 | preparations();
101 | jetpack.listAsync("file.txt").catch((err) => {
102 | expectations(err);
103 | done();
104 | });
105 | });
106 | });
107 |
108 | describe("respects internal CWD of jetpack instance", () => {
109 | const preparations = () => {
110 | fse.outputFileSync("a/b/c.txt", "abc");
111 | };
112 |
113 | const expectations = (data: string[]) => {
114 | expect(data).to.eql(["c.txt"]);
115 | };
116 |
117 | it("sync", () => {
118 | const jetContext = jetpack.cwd("a");
119 | preparations();
120 | expectations(jetContext.list("b"));
121 | });
122 |
123 | it("async", (done) => {
124 | const jetContext = jetpack.cwd("a");
125 | preparations();
126 | jetContext.listAsync("b").then((data) => {
127 | expectations(data);
128 | done();
129 | });
130 | });
131 | });
132 |
133 | describe("input validation", () => {
134 | const tests = [
135 | { type: "sync", method: jetpack.list as any, methodName: "list" },
136 | {
137 | type: "async",
138 | method: jetpack.listAsync as any,
139 | methodName: "listAsync",
140 | },
141 | ];
142 |
143 | describe('"path" argument', () => {
144 | tests.forEach((test) => {
145 | it(test.type, () => {
146 | expect(() => {
147 | test.method(true);
148 | }).to.throw(
149 | `Argument "path" passed to ${test.methodName}(path) must be a string or an undefined. Received boolean`
150 | );
151 | });
152 | });
153 | });
154 | });
155 | });
156 |
--------------------------------------------------------------------------------
/spec/log.spec.ts:
--------------------------------------------------------------------------------
1 | import * as util from "util";
2 | import { expect } from "chai";
3 | import * as jetpack from "..";
4 |
5 | // Test for https://github.com/szwacz/fs-jetpack/issues/29
6 | describe("console.log", () => {
7 | it("can be printed by console.log", () => {
8 | expect(() => {
9 | console.log(jetpack);
10 | }).not.to.throw();
11 | });
12 | });
13 |
--------------------------------------------------------------------------------
/spec/move.spec.ts:
--------------------------------------------------------------------------------
1 | import * as fse from "fs-extra";
2 | import { expect } from "chai";
3 | import path from "./assert_path";
4 | import helper from "./helper";
5 | import * as jetpack from "..";
6 |
7 | describe("move", () => {
8 | beforeEach(helper.setCleanTestCwd);
9 | afterEach(helper.switchBackToCorrectCwd);
10 |
11 | describe("moves file", () => {
12 | const preparations = () => {
13 | fse.outputFileSync("a/b.txt", "abc");
14 | };
15 |
16 | const expectations = () => {
17 | path("a/b.txt").shouldNotExist();
18 | path("c.txt").shouldBeFileWithContent("abc");
19 | };
20 |
21 | it("sync", () => {
22 | preparations();
23 | jetpack.move("a/b.txt", "c.txt");
24 | expectations();
25 | });
26 |
27 | it("async", (done) => {
28 | preparations();
29 | jetpack.moveAsync("a/b.txt", "c.txt").then(() => {
30 | expectations();
31 | done();
32 | });
33 | });
34 | });
35 |
36 | describe("moves directory", () => {
37 | const preparations = () => {
38 | fse.outputFileSync("a/b/c.txt", "abc");
39 | fse.mkdirsSync("x");
40 | };
41 |
42 | const expectations = () => {
43 | path("a").shouldNotExist();
44 | path("x/y/b/c.txt").shouldBeFileWithContent("abc");
45 | };
46 |
47 | it("sync", () => {
48 | preparations();
49 | jetpack.move("a", "x/y");
50 | expectations();
51 | });
52 |
53 | it("async", (done) => {
54 | preparations();
55 | jetpack.moveAsync("a", "x/y").then(() => {
56 | expectations();
57 | done();
58 | });
59 | });
60 | });
61 |
62 | describe("creates nonexistent directories for destination path", () => {
63 | const preparations = () => {
64 | fse.outputFileSync("a.txt", "abc");
65 | };
66 |
67 | const expectations = () => {
68 | path("a.txt").shouldNotExist();
69 | path("a/b/z.txt").shouldBeFileWithContent("abc");
70 | };
71 |
72 | it("sync", () => {
73 | preparations();
74 | jetpack.move("a.txt", "a/b/z.txt");
75 | expectations();
76 | });
77 |
78 | it("async", (done) => {
79 | preparations();
80 | jetpack.moveAsync("a.txt", "a/b/z.txt").then(() => {
81 | expectations();
82 | done();
83 | });
84 | });
85 | });
86 |
87 | describe("generates nice error when source path doesn't exist", () => {
88 | const expectations = (err: any) => {
89 | expect(err.code).to.equal("ENOENT");
90 | expect(err.message).to.have.string("Path to move doesn't exist");
91 | };
92 |
93 | it("sync", () => {
94 | try {
95 | jetpack.move("a", "b");
96 | throw new Error("Expected error to be thrown");
97 | } catch (err) {
98 | expectations(err);
99 | }
100 | });
101 |
102 | it("async", (done) => {
103 | jetpack.moveAsync("a", "b").catch((err) => {
104 | expectations(err);
105 | done();
106 | });
107 | });
108 | });
109 |
110 | describe("overwriting behaviour", () => {
111 | describe("does not overwrite by default", () => {
112 | const preparations = () => {
113 | fse.outputFileSync("file1.txt", "abc");
114 | fse.outputFileSync("file2.txt", "xyz");
115 | };
116 |
117 | const expectations = (err: any) => {
118 | expect(err.code).to.equal("EEXIST");
119 | expect(err.message).to.have.string("Destination path already exists");
120 | path("file2.txt").shouldBeFileWithContent("xyz");
121 | };
122 |
123 | it("sync", () => {
124 | preparations();
125 | try {
126 | jetpack.move("file1.txt", "file2.txt");
127 | throw new Error("Expected error to be thrown");
128 | } catch (err) {
129 | expectations(err);
130 | }
131 | });
132 |
133 | it("async", (done) => {
134 | preparations();
135 | jetpack.moveAsync("file1.txt", "file2.txt").catch((err) => {
136 | expectations(err);
137 | done();
138 | });
139 | });
140 | });
141 |
142 | describe("overwrites if it was specified", () => {
143 | const preparations = () => {
144 | fse.outputFileSync("file1.txt", "abc");
145 | fse.outputFileSync("file2.txt", "xyz");
146 | };
147 |
148 | const expectations = () => {
149 | path("file1.txt").shouldNotExist();
150 | path("file2.txt").shouldBeFileWithContent("abc");
151 | };
152 |
153 | it("sync", () => {
154 | preparations();
155 | jetpack.move("file1.txt", "file2.txt", { overwrite: true });
156 | expectations();
157 | });
158 |
159 | it("async", (done) => {
160 | preparations();
161 | jetpack
162 | .moveAsync("file1.txt", "file2.txt", { overwrite: true })
163 | .then(() => {
164 | expectations();
165 | done();
166 | });
167 | });
168 | });
169 |
170 | describe("can overwrite a directory", () => {
171 | const preparations = () => {
172 | fse.outputFileSync("file1.txt", "abc");
173 | fse.mkdirsSync("dir");
174 | };
175 |
176 | const expectations = () => {
177 | path("file1.txt").shouldNotExist();
178 | path("dir").shouldBeFileWithContent("abc");
179 | };
180 |
181 | it("sync", () => {
182 | preparations();
183 | jetpack.move("file1.txt", "dir", { overwrite: true });
184 | expectations();
185 | });
186 |
187 | it("async", (done) => {
188 | preparations();
189 | jetpack.moveAsync("file1.txt", "dir", { overwrite: true }).then(() => {
190 | expectations();
191 | done();
192 | });
193 | });
194 | });
195 | });
196 |
197 | describe("respects internal CWD of jetpack instance", () => {
198 | const preparations = () => {
199 | fse.outputFileSync("a/b.txt", "abc");
200 | };
201 |
202 | const expectations = () => {
203 | path("a/b.txt").shouldNotExist();
204 | path("a/x.txt").shouldBeFileWithContent("abc");
205 | };
206 |
207 | it("sync", () => {
208 | const jetContext = jetpack.cwd("a");
209 | preparations();
210 | jetContext.move("b.txt", "x.txt");
211 | expectations();
212 | });
213 |
214 | it("async", (done) => {
215 | const jetContext = jetpack.cwd("a");
216 | preparations();
217 | jetContext.moveAsync("b.txt", "x.txt").then(() => {
218 | expectations();
219 | done();
220 | });
221 | });
222 | });
223 |
224 | describe("input validation", () => {
225 | const tests = [
226 | { type: "sync", method: jetpack.move as any, methodName: "move" },
227 | {
228 | type: "async",
229 | method: jetpack.moveAsync as any,
230 | methodName: "moveAsync",
231 | },
232 | ];
233 |
234 | describe('"from" argument', () => {
235 | tests.forEach((test) => {
236 | it(test.type, () => {
237 | expect(() => {
238 | test.method(undefined, "xyz");
239 | }).to.throw(
240 | `Argument "from" passed to ${test.methodName}(from, to, [options]) must be a string. Received undefined`
241 | );
242 | });
243 | });
244 | });
245 |
246 | describe('"to" argument', () => {
247 | tests.forEach((test) => {
248 | it(test.type, () => {
249 | expect(() => {
250 | test.method("abc", undefined);
251 | }).to.throw(
252 | `Argument "to" passed to ${test.methodName}(from, to, [options]) must be a string. Received undefined`
253 | );
254 | });
255 | });
256 | });
257 |
258 | describe('"options" object', () => {
259 | describe('"overwrite" argument', () => {
260 | tests.forEach((test) => {
261 | it(test.type, () => {
262 | expect(() => {
263 | test.method("abc", "xyz", { overwrite: 1 });
264 | }).to.throw(
265 | `Argument "options.overwrite" passed to ${test.methodName}(from, to, [options]) must be a boolean. Received number`
266 | );
267 | });
268 | });
269 | });
270 | });
271 | });
272 | });
273 |
--------------------------------------------------------------------------------
/spec/path.spec.ts:
--------------------------------------------------------------------------------
1 | import * as pathUtil from "path";
2 | import { expect } from "chai";
3 | import * as jetpack from "..";
4 |
5 | describe("path", () => {
6 | it("if no parameters passed returns same path as cwd()", () => {
7 | expect(jetpack.path()).to.equal(jetpack.cwd());
8 | expect(jetpack.path("")).to.equal(jetpack.cwd());
9 | expect(jetpack.path(".")).to.equal(jetpack.cwd());
10 | });
11 |
12 | it("is absolute if prepending slash present", () => {
13 | expect(jetpack.path("/blah")).to.equal(pathUtil.resolve("/blah"));
14 | });
15 |
16 | it("resolves to CWD path of this jetpack instance", () => {
17 | const a = pathUtil.join(jetpack.cwd(), "a");
18 | // Create jetpack instance with other CWD
19 | const jetpackSubdir = jetpack.cwd("subdir");
20 | const b = pathUtil.join(jetpack.cwd(), "subdir", "b");
21 | expect(jetpack.path("a")).to.equal(a);
22 | expect(jetpackSubdir.path("b")).to.equal(b);
23 | });
24 |
25 | it("can take unlimited number of arguments as path parts", () => {
26 | const abc = pathUtil.join(jetpack.cwd(), "a", "b", "c");
27 | expect(jetpack.path("a", "b", "c")).to.equal(abc);
28 | });
29 | });
30 |
--------------------------------------------------------------------------------
/spec/read.spec.ts:
--------------------------------------------------------------------------------
1 | import * as fse from "fs-extra";
2 | import { expect } from "chai";
3 | import path from "./assert_path";
4 | import helper from "./helper";
5 | import * as jetpack from "..";
6 |
7 | describe("read", () => {
8 | beforeEach(helper.setCleanTestCwd);
9 | afterEach(helper.switchBackToCorrectCwd);
10 |
11 | describe("reads file as a string", () => {
12 | const preparations = () => {
13 | fse.outputFileSync("file.txt", "abc");
14 | };
15 |
16 | const expectations = (content: any) => {
17 | expect(content).to.equal("abc");
18 | };
19 |
20 | it("sync", () => {
21 | preparations();
22 | expectations(jetpack.read("file.txt")); // defaults to 'utf8'
23 | expectations(jetpack.read("file.txt", "utf8")); // explicitly specified
24 | });
25 |
26 | it("async", (done) => {
27 | preparations();
28 | jetpack
29 | .readAsync("file.txt") // defaults to 'utf8'
30 | .then((content) => {
31 | expectations(content);
32 | return jetpack.readAsync("file.txt", "utf8"); // explicitly said
33 | })
34 | .then((content) => {
35 | expectations(content);
36 | done();
37 | });
38 | });
39 | });
40 |
41 | describe("reads file as a Buffer", () => {
42 | const preparations = () => {
43 | fse.outputFileSync("file.txt", new Buffer([11, 22]));
44 | };
45 |
46 | const expectations = (content: Buffer) => {
47 | expect(Buffer.isBuffer(content)).to.equal(true);
48 | expect(content.length).to.equal(2);
49 | expect(content[0]).to.equal(11);
50 | expect(content[1]).to.equal(22);
51 | };
52 |
53 | it("sync", () => {
54 | preparations();
55 | expectations(jetpack.read("file.txt", "buffer"));
56 | });
57 |
58 | it("async", (done) => {
59 | preparations();
60 | jetpack.readAsync("file.txt", "buffer").then((content) => {
61 | expectations(content);
62 | done();
63 | });
64 | });
65 | });
66 |
67 | describe("reads file as JSON", () => {
68 | const obj = {
69 | utf8: "ąćłźż",
70 | };
71 |
72 | const preparations = () => {
73 | fse.outputFileSync("file.json", JSON.stringify(obj));
74 | };
75 |
76 | const expectations = (content: any) => {
77 | expect(content).to.eql(obj);
78 | };
79 |
80 | it("sync", () => {
81 | preparations();
82 | expectations(jetpack.read("file.json", "json"));
83 | });
84 |
85 | it("async", (done) => {
86 | preparations();
87 | jetpack.readAsync("file.json", "json").then((content) => {
88 | expectations(content);
89 | done();
90 | });
91 | });
92 | });
93 |
94 | describe("gives nice error message when JSON parsing failed", () => {
95 | const preparations = () => {
96 | fse.outputFileSync("file.json", '{ "abc: 123 }'); // Malformed JSON
97 | };
98 |
99 | const expectations = (err: any) => {
100 | expect(err.message).to.have.string("JSON parsing failed while reading");
101 | };
102 |
103 | it("sync", () => {
104 | preparations();
105 | try {
106 | jetpack.read("file.json", "json");
107 | throw new Error("Expected error to be thrown");
108 | } catch (err) {
109 | expectations(err);
110 | }
111 | });
112 |
113 | it("async", (done) => {
114 | preparations();
115 | jetpack.readAsync("file.json", "json").catch((err) => {
116 | expectations(err);
117 | done();
118 | });
119 | });
120 | });
121 |
122 | describe("reads file as JSON with Date parsing", () => {
123 | const obj = {
124 | utf8: "ąćłźż",
125 | date: new Date(),
126 | };
127 |
128 | const preparations = () => {
129 | fse.outputFileSync("file.json", JSON.stringify(obj));
130 | };
131 |
132 | const expectations = (content: any) => {
133 | expect(content).to.eql(obj);
134 | };
135 |
136 | it("sync", () => {
137 | preparations();
138 | expectations(jetpack.read("file.json", "jsonWithDates"));
139 | });
140 |
141 | it("async", (done) => {
142 | preparations();
143 | jetpack.readAsync("file.json", "jsonWithDates").then((content) => {
144 | expectations(content);
145 | done();
146 | });
147 | });
148 | });
149 |
150 | describe("returns undefined if file doesn't exist", () => {
151 | const expectations = (content: any) => {
152 | expect(content).to.equal(undefined);
153 | };
154 |
155 | it("sync", () => {
156 | expectations(jetpack.read("nonexistent.txt"));
157 | expectations(jetpack.read("nonexistent.txt", "json"));
158 | expectations(jetpack.read("nonexistent.txt", "buffer"));
159 | });
160 |
161 | it("async", (done) => {
162 | jetpack
163 | .readAsync("nonexistent.txt")
164 | .then((content) => {
165 | expectations(content);
166 | return jetpack.readAsync("nonexistent.txt", "json");
167 | })
168 | .then((content) => {
169 | expectations(content);
170 | return jetpack.readAsync("nonexistent.txt", "buffer");
171 | })
172 | .then((content) => {
173 | expectations(content);
174 | done();
175 | });
176 | });
177 | });
178 |
179 | describe("throws if given path is a directory", () => {
180 | const preparations = () => {
181 | fse.mkdirsSync("dir");
182 | };
183 |
184 | const expectations = (err: any) => {
185 | expect(err.code).to.equal("EISDIR");
186 | };
187 |
188 | it("sync", () => {
189 | preparations();
190 | try {
191 | jetpack.read("dir");
192 | throw new Error("Expected error to be thrown");
193 | } catch (err) {
194 | expectations(err);
195 | }
196 | });
197 |
198 | it("async", (done) => {
199 | preparations();
200 | jetpack.readAsync("dir").catch((err) => {
201 | expectations(err);
202 | done();
203 | });
204 | });
205 | });
206 |
207 | describe("respects internal CWD of jetpack instance", () => {
208 | const preparations = () => {
209 | fse.outputFileSync("a/file.txt", "abc");
210 | };
211 |
212 | const expectations = (data: any) => {
213 | expect(data).to.equal("abc");
214 | };
215 |
216 | it("sync", () => {
217 | const jetContext = jetpack.cwd("a");
218 | preparations();
219 | expectations(jetContext.read("file.txt"));
220 | });
221 |
222 | it("async", (done) => {
223 | const jetContext = jetpack.cwd("a");
224 | preparations();
225 | jetContext.readAsync("file.txt").then((data) => {
226 | expectations(data);
227 | done();
228 | });
229 | });
230 | });
231 |
232 | describe("input validation", () => {
233 | const tests = [
234 | { type: "sync", method: jetpack.read as any, methodName: "read" },
235 | {
236 | type: "async",
237 | method: jetpack.readAsync as any,
238 | methodName: "readAsync",
239 | },
240 | ];
241 |
242 | describe('"path" argument', () => {
243 | tests.forEach((test) => {
244 | it(test.type, () => {
245 | expect(() => {
246 | test.method(undefined, "xyz");
247 | }).to.throw(
248 | `Argument "path" passed to ${test.methodName}(path, returnAs) must be a string. Received undefined`
249 | );
250 | });
251 | });
252 | });
253 |
254 | describe('"returnAs" argument', () => {
255 | tests.forEach((test) => {
256 | it(test.type, () => {
257 | expect(() => {
258 | test.method("abc", true);
259 | }).to.throw(
260 | `Argument "returnAs" passed to ${test.methodName}(path, returnAs) must be a string or an undefined. Received boolean`
261 | );
262 | expect(() => {
263 | test.method("abc", "foo");
264 | }).to.throw(
265 | `Argument "returnAs" passed to ${test.methodName}(path, returnAs) must have one of values: utf8, buffer, json, jsonWithDates`
266 | );
267 | });
268 | });
269 | });
270 | });
271 | });
272 |
--------------------------------------------------------------------------------
/spec/remove.spec.ts:
--------------------------------------------------------------------------------
1 | import * as fse from "fs-extra";
2 | import { expect } from "chai";
3 | import path from "./assert_path";
4 | import helper from "./helper";
5 | import * as jetpack from "..";
6 |
7 | describe("remove", () => {
8 | beforeEach(helper.setCleanTestCwd);
9 | afterEach(helper.switchBackToCorrectCwd);
10 |
11 | describe("doesn't throw if path already doesn't exist", () => {
12 | it("sync", () => {
13 | jetpack.remove("dir");
14 | });
15 |
16 | it("async", (done) => {
17 | jetpack.removeAsync("dir").then(() => {
18 | done();
19 | });
20 | });
21 | });
22 |
23 | describe("should delete file", () => {
24 | const preparations = () => {
25 | fse.outputFileSync("file.txt", "abc");
26 | };
27 |
28 | const expectations = () => {
29 | path("file.txt").shouldNotExist();
30 | };
31 |
32 | it("sync", () => {
33 | preparations();
34 | jetpack.remove("file.txt");
35 | expectations();
36 | });
37 |
38 | it("async", (done) => {
39 | preparations();
40 | jetpack.removeAsync("file.txt").then(() => {
41 | expectations();
42 | done();
43 | });
44 | });
45 | });
46 |
47 | describe("removes directory with stuff inside", () => {
48 | const preparations = () => {
49 | fse.mkdirsSync("a/b/c");
50 | fse.outputFileSync("a/f.txt", "abc");
51 | fse.outputFileSync("a/b/f.txt", "123");
52 | };
53 |
54 | const expectations = () => {
55 | path("a").shouldNotExist();
56 | };
57 |
58 | it("sync", () => {
59 | preparations();
60 | jetpack.remove("a");
61 | expectations();
62 | });
63 |
64 | it("async", (done) => {
65 | preparations();
66 | jetpack.removeAsync("a").then(() => {
67 | expectations();
68 | done();
69 | });
70 | });
71 | });
72 |
73 | describe("will retry attempt if file is locked", () => {
74 | const preparations = () => {
75 | fse.mkdirsSync("a/b/c");
76 | fse.outputFileSync("a/f.txt", "abc");
77 | fse.outputFileSync("a/b/f.txt", "123");
78 | };
79 |
80 | const expectations = () => {
81 | path("a").shouldNotExist();
82 | };
83 |
84 | it("async", (done) => {
85 | preparations();
86 |
87 | fse.open("a/f.txt", "w", (err, fd) => {
88 | if (err) {
89 | done(err);
90 | } else {
91 | // Unlock the file after some time.
92 | setTimeout(() => {
93 | fse.close(fd);
94 | }, 150);
95 |
96 | jetpack
97 | .removeAsync("a")
98 | .then(() => {
99 | expectations();
100 | done();
101 | })
102 | .catch(done);
103 | }
104 | });
105 | });
106 | });
107 |
108 | describe("respects internal CWD of jetpack instance", () => {
109 | const preparations = () => {
110 | fse.outputFileSync("a/b/c.txt", "123");
111 | };
112 |
113 | const expectations = () => {
114 | path("a").shouldBeDirectory();
115 | path("a/b").shouldNotExist();
116 | };
117 |
118 | it("sync", () => {
119 | const jetContext = jetpack.cwd("a");
120 | preparations();
121 | jetContext.remove("b");
122 | expectations();
123 | });
124 |
125 | it("async", (done) => {
126 | const jetContext = jetpack.cwd("a");
127 | preparations();
128 | jetContext.removeAsync("b").then(() => {
129 | expectations();
130 | done();
131 | });
132 | });
133 | });
134 |
135 | describe("can be called with no parameters, what will remove CWD directory", () => {
136 | const preparations = () => {
137 | fse.outputFileSync("a/b/c.txt", "abc");
138 | };
139 |
140 | const expectations = () => {
141 | path("a").shouldNotExist();
142 | };
143 |
144 | it("sync", () => {
145 | const jetContext = jetpack.cwd("a");
146 | preparations();
147 | jetContext.remove();
148 | expectations();
149 | });
150 |
151 | it("async", (done) => {
152 | const jetContext = jetpack.cwd("a");
153 | preparations();
154 | jetContext.removeAsync().then(() => {
155 | expectations();
156 | done();
157 | });
158 | });
159 | });
160 |
161 | describe("removes only symlinks, never real content where symlinks point", () => {
162 | const preparations = () => {
163 | fse.outputFileSync("have_to_stay_file", "abc");
164 | fse.mkdirsSync("to_remove");
165 | fse.symlinkSync("../have_to_stay_file", "to_remove/symlink");
166 | // Make sure we symlinked it properly.
167 | expect(fse.readFileSync("to_remove/symlink", "utf8")).to.equal("abc");
168 | };
169 |
170 | const expectations = () => {
171 | path("have_to_stay_file").shouldBeFileWithContent("abc");
172 | path("to_remove").shouldNotExist();
173 | };
174 |
175 | it("sync", () => {
176 | preparations();
177 | jetpack.remove("to_remove");
178 | expectations();
179 | });
180 |
181 | it("async", (done) => {
182 | preparations();
183 | jetpack.removeAsync("to_remove").then(() => {
184 | expectations();
185 | done();
186 | });
187 | });
188 | });
189 |
190 | describe("input validation", () => {
191 | const tests = [
192 | { type: "sync", method: jetpack.remove as any, methodName: "remove" },
193 | {
194 | type: "async",
195 | method: jetpack.removeAsync as any,
196 | methodName: "removeAsync",
197 | },
198 | ];
199 |
200 | describe('"path" argument', () => {
201 | tests.forEach((test) => {
202 | it(test.type, () => {
203 | expect(() => {
204 | test.method(true);
205 | }).to.throw(
206 | `Argument "path" passed to ${test.methodName}([path]) must be a string or an undefined. Received boolean`
207 | );
208 | });
209 | });
210 | });
211 | });
212 | });
213 |
--------------------------------------------------------------------------------
/spec/rename.spec.ts:
--------------------------------------------------------------------------------
1 | import * as pathUtil from "path";
2 | import * as fse from "fs-extra";
3 | import { expect } from "chai";
4 | import path from "./assert_path";
5 | import helper from "./helper";
6 | import * as jetpack from "..";
7 |
8 | describe("rename", () => {
9 | beforeEach(helper.setCleanTestCwd);
10 | afterEach(helper.switchBackToCorrectCwd);
11 |
12 | describe("renames file", () => {
13 | const preparations = () => {
14 | fse.outputFileSync("a/b.txt", "abc");
15 | };
16 |
17 | const expectations = () => {
18 | path("a/b.txt").shouldNotExist();
19 | path("a/x.txt").shouldBeFileWithContent("abc");
20 | };
21 |
22 | it("sync", () => {
23 | preparations();
24 | jetpack.rename("a/b.txt", "x.txt");
25 | expectations();
26 | });
27 |
28 | it("async", (done) => {
29 | preparations();
30 | jetpack.renameAsync("a/b.txt", "x.txt").then(() => {
31 | expectations();
32 | done();
33 | });
34 | });
35 | });
36 |
37 | describe("renames directory", () => {
38 | const preparations = () => {
39 | fse.outputFileSync("a/b/c.txt", "abc");
40 | };
41 |
42 | const expectations = () => {
43 | path("a/b").shouldNotExist();
44 | path("a/x").shouldBeDirectory();
45 | };
46 |
47 | it("sync", () => {
48 | preparations();
49 | jetpack.rename("a/b", "x");
50 | expectations();
51 | });
52 |
53 | it("async", (done) => {
54 | preparations();
55 | jetpack.renameAsync("a/b", "x").then(() => {
56 | expectations();
57 | done();
58 | });
59 | });
60 | });
61 |
62 | describe("overwriting behaviour", () => {
63 | describe("does not overwrite by default", () => {
64 | const preparations = () => {
65 | fse.outputFileSync("file1.txt", "abc");
66 | fse.outputFileSync("file2.txt", "xyz");
67 | };
68 |
69 | const expectations = (err: any) => {
70 | expect(err.code).to.equal("EEXIST");
71 | expect(err.message).to.have.string("Destination path already exists");
72 | path("file2.txt").shouldBeFileWithContent("xyz");
73 | };
74 |
75 | it("sync", () => {
76 | preparations();
77 | try {
78 | jetpack.rename("file1.txt", "file2.txt");
79 | throw new Error("Expected error to be thrown");
80 | } catch (err) {
81 | expectations(err);
82 | }
83 | });
84 |
85 | it("async", (done) => {
86 | preparations();
87 | jetpack.renameAsync("file1.txt", "file2.txt").catch((err) => {
88 | expectations(err);
89 | done();
90 | });
91 | });
92 | });
93 |
94 | describe("overwrites if it was specified", () => {
95 | const preparations = () => {
96 | fse.outputFileSync("file1.txt", "abc");
97 | fse.outputFileSync("file2.txt", "xyz");
98 | };
99 |
100 | const expectations = () => {
101 | path("file1.txt").shouldNotExist();
102 | path("file2.txt").shouldBeFileWithContent("abc");
103 | };
104 |
105 | it("sync", () => {
106 | preparations();
107 | jetpack.rename("file1.txt", "file2.txt", { overwrite: true });
108 | expectations();
109 | });
110 |
111 | it("async", (done) => {
112 | preparations();
113 | jetpack
114 | .renameAsync("file1.txt", "file2.txt", { overwrite: true })
115 | .then(() => {
116 | expectations();
117 | done();
118 | });
119 | });
120 | });
121 | });
122 |
123 | describe("can overwrite a directory", () => {
124 | const preparations = () => {
125 | fse.outputFileSync("file1.txt", "abc");
126 | fse.mkdirsSync("dir");
127 | };
128 |
129 | const expectations = () => {
130 | path("file1.txt").shouldNotExist();
131 | path("dir").shouldBeFileWithContent("abc");
132 | };
133 |
134 | it("sync", () => {
135 | preparations();
136 | jetpack.rename("file1.txt", "dir", { overwrite: true });
137 | expectations();
138 | });
139 |
140 | it("async", (done) => {
141 | preparations();
142 | jetpack.renameAsync("file1.txt", "dir", { overwrite: true }).then(() => {
143 | expectations();
144 | done();
145 | });
146 | });
147 | });
148 |
149 | describe("respects internal CWD of jetpack instance", () => {
150 | const preparations = () => {
151 | fse.outputFileSync("a/b/c.txt", "abc");
152 | };
153 |
154 | const expectations = () => {
155 | path("a/b").shouldNotExist();
156 | path("a/x").shouldBeDirectory();
157 | };
158 |
159 | it("sync", () => {
160 | const jetContext = jetpack.cwd("a");
161 | preparations();
162 | jetContext.rename("b", "x");
163 | expectations();
164 | });
165 |
166 | it("async", (done) => {
167 | const jetContext = jetpack.cwd("a");
168 | preparations();
169 | jetContext.renameAsync("b", "x").then(() => {
170 | expectations();
171 | done();
172 | });
173 | });
174 | });
175 |
176 | describe("input validation", () => {
177 | const tests = [
178 | { type: "sync", method: jetpack.rename as any, methodName: "rename" },
179 | {
180 | type: "async",
181 | method: jetpack.renameAsync as any,
182 | methodName: "renameAsync",
183 | },
184 | ];
185 |
186 | describe('"path" argument', () => {
187 | tests.forEach((test) => {
188 | it(test.type, () => {
189 | expect(() => {
190 | test.method(undefined, "xyz");
191 | }).to.throw(
192 | `Argument "path" passed to ${test.methodName}(path, newName, [options]) must be a string. Received undefined`
193 | );
194 | });
195 | });
196 | });
197 |
198 | describe('"newName" argument', () => {
199 | describe("type check", () => {
200 | tests.forEach((test) => {
201 | it(test.type, () => {
202 | expect(() => {
203 | test.method("abc", undefined);
204 | }).to.throw(
205 | `Argument "newName" passed to ${test.methodName}(path, newName, [options]) must be a string. Received undefined`
206 | );
207 | });
208 | });
209 | });
210 |
211 | describe("shouldn't be path, just a filename", () => {
212 | const pathToTest = pathUtil.join("new-name", "with-a-slash");
213 | tests.forEach((test) => {
214 | it(test.type, () => {
215 | expect(() => {
216 | test.method("abc", pathToTest);
217 | }).to.throw(
218 | `Argument "newName" passed to ${test.methodName}(path, newName, [options]) should be a filename, not a path. Received "${pathToTest}"`
219 | );
220 | });
221 | });
222 | });
223 | });
224 |
225 | describe('"options" object', () => {
226 | describe('"overwrite" argument', () => {
227 | tests.forEach((test) => {
228 | it(test.type, () => {
229 | expect(() => {
230 | test.method("abc", "xyz", { overwrite: 1 });
231 | }).to.throw(
232 | `Argument "options.overwrite" passed to ${test.methodName}(path, newName, [options]) must be a boolean. Received number`
233 | );
234 | });
235 | });
236 | });
237 | });
238 | });
239 | });
240 |
--------------------------------------------------------------------------------
/spec/streams.spec.ts:
--------------------------------------------------------------------------------
1 | import * as fse from "fs-extra";
2 | import { expect } from "chai";
3 | import path from "./assert_path";
4 | import helper from "./helper";
5 | import * as jetpack from "..";
6 |
7 | describe("streams", () => {
8 | beforeEach(helper.setCleanTestCwd);
9 | afterEach(helper.switchBackToCorrectCwd);
10 |
11 | it("exposes vanilla stream methods", (done) => {
12 | fse.outputFileSync("a.txt", "abc");
13 |
14 | const input = jetpack.createReadStream("a.txt");
15 | const output = jetpack.createWriteStream("b.txt");
16 | output.on("finish", () => {
17 | path("b.txt").shouldBeFileWithContent("abc");
18 | done();
19 | });
20 | input.pipe(output);
21 | });
22 |
23 | it("stream methods respect jetpack internal CWD", (done) => {
24 | const dir = jetpack.cwd("dir");
25 |
26 | fse.outputFileSync("dir/a.txt", "abc");
27 |
28 | const input = dir.createReadStream("a.txt");
29 | const output = dir.createWriteStream("b.txt");
30 | output.on("finish", () => {
31 | path("dir/b.txt").shouldBeFileWithContent("abc");
32 | done();
33 | });
34 | input.pipe(output);
35 | });
36 | });
37 |
--------------------------------------------------------------------------------
/spec/symlink.spec.ts:
--------------------------------------------------------------------------------
1 | import * as fse from "fs-extra";
2 | import { expect } from "chai";
3 | import path from "./assert_path";
4 | import helper from "./helper";
5 | import * as jetpack from "..";
6 |
7 | describe("symlink", () => {
8 | beforeEach(helper.setCleanTestCwd);
9 | afterEach(helper.switchBackToCorrectCwd);
10 |
11 | describe("can create a symlink", () => {
12 | const expectations = () => {
13 | expect(fse.lstatSync("symlink").isSymbolicLink()).to.equal(true);
14 | expect(fse.readlinkSync("symlink")).to.equal("some_path");
15 | };
16 |
17 | it("sync", () => {
18 | jetpack.symlink("some_path", "symlink");
19 | expectations();
20 | });
21 |
22 | it("async", (done) => {
23 | jetpack.symlinkAsync("some_path", "symlink").then(() => {
24 | expectations();
25 | done();
26 | });
27 | });
28 | });
29 |
30 | describe("can create nonexistent parent directories", () => {
31 | const expectations = () => {
32 | expect(fse.lstatSync("a/b/symlink").isSymbolicLink()).to.equal(true);
33 | };
34 |
35 | it("sync", () => {
36 | jetpack.symlink("whatever", "a/b/symlink");
37 | expectations();
38 | });
39 |
40 | it("async", (done) => {
41 | jetpack.symlinkAsync("whatever", "a/b/symlink").then(() => {
42 | expectations();
43 | done();
44 | });
45 | });
46 | });
47 |
48 | describe("respects internal CWD of jetpack instance", () => {
49 | const preparations = () => {
50 | fse.mkdirsSync("a/b");
51 | };
52 |
53 | const expectations = () => {
54 | expect(fse.lstatSync("a/b/symlink").isSymbolicLink()).to.equal(true);
55 | };
56 |
57 | it("sync", () => {
58 | const jetContext = jetpack.cwd("a/b");
59 | preparations();
60 | jetContext.symlink("whatever", "symlink");
61 | expectations();
62 | });
63 |
64 | it("async", (done) => {
65 | const jetContext = jetpack.cwd("a/b");
66 | preparations();
67 | jetContext.symlinkAsync("whatever", "symlink").then(() => {
68 | expectations();
69 | done();
70 | });
71 | });
72 | });
73 |
74 | describe("input validation", () => {
75 | const tests = [
76 | { type: "sync", method: jetpack.symlink as any, methodName: "symlink" },
77 | {
78 | type: "async",
79 | method: jetpack.symlinkAsync as any,
80 | methodName: "symlinkAsync",
81 | },
82 | ];
83 |
84 | describe('"symlinkValue" argument', () => {
85 | tests.forEach((test) => {
86 | it(test.type, () => {
87 | expect(() => {
88 | test.method(undefined, "abc");
89 | }).to.throw(
90 | `Argument "symlinkValue" passed to ${test.methodName}(symlinkValue, path) must be a string. Received undefined`
91 | );
92 | });
93 | });
94 | });
95 |
96 | describe('"path" argument', () => {
97 | tests.forEach((test) => {
98 | it(test.type, () => {
99 | expect(() => {
100 | test.method("xyz", undefined);
101 | }).to.throw(
102 | `Argument "path" passed to ${test.methodName}(symlinkValue, path) must be a string. Received undefined`
103 | );
104 | });
105 | });
106 | });
107 | });
108 | });
109 |
--------------------------------------------------------------------------------
/spec/tmp_dir.spec.ts:
--------------------------------------------------------------------------------
1 | import * as fse from "fs-extra";
2 | import { expect } from "chai";
3 | import * as pathUtil from "path";
4 | import * as os from "os";
5 | import path from "./assert_path";
6 | import helper from "./helper";
7 | import * as jetpack from "..";
8 | import { FSJetpack } from "../types";
9 |
10 | describe("tmpDir", () => {
11 | beforeEach(helper.setCleanTestCwd);
12 | afterEach(helper.switchBackToCorrectCwd);
13 |
14 | describe("creates temporary directory", () => {
15 | const expectations = (jetpackContext: FSJetpack) => {
16 | path(jetpackContext.path()).shouldBeDirectory();
17 | expect(jetpackContext.path().startsWith(os.tmpdir())).to.equal(true);
18 | expect(jetpackContext.path()).to.match(/(\/|\\)[0-9a-f]+$/);
19 | };
20 |
21 | it("sync", () => {
22 | expectations(jetpack.tmpDir());
23 | });
24 |
25 | it("async", (done) => {
26 | jetpack
27 | .tmpDirAsync()
28 | .then((jetpackContext) => {
29 | expectations(jetpackContext);
30 | done();
31 | })
32 | .catch(done);
33 | });
34 | });
35 |
36 | describe("directory name can be prefixed", () => {
37 | const expectations = (jetpackContext: FSJetpack) => {
38 | path(jetpackContext.path()).shouldBeDirectory();
39 | expect(jetpackContext.path().startsWith(os.tmpdir())).to.equal(true);
40 | expect(jetpackContext.path()).to.match(/(\/|\\)abc_[0-9a-f]+$/);
41 | };
42 |
43 | it("sync", () => {
44 | expectations(jetpack.tmpDir({ prefix: "abc_" }));
45 | });
46 |
47 | it("async", (done) => {
48 | jetpack
49 | .tmpDirAsync({ prefix: "abc_" })
50 | .then((jetpackContext) => {
51 | expectations(jetpackContext);
52 | done();
53 | })
54 | .catch(done);
55 | });
56 | });
57 |
58 | describe("directory can be created in any base directory", () => {
59 | const expectations = (jetpackContext: FSJetpack) => {
60 | path(jetpackContext.path()).shouldBeDirectory();
61 | expect(jetpackContext.path().startsWith(jetpack.cwd())).to.equal(true);
62 | };
63 |
64 | it("sync", () => {
65 | expectations(jetpack.tmpDir({ basePath: "." }));
66 | });
67 |
68 | it("async", (done) => {
69 | jetpack
70 | .tmpDirAsync({ basePath: "." })
71 | .then((jetpackContext) => {
72 | expectations(jetpackContext);
73 | done();
74 | })
75 | .catch(done);
76 | });
77 | });
78 |
79 | describe("if base directory doesn't exist it will be created", () => {
80 | const expectations = (jetpackContext: FSJetpack) => {
81 | path(jetpackContext.path()).shouldBeDirectory();
82 | expect(jetpackContext.path().startsWith(jetpack.path("abc"))).to.equal(
83 | true
84 | );
85 | };
86 |
87 | it("sync", () => {
88 | expectations(jetpack.tmpDir({ basePath: "abc" }));
89 | });
90 |
91 | it("async", (done) => {
92 | jetpack
93 | .tmpDirAsync({ basePath: "abc" })
94 | .then((jetpackContext) => {
95 | expectations(jetpackContext);
96 | done();
97 | })
98 | .catch(done);
99 | });
100 | });
101 |
102 | describe("input validation", () => {
103 | const tests = [
104 | { type: "sync", method: jetpack.tmpDir as any, methodName: "tmpDir" },
105 | {
106 | type: "async",
107 | method: jetpack.tmpDirAsync as any,
108 | methodName: "tmpDirAsync",
109 | },
110 | ];
111 |
112 | describe('"options" object', () => {
113 | describe('"prefix" argument', () => {
114 | tests.forEach((test) => {
115 | it(test.type, () => {
116 | expect(() => {
117 | test.method({ prefix: 1 });
118 | }).to.throw(
119 | `Argument "options.prefix" passed to ${test.methodName}([options]) must be a string. Received number`
120 | );
121 | });
122 | });
123 | });
124 | describe('"basePath" argument', () => {
125 | tests.forEach((test) => {
126 | it(test.type, () => {
127 | expect(() => {
128 | test.method({ basePath: 1 });
129 | }).to.throw(
130 | `Argument "options.basePath" passed to ${test.methodName}([options]) must be a string. Received number`
131 | );
132 | });
133 | });
134 | });
135 | });
136 | });
137 | });
138 |
--------------------------------------------------------------------------------
/spec/utils/fs.spec.ts:
--------------------------------------------------------------------------------
1 | import * as fsNode from "fs";
2 | import { expect } from "chai";
3 | const fs: any = require("../../lib/utils/fs");
4 |
5 | describe("promised fs", () => {
6 | it("contains all the same keys as the node fs module", () => {
7 | const originalKeys = Object.keys(fsNode);
8 | const adaptedKeys = Object.keys(fs);
9 | expect(adaptedKeys).to.deep.equal(originalKeys);
10 | });
11 | });
12 |
--------------------------------------------------------------------------------
/spec/utils/matcher.spec.ts:
--------------------------------------------------------------------------------
1 | import { expect } from "chai";
2 | const matcher: any = require("../../lib/utils/matcher");
3 |
4 | describe("matcher", () => {
5 | it("can test against one pattern passed as a string", () => {
6 | const test = matcher.create("/", "a");
7 | expect(test("/a")).to.equal(true);
8 | expect(test("/b")).to.equal(false);
9 | });
10 |
11 | it("can test against many patterns passed as an array", () => {
12 | const test = matcher.create("/", ["a", "b"]);
13 | expect(test("/a")).to.equal(true);
14 | expect(test("/b")).to.equal(true);
15 | expect(test("/c")).to.equal(false);
16 | });
17 |
18 | describe("pattern types", () => {
19 | it("only basename", () => {
20 | const test = matcher.create("/", "a");
21 | expect(test("/a")).to.equal(true);
22 | expect(test("/b/a")).to.equal(true);
23 | expect(test("/a/b")).to.equal(false);
24 | });
25 |
26 | it("absolute", () => {
27 | let test = matcher.create("/", ["/b"]);
28 | expect(test("/b")).to.equal(true);
29 | expect(test("/a/b")).to.equal(false);
30 | test = matcher.create("/a", ["/b"]);
31 | expect(test("/a/b")).to.equal(false);
32 | });
33 |
34 | it("relative with ./", () => {
35 | const test = matcher.create("/a", ["./b"]);
36 | expect(test("/a/b")).to.equal(true);
37 | expect(test("/b")).to.equal(false);
38 | });
39 |
40 | it("relative (because has slash inside)", () => {
41 | const test = matcher.create("/a", ["b/c"]);
42 | expect(test("/a/b/c")).to.equal(true);
43 | expect(test("/b/c")).to.equal(false);
44 | });
45 | });
46 |
47 | describe("possible tokens", () => {
48 | it("*", () => {
49 | let test = matcher.create("/", ["*"]);
50 | expect(test("/a")).to.equal(true);
51 | expect(test("/a/b.txt")).to.equal(true);
52 |
53 | test = matcher.create("/", ["a*b"]);
54 | expect(test("/ab")).to.equal(true);
55 | expect(test("/a_b")).to.equal(true);
56 | expect(test("/a__b")).to.equal(true);
57 | });
58 |
59 | it("**", () => {
60 | let test = matcher.create("/", ["**"]);
61 | expect(test("/a")).to.equal(true);
62 | expect(test("/a/b")).to.equal(true);
63 |
64 | test = matcher.create("/", ["a/**/d"]);
65 | expect(test("/a/d")).to.equal(true);
66 | expect(test("/a/b/d")).to.equal(true);
67 | expect(test("/a/b/c/d")).to.equal(true);
68 | expect(test("/a")).to.equal(false);
69 | expect(test("/d")).to.equal(false);
70 | });
71 |
72 | it("**/something", () => {
73 | const test = matcher.create("/", ["**/a"]);
74 | expect(test("/a")).to.equal(true);
75 | expect(test("/x/a")).to.equal(true);
76 | expect(test("/x/y/a")).to.equal(true);
77 | expect(test("/a/b")).to.equal(false);
78 | });
79 |
80 | it("@(pattern|pattern) - exactly one of patterns", () => {
81 | const test = matcher.create("/", ["@(foo|bar)"]);
82 | expect(test("/foo")).to.equal(true);
83 | expect(test("/bar")).to.equal(true);
84 | expect(test("/foobar")).to.equal(false);
85 | });
86 |
87 | it("+(pattern|pattern) - one or more of patterns", () => {
88 | const test = matcher.create("/", ["+(foo|bar)"]);
89 | expect(test("/foo")).to.equal(true);
90 | expect(test("/bar")).to.equal(true);
91 | expect(test("/foobar")).to.equal(true);
92 | expect(test("/foobarbaz")).to.equal(false);
93 | });
94 |
95 | it("?(pattern|pattern) - zero or one of patterns", () => {
96 | const test = matcher.create("/", ["?(foo|bar)1"]);
97 | expect(test("/1")).to.equal(true);
98 | expect(test("/foo1")).to.equal(true);
99 | expect(test("/bar1")).to.equal(true);
100 | expect(test("/foobar1")).to.equal(false);
101 | });
102 |
103 | it("*(pattern|pattern) - zero or more of patterns", () => {
104 | const test = matcher.create("/", ["*(foo|bar)1"]);
105 | expect(test("/1")).to.equal(true);
106 | expect(test("/foo1")).to.equal(true);
107 | expect(test("/bar1")).to.equal(true);
108 | expect(test("/foobar1")).to.equal(true);
109 | expect(test("/barfoo1")).to.equal(true);
110 | expect(test("/foofoo1")).to.equal(true);
111 | });
112 |
113 | it("{a,b}", () => {
114 | const test = matcher.create("/", ["*.{jpg,png}"]);
115 | expect(test("a.jpg")).to.equal(true);
116 | expect(test("b.png")).to.equal(true);
117 | expect(test("c.txt")).to.equal(false);
118 | });
119 |
120 | it("?", () => {
121 | const test = matcher.create("/", ["a?c"]);
122 | expect(test("/abc")).to.equal(true);
123 | expect(test("/ac")).to.equal(false);
124 | expect(test("/abbc")).to.equal(false);
125 | });
126 |
127 | it("[...] - characters range", () => {
128 | const test = matcher.create("/", ["[0-9][0-9]"]);
129 | expect(test("/78")).to.equal(true);
130 | expect(test("/a78")).to.equal(false);
131 | });
132 |
133 | it("combining different tokens together", () => {
134 | const test = matcher.create("/", ["+(f?o|bar*)"]);
135 | expect(test("/f0o")).to.equal(true);
136 | expect(test("/f_o")).to.equal(true);
137 | expect(test("/bar")).to.equal(true);
138 | expect(test("/bar_")).to.equal(true);
139 | expect(test("/f_obar123")).to.equal(true);
140 | expect(test("/f__obar123")).to.equal(false);
141 | });
142 |
143 | it("comment character # has no special meaning", () => {
144 | const test = matcher.create("/", ["#a"]);
145 | expect(test("/#a")).to.equal(true);
146 | });
147 | });
148 |
149 | describe("negation", () => {
150 | it("selects everything except negated", () => {
151 | const test = matcher.create("/", "!abc");
152 | expect(test("/abc")).to.equal(false);
153 | expect(test("/xyz")).to.equal(true);
154 | });
155 |
156 | it("selects everything except negated (multiple patterns)", () => {
157 | const test = matcher.create("/", ["!abc", "!xyz"]);
158 | expect(test("/abc")).to.equal(false);
159 | expect(test("/xyz")).to.equal(false);
160 | expect(test("/whatever")).to.equal(true);
161 | });
162 |
163 | it("filters previous match if negation is farther in order", () => {
164 | const test = matcher.create("/", ["abc", "123", "!/xyz/**", "!789/**"]);
165 | expect(test("/abc")).to.equal(true);
166 | expect(test("/456/123")).to.equal(true);
167 | expect(test("/xyz/abc")).to.equal(false);
168 | expect(test("/789/123")).to.equal(false);
169 | expect(test("/whatever")).to.equal(false);
170 | });
171 | });
172 |
173 | describe("dotfiles", () => {
174 | it("has no problem with matching dotfile", () => {
175 | const test = matcher.create("/", ".foo");
176 | expect(test("/.foo")).to.equal(true);
177 | expect(test("/foo")).to.equal(false);
178 | });
179 |
180 | it("dotfile negation", () => {
181 | let test = matcher.create("/", ["abc", "!.foo/**"]);
182 | expect(test("/.foo/abc")).to.equal(false);
183 | test = matcher.create("/", ["abc", "!.foo/**"]);
184 | expect(test("/foo/abc")).to.equal(true);
185 | });
186 | });
187 | });
188 |
--------------------------------------------------------------------------------
/spec/utils/validate.spec.ts:
--------------------------------------------------------------------------------
1 | import { expect } from "chai";
2 | const validate: any = require("../../lib/utils/validate");
3 |
4 | describe("util validate", () => {
5 | describe("validates arguments passed to methods", () => {
6 | it("validates its own input", () => {
7 | expect(() => {
8 | validate.argument("foo(thing)", "thing", 123, ["foo"]);
9 | }).to.throw('Unknown type "foo"');
10 | });
11 |
12 | [
13 | {
14 | type: "string",
15 | article: "a",
16 | goodValue: "abc",
17 | wrongValue: 123,
18 | wrongValueType: "number",
19 | },
20 | {
21 | type: "number",
22 | article: "a",
23 | goodValue: 123,
24 | wrongValue: "abc",
25 | wrongValueType: "string",
26 | },
27 | {
28 | type: "boolean",
29 | article: "a",
30 | goodValue: true,
31 | wrongValue: "abc",
32 | wrongValueType: "string",
33 | },
34 | {
35 | type: "array",
36 | article: "an",
37 | goodValue: [],
38 | wrongValue: {},
39 | wrongValueType: "object",
40 | },
41 | {
42 | type: "object",
43 | article: "an",
44 | goodValue: {},
45 | wrongValue: [],
46 | wrongValueType: "array",
47 | },
48 | {
49 | type: "buffer",
50 | article: "a",
51 | goodValue: new Buffer(1),
52 | wrongValue: 123,
53 | wrongValueType: "number",
54 | },
55 | {
56 | type: "null",
57 | article: "a",
58 | goodValue: null,
59 | wrongValue: 123,
60 | wrongValueType: "number",
61 | },
62 | {
63 | type: "undefined",
64 | article: "an",
65 | goodValue: undefined,
66 | wrongValue: 123,
67 | wrongValueType: "number",
68 | },
69 | {
70 | type: "function",
71 | article: "a",
72 | goodValue: function () {},
73 | wrongValue: 123,
74 | wrongValueType: "number",
75 | },
76 | ].forEach((test) => {
77 | it(`validates that given thing is a(n) ${test.type}`, () => {
78 | expect(() => {
79 | validate.argument("foo(thing)", "thing", test.goodValue, [test.type]);
80 | }).not.to.throw();
81 |
82 | expect(() => {
83 | validate.argument("foo(thing)", "thing", test.wrongValue, [
84 | test.type,
85 | ]);
86 | }).to.throw(
87 | `Argument "thing" passed to foo(thing) must be ${test.article} ${test.type}. Received ${test.wrongValueType}`
88 | );
89 | });
90 | });
91 |
92 | [
93 | { type: "string", value: "abc", expect: "number" },
94 | { type: "number", value: 123, expect: "string" },
95 | { type: "boolean", value: true, expect: "number" },
96 | { type: "array", value: [], expect: "number" },
97 | { type: "object", value: {}, expect: "number" },
98 | { type: "buffer", value: new Buffer(1), expect: "number" },
99 | { type: "null", value: null, expect: "number" },
100 | { type: "undefined", value: undefined, expect: "number" },
101 | { type: "function", value: function () {}, expect: "number" },
102 | ].forEach((test) => {
103 | it(`can detect wrong type: ${test.type}`, () => {
104 | expect(() => {
105 | validate.argument("foo(thing)", "thing", test.value, [test.expect]);
106 | }).to.throw(
107 | `Argument "thing" passed to foo(thing) must be a ${test.expect}. Received ${test.type}`
108 | );
109 | });
110 | });
111 |
112 | it("supports more than one allowed type", () => {
113 | expect(() => {
114 | validate.argument("foo(thing)", "thing", {}, [
115 | "string",
116 | "number",
117 | "boolean",
118 | ]);
119 | }).to.throw(
120 | 'Argument "thing" passed to foo(thing) must be a string or a number or a boolean. Received object'
121 | );
122 | });
123 |
124 | it("validates array internal data", () => {
125 | expect(() => {
126 | validate.argument(
127 | "foo(thing)",
128 | "thing",
129 | [1, 2, 3],
130 | ["array of number"]
131 | );
132 | }).not.to.throw();
133 |
134 | expect(() => {
135 | validate.argument(
136 | "foo(thing)",
137 | "thing",
138 | [1, 2, "a"],
139 | ["array of number"]
140 | );
141 | }).to.throw(
142 | 'Argument "thing" passed to foo(thing) must be an array of number. Received array of number, string'
143 | );
144 | });
145 | });
146 |
147 | describe("validates options object", () => {
148 | it("options object might be undefined", () => {
149 | expect(() => {
150 | validate.options("foo(options)", "options", undefined, {
151 | foo: ["string"],
152 | });
153 | }).not.to.throw();
154 | });
155 |
156 | it("option key in options object is optional (doh!)", () => {
157 | expect(() => {
158 | validate.options("foo(options)", "options", {}, { foo: ["string"] });
159 | }).not.to.throw();
160 | });
161 |
162 | it("throws if option key definition not found", () => {
163 | expect(() => {
164 | validate.options(
165 | "foo(options)",
166 | "options",
167 | { bar: 123 },
168 | { foo: ["string"] }
169 | );
170 | }).to.throw('Unknown argument "options.bar" passed to foo(options)');
171 | });
172 |
173 | it("validates option", () => {
174 | expect(() => {
175 | validate.options(
176 | "foo(options)",
177 | "options",
178 | { foo: "abc" },
179 | { foo: ["string"] }
180 | );
181 | }).not.to.throw();
182 |
183 | expect(() => {
184 | validate.options(
185 | "foo(options)",
186 | "options",
187 | { foo: 123 },
188 | { foo: ["string"] }
189 | );
190 | }).to.throw(
191 | 'Argument "options.foo" passed to foo(options) must be a string. Received number'
192 | );
193 | });
194 | });
195 | });
196 |
--------------------------------------------------------------------------------
/spec/write.spec.ts:
--------------------------------------------------------------------------------
1 | import * as fse from "fs-extra";
2 | import { expect } from "chai";
3 | import path from "./assert_path";
4 | import helper from "./helper";
5 | import * as jetpack from "..";
6 |
7 | describe("write", () => {
8 | beforeEach(helper.setCleanTestCwd);
9 | afterEach(helper.switchBackToCorrectCwd);
10 |
11 | describe("writes data from string", () => {
12 | const expectations = () => {
13 | path("file.txt").shouldBeFileWithContent("abc");
14 | };
15 |
16 | it("sync", () => {
17 | jetpack.write("file.txt", "abc");
18 | expectations();
19 | });
20 |
21 | it("async", (done) => {
22 | jetpack.writeAsync("file.txt", "abc").then(() => {
23 | expectations();
24 | done();
25 | });
26 | });
27 | });
28 |
29 | describe("writes data from Buffer", () => {
30 | const expectations = () => {
31 | path("file.txt").shouldBeFileWithContent(new Buffer([11, 22]));
32 | };
33 |
34 | it("sync", () => {
35 | jetpack.write("file.txt", new Buffer([11, 22]));
36 | expectations();
37 | });
38 |
39 | it("async", (done) => {
40 | jetpack.writeAsync("file.txt", new Buffer([11, 22])).then(() => {
41 | expectations();
42 | done();
43 | });
44 | });
45 | });
46 |
47 | describe("writes data as JSON", () => {
48 | const obj = {
49 | utf8: "ąćłźż",
50 | };
51 |
52 | const expectations = () => {
53 | const content = JSON.parse(fse.readFileSync("file.json", "utf8"));
54 | expect(content).to.eql(obj);
55 | };
56 |
57 | it("sync", () => {
58 | jetpack.write("file.json", obj);
59 | expectations();
60 | });
61 |
62 | it("async", (done) => {
63 | jetpack.writeAsync("file.json", obj).then(() => {
64 | expectations();
65 | done();
66 | });
67 | });
68 | });
69 |
70 | describe("written JSON data can be indented", () => {
71 | const obj = {
72 | utf8: "ąćłźż",
73 | };
74 |
75 | const expectations = () => {
76 | const sizeA = fse.statSync("a.json").size;
77 | const sizeB = fse.statSync("b.json").size;
78 | const sizeC = fse.statSync("c.json").size;
79 | expect(sizeB).to.be.above(sizeA);
80 | expect(sizeC).to.be.above(sizeB);
81 | };
82 |
83 | it("sync", () => {
84 | jetpack.write("a.json", obj, { jsonIndent: 0 });
85 | jetpack.write("b.json", obj); // Default indent = 2
86 | jetpack.write("c.json", obj, { jsonIndent: 4 });
87 | expectations();
88 | });
89 |
90 | it("async", (done) => {
91 | Promise.all([
92 | jetpack.writeAsync("a.json", obj, { jsonIndent: 0 }),
93 | jetpack.writeAsync("b.json", obj), // Default indent = 2
94 | jetpack.writeAsync("c.json", obj, { jsonIndent: 4 }),
95 | ]).then(() => {
96 | expectations();
97 | done();
98 | });
99 | });
100 | });
101 |
102 | describe("writes and reads file as JSON with Date parsing", () => {
103 | const obj = {
104 | date: new Date(),
105 | };
106 |
107 | const expectations = () => {
108 | const content = JSON.parse(fse.readFileSync("file.json", "utf8"));
109 | expect(content.date).to.equal(obj.date.toISOString());
110 | };
111 |
112 | it("sync", () => {
113 | jetpack.write("file.json", obj);
114 | expectations();
115 | });
116 |
117 | it("async", (done) => {
118 | jetpack.writeAsync("file.json", obj).then(() => {
119 | expectations();
120 | done();
121 | });
122 | });
123 | });
124 |
125 | if (process.platform !== "win32") {
126 | describe("sets mode of the file (unix only)", () => {
127 | const preparations = () => {
128 | fse.writeFileSync("file.txt", "abc", { mode: "700" });
129 | };
130 | const expectations = () => {
131 | path("file.txt").shouldBeFileWithContent("xyz");
132 | path("file.txt").shouldHaveMode("711");
133 | };
134 |
135 | it("sync, mode passed as string", () => {
136 | jetpack.write("file.txt", "xyz", { mode: "711" });
137 | expectations();
138 | });
139 |
140 | it("sync, mode passed as number", () => {
141 | jetpack.write("file.txt", "xyz", { mode: 0o711 });
142 | expectations();
143 | });
144 |
145 | it("async, mode passed as string", (done) => {
146 | jetpack
147 | .writeAsync("file.txt", "xyz", { mode: "711" })
148 | .then(() => {
149 | expectations();
150 | done();
151 | })
152 | .catch(done);
153 | });
154 |
155 | it("async, mode passed as number", (done) => {
156 | jetpack
157 | .writeAsync("file.txt", "xyz", { mode: 0o711 })
158 | .then(() => {
159 | expectations();
160 | done();
161 | })
162 | .catch(done);
163 | });
164 | });
165 | }
166 |
167 | describe("can create nonexistent parent directories", () => {
168 | const expectations = () => {
169 | path("a/b/c.txt").shouldBeFileWithContent("abc");
170 | };
171 |
172 | it("sync", () => {
173 | jetpack.write("a/b/c.txt", "abc");
174 | expectations();
175 | });
176 |
177 | it("async", (done) => {
178 | jetpack.writeAsync("a/b/c.txt", "abc").then(() => {
179 | expectations();
180 | done();
181 | });
182 | });
183 | });
184 |
185 | describe("respects internal CWD of jetpack instance", () => {
186 | const expectations = () => {
187 | path("a/b/c.txt").shouldBeFileWithContent("abc");
188 | };
189 |
190 | it("sync", () => {
191 | const jetContext = jetpack.cwd("a");
192 | jetContext.write("b/c.txt", "abc");
193 | expectations();
194 | });
195 |
196 | it("async", (done) => {
197 | const jetContext = jetpack.cwd("a");
198 | jetContext.writeAsync("b/c.txt", "abc").then(() => {
199 | expectations();
200 | done();
201 | });
202 | });
203 | });
204 |
205 | describe("input validation", () => {
206 | const tests = [
207 | { type: "sync", method: jetpack.write as any, methodName: "write" },
208 | {
209 | type: "async",
210 | method: jetpack.writeAsync as any,
211 | methodName: "writeAsync",
212 | },
213 | ];
214 |
215 | describe('"path" argument', () => {
216 | tests.forEach((test) => {
217 | it(test.type, () => {
218 | expect(() => {
219 | test.method(undefined);
220 | }).to.throw(
221 | `Argument "path" passed to ${test.methodName}(path, data, [options]) must be a string. Received undefined`
222 | );
223 | });
224 | });
225 | });
226 |
227 | describe('"data" argument', () => {
228 | tests.forEach((test) => {
229 | it(test.type, () => {
230 | expect(() => {
231 | test.method("abc", true);
232 | }).to.throw(
233 | `Argument "data" passed to ${test.methodName}(path, data, [options]) must be a string or a buffer or an object or an array. Received boolean`
234 | );
235 | });
236 | });
237 | });
238 |
239 | describe('"options" object', () => {
240 | describe('"atomic" argument', () => {
241 | tests.forEach((test) => {
242 | it(test.type, () => {
243 | expect(() => {
244 | test.method("abc", "xyz", { atomic: 1 });
245 | }).to.throw(
246 | `Argument "options.atomic" passed to ${test.methodName}(path, data, [options]) must be a boolean. Received number`
247 | );
248 | });
249 | });
250 | });
251 | describe('"jsonIndent" argument', () => {
252 | tests.forEach((test) => {
253 | it(test.type, () => {
254 | expect(() => {
255 | test.method("abc", "xyz", { jsonIndent: true });
256 | }).to.throw(
257 | `Argument "options.jsonIndent" passed to ${test.methodName}(path, data, [options]) must be a number. Received boolean`
258 | );
259 | });
260 | });
261 | });
262 | });
263 | });
264 | });
265 |
--------------------------------------------------------------------------------
/spec/write_atomic.spec.ts:
--------------------------------------------------------------------------------
1 | import * as fse from "fs-extra";
2 | import { expect } from "chai";
3 | import path from "./assert_path";
4 | import helper from "./helper";
5 | import * as jetpack from "..";
6 |
7 | describe("atomic write", () => {
8 | const filePath = "file.txt";
9 | const tempPath = `${filePath}.__new__`;
10 |
11 | beforeEach(helper.setCleanTestCwd);
12 | afterEach(helper.switchBackToCorrectCwd);
13 |
14 | describe("fresh write (file doesn't exist yet)", () => {
15 | const expectations = () => {
16 | path(filePath).shouldBeFileWithContent("abc");
17 | path(tempPath).shouldNotExist();
18 | };
19 |
20 | it("sync", () => {
21 | jetpack.write(filePath, "abc", { atomic: true });
22 | expectations();
23 | });
24 |
25 | it("async", (done) => {
26 | jetpack.writeAsync(filePath, "abc", { atomic: true }).then(() => {
27 | expectations();
28 | done();
29 | });
30 | });
31 | });
32 |
33 | describe("overwrite existing file", () => {
34 | const preparations = () => {
35 | fse.outputFileSync(filePath, "xyz");
36 | };
37 |
38 | const expectations = () => {
39 | path(filePath).shouldBeFileWithContent("abc");
40 | path(tempPath).shouldNotExist();
41 | };
42 |
43 | it("sync", () => {
44 | preparations();
45 | jetpack.write(filePath, "abc", { atomic: true });
46 | expectations();
47 | });
48 |
49 | it("async", (done) => {
50 | preparations();
51 | jetpack.writeAsync(filePath, "abc", { atomic: true }).then(() => {
52 | expectations();
53 | done();
54 | });
55 | });
56 | });
57 |
58 | describe("if previous operation failed", () => {
59 | const preparations = () => {
60 | fse.outputFileSync(filePath, "xyz");
61 | // Simulating remained file from interrupted previous write attempt.
62 | fse.outputFileSync(tempPath, "123");
63 | };
64 |
65 | const expectations = () => {
66 | path(filePath).shouldBeFileWithContent("abc");
67 | path(tempPath).shouldNotExist();
68 | };
69 |
70 | it("sync", () => {
71 | preparations();
72 | jetpack.write(filePath, "abc", { atomic: true });
73 | expectations();
74 | });
75 |
76 | it("async", (done) => {
77 | preparations();
78 | jetpack.writeAsync(filePath, "abc", { atomic: true }).then(() => {
79 | expectations();
80 | done();
81 | });
82 | });
83 | });
84 | });
85 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target" : "es6",
4 | "noImplicitAny": true,
5 | "allowJs": false
6 | }
7 | }
8 |
--------------------------------------------------------------------------------